Claude Code Skill 机制深度解析:一个 Skill 的完整生命周期

3 阅读22分钟

Claude Code Skill 机制深度解析:一个 Skill 的完整生命周期

📍 导航指南

根据你的背景,选择合适的阅读路径:


目录

第一部分:Skill 是什么 🤔

第二部分:工作机制全景 🗺️

第三部分:加载阶段 📂

第四部分:首轮对话 💬

第五部分:Model 调用 Skill 🎯

第六部分:内容注入 📝

第七部分:压缩后恢复 🔄

第八部分:设计思想总结 💡


第一部分:Skill 是什么 🤔

Skill 的本质

Skill 的本质:一个可被 model 调用的 Markdown 指令包。

它不是一段可执行代码,而是封装了某个任务完整工作方法的模块。一个 skill 包含:

  • Markdown 指令内容:工作流程、检查清单、最佳实践
  • 元数据(frontmatter):描述、触发条件、路径规则
  • 参数系统${1}, ${2} 等占位符,调用时替换
  • 内联 shell!命令`` 语法,执行动态内容
  • 变量替换${CLAUDE_SKILL_DIR}, ${CLAUDE_SESSION_ID}
  • 附属文件(可选):配置、模板等

当 skill 被调用时,系统把处理后的内容注入到对话里,model 读到这些指令后按照指令执行。

关键点:调用者可以是用户(手动输入 /skill-name),也可以是 model 自己(看到 skill 列表后自主决定调用)。两种方式底层走的是同一套流程——都通过 SkillTool 把调用转化成 Markdown 指令注入对话。这是 skill 和传统命令的本质区别——model 有主动权。

Skill vs Tool

从调用方式看,Skill 也是通过 Skill tool 调用的,表面上"就是一个工具"。但 SkillTool 不是"功能执行器",而是"指令注入器"——这是核心区别。

执行流程对比

标准 Tool(如 Bash):
Model 调用 → 系统执行代码 → 返回结果 → Model 继续

Skill(如 systematic-debugging):
Model 调用 → SkillTool 注入 Markdown 指令到对话 → Model 读到指令 → Model 自己执行

维度对比

维度标准工具(Tool)Skill
本质确定性的可执行代码Markdown 指令包
SkillTool 的角色N/A只是"加载器",把 Markdown 注入对话
执行主体系统代为执行,返回结果model 读到指令后自己执行
用户可扩展性❌ 需要改源码并编译✅ 写 .md 文件放到 .claude/skills/
生命周期启动时静态注册运行时动态发现、按需激活
Token 消耗相对固定渐进式:最高单次 5k,支持压缩恢复
核心作用扩展 model 的"手"(物理操作能力)扩展 model 的"脑"(工作方法论)

实际意义:你可以写一个 my-code-review.md,立刻就能用。但你写不了一个新的 MyBashTool——那需要改 Claude Code 源码。Skill 是用户可编程的"方法论库",Tool 是系统内置的"能力库"。

小结:Skill 是 Markdown 指令包,不是可执行代码。它通过注入提示词的方式告诉 model "该怎么做",而不是代替 model 执行任务。Skill 和 Tool 的本质区别在于:Tool 扩展能力,Skill 扩展方法论。


第二部分:工作机制全景 🗺️

在深入每个环节之前,先建立完整的流程框架。一个 skill 从文件到被 model 使用,经历五个关键阶段。

三个核心问题

Skill 机制要解决三个根本问题:

  1. 发现问题:model 怎么知道有哪些 skill 可用?
  2. 调用问题:model 决定用某个 skill,怎么找到它、执行它?
  3. 持久化问题:压缩后 skill 内容会丢失吗?

完整时序

【启动阶段】第三部分
加载 skill 文件 → 解析出名字和描述 → 筛选出符合规范的
  ↓
【首轮对话】第四部分
分析用户消息,推荐相关 skill → 把完整 skill 列表告诉 model
  ↓
【Model 调用并注入】第五-六部分
Model 通过 SkillTool 选择某个 skill → 系统找到对应的 Markdown 文件
→ 读取并处理文件内容 → 把最终文本作为消息注入对话
  ↓
Model 在下一轮读到 skill 指令 → 按指令执行任务
  ↓
