Function Calling 决定 AI Agent 上限:工具层设计实战指南
2026/9/12 4:40:42 网站建设 项目流程

很多人做 AI Agent 的第一感受是“模型不够聪明,所以效果不好”。我踩了大半年这个坑之后想告诉你一个反直觉的结论:在绝大多数真实业务场景里,决定 Agent 上限的并不是底层模型的智力,而是工具层设计。这里的“工具层”,就是现在被反复讨论的 Function Calling / Tool Use——让大模型把用户的一句话,翻译成一段能被代码执行的“函数调用意图”,再由外部系统完成真实动作,最后把结果交回给大模型继续推理。可以毫不夸张地说,凡是宣称“能干活”的 Agent,底层基本都绕不开这套机制。

这篇文章我会从 Function Calling 的底层原理、工具 Schema 设计、生产环境中的异常处理、Multi-Agent 与 MCP 协议,以及上线之后的测试与成本控制这几个维度完整拆一遍。如果你正准备做 AI Agent 开发、在做技术选型、或者正在被 AI Agent 面试题折磨,这篇文章大概率比你看一堆碎片化教程更有用。

1. 为什么说 Function Calling 是 Agent 从“聊天玩具”跨向“能干活”的那道门槛

1.1 大模型的三个先天边界

我先把观点放这儿:只要你的 Agent 还在“生成文字”,它就还是个聊天机器人。要让它真正去订票、查库存、改数据库、发通知,就必须先承认大模型有三个绕不过去的边界。

第一,知识截止边界。模型训练完之后,它的世界就静止了,训练数据之外的事件一概不知道。你可以通过提示词引导它说得像模像样,但没法靠“想”就知道今天上海的温度。

第二,私有数据和业务系统边界。模型见过公开互联网,但没见过你公司的订单表、CRM、内部 API。你在 System Prompt 里写得再细,它也不可能凭空知道某个订单的最新状态。

第三,精确计算和操作边界。让模型连续做一百次乘法,很容易在中间某一步出错;让它“删除 A 用户并迁移他的所有订单”,它没有这只“手”。

所以你可以看到,RAG 解决的问题是“让模型知道更多”,Function Calling 解决的问题是“让模型能做更多”。这两者经常被并列提起,但本质目标完全不同。RAG 是一次性的读取,工具调用则是带着意图去操作系统并拿到反馈,形成一个闭环。

1.2 Function Calling 并不是“函数在调用”

这个名字很容易让人误解,以为大模型内部真的把一段 Python 函数执行了一遍。不是的。

从技术上说,Function Calling 只是大模型的一次“结构化输出”:服务端在请求里传入一批工具定义(工具名、描述、参数 JSON Schema),模型读完用户问题后,如果判断需要调用某个工具,就输出一个结构化对象,里面包含“我要调谁”和“参数是什么”。模型本身不执行任何函数,真正执行的是你身边那套代码。

打个生活化的比方:你请了一个前端客服,他没权限操作后台,但他可以填写一张“工单”,上面写清楚“帮我查订单 A 的物流状态”,然后你负责执行工单的同事拿到后去后台查,查完把结果写在回执上递给他。客服做的事就是 Function Calling 里的“输出意图”,执行的同事就是你代码里的函数本体,回执就是工具结果消息。

这个比喻能解释八成的初学者困惑:为什么模型会“调错参数”?因为它是在根据概率“填工单”,不是真的理解系统内部逻辑。它可能把城市名写成别名、把 ID 传成字符串,这些都需要你这一侧做校验和纠错。

1.3 一堆相近概念到底怎么区分

我在跟团队对齐时经常发现,Function Calling、Tool Use、Tools、Plugin、MCP 这几个词混着用,导致讨论总是鸡同鸭讲。我的理解里可以这样分层:

术语我的定义典型出现位置
Function Calling早期由 OpenAI API 提出的一种机制,通过 tools 参数传入函数,模型返回 tool_calls 结构化结果API、模型服务端
Tool Use / Tool Calling各家模型与框架对上述机制的通用叫法,现在基本当同义词用Claude、Gemini、LangChain 等
Plugin / Tools偏产品层概念,指“用户可以装配的一堆外部能力”ChatGPT Plugin、各类 Agent 产品
MCP协议层,统一工具描述、发现、鉴权与调用方式,底部仍是 Function Calling 的包装Model Context Protocol

面试里被问到这些问题,建议把术语先拆清楚,别把协议和机制混在一起讲,否则很容易被追问到卡壳。

2. 拆开原理:模型到底是怎么调用工具的

这一章我直接讲运行链路,带你从请求发起一路看到模型输出最终回答。我以 OpenAI 风格 API 为例来说明,其他家(包括 Claude、Gemini 和大量国内模型)的返回结构基本一致,只是字段名略有差异。

2.1 完整的消息循环

先看一个最简单的 Agent 循环:

  1. 用户发送问题,例如“上海现在多少度?”
  2. 请求里带上tools(工具定义列表),模型返回一个assistant消息,该消息不包含正常文本,而是包含一个tool_calls数组。
  3. 你的代码遍历tool_calls,解析函数名和参数,调用真实函数拿到结果。
  4. 将结果作为一条role: "tool"的新消息发回给模型,同时把原始assistant消息也保留在历史里。
  5. 模型看到工具结果,生成最终回答:“上海当前气温 28 度,湿度 83%。”

这个循环可以重复多次:比如 Agent 先查天气,再根据天气订机票,再根据机票结果安排日程。每一步都是“模型输出工具意图→代码执行→把结果喂回去”的轮转。

里面对应的一段核心消息序列,我给你看 JSON 更直观。第一步,模型返回的 tool_calls:

{ "role": "assistant", "content": null, "tool_calls": [ { "id": "call_abc123", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\": \"上海\"}" } } ] }

第二步,代码执行后返回的工具结果:

{ "role": "tool", "tool_call_id": "call_abc123", "content": "{\"temperature\": 28, \"humidity\": 83, \"condition\": \"多云\"}" }

这里有几个坑,光看文档容易踩。tool_call_id必须和上一步完全一致,否则 API 直接报错。arguments在传输层是字符串,不是对象,你必须先json.loads再传参。工具结果的content可以是普通文本,也可以是 JSON 字符串,但我建议统一成结构化格式,方便模型后续推理。

第二步之后如果有第三步,你只要把前两步的消息都放在历史里,再让模型继续生成即可。不要在这里自作聪明地折叠历史,否则模型会丢失“自己刚刚调过工具”的上下文。

2.2 工具定义 Schema 的写法

工具怎么被模型“看见”,取决于你传的tools数组。一个常见的最小示例:

[ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市当天的实时天气信息,包括温度、湿度、天气现象。当用户询问天气或出行建议时使用。", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市中文名或行政区划名,例如:上海、北京市、杭州。" } }, "required": ["city"] } } } ]

这个简单的结构里,namedescriptionparameters三个字段,哪个没写好,效果都会崩。我见过最多的问题是description写得太含糊,比如“天气工具”,模型在多工具之间就无法判断到底该调它还是调另一个。更糟糕的是参数描述也不写,大模型根本不知道city该传什么格式。

2.3 tool_choice 到底怎么选

tool_choice是控制模型调用工具行为的参数,面试官非常爱问:

  • "auto":模型自行决定调用哪个工具或不调用。日常用户对话、意图不明确时用这个。
  • "none":禁用工具,只当普通聊天模型用。比如闲聊机器人就不该让它调数据库。
  • "required":强制模型必须调用工具。适合客服机器人开场就需要查询用户信息的场景。
  • 指定某个函数名:直接锁定某个工具,比如命令模式下只允许调get_order_status

实际项目里,我的经验是:尽量用auto,在非常确定的子流程里才用required或指定函数名。原因是“强制”虽然让流程稳定,但会牺牲灵活性。比如用户只是问“在吗”,你强制查询订单状态,模型会非常痛苦。

