在 Agent 系统刚开始流行的时候,我们其实默认认为:

只要 Tool 足够完善,Agent 最终就会变成一个“会调用 API 的大脑”。

后来我们发现,在我们处理过的一些高数据量场景里,这个假设并不总是成立。

当 Tool 把大量原始数据直接返回给模型时,上下文压力会明显增加。日志分析、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 注入上下文。

取而代之的是三个步骤:

  1. 中止注入
  2. 将结果写入 CSV / Parquet
  3. 返回文件路径和少量摘要信息
  4. 强制 Agent 改走 Code 轨道

这里最重要的一点是:

这不是优化。

而是一次强制执行切换。

Agent 此时已经无法继续依赖 Tool 路径处理这些数据。

它只能进入代码环境。

后来我们内部一直把这个机制叫做:

Context Off-Ramp。

因为它本质上就像高速公路匝道。

当上下文流量开始失控时,系统会强制把数据流导向另一条轨道。


4. 三种方案的一次工程对比

后来我们拿一个真实任务比较三种实现:

在 1 万实体上做全量异常筛选,并生成 PDF 报告。

以下是这次运行的记录,不是同条件、重复多次的基准测试。尤其是纯 Tool 方案截断了返回数据,而另外两条路径能处理完整数据;结果差异不能只归因于 Tool、Code 或双轨架构本身。一个能在服务端过滤、分页或聚合的 Tool 也值得单独对照。


纯 Tool(Pure MCP)

这是最接近“经典 Agent”结构的一版。

这次运行的结果比我们预期差很多。

  • 总耗时约 13 分钟
  • AI 分析对话约 11 分钟
  • Tool 调用 7 轮
  • 单次全量返回接近 51 万 token

为了避免上下文直接爆炸,我们最后不得不对 API 返回结果做截断。

问题也从这里开始。

因为一旦截断,异常筛选本身就不再完整。

这次运行能直接观察到的是:大结果给上下文带来压力,而截断让后续筛选失去完整数据。

最明显的问题是:

系统大量时间都花在“维持上下文”。

而不是分析数据。

这也印证了前面的观察。

在高数据密度场景下,Tool 会开始从“控制接口”变成“上下文压力源”。


纯 Code-as-MCP

第二版则完全反过来。

所有事情都走代码。

这条路径能处理完整数据,但总体耗时更长。

  • 总耗时约 23 分钟
  • 工具调用约 15 轮

但这里也出现了另一个问题。

很多时间其实并不是花在分析。

而是花在:

  • 修 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 的数据,最后实际上只变成:

“一个文件路径 + 少量提示信息”

完整数据仍在文件里,但上下文只需要容纳路径和少量预览。

这次运行的结果:

  • 总耗时约 4 分 30 秒
  • 没有发生上下文爆炸

做到这里时,我们其实已经很难再把它理解成“优化”。

它更像是一种执行分层。


5. 为什么这种结构会反复出现

这次经验来自数字孪生场景。其他高数据量系统可能面临类似的控制与分析分工,但不能仅凭这一次对比断定双轨结构在所有场景里都最优。

因为很多系统本来就同时存在两类任务:

  • 低数据量、高风险控制
  • 高数据量、低风险分析

例如金融系统。

下单、修改仓位、风控指令,本质上更像 Tool。

因为这些动作必须:

  • 可验证
  • 可审计
  • 可限制权限

但策略回测、组合分析、历史数据推演,又明显更适合 Code。

DevOps 其实也一样。

例如:

“在 1GB 日志里找到异常请求”

天然适合代码分析。

但:

“重启 deployment”

则必须是受约束的 Tool。

这里的 off-ramp 很像:

“日志查询超出上下文 → 写入文件 → 进入代码分析 → 再映射回少量控制命令”

Tool 和 Code 的关系,可能更像控制平面 vs 数据平面。

而不是两种互相替代的 Agent 执行方式。


6. 最后

这篇文章最后真正想表达的,并不是:

  • Tool 比 Code 好
  • 或者 Code 比 Tool 更高级

而是:

在开放、反馈驱动、数据规模跨度巨大的系统里,单一执行方式很难同时撑住控制平面和数据平面。

控制层需要确定性。

数据层需要处理复杂数据的能力。

它们本来就不是同一种问题。

真正可行的方法是承认这种执行差异。

然后把它们放到适合的位置。