【压缩恢复】第七部分
压缩时历史清空 → 从记录中重建 skill 内容 → 重新注入
已发送列表保持不变(避免重复)
sequenceDiagram
    autonumber
    participant FS as 文件系统
    participant Loader as Skill 加载器
    participant Agent as Agent 核心
    participant Model as Claude 模型

    Note over Loader,FS: 启动阶段(仅读 frontmatter)
    Loader->>FS: 扫描 5 个目录
    FS-->>Loader: skill 文件列表
    Loader->>Loader: 解析 name / description,过滤不合规的

    Note over Agent,Model: 首轮对话
    Agent->>Model: 注入 skill_listing attachment(可用 skill 列表)

    Note over Agent,Model: Model 主动调用
    Model->>Agent: tool_use(name:"skill", input.skill:"commit")
    Agent->>Loader: getPromptForCommand(args)
    activate Loader
    Note over Loader: 参数替换 / 变量替换 / 内联 shell 执行
    Loader-->>Agent: 完整 Markdown 文本
    deactivate Loader
    Agent->>Agent: addInvokedSkill() 写入全局 state
    Agent->>Model: isMeta:true UserMessage 注入
    Model->>Model: 读到指令,按 skill 执行任务

    Note over Agent,Model: 压缩触发
    Agent->>Agent: createSkillAttachmentIfNeeded()<br/>从 invokedSkills 按时间戳重建
    Agent->>Model: invoked_skills attachment(上限 25k tokens)

设计原则

这五个阶段背后有几个一以贯之的设计原则:

  • 渐进式投递:不是一次性 dump 所有 skill,而是按需、增量、事件驱动
  • Token 预算意识:listing 8k、单 skill 5k、总计 25k,每个数字都是精心权衡
  • Agent 隔离:sentSkillNames、invokedSkills 都是 per-agent,子 agent 有独立状态
  • 性能优先:prefetch 与 streaming 并行,用户感知不到 discovery 延迟

小结:Skill 的完整生命周期分为五个阶段:加载(发现文件)→ 首轮对话(告知 model)→ Model 调用(选择使用)→ 内容注入(指令进入对话)→ 压缩恢复(持久化保证)。核心设计原则是渐进式、Token 预算管理、Agent 隔离。

接下来,逐节展开每个阶段的实现细节。


第三部分:加载阶段 📂

核心问题

启动时要解决两个问题:

  1. 去哪找 skill 文件:用户可能在多个地方存放 skill
  2. 是否所有 skill 都对 model 可见:加载 ≠ 可用,中间有过滤

扫描 5 个位置

启动时按优先级依次扫描,用 realpath 去重,同一文件不加载两次:

优先级目录说明
1getManagedSkillsDir()系统托管(官方 skill,禁用内联 shell)
2getUserSkillsDir()~/.claude/skills/用户全局 skill
3getProjectSkillsDir().claude/skills/项目级 skill
4getAdditionalSkillDirs()配置文件额外指定的目录
5getLocalCommandsDir()旧版 commands 目录(向下兼容)

解析出名字和描述

每个 skill 文件被读取并解析,从 Markdown frontmatter 提取关键信息:

---
name: create
description: Create git commits following conventional commit format
when_to_use: When you need to commit changes
---

解析为 Command 对象:

{
  name: 'commit',                        // 从文件路径推导
  description: 'Create git commits...',  // 从 frontmatter 读取
  whenToUse: 'When you need to...',      // 从 frontmatter 读取
  getPromptForCommand: async (args) => { // 被调用时加载完整 Markdown
    // 参数替换、变量替换、内联 shell
  }
}

关键:此时只读取了 frontmatter(名字、描述),完整的 Markdown 正文要等 model 调用时才通过 getPromptForCommand 加载。这就是渐进式加载——启动阶段只准备"目录",不加载"全文"。

条件激活的底层机制:基于目录/文件的动态发现

条件 skill 并不在启动时全部加载,而是通过**动态附件(Dynamic Attachment)**机制按需激活。当用户的当前工作目录或操作文件匹配特定模式时,系统实时扫描对应目录下的 skill 文件,将其追加到当前 agent 的可用列表中。

例如,以下 frontmatter 配置会让一个 skill 仅在 packages/database/ 目录下被激活:

---
name: db-migration
description: Generate and run database migrations
when_to_use: when working in packages/database/
paths: ["packages/database/**"]
---

