org.springframework.ai.chat.client.advisor.api

AI

# Advisor 排序机制 —— Spring AI 是怎么把 4 个 `@Component` 串成一条链的

配套阅读:[README.md](README.md) 第二节「Advisor 链执行顺序」给了结论图;这份文档补上****为什么是这个顺序、Spring 在哪一步排序、链是怎么“走”起来的****,源码级别讲清楚,面试被追问细节时不会卡壳。


## 一、结论先行 `getOrder()` **不是** Spring Bean 装配顺序,也****不是**** `@Order` 注解在起作用(虽然语义一样)。真正发生的事情是:

  1. 你的 4 个 advisor 实现 `Advisor extends Ordered` 接口的 `getOrder()`。2. `ChatClientConfig` 把它们传给 `ChatClient.builder().defaultAdvisors(…)`。3. Spring AI 内部的 `DefaultAroundAdvisorChain.Builder` 在构建时,用 `OrderComparator.sort(…)` 把 advisor 列表****按 `getOrder()` 升序重排**——不管你传参时写的是什么顺序。4. 排好序的 advisor 被塞进一个 `Deque`,`nextCall()` 每次 `pop()` 一个出来执行,advisor 内部再调用 `chain.nextCall(request)` 递归推进——这是一条**手写的洋葱模型调用链****,跟 Servlet `Filter` / Koa 中间件同构,不是 Spring AOP 代理链。

## 二、`getOrder()` 从哪来 ```java// org.springframework.ai.chat.client.advisor.api.Advisorpublic interface Advisor extends Ordered {    String getName();} // org.springframework.ai.chat.client.advisor.api.CallAdvisorpublic interface CallAdvisor extends Advisor {    ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain);}``` `Ordered` 就是 Spring Core 的 `org.springframework.core.Ordered`——跟 `@Order` 注解、`AnnotationAwareOrderComparator` 是同一套排序语义(数值越小,优先级越高)。Spring AI 直接复用这个约定,而不是自己造一套排序系统。 值得注意的常量(`Advisor` 接口里定义): ```javaint DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER = Ordered.HIGHEST_PRECEDENCE + 1000;``` Spring AI 内建的 `MessageChatMemoryAdvisor` 等默认排在 `HIGHEST_PRECEDENCE + 1000` 附近,刻意****留出 1000 个 slot**** 给用户插入更高优先级的自定义 advisor。本项目 4 个 advisor 用 1000 / 2000 / 3000 / 4000 这种“整千”编号,就是同一套惯例——每两个 advisor 之间留足空间,方便以后插队而不用改现有编号。

## 三、排序真正发生的位置(源码) `ChatClient.builder(…).defaultAdvisors(…)` 最终会把 advisor 列表交给 `DefaultAroundAdvisorChain.Builder`: ``java// org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain.Builderpublic Builder pushAll(List<? extends Advisor> advisors) {    ...    List<CallAdvisor> callAroundAdvisorList = advisors.stream()        .filter(a -> a instanceof CallAdvisor)        .map(a -> (CallAdvisor) a)        .toList();    callAroundAdvisorList.forEach(this.callAdvisors::push);    ...    this.reOrder();   // ← 每次 push 完都会重新排序    return this;} private void reOrder() {    ArrayList<CallAdvisor> callAdvisors = new ArrayList<>(this.callAdvisors);    OrderComparator.sort(callAdvisors);   // ← 按 getOrder() 升序排    this.callAdvisors.clear();    callAdvisors.forEach(this.callAdvisors::addLast);     // StreamAdvisor 走独立的一份 Deque,同样逻辑单独排序    ArrayList<StreamAdvisor> streamAdvisors = new ArrayList<>(this.streamAdvisors);    OrderComparator.sort(streamAdvisors);    ...}\` **\*\*结论\*\***:即使你在 \ChatClientConfig` 里把参数顺序写反—— ```java.defaultAdvisors(        outputGuardrailAdvisor,   // 写在最前面也没用        meteringAuditAdvisor,        promptInjectionAdvisor,        piiMaskingAdvisor)``` 最终执行顺序依然是 1000 → 2000 → 3000 → 4000,因为排序只看 `getOrder()`,跟传参顺序、Bean 注入顺序、`@Component` 扫描顺序完全无关。

