Claude Code 学习总结

《Learn Claude Code》复习

原文出处 :learn.shareai.run/zh/(开源教程 Learn Claude Code ,用 20 个可运行的 Python 教学脚本,从 102 行到 1708 行,逐层还原一个 AI 编程 Agent 的 harness(运行时外壳/驱动层) 是怎么搭起来的)。


📖 给谁看 & 建议阅读顺序

你是谁 建议读法 预期收获
只会调 API 的开发者(没写过 Agent) 只读「序章」+「第一部分 s01--s04」+「终章」 搞懂 Agent 和普通 chatbot 的本质差别:一个循环
写过 Agent demo,想做生产级 通读全文,重点看 s03/s08/s11 三章的"真实源码对照" 知道教学 demo 和线上系统差在哪:权限、压缩、错误恢复
在做 Agent 框架 / 多智能体 重点看第二部分 s05--s10 与第四部分 s15--s19 上下文工程 + 多 Agent 通信协议的完整骨架
只想要速查手册 直接跳到文末「附录 A 常量速查表」「附录 B 20 章总表」 一张表带走所有阈值与默认值

一句话主线 :全书 20 章 = 在 s01 那 30 行 while True 上,挂了 19 个机制。循环本身从头到尾没变过。


目录

  • 序章:全书在讲什么
  • [第一部分 · 让 Agent 动起来(s01--s04)](#第一部分 · 让 Agent 动起来(s01–s04))
  • [第二部分 · 让 Agent 不迷失(s05--s10)](#第二部分 · 让 Agent 不迷失(s05–s10))
  • [第三部分 · 让 Agent 不崩、能等、能定时(s11--s14)](#第三部分 · 让 Agent 不崩、能等、能定时(s11–s14))
  • [第四部分 · 让 Agent 组队干活(s15--s19)](#第四部分 · 让 Agent 组队干活(s15–s19))
  • [终章 · s20 一个完整 Agent Turn](#终章 · s20 一个完整 Agent Turn)
  • [附录 A:常量速查表](#附录 A:常量速查表)
  • [附录 B:20 章总表](#附录 B:20 章总表)

序章:全书在讲什么

先讲一个具体场景

你对大模型说:"帮我看看目录下有哪些文件,然后执行 XXX.py。"

模型很爽快地吐出一条 ls -la。然后呢?然后它停了。

它不会自己去跑这条命令,也看不到 ls 的输出后再接着推理。于是你手动复制、粘贴回对话框,它再给你一条 python XXX.py,你再跑一遍、再粘回去。

每一个来回,你都在充当"人肉胶水"。

所谓 Agent,本质上就是把这层胶水写成代码。而写出来的东西,出人意料地短------30 行。

全书唯一的骨架:那 30 行

python 复制代码
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":
                output = run_bash(block.input["command"])
                results.append({"type": "tool_result",
                                "tool_use_id": block.id, "content": output})
        messages.append({"role": "user", "content": results})   # 喂回去,继续

这 30 行里藏着全书最重要的分工:

模型负责决策 (要不要调工具、调哪个),harness 负责执行(调了就跑、结果喂回去)。

后面 19 章,全部是在这个循环上"挂钩子",循环本身一行没改。

全书分层地图

#mermaid-svg-C6mmsmOLOURbC0nL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-C6mmsmOLOURbC0nL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-C6mmsmOLOURbC0nL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-C6mmsmOLOURbC0nL .error-icon{fill:#552222;}#mermaid-svg-C6mmsmOLOURbC0nL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-C6mmsmOLOURbC0nL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-C6mmsmOLOURbC0nL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-C6mmsmOLOURbC0nL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-C6mmsmOLOURbC0nL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-C6mmsmOLOURbC0nL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-C6mmsmOLOURbC0nL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-C6mmsmOLOURbC0nL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-C6mmsmOLOURbC0nL .marker.cross{stroke:#333333;}#mermaid-svg-C6mmsmOLOURbC0nL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-C6mmsmOLOURbC0nL p{margin:0;}#mermaid-svg-C6mmsmOLOURbC0nL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-C6mmsmOLOURbC0nL .cluster-label text{fill:#333;}#mermaid-svg-C6mmsmOLOURbC0nL .cluster-label span{color:#333;}#mermaid-svg-C6mmsmOLOURbC0nL .cluster-label span p{background-color:transparent;}#mermaid-svg-C6mmsmOLOURbC0nL .label text,#mermaid-svg-C6mmsmOLOURbC0nL span{fill:#333;color:#333;}#mermaid-svg-C6mmsmOLOURbC0nL .node rect,#mermaid-svg-C6mmsmOLOURbC0nL .node circle,#mermaid-svg-C6mmsmOLOURbC0nL .node ellipse,#mermaid-svg-C6mmsmOLOURbC0nL .node polygon,#mermaid-svg-C6mmsmOLOURbC0nL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-C6mmsmOLOURbC0nL .rough-node .label text,#mermaid-svg-C6mmsmOLOURbC0nL .node .label text,#mermaid-svg-C6mmsmOLOURbC0nL .image-shape .label,#mermaid-svg-C6mmsmOLOURbC0nL .icon-shape .label{text-anchor:middle;}#mermaid-svg-C6mmsmOLOURbC0nL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-C6mmsmOLOURbC0nL .rough-node .label,#mermaid-svg-C6mmsmOLOURbC0nL .node .label,#mermaid-svg-C6mmsmOLOURbC0nL .image-shape .label,#mermaid-svg-C6mmsmOLOURbC0nL .icon-shape .label{text-align:center;}#mermaid-svg-C6mmsmOLOURbC0nL .node.clickable{cursor:pointer;}#mermaid-svg-C6mmsmOLOURbC0nL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-C6mmsmOLOURbC0nL .arrowheadPath{fill:#333333;}#mermaid-svg-C6mmsmOLOURbC0nL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-C6mmsmOLOURbC0nL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-C6mmsmOLOURbC0nL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-C6mmsmOLOURbC0nL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-C6mmsmOLOURbC0nL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-C6mmsmOLOURbC0nL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-C6mmsmOLOURbC0nL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-C6mmsmOLOURbC0nL .cluster text{fill:#333;}#mermaid-svg-C6mmsmOLOURbC0nL .cluster span{color:#333;}#mermaid-svg-C6mmsmOLOURbC0nL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-C6mmsmOLOURbC0nL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-C6mmsmOLOURbC0nL rect.text{fill:none;stroke-width:0;}#mermaid-svg-C6mmsmOLOURbC0nL .icon-shape,#mermaid-svg-C6mmsmOLOURbC0nL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-C6mmsmOLOURbC0nL .icon-shape p,#mermaid-svg-C6mmsmOLOURbC0nL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-C6mmsmOLOURbC0nL .icon-shape .label rect,#mermaid-svg-C6mmsmOLOURbC0nL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-C6mmsmOLOURbC0nL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-C6mmsmOLOURbC0nL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-C6mmsmOLOURbC0nL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} s01 Agent Loop

30 行 while True

(全书地基,永不改变)
执行层 s02--s04

工具分发 · 权限 · Hooks
上下文层 s05--s10

计划 · 子Agent · 技能 · 压缩 · 记忆 · Prompt
韧性/调度层 s11--s14

错误恢复 · 任务图 · 后台 · 定时
协作层 s15--s19

团队 · 协议 · 自治 · 隔离 · MCP
s20 综合

所有机制挂回同一个循环

代码量增长曲线(能直观看出"复杂度花在哪"):

章节 s01 s05 s08 s12 s15 s18 s20
LOC 102 236 414 297 745 802 1708
工具数 1 6 9 8 14 18 32

注意 s08→s12 的回落:教程为了讲清单点,每章会刻意精简掉前面章节的某些机制。真实 Agent 不会这样拆开跑------这正是 s20 存在的理由。


第一部分 · 让 Agent 动起来(s01--s04)

这一层回答:模型想干活,怎么让它真的干到,且不闯祸。

s01 · Agent Loop ------ "One loop & Bash is all you need"

Harness 层:循环 ------ 模型与真实世界的第一道连接。

解决什么:上面说的"人肉胶水"问题。

怎么做 :整个循环只有两个信号 ,靠 stop_reason 二分:

信号 含义 循环动作
stop_reason == "tool_use" 模型举手说"我要用工具" 执行 → 结果喂回去 → 继续
stop_reason != "tool_use" 模型说"我做完了" 退出循环

唯一的数据结构 就是 messages 这个不断追加的数组。工具结果以新的 user 消息身份回到数组里,循环继续。

📌 真实 Claude Code 差在哪(基于 src/query.ts,1729 行)
教学版 真实 CC
看 response.stop_reason 不看 。stop_reason 在流式响应里不可靠(可能还没更新,但内容里已有 tool_use 块)。CC 用 needsFollowUp 布尔标志(query.ts:554-558、:830-834),源码注释直接写着 // stop_reason === 'tool_use' is unreliable.
只用 messages State 对象 10 个字段 :messages、toolUseContext(s02)、autoCompactTracking(s08)、maxOutputTokensRecoveryCount(上限 3,s11)、hasAttemptedReactiveCompact(s08)、maxOutputTokensOverride(8K→64K,s11)、pendingToolUseSummary(s08)、stopHookActive(s04)、turnCount、transition(s11)
1 条退出路径 十几条退出/继续路径:blocking limit、prompt too long、model error、abort、hook stop、max turns、token budget continuation、reactive compact retry......
串行执行工具 StreamingToolExecutor(query.ts:561):模型还在生成时就开始并行执行

一句话 :1729 行的 query.ts,核心就是那 30 行 while True。所有复杂字段和退出路径都是保护机制。


s02 · Tool Use ------ "加一个工具,只加一行"

Harness 层:工具分发 ------ 扩展模型能触达的边界。

解决什么 :只有 bash 时,模型想"读这个文件",却必须先翻译 成 cat path/to/file。多一层翻译 = 多烧 token + 更容易拼错。

怎么做 :s01 的循环一行没改 ,只把硬编码的 run_bash() 换成查表分发:

python 复制代码
TOOL_HANDLERS = {"bash": run_bash, "read_file": run_read,
                 "write_file": run_write, "edit_file": run_edit, "glob": run_glob}

for block in response.content:
    if block.type == "tool_use":
        handler = TOOL_HANDLERS[block.name]   # ← 唯一的改动
        output = handler(**block.input)
        results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})

加一个工具 = 两件事 :① TOOLS 数组加一条 JSON Schema(告诉模型"我能做什么");② TOOL_HANDLERS 字典加一行映射。

⚠️ 留下的坑 :safe_path 只保护 file tools,bash 完全裸奔 → 引出 s03。

📌 真实 CC 差在哪
  • isConcurrencySafe(input) 按具体输入判断,不是"只读 vs 写" :FileRead/Glob 都 true;Bash ls true 但 Bash rm false ;TaskCreate 是 false/true------它改状态,但每次写不同文件,所以可以并发。
  • 分区算法 partitionToolCalls()(toolOrchestration.ts:91-115)不是简单分两组,而是按连续 block 分批 :
    [read A, read B, glob *.py, bash "rm x", read C] → batch1(并发) → batch2(串行: rm) → batch3(并发: read C)。batch 内并发(有上限),batch 间严格串行。
  • 5 步验证管线 (toolExecution.ts):Zod schema 验证 → validateInput() → PreToolUse hooks → 权限检查 → tool.call()。
  • maxResultSizeChars :结果超阈值落盘,模型只看预览 + 路径。而 FileRead 特设为 Infinity------否则会出现"读文件结果被落盘 → 模型再读那个落盘文件 → 再落盘"的无限循环。这种"一个常量背后的血泪史",是全书最有价值的部分之一。

s03 · Permission ------ 安全不能靠信任模型,要靠代码

Harness 层:权限 ------ 在工具执行前加一道门。

解决什么 :你让它"清理一下项目",它可能真的执行 rm -rf /。

怎么做 :三道闸门,顺序固定 ------ 硬拒绝优先,软询问次之,都没命中就放行(日常操作大多走最后这条):

python 复制代码
def check_permission(block) -> bool:
    if block.name == "bash":                       # 闸门1: 硬拒绝
        reason = check_deny_list(block.input.get("command", ""))
        if reason: print(f"\n⛔ {reason}"); return False
    reason = check_rules(block.name, block.input)  # 闸门2: 规则匹配
    if reason:                                     # 闸门3: 用户审批
        if ask_user(block.name, block.input, reason) == "deny":
            return False
    return True
  • 闸门 1 :DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if=", "> /dev/sda"]------教程自己标注了:简单子串匹配不是可靠安全机制,命令变体和 shell 展开能绕过。
  • 闸门 2 :写类工具检查路径是否 is_relative_to(WORKDIR);bash 检查 ["rm ", "> /etc/", "chmod 777"]。
  • 闸门 3 :input(" Allow? [y/N] ") ------ 默认拒绝,回车或任何非 y/yes 都回落 deny。
  • 被拒绝时不是崩溃 ,而是把 "Permission denied." 当 tool_result 喂回模型,让它自己想别的办法。
📌 真实 CC 差在哪
教学版 真实 CC
3 种结果 4 种 :allow / deny / ask / passthrough(工具不表态,交给通用管线)
1 个 deny list 规则来自 8 个来源 (types/permissions.ts:54-62):user / project / local / flag / policy / cliArg / command / session,优先级 user < project < local < flag < policy
手搓校验 6 阶段管线(toolExecution.ts:599-1745),含 backfillObservableInput()、resolveHookPermissionDecision();且「内容相关 ask 规则」与「安全检查违规」不可绕过
简单子串匹配 YoloClassifier:auto 模式不每次弹窗,用分类器 LLM 判断;连续拒绝太多次则回退人工
--- 权限冒泡 :子 Agent 的 permissionMode: 'bubble'(forkSubagent.ts:50),弹窗冒泡到父终端,而不是在子进程里静默拒绝

⚠️ 一个反直觉点:isDestructive()(Tool.ts:405-406)纯 UI 展示 ------只在工具列表显示 [destructive] 标签,不参与权限决策。


s04 · Hooks ------ "挂在循环上,不写进循环里"

Harness 层:hook ------ 扩展点不侵入循环。

解决什么 :每加一个检查就要改 agent_loop 本体:log_to_file()、check_permission()、notify_slack()、auto_git_add()......很快循环就认不出来了。教程的总结一针见血:

你想扩展的是 Agent 的行为,但你改的却是循环本身。循环应该是稳定的核心,扩展应该挂在外面。

怎么做 :注册表 + 触发器,短路约定:回调返回非 None 就立即中断后续。

python 复制代码
def trigger_hooks(event: str, *args):
    for callback in HOOKS[event]:
        result = callback(*args)
        if result is not None:   # ≠ None → hook 说"停"
            return result
    return None

四个事件覆盖一个完整 agent cycle:

事件 触发时机 返回非 None 的含义
UserPromptSubmit 用户输入后、进 LLM 前 教学版不使用
PreToolUse 工具执行前 阻止本次执行(该值作为 tool_result 喂回模型)
PostToolUse 工具执行后 教学版不使用
Stop 循环即将退出时 强制续跑(作为新 user 消息追加并 continue)

循环里唯一的改动,就是把 s03 的 check_permission(block) 换成 trigger_hooks("PreToolUse", block)。

📌 真实 CC 差在哪
  • 不是 4 个事件,是 27 个 (coreTypes.ts:25-53):涵盖会话(SessionStart/SessionEnd/Stop)、交互(Notification/PermissionRequest)、子 Agent(SubagentStart/Stop)、压缩(PreCompact/PostCompact)、团队(TeammateIdle/TaskCreated/TaskCompleted)等。
  • HookResult 14 个字段 (types/hooks.ts:260-275),常用的有 blockingError(注入对话让模型自纠)、permissionBehavior、updatedInput(直接修改工具输入 )、additionalContext。
  • 🔒 最关键的安全不变式 (toolHooks.ts:325-331):hook 返回 allow 时,仍然要检查 settings.json 的 deny/ask 规则。即使用户自己的 hook 脚本说"允许",只要在 settings.json 里禁用了该工具,依然被阻止。教学版没这层,教程直言"在生产环境中会形成安全漏洞"。
  • 🔁 stopHookActive 防无限循环 (query.ts:212,1300):Stop hook 产生 blockingError 时下一轮带此标志重入,后续迭代 Stop hook 就不再触发------避免"自纠→报错→再自纠→再报错"永不停机。

第二部分 · 让 Agent 不迷失(s05--s10)

这一层回答:对话越来越长、任务越来越复杂,怎么让它不跑偏、不忘事、不爆上下文。 这是全书的精华。

s05 · TodoWrite ------ 没有计划的 Agent 走哪算哪

解决什么 :任务"把所有 Python 文件改成 snake_case,然后跑测试,修好失败"。Agent 改了 3 个文件、跑测试、发现 2 个失败就开始修------修着修着忘了最初目标是重命名。对话越长越严重:工具结果不断填进上下文,系统提示的影响力被稀释。

怎么做 :新增 todo_write 工具,走完全不变的 dispatch 机制。它只是把列表存进全局变量并渲染到终端:

python 复制代码
def run_todo_write(todos: list) -> str:
    global CURRENT_TODOS
    CURRENT_TODOS = todos
    for t in CURRENT_TODOS:
        icon = {"pending": " ", "in_progress": "▸", "completed": "✓"}[t["status"]]
        print(f"  [{icon}] {t['content']}")
    return f"Updated {len(CURRENT_TODOS)} tasks"

关键洞察 :todo_write 不给 Agent 增加任何执行能力 。它增加的是规划能力。

Nag reminder :循环里计数,rounds_since_todo >= 3 时追加 <reminder>Update your todos.</reminder>。教程明确标注:这是教学机制,CC 源码中没有这个固定轮数逻辑。

📌 真实 CC:两套任务系统并存

isTodoV2Enabled() 切换------交互式会话默认 Task System(V2,即 s12) ,非交互/SDK 默认 TodoWrite(V1) ;环境变量 CLAUDE_CODE_ENABLE_TASKS 可强制开 V2。Task System 相对 TodoWrite 的五项增量:文件持久化、依赖图、proper-lockfile 并发安全、四个独立工具、生命周期 hooks。


s06 · Subagent ------ 大任务拆小,每个小任务干净的上下文

解决什么 :Agent 修 bug 读了 30 个文件、聊了 60 轮,messages 涨到 120 条,中间过程占着上下文,它越来越健忘。

怎么做 :新增 task 工具。子 Agent 拿到全新的 messages ,配独立的 SUB_SYSTEM,最多跑 30 轮 safety limit,结束后只回传结论。

python 复制代码
def spawn_subagent(description: str) -> str:
    sub_tools = [{"name":"bash",...}, ...]              # 注意:没有 task 工具
    messages = [{"role": "user", "content": description}]  # 全新 messages[]
    for _ in range(30):                                  # safety limit
        ...
    return extract_text(messages[-1]["content"])         # 只回传结论

三个设计取舍:

  1. 上下文隔离 ------ 中间过程不污染主上下文
  2. 只回传结论 ------ 不是整个 messages 列表
  3. 禁止递归 ------ 子 Agent 工具集是 bash/read/write/edit/glob,没有 task

🔑 一个容易踩的坑:上下文隔离 ≠ 权限隔离 。子 Agent 的工具调用仍走 PreToolUse/PostToolUse hook;写文件、跑命令的副作用依然留在工作目录。

教程类比 :"你修 bug 的时候,会开一个新终端来追踪调用链。追踪完了,终端关掉,结果写进笔记,回到原来的终端继续修 bug。"

📌 真实 CC 差在哪
  • 不是一种模式,是三种:Normal Subagent、Fork Subagent、General-Purpose。
  • Fork 模式是为了共享 prompt cache (forkSubagent.ts:60-71):forkedAgent.ts:57-68 要求 system prompt / tools / model / messages 前缀 / thinking config 五者字节级一致才能命中缓存。
  • 隔离的精确粒度 (createSubagentContext()):abortController 是新的 child controller,但 readFileState 从父克隆(文件读取状态共享)。
  • 递归防护不是"不给工具" :靠 isInForkChild() 检查历史里有无 FORK_BOILERPLATE_TAG;constants/tools.ts:36-46 中 Agent 默认在被禁集合。

s07 · Skills ------ 用到时再加载,别全塞 prompt 里

解决什么 :React 规范 2000 行 + SQL 指南 1500 行 + API 文档 3000 行,全拼进 SYSTEM 就是 6500 行。改个 CSS 颜色也带着 SQL 文档,99% 无关,纯烧 token。

怎么做 :两级加载。

级别 内容 成本 存活周期
第一级 · 目录 name + description 清单注入 SYSTEM ~100 tokens/skill 每轮都带
第二级 · 内容 load_skill(name) 查注册表返回正文 ~2000 tokens/skill 作为 tool_result 进 messages,直到 compact
python 复制代码
def _scan_skills():                      # 启动时扫描 skills/*/SKILL.md
    for d in sorted(SKILLS_DIR.iterdir()):
        meta, body = _parse_frontmatter((d / "SKILL.md").read_text())
        SKILL_REGISTRY[meta.get("name", d.name)] = {"description": ..., "content": raw}

def load_skill(name: str) -> str:        # 查表,不是拼路径 → 无路径遍历风险
    skill = SKILL_REGISTRY.get(name)
    return skill["content"] if skill else f"Skill not found: {name}"

金句 :"按需加载解决了'不该提前带的不要带',compact 解决'该丢的怎么丢'。" ------ s07 管入口 ,s08 管出口。

📌 真实 CC 差在哪
  • 技能来源不是一个目录 :user(~/.claude/skills/)、project(.claude/skills/)、--add-dir、legacy commands、bundled、plugin、MCP 远程技能,还有 conditional skills (带 paths frontmatter,按当前文件路径激活)。
  • frontmatter 字段 :name/description/when_to_use/allowed-tools/context(inline|fork)/model/hooks/paths/user-invocable。
  • 目录预算 :约上下文窗口的 1%,上限 8000 字符。
  • 🔑 一个教学版的错误简化 :SkillTool 返回的 tool_result 文本其实只是 "Launching skill: {name}",真正内容通过 newMessages 注入对话。教学版把两者合并了。

s08 · Context Compact ------ 上下文总会满,要有办法腾地方

解决什么 :读了 30 个文件、跑了 20 条命令,全堆在 messages 里,上下文窗口满了 API 直接报 prompt_too_long。

怎么做 :核心设计一句话------便宜的先跑,贵的后跑。每轮 LLM 调用前跑三个 0-API 预处理器,超阈值才花 1 次 LLM 摘要。

层 手段 触发条件 动作
L1 snip_compact 消息数 > 50 保留头 3 + 尾 47,中间插 [snipped N messages...] 占位
L2 micro_compact KEEP_RECENT_TOOL_RESULTS = 3 除最近 3 条外,长度 > 120 字符的旧 tool_result 换成一行占位符
L3 tool_result_budget 单条 user 消息内 tool_result 总和 > 200000 字符 按大小降序落盘 .task_outputs/,留标记 + 前 2000 字符预览
L4 compact_history 上面还不够 先写 transcript JSONL,再 LLM 摘要,整体替换为一条 [Compacted];连续失败 3 次熔断

⚠️ 顺序不能换 :budget(L3) → snip(L1) → micro(L2) 。因为 micro 会把大 tool_result 变成一行占位符,budget 必须在之前把完整内容落盘,否则内容就丢了。

python 复制代码
KEEP_RECENT_TOOL_RESULTS = 3

def micro_compact(messages):
    tool_results = collect_tool_result_blocks(messages)
    if len(tool_results) <= KEEP_RECENT_TOOL_RESULTS:
        return messages
    for _, _, block in tool_results[:-KEEP_RECENT_TOOL_RESULTS]:
        if len(block.get("content", "")) > 120:
            block["content"] = "[Earlier tool result compacted. Re-run if needed.]"
    return messages

应急通道 reactive_compact :捕获 PromptTooLongError → 摘要 + 保留尾部 5 条消息(同样要避开孤立的 tool_result),重试上限 1 次。另有 compact 工具让模型主动调用。

📌 真实 CC 差在哪
  • 真实顺序 (query.ts:379-468):applyToolResultBudget(L379) → snipCompact(L403) → microcompact(L414) → contextCollapse(L441,教学版没有) → autoCompact(L454)。教学版 L1/L2/L3/L4 的编号与实际顺序不一致,读的时候别被编号误导。
  • 阈值算法不同 :教学版用字符数估算,CC 用精确 token = contextWindow - maxOutputTokens - 13_000。
  • 关键常量 :AUTOCOMPACT_BUFFER_TOKENS=13,000、MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES=3、MAX_OUTPUT_TOKENS_FOR_SUMMARY=20,000、POST_COMPACT_TOKEN_BUDGET=50,000、POST_COMPACT_MAX_FILES_TO_RESTORE=5、POST_COMPACT_MAX_TOKENS_PER_FILE=5,000、时间型 micro compact 间隔 60 分钟。
  • snip 仅主线程启用 ,实现不在开源仓库(HISTORY_SNIP feature gate);头 3 + 尾 47 是教学版参数。
  • 压缩 prompt 双防呆 :开头 CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.,末尾再来一遍 REMINDER;要求先 <analysis> 再 <summary>,analysis 在格式化时剥离。
  • 压缩后按预算恢复最近文件 (≤5 个 × 5K token,总 50K);重复读未变文件返回 FILE_UNCHANGED_STUB。

s09 · Memory ------ 压缩会丢细节,要有一层不丢的

解决什么 :autoCompact 会把"用 tab 缩进不要用空格"压缩成"用户有代码风格偏好"这种废话;新开会话连摘要都没了。LLM 无持久状态 ,需要一层不参与压缩、跨会话保留的存储。

怎么做:

  • 存储 :.memory/*.md,YAML frontmatter(name/description/type),正文里写 **Why:** / **How to apply:**。MEMORY.md 是索引,一行一个链接。四类:user (你是谁)、feedback (怎么做事)、project (正在发生什么)、reference(东西在哪找)。
  • 加载两条路径 :① 索引常驻 SYSTEM(可被 prompt cache 缓存);② select_relevant_memories() 做一次轻量 side-query (max_tokens=200,返回 JSON 数组下标),最多 5 条 注入当前 user turn;side-query 失败降级到关键词匹配。
  • 写入 :extract_memories() 只在 stop_reason != "tool_use"(一段对话告一段落)时跑,取 messages[-10:]、dialogue[:4000]。
  • 整理 :CONSOLIDATE_THRESHOLD = 10,文件数达标才让 LLM 去重合并。

设计要点 :索引常驻 SYSTEM prompt(可被 prompt cache 缓存),文件内容按需注入到当前 user turn ------ 按 filename/description 匹配当前对话,不破坏 cache。

📌 真实 CC 差在哪
  • 选记忆用的是 Sonnet 本身,不是 embedding (findRelevantMemories.ts:18-24),最多 5 个,不确定就不选。
  • 注入预算 (utils/attachments.ts:269-288):每文件 ≤200 行 / 4096 字节 ,单 session 总 60KB ;MEMORY.md 上限 200 行 / 25KB。
  • 提取在 stop hook 里 fire-and-forget ,实际执行是 forked agent :受限权限、skipTranscript: true、maxTurns: 5,且有重叠保护(主 Agent 已写过就跳过)。
  • Dream(自动整理)四层门控 :时间 ≥ 24 小时 / 扫描节流 / 会话 ≥ 5 个 transcript / .consolidate-lock 文件锁(1 小时后自动过期)。

s10 · System Prompt ------ prompt 是组装出来的,不是写死的

解决什么 :s01 的 SYSTEM = f"You are a coding agent at {WORKDIR}." 到 s09 已经要追加 todo、skills、memories 一堆指令。三个痛点:换项目要重写整个 prompt、改一处影响全局、每次请求带全部内容浪费 token。

怎么做 :PROMPT_SECTIONS 字典分段 + assemble_system_prompt(context) 按需拼接 + 缓存。

python 复制代码
def get_system_prompt(context: dict) -> str:
    global _last_context_key, _last_prompt
    key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
    if key == _last_context_key and _last_prompt:
        return _last_prompt                      # 命中缓存,避免重复拼字符串
    _last_context_key = key
    _last_prompt = assemble_system_prompt(context)
    return _last_prompt

四条原则:

  1. 四个 section 两种策略:identity / tools / workspace 始终加载 ,memory 按需 (依据 .memory/MEMORY.md 是否存在)。
  2. section 是否加载取决于真实状态 (工具是否注册、文件是否存在),不是在消息里搜关键词。
  3. 缓存 key 用 json.dumps(...) 而不是 hash() ------Python 内置 hash() 有进程随机化,且遇到 list/dict 会报 unhashable type。
  4. 注意这只是"避免重复拼字符串",和 CC 的 API prompt cache 不是一回事。

为什么不全加载? 两条理由:token 有成本(system prompt 每轮计费);信息越少 LLM 越专注(无关指令是噪音)。

📌 真实 CC 差在哪
  • section 数量不固定 ,受 feature flag、output style、用户类型、token 预算影响。静态 :identity、system、doing_tasks、actions、using_tools、tone_style、output_efficiency;动态 :session_guidance、memory、language、output_style、mcp_instructions、scratchpad、token_budget......
  • mcp_instructions 是唯一易失性 section,因为 MCP server 可在轮次之间连接/断开。
  • 三层缓存 (教学版只有一层):① lodash memoize 缓存 getSystemContext/getUserContext;② STATE.systemPromptSectionCache 缓存动态 section,/clear 或 /compact 时清除;③ API 层 splitSysPromptPrefix() 按 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 分块到不同 cache scope。
  • 规模 :标准交互模式 system prompt 核心约 20--30KB ;CLAUDE_CODE_SIMPLE 约 150 字符(整个 prompt 2 行)。

第三部分 · 让 Agent 不崩、能等、能定时(s11--s14)

这一层回答:现实世界会报错、会慢、需要定时,怎么扛。

s11 · Error Recovery ------ 错误不是终点,是重试的起点

解决什么 :Agent 跑到一半报个 Error: 529 overloaded 就整体崩溃------没重试、没换模型、没精简上下文。

教程原话 :"一个不处理错误的 Agent 就像一个一碰就熄火的车。"

怎么做 :把 LLM 调用包进 try/except,按错误类型分三条恢复路径,恢复后一律 continue 回到循环开头重调。

路径 触发 动作 上限
1. 输出截断 stop_reason == "max_tokens" 不把截断输出追加进 messages ,把 max_tokens 从 8000 抬到 ESCALATED_MAX_TOKENS = 64000(8 倍)重发;还不够才 append + CONTINUATION_PROMPT 续写 升级只有一次机会,续写最多 3 次
2. 上下文超限 PromptTooLongError reactive_compact(messages) has_attempted_reactive_compact 只给一次机会
3. 瞬态故障 429 / 529 with_retry() 指数退避 + 抖动;连续 3 次 529 且有 FALLBACK_MODEL 则切模型 MAX_RETRIES = 10
python 复制代码
def retry_delay(attempt, retry_after=None):
    if retry_after:
        return retry_after
    base = min(500 * (2 ** attempt), 32000) / 1000
    return base + random.uniform(0, base * 0.25)   # +25% 抖动,防重试对齐

取舍背后的判断 :"超过就退出------继续续写也不会有实质产出" ;"压缩过了还是超限,只能退出------再压缩也不会变小。" 知道什么时候该放弃,和知道什么时候该重试一样重要。

📌 真实 CC 差在哪
  • 不是 3 条,是十几条 reason/transition :completed、next_turn、max_output_tokens_escalate、max_output_tokens_recovery、reactive_compact_retry、prompt_too_long、collapse_drain_retry、model_error、image_error、aborted_streaming、aborted_tools、stop_hook_blocking、stop_hook_prevented、hook_stopped、token_budget_continuation、blocking_limit、max_turns。
  • 退避精确公式 (withRetry.ts:530-548):min(500 × 2^(attempt-1), 32000) + random(0~25%),第 1/2/4/7+ 次分别 500ms/1000ms/4000ms/32000ms。
  • 续写提示原文 (query.ts:1225-1227):"Output token limit hit. Resume directly --- no apology, no recap of what you were doing. Pick up mid-thought if that is where the cut happened. Break remaining work into smaller pieces."
  • Diminishing Returns 检测 (tokenBudget.ts:60-62):连续 3 次 continuation 且 token 增量 < 500 就停。

s12 · Task System ------ 大目标拆成小任务,排好序,持久化

解决什么:任务"搭数据库 + 写 API + 加测试",Agent 列完清单就开始写 API,写到一半发现没表、回头补,加测试时接口签名又变了。

金句 :"盖房子不能先盖屋顶再打地基。" 任务之间有先后,依赖应构成有向无环图(DAG) 。而 s05 的 TodoWrite 是当前会话内存里的执行清单。
另一句 :"TODO 列表不是任务系统。"

怎么做 :每个任务一个 JSON 文件,落在 .tasks/{id}.json。

python 复制代码
@dataclass
class Task:
    id: str              # "task_{timestamp}_{random_hex(4)}"
    subject: str
    description: str
    status: str          # pending | in_progress | completed
    owner: str | None    # Agent 名,多 Agent 防重复认领
    blockedBy: list[str] # 上游任务 ID 列表
  • can_start(task_id) :遍历 blockedBy,任一依赖文件不存在或状态不是 completed 即 False。缺失依赖视为 blocked------避免引用错 ID 时崩溃。
  • claim_task :先查 status != "pending" → 拒绝(已被认领);再查 can_start → 返回 "Blocked by: [...];通过才设 owner 转 in_progress。
  • 状态机 :pending --claim--> in_progress --complete--> completed,没有回退路径。(队友异常终止时的状态回滚,是 s15 之后才补的。)
TodoWrite (s05) Task System (s12)
定位 当前任务执行清单 可恢复的任务系统
存储 进程内 / 会话状态 .tasks/{id}.json
依赖 无 blockedBy / blocks 依赖图
生命周期 当前会话 跨会话保留
分工 不认领 owner / claim
📌 真实 CC 差在哪
  • TaskRecord 是 9 个字段 (教学版 5 个):多出 activeForm(进行时态,spinner 显示)、blocks(下游)、metadata。
  • 不是"TodoWrite 升级版",是两个并列系统 ,靠 isTodoV2Enabled() 切换。
  • 双重锁防并发抢认领 (tasks.ts:541-612):proper-lockfile 锁 {taskId}.json,最多重试 30 次 、指数退避 5--100ms ;锁内重新读取防 TOCTOU。
  • 🔑 关键差异 :真实 claimTask 只设 owner 不改 status ,状态更新交给 TaskUpdate;教学版把 claim 和开工合成一步。
  • 四个工具均 isConcurrencySafe: true + shouldDefer: true(schema 不进初始 prompt,需 ToolSearch 后才可见)。

s13 · Background Tasks ------ 慢操作丢后台,Agent 继续处理

教程类比 :"你用过洗衣机吗?把衣服扔进去,按启动,然后去干别的......你不会站在洗衣机前面干等 30 分钟。"

解决什么 :pip install torch 十分钟、npm run build 三分钟。同步执行时 Agent 干等十分钟什么都不做------而 LLM 按 token 计费,空转就是烧钱。

怎么做:

python 复制代码
def should_run_background(tool_name: str, tool_input: dict) -> bool:
    """Model explicit request takes priority; fallback to heuristic."""
    if tool_input.get("run_in_background"):
        return True                                  # 模型显式请求优先
    return is_slow_operation(tool_name, tool_input)  # 关键词启发式(仅 bash)

关键词白名单:install/build/test/deploy/compile/docker build/pip install/npm install/cargo build/pytest/make,注释写明 "> 30s"。

  • 启动 :threading.Thread(daemon=True),全局 background_tasks / background_results 配 threading.Lock(),返回递增 ID bg_{counter:04d}。daemon=True 保证主进程退出时线程跟着死。
  • 回收 :扫出 status == "completed" 的 ID,pop 掉并格式化成 <task_notification> XML(含 <task_id>/<status>/<command>/<summary>,summary 截前 200 字符)。
  • 🔑 不复用原 tool_use_id :一个 tool_use 只能配对一个 tool_result,后台完成是独立事件。
  • 循环集成 :慢操作立即回占位 tool_result: "[Background task bg_0001 started]...",本轮结束时 <task_notification> 与同期 tool_result 合并成一条 user 消息的多个 block。
📌 真实 CC 差在哪
  • 没有真正的线程 :Node/Bun 单线程事件循环,"后台" = 不 await ,ShellCommand.background(taskId) 把 stdout/stderr 重定向到文件让子进程独立跑。
  • pendingToolUseSummary :Haiku 后台生成摘要(query.ts:1411-1482),prompt 要求 "think git-commit-subject, not sentence",过去时态、约 30 字符。Haiku ~1s 在主模型流式 5--30s 期间跑完,供 UI 展示进度。
  • 7 种后台任务类型 (Task.ts:7-13):local_bash、local_agent、remote_agent、in_process_teammate、local_workflow、monitor_mcp、dream。
  • 停滞看门狗 :45 秒输出无增长后检测 (y/n) 等交互式提示,防后台任务卡在无人应答的对话框。
  • 并发限制 :前台 CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY 默认 10;后台 bash 无硬限制。

s14 · Cron Scheduler ------ 按时间表生产工作,调度与执行解耦

金句 :"闹钟不需要你盯着它才会响。"

解决什么:s13 解决了慢操作不阻塞,但所有操作仍由人手动推------你说一句、Agent 动一下。"每天早上 9 点跑测试"不该每次靠人。

怎么做 :四层解耦模型。
#mermaid-svg-CFYF2f8vDSYzjnut{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CFYF2f8vDSYzjnut .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CFYF2f8vDSYzjnut .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CFYF2f8vDSYzjnut .error-icon{fill:#552222;}#mermaid-svg-CFYF2f8vDSYzjnut .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CFYF2f8vDSYzjnut .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CFYF2f8vDSYzjnut .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CFYF2f8vDSYzjnut .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CFYF2f8vDSYzjnut .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CFYF2f8vDSYzjnut .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CFYF2f8vDSYzjnut .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CFYF2f8vDSYzjnut .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CFYF2f8vDSYzjnut .marker.cross{stroke:#333333;}#mermaid-svg-CFYF2f8vDSYzjnut svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CFYF2f8vDSYzjnut p{margin:0;}#mermaid-svg-CFYF2f8vDSYzjnut .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CFYF2f8vDSYzjnut .cluster-label text{fill:#333;}#mermaid-svg-CFYF2f8vDSYzjnut .cluster-label span{color:#333;}#mermaid-svg-CFYF2f8vDSYzjnut .cluster-label span p{background-color:transparent;}#mermaid-svg-CFYF2f8vDSYzjnut .label text,#mermaid-svg-CFYF2f8vDSYzjnut span{fill:#333;color:#333;}#mermaid-svg-CFYF2f8vDSYzjnut .node rect,#mermaid-svg-CFYF2f8vDSYzjnut .node circle,#mermaid-svg-CFYF2f8vDSYzjnut .node ellipse,#mermaid-svg-CFYF2f8vDSYzjnut .node polygon,#mermaid-svg-CFYF2f8vDSYzjnut .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CFYF2f8vDSYzjnut .rough-node .label text,#mermaid-svg-CFYF2f8vDSYzjnut .node .label text,#mermaid-svg-CFYF2f8vDSYzjnut .image-shape .label,#mermaid-svg-CFYF2f8vDSYzjnut .icon-shape .label{text-anchor:middle;}#mermaid-svg-CFYF2f8vDSYzjnut .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CFYF2f8vDSYzjnut .rough-node .label,#mermaid-svg-CFYF2f8vDSYzjnut .node .label,#mermaid-svg-CFYF2f8vDSYzjnut .image-shape .label,#mermaid-svg-CFYF2f8vDSYzjnut .icon-shape .label{text-align:center;}#mermaid-svg-CFYF2f8vDSYzjnut .node.clickable{cursor:pointer;}#mermaid-svg-CFYF2f8vDSYzjnut .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CFYF2f8vDSYzjnut .arrowheadPath{fill:#333333;}#mermaid-svg-CFYF2f8vDSYzjnut .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CFYF2f8vDSYzjnut .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CFYF2f8vDSYzjnut .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CFYF2f8vDSYzjnut .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CFYF2f8vDSYzjnut .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CFYF2f8vDSYzjnut .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CFYF2f8vDSYzjnut .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CFYF2f8vDSYzjnut .cluster text{fill:#333;}#mermaid-svg-CFYF2f8vDSYzjnut .cluster span{color:#333;}#mermaid-svg-CFYF2f8vDSYzjnut div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CFYF2f8vDSYzjnut .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CFYF2f8vDSYzjnut rect.text{fill:none;stroke-width:0;}#mermaid-svg-CFYF2f8vDSYzjnut .icon-shape,#mermaid-svg-CFYF2f8vDSYzjnut .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CFYF2f8vDSYzjnut .icon-shape p,#mermaid-svg-CFYF2f8vDSYzjnut .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CFYF2f8vDSYzjnut .icon-shape .label rect,#mermaid-svg-CFYF2f8vDSYzjnut .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CFYF2f8vDSYzjnut .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CFYF2f8vDSYzjnut .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CFYF2f8vDSYzjnut :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 命中
Agent 空闲
跑完释放锁
① Scheduler

daemon 线程

每秒 sleep(1) 轮询

cron_matches?
② cron_queue

(队列)
③ Queue Processor

每 0.2s 检查

队列非空 且 拿到 agent_lock
④ Consumer

agent_loop

consume_cron_queue()

注入 Scheduled prompt

python 复制代码
@dataclass
class CronJob:
    id: str
    cron: str        # "0 9 * * *" 五段式
    prompt: str      # 触发时注入的消息
    recurring: bool  # True 周期性 / False 一次性(触发后自动 pop)
    durable: bool    # True 写 .scheduled_tasks.json 跨会话

三个容易写错的细节:

  1. cron_matches 的 DOM/DOW 语义 :分钟、小时、月必须全匹配;日(DOM)与星期(DOW)同时被约束时取 OR(Unix cron 的老规则)。
  2. 星期换算 :Python dt.weekday() 周一 = 0,cron 周日 = 0,需 dow_val = (dt.weekday() + 1) % 7。
  3. 防重复触发 :minute_marker = now.strftime("%Y-%m-%d %H:%M") 带日期,同分钟只发一次、跨天也不会漏。

⚠️ 重要前提 :调度器跑在 Agent 进程内,进程关了调度就停。Durable 只是定义跨重启保留,下次启动才发现"该补触发了"。要真正脱离进程请用系统 crontab / systemd timer。

📌 真实 CC 差在哪
  • 双重门控 :编译时 feature('AGENT_TRIGGERS') + 运行时 GrowthBook tengu_kairos_cron,另有 CLAUDE_CODE_DISABLE_CRON 本地覆盖。
  • 防惊群抖动 :重复任务延迟最多达周期的 10% (上限 15 分钟,基于任务 ID 的确定性哈希);一次性任务若落在 :00 或 :30,最多提前 90 秒。
  • 自动过期 :重复任务 7 天后过期(可配,上限 30 天),末次触发后自动删除。
  • 作业上限 MAX_JOBS = 50 (CronCreateTool.ts:25)。
  • QoS 降级 :cron 触发的请求标记 workload: WORKLOAD_CRON------API 在容量紧张时对 cron 请求降优先级。
  • .scheduled_tasks.lock 防止同项目多 session 重复触发;支持 *、*/N、N-M、N-M/S、N,M,...,不支持 L、W、?。

第四部分 · 让 Agent 组队干活(s15--s19)

这一层回答:一个 Agent 的注意力覆盖不了大项目,怎么让多个 Agent 协作而不打架。

s15 · Agent Teams ------ 一个搞不定,组队来

解决什么 :"重构整个后端"横跨认证、数据库、API 路由、测试------一个 Agent 在修 API 路由时,认证模块的细节已经不在上下文里了 。而 s06 的子 Agent 是临时工,叫来干一件事就走了,做不了需要持续通信的协作。

怎么做:三样新东西。

python 复制代码
class MessageBus:
    def send(self, from_agent, to_agent, content, msg_type="message"):
        msg = {"from": from_agent, "to": to_agent,
               "content": content, "type": msg_type, "ts": time.time()}
        with open(MAILBOX_DIR / f"{to_agent}.jsonl", "a") as f:   # 文件收件箱
            f.write(json.dumps(msg) + "\n")

    def read_inbox(self, agent: str) -> list[dict]:
        inbox = MAILBOX_DIR / f"{agent}.jsonl"
        msgs = [json.loads(l) for l in inbox.read_text().splitlines()]
        inbox.unlink()          # 消费式:读完删除
        return msgs
  • MessageBus :每个 Agent(含 Lead)一个 .mailboxes/{name}.jsonl。发送 = 往对方文件 append 一行 JSON;读取 = 读全文后 unlink(),消费式删除。选文件而非内存队列的理由:直观、跨线程可观察。
  • spawn_teammate_thread(name, role, prompt) :队友跑在自己的 daemon 线程里,有独立 system prompt("You are '{name}', a {role}.")、独立 messages、精简工具集(bash/read/write/send_message 共 4 个) ,每轮先把 inbox 包成 <inbox>{json}</inbox> 注入,教学版硬限 10 轮,跑完自动汇报。
  • Lead 侧注入 :主循环每轮结束读自己的 inbox,截断到 200 字符拼成 [Inbox]\nFrom {from}: {content} 追加进 history。
s06 子 Agent s15 队友
生命周期 一次性,用完销毁 多轮(教学版限 10 轮,CC 用 idle loop)
通信 只回传结论 异步收件箱,随时互发
上下文 完全隔离 通过消息共享
数量 1 主 + 偶尔子 Agent 1 Lead + N 队友
📌 真实 CC 差在哪
  • 没有中央总线,就是文件系统 :路径 ~/.claude/teams/{teamName}/inboxes/{agentName}.json(注意是 JSON 数组不是 JSONL ),写入用 proper-lockfile,最多重试 10 次,append 语义是读→追加→写回。
  • 15 种结构化消息类型 :idle_notification、permission_request/permission_response、plan_approval_request/plan_approval_response、shutdown_request/shutdown_approved/shutdown_rejected、task_assignment、teammate_terminated 等。文本消息包在 <teammate-message> XML 标签里交付。
  • 权限冒泡 (教学版省略):队友遇审批 → permission_request → Lead 的 useInboxPoller(每 1 秒 )→ UI 弹窗 → 用户审批 → permission_response → 队友 useSwarmPermissionPoller(每 500ms)收到后继续。
  • 队友不能嵌套 :AgentTool.tsx:273 明令禁止 "teammates spawning other teammates"。
  • ⚠️ 教学版 read_inbox 存在 read + unlink 竞态(多线程同读会丢消息)。

s16 · Team Protocols ------ 队友之间要有约定

解决什么 :s15 的协调是松散文本消息。两个场景暴露缺陷:关机 时直接杀线程,Alice 写了一半的文件留在磁盘;计划审批 时 Bob 要重构认证模块,Lead 完全不知情。两个场景结构相同------一方发请求、另一方回复,靠同一个 ID 关联。

怎么做 :把"发消息---收回复"升级成带 request_id 关联、有状态机追踪的请求-响应协议。

python 复制代码
def match_response(response_type, request_id, approve):
    state = pending_requests.get(request_id)
    if not state: return
    if state.type == "shutdown" and response_type != "shutdown_response": return        # 类型校验
    if state.type == "plan_approval" and response_type != "plan_approval_response": return
    if state.status != "pending": return          # 幂等:已决,跳过重复响应
    state.status = "approved" if approve else "rejected"

金句 :「request_id 是贯穿全链路的关联键,请求带着它出去,回复带着它回来。」「一个 shutdown_response 不会意外 approve 一个 plan_approval 请求。」

配套改动:队友生命周期改为 idle loop ------LLM 返回非 tool_use 后不退出,每秒轮询 inbox;consume_lead_inbox() 统一消费函数让 check_inbox 工具和主循环共用,避免"消息被读走但状态没更新"。

📌 真实 CC 差在哪
  • 关机是三向通信 (teammateMailbox.ts:720-763):shutdown_request → shutdown_approved/shutdown_rejected → 系统广播 teammate_terminated,随后自动清理 pane、unassign 任务、从 team config 移除成员。教学版合并成单一 shutdown_response + approve 字段。
  • 计划审批 由 ExitPlanModeV2Tool.ts:263-312 在 plan-mode-required 队友退出 plan mode 时产生;响应时支持同时设 permissionMode("批准但以 plan mode 运行")和 feedback 供队友重提交。
  • 消息是带 Zod schema 验证 的 JSON。字段名还不统一------permission 用 request_id,shutdown/plan approval 用 requestId。
  • CC 有强制 permission gating,教学版只演示消息流、不做执行拦截。

s17 · Autonomous Agents ------ 自己看板,自己认领

解决什么 :s16 里每个队友都等 Lead 派活,看板上 10 个未认领任务就要 assign 10 次,无法横向扩展。

怎么做 :生命周期从两阶段变三阶段。

阶段 行为
WORK inbox → LLM → 工具循环,stop_reason != tool_use 退出
IDLE 每 5s 轮询 inbox + 任务板(IDLE_POLL_INTERVAL=5),60s 超时(IDLE_TIMEOUT=60,即 12 次)
SHUTDOWN 发 summary 退出
python 复制代码
def scan_unclaimed_tasks() -> list[dict]:
    unclaimed = []
    for f in sorted(TASKS_DIR.glob("task_*.json")):
        task = json.loads(f.read_text())
        if (task.get("status") == "pending"
                and not task.get("owner")
                and can_start(task["id"])):      # 依赖判断:"blockedBy 中没有未完成的"
            unclaimed.append(task)
    return unclaimed

四个关键设计:

  1. 优先级:inbox 优先于任务板 (可能含 shutdown_request)。IDLE 阶段收到 shutdown_request 直接回复退出,不等下一轮 WORK------否则关机会被饿死。
  2. can_start 的语义是"blockedBy 中没有未完成的任务",不是"blockedBy 为空"。
  3. claim_task 拒绝非 pending 或已有 owner 的任务,调用方检查 "Claimed" in result 才算成功。
  4. 身份重注入 :if len(messages) <= 3 说明发生过 autoCompact(s08),重新插入 <identity>You are '{name}', role: {role}...</identity>。这个细节很容易漏------压缩会把队友的身份认知一起压掉。
📌 真实 CC 差在哪

教学版的 idle_poll 其实是四个机制合并 的简化:① sendIdleNotification()(inProcessRunner.ts:569-589)向 Lead 发空闲通知;② waitForNextPromptOrShutdown()(:689-868)是 500ms 轮询循环 ,shutdown_request 优先处理;③ useTaskListWatcher 用 fs.watch() 监听 .claude/tasks/,1 秒 debounce ;④ 轮询内部主动调 tryClaimNextTask()(:624-657)。另外 CC 没有固定超时(Lead 手动 shutdown),且 context compaction 天然保留 system prompt,无需重注入。


s18 · Worktree Isolation ------ 各干各的目录,互不干扰

解决什么 :s15--s17 解决了"谁干什么"和"怎么通信",但所有队友共享同一 WORKDIR------Alice 重构认证模块、Bob 重构 UI 登录页,都 write_file("config.py"),互相覆盖,且分不清改动是谁的、无法干净回滚。

怎么做 :用 git worktree 给每个任务一个独立目录 + 独立分支。

python 复制代码
def bind_task_to_worktree(task_id: str, worktree_name: str):
    task = load_task(task_id)
    task.worktree = worktree_name       # 只写 worktree 字段
    save_task(task)                     # 状态保持 pending,等队友 claim 时才推进

为什么要"只写字段不改状态" :这样 Lead 可以提前批量建好任务 + worktree ,队友 idle 时自动认领才推进 in_progress。

  • 命名校验 :validate_worktree_name 只允许 [A-Za-z0-9._-]{1,64},拒绝路径穿越。
  • 创建 :git worktree add <path> -b wt/<name> HEAD,成功才写日志。
  • 收尾二选一 :keep_worktree(留分支待人工 review 合并)或 remove_worktree(discard_changes=False)------有改动时默认拒绝 (统计 files/commits,>0 即拒),需显式 discard_changes=true;删除后 git branch -D wt/<name>。
  • 生命周期事件写入 .worktrees/events.jsonl 供审计。

金句 :「Keep = 留着分支,等人工 review 后合并到主分支。Remove = 有改动时默认拒绝。」

📌 真实 CC 差在哪
  • 🔑 CC 没有 task-worktree 绑定 。worktree 状态由 PersistedWorktreeSession(worktree.ts:756-768)管理,字段 originalCwd/worktreePath/worktreeName/worktreeBranch/originalBranch/originalHeadCommit/sessionId,没有 taskId 。CC 把两者当独立系统,靠 Agent 自己理解上下文关联。
  • EnterWorktree 是进程级目录变更 (EnterWorktreeTool.ts:92-97):创建后立刻 process.chdir(worktreePath) + setCwd() + saveWorktreeState(),不是 prompt 提醒。
  • 命名:真实路径 .claude/worktrees/、分支 worktree-{slug}(斜杠换 +);创建用 git worktree add -B,且优先基于 origin/<defaultBranch> 而非 HEAD。

s19 · MCP Tools ------ 外接工具,标准协议

解决什么 :s01--s18 所有工具都是手写的------bash/read/write/task/worktree,输入验证、执行、错误处理全靠一行行写。要接 Jira、自建部署系统、Notion 三个服务,不想重写三套工具代码。

怎么做 :外部服务只要实现 MCP 协议,Agent 就能发现并调用,不管它用什么语言写的。

python 复制代码
def assemble_tool_pool() -> tuple[list[dict], dict]:
    tools = list(BUILTIN_TOOLS); handlers = dict(BUILTIN_HANDLERS)
    for server_name, mcp_client in mcp_clients.items():
        safe_server = normalize_mcp_name(server_name)
        for tool_def in mcp_client.tools:
            prefixed = f"mcp__{safe_server}__{normalize_mcp_name(tool_def['name'])}"
            tools.append(...)
            handlers[prefixed] = (lambda *, c=mcp_client, t=tool_def["name"], **kw:
                                      c.call_tool(t, kw))     # 闭包默认参数捕获
    return tools, handlers
  • 命名 :mcp__{server}__{tool} 前缀避免跨 server 冲突;normalize_mcp_name 用 re.compile(r'[^a-zA-Z0-9_-]') 把非法字符全替换成 _。
  • 🔑 重要取舍:s10 起的 prompt cache 被去掉了 。因为 connect_mcp 后工具池变了,缓存里的工具列表是旧的,模型调不到新工具------所以每轮都要 assemble_tool_pool() + assemble_system_prompt() 重建。代价只是多花序列化时间。
  • 另外两个简化:MCP 工具 description 只用文本标注 (readOnly)/(destructive),不做权限拦截 ;MCP 工具只有 Lead 可用(真实 CC 中子 agent 继承父级 MCP 配置)。
📌 真实 CC 差在哪
  • 6 种 transport (types.ts:23-25):stdio / sse / http / ws / sse-ide / sdk,连接时本地批量 3 个、远程批量 20 个并发。
  • 🔑 assembleToolPool()(tools.ts:345-364)用 uniqBy([...builtInTools.sort(byName), ...filteredMcpTools.sort(byName)], 'name')------内置与 MCP 分开排序 。因为 claude_code_system_cache_policy 在最后一个内置工具之后放全局缓存断点,混排会破坏缓存。
  • MCP 工具有独立权限系统 ,checkPermissions() 按工具声明的 readOnly/destructive 决定是否需用户确认。
  • 配置优先级 从低到高:claude.ai 连接器 < plugin < user settings.json < approved project .mcp.json < local settings.local.json;企业 managed-mcp.json 存在时完全排除其他。
  • Channel 反向通知 (channelNotification.ts):server 声明 capabilities.experimental['claude/channel'] 后可主动推送,消息包进 <channel source="serverName">,Agent 被 SleepTool 1 秒内唤醒。
  • OAuth 2.0 + PKCE :RFC 8414/9728 发现元数据、本地回调、getSecureStorage() 持久化(macOS Keychain / Linux 加密文件 / Windows 凭据管理器)、过期前 5 分钟自动刷新。

终章 · s20 一个完整 Agent Turn

Harness 层:综合 ------ 把前 19 章的机制放回同一个可运行系统。 1708 LOC,32 个工具。

核心主张 :s20 不再发明新机制。难点也不是堆功能,而是看清每个机制挂在循环的哪个位置------"把所有组件归位"。

一个完整 turn 的触发顺序

#mermaid-svg-aVeFrjhAG2K3wnxQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aVeFrjhAG2K3wnxQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aVeFrjhAG2K3wnxQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aVeFrjhAG2K3wnxQ .error-icon{fill:#552222;}#mermaid-svg-aVeFrjhAG2K3wnxQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aVeFrjhAG2K3wnxQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aVeFrjhAG2K3wnxQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aVeFrjhAG2K3wnxQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aVeFrjhAG2K3wnxQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aVeFrjhAG2K3wnxQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aVeFrjhAG2K3wnxQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aVeFrjhAG2K3wnxQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aVeFrjhAG2K3wnxQ .marker.cross{stroke:#333333;}#mermaid-svg-aVeFrjhAG2K3wnxQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aVeFrjhAG2K3wnxQ p{margin:0;}#mermaid-svg-aVeFrjhAG2K3wnxQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aVeFrjhAG2K3wnxQ .cluster-label text{fill:#333;}#mermaid-svg-aVeFrjhAG2K3wnxQ .cluster-label span{color:#333;}#mermaid-svg-aVeFrjhAG2K3wnxQ .cluster-label span p{background-color:transparent;}#mermaid-svg-aVeFrjhAG2K3wnxQ .label text,#mermaid-svg-aVeFrjhAG2K3wnxQ span{fill:#333;color:#333;}#mermaid-svg-aVeFrjhAG2K3wnxQ .node rect,#mermaid-svg-aVeFrjhAG2K3wnxQ .node circle,#mermaid-svg-aVeFrjhAG2K3wnxQ .node ellipse,#mermaid-svg-aVeFrjhAG2K3wnxQ .node polygon,#mermaid-svg-aVeFrjhAG2K3wnxQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aVeFrjhAG2K3wnxQ .rough-node .label text,#mermaid-svg-aVeFrjhAG2K3wnxQ .node .label text,#mermaid-svg-aVeFrjhAG2K3wnxQ .image-shape .label,#mermaid-svg-aVeFrjhAG2K3wnxQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-aVeFrjhAG2K3wnxQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aVeFrjhAG2K3wnxQ .rough-node .label,#mermaid-svg-aVeFrjhAG2K3wnxQ .node .label,#mermaid-svg-aVeFrjhAG2K3wnxQ .image-shape .label,#mermaid-svg-aVeFrjhAG2K3wnxQ .icon-shape .label{text-align:center;}#mermaid-svg-aVeFrjhAG2K3wnxQ .node.clickable{cursor:pointer;}#mermaid-svg-aVeFrjhAG2K3wnxQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aVeFrjhAG2K3wnxQ .arrowheadPath{fill:#333333;}#mermaid-svg-aVeFrjhAG2K3wnxQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aVeFrjhAG2K3wnxQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aVeFrjhAG2K3wnxQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aVeFrjhAG2K3wnxQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aVeFrjhAG2K3wnxQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aVeFrjhAG2K3wnxQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aVeFrjhAG2K3wnxQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aVeFrjhAG2K3wnxQ .cluster text{fill:#333;}#mermaid-svg-aVeFrjhAG2K3wnxQ .cluster span{color:#333;}#mermaid-svg-aVeFrjhAG2K3wnxQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aVeFrjhAG2K3wnxQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aVeFrjhAG2K3wnxQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-aVeFrjhAG2K3wnxQ .icon-shape,#mermaid-svg-aVeFrjhAG2K3wnxQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aVeFrjhAG2K3wnxQ .icon-shape p,#mermaid-svg-aVeFrjhAG2K3wnxQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aVeFrjhAG2K3wnxQ .icon-shape .label rect,#mermaid-svg-aVeFrjhAG2K3wnxQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aVeFrjhAG2K3wnxQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aVeFrjhAG2K3wnxQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aVeFrjhAG2K3wnxQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
👤 用户输入
UserPromptSubmit hooks
cron queue + background notifications 注入
compaction pipeline

budget → snip → micro → history
assemble_system_prompt

memory + skills 目录 + MCP 状态
🤖 LLM 调用

(error recovery 包在外面)
content 里有

tool_use block?
Stop hooks
✅ 返回
PreToolUse hooks + permission
assemble_tool_pool 分发

内置 / MCP / background
PostToolUse hooks
tool_result / task_notification

回 messages

⚠️ 再次强调:CC 源码不信任 stop_reason == "tool_use" ,而以实际出现的 tool_use block 作为是否继续工具轮的信号。

各机制在循环中的位置(原教程总表)

位置 组件 作用
用户输入前后 UserPromptSubmit hooks 记录、注入、审计用户输入
LLM 前 cron queue 把定时触发的 prompt 注入 messages
LLM 前 background notifications 后台任务完成后以 <task_notification> 注入
LLM 前 compaction pipeline 先压大输出,再裁历史,再压旧 tool_result,必要时摘要
LLM 前 memory / skills / MCP state 组装 system prompt,让模型看到当前能力和长期上下文
LLM 调用 error recovery 429/529 重试,max_tokens 升级,prompt too long 触发 reactive compact
工具执行前 PreToolUse hooks + permission 拦截危险命令、写越界、破坏性 MCP 工具
工具分发 assemble_tool_pool 组装内置工具和 MCP 动态工具
工具执行时 background dispatch 慢 bash 放 daemon thread,主循环先返回占位结果
工具执行后 PostToolUse hooks 大输出告警、日志等后处理
返回循环 tool_result 每个 tool_use 对应一个 tool_result,再回到下一轮
停止时 Stop hooks 统计、清理、审计

按层归类:输入层 → 上下文层 → 模型调用层 → 执行层 → 回路层 → 终止层。

五个"成对出现"的设计,值得单独记住

对比项 A B 分野
计划 todo_write(会话内内存态) task graph(.tasks/*.json) 防单 Agent 漂移 vs 跨会话可依赖可认领
委派 task(一次性 subagent,独立 messages,只回摘要) spawn_teammate(持久线程,MessageBus + idle 轮询) 上下文隔离 vs 长期并行协作
隔离 独立 messages[] git worktree 上下文 vs 文件系统
压缩 便宜的(budget/snip/micro,0 API 调用) 贵的(LLM 摘要) 先跑便宜的
扩展 改循环 挂 hook 后者才是对的

全书终态代码(也是 s01 的原形)

python 复制代码
while True:
    response = LLM(messages, tools)
    if not has_tool_use(response.content):
        return
    results = execute_tools(response.content)
    messages.append(tool_results)

金句 :「Claude Code 的复杂性不是"另一个 agent 大脑",而是一个成熟 harness 的复杂性。模型负责判断和行动选择;harness 负责把环境、工具、权限、记忆、团队和外部能力组织好。」
「这就是全书的终点:机制很多,循环一个。」------ 结束亦是开始。


附录 A:常量速查表

教学版关键常量

常量 值 章节 含义
max_tokens 8000 s01 初始输出上限
ESCALATED_MAX_TOKENS 64000 s11 截断后升级值(8 倍),只有一次机会
MAX_RECOVERY_RETRIES 3 s11 续写最多 3 次
MAX_RETRIES / BASE_DELAY_MS 10 / 500 s11 429/529 指数退避,+25% 抖动,封顶 32s
MAX_529_RETRIES 3 s11 连续 3 次过载才切 fallback model
KEEP_RECENT_TOOL_RESULTS 3 s08 micro compact 保留最近几条 tool_result
micro compact 阈值 120 字符 s08 超过才替换成占位符
snip 阈值 50 条消息 s08 保留头 3 + 尾 47
tool_result_budget 200000 字符 s08 落盘阈值
compact 失败熔断 3 次 s08 连续失败就放弃
reactive_compact 保留 尾 5 条 / 重试 1 次 s08/s11 PTL 应急
CONSOLIDATE_THRESHOLD 10 s09 记忆文件数达标才整理
memory side-query max_tokens=200,最多 5 条 s09 失败降级关键词匹配
子 Agent 轮数 30 s06 safety limit
队友轮数 10 s15 教学版硬限
IDLE_POLL_INTERVAL / IDLE_TIMEOUT 5s / 60s s17 idle 轮询间隔与超时(12 次)
cron 轮询 1s(scheduler)/ 0.2s(processor) s14 ---
后台 summary 截断 200 字符 s13 <task_notification> 里的 summary

真实 CC 关键常量

常量 值 出处
AUTOCOMPACT_BUFFER_TOKENS 13,000 autoCompact.ts:62
MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES 3 autoCompact.ts:70
MAX_OUTPUT_TOKENS_FOR_SUMMARY 20,000 autoCompact.ts:30
POST_COMPACT_TOKEN_BUDGET 50,000 compact.ts:123
POST_COMPACT_MAX_FILES_TO_RESTORE 5 compact.ts:122
POST_COMPACT_MAX_TOKENS_PER_FILE 5,000 compact.ts:124
时间型 micro compact 间隔 60 分钟 timeBasedMCConfig.ts
退避公式 min(500×2^(n-1), 32000) + 0~25% 抖动 withRetry.ts:530-548
记忆注入预算 每文件 ≤200 行 / 4096 字节;session 总 60KB;MEMORY.md ≤200 行 / 25KB attachments.ts:269-288、memdir.ts:34-38
记忆扫描上限 最多 200 个,按 mtime 降序 memoryScan.ts:35-94
Dream 门控 ≥24h / ≥5 transcript / 锁 1h 过期 autoDream.ts:130-190
sessionMemoryCompact 阈值 ≥10K token、≥5 条文本消息、≤40K token sessionMemoryCompact.ts:56-61
MAX_JOBS(cron) 50 CronCreateTool.ts:25
cron 抖动 周期 10%,上限 15 分钟 cronScheduler.ts
cron 自动过期 7 天(上限 30 天) cronTasks.ts
task 锁重试 30 次,指数退避 5--100ms tasks.ts:541-612
mailbox 写入重试 10 次 teammateMailbox.ts
权限轮询 Lead 1s / 队友 500ms useInboxPoller.ts、useSwarmPermissionPoller.ts
后台看门狗 45 秒无输出增长 LocalShellTask.tsx:59-98
CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY 10 ---
skills 目录预算 上下文窗口 1%,上限 8000 字符 SkillTool.ts
system prompt 规模 20--30KB(标准)/ ~150 字符(CLAUDE_CODE_SIMPLE) ---

附录 B:20 章总表

# 机制 一句话 LOC 工具数
s01 Agent Loop 一个工具 + 一个循环 = 一个 Agent 102 1
s02 Tool Use 加一个工具只加一行,循环不动 135 5
s03 Permission 安全不能靠信任模型,要靠代码 180 5
s04 Hooks 挂在循环上,不写进循环里 232 5
s05 TodoWrite 它不增加执行能力,增加的是规划能力 236 6
s06 Subagent 独立 messages,只回传结论,禁止递归 304 7
s07 Skills 目录 ~100 tokens 常驻,正文 ~2000 tokens 按需 335 8
s08 Context Compact 便宜的先跑,贵的后跑;顺序不能换 414 9
s09 Memory 一层不参与压缩、跨会话保留的存储 528 6
s10 System Prompt 运行时按真实状态组装,不是硬编码 166 3
s11 Error Recovery 升级 token / 压缩上下文 / 切换模型 287 3
s12 Task System 盖房子不能先盖屋顶再打地基 297 8
s13 Background Tasks 洗衣机转着,你去干别的 379 8
s14 Cron Scheduler 闹钟不需要你盯着它才会响 645 11
s15 Agent Teams 一个 Agent 的注意力覆盖不了所有模块 745 14
s16 Team Protocols request_id 贯穿全链路 709 15
s17 Autonomous Agents 自己看板,自己认领 648 15
s18 Worktree Isolation 任务管目标,worktree 管目录 802 18
s19 MCP Tools 外接工具,标准协议 835 23
s20 Comprehensive Agent Turn 机制很多,循环一个 1708 32

写在最后:三条最该带走的认知

  1. Agent 的复杂度不在模型,在 harness。 模型只负责"下一步做什么",剩下的执行、权限、记忆、压缩、恢复、协作,全是 harness 的工程活。s01 的 30 行循环 + 19 章的挂载点,就是全部。

  2. 上下文是一种需要被管理的稀缺资源。 s05(计划)s06(隔离)s07(按需加载)s08(压缩)s09(记忆)s10(组装)这六章,本质上都在回答同一个问题:有限的窗口里,放什么、丢什么、什么时候丢。

  3. 知道什么时候该放弃,和知道什么时候该重试一样重要。 s11 里"升级只有一次机会""压缩过了还是超限就退出""连续 3 次 continuation 且 token 增量 < 500 就停"------这些熔断条件才是教学 demo 和线上系统真正的分水岭。


如果这篇笔记对你有帮助,欢迎点赞收藏。有任何错漏,欢迎在评论区指出。

相关推荐
维克兜率天2 小时前
【维克】复合型CTA:趋势+套利的双轮驱动
python·学习方法
伞伞悦读2 小时前
【第76期】两张3行表左连接变5行:主键、validate与未匹配排查完整教程
人工智能·python
用户8356290780513 小时前
使用 Python 在 Excel 中添加和自定义迷你图
后端·python
用户8356290780513 小时前
使用 Python 在 Excel 图表中添加趋势线与回归方程
后端·python
特立独行的猫A4 小时前
一份 Markdown 发十个平台:cspub 一稿多投
java·后端·python
深蓝AI4 小时前
HTTPX 超时 1 秒,接口为何还能跑 2.4 秒?
python
CyrusYiVPP4 小时前
哪个用户会再来买?RFM模型+plotly画出用户分层,数据分析实战第2期
人工智能·python
挖坑的张师傅5 小时前
开源一个 Managed Agents 云端托管智能体平台
后端·agent
挖坑的张师傅5 小时前
Agent 基建系列:用 MCP Tunnel 让云端 Agent 访问企业内网工具
后端·agent