Phi-3-mini-128k-instruct集成SpringBoot实战:构建智能问答微服务
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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 参数校验让接口更健壮 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency !-- Swagger自动生成API文档 -- dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-starter-webmvc-ui/artifactId version2.3.0/version /dependency !-- 异步任务支持 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-integration/artifactId /dependency !-- 简化HTTP调用的工具 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency /dependencies1.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的请求体 MapString, Object body new HashMap(); body.put(model, phi3:mini-128k-instruct); body.put(prompt, request.getQuestion()); body.put(stream, false); // 使用WebClient进行非阻塞调用 MonoMap responseMono webClient.post() .uri(ollamaApiUrl /api/generate) .contentType(MediaType.APPLICATION_JSON) .bodyValue(body) .retrieve() .bodyToMono(Map.class); // 这里为了简化使用block()转为同步实际生产环境可考虑完全异步 MapString, 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 ConcurrentMapString, AsyncTask taskStore new ConcurrentHashMap(); Async(qaTaskExecutor) // 指定使用自定义的线程池 public CompletableFutureString 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 MonoString askReactive(QuestionRequest request) { MapString, 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 CacheString, String answerCache Caffeine.newBuilder() .maximumSize(1000) // 最多缓存1000个答案 .expireAfterWrite(10, TimeUnit.MINUTES) // 10分钟后过期 .build(); // 请求合并的窗口期 private final Duration mergeWindow Duration.ofMillis(500); private final ConcurrentMapString, CompletableFutureString mergingRequests new ConcurrentHashMap(); public MonoString askWithCache(QuestionRequest request) { String question request.getQuestion(); String cachedAnswer answerCache.getIfPresent(question); if (cachedAnswer ! null) { return Mono.just(【缓存】 cachedAnswer); } // 检查是否有相同的请求正在处理中请求合并 return mergingRequests.computeIfAbsent(question, key - { CompletableFutureString future new CompletableFuture(); // 设置一个窗口期等待可能出现的相同请求 Mono.delay(mergeWindow) .then(optimizedQAService.askReactive(request)) .subscribe(answer - { // 处理完成移除并完成所有等待的Future CompletableFutureString removed mergingRequests.remove(question); if (removed ! null) { removed.complete(answer); } // 存入缓存 answerCache.put(question, answer); }, error - { CompletableFutureString removed mergingRequests.remove(question); if (removed ! null) { removed.completeExceptionally(error); } }); return future; }).thenApply(answer - 【实时】 answer); } }这个SmartQAService做了两件事一是用缓存记住已经回答过的问题二是把短时间内相同的多个问题合并成一个请求发给模型这对减轻后端压力很有帮助。4.3 监控与限流最后我们还需要知道服务运行得怎么样以及在压力大时保护服务。SpringBoot Actuator可以帮我们暴露健康检查和指标而Resilience4j可以实现限流和熔断。添加依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency dependency groupIdio.github.resilience4j/groupId artifactIdresilience4j-spring-boot2/artifactId version2.1.0/version /dependency然后在接口上添加限流注解RestController RequestMapping(/api/v2/qa) public class ResilientQAController { Autowired private SmartQAService smartQAService; PostMapping(/ask) RateLimiter(name qaRateLimit) // 每秒钟最多处理10个请求 public MonoAnswerResponse 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星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。