每当用户 cd 进入该目录或其子目录,或在其中读写文件时,系统会触发 getDynamicSkillAttachments() 扫描匹配的 skill,并将新 skill 的名称与描述通过 skill_listing attachment 注入对话。model 随后就能自主调用它。

整个流程是事件驱动的:目录切换 / 文件变更 → 路径模式匹配 → 解析 skill → 增量发送 listing,用户和 model 都无需感知加载过程,实现了真正的"上下文感知 skill"。

过滤规则

getSkillToolCommands 过滤出 model 可见的 skill:

return commands.filter(cmd =>
  cmd.type === 'prompt' &&
  !cmd.disableModelInvocation &&
  cmd.source !== 'builtin' &&
  (cmd.whenToUse != null || cmd.hasUserSpecifiedDescription)
)

关键:没有 descriptionwhenToUse 的 skill,model 看不到。

小结:加载阶段扫描 5 个目录,解析 frontmatter 提取名字和描述,过滤出标注清楚用途的 skill。此时只加载"目录",不加载"全文",这是渐进式加载的第一步。


第四部分:首轮对话 💬

核心问题

加载完 skill 后,model 不会自动知道它们的存在。需要有机制把"可用 skill 列表"告诉 model。

核心机制

<system-reminder>
The following skills are available for use with the Skill tool:

- commit: Create git commits following conventional commit format
- systematic-debugging: Use when encountering any bug or test failure
- ...
</system-reminder>

这是一个动态注入的 attachment,不是 system prompt 的一部分。

首轮发送完整列表,后续只发新增的:系统用 sentSkillNames 记录"已发送过哪些 skill",避免重复发送浪费 token。Model 能看到的 skill = 对话上下文里所有 skill_listing attachment 的累积。

小结:首轮对话通过 system-reminderskill_listing attachment 把可用 skill 列表告诉 model。后续只发送新增的 skill,已发送的通过 sentSkillNames 记录避免重复。


第五部分:Model 调用 Skill 🎯

核心问题

Model 决定用某个 skill 后,系统怎么找到它、加载它的完整内容?

SkillTool 的完整调用链

Model 通过 SkillTool 发出调用请求(标准 tool_use 格式):

{
  "type": "tool_use",
  "name": "skill",
  "input": { "skill": "commit", "args": "--amend" }
}

name 固定是 "skill"(SkillTool 的名称),input.skill 是具体 skill 名称。

SkillTool 收到请求后,调用层级如下:

SkillTool.call()
  └─ findCommand()                    // 按名精确匹配
       └─ fork? executeForkedSkill()
          else  processPromptSlashCommand()
                  └─ getPromptForCommand()   // 读取完整 Markdown
                       └─ addInvokedSkill()  // 写全局 state
                            └─ createUserMessage({ isMeta: true })

逐步拆解:

  1. findCommand("commit", commands) — 精确名称匹配。找不到直接报错,不猜测相似名称。

  2. fork 分叉 — 如果 skill 的 frontmatter 设置了 context: fork,走 executeForkedSkill(),在独立子 agent 中执行,不影响主对话上下文。默认走内联路径。

  3. getPromptForCommand(args) — 这里才真正读取 Markdown 文件全文。在这之前,系统只知道 skill 的名字和描述。读取完成后执行参数替换、变量替换、内联 shell。

  4. addInvokedSkill() — 把 skill 名称、路径、处理后的内容写入全局 state,key 是 agentId:skillName。不是给当下用的,是为压缩后恢复准备的。

  5. createUserMessage({ isMeta: true }) — 把 skill 内容包装成一条 isMeta 消息(可以理解为"系统塞给 model 的小纸条,用户在界面上看不到")注入对话流。Model 下一轮就能读到。

SkillTool 的本质:把 model 的一个"工具调用"(tool_use)转化成一段"Markdown 指令注入"(isMeta UserMessage)。Model 发出的是命令动作,SkillTool 把它转换成提示词,再交还给 model 执行。这是 skill 机制的核心转换点。

小结:Model 通过 SkillTool 发出调用请求,系统通过 findCommand 精确匹配找到 skill,调用 getPromptForCommand 加载完整内容,注册到 invokedSkills,最后以 isMeta UserMessage 注入对话。SkillTool 的本质是把"工具调用"转化成"指令注入"。


第六部分:内容注入 📝

问题

skill 找到了,内容处理好了,怎么让 model 在下一轮能读到这些指令?直接修改 system prompt?还是别的方式?

