2391 字
约 7 分钟
4
Spring AI Advisor:多 Agent 系统的调用链增强设计

怎么把记忆、日志、成本监控、工具调用这些非业务逻辑,从 Agent 代码里彻底抽出去。

Spring AI Advisor:多 Agent 系统的调用链增强设计

做一个简单的 AI Demo:

chatClient.prompt()
        .user(message)
        .call()
        .content();

就够了。

但生产环境里的 Agent,在真正调用模型之前和之后,还要处理很多事情:

加载聊天历史
记录请求日志
统计 Token
计算模型费用
调用工具
权限校验
异常处理
任务上下文注入

这些逻辑并不属于“岗位推荐”“简历优化”这样的业务本身。

如果每个 Agent 都自己实现一遍,代码很快就会失控。

Spring AI 的 Advisor,就是用来解决这个问题的。


1. Advisor 是什么?

可以把 Advisor 理解成:

LLM 调用链上的拦截器。

和 Spring MVC 的 Interceptor、Servlet 的 Filter 非常像。

正常调用:

User
 ↓
ChatClient
 ↓
LLM

加入 Advisor:

User
 ↓
Logger Advisor
 ↓
Memory Advisor
 ↓
ReAct Advisor
 ↓
LLM

每个 Advisor 都可以在模型调用之前和之后执行逻辑。

基本结构类似:

ChatClientResponse adviseCall(
        ChatClientRequest request,
        CallAdvisorChain chain) {

    // 请求前处理

    ChatClientResponse response =
            chain.nextCall(request);

    // 响应后处理

    return response;
}

这里最关键的是:

chain.nextCall(request)

它表示:

把请求继续交给下一个 Advisor。

如果不调用它,也可以直接结束整个调用。


2. Advisor Chain 怎么执行?

Advisor 是链式执行的。

假设有三个 Advisor:

A → B → C → LLM

请求进入时:

A before
 ↓
B before
 ↓
C before
 ↓
LLM

模型返回以后顺序反过来:

LLM
 ↓
C after
 ↓
B after
 ↓
A after

所以它本质是一个:

责任链
+
洋葱模型

不同 Advisor 就可以分别负责:

Memory
Logging
ReAct
Tracing
Security

而业务 Agent 完全不用关心。


3. 为什么 Memory 很适合做 Advisor?

用户:

帮我找武汉 Java 岗位

接着:

25K 以上的

第二句话必须依赖第一句话。

MessageChatMemoryAdvisor 就可以在请求模型之前:

读取 ChatMemory
 ↓
加载历史消息
 ↓
和当前消息合并
 ↓
发送给 LLM

模型回答以后再:

保存 User Message
+
保存 Assistant Message

所以这里一定要区分:

ChatMemory
负责保存聊天记录

Memory Advisor
负责把聊天记录加入模型调用

这就是 Advisor 最大的价值之一:

让业务代码感知不到记忆系统的存在。


4. 为什么还要自己写 ReActAdvisor?

Spring AI 可以自动执行 Tool Calling。

但是复杂 Agent 往往不是:

LLM
→ Tool
→ 完成

而是:

Reason
 ↓
调用岗位搜索
 ↓
Observation
 ↓
继续 Reason
 ↓
调用岗位详情
 ↓
Observation
 ↓
继续 Reason
 ↓
最终回答

这就是 ReAct:

Reason
→ Act
→ Observation
→ Reason

生产环境通常还需要控制:

每一轮用了多少 Token
工具执行多久
工具执行前是否有权限
最多循环多少次
某一轮失败怎么处理
整个调用花多少钱

如果工具执行全部封装在框架内部,这些细节就很难控制。

因此可以关闭自动工具执行:

internalToolExecutionEnabled(false)

然后自己实现:

ReActAdvisor

由它完全接管:

推理
→ 工具执行
→ 再推理
→ 再执行

5. ReActAdvisor 最值得学的设计

这里最关键的设计不是循环本身,而是:

第一次推理走完整 Advisor Chain,后面的 ReAct 推理直接调用 ChatModel。

第一次:

User
 ↓
Logger
 ↓
Memory
 ↓
ReActAdvisor
 ↓
LLM

这样 Memory Advisor 能正常加载历史。

如果模型要求调用工具:

执行 Tool
 ↓
Tool Result
 ↓
ChatModel.call()
 ↓
继续推理

后面的循环不再重新走:

Memory Advisor
Logger Advisor
...

为什么?

假设每一次 ReAct 都重新经过 Memory Advisor:

第一次:
历史 + 当前消息

第二次:
又加载历史
+
上一次已有历史

很容易出现:

消息重复
上下文膨胀
日志重复
Token 浪费

所以更合理的设计是:

第一次
走完整基础设施链

后续 ReAct
只维护当前推理上下文

这是这篇最值得沉淀的地方之一。


6. ReAct 循环要有安全阀

模型有可能出现:

调用 A
↓
调用 B
↓
再次调用 A
↓
再次调用 B
↓
……

理论上可能一直循环。

因此必须:

maxIterations = 10;

最终逻辑类似:

第一次 Reason

while 有 Tool Call:

    if iterations > max:
        break

    执行 Tool

    加入 Tool Result

    再次 Reason

所以生产级 Agent 一定要记住:

所有 Agent Loop 都必须存在最大迭代次数。

不能完全相信模型自己知道什么时候结束。


7. Advisor 和 Middleware 为什么要分两层?

这是整篇第二个非常值得学习的设计。

Advisor 拦截的是:

一次完整 LLM 调用

但是 ReAct 里面可能发生:

