阅读视图

发现新文章,点击刷新页面。

连载10-Sub-agents 深度解析:从源码理解 Claude Code 的分身术

cover_09_subagents.png

Sub-agents 深度解析:从源码理解 Claude Code 的分身术

AI Coding 系列第 09 篇 · 多 Agent 编排


这篇文章讲到最后只有一句话:Sub-agent 不是一个人,是一套机械规则。 你在后面三节遇到的每个"为什么它会这样做",答案都不在 prompt 工程里——在 runAgent.ts 的几行 if 里。带着这句话往下读,这篇 9000 字的源码分析会变成它的验证过程。


你已经会用 Claude Code 完成单轮任务,也了解 Skill 和 Hook 如何定义知识和自动化规则。但当你面对一个需要"同时做三件事"的复杂项目——比如一边重构后端接口、一边更新文档、一边跑测试——你本能地想让 Claude"分身"去并行处理。

这篇文章从源码层面拆解 Sub-agents 的运行机制。不是教你"可以并行"这种显而易见的话,而是帮你理解:Claude Code 内部是怎么 fork 一个 Agent 的,Agent 之间的上下文隔离到底意味着什么,以及 worktree 隔离、权限继承、生命周期管理这些你在官方文档里找不到细节的东西。


一、先建立正确的心智模型

很多人把 Sub-agent 理解成"多线程"。这个类比有误导性。

更准确的说法是:Sub-agent 是一个独立的 LLM 会话,拥有自己的消息历史、工具集、中止控制器和文件状态缓存,但与父 Agent 共享同一个 AppState 状态树。

看一眼 runAgent.ts 中创建子 Agent 上下文的核心代码:

📂 展开源码:createSubagentContext 调用
// src/tools/AgentTool/runAgent.ts
const agentToolUseContext = createSubagentContext(toolUseContext, {
  options: agentOptions,
  agentId,
  agentType: agentDefinition.agentType,
  messages: initialMessages,
  readFileState: agentReadFileState,
  abortController: agentAbortController,
  getAppState: agentGetAppState,
  shareSetAppState: !isAsync,  // 同步 Agent 与父共享状态写入
  shareSetResponseLength: true,
})

关键细节:

  • readFileState 是从父 Agent 克隆的,不是共享引用。子 Agent 读文件时走自己的缓存,不会污染父 Agent 的文件视图。
  • abortController 对异步 Agent 是全新的(new AbortController()),对同步 Agent 是共享父 Agent 的。这意味着你 Ctrl+C 取消父 Agent 时,同步子 Agent 也会被取消,但后台运行的异步 Agent 不会。
  • shareSetAppState: !isAsync 这行很关键——同步 Agent 能直接写入父 Agent 的状态树,异步 Agent 则完全隔离。

这不是多线程,更像是 Unix 的 fork():创建时复制父进程的内存快照,之后各走各路。

理解这个模型之后,你就能回答一个更根本的问题——为什么必须用 Sub-agent 而不是在主对话里多写几个 prompt?

答案不在"聪明"或"更强",而在结构。主对话的上下文是线性追加的,500 行测试日志、200 行 grep 结果、一堆中间推理——这些信息对"当下执行"是必要的,但对"后续决策"是噪声。Claude Code 不会自动区分临时执行数据和长期决策记忆,默认全当作重要信息存着。

而 Sub-agent 是 Claude Code 里唯一一个结构上允许"执行完即丢弃"的东西。它的上下文窗口独立——噪声进去,结论出来,窗口销毁。主对话永远看不到中间过程。不是优化,是架构层面的隔离。

09_subagent_fork_model.png


二、何时用 Sub-agent:四个问题搞定决策

你不需要读完剩余 700 行源码分析再做决定。问自己四个问题就够了。

09_decision_matrix.png

问题一:主对话真的需要这些中间过程吗?

如果任务的输出超过 50 行,而你只关心里面不到 10 行的内容——用子代理。

  • 跑测试:300 行日志 → 你只需要"通过/失败,哪个挂了" → 信噪比 1%
  • 代码搜索:grep 返回 50 个匹配 → 你需要 3 个关键文件 → 信噪比 6%
  • 日志分析:1000 行错误 → 你需要 1 条根因判断 → 信噪比 0.5%

噪声留在主对话的不是"看着乱",是 token 膨胀。一次 npm test 输出 300 行 ≈ 4500 tokens——后续每轮对话都要重新发送这些噪声。子代理把这些吞下去,吐回 200 tokens 的精炼摘要。从 8800 tokens 压到 3700 tokens,主对话瘦身 58%。

记一条规则:如果你想让主对话记住什么,就别让不重要的东西进入它的上下文。 这就是 Sub-agent 唯一不可替代的价值——结构上允许"执行完即丢弃"。

问题二:子 Agent 需要继承父 Agent 的上下文吗?

  • 需要 → 用 Fork。省略 subagent_type,子 Agent 继承父 Agent 的对话历史和系统提示。共享 prompt cache,便宜。适合"帮我分担当前任务的一部分"。
  • 不需要 → 用 Named Agent。指定 subagent_type,子 Agent 从零开始,只看你写在 prompt 里的信息。适合"帮我做一件独立的事"。

Fork 和 Named Agent 不是高级/低级的区别,是两种通信模式。Fork 是"你继续做这个,我分个身帮你分担";Named Agent 是"你去把这件事做了,我不管你之前干了什么"。

问题三:子 Agent 的修改会污染我当前的编辑工作吗?

  • → 加 isolation: "worktree"。子 Agent 在独立的 git worktree 里工作,修改不碰你的文件。完成后无变更则自动清理,有变更则保留分支让你 review 后合并。
  • 不会(只读/搜索/分析等不写代码的任务)→ 不需要。

Worktree 的附加代价:每个 worktree 借用一个 git branch;node_modules 等大目录通过 symlink 共享(但如果子 Agent npm install 了新包,注意不要污染主仓库的依赖)。详见第五节。

问题四:这笔账划得来吗?

每个 Sub-agent 启动有 1-3 秒开销:克隆文件缓存、构建系统提示、加载 Skills、连接 MCP。同时,它的上下文隔离帮你省下几千到几万 tokens 的噪声搬运。

结论不是"Sub-agent 很贵"也不是"很值",而是——值不值取决于任务信噪比。低信噪比任务(跑测试、搜代码、分析日志)用 Sub-agent 绝对划算;高信噪比任务(直接的对话互动)不需要画蛇添足。

经验法则:

子任务预计耗时 决策
> 3 分钟 启动成本忽略不计,大胆用
30 秒 ~ 3 分钟 信噪比判断决定
< 30 秒 不值得,主对话直接做完

实战:怎么在对话中调用子 Agent

讲的都是"什么时候用",现在说"怎么用"。Claude Code CLI 里只能输入自然语言。 文章中出现的 Agent({subagent_type: "...", ...}) 是 Claude 内部的工具调用格式,不是让读者直接在终端敲的——Claude 读自然语言,帮读者生成这些调用。

你说自然语言 → Claude 解析意图 → Claude 内部生成 Agent() 工具调用 → 子 Agent 干活 → 结果展示给你。

# 触发内置 Explore Agent
帮我找一下项目中所有和 JWT token 验证相关的代码

# 触发你定义的自定义 Agent(如果 .claude/agents/ 里有 code-reviewer.md)
用 code-reviewer 审查 src/auth/ 的安全问题

# 触发 Fork(Claude 判断需要继承上下文时)
重构好了,帮我顺便写一下这三个函数的单元测试

# 流水线(Claude 顺序执行多个 Agent)
用 bug-locator 找到 token 验证失败的原因,然后让 bug-analyzer 分析根因

Claude 内部做的事:匹配你提到的名字(如 code-reviewer)到 .claude/agents/ 或内置 Agent → 生成 Agent() 工具调用(参数是 Claude 自己根据你的描述推断的)→ 子 Agent 启动 → 完成后直接把结果展示给你。你不会看到中间的 Agent({...}) 调用,只看到最终的文字回复。