2.4 并行调用与采样参数

大模型可以一次返回多个tool_calls,这就是并行工具调用。并行能大幅降低延迟,适合多个互相独立的工具。但注意:如果工具 B 依赖工具 A 的输出,你不能让它们并行,必须等 A 执行完拿到结果后,第二轮再触发 B。

temperature在工具调用场景里建议调低,0 到 0.3 之间比较合适。原因很直接:工具调用本质是“结构化决策”,我们不需要模型有天马行空的创意。温度太高,模型容易把参数名写错、凭空多编字段。另外,不少新模型在tools参数里加了strict模式,要求你严格校验 Schema,这个细节我放下一章展开。

3. 工具定义的艺术:决定 Agent 智商上限的隐藏变量

3.1 一个让我彻底改观的小实验

去年我在一个电商客服 Agent 里调试,发现模型经常把“查订单”和“查物流”两个工具搞混。明明两个函数内部逻辑完全不同,模型却频繁用错。后来我把两个工具的 description 彻底重写了——“查订单”强调返回订单状态、金额、商品清单,用于订单售后;“查物流”强调返回快递轨迹、配送节点,用于包裹进度查询——结果准确率一下从 80% 出头提到了 94%。

这个案例告诉我:工具层决定的不是“能不能调”,而是“调得准不准”。同样的模型、同样的函数实现,效果可以差很多,差距基本都在写法上。

3.2 命名是一门学问

工具名是模型决策时最显眼的特征。我建议:

  • 动词开头,表达清晰的动作,如get_order_statuscreate_refund
  • 名字里带上对象域,比如get_user_balanceget_balance更不容易混淆。
  • 避免缩写和无意义词。工具名是给模型读的,不是给内部程序员省字用的。
  • 如果工具名出现歧义,优先改名或合并文档,而不是靠 description 里加一句“不要和 XX 混淆”。

真实反例:我见过有人把所有工具起名叫queryupdatedelete,然后靠 description 区分。模型很快就晕了,因为它做选择的时候,名字本身就是最重要的“注意力焦点”之一。

3.3 Description 要回答四个问题

一段好的工具 description,至少要让模型知道:

  1. 这个工具是干嘛的,属于什么业务场景。
  2. 什么时候该用它,什么时候不该用。
  3. 每个参数的含义、格式、取值范围。
  4. 返回值大概长什么样,或者会有什么副作用。

说得直接一点,description 就像你给一个新同事写的“操作手册”。不要惜字如金,也不要写一大段小说。我常跟团队说:好的 description,要让人只看描述不看代码,就能正确调用这个函数。模型本质上是在“读手册做选择题”,你得把选项讲清楚。

3.4 参数 Schema 的严谨细节

参数 Schema 直接决定模型有没有机会“瞎编”。几个关键细节:

  • enum约束枚举值,比如status只允许pending|paid|shipped|canceled
  • description写清楚单位、格式、边界,比如“时间格式 YYYY-MM-DD”。
  • 不要所有字段都可选。能requiredrequired,可选字段过多时模型会漏传。
  • 有些 API 支持strict: true(例如 OpenAI 的 Structured Outputs),它会要求模型严格符合你给的 Schema 和类型。虽然会限制部分灵活结构,但在工具调用场景里我强烈推荐开启,因为参数合法性是后续所有逻辑的地基。

3.5 工具数量与“工具选择噪声”

很多人认为工具越多越好,实际并非如此。工具数越多,模型在每一步的选择空间越大,选错的概率随之上升,而且每次请求都要把所有工具定义全量发给大模型,token 成本也会上涨。我见过一个项目把几十个工具一股脑塞给模型,后面的工具基本已经“看不见”了。

解决方案有几个:

  • 把工具按领域分组,拆给多个子 Agent(Multi-Agent)各管一组。
  • 用路由工具先粗分类,再进对应的子工具集。
  • 精简工具数:能合并成一个语义清晰的工具,就不要拆成两个。

