一次讲清 Function Calling:AI Agent 怎么从会聊天变成会做事

在这里插入图片描述

会聊天,不等于会做事

你做了一个终端里的 AI 助手,能多轮对话,能解释代码,Markdown 渲染也很漂亮。

然后你对它说:

帮我看看这个项目的入口文件里有什么。

如果它没有工具,最可能的回答是:

我无法直接访问你的文件系统。你可以把文件内容贴给我,我再帮你分析。

这句话听起来礼貌,但从 Agent 的角度看,问题很明显:它只有嘴,没有手。

它可以分析你贴进来的代码,却不能自己找文件;它可以告诉你“应该运行测试”,却不能自己执行命令;它可以建议你修改某一行,却不能自己打开文件、精确替换、再验证结果。

这就是聊天机器人和 Agent 的分界线:

聊天机器人:用户给上下文,模型生成文本
Agent:模型决定下一步动作,应用执行工具,结果再回到模型

Function Calling,也常被叫作 Tool Use,就是让模型从“只会说”走向“可以请求做事”的第一层协议。

一句话定义

Function Calling 可以这样理解:

模型根据用户问题和工具描述,生成一个结构化工具调用请求;应用接收请求、执行真实工具,再把执行结果作为上下文返回给模型。

注意这里的主语。

模型不直接读文件,不直接查数据库,不直接执行 shell,也不直接调用你的业务接口。它只输出一段结构化内容,类似:

{
  "name": "read_file",
  "arguments": {
    "path": "/workspace/src/main.ts",
    "offset": 1,
    "limit": 120
  }
}

真正决定“能不能读、怎么读、读完返回什么”的,是你的应用。

在这里插入图片描述

先纠正三个误解

误解一:模型调用了函数

更准确的说法是:模型请求调用函数。

OpenAI 的 Function Calling 文档把函数定义为一种工具,它通过 JSON Schema 描述参数,模型把数据传给你的应用,由你的代码访问数据或执行动作。Anthropic 的 Tool Use 文档也把客户端工具说得很明确:Claude 返回 tool_use 内容块,你的代码执行操作,再发送 tool_result

所以不要把 Function Calling 想成:

LLM -> function()

它更像:

LLM -> 结构化调用请求
应用 -> 权限/参数/执行/日志
应用 -> tool_result
LLM -> 基于结果继续回答或继续调用工具

这个边界非常重要。因为安全、权限、幂等、审计、超时、重试,都不应该交给模型“自觉遵守”。

误解二:有 JSON Schema 就够了

JSON Schema 能约束字段类型、必填项、枚举值和嵌套结构。它能告诉模型:path 是字符串,limit 是数字,mode 只能是 readwrite

但它不能天然知道:

  • 这个路径是否在工作区内;
  • 当前用户是否允许访问这个文件;
  • 这个 shell 命令是否危险;
  • 这个订单是不是当前用户的订单;
  • 这个工具结果是否包含敏感字段。

所以 Schema 是入场券,不是安全系统。

真正的工具系统,还需要输入校验、策略引擎、风险分级、执行沙箱、审计日志和错误回填。

误解三:工具调用就是最终答案

tool_use 不是答案,它只是下一步动作。

一次真正的 Agent 任务经常是这样的:

用户:帮我看登录接口为什么 500
模型:先 grep LoginController
应用:返回匹配文件
模型:读取 Controller 和 Service
应用:返回文件内容
模型:发现空指针风险,修改代码
应用:执行 EditFile
模型:运行测试
应用:返回测试失败
模型:继续修
应用:返回测试通过
模型:总结修复点

这条链路里,模型每一步都在根据工具结果更新判断。Function Calling 只是把“下一步动作”结构化了,Agent Loop 才会让这些动作串成完整任务。

官方协议的共同骨架

不同平台的字段名不完全一样,但核心骨架很接近。

阶段做什么关键点
定义工具把工具名、描述、输入 schema 提供给模型描述决定模型什么时候用工具
模型请求模型返回工具名和参数这只是请求,不代表已执行
应用执行本地代码调用文件、命令、API 或业务服务权限、超时、幂等、审计都在这里
回填结果把 tool_result 返回给模型错误也要作为结构化反馈
继续循环模型基于结果回答或继续调工具这就是 Agent Loop 的基础

MCP Tools 规范也是类似思路:服务端暴露工具,工具有名字、描述和输入 schema;工具被设计成模型可根据上下文自动发现和调用,但应用侧仍应提供清晰的工具暴露提示、调用可见性和人工确认机制。

这说明一个共识正在形成:

Tool Use 的关键不是“模型变成执行器”,而是“模型和执行系统之间有了清晰契约”。

工具描述比很多人想得更重要

很多人写工具时,最重视 execute(),最随便写 description

这很危险。

模型决定要不要用某个工具,主要看的就是工具名、描述和参数说明。Anthropic 的 Define tools 文档明确要求 description 说明工具做什么、什么时候用、行为如何。OpenAI 的文档也把 description 描述为告诉模型何时、如何使用函数的字段。

差描述通常长这样:

读取文件

好描述应该更像这样:

读取指定路径的文本文件内容,返回带行号的文本。
适用于需要查看文件完整内容或指定行范围的场景。
路径必须位于当前工作区内,不能读取二进制文件。
大文件请先用 grep 定位,再用 offset 和 limit 分段读取。
如果文件不存在、越权或是二进制文件,返回结构化错误。

这段描述告诉模型五件事:

  • 这个工具做什么;
  • 什么时候该用;
  • 什么时候不要用;
  • 参数有什么约束;
  • 返回值和错误长什么样。

一个可维护的工具定义,至少应该包含这些信息:

字段作用示例
name工具唯一标识ReadFile
description给模型看的使用说明什么时候读文件、限制是什么
input_schema参数结构约束JSON Schema
output_contract返回值约定文本、结构化 JSON、错误码
category工具分类file、search、shell、api
read_only是否只读Grep 是只读,WriteFile 不是
destructive是否高风险Bash、删除、退款重试
concurrency_safe是否可并发多个只读搜索可以并发
timeout_ms执行上限Bash 默认 30 秒
validate()执行前校验路径、参数、权限预检

不要把这些字段看成“工程洁癖”。它们会直接影响模型路由、运行时策略和后续审计。

一个工具运行时应该拆成几层

如果只是 Demo,你可以写一个 if tool_name == "read_file"

但只要工具数量超过三五个,最好尽快拆成运行时架构。

在这里插入图片描述

我会把最小工具运行时拆成六层。

1. Tool Registry

Registry 是工具注册中心。

它负责:

  • 注册工具;
  • 按名称查找工具;
  • 按场景启用或禁用工具;
  • 把内部工具定义转换成模型 API 需要的格式;
  • 控制本轮对话暴露哪些工具。

不要每次请求都手写工具列表。工具列表应该由 Registry 统一生成。

2. Schema Validator

模型生成的参数不能直接执行。

先用 schema 做结构校验:

  • 必填字段是否存在;
  • 类型是否正确;
  • 枚举值是否合法;
  • 数字范围是否合理;
  • 字符串长度是否超限。

结构校验失败,不应该让程序崩溃,而应该返回一个可给模型理解的错误结果,让模型补参数或修正参数。

3. Policy Engine

Schema 只能管结构,Policy 管边界。

例如:

  • 文件路径是否越过工作区;
  • 写文件是否需要用户确认;
  • Bash 命令是否命中危险模式;
  • 当前用户能否调用这个业务 API;
  • 本轮是否允许暴露高风险工具。

Policy Engine 的结果最好也是结构化的:允许、拒绝、需要确认、需要降级。

4. Tool Executor

Executor 负责真正执行工具。

它要处理:

  • 超时;
  • 并发;
  • 输出截断;
  • 非零退出码;
  • 资源清理;
  • 执行异常;
  • 结果归一化。

这里有一个容易踩的点:命令返回非零退出码,不一定是系统错误。

比如测试失败、编译失败、grep 没匹配到,都可能是对模型有价值的反馈。它们应该作为工具结果返回,而不是直接抛成 Agent 崩溃。

5. Result Adapter

工具内部结果不一定适合原样给模型。

Result Adapter 负责把执行结果转换成模型能消费的形式:

  • 成功结果保留关键内容;
  • 长输出做安全截断;
  • 敏感字段脱敏;
  • 错误信息给出可恢复建议;
  • 状态未知时明确标记 UNKNOWN,避免模型瞎承诺。

6. Trace / Audit

只要 Agent 能调工具,就必须记录轨迹。

至少要记录:

  • 哪次对话;
  • 哪个模型;
  • 哪个工具;
  • 参数摘要;
  • 策略判断;
  • 执行耗时;
  • 结果状态;
  • 错误码;
  • 最终回答是否引用了工具结果。

没有 Trace,Function Calling 就很难从 Demo 走到生产。

Coding Agent 最小六件套

如果目标是做一个能改代码的 Agent,第一版工具不需要很多,但要覆盖最基本的读、搜、写、改、跑。