如果想约束子 Agent 的行为(工具、权限、模型),要么在 .claude/agents/ 里预先配好(推荐),要么在自然语言里说清楚。话说得越具体,Claude 生成的调用参数越精确:

# 粗粒度(Claude 自己判断一切)
帮我审查代码

# 细粒度(你指定 Agent、范围、关注点)
用 code-reviewer 审查 src/auth/tokenValidator.ts,
重点关注硬编码密钥、缺少输入校验、auth 绕过风险,
用 sonnet 模型,最多调 20 轮工具

# 带 worktree 隔离(并行修改不要互相污染)
用 bug-fixer 修复 tokenValidator.ts:42-68 的竞态条件,
用 worktree 隔离,改完跑测试验证

Claude 会把"用 sonnet 模型"翻译成 model: "sonnet","最多调 20 轮工具"翻译成 maxTurns: 20。不是说自然语言万能——有些参数 Claude 可能理解偏差。关键的权限边界和工具白名单建议在 .claude/agents/ 配置里锁死,不要依赖自然语言。

常见疑问:如果同时有 code-review Skill 和 code-reviewer Sub-agent,Claude 选哪个?

源码里没有硬编码优先级。Claude 根据自己的判断二选一——它从系统提示里同时看到"可用 Skill 列表"和"可用 Agent 列表",靠任务特征自行裁量。简单规则性任务(如格式化输出)倾向 Skill;复杂多步骤、需要上下文隔离的任务倾向 Sub-agent。

但有一个非显而易见的耦合:Skill 的 frontmatter 里可以设 context: fork 这种情况下,Skill 的实际执行会被路由到 Sub-agent——Claude 表面在"调用 Skill",底层却启动了一个独立上下文的子 Agent。从这个角度看,Skill 和 Sub-agent 不是互斥选项——context: fork 的 Skill 就是用 Sub-agent 跑的 Skill。

调用后,后台的流程是透明的:

  1. Claude 把自然语言翻译成 Agent() 工具调用 → 源码根据 subagent_type(Claude 判断的)路由到对应 Agent 定义
  2. 创建独立的 LLM 会话——克隆文件缓存、构建系统提示、加载 Skills、连接 MCP
  3. 子 Agent 执行任务,它的工具调用和思考过程不会出现在你的对话里
  4. 完成后,只把最终的文字回复展示在你的对话中

你感知到的:子 Agent 执行期间终端可能显示它的工具调用(如果你开了详细输出),但它返回给你对话的内容只有最终的文字结果。500 行 grep 输出被子 Agent 吞掉了,你只看到"找到了 3 个相关文件,路径如下"。

(本文中的 Agent({...}) 代码示例展示的是 Claude 内部的工具调用格式,方便你理解参数含义。你不是在 CLI 里敲这些代码——这些是 Claude 在你的自然语言指令下生成的。)

下面走进源码,看这些机制具体怎么实现。


三、四种内置 Agent 类型:各有各的活法

09_agent_types_compare.png

Claude Code 不是只有一种 Sub-agent。打开 builtInAgents.ts,你会看到内置 Agent 的注册逻辑:

📂 展开源码:内置 Agent 注册
// src/tools/AgentTool/builtInAgents.ts
const agents: AgentDefinition[] = [
  GENERAL_PURPOSE_AGENT,
  STATUSLINE_SETUP_AGENT,
]

if (areExplorePlanAgentsEnabled()) {
  agents.push(EXPLORE_AGENT, PLAN_AGENT)
}

这段代码展示了本文重点关注的四种 Agent。完整源码中还有 CLAUDE_CODE_GUIDE_AGENT(回答 Claude Code 使用问题)和 VERIFICATION_AGENT(feature gate 控制的验证 Agent),以及 Coordinator Mode 下的动态 Agent 编排分支——它们各有专门的场景,不影响对核心四种的理解。

Explore Agent——只读搜索专家

Explore Agent 的系统提示开头就是一堵墙:

📂 展开源码:Explore Agent 的系统提示(只读限制)
=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===
This is a READ-ONLY exploration task. You are STRICTLY PROHIBITED from:
- Creating new files (no Write, touch, or file creation of any kind)
- Modifying existing files (no Edit operations)
...

它的 disallowedTools 直接禁掉了 AgentFileEditFileWriteNotebookEditExitPlanMode。这不是"建议"只读,而是工具级别的硬限制——Explore Agent 连尝试写文件的机会都没有。

注意这里的设计原则:安全感不是靠 prompt 劝说 Agent "请不要修改文件",而是从工具层把 Write 和 Edit 物理删掉。Agent 没有"自觉性"——唯一可依赖的,是它能调用哪些函数。这不是信任问题,是机械限制。

有两个值得注意的源码细节:

1. 它不带 CLAUDE.md:

📂 展开源码:omitClaudeMd 检查逻辑
// src/tools/AgentTool/runAgent.ts
const shouldOmitClaudeMd =
  agentDefinition.omitClaudeMd &&
  !override?.userContext

Explore Agent 设了 omitClaudeMd: true。原因是 Explore 只做搜索,commit 规范、PR 模板、lint 规则这些 CLAUDE.md 里的指令对它毫无意义。Anthropic 在代码注释里说这个优化"saves ~5-15 Gtok/week across 34M+ Explore spawns"——每周节省约 5-15 Gtok,覆盖 3400 万次以上 Explore 调用,每次省几千 token,累计节约量级惊人。

2. 它也不带 gitStatus:

📂 展开源码:gitStatus 省略逻辑
const { gitStatus: _omittedGitStatus, ...systemContextNoGit } =
  baseSystemContext
const resolvedSystemContext =
  agentDefinition.agentType === 'Explore' ||
  agentDefinition.agentType === 'Plan'
    ? systemContextNoGit
    : baseSystemContext

Explore 和 Plan 不需要会话开始时的 git status 快照(最多 40KB)。如果它们需要 git 信息,会自己跑 git status 获取实时数据,而不是依赖可能已经过期的快照。

Plan Agent——只读架构师

Plan Agent 和 Explore 共享同一套只读限制,但角色定位不同。它的系统提示要求输出结构化的实施方案:

End your response with:
### Critical Files for Implementation
List 3-5 files most critical for implementing this plan

Plan Agent 用 model: 'inherit',继承父 Agent 的模型。Explore 对外部用户默认用 Haiku(追求速度),Plan 则需要和父 Agent 一样强的推理能力。

General-Purpose Agent——全能型选手

tools: ['*'] 意味着它能使用所有工具(写代码、跑测试、执行 bash 命令),是真正的"全能分身"。注意:当 fork 功能开启时,省略 subagent_type 触发的是 Fork 而非 General-Purpose(见下文 Fork 小节)。

📂 展开源码:General-Purpose Agent 定义
export const GENERAL_PURPOSE_AGENT: BuiltInAgentDefinition = {
  agentType: 'general-purpose',
  tools: ['*'],     // 全部工具
  source: 'built-in',
  baseDir: 'built-in',
  // model is intentionally omitted - uses getDefaultSubagentModel()
  getSystemPrompt: getGeneralPurposeSystemPrompt,
}

Fork Agent——上下文继承的分身

Fork 的触发机制不是语义分析——是参数缺失时的默认路由:当 Claude 生成 Agent() 调用但省略了 subagent_type,且 fork feature gate 开启时,自动走 Fork 路径(AgentTool.tsx:322)。它的特征是继承父对话的完整上下文——你不需要在 prompt 里重复"我刚才重构了哪些文件",Fork 子 Agent 从对话历史里自己知道。

和 Named Agent 的区别一目了然:

你:重构 src/auth/ 的 token 验证逻辑,改了三个函数
Claude:完成
你:顺便帮我写一下这三个函数的单元测试吧
   → Claude 判断:子 Agent 需要知道"刚才重构了哪些函数"
   → 触发 Fork:子 Agent 从对话中直接知道,不需要你重复说

