最近一段时间,我基本上把自己手上那个Agent项目给推倒重来了。原来整套系统基于Function Calling,也就是用户说什么、模型输出一个工具调用指令、系统去执行API;现在我把大部分业务逻辑改成了Code Mode——让Agent自己写代码、跑代码、看结果、再改代码。项目从“工具调用”转向“Code Mode”之后,应对各种奇奇怪怪输入的能力一下子强了很多。这件事让我越来越确信,AI Agent的构建方式正在经历一场真正的范式转移,而不是简单的功能升级。
如果你正在做Agent类应用、正在为“工具越来越多但效果还是不行”发愁,或者只是好奇Code Mode和传统的工具调用到底差别在哪,这篇文章应该能给你一个可参考的答案。我会先讲清楚两种范式的本质差异,再把我自己迁移过程中的架构变化、量化数据、踩坑记录完整摊开。文中所有数据来自我的模拟项目,不具有普适性,但方法论可以复用。
1. 先弄清楚“工具调用”范式到底在做什么
1.1 工具调用的本质:把模型当大脑,把API当手脚
工具调用的设计思路,简单说就是把大模型当作一个“意图识别器”,模型本身不直接执行动作,而是输出一个结构化的调用意图,由外部代码去调用真实的API或函数,再把结果拼回上下文。比如用户问“帮我查一下某张订单的物流状态”,模型不会自己对接物流接口,而是输出一个类似这样的JSON:
{ "name": "query_logistics", "arguments": { "order_id": "A123456", "carrier": "default" } }宿主程序解析这个JSON后,执行query_logistics这个函数,拿到物流信息,再交给模型组织成自然语言答案。整个过程等于把“决策”和“执行”分开了:模型负责判断该做什么,代码负责真正把事情做掉。
这种范式的核心价值是可控。每个工具都有清晰的输入输出协议,有权限边界,有日志记录,出了问题还能审计。我早期做Agent时最喜欢的也是这一点,因为可以精确控制模型能碰到哪些系统,不能碰哪些系统。给模型暴露一个查询工具,它就只能查询;暴露一个写入工具,它才可能写入。权限收敛得很干净,对生产环境非常友好。
从实现角度看,工具调用还有一个很大的好处:它不需要模型具备很强的代码生成能力,只要它能从工具清单里选出正确的那一个,并填对参数就行。所以即使是不擅长写代码的模型,也能通过工具调用完成不少实际任务。这也是过去一年多里,各种Agent框架都把“Tool Use”或者“Function Calling”当成核心卖点的原因。
1.2 工具调用的优势与天花板
工具调用的优势很明显:可解释、可控、强约束、低幻觉风险。但也正是因为这些约束,它有一个隐藏的天花板——Agent的能力边界,等于你事先定义好的工具清单边界。
我实际遇到的情况是这样的。做一个报表处理Agent时,最开始我定义了十几个工具:读CSV、读Excel、读JSON、做数据透视、画柱状图、生成PDF。用户的需求只要落在这个清单里,表现还不错。但真实任务的麻烦在于,输入格式和排列组合几乎是无限的。今天来个带合并单元格的Excel,明天来一个以制表符分隔的日志文件,后天用户说“先按城市分组,再算每个组里Top 3产品的毛利率”。这时候我就得不断给Agent写新工具、改旧工具、更新system prompt里的工具说明。
这还不是最痛苦的。最痛苦的是,很多业务需求本质上是“算法逻辑”,没办法拆成一个一个离散动作。比如“把这三列数据做归一化后和另外一张表做模糊匹配”,你当然可以写一个fuzzy_merge工具,但这个工具要暴露哪些参数?阈值怎么给?匹配键是什么?当需求的组合一变,工具的接口就僵住了。我后来统计过,工具调用阶段我花了大量时间在写“一次性工具”,用完之后再也没有第二次调用,纯粹是给每个新需求定制拐杖。
用个生活化的比喻:工具调用就像给员工发一本操作手册,手册里写了几百条常见问题的处理步骤。问题一旦超出手册范围,员工就卡住了。你只能不停往手册里补页,最后手册几百页,员工还是只会在已有步骤里做选择,永远不能自己发明新解法。这就是工具调用范式最大的结构性瓶颈——它把Agent锁定在“有限动作空间”,而真实世界的工作流是连续、开放的。
2. Code Mode 是一种怎样的新范式
2.1 从“调用API”到“编写程序”的转变
Code Mode的核心思路,是不再让模型在“现有工具”里做选择,而是让模型直接编写一段代码,在受控的执行环境里运行这段代码,然后把执行结果(标准输出、报错信息、生成的文件)反馈给模型,让它根据反馈继续修改代码。这个闭环通常长这样:
- 用户提出一个复杂任务。
- Agent生成一段Python(或JavaScript/Shell)代码。
- 代码在沙箱环境中执行,输入数据被挂载到只读目录。
- 执行完毕,捕获stdout、stderr、退出码和生成文件列表。
- 这些反馈回到模型上下文。
- 模型判断结果是否满足要求,如果不满足,修改代码再来一轮。
这个过程看起来只是“从输出JSON变成输出代码”,但实际影响是全方位的。模型不再受限于“工具A”“工具B”的清单,它可以现场组合出任意计算流程。用户说“按城市分组,再看每组Top 3产品毛利率”,模型直接写一段Pandas代码就能跑,不需要我提前预测到这种需求并定义好对应工具。
我还记得第一次跑通这个闭环的时候,一个很直观的体感是:代码模式的容错能力高很多。工具调用时如果参数传错了,系统返回一个类型错误,整条链路就断了;代码模式下,模型看到报错信息,自己会改代码,比如发现Excel某列有空格,它会把strip()加上,然后重新执行。这就像把一个只能按按钮的机器人,换成了一个能看着仪表盘自己调整操作的工人。
2.2 为什么说这是范式转移而不是一个功能
我之所以坚持用“范式转移”这个词,是因为它改变的不是单个能力点,而是整套系统的设计重心。
第一个改变是能力边界。工具调用阶段,Agent能不能完成任务,取决于“你是否预先定义了一个合适的工具”;Code Mode阶段,Agent能不能完成任务,取决于“模型能不能写出一段能执行的代码”。编程语言是图灵完备的,能表达任意可计算逻辑。所以Code Mode把Agent的能力边界从“有限工具清单”扩展到了“任意程序”。
第二个改变是开发模式。工具调用阶段的开发工作主要在接口工程:我要写函数、定义JSON Schema、写工具说明、处理工具的错误返回。Code Mode阶段的开发工作主要在执行环境与治理:我要保证沙箱安全、控制资源、设计反馈回路、管理日志。前者是“造按钮”,后者是“造车间”。
第三个改变是失败模式。工具调用失败,最常见的是参数格式不匹配、工具内部异常、返回结构不符合预期,这些错误通常是离散的,无法靠Agent自己修复。Code Mode失败则大多是代码逻辑错误、环境缺少依赖、数据处理没考虑边界情况,这些错误是可以从报错信息里定位并修复的。也就是说,Code Mode把“失败”从死路变成了反馈信号。
我用一个更直白的对比表来总结:
| 维度 | 工具调用 | Code Mode |
|---|---|---|
| 能力边界 | 等于工具清单边界 | 等于编程语言表达能力 |
| 开发重心 | API对接、参数设计 | 沙箱、安全、反馈回路 |
| 新增需求 | 写新工具/改工具说明 | 改提示词或让模型自由发挥 |
| 失败类型 | 参数错误、接口错误 | 逻辑错误、环境错误 |
| 调试方式 | 人工看日志改工具 | 模型看输出改代码 |
| 可控性 | 精细但僵化 | 更灵活但需要治理 |
2.3 一个更贴近直觉的类比
如果非要我用一句话说清楚两者区别,我会说:工具调用像是点菜,Code Mode像是给厨师一间厨房和所有食材,让他根据客人的口味现场发挥。点菜只能点菜单上有的;炒菜则能应对“微辣、少油、不要香菜、但保留一点花生碎”这种组合需求。
这也解释了为什么Code Mode更适合长尾场景。菜单永远覆盖不了所有需求,但开放厨房可以。你不用担心“用户提了一个没见过的需求”,只要这个需求能转化成代码逻辑,模型就有尝试完成的可能。当然,开放厨房也有代价:厨师可能会把厨房弄得乱七八糟,你得装好摄像头、限好火候、规定好哪些食材不能碰。这就是下个部分我要讲的实操细节。
3. 实操:我从工具调用迁移到 Code Mode 的完整过程
3.1 我之前遇到的业务场景
模拟项目X的背景是这样的:某公司每个月要处理一批来源各异的报表文件,有CSV、有Excel、有JSON,偶尔还有一些从旧系统导出的带分隔符文本。处理流程包括读取文件、清洗字段、合并数据、计算指标、画统计图,最后生成一份带排版的PDF报告发给业务方。早期我用工具调用方案,定义了读文件、写文件、绘图、渲染PDF等十几个工具,看起来还算完整。
真实跑起来之后问题接踵而至:不同文件的列名写法不统一,有的叫“销售额”,有的叫“Sales”,还有的叫“sales_amt”;日期格式有的带时分秒,有的只到日;数字列里偶尔混进空值和字符串。我不得不在工具里加各种兼容逻辑,但每加一种兼容,工具的参数就会变多,模型在选择参数时又开始犯迷糊。最典型的是工具参数非常多的时候,模型会漏填、错填,或者把A工具的参数传给B工具。这个阶段的任务成功率大概在65%左右,遇上一个新格式就要我花半天到一天去改工具。
后来我痛定思痛,决定放弃“把所有能力都做成工具”的思路,转而让Agent直接生成Pandas脚本去处理数据。
3.2 用Code Mode重写后的执行链路
改造之后的链路很简洁。用户上传文件后,系统把文件挂载到沙箱的/data/input目录,然后给模型一个初始提示词,内容包括任务说明、文件位置、可用的Python库清单、输出文件应放到/data/output目录的约束。模型生成的代码被执行器捕获,执行结果再反馈给模型。
模型通常生成的代码大概是这样的:
import pandas as pd from pathlib import Path input_path = Path("/data/input/report.csv") df = pd.read_csv(input_path) print("columns:", df.columns.tolist()) print("shape:", df.shape) print("dtypes:\n", df.dtypes) print("missing_values:\n", df.isna().sum()) # 统一列名和日期格式 df = df.rename(columns={"Sales": "sales_amount", "sales_amt": "sales_amount"}) df["date"] = pd.to_datetime(df["date"], errors="coerce") # 计算指标 df["gross_profit"] = df["sales_amount"] * (1 - df["cost_ratio"]) monthly = df.groupby(df["date"].dt.to_period("M"))["gross_profit"].sum() print("monthly:\n", monthly)关键点是模型会在代码里主动打印中间结果,比如列名、缺失率、分组后的样例。这些print输出不是给人类看的,是给它自己下一轮判断用的。因为有这些中间输出,模型不需要“盲猜”文件结构,而是先看一遍再决定下一步怎么处理。
我实现的执行器核心逻辑也很简单,是一个很薄的封装:
def run_code(code: str) -> dict: result = sandbox.execute( code, timeout=120, memory_limit="2g", input_mount="/data/input", output_mount="/data/output", ) return { "exit_code": result.exit_code, "stdout": result.stdout[-3000:], "stderr": result.stderr[-2000:], "generated_files": result.generated_files, }把stdout和stderr截断后回传给模型,避免日志太长把上下文撑爆。这一步非常关键,因为模型在迭代中看的是这个返回值,而不是直接反向控制终端。反馈信息越干净,迭代效率越高。
3.3 搭建执行沙箱要抓的四个关键点
Code Mode最大的风险是“让模型执行代码”。如果没有约束,模型可能写出删库脚本、无限循环脚本,或者读取系统敏感文件。所以我在搭建执行环境时重点抓了四件事。
第一,隔离。所有代码在一个独立容器或云函数环境中运行,与宿主机完全隔离。禁止挂载宿主机的系统目录,只挂载输入和输出两个目录。输入目录设为只读,避免模型生成的代码意外改掉源文件。输出目录单独一个,任务结束后清空。
第二,限制资源。每次执行的超时时间我设为120秒,内存上限2GB,CPU配额限制在一个核心。这看起来粗暴,但能防住大部分失控场景。模型生成死循环的概率比想象中高,尤其当它尝试写“通用解法”时,很容易写出while True。超时不能完全指望模型自觉,必须由执行器强制终结。
第三,网络策略。默认禁止所有外网访问。大部分数据处理任务不需要联网,模型只要读本地文件就够了。只有少数需要抓网页的任务,才会单独开一个白名单网络通道。联网权限一定是按任务授予,而不是环境默认开启。
第四,静态检查加人工兜底。在执行前,会对生成的代码做一次静态检查,过滤掉明显危险的操作,比如删除文件、格式化磁盘、读取环境变量等。静态检查不是万无一失,所以对于高风险操作我还保留了“人工确认”环节。比如模型要调用一个外部接口并写入生产数据库,这个动作我不会自动放行,而是会暂停下来问一句“确认执行吗”。
这些安全措施不是胆小,而是Code Mode带来的新治理课题。工具调用阶段,能力边界由代码定义,你很容易控制;Code Mode阶段,能力边界由模型生成的代码定义,你必须靠环境来兜底。
3.4 两周跑下来的量化对比
迁移之后我跑了大概两个星期,把同样一批历史需求重新跑了一遍。数据不是严谨的基准测试,但能说明趋势。
| 指标 | 工具调用方案 | Code Mode方案 |
|---|---|---|
| 任务覆盖率 | 约65% | 约90% |
| 新增格式适配时间 | 1-2天写工具 | 0.5-1小时改提示词 |
| 单任务平均耗时 | 约8秒 | 约35秒 |
| 主要失败原因 | 工具参数不匹配 | 代码逻辑错误 |
| 人工干预频率 | 每次新格式都要介入 | 仅在复杂口径时介入 |
任务覆盖率提升的直接原因,就是模型可以自己写数据处理脚本。以前遇到一个表头不规整的文件,我得专门写一个“自适应读取工具”;现在模型通过打印前几行、尝试多种编码、动态调整sep参数,就能自己把文件读进来。这些能力不是工具清单能穷举的。
代价也很明显:单任务耗时和token消耗变高。工具调用一轮就能完成的简单任务,在Code Mode下可能要三四轮,因为模型要看一遍数据结构,再写一遍处理逻辑,最后还要验证结果。这种开销换来的好处是它能处理更复杂的任务,值不值看场景。如果任务本身足够固定、高频、低变化,工具调用仍然更香。
4. 常见问题与排查技巧实录
4.1 代码跑通了,结果却是错的
我在迁移过程中遇到最多的问题不是报错,而是“代码成功执行了,但结果不对”。模型看到文件读进来了,也看到计算结果了,但它可能没有意识到某个字段里混入了错误数据,或者某个分组逻辑不符合业务口径。更麻烦的是,模型有时候会“自信地”生成一个看起来结构完整、但实际逻辑有偏的代码,你不仔细检查根本发现不了。
我的解决方案是要求模型在代码里主动打印中间验证结果。比如读取数据后,打印每列的非空值数量和数据类型;做分组后,打印每个分组的行数;算完指标后,打印几行样例输出。这些中间结果会和最终结果一起回传,模型在下一轮可以根据预期值做对比。如果发现某一列的空值比例异常,它会在说明里标注出来,甚至提示“文件里的XXX字段疑似有几行脏数据”。
除了让模型自查,我还加了一层外部校验。对于固定产出格式的任务,会在执行完后运行一段验证脚本,检查输出文件是否存在、列名是否齐全、行数是否与输入一致、关键指标是否落在合理区间。校验不通过就自动触发重跑。这层校验非常重要,它把“看着没问题”变成“可量化的没问题”。
4.2 模型陷入“修bug死循环”
Code Mode的迭代机制会带来一个副产品:模型为了修一个小问题,反复执行相同或相似的代码,就像一个人对着同一个报错信息不停地改方案,但始终没改对。我见过最夸张的一次是同一段逻辑连续执行了8轮,每一轮都在微调打印格式,真正的问题完全没有解决。
处理这个问题我用了几招。第一招是限制最大迭代轮数,一般设为5轮,超过之后强制停止并把当前状态交给上层逻辑处理。第二招是检测重复代码,如果本轮代码和上一轮代码的哈希值相同,并且执行结果还是异常,就判定为“原地打转”,直接中止。第三招是要求模型在每次修改代码前,先用一句话说明“我判断问题出在哪里,所以我这次改哪里”。这个拟议的显式推理能逼着模型做有目的修改,而不是瞎试。
另外,我还会在反馈信息里明确告诉模型:你上一次的执行报错了,错误信息是什么,你准备怎么修。如果模型已经连续两次修改同一个地方但结果不变,我会直接给它一个新指令:“不要继续改这个文件,先检查输入数据是否和预期一致。”这种“干预式提示”能有效打断死循环,节省大量token。
4.3 安全与合规:别让Agent裸奔
Code Mode最让人担心的就是“让模型写代码并执行”,所以安全治理必须前置。我总结了一条主线:默认禁止,最小授权,人机协同。
默认禁止的意思是,所有能力默认都关闭,包括网络、磁盘写权限、环境变量读取、进程管理。只有任务明确需要时才开放,而且开放范围要尽量窄。比如任务需要读取某个目录下的文件,那就只挂载那个目录,而不是把整个数据相关目录都暴露给Agent。
最小授权是说,Agent运行时的身份权限要很低。在容器里不要用root用户,用普通用户;运行目录要独立;对输出目录的写入权限也不要全局放开。还有一个容易忽略的点:不要让Agent有访问API密钥的权限。如果模型生成的代码里真的出现读取密钥的调用,静态检查至少要能拦住并报警。
人机协同则是针对高风险操作。我的原则是:数据读取、格式转换这类低风险动作可以自动执行;一旦要写数据库、发消息、删除文件、调用外部接口,必须经过人工确认。这种“人在环上”的机制虽然增加了一点延迟,但能很大程度避免不可挽回的事故。合规审计也更说得过去,所有操作都有日志,出了问题能回溯到具体是哪次执行、谁的指令、哪段代码。
4.4 Token开销为什么会暴涨
Code Mode能处理长尾任务,但token消耗比工具调用贵得多,这一点必须提前有心理准备。算一笔小账:一个中等复杂度的报表任务,工具调用可能只需要1轮对话,输入里带工具定义约1500 token,模型输出一个工具调用参数约200 token,合计不到2000 token。Code Mode下,初始提示词加文件信息约2000 token,模型第一次生成代码可能要1000 token,执行后日志反馈1500 token,模型再修改代码又要1000 token,如果迭代3轮,总消耗可能达到10000到15000 token。相当于翻了5到10倍。
这个成本不是线性增长的,因为中途生成的日志也会累计进上下文。所以我做了三个控制手段:第一,日志截断,只回传最后几行和关键信息,不完整返回stdout;第二,执行轮数限制,不无限迭代;第三,模型分级,复杂的规划和最后总结用强模型,中间代码执行、文件读取、日志解析这种相对机械的步骤可以交给更便宜的模型来做。
还有一个容易忽略的点:如果任务的输入文件很大,直接让模型“看一眼数据”是不可能的。不要让模型读取整个文件,只让它读文件的前几行、统计schema、抽样一小批数据来做决策。否则光是数据token就会爆掉整个上下文。
5. 我对 Code Mode 的判断和担忧
5.1 适合用Code Mode的场景,以及最好别碰的场景
从我的实际经验来看,有几类场景天然适合Code Mode。第一类是数据清洗和ETL,输入格式乱七八糟,处理逻辑千变万化,正好是写代码的强项。第二类是格式转换,比如PDF转Excel、JSON转CSV、Markdown转HTML,模型写脚本比维护转换工具更灵活。第三类是探索性数据分析,用户不知道文件里有什么,想让Agent“先看看再说”,这时候让模型自己写pandas代码去探索是最顺的方式。第四类是临时的网页抓取和自动化操作,只要做好网络白名单和频率限制,效果也不错。
不适合的场景也很明确。首先是低延迟交互,比如客服机器人,用户等不了十几秒钟让Agent写完代码再执行。其次是强监管、需要严格审计的场景,比如金融交易指令,这种场景要的不是灵活,而是每一步都有明确规则和留痕,工具调用反而更可控。再次是涉及物理硬件和真实世界动作的任务,比如控制机械臂、操作现场设备,一旦代码逻辑有误,后果不是重跑一遍能解决的。最后是高风险的基础设施变更,比如删除数据库表、批量修改线上配置,这类操作即使有Code Mode能力,也应该走人工审批流程。
我个人的判断是,Code Mode更适合做“分析师”,而不是做“操作员”。它最适合在一个隔离的数字环境里折腾数据、生成内容、做分析;涉及真实世界状态变更的操作,还是要收敛到预先定义好的工具或人工流程里。
5.2 我最担心的一件事:看着合理,实际错误
Code Mode一个隐蔽的风险是“输出看起来很合理,但业务含义是错的”。工具调用时代,你定义一个calculate_tax工具,里面封装了税务规则,模型只要调对工具,结果基本可信。Code Mode时代,模型可能会自己凭印象写一段“计算税费”的代码,逻辑看起来很像那么回事,但没有经过税政校验,结果偏差很大。
这个问题光靠沙箱和日志是解决不了的,因为代码执行成功不等于业务正确。我的应对方案是给任务建立“验证器”体系。对于关键业务逻辑,不要指望模型“会算”,而是准备一套独立验证脚本或者测试用例,用输入样例跑一遍结果,对比期望值。如果模型生成的代码过不了测试,就判定为失败并重新生成。这个思路和测试驱动开发很接近,本质上是把“对结果的信任”建立在可重复验证上,而不是模型的自信上。
另外,输出后的抽检也很重要。我会在生成的报告里把模型认为“关键假设”的部分标注出来,人工抽查时优先看这些地方。比如某个字段缺失率40%,模型选择用均值填充,这个假设就要显式呈现给用户确认。不要让模型默默做了决定却不告诉你。
5.3 工具调用不会消失,它会变成基础设施
虽然我花了很大篇幅说Code Mode的好处,但我并不认为工具调用会被彻底淘汰。恰恰相反,工具调用会变成Code Mode的底层基础设施。
最简单的例子是,模型写了一段代码,代码里需要读取数据库、调用某个内部API,这种时候最常见的做法是让代码去调用预定好的SDK或工具函数,而不是让模型自己从头实现一套数据库驱动。也就是说,在Code Mode内部,依然会有工具调用,只不过工具不再是Agent与外部世界之间唯一的桥梁,而是“模型生成的程序”与外部系统之间的原子操作。
未来比较合理的形态应该是两层结构:底层是各种受控工具和API,提供稳定、可审计的原子能力;上层是Code Mode,负责把用户意图翻译成程序逻辑,按需调度这些原子能力。工具调用管“能力有没有”,Code Mode管“能力怎么组合”。前者像是提供标准零件,后者是让Agent自己当装配工。
所以我现在做Agent架构设计时,不会再纠结“这个功能应该做成工具还是让模型写代码”。我的判断标准很简单:如果这个动作是高频、稳定、需要严格权限控制的,就做工具;如果是低频、长尾、需要灵活组合的,就放给Code Mode。两者不是二选一,而是分工协作。
最后再聊一句个人体会。跑了一个多月Code Mode,我最明显的感觉是,开发Agent的视角变了:以前我是在“培训一个实习生用固定流程干活”,现在更像“给一个聪明的分析师一间办公室,让他自己想办法”。工具调用时代,Agent的上限由我写的工具决定;Code Mode时代,Agent的上限由模型本身的推理能力决定。作为开发者,我要做的更多是划定边界、提供环境、建立反馈,而不是穷举所有可能性。这种切换一开始会让人不适应,因为它意味着你放弃了“每个动作都在掌控中”的心理安全感。但当你看到模型自己写出一段你没想到但完美解决问题的代码时,你会觉得这个方向确实值得继续走下去。如果你想尝试迁移,建议先挑一批低风险、长尾、格式杂的数据任务跑两周,感受一下“让Agent自己写代码”这件事带来的冲击,再决定要不要在核心业务里全面铺开。