Spring AI 的 RAG 检索结果到底插在了哪里?聊聊 Advisor 链顺序这个隐形坑

发布时间:2026/6/30 14:59:25
Spring AI 的 RAG 检索结果到底插在了哪里?聊聊 Advisor 链顺序这个隐形坑 最近帮一个做企业知识库问答的同事看代码他遇到一个挺典型的问题RAG 检索明明命中了正确的文档相似度也不低可模型回答的时候完全没用上这些内容答得跟没接知识库一样。排查了半天最后发现问题出在 Advisor 的注册顺序上——这是 Spring AI 里一个很容易被忽视但一旦出问题就很难定位的细节。先看一下这个真实场景代码大概是这样写的var chatClient ChatClient.builder(chatModel) .defaultAdvisors( new SimpleLoggerAdvisor(), MessageChatMemoryAdvisor.builder(chatMemory).build(), RetrievalAugmentationAdvisor.builder() .documentRetriever(documentRetriever) .build() ) .build();三个 Advisor 都注册了日志能打出来记忆功能也正常唯独检索到的文档内容感觉没生效。如果你只看代码本身很难一眼看出问题——三个 Advisor 单独测都没问题组合起来就是不对。Advisor 链到底是怎么跑的Spring AI 官方文档里把这套机制说得很清楚每个 Advisor 的执行顺序由getOrder()方法决定数值越小优先级越高越先执行。但这里有个反直觉的地方——Advisor 链的工作方式是一个栈结构最先处理请求的 Advisor反而是最后处理响应的。换句话说如果你把 RAG 检索的 Advisor 放在链的最后order 值最大它确实会在请求阶段最后被处理——但这恰好是对的位置因为它需要在请求即将发往模型之前把检索到的文档塞进去。如果你把它放在最前面检索结果反而会被后面的 Advisor比如负责拼装记忆上下文的 Advisor覆盖或者忽略掉因为后面的 Advisor 拿到的请求里根本没意识到前面已经做了什么修改。这就是为什么三个 Advisor 单独测都没问题放在一起就错——单独测试时根本不存在顺序冲突的场景。Spring AI 1.1.x 里默认顺序是怎么设计的如果你用的是较新版本的 Spring AI框架已经帮一部分核心 Advisor 规划好了默认顺序理解这个设计能帮你少踩很多坑。工具调用相关的ToolCallingAdvisor默认 order 是HIGHEST_PRECEDENCE 300确保它在请求阶段尽可能早执行方便拦截后续工具调用的请求和响应。而内存类 AdvisorMessageChatMemoryAdvisor等的默认顺序常量DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER在近期版本里被调整为HIGHEST_PRECEDENCE 200也就是说记忆相关的处理被放在了工具调用之外这是官方为了避免内存仓库存储不支持的工具调用消息类型而专门做的调整。这个细节说明一件事Advisor 的顺序不是随手填几个数字就行的它直接决定了谁先看到原始请求谁先看到最终响应进而决定了 RAG 检索内容、对话历史、工具调用结果这几类数据到底以什么先后关系拼进最终的 Prompt。怎么排查和修正遇到类似问题建议按这个思路走第一步先确认 RAG 检索本身有没有问题。用ChatClientResponse拿到执行上下文检查RetrievalAugmentationAdvisor暴露的DOCUMENT_CONTEXT常量对应的内容确认文档确实被检索出来了而不是检索阶段就没命中。ChatClientResponse response chatClient.prompt() .user(question) .call() .chatClientResponse(); // 检查检索到的文档是否存在于上下文中 Object retrievedDocs response.context().get(RetrievalAugmentationAdvisor.DOCUMENT_CONTEXT);第二步显式给每个 Advisor 指定 order而不是依赖注册顺序的隐式排序。多个 Advisor 顺序相同时执行顺序是不保证的这一点官方文档专门强调过。RetrievalAugmentationAdvisor ragAdvisor RetrievalAugmentationAdvisor.builder() .documentRetriever(documentRetriever) .order(0) // 显式指定确保检索增强发生在合适的位置 .build(); MessageChatMemoryAdvisor memoryAdvisor MessageChatMemoryAdvisor.builder(chatMemory) .advisorOrder(100) // 大于 RAG advisor确保记忆拼装在检索之后处理 .build(); var chatClient ChatClient.builder(chatModel) .defaultAdvisors(new SimpleLoggerAdvisor(), ragAdvisor, memoryAdvisor) .build();第三步把SimpleLoggerAdvisor的 order 设置得足够大接近LOWEST_PRECEDENCE让它在链的末尾打印日志这样你看到的日志内容才是真正发给模型之前的最终请求而不是某个中间状态。这件事背后的工程化判断很多人接入 Spring AI 的第一印象是Advisor 机制很优雅链式拼装很方便这个判断没错。但优雅的代价是隐藏了执行细节多个 Advisor 组合时的数据流向不会主动报错只会在结果上表现为不对但不知道哪不对。这其实和 Spring MVC 里拦截器链、Filter 链的坑是同一类问题——单个组件测试通过不代表组合后行为正确。区别在于AI 应用里这种顺序错误不会抛异常模型只是安静地给出一个看起来正常但信息不准确的回答排查成本反而更高。如果你的项目里同时用到了 RAG、记忆、工具调用这几类 Advisor建议在写代码的时候就把每个 Advisor 的 order 显式写出来而不要依赖默认值或者注册顺序——具体 API 和默认 order 常量可能会随版本变化实际项目中应以官方文档为准但显式声明顺序、不依赖隐式行为这个原则不会变。我把这次排查过程整理成笔记主要是想提醒一句Advisor 链看起来是个简单的列表实际跑起来是一个有方向性的栈理解这个方向比记住几个 API 名字更重要。