你:检查 src/auth/ 有没有安全问题
   → Claude 判断:审查独立于之前的对话
   → 使用 code-reviewer(Named Agent):从零开始,只看你给的信息

Fork 不能嵌套——Fork 子 Agent 不能再创建自己的子 Agent。多层编排的工作由主对话负责。

📂 展开源码:Fork 的定义、触发路由、消息构建和防递归机制
// src/tools/AgentTool/forkSubagent.ts
// Not registered in builtInAgents — used only when !subagent_type
export const FORK_AGENT = {
  agentType: 'fork',
  tools: ['*'],
  maxTurns: 200,
  model: 'inherit',
  permissionMode: 'bubble',
  getSystemPrompt: () => '',  // 继承父 Agent 的系统提示
}

触发路由(AgentTool.tsx:322):

const effectiveType = subagent_type ??
  (isForkSubagentEnabled() ? undefined : 'general-purpose')
// subagent_type 缺失 + fork 门开启 → Fork 路径

Fork 子 Agent 产生字节级相同的 API 请求前缀,共享 prompt cache 所以比新建 Agent 便宜。防递归靠 isInForkChild() 检测对话中的 <fork_boilerplate> 标记直接拒绝。

四种 Agent 的分工很清楚:Explore 找,Plan 想,General-Purpose 做。Fork 省略 subagent_type 即可触发——需要继承上下文时不加字段走 fork,独立任务时指定类型走 Named Agent。不能在 .claude/agents/ 里定义 fork 类型的自定义 Agent。


四、.claude/agents/ 自定义 Agent:你只需要关注四个字段

除了内置 Agent,你可以在 .claude/agents/ 目录下用 Markdown + YAML frontmatter 定义自己的 Agent。

先说不该做的事:把 Zod Schema 里所有 14 个字段背下来。你真正每次都要认真设计的只有四个——description(何时触发)、tools / disallowedTools(权限边界)、model(成本决策)。其余字段有需要时再查。

一个接近实战的配置示例(不是模板,是设计思路的载体):

---
name: code-reviewer
description: "Reviews code changes for quality, security, and consistency with project conventions. Use when you want a second opinion on code before committing."
model: inherit
permissionMode: dontAsk
tools:
  - Read
  - Glob
  - Grep
  - Bash
disallowedTools:
  - Write
  - Edit
  - NotebookEdit
skills:
  - code-review
hooks:
  SubagentStop:
    - hooks:
        - type: command
          command: "echo 'Code review completed at $(date)' >> .claude/review-log.txt"
maxTurns: 30
---

You are a code review specialist. Your job is to analyze code changes and provide actionable feedback.

Focus on:
- Security vulnerabilities (injection, auth bypass, data exposure)
- Performance issues (N+1 queries, unnecessary allocations, blocking calls)
- Error handling gaps (missing try-catch, unhandled promise rejections)
- Consistency with existing patterns in the codebase

Be specific: cite file paths and line numbers. Don't flag style issues unless they affect readability.

四个需要认真设计的字段:

  • description:不是你写给人看的注释——是 Claude 判断"何时自动调用这个 Agent"的唯一依据。写清楚做什么什么时候用。关键词 proactively 会鼓励 Claude 在合适的时机主动委派。
  • tools vs disallowedTools:白名单黑名单二选一,不要同时用。只读审查用 tools: [Read, Grep, Glob];需要大部分工具但排除个别危险的用 disallowedTools: [Write, Edit]。原则:最小权限——能用 Read 完成的就不要给 Edit。
  • model:不是越强越好。代码审查/分析推理 → sonnet;执行固定流程/模式匹配 → haiku;需要和主对话同等推理 → inherit。选错模型比选错工具更贵——Anthropic 的研究表明,升级模型的性能提升往往超过翻倍 token 预算的效果。
  • skills:Agent 不会自动继承主对话的 Skill。如果子 Agent 需要某个 Skill 的知识(如链路的 SLA 约束、历史事故记录),必须在 skills 字段显式列出,Skill 内容会在 Agent 启动时注入为 isMeta: true 的系统消息。

其余字段说明(有需要再看):

字段 用途 何时需要考虑
permissionMode 覆盖权限确认行为 异步 Agent 建议设 dontAsk
maxTurns 限制工具调用轮数 防止跑飞,建议 20-50
background 强制后台运行 长时间任务的非阻塞执行
isolation worktree 隔离 并行修改不同模块时启用
mcpServers Agent 专属 MCP 连接 需要访问特定外部服务
hooks Agent 生命周期的自动动作 SubagentStop 写日志等
initialPrompt 首轮额外注入的提示 给 Agent 额外的任务约束
memory 记忆作用域 跨会话共享知识

Agent 来源优先级

同名 Agent,后加载的覆盖先加载的:

// 覆盖链:内置 → 插件 → 用户级(~/.claude/agents/) → 项目级(.claude/agents/) → 企业管理策略
const agentMap = new Map<string, AgentDefinition>()
for (const agents of [builtIn, plugin, user, project, flag, managed]) {
  for (const agent of agents) {
    agentMap.set(agent.agentType, agent)  // 后写入覆盖先写入
  }
}

这意味着:你在 .claude/agents/ 里定义的 general-purpose Agent 会替换内置的通用 Agent。企业管理员可以通过策略设置强制覆盖所有 Agent 定义。

所以你应该怎么做:配置完 Agent 后,用一个简单任务测试 description 是否能正确触发。如果 Claude 该用的时候不用,大概率是 description 写得像"自我介绍"而不是"使用条件"。


五、Worktree 隔离:给 Agent 一个独立的代码沙箱

当你设置 isolation: "worktree" 时,子 Agent 会在一个独立的 git worktree 中工作。先理解概念:git worktree 让你在同一个仓库里同时 checkout 出多个分支到不同目录——每个目录像一个独立的仓库副本,有各自的 HEAD,但共享同一个 .git 目录。你不必为了在新分支上工作而 stash 当前修改。

09_worktree_architecture.png

本质上就是 fork() + chroot():共享同一个 .git,但每个 Agent 看到的文件系统是独立的隔离视图。

创建流程

📂 展开源码:Worktree 创建流程 (createAgentWorktree)
// src/utils/worktree.ts - createAgentWorktree
export async function createAgentWorktree(slug: string): Promise<{
  worktreePath: string
  worktreeBranch?: string
  headCommit?: string
  gitRoot?: string
}> {
  validateWorktreeSlug(slug)

  // 关键:使用 findCanonicalGitRoot 而不是 findGitRoot
  // 确保 Agent worktree 总是创建在主仓库的 .claude/worktrees/ 下
  // 而不是嵌套在某个会话 worktree 的 .claude/worktrees/ 里
  const gitRoot = findCanonicalGitRoot(getCwd())

  const { worktreePath, worktreeBranch, headCommit, existed } =
    await getOrCreateWorktree(gitRoot, slug)

  if (!existed) {
    await performPostCreationSetup(gitRoot, worktreePath)
  }
  return { worktreePath, worktreeBranch, headCommit, gitRoot }
}

创建后的自动化设置

performPostCreationSetup 做了一系列你手动操作很容易遗漏的事:

  1. 复制 settings.local.json:本地设置可能包含敏感配置,需要传播到 worktree
  2. 配置 git hooks 路径:让 worktree 复用主仓库的 .husky.git/hooks,避免 pre-commit hook 失效
  3. 符号链接大目录:根据 settings.worktree.symlinkDirectories 配置,symlink node_modules 等目录避免磁盘膨胀
  4. 复制 .worktreeinclude 指定的文件:gitignore 的文件(如 .env、build 产物)不在 worktree 中,但可以通过 .worktreeinclude 声明需要哪些

Worktree 的生命周期管理

