607's Blog

Back

本文为作者学习Learn claude code过程中记下的笔记。 Learn Claude Code仓库:https://github.com/shareAI-lab/learn-claude-code


1. 工具的设计#

只有 bash 时, 所有操作都走 shell。cat 截断不可预测, sed 遇到特殊字符就崩, 每次 bash 调用都是不受约束的安全面。专用工具 (read_file, write_file) 可以在工具层面做路径沙箱。

这句话是在解释 为什么需要专门的工具,而不是什么都用 bash(shell命令)。

1.1 问题:只用 Bash 的缺点#

假设只有 bash 工具,LLM 想读文件时会这样:

cat myfile.txt           # 问题1: 文件大时输出超长
sed -i 's/old/new/g' f   # 问题2: 特殊字符会破坏命令 (如 `&`, `/`, `$`)
rm important.txt         # 问题3: bash 就是个安全漏洞,任何危险命令都能执行
ls / | some_command      # 问题4: 可以访问任意目录
python
缺点例子问题
输出不可控cat 大文件可能刷爆终端
特殊字符崩溃sed $var$ 被 shell 解析出错
不安全rm -rf /没有限制,LLM 可能误操作
路径逃逸cat /etc/passwd可以访问工作目录外的文件

1.2 解决方案:专用工具 + 路径沙箱#

def safe_path(p: str) -> Path:
    path = (WORKDIR / p).resolve()           # 把相对路径转为绝对路径
    if not path.is_relative_to(WORKDIR):      # 检查是否在允许范围内
        raise ValueError(f"Path escapes workspace: {p}")
    return path

def run_read(path: str, limit: int = None) -> str:
    text = safe_path(path).read_text()        # 只能在 WORKDIR 内读取
    # ...
python

这样 即使 LLM 被诱导 执行 read_file(path="/etc/passwd"),也会被 safe_path 拦截,因为 /etc/passwd 不在 WORKDIR 内。

1.3 总结#

方案优点缺点
只用 bash灵活强大危险、不可预测、路径不可控
专用工具安全、可控、可限制范围需要事先定义好要暴露的功能

核心思想:给 LLM 的能力要”最小化原则”——只给它完成目标需要的工具,而不是把所有权限都放开。


2. ToDoWrite#

2.1 问题#

多步任务中, 模型会丢失进度 — 重复做过的事、跳步、跑偏。对话越长越严重: 工具结果不断填满上下文, 系统提示的影响力逐渐被稀释。一个 10 步重构可能做完 1-3 步就开始即兴发挥, 因为 4-10 步已经被挤出注意力了。

通过 TodoWrite 让计划可见

我们不让模型在思维链中默默规划,而是强制通过 TodoWrite 工具将计划外化。每个计划项都有可追踪的状态(pending、in_progress、completed)。这有三个好处:(1) 用户可以在执行前看到 agent 打算做什么;(2) 开发者可以通过检查计划状态来调试 agent 行为;(3) agent 自身可以在后续轮次中引用计划,即使早期上下文已经滚出窗口。

同一时间只允许一个任务进行中

TodoWrite 工具强制要求任何时候最多只能有一个任务处于 in_progress 状态。如果模型想开始第二个任务,必须先完成或放弃当前任务。这个约束防止了一种隐蔽的失败模式:试图通过交替处理多个项目来’多任务’的模型,往往会丢失状态并产出半成品。顺序执行的专注度远高于并行切换


3. 子 Agent 上下文隔离#

“大任务拆小, 每个小任务干净的上下文” — 子agent用独立 messages[], 不污染主对话

agent工作越久, messages 数组越胖。每次读文件、跑命令的输出都永久留在上下文里。“这个项目用什么测试框架?” 可能要读 5 个文件, 但父agent只需要一个词: “pytest。

  1. 父agent有一个 task 工具。子agent拥有除 task 外的所有基础工具 (禁止递归生成)。
  2. 子agent以 messages=[] 启动, 运行自己的循环。只有最终文本返回给父agent。
  3. 子agent可能跑了 30+ 次工具调用, 但整个消息历史直接丢弃。父agent收到的只是一段摘要文本, 作为普通 tool_result 返回。