isMeta UserMessage

skill 内容被包装成一条特殊的 UserMessage 注入到对话流里:

createUserMessage({ content: finalContent, isMeta: true })

isMeta: true 标记这条消息是系统内部注入的——可以理解为"系统塞给 model 的小纸条":用户界面上看不到,但 model 在下一轮对话时完整可见。

整个注入链路:

getPromptForCommand(args, context)
    ↓
substituteArguments()       // ${1}, ${2} 参数替换
    ↓
变量替换                     // ${CLAUDE_SKILL_DIR}, ${CLAUDE_SESSION_ID}executeInlineShellCommands() // !`命令` 内联 shell 执行
                             // 仅用户/项目级 skill 开放,系统托管 skill 禁用addInvokedSkill()            // 注册到全局 state(key: agentId:skillName)
    ↓
isMeta UserMessage 注入到对话

addInvokedSkill:为什么要注册?

export function addInvokedSkill(skillName, skillPath, content, agentId = null) {
  const key = `${agentId ?? ''}:${skillName}`  // per-agent key
  STATE.invokedSkills.set(key, {
    skillName, skillPath, content,
    invokedAt: Date.now(),                      // 时间戳,压缩时排序用
    agentId,
  })
}

注册的目的不是当下,是为了压缩后恢复——这是下一节的核心。

小结:Skill 内容通过 isMeta UserMessage 注入对话,经过参数替换、变量替换、内联 shell 执行等处理。addInvokedSkill 把处理后的内容注册到全局 state,为压缩后恢复做准备。


第七部分:压缩后恢复 🔄

问题

Claude Code 的 context 压缩会清空历史对话(保留摘要)。

压缩触发条件:当对话上下文 token 占用达到阈值时(通常是模型窗口的 70-80%),系统自动触发压缩,将早期消息摘要化。用户也可通过 /compact 命令手动触发。

skill 内容是以 isMeta UserMessage 形式存在对话历史里的,压缩后这条消息消失了——model 忘了它调用过什么 skill,忘了 skill 里的指令。

这个问题在长对话中非常实际:用户调用了 systematic-debugging,跑了几十轮之后触发压缩,下一轮 model 按什么规则继续调试?

invoked_skills attachment

压缩触发后,createSkillAttachmentIfNeededinvokedSkills state(一直保留,不随压缩清除)里重建 skill 内容:

export function createSkillAttachmentIfNeeded(agentId?: string) {
  const invokedSkills = getInvokedSkillsForAgent(agentId)
  if (invokedSkills.size === 0) return null

  // 按最近调用时间排序,优先保留最近用的
  let usedTokens = 0
  const skills = Array.from(invokedSkills.values())
    .sort((a, b) => b.invokedAt - a.invokedAt)
    .map(skill => ({
      name: skill.skillName,
      content: truncateToTokens(skill.content, 5_000),  // 单 skill 上限
    }))
    .filter(skill => {
      const tokens = roughTokenCountEstimation(skill.content)
      if (usedTokens + tokens > 25_000) return false    // 总预算上限
      usedTokens += tokens
      return true
    })

  return createAttachmentMessage({ type: 'invoked_skills', skills })
}

Token 预算:

  • 单个 skill:最多 5,000 tokens(超出部分截断,保留开头的指令)
  • 所有 skill 合计:最多 25,000 tokens
  • 超出总预算的 skill 直接丢弃(最久未用的先丢)

为什么压缩后不重置 sentSkillNames?

如果重置,下一轮会重新发一次完整 skill_listing(约 4k token)。但这不只是 token 的问题——更深层的原因是认知负载:压缩后 context window 已经被清空,此时让 model 重新面对全量 skill 列表,不如让它直接聚焦在 invoked_skills(正在使用中的指令内容)上。后者信息密度更高,model 不需要再做一次"从列表中选哪个"的判断。

小结:压缩触发后,createSkillAttachmentIfNeededinvokedSkills state 重建内容,生成 invoked_skills attachment。单个 skill 最多 5k tokens,总计最多 25k tokens,按时间戳排序优先保留最近使用的。压缩后不重置 sentSkillNames,避免重复发送列表和增加认知负载。


第八部分:设计思想总结 💡

如果说 Tool 是 Claude 的"手",直接操作环境;那么 Skill 就是它的"工作手册"——一套被精心编排、可在需要时即时查阅的操作规程。这一系列设计决策,正是为了在 Token 预算、响应速度与智能体自主性之间找到最佳平衡。