Agent worktree 有一个优雅的"按需保留"机制:

📂 展开源码:Worktree 变更检查 (hasWorktreeChanges)
// 检查 worktree 是否有变更
export async function hasWorktreeChanges(
  worktreePath: string,
  headCommit: string,
): Promise<boolean> {
  // 检查 1: 有没有未提交的改动
  const status = await execFileNoThrowWithCwd(
    gitExe(), ['status', '--porcelain'], { cwd: worktreePath })
  if (statusOutput.trim().length > 0) return true

  // 检查 2: 有没有新的 commit
  const revList = await execFileNoThrowWithCwd(
    gitExe(), ['rev-list', '--count', `${headCommit}..HEAD`], { cwd: worktreePath })
  if (parseInt(revListOutput.trim(), 10) > 0) return true

  return false
}

如果子 Agent 完成后没有任何变更,worktree 会被自动清理。如果有变更(新 commit 或未提交的修改),worktree 和分支会保留,返回路径和分支名让你后续处理。

还有一个后台清理机制,定期扫描过期的临时 worktree:

📂 展开源码:临时 Worktree 清理模式 (EPHEMERAL_WORKTREE_PATTERNS)
const EPHEMERAL_WORKTREE_PATTERNS = [
  /^agent-a[0-9a-f]{7}$/,           // AgentTool 创建的
  /^wf_[0-9a-f]{8}-[0-9a-f]{3}-\d+$/, // WorkflowTool 创建的
  /^bridge-[A-Za-z0-9_]+(-[A-Za-z0-9_]+)*$/, // Bridge 创建的
]

只有匹配这些模式的 worktree 才会被自动清理——你手动通过 EnterWorktree 创建的 worktree(比如 feature-redesign)永远不会被误删。

Fork + Worktree 的组合

当 Fork 子 Agent 在 worktree 中运行时,会收到一条特殊的上下文通知:

📂 展开源码:Worktree 上下文通知 (buildWorktreeNotice)
// src/tools/AgentTool/forkSubagent.ts
export function buildWorktreeNotice(
  parentCwd: string, worktreeCwd: string,
): string {
  return `You've inherited the conversation context above from a parent
agent working in ${parentCwd}. You are operating in an isolated git
worktree at ${worktreeCwd} — same repository, same relative file
structure, separate working copy. Paths in the inherited context refer
to the parent's working directory; translate them to your worktree root.
Re-read files before editing if the parent may have modified them...`
}

这段提示告诉 Fork 子 Agent:你继承的上下文里的文件路径指向父 Agent 的工作目录,你需要把路径"翻译"到自己的 worktree 里。这是一个容易被忽略但非常关键的细节。

并行修改不同模块时启用 worktree,每个模块独立分支互不干扰。只读探索不需要。如果子 Agent 要在 worktree 里 npm install 新依赖,记得在 .worktreeinclude 里声明 .env 等被 gitignore 的关键文件。


六、权限模型:谁能做什么

Sub-agent 的权限控制是分层的,不是简单的"继承父 Agent 权限"。

权限模式覆盖

📂 展开源码:权限模式覆盖逻辑
// src/tools/AgentTool/runAgent.ts
const agentGetAppState = () => {
  const state = toolUseContext.getAppState()
  let toolPermissionContext = state.toolPermissionContext

  // Agent 定义的权限模式可以覆盖父 Agent 的
  // 但 bypassPermissions 和 acceptEdits 模式永远不会被覆盖
  if (
    agentPermissionMode &&
    state.toolPermissionContext.mode !== 'bypassPermissions' &&
    state.toolPermissionContext.mode !== 'acceptEdits'
  ) {
    toolPermissionContext = {
      ...toolPermissionContext,
      mode: agentPermissionMode,
    }
  }

  // 异步 Agent 不能显示权限弹窗——自动拒绝需要确认的操作
  if (shouldAvoidPrompts) {
    toolPermissionContext = {
      ...toolPermissionContext,
      shouldAvoidPermissionPrompts: true,
    }
  }
}

几条规则:

  • bypassPermissions(SDK 模式)和 acceptEdits 永远优先——子 Agent 不能收窄这两种宽松模式
  • 异步 Agent 设置 shouldAvoidPermissionPrompts: true,遇到需要用户确认的操作会自动拒绝
  • permissionMode: 'bubble' 是 Fork 的默认模式,权限请求会"冒泡"到父 Agent 的终端

工具过滤

📂 展开源码:工具过滤器 (filterToolsForAgent)
// src/tools/AgentTool/agentToolUtils.ts
export function filterToolsForAgent({ tools, isBuiltIn, isAsync }): Tools {
  return tools.filter(tool => {
    if (tool.name.startsWith('mcp__')) return true  // MCP 工具不受限
    if (ALL_AGENT_DISALLOWED_TOOLS.has(tool.name)) return false
    if (!isBuiltIn && CUSTOM_AGENT_DISALLOWED_TOOLS.has(tool.name)) return false
    if (isAsync && !ASYNC_AGENT_ALLOWED_TOOLS.has(tool.name)) return false
    return true
  })
}

三层过滤:

  1. 所有 Agent 禁用的工具ALL_AGENT_DISALLOWED_TOOLS):比如 ExitPlanMode——子 Agent 不应该改变父 Agent 的计划模式
  2. 自定义 Agent 额外禁用的CUSTOM_AGENT_DISALLOWED_TOOLS):用户定义的 Agent 比内置 Agent 受限更多
  3. 异步 Agent 的白名单:后台运行的 Agent 只能使用一个限定的工具子集

MCP 工具(mcp__ 前缀)不受这些限制,始终可用。

allowedTools 的权限隔离

📂 展开源码:allowedTools 权限隔离
// 父 Agent 的 session-level 权限不会泄露到子 Agent
if (allowedTools !== undefined) {
  toolPermissionContext = {
    ...toolPermissionContext,
    alwaysAllowRules: {
      cliArg: state.toolPermissionContext.alwaysAllowRules.cliArg, // 保留 SDK 级权限
      session: [...allowedTools], // 替换为子 Agent 自己的权限
    },
  }
}

注意这里的 cliArgsession 的区分:SDK 通过 --allowedTools 传入的权限(cliArg)是全局的,所有 Agent 都继承;而会话级别的权限(session)在子 Agent 创建时会被重置,防止父 Agent 运行时积累的权限无意间泄露给子 Agent。

自定义 Agent 首选白名单(tools),明确列出允许的工具,而不是依赖 disallowedTools 排除。异步 Agent 必须配合 permissionMode: 'dontAsk'bubble——否则需要确认的操作被静默拒绝,Agent 不知道原因就反复重试,看起来像卡住了。


七、Agent 的完整生命周期与 Hook 联动

Hook 不是独立于生命周期的外挂——SubagentStartSubagentStop 本身就是生命周期的两个关卡。先看 Hook 怎么嵌入,再看完整流程。

Hook 如何在生命周期中触发

在 Hooks 篇里讲过 SubagentStartSubagentStop 事件,这里从 Agent 源码看触发机制。

SubagentStart:启动前的注入

📂 展开源码:SubagentStart Hook 注入
// src/tools/AgentTool/runAgent.ts
// 执行 SubagentStart hooks 并收集额外上下文
const additionalContexts: string[] = []
for await (const hookResult of executeSubagentStartHooks(
  agentId, agentDefinition.agentType, agentAbortController.signal,
)) {
  if (hookResult.additionalContexts?.length > 0) {
    additionalContexts.push(...hookResult.additionalContexts)
  }
}

// 把 Hook 注入的上下文作为用户消息添加到初始对话中
if (additionalContexts.length > 0) {
  const contextMessage = createAttachmentMessage({
    type: 'hook_additional_context',
    content: additionalContexts,
    hookName: 'SubagentStart',
    ...
  })
  initialMessages.push(contextMessage)
}

