ARTICLE · 1062153
Spring AI 源码阅读(二)——Advisor 链:顺序、契约与扩展点
配套仓库:https://github.com/iweidujiang/spring-ai-source-notes
对照源码:Spring AI
2.0.1样例:仓库内
AdvisorChainController(/chat/advisors、/chat/advisors/block)
本文导航
上一篇把一次 call().content() 拆到了链尾的 ChatModelCallAdvisor。
业务里如果挂上自己的 Advisor,代码往往是这样:
@GetMapping("/chat/advisors")
public String advisors(@RequestParam(defaultValue = "用一句话介绍 Advisor") String q) {
returnthis.chatClient.prompt()
.advisors(new TraceAdvisor("late", 100), new TraceAdvisor("early", 10))
.user(q)
.call()
.content();
}
这里 advisors 中传了两个 Advisor , 在代码中的顺序为:先 late(100),再 early(10),很多人会默认:
谁写在前面,谁先执行。
跑起来日志却是 early 先 before、后 after,再做一个 故意不调用 nextCall 的 Advisor,后面的日志和模型调用会整段消失。
那么本文的重点就是:
Advisor 链如何用 order 排顺序、用 nextCall 传接力,以及为什么调模型也必须坐在这条链上。
本文就带你整明白这几件事:
看见多个 Advisor,先问:顺序听书写,还是听 getOrder()?看见 adviseCall,先问:不调用nextCall时,链上还剩谁会跑?看见内置 Advisor,先问:Memory、Tool、ChatModel 各自卡在哪一档 order?
一、它要解决什么问题
横切能力(日志、记忆、安全、工具循环……)如果散落在业务里,会出现:
ChatClient 或再套一层伪客户端 | |
ChatModel.call 各走各的,观测和重试对不齐 |
Spring AI 的选择是:
同一套管线:自定义 Advisor 与 ChatModelCallAdvisor都实现CallAdvisor。顺序显式化: Advisor继承Ordered,建链时按getOrder()重排。接力显式化:继续往下走必须调用 chain.nextCall(...);链尾不再往后传,而是调模型。
样例仓库里的 TraceAdvisor / BlockingAdvisor,就是为了把这三点跑成可观察的现象。
二、顺序:看 getOrder,而不是书写顺序
误区
.advisors(a, b)里谁在前,谁先执行。
实际设计
buildAdvisorChain 会把你传入的 Advisor 放进列表,再追加模型相关 Advisor,最后交给 DefaultAroundAdvisorChain.builder().pushAll(...).build()。
pushAll 之后会 reOrder:
OrderComparator.sort(order 值越小,排序后越靠前)按排序结果 addLast进双端队列执行时 nextCall用pop()取队头 —— 因此 order 越小越先进入、越后返回(更靠近用户一侧)
ChatModelCallAdvisor.getOrder() 是 Ordered.LOWEST_PRECEDENCE,所以它总在同步链的最内侧。
样例里故意写成 late(100) 在前、early(10) 在后,控制台仍应是:
[before] name=early order=10
[before] name=late order=100
(此处进入 ChatModelCallAdvisor → 模型)
[after] name=late order=100
[after] name=early order=10

记一句:
order 越小越外层;书写顺序只是入参,建链时会被丢掉。
源码落点
位置 1:DefaultChatClient#buildAdvisorChain
复制 this.advisorschain.add(ChatModelCallAdvisor...)(注释写明:放在 stack bottom,作为链上最后一环)同步场景还会带上 ChatModelStreamAdvisor,但content()走的是 Call 链
// At the stack bottom add the model call advisors.
List<Advisor> chain = new ArrayList<>(this.advisors);
chain.add(ChatModelCallAdvisor.builder().chatModel(this.chatModel).build());
chain.add(ChatModelStreamAdvisor.builder().chatModel(this.chatModel).build());
框架用注释把意图写死了:模型调用 Advisor 扮演链上的 last advisors,而不是业务里手写的特殊分支。
位置 2:DefaultAroundAdvisorChain.Builder#reOrder
OrderComparator.sort(callAdvisors);
this.callAdvisors.clear();
callAdvisors.forEach(this.callAdvisors::addLast);
先按 Ordered 排序,再依次加到队列尾部;随后
pop()从头部取,所以最小 order 最先被执行。
三、契约:adviseCall 与 nextCall
要解决的问题
过滤器链如果约定含糊,会出现两种极端:
扩展点偷偷调模型,链的后半截形同虚设; 扩展点忘了放行,请求静默结束,排查极难。
设计选择
CallAdvisor 只有一个核心方法:
ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain);
典型环绕写法(样例 TraceAdvisor,与官方 SimpleLoggerAdvisor 同构):
log.info("[before] name={} order={}", this.advisorName, this.order);
ChatClientResponse response = chain.nextCall(request);
log.info("[after] name={} order={}", this.advisorName, this.order);
return response;
DefaultAroundAdvisorChain.nextCall 的步骤是:
若队列空,抛 No CallAdvisors availablepop出一个 Advisor打 Advisor 层 Observation 调用该 Advisor 的 adviseCall(request, this)—— 把同一个 chain 传回去
因此:下一环仍然通过 同一个 chain 的 nextCall 触发;不是框架在 adviseCall 返回后再自动调下一个。
链尾的 ChatModelCallAdvisor **不再调用 nextCall**,而是:
ChatResponse chatResponse = this.chatModel.call(formattedChatClientRequest.prompt());
return ChatClientResponse.builder()
.chatResponse(chatResponse)
.context(...)
.build();
这是设计上的终点,不是漏写。
反例:BlockingAdvisor
样例 /chat/advisors/block 挂了一个 Ordered.HIGHEST_PRECEDENCE 的 Advisor,adviseCall 里 **不调用 nextCall**,直接返回空的 ChatClientResponse。
现象:
没有 TraceAdvisor的 before/after不会打到 ChatModel.content()得到null(没有ChatResponse可剥)