`CallAdvisor` 和 `StreamAdvisor` 是****两条独立的 Deque****,分别排序。本项目目前只用 `CallAdvisor`(同步调用),README「六、下一步扩展」里提到的流式版本要额外实现 `StreamAdvisor`,会走另一条链。


## 四、链是怎么“走”起来的(洋葱模型) `DefaultAroundAdvisorChain` 内部就是一个 Deque<CallAdvisor>\ + 一个 `nextCall()`: ```javapublic ChatClientResponse nextCall(ChatClientRequest chatClientRequest) {    if (this.callAdvisors.isEmpty()) {        throw new IllegalStateException(“No CallAdvisors available to execute”);    }    var advisor = this.callAdvisors.pop();          // 弹出队首(当前最靠前的 order)    return advisor.adviseCall(chatClientRequest, this);  // 把「剩下的链」传给它自己}`` 关键在于:**\*\*advisor 自己决定要不要、什么时候调用 \chain.nextCall(request)`**。这就是洋葱模型——前置逻辑在调用 `chain.nextCall()` 之前执行,后置逻辑在它返回之后执行。 拿本项目 4 个 advisor 对号入座: | Advisor | order | pre(调用前) | 调 `chain.nextCall()` | post(返回后) ||—|—|—|—|—|| `PiiMaskingAdvisor` | 1000 | 脱敏最后一条 user message | ✅ | 无 || `PromptInjectionAdvisor` | 2000 | 命中规则则直接 `throw`,**不调用**** `nextCall` | 仅未命中时 ✅ | 无 || `MeteringAuditAdvisor` | 3000 | 记录 `start = nanoTime()` | ✅ | 计算耗时、写 Micrometer 指标、写 `COMPLETED` 审计记录 || `OutputGuardrailAdvisor` | 4000 | 无 | ✅(最内层,直接转发给模型) | 检查模型输出,命中违禁词则 `throw` | ### 4.1 请求方向(pre,按 order 升序) ```ChatClient.call()  └─ chain.nextCall()  pop 1000 → PiiMaskingAdvisor.adviseCall       │ 脱敏 user message       └─ chain.nextCall()  pop 2000 → PromptInjectionAdvisor.adviseCall            │ 检测注入;命中 → throw GuardrailException(链到此为止,模型未被调用)            └─ chain.nextCall()  pop 3000 → MeteringAuditAdvisor.adviseCall                 │ start = nanoTime()                 └─ chain.nextCall()  pop 4000 → OutputGuardrailAdvisor.adviseCall                      └─ chain.nextCall()  callAdvisors 已空 → 真正调用 ChatModel``` ### 4.2 响应方向(post,按 order **降序**——因为是从最内层往外「退栈」) ```ChatModel 返回  └─ OutputGuardrailAdvisor(4000)先拿到结果       │ 检查输出是否含 “system prompt” / “api key” 等违禁词;命中 → throw       └─ 返回给 MeteringAuditAdvisor(3000)            │ latencyMs = nanoTime() - start;写指标;audit.record(COMPLETED)            └─ 返回给 PromptInjectionAdvisor(2000)—— 无 post 逻辑,直接透传                 └─ 返回给 PiiMaskingAdvisor(1000)—— 无 post 逻辑,直接透传                      └─ 返回给调用方 ChatController``` **这就是为什么 `OutputGuardrailAdvisor` 的注释写“innermost, post 最先跑”**:它 order 最大、最后被 `pop`、离真实模型调用最近,所以模型一返回,它的后置逻辑第一个执行。如果它判定要拦截并 `throw`,异常会往外抛穿过 3000 层——`MeteringAuditAdvisor` 的 `chain.nextCall(request)` 那一行直接抛出异常,**它自己的 `audit.record(COMPLETED)` 那几行代码根本不会执行到**,所以被拦截的响应不会被误记成 `COMPLETED`(会被 `GlobalExceptionHandler` 记成 `BLOCKED_OUTPUT`)。

