Spring AI 的 Advisor 机制,本质上就是 AI 调用链上的拦截器。
熟悉 Spring MVC 的小伙伴对 HandlerInterceptor 一定不会陌生——请求进来之前做预处理,响应出去之前做后处理。Advisor 干的是同一件事,只不过拦截的对象从 HTTP 请求变成了大模型调用。
这篇内容会从 Advisor 的链式执行机制讲起,再深入拆解求职派在它基础上做的两层改造。

自研 ReActAdvisor 替代了 Spring AI 内置的工具调用 Advisor,Middleware 插件体系实现了成本监控、日志审计等横切关注点。
读完就能搞清楚,一个生产级别的多 Agent 系统到底应该怎么在 AI 调用链上做拦截和增强。
01、Advisor 到底解决什么问题?
求职派同时对接了微信、钉钉、飞书三个渠道,每个渠道的消息最终都会流转到大模型进行推理。
如果没有 Advisor,每个业务 Agent 在调用大模型之前都需要自己处理一堆横切逻辑:加载对话历史、注入系统提示、记录调用日志、统计 Token 用量、计算费用。这些逻辑和业务无关,但每个 Agent 都得写一遍。

Advisor 把这些横切逻辑从业务代码中抽离了出来,变成了可插拔的拦截器组件。
Spring AI 提供了两套 Advisor 接口,分别对应同步和流式两种调用方式。
public interface CallAdvisor extends Advisor {
ChatClientResponse adviseCall(
ChatClientRequest request, CallAdvisorChain chain);
}
public interface StreamAdvisor extends Advisor {
Flux<ChatClientResponse> adviseStream(
ChatClientRequest request, StreamAdvisorChain chain);
}
ChatClientRequest 封装了请求参数,ChatClientResponse 封装了响应结果。每个 Advisor 拿到请求后,可以修改它、放行它、甚至直接阻断它返回自己构造的响应。
这个设计和 Servlet Filter 的链式调用如出一辙——调用 chain.nextCall(request) 就是把请求传给链上的下一个 Advisor,不调用就是阻断。
02、Advisor 链是怎么执行的?
Advisor 链采用的是栈式结构。

请求进来时,链首的 Advisor 最先处理;响应返回时,链尾的 Advisor 最先处理。和 Servlet Filter 的执行顺序完全一致。
完整的生命周期分为 6 步。
- 用户的提示词被封装成
ChatClientRequest,同时创建一个共享的上下文 Map,用来在调用链中传递信息 - 每个 Advisor 依次对请求进行处理,可以修改请求内容,也可以选择直接阻断请求并自己填充响应
- 如果请求一路没有被阻断,链条的最后一个 Advisor 会触发大模型调用
- 模型的响应结果沿着链条反向传递回来
- 链上的每个 Advisor 再依次对响应进行加工,分析、增强、过滤都可以
- 最终生成
ChatClientResponse返回给调用方

执行顺序由 getOrder() 方法决定,数值越小优先级越高。多个 Advisor 的 order 值相同时,执行顺序不确定,所以需要给每个 Advisor 设定不同的 order 值。
这套机制很清晰,Spring AI 也内置了一些常用的 Advisor,比如处理对话记忆的 MessageChatMemoryAdvisor、记录日志的 SimpleLoggerAdvisor。
但求职派在实际使用中发现,内置的工具调用 Advisor 满足不了需求。
03、为什么要自研 ReActAdvisor?
Spring AI 内置的 ToolCallAdvisor 负责处理工具调用(Function Calling),但它有一个根本限制——无法精细控制推理和执行的循环过程。
大模型在处理复杂任务时,往往需要多轮“推理 → 调用工具 → 拿到结果 → 再推理”的 ReAct 循环。比如用户问“帮我找一下北京的 Java 岗位,薪资 25k 以上”,模型可能需要经过这样的流程。
- 第一轮推理,判断需要调用岗位检索工具
- 执行工具,调岗位检索 API
- 第二轮推理,发现结果太多,需要进一步筛选
- 再次执行工具,用薪资条件过滤
- 第三轮推理,整理结果返回给用户
在这个过程中,求职派需要在每一轮工具调用前后插入自定义逻辑,比如记录日志、统计耗时和费用、注入计划提示。但 ToolCallAdvisor 把整个循环封装在了内部,留给开发者的扩展点几乎为零。

