夜雨聆风学习资料网

ARTICLE · 1062153

Spring AI 源码阅读(二)——Advisor 链:顺序、契约与扩展点

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?

一、它要解决什么问题

横切能力(日志、记忆、安全、工具循环……)如果散落在业务里,会出现:

麻烦
如果没有统一的 Advisor 链
顺序
各写各的包装,谁包谁全靠约定
扩展
每加一种能力就改 ChatClient 或再套一层伪客户端
调模型
横切与 ChatModel.call 各走各的,观测和重试对不齐

Spring AI 的选择是:

  1. 同一套管线:自定义 Advisor 与 ChatModelCallAdvisor 都实现 CallAdvisor
  2. 顺序显式化Advisor 继承 Ordered,建链时按 getOrder() 重排。
  3. 接力显式化:继续往下走必须调用 chain.nextCall(...);链尾不再往后传,而是调模型。

样例仓库里的 TraceAdvisor / BlockingAdvisor,就是为了把这三点跑成可观察的现象。

二、顺序:看 getOrder,而不是书写顺序

误区

.advisors(a, b) 里谁在前,谁先执行。

实际设计

buildAdvisorChain 会把你传入的 Advisor 放进列表,再追加模型相关 Advisor,最后交给 DefaultAroundAdvisorChain.builder().pushAll(...).build()

pushAll 之后会 reOrder

  1. OrderComparator.sortorder 值越小,排序后越靠前
  2. 按排序结果 addLast 进双端队列
  3. 执行时 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
advisor链执行顺序

记一句:

order 越小越外层;书写顺序只是入参,建链时会被丢掉。

源码落点

位置 1:DefaultChatClient#buildAdvisorChain

  • 复制 this.advisors
  • chain.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 的步骤是:

  1. 若队列空,抛 No CallAdvisors available
  2. pop 出一个 Advisor
  3. 打 Advisor 层 Observation
  4. 调用该 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 可剥)
blockingAdvisor效果

扩展点负责自己的事,并决定是否交给下一环;框架负责按 order 把环串好。

这和 Filter.doFilter / Gateway chain.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 也不容易晕。

四、内置坐标:谁排在谁外面

先建立相对位置:

角色
典型 order 取向
含义
自定义最外层(如安全、阻断)
靠近 HIGHEST_PRECEDENCE
最先看到请求,可选择不放行
Chat Memory
DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER
HIGHEST + 200
包在 Tool 循环外侧,注释写明了意图
Tool Calling
ToolCallingAdvisor.DEFAULT_ORDER
HIGHEST + 300
在 Memory 内侧,自己管工具多轮
你的业务 Trace/日志
中间任意档,只要别撞车
用日志验证即可
ChatModelCallAdvisor
LOWEST_PRECEDENCE
同步链最内层,真正调模型
执行顺序

Advisor 接口上对 Memory 默认顺序的注释,值得单独读一遍:

Memory 要包住 Tool 循环,Tool 自己维护中间多轮历史,这是 产品语义写进 order,不是随意拍的数字。

同步与流式拆成 CallAdvisor / StreamAdvisor 两套:content() 只推进 Call 队列;stream() 走另一条。

不要假设一个 adviseCall 能覆盖流式语义。

五、总览:一次带自定义 Advisor 的同步调用

Spring AI Advisor 调用时序.png)

和 Spring 熟悉概念对照:

Spring AI
可对照的熟悉概念
CallAdvisor.adviseCallFilter.doFilter
 / Gateway Filter
CallAdvisorChain.nextCallchain.doFilter
 / chain.filter
getOrder()
 + OrderComparator
Spring Ordered Bean 的优先级
ChatModelCallAdvisor
链尾真正发起调用的那一环
不调用 nextCall
Filter 里不调用 chain —— 请求被短路

六、读源码复盘

问问自己:

  1. 顺序的真相是什么?

    书写顺序、注册顺序,还是 Ordered?这里是建链时 reOrder,执行时 pop

  2. 接力权在谁手里?

    框架自动 for-each,还是扩展点必须调用 nextCall?这里是后者。

  3. 链尾是谁,凭什么可以不再放行?

    这里是 ChatModelCallAdvisor,它改为调用 ChatModel

  4. 短路时对外表现是什么?

    样例里是空 ChatClientResponse → content() 为 null;生产代码里短路通常要返回明确错误或降级结果。

这几个问题搞懂了,再去读 Memory、Tool、RAG Advisor,只是在同一套管线上挂不同 order 的实现而已。

七、小结

Advisor 链把横切与调模型收成同一件事:

  1. 顺序 —— getOrder() 越小越外层;书写顺序在 reOrder 后作废。
  2. 契约 —— 继续执行必须 nextCall;不调用等于短路整条链。
  3. 终点 —— ChatModelCallAdvisor 合法地不再 nextCall,改为 chatModel.call
  4. 坐标 —— Memory、Tool、Model 的默认 order 表达的是产品语义,不是魔法数。

下期预告:Tool Calling 循环 —— 模型返回 tool_calls 之后,谁在转、历史挂在哪、和 Advisor order 如何咬合。


本次导航结束,欢迎 关注、点赞、转发

相关学习资料