Back to list

Agent Harness 剖析

Jim Lin
AGITech

上周我把一个叫 learn-claude-code 的项目从 s01 跑到了 s20。

它用 20 章从零手写了一个 nano 版的 Claude Code,标语只有一行:Bash is all you need。让我意外的是,一个能干活的 coding agent,内核真的就是一段二十行的循环。后面所有花哨的能力,记忆、子任务、多 Agent 协作、上下文压缩,全是围着这二十行往外长出来的。

读完我的感受是,它讲清楚了一件我一直没说明白的事:写 Agent 的人,到底在写什么。这篇是我的读书笔记,把它的核心机制一个个拆开,配上图和我自己的理解。

它的前提:Agent = 模型 + Harness

项目开篇就立了一个我认为很关键的前提:

Agency(感知、推理、行动的能力)来自模型训练,不来自外部代码编排。一个 Agent 产品 = 模型 + Harness。

它用一段历史佐证。2013 年 DQN 只看像素和分数就学会打 Atari,2019 年 OpenAI Five 自己跟自己打了 45000 年 Dota 干掉了 TI8 冠军,AlphaStar 上了星际宗师段位,腾讯绝悟在王者里让职业选手 15 局只赢 1 局。这些系统的架构是同一个:一个训练出来的模型,放进一个环境,给它感知和行动的接口。

模型负责智能,环境负责行动空间,合起来才是一个完整的 Agent。Atari 模拟器、Dota 客户端、星际引擎,到了 coding agent 这里,环境就是 IDE 和终端。

把这个前提立住,"做 Agent"就只剩两种可能:要么你在训模型(调权重、RLHF、收轨迹数据),要么你在造 Harness(写让模型能在某个领域干活的那套代码)。大部分人做的是后者。这个词项目里叫 harness engineering,我更愿意叫它"造车"。

模型是司机,你造的是车

Harness 在项目里有一个干净的公式:

Harness = Tools + Knowledge + Observation + Action + Permissions
 
  Tools:        文件读写、shell、网络、数据库、浏览器
  Knowledge:    产品文档、领域参考、API 规范、风格指南
  Observation:  git diff、错误日志、浏览器状态
  Action:       CLI 命令、API 调用、UI 交互
  Permissions:  沙箱隔离、审批流、信任边界

模型做决策,Harness 负责执行。模型推理,Harness 提供上下文。项目里有一句话我很认同:Claude Code 优雅的地方,恰恰在于它"不做什么"。它不试图代替模型决策,不强加固定 workflow,不用手写规则替代模型的判断。给工具、给知识、给上下文管理、给权限边界,然后让开。

你不是在写智能,你是在造智能栖身的那个世界。世界的质量,直接决定智能能发挥到几成。

二十行的内核

前提和分工讲清楚了,看代码。这段循环从第 1 章到第 20 章一行都没改过:

def agent_loop(messages):
    while True:
        response = client.messages.create(model, system, messages, tools)
        messages.append(assistant_response)
        if response.stop_reason != "tool_use":
            return                      # 模型说停就停
        # 否则:执行模型要的工具,把结果追加回 messages,继续循环

模型要调工具就调,调完把结果塞回对话,循环到模型自己决定停下。代码不替它判断"任务完成了没",那是模型的事。

整个项目的精髓就在这里:这个 loop 不变,后面 19 章每一章只往它周围叠加一个机制,从不改写它本身。

Loop 属于 Agent,机制属于 Harness。

20 章我不逐章讲,挑其中最能体现设计思想的机制拆开。先放一张全图当地图。

20 章地图

章节主题一句话口号
s01Agent Loop一个循环加 bash,就是一个 Agent
s02Tool Use加工具等于加 handler,loop 不动
s03Permission先立边界,再给自由
s04Hooks在 loop 周围挂钩子,别改 loop
s05TodoWrite没计划的 Agent 会跑偏
s06Subagent大任务拆小,每个子任务给干净上下文
s07Skill Loading知识按需加载,不要一次塞满
s08Context Compact上下文总会满,得有腾地方的办法
s09Memory记住重要的,忘掉无关的
s10System PromptPrompt 运行时拼装,不是写死的
s11Error Recovery出错不是终点,是重试的起点
s12Task System大目标拆小任务,排序,落盘
s13Background Tasks慢操作丢后台,Agent 继续想
s14Cron Scheduler按时触发,不用人去戳
s15Agent Teams一个 Agent 干不完,叫队友
s16Team Protocols队友之间要有统一的通信规则
s17Autonomous Agents队友自己看板认领,不用人派活
s18Worktree Isolation各干各的目录,互不干扰
s19MCP Plugin能力不够,用 MCP 插更多工具进来
s20Comprehensive众多机制,回到一个 loop

