工具的一切

前言

工具调用(Tool Calling)是大语言模型从 Chatbot 走向 Agent 的核心能力。模型不再仅仅依靠参数内的知识回答问题,而是可以通过调用外部工具获取实时信息、执行操作,并与真实世界交互。

然而,很多人在初次接触工具调用时会产生一个根本误解:以为模型真的像程序一样直接执行了函数。事实上,模型所做的只是生成一份结构化的“调用意图”,真正的执行由 Harness 完成。

理解模型与 Harness 的边界,是掌握 Agent 开发的第一步。

本文将从一次最简单的天气查询出发,逐步展开工具调用的完整图景:模型、工具与 Harness 各自扮演什么角色;工具如何定义;调用模式如何从单次演进到并行、再到多轮;以及为什么说工具契约的质量直接决定了 Agent 的工程上限。

误区

很多人在第一次听到 Agent 工具时,会陷入一个误区:以为 LLM 真的会像程序调用接口函数一样,通过网络直接调用工具,然后跳进函数里把实现跑一遍。

因此,在一次工具调用过程中,必须分清楚:模型到底看到了什么,凭什么决定调用工具,Harness 又在中间做了什么。理解 Harness 和模型的边界非常重要。

在一次简单的工具调用流程中,Model、Tool 和 Harness 各自承担不同职责:

  • 模型:读取用户上下文和工具定义,然后输出结构化的 tool_call 意图,通常是 JSON 数据。
  • Tool:由开发者封装,可以是 MCP、API 或本地函数,负责查询、读写和执行等操作。
  • Harness:把工具说明提供给模型,将模型生成的调用请求转换为真实程序调用,再把执行结果写回新一轮上下文。 由此可以看到,模型并不会自己执行工具。模型只是生成一份工具调用意图,这个请求本质上是模型与 Harness 之间共同维护的一份规则,也是一份契约

在 Agent 开发中,我们编写工具,实际上是在给模型编写一份“可调用工具说明书”;我们编写 Harness 层,则是在检查调用请求的合法性、执行请求,并把结果回填到上下文。

Tool 的定义

以天气查询为例,我们声明一个 get_weather() 工具:

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Get the current weather for a city. Use this when the user asks about current weather conditions.",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {
          "type": "string",
          "description": "The city name, for example Beijing, Shanghai, Tokyo, or New York."
        }
      },
      "required": ["city"],
      "additionalProperties": false
    },
    "strict": true
  }
}
  • Name:描述工具叫什么。
  • Description:描述工具有什么用、何时使用。
  • Schema:描述参数名称、类型、含义、是否必填,以及可选值等约束。
  • Strict: true:强制模型严格遵循 parameters 结构,包括参数类型和必填字段,并禁止传入额外字段。
  • additionalProperties: false:模型不能在 JSON 中携带 parameters 之外的字段,例如自行添加 "country": "China",否则会被判定为格式错误。

演进:一次调用、并行调用与多轮调用

一次调用

仍以天气查询为例。当用户询问“杭州今天的天气怎么样?”时,Harness 会把上下文和 get_weather() 工具定义一起交给模型。模型判断仅靠已有知识无法提供实时天气,于是按照契约生成工具调用意图,把城市名称填入参数,剩下的执行交给 Harness。

User: 杭州现在天气怎么样?
Assistant tool_call: get_weather({"city":"杭州"})
Tool result: {"city":"杭州","temperature":25,"unit":"C","condition":"Cloudy"}
Assistant: 杭州现在约 25°C,多云,体感比较温和。

并行调用

换一个问题:“北京、上海、广州,哪个城市最热?”

模型不必先查北京、等待结果,再查上海和广州。更高效的做法是在同一个 Assistant Turn 中一次性输出三个 tool_call,由 Harness 并发执行,再把三个结果一并返回给模型。

需要澄清一个常见误解:并行 Tool Calling 并不意味着模型真的开启了三个线程,也不代表模型自己同时向网络发起三个 API 请求。模型所做的,仅仅是在本轮输出中声明三个调用意图;真正的并发发生在 Harness 侧。

Harness 可以使用 Promise.allasyncio.gather()、线程池,或其他应用层并发机制执行这些工具。模型的职责只有一个:把可以并行的工作表达出来。

const results = await Promise.all([
  fetchWeather('北京'),
  fetchWeather('上海'),
  fetchWeather('广州')
]);
results = await asyncio.gather(
    fetch_weather('北京'),
    fetch_weather('上海'),
    fetch_weather('广州')
)

多轮调用

场景继续升级:用户问“这周末上海、杭州、苏州哪里更适合户外徒步?”

这已经不是一次 get_weather 能解决的问题。徒步是否适合,要看天气、降雨概率、风速、紫外线强度,甚至还要看步道路况和空气质量。

