1. 企业智能客服系统的整体架构设计思路
1.1 为什么选择 Spring AI 作为技术底座
做企业级智能客服这件事,技术选型是第一道分水岭。我前后试过三种路线:纯 Python 技术栈、自研 Java 调用大模型 API、以及基于 Spring AI 框架搭建。最终把生产环境落在 Spring AI 上,原因很实在——企业已有的业务系统绝大多数是 Java 生态,订单、工单、用户中心、权限体系全在 Spring 体系里跑,客服系统不可能脱离这些数据孤岛独立存在。
Spring AI 的核心价值在于它把大模型交互抽象成了 Spring 风格的编程模型。你可以用ChatClient像调用普通 Service 一样调用大模型,用Advisor机制像 Spring AOP 一样织入记忆、RAG、日志等横切逻辑,用@Tool注解像声明 Bean 一样把业务方法暴露给模型调用。这种设计对 Java 开发者来说几乎没有学习成本,团队里写过 Spring Boot 的人两天就能上手。
另一个关键考量是可观测性与可治理性。企业客服不是玩具,每一次对话都可能涉及退款、投诉、合规审查。Spring AI 天然接入 Micrometer 指标体系和 Spring 的日志链路,配合 Actuator 可以实时看到 token 消耗、响应延迟、工具调用成功率。这些在纯脚本式调用里要自己造轮子,在 Spring AI 里基本是开箱即用。
1.2 五大能力模块的职责边界
标题里提到的 RAG、工具、记忆、流式、兜底,不是五个孤立功能,而是一条完整的对话处理流水线。我把它们的职责边界理清楚,后面每一课都围绕这个骨架展开。
| 模块 | 核心职责 | 解决什么问题 | 典型实现 |
|---|---|---|---|
| RAG | 知识检索增强 | 模型不知道企业私有知识 | 向量库 + 检索 Advisor |
| 工具 | 业务能力调用 | 模型无法操作真实业务系统 | Function Calling |
| 记忆 | 多轮上下文保持 | 模型记不住前几轮说了什么 | ChatMemory + 会话存储 |
| 流式 | 逐字输出响应 | 用户等待焦虑、体验差 | SSE + StreamingChatClient |
| 兜底 | 异常与降级处理 | 模型幻觉、超时、限流 | 熔断 + 规则引擎 + 人工转接 |
这五块的关系可以这样理解:用户提问进来,记忆模块先补全上下文,RAG模块去知识库捞相关资料,模型基于资料和上下文决定是否需要调用工具,如果需要就触发业务接口,最终结果通过流式通道推给用户,整个过程被兜底机制全程守护。任何一环出问题,兜底都能接住,保证客服系统不会"哑火"。
1.3 十课收官的学习路径规划
我把整个项目拆成十课,不是随便分的,而是按照"能跑起来 → 能答得准 → 能办成事 → 体验好 → 不出事"的递进逻辑。前十课里,前两课搭骨架,中间六课逐个攻破五大能力,最后两课做整合与压测。这个顺序很重要,我见过不少团队一上来就搞 RAG,结果连基础的对话链路都没跑通,调优时根本分不清是检索问题还是模型问题。
提示:如果你时间紧,至少要把记忆和兜底这两块吃透。记忆决定了多轮对话的智商下限,兜底决定了系统能不能上生产。RAG 和工具可以后续迭代,但这两块是地基。
2. RAG 检索增强的核心细节与实操要点
2.1 文档切分策略:别让切分毁了检索质量
RAG 效果好不好,七成看切分,两成看 embedding,一成看模型。这句话是我踩了无数坑之后总结的。很多教程上来就教你TokenTextSplitter按 800 token 切,但企业文档根本不是均匀的。产品手册有层级标题,工单记录有对话轮次,FAQ 有问答对,用同一种切法必然出问题。
我的做法是按文档类型分流切分。产品手册用标题感知切分,遇到##或###就断开,保证每个 chunk 语义完整;工单记录按对话轮次切,一个用户问题加一个客服回答算一个 chunk;FAQ 直接按问答对切,一问一答就是一个独立单元。这样切出来的 chunk 语义自洽,检索命中率能提升 30% 以上。
// 标题感知切分示例 TokenTextSplitter splitter = new TokenTextSplitter( 500, // 目标 chunk 大小 100, // 最小 chunk 大小 50, // 重叠 token 数 1000, // 最大 chunk 大小 true // 保留分隔符 );重叠 token 这个参数特别关键。我一开始设成 0,结果跨 chunk 的句子被切断,检索时经常捞到半句话。后来设成 chunk 大小的 10% 到 15%,效果明显改善。但也不能太大,否则重复内容多,浪费向量库空间还降低检索精度。
2.2 向量库选型:从开发到生产的平滑过渡
开发阶段我用的是内存向量库SimpleVectorStore,好处是零依赖、启动快、调试方便。但一上生产就露馅了——重启数据全丢,多实例无法共享,数据量一大内存直接爆。生产环境我最终选了 PostgreSQL 加 pgvector 扩展,理由是运维团队本来就维护着 PG,不用引入新的中间件,备份、监控、权限体系全是现成的。
选型对比我整理成表,你可以按自己团队情况对号入座:
| 向量库 | 适用阶段 | 优势 | 劣势 |
|---|---|---|---|
| SimpleVectorStore | 开发调试 | 零配置、内存级速度 | 不持久化、单机 |
| pgvector | 中小生产 | 复用 PG 运维、支持 SQL 混合查询 | 超大规模性能一般 |
| Milvus | 大规模生产 | 分布式、亿级向量 | 运维复杂、成本高 |
| Redis Stack | 中等规模 | 低延迟、缓存向量一体 | 内存成本高 |
我实测下来,十万级文档 chunk 用 pgvector 完全够用,检索延迟稳定在 50ms 以内。超过百万级再考虑 Milvus,别过早优化。
2.3 检索 Advisor 的织入与参数调优
Spring AI 的QuestionAnswerAdvisor是 RAG 的核心入口,它把"检索 + 拼接 prompt + 调用模型"这套流程封装成一个 Advisor。你只需要在 ChatClient 构建时挂上去,后面每次对话自动走 RAG 流程。
ChatClient chatClient = ChatClient.builder(chatModel) .defaultAdvisors( new QuestionAnswerAdvisor(vectorStore, SearchRequest.builder() .topK(5) // 返回最相似的5条 .similarityThreshold(0.7) // 相似度阈值 .build()) ) .build();topK和similarityThreshold这两个参数是调优重点。topK 太小,资料不够答不全;太大,噪声多模型容易跑偏。我一般从 5 开始试,看回答质量再上下调。相似度阈值 0.7 是个经验值,低于这个值的 chunk 基本是噪声,宁可不要。
注意:相似度阈值不是越高越好。设成 0.9 会导致很多边缘相关的资料被过滤掉,模型反而答不上来。我建议先用 0.6 到 0.7 跑一批真实问题,看召回情况再微调。
2.4 检索质量评估:别凭感觉,要量化
RAG 调优最怕凭感觉。我建立了一套简单的评估方法:准备 50 个真实用户问题,每个问题标注标准答案和应该命中的文档片段。每次调整切分或检索参数后,跑一遍评估集,统计三个指标——召回率(该命中的有没有命中)、准确率(命中的是不是对的)、答案满意度(人工打分)。
这套评估集我建议你从项目第一天就开始积累,让客服团队帮忙标注。没有评估集,你永远不知道改动是变好还是变坏。我见过团队调了两个月参数,结果发现还不如初始配置,就是因为没有量化标准。
3. 工具调用让客服真正能办事
3.1 Function Calling 的本质与适用场景
RAG 解决的是"知道",工具解决的是"做到"。用户问"我的订单到哪了",RAG 只能从知识库里找"如何查询订单"的说明,但用户要的是具体订单的物流状态。这时候就需要工具调用——模型识别出用户意图,触发一个查询订单的 Java 方法,把真实数据拿回来组织成回答。
Spring AI 的工具调用基于 Function Calling 机制。你用@Tool注解标记一个方法,框架会自动生成工具描述(包括方法名、参数、用途),在对话时把这些描述一起发给模型。模型判断需要调用工具时,返回一个工具调用请求,框架执行对应方法,把结果回传给模型,模型再组织成自然语言回答。
@Component public class OrderTools { @Tool(description = "根据订单号查询订单的物流状态和预计送达时间") public OrderStatus queryOrderStatus( @ToolParam(description = "订单号,通常是12位数字") String orderId) { // 调用真实业务接口 return orderService.getStatus(orderId); } }3.2 工具描述的撰写技巧
工具描述写得好不好,直接决定模型会不会在正确的时机调用正确的工具。我总结了几条经验:
第一,描述要写"什么时候用",而不只是"是什么"。比如"查询订单状态"不如"当用户询问订单物流、配送进度、预计送达时间时使用"。模型是靠语义匹配来决定调用的,把使用场景写清楚,命中率大幅提升。
第二,参数描述要包含格式和示例。模型不知道你的订单号是几位数、什么格式,写清楚"12位数字,如 202401150001",模型就能从用户的话里正确提取。
第三,工具粒度要适中。一个工具干一件事,别搞"万能工具"接收一个 action 参数然后内部 switch。模型对多参数、多分支的工具容易调用错误。宁可拆成多个小工具,让模型自己选。
3.3 多工具编排与调用链设计
真实客服场景往往需要多个工具配合。用户说"我要退掉上周买的那件衣服",这背后涉及:查订单、查商品是否在退货期、发起退货申请、生成退货单号。四个工具串成一条链。
Spring AI 支持模型在一次对话中连续调用多个工具。模型先调查询订单,拿到订单信息后调退货资格检查,通过后调发起退货,最后调生成单号。整个过程模型自主编排,你只需要把工具定义好。
但这里有个坑:工具调用是有轮次限制的。默认配置下模型最多连续调用几轮就会停止,防止无限循环。如果你的业务链路很长,需要在配置里调大这个限制,或者把多个步骤合并成一个粗粒度工具。
spring: ai: openai: chat: options: # 控制工具调用相关行为 tool-choice: auto3.4 工具调用的安全边界
工具能操作真实业务系统,这是能力也是风险。我设了三道防线:
第一道,权限校验前置。工具方法内部第一件事就是校验当前用户有没有权限操作这个订单。别指望模型帮你做权限判断,它只负责调用,权限必须代码里硬校验。
第二道,敏感操作二次确认。退款、改地址、取消订单这类操作,工具不直接执行,而是返回一个"待确认"状态,让模型问用户"确认要退款吗",用户确认后再调另一个执行工具。
第三道,调用频率限制。用 Resilience4j 给每个工具加限流,防止模型抽风疯狂调用把下游系统打挂。我设的是单用户每分钟最多 10 次工具调用,超过就返回"操作过于频繁"。
提示:工具方法的返回值尽量结构化,别返回一大段文本。模型对 JSON 结构的理解比自然语言好,而且结构化数据方便你后续做日志分析和问题排查。
4. 记忆机制让多轮对话有连续性
4.1 ChatMemory 的工作原理
没有记忆的客服系统,每轮对话都是失忆的。用户说"我要退货",系统问"退哪个订单",用户说"就刚才那个",系统懵了——它不知道"刚才那个"指什么。ChatMemory 就是解决这个问题的。
Spring AI 的ChatMemory本质是一个会话级的消息列表。每轮对话的用户消息和模型回复都追加进去,下次调用模型时把整个列表一起发过去,模型就有了上下文。默认实现InMemoryChatMemory用 ConcurrentHashMap 存,按 conversationId 隔离不同会话。
ChatMemory chatMemory = new InMemoryChatMemory(); ChatClient chatClient = ChatClient.builder(chatModel) .defaultAdvisors( MessageChatMemoryAdvisor.builder(chatMemory).build() ) .build(); // 调用时指定会话ID chatClient.prompt() .user("我要退货") .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "user-123-session-456")) .call() .content();4.2 会话存储的持久化方案
InMemoryChatMemory只能用于开发。生产环境有两个问题:一是重启丢失,二是多实例不共享。用户第一次请求打到实例 A,第二次打到实例 B,记忆就断了。
我的方案是用 Redis 做会话存储,自己实现一个ChatMemory接口。key 设计成chat:memory:{conversationId},value 存消息列表的 JSON 序列化结果,设置 30 分钟过期。这样多实例共享,重启不丢,还能通过 Redis 的过期机制自动清理僵尸会话。
public class RedisChatMemory implements ChatMemory { private final StringRedisTemplate redisTemplate; private final ObjectMapper objectMapper; private static final Duration TTL = Duration.ofMinutes(30); @Override public void add(String conversationId, List<Message> messages) { String key = "chat:memory:" + conversationId; // 读取现有消息,追加新消息,写回 // 设置TTL } @Override public List<Message> get(String conversationId) { // 从Redis读取并反序列化 } }4.3 上下文窗口管理与截断策略
记忆不是越多越好。模型的上下文窗口有限,把几十轮对话全塞进去,一是超 token 限制,二是早期无关内容会干扰模型判断。我见过用户聊了 50 轮之后,模型开始答非所问,就是因为上下文太长了。
我的截断策略是滑动窗口 + 摘要压缩结合。保留最近 10 轮完整对话,更早的对话用模型压缩成一段摘要放在最前面。这样既保留了长期记忆的要点,又控制了 token 消耗。
// 滑动窗口配置 MessageWindowChatMemory memory = MessageWindowChatMemory.builder() .maxMessages(20) // 最多保留20条消息 .build();20 条消息大概是 10 轮对话,对大多数客服场景够用。如果是复杂的技术支持场景,可以调到 30 到 40 条,但要相应关注 token 消耗成本。
4.4 记忆与 RAG 的协同
记忆和 RAG 容易打架。用户上一轮说"我要查订单",这一轮说"那退款呢",RAG 检索时如果只拿"那退款呢"去检索,什么都捞不到。正确做法是把记忆里的上下文和当前问题拼起来再检索。
Spring AI 的 Advisor 执行顺序决定了这个协同方式。我把记忆 Advisor 放在 RAG Advisor 前面,这样 RAG 拿到的 prompt 已经包含了历史上下文。但要注意,拼接后的查询可能很长,检索效果反而下降。我的折中方案是用最近 3 轮对话加当前问题做检索查询,更早的历史只用于生成回答不用于检索。
5. 流式输出提升交互体验
5.1 SSE 流式响应的实现
大模型生成一段 200 字的回答要 3 到 5 秒,如果等全部生成完再返回,用户盯着转圈圈会以为系统卡死。流式输出让模型每生成一个 token 就推给前端,用户看到文字一个个蹦出来,感知延迟从 5 秒降到 0.5 秒以内。
Spring AI 的流式调用用stream()替代call(),返回一个Flux<String>。配合 Spring WebFlux 的 SSE 支持,几行代码就能实现。
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> chatStream(@RequestParam String message, @RequestParam String sessionId) { return chatClient.prompt() .user(message) .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId)) .stream() .content(); }前端用 EventSource 接收,每收到一个 chunk 就追加到页面上。这个体验提升是立竿见影的,我们上线流式后用户满意度评分涨了 15 个百分点。
5.2 流式场景下的工具调用处理
流式和工具调用结合时有个特殊问题:模型决定调用工具的那一刻,流式输出会中断,等工具执行完再继续流。用户看到的是文字输出到一半停住了,过一会儿又继续。这个停顿如果超过 2 秒,体验就很差。
我的处理方式是在工具调用期间给前端推一个特殊事件,前端显示"正在为您查询..."的提示。Spring AI 的流式响应里可以通过检查 chunk 的元数据判断是否进入工具调用阶段。虽然框架层面没有直接暴露这个事件,但可以通过自定义 Advisor 拦截实现。
5.3 流式输出的错误处理
流式连接比普通 HTTP 请求脆弱得多。网络抖动、模型超时、下游工具异常,任何一个环节出问题都会导致流中断。用户看到的是文字输出到一半戛然而止,没有任何提示。
我的做法是在流式 Controller 外面包一层错误处理,捕获异常后推一个错误事件给前端,前端显示"回答生成中断,请重试"并提供重试按钮。同时后端记录完整的错误日志,方便排查。
return chatClient.prompt() .user(message) .stream() .content() .onErrorResume(e -> { log.error("流式输出异常", e); return Flux.just("[回答生成中断,请重试]"); });注意:流式场景下不要用
@Transactional包裹整个方法。流是异步的,事务在流开始时就提交了,后续的工具调用不在事务内。如果工具涉及数据库写操作,要在工具方法内部单独开事务。
6. 兜底机制保障系统稳定
6.1 模型幻觉的识别与拦截
模型幻觉是智能客服最大的风险。用户问"你们的退货政策是什么",模型可能编出一个根本不存在的"30天无理由退货"。这种回答一旦发给用户,轻则投诉,重则法律纠纷。
我的拦截策略是RAG 溯源 + 规则校验双管齐下。RAG 检索时记录命中的文档片段,模型回答后检查回答内容是否能在检索到的片段里找到依据。找不到依据的回答标记为"低置信度",触发兜底流程——要么让模型重新回答并强调"只根据资料回答",要么直接转人工。
规则校验针对特定场景。比如涉及金额的回答,用正则提取数字,和数据库里的真实金额比对,不一致就拦截。涉及政策的回答,维护一个关键词黑名单,出现"保证""一定""绝对"这类绝对化表述就人工复核。
6.2 超时与限流的降级方案
模型调用超时是常态。我设的超时是 10 秒,超过就触发降级。降级不是简单返回"系统繁忙",而是分级处理:
第一级降级,切换到更快的轻量模型。主模型用大参数版本保证质量,超时后切到小参数版本,虽然质量略降但响应快。
第二级降级,返回预设的模板回答。比如"您的问题正在处理中,请稍后重试,或拨打客服热线 XXX"。模板回答虽然不智能,但至少不会让用户干等。
第三级降级,直接转人工。所有自动处理都失败时,把会话转给人工客服,附带完整的对话历史,人工客服无缝接手。
限流同理,用 Resilience4j 的 RateLimiter 控制单用户和全局的请求频率。超过阈值返回排队提示,而不是直接拒绝。
6.3 人工转接的触发条件与流程
人工转接不是失败,而是兜底的最后一道防线。我设了几个明确的触发条件:
- 用户连续两次表达不满("你没听懂""我要投诉")
- 模型置信度低于阈值
- 涉及敏感操作(大额退款、账户安全)
- 用户主动要求转人工
- 工具调用连续失败
触发后,系统把当前会话标记为"待人工",推送到客服工作台,同时给用户提示"正在为您转接人工客服,预计等待 X 分钟"。人工客服接入后能看到完整的对话历史和 RAG 检索到的资料,不用用户重复描述问题。
6.4 全链路监控与告警
兜底机制要生效,前提是你知道什么时候该兜底。我搭了一套监控体系,核心指标包括:
| 指标 | 阈值 | 告警方式 |
|---|---|---|
| 模型响应 P99 延迟 | > 8s | 企业微信 |
| 工具调用失败率 | > 5% | 企业微信 + 短信 |
| 兜底触发率 | > 10% | 企业微信 |
| 用户负反馈率 | > 3% | 邮件日报 |
| Token 日消耗 | > 预算 80% | 邮件预警 |
这些指标通过 Micrometer 采集,Prometheus 存储,Grafana 展示。兜底触发率是最关键的指标,它直接反映系统健康度。一旦超过 10%,说明要么模型有问题,要么知识库该更新了,要么业务系统接口不稳定。
7. 常见问题与排查技巧实录
7.1 RAG 检索不到相关内容怎么办
这是最高频的问题。排查顺序我总结成一张表:
| 排查项 | 检查方法 | 常见原因 |
|---|---|---|
| 文档是否入库 | 查向量库记录数 | 入库任务失败 |
| 切分是否合理 | 抽样看 chunk 内容 | 切分太碎或太大 |
| Embedding 是否正常 | 对比相似问题向量距离 | 模型加载失败 |
| 相似度阈值 | 调低阈值重试 | 阈值设太高 |
| 查询改写 | 看实际检索 query | 用户口语化严重 |
我遇到最多的是切分问题。有一次产品手册按固定长度切,把一个完整的操作步骤切成三段,用户问"怎么配置",检索到的三段都不完整,模型拼出来的回答缺步骤。改成标题感知切分后立刻解决。
7.2 工具调用不触发或调错工具
模型不调工具,九成是工具描述没写好。检查三点:描述里有没有写清楚使用场景、参数描述够不够具体、工具名是不是太抽象。我见过一个工具叫process,模型根本不知道它是干嘛的,改成queryOrderLogisticsStatus后立刻正常触发。
调错工具通常是工具之间职责重叠。比如同时有"查订单"和"查物流"两个工具,模型分不清该用哪个。解决办法是合并成一个工具,内部根据参数判断,或者把描述写得更互斥。
7.3 多轮对话记忆丢失
记忆丢失先查 conversationId 是否稳定。前端每次请求如果生成新的 sessionId,记忆自然就断了。正确做法是 sessionId 在用户打开对话窗口时生成一次,整个会话期间保持不变,存在前端 localStorage 里。
如果 sessionId 稳定还丢,查 Redis 连接和序列化。我遇到过一次 Redis 里存的消息反序列化失败,原因是 Message 对象新增了字段,旧数据反序列化报错。解决办法是序列化时用兼容模式,或者升级时清空旧会话。
7.4 流式输出中断
流式中断排查分前端和后端。前端看 EventSource 的 onerror 回调,后端看日志有没有异常。常见原因:Nginx 缓冲了 SSE 响应(需要配置proxy_buffering off)、网关超时时间太短(调大到 60s)、模型侧连接断开。
Nginx 缓冲这个坑我踩过。默认配置下 Nginx 会缓冲响应,导致流式变成一次性返回,用户还是要等全部生成完。加上proxy_buffering off和X-Accel-Buffering: no响应头才解决。
7.5 兜底触发过于频繁
兜底频繁说明系统某环节不稳定。先看监控确定是哪类兜底:超时兜底多就优化模型调用或换更快的模型;幻觉兜底多就检查 RAG 检索质量;工具失败兜底多就查下游业务系统。
我遇到过一次兜底率突然飙升到 30%,排查发现是向量库连接池耗尽,RAG 检索全部超时。扩容连接池后恢复正常。所以兜底率异常时,第一件事是看依赖服务的健康状态。
8. 生产上线的经验与踩坑记录
8.1 灰度发布策略
智能客服直接面向用户,不能一次性全量上线。我的灰度策略是按用户 ID 哈希分批放量:先 1% 内部员工,观察一天;再 10% 低风险用户,观察三天;然后 50%,最后全量。每批放量前检查兜底率、负反馈率、平均响应时间三个指标,任一超标就回滚。
灰度期间保留旧版客服入口,用户可以通过特定指令切回旧版。这样即使新版出问题,用户也有退路。
8.2 成本控制的实际手段
大模型调用成本不低,企业客服日均几千次对话,一个月 token 费用可能上万。我用了几个手段压成本:
第一,缓存高频问题。用户问得最多的 20% 问题占了 80% 的请求量,这些问题的回答缓存起来,命中缓存直接返回,不走模型。缓存用 Redis,key 是问题的 embedding 哈希,相似度超过 0.95 视为同一问题。
第二,分级模型。简单问题用轻量模型,复杂问题才用大模型。判断逻辑是先用轻量模型试答,置信度不够再升级到大模型。
第三,控制上下文长度。记忆窗口从 20 条压到 12 条,RAG topK 从 5 降到 3,token 消耗降了 40%,质量只降了一点点。
8.3 知识库持续更新的机制
知识库不是一次建好就完事。产品更新、政策调整、新问题出现,都需要及时补充。我建立了一个闭环:客服团队每周整理高频未解决问题,标注标准答案,运营人员审核后入库,系统自动重新 embedding 并更新向量库。
这个流程的关键是降低入库门槛。我做了个简单的后台页面,运营人员粘贴问答对就能入库,不用懂技术。入库后自动触发向量化,几分钟后生效。没有这个闭环,知识库很快就过时了。
8.4 用户反馈的收集与利用
每条回答后面我都加了"有用/没用"按钮。用户点"没用"时弹出一个输入框让用户补充说明。这些反馈数据每周分析一次,分为三类处理:回答错误(补充知识库)、回答不完整(优化 RAG 检索)、回答语气不好(调整 prompt)。
积累三个月后,我拿这些反馈数据微调了一个小模型专门做意图识别,准确率比通用模型高 20%。这是后话,但反馈数据的价值从一开始就体现出来了。
9. 性能优化的关键参数与实测数据
9.1 响应延迟的构成分析
一次完整的客服响应,延迟由几部分组成:记忆读取(Redis,5ms)、RAG 检索(向量库,50ms)、模型首 token(300ms 到 1s)、模型生成(取决于长度)、工具调用(取决于业务接口)。流式输出下用户感知的是首 token 延迟,所以优化重点是前三个环节。
我把 RAG 检索从同步改成异步预取,用户消息进来就触发检索,和记忆读取并行执行,省了 50ms。模型首 token 延迟主要看模型服务商,换了个更快的接入点后从 1s 降到 400ms。
9.2 并发能力的压测结果
用 JMeter 压测,单实例在 4 核 8G 配置下,流式接口能扛住 200 并发,P99 延迟 3s。瓶颈在模型 API 的并发限制,不是应用本身。解决办法是加实例 + 模型 API 多 key 轮询。
压测时发现一个坑:流式连接占用线程时间长,Tomcat 默认 200 线程很快耗尽。换成 WebFlux 响应式栈后,同样配置能扛 500 并发。如果你们并发量预期高,建议直接上 WebFlux。
9.3 向量检索的性能调优
pgvector 默认用精确检索,数据量大了会慢。建 HNSW 索引后,十万级数据检索从 200ms 降到 20ms。索引参数m和ef_construction需要根据数据量调,我用的 m=16、ef_construction=64,平衡了构建速度和检索精度。
CREATE INDEX ON vector_store USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);提示:HNSW 索引是近似检索,会损失一点点召回率。如果业务对召回率要求极高,可以先用 HNSW 粗筛,再对 top 结果做精确重排。
10. 项目收官后的复盘与后续演进方向
十课走完,这套系统在我们内部已经稳定跑了半年,日均处理对话 3000 多次,兜底率控制在 5% 以内,用户满意度 85 分。回头看,最大的收获不是某个技术点,而是建立了一套"可观测、可评估、可迭代"的工程方法。没有评估集,调优就是盲人摸象;没有监控,出问题就是抓瞎;没有反馈闭环,知识库就是死水。
后续我打算往三个方向演进。一是多模态,支持用户发截图问问题,比如发一张报错截图让客服诊断。Spring AI 已经有多模态支持,接入成本不高。二是主动服务,不等用户问,系统监测到异常(比如订单超时未发货)主动推送提醒。三是个性化,根据用户历史对话调整回答风格,老用户简洁直接,新用户详细耐心。
这套东西没有什么黑魔法,就是把每个环节做扎实,把每个坑填上。你要是正在做类似的项目,建议先把记忆和兜底跑通,再逐步加 RAG 和工具。别一上来就追求大而全,能稳定跑起来的小系统,比跑不起来的完美架构有价值得多。