最近帮一个做企业知识库问答的同事看代码,他遇到一个挺典型的问题: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,确保它在请求阶段尽可能早执行,方便拦截后续工具调用的请求和响应。而内存类 Advisor(MessageChatMemoryAdvisor 等)的默认顺序常量 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 名字更重要。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