切换主题
L05:Advisor 环绕链——order 决定进入顺序,返回时方向相反
版本基准:Spring AI Core
1.1.3
建议时长:40~45 分钟
1. 本节只解决一个问题
假设存在三个 Advisor:
text
TracingAdvisor order = 10
MemoryAdvisor order = 20
SecurityAdvisor order = 30模型调用前后,真实日志顺序是什么?
很多人会凭直觉回答:
text
10 before
20 before
30 before
模型
10 after
20 after
30 after这是错的。
Advisor 是环绕链,不是两组独立的前置和后置拦截器。
正确结构是:
text
10 before
20 before
30 before
ChatModel
30 after
20 after
10 after本节目标是让你能从源码解释这个顺序,而不是靠背结论。
2. 先把 Advisor 看成嵌套函数
不要先把它想成 Spring MVC Interceptor。
先把每个 Advisor 简化成:
java
Response advise(Request request, Chain chain) {
Request processedRequest = before(request);
Response response = chain.next(processedRequest);
return after(response);
}三个 Advisor 嵌套后,相当于:
java
advisor10(
advisor20(
advisor30(
chatModel(request)
)
)
)因此:
text
before:由外向内
模型调用:最内层
after:由内向外这就是 Around Chain 的本质。
3. BaseAdvisor.adviseCall() 是最小真相
Spring AI 1.1.3 的同步 Advisor 默认流程可以压缩成:
java
ChatClientRequest processedRequest =
before(chatClientRequest, callAdvisorChain);
ChatClientResponse response =
callAdvisorChain.nextCall(processedRequest);
return after(response, callAdvisorChain);关键不是 before() 和 after() 两个方法本身,而是中间这一句:
java
callAdvisorChain.nextCall(processedRequest)它会把控制权交给下一个 Advisor。
所以上一个 Advisor 的 after() 必须等:
text
所有更内层 Advisor
+
ChatModel全部返回后才能执行。
同步失败时会怎样
如果:
java
callAdvisorChain.nextCall(...)抛出异常,默认 BaseAdvisor 没有 try/finally 包围它。
因此:
text
before 已执行
模型或下游抛异常
after 不执行
异常继续向外抛这会直接影响:
- Memory 写入;
- 耗时日志;
- 资源清理;
- 业务审计;
- 指标完整性。
如果某个资源无论成功失败都必须释放,不能只放在 after()。
4. Advisor Chain 怎样选择下一个 Advisor
DefaultAroundAdvisorChain 内部维护两个队列:
text
Deque<CallAdvisor>
Deque<StreamAdvisor>同步调用进入:
java
advisorChain.nextCall(request)核心过程可以简化为:
java
CallAdvisor advisor = callAdvisors.pop();
return advisor.adviseCall(request, this);也就是说:
text
每调用一次 nextCall()
→ 从队头弹出一个 Advisor
→ 执行它
→ 它内部再决定是否调用后续链一个非常重要的能力
Advisor 可以不调用:
java
chain.nextCall(request)这意味着它可以直接短路整个模型调用。
例如:
java
@Override
public ChatClientResponse adviseCall(
ChatClientRequest request,
CallAdvisorChain chain) {
if (cacheHit(request)) {
return cachedResponse(request);
}
return chain.nextCall(request);
}短路后:
text
后续 Advisor 不执行
ChatModelCallAdvisor 不执行
DeepSeek 不会收到请求所以 Advisor 不只是“前后打印日志”,它拥有调用链控制权。
5. order 到底怎样决定顺序
构建 Advisor Chain 时,Spring AI 会根据 Advisor 的:
java
int getOrder()进行排序。
规则是:
text
order 越小
→ 越靠外层
→ before 越早
→ after 越晚示例:
| Advisor | order | before | after |
|---|---|---|---|
| TracingAdvisor | 10 | 第 1 个 | 最后 1 个 |
| SecurityAdvisor | 20 | 第 2 个 | 倒数第 2 个 |
| MemoryAdvisor | 30 | 第 3 个 | 倒数第 3 个 |
| ChatModelCallAdvisor | 最低优先级 | 最后进入 | 它直接调用模型 |
记忆方式
text
小 order 是大外套
先穿上,最后脱下但不要只记口诀,必须能画出嵌套调用。
6. 为什么 ChatModelCallAdvisor 必须在链底
构建链时,DefaultChatClientRequestSpec.buildAdvisorChain() 会额外加入:
text
ChatModelCallAdvisor
ChatModelStreamAdvisor其中同步终点是:
java
ChatModelCallAdvisor.adviseCall(...)它最终执行:
java
ChatResponse response = chatModel.call(request.prompt());所以完整结构是:
text
业务 Advisor 1
→ 业务 Advisor 2
→ Memory Advisor
→ ChatModelCallAdvisor
→ DeepSeekChatModel.call()ChatModelCallAdvisor 的 order 是最低优先级位置,也就是数值很大,确保它最后进入。
自定义 Advisor 的危险配置
不要给自定义 Advisor 返回:
java
Ordered.LOWEST_PRECEDENCE然后依赖它一定在模型调用之前或之后。
当多个组件使用相同 order 时,顺序可能受注册方式和内部队列操作影响。
生产规则:
text
关键 Advisor 使用明确且互不冲突的 order 区间
不要依赖相同 order 下的偶然顺序建议规划:
text
-10000 ~ -9000 安全、租户、请求校验
-8000 ~ -7000 traceId、审计入口
-6000 ~ -5000 Memory 读取与写入
-4000 ~ -3000 RAG、Prompt 增强
-2000 ~ -1000 业务策略
正数区间 最终请求观察、兼容处理
最低优先级 ChatModelCallAdvisor这只是工程约定,不是 Spring AI 官方固定区间。
7. 第一个实验:打印真实进入和退出顺序
实现一个最小 Advisor:
java
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
public final class OrderProbeAdvisor implements BaseAdvisor {
private final String name;
private final int order;
public OrderProbeAdvisor(String name, int order) {
this.name = name;
this.order = order;
}
@Override
public ChatClientRequest before(
ChatClientRequest request,
AdvisorChain chain) {
System.out.printf("%s before, order=%d%n", name, order);
return request;
}
@Override
public ChatClientResponse after(
ChatClientResponse response,
AdvisorChain chain) {
System.out.printf("%s after, order=%d%n", name, order);
return response;
}
@Override
public int getOrder() {
return order;
}
}注册:
java
var advisorA = new OrderProbeAdvisor("A", 30);
var advisorB = new OrderProbeAdvisor("B", 10);
var advisorC = new OrderProbeAdvisor("C", 20);
chatClient.prompt()
.advisors(advisorA, advisorB, advisorC)
.user("只回复 OK")
.call()
.content();预期日志:
text
B before, order=10
C before, order=20
A before, order=30
模型调用
A after, order=30
C after, order=20
B after, order=10注意:注册顺序是 A、B、C,但执行顺序由 order 改成 B、C、A。
8. 第二个实验:证明 Advisor 可以短路模型
java
public final class BlockAdvisor implements BaseAdvisor {
@Override
public ChatClientResponse adviseCall(
ChatClientRequest request,
CallAdvisorChain chain) {
return ChatClientResponse.builder()
.context(request.context())
.build();
}
@Override
public ChatClientRequest before(
ChatClientRequest request,
AdvisorChain chain) {
return request;
}
@Override
public ChatClientResponse after(
ChatClientResponse response,
AdvisorChain chain) {
return response;
}
@Override
public int getOrder() {
return -10000;
}
}核心点不是这段 Demo 的返回值是否适合业务,而是:
text
adviseCall() 没有调用 chain.nextCall()因此模型不会执行。
生产中可用于:
- 黑名单拦截;
- 参数不合法直接拒绝;
- 语义缓存命中;
- 权限不足;
- 限流降级;
- 固定业务回答。
但必须明确记录:
text
这次响应来自 Advisor 短路
不是模型生成否则监控会把缓存响应误算为模型成功率。
9. 第三个实验:异常时哪些 after 不会执行
创建一个抛异常的内层 Advisor:
java
public final class FailureAdvisor implements BaseAdvisor {
@Override
public ChatClientRequest before(
ChatClientRequest request,
AdvisorChain chain) {
throw new IllegalStateException("模拟调用前失败");
}
@Override
public ChatClientResponse after(
ChatClientResponse response,
AdvisorChain chain) {
System.out.println("FailureAdvisor after");
return response;
}
@Override
public int getOrder() {
return 20;
}
}外层:
text
LoggingAdvisor order=10
FailureAdvisor order=20执行过程:
text
LoggingAdvisor.before
FailureAdvisor.before
抛异常不会出现:
text
FailureAdvisor.after
LoggingAdvisor.after工程含义
下面这些动作不能只依赖成功后的 after():
- 清除 ThreadLocal;
- 释放独占资源;
- 结束必须闭合的业务 span;
- 写入失败审计;
- 统计异常耗时。
需要根据功能选择:
text
try/catch/finally
Micrometer Observation
Reactor doOnError / doFinally
统一异常处理10. 流式 Advisor 与同步 Advisor 不能完全类推
同步默认路径:
text
before
→ nextCall
→ after流式默认路径大致是:
text
before
→ nextStream 返回 Flux
→ 持续发出多个 ChatClientResponse
→ 满足 finishReason 条件时执行 after这带来三个区别。
区别一:after() 不是每个 chunk 都执行
否则 Memory 会把每个字符片段都保存成一条 AssistantMessage。
区别二:用户取消可能没有正常 finishReason
例如前端断开 SSE:
text
Flux 被 cancel
→ 不一定收到完整完成 chunk
→ 默认 after 可能不执行区别三:某些 Advisor 会覆盖默认流式实现
MessageChatMemoryAdvisor 为了保存完整 AssistantMessage,会使用聚合器旁路聚合整个流,然后再调用 after()。
所以排查流式 Advisor 时必须先问:
text
这个 Advisor 直接使用 BaseAdvisor 默认 adviseStream
还是自己重写了 adviseStream?11. 自定义耗时 Advisor 的第一版与问题
第一版:
java
public final class TimingAdvisor implements BaseAdvisor {
private static final String START_TIME = "timing.start";
@Override
public ChatClientRequest before(
ChatClientRequest request,
AdvisorChain chain) {
request.context().put(START_TIME, System.nanoTime());
return request;
}
@Override
public ChatClientResponse after(
ChatClientResponse response,
AdvisorChain chain) {
long start = (long) response.context().get(START_TIME);
long costMs = (System.nanoTime() - start) / 1_000_000;
System.out.println("costMs=" + costMs);
return response;
}
}问题:
text
下游异常时 after 不执行
流式时一次调用有多个响应片段
用户取消时可能没有完成响应
context 中的值需要确保被响应链保留因此它只是教学 Demo,不是生产级计时方案。
生产中更合适的是:
- 使用 Spring AI 已有 Observation;
- 自定义 ObservationConvention;
- 对业务维度补充低基数 tag;
- 日志中关联 traceId 和 conversationId;
- 流式使用终止信号而不是只依赖 finishReason。
12. Advisor 顺序的架构取舍
假设同时存在:
text
权限 Advisor
Memory Advisor
RAG Advisor
Prompt 审计 Advisor
敏感词 Advisor
模型调用一种合理顺序:
text
权限校验
→ 租户和 conversationId 校验
→ Memory 读取
→ RAG 检索
→ 最终 Prompt 审计
→ 敏感信息脱敏或模型输入策略
→ ChatModel但这不是唯一答案。
需要根据目标取舍。
敏感词检查放在 Memory 前
优点:
text
非法输入不会进入历史记录缺点:
text
审计可能看不到完整对话上下文敏感词检查放在 Memory 后
优点:
text
可以结合历史消息判断风险缺点:
text
当前 UserMessage 可能已经被 Memory Advisor 写入所以 Advisor 顺序不是纯技术问题,而是数据治理和业务语义问题。
13. 掌握验证
有以下 Advisor:
text
A order=-100
B order=0
C order=100
ChatModelCallAdvisor order=最低优先级回答:
- before 顺序是什么?
- 模型返回后的 after 顺序是什么?
- B 不调用
nextCall()时,C 是否执行? - B 不调用
nextCall()时,DeepSeek 是否收到请求? - C.before 抛异常时,A.after 和 B.after 是否执行?
- 两个 Advisor 使用相同 order 时,生产代码能否依赖注册顺序?
- 流式响应被取消时,能否保证所有 after 都执行?
14. 本节结论
text
order 越小,Advisor 越靠外
before 越早,after 越晚每个 Advisor 都拥有三种能力:
text
修改请求
继续调用下游
短路调用并直接返回同步异常时,默认 after() 不保证执行;流式场景还要额外考虑完成信号、取消和聚合。
下一节进入 Memory 最关键的工程问题:
为什么模型失败后可能留下只有 UserMessage、没有 AssistantMessage 的半轮对话?