4. Skill Loader#

这是 两层 Skill 注入 的设计。让我逐步解释:

4.1 核心设计思想#

不要把所有 skill 内容都塞进 system prompt(太长了)
只在 system prompt 放 skill 名称列表(Layer 1
LLM 需要时,再通过 tool_result 把完整内容注入给 LLM(Layer 2
python

4.2 Layer 1:System Prompt(只放元数据)#

SYSTEM = f"""You are a coding agent at {WORKDIR}.
Skills available:
{SKILL_LOADER.get_descriptions()}"""
python

输出长这样:

You are a coding agent at C:\Users\wsmdm\Desktop\learn-claude-code.
Skills available:
  - pdf: Process PDF files
  - code-review: Review code changes
python

只占几十个 token,告诉 LLM”有哪些技能可以用”。

4.3 Layer 2:按需加载(完整内容)#

当 LLM 调用 load_skill("pdf") 时:

"load_skill": lambda **kw: SKILL_LOADER.get_content(kw["name"]),
python

返回的是 完整的 skill 文档,被包装成 <skill> 标签:

<skill name="pdf">
## PDF Processing

1. Use pymupdf to read PDF
2. Extract text with...
3. ...
</skill>
python

这通过 tool_result 机制注入:

results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(output)})
messages.append({"role": "user", "content": results})
python

4.4 为什么这样设计?#

问题System Prompt 全塞进去按需加载(两层)
token 消耗巨大(10个skill可能几万元)极低
LLM 能用到的 skill全部(但可能上下文不够)只有用到的那些
灵活性

4.5 流程图#

LLM 说:"我要处理 PDF"

调用 load_skill(name="pdf")

返回 <skill>pdf 完整文档</skill> 作为 tool_result

这条 tool_result 附加到 messages 里发回给 LLM

LLM 现在"看到了"完整的 PDF 处理指南

继续执行任务
python

简单说:System Prompt 只告诉你”有哪些工具箱”,等你真的要开某个工具时,才把工具箱里的说明书给你看。


5. 上下文压缩#

5.1 三层压缩策略#

  • 每轮运行的微压缩:几乎零成本:它截断旧消息中的 tool_result 块,去除不再需要的冗长命令输出。
  • token 数超过阈值时触发的自动压缩:调用 LLM 生成对话摘要,代价高但能大幅缩减上下文。
  • 用户触发的手动压缩:用于明确的’重新开始’场景。

分层意味着低成本操作持续运行(保持上下文整洁),而高成本操作很少触发(仅在真正需要时)。

5.2 摘要替换全部消息,而非保留部分历史#

自动压缩触发时,生成摘要并==替换全部消息历史==,不会在摘要旁保留最近的 N 条消息。

这避免了一个微妙的连贯性问题:如果同时保留近期消息和旧消息的摘要,模型会看到重叠内容的两种表示。摘要可能说’我们决定使用方案 X’,而近期消息仍在展示讨论过程,产生矛盾信号。

干净的摘要是一个连贯的单一叙述。

5.3 完整对话以 JSONL 格式归档到磁盘#

完整的未压缩对话仍会追加到磁盘上的 JSONL 文件中。每条消息、每次工具调用、每个结果都不会丢失。

压缩对内存上下文是有损操作,但对永久记录是无损的。事后分析(调试 agent 行为、计算 token 用量、提取训练数据)始终可以基于完整记录进行。JSONL 格式仅追加写入,对并发写入安全,易于流式处理。


6. 任务系统#

“大目标要拆成小任务, 排好序, 记在磁盘上” — 文件持久化的任务图, 为多 agent 协作打基础。

