Spring AI RAG 工程化:从能检索到可信回答
这篇更像一份学习和项目复盘,记录我在理解 Spring AI RAG 时,对文档切分、召回、重排、Advisor 和评估的一些真实思考。
最近看 Spring AI 的时候,我一开始对 RAG 的理解其实挺简单:把文档切块,转成向量,用户提问时检索几段相关内容,再丢给大模型回答。这个流程 Demo 很容易跑通,网上很多教程也是这么讲的。
但我后来越看越觉得,RAG 真正麻烦的地方不在“能不能跑”,而在“跑出来的东西能不能信”。尤其是如果把它放到运维 Agent 或企业知识库里,回答错了不是小问题。用户可能真的会照着它去排查线上故障,甚至执行一些有风险的操作。所以这篇不想写成那种很完整的官方教程,更像是我自己梳理 RAG 工程化时的一份笔记。
我最开始踩到的理解误区
最开始我觉得向量数据库很神奇,好像只要用了 Embedding,就能解决文档检索问题。后来发现不是这样。向量检索只是把“语义相似”这件事做得更方便,但它不保证召回的一定是正确上下文。
比如一个问题是:
订单服务接口超时,日志里出现 HikariPool exhausted,怎么排查?
如果只靠向量相似度,它可能会召回“订单服务慢查询优化”“数据库连接池配置说明”“接口超时统一处理方案”这些看起来都相关的文档。但真正有用的可能是某次历史故障复盘,里面写了“发布后某个接口忘记关闭流式查询,导致连接池被占满”。如果这段内容被切得不好,或者元数据没标出来,就很可能召回不到。
所以我现在理解的 RAG,不是“把大模型接上知识库”,而是一个检索系统、生成系统和评估系统的组合。
文档切分比我想象中重要
固定长度切块是最容易实现的,比如 500 字一段,重叠 100 字。这个方法适合入门,但放到故障手册、项目文档、接口说明里会比较粗糙。
我更倾向于先按文档结构切:
- 一级标题通常对应一个大主题。
- 二级标题通常对应一个具体问题。
- 表格、代码块、错误码说明不要随便切断。
- 故障复盘要尽量保留“现象 -> 排查 -> 根因 -> 解决方案”的完整链路。
如果一个 chunk 只有“解决方案”,没有“故障现象”,检索时不一定能命中;如果一个 chunk 只有“现象”,没有“根因”,模型回答时又容易说得很空。所以切分的时候,我会更关心语义完整性,而不是只关心长度。
我觉得比较合理的 chunk 元数据至少要有:
{
"service": "order-service",
"docType": "incident",
"version": "2026-05",
"source": "故障复盘",
"permission": "backend-team"
}
这些元数据后面会用于过滤。比如用户只能看自己项目组的知识库,那就不能只靠 prompt 说“不要回答无权限内容”,必须在检索阶段就过滤掉。
召回不能只靠一种方式
纯向量召回适合语义问题,比如“为什么接口突然变慢”。但很多工程问题其实是关键词问题,比如错误码、类名、配置项、表名、Topic 名称。
如果用户问:
Consumer group rebalance 频繁发生怎么办?
关键词 rebalance 非常重要。如果向量召回没把这个词权重体现出来,可能召回一堆 Kafka 消费者基础概念,而不是具体排查 rebalance 的文档。
所以我会考虑混合召回:
- 向量召回:解决语义相似。
- 关键词召回:解决错误码、类名、配置项。
- 元数据过滤:解决权限和业务范围。
- 重排:把粗召回结果重新排序。
这里的重排我觉得很关键。向量召回像是先捞一网,重排像是再仔细挑。实际系统可以先召回 20 条,再重排取前 5 条,这样比直接取 top5 稳一些。
Spring AI Advisor 的位置
Spring AI 里我比较喜欢 Advisor 这个抽象,因为它有点像 Spring 生态里熟悉的拦截器或者 AOP。RAG、记忆、日志、权限这些都不是业务 Controller 本身,却又会影响每一次模型调用。
我理解的调用链大概是:
用户问题
-> 权限/租户上下文
-> 查询改写
-> 知识库检索
-> 文档重排
-> Prompt 组装
-> 模型回答
-> 日志和评估记录
在 Spring AI 里,QuestionAnswerAdvisor 可以把 VectorStore 里的检索结果放进 prompt。MessageChatMemoryAdvisor 可以管理对话记忆。实际项目里不能把 Advisor 随便堆上去,因为它们的顺序会影响结果。
比如权限过滤一定要在检索前做,不能等模型回答完再过滤。记忆也不能无限加,不然历史对话会挤占知识库上下文。这个问题我以前没太意识到,后来算 token 的时候才发现,上下文窗口其实很宝贵。
Prompt 里要明确“不要瞎编”
RAG 不是为了让模型显得什么都会,而是让它在有依据的时候回答,没有依据的时候承认不知道。
我会在系统提示词里加类似这样的约束:
请优先依据检索到的上下文回答。
如果上下文没有足够证据,请说明信息不足,不要编造系统内部事实。
如果是排障建议,请区分“已确认事实”和“推测方向”。
涉及重启、删除、切流等高风险动作时,只能给出建议,不能直接假设已经执行。
这几句话看起来普通,但很重要。尤其是运维场景,模型如果一本正经地胡说,反而比“不知道”更危险。
评估这块我还在补
如果只是个人项目,很多时候会忽略评估,只是手动问几个问题,看回答还可以就算完成。但如果真的想把项目讲得有说服力,最好准备一小套评估集。
我现在会考虑整理这些问题:
- 能直接从文档找到答案的问题。
- 需要综合两三段文档的问题。
- 文档里没有答案的问题。
- 有权限限制的问题。
- 有旧版本和新版本冲突的问题。
- 运维排障类问题。
每次改切分策略、Embedding 模型、重排模型或 prompt,都用同一批问题回归一遍。评估指标不用一开始就搞得很复杂,先看召回是否命中、回答有没有引用依据、有没有瞎编,就已经比纯主观判断好很多。
如果放到我的项目里会怎么做
如果结合自动化运维 Agent,我会把 RAG 当成一个 Tool,而不是每次都强行检索。因为用户有些问题是通用技术问题,比如“解释一下 JVM 的 G1”,不一定要查内部知识库;但如果问“订单服务 P1 告警怎么处理”,就应该主动查内部故障手册和历史复盘。
一个比较理想的流程是:
- Agent 判断问题类型。
- 如果需要内部知识,调用 RAG 检索工具。
- RAG 工具返回结构化结果,包括文档标题、片段、相关度、来源。
- Agent 根据结果继续查日志、指标或生成回答。
- 最终回答里说明引用了哪些文档。
这样 RAG 不只是一个 prompt 拼接技巧,而是 Agent 可以主动使用的一种能力。
小结
我现在对 RAG 的理解是:它不是一个单点功能,而是一条工程链路。文档怎么切、怎么召回、怎么过滤权限、怎么重排、怎么约束模型、怎么评估效果,每一步都会影响最终回答质量。
Spring AI 降低了 Java 项目接入 RAG 的门槛,但要把它做得可信,还是要回到后端工程的基本功:数据建模、权限控制、链路日志、异常处理和持续评估。这个过程没有 Demo 看起来那么轻松,不过也正因为这样,才比较适合写进项目复盘里。