SpringBoot整合AI大模型:从零构建智能问答系统(实战指南)
1. 为什么你的SpringBoot项目需要一个“AI大脑”?
最近几年,AI大模型的发展速度真的太快了,从最初的ChatGPT到现在的各种国产模型,感觉每个月都有新东西出来。我身边很多做Java开发的朋友都在问,怎么才能把这些强大的AI能力快速集成到自己的SpringBoot项目里?毕竟,谁不想让自己的系统变得更“聪明”一点呢?
想象一下,如果你的在线教育平台能自动生成高质量的练习题,如果你的客服系统能理解用户的情绪并给出更贴心的回答,如果你的内容管理平台能自动撰写文章摘要……这些场景听起来很酷,但实现起来是不是感觉门槛很高?其实不然。我今天要跟你分享的,就是如何用SpringBoot这个我们最熟悉的框架,给项目装上“AI大脑”。整个过程就像搭积木一样,有现成的模块可以直接用,你只需要关心业务逻辑怎么设计就行。
我做过不少这类项目,从最初的直接调用API,到后来封装成通用模块,踩过不少坑,也总结出了一套比较成熟的做法。这篇文章我会手把手带你走一遍,从环境准备、API接入,到模块封装、Prompt设计,最后前后端联调,每个环节我都会给出详细的代码和配置。你完全可以把这篇文章当作一个实战指南,跟着做下来,就能在自己的项目里用上AI能力了。
2. 环境准备与模型平台选择
2.1 主流AI平台对比
在开始写代码之前,我们得先选一个AI平台。现在国内外的选择很多,各有各的特点。我整理了一个简单的对比表格,帮你快速了解:
| 平台名称 | 特点 | 适合场景 | 接入难度 |
|---|---|---|---|
| 智谱AI (ChatGLM) | 清华系背景,中文理解强,有免费额度 | 中文对话、代码生成、内容创作 | ★★☆☆☆ |
| 百度文心一言 | 百度生态完善,文档齐全 | 搜索增强、知识问答、多模态 | ★★★☆☆ |
| 阿里通义千问 | 阿里云集成好,企业级支持 | 电商、客服、企业应用 | ★★★☆☆ |
| 深度求索 (DeepSeek) | 完全免费,API开放 | 学习实验、个人项目、成本敏感 | ★★☆☆☆ |
| OpenAI GPT系列 | 效果稳定,生态成熟 | 国际化项目、复杂推理 | ★★★★☆ |
我个人的经验是,如果是国内项目,特别是对中文支持要求高的,智谱AI和文心一言都是不错的选择。它们提供了完整的Java SDK,文档也比较友好。这篇文章我会以智谱AI为例来演示,但我要强调的是,封装思路是通用的。你学会了这套方法,换到其他平台也就是改个依赖和配置的事儿。
2.2 创建SpringBoot项目
我们先从创建一个干净的SpringBoot项目开始。我习惯用Spring Initializr,选上这几个依赖:
- Spring Web
- Lombok(简化代码的神器)
- Configuration Processor(让配置有提示)
<!-- 这是pom.xml的基础部分 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
项目创建好后,我建议你先建立一个清晰的包结构。这是我的习惯:
src/main/java/com/yourcompany/ai/
├── config/ # 配置类
├── manager/ # AI管理模块
├── model/ # 请求响应实体
├── constant/ # 常量定义
└── controller/ # 对外接口
好的结构能让后续的维护轻松很多。特别是当AI功能越来越多时,你不会被一堆散落的代码搞晕。
2.3 获取API密钥
接下来是获取API密钥,这一步很重要,也需要注意安全。以智谱AI为例,你需要:
- 访问智谱AI开放平台官网
- 注册账号并完成实名认证(国内平台基本都需要)
- 在控制台创建应用,然后就能看到API Key了
这里有个重要提醒:API Key就像你的银行卡密码,绝对不能直接写在代码里提交到Git!我见过太多因为密钥泄露导致账单暴增的案例。正确的做法是放在环境变量或者配置文件中,然后通过.gitignore排除。
我通常这样处理:在application.yml里配置,但实际的值从环境变量读取:
# application.yml
ai:
zhipu:
api-key: ${ZHIPU_API_KEY:} # 优先从环境变量读取
base-url: https://open.bigmodel.cn/api/paas/v4
然后在启动项目时设置环境变量。本地开发可以用.env文件,生产环境用K8s ConfigMap或者云平台的密钥管理服务。
3. 基础接入:第一个AI对话接口
3.1 添加SDK依赖
选好平台后,第一步就是引入对应的SDK。智谱AI的Java SDK在Maven中央仓库,添加很方便:
<dependency>
<groupId>cn.bigmodel.openapi</groupId>
<artifactId>oapi-java-sdk</artifactId>
<version>release-V4-2.3.0</version>
</dependency>
如果你用的是其他平台,比如百度文心一言,依赖可能是这样的:
<!-- 百度文心一言示例 -->
<dependency>
<groupId>com.baidu.aip</groupId>
<artifactId>java-sdk</artifactId>
<version>4.16.5</version>
</dependency>
添加完依赖,记得刷新Maven。有时候SDK版本更新比较快,建议去官方文档确认最新版本号。我遇到过因为用了旧版本SDK导致某些新功能不可用的情况。
3.2 编写配置类
有了依赖,接下来要创建配置类。这个类的目的是把API Key等配置信息管理起来,并且创建好AI客户端实例。我习惯用@ConfigurationProperties,这样配置可以在yml文件里自动补全:
@Configuration
@ConfigurationProperties(prefix = "ai.zhipu")
@Data
public class ZhipuAiConfig {
private String apiKey;
private String baseUrl;
@Bean
public ClientV4 zhipuClient() {
return new ClientV4.Builder(apiKey)
.build();
}
}
注意这里的@Data是Lombok注解,自动生成getter/setter。如果你不用Lombok,需要自己写这些方法。
配置类写好后,在application.yml里加上配置:
ai:
zhipu:
api-key: "你的API密钥"
base-url: "https://open.bigmodel.cn/api/paas/v4"
3.3 实现简单的对话接口
现在可以写第一个AI接口了。我们先创建一个Controller,实现最基本的问答功能:
@RestController
@RequestMapping("/api/ai")
public class ChatController {
@Autowired
private ClientV4 zhipuClient;
@PostMapping("/chat")
public String simpleChat(@RequestParam String question) {
// 构造消息
List<ChatMessage> messages = new ArrayList<>();
ChatMessage userMessage = new ChatMessage(
ChatMessageRole.USER.value(),
question
);
messages.add(userMessage);
// 构建请求
ChatCompletionRequest request = ChatCompletionRequest.builder()
.model(Constants.ModelChatGLM4)
.messages(messages)
.stream(Boolean.FALSE)
.build();
try {
// 调用API
ModelApiResponse response = zhipuClient.invokeModelApi(request);
return response.getData().getChoices().get(0).getMessage().getContent();
} catch (Exception e) {
throw new RuntimeException("AI服务调用失败", e);
}
}
}
写完后启动项目,用Postman测试一下:
POST http://localhost:8080/api/ai/chat?question=你好,介绍一下SpringBoot
如果一切正常,你应该能收到AI的回答。不过这个版本还很简陋,没有错误处理,也没有考虑性能问题。别急,我们一步步优化。
3.4 添加异常处理
在实际项目中,网络可能不稳定,API可能限流,各种异常都需要处理。我建议创建一个统一的异常处理机制:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAiException(Exception e) {
ErrorResponse error = new ErrorResponse();
error.setCode("AI_SERVICE_ERROR");
error.setMessage("AI服务暂时不可用");
error.setDetail(e.getMessage());
error.setTimestamp(LocalDateTime.now());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(error);
}
}
@Data
class ErrorResponse {
private String code;
private String message;
private String detail;
private LocalDateTime timestamp;
}
这样前端收到错误时,能有一个统一的格式,方便展示给用户。
4. 封装通用AI管理模块
4.1 为什么需要封装?
直接在每个Controller里写AI调用代码行不行?技术上可以,但维护起来会很痛苦。想象一下,如果项目里有10个地方需要调用AI,每个地方都要写一遍构建请求、处理响应的代码,哪天API升级或者要换平台,你得改10个地方。
封装的目的就是一次编写,多处使用。好的封装应该:
- 隐藏底层API的复杂性
- 提供统一的调用接口
- 内置常用功能(重试、限流、监控)
- 方便切换AI平台
我设计了一个AiManager类,这是整个封装的核心。
4.2 核心管理器设计
先看AiManager的基本结构:
@Component
@Slf4j
public class AiManager {
@Resource
private ClientV4 zhipuClient;
// 温度参数:控制输出的随机性
private static final float DEFAULT_TEMPERATURE = 0.7f;
private static final float CREATIVE_TEMPERATURE = 0.9f; // 创意任务用
private static final float PRECISE_TEMPERATURE = 0.3f; // 精确任务用
/**
* 同步调用 - 最常用的方法
*/
public String syncChat(String systemPrompt, String userMessage) {
return syncChat(systemPrompt, userMessage, DEFAULT_TEMPERATURE);
}
/**
* 带温度参数的同步调用
*/
public String syncChat(String systemPrompt, String userMessage, float temperature) {
List<ChatMessage> messages = buildMessages(systemPrompt, userMessage);
ChatCompletionRequest request = ChatCompletionRequest.builder()
.model(Constants.ModelChatGLM4)
.messages(messages)
.temperature(temperature)
.stream(Boolean.FALSE)
.build();
try {
ModelApiResponse response = zhipuClient.invokeModelApi(request);
return extractContent(response);
} catch (Exception e) {
log.error("AI同步调用失败", e);
throw new AiServiceException("AI服务调用失败", e);
}
}
/**
* 流式调用 - 适合长文本生成
*/
public Flux<String> streamChat(String systemPrompt, String userMessage) {
return Flux.create(sink -> {
try {
List<ChatMessage> messages = buildMessages(systemPrompt, userMessage);
ChatCompletionRequest request = ChatCompletionRequest.builder()
.model(Constants.ModelChatGLM4)
.messages(messages)
.stream(Boolean.TRUE)
.build();
ModelApiResponse response = zhipuClient.invokeModelApi(request);
response.getFlowable().subscribe(
data -> sink.next(data.getContent()),
sink::error,
sink::complete
);
} catch (Exception e) {
sink.error(e);
}
});
}
// 辅助方法
private List<ChatMessage> buildMessages(String systemPrompt, String userMessage) {
List<ChatMessage> messages = new ArrayList<>();
if (StringUtils.hasText(systemPrompt)) {
messages.add(new ChatMessage(ChatMessageRole.SYSTEM.value(), systemPrompt));
}
messages.add(new ChatMessage(ChatMessageRole.USER.value(), userMessage));
return messages;
}
private String extractContent(ModelApiResponse response) {
if (response == null || response.getData() == null
|| response.getData().getChoices() == null
|| response.getData().getChoices().isEmpty()) {
throw new AiServiceException("AI返回结果为空");
}
return response.getData().getChoices().get(0).getMessage().getContent();
}
}
这个设计有几个关键点:
- 方法重载:提供不同参数组合的方法,方便调用
- 温度参数:这是控制AI创造力的关键参数,我提供了几个预设值
- 异常处理:统一捕获异常,并转换为自定义异常
- 流式支持:用Flux支持流式响应,用户体验更好
4.3 支持多模型切换
实际项目中,你可能需要根据场景选择不同的模型。比如简单问答用轻量模型,复杂推理用大模型。我在AiManager里增加了模型选择支持:
public class AiManager {
// ... 其他代码
/**
* 模型枚举
*/
public enum Model {
GLM_4("glm-4", "通用大模型"),
GLM_4_AIR("glm-4-air", "轻量模型,响应快"),
GLM_4_LONG("glm-4-long", "长文本模型");
private final String code;
private final String desc;
Model(String code, String desc) {
this.code = code;
this.desc = desc;
}
public String getCode() { return code; }
public String getDesc() { return desc; }
}
/**
* 指定模型的调用
*/
public String chatWithModel(Model model, String systemPrompt, String userMessage) {
List<ChatMessage> messages = buildMessages(systemPrompt, userMessage);
ChatCompletionRequest request = ChatCompletionRequest.builder()
.model(model.getCode()) // 使用指定模型
.messages(messages)
.stream(Boolean.FALSE)
.build();
// ... 调用逻辑
}
}
这样调用时就可以根据需求选模型了:
// 快速响应场景
String quickAnswer = aiManager.chatWithModel(
AiManager.Model.GLM_4_AIR,
"你是一个简洁的助手",
"今天天气如何?"
);
// 复杂推理场景
String complexAnswer = aiManager.chatWithModel(
AiManager.Model.GLM_4,
"你是一个专业的分析师",
"分析一下当前市场趋势..."
);
4.4 添加重试机制
网络请求难免失败,特别是调用外部API时。我建议加上重试机制:
@Slf4j
@Component
public class AiManager {
// ... 其他代码
@Resource
private RetryTemplate retryTemplate;
/**
* 带重试的调用
*/
public String chatWithRetry(String systemPrompt, String userMessage) {
return retryTemplate.execute(context -> {
log.info("第{}次尝试调用AI服务", context.getRetryCount() + 1);
return syncChat(systemPrompt, userMessage);
});
}
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate template = new RetryTemplate();
// 指数退避策略
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(1000); // 初始间隔1秒
backOffPolicy.setMultiplier(2.0); // 每次翻倍
backOffPolicy.setMaxInterval(10000); // 最大间隔10秒
// 重试策略:最多3次,遇到网络异常重试
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(3);
template.setBackOffPolicy(backOffPolicy);
template.setRetryPolicy(retryPolicy);
return template;
}
}
重试机制能显著提高系统的健壮性。特别是对于偶尔的网络抖动,自动重试往往能解决问题。
5. Prompt工程实战技巧
5.1 什么是好的Prompt?
很多人觉得AI不好用,其实问题往往出在Prompt上。好的Prompt就像给AI清晰的指令,它才知道你想要什么。我总结了几条原则:
- 明确角色:告诉AI它应该扮演什么角色
- 具体任务:清晰描述要做什么
- 输出格式:指定返回数据的格式
- 约束条件:有哪些限制和要求
让我用实际例子说明。假设我们要让AI生成产品描述:
差的Prompt:"写一个产品描述"
好的Prompt:
你是一名专业的电商文案写手。请为以下产品撰写吸引人的描述:
产品名称:无线蓝牙耳机
特点:降噪、续航30小时、防水、轻便
目标用户:通勤族、运动爱好者
要求:
1. 突出降噪和续航优势
2. 语言生动有感染力
3. 不超过200字
4. 包含3个卖点
看到区别了吗?好的Prompt让AI知道怎么发挥。
5.2 常用Prompt模式
在实际开发中,我整理了一些常用的Prompt模式,放在常量类里复用:
public class PromptTemplates {
// 文本总结
public static final String SUMMARIZE = """
你是一个专业的文本总结助手。请将以下文本总结为简洁的要点:
文本:%s
要求:
1. 提取核心信息
2. 分条列出
3. 每点不超过20字
4. 保留关键数据
""";
// 代码解释
public static final String EXPLAIN_CODE = """
你是一个资深的程序员。请解释以下代码的功能和逻辑:
代码:
```java
%s
```
要求:
1. 说明代码作用
2. 分析关键逻辑
3. 指出可能的优化点
4. 用中文回答
""";
// 数据提取
public static final String EXTRACT_DATA = """
你是一个数据提取专家。请从以下文本中提取结构化信息:
文本:%s
提取字段:
1. 人名
2. 日期
3. 金额
4. 产品名称
以JSON格式返回:
{
"people": [],
"dates": [],
"amounts": [],
"products": []
}
""";
// 客服回复
public static final String CUSTOMER_SERVICE = """
你是一家电商平台的客服助手。请根据用户问题提供专业回复:
用户问题:%s
要求:
1. 态度友好亲切
2. 解决实际问题
3. 提供明确方案
4. 适当使用表情符号
5. 不超过150字
""";
}
使用时这样调用:
String text = "这是一段很长的文章内容...";
String prompt = String.format(PromptTemplates.SUMMARIZE, text);
String summary = aiManager.syncChat(prompt, "");
5.3 动态Prompt构建
有时候Prompt需要根据业务数据动态生成。我设计了一个PromptBuilder工具类:
@Component
public class PromptBuilder {
/**
* 构建产品推荐Prompt
*/
public String buildProductRecommendationPrompt(User user, List<Product> history) {
StringBuilder prompt = new StringBuilder();
prompt.append("你是一个个性化的购物助手。");
prompt.append("用户信息:").append(user.getAge()).append("岁,")
.append(user.getGender()).append(",兴趣:").append(user.getInterests());
if (!history.isEmpty()) {
prompt.append("\n历史购买:");
history.forEach(p ->
prompt.append(p.getName()).append("、")
);
}
prompt.append("\n请推荐3个适合的商品,说明推荐理由。");
return prompt.toString();
}
/**
* 构建学习计划Prompt
*/
public String buildStudyPlanPrompt(String subject, String level, int hoursPerWeek) {
return String.format("""
你是一个教育专家。请为一名%s水平的学习者制定%s学习计划。
要求:
1. 每周学习%d小时
2. 分四周进行
3. 每周有具体目标和练习
4. 包含学习资源和检测方法
以表格形式输出计划。
""", level, subject, hoursPerWeek);
}
}
5.4 Prompt版本管理
随着业务发展,Prompt可能需要调整优化。我建议对Prompt进行版本管理:
@Configuration
@RefreshScope // 支持热更新
public class PromptConfig {
@Value("${prompt.version:1.0}")
private String promptVersion;
@Bean
public Map<String, String> promptTemplates() {
Map<String, String> prompts = new HashMap<>();
// 根据版本加载不同的Prompt
if ("1.0".equals(promptVersion)) {
prompts.put("summarize", PromptTemplates.SUMMARIZE_V1);
prompts.put("recommend", PromptTemplates.RECOMMEND_V1);
} else if ("2.0".equals(promptVersion)) {
prompts.put("summarize", PromptTemplates.SUMMARIZE_V2);
prompts.put("recommend", PromptTemplates.RECOMMEND_V2);
}
return prompts;
}
}
这样可以在不重启服务的情况下切换Prompt版本,方便A/B测试。
6. 业务集成实战案例
6.1 智能客服系统
让我们看一个实际的业务场景:智能客服。需求是用户输入问题,AI自动回复,如果AI不确定就转人工。
首先定义业务逻辑:
@Service
@Slf4j
public class CustomerService {
@Resource
private AiManager aiManager;
@Resource
private HumanAgentService agentService;
/**
* 处理用户咨询
*/
public ChatResponse handleInquiry(String userId, String question) {
// 1. 构建客服Prompt
String systemPrompt = """
你是XX公司的智能客服,专业、友好、乐于助人。
公司产品:软件服务、技术支持、培训课程
服务时间:周一至周五 9:00-18:00
回答要求:
1. 准确回答产品相关问题
2. 技术问题提供解决思路
3. 价格问题引导联系销售
4. 如果无法确定,建议转人工
5. 回复不超过100字
""";
// 2. 调用AI
String aiResponse = aiManager.syncChat(systemPrompt, question);
// 3. 判断是否需要转人工
if (needHumanAgent(aiResponse, question)) {
log.info("用户{}的问题需要人工处理", userId);
agentService.assignToAgent(userId, question);
return ChatResponse.transferToHuman();
}
// 4. 记录对话历史
saveChatHistory(userId, question, aiResponse);
return ChatResponse.aiResponse(aiResponse);
}
/**
* 判断是否需要人工客服
*/
private boolean needHumanAgent(String aiResponse, String question) {
// 规则1:AI明确说需要人工
if (aiResponse.contains("转人工") || aiResponse.contains("联系客服")) {
return true;
}
// 规则2:用户明确要求人工
if (question.contains("人工") || question.contains("真人")) {
return true;
}
// 规则3:AI回答置信度低(这里简化处理)
if (aiResponse.length() < 20 || aiResponse.contains("不太确定")) {
return true;
}
return false;
}
}
这个实现有几个亮点:
- 清晰的职责分离:AI处理标准问题,复杂问题转人工
- 智能判断逻辑:多种条件判断是否需要人工
- 完整的业务流程:包含历史记录和状态管理
6.2 内容自动生成系统
另一个常见场景是内容生成,比如自动写文章摘要、生成产品描述等。
@Service
public class ContentGenerator {
@Resource
private AiManager aiManager;
/**
* 生成文章摘要
*/
public Summary generateArticleSummary(Article article) {
String prompt = String.format("""
你是一个专业的编辑。请为以下文章生成摘要:
标题:%s
作者:%s
正文:
%s
摘要要求:
1. 提炼核心观点
2. 保留关键数据
3. 语言简洁明了
4. 不超过150字
5. 包含文章价值点
返回格式:
{
"summary": "摘要内容",
"keywords": ["关键词1", "关键词2", "关键词3"],
"readingTime": 预估阅读分钟数
}
""", article.getTitle(), article.getAuthor(), article.getContent());
String aiResponse = aiManager.syncChat(prompt, "");
// 解析AI返回的JSON
return parseSummary(aiResponse);
}
/**
* 批量生成产品描述
*/
public List<ProductDescription> batchGenerateDescriptions(List<Product> products) {
List<ProductDescription> results = new ArrayList<>();
for (Product product : products) {
String prompt = buildProductPrompt(product);
String description = aiManager.syncChat(prompt, "");
ProductDescription pd = new ProductDescription();
pd.setProductId(product.getId());
pd.setDescription(description);
pd.setGeneratedTime(LocalDateTime.now());
results.add(pd);
// 避免频繁调用,加一点延迟
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
return results;
}
private String buildProductPrompt(Product product) {
return String.format("""
你是一个电商文案专家。请为以下产品撰写吸引人的描述:
产品名称:%s
类别:%s
特点:%s
目标用户:%s
价格:%s元
要求:
1. 突出产品卖点
2. 激发购买欲望
3. 适合电商平台展示
4. 包含3个使用场景
5. 200字左右
""", product.getName(), product.getCategory(),
product.getFeatures(), product.getTargetUsers(), product.getPrice());
}
}
6.3 代码审查助手
对于技术团队,AI可以帮助审查代码:
@Service
public class CodeReviewAssistant {
@Resource
private AiManager aiManager;
/**
* 审查Java代码
*/
public CodeReviewResult reviewJavaCode(String code, String requirements) {
String prompt = String.format("""
你是一个资深的Java架构师。请审查以下代码:
需求背景:%s
代码:
```java
%s
```
请从以下角度审查:
1. 代码规范(命名、注释、格式)
2. 性能问题(时间复杂度、内存使用)
3. 安全性(SQL注入、XSS等)
4. 可维护性(复杂度、耦合度)
5. 最佳实践遵循情况
发现问题请给出:
- 问题描述
- 风险等级(高/中/低)
- 修改建议
- 示例代码
以JSON格式返回审查结果。
""", requirements, code);
String reviewResult = aiManager.syncChat(prompt, "");
return parseReviewResult(reviewResult);
}
/**
* 生成单元测试
*/
public String generateUnitTest(String className, String methodSignature, String logicDescription) {
String prompt = String.format("""
你是一个测试专家。请为以下方法生成单元测试:
类名:%s
方法签名:%s
方法逻辑:%s
要求:
1. 使用JUnit 5
2. 覆盖正常场景和边界条件
3. 包含必要的Mock
4. 添加有意义的断言
5. 包含测试说明
只返回测试代码,不要解释。
""", className, methodSignature, logicDescription);
return aiManager.syncChat(prompt, "");
}
}
7. 高级功能与性能优化
7.1 流式响应实现
对于长文本生成,流式响应能显著提升用户体验。Spring Boot支持Server-Sent Events (SSE):
@RestController
@RequestMapping("/api/ai/stream")
public class StreamChatController {
@Resource
private AiManager aiManager;
/**
* 流式对话接口
*/
@GetMapping("/chat")
public Flux<ServerSentEvent<String>> streamChat(
@RequestParam String question,
@RequestParam(defaultValue = "false") boolean creative) {
String systemPrompt = "你是一个有帮助的助手";
float temperature = creative ? 0.9f : 0.7f;
return aiManager.streamChat(systemPrompt, question, temperature)
.map(content -> ServerSentEvent.builder(content).build())
.onErrorResume(e -> {
log.error("流式对话失败", e);
return Flux.just(
ServerSentEvent.builder("[服务异常]")
.id("error")
.build()
);
});
}
/**
* 长文本生成(如文章写作)
*/
@PostMapping("/generate")
public Flux<ServerSentEvent<String>> generateArticle(
@RequestBody ArticleRequest request) {
String prompt = String.format("""
请以'%s'为主题写一篇文章。
要求:
1. 字数:%d字左右
2. 风格:%s
3. 结构:引言、正文、结论
4. 包含实际案例
请逐步生成,先写大纲,再填充内容。
""", request.getTopic(), request.getWordCount(), request.getStyle());
return aiManager.streamChat(prompt, "")
.delayElements(Duration.ofMillis(100)) // 控制输出速度
.map(chunk -> {
// 可以在这里添加进度信息
return ServerSentEvent.builder(chunk)
.event("chunk")
.build();
});
}
}
前端可以这样接收流式响应:
// 前端JavaScript代码
const eventSource = new EventSource('/api/ai/stream/chat?question=' + encodeURIComponent(question));
eventSource.onmessage = function(event) {
const chunk = event.data;
// 逐步显示到页面
document.getElementById('response').innerHTML += chunk;
// 自动滚动到底部
const container = document.getElementById('response-container');
container.scrollTop = container.scrollHeight;
};
eventSource.onerror = function(event) {
console.error('流式连接错误', event);
eventSource.close();
};
7.2 缓存优化策略
AI调用比较耗时,合理使用缓存能大幅提升性能:
@Service
@Slf4j
public class CachedAiService {
@Resource
private AiManager aiManager;
@Resource
private CacheManager cacheManager;
/**
* 带缓存的AI调用
*/
@Cacheable(value = "aiResponses", key = "#systemPrompt + '|' + #userMessage")
public String cachedChat(String systemPrompt, String userMessage) {
log.info("缓存未命中,调用AI服务");
return aiManager.syncChat(systemPrompt, userMessage);
}
/**
* 带条件缓存的调用
*/
@Cacheable(value = "aiResponses",
key = "#systemPrompt + '|' + #userMessage",
condition = "#userMessage.length() < 100")
public String conditionalCachedChat(String systemPrompt, String userMessage) {
// 只有短问题才缓存
return aiManager.syncChat(systemPrompt, userMessage);
}
/**
* 手动缓存管理
*/
public String manualCachedChat(String systemPrompt, String userMessage) {
String cacheKey = generateCacheKey(systemPrompt, userMessage);
Cache cache = cacheManager.getCache("aiResponses");
// 先查缓存
Cache.ValueWrapper cached = cache.get(cacheKey);
if (cached != null) {
log.info("缓存命中: {}", cacheKey);
return (String) cached.get();
}
// 调用AI
String response = aiManager.syncChat(systemPrompt, userMessage);
// 存入缓存(带TTL)
cache.put(cacheKey, response);
return response;
}
/**
* 批量预缓存
*/
@Scheduled(fixedDelay = 3600000) // 每小时执行一次
public void preCacheCommonQuestions() {
List<CommonQuestion> questions = questionService.getHotQuestions();
for (CommonQuestion question : questions) {
try {
String response = aiManager.syncChat(
"你是智能助手",
question.getContent()
);
cacheManager.getCache("aiResponses")
.put(question.getContent(), response);
log.info("预缓存问题: {}", question.getContent());
// 避免频繁调用
Thread.sleep(500);
} catch (Exception e) {
log.warn("预缓存失败: {}", question.getContent(), e);
}
}
}
}
缓存配置示例:
# application.yml
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterWrite=1h
7.3 限流与降级
AI API通常有调用限制,我们需要在客户端做好限流:
@Component
public class RateLimiterService {
// 令牌桶限流器:每秒10个令牌
private final RateLimiter rateLimiter = RateLimiter.create(10.0);
// 并发信号量:最大10个并发
private final Semaphore concurrencySemaphore = new Semaphore(10);
/**
* 带限流的AI调用
*/
public String rateLimitedChat(String systemPrompt, String userMessage) {
// 1. 获取令牌(等待最多1秒)
if (!rateLimiter.tryAcquire(1, TimeUnit.SECONDS)) {
throw new RateLimitException("请求过于频繁,请稍后重试");
}
// 2. 获取并发许可
if (!concurrencySemaphore.tryAcquire()) {
throw new ConcurrencyLimitException("系统繁忙,请稍后重试");
}
try {
// 3. 调用AI
return aiManager.syncChat(systemPrompt, userMessage);
} finally {
// 4. 释放并发许可
concurrencySemaphore.release();
}
}
/**
* 降级策略:当AI服务不可用时
*/
@Slf4j
@Component
public static class FallbackService {
private final Map<String, String> fallbackResponses = new HashMap<>();
public FallbackService() {
// 初始化兜底回答
fallbackResponses.put("你好", "您好!我现在有点忙,请稍后再试。");
fallbackResponses.put("天气", "天气查询功能暂时不可用。");
fallbackResponses.put("帮助", "请尝试刷新页面或联系客服。");
}
/**
* 获取兜底回答
*/
public String getFallbackResponse(String question) {
// 简单关键词匹配
for (Map.Entry<String, String> entry : fallbackResponses.entrySet()) {
if (question.contains(entry.getKey())) {
return entry.getValue();
}
}
// 默认回答
return "抱歉,服务暂时不可用。我们已经收到您的问题,稍后会为您处理。";
}
}
}
7.4 监控与日志
完善的监控能帮助我们及时发现和解决问题:
@Aspect
@Component
@Slf4j
public class AiMonitorAspect {
@Around("execution(* com..AiManager.*(..))")
public Object monitorAiCall(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
long startTime = System.currentTimeMillis();
boolean success = false;
try {
Object result = joinPoint.proceed();
success = true;
return result;
} catch (Exception e) {
log.error("AI调用失败 - 方法: {}, 参数: {}", methodName, args, e);
throw e;
} finally {
long cost = System.currentTimeMillis() - startTime;
// 记录指标
recordMetrics(methodName, success, cost, args);
// 慢查询告警
if (cost > 5000) { // 超过5秒
log.warn("AI调用过慢 - 方法: {}, 耗时: {}ms", methodName, cost);
alertSlowCall(methodName, cost, args);
}
}
}
private void recordMetrics(String method, boolean success, long cost, Object[] args) {
// 这里可以接入监控系统,如Prometheus
Metrics.counter("ai_calls_total",
"method", method,
"success", String.valueOf(success))
.increment();
Metrics.timer("ai_call_duration",
"method", method)
.record(cost, TimeUnit.MILLISECONDS);
// 记录到日志系统
log.info("AI调用统计 - 方法: {}, 成功: {}, 耗时: {}ms",
method, success, cost);
}
private void alertSlowCall(String method, long cost, Object[] args) {
// 发送告警通知
String message = String.format(
"AI服务响应过慢\n方法: %s\n耗时: %dms\n时间: %s",
method, cost, LocalDateTime.now()
);
// 可以集成企业微信、钉钉、邮件等通知方式
notificationService.sendAlert(message);
}
}
8. 前后端联调与部署
8.1 前端集成示例
前端调用AI接口时,需要考虑用户体验。这里是一个Vue 3的示例:
<template>
<div class="chat-container">
<div class="messages" ref="messagesRef">
<div v-for="(msg, index) in messages" :key="index"
:class="['message', msg.role]">
{{ msg.content }}
</div>
<!-- 流式响应显示 -->
<div v-if="streaming" class="message assistant streaming">
{{ streamContent }}
<span class="cursor">▌</span>
</div>
</div>
<div class="input-area">
<textarea v-model="inputText"
@keydown.enter.prevent="sendMessage"
placeholder="输入您的问题...">
</textarea>
<button @click="sendMessage" :disabled="loading">
{{ loading ? '思考中...' : '发送' }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
const messages = ref([])
const inputText = ref('')
const loading = ref(false)
const streaming = ref(false)
const streamContent = ref('')
const messagesRef = ref(null)
// 发送消息
const sendMessage = async () => {
if (!inputText.value.trim() || loading.value) return
const userMessage = inputText.value.trim()
inputText.value = ''
// 添加用户消息
messages.value.push({
role: 'user',
content: userMessage
})
// 滚动到底部
scrollToBottom()
// 调用AI接口
loading.value = true
streaming.value = true
streamContent.value = ''
try {
const response = await fetch('/api/ai/stream/chat?question=' +
encodeURIComponent(userMessage))
if (!response.ok) throw new Error('请求失败')
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value)
streamContent.value += chunk
// 每次收到数据都滚动到底部
scrollToBottom()
}
// 流式接收完成,添加到消息列表
messages.value.push({
role: 'assistant',
content: streamContent.value
})
} catch (error) {
console.error('AI对话失败:', error)
messages.value.push({
role: 'assistant',
content: '抱歉,服务暂时不可用。'
})
} finally {
loading.value = false
streaming.value = false
streamContent.value = ''
scrollToBottom()
}
}
// 自动滚动到底部
const scrollToBottom = () => {
nextTick(() => {
if (messagesRef.value) {
messagesRef.value.scrollTop = messagesRef.value.scrollHeight
}
})
}
// 初始化欢迎消息
onMounted(() => {
messages.value.push({
role: 'assistant',
content: '你好!我是AI助手,有什么可以帮您的?'
})
})
</script>
<style scoped>
.chat-container {
max-width: 800px;
margin: 0 auto;
height: 600px;
display: flex;
flex-direction: column;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #f5f5f5;
}
.message {
margin-bottom: 15px;
padding: 12px 16px;
border-radius: 12px;
max-width: 80%;
word-wrap: break-word;
}
.message.user {
background: #007bff;
color: white;
margin-left: auto;
}
.message.assistant {
background: white;
border: 1px solid #ddd;
}
.message.streaming {
background: #f0f8ff;
border-left: 4px solid #007bff;
}
.cursor {
animation: blink 1s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.input-area {
padding: 20px;
background: white;
border-top: 1px solid #ddd;
display: flex;
gap: 10px;
}
textarea {
flex: 1;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
resize: none;
font-size: 14px;
}
button {
padding: 12px 24px;
background: #007bff;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
8.2 部署配置建议
部署AI应用时,有几个关键点需要注意:
Docker配置示例:
# Dockerfile
FROM openjdk:17-jdk-slim
WORKDIR /app
# 复制构建产物
COPY target/ai-application.jar app.jar
# 设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# JVM参数优化
ENV JAVA_OPTS="-Xmx2g -Xms1g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError"
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
Kubernetes部署配置:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ai-application
spec:
replicas: 3
selector:
matchLabels:
app: ai-application
template:
metadata:
labels:
app: ai-application
spec:
containers:
- name: ai-app
image: your-registry/ai-application:latest
ports:
- containerPort: 8080
env:
- name: ZHIPU_API_KEY
valueFrom:
secretKeyRef:
name: ai-secrets
key: zhipu-api-key
- name: SPRING_PROFILES_ACTIVE
value: "prod"
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 5
---
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: ai-application-service
spec:
selector:
app: ai-application
ports:
- port: 80
targetPort: 8080
type: ClusterIP
---
# ingress.yaml (如果需要外部访问)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ai-application-ingress
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
spec:
rules:
- host: ai.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ai-application-service
port:
number: 80
8.3 安全考虑
AI应用涉及用户数据,安全非常重要:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() // API服务通常禁用CSRF
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/ai/**").authenticated()
.anyRequest().permitAll()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
/**
* 输入内容安全检查
*/
@Component
public class ContentValidator {
private final List<String> forbiddenPatterns = Arrays.asList(
"恶意关键词1", "恶意关键词2"
);
public ValidationResult validateInput(String input) {
ValidationResult result = new ValidationResult();
// 1. 长度检查
if (input.length() > 1000) {
result.setValid(false);
result.setMessage("输入内容过长");
return result;
}
// 2. 敏感词检查
for (String pattern : forbiddenPatterns) {
if (input.contains(pattern)) {
result.setValid(false);
result.setMessage("包含不允许的内容");
return result;
}
}
// 3. SQL注入检查(简单示例)
if (input.matches(".*([';]+|(--)+).*")) {
result.setValid(false);
result.setMessage("输入内容不安全");
return result;
}
result.setValid(true);
return result;
}
}
/**
* 请求限流(防止滥用)
*/
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags(
"application", "ai-service"
);
}
@Bean
public MeterFilter meterFilter() {
return MeterFilter.deny(id -> {
String uri = id.getTag("uri");
return uri != null && uri.contains("/actuator");
});
}
}
8.4 监控告警配置
最后,设置监控告警,确保服务稳定:
# prometheus监控配置
scrape_configs:
- job_name: 'ai-application'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['ai-application:8080']
relabel_configs:
- source_labels: [__address__]
target_label: instance
regex: '(.*):.*'
replacement: '${1}'
# alertmanager告警规则
groups:
- name: ai-service-alerts
rules:
- alert: HighErrorRate
expr: rate(http_server_requests_errors_total{job="ai-application"}[5m]) > 0.1
for: 2m
labels:
severity: warning
annotations:
summary: "AI服务错误率过高"
description: "错误率超过10%,当前值 {{ $value }}"
- alert: SlowAIResponse
expr: histogram_quantile(0.95, rate(ai_call_duration_seconds_bucket[5m])) > 5
for: 5m
labels:
severity: warning
annotations:
summary: "AI响应时间过长"
description: "95%分位响应时间超过5秒,当前值 {{ $value }}s"
- alert: HighAICallRate
expr: rate(ai_calls_total{job="ai-application"}[5m]) > 100
for: 2m
labels:
severity: warning
annotations:
summary: "AI调用频率过高"
description: "可能遭到滥用,当前调用频率 {{ $value }}/s"
这些配置能帮助你在生产环境中稳定运行AI服务。记住,监控和告警不是一次性的工作,需要根据实际运行情况不断调整优化。
更多推荐

所有评论(0)