从 Hooks 到 TodoWrite 再到 Subagent:一个 Claude Code 风格 Harness 的构造过程 一、什么是 Harness? 在 Agent 系统里,LLM 本身只负责“思考”和“决定下一步”。但真正让它能稳定工作的,不只是模型,而是模型外面那层控制结构,也就是 harness 。
可以把 harness 理解成:
包住 Agent 的运行外壳,负责接收用户输入、调用模型、分发工具、拦截危险操作、记录状态、控制循环退出,以及在复杂任务中帮 Agent 维持方向。
这三个文件刚好按顺序展示了一个 harness 的逐步增强过程:
s04:加入 Hooks,让循环可扩展;
s05:加入 TodoWrite,让 Agent 先规划再执行;
s06:加入 Subagent,让复杂任务拆出去,用干净上下文处理。
第一部分:s04 Hooks —— 不要把扩展逻辑写死在循环里 1. 问题:agent_loop 会越来越臃肿 在 s03 里,Agent 已经能调用工具,也能做权限检查。
但是问题来了:如果后面还想加这些功能:
每次 bash 调用都写日志;
写文件后自动 git add;
工具输出太大时提醒;
执行危险命令前拦截;
Agent 结束前做清理;
如果每加一个功能,就往 agent_loop 里面塞一行代码,那么核心循环会越来越乱。
原本循环应该只负责:
1 调用模型 → 解析 tool_use → 执行工具 → 回填结果 → 继续循环
但如果扩展逻辑都写进去,就会变成:
1 2 3 4 5 log_to_file(block) check_permission(block) notify_slack(block) execute(block) auto_git_add(block)
这就破坏了 harness 的核心原则:
agent_loop 应该稳定,扩展逻辑应该挂在循环外面。
所以 s04 引入了 Hooks。
2. 核心设计:HOOKS 注册表 s04 的核心代码是一个 Hook 注册表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 HOOKS = { "UserPromptSubmit" : [], "PreToolUse" : [], "PostToolUse" : [], "Stop" : [], } def register_hook (event: str , callback ): HOOKS[event].append(callback) def trigger_hooks (event: str , *args ): for callback in HOOKS[event]: result = callback(*args) if result is not None : return result return None
这个结构很像 Spring 里的拦截器、过滤器,也像前端里的生命周期钩子。
它的意思是:
register_hook():注册一个扩展函数;
trigger_hooks():在某个时机触发对应函数;
如果 hook 返回 None,说明继续执行;
如果 hook 返回非 None,说明要中断或者改变流程。
3. 四个关键 Hook 时机 s04 只实现了四类事件,但已经覆盖了 Agent 一轮运行的关键节点。
1)UserPromptSubmit 触发位置:用户输入之后,进入 LLM 之前。
用途:
打印当前工作目录;
注入上下文;
校验用户输入;
修改用户 prompt。
示例:
1 2 3 4 5 def context_inject_hook (query: str ) -> str | None : print (f"[HOOK] UserPromptSubmit: working in {WORKDIR} " ) return None register_hook("UserPromptSubmit" , context_inject_hook)
主流程中这样触发:
1 2 3 4 query = input ("s04 >> " ) trigger_hooks("UserPromptSubmit" , query) history.append({"role" : "user" , "content" : query}) agent_loop(history)
也就是说,用户输入不会直接进入 Agent,而是先经过 Hook 层。
触发位置:工具执行前。
用途:
权限检查;
日志记录;
拦截危险命令;
判断是否允许写文件。
例如权限检查:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def permission_hook (block ): if block.name == "bash" : for pattern in DENY_LIST: if pattern in block.input .get("command" , "" ): return "Permission denied by deny list" if block.name in ("write_file" , "edit_file" ): path = block.input .get("path" , "" ) if not (WORKDIR / path).resolve().is_relative_to(WORKDIR): choice = input ("Allow? [y/N] " ).strip().lower() if choice not in ("y" , "yes" ): return "Permission denied by user" return None
这里的重点是:
权限检查不再写死在 agent_loop 中,而是变成了一个 PreToolUse hook。
执行工具前,循环只需要这样写:
1 2 3 4 5 6 7 8 9 blocked = trigger_hooks("PreToolUse" , block) if blocked: results.append({ "type" : "tool_result" , "tool_use_id" : block.id , "content" : str (blocked) }) continue
如果 hook 返回了拦截信息,这次工具调用就不会执行,而是把“被拦截的原因”作为 tool_result 返回给模型。
这非常关键,因为模型可以看到失败原因,然后自己调整下一步。
3)PostToolUse 触发位置:工具执行后。
用途:
记录工具结果;
检查输出是否过大;
自动执行副作用;
做工具结果审计。
例如:
1 2 3 4 5 def large_output_hook (block, output ): if len (str (output)) > 100000 : print (f"[HOOK] Large output from {block.name} " ) register_hook("PostToolUse" , large_output_hook)
工具真正执行后:
1 2 3 4 handler = TOOL_HANDLERS.get(block.name) output = handler(**block.input ) if handler else f"Unknown: {block.name} " trigger_hooks("PostToolUse" , block, output)
这说明 PostToolUse 不负责执行工具,它只是在工具执行完成后“顺手检查一下”。
4)Stop 触发位置:模型不再请求工具,即 Agent 准备退出时。
用途:
打印总结;
清理资源;
决定是否强制继续;
做最后检查。
示例:
1 2 3 4 5 6 7 8 9 10 11 def summary_hook (messages: list ) -> str | None : tool_count = sum ( 1 for m in messages for b in (m.get("content" ) if isinstance (m.get("content" ), list ) else []) if isinstance (b, dict ) and b.get("type" ) == "tool_result" ) print (f"[HOOK] Stop: session used {tool_count} tool calls" ) return None register_hook("Stop" , summary_hook)
循环退出前:
1 2 3 4 5 6 7 8 if response.stop_reason != "tool_use" : force = trigger_hooks("Stop" , messages) if force: messages.append({"role" : "user" , "content" : force}) continue return
这里有一个小细节:如果 Stop hook 返回了内容,就会把它重新塞回 messages,让 Agent 继续跑。
这就给 harness 提供了“临门一脚”的控制能力。
4. s04 的核心价值 s04 的核心不是增加了某个具体功能,而是改变了架构方式。
以前是:
1 agent_loop 里面写权限、日志、提醒、清理
现在是:
1 2 agent_loop 只触发 hook 具体逻辑挂到 HOOKS 里
所以 s04 的本质是:
把 Agent 循环变成稳定内核,把扩展能力放到外部插件系统里。
这一步非常重要。因为后面的 TodoWrite 和 Subagent 都是在这个稳定循环上继续加能力。
第二部分:s05 TodoWrite —— 让 Agent 先列计划,再开始干活 1. 问题:Agent 容易在长任务中跑偏 有了 Hooks 后,Agent 可以安全地执行工具了。
但新的问题出现了:复杂任务中,Agent 很容易忘记原始目标。
比如用户说:
把所有 Python 文件改成 snake_case,运行测试,并修复失败。
Agent 可能一开始确实去改文件名,但跑测试发现失败后,它的注意力就被测试错误吸走了。修着修着,它可能忘了最开始还有“统一 snake_case”这个目标。
原因是:
对话轮数越来越多;
工具结果越来越多;
messages 里塞满了中间过程;
原始目标在上下文中被稀释。
所以 s05 加了一个新的工具:todo_write。
2. TodoWrite 的核心作用 todo_write 不执行真实任务。
它不会读文件,不会写文件,也不会运行命令。
它只做一件事:
让 Agent 把任务拆成清单,并维护每一步的状态。
这其实就是给 Agent 加了一个“任务白板”。
3. CURRENT_TODOS:在进程内保存任务列表 核心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 CURRENT_TODOS: list [dict ] = [] def run_todo_write (todos: list ) -> str : global CURRENT_TODOS CURRENT_TODOS = todos lines = ["\n## Current Tasks" ] for t in CURRENT_TODOS: icon = { "pending" : " " , "in_progress" : "▸" , "completed" : "✓" }[t["status" ]] lines.append(f" [{icon} ] {t['content' ]} " ) print ("\n" .join(lines)) return f"Updated {len (CURRENT_TODOS)} tasks"
这个函数做了几件事:
接收模型传来的 todos;
把 todos 存到全局变量 CURRENT_TODOS;
根据状态显示不同图标;
在终端打印当前任务列表;
返回更新结果给模型。
每个 todo 大概长这样:
1 2 3 4 { "content" : "Read project files" , "status" : "pending" }
状态只有三种:
1 2 3 pending in_progress completed
也就是:
pending:还没做;
in_progress:正在做;
completed:已完成。
4. 把 todo_write 加入工具系统 s05 没有改变工具分发机制。
它只是把 todo_write 加入 TOOLS 和 TOOL_HANDLERS。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 TOOLS = [ {"name" : "bash" , ...}, {"name" : "read_file" , ...}, {"name" : "write_file" , ...}, {"name" : "edit_file" , ...}, {"name" : "glob" , ...}, { "name" : "todo_write" , "description" : "Create and manage a task list ..." , "input_schema" : { "type" : "object" , "properties" : { "todos" : { "type" : "array" , "items" : { "type" : "object" , "properties" : { "content" : {"type" : "string" }, "status" : { "type" : "string" , "enum" : [ "pending" , "in_progress" , "completed" ] }, }, }, }, }, }, }, ] TOOL_HANDLERS["todo_write" ] = run_todo_write
这里有一个非常关键的设计点:
todo_write 也是普通工具,仍然通过 TOOL_HANDLERS[block.name] 分发。
也就是说,harness 的工具执行模型没有变,只是多挂了一个工具。
5. Reminder:防止 Agent 忘记更新 TODO s05 还加了一个提醒机制。
如果 Agent 连续 3 轮没有调用 todo_write,harness 就自动给 messages 里追加一条提醒:
1 2 3 4 5 6 if rounds_since_todo >= 3 and messages: messages.append({ "role" : "user" , "content" : "<reminder>Update your todos.</reminder>" , }) rounds_since_todo = 0
这条 reminder 的作用不是给用户看,而是给模型看。
它相当于在提醒模型:
你该更新一下任务进度了。
于是典型流程变成:
1 2 3 4 5 6 7 8 9 10 11 12 13 用户提出复杂任务 ↓ Agent 首先调用 todo_write,列出所有步骤 ↓ 把第一个任务设为 in_progress ↓ 执行工具 ↓ 完成后改成 completed ↓ 继续下一个 pending ↓ 如果太久没更新,harness 自动提醒
6. s05 的核心价值 s05 的重点不是让 Agent 多了一个执行能力,而是让它多了一个规划能力。
以前 Agent 是:
现在变成:
所以 TodoWrite 的价值是:
它不增强 Agent 的手脚,而是增强 Agent 的方向感。
对于复杂任务来说,这很关键。
第三部分:s06 Subagent —— 大任务拆出去,用干净上下文处理 1. 问题:TODO 解决不了上下文污染 TodoWrite 可以让 Agent 不容易跑偏,但它仍然解决不了一个更大的问题:
复杂任务会污染主上下文。
比如 Agent 要修一个 bug,它可能需要:
查 30 个文件;
跟踪调用链;
运行测试;
分析日志;
尝试多个修复方向。
这些中间过程都会进入主 Agent 的 messages[]。
当 messages 越来越长,主 Agent 的注意力就会被大量细节占满。最后它可能连原本要修什么 bug 都忘了。
这时候,光有 TODO 不够。
所以 s06 加了 Subagent。
2. Subagent 的核心思想 Subagent 的思路很像我们自己写代码时的习惯:
遇到复杂问题,单独开一个终端去查。查完后,只把结论带回来,不把所有中间过程都复制回来。
在 Agent 中,就是:
1 2 3 4 5 6 7 8 9 10 11 12 13 主 Agent 遇到复杂子任务 ↓ 调用 task 工具 ↓ task 创建一个子 Agent ↓ 子 Agent 使用新的 messages[] ↓ 子 Agent 自己读文件、运行命令、分析问题 ↓ 最后只返回总结 ↓ 主 Agent 继续处理主任务
这样主 Agent 的上下文不会被污染。
3. spawn_subagent:创建一个独立 Agent 核心函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 def spawn_subagent (description: str ) -> str : sub_tools = [...] messages = [ { "role" : "user" , "content" : description } ] for _ in range (30 ): response = client.messages.create( model=MODEL, system=SUB_SYSTEM, messages=messages, tools=sub_tools, max_tokens=8000 , ) messages.append({ "role" : "assistant" , "content" : response.content }) if response.stop_reason != "tool_use" : break results = [] for block in response.content: if block.type == "tool_use" : blocked = trigger_hooks("PreToolUse" , block) if blocked: results.append({ "type" : "tool_result" , "tool_use_id" : block.id , "content" : str (blocked) }) continue handler = SUB_HANDLERS.get(block.name) output = handler(**block.input ) if handler else "Unknown" trigger_hooks("PostToolUse" , block, output) results.append({ "type" : "tool_result" , "tool_use_id" : block.id , "content" : output }) messages.append({ "role" : "user" , "content" : results }) return extract_text(messages[-1 ]["content" ])
这个函数里有几个关键点。
4. 关键点一:子 Agent 使用新的 messages[] 1 2 3 4 5 6 messages = [ { "role" : "user" , "content" : description } ]
这就是上下文隔离的核心。
子 Agent 不继承主 Agent 的完整聊天历史,只拿到一个任务描述。
这意味着:
子 Agent 可以专心做一个子任务;
它的中间工具调用不会塞进主 Agent 的 messages;
它查文件、跑命令、分析失败的过程都会被丢弃;
主 Agent 最后只拿到一个总结。
5. 关键点二:子 Agent 也有自己的循环 子 Agent 不是简单调用一次模型,而是自己跑一个小型 agent_loop。
它也会:
1 2 3 4 5 6 7 8 9 调用模型 ↓ 判断是否 tool_use ↓ 执行工具 ↓ 回填 tool_result ↓ 继续下一轮
所以 Subagent 本质上是:
在主 Agent 的工具调用里,又启动了一个小 Agent。
但是为了防止无限跑,代码加了安全上限:
也就是最多跑 30 轮。
6. 关键点三:子 Agent 没有 task 工具
文件中说明,子 Agent 有基础工具:
bash;
read_file;
write_file;
edit_file;
glob。
但是没有 task。
原因很简单:
防止子 Agent 再创建子 Agent,导致递归失控。
如果允许递归,可能出现:
1 2 3 4 5 主 Agent └── 子 Agent └── 子 Agent └── 子 Agent └── ...
所以教学版直接把 task 从子 Agent 工具列表里拿掉。
7. 关键点四:权限 Hook 仍然生效 子 Agent 虽然有独立上下文,但不代表它可以绕过安全机制。
它执行工具前仍然会走:
1 blocked = trigger_hooks("PreToolUse" , block)
工具执行后仍然会走:
1 trigger_hooks("PostToolUse" , block, output)
这说明:
上下文可以隔离,但权限不能隔离。
子 Agent 做危险操作时,依然会被主 harness 的 hook 拦截。
8. 把 task 加入主 Agent 工具系统 主 Agent 把 task 当作普通工具使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 TOOLS = [ {"name" : "bash" , ...}, {"name" : "read_file" , ...}, {"name" : "write_file" , ...}, {"name" : "edit_file" , ...}, {"name" : "glob" , ...}, {"name" : "todo_write" , ...}, { "name" : "task" , "description" : "Launch a subagent to handle a complex subtask. Returns only the final conclusion." , "input_schema" : { "type" : "object" , "properties" : { "description" : { "type" : "string" } }, "required" : ["description" ] } }, ] TOOL_HANDLERS["task" ] = spawn_subagent
这点很优雅:
Subagent 没有破坏原来的工具分发系统,它只是新增了一个工具 handler。
主 Agent 看到复杂任务时,会调用:
1 task(description="Find what testing framework this project uses" )
然后 harness 实际执行的是:
1 spawn_subagent(description)
最后返回一段总结文本给主 Agent。
第四部分:完整 Harness 构造过程 把 s04、s05、s06 串起来,一个完整 harness 的构造过程大概是这样的。
第一步:准备基础工具 最开始要有基础工具能力:
1 2 3 4 5 6 7 TOOLS = [ bash, read_file, write_file, edit_file, glob, ]
同时准备工具执行映射:
1 2 3 4 5 6 7 TOOL_HANDLERS = { "bash" : run_bash, "read_file" : read_file, "write_file" : write_file, "edit_file" : edit_file, "glob" : glob_files, }
这个映射表是工具分发的核心。
模型不会直接调用 Python 函数,它只会返回:
1 2 3 4 5 6 tool_use: { name: "read_file" , input : { path: "main.py" } }
harness 拿到 name 后,再从 TOOL_HANDLERS 里找到对应函数:
1 2 handler = TOOL_HANDLERS.get(block.name) output = handler(**block.input )
这就是工具调用真正发生的位置。
第二步:构造 Agent 主循环 主循环负责:
调用 LLM;
保存 assistant 回复;
判断是否有工具调用;
执行工具;
把工具结果塞回 messages;
继续下一轮。
伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 def agent_loop (messages ): while True : response = client.messages.create( model=MODEL, system=SYSTEM, messages=messages, tools=TOOLS, max_tokens=8000 , ) messages.append({ "role" : "assistant" , "content" : response.content }) if response.stop_reason != "tool_use" : return results = [] for block in response.content: if block.type != "tool_use" : continue handler = TOOL_HANDLERS.get(block.name) output = handler(**block.input ) results.append({ "type" : "tool_result" , "tool_use_id" : block.id , "content" : output }) messages.append({ "role" : "user" , "content" : results })
这是 harness 的骨架。
后面所有能力都围绕它增强。
第三步:加入 Hooks,让循环可扩展 定义 Hook 注册表:
1 2 3 4 5 6 HOOKS = { "UserPromptSubmit" : [], "PreToolUse" : [], "PostToolUse" : [], "Stop" : [], }
提供注册和触发函数:
1 2 3 4 5 6 7 8 9 def register_hook (event: str , callback ): HOOKS[event].append(callback) def trigger_hooks (event: str , *args ): for callback in HOOKS[event]: result = callback(*args) if result is not None : return result return None
然后在关键位置挂 Hook:
用户输入后:
1 trigger_hooks("UserPromptSubmit" , query)
工具执行前:
1 blocked = trigger_hooks("PreToolUse" , block)
工具执行后:
1 trigger_hooks("PostToolUse" , block, output)
循环退出前:
1 force = trigger_hooks("Stop" , messages)
这一步完成后,harness 就不再需要把权限、日志、清理逻辑写死在 loop 里了。
权限检查函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def permission_hook (block ): if block.name == "bash" : for pattern in DENY_LIST: if pattern in block.input .get("command" , "" ): return "Permission denied by deny list" if block.name in ("write_file" , "edit_file" ): path = block.input .get("path" , "" ) if not (WORKDIR / path).resolve().is_relative_to(WORKDIR): choice = input ("Allow? [y/N] " ).strip().lower() if choice not in ("y" , "yes" ): return "Permission denied by user" return None
注册:
1 register_hook("PreToolUse" , permission_hook)
工具执行处变成:
1 2 3 4 5 6 7 8 9 blocked = trigger_hooks("PreToolUse" , block) if blocked: results.append({ "type" : "tool_result" , "tool_use_id" : block.id , "content" : str (blocked) }) continue
这一步让安全逻辑和主循环解耦。
第五步:加入 TodoWrite 工具 定义状态:
1 CURRENT_TODOS: list [dict ] = []
定义工具函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def run_todo_write (todos: list ) -> str : global CURRENT_TODOS CURRENT_TODOS = todos lines = ["\n## Current Tasks" ] for t in CURRENT_TODOS: icon = { "pending" : " " , "in_progress" : "▸" , "completed" : "✓" }[t["status" ]] lines.append(f" [{icon} ] {t['content' ]} " ) print ("\n" .join(lines)) return f"Updated {len (CURRENT_TODOS)} tasks"
加入工具列表:
1 2 3 4 5 TOOLS.append({ "name" : "todo_write" , "description" : "Create and manage a task list ..." , "input_schema" : ... })
加入 handler:
1 TOOL_HANDLERS["todo_write" ] = run_todo_write
从这一步开始,Agent 就能主动维护任务清单了。
第六步:加入 Todo Reminder 定义计数器:
如果多轮没有调用 todo_write,自动提醒:
1 2 3 4 5 6 if rounds_since_todo >= 3 and messages: messages.append({ "role" : "user" , "content" : "<reminder>Update your todos.</reminder>" , }) rounds_since_todo = 0
每次执行工具时,如果发现工具名是 todo_write,就重置计数器。
这样 harness 不只是被动执行工具,还会主动提醒 Agent 保持计划更新。
第七步:加入 task 工具 定义主 Agent 的新工具:
1 2 3 4 5 6 7 8 9 10 11 12 13 { "name" : "task" , "description" : "Launch a subagent to handle a complex subtask. Returns only the final conclusion." , "input_schema" : { "type" : "object" , "properties" : { "description" : { "type" : "string" } }, "required" : ["description" ] } }
注册 handler:
1 TOOL_HANDLERS["task" ] = spawn_subagent
从主 Agent 角度看,task 只是一个普通工具。
但是从 harness 角度看,它会启动一个新的 Agent 循环。
第八步:实现 spawn_subagent 子 Agent 的核心构造:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 def spawn_subagent (description: str ) -> str : sub_tools = [...] messages = [ { "role" : "user" , "content" : description } ] for _ in range (30 ): response = client.messages.create( model=MODEL, system=SUB_SYSTEM, messages=messages, tools=sub_tools, max_tokens=8000 , ) messages.append({ "role" : "assistant" , "content" : response.content }) if response.stop_reason != "tool_use" : break results = [] for block in response.content: if block.type != "tool_use" : continue blocked = trigger_hooks("PreToolUse" , block) if blocked: results.append({ "type" : "tool_result" , "tool_use_id" : block.id , "content" : str (blocked) }) continue handler = SUB_HANDLERS.get(block.name) output = handler(**block.input ) if handler else "Unknown" trigger_hooks("PostToolUse" , block, output) results.append({ "type" : "tool_result" , "tool_use_id" : block.id , "content" : output }) messages.append({ "role" : "user" , "content" : results }) return extract_text(messages[-1 ]["content" ])
它的完整流程是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 收到 description ↓ 创建新的 messages[] ↓ 用 SUB_SYSTEM 调用模型 ↓ 子 Agent 自己执行工具 ↓ 工具执行仍走 Hooks ↓ 最多跑 30 轮 ↓ 提取最后结论 ↓ 返回给主 Agent
这一步让 harness 具备了拆任务能力。
第五部分:三层 Harness 能力的关系 这三个文件不是孤立的,而是一层一层往上加能力。
s04:Hooks 解决的是:
怎么在不污染 agent_loop 的情况下扩展 Agent 行为?
核心能力:
它让 harness 从“硬编码循环”变成“可扩展循环”。
s05:TodoWrite 解决的是:
怎么让 Agent 在复杂任务中不忘记计划?
核心能力:
它让 harness 从“只会执行”变成“会督促 Agent 规划”。
s06:Subagent 解决的是:
怎么避免大任务污染主上下文?
核心能力:
1 2 3 4 5 6 创建子 Agent 使用独立 messages[] 只返回最终总结 不返回中间过程 子 Agent 不允许递归创建 task 权限 Hook 仍然生效
它让 harness 从“单一上下文执行”变成“可拆分、可隔离的多 Agent 执行”。
第六部分:最终形成的 Harness 运行流程 完整流程可以这样理解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 用户输入 ↓ UserPromptSubmit Hook ↓ 写入主 messages ↓ 进入 agent_loop ↓ 调用 LLM ↓ LLM 返回普通文本? 是 → 触发 Stop Hook → 退出或继续 否 → 返回 tool_use ↓ 遍历 tool_use ↓ PreToolUse Hook 如果被拦截 → 返回拦截结果给模型 如果通过 → 执行工具 ↓ 根据工具名分发: bash/read/write/edit/glob → 执行基础工具 todo_write → 更新任务清单 task → 启动子 Agent ↓ PostToolUse Hook ↓ 收集 tool_result ↓ 把 tool_result 作为 user message 放回 messages ↓ 继续下一轮
如果工具是 task,则进入子流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 task(description) ↓ spawn_subagent(description) ↓ 创建新的 messages[] ↓ 子 Agent 调用 LLM ↓ 子 Agent 执行工具 ↓ 子 Agent 工具调用仍经过 Hooks ↓ 子 Agent 完成 ↓ 只提取最终文本总结 ↓ 返回给主 Agent
总结 这三个章节其实在讲一个非常清晰的 Agent harness 演进路线。
最开始,Agent 只有一个简单循环:
s04 加入 Hooks,让循环可以被扩展,但不被污染。
s05 加入 TodoWrite,让 Agent 在复杂任务中先规划、再执行、持续更新状态。
s06 加入 Subagent,让大任务可以拆给独立上下文处理,主 Agent 只接收最终结论。
所以最终的 harness 不只是“调用大模型的代码”,而是一套控制系统:
Hooks 负责扩展;
TodoWrite 负责规划;
Subagent 负责隔离;
TOOL_HANDLERS 负责工具分发;
messages 负责上下文流转;
agent_loop 负责整体调度。
一句话概括:
一个可靠的 Agent,不是靠模型自己一直聪明,而是靠 harness 在模型外面帮它守住流程、计划和上下文边界。