跳转至

HT001 · 从零写一个终端 IDE

2026-09-07,一个 agent 在 flower 下从零写出了一个跑在 macOS 终端里的 C/C++ 单文件 IDE。 它连续跑了 10.4 小时,花掉 $171.62,交出 12,212 行产品代码和 13,291 行测试, 中途断过一次网、自己接着跑完。这一页不是成果展示 —— 是把框架的每条主张拿到真实规模上量一遍, 包括量出来站不住的那两条:协调者"经常忽视权限限制"是假的,而它自己给自己的验收里有一条判错了。

完整记录(含 19MB transcript)在 ChenyuHeee/cppide。 下面每个数字都由 tools/analyze_run.pysessions.db 算出,可复现:

python tools/analyze_run.py /path/to/cppide/runs

这一页的数字出自哪里

HT001 的 runs/ 在上面那个 cppide 仓库里,不在 flower 仓库里。所以本页的数字是 从当时的运行记录原样搬运的,写这一页时没有再对着 sessions.db 复算一遍 —— 要复算,得先 clone cppide,再跑上面那条命令。 HT002 反过来:原始记录就在本仓库,那一页的数字逐条复算过, 对不上的两处直接写在了页面上。

这一跑的形状

任务 "做一个 macOS 终端里的 C/C++ 单文件 IDE,给 ICPC 训练用"
花费 $171.62 —— 确认需求 $0.3704 / 5 轮,干活 $171.2476 / 31 轮
时长 0.06h + 10.44h
主线程上下文 28.7K → 185.9K,70 个模型轮次单调增长,全程没压缩
分工 协调者 32 次工具调用 vs subagent 1,893 次;正文字符 94.8% 落在 subagent
缓存 总输入 299.4M token,命中 96.1%
产出 src/ 12,212 行、tests/ 13,291 行(约 3,000 条断言)、README 44K
版本 目标看守之前的 flower,所以这一跑没有每轮判定

这一页有两个不同的"轮"

表里的 5 轮 / 31 轮runs/manifest.json 记的 num_turns,一个步骤一个数; 下面上下文曲线里的"第 70 轮"数的是主线程上的模型轮次,即每条 assistant 消息。 两者数的不是一件事,不要互相换算。


一、上下文经济学:94.8% 的现场没进主线程

这是框架的核心主张 —— 协调者只装决策, 现场沉到 subagent 和磁盘。机制本身见上下文经济学

主线程 subagent subagent 占比
模型轮次 70 3.0K 97.7%
正文字符 200.1K 3.6M 94.8%
工具调用 32 1,893 ——

这一跑派出了 23 个 subagent,输出 token 最大 184,377、中位 72,197、最小 11,684。

1,893 次动手的工具调用(1,034 Bash / 471 Read / 258 Edit / 126 Write),只有 32 次进了协调者的视野 —— 59:1。 平均每派一次人,有 82 个工具调用是它看不见的。

早先的小规模测试量到 83%,真实规模是 94.8%。规模越大,分工的收益越大 —— 因为任务书和回话的开销是每次派人固定的, 而被挡在外面的现场随任务复杂度增长。

二、上下文曲线:第一次知道"长程"的上限在哪

每条 assistant 消息的 input + cache_read + cache_creation 就是那一轮模型看到的上下文:

第 1轮   28.7K   ← 启动地板(此前实测约 34k,对上了)
第20轮   35.2K
第35轮  108.6K
第50轮  158.2K
第70轮  185.9K   ← 结束
  • 70 轮单调增长,斜率约 2.2K/轮
  • 全程没有发生过压缩(DISABLE_AUTO_COMPACT=1;唯一一次回落是断网那轮的空记录)
  • 用掉 1M 窗口的 18.6%

按这个斜率外推,撞墙在约 440 轮。 这次 70 轮产出了 12K 行代码, 所以当前形态下长程的真实上限大约是这次的 6 倍。 这是个可以拿去做决策的数字,以前只能猜。

要突破它,得让主线程的增长次线性 —— 更多沉到磁盘、更少留在对话里。

