从Function Calling到Code Mode:AI Agent工具调用的范式跃迁与工程实践

发布时间:2026/9/30 9:32:34
从Function Calling到Code Mode:AI Agent工具调用的范式跃迁与工程实践 最近跟几个做 Agent 的朋友聊天发现一个特别有意思的现象大家的话题已经从怎么封装 Function Calling变成了我怎么又卡在 Function Calling 的工具参数上了。然后有人丢出一句挺激进的话——Function Calling 该退休了新神叫 Code Mode。乍一听像标题党可真把两种方案放到真实工作流里跑一遍会发现这个说法虽然不严谨但方向是对的以“预先声明函数”为核心的 Agent 交互太僵硬而以“直接生成并执行代码”为底座的交互模式正在从玩具变成生产力。这篇文章不打算做那种纯概念的对比我想直接用工程视角拆一遍Function Calling 到底解决什么问题、卡在哪里Code Mode 是什么原理、怎么落地搭一套以及两者在实际项目里怎么共存。如果你正在做 AI Agent、自动化脚本工具、数据分析型助手这篇文章值得你花十分钟看下去。1. 接线员模式的真相Function Calling 是怎么工作的1.1 管中窥豹一条请求的完整链路先快速回忆一下 Function Calling 的标准流程。你把工具清单定义成 JSON Schema塞进 system message让模型在合适的时机输出一个特殊结构体比如{name: get_weather, arguments: {city: 北京}}。你的后端代码收到这个输出后拿着参数去执行真正的业务函数再把结果拼回对话上下文让模型基于工具结果继续回答。这个过程本质上是一个接线员模型模型不直接做任何事它只负责说清楚请谁干、用什么参数干真正动手的是你写在服务端的那一堆函数。这种设计在 2023 年刚出来的时候确实优雅它让大模型可以用很小的成本接入任意外部系统天气查询、库存检索、订单创建、数据库查询全都变成了模型口中的一句指令代码层面只需要维护好 schema 和分发逻辑。我在第一批 Agent 项目里就是这么干的。一开始只接了三个工具很顺手。后来工具涨到了二十几个问题就来了工具的字段像传染病一样互相嵌套一个下单函数要传优惠券 ID、收货地址、支付方式模型经常把参数类型搞错更麻烦的是只要业务接口一变我这边就得跟着改 schema然后重新测试一遍没跑顺模型就开始在工具名上编造。这个阶段我踩过的最大认知坑是以为 Function Calling 是给模型增加能力的后来才明白它只是给模型增加了一张菜单而且这张菜单必须由人类事先写死。菜单上的菜看似多模型却没有任何自由发挥的空间它只能从已定义好的选项里选不能自己发明一个做法。1.2 三个典型痛点都是结构性的第一个痛点是工具粒度不可调。Function Calling 要求你把每一个动作拆成一个函数。做数据分析的时候就特别难受你是定义一个calculate_mean还是calculate_something? 数据分析的中间步骤可能有几十种你不可能把 pandas 的每个方法都定义成函数。结果是工具越建越粗模型只能在粗粒度函数里凑合输出结果经常不是你想要的。第二个痛点是错误恢复靠撞运气。模型生成了工具调用你的代码执行报错了这时候该怎么办标准的操作是把这个错误信息重新塞给模型求它再调一次。但模型并不知道你代码的上下文它只能凭空猜测是参数错了还是函数不存在。一次两次还能忍多步工具链中间断一次整个流程就可能螺旋式下降耗光上下文。第三个痛点是模型并不能真用工具它只是在请求你使用工具。真正的状态管理、异常分支、参数校验全在你的业务代码里。换句话说模型在 Agent 里更像一个发号施令的中控而实现细节全是人肉硬编码的。这种架构在游戏规则固定、工具数量少的系统里还挺干净可一旦任务变成开放式的——比如这份数据里有脏数据帮我清洗后做个相关性分析再画三张图——固定函数集就接不住了。你会发现自己写了一个不停膨胀的转发层维护成本比业务逻辑还高。2. Code Mode:把代码当作 Agent 的第一语言2.1 理念上的彻底反转Code Mode 这个模式说白了就是不要预先定义函数而是让模型直接写一段代码然后在受控环境里执行这段代码执行结果再作为上下文反馈给模型。模型不再说请帮我调用一个函数而是说我自己动手写一段 Python 脚本把活干了。这个转变最迷人的地方在于它把工具的形状从人类预先定义变成了模型现场铸造。Function Calling 时代工具的形状是被 schema 固定死的模型只能在固定的槽位里填参数Code Mode 时代工具的形状是模型根据任务现场生成的任务变了代码也跟着变几乎没有迁移成本。举一个我实际跑过的例子。有一回要处理一份 CSV里面大约五十万行日志数据任务是找出所有异常时间戳的分布规律。用 Function Calling 做法是我先准备一个数据分析函数参数包括文件路径、分析类型、输出格式然后让模型填参数。模型填得对不对先不说即使填对了函数内部写死的分析逻辑也未必覆盖异常时间戳这种灵活的语义。后来我改成 Code Mode我只需要给模型一句话你有一个 Python 沙箱文件挂在 /data/raw.csv自己写脚本做分析最后给我结论和关键数字。模型写出来的脚本会自己过滤脏数据、自己算时间窗口、自己输出一个摘要我只要把摘要和结论展示给用户就行。这就是 Code Mode 的杀伤力它把 Agent 的功能边界从菜单里有什么扩展成了语言模型会什么。模型会 PythonAgent 就等于拥有了 Python 的整个生态模型会 SQLAgent 就等于能直接用数据库。2.2 一次类比接线员与包工头用真人类比会更好懂。Function Calling 就像你给一个接线员打电话告诉他要订披萨接线员背后有一套标准流程可选但他不能自己决定今天用哪家店也不能临时改烹饪流程。Code Mode 则是你请了一个包工头你描述想要的效果他现场画图纸、买材料、安排工序如果有偏差他可以现场调整。包工头比接线员笨一点、开销大一点、难以完全预测但面对未知任务时包工头的完成度和灵活性是接线员没法比的。藏在背后的技术原因是LLM 经过海量代码训练对代码执行结果的预测能力通常比对业务函数调用结果的预测能力更稳定。你让一个模型预测pd.read_csv返回什么对象它可能答错细节但整体逻辑是对的你让它预测一个自定义业务函数的返回值因为业务逻辑根本不在它的知识范围内它只能瞎蒙。我自己衡量两种方案的效率差异最直观的一句话是Function Calling 适合固定的流程Code Mode 适合变化的目标。当前 Agent 的场景正在从问答查询走向任务执行后者占据的比例越来越大Code Mode 自然就显得越来越香。3. 从零搭一套 Code Mode 执行环境3.1 最小实现骨架Code Mode 听起来高大上落地架构其实不复杂。核心就三块一个能运行代码的沙箱、一个把执行结果反馈给模型的循环、一个控制截止条件的开关。下面是我在一个内部工具里用过的最小实现思路不是生产级代码但足够让你理解全貌。# 伪代码关键节点示意 def code_mode_agent(user_task, max_rounds5, timeout120): prompt build_system_prompt(user_task) for i in range(max_rounds): code llm_generate_code(prompt, last_resultNone if i 0 else result_summary) exec_output sandbox_run(code, timeout_secondstimeout) if exec_output.exit_code 0: answer llm_generate_answer(prompt, code, exec_output.stdout) return answer else: result_summary f出错信息:{exec_output.stderr} prompt result_summary return 超过最大迭代次数任务失败先看这段伪代码里的两个关键选择。第一为什么让模型每次重新生成完整代码而不是生成补丁因为大多数模型在修改已有代码时的成功率并不比分步重写高多少反而重写能让模型少受上下文污染。第二为什么要设置max_rounds因为模型的自我修复能力不是无限的如果同一段代码连续报错三到五次强行让它继续修只会消耗上下文和算力。我在项目里把次数限制在 5 轮以内超过就挂起转人工处理。沙箱本身我没用太重的方案。线上服务用容器隔离本地跑就直接在子进程里加超时限制。Python 沙箱要注意的是别让模型能随便写磁盘和读环境变量权限能关多少关多少。如果只是处理数据可以给它一个只读数据目录和一个独立的输出目录禁止联网。别小看这条模型写的代码经常不按常理出牌比如为了求一个聚合值直接去访问公网 API这在合规上是很大的隐患。3.2 一次真实任务对比写一个更具体的案例方便你理解两种方案在同样任务上的差距。任务是分析sales_data.csv中最近三个月的销售趋势找出增长最快的品类并计算该类目的平均客单价。Function Calling 方案下你至少要准备两个工具函数query_sales_data和calculate_category_growth。前者参数是日期范围、字段名、聚合粒度后者参数是品类、开始日期、结束日期。模型需要连续调用两个函数If 中间环节的参数传错就得重新来一轮。最烦的是你必须在函数内部提前写好增长最快的品类这个判断逻辑等于把业务预判写死在了代码里。Code Mode 方案则清爽得多。我给模型一个系统提示当前目录挂载了/data/sales_data.csv你可以用 pandas 读取环境里有任何常见的数据处理库你的输出应包含结论和关键代码片段。模型自然就会写出一段脚本自己读 CSV、自己按月份聚合、自己计算环比、选出增量最大的品类、再算客单价。整个过程中真正的工具就是那只沙箱和 Python 运行时我不必为了这个任务提前定义任何函数。有人可能会说这不算严格对比因为关键在于模型写代码的能力足够强。确实模型本身的代码能力决定了 Code Mode 的上限。但我的经验是在 2025 年这个节点主流模型的代码生成能力已经远超大部分业务工具的 schema 描述能力。与其费力把业务逻辑翻译成函数定义不如直接让模型写脚本再由人类审查脚本。这种方式反而更容易控制质量。3.3 提示词和系统设计的几个要点在 Code Mode 下面提示词的设计逻辑跟 Function Calling 完全不同。Function Calling 的提示词主要是工具定义和方法说明Code Mode 的提示词更像是在给一个实习生布置编程任务。我总结过一套比较稳的说法告诉它环境里有什么你可以用 Python 3.11、pandas、numpy、matplotlib数据文件在 /data 下。告诉它输出要什么最终结果需要给出结论数字并附上关键处理步骤的摘要不要输出完整源码。告诉它边界是什么不要尝试安装新依赖不要访问外网不要读取 /data 以外的文件。为什么要把输出限制成结论数字摘要因为在真实项目里如果模型把整个脚本的 stdout 全部打印回来内容动辄几千行很轻松就把上下文窗口撑爆。更合理的做法是让脚本自己输出一个精简的结果再由执行层把 stdout 裁剪到一定长度只保留头部和尾部。我通常把 stdout 限制在 4000 个字符左右超过就做截断并在末尾标注[截断]。迭代机制上还要注意一点模型的修复代码不能无限骚扰沙箱。我在执行层加的约束是单次执行最长 120 秒单任务最多执行 8 次一旦超时或者超次数立即终止。这样即使模型写的代码有死循环也不会拖垮整个服务。4. Function Calling 没死它在哪些位置依然正确4.1 真正适合 Function Calling 的场景说了这么多 Code Mode 的好话如果直接说Function Calling 该退休了那是不负责任的。实际上我在生产系统里依然大量保留 Function Calling尤其是在需要强约束、可审计、低延迟的场景里。最典型的就是权限敏感操作。比如给指定用户发放优惠券、创建订单、发送短信这类动作你必须用预先定义的函数做一层校验防止模型乱来。Code Mode 虽然灵活但模型写出来的代码你没法保证每一次都经过权限校验风险很高。这个场景下Function Calling 的价值不是智能而是稳定和可追踪。还有一个场景是多 Agent 协作。多个 Agent 之间互相调用对方的工具时接口契约比灵活性重要函数签名是双方通信的协议。这时候 Function Calling 的规范化描述天然适合当协议Code Mode 反而会因为太灵活导致上下文对接失控。延迟方面也要说一句。Function Calling 只需要一次模型输出加一次函数执行通常在几百毫秒到两秒内就结束而 Code Mode 需要生成代码、执行沙箱、可能还要迭代修复动辄三五秒起步。在高频查询场景下这个延迟差异是会直接影响体验的。4.2 长远看这是分工问题不是替代问题我现在的判断是Function Calling 与 Code Mode 更像是同一个问题的两个极端中间还有很宽的灰度带。你在架构一个 Agent 的时候可以把任务分成两层需要跟外部系统刚性对接的走 Function Calling需要开放探索和处理复杂数据的走 Code Mode。两者并不是敌人而是分工。有一个挺实际的组合用法我最近很常用用 Function Calling 控制动作——比如让模型决定要不要发邮件、要不要下单、要不要更新数据库用 Code Mode 控制思考——比如让模型分析数据、整理报告、做预测。动作层要的是安全和稳定思考层要的是开放和灵活各取所长。只要你把这两层边界划清楚Agent 的整体可靠性会大幅提升。所以与其说 Function Calling 该退休不如说它应该退到更合适的工位上。标题说新神叫 Code Mode我更愿意把这句话理解成新一代的智能体工作流重心正在从调用工具转移到编写工具这是更本质的范式变化。5. 高频翻车点与实战排查实录5.1 五个常见坑动手做 Code Mode 的人往往会在一开始靠得太顺然后突然被几个隐蔽问题打趴。这里列一下我实打实碰到过的坑你可以直接当避坑清单用。一是沙箱没限制出网模型主动外联。明明只是分析本地文件模型脚本却去请求某个公开 API。不是说外联一定有害而是这个行为你根本没法预判。解决思路是默认禁止出网真有需要再单独开白名单。对绝大多数数据处理任务来说本地环境完全够用不需要外网。二是模型疯狂打印大对象。比如直接把整个 DataFrame 打出来输出几万行上下文爆掉。我处理的办法是给 stdout 加截断同时在提示词里反复强调只打印必要结果。另外还可以在模型生成代码后用静态规则把print(df)这类可疑语句提示给模型让它改成print(df.head())。三是想让它修 bug结果它另起炉灶。我第一次碰到的翻车是代码报错后我把 stderr 原样丢回模型模型直接重写了一个跟之前逻辑完全不一样的脚本前文辛苦确定的数据口径全丢了。后来纠正的办法是把历史代码和当前错误一起给它并明确要求它在原代码基础上做最小改动禁止重写整个流程。四是依赖缺失导致连环失败。沙箱里没装某个库模型又反复尝试pip install。这时候要么提前把常用库装好要么在系统提示里写清楚环境内置 pandas/numpy/matplotlib不可安装新包。这个提示对减少无谓迭代非常有效。五是循环迭代失控。一个看起来很简单的问题模型修了十几次都没好最后把整段上下文变成了垃圾。这时候一味让它继续修既慢又费钱。我在工程上加了一个隐蔽的健康度指标——比如连续 3 轮错误信息完全一样就说明模型进入了死胡同直接停止循环并返回当前错误给用户或人工处理。5.2 我踩过的几个真实案例有一个特别值得拿出来说的案例发生在处理非结构化文本的时候。我一开始让模型自己读日志文件、自己洗数据、自己算统计听起来很完美结果模型每次都要把整个文件读进内存用 Python 正则硬洗。文件一上 GB沙箱直接内存溢出。后来我把方案改成让模型先把任务拆成两段第一段写脚本做快速统计第二段在统计结果里找规律。这个拆分带来的收益非常直观执行时间从几分钟降到了十几秒。还有一个多 Agent 架构里的问题。我让一个主 Agent 用 Code Mode 做分析再把结果交给另一个 Agent 让执行 Function Calling 发送通知。问题出在数据格式衔接分析 Agent 输出的 JSON 结构不稳定同一个字段名一会是total_amount一会是total_sales。这个问题让我明白Code Mode 输出的数据也需要做一层格式对齐不能直接拿去调函数。后来我给主 Agent 加了一个输出模板校验必要的时候让模型重写输出才把这个问题治住。5.3 实用的排查技巧如果你也打算把现有 Agent 从 Function Calling 往 Code Mode 方向迁移不要一上来就全量替换。我推荐的做法是做三个月并行先在数据分析和报表生成这类非敏感任务上用 Code Mode等模型产出质量稳定了再把它推向更多场景。同时一定要保留日志系统记录模型生成的代码、执行结果、迭代过程这不仅能帮你复盘任务失败的原因还能把高质量的成功代码沉淀成模板下次任务优先让模型参考。另一个技巧是把固定套路沉淀到系统提示里。我在与模型协作一段时间后会发现某些模式成功率特别高比如先画数据分布再建模、先做数据质量检查再聚合这类通用流程。把它固化描述进提示里会让模型生成代码的整体质量高一大截比让它自由发挥稳定得多。如果你是在给本地用户提供 Code Mode 功能还要注意一个体验问题执行不一定按秒完成所以交互上要做成异步任务先给用户一个运行中的反馈等执行完了再推送结果。我见过很多项目因为这一步没做好用户觉得工具卡住了实际只是沙箱在跑。最后聊点我的实际感受如果让我给一个容易记住的结论我会说Function Calling 教你给 Agent 配一整套精密的工具而 Code Mode 教你让 Agent 在没有工具时自己造工具——真正到了复杂任务里后者往往能救你命。我自己最近的大多数新项目已经默认先把 Code Mode 底座搭好再在需要跟外部系统刚性对接的位置补上 Function Calling这个组合比任何一种单用都好用。Code Mode 也不是没有短板它依赖模型代码能力、需要更严格的沙箱机制、延迟更高这些我都体会过。但往未来看模型写代码的能力只会越来越强沙箱基建也会越来越成熟这条路的想象空间非常大。如果你还在纠结手里的 Agent 为什么不够智能我建议别急着加更多工具定义先给它一个能写代码、能跑代码的沙箱再看效果。实测下来那种质变的感觉几乎每次都让我意外。