于是求职派自研了 ReActAdvisor,完全接管了推理和工具执行的循环控制。
源码文件在 core/src/main/java/com/git/hui/jobclaw/core/agent/react/ReActAdvisor.java,它同时实现了 CallAdvisor 和 StreamAdvisor 两个接口,同步和流式场景都能覆盖。
使用 ReActAdvisor 时有一个硬性前提,必须禁用 Spring AI 的自动工具执行。否则工具会被执行两遍,一遍是 Spring AI 自动执行的,一遍是 ReActAdvisor 手动执行的。
04、ReAct 循环的核心设计是什么?
ReActAdvisor 最精妙的设计在于对 Advisor 链的分层使用。
第一次推理走完整的 Advisor Chain,这样对话记忆 Advisor、日志 Advisor 等前置 Advisor 都能正常触发。后续每一轮迭代则直接调用 ChatModel,跳过 Advisor Chain,避免对话记忆被重复加载、日志被重复记录。
用一段源码来看这个分层设计。
// 第一次推理走 Advisor Chain(保留 Memory 等前置效果)
ChatClientResponse firstResponse;
try {
firstResponse = chain.nextCall(request);
} catch (RuntimeException e) {
notifyError(e, 0, chatId);
throw e;
}
这是 adviseCall 方法的入口,第一次推理调用了 chain.nextCall(request),走的是完整的 Advisor 链。对话记忆 Advisor 会在这个环节加载历史消息,日志 Advisor 会记录请求信息。
如果第一次推理的结果包含了工具调用,就进入 ReAct 循环。循环内部的后续推理走的是另一条路。
private ChatResponse callModelDirect(
List<Message> messages, ChatClientRequest request, String chatId) {
ToolCallback[] toolArray = resolveToolCallbacks(request);
ChatOptions opts = ToolCallingChatOptions.builder()
.internalToolExecutionEnabled(false)
.toolCallbacks(toolArray).build();
return chatModel.call(new Prompt(messages, opts));
}
callModelDirect 直接调用了 chatModel.call(prompt),完全绕过了 Advisor Chain。这个设计避免了一个很常见的 bug:如果后续迭代也走 Advisor Chain,对话记忆 Advisor 就会把历史消息再加载一遍,导致消息重复。

ReAct 循环的核心逻辑在 runReactLoop 方法中,每一轮迭代都是“执行工具 → 再次推理 → 检查是否还有工具调用”的三步循环。循环有一个安全阀——maxIterations 默认值是 10。如果模型反复调用工具超过 10 次还没得出最终结果,循环会强制终止。这个上限通过 Builder 模式配置,可以根据业务场景调整。
工具执行时还有一个容易被忽略的细节,就是 ToolContext 的传递。ReActAdvisor 参照了 Spring AI 内置的 DefaultToolCallingManager 的实现,从 ToolCallingChatOptions 中提取上下文信息,封装成 ToolContext 传入每一次工具调用。这样工具方法就能拿到用户 ID、对话信息等请求上下文。

这个分层设计解决了一个看似简单但实际很棘手的架构问题:既要让 Memory、Logger 等前置 Advisor 正常工作,又不能让它们在多轮 ReAct 循环中重复触发。
05、Middleware 怎么给 ReAct 循环装上“插件”?
ReActAdvisor 的第二个核心设计是 Middleware 接口。
Advisor 是调用链层面的拦截器,粒度是“整个调用”。但在 ReAct 循环内部,每一轮推理和工具执行都可能需要插入自定义逻辑。Middleware 提供的就是这个更细粒度的拦截能力。
ReActMiddleware 接口定义了 7 个生命周期钩子,覆盖了 ReAct 循环的每一个阶段。
setContext— 循环开始前,提取并缓存请求上下文beforeReasoning/afterReasoning— 每轮推理的前后beforeActing/afterActing— 每次工具执行的前后onComplete— 循环正常结束onError— 循环中发生异常
所有方法都有默认空实现,按需覆写即可。
求职派目前注册了 3 个 Middleware,各自负责一个横切关注点:
①、日志中间件在每一轮推理和工具执行前后记录详细日志,包括消息数量、推理结果文本、工具调用参数和返回值。开发阶段排查问题全靠它。
②、监控中间件是整个 Middleware 体系中最重的一个,它在 afterReasoning 钩子里完成了三件事——统计 Token 用量、计算调用费用、发布监控指标。
费用计算的核心逻辑如下。
private BigDecimal calculateCost(ModelConfig.ModelInfo info, Long input, Long output) {
BigDecimal cost = BigDecimal.ZERO;
if (input != null && info.getInputPricePerMillionTokens() != null)
cost = cost.add(info.getInputPricePerMillionTokens().multiply(BigDecimal.valueOf(input)));
if (output != null && info.getOutputPricePerMillionTokens() != null)
cost = cost.add(info.getOutputPricePerMillionTokens().multiply(BigDecimal.valueOf(output)));
return cost.divide(BigDecimal.valueOf(1_000_000), 8, RoundingMode.HALF_UP);
}
每一轮推理结束后,从模型响应的元数据中提取输入和输出 Token 数,乘以对应模型的单价,就得到了这一轮的费用。因为 ReAct 循环可能有多轮推理,监控中间件会累加每一轮的费用,在 onComplete 时发布整个调用的汇总统计。

