Phi-3-mini-128k-instruct集成SpringBoot实战:构建智能问答微服务

最近在做一个内部知识库系统,需要接入一个AI问答能力。要求很明确:响应要快、资源占用要小、还得能方便地集成到现有的Java技术栈里。找了一圈,微软开源的Phi-3-mini-128k-instruct模型一下就吸引了我的注意。它参数小,推理速度快,而且指令跟随能力不错,特别适合这种轻量级的应用场景。

但光有模型还不够,怎么把它变成一个稳定、可扩展的微服务,让业务系统能方便地调用,这才是关键。今天我就结合自己的实践,聊聊怎么用SpringBoot把Phi-3-mini封装成一个智能问答微服务,重点会放在API设计、异步处理和性能优化这些实际工程问题上。

1. 项目准备与环境搭建

首先得把基础环境准备好。这个项目本质上是一个标准的SpringBoot应用,核心是要引入模型推理的能力。

我选择用Ollama来本地部署和管理Phi-3-mini模型,因为它对开发者太友好了,一条命令就能把模型跑起来,还提供了清晰的HTTP API供我们调用。SpringBoot这边,我们只需要构建一个标准的Web服务来对接它。

1.1 核心依赖配置

打开你的pom.xml,除了SpringBoot的基础Web依赖,我们还需要一些帮手。

<dependencies>
    <!-- SpringBoot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- 参数校验,让接口更健壮 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    
    <!-- Swagger,自动生成API文档 -->
    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        <version>2.3.0</version>
    </dependency>
    
    <!-- 异步任务支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-integration</artifactId>
    </dependency>
    
    <!-- 简化HTTP调用的工具 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
</dependencies>

1.2 启动并配置Ollama

在服务器上安装Ollama非常简单,官网提供了各种系统的安装脚本。安装好后,拉取并运行Phi-3-mini模型就两行命令:

# 拉取模型(模型不大,很快)
ollama pull phi3:mini-128k-instruct

# 在本地启动模型服务,默认端口11434
ollama run phi3:mini-128k-instruct

服务启动后,你可以用curl简单测试一下:

curl http://localhost:11434/api/generate -d '{
  "model": "phi3:mini-128k-instruct",
  "prompt": "你好,请介绍一下你自己。",
  "stream": false
}'

如果能看到返回的JSON格式的文本,说明模型服务已经就绪。接下来,就是让SpringBoot应用和它对话了。

2. 设计问答服务的核心接口

微服务的核心是提供清晰、易用的API。对于问答服务,我设计了两个主要接口:一个用于实时问答,另一个用于提交需要长时间处理的复杂任务。

2.1 定义请求与响应对象

先定义好数据怎么传。创建一个QuestionRequest类来封装用户的提问:

@Data
public class QuestionRequest {
    @NotBlank(message = "问题内容不能为空")
    private String question;
    
    // 可以扩展更多参数,比如系统指令、温度参数等
    private String systemPrompt;
    private Double temperature;
}

再创建一个AnswerResponse来统一返回格式:

@Data
public class AnswerResponse {
    private boolean success;
    private String answer;
    private String errorMessage;
    private Long costTime; // 耗时,单位毫秒
    private String taskId; // 用于异步任务查询
}

2.2 实现同步问答接口

这是最常用的接口,用户提问,服务立刻调用模型并返回答案。我们创建一个QAController

@RestController
@RequestMapping("/api/v1/qa")
@Tag(name = "智能问答接口", description = "提供基于Phi-3-mini模型的智能问答能力")
public class QAController {
    
    @Autowired
    private QAService qaService;
    
    @PostMapping("/ask")
    @Operation(summary = "同步问答", description = "提交问题,立即获取模型生成的答案")
    public AnswerResponse askQuestion(@Valid @RequestBody QuestionRequest request) {
        long startTime = System.currentTimeMillis();
        try {
            String answer = qaService.askSync(request);
            AnswerResponse response = new AnswerResponse();
            response.setSuccess(true);
            response.setAnswer(answer);
            response.setCostTime(System.currentTimeMillis() - startTime);
            return response;
        } catch (Exception e) {
            AnswerResponse response = new AnswerResponse();
            response.setSuccess(false);
            response.setErrorMessage("处理请求时发生错误: " + e.getMessage());
            response.setCostTime(System.currentTimeMillis() - startTime);
            return response;
        }
    }
}

