跳到主要内容

为什么你的 AI Agent 应该编写代码而不是调用工具

· 阅读需 13 分钟
Tian Pan
Software Engineer

大多数 AI 智能体之所以昂贵,是因为一个细微的架构错误:它们将每一个中间结果都视为要反馈给模型的消息。每一次工具调用都变成了 LLM 上下文窗口的一次往返,而当一个中等复杂度的任务完成时,你已经为处理相同的数据支付了五次、十次、甚至二十次的费用。一个在三个分析工具之间传递的 2 小时销售录音,可能在路由上就花费你 50,000 个 token —— 而这还不是为了分析,仅仅是为了路由。

有一种更好的方法。当智能体编写并执行代码而不是逐个调用工具时,中间结果会保留在执行环境中,而不是上下文窗口中。模型看到的是摘要和过滤后的输出,而不是原始数据。这种差异不是渐进式的 —— 在实际工作负载中,token 消耗量减少了 98–99%。

工具调用的“税费”

当你使用标准的函数调用(function-calling)模式将智能体连接到一系列工具时,每次工具调用都遵循相同的路径:模型生成一个结构化调用,运行时执行它,完整的结果流回上下文中,模型决定下一步做什么,循环往复。当工具输出较小时,这没问题。但在大规模场景下,这种模式会彻底崩溃。

想象一个集成了 CRM、日历 API、文档库和数据仓库的智能体。每个连接的服务可能暴露数十个端点。在智能体开始工作之前,所有这些工具定义都需要加载到上下文窗口中。一个挂载了多个 MCP 服务端的生产部署,在处理第一个实际查询之前,仅工具定义就可能消耗 150,000 个 token。按照最先进模型的定价,这绝对不是一个可以忽略的零头。

然后是数据放大效应。当智能体从数据库中提取 10,000 行数据,只是为了回答其中 5 行的问题时,朴素的实现方式会将所有 10,000 行都传给模型。模型读取它们,提取相关的 5 行,为下一个工具调用格式化它们,并发送出去。你现在已经为那 9,995 行无关的数据支付了两次费用。

根本问题在于,工具调用将模型视为数据总线。所有东西都流经它,因为这就是函数调用的设计方式。而代码执行打破了这一假设。

代码是智能体的原生语言

这里有一个值得深思的观察:编程语言是专门为表达计算机应执行的操作而设计的。而 JSON 不是。当智能体需要循环处理记录、过滤数据集、处理错误或链接操作时 —— 这些在实际的智能体任务中都是家常便饭 —— 它必须将这些意图编码成一个扁平的、字面意义上的工具调用对象序列。

代码可以用一行来表达条件语句。而 JSON 工具调用则需要模型检查条件、发出工具调用、接收结果、发出依赖于第一个调用的另一个工具调用,依此类推。代码执行智能体在基准测试中显示出的智能体步骤减少 30%,正是反映了这一点:更少的往返,因为每次模型调用完成了更多的工作。

这不仅仅是一个哲学观点。三篇独立的论文都趋向于这一结论 —— 研究表明,与 JSON 工具链相比,代码动作(code actions)能诱发出更好的 LLM 行为;代码可以自然地在步骤之间管理像 dataframe 和图像这样的复杂对象,而 JSON 则不行;而且生成式模型在训练过程中接触到的高质量代码远比高质量的 JSON 动作模式多得多。模型在代码中表达智能体动作的能力,天生就比在结构化工具调用中强。

实践中的 MCP 代码执行模式

这个想法最简洁的实现方式如下。工具不再是预先将所有定义加载到上下文中,而是作为虚拟文件系统中的文件暴露出来,按服务端组织。一个处理 Google Drive、CRM 和数据库的智能体可能会看到类似 ./servers/google-drive/./servers/salesforce/./servers/postgres/ 的目录结构。为了使用某个工具,智能体从文件系统中读取其定义 —— 按需读取 —— 然后编写调用它的代码。

这就是渐进式披露。模型永远不会为它不使用的工具定义付费。你可能只需为与给定任务实际相关的五个工具定义支付 2,000 个 token,而不是预先加载 150,000 个 token。

