← 全部文章

Tool Calling:Agent 真正开始干活的地方

模型并不会直接执行函数。Tool Calling 是模型与应用之间的一份结构化行动请求,也是 Agent 接触外部世界的第一道接口。

Tool Calling:Agent 真正开始干活的地方
文章目录
  1. 导语
  2. 一、Tool Calling 不是模型在执行函数
  3. 二、一次调用包含五个环节
  4. 1. 应用告诉模型“有哪些工具”
  5. 2. 模型选择回复或请求工具
  6. 3. Runtime 校验调用
  7. 4. 应用执行工具
  8. 5. 工具结果回到模型
  9. 三、一个可运行心智模型的 TypeScript 示例
  10. 四、Tool、Function 和 MCP 不要混为一谈
  11. Tool 是模型可请求的能力
  12. Function Calling 是一种调用表达方式
  13. MCP 是工具发现与调用的一种协议
  14. 五、读工具和写工具,风险不一样
  15. 六、并行调用什么时候成立
  16. 七、前端和后端看到的不是同一个东西
  17. 前端不该只显示“正在思考”
  18. 后端需要保留可追踪的调用记录
  19. 八、五个最容易犯的错误
  20. 把 Tool Call 当成已执行
  21. 只校验 JSON,不校验权限
  22. 让模型填写可信身份
  23. 失败后丢掉调用关联
  24. 把所有能力塞进一个万能工具
  25. 九、用一个新场景检查是否理解
  26. 十、这一篇真正要记住的事
  27. 下一篇预告
  28. 参考资料

导语

上一篇,我们写出了 Agent Loop 的最小骨架:模型提出下一步,Runtime 校验并执行工具,Tool Result 再回到下一轮。

现在把其中一次动作放大。

用户说:

查一下登录接口为什么返回 401。

模型需要查询日志。可模型既没有日志系统的账号,也不能在你的服务器里直接运行代码。它能做的,是返回一份结构化请求:

{
  "name": "search_logs",
  "arguments": {
    "query": "status:401 path:/api/login",
    "environment": "staging"
  }
}

应用收到请求后,找到对应工具,校验参数与权限,然后才真正访问日志系统。

这套“模型提出调用,应用负责执行”的接口,就是 Tool Calling,也常被称为 Function Calling。

这一篇不急着讨论怎样写出最优 Tool Schema,也不展开 Tool Result 的字段设计——它们分别属于下一篇和下下篇。本文只解决一个问题:一次工具调用从哪里开始,到哪里结束?


一、Tool Calling 不是模型在执行函数

“模型调用函数”是一种方便但不够精确的说法。

模型真正产生的只是输出数据。它可能在响应中选择某个工具,并给出一组参数。真正执行函数的,仍然是你的应用代码、Agent Runtime 或模型服务提供的托管工具环境。

以读取文件为例:

模型输出
read_file({ path: "src/pages/Login.tsx" })

Runtime
检查 read_file 是否开放、路径是否合法

工具实现
从文件系统读取内容

Runtime
把读取结果送回模型

这条边界很重要。因为模型输出可能有错,甚至可能被恶意内容诱导。只要真实动作仍经过应用控制,我们就能在执行前校验、拒绝或请求用户确认。

因此,Tool Call 更像一张“行动申请单”,不是已经发生的事实。


二、一次调用包含五个环节

OpenAI 的 Function Calling 文档把一次调用概括为五步:应用向模型提供工具,模型返回调用请求,应用执行代码,再把调用结果交给模型,模型随后回答或继续调用。

把这五步放回我们的 Agent Runtime,可以写成:

流程图

逐步看一遍。

1. 应用告诉模型“有哪些工具”

模型不会自动知道你的项目里存在 search_logs。调用模型时,应用要同时提供工具定义:

const tools = [
  {
    type: 'function',
    name: 'search_logs',
    description: '查询指定环境的应用日志',
    parameters: {
      type: 'object',
      properties: {
        query: { type: 'string' },
        environment: {
          type: 'string',
          enum: ['staging', 'production']
        }
      },
      required: ['query', 'environment'],
      additionalProperties: false
    }
  }
]

这段定义是给模型看的能力说明,不是工具实现本身。

2. 模型选择回复或请求工具

模型看到用户任务和工具定义后,可能直接回答,也可能返回 Tool Call。

{
  "type": "function_call",
  "call_id": "call_7f2",
  "name": "search_logs",
  "arguments": "{\"query\":\"status:401 path:/api/login\",\"environment\":\"staging\"}"
}