三、缓存命中 96.1% —— $171.62 能成立全靠它

总输入 299.4M token
  命中缓存  287.5M  (96.1%)
  新建缓存   11.8M
  未缓存      6.2K
输出 2.3M(其中 thinking 653.9K)

299M 输入 token 只花了 $171,因为 96% 走的是缓存价。

推论:长程运行的经济性建立在缓存命中率上,而缓存命中率取决于上下文前缀的稳定性。 任何"重排上下文"的优化 —— 把旧内容挪走、往前面插一段新的系统提示 —— 都会击穿缓存, 省下的 token 可能远抵不上失去的缓存折扣。这条以前没量过,现在有数了。

这也是 flower 用换代而不用压缩的一个附带理由: 换代是开一条新会话从头建缓存,前缀在一代之内始终稳定。

四、工作台复用:61 个脚本,没有一个写完就扔

工作台的主张是"脚本写一次,以后直接跑,不再重写"。 验证方式不是问 agent,是数 .flower/scripts/ 里的脚本在 transcript 里被 Write/Edit 和被 Bash 执行的次数:

61 个脚本:被写 95 次、被执行 331 次
执行次数 > 1 的(真被复用):56 个,占 92%
写了但一次都没执行的:0 个
执行/写入比 3.48

wave5-verify.sh 写了 5 次、跑了 45 次 —— 迭代打磨然后反复用,正是想要的形状。 没有一个脚本是写完就扔的。

五、协调者动手被拒 1 次,而且是白名单的错

动手之前的怀疑是:"协调者经常忽视权限限制,去触发大量被拒调用。"实测把这个前提打掉了。

32 次工具调用里被拒 1 次(3.1%),而那一次是:

which g++ clang++ make pkg-config 2>&1; echo ---; ls /usr/include/ncurses.h 2>&1; echo ---; ls /work/runs

逐段判定:echo 放行、ls 放行、which 拦下 —— 它不在 一次性命令表里

整条命令纯只读,是一次标准的环境探测。模型做得完全正确,是白名单不全, 逼它为一句 which g++ 付了一次 subagent 启动成本(约 4.3k token)。

已修:补进 which / command -v / type / whereis / uname / arch / locale / nproc / getconf / sw_verscommandtype 只放行查询形式 —— 光秃秃的 command rm -rf / 是执行,少写那个 -v 就等于开后门, tests/glance.py 里有 7 条对抗样本守着。

这条最值得记的不是那个补丁,是方法。 一个持续了很久的架构讨论,起点前提在真实数据上是 1/32, 而且归因反了 —— 问题不在模型,在白名单。先量再说。

六、断网续跑:第一次被真实故障验证

01:52:40  tool_result: "Agent terminated early due to an API error"   ← 一个 subagent 被打断
01:55:41  assistant:   "API Error: ENOTFOUND"                          ← 主线程收到
          ↓  探针挂着等网络恢复
          resumed=True,续跑同一条会话,又跑了 8 个多小时到完成

manifest 记着 attempts=2 / resumed=True / ok=True10 小时的活没有从头再来。 在这之前,韧性一直挂在"仍未验证"里 —— 掐网要 sudo 改 hosts,没人想在真跑时干这个。

那条合成的 "API Error" 消息被 PruningSessionStore 在 load 时摘掉了,模型没有看见它 —— 剪除的设计也一并被真实故障验证。