## 五、和 Servlet Filter / AOP 代理的关键区别 容易混淆的三个概念,分清楚面试不会被绕进去: | | 排序依据 | 链的实现方式 | 典型例子 ||—|—|—|—|| **本项目:Spring AI Advisor** | `getOrder()`(`Ordered` 接口),框架****构建时**排一次 | 手写 `Deque` + 递归 `nextCall()`,advisor 决定何时短路 | `PiiMaskingAdvisor` 等 4 个 || Spring AOP(`@Around` 通知) | `@Order` 注解 / `Ordered`,代理**创建时**排一次 | 动态代理 + `MethodInterceptor` 链,`ProceedingJoinPoint#proceed()` | `@Transactional`、自定义 `@Aspect` || Servlet `Filter` | `web.xml` / `FilterRegistrationBean` 注册顺序,**没有**** `getOrder()` | Servlet 容器维护的 `FilterChain`,`chain.doFilter()` | Spring Security 的 `FilterChainProxy` | 三者本质都是“洋葱模型”(before → 内层 → after),但排序****依据**、排序**时机**、链的**载体****都不同。Spring AI Advisor 复用了 `Ordered` 的语义(跟 AOP 一致),但链本身是 Spring AI 自己维护的一个 `Deque`,跟 Spring 容器的 Bean 生命周期、`BeanPostProcessor` 都没有关系——这也是为什么 `getOrder()` 能不依赖 Bean 装配顺序独立生效。

## 六、常见追问 / 陷阱 **Q:两个 advisor `getOrder()` 返回值相同会怎样?**`OrderComparator` 遇到相等值时退化为不保证顺序的相对稳定排序(依赖原始集合遍历顺序,`Deque` 用的是 `push` 插入序)。生产代码不应该依赖这种“意外稳定”,应该给不同职责的 advisor 明确错开编号——本项目留了 1000 的间隔正是为了避免这个坑。 **Q:`.defaultAdvisors(…)` 里传参顺序真的完全不重要吗?**对 `CallAdvisor`/`StreamAdvisor` 而言是的,`reOrder()` 每次 `push` 后都会重新按 `getOrder()` 排序。但为了代码可读性,[ChatClientConfig.java](src/main/java/com/kg2s/gateway/config/ChatClientConfig.java) 仍然按 order 从小到大的顺序书写参数并加注释——这是团队约定,不是框架要求。 **Q:`.advisors(a -> a.param(…))` ([ChatController.java:35](src/main/java/com/kg2s/gateway/web/ChatController.java#L35))会不会打乱这条链?**不会。那是运行时通过 `AdvisorSpec` 往 `ChatClientRequest` 的 context 里塞一个 `conversationId` 参数,不涉及 advisor 列表本身,`defaultAdvisors` 注册的链结构不受影响。 **Q:如果我想加一个「限流」advisor,该给它多大的 order?**取决于你想让它多早短路请求。想比 PII 脱敏更早拦截(省下脱敏的开销)就给它 < 1000 的值,比如 500;想在脱敏之后、注入检测之前跑就给 1500。原则:**越早需要短路请求(越可能拒绝掉这个请求)的 advisor,order 应该越小**,这样无效请求能尽早从链上退出,不浪费后面步骤的开销。

## 七、一图流总结 ``getOrder() 的值      ──决定──▶  DefaultAroundAdvisorChain.Builder.reOrder()                                  用 OrderComparator.sort() 排一次 Deque                                            │                                            ▼                          Deque<CallAdvisor>(升序:1000,2000,3000,4000)                                            │                        nextCall() 每次 pop() 队首 advisor 并调用它                                            │              advisor.adviseCall(request, chain) 内部自己决定:              ① 要不要在 pre 阶段短路(throw,不调 chain.nextCall)              ② 何时调用 chain.nextCall(request) 推进到下一个 advisor              ③ chain.nextCall() 返回之后要不要做 post 处理                                            │                          洋葱模型:pre 升序进入 → 真实模型调用                                    → post 降序退出(4000 先跑)\``