SpringBoot集成通义千问实战:从零搭建企业级智能客服,支持流式输出+对话记忆
前言
现在Java圈人人都在聊AI,身边很多同行都有同款焦虑:
- AI全是Python生态,Java开发会不会被淘汰?
- 想转型AI,从零学算法、学Python,成本太高根本卷不动
- 公司要求落地AI功能,不知道怎么和现有Java业务系统结合
其实完全没必要焦虑。企业里90%的核心业务系统都是Java栈,AI要真正落地生产、承接高并发、做数据合规,永远绕不开Java生态。我们不用去卷底层算法,只要学会「用Java对接大模型能力,嵌入现有业务」,就已经甩开了80%的同阶段开发者。
今天就带大家从零实现:SpringBoot + 通义千问大模型,搭建支持上下文记忆、流式输出的企业级智能客服。全程Java原生技术栈,代码复制就能跑,看完可以直接落地到公司项目里。
一、前置准备
1. 申请API Key
登录阿里云「百炼平台」申请通义千问API Key,新用户有免费额度,个人开发完全够用:
创建API-KEY后复制保存,后面配置文件里要用。
2. 技术栈说明
| 技术 | 版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 兼容绝大多数企业项目 |
| SpringBoot | 2.7.18 | 稳定版,生产环境常用 |
| 通义千问SDK | 2.14.5 | 官方最新稳定SDK,修复了流式输出bug |
| Lombok | 1.18.30 | 简化代码 |
踩坑提醒:2.14之前版本的SDK流式输出存在断句bug,建议直接用2.14.5及以上版本。
二、项目搭建
1. 核心Maven依赖
<dependencies>
<!-- SpringBoot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.18</version>
</dependency>
<!-- 通义千问官方SDK -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.14.5</version>
</dependency>
<!-- Lombok简化代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<optional>true</optional>
</dependency>
</dependencies>
2. application.yml配置
server:
port: 8080
# 通义千问配置
ai:
qwen:
api-key: 你的API_KEY
model: qwen-turbo # 轻量版,响应快成本低,适合客服场景
temperature: 0.7 # 创意度:0偏严谨,1偏发散
max-tokens: 2048 # 单次回复最大长度
三、核心功能实现
1. 统一请求响应实体
// ChatRequest.java 请求实体
@Data
public class ChatRequest {
private String message; // 用户提问
private String sessionId; // 会话ID,用于上下文记忆
}
// Result.java 统一返回结果
@Data
@AllArgsConstructor
public class Result<T> {
private Integer code;
private String msg;
private T data;
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}
public static <T> Result<T> fail(String msg) {
return new Result<>(500, msg, null);
}
}
2. 对话记忆管理
用本地缓存维护会话上下文,生产环境可替换为Redis:
@Component
public class ChatContextHolder {
// key: sessionId value: 历史消息列表
private final Map<String, List<Message>> contextMap = new ConcurrentHashMap<>();
// 单轮对话最多保留10条历史,避免token超限
private static final int MAX_HISTORY = 10;
/**
* 获取会话历史消息
*/
public List<Message> getHistory(String sessionId) {
return contextMap.computeIfAbsent(sessionId, k -> new ArrayList<>());
}
/**
* 添加消息到历史
*/
public void addMessage(String sessionId, Message message) {
List<Message> history = getHistory(sessionId);
history.add(message);
// 超出最大长度则移除最早的消息
if (history.size() > MAX_HISTORY) {
history.remove(0);
}
}
/**
* 清空会话
*/
public void clearSession(String sessionId) {
contextMap.remove(sessionId);
}
}
3. 核心对话服务
@Service
public class QwenChatService {
@Value("${ai.qwen.api-key}")
private String apiKey;
@Value("${ai.qwen.model}")
private String model;
@Value("${ai.qwen.temperature}")
private Float temperature;
@Value("${ai.qwen.max-tokens}")
private Integer maxTokens;
@Autowired
private ChatContextHolder contextHolder;
/**
* 带上下文的对话
*/
public String chatWithContext(String sessionId, String userMessage) {
// 1. 初始化客户端
DashScope dashScope = new DashScope(apiKey);
// 2. 获取历史消息,设置系统人设
List<Message> messages = contextHolder.getHistory(sessionId);
// 首次对话加入系统prompt,定义客服人设
if (messages.isEmpty()) {
messages.add(Message.builder()
.role(Role.SYSTEM)
.content("你是专业的企业智能客服,回答严谨简洁,只回答业务相关问题,不知道的就说暂无相关资料。")
.build());
}
// 3. 添加用户当前提问
Message userMsg = Message.builder().role(Role.USER).content(userMessage).build();
messages.add(userMsg);
// 4. 构建请求
GenerationRequest request = GenerationRequest.builder()
.model(model)
.input(MessageInput.builder().messages(messages).build())
.temperature(temperature)
.maxTokens(maxTokens)
.resultFormat(ResultFormat.MESSAGE)
.build();
try {
// 5. 调用大模型
GenerationResult result = dashScope.call(request);
String reply = result.getOutput().getChoices().get(0).getMessage().getContent();
// 6. 将AI回复存入上下文
contextHolder.addMessage(sessionId,
Message.builder().role(Role.ASSISTANT).content(reply).build());
return reply;
} catch (Exception e) {
e.printStackTrace();
return "抱歉,系统繁忙,请稍后再试。";
}
}
}
4. 流式输出实现(SSE)
企业级客服必备功能,逐字返回体验更好,避免用户长时间等待:
@RestController
@RequestMapping("/api/chat")
public class ChatController {
@Autowired
private QwenChatService chatService;
/**
* 普通对话接口
*/
@PostMapping("/send")
public Result<String> chat(@RequestBody ChatRequest request) {
String reply = chatService.chatWithContext(request.getSessionId(), request.getMessage());
return Result.success(reply);
}
/**
* 流式输出接口 SSE
*/
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamChat(String sessionId, String message) {
SseEmitter emitter = new SseEmitter(60000L);
// 异步处理流式返回
CompletableFuture.runAsync(() -> {
try {
DashScope dashScope = new DashScope(apiKey);
// 构建流式请求
GenerationRequest request = GenerationRequest.builder()
.model(model)
.input(MessageInput.builder()
.messages(contextHolder.getHistory(sessionId))
.build())
.incrementalOutput(true) // 开启增量输出
.build();
// 流式回调,逐段返回内容
dashScope.streamCall(request, new ResultCallback<GenerationResult>() {
@Override
public void onEvent(GenerationResult result) {
try {
String text = result.getOutput().getChoices().get(0).getMessage().getContent();
if (StrUtil.isNotBlank(text)) {
emitter.send(SseEmitter.event().data(text));
}
} catch (IOException e) {
emitter.completeWithError(e);
}
}
@Override
public void onComplete() {
emitter.complete();
}
@Override
public void onError(Exception e) {
emitter.completeWithError(e);
}
});
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
}
四、接口测试
1. 普通对话
POST请求 http://localhost:8080/api/chat/send
请求体:
{
"sessionId": "test001",
"message": "你们公司有哪些产品?"
}
返回示例:
{
"code": 200,
"msg": "success",
"data": "我们公司目前有企业管理系统、智能客服平台、数据中台三大核心产品,请问您想了解哪一款?"
}
同一会话ID继续提问,会携带上下文,实现多轮对话。
2. 流式输出
GET请求 http://localhost:8080/api/chat/stream?sessionId=test001&message=介绍一下智能客服平台
浏览器或前端直接调用,会看到文字逐段输出,和ChatGPT体验一致。
五、企业级落地优化建议
- API Key安全:生产环境不要硬编码在配置文件里,建议用Nacos/Apollo配置中心加密存储,配合网关鉴权。
- 限流降级:增加接口限流(比如Sentinel),防止恶意刷接口消耗token;大模型故障时自动降级为人工客服入口。
- 敏感词过滤:提问和回复都要过敏感词校验,避免违规内容,满足等保合规要求。
- 知识库对接:纯大模型回答会胡说八道,生产环境要配合RAG检索增强,基于企业内部文档回答,准确率更高。
- 数据统计:记录每轮对话的token消耗、响应时长、用户满意度,方便成本核算和效果优化。
六、写在最后
AI不是来淘汰Java开发的,是来淘汰「只会写CRUD、不肯迭代自己」的开发者的。 不用放弃多年的Java经验去卷纯算法,在现有技术栈上叠加AI落地能力,就是接下来3年最稳的职业护城河。
后续我会持续更新Spring AI、LangChain4j、Java+AI项目落地、一线踩坑经验,陪所有Java同行一起在AI时代站稳脚跟。
原创不易,如果文章对你有帮助,欢迎点赞、转发给身边的同事~