经过七个部分的分析,现在可以回答:Claude Code 的 skill 机制为什么这么设计?这些设计决策背后的权衡是什么?

1. Skill 内容是 UserMessage,不是 SystemPrompt

决策:skill 内容通过 isMeta: true 的 UserMessage 注入,而不是写进 system prompt。

为什么

  • 时机控制:system prompt 在每轮都存在,无法控制"何时出现"。UserMessage 可以精确控制在哪一轮注入,实现按需加载。
  • 动态性:system prompt 在对话开始时固定,skill 是运行时动态选择的。如果 skill 内容在 system prompt 里,就失去了"model 自主选择 skill"的能力。
  • token 效率:只有被调用的 skill 内容才进入上下文,未用的 skill 不占用 token。

这个决策让 skill 从"启动时配置"变成了"运行时工具"。

2. Model 是主动调用者,不是被动接收者

决策:把 skill 列表发给 model,让 model 通过 SkillTool 自主调用,而不是系统预判"用户意图 → 自动注入对应 skill"。

为什么

  • 灵活性:model 可以根据对话上下文、当前任务阶段、已有信息动态决定是否需要 skill,比规则匹配更智能。
  • 可组合性:model 可以在一次对话中调用多个 skill、按顺序调用、甚至根据前一个 skill 的结果决定下一个。
  • 可扩展性:新增 skill 只需要加文件,不需要修改调用逻辑——model 自动"学会"用新 skill。

这是 Claude Code skill 和传统 macro/snippet 系统的本质区别:从"预设规则触发"到"智能体自主决策"。

3. 渐进式加载的四个阶段

决策:启动过滤 → 首轮全量 → 后续增量 → 按需激活,而不是"启动时一次性发完所有 skill"。

为什么

  • Token 压力:100+ skill 的完整列表可能超过 20k token,每轮都发会耗尽预算。
  • 信息过载:model 一次看到所有 skill,难以找到当前相关的。渐进式加载让 model 在每个时刻只看到"当前上下文相关"的 skill。
  • 性能:prefetch 与 streaming 并行,discovery 不阻塞主流程。
  • 适应性:动态发现让 monorepo、插件系统等场景自然工作。

这个设计体现了"lazy evaluation"思想:不提前计算不需要的东西。

4. sentSkillNames 的粒度是 agentId

决策Map<agentId, Set<string>>,每个 agent 独立追踪已发送的 skill 集合。

为什么

  • 信息完整性:子 agent 不会因为主线程已发送而漏掉 skill 列表。每个执行单元有独立的知识状态。
  • 隔离性:主线程用了 100 个 skill,子 agent 只看到自己需要的 5 个,不会被噪声淹没。
  • 正确性:避免"主线程发过、子 agent 以为自己知道但实际没见过"的幽灵状态。

这是分布式系统里"每个节点独立状态"的经典设计。

5. 压缩后不重置 sentSkillNames

决策:压缩触发后,invokedSkills state 重建内容注入,但 sentSkillNames 保持不变。

为什么

  • Token 优化invoked_skills attachment 已经覆盖了最重要的内容(25k token 预算),重新发 skill_listing(4k token)是纯浪费。
  • 信息不丢失:model 从 invoked_skills 里读到了具体指令内容,比 skill 列表更有价值。
  • 一致性:避免压缩前后 model 看到的信息"先详细后简略"的割裂感。

这个决策是"最小化冗余"原则的体现。

6. 条件激活 + 动态发现的组合

决策:skill 不只是"启动时加载的静态列表",还有两种运行时激活机制。

为什么

  • 上下文相关性:数据库迁移 skill 只有在碰到 migration 文件时才有意义,平时不应该占据 model 的注意力。
  • 可扩展性:monorepo 的每个子项目可以有自己的 skill,不需要全局注册。
  • 避免预配置负担:不需要在启动时"猜测"用户会用到哪些目录,运行时自动发现。

这是"event-driven"架构在 skill 加载上的应用。

给开发者的启示

