在 Agent 系统刚开始流行的时候,我们其实默认认为:
只要 Tool 足够完善,Agent 最终就会变成一个“会调用 API 的大脑”。
后来我们发现,这个假设只在小规模场景里成立。
一旦数据规模继续上升,尤其是在日志分析、RAG、监控系统、数字孪生这类高数据量环境里,纯 Tool 架构会开始出现非常明显的问题。
最典型的一类任务是:
“在整个数据集中筛选异常,并生成分析结果。”
它的问题不在于推理复杂。
返回的数据量会大到让上下文系统本身开始崩溃。
我们后来在一个约 1 万实体的真实任务里,对比了三种方案:
- 纯 Tool(标准 MCP)
- 纯 Code-as-MCP
- Tool + Code 的双轨执行模型
纯 Tool 会被上下文拖死。
纯 Code 虽然准确,但整体延迟会迅速上升。
最后真正稳定下来的,是一种双轨执行模型:
- Tool 负责控制平面
- Code 负责数据平面
- 中间通过一个很小的 “Context Off-Ramp” 做切换
这篇文章主要讲这个结构为什么会出现,以及我们后来是怎么把它真正跑起来的。
1. 为什么纯 Tool 和纯 Code 都会开始失控
在上一篇里,我们已经区分过 MCP(基于 Tool)和 Code-as-MCP 的两个核心差异:
- 动作是如何表达的
- 系统在什么时候做校验
这些差异在小规模场景下其实不明显。
但系统复杂度一旦继续上升,问题会开始以一种非常不线性的方式暴露出来。
我们后来反复遇到两种典型的“成本悬崖”。
Tool 的问题:上下文开始维护自己
最开始我们其实非常偏向 Tool。
因为它天然适合:
- schema 校验
- 权限控制
- UI 操作
- 状态修改
- 可审计动作
很多事情本来就应该是 Tool:
- 删除对象
- 修改状态
- 缩容 deployment
- 执行交易
这些动作如果交给自由生成代码,其实风险会明显变高。
但问题在于,大部分 Tool 系统默认假设返回结果不会太大。
这在真实生产环境里经常不成立。
例如:
“列出当前数据集中所有异常实体。”
这个任务本身并不复杂。
真正的问题是它可能一次性返回几千、几万条记录。
如果这些结果被原样序列化并注入上下文,事情会迅速失控。
我们在实验里实际见过单次返回超过 50 万 token。
真正麻烦的不是成本。
而是整个 Agent 会开始进入一种很奇怪的状态:
- 响应时间明显变长
- tool loop 开始增加
- prompt 约束逐渐丢失
- 原始任务开始漂移
- 后部数据开始覆盖前部目标
到后面你会发现:
系统已经不是“在思考任务”。
而是在努力维持上下文本身。
这是我们后来观察到最明显的一个现象。
Tool 原本是为了“精确控制”设计的。
但在高数据密度场景下,它反而会变成上下文的主要压力源。
Code 的问题:很多时间花在“让代码跑起来”
后来我们尝试过另一个极端。
既然 Tool 会把上下文撑爆,那是不是应该让 Agent 全部走代码?
结果也并不理想。
因为很多任务本身其实是原子操作。
例如:
“选中对象 #42”
这种动作本质上只是一次确定性的状态修改。
但如果让 Agent:
- 生成脚本
- 调用 sandbox
- 执行
- 检查结果
- 修复错误
那整个系统会开始为“处理复杂数据的能力”付出额外成本。
而这些成本很多时候和业务本身没什么关系。
我们在纯 Code 架构里反复看到几类问题:
- 代码生成本身耗时很长
- dependency 问题开始增加
- sandbox 调试循环很多
- Agent 会为了小操作生成复杂逻辑
最后虽然结果准确率更高,但整体延迟明显上升。
很多时间花在让代码终于能跑起来。
这也是纯 Code Agent 很容易出现的问题。
它们非常灵活。
但很多原本只需要一个 Tool 调用的小操作,最后会被放大成一整个执行链。
2. 后来我们把执行路径拆开了
做到后面,我们其实慢慢意识到
控制类任务和分析类任务,本来就不属于同一种执行模式。
于是后来整个系统被拆成了两条轨道。
Tool 控制路径
这一条路径专门负责:
- UI 操作
- 状态变更
- 单实体查询
- 小规模返回
- 高风险动作
这一层的目标非常明确:
快、确定、可验证。
所以这里保留强 schema、严格校验,以及有限可调用操作。
本质上,它更像传统软件系统。
只是调用者从“人”变成了 Agent。
Code 分析路径
另一条轨道则专门负责:
- 聚合分析
- 批量计算
- 大规模异常筛选
- 可视化
- 多步逻辑推导
这里我们反而主动放弃一部分约束。
因为这些任务真正需要的是:
处理复杂数据的能力。
代码直接运行在沙箱环境中。
它面对的是文件、DataFrame 和真实数据。
而不是 Chat 上下文窗口。
这是一个很重要的变化。
因为数据一旦进入代码环境,Agent 就不再需要“记住全部数据”。
上下文终于重新回到了控制层。
而不是继续承担数据平面。
3. 真正关键的是 Context Off-Ramp
真正让整个系统稳定下来的,其实不是“双轨”。
而是:
什么时候强制切轨。
系统会持续监控一次 Tool 调用返回的数据规模。
当返回结果接近 token 阈值时,编排器不会再继续把完整 JSON 注入上下文。
取而代之的是三个步骤:
- 中止注入
- 将结果写入 CSV / Parquet
- 返回文件路径和少量摘要信息
- 强制 Agent 改走 Code 轨道
这里最重要的一点是:
这不是优化。
而是一次强制执行切换。
Agent 此时已经无法继续依赖 Tool 路径处理这些数据。
它只能进入代码环境。
后来我们内部一直把这个机制叫做:
Context Off-Ramp。
因为它本质上就像高速公路匝道。
当上下文流量开始失控时,系统会强制把数据流导向另一条轨道。
4. 三种方案的真实实验
后来我们拿一个真实任务做了完整实验:
在 1 万实体上做全量异常筛选,并生成 PDF 报告。
我们分别实现了三种方案。
纯 Tool(Pure MCP)
这是最接近“经典 Agent”结构的一版。
结果比我们预期差很多。
- 总耗时约 13 分钟
- AI 分析对话约 11 分钟
- Tool 调用 7 轮
- 单次全量返回接近 51 万 token
为了避免上下文直接爆炸,我们最后不得不对 API 返回结果做截断。
问题也从这里开始。
因为一旦截断,异常筛选本身就不再完整。
最终结果准确率只有约 23%。
最明显的问题是:
系统大量时间都花在“维持上下文”。
而不是分析数据。
这也印证了前面的观察。
在高数据密度场景下,Tool 会开始从“控制接口”变成“上下文压力源”。
纯 Code-as-MCP
第二版则完全反过来。
所有事情都走代码。
结果准确率明显提升。
- 总耗时约 23 分钟
- 工具调用约 15 轮
- 异常分析准确率约 85%
但这里也出现了另一个问题。
很多时间其实并不是花在分析。
而是花在:
- 修 sandbox 问题
- 调整依赖
- 修生成代码
- retry 执行
最后整个系统虽然“聪明”,但明显太慢。
尤其很多原本只需要一个 Tool 调用的小操作,也会被放大成完整代码执行链。
双轨执行模型(Tool + Code + Context Off-Ramp)
第三版则是现在的结构。
也是目前唯一真正稳定的一版。
整个任务耗时约 4 分 30 秒。
中间可以看到非常明显的:
Tool → Code
切轨过程。
其中几个关键节点非常有代表性。
R2:
entities_keyword_search 返回约 108,634 tokens。
系统判定 oversized。
数据被直接写入 CSV。
上下文里只保留前 100 行指针信息。
R3:
进一步 refine 后,返回规模达到 512,675 tokens。
再次触发 off-ramp。
完整数据被写入文件。
Agent 被强制进入 execute_data_analysis。
后面的约 3.5 分钟里:
所有数据处理都发生在 Python Sandbox。
而不是 Chat 主线程。
这是整个系统最关键的变化。
因为原本 50 万级 token 的数据,最后实际上只变成:
“一个文件路径 + 少量提示信息”
等价于把 0.5M token 压缩成几十 token。
最终结果:
- 总耗时约 4 分 30 秒
- 分析准确率约 90%
- 没有发生上下文爆炸
做到这里时,我们其实已经很难再把它理解成“优化”。
它更像是一种执行分层。
5. 为什么这种结构会反复出现
虽然这篇文章的实验来自数字孪生场景。
但这种结构几乎会自然出现在所有高数据密度 Agent 系统里。
因为很多系统本来就同时存在两类任务:
- 低数据量、高风险控制
- 高数据量、低风险分析
例如金融系统。
下单、修改仓位、风控指令,本质上更像 Tool。
因为这些动作必须:
- 可验证
- 可审计
- 可限制权限
但策略回测、组合分析、历史数据推演,又明显更适合 Code。
DevOps 其实也一样。
例如:
“在 1GB 日志里找到异常请求”
天然适合代码分析。
但:
“重启 deployment”
则必须是受约束的 Tool。
这里的 off-ramp 很像:
“日志查询超出上下文 → 写入文件 → 进入代码分析 → 再映射回少量控制命令”
Tool 和 Code 的关系,可能更像控制平面 vs 数据平面。
而不是两种互相替代的 Agent 执行方式。
6. 最后
这篇文章最后真正想表达的,并不是:
- Tool 比 Code 好
- 或者 Code 比 Tool 更高级
而是:
在开放、反馈驱动、数据规模跨度巨大的系统里,单一执行方式很难同时撑住控制平面和数据平面。
控制层需要确定性。
数据层需要处理复杂数据的能力。
它们本来就不是同一种问题。
真正可行的方法是承认这种执行差异。
然后把它们放到适合的位置。