模型可能先查询候选城市的天气,发现某些城市风速偏大,于是再调用 get_wind_forecast 确认细节;如果天气条件都尚可,它还可能继续调用 get_trail_condition 查看具体路况。

这类任务的本质是多轮 ReAct Loop:每一轮工具返回的结果都会影响下一轮该查什么

  • 单次调用解决的是“缺一个事实”。
  • 并行调用解决的是“缺一组相互独立的事实”。
  • 多轮调用解决的是“下一步查什么,取决于上一步看到了什么”。

这是工具调用从演示玩具真正演进为 Agent 工作流的关键分界。

工具契约质量决定工程质量

真实工具场景的复杂度远远高于天气查询。因此,一份好的工具契约直接决定了 Harness 的工程上限。

Bad Case

先看一个不好的例子:

{
  "name": "weather",
  "description": "Get weather info.",
  "parameters": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string"
      }
    }
  }
}

这个 Bad Case 主要有四个问题。

第一,函数名太模糊。 weather 这个名字太泛,看不出是查询当前天气、未来预报还是历史天气,模型不知道应该在什么场景下调用它。

第二,描述几乎没有信息。 Get weather info. 没有说明工具的使用时机和适用范围,模型无法判断用户问“下个月适合旅游吗?”时是否应该调用它。

第三,参数设计过于宽泛。 只有一个 query 字符串,所有信息都要塞进去。模型不知道应该使用什么格式,工具端拿到参数后还要再次进行自然语言解析,极易出错。

第四,缺少约束。 没有 requiredadditionalProperties: falsestrict: true。模型可能不传参数就发起调用,也可能自行增加额外字段,导致工具端需要编写大量防御性代码。

这些问题叠加后,模型既不知道该不该使用工具,也不知道如何构造参数,即使发起调用也很容易出错。

工具调优

应该如何调优工具契约?

  1. 工具命名要见名知意,并具备足够辨识度。

    工具名应准确描述能力,避免多个职责堆叠,也要避免名称过于相似。例如 get_issueget_issues 仅相差一个复数 s,当工具数量较多、上下文较长或模型能力较弱时,很容易造成工具误选。

  2. Description 不仅要描述“工具是什么”,还要明确“什么时候使用”。

    Description 是模型进行 Tool Selection 的重要依据,应清晰定义工具的适用场景、使用边界,以及与其他相似工具之间的区别。

  3. Schema 应尽可能收窄工具的输入空间。

    通过明确字段类型、取值范围、枚举值、必填项以及是否允许额外字段,减少模型生成非法参数的可能性。Schema 越明确,模型需要自行推断的空间越小。

  4. 参数字段本身也应提供清晰的 Description。

    不仅要定义参数类型,还要解释参数语义、取值规则、使用限制以及特殊情况,帮助模型正确构造 Tool Call。

  5. 工具返回结果应稳定、短小、结构化,并面向下一轮模型消费。

    特别是在对 Legacy API 进行 Agent 化时,不要直接把面向前端设计的原始接口暴露给 Agent。这类接口通常包含大量冗余字段、展示层数据和不稳定结构,会增加上下文负担和理解成本。更合理的方式是增加一层适配,把结果转换成面向 Agent 的精简结构。

  6. 对于返回结果特别大的工具,应设计合理的 Offloading 策略。

    不要将大量原始数据直接塞回 Context。可以根据场景采用文件落盘、按需读取、分页或摘要等方式,将大结果从主上下文卸载,同时保留 Agent 后续获取详细信息的能力。

  7. 从“能调用”走向“会组织”。

    当系统拥有几十个工具时,需要进一步考虑哪些工具应该直接进入上下文,哪些应该延迟加载,以及如何组织工具之间的协作。

总结

工具调用的核心可以归纳为以下几点:

  1. 模型不执行,只表达意图。 模型生成结构化的 tool_call,真正的调用执行和结果回填由 Harness 完成。二者之间是一份契约关系。

  2. 调用模式有三级演进。 单次调用解决“缺一个事实”;并行调用解决“缺一组相互独立的事实”,由 Harness 侧并发执行;ReAct Loop 解决“下一步查什么取决于上一步看到了什么”,这是 Agent 工作流的关键分界。

  3. 工具契约质量决定工程上限。 命名见名知意、Description 明确使用时机、Schema 收窄输入空间、参数拥有清晰描述、返回结果精简且结构化——这些细节直接影响模型能否正确、稳定地调用工具。

  4. 从能调用到会组织。 当工具数量达到几十个时,问题不再是“能不能调用”,而是“该把哪些工具放进上下文、哪些延迟加载、如何组织工具之间的协作”。这是 Agent 工程从入门走向进阶的下一个课题。

理解模型与 Harness 的边界,掌握工具定义的调优方法,就具备了构建可靠 Agent 系统的基础。剩下的,是在真实场景中不断打磨工具契约、优化调用流程。