📌 系列文章
- Spring AI Alibaba学习(一)------ RAG
- Spring AI Alibaba 学习(二):Agent 智能体架构深度解析
- Spring AI Alibaba 学习(三):Graph Workflow 深度解析(上篇)
- Spring AI Alibaba 学习(三):Graph Workflow 深度解析(下篇)
- Spring AI Alibaba 学习(四):会使用工具的才能为Agent(本文)
目录
[📌 系列文章](#📌 系列文章)
[📖 前言](#📖 前言)
[一、 什么是工具调用链路?🎯](#一、 什么是工具调用链路?🎯)
[工具调用 vs 传统函数调用](#工具调用 vs 传统函数调用)
[2.1 阶段 1:工具注册与发现(Tool Registration & Discovery)](#2.1 阶段 1:工具注册与发现(Tool Registration & Discovery))
[2.1.1 工具是什么?](#2.1.1 工具是什么?)
[2.1.2 工具元数据(ToolDefinition)](#2.1.2 工具元数据(ToolDefinition))
[2.1.3 工具注册流程](#2.1.3 工具注册流程)
[2.2 阶段 2:工具选择(Tool Selection)](#2.2 阶段 2:工具选择(Tool Selection))
[2.2.1 LLM 如何决定调用哪个工具?](#2.2.1 LLM 如何决定调用哪个工具?)
[2.2.2 工具描述的重要性](#2.2.2 工具描述的重要性)
[2.3 阶段 3:工具调用请求构建(Tool Call Request)](#2.3 阶段 3:工具调用请求构建(Tool Call Request))
[2.3.1 ToolCallRequest 的结构](#2.3.1 ToolCallRequest 的结构)
[2.3.2 参数解析与验证](#2.3.2 参数解析与验证)
[2.3.3 ToolContext 的传递](#2.3.3 ToolContext 的传递)
[2.4 阶段 4:工具执行(Tool Execution)](#2.4 阶段 4:工具执行(Tool Execution))
[2.4.1 AgentToolNode 的执行流程](#2.4.1 AgentToolNode 的执行流程)
[2.4.2 拦截器链(InterceptorChain)](#2.4.2 拦截器链(InterceptorChain))
[2.5 阶段 5:工具响应处理(Tool Response)](#2.5 阶段 5:工具响应处理(Tool Response))
[2.5.1 ToolCallResponse 的结构](#2.5.1 ToolCallResponse 的结构)
[2.5.2 结果返回给 LLM](#2.5.2 结果返回给 LLM)
[2.5.3 循环决策](#2.5.3 循环决策)
[三、Hook 机制详解🔗](#三、Hook 机制详解🔗)
[3.1 Hook 的执行时机](#3.1 Hook 的执行时机)
[3.2 ToolInjection - 工具注入](#3.2 ToolInjection - 工具注入)
[3.3 ToolCallLimitHook - 限制工具调用次数](#3.3 ToolCallLimitHook - 限制工具调用次数)
[四、 关键要点总结💡](#四、 关键要点总结💡)
[4.1 五个阶段的核心职责](#4.1 五个阶段的核心职责)
[4.2 Hook vs Interceptor](#4.2 Hook vs Interceptor)
📖 前言
上回说到,我们学习了用Graph Workflow搭建属于我们自己的一套工作流,甚至还了解了DeepResearch机制------一种对于人类学习模式参考执行(思考-规划-执行),于是我们能够把这条思维链路串联起来,但在真实的业务或者说在现实生活中Agent之所以不是像LLM那样的ChatBot,不单单有脑子,还有无形而迅猛的手,正是因为他们具备稳定、可控地调用工具的行为。
像人类在演化的过程中使用工具行为一样,工具赋予了LLM从空想者变成了一位实干家,让LLM能够有改变世界的能力。相信你和我一样存在下面这些疑问?那么工具是如何调用的?怎么保证稳定调用?工具出现异常情况了该怎么办?那么请听我为你娓娓道来~
本文将涵盖:
- ✅ Tool Calling 的整体架构:从 LLM 决策到工具执行再到结果回流的完整闭环
- ✅ 工具调用的五大阶段:注册、选择、请求构建、执行、响应处理
- ✅ AgentToolNode源码拆解:看看工具调用在 Spring AI Alibaba(下简称"SAA")中到底是怎么跑起来的
- ✅ Hook 与 Interceptor 机制:如何在链路中插入限流、重试、日志、容错等能力
- ✅ ToolContext上下文传递:状态、配置、线程信息如何跨节点注入到工具中
- ✅ 一套可落地的实践思路:让你的 Agent 工具调用从"能用"走向"稳定可运维"
一、 什么是工具调用链路?🎯
工具调用链路是 Agent 从决策调用工具 到获得工具结果 的完整过程。这是 ReAct 模式中 Acting(行动) 阶段的核心实现。
工具调用 vs 传统函数调用
可能大家会混淆,甚至和博主初学Agent Tools都有一个共识,什么Tools,不就是一个函数、方法吗?有必要吹嘘得这么玄乎吗?

这显然就是一个误区。工具似函数,亦非函数。函数负责"执行",代表的是一次单次执行的行为! 而 Agent Tool 负责"决策 + 执行 + 回传 + 再决策"------它活在一条完整链路里,不是单次调用。所以 Tools 看着像函数,骨子里却是一个可编排、可治理、可观测的能力节点。
传统函数调用与工具调用对比
二、工具调用的生命周期阶段🏗️
如果把 Agent 看成一个会思考的"项目经理",那 Tool Calling 就是它把决策落地成行动的执行流水线。同好们可能会把工具调用理解为"调用一个方法然后拿结果回来",但在真实链路里,工具调用远不止执行本身:它还包含工具发现、模型选择、参数组装、链路拦截、结果回流以及下一步决策。

也正因为如此,我们需要用"生命周期"的视角来理解 Tool Calling。从"LLM 产生调用意图"到"工具结果影响下一轮推理",每个阶段都有明确职责,并且对应着源码中的关键组件。下面这张图,展示了工具调用在 SAA 中的完整阶段划分。你可以先有全局感,再跟着后文逐段拆解每个阶段的实现细节与设计意图:

2.1 阶段 1:工具注册与发现(Tool Registration & Discovery)
2.1.1 工具是什么?
在 SAA 中,工具由**ToolCallback** 接口定义:
java
public interface ToolCallback {
/**
* 获取工具的元数据定义
*/
ToolMetadata getToolMetadata();
/**
* 执行工具
* @param arguments 工具参数(JSON 格式)
* @param context 工具上下文
* @return 工具执行结果
*/
String call(String arguments, ToolContext context);
}
2.1.2 工具元数据(ToolDefinition)
每个工具都需要定义元数据,告诉 LLM 这个工具是什么、怎么用。元数据不是给人看的,是给模型做决策和给框架做路由用的。
java
public class ToolDefinition {
// 工具名称(LLM 用这个名字调用工具)
private String name;
// 工具描述(LLM 用这个理解工具的功能)
private String description;
// 工具参数的 JSON Schema
private Map<String, Object> inputSchema;
// 是否直接返回结果(不再调用 LLM)
private boolean returnDirect;
}
小🌰:天气查询工具
java
ToolDefinition weatherTool = ToolDefinition.builder()
.name("get_weather") // 填充上工具的名称
.description("查询指定城市的实时天气信息") // 描述LLM在什么时机情况下去调用
.inputSchema(Map.of( // 配置输入
"type", "object",
"properties", Map.of(
"city", Map.of(
"type", "string",
"description", "城市名称,如:北京、上海"
)
),
"required", List.of("city")
))
.returnDirect(false) // 直接返回结果,不再调用 模型
.build();
🌰参数对照表
|-----------------|-----------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 参数字段名 | 含义 | 传参要求 |
| name | 工具唯一名称。 LLM 在生成 tool_call 时会使用这个名字,执行层也靠它定位到对应工具。 | 要求:稳定、唯一、语义清晰(建议用动词开头,如 get_weather、search_docs)。 |
| description | 工具描述,给 LLM 的"使用说明"。 模型主要根据它判断"该不该调用这个工具"。 | 需要写清适用场景 + 不适用场景 + 参数语义。 |
| inputSchema | 输入参数规范(JSON Schema)。 告诉 LLM 参数结构、类型、必填项,约束模型生成合法参数。 | 在这个示例里: type: "object":参数整体是一个对象 properties:对象里可用的字段定义 city.type: "string":city 必须是字符串 city.description:提示模型该字段怎么填(如"北京、上海") required: ["city"]:city 是必填参数 |
2.1.3 工具注册流程

代码示例:注册工具
java
public class WeatherTool implements ToolCallback {
@Override
public ToolMetadata getToolMetadata() {
return ToolMetadata.builder()
.name("get_weather")
.description("查询指定城市的实时天气信息")
.inputSchema(Map.of(
"type", "object",
"properties", Map.of(
"city", Map.of(
"type", "string",
"description", "城市名称"
)
),
"required", List.of("city")
))
.returnDirect(false)
.build();
}
@Override
public String call(String arguments, ToolContext context) {
Map<String, Object> args = JsonUtils.parseJson(arguments);
String city = (String) args.get("city");
String weather = queryWeatherAPI(city);
return weather;
}
}
// 注册到 Agent
ReactAgent agent = ReactAgent.builder()
.name("weather_agent")
.model(chatModel)
.tools(List.of(new WeatherTool()))
.build();
2.2 阶段 2:工具选择(Tool Selection)
2.2.1 LLM 如何决定调用哪个工具?

2.2.2 工具描述的重要性
一个好的工具描述能够消除模型调用时的二义性,能够方便Agent更快地根据意图分析出当前场景适宜的工具。从而达到提升工具调用的准确性和响应速度,从而优化整体任务执行效率。
java
// ❌ 不好的描述
.description("查询天气")

java
// ✅ 好的描述
.description("查询指定城市的实时天气信息,包括温度、湿度、风速等。" +
"支持全球主要城市。" +
"参数:city(城市名称,如:北京、上海、纽约)")

2.2.3ToolSelectionInterceptor
java
public class ToolSelectionInterceptor extends ModelInterceptor {
@Override
public ChatResponse interceptModelCall(
ChatRequest request,
ModelCallHandler handler) {
// 1. 分析用户问题
String userQuestion = extractUserQuestion(request);
// 2. 过滤相关工具
List<ToolDefinition> relevantTools = filterRelevantTools(
userQuestion,
request.getTools()
);
// 3. 调整工具顺序(相关性高的放前面)
List<ToolDefinition> sortedTools = sortToolsByRelevance(
userQuestion,
relevantTools
);
// 4. 修改请求,只包含相关工具
ChatRequest modifiedRequest = request.withTools(sortedTools);
// 5. 调用 LLM
return handler.call(modifiedRequest);
}
}
2.3 阶段 3:工具调用请求构建(Tool Call Request)
2.3.1 ToolCallRequest 的结构
java
public class ToolCallRequest {
// 原始的工具调用信息(来自 LLM)
private AssistantMessage.ToolCall toolCall;
// 工具名称
private String toolName;
// 工具参数(JSON 字符串)
private String arguments;
// 工具调用 ID(用于追踪)
private String toolCallId;
// 请求上下文
private Map<String, Object> context;
// 执行上下文(包含 Agent 状态、配置等)
private ToolCallExecutionContext executionContext;
}
2.3.2 参数解析与验证
java
// LLM 生成的工具调用
AssistantMessage.ToolCall toolCall = new AssistantMessage.ToolCall(
"call_123",
"get_weather",
"{\"city\": \"北京\", \"unit\": \"celsius\"}"
);
// 构建 ToolCallRequest
ToolCallRequest request = ToolCallRequest.builder()
.toolCall(toolCall)
.toolName(toolCall.name())
.arguments(toolCall.arguments())
.toolCallId(toolCall.id())
.context(new HashMap<>())
.executionContext(new ToolCallExecutionContext(config, state))
.build();
// 参数验证
Map<String, Object> parsedArgs = JsonUtils.parseJson(
request.getArguments());
validateArguments(parsedArgs, toolDefinition.getInputSchema());
2.3.3 ToolContext 的传递
ToolContext 是工具执行时的"上下文容器",它承载了工具运行所需的全部外部信息,包括参数、Agent 状态、配置以及自定义数据。在 ToolContext的支持下,工具不仅能拿到调用时传入的参数,还能感知当前 Agent 的全局状态(如历史消息、对话 ID)、运行配置(如线程 ID、超时设置),甚至可以在不同工具之间传递临时数据。

于是乎Agent在使用工具的时候具备了"感知环境"的能力,让工具不再是孤立的方法,而是与 Agent 运行态深度融合的智能节点。
java
public class ToolContext {
// 工具参数
private Map<String, Object> arguments;
// Agent 状态(OverAllState)
private OverAllState agentState;
// Agent 配置(RunnableConfig)
private RunnableConfig agentConfig;
// 自定义上下文数据
private Map<String, Object> customContext;
// 线程 ID(用于追踪)
private String threadId;
}
代码示例:在工具中访问上下文
java
@Override
public String call(String arguments, ToolContext context) {
// 获取工具参数
Map<String, Object> args = JsonUtils.parseJson(arguments);
String city = (String) args.get("city");
// 获取 Agent 状态
OverAllState state = context.get(AGENT_STATE_CONTEXT_KEY);
// 获取 Agent 配置
RunnableConfig config = context.get(AGENT_CONFIG_CONTEXT_KEY);
// 获取线程 ID(用于日志追踪)
String threadId = config.threadId().orElse("default");
logger.info("[ThreadId {}] Executing get_weather for city: {}",
threadId, city);
// 执行业务逻辑
String weather = queryWeatherAPI(city);
return weather;
}
2.4 阶段 4:工具执行(Tool Execution)
2.4.1 AgentToolNode 的执行流程

2.4.2 拦截器链(InterceptorChain)

拦截器链是一个责任链模式的实现,可以将多个拦截器按顺序串联,每个拦截器都可以在调用下一个处理器前后执行自定义逻辑,如重试、限流、日志等。这种设计使得工具调用链路具备高度的可扩展性与可插拔性,新增拦截器时无需改动核心代码,只需配置顺序即可生效。
java
public class InterceptorChain {
public static ToolCallHandler chainToolInterceptors(
List<ToolInterceptor> interceptors,
ToolCallHandler baseHandler) {
// 从后往前构建链
ToolCallHandler handler = baseHandler;
for (int i = interceptors.size() - 1; i >= 0; i--) {
ToolInterceptor interceptor = interceptors.get(i);
ToolCallHandler currentHandler = handler;
// 每个拦截器包装前一个处理器
handler = request ->
interceptor.interceptToolCall(request, currentHandler);
}
return handler;
}
}
2.5 拦截器链的执行顺序:

2.5 阶段 5:工具响应处理(Tool Response)
2.5.1 ToolCallResponse 的结构
java
public class ToolCallResponse {
// 工具调用 ID(用于匹配请求)
private String toolCallId;
// 工具名称
private String toolName;
// 工具执行结果
private String result;
// 是否直接返回(不再调用 LLM)
private boolean returnDirect;
}
2.5.2 结果返回给 LLM
java
// 工具执行结果
ToolCallResponse response = ToolCallResponse.of(
"call_123",
"get_weather",
"北京:晴天,温度 25°C,湿度 60%"
);
// 转换为 ToolResponseMessage
ToolResponseMessage.ToolResponse toolResponse =
ToolResponseMessage.ToolResponse.builder()
.id(response.getToolCallId())
.name(response.getToolName())
.response(response.getResult())
.build();
// 构建消息
ToolResponseMessage message = ToolResponseMessage.builder()
.responses(List.of(toolResponse))
.build();
// 添加到消息历史
messages.add(message);
2.5.3 循环决策

三、Hook 机制详解🔗
Hook 是在工具调用生命周期关键节点上"挂钩"的扩展机制。它允许我们在不改动核心执行链路的前提下,把自定义逻辑插入到调用前、调用后以及异常分支中。例如在调用前做参数校验和权限判断,在调用后做结果加工与埋点统计,在异常时做降级、告警或重试决策。
换句话说,Hook 解决的不是"工具能不能执行",而是"工具调用过程能否被治理、被观测、被控制"的问题,是把 Tool Calling 从"可用"升级到"工程可落地"的关键能力。
3.1 Hook 的执行时机

3.2 ToolInjection - 工具注入
ToolInjection 看起来只是 3 个方法,但它解决的是一个很工程化的问题:当 Hook 需要依赖某个具体工具时,如何做到"声明式绑定",而不是手动硬编码查找?
它的设计思想是"约定优于配置":
- 通过
getRequiredToolName()按工具名精确匹配; - 通过
getRequiredToolType()按工具类型匹配; - 两者都不填时,框架可退化为默认注入策略。
这意味着 Hook 可以专注于业务逻辑(比如审计、限权、埋点),而把"找工具、绑工具"交给框架完成,降低耦合度。
java
public interface ToolInjection {
// 框架注入匹配到的工具实例
void injectTool(ToolCallback tool);
// 按名称匹配(优先用于精确绑定)
default String getRequiredToolName() {
return null;
}
// 按类型匹配(适合同类工具统一处理)
default Class<? extends ToolCallback> getRequiredToolType() {
return null;
}
}
3.3 ToolCallLimitHook - 限制工具调用次数
博主第一次理解"调用限额"会理所当然的把其设计想象为:"每次工具调用前 +1 并检查"。但 SAA 的实现更完整:它把限额做成了一个模型级治理 Hook,核心能力有三层
|-------------|-------------------------------------------------------|
| 能力名称 | 具体能力 |
| 双维度限额 | threadLimit:线程级上限(整个会话累计) runLimit:单次运行上限(本轮执行累计) |
| 按工具粒度统计 | 可统计所有工具 也可只统计某个指定 toolName |
| 可配置退出策略 | END:优雅结束(给出提示并跳到结束节点) ERROR:抛异常终止(更强硬,便于外层监控感知) |
限定调用次数的Hook实现类
java
public class ToolCallLimitHook implements Hook {
private int maxToolCalls = 10;
private int currentToolCalls = 0;
@Override
public void beforeToolCall(ToolCallRequest request) {
currentToolCalls++;
if (currentToolCalls > maxToolCalls) {
throw new ToolCallLimitExceededException(
"Tool call limit exceeded: " + maxToolCalls);
}
logger.info("Tool call count: {}/{}",
currentToolCalls, maxToolCalls);
}
}
设定工具调用上限的例子:
java
ToolCallLimitHook hook = ToolCallLimitHook.builder()
.toolName("get_weather") // 可选:仅限制某个工具;不填则限制全部工具
.threadLimit(20) // 会话级累计上限
.runLimit(8) // 单次运行上限
.exitBehavior(ExitBehavior.END) // END 或 ERROR
.build();
四、 关键要点总结💡
走到这里,你可以把 Tool Calling 理解成一句话:
它不是一次函数调用,而是一条从"模型决策"到"执行反馈"再到"继续推理"的闭环链路。
前面的五个阶段,本质上是在解决三个工程问题:
- 可调用:模型能不能正确选中并调用工具(注册、选择、参数契约)
- 可治理:调用过程能不能被控制(重试、限流、异常处理、短路返回)
- 可演进:业务变复杂后,链路还能不能扩展(Hook/Interceptor 插拔式增强)
这套机制的价值,不在"调用了一次工具",而在"让工具调用在复杂场景下仍然稳定可控"。
4.1 五个阶段的核心职责
| 阶段 | 核心职责 | 关键类 |
|---|---|---|
| 工具注册 | 定义工具元数据 | ToolCallback, ToolDefinition |
| 工具选择 | LLM 决定调用哪个工具 | ToolSelectionInterceptor |
| 请求构建 | 解析参数、构建上下文 | ToolCallRequest, ToolContext |
| 工具执行 | 实际调用工具、处理异常 | AgentToolNode, InterceptorChain |
| 响应处理 | 返回结果、继续推理 | ToolCallResponse, ToolResponseMessage |
4.2 Hook vs Interceptor
| 特性 | Hook | Interceptor |
|---|---|---|
| 作用 | 在关键节点插入逻辑 | 拦截和修改请求/响应 |
| 执行时机 | 固定的生命周期节点 | 链式执行 |
| 修改能力 | 有限 | 强大 |
| 使用场景 | 日志、监控、权限检查 | 重试、缓存、错误处理 |
可以把两者的分工记成一句口诀:Hook 管"节点",Interceptor 管"链路"。(即:Hook是"点",而Interceptor是"线")
- 当你要在固定时机加逻辑(如 before/after/error)时,用 Hook 更直接;
- 当你要对请求与响应做包裹式增强(如重试、缓存、熔断、Mock)时,用 Interceptor 更合适。
两者并非是替代的关系,而是相互协作关系:Hook 负责把控"在哪个阶段做事";Interceptor 负责把控"这次调用怎么做事"。在工程实践里, Hook 做轻量治理(日志、审计、权限),Interceptor 做重度策略(重试、错误兜底、结果改写)。
如果这篇文章对你有帮助,欢迎:
- 👍 点赞支持
- 💬 评论交流
- ⭐ 收藏备用
- 🔗 分享给更多人
有任何问题或建议,欢迎在评论区留言讨论!会调用工具的 Agent 很常见,**但真正难的,从来不是"会做事",而是"做完之后还记得自己做过什么"。**当上下文变长、会话被打断、任务需要续跑时,SAA 是如何让 Agent 既能记住过去、又能稳定回到正确状态的?下一回,我们就来揭开这套"记忆系统"的面纱。敬请期待~

