墨语灵犀Java开发实战:集成SpringBoot构建智能问答微服务
墨语灵犀Java开发实战:集成SpringBoot构建智能问答微服务
最近在做一个内部知识库系统,需要给用户提供一个智能问答入口。传统的搜索框虽然能用,但总感觉差点意思,用户得自己提炼关键词,出来的结果还得再筛选。正好手头有墨语灵犀大模型的API,就琢磨着能不能用Java,特别是咱们最熟悉的SpringBoot,把它集成进来,做成一个能理解自然语言、直接给出答案的微服务。
说干就干。整个过程下来,发现用SpringBoot来封装大模型能力,比想象中要顺畅不少。从基础的API调用,到异步处理、结果缓存,再到应对高并发的一些小技巧,形成了一套还算完整的方案。今天就把这个实战过程梳理一下,如果你也在考虑给Java应用加点AI能力,特别是想快速搭建一个智能客服或者问答服务,或许能给你一些参考。
1. 为什么选择SpringBoot集成大模型?
你可能想问,现在Python不是搞AI的主流吗,为啥要用Java和SpringBoot?这其实得看场景。如果你的团队主力是Java技术栈,业务系统也是基于SpringCloud那一套微服务架构,那么引入一个全新的Python服务来做AI,会带来额外的运维、部署和联调成本。让Java服务直接调用模型API,是更平滑、更“企业级”的做法。
SpringBoot在这方面有几个天然优势。首先是它的自动配置和起步依赖,能让我们快速搭建一个稳健的Web服务层。其次,Spring强大的生态,比如对异步编程、缓存、连接池的支持,能很好地应对大模型API调用可能遇到的延迟、高并发等问题。最后,也是最重要的,它能让我们把AI能力像普通业务服务一样进行管理、监控和集成,无缝对接到现有的用户认证、日志、链路追踪体系中。
所以,这个方案的核心思路就是:将墨语灵犀大模型视为一个外部智能服务,通过SpringBoot构建一个轻量、高效、可靠的代理层,向上对业务提供简洁的问答接口,向下处理与模型API的复杂交互。
2. 项目搭建与基础依赖
我们从零开始。使用Spring Initializr或者IDE直接创建一个新的SpringBoot项目,我用的版本是2.7.x,当然3.x系列也完全没问题。
核心的依赖其实不多,主要就下面这几个:
<dependencies>
<!-- 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>
<!-- HTTP客户端,用于调用模型API -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- 缓存支持,后面优化会用 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
这里特意引入了WebFlux,不是为了做响应式编程,而是看中了它底层的WebClient。相比传统的RestTemplate,WebClient对异步和非阻塞的支持更友好,性能也更好,更适合用来调用外部API。
接下来,在application.yml里配置一些基础信息,特别是模型API的地址和密钥(这些敏感信息建议放在配置中心或环境变量里):
app:
llm:
# 墨语灵犀API的基础地址
base-url: https://api.moyulingxi.com/v1
# 你的API密钥
api-key: ${LLM_API_KEY:your-api-key-here}
# 默认使用的模型
default-model: moyu-pro
# 请求超时时间(毫秒)
timeout: 30000
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=1000, expireAfterWrite=10m
3. 核心层设计:封装模型API
直接在每个Controller里写HTTP调用代码是灾难的开始。我们需要一个专门的“服务层”来隔离这种复杂性。我设计了一个三层结构:Client -> Service -> Controller。
第一步,创建API请求与响应的数据模型。 这其实就是定义Java类,对应API需要的JSON结构。
import lombok.Data;
@Data
public class ChatCompletionRequest {
private String model;
private List<Message> messages;
private Double temperature = 0.7; // 控制创造性
private Integer maxTokens = 2000; // 回复最大长度
@Data
public static class Message {
private String role; // "user" 或 "assistant"
private String content;
}
}
import lombok.Data;
import java.util.List;
@Data
public class ChatCompletionResponse {
private String id;
private String object;
private Long created;
private String model;
private List<Choice> choices;
private Usage usage;
@Data
public static class Choice {
private Message message;
private Integer index;
private String finishReason;
}
@Data
public static class Usage {
private Integer promptTokens;
private Integer completionTokens;
private Integer totalTokens;
}
}
第二步,实现HTTP客户端(Client层)。 这里用WebClient来发送请求。
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
@Component
public class LlmApiClient {
@Value("${app.llm.base-url}")
private String baseUrl;
@Value("${app.llm.api-key}")
private String apiKey;
private WebClient webClient;
@PostConstruct
public void init() {
this.webClient = WebClient.builder()
.baseUrl(baseUrl)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
public Mono<ChatCompletionResponse> createChatCompletion(ChatCompletionRequest request) {
return webClient.post()
.uri("/chat/completions")
.bodyValue(request)
.retrieve()
.bodyToMono(ChatCompletionResponse.class)
.timeout(Duration.ofMillis(30000)) // 配置超时
.onErrorResume(e -> {
// 这里可以加入更精细的错误处理和日志
log.error("调用大模型API失败", e);
return Mono.error(new ServiceException("智能服务暂时不可用,请稍后重试"));
});
}
}
第三步,创建业务服务层(Service层)。 这一层对上提供简洁的问答方法,对下调用Client,并可以在这里加入业务逻辑,比如提示词工程。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Arrays;
@Service
public class SmartQaService {
@Autowired
private LlmApiClient llmApiClient;
public String askQuestion(String userQuestion, String context) {
// 构建系统提示词,可以引导模型扮演特定角色,比如“专业客服”
String systemPrompt = "你是一个专业的智能助手,请根据用户的问题,提供准确、简洁、有帮助的回答。";
if (context != null && !context.isEmpty()) {
systemPrompt += "以下是一些参考信息:" + context;
}
ChatCompletionRequest request = new ChatCompletionRequest();
request.setModel("moyu-pro");
request.setMessages(Arrays.asList(
new ChatCompletionRequest.Message("system", systemPrompt),
new ChatCompletionRequest.Message("user", userQuestion)
));
// 暂时使用阻塞式调用,后面会优化
ChatCompletionResponse response = llmApiClient.createChatCompletion(request).block();
if (response != null &&
response.getChoices() != null &&
!response.getChoices().isEmpty()) {
return response.getChoices().get(0).getMessage().getContent();
}
return "抱歉,我暂时无法回答这个问题。";
}
}
4. 提供RESTful API与控制层
服务层准备好了,现在通过一个Controller暴露给外部调用。设计一个简单明了的API。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/v1/qa")
public class QaController {
@Autowired
private SmartQaService qaService;
@PostMapping("/ask")
public ApiResponse<String> ask(@Valid @RequestBody QuestionRequest questionRequest) {
String answer = qaService.askQuestion(questionRequest.getQuestion(), questionRequest.getContext());
return ApiResponse.success(answer);
}
// 简单的请求体
@Data
public static class QuestionRequest {
@NotBlank(message = "问题不能为空")
private String question;
private String context; // 可选的上下文信息
}
// 统一的响应包装
@Data
public static class ApiResponse<T> {
private Integer code;
private String msg;
private T data;
public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> resp = new ApiResponse<>();
resp.setCode(200);
resp.setMsg("success");
resp.setData(data);
return resp;
}
}
}
现在,启动应用,用Postman或者curl测试一下POST /api/v1/qa/ask,传入{"question": "SpringBoot有什么优点?"},你应该就能收到一段由墨语灵犀生成的、关于SpringBoot优点的回答了。基础流程这就跑通了。
5. 性能优化实战:异步、缓存与并发
基础功能有了,但直接投入生产环境肯定不行。用户提问可能很频繁,而大模型API调用通常有几百毫秒到几秒的延迟,这会很快拖慢整个服务。我们需要进行几轮优化。
优化一:异步非阻塞处理。 这是提升吞吐量的关键。我们改造Service层,让Controller调用立刻返回一个Future或CompletableFuture,而不是阻塞等待。
@Service
public class AsyncSmartQaService {
@Autowired
private LlmApiClient llmApiClient;
// 注入Spring提供的异步任务执行器
@Autowired
private AsyncTaskExecutor taskExecutor;
public CompletableFuture<String> askQuestionAsync(String userQuestion, String context) {
return CompletableFuture.supplyAsync(() -> {
// 这里是实际的、可能耗时的调用逻辑
return doAskQuestion(userQuestion, context);
}, taskExecutor);
}
private String doAskQuestion(String userQuestion, String context) {
// ... 同之前的askQuestion方法逻辑
ChatCompletionRequest request = buildRequest(userQuestion, context);
ChatCompletionResponse response = llmApiClient.createChatCompletion(request).block();
return extractAnswer(response);
}
}
然后Controller可以这样调用:
@PostMapping("/ask-async")
public CompletableFuture<ApiResponse<String>> askAsync(@Valid @RequestBody QuestionRequest questionRequest) {
return qaService.askQuestionAsync(questionRequest.getQuestion(), questionRequest.getContext())
.thenApply(ApiResponse::success);
}
这样,请求进来后,Spring会立即返回一个DeferredResult或CompletableFuture,释放了处理请求的线程。实际的重活(调用模型API)在另一个线程池里慢慢干,干完了再把结果填回去。服务的并发能力一下子就上来了。
优化二:引入缓存。 很多用户问的问题可能是相似的,比如“公司年假制度是什么?”。我们没必要每次都花一两秒去问大模型。可以对问题进行摘要(比如MD5哈希)作为key,将答案缓存起来。
@Service
public class CachedSmartQaService {
@Autowired
private SmartQaService delegate; // 委托给实际的服务
// 使用Spring Cache抽象,底层我们配了Caffeine
@Cacheable(value = "qaCache", key = "#userQuestion.concat(#context ?: '')")
public String askQuestionWithCache(String userQuestion, String context) {
// 只有当缓存未命中时,才会执行这个方法
return delegate.askQuestion(userQuestion, context);
}
}
这里用@Cacheable注解,Spring会自动帮我们管理缓存。key的设计很重要,要确保相同的问题(和上下文)能命中同一个key。对于更复杂的场景,你可能需要先对用户问题进行一些标准化处理(比如去除空格、转换成小写)再生成key。
优化三:应对高并发与限流。 当突发大量请求涌向模型API时,可能会触发对方的限流,或者拖垮我们自己的服务。我们需要一个“闸口”。
- 使用连接池:确保
WebClient使用了连接池(默认是有的),避免频繁创建连接的开销。 - 客户端限流:可以使用Resilience4j或Sentinel这样的库,为
LlmApiClient.createChatCompletion方法添加一个限流器(Rate Limiter)或熔断器(Circuit Breaker)。 - 服务端限流:在Controller入口,可以使用
@RateLimiter等注解,或者利用网关(如Spring Cloud Gateway)的全局限流功能,保护下游服务。
这里给一个Resilience4j限流的简单示例:
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
@Service
public class RateLimitedLlmService {
@Autowired
private LlmApiClient llmApiClient;
@RateLimiter(name = "llmApiRateLimiter", fallbackMethod = "rateLimitFallback")
public Mono<ChatCompletionResponse> callWithRateLimit(ChatCompletionRequest request) {
return llmApiClient.createChatCompletion(request);
}
// 降级方法,当被限流时返回友好提示
private Mono<ChatCompletionResponse> rateLimitFallback(ChatCompletionRequest request, Throwable t) {
log.warn("触发模型API限流降级");
// 返回一个包含提示信息的响应,或者放入队列稍后重试
return Mono.just(createBusyResponse());
}
}
6. 一个更完整的实战案例:智能客服工单分类
光有问答还不够,我们来看一个更具体的应用场景:自动化工单分类。用户提交一段文字描述问题,系统需要自动判断它属于“账号问题”、“支付问题”、“技术故障”还是“产品咨询”。
我们可以利用大模型的文本理解能力,通过设计好的“提示词”(Prompt)让它来完成分类。
在SmartQaService里增加一个方法:
public String classifyTicket(String description) {
String systemPrompt = """
你是一个工单分类助手。请根据用户对问题的描述,将其归类到以下类别之一:
- ACCOUNT: 账号相关,如登录、注册、密码修改。
- PAYMENT: 支付相关,如扣款失败、退款申请。
- TECH: 技术故障,如页面打不开、功能报错。
- PRODUCT: 产品咨询,如功能如何使用、价格咨询。
- OTHER: 其他类别。
请只输出类别英文代号,不要输出任何其他解释。
用户描述:
""";
ChatCompletionRequest request = new ChatCompletionRequest();
request.setModel("moyu-pro");
request.setTemperature(0.1); // 降低创造性,让输出更确定
request.setMaxTokens(10);
request.setMessages(Arrays.asList(
new ChatCompletionRequest.Message("system", systemPrompt),
new ChatCompletionRequest.Message("user", description)
));
ChatCompletionResponse response = llmApiClient.createChatCompletion(request).block();
String rawOutput = extractAnswer(response);
// 简单清理输出,确保是预定义的类别
return validateCategory(rawOutput.trim());
}
这样,后端收到工单描述后,可以先调用这个分类服务,自动打上标签,再路由给相应的客服团队,大大提升了效率。
7. 部署与监控建议
开发完了,最后聊聊部署和上线后怎么管。
部署:和部署任何SpringBoot应用一样,打成JAR包,用java -jar运行,或者放在Docker容器里。关键是要妥善管理配置文件,尤其是API密钥,一定要用环境变量或配置中心注入。
监控:这是保证服务稳定的眼睛。
- 应用监控:集成Spring Boot Actuator,暴露
/health,/metrics,/prometheus端点,监控服务状态、JVM内存、线程池情况。 - 业务监控:在
LlmApiClient和SmartQaService中关键位置埋点,记录每次调用的耗时、成功与否、Token消耗量。这些数据对成本控制和性能分析至关重要。 - 日志:确保所有请求、响应(可脱敏)以及异常都有清晰的日志记录,方便排查问题。
- 告警:为API调用平均耗时、失败率、缓存命中率等关键指标设置告警阈值。
8. 总结
走完这一趟,感觉用SpringBoot来集成像墨语灵犀这样的大模型API,是一条非常可行的路径。它没有想象中那么“黑科技”,本质上还是对外部HTTP服务的调用和封装。SpringBoot生态提供的武器,比如WebClient、异步编程、缓存、限流熔断,都能很好地应对引入AI能力后带来的新挑战——延迟、波动、成本。
这套方案的价值在于,它让AI能力以一种低侵入、易管理的方式融入到了现有的Java技术体系中。你不需要组建专门的AI运维团队,业务开发同学就能理解和维护。从简单的智能问答,到复杂的工单分类、内容审核、数据提取,都可以基于这个微服务快速扩展。
当然,实际应用中还会遇到更多细节问题,比如提示词如何迭代优化、如何评估回答质量、如何设计更高效的缓存策略。但有了这个基础框架,剩下的就是不断迭代和打磨了。如果你正准备尝试,建议先从一个小而具体的场景开始,快速验证效果,再逐步扩大应用范围。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
更多推荐


所有评论(0)