理解智能体中的工具调用
上一篇《从最小 Agent 看懂工具、检索与记忆》里,我们用一个计算器认识了工具调用的基本闭环:模型提出调用请求,Java 程序执行函数,再把结果交回模型。
这个例子只有一个计算器。真实应用可能有查询订单、查库存、退款、发邮件等许多工具。工具多起来以后,问题就不只是“怎么调用”,还包括“工具怎样定义”“本轮把哪些工具交给模型”“程序怎样安全地执行”。本文结合 OpenAI Function Calling 指南,把这些问题连起来说明。
一、工具调用是什么,基本过程怎样实现?
先从一个简单例子理解工具调用,再逐步拆解模型和应用各自负责什么。下面五点回答两个基础问题:工具调用是什么?一个工具调用从定义到执行、回传结果,程序要怎样完成?
第一点:工具调用到底是什么?
先用一个天气查询例子说明。用户问:
“今天杭州天气怎么样?”
假设应用提供了一个天气工具 get_weather(city)。模型不会自己打开天气网站,而是根据问题和工具说明,判断需要查询天气,并提出类似“调用 get_weather,参数 city=杭州”的结构化请求。应用收到后才调用天气服务,再把天气数据交回模型组织答案。
用户问天气 ↓ 应用把问题和“天气工具说明”发给模型 ↓ 模型决定调用 get_weather(city="杭州") ↓ 应用执行天气查询 ↓ 查询结果返回给模型 ↓ 模型整理成自然语言答复
关键在于:模型提出调用请求,应用或工具服务才真正执行。模型通常不会自己连接天气网站;它会返回“请调用 get_weather,参数是杭州”这样的结构化请求。应用收到后,才运行对应函数并把结果交回模型。这个“定义工具—接收调用请求—执行—回传结果”的交互方式,可以对照 OpenAI 工具调用流程理解。
先认识 5 个基本概念
| 概念 | 在天气例子里是什么 |
|---|---|
| 工具 | 查询天气的能力,例如 get_weather |
| 工具定义 | 告诉模型工具叫什么、能做什么、需要哪些参数 |
| 调用请求 | 模型提出调用 get_weather,并传入 city="杭州" |
| 工具执行 | 应用真正查询天气服务 |
| 工具结果 | 天气服务返回的数据,再交给模型组织答案 |
因此,工具调用不是“模型说要查天气,天气就自动出现”。应用必须接收调用请求、执行工具,并把结果传回模型。
第二点:LLM 在工具调用中做什么?
LLM 根据用户任务、对话上下文和应用提供的工具定义,判断是否需要使用工具;如果需要,就选择工具并生成参数。收到工具执行结果后,LLM 再判断信息是否足以回答,还是需要继续调用其他工具。例如天气查询失败后,它可以根据返回的错误决定重试、换用其他工具或说明暂时查不到;查到天气后,也可以继续查询空气质量。
因此,LLM 负责判断下一步并生成调用请求,不负责实际运行工具。应用负责接收请求、校验参数与权限、调用对应函数,并把结果交回 LLM。LLM、应用和工具共同组成的系统才是 Agent;Agent 的完整行为不等同于 LLM 单独的行为。
第三点:用户、应用、模型和工具怎样配合?
以“今天杭州天气怎么样?”为例,可以沿着四个参与方看完整过程。应用是中间的协调者:它把问题和天气工具定义发给模型,接住模型的调用请求,执行天气查询,再将结果交回模型并把最终答案发给用户。
第四点:模型返回的调用请求长什么样?
模型不会把“我正在查天气”当作普通回答交给应用,而是返回一条机器可以读取的结构化调用项。OpenAI Responses API 的 function call 会带有调用标识、函数名称和参数。下面是简化示意:
{
"type": "function_call",
"call_id": "call_abc123",
"name": "get_weather",
"arguments": "{\"city\":\"杭州\"}"
}
name告诉应用要找哪个工具处理器。arguments是模型按工具参数定义生成的输入;应用需要解析并校验它。call_id用来将这次请求和稍后返回的工具结果对应起来。
字段形式会随 API 模式或服务商改变;核心信息是“调用哪个工具、参数是什么、这次调用的标识是什么”。这条数据是待执行请求,不是天气结果,也不是一段应该直接展示给用户的最终答案。
第五点:应用怎样执行调用并把结果交回去?
应用通常维护一个工具注册表,把每个工具名称映射到本地处理器。例如 get_weather 映射到天气服务客户端。处理器接收解析后的参数,执行相应操作,并将结果转成简洁、明确的文本或 JSON。
调用请求 name = get_weather
↓
工具注册表查找 get_weather 对应的处理器
↓
解析 arguments,校验 city 参数和访问权限
↓
调用天气服务,得到天气数据
↓
组成 tool result,并保留 call_id
天气服务返回数据后,应用再把结果作为对应调用的输出消息交回模型。Responses API 的消息大致如下:
{
"type": "function_call_output",
"call_id": "call_abc123",
"output": "{\\"location\\":\\"杭州\\",\\"temperature_celsius\\":20}"
}
这里的 call_id 与模型原先返回的调用请求相同,用来让 API 知道这是哪一次调用的结果。output 可以是 JSON、普通文本或错误信息;应用随后再次请求模型,模型根据工具结果生成面向用户的答复,或提出下一次工具调用。
实现时的重要边界:不要把模型返回的工具名直接当成任意 Java 方法名去执行。应用应使用明确的白名单映射(例如 switch 或工具注册表)分发请求;未注册的工具名直接拒绝。调用请求仍须经过参数校验、权限检查,以及必要的用户确认。
二、实际项目中,工具多了怎么办?
理解基本调用闭环后,实际系统还要面对规模和业务复杂度:用户的问题属于哪个领域?本轮应该开放哪些工具?工具达到数百或上千个时,怎样避免全部发送给模型?下面讨论这些落地问题。
第六点:工具很多时,每轮提供哪些工具?
如果只有天气查询、计算器等少量工具,应用可以把这些工具定义一起发给模型,让模型根据问题决定是否调用以及调用哪一个。工具达到几十、几百甚至上千个时,通常不会把完整清单每轮都发送;工具说明会占用上下文,候选过多也会增加选错的机会。
常见做法是先由应用缩小范围,再由模型做具体选择。应用可以结合用户原话、当前页面、业务对象和权限信息,先判断业务类别;例如“退款进度”进入支付/退款工具组,“某商品还有库存吗”进入商品/库存工具组。接着,只把这一组里可授权的少数候选工具交给模型。
场景判断可以使用固定规则、页面入口、分类器或轻量模型。类别明确时规则简单可靠;表达方式多时,可以让分类模型从预设类别中选择。类别不确定时可放宽候选集合或追问。无论模型把请求分到哪个场景,最终权限检查仍由应用执行。
第七点:1000 个工具要一起发给模型吗?Tool Search 怎么做?
通常不需要。可以先建立工具目录,保存每个工具的名称、用途描述、所属领域、参数摘要和权限信息;请求到来时,应用先按场景和权限过滤,再用关键词或向量搜索从描述中找出少量候选工具。
OpenAI Function Calling 指南还介绍了 Tool Search:初始请求不必暴露所有低频工具,模型可以先搜索工具目录,找到相关工具后再加载其定义并调用。截图所示指南注明,内置 tool_search 的支持范围以文档列出的模型为准,页面标明 GPT-5.4 及之后的模型支持。其他模型/框架也可以通过应用侧的工具目录检索实现类似的按需发现流程。
大量工具目录(不全部发给模型)
↓ 按上下文、场景、权限过滤
本轮候选工具(仍过多时继续检索)
↓
模型从候选中选择 / 按需 Tool Search
↓
应用校验并执行所选工具
第八点:框架可以替我们做哪些事?
工具调用可以手写,也可以用框架减少重复工作。框架通常帮助注册工具、生成工具定义、解析模型调用请求、分发到处理器、回传结果并继续模型循环;它不会自动理解企业业务,也不会替应用决定用户权限。
- LangChain4j:Java 框架。可用
@Tool描述方法,也可通过ToolProvider按请求动态提供工具;官方工具文档还包括面向大工具集的搜索机制。 - Spring AI:适合 Spring 应用,可用
@Tool或 ToolCallback 暴露 Java 方法,并用 ChatClient 管理常见的调用往返。
对于学习和小规模 Demo,先看清自己写出的“请求—调用—执行—回传”最容易理解;业务复杂、需要动态工具、错误恢复和观测时,再让框架接管通用环节。
总结:工具调用的完整闭环
定义工具 → 根据场景与权限提供候选工具 → 模型选择并提出调用 → 应用校验并执行 → 结果回到模型 → 模型继续回答或调用下一项工具
用户提出任务后,应用整理可信上下文,并把适合本轮且有权限的工具定义提供给模型。模型判断直接回答还是提出结构化调用请求;应用解析请求、校验参数与权限,执行对应工具,再把结果和调用标识交回模型。Agent 根据结果决定继续调用、换工具、追问、转人工或结束。模型最终给出答复,应用再返回给用户。模型负责提出行动并根据观察结果决策,应用负责授权、执行和控制整个调用边界。
参考:OpenAI Function Calling 指南、LangChain4j Tools、Spring AI Tool Calling。