有个坑先提醒你:仓库里现在两套教程并存。根目录的 s01s20 是新的正式版本,docs/ 和 web 平台还是旧的 12 章版本,章节号对不上。新读者直接读根目录。

下面的拆解没按章节号排,而是按主题归了组,从给 Agent 装手、立规矩,到管好喂进模型的上下文,再到扛住失败、扩展成团队。顺序跳一点,但读起来更成片。

加一个工具,就是加一个 handler

第 2 章解决一个最基础的问题:怎么给 Agent 加能力,又不动那个内核循环。

答案是一张分发表。每个工具就是一个函数,注册进一个名字到函数的字典里。模型说要调 bash,循环就去表里找 bash 对应的 handler 执行;说要调 read_file,就找 read_file。循环本身永远只做一件事:把模型点名的工具名,转给对应的处理函数。

这有点像总机话务员。来电报一个分机号,话务员只负责接通,不关心那头是销售还是客服。你想加一个新部门,只要在交换机上登记一个分机号,话务员的工作流程一个字都不用改。

这个设计的价值在于扩展成本恒定。加第 1 个工具和加第 100 个工具,对循环来说没区别,都是往字典里多塞一项。Claude Code 那么多工具,底层就是这么一张表。

先立边界,再给自由

工具越强,越危险。一个能跑 bash 的 Agent,理论上能 rm -rf 掉你的整个目录。第 3 章和第 4 章处理的就是这件事。

第 3 章是权限。它把每个工具调用过一遍判断:有些可以直接放行(读文件),有些必须拦下(删根目录),有些得停下来问人(写生产配置)。这就像给一个新来的实习生开权限,能做的列清楚,红线划明白,拿不准的先报备。

第 4 章是 hooks,在工具执行的前后插入扩展点。PreToolUse 在工具跑之前触发,PostToolUse 在之后触发。重点是它的口号:在 loop 周围挂钩子,别改 loop。想加日志、加审计、加自定义拦截,都从钩子进,内核循环依然纹丝不动。这跟前面那张分发表是一个哲学,所有扩展都发生在循环外围。

上下文总会满:四层压缩

装好手、立好规矩,Agent 就能动了。但任务一长,它立刻撞上同一堵墙:上下文窗口满了。接下来这几章都在解决同一件事,怎么管好喂进模型的那段上下文,也是我最喜欢的一组。第 8 章先接最直接的问题:窗口满了怎么办。它给的不是一个开关,是一条四层流水线,原则是"便宜的先做,贵的最后做"。

我会用收拾一张越堆越满的桌子来理解它。先把大件文件归进抽屉(把超大的工具结果落盘,上下文里只留一个引用),再扔掉中间没用的草稿(裁剪旧消息),再把不常翻的旧文件换成一张便利贴标记(旧工具结果换占位符)。这三步都很便宜。只有当桌子还是满,才花时间做一份会议纪要,把一摞纸压成一页(让模型对历史做全量摘要,这一步要花一次 API 调用,所以放最后)。万一摘要完 API 还是报超长,再走紧急兜底。

注释里写明这套执行顺序对齐了 Claude Code 的真实行为。把"上下文管理"从一个模糊的概念,落成一条有先后、有成本意识的流水线,这是我觉得这章最值钱的地方。

记住重要的,忘掉无关的

压缩解决的是单次会话内的空间,记忆解决的是跨会话的延续。第 9 章把记忆拆成三个子系统,我觉得这个拆法很接近人脑。