这一部分对实际项目的影响,往往比选哪个模型更明显。模型可以在很多开源模型里挑,工具定义的质量却只能靠你自己一遍遍打磨。

4. 工具调用真正的麻烦在于失败:错误处理、重试与安全护栏

看到这个标题,说明你在做真实项目,而不只是跑 Demo。工具调用在 Demo 里总是顺利的,但在生产环境,工具会超时、会返回脏数据、会缺少权限,模型也会吐出非法参数。这一章我按“失败发生后的完整链路”来讲。

4.1 工具执行失败的四种常见场景

按我遇到过的频率排序:

  1. 参数非法:模型传了个不存在 ID、日期格式不对、枚举值不匹配。
  2. 外部依赖失败:第三方 API 超时、限额超了、上游系统返回 500。
  3. 业务规则拒绝:工具逻辑执行到一半发现不满足条件,比如余额不足。
  4. 安全与权限拒绝:用户没有操作某资源的权限,或操作属于高风险动作需要审批。

每一种场景,给模型回传的错误信息都应该不一样。不能一律抛一个异常字符串,模型看到后会一头雾水,不知道是该换参数重试,还是该直接向用户道歉。

4.2 把错误“讲给模型听”

工具返回的错误内容会自动作为上下文进入模型下一轮推理,所以错误信息的质量直接影响模型能不能“自救”。我的三条经验:

  • 错误信息要口语化、可执行。例如“订单 ID 不存在,请让用户确认订单号后重试”,而不是OrderNotFoundError: code 404
  • 不要重复堆一长串 stack trace。模型不需要知道内部异常栈,需要的是“下一步该怎么处理”。
  • 把“可修复的失败”和“不可修复的失败”区分开。可修复的失败(参数不完整)要让模型补充信息再来一轮;不可修复的失败(用户无权限)要让它直接告诉用户,而不是无限重试。

举一个典型写法。当工具返回“订单 ID 不存在”时,错误消息可以写成:

{ "status": "error", "error_type": "NOT_FOUND", "message": "没有找到订单号为 ORD_20240101_888 的订单,请向用户确认是否正确,或建议用户重新查询订单列表。" }

4.3 防呆逻辑:最大迭代、死循环与极端重试

工具调用出现死循环太常见了:模型反复调用同一个失败工具、反复重试同一句回答。我在工程里一般设置最大工具调用轮数(比如 8 轮),超过后强制终止并把当前状态汇总给用户。更精细的话,可以设置“同一个工具连续失败 N 次后不再调用”,或者“同一工具在最近 M 轮内调用超过 K 次就降级”。

重试策略要区分:网络类失败(超时、连接断开)值得自动重试,业务类失败(被业务规则拒绝)不值得重试,因为重试也不会改变结果,只会浪费 token。

4.4 工具输出过长与上下文爆炸

一个检索工具可能返回几十页的 PDF 内容,直接塞进消息历史会把上下文窗口撑爆,也把后续轮次的成本抬高。我常用的处理手法:

  • 截断:取前 N 个字符,并在末尾提示“内容过长已截断”。
  • 摘要:另起一轮让一个小模型把工具输出摘要成结构化信息,再把摘要回传。
  • 结构化:只保留关键字段,忽略大段非必要文本。

这一块没有银弹,但在接数据库和文档类工具时几乎必做。

4.5 别让模型裸奔在真实世界里

工具调用本质是把决策权交到大模型手里,那就要考虑安全边界。我上生产前至少会做这几件事:

  • 对每个工具声明“可写操作”还是“只读操作”。只读工具可直接自动执行,可写工具必须走审批或二次确认。
  • 在工具执行前做参数白名单校验:比如金额上限、时间窗口、允许操作的资源范围。
  • 对高权限工具加“人工审核”节点:下单、转账、删除、发送对外消息,属于高危动作。
  • 不要信任模型生成的内部 ID 和枚举值,一定要在服务端再做一遍合法性校验。

