Function Calling 工程化实战:从工具定义到执行编排的完整链路

发布时间:2026/10/17 8:11:05
Function Calling 工程化实战:从工具定义到执行编排的完整链路 1. 为什么 Function Calling 是 Agent 从玩具走向工具的分水岭如果你跟着这个系列一路做下来到第三篇为止手里的 Agent 大概率已经能聊天、能记住上下文、能按预设流程走几步了。但只要你真拿它去干点正事就会立刻撞上一堵墙它只会“说”不会“做”。你问它今天天气怎么样它要么编一个要么告诉你它无法获取实时信息。你让它帮你查一下数据库里某个订单的状态它只能给你一段看起来很像 SQL 的文字然后眼巴巴看着你等你手动去执行。这个阶段我称之为“嘴炮 Agent”——知识面挺广表达也流畅但所有输出都停留在文本层面跟真实世界之间隔着一层玻璃。Function Calling 就是砸碎这层玻璃的那把锤子。它让模型不再只是生成文字而是能生成一个结构化的“调用意图”由你的代码去执行真实操作再把结果喂回模型让它基于真实数据继续推理。说白了就是从“我觉得应该是这样”变成“我去查了一下确实是这样”。这一篇我会把 Function Calling 的完整链路拆开讲透。不是那种“调个 API 就完事”的浅层教程而是从协议设计、参数约束、执行编排、错误恢复到多轮工具调用的工程化落地。如果你正在做自己的 Agent 项目或者准备把 LLM 接入现有业务系统这篇的内容可以直接拿去抄作业。我踩过的坑、调过的参数、翻过的文档都会尽量还原成你能直接复用的经验。2. Function Calling 到底在做什么从模型视角拆解2.1 模型不是执行者它只是“意图翻译器”很多人第一次接触 Function Calling 时会有个误解以为模型真的去调用了某个函数。不是的。模型做的事情只有一件根据你的问题和你提供的工具描述判断“要不要调工具”“调哪个工具”“参数填什么”然后输出一段符合约定格式的 JSON。真正执行函数的是你自己的代码。这个认知非常重要因为它决定了整个架构的设计方向。模型负责的是语义理解和意图映射你的代码负责的是实际执行和结果处理。两者之间通过一个结构化的协议来通信。理解了这个分工你就不会纠结“为什么模型调完函数之后还要我再调一次模型”——因为模型需要看到执行结果才能继续推理。我习惯用一个类比来解释模型就像一个坐在指挥中心的调度员它能看到所有工具清单能听懂用户的需求然后决定“这个活该派给谁、带什么参数去”。但调度员自己不出去干活它只负责下指令。干完活之后结果要回报给调度员它才能决定下一步是继续派活还是给用户答复。2.2 一次完整的 Function Calling 生命周期把链路拆开来看一次标准的 Function Calling 交互包含以下几个阶段第一阶段工具声明。你在请求里附带一个 tools 数组里面每个工具都有名字、描述和参数 schema。这个 schema 通常用 JSON Schema 格式描述告诉模型这个函数接受哪些参数、每个参数是什么类型、哪些是必填的。第二阶段模型决策。模型收到用户消息和工具列表后判断是否需要调用工具。如果需要它不会直接回复文本而是返回一个 tool_calls 结构里面包含函数名和参数 JSON 字符串。第三阶段本地执行。你的代码解析这个 tool_calls找到对应的本地函数把参数传进去执行拿到返回值。第四阶段结果回传。你把执行结果包装成一条 role 为 tool 的消息附上对应的 tool_call_id追加到对话历史里再次请求模型。第五阶段模型整合。模型看到工具返回的真实数据后结合原始问题生成最终回复。如果一次调用不够它可能再次发起新的 tool_calls进入下一轮循环。这五个阶段构成了一个完整的闭环。实际工程中最难的不是调通这个流程而是处理各种边界情况模型返回的参数格式不对怎么办、函数执行超时怎么办、模型连续调用同一个函数陷入死循环怎么办。这些后面会逐一展开。2.3 为什么不用 Prompt 硬编码而要用原生 Function Calling有人可能会想我直接在 prompt 里写“如果你需要查天气就输出 CALL:get_weather city北京”然后自己解析不就行了早期确实有人这么干我也试过。但很快就会发现几个致命问题。第一格式稳定性极差。你永远不知道模型会在哪个位置多打一个空格、少写一个引号或者把参数顺序换一下。解析正则写到崩溃是常态。第二无法利用模型的原生训练。主流模型在训练阶段就见过大量 Function Calling 格式的数据它们对这种结构化输出有天然的亲和力比你自己发明的格式靠谱得多。第三多工具并发调用。原生 Function Calling 支持一次返回多个 tool_calls你的代码可以并行执行而 prompt 硬编码很难做到这一点。所以我的建议很明确只要模型支持原生 Function Calling就别自己造轮子。省下来的时间拿去处理业务逻辑比什么都值。3. 工具定义的艺术让模型准确理解你的意图3.1 工具描述写得好调用准确率翻倍Function Calling 的准确率八成取决于你的工具描述写得怎么样。我见过太多人抱怨“模型怎么老是调错函数”结果一看他的工具描述就写了一行“查询数据”参数叫“param1”。这能调对才怪。工具描述的本质是给模型看的“使用说明书”。你要假设模型对你的系统一无所知它只能通过你给的文字来判断这个工具是干什么的、什么时候该用、参数怎么填。所以描述要具体、要有场景、要有边界。举个例子假设你有一个查询订单状态的工具。差的描述是这样的{ name: query_order, description: 查询订单, parameters: { type: object, properties: { id: {type: string} } } }好的描述应该是这样的{ name: query_order_status, description: 根据订单号查询订单的当前状态包括待付款、已付款、已发货、已签收、已取消。当用户询问某个订单的进度、物流状态或是否已发货时使用此工具。不适用于查询商品信息或用户账户信息。, parameters: { type: object, properties: { order_id: { type: string, description: 订单号通常是12位数字组成的字符串例如 202405180001 } }, required: [order_id] } }差别在哪里好的描述告诉模型这个工具能返回什么状态、什么场景该用、什么场景不该用、参数长什么样。这些信息会直接影响模型的判断准确率。我实测下来光是优化工具描述这一项调用准确率就能从六成提到九成以上。3.2 参数 schema 的约束技巧JSON Schema 提供了丰富的约束能力但很多人只用了 type 和 required。其实还有几个关键字段能大幅提升参数质量。enum 枚举约束。如果某个参数只能取固定几个值一定要用 enum 列出来。比如订单状态查询如果支持按状态筛选就把所有合法状态列进去。这样模型不会瞎编一个“运输中”这种你系统里根本不存在的状态。description 参数级说明。每个参数都要写 description尤其是那些名字不够自解释的。比如一个参数叫type鬼知道是什么 type。写成description: 查询类型可选值by_date 按日期查by_status 按状态查就清楚多了。format 格式提示。日期、邮箱、URL 这类有固定格式的参数用 format 字段标注。虽然模型不一定严格遵守但有个提示总比没有好。嵌套对象和数组。复杂参数用 object 和 array 嵌套描述。但要注意嵌套层级不要太深超过三层模型就容易出错。如果参数确实复杂考虑拆成多个工具。3.3 工具粒度的取舍粗一点还是细一点这是我在实际项目中纠结最久的问题。工具到底是设计得粗一些好还是细一些好粗粒度工具的例子一个manage_user函数通过 action 参数区分创建、查询、更新、删除。细粒度工具的例子拆成create_user、get_user、update_user、delete_user四个独立函数。我的经验是优先细粒度但不要过细。细粒度的好处是每个工具职责单一模型判断起来更简单参数也更清晰。粗粒度的问题是 action 参数容易填错而且不同 action 需要的参数不一样schema 会变得很复杂模型容易漏填或填错。但也不能细到每个字段一个函数。比如查询用户不要拆成get_user_name、get_user_email、get_user_phone这纯属给自己找麻烦。合理的粒度是一个工具对应一个完整的业务操作参数控制在五个以内。如果确实需要粗粒度比如某些系统接口就是统一的 action 模式那就在 description 里把每个 action 的用途和所需参数写清楚并且在 schema 里用条件约束尽量表达。不过说实话条件约束在 JSON Schema 里表达起来很别扭模型也不一定看得懂能拆还是拆。4. 执行编排从模型输出到真实操作的工程化落地4.1 解析 tool_calls 的正确姿势模型返回的 tool_calls 结构大概长这样{ role: assistant, content: null, tool_calls: [ { id: call_abc123, type: function, function: { name: query_order_status, arguments: {\order_id\: \202405180001\} } } ] }注意 arguments 是一个 JSON 字符串不是对象。你需要先 json.loads 解析它。这里有个坑模型有时候会返回不完整的 JSON比如少个括号或者多个逗号。虽然主流模型现在这种情况少了很多但还是要做防御性处理。我的做法是包一层 try-except解析失败就返回一个错误信息给模型让它重新生成。还有一个细节tool_calls 可能包含多个调用。比如用户问“北京和上海今天天气怎么样”模型可能一次返回两个 get_weather 调用。你的代码要能处理这种并发情况。简单做法是串行执行但更好的做法是用线程池或异步并发执行尤其是当工具涉及网络请求时并发能省不少时间。4.2 函数注册与路由设计在你的代码里需要维护一个从函数名到实际函数的映射表。最简单的做法是用字典TOOL_REGISTRY { query_order_status: query_order_status, get_weather: get_weather, search_products: search_products, }然后根据模型返回的函数名去查表执行。但实际项目中我建议加一层封装把参数校验、异常处理、日志记录都统一管起来。比如def execute_tool_call(tool_call): name tool_call.function.name args json.loads(tool_call.function.arguments) if name not in TOOL_REGISTRY: return {error: f未知工具: {name}} try: result TOOL_REGISTRY[name](**args) return {result: result} except TypeError as e: return {error: f参数错误: {str(e)}} except Exception as e: return {error: f执行失败: {str(e)}}这样做的好处是无论工具执行成功还是失败你都能返回一个结构化的结果给模型。模型看到错误信息后可以决定是重试、换参数还是告诉用户出了问题。千万别让异常直接抛出来中断整个流程那样用户体验极差。4.3 多轮工具调用的循环控制模型可能一次调用不够需要多轮。比如先查订单状态发现已发货再调物流查询接口拿物流详情。你的代码需要用一个循环来驱动这个过程messages [{role: user, content: user_input}] for _ in range(MAX_ROUNDS): response call_llm(messages, toolsTOOLS) messages.append(response) if not response.tool_calls: break for tool_call in response.tool_calls: result execute_tool_call(tool_call) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result) }) return response.content这里 MAX_ROUNDS 是必须的。我一般设 5 到 8 轮。设太少复杂任务跑不完设太多万一模型陷入死循环你的 API 账单会教你做人。我遇到过模型反复调用同一个函数十几次的情况原因是我工具返回的错误信息不够明确模型以为换个参数就能成功结果一直试。后来我在错误信息里加了“请勿重试直接告知用户”的提示情况就好多了。5. 实战中的坑与排查手册5.1 模型不调用工具怎么办这是最常见的问题。用户明明问了需要实时数据的问题模型却直接编了一个答案。原因通常有三个工具描述不够清晰、模型不知道有这个工具、或者模型觉得自己的知识够用。排查顺序先看工具描述是不是太模糊把使用场景写具体。然后在 system prompt 里加一句“当需要实时数据或外部信息时优先使用提供的工具不要依赖自身知识”。如果还不行可以在工具描述里加一些“必须使用此工具”的强提示。实测下来这三板斧下去九成的情况都能解决。5.2 参数填错或漏填模型有时候会漏掉必填参数或者把参数类型搞错。比如把数字填成字符串把日期格式写错。防御手段有两个一是在 schema 里把 required 和 type 写清楚二是在函数内部做二次校验。我习惯在函数入口加一层参数校验不合法就返回明确的错误信息。比如“order_id 必须是12位数字字符串你提供的是 abc请重新提取”。模型看到这种具体错误下一轮通常能改对。如果只说“参数错误”模型往往不知道该改什么。5.3 工具执行超时或失败外部 API 挂了、数据库连不上、网络超时这些都会导致工具执行失败。你的代码要能捕获这些异常并返回一个模型能理解的错误信息。不要返回一堆堆栈信息模型看不懂。返回类似“物流查询服务暂时不可用请稍后重试或告知用户手动查询”这样的自然语言描述模型更容易处理。另外对于可能超时的操作建议加一个超时时间。比如 HTTP 请求设 5 秒超时超了就返回错误。不要让整个 Agent 卡死在一个慢接口上。5.4 常见问题速查表问题现象可能原因解决方向模型不调用工具描述模糊、缺少场景说明优化工具描述加 system prompt 引导调用错误的工具工具之间描述重叠明确各工具边界加“不适用于”说明参数格式错误schema 约束不足加 enum、format、description参数漏填required 未标注补全 required 列表多轮调用死循环错误信息不明确错误信息加“请勿重试”提示工具执行超时外部依赖慢加超时控制返回友好错误并发调用结果错乱tool_call_id 未对应确保每个结果绑定正确的 id6. 进阶技巧让 Function Calling 更稳更高效6.1 工具返回结果的精简与结构化工具返回的数据不要一股脑全塞给模型。比如你查数据库返回了 50 个字段模型根本处理不过来还浪费 token。我的做法是在工具函数内部就把结果精简成模型需要的字段并且用清晰的结构返回。比如订单查询返回{order_id: xxx, status: 已发货, tracking_no: SF123456, estimated_delivery: 2024-05-20}就够了不需要把创建时间、修改时间、操作人这些无关字段带上。如果确实需要多个字段也尽量用扁平结构别嵌套太深。6.2 并行工具调用的处理当模型一次返回多个 tool_calls 时如果这些调用之间没有依赖关系可以并行执行。Python 里用 concurrent.futures 的 ThreadPoolExecutor 就能搞定from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers5) as executor: futures [executor.submit(execute_tool_call, tc) for tc in tool_calls] results [f.result() for f in futures]但要注意如果工具之间有依赖比如第二个工具需要第一个工具的返回值作为参数那就不能并行。这种情况模型通常会分两轮调用你按顺序处理就行。6.3 工具调用的可观测性生产环境里你需要知道模型调了什么工具、传了什么参数、返回了什么结果、耗时多久。这些信息对于排查问题和优化工具体验至关重要。我一般会在 execute_tool_call 里加日志记录完整的调用链路。如果用的是 LangSmith 或类似的可观测性平台可以直接接入可视化效果更好。另外建议给每个工具调用加一个唯一的 trace_id方便在日志里串联。尤其是多轮调用场景没有 trace_id 的话日志看起来就是一锅粥。6.4 安全边界别让模型为所欲为Function Calling 给了模型操作真实系统的能力这也意味着风险。模型可能被诱导调用不该调用的函数或者传入恶意参数。几个基本的安全措施第一工具白名单。只注册你允许模型调用的函数不要动态注册。第二参数校验。在函数内部严格校验参数尤其是涉及数据库操作、文件操作、外部请求的。第三权限控制。敏感操作加确认机制比如删除数据前要求用户二次确认。第四速率限制。防止模型疯狂调用某个工具把你的配额刷爆。我见过有人把数据库的 raw query 接口直接暴露给模型结果模型生成了一条 DROP TABLE。虽然模型本身没有恶意但用户可能通过 prompt 注入诱导它这么做。所以安全边界一定要在代码层面守住不能指望模型自觉。7. 我个人在实际项目中的几点体会Function Calling 这个机制刚上手的时候觉得挺简单不就是定义几个函数让模型调嘛。但真做到生产级别细节多到令人发指。我最大的体会是工具描述的质量决定了整个系统的上限。你花一个小时优化工具描述可能比花一天调 prompt 效果还好。另一个体会是错误处理比正常流程更重要。正常流程跑通很容易但用户不会按你设想的路径走。他们会问奇怪的问题会在网络不好的时候使用会在工具挂掉的时候继续操作。你的 Agent 能不能在这些情况下优雅降级才是真正体现工程能力的地方。最后分享一个小技巧如果你不确定工具描述写得好不好可以拿给一个完全不了解你系统的同事看问他“你觉得这个工具是干什么的、什么时候该用”。如果他看完还是一头雾水那模型大概率也看不懂。工具描述是写给人看的顺便让模型也能看懂这个标准不能降。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询