从Function Calling到Code Mode:AI Agent工具调用的新范式 1. 先聊个真实感受Function Calling 用久了真的会心累如果你过去一年在做大模型应用Function Calling 这个名字你不可能没听过。它几乎是 Agent 和工具调用的默认姿势你把工具写成 JSON Schema模型在推理时输出一段结构化的参数你的代码再去执行对应的函数。刚上手那会儿确实觉得“哇模型真的能自己决定调用什么工具了”但用久了你会发现这套东西的别扭之处全藏在日常里。举一个我自己的例子。做一个内部数据看板需要让模型根据用户的自然语言查询去聚合几十张表。第一版方案很常规注册了一个 query_data 的 function参数是表名、筛选条件、聚合方式、时间范围。刚开始测几个固定问题效果还行。一旦用户问得稍微绕一点比如“这个季度每个区域的毛利率环比变化找出增速最慢的两个”Function Calling 就开始露怯了。模型要生成非常复杂的嵌套参数一个不小心就结构错误参数漏字段、枚举值写错、日期格式不统一各种问题轮番来。更烦的是维护成本。业务上每加一种查询方式就得改一遍 schema 描述还要想方设法把字段说明写得足够清楚。模型有时理解不到位的你甚至得在描述里写“如果用户提到 XX则应当把条件转换为 XX”。这哪里是给模型用的接口这分明是在用自然语言写业务规则。所以当 Code Mode 这个思路开始在圈子里扩散的时候我几乎是第一时间就去试了。试完之后我的感觉是Function Calling 确实该退休了至少在有相当一部分场景里它已经被更本质的做法取代了。2. 为什么 Function Calling 会走到这一步2.1 先复盘一下 Function Calling 到底解决了什么问题我们回过头看 Function Calling 的初衷。在 GPT 这类模型刚接入业务系统时一个大问题是模型只会说话不会动作。你问它“帮我查一下订单数”它只能给一段文字回复不可能真的去碰你的数据库。Function Calling 做的事情就是给模型一个“行动的接口”模型知道自己有哪些工具可用理解了用户的意图后输出一个符合格式的调用请求由你的业务代码真正去执行。这套机制的核心优势是可控。工具的参数有 schema枚举值有约束模型输出的 JSON 可以被程序严格校验错了还能重试。对于调用外部 API、执行简单查询、操作固定功能这类任务Function Calling 非常合适。比如查天气、翻译一段话、发一条消息、创建一条工单这种“功能边界清晰、参数数量有限”的场景Function Calling 的表现稳定得让人放心。它也带动了整个 Agent 生态的爆发。你看到的各种自动化工作流底层几乎都是 Function Calling 的变体。模型做规划调用一堆工具再根据结果回填下一步。这套架构支撑了很多产品从 Demo 走向可用。要说 Function Calling 没有价值那肯定是不客观的。2.2 但问题的根源在于它把“工具调用”变成了一个 JSON 生成任务Function Calling 的真正尴尬之处不是它不稳定而是它让模型做了一件并不擅长的事把复杂的操作意图硬生生压缩成一层有限的 JSON 结构。模型最擅长的是什么是生成连贯的自然语言和代码。你让模型写一段 Python 脚本它可以自动处理循环、判断、异常、变量传递这些是它在海量代码语料里学出来的本能。但你让模型输出一份 JSON 参数它要考虑的却是这个工具的入参怎么嵌套、缺失字段怎么处理、日期格式怎么表达。JSON 结构一旦变得稍微复杂一点模型的错误率就开始飙升你的代码必须花大量精力去兜底。更重要的是Function Calling 的每个工具都是静态的。你注册了什么模型就只能用什么。业务逻辑越复杂工具就越多工具一多模型的上下文就开始被工具的描述占满。我见过有些 Agent 项目光工具描述就有两万多个 token模型在真正的用户问题还没进来之前就已经被系统提示词和工具清单拖住了。这个架构走到后期就是在用堆工具的方式对抗复杂性而复杂性的增长永远比你在 schema 上的打补丁快。2.3 还有一层隐性问题工具之间的逻辑编排极其笨拙Function Calling 的另一个隐含成本是编排。如果你要完成的任务需要五步操作每一步依赖上一步的结果你需要在代码里写一个复杂的 orchestrator。比如模型先调 search_tool拿到结果后你返回给模型模型再决定调 summarize_tool再然后你可能需要把结果格式化后调 send_email_tool。每一步都要经历一次完整的推理环节中间任何一个环节的 JSON 解析出错后面的链条就断了。我在实际项目里还踩过另一个坑当两个工具的参数结构非常相似时模型经常会选错。比如“创建用户”和“更新用户”这两个工具参数就差一个 user_id模型有时候就会在这个字段上犯迷糊把创建当更新用。为了规避这个问题我们不得不在 schema 描述里反复强调两者的区别甚至把工具名改得冗长拗口就为了让模型分得清。这种做法修起来快但明眼人都知道这是下策它反映的正是 Function Calling 在表达复杂行为时的先天不足。3. Code Mode 是什么它为什么值得被叫一声“新神”3.1 核心转变从“预定义工具”到“现写代码”Code Mode 的思路非常直接别再让模型去输出 JSON 调用预定义函数了直接让模型写代码然后在受控环境里把代码跑起来拿到真实结果。我第一次看到这个思路时的反应是“这不就是让模型写脚本吗”跟之前那些“让 ChatGPT 生成 Python 代码然后你手动跑”的玩法有什么区别仔细了解之后才发现区别大了。Code Mode 不是让模型在聊天框里生成一段代码给你而是让模型在一个执行环境里既能写代码又能立刻执行代码还能读取执行结果根据结果继续修改代码。模型从一个“提出建议的人”变成了“亲自动手解决问题的人”。以数据分析为例。传统 Function Calling 的做法是你提前给它准备一堆分析函数它选一个来调用。Code Mode 的做法是用户直接说“我想看一下各区域最近三个月的销售趋势异常值单独标出来”模型自己写 Pandas 脚本读数据、算指标、画图把结果文件给你。中间没有任何人提前设计过这个分析流程一切由模型临场发挥。这是两种完全不同的范式。3.2 为什么代码比 JSON 更适合作为模型的操作语言我用一个简单的类比来说。你雇了一个实习生Function Calling 是给这个实习生一本员工手册手册里规定了所有他能做的事每件事的流程都写死了他只能按手册执行。Code Mode 则是你告诉实习生目标让他自己想办法他可以上网查、写脚本、做实验只要最后把事情办成就行。哪个实习生解决复杂问题的能力强显然是后者。代码本身就是一种极其灵活的表达语言。循环、条件、异常处理、模块组合这些能力天然存在于代码里模型生成 Python 脚本的时候不需要像生成 JSON 那样刻意考虑 Schema 约束只需要遵循代码语法和逻辑。模型在这方面的训练数据极其丰富生成质量远比生成结构复杂的 JSON 稳定。而且代码是可执行的执行之后产生的错误信息、输出结果又能反馈给模型形成迭代闭环。模型可以看着报错信息自己修 bug这比 Function Calling 中“解析失败——让你重试——再解析失败”的体验顺畅太多。从我实际用下来的体验看Code Mode 在处理开放式任务、多步骤任务、需要临时决策的任务时效率优势非常明显。模型不再需要先声明“我要调用 search_file 工具、我要调用 aggregate_data 工具”而是直接用一段脚本把这两个逻辑写在一起一次执行搞定。3.3 为什么叫“新神”它重新定义了 Agent 的工作方式圈子里有人调侃说Function Calling 把模型限制成了“会说话的接口调用器”Code Mode 则让模型真正变成了“会干活的程序员”。这个说法虽然有些夸张但确实点中了要害。Code Mode 把 Agent 的工作方式从“模型——工具——再模型——再工具”变成了“模型——代码——执行——反馈——修改代码”。这中间少了一层工具注册的繁文缛节多了一层执行反馈的闭环。AI Agent 不再是被动等待工具输出的调度器而是一个主动写代码、调试代码、最终交付结果的执行者。这种变化放在 Agent 演进的大背景里看确实配得上“新神”这个称呼——它把模型在编程上的能力完整地释放到了自动化任务里。4. 实操对比同一个任务两个方案的高下立见4.1 场景设定让模型分析一批 CSV 并输出洞察报告为了说清楚差异我设计一个最常见的任务。现在有一个 sales_data 文件夹里面放了 12 个月的销售记录每份文件包含日期、区域、品类、销售额、成本、数量这些字段。需求是分析全年各区域的总销售额和毛利率找出同比增长最快的品类并输出一份 Markdown 报告。我分别用 Function Calling 和 Code Mode 来实现这个需求把过程中的真实体验记录下来差异非常直观。4.2 Function Calling 方案的实现过程如果走 Function Calling 路线我首先需要定义工具。大致要注册这些函数def list_files(directory: str) - list[str]: ... def load_csv_to_dataframe(file_path: str) - DataFrame: ... def compute_total_sales(df, group_by: str) - DataFrame: ... def compute_gross_margin(df, group_by: str) - DataFrame: ... def find_fastest_growing_category(df, start_month: str, end_month: str) - str: ... def write_markdown_report(content: str, output_path: str) - str: ...每个函数的参数都需要定义 JSON Schema。比如 compute_total_sales 的 schema{ name: compute_total_sales, description: 按指定列分组计算销售额总和, parameters: { type: object, properties: { group_by: {type: string, enum: [region, category]}, sort: {type: boolean, description: 是否按销售额降序排列} }, required: [group_by] } }工具准备好之后调用链路是这样的用户提出需求模型分析后决定先调用 list_files 获取文件列表代码解析返回的 JSON把文件列表送回模型模型决定调用 load_csv_to_dataframe但你需要告诉它这个函数是加载某个文件并产生 DataFrame问题在于“DataFrame 无法通过 JSON 返回”于是你只能让它先把 dataframe 转成 csv 存到临时目录再把路径传下去模型再调用 compute_total_sales它需要知道之前生成的临时文件路径每一步都要维护一个状态上下文把中间结果传来传去。这里的麻烦点非常具体。第一中间状态很难通过 JSON 传递DataFrame、图片、模型对象这类二进制或内存对象都得落盘或者序列化路径管理变得复杂无比。第二模型每一步的决策都需要解析一次 JSON 参数表出错率跟步骤数量成正比。第三我把“计算毛利率”“找增长最快品类”拆成了独立工具后模型经常搞混参数把 group_by 填反或者把年月范围填错。整个过程走下来我最大的感受是十个工具的 Function Calling 方案比直接手写一个 50 行的 Pandas 脚本要慢得多也脆弱得多。4.3 Code Mode 方案的实现过程换成 Code Mode 之后整个流程就完全不一样了。我不需要预定义任何工具直接把需求告诉模型就行。模型的执行逻辑大致长这样import pandas as pd import glob import os files sorted(glob.glob(sales_data/*.csv)) df_list [] for f in files: df pd.read_csv(f) df_list.append(df) df pd.concat(df_list, ignore_indexTrue) df[date] pd.to_datetime(df[date]) df[month] df[date].dt.to_month df[profit] df[sales] - df[cost] # 各区域总销售额与毛利率 region_stats df.groupby(region).agg( total_sales(sales, sum), total_cost(cost, sum) ) region_stats[gross_margin] (region_stats[total_sales] - region_stats[total_cost]) / region_stats[total_sales] # 各品类同比增速 pivot df.pivot_table(indexregion, columnscategory, valuessales, aggfuncsum) growth pivot.pct_change() * 100 # 生成报告 report_lines [] for region, row in region_stats.iterrows(): report_lines.append(f### {region}: 销售额 {row[total_sales]:.2f}, 毛利率 {row[gross_margin]:.1%}) fastest_categories growth.sum().sort_values(ascendingFalse).head(3) report_lines.append(f### 同比增长最快的品类: {fastest_categories.index.tolist()}) with open(report.md, w) as f: f.write(\n.join(report_lines))模型在执行环境里跑这段脚本如果中途报错比如日期格式不匹配、列名写错执行环境的报错信息会回传给模型模型自己就能修改脚本重新跑。整个过程不再需要我注册任何工具、定义任何 schema、维护任何状态。我只需要给一个目标然后等它交付报告文件。我自己还试过更复杂的场景比如“把每个区域的销售额按周做平滑然后画趋势图并标注异常点”。Code Mode 可以自己调用 matplotlib 画图、自己写一个简单的移动平均函数、自己判断异常点的阈值最后生成一张图。这在 Function Calling 里几乎不可能做到因为你需要预先定义一整套图表生成的工具还要把“异常点怎么定义”这种主观判断写成工具参数光想想就头大。4.4 两个方案的成本对比我整理了一个直观的对比表对比维度Function CallingCode Mode工具注册成本每个功能都要写 schema、维护描述无需注册直接描述需求中间状态传递需要落盘/序列化/上下文管理代码变量直接传递复杂逻辑支持依赖模型拆解步骤容易断链模型直接写循环、判断、异常处理错误恢复参数解析失败后由外部重试执行报错自动反馈给模型修改动态决策只能调用预先定义好的操作模型可按需生成任何操作上下文开销工具描述占用大量 token无需工具描述token 更聚焦单看这张表Code Mode 在几乎所有维度上都更轻便、更灵活。这也是为什么它一出现就让很多做 Agent 的开发者感到兴奋。5. 落地 Code Mode 要注意的那些坑5.1 第一个必须解决的问题安全隔离让模型写代码然后直接执行第一个反应必然是它写出来的代码安全吗这确实是 Code Mode 最大的争议点。如果模型生成的代码在你的主服务器上直接运行一旦它被提示注入或者生成了一段恶意的系统操作后果不堪设想。我自己的做法是给 Code Mode 搭一个沙箱执行环境。最省事的方式是用 Docker 容器把执行环境限制在一个无网络或受限网络的容器里只挂载必要的输入输出目录限制 CPU 和内存。我用的配置大概是这样的docker run --rm \ --network none \ --memory 2g \ --cpus 2 \ --read-only \ -v /data/input:/workspace/input:ro \ -v /data/output:/workspace/output:rw \ sandbox-image python /workspace/run.py几个关键点--network none是为了阻止模型生成的代码访问外部网络防止数据外泄--read-only让根文件系统只读容器里改不了系统文件内存和 CPU 限制是为了防止模型写了一个死循环或者内存爆炸的脚本拖垮宿主机。输入目录只读挂载输出目录才是唯一可写入的地方。如果不用 Docker也有基于 Jupyter Kernel 的隔离方案或者用 Deno 的权限沙箱跑 JavaScript 代码。但不管用什么方案安全隔离是 Code Mode 的第一道防线绝对不能省。5.2 第二个问题执行反馈的质量决定迭代效率Code Mode 之所以能自我纠错靠的是执行环境返回的反馈。反馈质量越高模型的修复效率越高。这里面有个很容易踩的坑如果你只是把一个 Python traceback 原样丢给模型它往往能修但有时候会绕圈子。更好的做法是把标准输出、标准错误、当前工作目录的文件列表、以及执行耗时一起返回给模型。我在实现反馈逻辑的时候会把反馈整合成一个结构化的文本再交给模型feedback f ## 执行输出 {stdout[:2000]} ## 错误信息 {stderr[:1000]} ## 当前目录文件 {file_list} ## 说明 如果脚本已经正确完成任务请直接总结结果如果报错请修复后重新执行。 这个做法的好处是模型能同时看到输出和报错自己判断是继续修改还是任务已经完成。输出截断也很重要不然模型生成一段死循环脚本时stdout 可能膨胀到十几 MB直接把上下文撑爆。控制上限是一个必要的保护措施。5.3 第三个问题不是所有场景都适合 Code Mode尽管 Code Mode 在灵活任务上表现神勇但它不是万能药。低延迟的轻量级交互场景比如用户问“明天的天气怎么样”你让模型现写一段代码然后跑起来那纯属脱裤子放屁。Function Calling 在这种场景下更快、更稳性能也更好。还有强约束的业务场景比如调用支付 API、发送短信这些操作涉及资金和安全不可能让模型自由发挥去写代码固定的工具调用反而更可控。我目前的经验是如果任务的逻辑可以提前穷举就用 Function Calling如果任务的逻辑是开放的、依赖数据的、需要灵活应变的就用 Code Mode。不要搞“万物皆 Code Mode”但也不要死守 Function Calling 不放。聪明的做法是两者结合用 Function Calling 做稳定可控的基础操作用 Code Mode 处理开放式分析和复杂编排。5.4 第四个问题工具注册和代码生成的协作我实际用的方案是 Hybrid。系统里同时启用 Function Calling 和 Code Mode。模型先判断任务属于哪一类如果需要调用固定 API就走 Function Calling如果用户的需求需要多步骤数据操作或复杂计算就走 Code Mode。在 Code Mode 的执行环境里模型也可以通过固定的功能函数去调用那部分已经定义好的工具。这样做的好处很明显稳定部分用 schema 约束动态部分用代码自由发挥两边都处在自己最擅长的位置。代码里甚至可以支持 JSON 格式的 Agent 工具调用因为模型在代码里写一个call_tool(search_product, {keyword: ...})的过程远比让模型试图“同时生成多个工具调用 JSON”稳定得多。6. 一些实操体验和参数建议6.1 执行循环的次数怎么控制Code Mode 天然是一个多轮迭代的过程。模型写代码、跑代码、报错、改代码可能循环很多轮。如果你不限制循环次数遇到模型陷入死循环改不好的情况资源会被白白消耗掉。我自己一般会设置最大迭代次数为 5 次超过 5 次就返回失败并附上最后一次的错误信息让上层逻辑决定怎么处理。在模型参数上temperature 建议设置低一点。写代码讲究确定性我用 0.1 到 0.2 之间。如果你用 0.8模型可能在同一问题上每次想出不同的解决思路容易出现前一轮的修复把原来的正确逻辑也改坏了的情况。低温度可以让修复过程更收敛。6.2 代码生成的上下文管理还有一个容易被忽略的细节每次把代码交给执行环境跑之前最好先把模型生成的代码展示给用户确认。不是说要用户审批每一行代码而是把代码作为一种可追溯的记录呈现出来。这样出了问题你知道模型到底做了什么而不是黑盒操作。我在实际产品里的做法是界面左侧显示模型的“思考过程”右侧显示它生成的代码和执行结果用户可以直接看到模型每一步在干什么。这种方式比一个黑盒 Agent 更让人放心也让 Code Mode 的“能力感”更强。6.3 从“工具调用”到“代码执行”的心态转型最后想说的是Function Calling 和 Code Mode 之争本质上不是哪个技术更高级而是我们如何看待模型的角色。把模型当成一个“连接器”你会很自然地用 Function Calling把模型当成一个“执行者”你会走向 Code Mode。我个人的体会是随着模型代码能力的持续增强未来会有越来越多的工作流直接交给模型以代码的形式完成工具注册和 schema 维护会逐渐变薄。我自己在切换到 Code Mode 之后最明显的变化是我不再为“每个新需求都新增一个工具”而焦虑。用户来一个从来没见过的分析需求我不需要去设计新工具只需要把环境和数据权限给它剩下的交给模型。这种从“给模型造工具”到“给模型一个环境”的转变才是 Code Mode 让我真正觉得“新神”的地方。