上下文即代码:150 行的 IPython agent,上下文成本降到 30%,正确率不变
这个 harness 只有 150 行,没有工具调用协议,执行后端是 Docker 里的真 IPython。同任务、同模型下,它平均每个任务使用 2062 tokens 上下文,传统 CLI agent 要 6855,答案质量不变。为什么相差三倍多?我们用 336 个受控 trial(API 总成本约 $0.2)拆开成本。最反直觉的结果是,把数据表示成代码并不会让模型更懂。在受控对照下,这一层的收益恰好为零。实验代码与全部轨迹在 agentic-playground。
什么是"上下文即代码"
先看实物。下面是实验里 agent 开始工作之前就"已经存在"的上下文(截取):
In [1]: import json, os
...: sorted(os.listdir('data'))
Out[1]: ['events.jsonl', 'projects.jsonl', 'tasks.jsonl', 'teams.jsonl', 'users.jsonl']
In [2]: def load(name):
...: return [json.loads(l) for l in open(f'data/{name}.jsonl')]
...: teams, users, projects, tasks, events = (load(n) for n in
...: ['teams', 'users', 'projects', 'tasks', 'events'])
...: len(teams), len(users), len(projects), len(tasks), len(events)
Out[2]: (30, 300, 80, 5000, 20000)
In [3]: sorted({t['status'] for t in tasks}), sorted({e['action'] for e in events})
Out[3]: (['blocked', 'done', 'in_progress', 'review', 'todo'],
['commented', 'created', 'reassigned', 'status_changed'])
关键在于,这段会话历史是伪造的。模型此刻还没说过一个字,但每个 Out 都是真的。harness 把这些 cell 交给真实 kernel 执行,再把输出拼进历史。变量 tasks、events 从此留在运行时命名空间里。agent 的第一条消息就可以直接写 [t for t in tasks if ...]。对模型而言,这不是一份描述数据的文档,而是一个可以直接续写的行为前缀。
如果你做 agent 开发,大概已经知道 CodeAct、smolagents、Cloudflare Code Mode 这类"代码作为行动空间"的工作。本文考察的是上下文。行动可以写成代码,上下文也可以是一段可续写、可执行的会话。我们把它的成本拆开来算。
实验设计:一把自由度阶梯
所有组使用同一套程序生成的项目管理数据集:xl 档有 300 个 users、5000 个 tasks、20000 个 events,约 3.5MB JSONL 全部落盘。上下文只给文件行数和每个文件末尾一两行,所有任务都需要多步交互。我们也固定了 8 个查询任务模板(过滤、聚合、多跳,以 gold 程序判分)和模型(通过 OpenRouter 调用 deepseek-v4-flash)。唯一改变的是 agent 与执行环境之间隔着多少层协议。
按自由度递减排:
- 传统 CLI agent(A):数据是文件,skill 文档描述布局,agent 有 read 和 bash 工具。
- 伪造会话 + eval 工具(B):上一节展示的形态,使用单一 eval 工具和持久 Python 命名空间。关键对照组 B0 不绑定伪造示范中的变量,每次重复完整表达式。它把"数据写成代码的样子"和"会话带来的工作习惯"分开。
- 裸 IPython,无协议(D):去掉工具调用协议,使用纯 completion 循环。assistant 消息就是下一个输入 cell(裸代码),回复就是 cell 输出。标题中的 150 行 harness 属于这一组,在 Docker
--network none中运行真 IPython。 - 无循环单发(C):B 的会话渲染成纯文本 transcript,模型输出一段代码,harness 代为执行,不重试。
Round 1 在两个更小的数据档先跑过一轮,结论方向相同,本文只列 Round 2 的数。
主结果:成本阶梯
| 组 | ctx tokens | ctx 中位数 | out tokens | 轮数 | 墙钟 | 成本/trial |
|---|---|---|---|---|---|---|
| A 传统 CLI | 6855 | 7032 | 506 | 3.62 | 16.2s | $0.000484 |
| B0 会话不绑变量 | 7263 | 6926 | 438 | 3.62 | 15.8s | $0.000470 |
| B 伪造会话 | 4803 | 3540 | 298 | 2.62 | 9.9s | $0.000358 |
| D 裸 IPython | 2062 | 2012 | 203 | 2.04 | 8.1s | $0.000184 |
| C 单发 | 854 | 850 | 363 | 1 | 8.5s | $0.000113 |
五组正确率都是 100%,已贴近天花板。以下结论只讨论 token 成本,不讨论能力上限。
阶梯本身不意外,意外在每一级的成因。
逐层归因
表示形式:值 0%
B0 与 A 打平:轮数完全相同(3.62),ctx/out 差异在噪声内,两轮实验都复现了这个零结果。同样的数据、同样的 IPython 会话外观,只要示范不绑定变量,成本就与传统 CLI agent 没有区别。"表示成代码模型更懂"这一层,受控对照下收益为零。
差距来自行为。同一个任务(t6)中,B 的第一条 eval 直接写 [p for p in projects if ...],引用伪造历史里已 load 的变量,3 次调用完成。B0 的每条调用都重新用 [json.loads(l) for l in open(...)] 全量重载,共用了 4 次。
另一个发现解释了为什么 B0≈A。A 条件的 24 个 trial 共 63 次工具调用,全部是 bash 中的 python3 -c 多行脚本,内置 read 工具一次也没用。模型拿到原语工具后仍选择编程语言。A 实际上成了一个没有持久状态的 Python 条件。
工作习惯:ctx −30%,out −41%
相对 A,B 的 ctx 降 30%(中位数降 50%),output 降 41%,少一轮,墙钟时间降 39%。伪造历史让模型复用变量,并在一个 cell 中完成更多步骤。A 和 B0 没有持久状态,agent 会把上一步算出的任务 ID 集合整段作为字面量粘进下一条命令。这直接造成 B 的 output 比它们低四成。文档可以说明这些习惯,历史示范会让模型直接照做。
协议层:−57%
D 去掉了工具调用协议、tool schema、JSON 参数包装和 harness 封装,ctx 从 4803 降到 2062。B 要靠伪造维持"每条 assistant 消息都是 eval 调用"的形式,D 的消息格式本来就是这样。示范前缀因此可以直接作为合法对话前缀。
循环:−59%
C 连 agent 循环也去掉,只生成一次代码,ctx 降到 854。D 的 ctx 下界约等于轮数乘以前缀长度。这是保留多轮纠错能力的成本。任务可以一次命中时,循环就是额外开销。我们仍坚持执行代码,而不是让模型直接作答。Round 1 中,数据全量可见时,C 的模型在长上下文中直接数 distinct 评论者,答成 2,实际是 3。模型会数错,解释器不会。
示范与说教的边界
D 定稿前做了一轮提示词消融(各格 n=23–24):
| 变体 | 系统提示词 | 前缀 | 正确率 | ctx | out | 轮数 |
|---|---|---|---|---|---|---|
| v1 规则说教 | 完整规则,无注释通道 | 探索 | 95.8% | 2080 | 248 | 2.12 |
| v2 注释指令(定稿) | 完整规则 + "思考写成 # 注释" | 探索 | 100% | 2062 | 203 | 2.04 |
| v3 纯示范 | 一句话 | 探索+热身问答示范 | 39.1% | 9747 | 1045 | 8.26 |
| v4 示范+终止一句 | 一句话+ANSWER 说明 | 探索+示范 | 87.0% | 2115 | 280 | 2.17 |
v3 想用示范取代全部规则,正确率降到 39.1%。模型把 16 算对后,把显示这个值当作回答结束,随后反复输出 16,最后输出散文并触发 SyntaxError。这很像人类在 IPython 里查看中间结果的习惯。终止协议属于会话的元规则,不在会话内容的分布中,一次示范无法稳定传达。v4 加回一句 ANSWER 说明,正确率回到 87%。剩下 3 个失败都是未经执行的猜值,说明"答案必须来自已执行输出"这条约束不可少。
边界很清楚。示范适合传递注释、批处理和变量复用等习惯,v3 中这些风格保持良好。规则适合传递终止协议和防猜约束等元规则。少掉其中任何一类,正确率都会下降。
同模态:编程语言的真正角色
编程语言的作用不是让模型更懂这种表示,这一层的收益为零。它让示范与行动处于同一模态:示范里的每一行都能执行,示范中绑定的变量也真实存在于运行时,因此模型可以直接续写示范。纯文本 skill 文档只能描述行为,不能成为行为前缀。
邻近工作主要讨论"行动"侧。CodeAct 与 smolagents 讨论代码行动能否减少步数,Cloudflare Code Mode 与 Anthropic 的 code execution with MCP 将工具封装为代码 API。近期 NVIDIA-labs 的 OO Agents(NOOA)从另一侧把角色、状态和能力收回到 Python 对象:模型面对的是对象的方法与活运行时状态,而非另一套框架专属的协议。这与本文的方向相邻,但侧重点不同:NOOA 提出 Agent 的编程模型;本文把上下文实现为可执行会话,并分别测量表示形式、工作习惯、协议层和循环的成本。拆开后,被讨论最多的表示形式反而是唯一没有收益的一层。
如果你在做 agent
- 不必改变数据表示形式。受控对照下,这一层没有收益。
- 要传递变量复用、批处理和注释思考等习惯,就提供一段伪造历史,而不是用文档说明。
- 协议层是本实验中节省最多的一项,ctx 降了 57%。可以考虑裸 completion 循环,让消息直接成为 cell。
- 任务能一次命中时,不要默认开启循环。循环为多轮纠错付费,本实验中再增加了 59% ctx。
局限
- 单模型(deepseek-v4-flash)。未在重度 tool-calling RL 的模型(Claude/GPT 系)上复验,它们可能对协议层有格式依赖,结论方向未知。
- 单任务族:结构化数据的只读查询,天然利好代码条件;写操作、多步依赖任务未测。
- 正确率几乎全程天花板(Round 2 五组全 100%),所以全部结论只关于成本,不关于能力上限。
- 每模板重复 n=3,无显著性检验;OpenRouter 上游路由方差未控。
- "原语工具"条件(read-only、不可组合)从未真正测到,因为模型自发绕开了它。
下一步
Round 3 已排期。它会把 MCP 工具分别以 tool-calling 和 Python API 暴露给同一个 agent,在 N+1 组合压力下对比 A 与 D,测量 Code Mode 类主张的成本。