ReadFile:要带行号和范围

ReadFile 看起来简单,其实有三个细节:

  • 返回内容最好带行号,方便模型引用和后续 Edit;
  • 支持 offsetlimit,避免一次读爆上下文;
  • 检测二进制文件,不要把乱码塞给模型。

错误也要区分:文件不存在、路径越权、权限不足、二进制文件,不要统一返回 read failed

Glob:找文件,不要扫垃圾目录

Glob 用来找文件名和项目结构。

它应该默认排除:

.git
node_modules
target
dist
vendor
__pycache__
.idea

结果要限制数量,并按最近修改或路径稳定排序。否则一个 **/* 就能把上下文打爆。

Grep:找内容,但要控制噪音

Grep 是 Coding Agent 的眼睛之一。

它要支持:

  • 正则搜索;
  • 文件类型过滤;
  • 上下文行;
  • 最大结果数;
  • 二进制排除;
  • 输出 path:line:content

但 Grep 只知道字符串,不知道符号关系。复杂 Java/Spring 项目后续最好接 LSP,让 Agent 能查 definition、references 和 diagnostics。

WriteFile:覆盖写入要谨慎

WriteFile 适合创建新文件或完整覆盖小文件。

它至少要:

  • 创建父目录;
  • 明确是否覆盖已有文件;
  • 返回写入字节数;
  • 写前触发策略检查;
  • 写后可选返回摘要。

不要让模型随手覆盖大文件,尤其不要在没有确认的情况下覆盖配置、迁移脚本和生产资源文件。

EditFile:要求唯一匹配

EditFile 是最省 token 的修改方式。

它的核心参数通常是:

{
  "path": "/workspace/src/UserService.java",
  "old_string": "return userRepository.findById(id);",
  "new_string": "return userRepository.findByIdAndTenantId(id, tenantId);"
}

关键约束:old_string 必须唯一匹配。

如果出现 0 次,说明模型上下文过期或文件读错;如果出现多次,说明上下文不够,应该让模型提供更长片段,而不是帮它猜。

Bash:最强,也最危险

Bash 能编译、测试、安装依赖、查看环境、运行脚本。

也正因为它太强,默认应该最保守:

  • 设置工作目录;
  • 设置超时;
  • 合并 stdout/stderr;
  • 长输出截断;
  • 危险命令拦截或确认;
  • 默认不并发;
  • 记录完整命令和退出码。

对模型来说,Bash 的输出经常是最有价值的反馈。测试失败不是坏事,隐藏失败才是坏事。

流式 tool_use 为什么更麻烦

普通文本流很好处理:来了就追加。

但工具调用流不是这样。

Anthropic 的 streaming 文档里,工具输入参数会以 input_json_delta 的形式分片到达,每个 delta 只是一段 partial_json。Fine-grained tool streaming 还提醒开发者:为了降低延迟,参数流可能绕过完整 JSON 缓冲,你需要处理 partial 或 invalid JSON 的边界。

也就是说,你不能收到一小段就立刻 JSON.parse()

一个更稳的处理方式是:

content_block_start(tool_use)
  记录 id 和 name
  初始化 inputBuffer

content_block_delta(input_json_delta)
  inputBuffer += partial_json

content_block_stop
  parse inputBuffer
  生成完整 ToolUse 事件

伪代码可以这样写:

type PendingToolUse = {
  id: string;
  name: string;
  inputBuffer: string;
};

let pending: PendingToolUse | null = null;

function onContentBlockStart(block: any) {
  if (block.type === "tool_use") {
    pending = {
      id: block.id,
      name: block.name,
      inputBuffer: ""
    };
  }
}

function onContentBlockDelta(delta: any) {
  if (pending && delta.type === "input_json_delta") {
    pending.inputBuffer += delta.partial_json;
  }
}

function onContentBlockStop() {
  if (!pending) return;

  try {
    const input = JSON.parse(pending.inputBuffer);
    emitToolUse({
      id: pending.id,
      name: pending.name,
      input
    });
  } catch (error) {
    emitToolUseParseError({
      id: pending.id,
      name: pending.name,
      raw: pending.inputBuffer,
      message: String(error)
    });
  } finally {
    pending = null;
  }
}

这里最重要的是两个字:缓冲。

不要把流式 tool_use 当普通文本;也不要假设模型输出的每个参数片段都是可解析 JSON。

错误也是工具结果的一部分

工具系统里,一个成熟设计是:把可恢复错误返回给模型,而不是让 Agent Loop 崩掉。

例如 ReadFile 返回:

{
  "is_error": true,
  "code": "FILE_NOT_FOUND",
  "message": "文件不存在:/workspace/config.yaml。可以先用 Glob 搜索 *.yaml。"
}

这比抛一个内部异常更有用。

模型看到这个结果,可能会继续调用 Glob 找配置文件;看到权限拒绝,它应该停止;看到参数缺失,它可以追问;看到测试失败,它可以根据报错继续修。

我建议至少把工具结果分成这几类:

状态含义模型应该怎么做
SUCCESS工具执行成功基于结果继续
VALIDATION_ERROR参数结构不对修正参数或追问
POLICY_DENIED权限或策略拒绝停止绕路,向用户说明
EXECUTION_ERROR工具执行失败但状态明确根据错误调整
TIMEOUT执行超时不要假装成功
UNKNOWN写操作状态不确定等待确认,不要重试高风险动作

如果只返回一段模糊的 failed,模型很容易瞎补。

在这里插入图片描述

一套最小落地清单

如果你正在给自己的 Agent 加工具系统,可以按这个顺序做:

  1. 先定义工具接口,不要只写函数。
  2. 每个工具都写清 namedescriptioninput_schemaread_onlydestructivecategory
  3. 工具描述里写明“什么时候用、什么时候不用、参数限制、返回格式、错误格式”。
  4. Registry 统一生成模型 API 需要的工具定义。
  5. 执行前先做 schema 校验,再做 policy 校验。
  6. 写操作和 Bash 默认需要更严格的确认或沙箱。
  7. 工具错误返回给模型,但要结构化,不要只给 failed
  8. Bash 非零退出码通常是工具反馈,不要直接当程序崩溃。
  9. 流式 tool_use 要缓冲 JSON 片段,到 block stop 再解析。
  10. 记录 Trace,至少覆盖工具名、参数摘要、策略判断、结果状态和耗时。
    在这里插入图片描述

面试或技术分享可以怎么讲

如果被问到“Function Calling 是怎么工作的”,不要只说“模型会调用函数”。

可以这样回答:

Function Calling 的本质是模型和应用之间的结构化动作协议。应用先把可用工具的名称、描述和 JSON Schema 发给模型;模型根据上下文输出工具调用请求,包括工具名和参数;应用拿到请求后做参数校验、权限判断和真实执行;再把 tool_result 返回给模型。模型不会直接执行函数,它只是提出调用意图。真正的执行边界、错误处理、审计和权限控制都在应用侧。

如果继续问“工具系统怎么设计”,可以补一句:

工具不应该只有 name 和 execute。生产级工具至少要有 description、input schema、只读/破坏性标记、分类、超时、并发安全、validate 和结构化 ToolResult。工具描述影响模型选工具,元信息影响运行时策略,ToolResult 影响 Agent Loop 的恢复能力。

这比“给模型挂几个 API”更接近真实工程。

总结

Function Calling 让 Agent 有了“动手”的入口,但它不是魔法。

模型仍然只是在生成内容,只是这次生成的是结构化调用请求。你的应用才是真正的执行者。

所以一个靠谱的工具系统,重点不是能不能把某个函数暴露给模型,而是能不能回答这些问题:

  • 模型知道什么时候该用这个工具吗?
  • 参数结构能被校验吗?
  • 工具执行前有权限和风险判断吗?
  • 错误能作为反馈回到模型吗?
  • 流式 tool_use 能稳定解析吗?
  • 工具调用轨迹能被审计和回放吗?

一句话记住:

Function Calling 的核心不是“让模型执行函数”,而是“让模型提交动作请求,让应用受控执行”。

参考资料

  • OpenAI API Docs:Function Calling
    https://developers.openai.com/api/docs/guides/function-calling
  • Anthropic Claude Docs:Tool use overview
    https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview
  • Anthropic Claude Docs:Define tools
    https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools
  • Anthropic Claude Docs:Fine-grained tool streaming
    https://platform.claude.com/docs/en/agents-and-tools/tool-use/fine-grained-tool-streaming
  • Anthropic Claude Docs:Streaming messages
    https://platform.claude.com/docs/en/build-with-claude/streaming
  • Model Context Protocol Specification:Tools
    https://modelcontextprotocol.io/specification/2025-06-18/server/tools
  • Spring AI Reference:Tool Calling
    https://docs.spring.io/spring-ai/reference/api/tools.html
  • LangChain4j Docs:Tools Function Calling
    https://docs.langchain4j.dev/tutorials/tools/
  • JSON Schema:Object reference
    https://json-schema.org/understanding-json-schema/reference/object
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值