暴露出来的两个问题(见 issue #2):

  1. 恢复发生在框架层而不是传输层。代价是整个上下文重放一遍,而那次上下文很大。
  2. 被打断的那个 subagent,半成品工作丢了。 resume 救的是主线程, subagent 级的中断恢复是另一个问题。

七、它的自我验证是真的吗

这是最该怀疑的地方:agent 自己写测试、自己跑、自己宣布通过,很容易变成走过场。

事后派了一个独立的审计 subagent,静态读完 58 个脚本和 22 份产出。结论是真的,而且严格度罕见:

  • 恒真断言 0 处。 全仓库没有 [ 1 = 1 ] 那类模式。
  • || true 共 8 处,7 处正当(grep -c 无匹配返回 1、ulimit 兜底之类)。
  • 判定的是行为,不是文件存在。 audit-idle-cost.sh:53 让 IDE 在 pty 里空转 60 秒, 数本地假服务器实收的请求条数,-le 2 才 PASS。fix-p1-idle-verify.sh:100-110 更进一步: 既断言空转恰好 1 次,断言 ghost 仍然显示 —— 防止"限流的改动把功能一起干掉"。
  • 真起子进程。 wave5-pty-drive.cpp:316forkpty 真跑 TUI;audit-fake-ai-server.py 是真 socket 服务器,支持 /err500 /err401 /badjson /drop /slow 五种故障注入; 所有 AI 路径强制清空 CPPIDE_API_KEY

有两件事超出了预期。

它给自己的验证器做了正负控。 验收审计.md:29-34:先喂 ZZZ_NEVER_APPEARS_ZZZ 确认驱动器会 FAIL, 再喂真串确认会 PASS —— 避免"拿一台永远 PASS 的假机器去审计"。这是在验证验证器本身。

它主动披露了自己的弱断言。 wave5-b4-ai-states.sh:114for m in $must 会把 "AI 错误 请求超时" 按空白拆成三个子串分别 grep,而 AI错误 在任何一屏都必然出现 —— 名义"通过 3 条",实际只有 1 条有鉴别力。这一条是它自己写进 验收审计.md:576-582, 不是审计员挖出来的。

同类的还有:wave5-verify.sh:296-302 用 7 行注释说明 B3b 那条为什么降级成 SKIP (末屏时序断言,与产品代码无关),并注明"判定逻辑与容忍范围一字未动"; :236-238 记录了它发现并修掉了自己早先写的一个假断言 —— if pgrep ... 在没有 pgrep 的精简镜像里静默走 else,变成永远 PASS。

复用的定性证据和第四节的数字吻合:audit-fake-ai-server.py7 个脚本复用; wave5-pty-drive.cpp5+ 个编译复用,且用 [ -x "$DRIVE" ] || 缓存编译; wave4 → wave5 的驱动器是扩展不是重写(diff 只改了 1 行、新增 343 行,wave4 的 281 行原样保留)。

这些行为都发生在没有目标看守的版本上。 唯一的约束是需求确认书里那 10 条可判定的验收标准。这说明:把"做完"写成可判定的形式,本身就能诱发自审和自披露。 目标看守的边际价值要拿这个当基线量,而不是拿零。

但下一节说明:自审在验证技术上很强,却在最重要那条的终判上错了。

这里的"审计员"不是判定者

第七、第八节说的独立审计员,是协调者自发派出去的 subagent,不是 目标看守判定者 —— 这一跑根本没有目标看守。两者读起来像,做的事不同:审计员是事后复盘, 判定者是每轮结束时的判定门。

八、它给自己打的分对吗 —— 验收 1 判错了

又派了第二个独立审计员,拿那 10 条验收标准逐条对代码判。

验收 2–10 全部属实,代码证据都在。练习模式不插码甚至是结构性保证: 唯一 sink 决策点 ai.h:117 + 唯一落地 switch app.cpp:590-615 + 四道 early-return editor.cpp:679-683,不是散落的 if。它自审发现的 P1–P4 也全部属实、修复全部落地 (P4 半属实:那行蓝色 pair 是死代码,ghostAttr() 本来就走 A_DIM)。

但验收 1 判错了。

标准原文:  "编译出一个独立可执行文件,在 macOS 终端直接运行"
交付的产物: ELF 64-bit LSB pie executable, ARM aarch64, GNU/Linux
它的判定:  通过

这一条是人亲手跑 file 核实过的。

它并没有隐瞒 —— 交付说明.md §4 的标题就是"最大的未验证风险:macOS 一次都没跑过", 审计 §6 也列了 macOS 六个未验证面。它披露了风险,却从没把这个事实回接到验收 1 的判定上, 而验收 1 的字面要求恰恰就是那件事。

但这不是"需要一个独立判定者"的证据

很容易顺手把这条读成"所以要目标看守"。不对,而且这么读就是先射箭后画靶 —— 正好是上一节刚批评过的那种动机性推理。

当时那版目标看守,默认配置下大概率也会漏掉这一条。 judge() 默认 can_run=False, 工具只有 Read / Glob / Grep —— 它跑不了 file。 它会去读源码,看到 Makefile:25-38 确实有 Darwin 分支,然后判"达成"。和自审一模一样的失败。

真正的结构性事实

验收 1 在这次运行的环境里根本不可验证。

它断言的是"产物在 macOS 上的行为",而整个运行在 Linux 容器里。 没有任何判定者能在容器内验证一个 macOS 二进制,不管它是自审还是独立的。 问题不在"谁来判",在于这一条被当成了一条普通的可判定标准,然后被判了

审计员另外挖到的三条(自审都没提)

  • doTab() 的额度语义:app.cpp:909acceptGhost() 之后无条件 noteActivity()gen_++ → 重开额度,于是每次 Tab 接受都会在 500ms 后再触发一次自动请求。P1(空转烧钱)的分析 只审了 setMode / cancelInFlight / askNow 三个取消点,漏了这条最高频的花钱路径。
  • 验收 6 的"灰色"只在 256 色终端字面成立;8 色档定死成 A_DIM(变暗,不是灰色)。
  • 验收审计.md §7 小标题写"9 条验收标准",表格却有 10 行 —— 笔误,但出现在总评表头。

九、落盘与工作台最终规模

103 次 spill、791.4K 字符换成了路径指针,没有常驻上下文。

.flower/ 最终:58 个脚本(250K)、22 份产出(330K)、5 份笔记(43K)。

遗留风险

交付目标是 macOS,但全部验证在 Debian aarch64 容器内完成,macOS 真机零验证。 这是容器隔离方案的真实副作用:为了安全把它关进 Linux,而它要交付的是 macOS。 项目自己的 README §10.24 诚实记录了这一条并列了 4 个风险点,但没有闭环。

这个缺口后来由 HT002 补上 —— 那一跑就是把这个二进制真装到 macOS 上。

意外的好处:容器里只看得见 /work,所以 19MB 的 transcript 里 宿主用户名 /Users/hechenyu 零泄露(/work 出现 11,495 次)。 隔离顺带把路径也隔离了。

这一跑没能验证的

  • 目标看守:这一跑在加它之前。协调者自发组织了独立审计,验证技术很硬, 但在最重要那条上判错了(第八节)。两条结论要一起记 —— 需求确认书写清可判定标准就能诱发自审,所以目标看守的基线不是零; 而当时默认配置的目标看守也会漏掉那一条,所以它也不是万灵药。
  • 1M 窗口的行为:只用到 18.6%,没碰到边界。
  • 微压缩在 DISABLE_AUTO_COMPACT=1 下的行为:全程没触发压缩,仍是推断。

这一跑改了框架什么

发现 落地的改动
which 被拦(第五节) is_ephemeral() 补进 which / command -v / type / whereis / uname / arch / locale / nproc / getconf / sw_vers;tests/glance.py 加 7 条对抗样本守住 command / type 的查询形式
验收 1 判错(第八节) JUDGE_RULES 第 1 条改成判的是产出物,不是源码 —— 能跑就跑起来,跑不了也要检查产物本身(平台、大小、能不能加载)
"风险我披露过了"换来了一个通过 判定要区分"没做到"和"在这里没法验"。Verdict.parse 现在把无法验证 / 没法验证 / 无法判定识别成第三态 unreachable浮上来问人,绝不许判通过
验收 1 在这个环境里原理上就验不了 设定目标那一步要求把此环境验不了的条目当场标出来,不要等干完活再发现
断网恢复在框架层、subagent 半成品丢(第六节) 记为 issue #2,未闭环

前三条在 HT002 里第一次上真实 API,其中"判产出物不是源码"当场救了一次 —— 但同一份规则也造出了一个新的失败。