这里的QAService是业务逻辑层,它负责去调用Ollama的API。我们来实现它:

@Service
public class QAService {
    
    // Ollama服务的地址,可以放在配置文件中
    @Value("${ollama.api.url:http://localhost:11434}")
    private String ollamaApiUrl;
    
    private final WebClient webClient;
    
    public QAService() {
        this.webClient = WebClient.builder().build();
    }
    
    public String askSync(QuestionRequest request) {
        // 构建发送给Ollama的请求体
        Map<String, Object> body = new HashMap<>();
        body.put("model", "phi3:mini-128k-instruct");
        body.put("prompt", request.getQuestion());
        body.put("stream", false);
        
        // 使用WebClient进行非阻塞调用
        Mono<Map> responseMono = webClient.post()
                .uri(ollamaApiUrl + "/api/generate")
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(body)
                .retrieve()
                .bodyToMono(Map.class);
        
        // 这里为了简化,使用block()转为同步,实际生产环境可考虑完全异步
        Map<String, Object> response = responseMono.block(Duration.ofSeconds(30));
        
        if (response != null && response.containsKey("response")) {
            return (String) response.get("response");
        } else {
            throw new RuntimeException("从模型服务获取响应失败");
        }
    }
}

这样,一个最简单的同步问答接口就完成了。启动应用,访问 http://localhost:8080/swagger-ui.html 就能看到自动生成的API文档,并直接测试接口。

3. 引入异步处理应对复杂场景

同步接口虽然简单,但如果用户的问题很复杂,模型推理需要十几秒甚至更长时间,让HTTP连接一直等着就不合适了。这时候就需要异步任务机制。

3.1 设计异步任务接口

我们增加两个新接口:一个用于提交异步任务,另一个用于查询任务结果。

首先,在QAController里添加:

@PostMapping("/ask/async")
@Operation(summary = "提交异步问答任务", description = "提交复杂问题,返回任务ID,后续可凭ID查询结果")
public AnswerResponse askQuestionAsync(@Valid @RequestBody QuestionRequest request) {
    String taskId = qaService.submitAsyncTask(request);
    AnswerResponse response = new AnswerResponse();
    response.setSuccess(true);
    response.setTaskId(taskId);
    response.setAnswer("任务已提交,请使用taskId查询结果");
    return response;
}

@GetMapping("/task/{taskId}")
@Operation(summary = "查询异步任务结果", description = "根据任务ID查询异步问答任务的处理状态和结果")
public AnswerResponse getTaskResult(@PathVariable String taskId) {
    return qaService.getAsyncTaskResult(taskId);
}

3.2 实现异步任务队列

Spring为我们提供了强大的@Async注解和线程池支持,可以很方便地实现后台任务。

@Service
public class AsyncQAService {
    
    // 用一个ConcurrentMap来模拟任务存储,生产环境可以用Redis或数据库
    private final ConcurrentMap<String, AsyncTask> taskStore = new ConcurrentHashMap<>();
    
    @Async("qaTaskExecutor") // 指定使用自定义的线程池
    public CompletableFuture<String> processAsync(QuestionRequest request, String taskId) {
        AsyncTask task = new AsyncTask(taskId, "PROCESSING", null, System.currentTimeMillis());
        taskStore.put(taskId, task);
        
        try {
            // 这里调用真实的模型推理逻辑,和同步接口类似
            String answer = "这是模型生成的答案..."; // 实际调用模型服务
            task.setStatus("SUCCESS");
            task.setResult(answer);
            task.setFinishTime(System.currentTimeMillis());
            return CompletableFuture.completedFuture(answer);
        } catch (Exception e) {
            task.setStatus("FAILED");
            task.setResult("处理失败: " + e.getMessage());
            task.setFinishTime(System.currentTimeMillis());
            return CompletableFuture.failedFuture(e);
        }
    }
    
