← 返回博客

写过的 Agent 越多,我越怀疑 Function Calling

AI工具

我第一次写 Function Calling 的 demo,连写带调不到半小时就搞定了。一个天气查询工具,几行 OpenAI SDK,三分钟就从 “北京今天天气怎么样” 走到了返回结果。当时觉得这玩意真顺。

后来这个项目进了生产。工具定义从 1 个变成 47 个,Provider 从 OpenAI 一个变成混着用好几家,用户从我自己变成几千个活跃账号。我花了一个月才把 agent 调用工具的准确率从 85% 推到 95%。剩下那 5%,到现在也没消失。

教科书里的 Function Calling 和你写进代码的,中间隔着一层不太好看的现实。

模型选工具,比我预想的要不靠谱

所有教程都先画那个经典流程图:用户提问、模型判断要工具、输出参数、宿主执行、返回结果。一条干净的单向管道。

实际跑起来,模型在第一步”选哪个工具”就开始出状况。

我有两个工具:search_knowledge_basequery_documents,底层是同一个向量数据库,只是检索策略不同。按设计,查考勤制度走前者,查销售数据走后者。模型经常随机给一个,有时候两个都调,然后对着两份差不多的结果愣住。我在 description 里写过无数次”这个工具用来查制度文档,不要查数据”,它当没看见。

后来换了策略:只暴露当前场景下最少的工具。 对话开始只有一个 classify_intent,它判断场景,然后只加载对应的三到五个工具。模型在小范围里选,准确率从 72% 跳到 91%。

还有一个技巧也管用:在描述里写反例。不只写”这个工具有什么用”,同时写”这个工具不适用于什么”。

获取指定股票的实时行情数据。
参数:symbol(股票代码)。
不适用于:查询历史行情、查询基金净值、查询指数信息。
用 search_historical_prices 处理这些情况。

误调用比例降了一半。

参数这东西,模型常传错

内容幻觉大家都知道,模型编假话。参数幻觉很少有人提,但在实际系统里可能更麻烦。

模型可能选对了工具,但传的参数是错的。有个例子我印象很深:一个叫 get_user_info 的工具,参数要传 user_id(六位字符串)。模型有一次传了 user_name,值填了「张三」,然后报错。报错后它换了个叫 search_users 的工具,参数传 name: "张三",查到几条结果,从里面拿了一个 user_id,又调回 get_user_info。整个过程自己走完了,没报错,但查到的根本不是同一个人。

参数幻觉的麻烦在于,如果工具链够长,模型会自动绕过去,错误被后续调用掩盖。你从用户侧只看到一个错误回答,中间发生了什么完全不透明。

解法说起来很基础:每个参数加 examples,每个必填参数加上值域校验,宿主代码做类型检查。模型输出 JSON 格式正确不意味着数值正确,宿主必须再做一层守卫。

Token 比你以为的烧得快

只在教程里调过一两次 function calling 的人,可能对 token 消耗没概念。

每次 API 调用,所有工具定义都塞进上下文。三十个工具,每个平均 150 tokens,光定义就 4500。一次两轮调用加历史消息,轻松上万 token,成本是一次普通对话的 5 到 8 倍。

ReAct 模式更贵。模型每一步都得输出思考过程,一个四步循环可能消耗 6000 到 8000 output tokens,按 GPT-4o 的价格,这部分是输入的三倍。

前面提的分层加载能省钱。还有一个没那么明显的技巧:工具返回的结果要做压缩。 很多工具返回的数据结构很完整,但模型只需要几个字段。在宿主编排层抽一次,只喂需要的字段回去,上下文能省很多。

我见过最极端的例子:一个搜索工具返回 20 条完整结果带摘要元数据,一共 8000 tokens,模型只需要前三条的标题和链接。精简后 200 tokens。

工具出错了怎么办,这事教程不教

教程里的工具永远不会失败。但真实世界里,API 超时、数据库断连、第三方 429、用户查了个不存在的东西。

问题不是错误会不会发生,是模型收到错误之后的行为。

默认行为是重试,同样参数再调一次。又失败,有些模型会换参数再试。还失败,有的模型会换工具。听起来挺智能的对吧。但我见过模型因为一个不太关键的工具连跪四次,每次写一段”抱歉我遇到了问题”然后重试。一个本该 5 秒的请求跑了 40 秒,消耗上万 token,用户最后看到的是一句”我现在无法完成这个请求”。

我后来试了一个比较直接的办法:在工具描述里写清楚失败场景的处理逻辑。 不在代码层写,在描述层写。

search_products(query, category, page)
搜索无结果时不要重试,直接返回空列表。
返回 429 时等待 2 秒重试最多一次,仍失败就告知用户服务暂时不可用。

写成自然语言的兜底逻辑,比在宿主代码里堆 try-catch 管用。因为宿主 catch 拿不到”模型想做什么”,而模型知道自己只是想查个商品。

MCP 很好,但也不是万能

Anthropic 推 MCP 的时候我激动了一下。统一协议、即插即用、生态共建,听起来像工具调用界的 USB-C。

实际用下来,MCP 解决了它想解决的问题,也带来了新的问题。

好的地方:工具标准化成 MCP Server 后,接新客户端确实简单了。不用给每个模型写适配层,MCP client 做一次转换就够。

不好的地方:MCP 的调试体验比较差。 工具调用出错时,你很难判断是协议层的问题、工具实现的问题、还是模型判断的问题。stdio 模式的 MCP Server 挂在子进程里,日志混在一起,排错觉像在黑盒里摸。SSE 好一些,但又多了网络开销和鉴权成本。

我的判断:工具不到 10 个,或者只用一个模型 API 的话,MCP 带来的额外复杂度可能超过收益。原生 API 的 tool calling 直接写更简单。团队超过 5 个人、工具超过 20 个、对接多个模型,这时候 MCP 的价值才体现出来。

一个不太好听但不得不说的话

写了半年代理系统之后回头看,Function Calling 确实是大模型从聊天机器人走向 Agent 的关键能力。只不过它”关键”的方式不太性感 — 它把复杂度从模型一侧平移到了工程一侧,而不是消灭了复杂度。

教程给你看的是那条干净的单向流水线。工程做的事完全不是那样:工具描述的每个字都得当 prompt 来磨,宿主代码里每个参数都要校验,每次调用前后要审计 token,每层抽象都不能忘了留错误恢复通道。这些事没有一件是模型帮你做的。

剩下那 5% 的成功率缺口,跟模型聪明不聪明没关系。是工程上再往上追,投入产出比已经不划算了。如果你在生产环境跑 function calling,95% 已经是一个说得过去的数字。认了,比等着模型某天突然学会自我纠错要实际。

原创技术博客 · 开源项目分享 · AI全栈创作社区 idao.fun