扩展点负责自己的事,并决定是否交给下一环;框架负责按 order 把环串好。
这和
Filter.doFilter/ Gatewaychain.filter是同一类契约。
源码落点
位置 3:DefaultAroundAdvisorChain#nextCall
var advisor = this.callAdvisors.pop();
// ...
var chatClientResponse = advisor.adviseCall(chatClientRequest, this);
pop+ 把this作为 chain 传入:接力权在 Advisor 手里,不在框架的 for 循环里。
位置 4:ChatModelCallAdvisor#adviseCall
链尾唯一合法的“不调用 nextCall”:改为
chatModel.call。自定义 Advisor 若也这样干,等于私自截胡模型调用。
位置 5:样例 TraceAdvisor / BlockingAdvisor
一个示范契约,一个示范违约。读官方源码时,先会写最小 Advisor,再去看 Memory、Tool 也不容易晕。
四、内置坐标:谁排在谁外面
先建立相对位置:
HIGHEST_PRECEDENCE | ||
DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDERHIGHEST + 200) | ||
ToolCallingAdvisor.DEFAULT_ORDERHIGHEST + 300) | ||
LOWEST_PRECEDENCE |

Advisor 接口上对 Memory 默认顺序的注释,值得单独读一遍:
Memory 要包住 Tool 循环,Tool 自己维护中间多轮历史,这是 产品语义写进 order,不是随意拍的数字。
同步与流式拆成 CallAdvisor / StreamAdvisor 两套:content() 只推进 Call 队列;stream() 走另一条。
不要假设一个 adviseCall 能覆盖流式语义。
五、总览:一次带自定义 Advisor 的同步调用
Spring AI Advisor 调用时序.png)
和 Spring 熟悉概念对照:
CallAdvisor.adviseCall | Filter.doFilter |
CallAdvisorChain.nextCall | chain.doFilterchain.filter |
getOrder()OrderComparator | Ordered Bean 的优先级 |
ChatModelCallAdvisor | |
nextCall |
六、读源码复盘
问问自己:
顺序的真相是什么?
书写顺序、注册顺序,还是
Ordered?这里是建链时reOrder,执行时pop。接力权在谁手里?
框架自动 for-each,还是扩展点必须调用
nextCall?这里是后者。链尾是谁,凭什么可以不再放行?
这里是
ChatModelCallAdvisor,它改为调用ChatModel。短路时对外表现是什么?
样例里是空
ChatClientResponse→content()为null;生产代码里短路通常要返回明确错误或降级结果。
这几个问题搞懂了,再去读 Memory、Tool、RAG Advisor,只是在同一套管线上挂不同 order 的实现而已。
七、小结
Advisor 链把横切与调模型收成同一件事:
顺序 —— getOrder()越小越外层;书写顺序在reOrder后作废。契约 —— 继续执行必须 nextCall;不调用等于短路整条链。终点 —— ChatModelCallAdvisor合法地不再nextCall,改为chatModel.call。坐标 —— Memory、Tool、Model 的默认 order 表达的是产品语义,不是魔法数。
下期预告:Tool Calling 循环 —— 模型返回 tool_calls 之后,谁在转、历史挂在哪、和 Advisor order 如何咬合。
本次导航结束,欢迎 关注、点赞、转发。