如果你在构建类似的 agent harness,这些设计值得参考:

  1. 指令包 > 可执行代码:让 model 自主选择、组合工具,而不是硬编码调用逻辑
  2. 渐进式 > 一次性:按需加载、增量更新,在 token 和信息质量之间找平衡
  3. 隔离 > 共享:每个执行单元独立状态,避免状态污染
  4. 并行 > 串行:把耗时操作(discovery)和主流程(streaming)并行,用户感知不到延迟
  5. token 预算是一等公民:每个数字(8k/5k/25k)都是权衡的结果,不是随意拍脑袋

小结:Claude Code 的 skill 机制体现了六大设计原则:Skill 内容作为 UserMessage 而非 SystemPrompt(时机控制)、Model 主动调用而非被动接收(智能决策)、渐进式加载(lazy evaluation)、per-agent 状态隔离(分布式设计)、压缩后不重置列表(最小化冗余)、条件激活+动态发现(event-driven)。这些原则共同构建了一个高效、灵活、可扩展的 skill 系统。


关键源码路径

文件作用
src/skills/loadSkillsDir.ts五级目录扫描、createSkillCommand、条件 skill 激活
src/commands.tsgetSkillToolCommands 过滤逻辑
src/utils/processUserInput/processSlashCommand.tsxfindCommand 精确匹配
src/attachments.tsgetSkillListingAttachments、sentSkillNames、getDynamicSkillAttachments
src/bootstrap/state.tsaddInvokedSkill、invokedSkills state
src/compact.tscreateSkillAttachmentIfNeeded、压缩后恢复逻辑
src/query.tsprefetch 启动与收集(Turn-0 vs Inter-turn 差异)

附录:最小可用 Skill 完整案例

下面演示一个完整 skill 的生命周期:从文件创建、触发调用、内容注入,到压缩恢复。

1. 创建 skill 文件

在项目根目录下创建 .claude/skills/conventional-commit.md

---
description: Create git commits following conventional commit format
when_to_use: When you need to commit staged changes
---

# Conventional Commit Skill

Follow these steps to create a commit:

1. Run `git diff --staged` to review staged changes.
2. Determine the type: feat, fix, docs, refactor, test, chore, etc.
3. Write a commit message in the format: `type(scope): description`
4. If the user provided a specific instruction (`${1}`), prioritize it.
5. Run `git commit -m "<message>"` and confirm success.

2. 启动加载

启动 Claude Code 后,loadSkillsDir 扫描到该文件,解析出:

  • name: conventional-commit(从文件名推导)
  • description: Create git commits following conventional commit format
  • whenToUse: When you need to commit staged changes

过滤检查通过,进入可用列表。

3. 首轮对话:model 发现 skill

用户说"帮我提交代码"。首轮 system-reminder 注入 skill_listing

The following skills are available for use with the Skill tool:

- conventional-commit: Create git commits following conventional commit format
- ...

Model 看到后决定调用。

4. Model 调用 Skill

Model 发出工具调用:

{
  "type": "tool_use",
  "name": "skill",
  "input": { "skill": "conventional-commit", "args": "" }
}

5. 内容注入

SkillTool 执行 getPromptForCommand,读取完整 Markdown(此时无参数替换和内联 shell),包装为 isMeta: true 的 UserMessage 注入对话。Model 在下一轮读到指令,开始逐步执行。

6. 压缩恢复

假设对话很长触发了压缩。压缩后历史清空,但 invokedSkills state 中仍有记录。createSkillAttachmentIfNeeded 重建 invoked_skills attachment,将完整指令(截断到 5k tokens)重新注入上下文,model 可以继续按规范提交。


本文源码基于 Claude Code 源码分析,完整源码笔记见 learn-claude-code/notes/claude-code-skill在queryloop中的实现.md

附录:Claude Code 核心组件速查 🔧

在阅读本文时,你会频繁遇到这些 Claude Code 内部组件。这里提供快速参考,帮助理解它们在 Skill 机制中的角色。

Attachment(附件)

是什么:Claude Code 在对话中注入的结构化元数据,不显示在用户界面,但 model 可见。

典型用途

  • skill_listing attachment:可用 skill 列表
  • invoked_skills attachment:压缩后恢复的 skill 内容
  • system-reminder attachment:动态注入的系统提示

为什么需要它

  • 与 system prompt 分离,可以动态增删
  • 支持分类管理不同类型的上下文信息
  • 不占用用户可见的对话历史

在 Skill 中的作用

  • 首轮对话通过 skill_listing attachment 告知 model 可用的 skill
  • 压缩后通过 invoked_skills attachment 恢复已调用的 skill 内容