extraction 是随手记笔记,每轮对话结束,从原始消息里抽出值得留下的东西写进一个文件。selection 是用到时翻出相关的那几页,按文件名和描述把当前任务相关的记忆调进上下文,而不是把整本笔记本都背上。consolidation 项目里叫"做梦",周期性地把零碎笔记整理、合并、去重,免得记忆库越长越乱。

存储上它用一个 MEMORY.md 当索引,每条记忆一行,底下挂一堆带 YAML frontmatter 的单文件。这个"索引加按需加载"的结构,跟它处理技能、处理上下文是同一套思路:永远不要把全部内容一次性塞进去。

Prompt 不写死,运行时拼装

第 10 章处理 system prompt。大多数人会把它写成一个固定的大字符串,从头到尾不变。这一章的做法不一样:把 prompt 拆成一段段(身份、工具说明、工作目录、记忆等),每一轮根据当前真实状态现拼。

关键是"按真实状态",不是按关键词猜。比如记忆那一段,只有当 .memory/MEMORY.md 真的存在时才加进去,不存在就不占位置。拼好之后还做了一层确定性缓存,相同状态拼出来的 prompt 完全一致。

像出门前收拾背包。固定要带的钥匙钱包每次都在,下雨才塞把伞,出差才装电脑。你不会背一个塞满所有可能用到的东西、永远不变的大包出门。

这么做有两个好处。一是上下文干净,每轮只放当前真正相关的段。二是缓存友好,身份和工具说明这类稳定前缀可以被 prompt caching 命中,省钱也省延迟。本质上它和压缩、记忆是同一套哲学的又一次出现:能不放进上下文的,就别放。

脏活交给替身

把内容挡在上下文外面,还有一招更狠的:一整段脏活都不让它进主上下文。第 6 章的 subagent 干的就是这个,名字听着像多智能体那套玄学,其实本质就是上下文隔离。

主 Agent 遇到一件需要翻很多资料的脏活,比如"在整个代码库里找出所有调用了某个废弃接口的地方",它不自己干,而是派一个全新空白上下文的子 Agent 去。子 Agent 翻了几十个文件、跑了一堆 grep,过程极其啰嗦,但这些噪音全留在它自己的上下文里。干完它只把一句结论带回主线。

像你让助理去查档案。他翻了一上午,最后给你一页摘要,不会把整个档案室搬到你桌上。主 Agent 的上下文因此始终干净,省下来的空间留给真正重要的推理。

知识按需加载

第 7 章的 skill loading 是同一个"按需"哲学的又一次出现。

一个 Agent 可能会几十上百种技能,但你不能把它们全写进 system prompt,那会瞬间撑爆上下文,而且大部分技能这次任务根本用不上。它的做法是分两层:先给模型一份目录,列出有哪些 skill、每个大概干嘛,这份目录很轻;模型判断这次要用哪个,才把那一篇技能的完整内容展开注入。

就是图书馆的逻辑。你先看索引卡,知道某本书在哪、讲什么,需要哪本再去书架上取,没人会把整个书库搬回家。Agent 的能力上限,很大程度上就取决于这种"压缩与按需展开"做得好不好。

出错不是终点

上下文管到这儿就差不多了,但还有一个前面一直回避的现实:LLM 调用本身会失败,而且会用好几种方式失败。第 11 章把调用包进 try/except,按错误类型分了三条恢复路径。

我用开长途车来理解这三条路。话没说完就被打断(输出被 max_tokens 截断),那就深吸一口气接着说;后备箱塞太满关不上(prompt 超长),先扔掉点东西再上路(触发压缩);前面堵车、服务区排长队(被限流、服务过载),就等一会儿再走,等太久干脆换一辆备用车(fallback model)。

错误类型不同,恢复路径就不同。不是一律重试,也不是一遇错就退出。

这章看着不起眼,却是从 demo 走到能用的分水岭。真实的 Agent,难的从来不是顺利那条路,是各种 API 抽风时还能优雅地接住。把恢复策略按错误类型分门别类,是健壮性的起点。

一个人干不完,就叫队友

单个 Agent 再健壮也有上限,有些任务一个脑子装不下。从第 12 章到第 18 章,项目把它扩展成一个能协作的团队。这几章拼在一起是一套完整的多 Agent 最小实现,我把它们合着讲。

