想做一套能写进简历、又能真跑起来的企业级 AI 应用,SpringAI 智能客服是目前门槛比较友好的一个方向:后端用 SpringBoot 承载业务与会话,前端用 Vue3 做对话工作台,中间用 SpringAI 把大模型、知识库检索、会话记忆、工具调用串起来。它不是那种只写个 chat 接口就算完的 Demo,而是把流式输出、多轮上下文、RAG 知识库、敏感词过滤、会话持久化、批量问答这些企业里真正要用的东西都摆到台面上。
这篇文章围绕一套 SpringBoot + Vue3 + SpringAI 的智能客服实战项目展开。核心关注点有三个:一是能不能在普通开发机上跑起来,不依赖特殊硬件;二是知识库、多轮记忆、流式输出这些能力到底怎么落地;三是接口怎么设计、批量任务怎么组织、出问题怎么排查。全文给的配置和代码都是可复制的模板,路径、模型名、端口按自己环境替换即可。
硬件门槛先说清楚:这类项目的主要算力压力在模型侧,不在 SpringBoot 侧。如果你用本地 Ollama 跑 7B 级别的模型,显存占用取决于量化方式和上下文长度,需要以本机实测为准;如果你对接的是团队统一的模型服务,那么开发机基本只需要能跑 JDK、Node 和数据库。本文不假设任何特定显卡型号,也不编造显存数字,所有涉及资源的结论都以“可观察、可验证”的方式给出。
下面按“能力速览 → 场景边界 → 架构拆解 → 环境准备 → 后端启动 → 前端联调 → 功能测试 → 接口与批量 → 资源观察 → 排错 → 最佳实践”的顺序推进,适合正在做 Java 后端转型 AI 应用、准备面试项目、或者要给公司内部搭一个客服助手的读者。
1. SpringAI 智能客服核心能力速览
先把这套项目的能力边界列成一张表,方便你判断要不要花时间跟下去。表中凡是标注“按实际环境测试”的项,都是因为它依赖模型版本、上下文长度或部署方式,写死数字没有意义。
| 能力项 | 说明 |
|---|---|
| 项目类型 | 企业级 AI 智能客服系统,前后端分离 Web 应用 |
| 技术栈 | SpringBoot 3 + SpringAI + Vue3 + MyBatis-Plus + MySQL + Redis + 向量库 |
| 模型接入方式 | 通过 SpringAI 的 OpenAI 兼容协议接入,可对接本地 Ollama、团队统一模型网关或自建推理服务 |
| 核心功能 | 多轮对话、流式输出、知识库检索增强(RAG)、会话记忆、工具调用、敏感词过滤、会话管理 |
| 启动方式 | 后端 mvn spring-boot:run 或打包后 java -jar;前端 npm run dev;模型侧独立启动 |
| 是否支持 API | 支持,后端提供 REST + SSE 两类接口,可被第三方系统调用 |
| 是否支持批量任务 | 支持,可通过批量问答接口或离线任务表驱动,按实际实现为准 |
| 推荐硬件 | 开发机正常配置即可;本地跑模型时显存需求按模型规模与量化方式实测 |
| 支持平台 | Windows、Linux、macOS 均可,注意 JDK 与 Node 版本一致性 |
| 适合场景 | 企业内部知识问答、电商售前售后、工单预处理、面试项目、技术选型验证 |
| 不适合场景 | 高并发生产直连、无授权数据的问答、需要强合规审计但未做治理的场景 |
这张表里最容易被忽略的是“模型接入方式”这一行。很多同学第一次接触 SpringAI,会以为必须绑定某一家云服务,其实 SpringAI 的价值恰恰在于抽象层:ChatModel、EmbeddingModel、VectorStore、ChatMemory 都是接口,换个实现类就能换后端。所以你在本地用 Ollama 调试、上线时换成团队统一网关,代码主体几乎不用动,改的是配置文件。
另一个需要提前建立认知的点是:智能客服的质量下限由工程决定,上限由数据和模型决定。工程侧要做的是把提示词模板管理好、把检索片段控制好、把上下文长度裁剪好、把异常兜底写好;数据侧要做的是知识库切片干净、FAQ 覆盖到位、权限边界清晰。这两件事缺一个,效果都会明显打折。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 适用场景与使用边界
这套项目最适合三类人。第一类是 Java 后端想往 AI 应用方向走的开发者,SpringBoot 的工程结构你熟悉,缺的只是怎么把模型调用、向量检索、流式响应接进现有分层;第二类是要给公司内部搭知识库助手的团队,客服场景的通用能力(会话、检索、反馈、转人工)几乎可以直接复用;第三类是在准备面试项目的人,这套东西能把“会调 API”和“会做系统”区分开。
它能解决的问题也比较明确:把散落在文档、FAQ、历史工单里的知识,变成可以对话检索的入口;把重复性咨询挡在人工之前;把会话记录沉淀下来做质量分析。但它不适合直接当成生产级高并发系统来用,也不适合在没有数据授权的情况下把内部文档一股脑灌进向量库。
这里必须强调几条使用边界。第一,知识库内容必须是你有权限使用的资料,涉及客户信息、合同、个人信息的内容,入库前要做脱敏,向量库不等于保险箱。第二,涉及录音、聊天记录、工单原文的处理,要确认授权范围和留存周期,日志里不要打印完整原文。第三,如果客服系统要对外提供服务,必须在入口做身份校验和频率限制,不能把模型接口裸奔在公网。第四,模型输出不等于事实,涉及价格、政策、法律条款的回答,要设计“必须引用知识库片段”的约束,并保留人工复核通道。
还有一条经常被踩的坑:不要指望用提示词解决所有幻觉问题。更稳妥的做法是把“检索不到就明确说检索不到”写进系统提示词,同时在代码层面判断检索相似度阈值,低于阈值直接返回兜底话术,而不是让模型自由发挥。这个逻辑放在 Service 层,比放在提示词里可靠得多。
3. 技术架构与模块拆解
项目整体是标准的前后端分离结构,后端按职责分层,前端按页面和组件拆分。下面的表格是模块级别的拆解,跟敲的时候建议按这个顺序推进,不要一上来就写 RAG。
| 层次 | 模块 | 职责 |
|---|---|---|
| 前端 | 对话工作台 | 消息列表、输入框、流式渲染、停止生成、会话切换 |
| 前端 | 知识库管理 | 文档上传、切片预览、入库状态、删除重建 |
| 前端 | 会话管理 | 历史会话列表、消息回溯、导出 |
| 后端 | Controller 层 | 对话接口、流式接口、知识库接口、会话接口 |
| 后端 | Service 层 | 提示词组装、检索编排、上下文裁剪、异常兜底 |
| 后端 | AI 适配层 | ChatClient 封装、Embedding 封装、VectorStore 操作 |
| 后端 | 持久层 | 会话表、消息表、知识库文档表、切片表 |
| 基础设施 | MySQL | 业务数据与会话记录 |
| 基础设施 | Redis | 会话短期记忆、限流计数、热点缓存 |
| 基础设施 | 向量库 | 切片向量存储与相似度检索 |
分层里最关键的是 AI 适配层。不要在 Controller 里直接 new 一个模型客户端,也不要把提示词硬编码在业务方法里。推荐做法是定义一个 ChatService 接口,内部持有 ChatClient,对外只暴露“根据会话和问题返回答案”这一层语义。这样后面要换模型、加检索、加工具调用,改动都收敛在一个地方。
会话记忆这块建议做双存储:短期上下文放 Redis,按 sessionId 存最近 N 轮;长期记录落 MySQL,用于历史查询和数据分析。只放 Redis 会丢历史,只放 MySQL 每次都要查库拼上下文,性能不好。上下文条数不要贪多,超过模型窗口会被截断,反而把关键信息挤掉。
4. SpringAI 项目环境准备与前置条件
4.1 基础运行环境检查
动手之前先把版本对齐,这是最容易浪费时间的一步。JDK 建议 17 及以上,SpringBoot 3.x 最低要求就是 17;Maven 建议 3.8 以上;Node 建议 18 以上,Vue3 + Vite 对 Node 版本比较敏感;MySQL 8.x,Redis 6.x 以上。版本不确定时不要猜,先用命令确认。
bash复制java -version
mvn -v
node -v
npm -v
mysql --version
redis-server --version
如果你本机同时装了多个 JDK,注意 JAVA_HOME 指向的是哪一个,IDEA 里的 Project SDK 和 Maven 的 JDK 要一致,否则会出现“编译通过但启动报类找不到”的怪问题。Node 侧同理,建议用 nvm 管理版本,切项目时不会互相干扰。
4.2 模型服务准备
模型侧有两种常见做法。第一种是本地跑 Ollama,优点是离线、免费、调试快,缺点是占内存和显存,长上下文时速度会明显下降;第二种是对接团队统一的模型网关,优点是不占本机资源,缺点是需要申请密钥和配额。两种方式在 SpringAI 里的配置差异很小,主要是 base-url 和 api-key。
bash复制# 本地模型服务示例,模型名与端口按实际安装情况替换
ollama serve
ollama pull qwen2.5:7b
ollama list
拉完模型后先单独验证服务是否可用,不要等 SpringBoot 启动失败才回头查。用 curl 打一次最简单的对话接口,能返回内容说明链路通了。
bash复制curl http://127.0.0.1:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen2.5:7b",
"messages": [{"role": "user", "content": "你好,帮我介绍一下退货流程"}]
}'
Embedding 模型要单独确认。RAG 依赖向量化,如果 Embedding 模型没起来,对话能跑但知识库检索一定是空的。这一点在排查时要分开验证:先确认 Chat 模型通,再确认 Embedding 模型通,最后才验证检索结果。
4.3 数据库与向量库准备
MySQL 建库建表,至少需要会话表、消息表、知识库文档表、切片表四张。字符集用 utf8mb4,否则中文和表情符号会出问题。Redis 用来存短期记忆和限流计数,设置合理的过期时间,避免长期占用内存。
向量库按项目实际情况选择:数据量小可以直接用内存版或基于本地文件的实现,方便调试;数据量大再换独立部署的向量数据库。切换时只改 VectorStore 的实现和配置,业务代码不动。建表脚本建议纳入版本管理,跟敲时不要手工改表结构,否则后面同步代码会很痛苦。
5. SpringAI 后端配置与启动方式
5.1 依赖引入与版本对齐
SpringAI 的 starter 名称在不同版本之间有调整,0.8.x 系列和 1.0 之后不一样,直接抄网上的 pom 很容易踩坑。稳妥做法是先确认你用的 SpringAI 版本,再按官方文档选 artifactId。下面给的是 OpenAI 兼容 starter 的常见写法,实际以你引入的版本为准。
xml复制<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
</dependencies>
注意 spring-boot-starter-webflux 这一项。流式输出返回 Flux,没有 WebFlux 依赖会编译不过。如果项目本身是纯 Servlet 栈,也可以用 SseEmitter 做流式,两种方案都能落地,前者写法更简洁,后者和现有拦截器、鉴权体系融合更自然。
5.2 application.yml 核心配置
配置文件是这套项目的“神经中枢”,模型地址、密钥、向量库、会话记忆、超时全在这里。建议按环境拆成 application-dev.yml 和 application-prod.yml,密钥走环境变量,不要提交到仓库。
yaml复制server:
port: 8080
servlet:
encoding:
charset: UTF-8
force: true
spring:
ai:
openai:
base-url: http://127.0.0.1:11434
api-key: ${MODEL_API_KEY:local-test}
chat:
options:
model: qwen2.5:7b
temperature: 0.3
max-tokens: 2048
embedding:
options:
model: bge-m3
datasource:
url: jdbc:mysql://127.0.0.1:3306/ai_cs?useUnicode=true&characterEncoding=utf8mb4
username: root
password: ${MYSQL_PASSWORD:root}
data:
redis:
host: 127.0.0.1
port: 6379
timeout: 3s
app:
chat:
memory-max-messages: 10
retrieve-top-k: 4
similarity-threshold: 0.65
request-timeout-seconds: 120
temperature 在客服场景不要设太高,0.2 到 0.4 之间比较稳,太高会出现答非所问。max-tokens 要跟你的上下文预算匹配,设得太大既慢又容易把关键信息挤掉。similarity-threshold 这个阈值非常关键,低于它就别让模型硬答,直接走兜底话术。
5.3 ChatClient 封装与提示词管理
把 ChatClient 封装成 Bean,统一挂系统提示词和会话记忆,业务层只关心“问什么”。提示词不要散落在各个 Service 里,抽成常量类或配置项,方便后续做 A/B 对比。
java复制@Configuration
public class ChatClientConfig {
@Bean
public ChatClient customerServiceChatClient(ChatModel chatModel) {
String systemPrompt = """
你是企业客服助手,回答必须遵守以下规则:
1. 只根据提供的知识库片段和对话上下文作答,不编造政策、价格、时效。
2. 如果知识库片段中没有相关信息,明确回复“暂无相关说明,建议转人工”。
3. 回答简洁,分点说明,避免超过 200 字。
4. 不输出内部系统名称、接口地址、密钥等敏感信息。
""";
return ChatClient.builder(chatModel)
.defaultSystem(systemPrompt)
.build();
}
}
系统提示词里“不编造”和“明确说不知道”这两条要写在最前面,模型对开头的约束更敏感。另外建议加一条“不输出内部信息”,因为客服场景很容易被用户套出内部字段名,这是实际会遇到的问题。
5.4 启动与健康检查
后端启动命令很简单,关键看日志里模型客户端有没有初始化成功、数据库连接池有没有报错。
bash复制# 开发环境启动
mvn spring-boot:run -Dspring-boot.run.profiles=dev
# 打包后启动
mvn clean package -DskipTests
java -jar target/ai-customer-service.jar --spring.profiles.active=dev
启动成功后先别急着开前端,用 curl 打一次普通对话接口,确认模型链路通。这一步能省掉大量“到底是前端问题还是后端问题”的扯皮。
bash复制curl -X POST http://127.0.0.1:8080/api/chat \
-H "Content-Type: application/json" \
-d '{
"sessionId": "test-001",
"message": "你们的退货时效是多久"
}'
如果返回里带了知识库引用片段,说明检索链路也通了;如果只返回纯文本、没有引用,多半是向量库没数据或者相似度阈值设太高,这两个方向分开查。
6. Vue3 前端启动与流式联调
前端用 Vite 起项目,开发期配 proxy 转发到后端,避免跨域配置来回折腾。对话页面的核心不是 UI 多漂亮,而是流式渲染要稳:字符逐个追加、滚动条跟随、生成中可以停止、异常要能提示。
bash复制npm create vite@latest ai-customer-web -- --template vue
cd ai-customer-web
npm install
npm install axios markdown-it
npm run dev
开发代理配置示例:
javascript复制// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://127.0.0.1:8080',
changeOrigin: true
}
}
}
})
流式请求用 fetch 读 ReadableStream 更直接,注意处理分块粘包:SSE 以 data: 开头、以空行分隔,一个 chunk 里可能有多条事件,也可能一条事件被拆成两个 chunk。下面的写法做了缓冲拼接,能覆盖大部分情况。
javascript复制export async function streamChat({ sessionId, message, onDelta, onDone, onError }) {
const controller = new AbortController()
try {
const resp = await fetch('/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId, message }),
signal: controller.signal
})
if (!resp.ok || !resp.body) throw new Error('stream request failed')
const reader = resp.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 })
const blocks = buffer.split('\n\n')
buffer = blocks.pop() || ''
for (const block of blocks) {
const line = block.split('\n').find(l => l.startsWith('data:'))
if (!line) continue
const text = line.slice(5).trim()
if (text === '[DONE]') { onDone(); return }
onDelta(text)
}
}
onDone()
} catch (e) {
if (e.name !== 'AbortError') onError(e)
}
return controller
}
AbortController 是“停止生成”的关键,用户点停止时调 controller.abort(),后端连接断开,模型侧也会随之中断推理,能省掉无效算力。这个细节在演示时很加分,也确实是生产里该做的。
7. 智能客服核心功能测试与效果验证
功能测试建议按“从简到繁”的顺序推进,每步都有明确的成功标准,别一次性全开然后对着报错发呆。
7.1 单轮问答与兜底话术
测试目的:确认模型链路通、系统提示词生效、兜底逻辑可靠。
操作步骤:先用一句完全无关的问题(比如“今天天气怎么样”)测试兜底,再用一句知识库里有明确答案的问题测试正常回答。预期结果是前者返回“暂无相关说明,建议转人工”这类话术,后者返回带引用片段的答案。
判断成功的标准有两条:一是兜底话术是代码层面返回的,不是模型自由发挥的;二是正常回答里能对上知识库原文的关键信息,没有明显编造。常见失败原因是相似度阈值设太低导致无关内容被召回,或者系统提示词被用户输入覆盖,后者要做输入过滤。
7.2 多轮会话记忆验证
测试目的:确认同一 sessionId 下模型能记住前文,不同会话之间不串味。
操作步骤:第一轮说“我要退一台冰箱”,第二轮只问“多久能到账”,看模型是否能联系上文理解成退货退款时效。然后换一个 sessionId 重复第二轮问题,预期是模型反问“您指的是哪笔订单”。
判断成功的标准是:上下文连贯且不越界。常见失败原因有两个,一是 Redis key 设计没带 sessionId 前缀,导致所有人共享一份记忆;二是上下文条数设太多,把系统提示词挤掉,模型开始胡说。建议把记忆条数控制在 10 条以内,并在拼接时始终保留系统提示词。
7.3 RAG 知识库检索验证
测试目的:确认文档切片、向量化、召回、拼装整条链路可用。
操作步骤:上传一份 10 页以内的产品手册,等入库完成后问一个手册里明确写着的参数问题。先从日志看召回了几条、相似度分别是多少,再看最终答案是否引用了正确片段。
判断成功的标准是:召回片段与问题语义相关,答案能追溯到原文。常见失败原因包括切片太大导致语义稀释、切片太小导致上下文断裂、Embedding 模型和 Chat 模型不匹配、入库任务没跑完就发起提问。排查时优先看切片内容,切片不干净,后面调什么参数都没用。
7.4 流式输出与中断验证
测试目的:确认首字延迟可接受、中途停止能生效、前端渲染不错乱。
操作步骤:提一个需要较长回答的问题,观察首字出现时间;在生成中途点停止,看后端日志是否立刻断开、前端是否保留已生成内容。
判断成功的标准是:首字延迟在可接受范围(本地模型通常比远程服务慢,以实测为准),停止后不继续消耗算力,已生成内容不会突然清空。常见失败原因是反向代理开了缓冲,导致流式变成一次性返回,需要在网关层关闭响应缓冲。
7.5 工具调用与业务动作验证
测试目的:确认模型能正确触发“查订单”“转人工”这类业务动作,而不是只在文本层面回答。
操作步骤:配置一个查询订单状态的工具函数,提问“帮我查一下订单 12345 的状态”,观察模型是否发起工具调用、参数是否解析正确、返回结果是否被正确组织成自然语言。
判断成功的标准是:工具被正确触发、参数没有错位、异常订单号有兜底提示。常见失败原因是工具描述写得含糊,模型不知道该在什么时候调用;或者参数类型定义和实际不一致,导致反序列化失败。工具描述要写得像给新人看的接口文档,越明确越好。
8. 接口 API 设计与批量任务组织
8.1 对话接口与流式接口
接口设计上建议拆成两个:一个同步接口用于简单问答和第三方系统集成,一个流式接口用于前端对话。参数结构保持一致,方便复用。
java复制@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final CustomerServiceChatService chatService;
public ChatController(CustomerServiceChatService chatService) {
this.chatService = chatService;
}
@PostMapping
public ChatResponse chat(@RequestBody @Valid ChatRequest request) {
return chatService.chat(request.getSessionId(), request.getMessage());
}
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestBody @Valid ChatRequest request) {
return chatService.stream(request.getSessionId(), request.getMessage())
.timeout(Duration.ofSeconds(120))
.onErrorResume(e -> Flux.just("服务繁忙,请稍后重试"));
}
}
timeout 和 onErrorResume 这两行别省。模型服务偶尔会卡住,没有超时就会一直挂着连接,前端表现为“一直在转圈”。兜底返回一句友好提示,比抛 500 体验好得多。
8.2 批量问答任务
批量任务适合两类需求:一是历史工单的自动归类打标,二是知识库问答的回归测试。实现上不建议在一个请求里循环调模型,容易超时,正确做法是落任务表 + 异步消费。
json复制{
"taskId": "batch-20250101-001",
"inputFile": "./data/questions.csv",
"outputFile": "./data/answers.csv",
"concurrency": 2,
"retry": 2,
"timeoutSeconds": 120,
"model": "qwen2.5:7b"
}
concurrency 不要设太高,本地模型并发一高,单条延迟会成倍上升,反而更慢。建议从 1 到 2 起步,观察显存和响应时间再调。retry 是必要的,模型服务偶发超时很常见,重试两次能覆盖大部分抖动。输出文件按行写入并记录失败行号,方便断点续跑。
python复制import csv
import time
import requests
API = "http://127.0.0.1:8080/api/chat"
ok, fail = 0, 0
with open("./data/questions.csv", encoding="utf-8") as fin, \
open("./data/answers.csv", "w", encoding="utf-8", newline="") as fout:
reader = csv.DictReader(fin)
writer = csv.writer(fout)
writer.writerow(["question", "answer", "status"])
for idx, row in enumerate(reader):
for attempt in range(3):
try:
resp = requests.post(API, json={
"sessionId": f"batch-{idx}",
"message": row["question"]
}, timeout=120)
data = resp.json()
writer.writerow([row["question"], data.get("answer", ""), "ok"])
ok += 1
break
except Exception:
if attempt == 2:
writer.writerow([row["question"], "", "failed"])
fail += 1
time.sleep(2)
print(f"done, ok={ok}, fail={fail}")
批量脚本里每个问题用独立 sessionId,避免上下文互相污染。如果确实需要多轮批量,那就按“会话”分组,同一组内复用 sessionId。
9. 资源占用与性能观察方法
观察资源要分两个层面。模型侧看显存、内存、GPU 利用率;应用侧看线程池、连接池、GC 和接口耗时。两边的瓶颈要分开定位,否则容易误判成“SpringBoot 性能不行”。
模型侧的观察方式:本地部署时看 nvidia-smi 的显存占用和利用率,或者看模型服务自己的日志输出。重点观察三个变量对资源的影响:上下文长度、并发数、输出长度。上下文翻倍时显存占用通常明显上升,并发数从 1 提到 2 时延迟可能接近翻倍,输出长度直接决定生成耗时。这三个是调优的主要杠杆。
应用侧的观察方式:打开 Actuator 的指标端点,或者用日志记录每次请求的耗时、召回条数、token 数。建议在 Service 层埋一个统计:请求开始时间、检索耗时、模型耗时、总耗时。跑二三十条测试问题后,能很直观地看出时间花在哪。
降低资源占用的几个实用手段:缩短系统提示词,别把一堆规则全塞进去;控制召回条数,top-k 从 4 降到 2 往往效果差不多但省不少 token;限制输出长度,客服场景 200 字以内基本够用;长会话做摘要压缩,把早期对话总结成一句话再拼进上下文,而不是原样堆着。
避免进程残留也很重要。本地调试时反复重启,旧进程可能还占着 8080 端口,新进程起不来或者被路由到旧服务,表现是“代码改了没生效”。排查命令很简单:
bash复制# Linux / macOS
lsof -i:8080
kill -9 <PID>
# Windows
netstat -ano | findstr :8080
taskkill /PID <PID> /F
10. SpringAI 智能客服常见问题与排查方法
下面这张表覆盖了跟敲过程中出现频率最高的问题。遇到报错先对号入座,别一上来就怀疑框架。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 启动报类找不到 | SpringAI 版本与 starter 名称不匹配 | 看 pom 里的版本和依赖树 | 统一版本,按官方文档选 artifactId |
| 流式输出变成一次性返回 | 网关或容器开了响应缓冲 | 看响应头与代理配置 | 关闭响应缓冲,设置 text/event-stream |
| 模型调用 401 | 密钥未注入或环境变量名写错 | 打印配置项,确认环境变量 | 走环境变量配置,避免硬编码 |
| 知识库检索结果为空 | 入库未完成或 Embedding 未配置 | 查切片表条数与向量维度 | 补齐 Embedding 配置,重新入库 |
| 回答明显编造 | 相似度阈值过低或未做兜底 | 看召回片段相似度分数 | 提高阈值,代码层加兜底话术 |
| 多轮对话串味 | 记忆 key 未按会话隔离 | 检查 Redis key 结构 | key 加 sessionId 前缀 |
| 长时间无响应 | 未设置超时 | 看线程栈与模型服务日志 | 加 timeout 与熔断兜底 |
| 批量任务卡住 | 并发过高导致排队 | 观察模型服务利用率 | 降低并发,加重试与超时 |
| 中文乱码 | 数据库或响应编码未设 utf8mb4 | 检查表和连接串 | 统一字符集,Servlet 强制 UTF-8 |
| 前端跨域失败 | 未配代理或 CORS | 看浏览器控制台 | 开发期用 Vite proxy,生产配网关 |
有几个排查经验值得单独说。第一,模型相关的报错,先把模型服务单独用 curl 验证一遍,排除掉网络和密钥问题,再回到 SpringBoot 里找。第二,检索相关的报错,先看切片内容干不干净,再看相似度分数,最后才看提示词。第三,流式相关的问题,先用 curl 直连后端看输出是不是逐条返回,如果直连正常、经过前端不正常,那就是前端读流或代理的问题。
还有一个高频坑是“配置改了没生效”。SpringBoot 的配置加载有优先级,application.yml、application-dev.yml、环境变量、命令行参数层层覆盖,改错文件就会以为没生效。启动日志里会打印激活的 profile,先确认它,再确认配置来源。
11. 工程化最佳实践与上线前检查
第一,把提示词当代码管理。提示词改动会直接影响输出质量,应该纳入版本管理,最好做一个简单的版本号和效果记录,改完跑一遍回归测试集,别凭感觉调。
第二,接口要限流和鉴权。模型调用是有成本的,公网暴露的接口必须做身份校验、频率限制和请求体大小限制。内部接口也建议加个调用方标识,方便出问题时定位来源。
第三,会话数据分级存储。短期上下文放 Redis 并设过期时间,长期记录落库并按周期归档。日志里不要打印完整对话原文,涉及个人信息的字段要脱敏,这是合规的底线。
第四,为失败设计路径。模型超时、检索为空、工具调用失败,这三种情况都要有明确兜底,不能把异常直接抛给前端。兜底话术要统一,别每个模块各写一套。
第五,批量任务要可观测。任务表里记录状态、开始时间、结束时间、成功数、失败数、失败原因,跑完能出一份报告。没有这些,批量任务出了问题只能靠猜。
第六,涉及真实客户数据的场景,上线前要确认授权范围、留存周期、访问权限和审计日志。知识库内容要有人负责审核,不能什么文档都往里灌。对外提供客服服务时,建议保留人工转接入口,模型只是第一道过滤。
第七,先做小规模验证再扩容。用 20 到 50 条典型问题跑一轮,看准确率、平均耗时、失败率,数据能接受再往上加量。直接上大批量,出了问题很难定位是数据问题还是工程问题。
12. 这套 SpringAI 项目接下来怎么推进
最值得先验证的三件事:一是模型链路能不能通,用一句最简单的对话确认;二是 RAG 检索准不准,用一条知识库里明确写着的问题确认;三是流式输出稳不稳,用长回答加中途停止确认。这三件事跑通,项目骨架就立住了,剩下的都是在这上面加功能。
最容易踩的坑集中在三个地方:SpringAI 版本与 starter 名称不匹配、Embedding 模型没配导致检索永远为空、流式被代理缓冲导致前端体验崩掉。这三个问题都不难,但会卡掉很多时间,建议在动手前就把验证步骤准备好。
往后可以继续扩展的方向也不少:加多路召回和重排序提升检索质量;加会话摘要压缩降低长对话的 token 消耗;加评测集做自动化回归,每次改提示词都跑一遍;加多租户隔离,让一套系统服务多个业务线;加人工坐席工作台,做模型与人工的协同。每一步都不需要推翻现有结构,这也是分层设计带来的好处。
如果只记一件事,那就是:智能客服的效果上限取决于数据质量,工程能保证的是下限。把兜底写好、把检索调准、把日志埋全,这套系统就已经能用了。