Reason 1
Tool 1
Reason 2
Tool 2
Reason 3

如果我们想监控每一轮,就需要比 Advisor 更细的粒度。

因此内部再设计:

ReActMiddleware

架构变成:

                 Advisor Chain
                      │
        ┌─────────────┼─────────────┐
        ↓             ↓             ↓
     Memory         Logger       ReActAdvisor
                                     │
                                     ↓
                              ReAct Middleware
                                     │
                         ┌───────────┼───────────┐
                         ↓           ↓           ↓
                      Logging     Monitor     PlanHint

简单理解:

Advisor
管“整次调用”

Middleware
管“ReAct 内部每一轮”

这个职责划分非常清楚。


8. Middleware 可以有哪些生命周期?

一个 ReAct Loop 可以抽象成:

Loop Start
   ↓
Before Reasoning
   ↓
LLM Reasoning
   ↓
After Reasoning
   ↓
Before Acting
   ↓
Tool Execution
   ↓
After Acting
   ↓
……
   ↓
Complete / Error

这样就可以插入不同能力。

例如日志 Middleware:

记录这一轮模型输出
记录 Tool 名称
记录参数
记录 Tool Result

监控 Middleware:

Input Tokens
Output Tokens
Latency
Cost

Plan Middleware:

在每轮 Reasoning 前
注入当前执行计划

这样新增监控能力时,不需要修改 ReActAdvisor。

只需要新增:

@Component
class MyMiddleware implements ReActMiddleware {
}

然后自动发现。

这其实就是:

插件化架构。


9. 为什么 Token 和 Cost 也应该放 Middleware?

ReAct 和普通聊天最大的不同是:

一次用户请求可能调用模型很多次。

例如:

用户请求
   ↓
Reason 1 → 1000 tokens
   ↓
Tool
   ↓
Reason 2 → 800 tokens
   ↓
Tool
   ↓
Reason 3 → 600 tokens

用户只发送了一条消息,但实际上:

LLM 调用了 3 次

所以真正成本应该是:

Total Cost
=
Reason1 Cost
+
Reason2 Cost
+
Reason3 Cost

这时候就在:

afterReasoning()

里读取:

input tokens
output tokens
model price

计算:

cost =
inputTokens × inputPrice
+
outputTokens × outputPrice

最终在:

onComplete()

汇总。

所以:

Agent 成本监控必须看整个 ReAct Loop,而不是只统计最外层的一次请求。


10. ToolContext 解决什么问题?

Tool 参数有两类。

模型决定的:

keyword
city
salary
jobName

应用决定的:

userId
tenantId
conversationId
权限
当前消息

后面这些不能让 LLM 自己生成。

因此调用时:

.toolContext(Map.of(
    "userId", userId,
    "conversationId", conversationId
))

ReActAdvisor 在真正执行工具时继续把它传下去:

ChatClient
 ↓
ReActAdvisor
 ↓
ToolContext
 ↓
Tool

最终工具:

@Tool
String createTask(
        String taskName,
        ToolContext context) {

    String userId =
        context.getContext().get("userId");
}

所以:

ToolContext 是业务上下文进入 Tool Execution 的安全通道。


11. 最终调用架构

把整个设计压缩一下:

                        Agent
                          ↓
                      LlmCaller
                          ↓
                     ChatClient
                          ↓
                  Advisor Chain
               ┌──────────┼──────────┐
               ↓          ↓          ↓
            Memory      Logger   ReActAdvisor
                                     ↓
                              ReAct Loop
                                     ↓
                           Middleware Chain
                       ┌────────┼─────────┐
                       ↓        ↓         ↓
                    Logging   Monitor   Plan
                       │
                       ↓
                 Reason → Tool
                       ↑     ↓
                       └─────┘
                       ↓
                  Final Answer

这样业务 Agent 最后可能只剩:

llmCaller.call(user, prompt);

剩下:

记忆
日志
工具循环
成本
监控
计划

全部属于基础设施层。


12. 这篇真正应该沉淀什么?

核心可以压缩成这几个概念:

Advisor
→ LLM 调用级拦截器

Advisor Chain
→ 横切能力责任链

ReActAdvisor
→ 控制 Reason + Tool 循环

Middleware
→ ReAct 内部生命周期插件

ToolContext
→ 给 Tool 传业务上下文

maxIterations
→ 防止 Agent 死循环

Monitoring
→ 每轮统计 Token / Cost

其中最重要的是这个分层:

外层 Advisor
负责:
Memory / Logger / ReAct

内层 Middleware
负责:
Reason / Tool / Cost / Monitor

最值得记住的一句话

Advisor 解决的是“整次 AI 调用怎么增强”,Middleware 解决的是“Agent 每一轮 ReAct 怎么增强”。

最终架构思想就是:

业务逻辑
不要关心
Memory
Logging
Monitoring
Tool Loop
Cost

而应该:

Agent
只负责业务

Advisor
负责调用链横切能力

ReActAdvisor
负责 Agent 循环

Middleware
负责循环内部监控和增强

把它和你前面几篇串起来,就更加完整了:

Model Routing
→ 用哪个模型

ChatMemory
→ 记住什么

ChatClient
→ 怎么组织调用

Advisor
→ 调用过程怎么增强

Function Call
→ 怎么调用工具

ReAct
→ 怎么连续推理和使用工具

MCP
→ 外部工具怎么标准化接入

Structured Output
→ 最终结果怎么变成 Java 对象
Spring AI Advisor:多 Agent 系统的调用链增强设计
http://www.clxhxhhr.top/posts/661/
作者
clxstart
发布于
2026-09-17
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。