    public AsyncTask getTask(String taskId) {
        return taskStore.get(taskId);
    }
}

// 简单的任务状态对象
@Data
class AsyncTask {
    private String taskId;
    private String status; // PENDING, PROCESSING, SUCCESS, FAILED
    private String result;
    private Long submitTime;
    private Long finishTime;
    
    public AsyncTask(String taskId, String status, String result, Long submitTime) {
        this.taskId = taskId;
        this.status = status;
        this.result = result;
        this.submitTime = submitTime;
    }
}

别忘了配置一个专用的线程池,避免所有异步任务挤占Web容器的线程:

@Configuration
@EnableAsync
public class AsyncConfig {
    
    @Bean("qaTaskExecutor")
    public Executor qaTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("qa-async-");
        executor.initialize();
        return executor;
    }
}

这样,当用户提交一个复杂问题时,服务会立即返回一个任务ID,然后模型在后台慢慢推理。用户可以通过轮询/api/v1/qa/task/{taskId}接口来获取最终结果。这种模式对于需要长时间处理的AI任务非常友好。

4. 性能优化与生产就绪

服务能跑起来只是第一步,要真正用于生产环境,还得在性能和稳定性上下功夫。

4.1 连接池与超时设置

频繁地创建和销毁HTTP连接开销很大,特别是面对高并发请求时。我们可以配置一个HTTP连接池来复用连接。

application.yml中添加配置:

# 自定义Ollama客户端配置
ollama:
  api:
    url: http://localhost:11434
    connect-timeout: 5000   # 连接超时5秒
    read-timeout: 30000     # 读取超时30秒,给模型推理留足时间
    
# 调整WebClient的连接池
spring:
  webflux:
    client:
      max-memory-size: 10MB

然后改进我们的QAService,使用连接池:

@Service
public class OptimizedQAService {
    
    private final WebClient webClient;
    
    public OptimizedQAService(@Value("${ollama.api.url}") String ollamaUrl) {
        // 配置连接池
        ConnectionProvider provider = ConnectionProvider.builder("ollamaPool")
                .maxConnections(50) // 最大连接数
                .pendingAcquireTimeout(Duration.ofSeconds(10)) // 获取连接超时
                .build();
        
        HttpClient httpClient = HttpClient.create(provider)
                .responseTimeout(Duration.ofMillis(30000)); // 响应超时
        
        this.webClient = WebClient.builder()
                .baseUrl(ollamaUrl)
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }
    
    // 使用配置好的WebClient进行调用
    public Mono<String> askReactive(QuestionRequest request) {
        Map<String, Object> body = Map.of(
            "model", "phi3:mini-128k-instruct",
            "prompt", request.getQuestion(),
            "stream", false
        );
        
        return webClient.post()
                .uri("/api/generate")
                .bodyValue(body)
                .retrieve()
                .bodyToMono(Map.class)
                .map(response -> (String) response.get("response"))
                .timeout(Duration.ofSeconds(30)) // 操作超时
                .onErrorResume(e -> Mono.just("请求模型服务超时或失败"));
    }
}

4.2 实现简单的请求合并与缓存

对于一些常见、重复的问题,我们可以引入缓存机制,避免重复调用模型。对于短时间内相同的多个问题,甚至可以合并请求。

@Service
public class SmartQAService {
    
    @Autowired
    private OptimizedQAService optimizedQAService;
    
    // 使用Caffeine作为本地缓存
    private final Cache<String, String> answerCache = Caffeine.newBuilder()
            .maximumSize(1000) // 最多缓存1000个答案
            .expireAfterWrite(10, TimeUnit.MINUTES) // 10分钟后过期
            .build();
    
    // 请求合并的窗口期
    private final Duration mergeWindow = Duration.ofMillis(500);
    private final ConcurrentMap<String, CompletableFuture<String>> mergingRequests = new ConcurrentHashMap<>();
    
