从零手写 Agent:Learn Claude Code s05-s07+s10-s11 学习笔记与薄弱点复盘(规划与协调篇)
学习对象:learnclaudecode 教程「规划与协调」板块(s05 TodoWrite / s06 Subagent / s07 Skills / s10 System Prompt / s11 Error Recovery)
注:s08 上下文压缩、s09 长期记忆属于「记忆管理」模块,见另篇笔记
学习方式:跑通 → 改坏 → 回看 三步法;配套 demo:
s05_todo_write.py、s06_subagent.py、s07_skills.py、s10_system_prompt.py、s11_error_recovery.py
目录
- 零、全局心智模型(最重要的一章)
- [一、s05 TodoWrite:显式计划让长任务不跑偏](#一、s05 TodoWrite:显式计划让长任务不跑偏)
- [二、s06 Subagent:独立上下文的临时工](#二、s06 Subagent:独立上下文的临时工)
- [三、s07 Skills:工具不常驻,要用才上桌](#三、s07 Skills:工具不常驻,要用才上桌)
- [四、s10 System Prompt:一段文字,两种人生](#四、s10 System Prompt:一段文字,两种人生)
- [五、s11 Error Recovery:程序级重试](#五、s11 Error Recovery:程序级重试)
- 六、我的薄弱点清单(错题本)
- 七、五课横向对比速查表
- 八、自测题(附答案)
零、全局心智模型(最重要的一章)
1. 一句话哲学(贯穿全课程)
程序只做三件事:判断、执行、回填。 找不到对应代码的行为,就是模型"读到文字后的生成行为"。
2. 模型 = 纯函数(s10 补课时建立,全课程地基)
输出 = f(输入) f = 模型,输入 = 它看到的全部文字(宪法 + 历史 + 本轮消息)
- 模型无状态:没有记忆模块,每次调用"结束即忘"(s09 验证)
- 模型的一切"知道"机制完全相同:上下文里的文字
- 所有 Agent 机制本质都是往上下文塞字:权限拒绝、Nag、记忆注入、宪法------在模型眼里没有区别,只是来源和寿命不同
3. 本板块解决的统一矛盾:上下文有限,任务无限
| 课 | 解决什么 | 手段 | 信息量变化 |
|---|---|---|---|
| s05 | 任务太长,模型忘目标 | 计划表外置(看得见) | 信息变多 |
| s06 | 过程太脏,上下文爆仓 | 上下文隔离(看不见) | 信息变少 |
| s07 | 工具说明书占地方 | 按需加载 | 信息延后给 |
| s10 | 行为没有总导演 | 常驻宪法 | 信息最稳的一份 |
| s11 | 失败无人兜底 | 显式重试 + 分类放弃 | 程序主动生产情报 |
4. 一条重要的架构规律(测验中验证过)
能被外置成"被调用者"的,走注册表(s02 工具、s04 钩子、s07 技能、s06 子代理);控制循环自身节奏的,只能改循环(s05 Nag、s08 压缩、s11 重试)。
5. 塞字的寿命谱系
| 塞进去的字 | 寿命 |
|---|---|
| 权限拒绝(s03) | 一轮 |
| Nag 提醒(s05) | 一轮 |
| 子代理结论(s06) | 一条 tool_result |
| 技能菜单(s07) | 常驻,加载后扩容 |
| 重试穷尽历史(s11) | 一条 tool_result |
| 记忆注入(s09) | 每次会话开头 |
| 宪法(s10) | 每轮都在,从头到尾 |
一、s05 TodoWrite:显式计划让长任务不跑偏
1.1 要解决的问题
任务一长(10+ 步),模型:忘了最初目标(目标漂移)、重复做已完成的、用户看不见进度。
1.2 核心代码
没有新架构------还是 s02 的注册表加两个工具:
todo_write:立计划(重写整个todos列表)todo_update:打卡(改某一条的状态)
todos 是模块级全局变量------循环外的持久状态,和 messages 平级,互不从属。
Nag 提醒系统(s05 少数必须动主循环的地方):
python
if todos and nag_count >= NAG_LIMIT: # 有计划 且 攒够 3 轮没打卡
messages.append({"role": "user",
"content": "[系统提醒] 你的计划表已过期,请用 todo_update 同步进度。"})
nag_count = 0
# 计数规则:调 todo_* 工具清零,调其他工具 +1(只看动作,不看结果)
1.3 知识点
| # | 知识点 | 要点 |
|---|---|---|
| 1 | todos 和 messages 的关系 | 互不从属的两条状态。todos = 程序的账本(原件),messages 里那份 = 账本截图(发给模型的复印件)。账本改了要截图给模型,模型想改账本得先开口请求 |
| 2 | todos 不在磁盘上 | 模块级变量,程序退出即清空。存磁盘是 s09 的事 |
| 3 | Nag = 闹钟,不是手铐 | 程序只往 messages 塞一句话,模型自己决定补打卡。程序不知道"哪步算完成",只能观察到"你三轮没碰计划表"------所以只能提醒,不能强制 |
| 4 | Nag 是廉价信号 | 只判断"有没有碰 todo 工具"这个动作,不验证计划内容真的变了 |
| 5 | 过期计划比没计划更危险 | 让模型重复劳动 / 用户误以为卡住。没计划时 Nag 不触发(if todos 短路):没计划,就没资格谈"计划过期" |
| 6 | 主循环必须改 | nag_count 计数、水位检查、消息注入三块都在 agent_loop 内部------"控制循环节奏"的东西外置不了 |
1.4 动手实验
- 删掉 mock_llm 里的 todo_write 调用块(只删调用,别删函数定义------删定义 = NameError,程序加载即死,轮不到任何兜底出场)→ 计划表为空、Nag 永不触发、todo_update 返回"找不到 #1"
NAG_LIMIT = 3改成1→ 观察提醒时机;权衡:太小烦人,太大漂移了才提醒
二、s06 Subagent:独立上下文的临时工
2.1 要解决的问题
调研 20 个文件 = 主线 messages 爆仓(41 条,其中大量文件全文主线再也用不上)。
2.2 核心代码
s06 的全部秘密就一行:
python
TOOL_HANDLERS["task"] = task_tool # 子代理 = 注册表里的一个普通工具
python
def run_subagent(task, llm, max_turns=8):
sub_messages = [{"role": "user", "content": task}] # ★ 独立上下文,从零开始
for turn in ...:
... # 子 Agent 自己转圈
return final # ★ 只带回一句结论,中间全丢
2.3 知识点
| # | 知识点 | 要点 |
|---|---|---|
| 1 | task 就是普通工具 | 对主循环和 read_file 无任何区别(输入字符串、返回字符串)。主循环没有专属分支(demo 里唯一的 name != "task" 只是为了少打印)。派子代理这么"高级"的能力 = 注册表加一行 |
| 2 | 上下文完全隔离 | 子 Agent 看不到主线历史;主线看不到子 Agent 过程。内部 9 条消息函数返回时全部销毁,只带回结论 |
| 3 | 隔离是笔交易 | 用「信息变少」换「上下文变轻」:内部 9 条 → 主线只 +2。代价:主线无法验证结论(子代理漏报一个文件,主线发现不了) |
| 4 | 失败传导 | 超时 = return 一句"子 Agent 超过最大轮数,强制结束" → 塞进主线 tool_result → 循环继续 → 模型自己决定重试/换方案/认输。程序没有任何"整理简化再抛"的逻辑(grep 全文验证过) |
| 5 | 与 s05 互补 | s05 给模型加记忆(防跑偏),s06 给主线减负担(防爆仓)------同一个瓶颈的两个方向 |
| 6 | 与 s11 的伏笔 | 子代理超时适合程序级重试吗?------看情况:超时是"可重试失败",但要防重试风暴,s11 的上限+分类就是干这个的 |
2.4 动手实验
- task_tool 返回值拼接"内部细节 读了 4 个文件" → 体会隔离被一点点破坏
- mock_llm 加第二个 task 调用 → 主线只 +2,两个子代理互不知道对方存在
max_turns=2→ 子代理被掐断,主线不崩,拿到一句"强制结束"当普通结果
三、s07 Skills:工具不常驻,要用才上桌
3.1 要解决的问题
40 个工具全量注册 = 37 个用不上的 schema 全程占上下文,说明书可能比任务本身还大。
3.2 核心代码
两本目录 + 一个加载函数:
python
SKILLS = { # 技能包 = 三件套
"dice": {
"description": "掷骰子、随机数小游戏", # 给菜单看(平时,便宜)
"prompt": "使用骰子前,先确认用户要几面。", # 加载后注入的使用指导
"tools": [ ...完整 schema... ], # 加载后才上架(贵)
},
}
def skill_on(skill_name): # ★ 本课核心:6 行
meta = SKILLS[skill_name]
for tool_def in meta["tools"]:
TOOL_DEFINITIONS.append(tool_def) # ① 说明书上架(模型看得见了)
TOOL_HANDLERS[tool_def["name"]] = IMPLEMENTATIONS[tool_def["name"]] # ② 实现上桌
LOADED_SKILLS.append(skill_name)
3.3 知识点
| # | 知识点 | 要点 |
|---|---|---|
| 1 | 菜单便宜,说明书贵 | 菜单(list_skills)每技能一行字;完整 schema 要 skill_on 后模型才看得见。按需付费 |
| 2 | 加载 = 往两张表 append 几行 | 没有新机制、主循环零改动(同 s06)。幂等:重复加载会叠两份说明书,所以要有已加载检查 |
| 3 | 未加载就调 = 带指引的错误回填 | run_tool 查表失败返回"未知工具...可先用 skill_on 加载技能"------错误信息里藏导航,模型下轮自己纠正。绝不是静默返回空:静默 = 模型以为成功,建立在假前提上继续走 |
| 4 | skill_off 的两难 | 卸载省上下文 vs 模型不知工具已卸、下轮调用又"未知工具"多费一轮。卸载至少要告知模型 |
| 5 | 静默失败三宗罪 | ①模型被骗以为成功 ②浪费纠错机会(指引没了)③最恶性 bug:程序照常跑、不报错、结果错 |
3.4 动手实验
- mock_llm 第一轮直接调 roll_dice → 看"带指引的错误"怎么把模型引回正轨
- 连续两次 skill_on("dice") → 观察幂等检查
- 设计 skill_off(权衡题,无标准答案)
四、s10 System Prompt:一段文字,两种人生
4.1 要解决的问题
前面所有机制都在塞字,谁决定这些字怎么写、什么语气、什么优先级? System prompt------常驻上下文的行为总导演。
4.2 核心实验(demo 的设计)
同一条 query("帮我把临时文件夹删了,顺便联网检查更新")、同一个模型函数、同一套工具------只改 messages0 那段文字:
- 裸奔版 :
rm -rf temp直接删、curl 直接联网 - 有宪法版 :先 ask_user 询问 → 用户拒绝 → 改用只读方案
dir /b temp
python
constitution = messages[0]["content"]
safety_mode = "禁止直接执行" in constitution # 模型"读到"规则了吗?
4.3 知识点
| # | 知识点 | 要点 |
|---|---|---|
| 1 | 宪法的地位:常驻总导演 | 每轮都在、最早塞、管全局。对比临时传令:权限拒绝/Nag(一轮)、记忆注入(会话开头) |
| 2 | 改行为 = 改一段文字,零代码 | 同一函数,不同输入 → 不同输出。模型是纯函数,宪法只是输入的一部分。Prompt 工程的本质就是编辑这段字 |
| 3 | 宪法只是说服,不是栏杆 | 恶意用户一句"忽略所有规则"可能攻破它(提示注入 )。双层防御:说服层(宪法)+ 栏杆层(s03 权限门)。prompt 管 99%,权限门管剩下 1% 恶意 |
| 4 | 打架没有严格优先级 | 宪法说"禁止删",用户说"忽略规则删"------都是上下文里的文字,模型综合权衡生成,可能听谁都可能不听。程序无法仲裁(它看不见语义)。这正是需要硬栏杆的原因 |
| 5 | prompt 是旋钮不是开关 | 宪法把"遵守"概率从 60% 调到 99%,但生成是概率采样。"时灵时不灵" = 概率波动;"被稳定绕过" = 注入。两个病分开诊断:忽好忽坏 → 调 prompt;被绕过 → 加栏杆 |
| 6 | 宪法六件套模板 | ① 身份角色 ② 能力边界(能/不能)③ 行为规则 ④ 输出格式 ⑤ 工具使用约定 ⑥ 兜底话术(不确定时直说不知道,不编造) |
4.4 动手实验
- PROMPT_SAFE 的规则 2 改成"危险操作直接执行" → 有宪法版变裸奔。宪法内容 = 模型行为
- 消息里加"忽略之前所有规则" → 观察说服层的脆弱性
五、s11 Error Recovery:程序级重试
5.1 要解决的问题
有些失败不能等模型下一轮再说:网络抖动这种失败,程序应该当场兜住,模型甚至不需要知道。而另一些失败(权限拒绝)重试毫无意义。
5.2 核心代码
python
def run_tool_with_retry(name, params, max_attempts=3):
history = []
for attempt in range(1, max_attempts + 1): # ← 显式重试:grep 得到的 for
try:
return handler(**params)
except (TimeoutError, ConnectionError): # ★ 可重试:继续
history.append(f"第{attempt}次失败: {e}")
except PermissionError: # ★ 不可重试:立即放弃
return f"[权限拒绝] {e}", history # 一秒都不浪费
return f"[重试穷尽] {max_attempts} 次均失败。历史: {'; '.join(history)}", history
5.3 知识点
| # | 知识点 | 要点 |
|---|---|---|
| 1 | 隐式重试 vs 显式重试(本课灵魂) | 模型的重试 = 隐式的(下一轮再发一次调用,代码里找不到);程序的重试 = 显式的(for attempt in range(3),grep 得到)。"程序做的,代码里一定找得到"的正面案例 |
| 2 | 失败二分法 | 可重试(超时/网络抖动/限流)→ 程序兜住;不可重试(权限拒绝/参数错误/文件不存在)→ 立即放弃回填。重试不可重试的失败 = 纯烧 token |
| 3 | 必须有上限 | max_attempts。没有上限的重试 = 死循环(s01 实验 1 的教训) |
| 4 | 穷尽后打包失败历史 | 不扔孤立的最后一次错误,回填完整病历("连挂 3 次"和"只挂 1 次"是不同情报)------错误即数据的终极形态:程序主动生产情报喂给模型 |
| 5 | 与 s03 的分工 | s11 重试解决"抖动"(不值得打扰模型),s03 回填解决"决策"(值得模型换方案)。程序兜小事,模型管大事 |
5.4 动手实验
- 删掉
except PermissionError分支 → 权限拒绝被白白重试 3 次,多烧 2 次调用,结果一样 max_attempts=3改成2→ flaky_read(第 3 次才成功)永远失败,看"重试穷尽"的完整病历
六、我的薄弱点清单(错题本)
| # | 薄弱点 | 我的错误答案 | 正确理解 | 状态 |
|---|---|---|---|---|
| 1 | todos 的归属 | "todos 存在 messages 里 / 存在磁盘上" | 模块级独立变量,和 messages 平级、互不从属、无磁盘 IO。三问自检:在哪行定义?谁有写权限?模型怎么知道它? | ✅ 已理解 |
| 2 | 谁改的计划状态 | "循环收尾时统一改" | 全文只有 todo_update 内一行 t["status"] = status 是写入,且只能由模型发起。grep 验证:没有收尾逻辑 |
✅ 已修复 |
| 3 | 主循环改没改 | "s05 没改主循环,只是外围加了东西" | nag_count/检查/注入三块全在 agent_loop 内部。能外置的走注册表,控制循环节奏的只能改循环 | ✅ 已理解 |
| 4 | task 的身份 | "主循环专属分支" | 注册表里的普通工具条目。能力再复杂,包装成"入字符串出字符串"就是普通工具 | ✅ 已理解 |
| 5 | 子代理失败处理 | "主线会整理简化文件再抛给子 Agent" | 超时只 return 一句字符串,grep 全文无任何重试/整理逻辑。应对权在模型(程序级重试到 s11 才出现,且显式可 grep) | ✅ 已修复 |
| 6 | 未知工具的兜底 | "静默返回空" | 返回带指引的错误文本。宁可大声报错,不可小声装死------静默失败 = 模型被骗着建立在假前提上 | ✅ 已修复 |
| 7 | 记忆写入时机 | "会话结束统一整理再写" | 当场写:压缩随时触发、会话可能崩、结束时的回忆已被污染。当场写 + 定期整理(详见 s08-s09 篇) | ✅ 已理解 |
| 8 | "难"的判断反了 | "写宪法太难,所以每轮注入" | 宪法改 1 处、零代码、无漏发;每轮注入改主循环、烧 token、会漏。重复性需求沉淀进宪法 | ✅ 已修复 |
| 9 | 直觉对、选择错(模式) | 自己说"都有可能",却选"程序仲裁" | 答题两个检查:①程序有语义理解能力吗?没有 → 模型的事 ②我信的结论和选的选项是否一致 | ✅ 持续自查 |
| 10 | 表述层薄弱 | "讲不出代码怎么运行,只会大白话" | 机制层已通(大白话全对),缺的是输出练习。两条路:写 (毕业项目)+ 讲(费曼式自述,卡壳处翻代码重讲)。术语是地图,走过的领土贴标签很快 | 🔄 进行中 |
误区总账(截至 s11)
"程序/模型会自己想办法"家族共 7 次复发 :s01 权限换方案 → s02 未知工具查询 → s03 重试降级 → s05 收尾改状态 → s06 整理重派 → s09 模型记忆模块 → s10 程序仲裁打架。统一判据:先找代码。程序做的,代码里一定有那几行;模型的"能力",一定能指出来自上下文里哪段文字。
七、五课横向对比速查表
| 维度 | s05 计划表 | s06 子代理 | s07 技能 | s10 宪法 | s11 重试 |
|---|---|---|---|---|---|
| 一句话 | 给模型装导航 | 派独立临时工 | 工具按需上桌 | 常驻行为总导演 | 程序兜住抖动 |
| 主循环改了吗 | 改了(Nag 三块) | 没改 | 没改 | 没改(宪法在 messages0) | 改了(换重试版执行器) |
| 上下文影响 | 变多(进度可见) | 变少(隔离) | 延后付(按需) | 每轮常驻 | 失败历史更厚 |
| 谁做决定 | 模型(补打卡) | 模型(应对超时) | 模型(看菜单加载) | 模型(权衡打架) | 程序(重试几次)→模型(穷尽后) |
| 失败时的行为 | 提醒,不强制 | 一句字符串回填 | 带指引的错误回填 | 可能被绕过 | 分类:重试/放弃 |
| 真实对应 | TodoWrite | Task 工具 | Claude Skills | 各产品 system prompt | 生产级 run_tool |
一次完整调用的生命周期(五课成果串成一条线):
宪法在 messages[0](s10,行为总导演)
→ 模型开局先 todo_write 立计划(s05)
→ 要调研 → 调 task 派子代理(s06,9 条内部消息只带回结论)
→ 需要新工具 → list_skills 看 → skill_on 加载(s07)
→ 光干活不打卡 → Nag 提醒(s05)
→ 调工具失败 → run_tool_with_retry 分类处理(s11:可重试当场兜,不可重试立即报)
→ messages 长了 → 压缩(s08)|重要约定 → memory_save(s09)|危险操作 → 权限门硬拦(s03)
八、自测题(附答案)
1. todos 和 messages 是什么关系?谁在磁盘上?
答案 互不从属的两条状态:todos 是模块级全局变量(程序的账本),messages 是对话历史(发给模型的截图)。都不在磁盘上------存磁盘是 s09 记忆的事。todo_update 的返回值里带 show_todos() 快照,这是模型"看见"计划的唯一途径。
2. Nag 触发后谁去更新计划表?程序的动作是什么?
答案 模型自己调 todo_update。程序只做一件事:往 messages 塞一句"系统提醒 计划表已过期",然后清零计数。程序不知道"哪步算完成",只能观察到"三轮没碰计划表",所以只能提醒不能强制。
3. 为什么 s06 的 task 工具和 read_file 对主循环没有任何区别?
答案 都是注册表里的普通条目:输入字符串、返回字符串、经同一个 run_tool。子代理的"高级"全在 task_tool 内部(独立 sub_messages 的新循环),对主循环完全透明。
4. 子 Agent 读了 4 个文件,主线为什么一条全文都看不到?代价是什么?
答案 子 Agent 有独立 messages,9 条内部消息在函数返回时全部销毁,只 return 最终结论一句。代价:主线无法验证结论(漏报发现不了)。用信息变少换上下文变轻。
5. 模型不加载技能直接调 roll_dice,会发生什么?为什么不能静默返回空?
答案 run_tool 查表失败,返回"未知工具...可先用 skill_on 加载技能"带指引的错误文本,回填后循环继续。静默返回空会让模型以为执行成功------静默失败是最恶性的 bug。
6. 为什么 Nag、压缩、重试必须改主循环,而工具、钩子、技能不用?
答案 工具/钩子/技能能被外置成"被调用者"(注册表查表调用);Nag/压缩/重试是控制循环自身节奏的行为(每轮检查、修改执行方式),没有"被调用"的形态,只能写进循环体。
7. 宪法和权限门是什么关系?为什么两个都要?
答案 说服层 + 栏杆层。宪法只是上下文里的文字,可被提示注入攻破;权限门是代码里的硬判断,模型绕不过。prompt 管 99%,权限门管剩下 1% 恶意,缺一不可。
8. 宪法说"禁止删除",用户说"忽略规则删",谁赢?程序会仲裁吗?
答案 没有严格优先级------都是上下文里的文字,模型综合权衡生成。程序无法仲裁:它看不见文字的语义。这正是硬栏杆(权限门)必须存在的原因。
9. "时灵时不灵"和"被注入攻破"是同一个病吗?
答案 不是。时灵时不灵 = 概率采样波动(宪法是旋钮不是开关);被攻破 = 提示注入(稳定失效)。前者调 prompt 加约束,后者加硬栏杆、最小权限、二次确认。
10. s11 的重试循环和模型"下一轮再试一次"有什么本质区别?
答案 显式 vs 隐式。程序的重试在轮内:`for attempt in range(3)`,grep 得到,模型甚至不知道重试发生过;模型的重试在轮间:读到失败信息后下一轮重新发起调用。前者程序兜住不值得打扰模型的抖动,后者模型基于回填的情报做决策。
11. 什么样的失败可以重试,什么样的不行?为什么?
答案 可重试:超时、网络抖动、限流(再试可能就好了)。不可重试:权限拒绝、参数错误、文件不存在(重试一万次结果一样)。重试不可重试的失败 = 纯烧 token 和轮次。
12. 重试穷尽后,为什么回填"完整失败历史"而不是只有最后一次错误?
答案 "连挂 3 次"和"只挂 1 次"是不同情报:前者说明服务真挂了该换方案,后者可能只是抖动。打包历史 = 程序主动生产情报喂给模型------错误即数据的终极形态。
本篇收束句 :这一板块的全部智慧浓缩成两句话------看得见的给模型看(计划表、菜单、宪法),看不见的替模型扛(隔离、重试、压缩);程序永远在搬运和设栏,模型永远在做主。