版本:Spring AI 2.0.1
目标:弄清 ChatClient 上的 Advisor 链——契约、挂法、内置门牌、顺序语义,以及如何自定义。 本章只讲「链」本身;会话记忆、RAG、Tool 等具体装配不在这里展开。

上图按「由外到内」画出一条完整示意链:安全 → 记忆 → 检索/缓存(可选)→ 工具循环 → 结构化校验(可选)→ ChatModel → 响应。不是每次请求都挂满;检索与结构化校验按场景选配。
可以把 Around 链理解成:
请求进入
→ Advisor 链(按 getOrder(),数字越小越靠外)
→ 前置 before:改请求 / 注入上下文 / 安全检查 / 读记忆 / 检索 / 日志 …
→ 末端:ChatModelCallAdvisor / ChatModelStreamAdvisor 真正调模型
→ 后置 after:写回记忆 / 结构化校验 / 累加 Usage / 写外部系统 …
响应离开
ToolCallingAdvisor 比较特殊:模型一旦要求调工具,它会在链内循环,直到不再需要工具,或触达调用上限。多轮 tool 的 Usage 通常按整次 ChatClient 调用累计。谁先谁后看 getOrder(),默认数值见后文顺序图。
本章顺序:契约与挂法 → 内置门牌 → 顺序与栈语义 → 自定义与调试 → 常见坑。
6.1 Advisor 是什么
Advisor 是 ChatClient 请求链上的可插拔环节。一次 call() / stream() 不会直接落到 ChatModel,而是先穿过这条链。
常见分层:
| 类型 | 作用 |
|---|---|
CallAdvisor / StreamAdvisor | 同步与流式两条路径上的顾问接口 |
BaseAdvisor | 提供默认的 adviseCall / adviseStream,你只需实现 before / after |
MemoryAdvisor / ToolAdvisor | 记忆类、工具类的标记与契约 |
DefaultAroundAdvisorChain | 默认链式实现 |
挂法在 ChatClient 上很直接:
// Builder 默认:对该 Client 的所有请求生效
.defaultAdvisors(new SimpleLoggerAdvisor(), safeGuardAdvisor)
// 单次请求追加挂载
.advisors(anotherAdvisor)
// 单次请求传上下文参数(键由具体 Advisor 约定)
.advisors(a -> a
.advisors(someAdvisor)
.param("my.context.key", value))
defaultAdvisors 与单次 advisors(...) 会一起进入本次请求的链;真正谁先谁后,仍看各自的 getOrder(),不是「谁后写谁更外」。
先记住四点:
-
Advisor 改的是「这一次请求」的前后处理,不是另一套协议;底下仍是 Message / Prompt / ChatResponse。
-
同步与流式是分开的路径;只实现 Call 或只实现 Stream,另一条路径不会自动带上。自定义优先用
BaseAdvisor一次覆盖两边。 -
顺序由
getOrder()决定,不要依赖 Bean 注册顺序碰运气。 -
上下文参数走
advisors(a -> a.param(...)),键名由具体 Advisor 约定(例如记忆类会约定会话键)。
BaseAdvisor 的执行骨架:
before(request)
→ chain.next...(request)
→ after(response)
6.2 常见内置 Advisor

ToolCallingAdvisor ChatClient 默认常会装配,负责工具调用循环。旧名 ToolCallAdvisor 已废弃,新代码只写这个。默认 order 约为 HIGHEST_PRECEDENCE + 300。
MessageChatMemoryAdvisor 基于 ChatMemory 的会话记忆:调用前注入历史,调用后写回本轮消息。需要会话键等上下文参数。默认 order 约为 HIGHEST_PRECEDENCE + 200(比 ToolCalling 更靠外,从而包住工具循环)。
SafeGuardAdvisor 敏感词 / 安全拦截。可与 ModerationModel 组合:一个做词表快筛,一个做模型审核。
SimpleLoggerAdvisor 调试时看最终发给模型的内容。生产环境注意脱敏。
StructuredOutputValidationAdvisor 在 Advisor 层做结构化输出校验,与 entity(...).validateSchema() 一类能力相关。默认更靠内(接近末端)。
ChatModelCallAdvisor / ChatModelStreamAdvisor 链的末端,真正执行 call / stream。order 通常是 LOWEST_PRECEDENCE。一般不用手写,理解链路时知道「最后一环是谁」即可。
另外还有辅助类,例如按 token 预算裁剪上下文的 content purger,以及工具循环场景下的 Usage 累计器。
扩展门牌(本章只认名字与职责):
| Advisor | 大致职责 |
|---|---|
QuestionAnswerAdvisor | 经典向量库 RAG:检索文档并注入 Prompt |
RetrievalAugmentationAdvisor | Modular RAG:查询改写 / 扩展 / 检索 / 拼接可组合 |
SemanticCacheAdvisor | 相似问题命中后短路模型调用(偏省钱加速,不是「补知识」) |
ToolSearchToolCallingAdvisor | 工具很多时先搜目录再调用;小项目通常直接 defaultTools 即可 |
VectorStoreChatMemoryAdvisor | 用向量库做语义相关的历史召回(与窗口 ChatMemory 不是同一条路径) |
有些示例名容易误导。例如某些 ReReading 示例可能只出现在测试代码里,不要当成生产内置 Advisor 去依赖。
6.3 顺序为什么重要