这些听起来繁琐,但一旦 Agent 被放在无人值守的自动化流程里,安全设计就是最贵的保险。

5. 从单 Agent 到 Multi-Agent:工具共享、编排模式与 MCP 协议

5.1 为什么单 Agent 撑不住复杂场景

单个 Agent 一旦要负责太多领域,工具会越来越多,模型每次看到的工具列表越来越长,选择越来越不准,上下文压力也越来越大。我在做企业助手时,早期一个 Agent 挂了二十几个工具,效果一直上不去。后来把“订单相关工具”拆成订单 Agent、“库存相关工具”拆成库存 Agent,让一个 Router Agent 先判断该把请求转给谁,效果立刻好转。

5.2 编排层里的工具分配

这里必须提到 LangGraph、Spring AI Multi Agent 这些思路。LangGraph 用图结构把节点(Agent/工具/条件判断)串起来,你可以在节点内定义不同的工具集,让 Agent 只看到自己负责的那部分;Spring AI Multi Agent 是 Java 生态里做的多 Agent 编排,思路类似,把大任务拆给不同 Agent 并协调工具调用。无论框架叫什么,核心都是一件事:让每个 Agent 只看到自己需要的工具,减少选择噪声,提升准确性。

编排模式我常用的有三类:

  • 路由器模式(Router):入口 Agent 根据用户意图,把任务转给某一个专业 Agent。
  • 层次化模式(Hierarchical):上层 Agent 拆解任务,分发给下层多个 Agent 并行执行,再汇总。
  • 流水线模式(Pipeline):按固定顺序走多个 Agent,前一个 Agent 的输出作为后一个 Agent 的输入。

为了方便理解,我举一个最简单的调度伪代码:

if intent == "order": return order_agent.run(user_query, tools=order_tools) elif intent == "inventory": return inventory_agent.run(user_query, tools=inventory_tools) else: return general_agent.run(user_query, tools=[])

这样每个 Agent 的工具列表都短,模型选择准,调试也容易。

5.3 MCP 协议在解决什么

MCP(Model Context Protocol)是最近非常热的概念,热度不比 Agent 本身低。它的核心诉求是:把“模型怎么访问工具/数据”这个动作标准化。在没有 MCP 时,你想接入一个数据库、一个文件系统、一个第三方服务,每个都需要为当前框架写一套适配器,切换框架又要重写。有了 MCP 后,服务方只暴露一个 MCP Server,Agent 平台按照 MCP 客户端的方式去发现和调用工具,理论上“一次接入、到处复用”。

注意一点:MCP 并没有发明新的“模型调用工具”机制,它本质上仍是工具定义和调用结果的标准化包装,最终的 Function Calling 还是要经过大模型的输出。只是接入、发现和授权变得有标准了。

5.4 我建议怎么落地 MCP

如果你正在搭建工具层,我觉得不需要一上来就全面切到 MCP。先跑通原生 Function Calling,把业务闭环验证好;等需要统一接入多个企业系统(数据库、飞书、JIRA、内部平台)时,再用 MCP Server 把它们包起来。这样不会为了追概念而增加不必要的抽象层,也更方便团队维护。

6. 从 Demo 到生产:延迟、成本、测试与可观测性

6.1 延迟预算:一次工具调用其实要两次模型往返

你在 Demo 里点击一次,感觉很快,但工具类 Agent 的延迟模型是相乘的:一次交互至少包含“模型生成工具意图→工具执行→模型生成最终回答”两个大模型往返。每一步都可能几百毫秒到两三秒,所以一次复杂的 Agent 对话很容易上十秒。

优化手段:

  • 能并行就并行,独立工具同时发起。
  • 减少自问自答的思维链空隙,在工具调用上尽量一次到位。
  • 用快模型做意图路由,用强模型做最终生成。
  • 对工具结果做缓存:同样的查询在短时间窗口内直接复用。

6.2 成本构成比你想的更“烧钱”