    public Mono<String> askWithCache(QuestionRequest request) {
        String question = request.getQuestion();
        String cachedAnswer = answerCache.getIfPresent(question);
        
        if (cachedAnswer != null) {
            return Mono.just("【缓存】" + cachedAnswer);
        }
        
        // 检查是否有相同的请求正在处理中(请求合并)
        return mergingRequests.computeIfAbsent(question, key -> {
            CompletableFuture<String> future = new CompletableFuture<>();
            
            // 设置一个窗口期,等待可能出现的相同请求
            Mono.delay(mergeWindow)
                .then(optimizedQAService.askReactive(request))
                .subscribe(answer -> {
                    // 处理完成,移除并完成所有等待的Future
                    CompletableFuture<String> removed = mergingRequests.remove(question);
                    if (removed != null) {
                        removed.complete(answer);
                    }
                    // 存入缓存
                    answerCache.put(question, answer);
                }, error -> {
                    CompletableFuture<String> removed = mergingRequests.remove(question);
                    if (removed != null) {
                        removed.completeExceptionally(error);
                    }
                });
            
            return future;
        }).thenApply(answer -> "【实时】" + answer);
    }
}

这个SmartQAService做了两件事:一是用缓存记住已经回答过的问题,二是把短时间内相同的多个问题合并成一个请求发给模型,这对减轻后端压力很有帮助。

4.3 监控与限流

最后,我们还需要知道服务运行得怎么样,以及在压力大时保护服务。SpringBoot Actuator可以帮我们暴露健康检查和指标,而Resilience4j可以实现限流和熔断。

添加依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
    <version>2.1.0</version>
</dependency>

然后在接口上添加限流注解:

@RestController
@RequestMapping("/api/v2/qa")
public class ResilientQAController {
    
    @Autowired
    private SmartQAService smartQAService;
    
    @PostMapping("/ask")
    @RateLimiter(name = "qaRateLimit") // 每秒钟最多处理10个请求
    public Mono<AnswerResponse> askQuestionResilient(@Valid @RequestBody QuestionRequest request) {
        return smartQAService.askWithCache(request)
                .map(answer -> {
                    AnswerResponse response = new AnswerResponse();
                    response.setSuccess(true);
                    response.setAnswer(answer);
                    return response;
                })
                .onErrorResume(e -> Mono.just(createErrorResponse(e)));
    }
    
    private AnswerResponse createErrorResponse(Throwable e) {
        AnswerResponse response = new AnswerResponse();
        response.setSuccess(false);
        response.setErrorMessage("服务暂时不可用: " + e.getMessage());
        return response;
    }
}

application.yml中配置限流规则:

resilience4j:
  ratelimiter:
    instances:
      qaRateLimit:
        limit-for-period: 10   # 时间窗口内允许的调用次数
        limit-refresh-period: 1s # 时间窗口长度
        timeout-duration: 0     # 等待超时时间,0表示立即失败

现在,我们的服务就具备了基本的自我保护能力,当请求过多时,超出的部分会立即被拒绝,避免服务被压垮。

5. 总结

走完这一整套流程,一个基于Phi-3-mini和SpringBoot的智能问答微服务就初具雏形了。从最基础的同步接口,到支持长时间任务的异步处理,再到缓存、合并请求、限流等优化策略,我们一步步让服务变得更健壮、更高效。

实际用下来,这种轻量级模型加微服务的组合确实很灵活。Phi-3-mini推理速度快,资源要求不高,在普通的云服务器上就能跑得很顺畅。SpringBoot那套成熟的生态,让我们能快速集成监控、文档、限流等各种企业级功能。

当然,这只是个起点。根据实际业务需求,你可能还需要考虑模型的热更新、多模型路由、更复杂的对话状态管理等等。但有了这个基础框架,后续的扩展都会容易很多。如果你也在考虑为系统添加AI能力,不妨从这个小而美的方案开始试试。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