Around 链按 order 排序:数字越小越靠外;末端永远是真正调模型的 Advisor。
一个实用分层(从外到内)可以是:
-
安全(SafeGuard)
-
记忆读取 / 写回
-
检索或语义缓存
-
工具调用 / Tool Search
-
结构化校验
-
ChatModel 调用
和源码默认值对齐时,至少记住这两档:
| Advisor | 常见默认 order(约) |
|---|---|
MessageChatMemoryAdvisor | HIGHEST_PRECEDENCE + 200 |
ToolCallingAdvisor | HIGHEST_PRECEDENCE + 300 |
ChatModelCallAdvisor | LOWEST_PRECEDENCE |
也就是说:记忆默认包在工具循环外面——工具多轮中间态由 ToolCalling 自己管,会话级历史由记忆 Advisor 在整次调用前后读写。
调试日志可插在合适位置,用来观察最终 Prompt。
再记住三个栈语义:
-
短路合法:例如语义缓存命中后,可以不再往内走到 RAG / Tool / 模型。
-
循环合法:ToolCalling、结构化校验可能多次触达模型;Usage 按整次 ChatClient 调用看。
-
覆盖危险:多个 Advisor 都在
before里改 system / user 时,后执行的前置逻辑可能改掉先注入的内容。
自定义 Advisor 务必给出稳定的 getOrder()。流式路径与同步路径是分开的;某些校验重试明确不支持 streaming——要重试就走同步,要打字机就流式,结构化可另开接口。
6.4 自定义 Advisor
自定义时优先实现 BaseAdvisor。最小可运行骨架:
import java.util.HashMap;
import java.util.Map;
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;
import org.springframework.core.Ordered;
public final class TenantTagAdvisor implements BaseAdvisor {
public static final String TENANT_KEY = "tenant.id";
@Override
public ChatClientRequest before(ChatClientRequest request, AdvisorChain chain) {
Object tenantId = request.context().get(TENANT_KEY);
if (tenantId == null) {
return request;
}
// 往 context 写入(也可 mutate Prompt)
Map<String, Object> ctx = new HashMap<>(request.context());
ctx.put("tenant.resolved", Boolean.TRUE);
return request.mutate().context(ctx).build();
}
@Override
public ChatClientResponse after(ChatClientResponse response, AdvisorChain chain) {
// 后置:写审计、补 metadata 等
return response;
}
@Override
public int getOrder() {
// 比记忆(约 +200)更靠外的示例
return Ordered.HIGHEST_PRECEDENCE + 100;
}
}
请求时传上下文:
chatClient.prompt()
.advisors(a -> a.param(TenantTagAdvisor.TENANT_KEY, "acme"))
.user("你好")
.call()
.content();
挂到 Client:
@Bean
ChatClient chatClient(ChatClient.Builder builder, TenantTagAdvisor tenantTagAdvisor) {
return builder
.defaultAdvisors(tenantTagAdvisor, new SimpleLoggerAdvisor())
.build();
}
先确认官方是否已有现成实现,再决定是否自研。
多个 Advisor 叠在一起时,上下文会被一起吃掉。实务上要同时考虑:模型上下文窗口、记忆 / 检索注入量、工具循环轮次。三项一起涨,延迟和成本通常也会一起涨。
6.5 观察与调试提示
-
开发期可临时挂
SimpleLoggerAdvisor,确认最终 Prompt 是否符合预期。 -
生产打开 prompt / completion 日志前,先做脱敏与采样,避免密钥与隐私落盘。
-
Advisor 层有独立 Observation 挂钩;换 Advisor 实现时,尽量不要丢掉观测约定。
-
排查「某个 Advisor 没生效」时,先看是否挂上、
getOrder()是否被盖住、同步 / 流式是否走了对应路径。
6.6 常见坑(本章范围)
| 现象 | 先查什么 |
|---|---|
| Advisor「挂了但没反应」 | 是否 defaultAdvisors / 单次 advisors 写错对象;Bean 是否注入成功 |
| 行为与注册顺序不一致 | 是否依赖了注册顺序;getOrder() 是否稳定、是否冲突 |
| 记忆没包住工具循环 | 记忆 order 是否仍小于 ToolCalling(默认 +200 vs +300) |
| 流式下校验 / 写回异常 | 该 Advisor 是否支持 streaming;是否应改走同步路径 |
| Prompt 被后挂的 Advisor 改掉 | 多个 Advisor 是否都改 system / user;对照 order 看谁后执行 |
| 工具循环 Usage 对不上单次 call | Usage 常按整次 ChatClient 调用累计,含多轮 tool |
| 日志里出现敏感原文 | SimpleLoggerAdvisor / Observation 是否记录了 prompt / completion |
| 只实现了 CallAdvisor | 流式请求不会走它;需要 Stream 路径或改用 BaseAdvisor |
6.7 本章小结
到这里应能:
-
说明 Advisor 链的前置、末端与后置,以及它与 Message / Prompt / ChatResponse 的关系
-
用
defaultAdvisors/advisors/param正确挂载,并知道顺序看getOrder() -
理解记忆包住工具循环的默认 order 关系,以及短路、循环、覆盖风险
-
认清常见内置 Advisor 与扩展门牌各自干什么
-
用
BaseAdvisor写出最小自定义实现(含 context),并分开对待同步 / 流式路径
1550



被折叠的 条评论
为什么被折叠?