工具调用的成本不只在“调那一下”,还包括:

  • 每次请求都要把所有工具 Schema 拼进历史,一次性几十上百行。
  • 工具执行结果作为历史回传,下一轮继续烧 token。
  • 如果做了摘要(上文说的“再调一个小模型摘要”),又是一次计费。
  • 多轮工具循环下来,单次会话的 token 消耗比纯聊天高一个量级。

我的成本控制三板斧:工具 Schema 做精简(description 精炼但不失准确),工具结果能截断就截断,能缓存就缓存,尽量不引入无谓的第二轮小模型。

6.3 怎么测试工具调用系统

很多人觉得 LLM 应用没法测,实际上可以测,关键是把“变的部分”和“不变的部分”分开。

  • 单元测试:测每个工具函数本体,和普通后端没有区别。
  • Schema 测试:用 JSON Schema 校验器,检查工具定义是否合法、required 是否完整。
  • 消息序列测试:写死一段用户问题和工具调用历史,断言模型是否选择了预期的工具。这步不稳定,但可以作为回归信号。
  • 评估集:收集真实对话中“理应调用某工具”的样本,跑一遍全链路,统计工具调用准确率和最终答案正确率。

评估集是这中间最重要的部分。没有评估集,你改一次 description、改一次 schema,都不知道是变好还是变坏。

6.4 监控与可观测性

生产环境的 Agent,我至少盯着这几类指标:

  • 工具命中率:一次会话中工具调用占比,过低说明工具没被用上。
  • 工具失败率:调用之后出错的比例,按工具维度拆。
  • 重试率与轮数:代表 Agent 卡住的频率。
  • Token 消耗:按会话维度预估成本。
  • 用户最终满意度修正:用户是否重复提问、是否转人工。

这些指标一定要按“工具维度”和“意图维度”拆开,不要只看总均值,否则所有问题都被平均数掩盖。

7. 面试与团队落地:Function Calling 高频考点与我的回答思路

7.1 我在面试里最常问到的几个题

“AI Agent 面试题”最近特别火,很多人在准备这类面试,我把高频问题列出来:

  • Function Calling 和普通 Prompt 要求输出 JSON 有什么区别?
  • 工具调用失败后应该怎么处理?
  • 多个工具之间互相依赖时,怎么编排?
  • 上下文超长怎么破?
  • 如何设计一套不容易选错的工具集?
  • MCP 协议和 Function Calling 是什么关系?
  • 如何评估工具调用的效果?

7.2 面对这些题,一个能打通的答题框架

在这些问题里,我一直推荐一个答题框架:先分层(协议层、执行层、业务层),再理链路(意图→工具选择→执行→结果回传→综合回答),最后谈机制(重试、安全、缓存、成本)。

比如“Function Calling 和 Prompt 输出 JSON 有什么区别”这个问题,很多人只回答“Function Calling 更稳定、有工具字段”。我会再补一层:Function Calling 本质是把“输出 JSON”这个行为变成了模型服务端的一个原生能力,你不需要在 Prompt 里展示复杂例子,模型会遵从一个显式的工具 Schema;在失败率、参数校验、并行调用、强制调用这些能力上,也比单纯 Prompt 输出要规范得多。同时,工具结果会以独立消息回传,天然支持多轮对话状态管理。

这样可以把面试问题答得更有层次,也更容易让面试官觉得“你真做过项目”,而不是只会背概念。

写在最后的一点个人体会

最后聊点我自己的偏好。我做了这么多 AI Agent 相关项目,最大的体会是:不要一开始就试图做出一个“什么都会”的超级 Agent。先让两三个工具跑通,再慢慢加;每次加工具都问自己一句:这个工具定义,模型能不能一眼看懂?团队里任何人只看工具描述,能不能正确调用?能的话,才放进去。这比选模型、比调 Prompt 都更重要——因为在这个系统里,工具层就是 Agent 的双手,手都不稳,脑子再聪明也白搭。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询