③、计划提示中间件的用途比较特殊,它在 beforeReasoning 钩子中注入当前任务的计划提示,让模型在推理时能参考到用户正在进行的计划信息。
这三个 Middleware 都标注了 @Component,Spring 容器启动时会自动注册为 Bean。ReActAdvisor 在构建时调用 autoInjectMiddleware() 方法,通过 Spring 的 getBeansOfType(ReActMiddleware.class) 自动发现并注入所有 Middleware 实例:
public Builder autoInjectMiddleware() {
var middlewareBeans = SpringUtil.getContext()
.getBeansOfType(ReActMiddleware.class);
middlewareBeans.values().forEach(this::addMiddleware);
return this;
}
新增一个横切关注点,只需要实现 ReActMiddleware 接口并标注 @Component,不需要修改 ReActAdvisor 的任何代码。
06、三层 LlmCaller 怎么组装 Advisor 链?
求职派的 LLM 调用封装分为三层,每一层注册的 Advisor 组合不同。
最简层只注册了 ReActAdvisor 一个 Advisor。它没有对话记忆、没有日志记录,适用于不需要上下文的一次性调用场景,比如意图分类、文本提取这类“问一句答一句”的任务。
var builder = ChatClient.builder(chatModel)
.defaultOptions(ToolCallingChatOptions.builder()
.internalToolExecutionEnabled(false).build())
.defaultAdvisors(
ReActAdvisor.builder().chatModel(chatModel)
.autoInjectMiddleware().build()
);
注意 internalToolExecutionEnabled(false) 这一行,这是使用 ReActAdvisor 的前提,禁用了 Spring AI 的自动工具执行,把工具调用的控制权完全交给了 ReActAdvisor。
标准层注册了三个 Advisor,组成了完整的拦截链。
.defaultAdvisors(
reactBuilder.build(),
SimpleLoggerAdvisor.builder().build(),
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
ReActAdvisor 负责 ReAct 循环,日志 Advisor 负责记录请求响应,对话记忆 Advisor 负责加载和保存对话历史。这个组合覆盖了大部分业务 Agent 的需求,岗位检索、岗位推荐、身份采集都用的这一层。

这里有个细节——求职派用的对话记忆 Advisor 不是 Spring AI 的原版,而是一个改进版本。原版在加载历史消息时会和当前请求的消息产生重复,改进版做了去重处理,并且确保系统消息始终排在消息列表的第一位。
调用时还会通过 advisors() 方法传入会话 ID,通过 toolContext() 方法传入用户信息和消息内容。会话 ID 用于从 ChatMemory 中加载正确的对话历史,而 toolContext 里的用户信息会一路传递到 ReActAdvisor 的工具执行环节,最终被工具方法通过 ToolContext 参数读取。
这个设计让业务 Agent 的代码非常干净,只需要关注对话逻辑本身。记忆管理、日志记录、成本监控、工具调用控制这些横切关注点,全部由 Advisor 链和 Middleware 体系自动处理了。
如何把求职派写到简历上?
项目名称:求职派(JobClaw)— 多 Agent 智能求职系统
项目简介:基于 Spring AI 构建的多 Agent 求职系统,对接微信、钉钉、飞书三个 IM 渠道,通过意图识别和 Agent 路由实现岗位检索、岗位推荐、身份采集等业务场景。
技术栈:Java 21、Spring Boot 4.0、Spring AI 2.0、LangGraph4J、React 19 / Next.js 15
核心职责:
- 基于 Spring AI Advisor 机制设计三层 LLM 调用封装(简单调用 / 业务 Agent / 用户偏好),通过链式拦截器实现对话记忆管理、请求日志记录、调用成本监控等横切关注点的统一处理
- 自研 ReActAdvisor 替代 Spring AI 内置的工具调用 Advisor,实现推理-执行循环的精细控制,首次推理走完整 Advisor Chain 保留记忆效果、后续迭代直接调用模型避免重复触发,最大迭代次数可配置
- 设计 Middleware 插件体系,定义 7 个生命周期钩子覆盖 ReAct 循环全阶段,通过 Spring Bean 自动发现机制实现热插拔扩展,已实现日志审计、成本监控、计划提示注入 3 个中间件
- 实现 LLM 调用成本实时监控,从模型响应元数据提取 Token 用量,结合供应商单价计算费用,通过 Micrometer 发布请求计数、延迟分布、Token 消耗、预估成本 4 类监控指标
- 改进 Spring AI 的对话记忆 Advisor,解决了历史消息与当前请求消息重复加载的问题,并确保系统消息在消息列表中的优先排序
ending
【Spring AI 的 Advisor 机制本身并不复杂,复杂的是在生产环境中如何正确地使用它。】
求职派的做法是把 Advisor 分成了两层——外层是调用链级别的 Advisor(Memory、Logger),内层是 ReAct 循环级别的 Middleware(Monitor、Logging、PlanHint),各管各的,互不干扰。
技术框架提供的是接口和契约,真正的设计能力体现在你怎么在这些接口上构建出符合业务需要的架构。
加油吧,兄弟姐妹们。
下期见。
回复