SubagentStart Hook 可以向子 Agent 注入额外的上下文信息——比如团队编码规范的摘要、当前 Sprint 的约束条件、或者从 CI 系统拉取的最新构建状态。

Agent 自带的 Hooks

Agent 定义的 frontmatter 可以声明自己的 hooks,这些 hooks 会在 Agent 启动时注册为 session hooks,Agent 结束时自动清理:

📂 展开源码:Agent 专属 Hooks 注册/清理
// 注册 Agent frontmatter 中的 hooks
// isAgent=true 会把 Stop hooks 转换为 SubagentStop
if (agentDefinition.hooks && hooksAllowedForThisAgent) {
  registerFrontmatterHooks(
    rootSetAppState,
    agentId,
    agentDefinition.hooks,
    `agent '${agentDefinition.agentType}'`,
    true,  // isAgent - converts Stop to SubagentStop
  )
}

// ... Agent 运行 ...

// 清理
finally {
  if (agentDefinition.hooks) {
    clearSessionHooks(rootSetAppState, agentId)
  }
}

isAgent = true 这个参数把 Agent frontmatter 里声明的 Stop hooks 自动转换成 SubagentStop hooks。因为子 Agent 完成时触发的不是 Stop(那是主会话结束时的事件),而是 SubagentStop

完整流程:从创建到销毁

一个 Sub-agent 从创建到销毁经历的完整流程:

启动阶段:

  1. 生成唯一的 agentIdcreateAgentId()
  2. 解析模型选择(Agent 定义 → 父 Agent 模型 → 默认模型)
  3. 如果启用了 Perfetto tracing,在追踪树中注册
  4. 克隆父 Agent 的 readFileState(文件缓存隔离)
  5. 构建上下文:Explore/Plan 去掉 CLAUDE.md 和 gitStatus
  6. 执行 SubagentStart hooks,收集额外上下文
  7. 注册 frontmatter hooks(Stop → SubagentStop 转换)
  8. 预加载 frontmatter 中声明的 Skills
  9. 初始化 Agent 专属的 MCP Servers
  10. 记录初始消息到 sidechain transcript

运行阶段:

📂 展开源码:生命周期:运行阶段 (query loop)
for await (const message of query({
  messages: initialMessages,
  systemPrompt: agentSystemPrompt,
  canUseTool: hasPermissionsToUseTool,
  toolUseContext: agentToolUseContext,
  querySource,
  maxTurns: maxTurns ?? agentDefinition.maxTurns,
})) {
  // 转发 API metrics 到父 Agent 的显示
  // 记录每条消息到 sidechain transcript
  // 检测 max_turns_reached 信号
  yield message  // 流式输出给父 Agent
}

清理阶段(finally 块):

📂 展开源码:生命周期:清理阶段 (finally cleanup)
finally {
  await mcpCleanup()                          // 清理 Agent 专属 MCP 连接
  clearSessionHooks(rootSetAppState, agentId)  // 清理 session hooks
  cleanupAgentTracking(agentId)               // 清理 prompt cache 追踪
  agentToolUseContext.readFileState.clear()    // 释放文件缓存内存
  initialMessages.length = 0                  // 释放 fork 上下文消息
  unregisterPerfettoAgent(agentId)            // 释放 Perfetto 注册
  clearAgentTranscriptSubdir(agentId)         // 释放 transcript 映射
  // 清理 AppState.todos 中的孤儿条目
  // 杀死 Agent 启动的后台 bash 任务
  // 杀死 Agent 启动的 Monitor 任务
}

每一步清理都有明确的必要性。比如最后两步——如果不杀死 Agent 启动的后台 shell 循环(run_in_background 的任务),这些进程的父进程退出后会被 init 进程(PID=1)接管,变成"僵尸进程",在主会话退出后依然残留运行。


八、异步 Agent vs 同步 Agent:不只是"后台运行"

这不是简单的"加个 run_in_background: true"的区别。两种模式在架构上有本质不同。

fork() 的术语来说:同步 Agent 是 fork() + waitpid()——父进程阻塞等子进程结束;异步 Agent 是 fork() + detach——子进程独立运行,父进程继续干活。

09_sync_vs_async.png

