Skip to content

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 越晚

示例:

Advisororderbeforeafter
TracingAdvisor10第 1 个最后 1 个
SecurityAdvisor20第 2 个倒数第 2 个
MemoryAdvisor30第 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()

ChatModelCallAdvisororder 是最低优先级位置,也就是数值很大,确保它最后进入。

自定义 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=最低优先级

回答:

  1. before 顺序是什么?
  2. 模型返回后的 after 顺序是什么?
  3. B 不调用 nextCall() 时,C 是否执行?
  4. B 不调用 nextCall() 时,DeepSeek 是否收到请求?
  5. C.before 抛异常时,A.after 和 B.after 是否执行?
  6. 两个 Advisor 使用相同 order 时,生产代码能否依赖注册顺序?
  7. 流式响应被取消时,能否保证所有 after 都执行?

14. 本节结论

text
order 越小,Advisor 越靠外
before 越早,after 越晚

每个 Advisor 都拥有三种能力:

text
修改请求
继续调用下游
短路调用并直接返回

同步异常时,默认 after() 不保证执行;流式场景还要额外考虑完成信号、取消和聚合。

下一节进入 Memory 最关键的工程问题:

为什么模型失败后可能留下只有 UserMessage、没有 AssistantMessage 的半轮对话?

源码入口

Built with VitePress. Deployed on Cloudflare Pages.