ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Function Calling 设计实战:从函数定义到工程落地的关键取舍

Function Calling 设计实战:从函数定义到工程落地的关键取舍 最近在做智能体项目把 function calling 这块从零到一搭了一遍过程中踩了不少坑也推翻过好几版设计。趁着 2026-09-03 这个时间节点把我在 function calling 设计上的取舍写下来。这个功能说白了就是让大模型不只是“嘴上说说”而是能真正去调用工具、查数据库、调 API、操作业务系统。很多人以为它不过是在系统提示词里塞几个函数定义实际做下来才发现函数定义怎么写、什么时候调、参数怎么校验、结果怎么回灌每一步都是取舍每一步都在影响最终效果和稳定性。这篇文章不打算讲某个厂商 SDK 的调用方法那些文档都有。我想讲的是底层设计逻辑核心决策点有哪些、为什么这样做、我在实际项目中遇到过什么问题。适合正在做 Agent、RAG 外挂工具、或者想把大模型接入内部系统的工程师参考。1. 先想清楚function calling 到底在解决什么问题1.1 从“模型不会算数”说起大模型本质上是文本生成模型擅长的是理解语义、生成内容、抽取信息但面对需要精确计算、实时查询、操作外部系统这类任务它天然不擅长。比如你问它“帮我查一下本月订单总额”模型如果只凭训练数据里的知识大概率会瞎编一个数字。这种时候就需要给模型一个“手”让它输出一个结构化的函数调用指令由外部程序真正去执行查询再把结果带回给模型组织语言。这件事的价值在于模型从“什么都知道一点但什么都不精确”的聊天机器人变成了一个能够编排真实业务的调度器。你不再需要把业务系统的数据全部塞进上下文里只需要告诉模型“你有这些能力按需调用”剩下的事情由模型自己规划、调用、整理回答。1.2 一套体系里的四个角色function calling 不是一个孤立的接口而是一套完整链路核心是这四个角色模型负责理解用户意图决定“要不要调用、调用哪个函数、参数怎么填”函数定义你给模型的一份“能力清单”描述每个函数能干什么、需要什么参数运行时负责接收模型输出的结构化调用请求校验参数执行真实函数并在必要时把结果回传给模型下游系统真正被调用的数据库、API、业务服务它们是 final 执行者在设计取舍时每一个角色都有两个方向的分岔口。模型给不给调用开关定义写得粗略还是精确运行时是简单透传还是做完整生命周期管理下游系统是否做权限隔离这些都直接决定上线后的稳定性。2. 函数定义最容易忽视的取舍战场2.1 描述写得好不好效果差一个量级很多人写函数定义非常随意觉得只要函数名和参数名对得上就行。实测下来函数描述写得清楚与否对调用准确率的影响非常大。我在一个天气查询场景里做过对比一个函数描述只写了“获取天气”另一个写清楚“按城市名获取最近三天的天气预报包含温度、湿度、降水概率”后者在测试集上的调用准确率高出将近二十个百分点。描述要包含几个关键信息这个函数是干什么的、什么场景下应该调用、什么场景下不该调用、输入参数的单位和边界。尤其是“什么场景下不该调用”这个很容易被忽略。比如你有一个函数是“查询订单物流”需要用户在登录状态下才能调如果描述里不写清楚“仅当用户已登录时才可调用”模型可能会在冷启动对话里就尝试调用这个函数导致后端报错。对于参数我强烈建议写清楚每个字段的约束。类型、枚举值范围、格式要求都要写死。比如手机号字段如果你不告诉模型中国的手机号是 11 位数字且以 1 开头模型可能把“座机号”也塞进来。更稳妥的做法是在参数描述里直接给一个示例值比如“示例13800138000”模型对示例的语义理解往往比对抽象描述更准确。2.2 参数要“窄”不要“宽”枚举值胜过自由文本函数参数是模型最容易“自由发挥”的地方。如果定义得宽泛比如把时间参数写成“任意字符串”模型可能会输出“明天早上”“下周一”这种自然语言然后你的运行时就需要写一堆解析逻辑而你费劲写好的解析逻辑大概率会漏掉边界情况。一个有效做法是尽量用枚举值约束参数范围。比如一个用于筛选数据状态的参数与其接受自由字符串不如限定为pending、processing、done三个枚举值并在描述里注明“只能从以下值中选择”。模型对枚举的遵循程度远高于对自然语言的遵循程度。这不只是减少了解析工作量更重要的是减少了下游系统拿到非法参数的概率。参数的“窄”还体现在另一个层面能少传就少传。函数定义的入参不要追求“一步到位”不要总想着把所有可能的筛选条件都塞进一个函数里。每多一个参数模型在选择和填值的时候就会多一分出错的可能。宁可把一个大函数拆成几个小函数让模型只选择它需要的那个也不要在一个大函数里放一大堆可选参数。这也是我在重构里反复做的一个动作删参数比加参数难但删完之后调用准确率明显上去了。2.3 函数切开还是合并一个经典的粒度问题函数粒度设计是这个领域最纠结的问题之一。拆得太细模型需要做更多路由决策一次调用可能要连续调好几个函数才能完成用户请求不仅延迟高中间任何一环出错都会导致整体失败。合并得太粗函数变成一个“万能总线”参数列表又臭又长模型填错参数的风险直线上升。我的经验是以“用户意图的直接结果”为粒度单位。比如一个“订会议室”的操作不要拆成“查会议室列表”“查忙闲”“创建预订”三个独立函数让模型自己编排而是可以合成一个“订会议室”函数参数里带上时间、人数、地点偏好由后端一次完成三个步骤。这样既减少了模型的多步推理负担也把复杂逻辑收到了后端可控的代码里。但如果“查会议室列表”本身也是一个高频用户需求那它就该单独存在。判断标准不是功能边界而是意图边界每次用户提问他最想要的那个直接结果是什么函数粒度就定在那里。3. 调度策略什么时候调、调哪个、一次调几个3.1 强制调用与自主判断的博弈function calling 的调度策略里模型不一定要自己决定“要不要调”你也可以在 API 层面强制指定。比如平台的tool_choice参数可以设置为auto模型自主判断、required必须调用一个函数和指定某一个特定函数。强制调用模式非常有用但也有明显的副作用。我在一个查询类场景里把tool_choice设成了required结果用户明明说了一句“你好”模型也硬生生构造了一个函数调用去查不知道哪来的数据。后来改成auto但增加了一个前置意图识别逻辑只有确认用户有查询意图时才注入函数定义列表闲聊场景直接走普通对话通道。这个“先分流、再决定注入哪些工具”的思路比单纯依赖模型的判断要稳定很多。它们之间的取舍是全自动模式灵活但不可控全强制模式可控但容易误调用。一个工程化的系统需要在前端做一次初筛把明显不需要工具的请求拦掉再把剩余请求交给带工具的模型。不要指望把所有智能都放在模型层编排层的事该自己做还是自己做。3.2 多函数路由靠模型还是靠规则当系统里有几十个甚至上百个函数时路由就成了问题。模型需要从这么多候选里选出正确的一个准确率会随着函数数量增加而下降。我在项目里试过两种方案实测下来各有优劣。第一种是纯模型路由把所有函数定义全部塞给模型让它自己挑。优点是简单直接适合函数数量少少于 15 个的场景。缺点是函数一多模型容易混淆函数边界选错函数而且大量函数定义会占掉不少上下文 token增加成本。第二种是规则预筛 模型精排。用一个轻量级的意图分类器或关键词规则先把候选函数缩减到 3 到 5 个再把这几个函数的定义拼进 prompt 里交给模型选择。这个方案在函数数量超过 30 个时效果明显更好。代价是你需要维护一套“意图到函数”的映射关系而且规则多了之后同样面临维护成本。我的建议是先想清楚你的函数会不会超过 20 个如果会那就尽早引入规则预筛别等上线后再改不然后期返工成本很高。3.3 并行调用提效利器也是失控隐患新版模型普遍支持一次返回多个函数调用也就是并行调用。它在某些场景下很好用比如用户一次性问了三只股票的价格模型可以同时调三次查询函数大幅减少交互轮数。但并行调用有个前提就是这些函数相互独立不依赖彼此的返回结果。如果函数之间有依赖关系比如“先查用户余额再根据余额决定是否下单”那就不能并行必须拆成多轮第一轮模型输出查余额执行完后把结果回灌模型再决定下一步。我见过不少项目图省事把所有能拆的都让模型并行调结果出现“查完库存又下单下单时库存在那一刻已经被另一次并行调用改了”这种竞态问题。底层逻辑是函数调用是一个动作动作之间的依赖要写在业务代码里而不是指望模型自己规避。能并行的函数放到一个批次有依赖的必须在控制流里显式分轮。对每个函数你都要在定义阶段就标注清楚它是否可以与其它函数并行执行这个标记会在运行时决定调度方式。4. 参数提取、校验与容错系统长在 function calling 上4.1 参数缺失别急先给模型一次补全机会模型在生成函数调用时偶尔会出现参数缺失的情况。这有时候是因为用户本身没提供完整信息有时候是模型提取语义时漏了字段。这里有个关键取舍到底是后端智能地做一次“容错处理”还是让模型回去再问用户我踩过一次坑一个“创建工单”的函数模型漏传了“优先级”字段后端图省事给个默认值直接创建了。结果用户本来想提一个“紧急”工单被当成普通工单处理隔了好久才被发现。后来改成如果函数定义里标记了“必须由用户确认”的字段缺失就不要自作主张补默认值而是触发一次澄清提问模型会返回一句“请问这个工单的优先级是”。这样多做一轮对话但避免了业务数据错误。当然不是所有字段都值得多问一轮。我的做法是把参数分成三档必须有且必须用户确认的、必须有但可以用默认值的、可选且有默认值的。第一档缺失就反问第二档缺失直接补默认值第三档缺失就传空。这个分级逻辑写清楚之后用户体验和系统容错率都好了很多。另外一个细节是有些 API 平台支持在函数定义中把参数标记为 required模型对 required 字段的遵循度明显更高建议把真正重要的字段全部标成 required。4.2 输出格式校验堵住模型“自由发挥”的口子即便模型大部分时候能生成结构良好的调用你也必须假设它会出错。我在生产系统里见过模型把参数名拼错、把枚举值写成不在列表里的字符串、把数字类型参数填成文字等各类情况。有些人觉得这是解析层的事交给 JSON 解析就行但实际问题远不止于 JSON 语法。标准做法是在运行时做三层校验第一层是 JSON 结构校验确认输出是合法的 JSON第二层是类型和值域校验确认参数类型正确、枚举值在允许范围内第三层是业务规则校验比如时间范围不早于当前日期、起始日期不晚于结束日期等。前两层可以靠 JSON Schema 通用校验完成第三层必须写业务代码逐项判断。第三层校验失败时我有一个比较实用的兜底策略把校验失败的错误信息转成自然语言回灌给模型让它基于错误修正自己的调用。比如校验器返回“结束日期早于开始日期请确认”模型会意识到自己的参数不合理重新生成一个更合理的调用。这个小技巧能挽回不少看似不可救药的错误。4.3 多轮调用与执行结果回灌让 function calling 有价值函数执行完后真正让系统“变聪明”的环节才刚刚开始把执行结果回灌给模型。这一步的取舍在于回灌什么、以什么形式回灌。直接回灌原始 JSON 结构看似省事实际会让模型在组织回答时不知所云尤其当结果是一个几十字段的业务对象时模型无法准确抓住重点回应用户。我的做法是在函数定义阶段就给每个函数配置一个“结果摘要模板”由后端把执行结果加工成一段简洁的自然语言或结构化的短文本再回灌。比如查订单结果不是回灌整个订单 JSON而是加工成“订单号 20260903001总计 368 元状态为已发货预计 3 天后到达”。模型拿到这段内容再用它组织用户回复准确性和可读性都高很多。这个过程也决定了函数执行结果不仅是给用户看的更是给模型一个“新的事实输入”让它基于最新的真实数据继续推理。如果一次任务需要多轮函数调用比如“查库存 - 算运费 - 下单”还要注意每一轮都把上一轮的结果作为上下文传给下一轮。同时要在 prompt 里暗示模型“你已经有查询结果了请直接使用不要重复查询”。不然模型经常会在第二轮又开始重新调同一个查询函数导致死循环。5. 工程化实践中的几个关键问题5.1 安全与权限最少权限原则function calling 给了模型操作真实系统的能力这也意味着安全事故面被放大了。我在项目上线前做过一次安全审计发现一个挺吓人的潜在问题测试时模型可以调用“批量删除用户”这种函数没有任何权限校验。如果被恶意用户诱导即 prompt injection 场景后果不堪设想。设计上一定要遵循最小权限原则。给模型注册的函数必须限定在“当前用户会话允许执行的操作”范围内。后端在执行函数前同样要做一次独立的权限校验不能依赖模型输出。模型可能“没想那么多”把越权函数的参数填得漂漂亮亮但执行层必须拦住。另外一类值得注意的操作是“写操作”或“敏感操作”扣款、删除、发送消息等。这类操作我建议在函数描述里加上一句“该操作会真实写入数据请二次确认用户意图”同时在业务侧对关键写操作做人工确认或二次验证机制。成本会增加但安全上这笔投入不能省。5.2 成本、延迟与成功率的三角博弈function calling 并不是免费的午餐尤其是在成本与延迟这两项上很多人容易忽略。函数定义虽然只在系统层面以文本形式存在但每个函数描述都会占用 token每多一个函数每次请求的 prompt 长度就多一截。如果你有 30 个函数且每个描述都写了 200 个 token光函数定义就占了 6000 token这不是一个小数目。候选函数收缩不仅能提升路由准确率也能直接砍掉成本这也是为什么我前面强调规则预筛的意义所在。延迟方面一次函数调用天然比纯文本回答多一次 API 往返第一次模型生成调用请求后端执行函数再把结果送回模型生成最终回答。这个两段式延迟无法避免但可以优化比如后端执行函数时减少额外网络开销、缩短一个函数到几百毫秒这样总延迟还能维持在用户可接受的范围内。还有一个常被忽视的成本点是“失败重试”。模型调用函数失败后如果直接让模型再试一次就是又一次完整的模型请求。我在设计里会把可预判的调用失败分成两类参数校验失败走“回灌修正”业务执行失败走“返回错误信息换取模型切换方案”。重试次数要设置上限一般两轮就够了超过之后直接给用户一个兜底文案避免在失败分支里烧钱。5.3 日志归因与评测没有反馈就无法优化function calling 系统上线之后最怕的就是“黑盒运行”——只看到用户对话不知道模型在背后调了什么函数、参数填对了没有、执行结果是否满足用户需求。我强烈建议每个调用步骤都记录结构化日志至少包括触发时的用户输入、模型输出的函数调用参数、后端执行结果、回灌给模型的文本、最终对用户的回答。有了这些日志你才能做归因分析。比如用户反馈“下载不下来”你查日志才能知道模型调的是一个“查询报告”函数而不是“下载报告”函数问题出在函数描述有歧义而不是执行端有问题。这类问题如果不开日志可能要排查很久。评测方面常用做法是准备一组覆盖典型场景的测试样本每一条包含用户问题、期望调用的函数、期望的参数取值。然后用你的系统跑一遍统计函数选对率、参数填对率和最终回答满意度。这个评测集最好持续迭代每发现一个错误案例就把它补充进去防止同一个问题回归。我个人的经验是function calling 项目 70% 的优化时间都花在“通过日志发现问题 - 改进函数定义 - 评测集验证”这条循环里循环走得越勤系统越稳。6. 插件化思路、开源方案与未来延展6.1 小而美的本地实现 vs 平台级方案在设计取舍上还有一个绕不开的问题是自建一套 function calling 管道还是直接用平台级方案我的答案是看你的系统复杂度。早期只有几个函数、单模型跑通 Demo 的阶段直接用平台 SDK 就够比如各家大模型平台提供的 function calling 接口、工具调用协议。这个阶段没必要重复造轮子重点是先跑通业务闭环积累真实的调用案例。但当函数数量超过 20 个、有权限隔离需求、要支持多模型切换或多租户隔离时平台 SDK 的简单透传就不够用了你需要在自己的服务层做一层抽象。这层抽象核心要做的事包括函数注册中心、权限过滤、参数校验、执行器、结果回灌、日志缓冲、失败重试策略。它最好与具体模型解耦这样你换模型厂商时只需要适配一个调用协议函数定义和执行逻辑都不用动。这个架构的成本不低但如果你判断自己的业务会长期演进这套抽象迟早要做早做比晚做好。6.2 让模型学习新增函数动态注册的玩法固定函数列表是起步阶段的做法真正灵活的系统需要支持动态注册。比如你在运营一个电商助手每周都要上架新的促销活动接口不可能每次都在代码里加一个函数定义并发版。这时可以把函数定义做成“可插拔”的函数定义存在配置中心或数据库里系统启动时按业务场景加载。这里有个容易被忽略的取舍动态加载的函数也需要配套的测试与校验。我在一次改动里给系统新增了一个函数定义有问题参数枚举值写错结果模型调用时频繁填出非法参数影响了整条链路。后来我加了一个“注册前自动校验”流程新函数定义提交时会用几个内置的测试输入跑一遍调用生成确认模型能够正确理解和使用这个函数后才允许它进入线上列表。这个流程看起来很笨但对稳定性帮助很大。6.3 如果让我重做一版我会改什么回顾这个项目从设计到落地的过程如果让我重新做一版我会把更多时间花在“函数定义评审”和“评测集建设”上而不是急着写代码。因为所有调度策略、安全设计、问题排查本质上都依赖函数定义的质量。定义好在哪模型就在哪发挥定义模糊的地方再聪明的模型也会犯错。另外一个会改的点是更早引入规则预筛。第一版里我把所有函数一股脑塞给模型函数一多就出现选择混乱。后来加上轻量预筛后准确率和成本同时改善。这个经验让我意识到大模型擅长的是理解复杂的用户表达和推理而不是从几十个看似相似的工具里做精确选择后者更适合用确定性规则解决。最后再分享一个小技巧给函数增加版本号。函数定义变更后旧日志里的调用记录还能对应到当时的函数结构方便做前后对比分析。这个小改动看起来很不起眼但在你回头排查“为什么这个月参数错误率比上个月高”这种问题时能救你一命。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进