这个项目的定位很直接:把大模型能力塞进一个标准的 Java 企业级工程里,做成一套能打包、能部署、能接数据库、能做权限控制的智能客服系统。技术栈是 Spring Boot + Vue3 + Spring AI,后端负责对话编排、知识库检索、会话状态管理和业务接口,前端负责用户侧聊天窗口和客服工作台。它不是跑在 notebook 里的几行 demo,而是一套前后端分离、有完整分层、可以照着抄进自己项目的实战工程。
Spring AI 在这套系统里承担的是“胶水层”的角色。对话模型、向量化模型、向量库、会话记忆、工具调用,这些东西原本每换一个厂商就要改一遍代码,Spring AI 用 ChatClient、ChatModel、EmbeddingModel、VectorStore 这一套抽象把它们统一了。对 Java 开发者来说,这件事的价值很大:你不用去学 Python 生态那一堆 SDK,用熟悉的 Starter 依赖注入、application.yml 配置、Service 分层就能把 AI 链路搭起来。
这篇要讲的东西偏工程化,重点不在“大模型有多神”,而在“怎么把它接进一个真实业务系统并且跑稳”。核心会覆盖几个点:Spring AI 的接入方式和配置项、RAG 知识库的构建与检索、SSE 流式输出的前后端联调、多轮会话记忆、接口调用与压测、JVM 和连接数的资源观察,以及一堆实际部署时会撞上的坑。硬件门槛这块先说清楚,这套系统本身是 Java 服务,跑起来吃的是内存和 CPU,真正吃显卡的是背后的模型服务,你可以走本地推理也可以走企业已有的模型网关,两种方式后面都会提到。
适合看这篇的人有三类:Java 后端想补 AI 应用落地经验的,需要交课程设计或者毕设但不想写烂大街 CRUD 的,以及要给公司内部做客服、工单、知识问答系统的开发者。前端部分会讲 Vue3 的启动和流式接收,但不会大篇幅讲样式和组件库,重心放在链路上。
1. Spring AI 智能客服核心能力速览
先把这套系统的能力边界列清楚,避免看到后面才发现“原来它不做这个”。下表里的参数项,凡是材料没有明确给出的,都标注为需要按实际项目版本确认,不做臆测。
| 能力项 | 说明 |
|---|---|
| 项目类型 | 前后端分离的企业级 AI 应用,智能客服 / 知识问答方向 |
| 技术栈 | Spring Boot + Spring AI + Vue3 + 关系型数据库 + 向量存储 |
| 模型接入方式 | 走 Spring AI 的 ChatModel 抽象,可对接兼容 OpenAI 协议的模型服务,也可对接本地推理服务 |
| 核心 AI 能力 | 多轮对话、知识库检索增强(RAG)、流式输出、工具调用 |
| 知识库能力 | 文档切分、向量化、相似度检索、上下文拼装 |
| 会话能力 | 会话隔离、历史消息记忆、上下文窗口控制 |
| 前端形态 | 用户侧聊天窗口 + 客服侧工作台,Vue3 单页应用 |
| 接口形态 | RESTful 接口 + SSE 流式接口 |
| 启动方式 | 后端 Maven 打包运行,前端 npm 启动,也可容器化部署 |
| 是否支持批量任务 | 需要按项目实际实现确认,通用做法是异步任务 + 队列 |
| 推荐硬件 | Java 服务侧以 CPU 和内存为主;模型侧按所选模型和部署方式确定 |
| 显存占用 | 取决于模型部署方式,走远程模型服务时本地无显存占用,需按实际环境测试 |
| 适合场景 | 企业内部知识问答、电商售前售后、SaaS 产品在线支持、毕设课设 |
| 不适合场景 | 需要强实时决策的交易链路、无授权数据的大规模抓取问答 |
这张表里最容易被忽略的一行是“显存占用”。很多同学看到 AI 项目第一反应是“我 8G 显卡能不能跑”,但这套系统的架构决定了显存不在 Java 进程这一侧。如果你的模型走的是远程服务或者公司统一的模型网关,本地一行显存都不会占;如果你打算用本地推理服务跑一个开源模型,那显存需求由那个模型本身决定,跟 Spring Boot 工程无关。这个认知先建立起来,后面配置的时候就不会纠结。
另外要说明的是,Spring AI 目前处于快速迭代阶段,不同小版本之间 API 命名、Starter 坐标、Advisor 的构造方式都出现过调整。本文给出的代码是结构示意,落到你本机的时候,务必对着项目 pom.xml 里的依赖版本和官方文档核对一遍方法签名,不要直接复制粘贴就运行。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 适用场景与使用边界
智能客服这个赛道听起来很卷,但真正落到具体业务里,需求差异非常大。这套工程更适合的场景是“有明确知识边界、答案可以溯源”的问答,比如产品手册问答、售后政策查询、内部制度查询、IT 工单自助受理。这类场景的共同点是:问题范围收敛,标准答案存在于文档里,模型的作用是把自然语言问题映射到文档片段上再组织成通顺回复。
不太适合的场景也要讲清楚。第一类是强一致性的业务查询,比如“我的订单到哪了”“帮我退款”,这类问题不应该让模型自由发挥,正确做法是用工具调用(Tool Calling)把请求转成确定性的后端接口查询,模型只负责理解意图和播报结果,绝不能让它编造订单状态。第二类是高危领域的建议,医疗、法律、金融投资这类,模型输出必须加免责声明并且有人工兜底,工程上要做敏感词拦截和转人工策略。
合规这块必须强调。客服系统天然会接触用户手机号、地址、订单信息,这些数据在拼进 Prompt 之前一定要做脱敏处理,能不给模型的就不给。知识库文档的上传要有权限校验,不能随便一个人就能把内部文档灌进去。对话日志的存储要遵循最小必要原则,保留时长要有明确策略,导出和删除要有审计。如果你的系统涉及录音、图像、人脸这类素材,必须确保已获得当事人明确授权,并且只用于声明的用途。
还有一条容易被忽视的边界:模型输出不等于事实。工程上要做的是让答案可溯源,也就是回复里带上引用的文档片段或者知识条目 ID,让客服人员能一键核对。这不是为了好看,是为了在出问题的时候能定位到底是检索错了还是模型编了。
3. Spring AI 智能客服本地部署环境准备
环境准备这块最容易翻车,因为它是 Java 生态和 AI 生态的交界处,两边的坑都得踩一遍。下面给一份检查清单,版本号不写死,按你项目 pom.xml 和 package.json 里的实际声明来。
后端侧需要:
- JDK 17 或以上,Spring Boot 3.x 的最低要求就是这个,别再拿 JDK 8 硬试。
- Maven 3.8+ 或者 Gradle,取决于项目用的是哪个构建工具。
- 关系型数据库,MySQL 8 或者 PostgreSQL,用来存用户、会话、消息、知识库元数据。
- Redis,通常用来做会话缓存、限流、验证码,部分项目也拿它当向量库。
- 向量存储,可以是 Redis、PostgreSQL 的 pgvector 扩展、Milvus,或者先用内存版 VectorStore 跑通链路。
- 可用的模型服务,要么是公司给的模型网关地址和 Key,要么是本地起一个兼容 OpenAI 协议的推理服务。
前端侧需要:
- Node.js 18 LTS 或以上。
- npm 或 pnpm,跟项目锁文件保持一致,别混用。
先把这些装好再动代码。我见过太多人 JDK 版本不对,编译报一堆找不到符号,然后开始怀疑人生。检查方式很简单,三条命令跑一遍:
bash复制java -version
mvn -v
node -v && npm -v
输出里 JDK 主版本低于 17 就直接换,不要试图降级 Spring Boot 版本来适配,那样会连锁引发一堆依赖冲突。数据库和 Redis 建议用 Docker 起,隔离干净,删起来也方便:
bash复制# 数据库与缓存的最小启动示例,端口和密码按自己环境改
docker run -d --name cs-mysql -p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=your_password \
-e MYSQL_DATABASE=customer_service \
mysql:8.0
docker run -d --name cs-redis -p 6379:6379 redis:7
启动完先用客户端连一下,确认能通再往下走。端口冲突是新手最常见的第一个坑,3306 和 6379 被本机已有服务占用的情况非常普遍,起之前先 netstat -ano | findstr 3306(Windows)或者 lsof -i:3306(macOS/Linux)看一眼。
4. Spring AI 配置与后端服务启动
后端能不能跑起来,八成看配置文件写对没有。Spring AI 的配置集中在 application.yml 里,核心是模型服务的 base-url、api-key、模型名,以及向量库和 Embedding 模型的配置。下面给一份结构示意,字段名要对照你实际引入的 Starter 调整:
yaml复制server:
port: 8080
spring:
application:
name: ai-customer-service
datasource:
url: jdbc:mysql://127.0.0.1:3306/customer_service?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
data:
redis:
host: 127.0.0.1
port: 6379
database: 0
ai:
openai:
# 模型服务地址,可以是企业网关,也可以是本地推理服务暴露的兼容地址
base-url: http://127.0.0.1:11434
api-key: ${MODEL_API_KEY:not-needed-for-local}
chat:
options:
model: your-chat-model
temperature: 0.3
embedding:
options:
model: your-embedding-model
# 业务自定义配置
customer-service:
ai:
system-prompt: "你是某公司的在线客服助手,只回答与产品相关的问题,不确定时引导用户转人工。"
max-history-rounds: 10
retrieve-top-k: 4
similarity-threshold: 0.6
几个参数值得单独说一下。temperature 在客服场景不要开高,0.2 到 0.4 之间比较合适,太高了回复会飘,同一句话问两遍给出两个答案,用户会直接失去信任。max-history-rounds 控制带多少轮历史消息进上下文,太长会撑爆 token 预算,太短又记不住前文,十轮左右是个常见起点。retrieve-top-k 是 RAG 检索返回的片段数,给太多会把无关内容塞进 Prompt 反而干扰模型,给太少又可能漏掉关键信息,四个是个可以拿来起步的值,实际要根据文档粒度调。
配置好之后定义 ChatClient。这是 Spring AI 里最核心的一个门面类,负责把系统提示词、会话记忆、检索增强这些能力组装到一起:
java复制@Configuration
public class AiConfig {
@Bean
public ChatClient chatClient(ChatClient.Builder builder,
ChatMemory chatMemory,
VectorStore vectorStore) {
return builder
// 系统提示词决定角色边界,务必写清楚能答什么、不能答什么
.defaultSystem("你是某公司的在线客服助手,只回答与产品相关的问题,"
+ "不了解的内容请引导用户转人工,不要编造。")
// 会话记忆,构造方式随 Spring AI 版本略有差异,以本机依赖为准
.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
// 检索增强,把向量库里召回的内容拼进上下文
.defaultAdvisors(new QuestionAnswerAdvisor(vectorStore))
.build();
}
}
这段代码的价值在于它把两件事解耦了:业务代码只管调 chatClient.prompt().user(question).stream().content(),至于消息怎么存、文档怎么检索,全在 Advisor 层完成。后面不管换模型还是换向量库,业务层基本不用动。
启动方式就是常规的 Spring Boot 流程:
bash复制# 在项目根目录执行,跳过测试加快首次启动
mvn clean package -DskipTests
java -jar target/ai-customer-service-0.0.1-SNAPSHOT.jar
# 开发阶段更推荐直接用插件跑,改动自动生效
mvn spring-boot:run
启动日志里重点看三行:端口有没有绑定成功、数据源有没有连上、向量库有没有初始化完成。这三项任何一个报错,后面调接口都是白费。常见报错是数据源连接超时和向量库索引不存在,前者检查数据库是否启动、账号密码是否正确,后者通常在首次启动时自动建索引,如果没建成功,去看启动日志里的 DDL 语句有没有执行。
5. Vue3 前端启动与流式对话联调
前端部分本身不复杂,Vue3 + Vite 的启动流程大家应该都熟,重点在流式对话的接收上。先把依赖装好跑起来:
bash复制# 进入前端目录
cd web-ui
# 安装依赖,有 lock 文件就用 npm ci 保证版本一致
npm install
# 开发模式启动,默认 5173 端口
npm run dev
启动后如果页面能打开但接口全 404,九成是跨域或者代理没配。Vite 里配代理比后端配 CORS 干净得多,改 vite.config.js:
javascript复制import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 5173,
proxy: {
// 把 /api 前缀的请求转发到后端,避免浏览器跨域
'/api': {
target: 'http://127.0.0.1:8080',
changeOrigin: true
}
}
}
})
这里有个细节要提醒:流式接口如果用 SSE,浏览器原生的 EventSource 只支持 GET 请求,传不了复杂的请求体,而且不方便带自定义请求头。所以实际项目里更常见的做法是用 fetch 配合 ReadableStream 手动解析流。下面是一个可以直接用的接收示例:
javascript复制export async function streamChat(payload, onChunk) {
const response = await fetch('/api/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token') || ''}`
},
body: JSON.stringify(payload)
})
if (!response.ok) {
throw new Error(`请求失败: ${response.status}`)
}
const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8')
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
// 按 SSE 的双换行切分事件块
const events = buffer.split('\n\n')
buffer = events.pop() || ''
for (const evt of events) {
const line = evt.split('\n').find(l => l.startsWith('data:'))
if (!line) continue
const text = line.replace(/^data:\s?/, '')
if (text === '[DONE]') return
onChunk(text)
}
}
}
这段代码的关键在于 buffer 的处理。网络传输是分片的,一个 JSON 事件可能被切成两半传过来,如果你直接对每个 chunk 做 split 就会解析出半截数据。用 buffer 缓存不完整的尾部,下次拼接再解析,这是流式解析的标准做法。很多同学写完发现偶尔出现乱码或者半句话,就是这个原因。
联调时先测非流式接口,确认业务逻辑通了再切流式。因为流式一旦出问题,报错信息会被切成碎片,排查成本翻倍。
6. RAG 知识库构建与检索效果验证
客服系统答得准不准,八成取决于知识库这一层。Spring AI 提供了完整的 ETL 链路:读取文档、切分文本、向量化、写入向量库。整个流程可以写成一个启动时执行的初始化任务,也可以做成后台上传接口实时灌入,取决于你的文档更新频率。
切分策略是这一步的核心。切得太碎,单个片段缺少上下文,检索出来答非所问;切得太大,一个片段混进好几个主题,模型容易被干扰。常见的做法是按语义段落切,配合一定的重叠长度:
java复制@Service
public class KnowledgeIngestService {
private final VectorStore vectorStore;
private final TokenTextSplitter splitter;
public KnowledgeIngestService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
// 分片大小与重叠长度按文档类型调整,不要照搬
this.splitter = new TokenTextSplitter(600, 200, 5, 10000, true);
}
public int ingest(Resource resource) {
// 读取文档内容
TextReader reader = new TextReader(resource);
reader.getCustomMetadata().put("source", resource.getFilename());
List<Document> raw = reader.get();
// 切分成适合向量化的片段
List<Document> chunks = splitter.apply(raw);
// 写入向量库,EmbeddingModel 由 VectorStore 内部持有
vectorStore.write(chunks);
return chunks.size();
}
}
写作的时候特意保留了 source 这个元数据,因为它在检索返回时非常有用——回复里可以带上“来源:产品手册第 3 章”,用户看到这个信任度完全不一样。元数据里还可以塞业务线、版本号、生效时间,后面做过滤检索时能派上大用场。
验证检索效果不要凭感觉,要有可重复的测试集。准备二十到三十个真实用户会问的问题,每个问题标注出它应该命中的文档片段,然后跑一遍检索,看 top-k 里命中的比例。命中率低于七成,说明切分粒度或者 Embedding 模型不合适,不要急着调大模型参数,先把检索修好。检索不准的情况下,模型再强也只能胡说。
还有一个细节:相似度阈值要设。不加阈值的话,用户问一个知识库里完全没有的问题,系统依然会召回几个最“像”的片段塞给模型,模型一看有上下文就硬答,这就产生了幻觉。设一个下限,低于阈值就直接走兜底话术“这个问题我暂时无法回答,为您转接人工”,比答错强得多。
7. 对话链路功能测试与效果验证
功能测试建议按链路拆开一项项验,不要一上来就测端到端,出问题定位不了。下面几个测试维度按顺序做,每项都给出判断标准。
7.1 基础单轮问答测试
输入一句知识库里有明确答案的问题,看回复是否命中。判断标准有三条:内容正确、带引用来源、首字延迟在可接受范围。首字延迟是流式场景的核心指标,用户感知的不是总耗时而是“多久看到第一个字”。如果首字超过三秒,用户会觉得系统卡死了,哪怕后面输出很快。
失败排查顺序:先看请求有没有到后端(日志打点),再看模型服务有没有返回(抓 ChatClient 的响应),最后看检索召回的内容对不对。三层定位,比盲目改代码快得多。
7.2 多轮会话记忆测试
连续问三个有关联的问题,比如第一句“你们的旗舰机型是哪款”,第二句“它支持快充吗”,第三句“保修多久”。第二句和第三句都依赖第一句的上下文。如果模型在第三句答非所问,说明会话记忆没生效。
排查点:会话 ID 是否在每轮请求里都带上了、ChatMemory 的存储介质是否正确(内存版重启就丢)、历史消息有没有被截断策略砍掉。内存版 ChatMemory 适合开发调试,生产环境一定要换成 Redis 或者数据库实现,否则用户刷新页面上下文就没了。
7.3 知识库兜底测试
问一个知识库里绝对没有的问题,比如“你们老板的电话是多少”。正确行为是明确拒答并引导转人工,错误行为是编一个号码出来。这一项是幻觉检测的核心用例,每次改完 Prompt 或者换模型都要重跑一遍。如果发现模型还是硬答,检查相似度阈值是否生效、系统提示词里有没有明确写“不知道就说不知道”。
7.4 敏感内容与越权测试
输入包含个人信息、违规请求、越权指令的内容,看系统是否正确拦截。常见攻击方式是让模型“忽略之前的所有指令”然后套取系统提示词,工程上要在 Prompt 层加防护,更要在应用层做正则和词表过滤,别指望模型自己扛。这一项的判断标准很明确:拦截日志里能看到记录,用户侧收到的是统一的拒答话术,不泄露任何内部信息。
7.5 长对话与上下文溢出测试
连续对话二十轮以上,看系统是否还能正常工作。这里常见的两个问题:上下文超长导致模型报错,会话记忆无限增长导致内存膨胀。解决方案是滑动窗口截断加摘要压缩,保留最近 N 轮原文,更早的内容用模型压缩成一句摘要。这个策略要在配置里做成可调参数,不同业务的最优值不一样。
8. 接口 API 设计与批量调用示例
接口这块先说清楚一个原则:流式接口给聊天窗口用,非流式接口给系统集成用。两套接口共用同一套业务逻辑,不要写两份。下面给一个非流式接口的骨架:
java复制@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient chatClient) {
this.chatClient = chatClient;
}
@PostMapping("/completion")
public ResponseEntity<ChatResponse> completion(@RequestBody @Valid ChatRequest request) {
String answer = chatClient.prompt()
.user(request.getQuestion())
.advisors(a -> a.param("chat_memory_conversation_id", request.getSessionId()))
.call()
.content();
return ResponseEntity.ok(new ChatResponse(request.getSessionId(), answer));
}
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestBody @Valid ChatRequest request) {
return chatClient.prompt()
.user(request.getQuestion())
.advisors(a -> a.param("chat_memory_conversation_id", request.getSessionId()))
.stream()
.content();
}
}
注意流式接口返回 Flux<String> 需要类路径里有 Reactor,如果项目是纯 Spring MVC 且没有引入 webflux,可以用 SseEmitter 手动推。另外 produces 必须声明 text/event-stream,否则浏览器不会按流处理,会等整个响应结束才渲染,流式就白做了。
接口调通的验证方式,用 curl 最快:
bash复制# 非流式接口验证
curl -X POST http://127.0.0.1:8080/api/chat/completion \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your_token" \
-d '{
"sessionId": "test-session-001",
"question": "你们的产品支持七天无理由退货吗"
}'
# 流式接口验证,-N 关闭缓冲,能实时看到输出
curl -N -X POST http://127.0.0.1:8080/api/chat/stream \
-H "Content-Type: application/json" \
-d '{"sessionId":"test-session-002","question":"介绍一下你们的售后政策"}'
流式那条命令如果加了 -N 还是等半天一次性吐出来,说明中间有代理或者网关做了缓冲,先绕过代理直连后端确认是应用问题还是链路问题。
批量场景下用 Python 跑一遍测试集最省事,也方便算准确率:
python复制import time
import requests
BASE_URL = "http://127.0.0.1:8080/api/chat/completion"
HEADERS = {"Content-Type": "application/json", "Authorization": "Bearer your_token"}
questions = [
"退货需要满足什么条件",
"发票怎么开",
"发货后多久能到",
]
for idx, q in enumerate(questions):
payload = {"sessionId": f"batch-{idx}", "question": q}
start = time.time()
try:
resp = requests.post(BASE_URL, json=payload, headers=HEADERS, timeout=90)
resp.raise_for_status()
data = resp.json()
print(f"[{idx}] 耗时 {time.time()-start:.2f}s 问: {q}")
print(f" 答: {data.get('answer', '')[:80]}")
except Exception as e:
# 批量任务一定要记录失败项,方便重跑
print(f"[{idx}] 失败: {e}")
with open("failed.txt", "a", encoding="utf-8") as f:
f.write(q + "\n")
批量跑的时候有几个工程细节:一定要设超时,不设的话一个卡住的请求能把整个批次拖死;失败项要单独落盘而不是直接抛异常退出;批次之间加个小延时,避免把模型服务的限流打满。如果要做成正式的后台批处理,建议用队列加异步任务,把“提交”和“消费”解耦,前端只看任务状态。
9. 资源占用与性能观察方法
这套系统的性能瓶颈通常不在 Java 层,但 Java 层的问题最容易被忽视。观察维度分三块。
第一块是 JVM 本身。堆内存、GC 频率、线程数,这三项用 jconsole 或者 Actuator 的 metrics 端点都能看到。典型的坑是会话记忆用了内存实现,用户量一上来堆就爆了。另一个坑是流式接口每个连接占一个请求线程,并发一高线程池就打满。Spring MVC 默认线程池是 200,撑不住高并发长连接,要么上异步 Servlet 要么换成 WebFlux。
第二块是下游依赖耗时。模型服务的首 token 延迟和总生成耗时是两个独立指标,必须分开埋点。知识库检索的耗时也要单独打,因为它直接决定首字延迟。一个健康的分布大概是:检索 100 毫秒以内,模型首 token 一到两秒,总体回答三到八秒。超出这个范围就要找是哪个环节慢。
第三块是成本。大模型是按 token 计费的,客服系统的长会话是 token 消耗大户。要监控的指标包括:单次对话输入 token 数、输出 token 数、按会话统计的累计消耗。输入 token 数突然变大,通常是历史消息没截断或者检索片段塞太多。这两项都可以通过配置直接控制,见效很快。
降低资源消耗的手段,按性价比排序:先砍历史轮数,再砍检索片段数,然后换更小的模型,最后才考虑加机器。换小模型这件事要谨慎,客服场景对答案准确性要求高,模型降级带来的体验损失往往比省下的成本更贵。
端口和进程管理也提一句。本地开发经常遇到“端口被占用但找不到进程”,Windows 下用 netstat -ano | findstr :8080 找到 PID 再 taskkill /PID xxx /F,macOS/Linux 用 lsof -i:8080 加 kill -9。热部署工具偶尔会留下僵尸进程,重启前先确认干净。
10. 常见问题与排查方法
下面这张表基本覆盖了这套系统从启动到上线会撞上的大部分问题,遇到报错先在这里翻一遍。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 启动报找不到符号或类 | JDK 版本低于 17,或依赖没下全 | 执行 java -version,看 mvn dependency:tree |
升级 JDK,删除本地仓库对应目录重新拉依赖 |
| 启动报数据源连接失败 | 数据库未启动、账号密码错、时区配置缺失 | 用客户端直连验证 | 补齐 serverTimezone 参数,核对账号权限 |
| 向量库初始化报索引不存在 | 未配置自动建索引,或向量维度不匹配 | 看启动日志里的 DDL 和维度信息 | 手动建索引,确认 Embedding 模型维度与索引一致 |
| 页面能开但接口全 404 | 跨域未配置或代理规则写错 | 浏览器 Network 面板看请求实际地址 | 用 Vite proxy 转发,路径前缀对齐 |
| 模型调用报 401 | Key 缺失、过期或环境变量没注入 | 打印配置项确认值非空 | 换成有效 Key,敏感信息走环境变量 |
| 模型调用报超时 | 网络不通、模型服务过载、超时阈值太短 | 先 curl 直连模型服务验证连通性 | 调整超时时间,加失败重试和降级兜底 |
| 流式输出一次性全出来 | 有代理或网关做了缓冲 | 绕过代理直连后端测试 | 关闭缓冲,或调整网关的 chunked 配置 |
| 流式输出中文乱码 | 前端解码未用 utf-8 流式模式 | 检查 TextDecoder 参数 | 使用 { stream: true } 分批解码 |
| 多轮对话记不住上文 | 会话 ID 未透传、记忆存储被重置 | 日志打印会话 ID 确认一致 | 统一会话 ID 生成规则,换成持久化记忆实现 |
| 回答出现幻觉 | 检索召回不相关内容、未设相似度阈值 | 打印召回片段人工核对 | 加阈值,优化切分粒度,强化系统提示词边界 |
| 显存或内存暴涨 | 会话记忆无限增长、批量任务并发过高 | 看堆内存曲线和活跃会话数 | 加滑动窗口截断,限制并发,分批处理 |
| 批量任务卡住不动 | 单请求无超时、异常未捕获导致线程阻塞 | 看线程栈 jstack |
全部请求加超时,失败项落盘支持重跑 |
这张表里最值得展开的是“回答出现幻觉”那一行。很多人第一反应是换更大的模型,实际上八成问题出在检索层。判断方法很简单:把召回的知识片段打印出来,人工看一眼跟问题有没有关系。如果召回的都是不相关的内容,换再大的模型也只是让它把不相关的内容组织得更通顺而已。
11. 最佳实践与上线建议
第一件事是把最小可运行配置固定下来。开发调试阶段大家喜欢乱改参数,改到最后忘了哪个组合是能跑通的。建议单独维护一份 application-dev.yml,把模型名、温度、检索参数、历史轮数这几个关键项锁死,任何实验性改动都开分支,别在主配置上直接动。
目录和配置的隔离也要做好。知识库原始文档、切分后的中间产物、向量库存量数据,这三类东西生命周期不同,不要混在一个目录里。配置文件里的 API Key、数据库密码一律走环境变量,不要提交到仓库,哪怕是私有仓库也不要,这是基本的安全习惯。
批量任务和异步链路要加日志和幂等。每批任务分配一个批次 ID,每条记录的开始、结束、失败原因都带上这个 ID 写日志,出问题能整批追溯。写入知识库或者更新会话状态这类操作要考虑重复执行的情况,用业务唯一键做幂等,避免重试把数据写重。
接口服务的访问范围要限制。客服系统涉及用户隐私数据,接口不能裸奔。至少要做的三件事:鉴权(JWT 或者 Session 都行)、限流(按用户和按 IP 双维度)、审计日志(谁在什么时间查了什么)。对话历史接口尤其要注意越权,防止 A 用户通过改 sessionId 读到 B 用户的对话。
内容合规这块再强调一遍。知识库上传要有审核流程,不能让未经审核的文档直接进检索池。模型输出要做敏感词过滤,尤其是涉及承诺、价格、时效的表述。涉及录音、图像、人脸、声音克隆的功能,必须确认已获得当事人明确授权,且用途与授权范围一致。对外发布前建议做一轮人工抽检,随机抽五十到一百条真实对话看回复质量,这一步没有技术手段可以替代。
最后是版本管理。Spring AI 迭代速度快,升级前先在分支上跑一遍完整的回归测试集,确认检索命中率、首字延迟、拒答率这几个指标没有明显退化再合入。直接在主分支上升级依赖,出了问题很难回滚。
12. 这套系统值得先动手验证的三件事
如果时间有限,只想快速判断这个项目值不值得投入,建议按这个顺序做。
先跑通一条最短链路:本地起后端和前端,配置一个可用的模型服务,在聊天窗口问一句知识库里有的问题,看到流式输出正常返回并带引用来源。这一步能验证环境、配置、接口、前端四条链路是否打通,大概半小时能出结果。如果这一步卡住超过两小时,八成是环境问题而不是代码问题,先把 JDK、数据库、模型服务三个依赖逐个 curl 或直连验证一遍。
然后测幻觉边界。问三个知识库里没有的问题,看系统是老实说不知道还是硬编。这一项直接决定这套系统能不能用在真实客服场景,答错一次用户信任就没了。如果发现硬编,优先改检索阈值和系统提示词,别急着换模型。
最后压一轮并发。用一百个左右的并发连接跑流式接口,观察线程池、内存和下游耗时。这一步能提前暴露线程模型和连接数的问题,比上线后半夜被告警叫醒强得多。
最容易踩的坑其实不在 AI 那一侧,而在工程侧:会话记忆用了内存实现、流式接口没配好 produces、前端流式解析没处理分包、历史消息没截断。这四个问题占了实际调试时间的大半。至于模型选型,那是后面才需要考虑的优化项,先把链路跑稳再说。
后续可以扩展的方向也很清楚:工具调用接订单查询和物流接口,把“能查的”交给确定性代码;会话摘要压缩,让长对话不爆上下文;多租户隔离,一套系统服务多个业务线;离线评测集自动化,每次改 Prompt 都跑一遍回归。这些做完,这套工程才算真正具备上生产的基础。