写入磁盘,建立依赖关系,实现任务持久化

s03 的 TodoManager 问题:

todos = [
    {"content": "任务 A"},
    {"content": "任务 B"},
    {"content": "任务 C"},
]
python

6.1 只有内容 + 是否完成,没有结构#

真实目标的样子:

graph TD
    A[A 完成] --> B
    A --> C
    B & C --> D & E
    D & E --> F
    style A fill:#90EE90
    style B fill:#fff
    style C fill:#fff
    style D fill:#fff
    style E fill:#fff
    style F fill:#fff
mermaid
  • A (完成) → B 和 C 可以并行
  • D 和 E 都完成后才能做 F

为什么”agent分不清”

  1. 不知道什么能做
Agent: "还有啥任务?"
系统:  [A, B, C, D, E, F]
python
  • Agent 不知道 B 被 A 卡住,也不知道 C 和 D 可以同时跑 → 只能瞎猜或者一股脑全做
  1. 不知道什么被卡住 A 没做完时,B 实际上”被阻塞”了,但清单里 B 就是一条记录,状态还是 “todo”,Agent 以为能做,其实做了也白做(等 A 成果)
  2. 不能表达并行
  • C 和 D 独立,没有任何依赖
  • 但清单里跟 B 排在一起,看不出谁和谁相关
  • → Agent 可能串行做 C、D,白白浪费时间
  1. 上下文压缩就丢了

6.2 TaskManager: JSON 文件持久化#

S03: 内存中的列表#

self.todos = []
python

S06 的 micro_compact 会压缩消息 如果任务状态只存在于 Messages 列表里,压缩后任务清单就消失了!

S07 的改进#

{
    "id": 1, "subject": "A", "status": "completed", "blockedBy": [], "blocks": [2, 3]
}
{
    "id": 2, "subject": "B", "status": "pending", "blockedBy": [1], "blocks": [4]
}
{
    "id": 3, "subject": "C", "status": "pending", "blockedBy": [1], "blocks": [4]
}
{
    "id": 4, "subject": "D", "status": "pending", "blockedBy": [2, 3], "blocks": []
}
python
  • 文件持久化: .tasks/task_1.json 在磁盘上,s06 压缩不影响
  • blockedBy 字段: 显式表达”被谁阻塞”
  • blocks 字段: 双向维护,D 知道自己在等谁
  • scan_unclaimed_tasks(): 能筛选出 pending + 无 owner + 无 blockedBy 的任务
  • 并行检测: 多个任务同时 blockedBy: [] → 可以并行

6.6 核心区别#

维度s03 扁平清单s07+ 有向图
依赖关系blockedBy/blocks
状态done/not donepending/in_progress/completed
持久化内存JSON 文件
可执行判断不知道blockedBy 为空才能做
并行识别不知道无依赖的任务可并行

6.7 后台任务#

“慢操作丢后台, agent 继续想下一步” — 后台线程跑命令, 完成后注入通知。

启动后台任务不等待,任务完成后放队列,LLM调用前取走结果,队列保证不重复通知

6.8 Agent Team#

6.8.1 实现架构#


6.8.2 关键组件#

6.8.2.1 守护线程 (daemon=True)#
thread = threading.Thread(
    target=self._execute,      # 线程执行的函数
    args=(task_id, command),   # 函数参数
    daemon=True                 # 守护模式
)
thread.start()
python

守护线程 = 主进程退出时会被强制终止的线程

sequenceDiagram
    participant User as 用户
    participant Main as 主进程
    participant BG as 守护线程

    User->>Main: quit
    Main->>Main: 退出
    Note over Main: 守护线程被杀死 (不管任务跑没跑完)
mermaid

为什么用守护线程?

  • 用户 quit 后,不需要后台任务结果了
  • 避免主进程卡住等慢任务
6.8.2.2 线程锁 (threading.Lock)#
self._lock = threading.Lock()