agentId(Agent 标识符)

是什么:每个执行单元(主 agent 或子 agent)的唯一标识符,格式通常是 UUID。agentId 通常在 agent 创建时由系统随机生成(基于 UUID v4),子 agent 会获得全新的独立标识。

为什么需要它

  • 支持多 agent 并发:主线程和多个子 agent 同时工作
  • 状态隔离:每个 agent 有独立的 skill 列表、已调用记录
  • 正确性保证:避免"主线程发过、子 agent 没收到"的状态不一致

在 Skill 中的作用

  • sentSkillNames 的 key:Map<agentId, Set<skillName>>
  • invokedSkills 的 key:${agentId}:${skillName}
  • 子 agent 创建时分配新 agentId,继承空的 skill 状态

实际场景

agent (agentId: abc-123)
  └─ 已发送 skill: [commit, debug, test]
  └─ 已调用 skill: [debug]agent (agentId: def-456)  
  └─ 已发送 skill: []  // 独立状态,需要重新发送列表
  └─ 已调用 skill: []

Compaction(上下文压缩)

是什么:当对话历史超过 token 限制时,Claude Code 自动触发的上下文清理机制。

工作原理

  1. 保留最近 N 轮对话
  2. 将早期对话压缩成摘要(summary)
  3. 清空中间的详细历史

影响范围

  • 保留:system prompt、最近对话、全局 state(invokedSkills)
  • 清空:历史对话中的 isMeta UserMessage(包括 skill 内容)

为什么 Skill 需要关心压缩

  • Skill 内容以 isMeta UserMessage 形式存在对话历史中
  • 压缩后这些消息消失,model 会忘记它正在使用的 skill 指令
  • 解决方案:从 invokedSkills state 重建 invoked_skills attachment

压缩触发时机

  • Token 达到阈值(通常是 context window 的 70-80%)
  • 用户手动触发压缩

Skill 的压缩恢复策略

// 压缩前
对话历史: [user1, assistant1, isMeta(skill内容), user2, assistant2, ...]
invokedSkills state: { "abc:debug": {content: "...", invokedAt: 123} }

// 压缩后
对话历史: [summary, user_recent, assistant_recent, ...]
invokedSkills state: { "abc:debug": {content: "...", invokedAt: 123} }  // 保留
invoked_skills attachment: [{ name: "debug", content: "..." }]  // 重建并注入

isMeta UserMessage

是什么:一种特殊的用户消息,标记 isMeta: true

特性

  • Model 可见:在下一轮对话时完整可见
  • 用户不可见:不显示在用户界面的对话历史中
  • 可以理解为"系统塞给 model 的小纸条"

与普通 UserMessage 的区别

维度普通 UserMessageisMeta UserMessage
来源用户输入系统内部注入
用户可见✅ 显示在界面❌ 隐藏
Model 可见✅ 完整可见✅ 完整可见
典型用途用户问题、指令Skill 内容、系统指令

在 Skill 中的作用

  • Skill 的完整 Markdown 内容以 isMeta UserMessage 形式注入
  • Model 在下一轮读到这些指令,按指令执行任务
  • 压缩时会被清空,需要通过 attachment 恢复

代码示例

// Skill 内容注入
createUserMessage({ 
  content: skillMarkdownContent, 
  isMeta: true  // 用户看不到,但 model 读得到
})

State(全局状态)

是什么:Claude Code 在整个会话期间持久化的全局变量,不随对话压缩而清空。

典型内容

  • invokedSkills:已调用的 skill 记录
  • sentSkillNames:已发送给各 agent 的 skill 列表
  • 其他系统状态(工作目录、环境变量等)

为什么需要 State

  • 对话历史会被压缩清空,但某些信息需要持久化
  • State 是"对话之外的记忆"

State vs 对话上下文

维度对话上下文State
存储位置对话历史全局变量
压缩影响会被清空/摘要不受影响
Model 可见直接可见需要通过 attachment 注入
典型用途对话内容、消息流持久化状态、元数据

在 Skill 中的作用

// 调用时写入 State
STATE.invokedSkills.set('abc:debug', {
  skillName: 'debug',
  content: '...',
  invokedAt: Date.now()
})

// 压缩后从 State 读取
const skills = STATE.invokedSkills.get('abc:debug')
// 重建 invoked_skills attachment