第 12 章先把目标拆成带依赖关系的任务,落盘成磁盘上的任务图,blockedBy 字段记着谁卡着谁。第 15、16 章给队友配了文件邮箱,一堆 .jsonl 文件当异步信箱,队友在后台线程里跑各自简化版的循环,消息会被注入回主 Agent 的对话历史,而不只是打印出来。第 17 章更进一步,队友自己去任务板上认领活,不需要主 Agent 一个个派。第 18 章给每个任务分配独立的 git worktree,各自在自己的分支目录里干活,互不踩脚。

像一个小团队运作:墙上贴着任务板和依赖关系,成员自己认领,通过信箱异步沟通,每个人在自己的工位上干活。把这套机制拆开看,所谓"多智能体"并不神秘,就是任务图加邮箱加目录隔离。

能力不够,就插上去

第 19 章是 MCP,把外部能力接进同一个工具池。

对模型来说,一个通过 MCP 接进来的外部工具,和一个本地内置的 bash,长得一模一样,都是工具池里的一项。这就像电脑的扩展坞:你不用拆开机箱改主板,插上一个坞,外接屏幕、网口、读卡器一下都有了。Agent 的能力边界,因此可以在不改内核的前提下不断外扩。

到第 20 章,前面所有机制汇总,重新围着那个二十行的循环组装成一个完整的 Agent。绕了一大圈,内核还是那个内核。

怎么读,怎么上手

每一章是一个文件夹,结构统一:一篇叙事 README.md(中文,含内联代码,还有英日翻译),一段独立可跑的 code.py,必要时配 SVG 图。复杂章节用 <details> 折叠深水区,想钻再展开。

读法很简单,从 s01 顺着读到 s20,后一章默认你读过前面,结尾会埋一个钩子勾到下一章。上手三行:

git clone https://github.com/shareAI-lab/learn-claude-code
pip install -r requirements.txt && cp .env.example .env   # 配 ANTHROPIC_API_KEY
python s01_agent_loop/code.py                              # 从一个 loop 加 bash 起步

跑完 s01 你大概率会上头,因为你会亲眼看到一个能干活的 coding agent,核心真的就是那二十行。再去 python s08_context_compact/code.py 看压缩流水线,python s20_comprehensive/code.py 看所有机制合体。项目还顺手给了两个产品化出口:Kode Agent CLI 和姊妹教程 claw0(讲 heartbeat 和 cron 怎么把一次性 Agent 变成常驻助手),读完主线想往前走,可以当路标。

我的思考

跑完这 20 章,几个判断对我来说更清楚了。

第一,"编排"这个词被高估了。市面上一大半 Agent 平台卖的是编排能力,拖拽、连线、配置分支。但编排的本质是在替模型做决策,是用确定性的代码去框住一个本该不确定的智能。模型越强,你框得越多,浪费得越多。真正该投入的不是编排,是工具的质量、知识的组织、上下文的管理、权限的边界,也就是 Harness 本身。

第二,Harness 是有壁垒的,而且壁垒在变深。模型是共享的,谁都能调 API。但同一个模型,套上一个好 Harness 和一个差 Harness,表现能差出一个数量级。Agent 的能力上限不在模型参数,在你给它的那套环境,而这部分恰恰是工程师能掌控、能积累、能形成差异的地方。

第三,克制是一种很高级的工程能力。这个项目最打动我的不是它实现了多少机制,是它有一个二十行的 loop 二十章都没动过。知道什么不该写,比知道什么该写更难。很多人写 Agent 的毛病,是手痒,是不信任模型,是总想多管一点。学会让开,是它给我的最大一课。

第四,轨迹即数据。项目里有一句被低估的话:Agent 在你的 Harness 里跑出来的每一段行动序列,都是训练信号。你今天造的车,跑出来的路径,是明天那个更强模型的原料。Harness 工程师不只是在用模型,也在为模型的下一代喂数据。这个闭环一旦想明白,造 Harness 这件事的长期价值就完全不一样了。

如果你也在做 Agent,我的建议是先把 s01 跑一遍,再回头看你手头那套系统,问自己一个问题:我写的这些代码,有多少在帮模型,有多少在替模型?

参考