with self._lock:
    self._notification_queue.append({…})
python

为什么需要锁?

时间线:

sequenceDiagram
    participant Agent as Agent 线程
    participant BG as 后台线程

    Note over Agent: drain() 读队列 (还没数据)
    BG->>BG: 命令执行完,写队列
    BG->>BG: with lock: queue.append(结果)
    Agent->>Agent: 看到空队列,返回 []
    Note over Agent: 结果丢了,没人拿到
mermaid

加锁保证:读写队列不会同时发生。

6.8.2.3 通知队列#
_notification_queue = []
python

后台线程执行完,写入:

with self._lock:
    _notification_queue.append({
        "task_id": "abc",
        "status": "completed",
        "result": "…"
    })
python

Agent 线程取出:

with self._lock:
    notifs = list(_notification_queue)
    _notification_queue.clear()  # 清空
return notifs
python

为什么要清空?

第1轮: 队列 = [结果A] drain → notifs = [结果A],队列 = []

第2轮: 队列 = [] drain → notifs = [],队列 = []

如果不清空: 第2轮: 队列 = [结果A] drain → notifs = [结果A] ← 重复通知,LLM 会困惑


6.8.3 完整流程#


6.8.3 完整流程#


7. Agent Teams#

7.1 Teammates + Mailboxes#

“任务太大一个人干不完, 要能分给队友” — 持久化队友 + JSONL 邮箱。

Harness 层: 团队邮箱 — 多个模型, 通过文件协调

子agent (s04) 是一次性的: 生成、干活、返回摘要、消亡。没有身份, 没有跨调用的记忆。后台任务 (s08) 能跑 shell 命令, 但做不了 LLM 引导的决策。

真正的团队协作需要三样东西:

  • (1) 能跨多轮对话存活的持久agent,
  • (2) 身份和生命周期管理
  • (3) agent之间的通信通道。

7.2 设计决策#

7.2.1 与subagent的区别#

  • 在 s04 中,子agent是临时的:创建、执行一个任务、返回结果、销毁。它们的知识随之消亡。
  • 在 s09 中,队友是具有身份(名称、角色)和配置文件的持久化线程。队友可以完成任务 A,然后被分配任务 B,并携带之前学到的所有知识。持久化队友积累项目知识,理解已建立的模式,不需要为每个任务重新阅读相同的文件。

我理解的teamate就是状态不隔离,持久化,可互相通信的subagent

7.2.2 团队配置持久化#

  • 团队结构(成员名称、角色、agent ID)存储在 JSON 配置文件中,而非任何 agent 的内存中。
  • 任何 agent 都可以通过读取配置文件发现队友——无需发现服务或共享内存。如果 agent 崩溃并重启,它读取配置即可知道团队中还有谁。
  • 这与 s07 的理念一致:文件系统就是协调层。配置文件人类可读,便于手动添加或移除团队成员、调试团队配置问题。

7.2.3 工具分发#

  • 团队组长获得 ALL_TOOLS(包括 spawn、send、read_inbox 等),而队友获得 TEAMMATE_TOOLS(专注于任务执行的精简工具集)。
  • 队友专注于做事(编码、测试、研究)
  • 组长专注于协调(创建任务、分配工作、管理沟通)。

8. team_protocols#

“队友之间要有统一的沟通规矩” — 一个 request-response 模式驱动所有协商。

基于 s09 的团队消息系统,加了 Shutdown 协议 和 Plan Approval 协议,两个协议都通过 request_id 关联请求和响应。


8.1 核心:request_id 关联模式#

发请求时生成 ID → 响应时带上同样 ID → 精准匹配

sequenceDiagram
    participant Lead
    participant Alice

    Lead->>Alice: req_id = "abc123"
    Note over Alice: 处理中…
    Alice-->>Lead: 带上 "abc123" 响应
    Note over Lead: 收到响应,查 "abc123",知道是谁在回应
mermaid

