1. 从工具调用到 Code Mode:你看到的不是升级,是重写
最近圈子里聊 AI Agent,话题已经从“怎么接工具”变成了“要不要让模型直接写代码”。我自己的感受特别明显:半年前做 Agent 第一反应是去列一堆 function schema,把每个操作包装成工具,再祈祷模型能按格式调对。这半年我越来越倾向另一条路——干脆让 Agent 进入 Code Mode,让它自己写 Python、执行、看输出、改代码、再执行。这一跳不是工程上的小迭代,背后是整个 Agent 的执行哲学被换掉了。
所以这篇东西不是给你讲某个新框架的 API 用法,而是想聊清楚:工具调用那套玩法为什么会被 Code Mode 逼到墙角,Code Mode 的核心优势到底在哪,以及你从“工具思维”切到“代码思维”时最容易踩的坑。适合已经在做 Agent 开发、或者正准备从 Function Calling 迁移到代码执行模式的工程师。还没入门的读者也不用慌,我会尽量把背后的逻辑也讲明白——你知道了“为什么”,迁移的时候才不会跟风瞎改。
什么是我说的 Code Mode?我用一句话概括:Agent 不再通过预设的函数签名跟外部世界打交道,而是把推理结果直接变成一段可执行代码,在受控的运行环境里跑起来,拿到 stdout、文件系统状态、错误堆栈之后,再决定下一步做什么。
这不是某一家公司发明的私有模式,而是在 Agent 工程里逐渐沉淀出来的一套通用策略。它跟 Tool Calling 的区别,比“方式不同”要深得多:你把代码当成 Agent 的“语言”,而不是把工具当成 Agent 的“拐杖”。本质上这是 Agent 对外部世界的表示能力在升级。下面我会从几个维度详细拆。
2. 工具调用为什么不够用了:一切问题都出在“离散”这两个字上
2.1 Tool Calling 的本质是人工预编译
先回顾一下工具调用是什么。你定义一组函数,每个函数有名字、描述、参数结构。模型在推理时输出一个结构化 JSON,框架负责把 JSON 翻译成真正的函数调用。整个过程像什么?像你在让一个实习生按照一份写好的手册去按按钮:手册已经把“什么情况按哪个按钮、参数怎么填”全部定义好了,模型只负责在手册里做选择题。
这个模式的优点很直接:可控、稳定、每一步都在预期内。但问题也藏在预设里。
一是表达能力被“枚举”锁死了。你能给 Agent 多少工具,它就只能在多少个工具之间做路由。今天模型能干的事已经远远超出任何一份 API 清单能覆盖的范围。比如让模型分析一份 Excel,工具调用的做法是你得先准备“读取Excel”“做透视表”“画图”“算均值”这一堆函数,等于你把数据分析的所有常见操作都枚举一遍。现实里的分析需求千奇百怪,枚举永远追不上需求。
二是工具之间的“数据流”很别扭。函数返回的结果得塞回上下文,模型读完再决定下一个调哪个。多步任务里,中间结果的格式、精度、结构,每一步都在“序列化→塞上下文→再序列化”里磨损。你要是让模型调三次工具,第二次调用的参数可能已经忘掉第一次返回里那个关键的字段。为什么?因为上下文是一大坨文本,模型本质上是在文本里做模式匹配,没有真正的“变量”概念。
2.2 工具的“语义缝隙”让模型背锅
我做过一个对比测试,同一个任务:让 Agent 把一批图片批量压缩并重命名。Tool Calling 版我准备了四五个工具,结果模型偶尔会把参数顺序搞反,或者把 JPEG 质量参数填成 120——工具 schema 里明明写了范围,但只要 prompt 和 schema 稍微长一点,模型的“选择疲劳”就会出现。不是模型能力不行,是这个交互范式本身在给模型挖坑:它要把“意图”翻译成“JSON”,再让运行时的校验器去查错,错误信息又得通过 prompt 重新喂给模型,来回折腾三五轮才能跑通。
而 Code Mode 那个版本,Agent 自己就写出了完整的 Python 脚本,用 PIL 开图、压缩、加时间戳重命名,一次跑完。我复盘的时候意识到:工具调用的每一步都是“离散动作”,离散动作之间的语义联系靠上下文里那几行文本来维持,这个缝隙永远堵不上。模型明明能理解“压缩图片”这个连续语义,你非把它掰成“打开文件→设置质量→保存文件”几个槽位去填空,这不是让它做有意义的推理,这是让它做低效的翻译。
2.3 长链路任务里,组合爆炸是绕不开的坎
工具调用的组合复杂度跟工具数量呈指数关系。你暴露 10 个工具,模型要考虑的路径就有 10 的 N 次方种。虽然实际推理不会真的全枚举,但模型在每一步做选择时,注意力会被工具描述、历史输出、系统提示里的噪音干扰。尤其是我们把这些工具描述写得很详细(为了帮助模型),结果描述本身都成了占用上下文的负担。
有人说“那我把工具拆细一点,让每个工具原子化不就好了”。我试过,拆太细会导致模型需要多步调用才能完成任务,每一步都有失败概率,链路一长错误率直线上升。拆太粗又导致工具复用率低,写一堆几乎只用一次的函数。这个平衡在工具范式下很难拿捏。Code Mode 不一样,它面对的问题不是“选哪个工具”,而是“写什么步骤”,代码里的函数和变量天然支持组合,一步脚本能抵得上五次工具调用。
2.4 工具调用的幻觉问题其实比你想的更严重
模型“幻觉”在工具调用场景里还多一层表现:它不知道该不该调用工具,也不知道该不该相信工具返回的结果。如果某个工具返回了空结果,模型往往会顺着空结果继续编,而不是停下来质疑。在 Code Mode 里,代码跑出来的结果就是结果,文件有没有生成、数据有没有变化一目了然。空列表就是空列表,不会引发模型“脑补”。这个差异在做数据处理的 Agent 时尤其致命。你要是用工具调用去做报表生成的 Agent,一个字段没取到,模型大概率会硬编一个数进去,然后继续写下面的步骤。换成 Code Mode,字段取不到代码直接报错,Agent 就必须正面处理这个错误。哪种更可靠,一目了然。
3. Code Mode 的核心逻辑:让 Agent 进入“思考→执行→观察”的闭环
3.1 用代码代替“函数路由”,Agent 第一次真正拥有了计算力
在工具调用的范式下,模型本身没有“计算能力”可言,它只会“请求计算”。计算发生在工具里,工具是黑盒,模型看到的只是返回结果。而 Code Mode 把“计算”本身变成了模型可以直接操作的对象。模型写出来的循环、条件、异常处理、数据结构,每一步都是它自己思考的外化。你不再是给它一堆按钮,而是给了它一支笔和一张草稿纸。
这就带来一个质变:Agent 可以在“运行时”修正自己。它发现自己 parse JSON 失败,可以当场补一段容错代码;它发现数据量太大,可以自己决定分块处理;它发现依赖缺失,可以自己 pip install(前提是容器允许)。工具调用做不到这种灵活的运行时反应,因为工具是静态的,模型只能等在错误返回码上去找下一个工具碰运气。
3.2 代码本身就是最好的“中间表示”
为什么模型写代码这么快就成了趋势?因为代码是这个世界上表达“过程逻辑”最高效、最严谨的形式。自然语言有歧义,JSON 不够表达分支和循环,而代码同时具备“可读”和“可执行”两个特性。模型训练数据里包含海量代码,它对“写代码”这件事的掌握程度,比对“写 JSON 调用特定 API”高得多。你让一个编程能力很强的模型去写一段 Python 完成数据分析,它输出的代码几乎可以零修改运行;但你给它一份陌生的工具 schema,哪怕是强模型也要试错几次才能调对。
所以 Code Mode 的本质是:把 Agent 的推理过程,映射到一个模型最擅长的输出空间里(代码),然后把执行过程交给一个绝对可靠的解释器去完成(Python/Shell)。模型负责策略,解释器负责执行,各司其职。你可以把代码当成人脑思考时用的“内部语言”,只不过这个内部语言能被计算机直接吃进去。
3.3 open-loop 变成 closed-loop,错误处理从“猜”变成“看”
工具调用模式下,Agent 的运行循环其实是开环的:它发起一个工具请求,得到返回值,然后手动决定下一步。如果返回值跟预期不符,它只能“猜”原因。Code Mode 的闭环体现在哪里?代码执行后的 stdout、stderr、exit code、生成文件、目录结构,这些是最直接的“观察”。模型读到报错信息,等于亲眼看程序在哪个字符上失败,然后它可以针对这个报错定向修改。每一次观察都会压缩下一次行动的不确定性。
这很像人类程序员调试代码:写一段,跑一下,看结果,改。Agent 终于不是“把一切寄托在 prompt 魔法上”,而是真正开始“干活”了。
3.4 从“API 思维”到“环境思维”的转变
工具调用只能做“已经定义好的事”。Code Mode 能做“环境允许的事”。你给 Agent 一个 Python 环境,它理论上能做任何 Python 能做的事;再给它 Shell 权限,它能做任何命令行能做的事。这就像你雇了一个人——你不告诉他具体怎么干,而是把工具间、原料仓库和使用手册都给他,让他自己琢磨怎么完成任务。对人来说这是常识,但对 Agent 来说这是指数级的能力扩展。
代价是安全与可控性需要用沙箱和技术手段在底层兜住,不能依赖模型自觉。这个我后面专门说。
4. 实操:我如何把一个“工具型 Agent”重构成“Code Mode Agent”
下面这部分是实际操作。我们以一个典型的 Agent 任务为例:让 Agent 下载一份 CSV 数据集,做清洗和统计分析,最后生成一份 Markdown 报告。先用工具调用的思路搭一遍,再改成 Code Mode,你就能直观感受到差在哪。
4.1 工具调用的原始设计(为什么别扭)
传统方案需要定义以下工具:
{ "fetch_csv": {"url": "string"}, "inspect_columns": {"dataframe_id": "string"}, "clean_missing": {"dataframe_id": "string", "strategy": "string"}, "compute_stats": {"dataframe_id": "string", "columns": ["string"]}, "write_report": {"content": "string", "path": "string"} }看着还行是吧?但真正跑起来问题一箩筐。首先是状态管理:dataframe_id 是什么?你需要在工具层维护一个全局的 DataFrame 注册表,每次工具调用把返回值存进去,给模型一个句柄。模型在第二步“inspect_columns”时要求输入第一步返回的 dataframe_id,一旦模型把这个 ID 记错,整个链路就断了。更麻烦的是,清洗策略你只能在 schema 里给个枚举(“drop”或“fill_zero”),模型要是想“按列分别处理:数值列填充中位数、分类列填充众数”,你的工具根本表达不了,只能加新参数、新枚举、新分支。你越想让模型灵活,功能越复杂,schema 越臃肿,然后模型越容易调用错。死循环。
4.2 Code Mode 的 prompt 设计(原来可以这么简单)
同样的任务,Code Mode 的 Agent 核心指令我只写了这么一小段:
你是一个数据分析 Agent,运行在 Python 3.11 的沙箱环境里。 你可以执行任意 Python 代码来读取、清洗、分析、可视化数据。 执行代码后你会收到 stdout、stderr、exit code 以及当前目录文件列表。 如果任务没有完成,根据这些反馈修改代码,继续执行。 最终你需要输出一份 Markdown 报告,写入 report.md。 当前任务:分析数据集 data.csv,完成清洗和统计,生成报告。没有工具 schema,没有函数注册表,没有参数校验。模型自己决定用 pandas 还是 polars,自己决定清洗逻辑,自己决定输出哪些统计量——然后生成报告。看明白了吗?我把“分析数据”这个抽象目标直接交给模型,它用代码去具现化每一个决策步骤。我再也不用提前猜测模型会要什么中间结果,因为它自己会创建中间变量。数据的临时状态存在于代码执行的内存里,而不是存在于我维护的全局表里。这是一个从“人给 Agent 搭脚手架”到“Agent 自己搭脚手架”的根本变化。
4.3 一次真实的执行 trace(观察它如何自我修正)
我截取一段之前在测试环境里的真实 trace(脱敏简化),帮你看清 Code Mode 的闭环长什么样:
user> 分析 data.csv,重点关注缺失值分布和销售额的月度趋势。 assistant> 先读取数据,看一下结构和缺失情况。 tool (code)> import pandas as pd df = pd.read_csv("data.csv") print(df.info()) print(df.isnull().sum()) print(df.head()) ------ stdout> RangeIndex: 4820 entries, 0 to 4819 Data columns: 12 columns order_date 有 120 个缺失,sales_amount 有 30 个缺失 assistant> 注意到 order_date 缺失较多,先检查日期缺失的行是否有规律;sales_amount 看能否根据其他字段推断。 tool (code)> ...它不是“调用一个处理缺失的工具”,而是“先查缺失的分布规律,再决定策略”。你能看到它的每一步都基于上一步命令的 stdout 做决定,跟人坐在 Jupyter Notebook 里分析数据的状态几乎一样。这种“观察→假设→验证→修正”的循环是工具调用实现不了的——因为工具的粒度把人家的思考过程切碎了,模型只能在一堆具名操作里挑拣,而代码模式给了它完整的表达空间。
4.4 关键配置:沙箱参数到底怎么调才不踩雷
要让 Code Mode 真正稳定落地,光把 prompt 改了还不够。我在生产环境里总结出几个非常关键的配置项:
运行环境:一定要用容器(Docker 或类似隔离技术),CPU 限核,内存限 1~2GB,磁盘限 500MB 以内。别给 Agent 无限资源——模型生成的代码偶尔会有死循环,或者一次性读进超大文件,资源隔离是底线。设置执行超时,我一般设 30-60 秒,超过直接终止当前代码,把 Timeout 错误返回给模型重新写。
依赖管理:预装常用库(pandas、numpy、requests、matplotlib、openpyxl 等),作为 baseline image。同时允许模型在代码里自行 pip install(限定索引源),但每次执行前检查一下有没有装可疑包。你自己的镜像要定期更新,避免模型要用的新库装不上。
文件读写控制:给 Agent 一个工作目录(例如 /workspace),所有读写限制在里面。不允许它访问系统目录。这比让它自由发挥安全得多。模型要是试图读取 /etc/passwd,你直接在挂载层就拦住,不用等它犯错。
上下文管理:Code Mode 有一个隐患——执行输出的 token 会累积。模型连续跑十次代码,每次 stdout 几百行,上下文很快就爆了。我常用的方案是给 stdout 做摘要:只保留最近 1000 个字符给模型,前面的全部截断,或者让它把中间结果写入文件,只告诉它“数据处理到第几阶段,文件已生成”。我曾经见过一个 Agent 因为 stdout 太长导致上下文溢出,结果它开始“凭空想象”后续结果,非常危险。
代码 review 机制:生产环境不建议让 Agent 生成的代码直接执行。可以加一个“前置拦截器”:代码是否包含危险调用(eval、exec、subprocess 中的敏感命令),是则拒绝执行;文件操作是否越界;网络请求是否指向非白名单地址。拦截器只加一道轻量检查,开销不大,但能挡住 90% 的低级安全风险。
提示:别把安全寄托在 prompt 上。prompt 里写“不要访问系统文件”几乎没用。真正的安全靠沙箱、权限、资源限制,而不是靠模型自觉。
4.5 从工具调用到 Code Mode 的迁移清单
如果你已经在维护一个工具型 Agent,想切换到 Code Mode,我建议按下面的顺序走。
第一步:盘一下现有 Agent 的核心任务类型。凡是“数据处理类”“计算类”“批量操作类”的任务,优先迁移到 Code Mode;凡是“调用外部私有 API”“需要鉴权写数据库”之类的任务,继续保留工具调用,不要一刀切。混合架构完全可行,甚至应该混着用。
第二步:把工具背后能复用的逻辑改造成 Python 函数库。工具调用时期你写了“fetch_csv”“clean_missing”这些逻辑,别丢,把它们整理成项目内可 import 的工具模块。Code Mode 的模型可以直接 import 你的模块调用,这比它把逻辑重写一遍稳定得多。有人担心模型不会用你的内部库,实际上只要你在 prompt 里给它一个“可用模块的文档”或直接用代码暴露几个 import 示例,它的遵循能力远比调用陌生工具强。
第三步:在沙箱环境里做一次影子测试。把你的 Agent 接到沙箱后的头几天设为“影子模式”:所有代码都执行,但不影响生产数据,只生成日志和中间结果。观察它写的代码风格、出错率、耗时时长,再决定是否全量切换。
第四步:设计好“人工介入”流程。无论 Code Mode 多强,总有它搞不定的场景。你要在 Agent 连续失败 N 次(比如 3 次)后中止循环,进入人工队列。同时保留“回滚”能力:每次执行前后备份工作目录,避免它把中间文件搞烂了没法恢复。
5. Code Mode 落地遇到的五个高频坑(这些我都踩过)
5.1 坑一:模型执着于“一次写对”,而不是“迭代试错”
很多模型(尤其是经过 RLHF 对齐的)在 Code Mode 里有一个坏倾向:它希望第一次生成的代码就是完美的,写完就认为完事了,不再去执行验证。结果是它“以为”自己搞定了,实际上代码根本没跑过。这个问题在高强度对齐的模型上尤其明显。
我的处理方式是在 prompt 里明确写:**“每次生成代码后,必须执行验证;如果还没执行,就不知道结果,需要继续。”**同时让框架侧检测“模型生成了代码但没有触发执行工具”的情况,一旦发现就自动补一次执行。这个机制加上去之后,Agent 的整体成功率明显提升。说白了,你不能只靠模型自觉去闭环,框架要做闭环的最后守门员。
5.2 坑二:stdout 太长,模型被垃圾信息淹没
刚开始跑 Code Mode 时,我让 Agent 跑了一个数据分析任务,它 print 了完整的数据框,几千行。模型后续的注意力基本全费在看这些输出上,甚至还把某几行数据当成“结论”写进了最终报告。这是 Code Mode 特有的一类幻觉:模型分不清“数据打印样本”和“分析结论”的区别。
解决办法是上面提过的 stdout 截断和摘要。另外,可以在 prompt 里给模型一个“内部约定”:不要在代码里打印无关内容,用 print 只输出关键信息和结论;中间过程不要 print 全部数据,只 print shape、head 等概要。用代码习惯去约束,比事后清理更高效。
5.3 坑三:环境一复杂,Agent 就迷路
如果你的 Agent 要操作多个服务(比如连数据库、调内部 API、再跑模型推理),Pure Code Mode 会乱套。代码里硬编码连接参数、密钥、环境地址,既不安全又难维护。我在实践中的做法是:把基础设施操作继续封装成工具暴露给模型,把业务逻辑和数据分析交给 Code Mode。这样是“工具调用负责管道,Code Mode 负责思考”。
说实话,把这两者对立起来是伪命题。现在好一点的 Agent 架构都是混用的:模型可以主动选择“调用工具”或“写代码执行”,两者同时存在于它的行为空间里。你只需要做好权限边界和行为约束,让该用工具的地方用工具,该写代码的地方写代码。
5.4 坑四:代码执行时间不可控
不是所有任务都能在 30 秒内跑完。你要是让 Agent 跑一个数据量很大的机器学习训练,一跑就是几分钟,你不可能让它一直阻塞在那里。我建议把执行机制分成“同步执行”和“异步任务”两种:短任务(轻量操作、小数据)同步执行,立刻回传;长任务(训练模型、爬大量页面)丢到后台队列,Agent 先做别的,完成后再回来收集结果。异步机制会让架构复杂度上升,但是对于真实复杂任务,几乎绕不开。
5.5 坑五:模型写的代码有“脏副作用”
我遇到过模型在代码里创建了一个叫 /tmp/xxx 的文件,忘了删,结果污染了后续任务。还遇到过模型用相对路径写文件,结果每次执行的工作目录不一致,文件散落得到处都是。对这种问题,我总结出的经验是:每次 Agent 任务开始前,重建工作目录并清空。任务结束后,把整个容器销毁重建,不留任何残留状态。如果你做的是高并发多租户场景,这点尤为重要——状态残留是最隐蔽的 bug 来源。
6. 从工具到代码的迁移,本质上是 Agent 从“填鸭式执行”走向“自主式创造”
回顾整个范式转移,我的核心判断是:工具调用把 Agent 框在“人类预设维度”里,Agent 的每一步都依赖于你对问题空间的预判;Code Mode 则把 Agent 释放到“计算空间”里,它的边界不再是某个函数的参数个数,而是环境权限和算力开销。这个变化对 Agent 能力的影响,跟“脚本语言取代手工命令”一样深。
当然,Code Mode 不是银弹。对简单操作来说,工具调用更轻、更快、更省 token;对外部系统交互来说,工具调用仍然是必要的“管道”。但对“需要推理、需要探索、需要创造性解决方案”的任务,Code Mode 的优势是碾压性的。我从半年实践中得到的最大体感是:以后 Agent 开发者的核心能力不再是“设计完美工具”,而是“搭好沙箱、建好护栏、定义好目标,然后放手让模型写代码”。这是一种更接近管理者的工作方式,也是一个真正有意思的方向。
最后再补一句我在实际项目中反复体会到的原则:测试 Code Mode Agent,千万不要只给“标准答案型”任务。多给它一些开放式任务,比如“这份数据有什么异常”“这个项目有哪些潜在风险”“这个代码为什么慢”。开放式任务才能逼出 Code Mode 的真正优势——它不只是执行者,它是解决问题的“对话者”。你把它当实习生带,它给你的回报会远超预期。