维度 同步 Agent 异步 Agent
AbortController 共享父 Agent 的 独立的新实例
setAppState 共享父 Agent 的 隔离(通过 rootSetAppState 间接写入)
权限弹窗 可以显示 自动拒绝(shouldAvoidPermissionPrompts
工具集 完整(经过过滤) ASYNC_AGENT_ALLOWED_TOOLS 白名单
非交互模式 继承父 Agent 强制 isNonInteractiveSession: true
thinking 禁用 禁用
完成通知 直接返回结果 通过 enqueueAgentNotification

一个容易踩坑的点:异步 Agent 用 bubble 权限模式时,权限请求会冒泡到父 Agent 的终端,看起来像是同步的权限请求,但其实来自一个后台 Agent。这在同时运行多个异步 Agent 时可能造成困惑。

还有一个更隐蔽的坑:Agent 被静默拒绝后,不会告诉你"我卡在权限上了"。它只知道"操作失败了",然后用同样的方式重试。

所以你应该怎么做:短任务用同步(能直接看输出),长任务(>2 分钟)用异步(不阻塞主对话)。异步 Agent 启动前确保配了 permissionMode: 'dontAsk'bubble,并限定工具白名单——否则背景 Agent 会因权限不足静默失败,反复重试你也不知道为什么。


九、实战案例:基于源码理解的正确用法

本节中的 Agent({...}) 示例是 Claude 内部生成的工具调用格式,展示参数含义。CLI 中实际输入的是自然语言——Claude 帮你翻译成这些调用。

下面四个案例从简单到复杂递进:单一 Agent 审查 → 探索+实现串行流水线 → 多 Agent worktree 并行重构 → 影响面分析的事前拦截。

案例 1:并行代码审查

最基础的用法——四个完全独立的只读任务,并行执行。你要审查一个大 PR,涉及四个模块。每个模块的审查完全独立——审查 auth 的结果不影响审查 payment 的判断。

# 你在 CLI 里说:
用 code-reviewer 同时审查 src/auth/、src/payment/、src/order/、src/user/
四个模块的最新改动,每个模块独立审查,汇总成一份安全报告。

Claude 内部会把这一句话拆成四个并行的 Agent({subagent_type: "code-reviewer", ...}) 调用,四个审查 Agent 同时启动,各自只读分析自己负责的模块。

为什么这里必须用 Named Agent 而不是 Fork?因为审查 Agent 不需要知道你之前和 Claude 聊了什么——它只需要知道"去读哪几个文件"。Named Agent 从零开始,干净;Fork 继承你的对话历史,多余。

案例 2:探索 + 实现的流水线

案例 1 是"四个任务互不依赖"的并行模式。但现实中有很多任务是串行依赖的——先探索再实现,后一步需要前一步的输出。

# ❌ 错误:你说"帮我把 auth token 验证改成用 JWT,同时探索一下现在怎么实现的"
# → Claude 可能并行启动搜索和实现 → 实现 Agent 不知道搜索的发现

# ✅ 正确:
# 第一步:先搜索
用 Explore 找到项目中所有和 auth token 验证相关的实现,返回文件路径和函数名。

# 第二步:拿到搜索结果后,基于结果去改
# Claude 返回:tokenValidator.ts:42 用自定义 HMAC,session.ts:18 管理令牌生命周期
基于刚才 Explore 的结果,用 general-purpose Agent 把 tokenValidator.ts:42
的 HMAC 验证改成 JWT,同时更新 session.ts:18 的令牌生命周期逻辑。
# 注意这里 Claude 继承了对话上下文(Fork),知道 Explore 返回了什么

案例 3:Worktree 隔离的并行重构

案例 2 是串行流水线。现在回到并行——但这次每个 Agent 都会改文件,不再是只读。四模块重构可以并行,但需要各自独立的分支,互不污染。

# 你在 CLI 里说:
用四个 Agent 并行重构 user、product、order、payment 模块,
都改成 repository 模式。每个 Agent 用 worktree 隔离,
在自己的 git 分支上改。完成后告诉我各自的分支名。

Claude 内部给四个 Agent 各加 isolation: "worktree"。完成后每个 Agent 的改动在各自的临时分支上——你可以逐个 git diff 审查,不满意的直接删分支。四个重构互不干扰,也不用 stash 你当前的工作。

案例 4:影响面分析——堵住"正确代码、错误后果"的漏洞

前三个案例关注的是"怎么做"。案例 4 关注的是"该不该做"——用 Agent 在代码动工之前完成安全检查。

一个真实线上事故:开发者让 AI 对存量系统做功能迭代。代码本身没 bug,逻辑完全正确。上线后用户端 7 秒拿不到返回结果——新加的数据库查询增加了约 200ms 延迟,压垮了一个只剩 500ms 余量的 SLA 链路。

根因不是代码质量——是设计阶段缺少影响面分析

# 你说:
我准备重构 src/auth/tokenValidator.ts 的令牌验证逻辑。
先用 impact-analyzer 检查这个改动会影响哪些调用链,有没有 SLA 风险。

Claude 启动 impact-analyzer——这个 Agent 通过 skills: ["chain-knowledge"] 预加载了链路拓扑和 SLA 约束,能追踪每一层调用关系。它返回的分析报告会告诉你:这个改动会影响订单服务和支付回调链,SLA 余量只剩 300ms,你的改动可能让端到端延迟超限。

只有当影响面分析通过后,才启动修改 Agent。 这个流程把 Sub-agent 从一个"事后审查"的辅助角色,升级成了"事前拦截"的工程防线——不是代码写好后再检查,而是代码还没写就先堵漏洞。

流水线中的交接契约

案例 2 展示了一条串行流水线——Explore 找 → General-Purpose 改。当流水线拉长到三四个阶段时,上下游之间需要交接契约(Handoff Contract):上游为下游准备的结构化信息,让下游不需要重复任何搜索就能开始自己的分析。

反面教材:Bug Locator 输出"bug 可能在 auth 模块里" → Analyzer 收到后不得不自己又搜了一遍 → 流水线形同虚设。合格交接至少包含:具体文件路径、函数名、行号范围、搜索证据(搜过什么、排除了什么)、为什么怀疑这个位置。

扩展视角:从子代理到 Agent Teams

本文的 Sub-agent 有一个硬约束:子代理只能向主对话汇报,不能互相通信。 打破这个限制的是 Claude Code 的实验性功能 Agent Teams——下篇详解。


十、常见失败模式与源码级诊断

每个失败模式背后都有一个被源码证实了的心理误判。知道"为什么掉坑"比知道"坑在哪"更有用。

失败模式 1:Agent 消耗 token 却不返回有用结果

症状:Agent 运行了很久,做了很多工具调用,最终报告里信息很少——像是做了一大堆工作但没有总结。

心理根因:你以为 Agent 会"自然地"在最后做总结。LLM 没有总结本能——它只是在生成下一个 token。如果最后一轮恰好是工具调用,它不会"觉得"自己需要再补一段文字总结。

源码级原因finalizeAgentTool 优先提取最后一条 assistant 消息中的 text block(agentToolUtils.ts:301-303)。如果为空,会反向遍历所有历史 assistant 消息找第一个有 text 的(agentToolUtils.ts:307-315)——这个 fallback 能兜住一部分情况,但当 fallback 命中的是一条中间过程的思考而不是最终总结时,仍然拿不到有用的结果。根源还是 LLM 本身没有总结本能,以工具调用结束时不会自觉补一段文字。

解决方案:在 Agent 的 prompt 里明确要求"最后一条消息必须是文字总结,不要以工具调用结束"。不是你提示写得不够好——是提取逻辑本身只看最后一条消息。

失败模式 2:异步 Agent 被权限请求卡住

症状:异步 Agent 看起来卡住了,没有错误信息,没有进度,就像"死掉了"。

心理根因:你以为"后台运行 = 自动获得所有权限"。实际上后台运行的真相是"不能弹窗问你 → 自动拒绝 → Agent 不理解为什么被拒 → 重试 → 再次被拒 → 无限循环"。Agent 不会告诉你"我被权限卡住了",因为它的上下文里只有"操作失败了"。

源码级原因:当 shouldAvoidPermissionPrompts 为 true 时,需要权限确认的操作会被自动拒绝。Agent 不理解"拒绝"和"失败"的区别,继续用相同方式重试。

解决方案

  • 给异步 Agent 配置 permissionMode: 'dontAsk' 加上明确的 allowedTools(治本)
  • 或者用 permissionMode: 'bubble' 让权限请求冒泡到你的终端,但多 Agent 并行时一堆弹窗会让你困惑(治标)

失败模式 3:Fork 子 Agent 试图再 fork

症状:Fork 子 Agent 的对话突然终止,没有输出,也没有错误提示。

心理根因:你以为 Fork 就是"一个普通的 Agent,可以再调 Agent"。但 Fork 的本质是"克隆了主对话的上下文,带上防递归标记"。它的设计意图就是"只执行,不分发"——分发的责任在主对话。

源码级原因isInForkChild() 检测到对话中的 <fork_boilerplate> 标记,拒绝了 fork 请求。这不是 bug——所有编排必须由主对话完成,子 Agent 不能嵌套。

解决方案:Fork 子 Agent 收到的 boilerplate 里已经说了"Do NOT spawn sub-agents; execute directly"。如果你的任务确实需要多层 Agent 协作,用 Named Agent 而不是 Fork——让主对话作为唯一的编排者逐阶段调用。

这三个失败模式的共同根因:你把 Agent 当成了,但源码里它是一套机械规则。它不会"觉得该总结了"、"理解权限为什么被拒"、"知道不该再 fork"。每当 Agent 的行为不如预期,第一反应不是改进 prompt,而是去查对应的源码逻辑——通常答案就在几行代码里。


本篇实践任务

任务 1:解剖你项目中的 Agent 调用

在一个中等复杂度的项目上,让 Claude Code 做一个涉及搜索 + 修改的任务(比如"找到所有硬编码的 API URL 并替换为环境变量")。观察它是否主动使用了 Sub-agent,用的是哪种类型,prompt 是怎么写的。对比它的选择和你的直觉。

任务 2:写一个自定义 Agent 配置

.claude/agents/ 下创建一个只读的代码审查 Agent,配置 disallowedToolspermissionModemaxTurns。然后用它审查你最近的一次 commit,观察它的行为是否被配置正确约束了。

任务 3:测试 Worktree 隔离

对一个有测试的项目,启动两个 isolation: "worktree" 的 Agent 并行修改不同模块。完成后检查:各自的 worktree 分支是否独立?git log 是否只包含各自模块的修改?合并时是否有冲突?


下篇预告

第 10 篇:Agent Teams——当子 Agent 开始互相说话

本文讲的 Sub-agent 有一个硬约束:子 Agent 只能向主对话汇报,不能互相通信。而 Claude Code 的实验性功能 Agent Teams 打破了这个限制——Teammates 可以直接发消息、互相挑战结论、共享发现。下一篇讲 Agent Teams 的源码实现和四种核心协作模式:竞争假设、分层评审、模块化开发、规划审批。


AI Coding 系列持续更新。Sub-agent 不是让 Claude 做更多,而是让它记更少——噪声隔离有边界,编排决策有框架。

给 AI Agent 装上"长期记忆":Karpathy 的 LLM Wiki 思想,我做成了工具

你的 AI 每次对话都在重新推导知识。而一个由 Agent 自己维护、会复利增长的 Wiki,让它越用越聪明。

这篇文章不是教你怎么敲 CLI 命令。memex 的入口在 agent 对话里——你只需要说 /memex:capture/memex:ingest/memex:query,Agent 自己知道怎么做。


一、Karpathy 在 2026 年 4 月提出了一个思想

HE9kEdZaMAADLIU.jpg

Andrej Karpathy 是 OpenAI 创始团队成员、前 Tesla AI 总监。2026 年 4 月 4 日,他在 GitHub Gist 上发布了一篇 LLM Wiki Pattern,系统阐述了一个思想:

为什么人类用 Wiki 积累知识,而 AI 每次对话都在从零推导?

他的主张很直接:给 LLM 一个结构化 Markdown Wiki,让它自己维护。人类只负责往 raw/ 里扔源材料,LLM 负责把知识编译进 wiki/——更新概念页、建立交叉引用、标注矛盾、写综合页。每轮对话不是"检索",是"阅读一本已经写好的书"。

他打了个比方,传得很广:

"Obsidian is the IDE, the LLM is the programmer, the wiki is the codebase."

翻译过来就是:"Obsidian 是 IDE,LLM 是程序员,Wiki 是代码库。"

什么意思?你写代码时——IDE 是你的界面,程序员是写代码的人,代码库是持续构建的产物。类比到这里——Obsidian(或任意 Markdown 浏览器)只是你看知识的界面,LLM 才是真正写知识的人,Wiki 就是 LLM 持续构建和维护的知识产物。你不写 Wiki,你看 Wiki;LLM 不读 Wiki,LLM 写 Wiki。

Karpathy 的核心洞见其实用一句话就能说清——他把知识库当代码仓库管理:

软件工程 知识库工程
src/ raw/(原始资料,不可变)
build/ wiki/(编译产物,LLM 自动生成)
编译器 LLM(把 raw 编译成结构化 wiki)
IDE Obsidian / 任意 Markdown 浏览器
Lint / CI 健康检查(断链、矛盾、过期页)
增量编译 每次只 ingest 新增的 raw,不改旧文件

我是开发出身,第一眼看到这张表就懂了。这不就是 CI/CD 的知识库版本吗?

软件工程 → 知识库工程 映射

而 Karpathy 用了一个词来概括这一切——编译(Compile)。把原始资料编译成结构化知识。raw 是源码,wiki 是编译产物。你不会把 .class.java 混在一起,笔记也一样。

核心区别在于:RAG 每次重推,Wiki 持续复利。

这句话拆开看——

RAG LLM Wiki
知识形态 文档切片,无关联 结构化页面,交叉引用
更新方式 重新索引 Agent 直接编辑 Markdown
查询 向量相似度拼凑 读已组织好的页面
累积性 没有复利 每次 ingest 在旧知识上修改、关联
所有权 在厂商的向量库里 在本地 Git 仓库里

Karpathy 给的是思想。我把它做成了工程:memex


二、memex 怎么用?在 agent 对话里说话就行

最重要的概念先摆出来——

你不是在终端敲 memex distillmemex ingest。你是在 agent 对话框里说 /memex:capture/memex:ingest/memex:query。CLI 只在 Agent 脚下跑,你感觉不到它。

memex 提供了 6 个 slash command,覆盖完整的知识生命周期:

Slash Command 你做什么 Agent 做什么
/memex:capture 给 Agent 一个 URL、一段文字、一个文件 Agent 保存到 raw/,记录出处,不变形
/memex:ingest "把这些新东西消化进知识库" Agent 读 raw 源材料,更新 concept/entity/source 页面,写交叉引用,更新 index
/memex:query "关于 X,我们知道哪些?" Agent 搜 wiki,综合答案,带引用
/memex:distill "这次对话有不少好结论,存下来" Agent 把会话要点蒸馏成结构化 raw 笔记
/memex:lint "检查一下知识库健不健康" Agent 跑机械检查 + 语义扫描,报问题,修问题
/memex:status "看看知识库现在什么状态" Agent 报告页面数、最近变化、待处理项

你不需要记住命令参数。你只需要用自然语言告诉 Agent 你想干什么,Agent 自己调对应的 slash command。

别上来就搞 RAG

一提"AI + 笔记",很多人的第一反应是搭 RAG:选 Embedding 模型、搭向量数据库、调切片策略。整套架构搞了一个月,笔记库里还是只有 20 篇文章。

Karpathy 的思路反过来:先跑通流程,再优化基础设施。 知识库规模不大的时候(几百篇文章以内),维护几个索引文件就够了。LLM 先读 index.md 定位,再直接阅读相关内容。简单、可靠、零额外成本。等你的笔记真的过了一万条,搜东西开始找不到、找不全了,再考虑 RAG 不迟。

这道理写代码的人都懂,但轮到自己搭知识库的时候就忘了。

每次问答也能存回知识库

还有一个 Karpathy 特别强调的设计:好的问答结果应该存回 wiki,而不是消失在聊天记录里。 你问了一个复杂问题,Agent 查 wiki、综合答案、带引用——这个答案本身就是一份有价值的知识产物。把它存成新页面。下次类似问题,Agent 直接读已有的分析,不用重新推导。

你每跟 AI 聊一次,知识库就增加一层。这就是复利。

知识编译管线:capture → ingest → query → lint


三、五个场景:memex 到底能带来什么价值

下面这五个场景,是我自己用了三个月的真实感受。

场景 1:长期研究 —— 让知识库自己长起来

痛点:你在研究"Agent Memory vs RAG"这个话题,今天看一篇论文,明天读一个开源项目,后天和 AI 讨论两个小时。三周后你想写篇总结文章——发现所有讨论散落在十几个聊天窗口里,找不到线索。

怎么做

你:/memex:capture https://arxiv.org/abs/xxxx --scene research
你:读到新的论文或讨论出新想法时,继续 capture 进去
你:积累几份材料后——
你:/memex:ingest 把这些新研究材料消化进 wiki
你:/memex:query "agent memory 和 RAG 的设计取舍,我们目前知道哪些?"

你始终在 agent 对话里。Agent 负责:

  • 把每篇论文、每次讨论存成 raw/research/ 下的源文件
  • ingest 时把新知识合并进 concepts/agent-memory.md、更新对比页 summaries/agent-memory-vs-rag.md
  • query 时综合 wiki 里的所有内容,带引用回答

价值:三周后,你拥有的不是十几个聊天窗口,而是一个结构化的知识地图——概念定义、方案对比、源材料索引、开放问题清单。写文章时,直接 /memex:query "agent memory 技术路线对比"

场景1:长期研究 — 知识库随时间生长

场景 2:长期项目 —— 让项目记忆可继承

痛点:你的项目已经迭代了三个月。今天用 Claude Code,明天用 Codex,后天用 Cursor。每个新 Agent 都要重新理解架构、踩过的坑、命名的原因、测试的边界。

怎么做

你:帮我连接这个项目到 memex 知识库
Agent:安装项目级别的 context 文件,记录相关的 scene

你:读当前代码和文档,然后起草这个项目的 architecturecommand-designknown-pitfalls 页面
Agent:读源码,写带有文件路径引用的 code-reading 笔记到 raw/

你:/memex:ingest 把这次 code-reading 结论写进项目 wiki
Agent:更新架构决策页、命令设计页、已知坑页、测试契约页

每次新会话开始:

你:/memex:query "继续 ai-memex-cli 网站和文档工作"
Agent:从 wiki 拉出最近的 handoff 笔记、未完成的任务、需要遵守的测试契约
你:从上次中断的地方继续

价值:项目知识不再是散落在聊天里的只言片语。新 Agent 开局就能回答"为什么这么设计"、"哪些地方容易踩坑"、"上次改到哪了"。代码仓库本身就是 source of truth,wiki 存的是 Agent 从代码、文档、issue、反馈中提炼出来的可继承理解

场景2:长期项目 — 三个 Agent 共用一个 wiki

场景 3:跨会话继承 —— 多次会话之间携带上下文

痛点:今天 Claude Code 做了一半,明天 Codex 继续,后天出差回来用 Cursor 检查。每个新会话都是一个黑洞——上下文全丢。

怎么做

你:/memex:distill 这次 Codex 会话,写清楚做到了哪、下一步做什么、有没有阻塞
Agent:找到当前 agent 的会话数据,蒸馏成 raw/sessions/ 下的结构化笔记

你:/memex:ingest 把这次 handoff 合并进项目记忆
Agent:更新项目 wiki 中的进度页和 log.md

——第二天,换了一个 agent——

你:/memex:query "上次中断的工作,下一步是什么"
Agent:从 wiki 里拉出 handoff 笔记和未完成项

跨 Agent 完全无感——Claude Code 写的,Codex 能读;Codex 补充的,Cursor 继续改。它们不共享一个聊天窗口,它们共享 raw/wiki/index.mdlog.md

价值:连续性不再绑定任何一个厂商。你可以换 Agent、换模型、等一周再回来,任务状态还在同一个 wiki 里等你。

场景3:跨会话继承 — 有无 memex 的对比

场景 4:对话沉淀 —— 把聊天里的好结论留下

痛点:一场深入对话里,你们讨论了产品定位、架构边界、某个 bug 根因、三个被否决的方案。聊完很爽,一周后只记得大概——细节全丢了。

怎么做

你:/memex:distill 这次对话,我们聊清楚了产品定位和几个关键的取舍
Agent:把对话蒸馏成 source 页,保留上下文、决策、未解问题

你:/memex:ingest 只要这次确定的稳定结论,合并到已有的 positioning 页面里
Agent:读蒸馏产物,提取可复用的结论,增量更新已有页面,不复制已有内容

什么样的结论值得沉淀?

  • 产品定位:怎么描述产品、避免用什么说法
  • 架构边界:为什么 CLI 不做语义层、为什么 raw 不可变
  • Bug 根因:排查路径、实际原因、回归测试要点
  • 被否决的方案:为什么没选、当时的前提是什么

价值:聊天不再是消耗品。重要推理先变成可追溯的 source,再变成结构化的 wiki 知识。下次 query 时,能同时看到结论和它为什么成立。如果前提变了,wiki 也能记录"老判断基于什么、新判断基于什么"。

场景4:对话沉淀 — 从聊天到 wiki 的蒸馏流

场景 5:结构化维护 —— 让 Agent 持续维护知识,而不是只回答一次

痛点:大部分人用 AI 的模式是"问一次答一次"。知识在回答完后原地消失。没人去更新、去合并重复页、去修断链、去标记过期内容。

怎么做

你:/memex:status
Agent:报告 vault 整体健康状况——页面数、最近更新的 source、哪些页面过时了、哪些维护任务待处理

你:/memex:lint 检查断链、孤儿页、过期页、缺失的 frontmatter
Agent:跑机械 lint(路径、链接、frontmatter 正确性)+ 语义扫描(矛盾、重复、过时论断)
你:机械问题直接修,语义问题先给我看方案
你:把 Karpathy 的 LLM Wiki gist 加入知识库
Agent:capture 源文件 → 创建 concept 页 → 更新相关页面交叉引用 → 写 log
你:告诉我改了什么,还有什么需要 review

价值:Wiki 不是一堆文件的堆积。它是一个被持续维护的结构化系统。每次 Agent 用它,也能同时改善它。重复页被合并或标注、孤儿页被找到、断链被修复、index.md 是真正的导航入口而非文件列表。

场景5:结构化维护 — lint 健康检查的四个维度


四、Agent 和 CLI 的分工边界

这里有一个设计决策需要讲清楚——CLI 永远只做机械正确性的事,不做语义判断。

谁负责 做什么
Agent Claude Code / Codex / Cursor 判断哪些页面要更新、哪些概念要链接、哪些矛盾要保留、哪些总结要重写
Slash Command /memex:capture 等 6 个 把用户的自然语言意图翻译成底层 CLI 调用
CLI memex 命令行工具 文件读写、frontmatter 校验、链接检查、关键词搜索、会话解析——纯机械,不调 LLM API

这意味着:

  • 你的知识不绑定任何厂商——Agent 可以换,wiki 不变
  • 你的知识是 Git 化的 Markdown——可以 diff、可以 blame、可以回退
  • CLI 永远不帮你做语义决策——"这两个页面是不是该合并"这种问题,Agent 自己判断但会问你

memex 三层架构:raw(不可变)→ wiki(编译)→ 输出

CLI 的补充能力

上面 6 个 slash command 覆盖日常 90% 的交互。CLI 底层还提供几个高阶能力,但不建议作为日常入口:

CLI 命令 用途 说明
memex watch 自愈守护进程 监听 raw/ 变化,自动触发 ingest → lint 循环。适合长期跑
memex inject 上下文注入 会话开始前,按任务描述从 wiki 拉最相关页面注入当前上下文
memex install-hooks 安装 Agent hooks 把 SessionStart / SessionEnd hook 写入 Agent 配置,自动 distill 和 inject
memex search 命令行搜索 全文搜索 wiki,适合脚本化场景

但这些不是入口。日常入口是 agent 对话框,是说 /memex:query 而不是敲 memex search


五、两周跑通最小闭环

如果你想试,不需要什么额外工具。装好 memex,在你的 Agent 里说话就行。

第一周:搭 raw → wiki 的最小循环。 装好 memex,运行 memex onboard。然后开始往知识库喂东西——看到好文章、好推文、好想法,直接对 Agent 说 /memex:capture。攒够 5 到 10 条后,说 /memex:ingest 把这些新素材消化进知识库。Agent 会生成摘要、提取概念、更新索引。

第二周:让问答开始积累,跑第一次健康检查。 每次对知识库做复杂提问,结果让 Agent 存回 wiki。然后说 /memex:lint 给知识库做一次全面体检。Agent 会扫出断链、矛盾、过期页、孤儿页——先让它修机械问题,语义问题你看一下再决定。

两周之后你有一个能持续运转的小系统。规模不重要,流程跑通了就行。后面就是往 raw/ 里不断喂素材,让 Agent 持续编译。


六、知识库的"GitHub 时刻"

回到 Karpathy。他那篇 Gist 的最后一句话是:

这套东西目前仍然像一堆 hacky scripts,但有空间做成 incredible new product。

我想到 2006 年前的版本控制。那时候也是 svn、cvs、git 命令行,只有程序员在用。然后有人把它做成了 GitHub,整个协作方式都变了。

个人知识库可能正在类似的节点。今天它是 Obsidian + LLM + 手搓脚本的组合,看起来还很粗糙。但底层范式已经有了:把知识当代码管理。 有输入,有编译,有产物,有测试。

如果你是程序员,好消息是你不需要学任何新东西。代码仓库怎么管,知识库就怎么管。你积累了这么多年的工程直觉,终于可以用在自己的笔记上了。

Karpathy 原文里还有一段话:

人类放弃 Wiki 是因为维护负担的增长速度永远超过它带来的价值。你得亲手写每个页面、手动保持一致性、记住所有交叉引用。

但 LLM 不会无聊。它可以一次触碰 10-15 个页面,把新知识合并进去,更新索引,同时保持系统自洽。

人的工作:策展、取舍、提问、思考。LLM 的工作:剩下的全部。

memex 做的,就是把这句话变成可以跑的东西。

别让你的笔记腐烂。让它们被编译。


快速开始:

npm install -g ai-memex-cli
memex onboard

然后在你的 Claude Code / Codex / Cursor 里说第一句话:

你:/memex:capture https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f --scene research
你:/memex:ingest Karpathy 的 LLM Wiki 思想,作为 research 场景的第一份材料
你:/memex:query "Karpathy 的 LLM Wiki 核心思想是什么?"

给 AI 一份会生长的记忆。


项目地址: github.com/zelixag/ai-…

理念来源: Karpathy's LLM Wiki Pattern

❌