8.2 协议一:Shutdown 关闭协议#

目的:优雅关闭队友,而不是强制杀死线程。

流程

  1. Lead 想关闭 alice
  2. 生成 req_id = “abc123”
  3. 把 req_id 存到 tracker
  4. 发送 shutdown_request 给 alice
  5. alice 收到,决定批准还是拒绝
  6. 发送 shutdown_response 带上 req_id
  7. Lead 更新 tracker 状态
  8. alice 线程退出或继续工作

状态机

pending → approved (线程退出)
        → rejected (继续工作)
python

8.3 协议二:Plan Approval 计划审批协议#

目的:队友做重大操作之前,必须获得 Lead 批准。

流程

  1. alice 要做大事 (比如重构整个模块)
  2. 生成 req_id = “xyz789”
  3. 把计划和 req_id 发给 Lead
  4. Lead 看计划,决定批准还是拒绝
  5. alice 收到响应
    • approve=true → 开始执行
    • approve=false → 修改计划,重新提交

8.4 图解#


8.5 设计决策#

JSONL 收件箱文件而非共享内存

收发信息即维护JSON,只加不减。保证并发写入时的安全,不破坏数据;如果写入时崩溃仅留下不完整数据,读取者可以跳过。

五种消息类型覆盖所有协调模式

消息系统恰好支持五种类型:

  • (1) message 用于两个 agent 间的点对点通信;
  • (2) broadcast 用于全团队公告;
  • (3) shutdown_request 用于优雅终止;
  • (4) shutdown_response 用于确认终止;
  • (5) plan_approval_response 用于组长批准或拒绝队友的计划。

这五种类型映射到基本协调模式:直接通信、广播、生命周期管理和审批流程。

每次 LLM 调用前检查收件箱

队友在每次 agent 循环迭代的顶部、调用 LLM API 之前检查收件箱文件。这确保了对传入消息的最大响应性:响应快,成本低(读取小文件消耗token少),可以影响下次调用。


9. 自主Agent#

9.1 Scan Board, Claim Tasks#

9.2 设计决策#

9.2.1 轮询未认领任务而非事件驱动通知#

  • 自主队友每隔约 1 秒轮询共享任务板任务。
  • 轮询从根本上比发布/订阅更简单:
    • 没有订阅管理
    • 没有事件路由
    • 没有事件丢失的 bug
  • 在基于文件的持久化下,轮询保证响应速度,降低文件系统开销。

9.2.2 空闲 60 秒后自动终止#

不会因为任务完成到新任务创建之间的短暂间隔而导致过早关闭;又足够短,不会让闲置队友浪费资源。

9.2.3 上下文压缩后重新注入队友身份#

  • 自动压缩对话时,生成的摘要会丢失关键元数据:队友的名称、所属团队和 agent_id。没有这些信息,队友无法认领任务(任务按名称归属)、无法检查收件箱(收件箱文件以 agent_id 为键)、也无法在消息中表明身份。
  • 因此每次自动压缩后,系统会向对话中重新注入一个结构化的身份块:‘你是 [team] 团队的 [name],你的 agent_id 是 [id],你的收件箱在 [path]。‘这是队友在记忆丢失后保持功能所需的最小上下文。

10. Worktree + 任务隔离#

(codex的worktree应该也是这个思路)

任务管目标, worktree 管目录, 按 ID 绑定

agent已经能自主认领和完成任务。但所有任务共享一个目录。两个agent同时重构不同模块 — A 改 config.py, B 也改 config.py, 未提交的改动互相污染, 谁也没法干净回滚。

任务板管 “做什么” 但不管 “在哪做”。解法: 给每个任务一个独立的 git worktree 目录, 用任务 ID 把两边关联起来。

用 git worktree 做目录级隔离,让多个任务真正并行执行而互不干扰。

10.1 场景#

核心问题

  • 任务 A: 重构登录模块
  • 任务 B: 修复支付 bug

