这是理解 agent 的第一块基石,也是最常被误解的一点。function calling(函数调用)听起来像模型在调用函数,实际发生的是:
模型生成了一段结构化文本,内容是「我想调用 get_weather,参数是 {"city": "北京"}」。仅此而已。它没有联网,没有访问数据库,没有执行任何代码。真正去执行的是你的程序——你解析这段输出,决定要不要执行、以什么权限执行,然后把结果作为新的一轮输入送回模型。
这个认识有一个直接的工程推论:agent 的安全性完全由你在③处的实现决定,与模型有多「听话」无关。再好的 prompt 也只是降低模型提出危险请求的概率,而③处的权限校验才是真正的保证。
模型凭什么知道该调哪个工具、参数怎么填?只凭你给的那段 schema——工具名、描述、参数名、参数描述。它不能读你的源码,不知道你的业务约定,也没有试错的机会。
所以 tool schema 不是接口文档,它是 prompt 的一部分,应该按写 prompt 的标准去写,而不是按写 API 文档的标准。这个视角转换能解决大部分工具调用失败:
| 要素 | ✗ 常见写法 | ✓ 更好的写法 |
|---|---|---|
| 工具名 | qry_usr_ord_v2 | get_user_orders——用模型能理解的自然命名,缩写和版本号只会制造歧义 |
| 描述 | 「查询订单」 | 「查询指定用户的历史订单列表。仅返回近 12 个月内的订单;查单笔订单详情请用 get_order_detail」——写清边界和与相邻工具的区别 |
| 参数 | status: string | status: enum["pending","shipped","done"]——能用枚举就别用自由文本,这一条改动往往能消除大半参数错误 |
| 必填项 | 全部可选,缺失时报错 | 明确标出必填;对可选参数说明默认行为 |
| 错误返回 | {"error": "invalid input"} | 「city 参数无效:'北平'。有效值为中国城市的现用名称,如 '北京'」——错误信息是模型下一轮的输入,要写成教学材料 |
最容易被忽略的一条:错误信息是给模型看的。传统 API 的错误信息写给开发者,可以简略;agent 的错误信息会直接进入模型的下一轮上下文,成为它自我纠正的唯一依据。一句「参数错误」让模型只能瞎猜;一句「日期格式应为 YYYY-MM-DD,你传的是 '下周三'——如需相对日期请先调用 get_current_date」则几乎必然让它一次改对。把错误信息当 prompt 写,是投入产出比最高的 agent 优化之一。
① 为模型设计,不要镜像内部 API。内部 API 是为程序员设计的,往往粒度细、参数多、需要多步组合。直接暴露给模型,等于要求它现场学习你的系统架构。更好的做法是包一层面向任务的工具:与其给出 get_user + get_orders + get_shipping 三个工具让模型自己拼,不如提供一个 get_order_status(order_id) 直接返回它需要的全部信息。每减少一次模型的推理跳跃,就减少一处出错的机会。
② 工具数量要控制。工具越多,选错的概率越高,且每个工具的 schema 都要占用上下文(并影响 2.2 的缓存前缀长度)。当工具超过十几个时,应考虑分组按需加载——先让模型选择任务类别,再只加载该类别下的工具。
③ 消除工具之间的语义重叠。如果两个工具的描述让模型难以区分该用哪个,那么错误就是必然的,且这类错误无法靠 prompt 修好。重叠是设计问题,要在 schema 层面消除——合并、或在描述里写明「什么时候用这个而不用那个」。
背景:一个内部运维 agent,把已有的运维 API 逐个包装成工具暴露给模型,共 18 个。上线后调用成功率始终不理想,团队反复调整 system prompt 收效甚微。
诊断:把失败轨迹按错误类型归类后,问题分布得很清楚——错误集中在三类,而没有一类是「模型不够聪明」:
三处针对性修改:
① 合并重叠工具:三个查询工具合并为一个,用 resource_type 枚举参数区分。模型不再需要在语义相近的选项间做判断——把「选择」变成「填参数」,难度下降一个量级。
② 参数改枚举 + 结构化:时间参数从自由文本改为 {"range": enum["last_1h","last_24h","last_7d"]},覆盖 90% 的真实用法;确需精确时间时用单独的可选字段。枚举把「模型要生成正确格式」变成了「模型只需选一个」。
③ 为高频任务提供组合工具:把那个需要 4 步的常见诊断流程,包装成单个 diagnose_service(service_name),内部由代码按固定顺序编排。确定性的流程就该用代码写死,不该让模型每次现场规划——模型只在真正需要判断的地方发挥。
18 个工具最终收敛到 6 个。整个过程没有更换模型、没有微调、system prompt 基本没动。
可迁移的判断:当 agent 表现不佳时,团队的第一反应通常是「调 prompt」或「换更强的模型」,但很多失败的根因在工具集的设计里。一个有效的自检问题是:把这套工具的 schema 打印出来交给一个不了解你系统的新同事,他能只凭这些描述正确完成任务吗?如果不能,模型也不能——它掌握的信息不会比这更多。
Schema 格式:主流 API 普遍以 JSON Schema 描述工具参数。多家提供「严格模式」(strict / structured outputs),可在解码层保证输出符合 schema——这类似 3.4 提到的约束解码,能从机制上消除结构性错误,能开就开。
并行工具调用:多数模型支持一轮返回多个工具调用请求。若这些调用彼此独立,并行执行可显著降低总延迟(见 4.2 关于轮数放大延迟的讨论)。注意你的执行层要真的并发执行,否则这个能力白给。
MCP(Model Context Protocol):把工具定义与实现从应用中解耦的开放协议,让同一批工具可被不同的 agent 应用复用。适合工具需要跨多个应用共享的场景;单一应用内的少量工具直接实现更简单。
本地/开源模型:工具调用能力差异比通用能力差异更大。小模型常见的失败是「生成了正确 JSON 但选错工具」或「参数幻觉」。若必须用小模型,严格模式 + 极少的工具数 + 全枚举参数是可行的组合。
务必记录的可观测数据:每次工具调用的名称、参数、返回、耗时、成功与否,以及失败后模型的下一步动作。这是 agent 排查的第一手材料,也是 4.5 轨迹评估的数据基础——不记录,后面所有分析都做不了。
不准确之处:模型没有、也不可能「调用」任何接口。它只是生成了一段文本,内容是「建议调用 delete_records,参数为 ...」。真正执行删除的是应用代码——是它读取了这段输出,并选择了执行。
正确表述:「应用代码在没有充分校验的情况下,执行了模型建议的删除操作。」
为什么这个区分很重要:它决定了防护点在哪。如果认为「模型删了数据」,修复方向会是「改 prompt 让模型别这么做」——这只能降低概率,永远无法保证,而且在 prompt injection 面前完全失效(6.2)。
正确的防护点全在执行侧:① 最小权限——agent 使用的凭证本身就不应具备删除生产数据的权限;② 危险操作需二次确认——不可逆操作必须由人确认,而不是由模型判断该不该做;③ 参数白名单与范围校验——限制作用范围;④ 可审计——全部工具调用留痕,可回溯可回滚。
一句话:把模型的输出当作来自不可信用户的请求来对待,你为普通用户输入做的所有校验,一个都不能少。
为什么 prompt 不管用:模型选工具时的主要依据是工具自身的 schema——名称与描述,它们与查询的语义相关性决定了选择。system prompt 里的「注意区分 A 和 B」是一句远离决策点的抽象指令,很容易被具体的语义相似度压过去。在错误的层面上修问题,只能靠概率,修不干净。
按优先级的修法:
① 合并。最彻底的解法——如果两个工具真的相近,就合并成一个,用枚举参数区分。把「选择题」变成「填空题」,问题从根上消失。
② 在描述里写明分界。不能合并时,在每个工具的描述里明确写出边界与对照:「查询订单列表用本工具;查询单笔订单详情请用 get_order_detail」。相互引用的描述比孤立的描述有效得多。
③ 重命名。让名称本身携带区分度:search_orders_by_user / get_order_by_id 一眼可辨,胜过 query_orders / fetch_order。
④ 分组按需加载。工具很多时,先判断任务类别,只加载相关工具——模型面对的选项少了,选错的机会自然少。
通用原则:能在 schema 层面消除的歧义,不要留给 prompt 去弥补。结构约束优于语言劝导——这与 3.4 中「格式问题用约束解码而非微调」是同一条思路。
execute_sql(query: string),让模型自己写 SQL 查数据库。评审这个设计。优点:极其灵活,一个工具覆盖所有查询需求,无需为每种查询单独建工具。在只读、内部、低风险的数据探索场景下,这是一个合理甚至优秀的设计。
但作为面向用户的生产设计,它有四个严重问题:
① 权限边界完全消失。一个能执行任意 SQL 的工具,其权限等于该数据库连接的全部权限。模型写出 DROP TABLE 或跨租户查询时,你没有任何结构化的手段去拦截——只能做字符串匹配,而这是出了名的拦不干净。
② prompt injection 的直通车。如果查询涉及用户可控的内容,攻击者可以诱导模型生成任意 SQL。这把一个 LLM 问题直接升级成了 SQL 注入(6.2)。
③ 正确性无法保证。模型需要准确掌握表结构、字段含义、join 关系与业务约定。schema 稍复杂,它写出的 SQL 就可能语法正确但语义错误——返回一个看起来合理的错误数字,比报错危险得多。
④ 性能不可控。模型可能生成全表扫描或笛卡尔积,拖垮生产库。
改进方案:① 参数化的任务级工具——把真实需要的查询固化成 get_sales_by_region(region, month) 这样的工具,SQL 由代码写死,模型只填参数。这解决全部四个问题,是绝大多数生产场景的正确答案。② 若确需灵活性:只读副本 + 只读账号 + 查询超时 + 结果行数上限 + 表/列白名单 + 全量审计日志,并把它限制在内部可信用户使用。③ 折中:让模型从预定义的查询模板中选择并填参数——保留部分灵活性,同时保住可控性。
贯穿的判断:工具的能力上限就是攻击面的上限。设计工具时问一句「如果模型完全被攻击者控制,这个工具最坏能造成什么后果」——答案不可接受,就该收窄它。