具体字段会随 SDK 和模型服务变化。上面的结构接近 OpenAI Responses API:arguments 是 JSON 编码字符串,call_id 用于把稍后的工具结果关联回这次调用。

3. Runtime 校验调用

模型生成的工具名和参数都不能直接信任。Runtime 至少检查:

  • 工具是否存在并向本轮开放;
  • 参数是否符合 schema;
  • 当前用户能否访问目标环境;
  • 这次动作是否需要用户确认。

这里的 schema 校验只能证明“参数形状正确”。它不能证明用户有权限,也不能证明动作符合业务规则。

4. 应用执行工具

检查通过后,应用把参数交给真正的工具函数:

const handlers = {
  search_logs: async (args: SearchLogsArgs, ctx: ToolContext) => {
    return logClient.search({
      tenantId: ctx.tenantId,
      environment: args.environment,
      query: args.query
    })
  }
}

登录凭证和 tenantId 来自受信任的执行上下文,不应由模型自由填写。否则模型可能通过参数访问错误账号或其他租户的数据。

5. 工具结果回到模型

工具执行完成后,Runtime 构造与原调用对应的结果:

{
  "type": "function_call_output",
  "call_id": "call_7f2",
  "output": "{\"matches\":12,\"topError\":\"INVALID_SIGNATURE\"}"
}

模型下一轮会同时看到原任务、自己的 Tool Call 和这份结果,于是可以继续查询或组织最终回答。

如果应用执行了工具,却没有把结果送回模型,Agent Loop 就断了。


三、一个可运行心智模型的 TypeScript 示例

下面的代码不绑定具体 SDK,只表达一次 Tool Call 在应用侧怎样被接住。

type ToolDefinition = {
  name: string
  description: string
  parameters: unknown
}

type ToolCall = {
  id: string
  name: string
  arguments: unknown
}

type ToolContext = {
  userId: string
  tenantId: string
  approvals: Set<string>
}

type Tool = {
  definition: ToolDefinition
  execute: (
    args: unknown,
    context: ToolContext
  ) => Promise<unknown>
}

async function handleToolCall(
  call: ToolCall,
  tools: Tool[],
  context: ToolContext
) {
  const tool = tools.find(
    item => item.definition.name === call.name
  )

  if (!tool) {
    return toolError(call, 'TOOL_NOT_ALLOWED')
  }

  try {
    const args = validate(
      tool.definition.parameters,
      call.arguments
    )

    await authorize({
      userId: context.userId,
      tenantId: context.tenantId,
      toolName: call.name,
      args
    })

    const output = await tool.execute(args, context)
    return toolSuccess(call, output)
  } catch (error) {
    return toolError(call, classifyToolError(error))
  }
}

这段代码有四个值得注意的地方。

第一,模型只提供 call.name 和 call.arguments,不能指定执行函数本身。

第二,Runtime 从本轮允许的 tools 中查找工具。即使系统注册了一百个工具,某次任务也可以只开放三个。

第三,用户和租户信息来自 ToolContext,而不是模型参数。

第四,无论成功还是失败,返回结果都保留原调用 ID。下一轮才知道哪次动作发生了什么。

这只是接口骨架。validate 怎样实现、错误怎样分类、结果怎样截断,会在后面逐篇补齐。


四、Tool、Function 和 MCP 不要混为一谈

这三个词经常一起出现,但它们不在同一层。

Tool 是模型可请求的能力

例如读取文件、查天气、搜索日志、运行测试。它是能力层的统称。

Function Calling 是一种调用表达方式

应用用结构化定义告诉模型有哪些函数工具;模型以结构化数据返回调用请求;应用再执行并回传结果。

有些模型平台还提供 Web Search、Code Interpreter 等托管工具。它们仍属于工具,但执行环境可能由平台负责,而不是你的服务器。

MCP 是工具发现与调用的一种协议

Model Context Protocol(MCP)规定客户端与服务器怎样列出工具、调用工具和返回结果。一个 MCP Server 可以向 Agent 提供多项工具。

但用了 Tool Calling,不代表一定使用 MCP。本地 TypeScript 函数、公司内部 HTTP API 或云平台托管工具,都可以通过 Tool Calling 接入 Agent。

可以这样理解:

Tool:模型能使用什么能力
Function Calling:模型怎样表达一次结构化调用请求
MCP:工具提供方和 Agent 之间怎样标准化连接

MCP 会在第 26 篇单独展开。现在不要为了调用一个本地函数,先引入一整套协议。


五、读工具和写工具,风险不一样

同样是 Tool Call,下面两个动作的副作用完全不同:

read_file({ path: "src/auth.ts" })
delete_user({ userId: "u_123" })

前者通常只读取信息;后者会改变真实业务状态,而且可能难以恢复。

第一版 Agent 可以按风险把工具粗分为:

类型 示例 常见处理
只读 查日志、读文件、列出订单 权限检查后可自动执行
可恢复写入 生成草稿、创建 Patch 执行后保留预览或撤销路径
高副作用 删除数据、付款、部署生产 执行前要求明确确认

这不是通用安全标准,只是一种起步分法。真正的权限判断还要考虑用户身份、目标资源、金额、环境和组织策略。

关键结论是:模型选中了工具,不代表系统已经授权执行。

第 9、10 篇会继续设计权限控制和 Human-in-the-loop。在第三篇,我们先把检查入口留在 Runtime 中。


六、并行调用什么时候成立

模型一次响应可能返回多个 Tool Call。例如排查页面异常时,同时读取前端组件和无依赖的配置文件。

如果两个调用互不依赖、没有冲突副作用,Runtime 可以并行执行:

const results = await Promise.all(
  toolCalls.map(call =>
    handleToolCall(call, tools, context)
  )
)

但下面这种链路不能盲目并行:

先创建订单 → 再使用订单 ID 发起支付

第二个动作依赖第一个动作的输出,只能串行。

写操作之间还可能发生冲突。生产 Runtime 应允许工具声明执行模式,或由编排层判断依赖关系。并行不是越多越好,它只在结果独立且副作用可控时减少等待时间。


七、前端和后端看到的不是同一个东西

Tool Calling 是后端接口,也是产品交互事件。

前端不该只显示“正在思考”

Runtime 可以把工具事件转换成用户能理解的状态:

正在读取登录页面
正在查询测试环境日志
等待确认:是否运行集成测试
日志查询失败,可重试

不要直接把完整参数、访问令牌或内部报错堆栈展示给用户。前端需要的是动作名称、目标对象、状态、可执行操作和安全的结果摘要。

后端需要保留可追踪的调用记录

一次调用至少应能追踪:

  • 调用 ID;
  • 工具名;
  • 脱敏后的参数;
  • 发起和完成时间;
  • 成功、失败或等待确认;
  • 结构化错误码;
  • 结果摘要或产物引用。

这些记录既用于恢复和排错,也能支持前端实时更新状态。后面的状态管理与可观测性会给出更完整的数据结构。


八、五个最容易犯的错误

把 Tool Call 当成已执行

模型只是请求动作。只有工具真正返回成功,系统才能记录 Action 已发生。

只校验 JSON,不校验权限

参数格式正确,不代表当前用户可以读取生产日志或删除订单。

让模型填写可信身份

tenantId、访问令牌和当前登录用户应来自服务端上下文,而不是模型可控参数。

失败后丢掉调用关联

错误结果也要对应原调用 ID。否则多个并行调用发生错误时,模型和日志都难以判断谁失败了。

把所有能力塞进一个万能工具

execute(action: string, payload: unknown)

这种接口把大量选择藏在字符串中,既难校验,也难做细粒度权限。工具应该怎样拆分,是下一篇 Tool Schema 的核心问题。


九、用一个新场景检查是否理解

用户说:

查出订单 20260813001 当前为什么没有发货,不要修改订单。

Agent 可以使用 get_order 和 get_fulfillment_events 两个只读工具。

试着不回看前文,回答:

  1. 模型返回 get_order 后,订单数据已经读取了吗?
  2. orderId 应该怎样校验?
  3. 当前用户的租户身份应该从哪里来?
  4. 工具返回后,模型为什么能继续查询履约事件?

正确思路是:模型只提出调用;Runtime 校验工具、参数和资源权限;可信身份来自服务端执行上下文;结果带着调用关联回到下一轮,模型才获得新信息。

如果能用订单场景重新讲清这条链路,就说明你理解的是 Tool Calling 的通用机制,而不是记住了登录排障的步骤。


十、这一篇真正要记住的事

Tool Calling 给模型的不是直接执行权,而是提出结构化行动请求的能力。

一次完整调用需要:

应用提供工具定义
→ 模型返回 Tool Call
→ Runtime 校验工具、参数和权限
→ 应用执行真实代码
→ Tool Result 关联回原调用
→ 模型基于结果继续

理解这条边界后,下一篇的问题就自然出现了:模型靠什么分清两个相似工具?参数怎样写,才能既好用又不越界?

答案藏在 Tool Schema 里。


下一篇预告

下一篇:Tool Schema 设计:为什么你的 Agent 总是调用错工具?


参考资料