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>
<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星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
更多推荐


所有评论(0)