Spring AI Alibaba 学习(四):ToolCalling —— 从LLM到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.2.3ToolSelectionInterceptor

[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 - 限制工具调用次数)

限定调用次数的Hook实现类

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

[四、 关键要点总结💡](#四、 关键要点总结💡)

[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")中到底是怎么跑起来的
  • HookInterceptor 机制:如何在链路中插入限流、重试、日志、容错等能力
  • 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_weathersearch_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 理解成一句话:

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

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

  1. 可调用:模型能不能正确选中并调用工具(注册、选择、参数契约)
  2. 可治理:调用过程能不能被控制(重试、限流、异常处理、短路返回)
  3. 可演进:业务变复杂后,链路还能不能扩展(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 既能记住过去、又能稳定回到正确状态的?下一回,我们就来揭开这套"记忆系统"的面纱。敬请期待~
相关推荐
Ivanqhz2 小时前
linearize:控制流图(CFG)转换为线性指令序列
开发语言·c++·后端·算法·rust
云和数据.ChenGuang2 小时前
langchain安装过程中的故障bug
人工智能·langchain·bug·langsmith·langchain-core
一直都在5722 小时前
Java线程池
java·开发语言
2401_873204652 小时前
基于C++的区块链实现
开发语言·c++·算法
得物技术2 小时前
Claude在得物App数仓的深度集成与效能演进
大数据·人工智能·llm
weixin_6682 小时前
BPMN.io全方位深度分析报告架构解析 - AI分析分享
人工智能·架构·开源
Elastic 中国社区官方博客2 小时前
Observabilty:自动化错误分诊 - 从被动到自主
大数据·运维·人工智能·elasticsearch·搜索引擎·自动化·全文检索
智算菩萨2 小时前
OpenCV几何图形绘制工具全栈开发:从中文路径支持到交互式GUI的完整实战(附源码)
开发语言·图像处理·人工智能·python·opencv·计算机视觉
掘金者阿豪2 小时前
从聊天入口到系统治理:深度解读“小龙虾 Web / OpenClaw”左侧导航的产品设计逻辑
后端