从零手写一个最小化的 agentic coding harness,目的不是做出能用的产品,而是通过亲手实现来理解 Claude Code / Codex CLI / DeepSeek Harness 这类真实产品的架构决策为什么长这样。
配套的逆向工程笔记(对应 Obsidian vault AGENT/06 Architecture Reverse Engineering):
- Claude Code — auto mode 分类器、ToolSearch 延迟加载、fork 子 agent、prompt caching 三层结构
- Codex — turn/step 循环、
apply_patch精简 diff、sandbox 与 approval policy 的正交拆分、AGENTS.md 约定 - DeepSeek Harness — 插件化事件总线、context caching、Agent Teams
每个 stage 对应架构里的一个维度,按"没有它会出什么问题"的顺序排列,故意让每一步都能看到 "缺了上一层会怎么坏"。所有代码调用真实的 Claude API(不是模拟),这样观察到的行为 (cache hit/miss、工具调用格式、压缩效果)都是真的,不是编出来的示意。
pip install -r requirements.txt
cp .env.example .env # 填入你的 ANTHROPIC_API_KEY
python stage1_bare_loop.py| Stage | 文件 | 对应架构维度 | 状态 |
|---|---|---|---|
| 1 | stage1_bare_loop.py |
无工具的裸循环(对照组) | ✅ 已完成 |
| 2 | stage2_tool_loop.py |
Core agentic loop(turn/step) | ✅ 已完成 |
| 3 | stage3_tool_registry.py |
Tool design(工具注册表 + 只读/有副作用标签) | ✅ 已完成 |
| 4 | stage4_context_management.py |
Context management(prompt caching + auto-compaction) | ⏳ 计划中 |
| 5 | stage5_planning.py |
Planning(update_plan 风格工具) |
⏳ 计划中 |
| 6 | stage6_permissions.py |
Permission & safety(审批门 + 简单沙箱) | ⏳ 计划中 |
| 7 | stage7_subagent.py |
Multi-agent(子 agent 委派 / fork 语义对比) | ⏳ 计划中 |
目标:钉死"turn"这个最基础的概念——一个 turn = 一次用户输入 + 一次模型请求 + 一条 assistant 消息,没有工具,没有多 step。作为后面所有 stage 的对照组,之后每加一层都能回头对比"多出了什么"。
学到的东西:
max_tokens只是单次回复的输出长度上限,不是上下文窗口限制。真正的上限是 Claude Sonnet 4.5 的 context window(远大于max_tokens),但因为没有压缩机制,messageslist 会随对话轮数无限增长,长时间聊下去迟早会撞到这个更大的上限,而且没有 prompt caching 时 每一轮都要把全部历史重新算一遍 token,成本会显著上升——这正是 Stage 4 要解决的问题, 提前意识到这一点后主动停止了继续消耗 token 的测试。
目标:加入一个 bash 工具,把"一个 turn 内部可能包含多个 step"这个真实 harness 都有、
但通常不对外暴露的结构显式打印出来。事件命名沿用 DeepSeek Harness 架构文档里的
turn/start → step/start → tool/call → tool/result → step/end → turn/end。
循环终止依据 API 返回的 stop_reason:tool_use 就继续下一个 step,end_turn 才真正结束
这个 turn;MAX_STEPS_PER_TURN 是防止模型在工具调用上死循环的安全阀,所有真实 harness
(Claude Code、Codex)都有类似机制。
学到的东西:亲自观察到多 step 是如何被模型自主触发的——不是我们在代码里硬编码"先读文件 再运行"这种顺序,而是模型自己根据上一步工具返回的结果决定要不要再调用一次工具。这个 observation-dependent 的循环结构就是 ReAct 模式的最小实现。
目标:从"一个工具用 if/elif 判断"扩展成一个真正的工具注册表,加入 read_file /
write_file / edit_file,每个工具在注册表里显式标注 read_only: bool。edit_file 用
find-replace(要求 old_string 在文件中唯一匹配)而不是重写整个文件,对应 Codex 的
apply_patch 走精简 diff 而不是整文件替换的同一个理由:模型只需要生成变化的部分,token 更省,
出错空间更小。
学到的东西:工具粒度不是审美选择——拆成独立工具之后,每个工具才能独立打权限标签,这是
Stage 6 审批门能够存在的前提。edit_file 的"不唯一就报错"分支也不是摆设,它逼着模型在
替换前提供足够上下文来定位目标,防止改错地方。
目标:解决 Stage 1 里发现的问题——上下文只涨不降,成本线性上升。这一步会做两件事:
- 接入真实的 Anthropic prompt caching(
cache_controlblock),观察真实的 cache hit/miss 数据,而不是模拟。 - 实现一个简单的 auto-compaction:超过 token 阈值时,调用模型自己把历史总结成一段摘要,
替换掉原始的
messageslist。会对照 Claude Code(本地重新生成可读摘要文本)和 Codex (调用 API 端专用压缩端点、返回不透明的encrypted_contentblob)两种不同的压缩哲学, 自己选一种实现并说明取舍。
目标:加一个 update_plan 风格的工具,模型自己决定要不要用(不是每次任务都强制走计划
模式,对应 Codex 里"仅在非平凡多步任务时使用"的设计),并强制"同一时间只能有一个
in_progress 步骤"这条约束。会故意去掉这条约束试一次,观察模型是否会尝试并行开工却没有
一个真正完成,从而理解这条看似多余的规则实际防止了什么问题。
目标:给 Stage 3 里打好的 read_only 标签接上真正的拦截逻辑——只读工具直接放行,
有副作用的工具默认询问用户确认。再加一个简单沙箱(子进程 + 工作目录白名单,不做真正的
OS 级隔离),用来体会 Codex 架构里"sandbox(技术上能做什么)"和"approval policy
(什么时候要问人)"是两根独立坐标轴这个设计——一个命令可以在沙箱范围内、但仍然需要审批,
反之亦然。
目标:实现"父 agent 委派子 agent、子 agent 独立上下文、只把最终结果带回父级"的最小版本。
会额外实现一个"fork"变体(子 agent 继承父级完整上下文和历史,而不是从零开始),对比两种
语义在实际 token 消耗和结果质量上的差异——这对应 Claude Code 里 Agent 工具(普通子 agent)
和 subagent_type: "fork"(共享上下文)的真实区别。
- 所有代码直接调用 Claude API(
claude-sonnet-4-5),不模拟工具调用协议。 - 每个 stage 是独立可运行的脚本,不依赖后面 stage 的代码;新 stage 通常是在前一个 stage 的基础上做增量修改。
.env存放 API key,已加入.gitignore,不会被提交。