📌 系列文章


目录

📌 系列文章

📖 前言

一、 什么是工具调用链路?🎯

工具调用 vs 传统函数调用

二、工具调用的生命周期阶段🏗️

2.1 阶段 1:工具注册与发现(Tool Registration & Discovery)

2.1.1 工具是什么?

2.1.2 工具元数据(ToolDefinition)

 🌰参数对照表

2.1.3 工具注册流程

2.2 阶段 2:工具选择(Tool Selection)

2.2.1 LLM 如何决定调用哪个工具?

2.2.2 工具描述的重要性

2.2.3ToolSelectionInterceptor

2.3 阶段 3:工具调用请求构建(Tool Call Request)

2.3.1 ToolCallRequest 的结构

2.3.2 参数解析与验证

2.3.3 ToolContext 的传递

2.4 阶段 4:工具执行(Tool Execution)

2.4.1 AgentToolNode 的执行流程

2.4.2 拦截器链(InterceptorChain)

2.5 阶段 5:工具响应处理(Tool Response)

2.5.1 ToolCallResponse 的结构

2.5.2 结果返回给 LLM

2.5.3 循环决策

三、Hook 机制详解🔗

3.1 Hook 的执行时机

3.2 ToolInjection - 工具注入

3.3 ToolCallLimitHook - 限制工具调用次数

限定调用次数的Hook实现类

 设定工具调用上限的例子:

四、 关键要点总结💡

4.1 五个阶段的核心职责

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 接口定义:

public interface ToolCallback {
    
    /**
     * 获取工具的元数据定义
     */
    ToolMetadata getToolMetadata();
    
    /**
     * 执行工具
     * @param arguments 工具参数(JSON 格式)
     * @param context 工具上下文
     * @return 工具执行结果
     */
    String call(String arguments, ToolContext context);
}

2.1.2 工具元数据(ToolDefinition)

  每个工具都需要定义元数据,告诉 LLM 这个工具是什么、怎么用。元数据不是给人看的,是给模型做决策和给框架做路由用的。

public class ToolDefinition {
    
    // 工具名称(LLM 用这个名字调用工具)
    private String name;
    
    // 工具描述(LLM 用这个理解工具的功能)
    private String description;
    
    // 工具参数的 JSON Schema
    private Map<String, Object> inputSchema;
    
    // 是否直接返回结果(不再调用 LLM)
    private boolean returnDirect;
}

小🌰:天气查询工具

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 工具注册流程

代码示例:注册工具

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更快地根据意图分析出当前场景适宜的工具。从而达到提升工具调用的准确性和响应速度,从而优化整体任务执行效率。

// ❌ 不好的描述
.description("查询天气")

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

2.2.3ToolSelectionInterceptor

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 的结构

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 参数解析与验证

// 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 运行态深度融合的智能节点。

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;
}

代码示例:在工具中访问上下文

@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)

拦截器链是一个责任链模式的实现,可以将多个拦截器按顺序串联,每个拦截器都可以在调用下一个处理器前后执行自定义逻辑,如重试、限流、日志等。这种设计使得工具调用链路具备高度的可扩展性与可插拔性,新增拦截器时无需改动核心代码,只需配置顺序即可生效。

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 的结构

public class ToolCallResponse {
    
    // 工具调用 ID(用于匹配请求)
    private String toolCallId;
    
    // 工具名称
    private String toolName;
    
    // 工具执行结果
    private String result;
    
    // 是否直接返回(不再调用 LLM)
    private boolean returnDirect;
}

2.5.2 结果返回给 LLM

// 工具执行结果
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 可以专注于业务逻辑(比如审计、限权、埋点),而把“找工具、绑工具”交给框架完成,降低耦合度。

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实现类

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);
    }
}

 设定工具调用上限的例子:

ToolCallLimitHook hook = ToolCallLimitHook.builder()
    .toolName("get_weather")      // 可选:仅限制某个工具;不填则限制全部工具
    .threadLimit(20)              // 会话级累计上限
    .runLimit(8)                  // 单次运行上限
    .exitBehavior(ExitBehavior.END) // END 或 ERROR
    .build();

四、 关键要点总结💡

走到这里,你可以把 Tool Calling 理解成一句话:

它不是一次函数调用,而是一条从“模型决策”到“执行反馈”再到“继续推理”的闭环链路。

前面的五个阶段,本质上是在解决三个工程问题:

  1. 可调用:模型能不能正确选中并调用工具(注册、选择、参数契约)
  2. 可治理:调用过程能不能被控制(重试、限流、异常处理、短路返回)
  3. 可演进:业务变复杂后,链路还能不能扩展(Hook/Interceptor 插拔式增强)

这套机制的价值,不在“调用了一次工具”,而在“让工具调用在复杂场景下仍然稳定可控”。

4.1 五个阶段的核心职责

阶段核心职责关键类
工具注册定义工具元数据ToolCallback, ToolDefinition
工具选择LLM 决定调用哪个工具ToolSelectionInterceptor
请求构建解析参数、构建上下文ToolCallRequest, ToolContext
工具执行实际调用工具、处理异常AgentToolNode, InterceptorChain
响应处理返回结果、继续推理ToolCallResponse, ToolResponseMessage

4.2 Hook vs Interceptor

特性HookInterceptor
作用在关键节点插入逻辑拦截和修改请求/响应
执行时机固定的生命周期节点链式执行
修改能力有限强大
使用场景日志、监控、权限检查重试、缓存、错误处理

可以把两者的分工记成一句口诀:Hook 管“节点”,Interceptor 管“链路”。(即:Hook是“点”,而Interceptor是“线”)

  • 当你要在固定时机加逻辑(如 before/after/error)时,用 Hook 更直接;
  • 当你要对请求与响应做包裹式增强(如重试、缓存、熔断、Mock)时,用 Interceptor 更合适。

两者并非是替代的关系,而是相互协作关系:Hook 负责把控“在哪个阶段做事”;Interceptor 负责把控“这次调用怎么做事”。在工程实践里, Hook 做轻量治理(日志、审计、权限),Interceptor 做重度策略(重试、错误兜底、结果改写)。


如果这篇文章对你有帮助,欢迎:

  • 👍 点赞支持
  • 💬 评论交流
  • ⭐ 收藏备用
  • 🔗 分享给更多人

有任何问题或建议,欢迎在评论区留言讨论!会调用工具的 Agent 很常见,但真正难的,从来不是“会做事”,而是“做完之后还记得自己做过什么”。当上下文变长、会话被打断、任务需要续跑时,SAA 是如何让 Agent 既能记住过去、又能稳定回到正确状态的?下一回,我们就来揭开这套“记忆系统”的面纱。敬请期待~

​编辑

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