
1. 从“会聊天”到“能干活”工具调用到底解决了什么问题大模型刚火起来那阵子大家最直观的体验就是“它能陪我聊天”。你问它答写诗、翻译、润色、编故事样样都行。但真把它放进一个需要“动手”的场景里问题立刻就暴露了你让它帮你查一下明天某地的天气它只能凭训练数据里的旧信息瞎猜你让它帮你算一笔复杂的账它可能一本正经地给出一个错误答案你让它帮你把一段文字存进某个系统它压根没有“手”只能告诉你“我做不到”。这就是纯语言模型的天花板——它本质上是一个“下一个词预测器”所有的能力都局限在“生成文本”这个动作里。它没有实时数据没有计算器没有数据库没有外部系统的操作权限。你问它今天有什么新闻它只能基于训练截止日期之前的记忆来回答而那个日期可能已经是几个月甚至几年前了。工具使用与函数调用就是用来捅破这层天花板的。它的核心思路非常朴素既然模型自己不会查天气、不会算数、不会操作数据库那就给它配一套“外部工具”让模型在需要的时候主动“喊一声”——“我要调用查天气的工具参数是某某城市”——然后由外部的程序去真正执行这个动作再把结果塞回给模型让它基于真实结果继续对话。打个比方纯语言模型就像一个知识渊博但被关在房间里的人他读过很多书但看不到窗外也没有电话。函数调用就是给他装了一部电话和一本通讯录他可以在需要的时候拨号给“天气台”“计算中心”“数据库管理员”拿到最新信息后再继续跟你聊。这套机制的价值在于它把大模型从一个“信息孤岛”变成了一个“调度中枢”。模型负责理解你的意图、决定该用什么工具、提取工具需要的参数、解读工具返回的结果而真正的执行动作交给外部程序去完成。两者分工明确各司其职。对于正在学习这一讲的读者来说你需要建立的第一个认知是函数调用不是模型在“执行”函数而是模型在“生成一段结构化的调用意图”。真正执行函数的是你自己的代码。这个认知如果搞混了后面所有的调试都会一头雾水。这一讲的内容我会围绕“工具怎么定义”“模型怎么决定调用”“参数怎么传”“结果怎么回灌”“多轮怎么串起来”“生产环境怎么防坑”这几条线展开尽量把每一步背后的“为什么”讲透同时给出可以直接抄作业的代码骨架和参数配置。2. 工具描述文件模型眼里的“工具说明书”长什么样2.1 工具定义的三要素名称、描述、参数模式模型本身并不知道你有哪些工具可用。它之所以能“决定调用某个工具”是因为你在每一次请求里把可用工具的清单以结构化格式一起发给了它。这份清单就是模型眼里的“工具说明书”。一份标准的工具描述通常包含三个核心部分名称name工具的标识符模型在生成调用意图时会引用这个名字。命名要短、要唯一、要能自解释比如get_weather、search_flights、create_calendar_event。不要用tool1、func_a这种毫无信息量的名字模型在多个工具之间做选择时名称本身就是重要的判断依据。描述description用自然语言说明这个工具是干什么的、什么时候该用、什么时候不该用。这是整个工具描述里最容易被低估、却最影响调用准确率的部分。很多人写描述就一句话“查询天气”结果模型在用户问“明天出门要不要带伞”时根本想不到该调用它。好的描述应该写清楚功能边界、典型触发场景、返回什么信息。参数模式parameters schema用 JSON Schema 格式描述这个工具需要哪些参数、每个参数是什么类型、哪些是必填、取值范围是什么。模型会根据这个模式来提取和组装参数。下面是一个查天气工具的完整描述示例{ name: get_weather, description: 查询指定城市在指定日期的天气情况。当用户询问天气、气温、是否下雨、是否需要带伞、适合穿什么衣服等问题时使用。返回该城市当天的天气状况、最高最低气温、降水概率。, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如北京、上海、广州 }, date: { type: string, description: 查询日期格式为YYYY-MM-DD。如果用户说今天或明天请根据当前日期换算成具体日期。 } }, required: [city, date] } }2.2 描述写得好不好直接决定调用准不准我踩过的最大的坑就是早期写工具描述太随意。当时做了一个“发送邮件”的工具描述只写了“发送邮件”。结果用户说“帮我通知一下团队明天开会”模型完全没反应因为它不知道“通知团队”可以通过发邮件来实现。后来我把描述改成“向指定收件人发送邮件。当用户需要通知某人、告知信息、发送提醒、传递文件时使用”调用率立刻上来了。这里有一条经验法则工具描述要站在“用户会怎么说”的角度来写而不是站在“程序怎么实现”的角度来写。用户不会说“调用SMTP协议发送一封MIME格式的邮件”用户会说“帮我告诉老王明天别迟到”。你的描述里要包含这些口语化的触发场景。另一个常见问题是描述里塞了太多技术细节。比如“使用OAuth2.0认证的RESTful接口返回JSON格式数据”——这些信息对模型判断“该不该调用”毫无帮助反而会干扰它的判断。描述应该聚焦在“这个工具能帮用户完成什么事”上。2.3 参数模式的设计陷阱类型、枚举与嵌套参数模式看起来简单但有几个坑非常容易踩。第一个坑是类型模糊。比如日期参数如果你只写type: string模型可能会传“明天”“下周三”“2024年3月”各种格式。正确做法是在描述里明确格式要求必要时用enum限定取值范围。如果日期必须是YYYY-MM-DD格式就在描述里写死并且给一个示例。第二个坑是该用枚举却用了自由文本。比如一个“设置优先级”的参数取值只有“高、中、低”三种如果你不限定模型可能传“紧急”“一般”“普通”各种同义词。用enum限定后模型就只能从固定集合里选下游处理逻辑会简单很多。{ priority: { type: string, enum: [high, medium, low], description: 任务优先级只能是high、medium、low三者之一 } }第三个坑是嵌套对象。有些工具需要复杂参数比如“创建一个会议”需要标题、时间、参与人列表、地点等多个字段。这时候用嵌套的object和array来描述。但要注意嵌套层级越深模型出错的概率越高。我的经验是尽量把参数拍平能一层解决就不要两层。如果确实需要嵌套在描述里给一个完整的参数示例模型照着抄的准确率会高很多。2.4 工具数量与选择准确率的权衡还有一个实战中才会遇到的问题当你给模型提供几十个工具时它的选择准确率会明显下降。这很好理解就像你同时给一个人递过去三十份说明书让他快速决定用哪一份他也会懵。业界的常见做法是分层路由先用一个轻量级的分类器或者模型本身判断用户意图属于哪个大类然后只把该类下的工具清单发给模型。比如用户问的是“日程相关”的问题就只传日历、提醒、会议这几个工具而不是把天气、计算、数据库所有工具都塞进去。如果工具数量确实很多又没法分层另一个技巧是在工具描述里加入“互斥提示”。比如在“查询天气”的描述里写“本工具仅用于查询天气不用于查询航班、酒店等其他信息”帮助模型划清边界。3. 模型如何“决定”调用从意图识别到参数组装3.1 调用决策发生在哪一步很多人以为函数调用是模型“额外做了一件事”其实不是。在支持函数调用的模型里整个流程仍然是一次普通的文本生成只不过模型被允许生成一种特殊格式的输出——通常是一个结构化的 JSON里面包含tool_calls字段标明它想调用哪个工具、传什么参数。换句话说模型在生成每一个 token 的时候它的候选空间里既包含“普通文本回复”也包含“工具调用意图”。当它判断“这个问题我需要借助外部工具才能回答”时它就会生成工具调用意图而不是直接编一个答案。这个判断过程本质上是一次意图识别 能力匹配用户想要什么我手上有没有对应的工具如果有该用哪个。这三步里任何一步出错都会导致调用失败或调用错误。3.2 什么情况下模型会选择调用工具模型决定调用工具通常基于几个信号问题涉及实时信息用户问“今天天气”“现在股价”“最新新闻”模型知道自己没有实时数据倾向于调用工具。问题涉及精确计算用户问“1234乘以5678等于多少”模型知道自己算数容易出错倾向于调用计算器工具。问题涉及私有数据用户问“我上个月的订单”模型知道自己没有这个数据倾向于调用数据库查询工具。问题涉及外部动作用户说“帮我发一封邮件”“帮我创建一个提醒”模型知道自己没有执行能力倾向于调用对应工具。反过来如果用户问的是“解释一下什么是递归”“写一首关于春天的诗”模型知道自己能直接回答就不会调用工具。这里有一个反直觉的点模型并不总是“理性”地判断该不该调用。有时候它明明可以直接回答却非要调用一个工具有时候它明明该调用工具却硬编了一个答案。前者叫“过度调用”后者叫“调用遗漏”。这两个问题在后面的调试章节会详细讲怎么处理。3.3 参数提取从自然语言到结构化数据模型决定调用工具之后下一步是从用户的自然语言里提取参数。比如用户说“帮我查一下后天上海的天气”模型需要提取出city上海、date后天对应的日期。这一步的难点在于自然语言里的隐含信息。用户不会说“请调用get_weather工具参数city为上海date为2024-03-17”用户说的是“后天上海天气怎么样”。模型需要识别出“后天”是相对日期需要结合当前日期换算识别出“上海”是城市参数把这两个信息组装成工具要求的格式。如果工具描述里写清楚了“如果用户说今天或明天请根据当前日期换算成具体日期”模型就能正确处理相对日期。如果没写模型可能直接把“后天”两个字塞进 date 参数导致下游解析失败。还有一个常见问题是参数缺失。用户说“帮我查一下天气”没说哪个城市。这时候模型有两种处理方式一是追问用户“请问您想查哪个城市的天气”二是在调用时留空或填一个默认值。哪种更好取决于你的产品设计。如果工具参数是必填的模型应该追问如果参数有合理默认值模型可以自动填充。3.4 并行调用与串行调用当用户的一个请求需要多个工具配合时就涉及调用顺序的问题。并行调用是指模型一次性生成多个工具调用意图这些调用之间没有依赖关系可以同时执行。比如用户问“北京和上海今天天气怎么样”模型可以同时调用两次get_weather一次查北京一次查上海。串行调用是指后一个调用依赖前一个调用的结果。比如用户问“帮我查一下我最近的订单然后给订单里的商品写一条评价”模型需要先调用“查询订单”工具拿到订单信息后再调用“写评价”工具。这种依赖关系模型通常能自己推理出来但前提是工具描述里写清楚了输入输出关系。在实际工程中并行调用能显著降低延迟因为多个工具可以同时执行。但要注意不是所有模型都支持一次生成多个调用意图这取决于你使用的具体模型版本和接口能力。4. 结果回灌工具返回后模型怎么接着聊4.1 工具结果的消息格式工具执行完之后结果需要以特定格式回传给模型。在大多数接口里这个格式是一条role为tool的消息里面包含tool_call_id对应之前模型生成的调用 ID和content工具返回的实际内容。{ role: tool, tool_call_id: call_abc123, content: {\city\: \上海\, \date\: \2024-03-17\, \weather\: \多云\, \temp_high\: 18, \temp_low\: 12, \precipitation\: \10%\} }模型收到这条消息后会把它当作“自己刚才调用的工具返回了结果”然后基于这个结果继续生成回复。比如它会说“上海后天多云气温12到18度降水概率10%出门不用带伞”。这里有一个关键细节工具返回的内容最好是结构化的 JSON而不是一大段自然语言。结构化数据让模型更容易准确提取信息也方便你在日志里排查问题。如果工具返回的是一大段文本模型可能会漏掉关键信息或者误读。4.2 结果太长怎么办截断、摘要与分页工具返回的结果可能非常长。比如你调用一个“搜索文档”的工具返回了二十页的搜索结果。如果原封不动塞给模型会带来两个问题一是超出上下文窗口二是模型在长文本里找关键信息的能力会下降。常见的处理方式有三种截断只取前 N 条结果或者只取最相关的几条。比如搜索结果只返回前5条每条只保留标题和摘要。摘要在工具内部先用一个小模型或者规则逻辑把结果压缩成关键信息再回传给主模型。分页如果结果确实需要全部展示可以让模型先拿到第一页然后根据需要再调用一次工具拿下一页。我的经验是工具返回给模型的内容应该以“模型能快速做决策”为目标来设计而不是“把所有数据都倒给模型”。模型需要的是“够用的信息”不是“全部的信息”。4.3 工具执行失败时的回传策略工具执行失败是常态不是异常。网络超时、参数错误、权限不足、下游服务挂了各种情况都可能发生。关键问题是失败信息怎么回传给模型。有两种策略第一种是把错误信息原样回传让模型自己决定怎么跟用户解释。比如回传{error: 城市名称无法识别}模型可能会说“抱歉我没能找到这个城市请您确认一下城市名称”。第二种是在工具层做一层包装把技术错误翻译成用户能理解的语言再回传。比如把{error: HTTP 503}翻译成{error: 天气服务暂时不可用请稍后再试}。两种策略各有适用场景。如果错误是用户输入导致的比如城市名写错了适合第一种让模型引导用户修正。如果错误是系统层面的比如服务挂了适合第二种直接给用户一个友好的提示不要让模型去解释技术细节。注意无论哪种策略都不要把原始的技术堆栈信息直接回传给模型。模型可能会把这些信息原样复述给用户造成困惑甚至信息泄露。4.4 多轮对话中的工具调用状态管理在多轮对话里工具调用的状态管理是一个容易被忽略的复杂点。假设用户第一轮问“上海天气怎么样”模型调用了天气工具拿到了结果。第二轮用户接着问“那北京呢”。这时候模型需要理解“那北京呢”是在延续上一轮的意图应该再次调用天气工具只是把城市换成北京。这个“延续”能力依赖于你把完整的对话历史包括之前的工具调用和工具返回都保留在上下文里。如果只保留用户的自然语言消息丢掉了工具调用的记录模型就会失去上下文不知道“那北京呢”指的是什么。所以在多轮场景下对话历史里必须完整保留assistant的工具调用消息和tool的工具返回消息。这些消息虽然用户看不到但它们是模型理解对话状态的关键依据。5. 从 Demo 到生产那些只有踩过才知道的坑5.1 参数校验不能省模型也会“手滑”即使工具描述写得再清楚模型仍然可能传错参数。我见过模型把日期传成“2024-13-45”把城市名传成“上海省”把枚举值传成“HIGH”而不是“high”。这些错误如果不在工具层做校验直接打到下游服务轻则报错重则写入脏数据。所以工具函数的第一件事永远是参数校验。校验内容包括必填参数是否存在、类型是否正确、枚举值是否在允许范围内、数值是否在合理区间、字符串长度是否超限。校验不通过时返回一个清晰的错误信息给模型让它重新提取参数或者向用户追问。def get_weather(city: str, date: str): if not city or not isinstance(city, str): return {error: 城市名称不能为空} if not re.match(r^\d{4}-\d{2}-\d{2}$, date): return {error: 日期格式必须为YYYY-MM-DD} # 继续执行实际查询逻辑5.2 超时与重试别让一个慢工具拖垮整个对话外部工具的执行时间是不可控的。查天气可能200毫秒返回查数据库可能2秒调用一个第三方接口可能10秒还没响应。如果不设超时用户就会一直等在那里体验极差。我的做法是给每个工具设置一个合理的超时时间通常3到10秒视场景而定超时后立即返回一个“服务繁忙”的错误信息给模型让模型告诉用户“稍后再试”。同时对于幂等的查询类工具可以配置一次自动重试对于非幂等的操作类工具比如“创建订单”重试要非常谨慎避免重复创建。5.3 安全边界模型能调用的工具必须白名单化这是一个安全红线问题。模型能调用的工具必须是你明确注册在白名单里的。绝对不能允许模型动态生成工具名称或者动态执行任意代码。我见过一些早期实现为了“灵活”让模型直接生成 SQL 或者 shell 命令去执行。这是极其危险的。模型可能被用户诱导生成恶意命令造成数据泄露或系统破坏。正确的做法是所有可调用工具都是预先定义好的、经过审核的、参数受控的函数。模型只能在给定的工具集合里选择不能越界。对于涉及敏感操作的工具比如删除数据、发送邮件、修改配置还应该增加额外的确认机制比如要求用户二次确认后才真正执行。5.4 日志与可观测性出问题时你怎么查函数调用链路比普通对话长得多用户输入 → 模型生成调用意图 → 参数提取 → 工具执行 → 结果回传 → 模型生成最终回复。任何一个环节出问题最终表现都是“模型回答不对”但根因可能在链路的任何一处。所以完整的日志记录是必须的。至少要记录每次请求的完整消息列表、模型生成的工具调用意图工具名 参数、工具的实际执行结果成功或失败、最终返回给用户的回复。有了这些日志出问题时你才能快速定位是模型选错了工具、参数提取错了、工具执行失败了还是结果回传后模型解读错了。我习惯在日志里给每次调用打一个唯一的 trace_id把上述所有环节串联起来。排查问题时拿着 trace_id 一搜整条链路一目了然。5.5 成本控制工具调用会显著增加 token 消耗这一点很多人一开始不会注意到。工具描述本身要占 token工具调用的意图生成要占 token工具返回的结果要占 token模型基于结果生成的回复也要占 token。一轮带工具调用的对话token 消耗可能是普通对话的三到五倍。如果工具描述很长、工具数量很多、返回结果很大成本会进一步上升。所以在上线前一定要估算清楚平均每次对话会触发多少次工具调用、每次调用带来多少额外 token、总体成本是否在可接受范围内。优化方向包括精简工具描述、控制返回结果长度、对简单问题引导模型直接回答而不调用工具、对高频查询做缓存等。6. 把这一讲串起来一个可复现的最小实现骨架6.1 整体流程回顾把前面几节的内容串起来一次完整的函数调用流程是这样的你定义好工具清单名称、描述、参数模式随请求一起发给模型模型判断需要调用工具生成包含工具名和参数的结构化调用意图你的代码解析这个意图找到对应的工具函数执行它工具函数返回结果成功数据或错误信息你把结果以tool消息格式回传给模型模型基于结果生成最终的自然语言回复给用户。这个流程可以循环多次直到模型不再生成工具调用意图而是直接输出最终回复。6.2 关键参数配置速查配置项建议值说明工具超时3-10秒视工具类型调整查询类可短操作类可长最大调用轮数5-10轮防止模型陷入无限调用循环返回结果长度控制在上下文窗口的10%以内过长时截断或摘要参数校验必做类型、枚举、范围、长度全查日志级别记录完整消息链路便于排查问题敏感操作二次确认删除、发送、支付类操作必须加确认6.3 我个人的几条经验最后分享几条我在实际项目里总结出来的经验都是文档里不会写的。第一条先从一两个工具开始不要一上来就搞几十个。工具越多模型选择越容易出错调试也越复杂。先把一两个核心工具跑通把描述、参数、错误处理都打磨好再逐步扩展。第二条工具描述要反复迭代。第一版描述几乎不可能完美。上线后观察哪些该调用没调用、哪些不该调用乱调用针对性地修改描述通常迭代三五轮之后准确率会有明显提升。第三条给模型“不调用工具”的选项。有些问题模型直接回答就好不需要调用工具。如果你在提示词里强调“尽量使用工具”模型可能会过度调用。反过来如果你发现模型该调用却不调用可以在系统提示里加一句“当问题涉及实时信息或外部数据时优先使用提供的工具”。第四条工具返回结果要“说人话”。虽然结构化 JSON 方便模型解析但如果返回的是给用户看的最终数据最好在工具层就做好格式化。比如温度返回“18°C”而不是“18”日期返回“3月17日”而不是“2024-03-17”。这样模型在生成最终回复时直接引用即可减少二次转换出错的可能。第五条永远假设模型会犯错。参数会传错工具会选错结果会解读错。你的系统设计要能容忍这些错误而不是假设模型永远正确。参数校验、超时重试、错误回传、日志记录这些“防御性”的工作才是让一个 Demo 变成可用产品的关键。