如果都在同一个目录改代码:

A 在改 auth.py  ←──────────────┐
B 也在改 auth.py  ──────────────┼──→ 冲突!代码乱套
python

结果:谁先提交谁覆盖,谁都可能被卡住等对方


解决方案:目录隔离

主仓库
├── auth.py (原始版本)

├── .worktrees/
│    ├── auth-refactor/     ← A 的工作目录 (wt/auth-refactor 分支)
│    │    └── auth.py        (重构版本)
│    │
│    └── payment-fix/        ← B 的工作目录 (wt/payment-fix 分支)
│         └── auth.py        (支付修复版本)

A 和 B 在各自目录改代码
互不干扰,可以同时工作
完成后可以选择:
  - keep: 保留这个 worktree,合并到主分支
  - remove: 删除 worktree,代码丢弃
python

10.2 三个核心组件#

  1. TaskManager - 控制平面

    任务 A (id=1): “重构登录” → worktree: “auth-refactor” 任务 B (id=2): “修复支付” → worktree: “payment-fix”

    • 任务是”想法”,存在 .tasks/ 目录
    • 记录谁在做,状态是什么
  2. WorktreeManager - 执行平面

    创建 worktree → 绑定到某个任务 在 worktree 里执行命令 完成后 keep 或 remove

  3. EventBus - 可观测性

    事件说明
    worktree.create.before创建前
    worktree.create.after创建后
    worktree.remove.before删除前
    worktree.remove.after删除后
    task.completed任务完成
    用于追踪整个生命周期,出问题了能查日志

10.3 工作流程#

  1. 创建任务
    task_create("重构登录模块")
    
    任务写入 .tasks/task_1.json
    status: pending
    python
  2. 创建 worktree 并绑定
    worktree_create("auth-refactor", task_id=1)
    
    git worktree add -b wt/auth-refactor .worktrees/auth-refactor
    
    任务状态: pending → in_progress
    worktree 绑定到任务
    python
  3. 在 worktree 里工作
    worktree_run("auth-refactor", "git status")
    worktree_run("auth-refactor", "python test.py")
    
    所有命令在隔离目录里执行
    python
  4. 完成后选择
    worktree_keep("auth-refactor")
        → 保留,继续在 auth-refactor 分支工作
    
    worktree_remove("auth-refactor", complete_task=True)
        → 删除 worktree,任务标记完成
    python

10.4 架构图#

目录结构:

.git/                           # 主仓库
├── auth.py                     # 原始文件
├── .worktrees/
│   ├── index.json             # 工作树索引
│   ├── events.jsonl           # 生命周期事件日志
│   ├── wt/
│   │   ├── auth-refactor/     # 任务 A 的 worktree (独立分支)
│   │   └── payment-fix/       # 任务 B 的 worktree (独立分支)
plaintext

总结#

这篇文章梳理了 Claude Code 的 10 个核心设计:

章节设计核心价值
1专用工具 + 路径沙箱安全可控
2TodoWrite计划可见、状态可追踪
3子 agent 上下文隔离避免污染、保持简洁
4Skill Loader 两层注入按需加载、节省 token
5上下文压缩平衡成本与历史
6任务系统 + 后台任务持久化、并行化
7Agent Teams多 agent 协作
8Team Protocols统一的沟通协议
9自主 agent自动认领任务
10Worktree 隔离任务级目录隔离

核心思想

  • 可控性:最小权限原则,只暴露必要的工具
  • 持久化:磁盘文件作为协调层,重启不丢失
  • 隔离性:子 agent 独立上下文、任务独立目录
  • 按需加载:Skill 按需注入,控制 token 成本

这些设计的共同目标是让 agent 在长期、多任务、复杂场景下保持稳定、可控、可扩展。

Learn Claude Code 学习笔记
https://rosemary1812.github.io/blog/learn-claude-code-notes/
Author 607
Published at 2026年3月29日