Cloudflare 对这一模式的实现甚至更加精简:总共只有两个工具,一个用于查询 API 规范的 search() 函数,以及一个在隔离的 Worker 中运行经过身份验证调用的 execute() 函数。当你有 2,500 个 API 端点时,传统模式会将约 1,170,000 个 token 的工具定义放入上下文中,而代码执行模式将其降至约 1,000 个。OpenAPI 规范保留在服务器上;模型只查询它需要的部分。

除了 token 效率,这种架构还催生了几种能力:

数据保留在执行环境中。 智能体可以加载大型数据集,在代码中运行转换,并且只向模型的上下文返回输出。模型根本看不到原始数据。

控制流是免费的。 循环、条件分支、重试和错误处理都在沙箱中执行,不需要模型往返。原本需要 20 次工具调用迭代的复杂工作流,可以在 3-4 次模型调用中完成。

隐私变得可强制执行。 敏感字段 —— PII、凭据、专有记录 —— 可以在代码中处理,永远不会出现在模型上下文中。这对于企业合规性至关重要:你可以编写处理客户数据、提取汇总统计信息的代码,并仅返回这些统计信息。原始记录永远不会经过 LLM。

技能库随会话累积。 当智能体保存一个任务中的有用函数以便在以后的任务中重用时,它们就在构建一个不断进化的能力库,而无需任何模型微调。昨天编写了有用数据归一化函数的智能体,今天就可以直接导入它。

选择沙箱

代码执行需要一个安全运行代码的地方。目前出现了三种主流方案,各有优劣:

Firecracker microVMs (E2B 及类似产品) 是云端部署智能体(agent)的生产级选择。冷启动时间约为 150ms,在 LLM 推理时间的背景下几乎察觉不到。每个沙箱都是一个完全隔离的 VM,具有网络访问权限、真实的文件系统,并支持任意 Python 包。当智能体需要与外部服务交互或运行复杂的库代码时,这是正确的选择。

V8 isolates (Cloudflare Workers) 提供毫秒级以下的启动速度,专为智能体主要需要针对已知服务集进行身份验证 API 调用而设计。默认情况下禁用对任意外部主机的网络访问——这是一个有用的安全特性——但也限制了智能体在结构化 API 调用之外的能力。

WebAssembly / Pyodide 可以在浏览器或受限环境中执行 Python,无需任何服务器基础设施。WASM 的线性内存模型在设计上进行了边界检查,使得沙箱逃逸在架构上变得更加困难。权衡点在于速度和库支持:并非所有内容都能编译为 WASM,且执行速度比原生慢。但对于希望零服务器基础设施和零跨用户污染风险的客户端智能体部署来说,它是最佳选择。

两年前让沙箱显得昂贵的冷启动延迟问题现在基本上已经过时了。智能体执行的瓶颈在于模型推理,沙箱开销只是微不足道的噪音。

安全性现状核查

以下是让经验丰富的工程师对代码执行感到不安的原因:威胁模型确实很难处理。AI 生成的代码必须被视为不受信任的输入,其严谨程度应与你在公共执行环境中处理用户提交的代码相同。

静态清洗——过滤危险模式的输出、禁用特定函数——并不能可靠地工作。蓄意的攻击者(或嵌入在智能体检索文档中的巧妙提示词注入攻击)可以通过编码、库链和命名空间操作绕过过滤器。一个公开披露的漏洞展示了如何通过多阶段 numpy 属性操作,在看似安全的科学计算代码中实现系统命令执行。

2025 年的 SaaStr 事件值得了解:一个 AI 编码智能体为 1,200 多名高管删除了整个生产环境的 PostgreSQL 数据库。这并非源于复杂的攻击——而是因为该智能体拥有广泛的数据库权限,且在执行破坏性操作前缺乏足够的确认逻辑。模型准确地执行了指令,但在一个它拥有过高权限的环境中。

这些失败都有一个共同的根源:智能体的能力范围是按照其最坏可能的用途划定的,而不是按照其预期的用途。真正有效的技术对策:

  • 沙箱化是不可逾越的底线。 当清洗失效时,容器化可以限制爆炸半径,而清洗迟早会失效。沙箱不能阻止生成坏代码,但可以防止坏代码影响宿主系统。
  • 用户级隔离。 用户之间的交叉污染——一个智能体的执行影响另一个智能体的环境——是共享沙箱部署中的真实风险。
  • 最小特权工具范围。 被授权从数据库读取数据的智能体默认不应拥有写入权限,即使系统支持写入。
  • 选择远程执行而非本地 Docker。 本地容器共享宿主内核。基于 Hypervisor 的隔离 (Firecracker, WASM) 提供了更强的保证。

提示词注入问题——检索数据中的恶意内容劫持代码生成——目前仍未真正解决。深度防御(以沙箱作为最后一道防线)是目前最好的方法。

可靠性:无人讨论的问题

在大型评估集中,代码执行智能体的解析失败率约为 2.4%。这听起来很小,直到你运行 100 个智能体任务,其中 6 个生成了运行时无法执行的格式错误代码。当解析失败时,任务成功率会比解析成功的情况下降 20 个百分点以上——因为失败会迫使系统进入昂贵的恢复路径或产生错误的结果。

目前的最佳实践是将智能体输出封装在一个薄薄的结构化外壳中:一个包含 thoughtscode 字段的 JSON 对象。这结合了结构化生成(可靠的解析)和代码执行(表达能力),在标准基准测试中将端到端任务成功率提高了 2–7 个百分点。

问题在于:这仅在大型模型上可靠。在大约 320 亿参数以下,生成有效的 JSON 包装 Python 的认知开销会严重降低指令遵循能力,以至于你最好选择纯代码生成或标准函数调用。对于小型模型来说,“代码智能体绝对更好”的说法并不成立。

这对系统设计具有实际意义。代码执行智能体并非函数调用的通用替代品。对于流程定义明确、变化较少,且你预先知道工具调用模式且任务复杂度适中的场景,标准函数调用仍然是正确的默认选择——更简单、更可预测、更容易评估。随着任务复杂度的增加、连接工具数量的增多以及中间数据量的上升,代码执行的优势才会显现。请根据实际任务需求匹配架构。

随时间产生的复利效应

代码执行代理(code-executing agents)最常被忽视的优势在于时间维度。代理编写并保存的每一个可重用函数,都是它带入未来会话的一项能力。一个学会了如何高效地将三种不同 CRM 格式的客户地址数据归一化的代理,可以保存该函数并重复使用。下次遇到类似任务时,它会直接导入该函数,而不是从零开始解决问题。

这是一种不需要模型微调、不需要整理新训练数据、也不需要对底层模型进行任何改动的学习形式。这种能力在为代理提供持久化代码存储空间时自然产生。技能库会随着时间的推移产生复利,这是静态工具注册表(static tool registries)无法比拟的——因为这些技能是由代理自身生成的,并针对其运行环境中的特定数据形态和特性进行了优化。

对于那些构建旨在同一环境中重复运行的代理(如内部工具代理、数据处理流水线、工作流自动化)的团队来说,从一开始就针对这一点进行设计是非常值得的。构建一个技能持久化层。追踪哪些函数被重复使用,哪些没有。那些在数周运行中积累了可重用技能的代理,其表现将优于那些每次会话都从零开始的代理。

总结与展望

从函数调用(function-calling)到代码执行代理的架构转变并非推倒重来,而是计算发生位置的改变。将处理过程移至执行环境。保持上下文整洁。像对待任何不受信任的输入一样,对生成的代码应用同样的安全性规范。

Token 经济学带来的效益如此显著,以至于任何在每个任务中进行多次工具调用的代理,都应该评估代码执行是否是更合适的架构。Cloudflare 的数据——在复杂的 API 工作流中减少了 6 个数量级的 Token——虽然不能代表所有场景,但即使是中等规模的工作负载,也一致显示出 50–80% 的 Token 减幅。在大规模运行时,这会带来显著的成本和延迟差异。

安全性和可靠性方面的担忧是现实存在的,值得在工程上引起高度重视。沙箱化、最小权限访问以及代码生成的结构化输出,对于生产环境部署来说并非可选功能,而是必须。但这些问题已有成熟的工具可以解决,不应成为规避这种架构的理由。

References:Let's stay in touch and Follow me for more thoughts and updates