第7章 程序性知识——Skill 系统与草稿晋升
2026.07.29Agent 需要知道"怎么做"——但"怎么做"和"知道什么"是两类知识,需要不同的载体。
模式层
7.1 程序性知识 vs 事实性知识
在 agent 的知识体系中,有两类性质不同的知识:
事实性知识描述"是什么"。代码风格规范、API 调用签名、数据库表结构、项目约束条件。事实性知识的特点是:它需要被精确引用,不需要被"执行"。
LLM 本身擅长存储事实性知识。模型训练数据中包含大量事实——语言规则、框架文档、最佳实践。对于项目特有的事实性知识(如"本项目使用 nightly Rust"),可以通过 system prompt 注入或 memory 工具持久化。
程序性知识描述"怎么做"。代码审查的步骤、调试流程的顺序、编写 API 文档前需要确认的检查清单、从 issue 到 PR 的工作流。程序性知识的特点是:它是一组有序的操作步骤,需要 LLM 在决策时"按这个顺序做"。
程序性知识不适合存储在 LLM 的权重中——因为步骤可能随项目演化而变化,而且不同的项目需要不同的步骤。它也不适合放在 system prompt 中全量注入——不是每个 turn 都需要使用所有步骤。
Skill 是程序性知识的载体。每个 Skill 文件是一套完整的操作流程,以 Markdown 格式存储在 skills/ 目录中。Agent 在需要时读取它。
载体选择原则:
| 知识类型 | 载体 | 理由 |
|---|---|---|
| 通用事实(语言规则、文档) | LLM 训练数据 | 不需要额外操作 |
| 项目特有事实 | system prompt / memory | 跨 session 持久但全量注入成本低 |
| 程序性知识(步骤、方法) | Skill 文件 | 可独立修改、按需加载、可追踪版本 |
7.2 三种注入时机
Skill 的加载有三种时机,分别对应不同的使用频率和成本:
全量注入(常驻目录)。
skills/ 目录下的所有文件在启动时被 Registry 扫描,内容拼入 system prompt。这意味着 agent 在每个 turn 开始时都能读到所有 Skill 的内容。
成本是 token 预算。每增加一个 Skill,system prompt 前缀增加数百 token。对于 6 个 Skill 的场景(CodeCoder 写作时的规模),这个成本可以忽略。但如果 Skills 增长到 50 个以上,全量注入会显著压缩 agent 可用于对话和工具调用的 token 预算。
缓解策略:Skill 的内容在注入前可以摘要为 2-3 行的描述,只在需要时加载全文。当前 CodeCoder 的做法是全量注入全文——因为在 6 个 Skill 的规模下,摘要的复杂度不值得引入。
按需激活(use_skill)。
agent 通过 use_skill 工具主动要求加载某个 Skill。调用时,System 将 Skill 文件的全文注入当前上下文。
use_skill 的解析优先级是:先查 skills/,再查 prompts/。如果两处都没有,返回错误"no such skill"。
按需激活适合使用频率中等(每周几次到每天几次)的 Skill。它避免了全量注入的 token 成本,但增加了 agent 操作的显式步骤——agent 需要记得在合适的时机调用 use_skill。
回退草稿(prompts/)。
当 use_skill 在 skills/ 中找不到时,回退到 prompts/ 目录。prompts/ 存放草稿状态的 Skill——那些尚未正式化但值得试用的知识。
草稿不会在启动时自动注入 system prompt。use_skill 从 prompts/ 加载的内容与从 skills/ 加载的内容在行为上没有区别——唯一的区别是加载优先级。
7.3 草稿晋升设计
从草稿到正式 Skill 的晋升不是自动的。
当 agent 用 generate_prompt 写了一个草稿到 prompts/,它不会自动进入 skills/。晋升发生在 agent 显式调用 promote_prompt 时:
promote_prompt("my-new-skill")
→ 检查 prompts/my-new-skill.md 是否存在
→ 检查 skills/ 中是否已有同名文件(撞名报错)
→ 文件从 prompts/ 移动到 skills/
→ Registry 更新常驻目录表
→ 下次 turn 开始,这个 Skill 自动注入 system prompt
晋升之后,草稿目录中的文件被删除——避免同一个 Skill 在两个目录中都存在的不一致状态。
晋升的门槛不是代码质量检查——CodeCoder 不验证 Skill 文件的内容质量。门槛是"agent 经过试用后认为这个 Skill 值得正式化"。试用阶段(草稿在 prompts/ 中被 use_skill 调用)的目的是收集使用反馈:这个 Skill 的步骤是否合理?覆盖了正确的场景吗?有没有漏掉边界情况?
如果试用发现 Skill 需要大幅修改,agent 可以删除草稿、重新生成,或者直接编辑草稿文件。修改草稿不触发任何操作——它只是一个文件编辑。prompts/ 的设计本来就是"可以随意修改、删除、重写"的。
案例层
7.4 use_skill 解析流程
use_skill 工具的执行流程:
fn execute_use_skill(args: UseSkillArgs, context: &Context) -> Result<()> {
let name = &args.name;
// 1. 从 skills/ 查找
if let Some(skill) = context.registry.skills.get(name) {
// 注入 skill 全文到当前上下文
context.inject(&skill.content);
return Ok(());
}
// 2. 从 prompts/ 回退查找
if let Some(prompt) = context.registry.prompts.get(name) {
context.inject(&prompt.content);
return Ok(());
}
// 3. 两处都没有
Err(format!("skill not found: {}", name))
}
context.inject() 的实现是将 Skill 内容作为 system 角色的消息插入到当前上下文——位于用户消息之前、系统 prompt 之后。这个位置确保 agent 在阅读用户消息之前就能读到 Skill 的内容。
7.5 三个生成工具的职责分工
三个工具负责 Skill / Prompt 的创建和晋升:
// generate_prompt:写草稿到 prompts/
fn generate_prompt(name: String, content: String) -> Result<()> {
let path = prompts_dir().join(format!("{}.md", name));
fs::write(&path, &content)?;
// 在 Registry 中注册
registry.prompts.insert(name, PromptEntry {
path,
content,
created_at: now(),
});
Ok(())
}
// generate_skill:直接写正式 Skill 到 skills/
fn generate_skill(name: String, content: String) -> Result<()> {
let path = skills_dir().join(format!("{}.md", name));
fs::write(&path, &content)?;
registry.skills.insert(name, SkillEntry {
path,
content,
promoted: false, // 直接生成,未经晋升
source: None,
});
Ok(())
}
// promote_prompt:从草稿晋升为正式 Skill
fn promote_prompt(name: String) -> Result<()> {
let prompt_path = prompts_dir().join(format!("{}.md", &name));
let skill_path = skills_dir().join(format!("{}.md", &name));
// 检查 skills/ 中是否已有同名文件
if skill_path.exists() {
return Err("skill already exists with this name");
}
// 从 prompts/ 移动到 skills/
fs::rename(&prompt_path, &skill_path)?;
// 更新 Registry
let prompt = registry.prompts.remove(&name).unwrap();
registry.skills.insert(name, SkillEntry {
path: skill_path,
content: prompt.content,
promoted: true, // 标记为"经由晋升而来"
source: prompt.source,
});
Ok(())
}
三个工具的分工反映了一个重要的设计原则:创建(generate)和晋升(promote)是分离的。 Agent 不能用一个操作同时做到"写内容"和"把它放进正式知识库"。generate_prompt 只写草稿,promote_prompt 只晋升——两个操作的组合需要 agent 在实际使用中验证 Value 后主动触发晋升。
generate_skill 的存在是为了支持"直接写入正式 Skill"的场景——比如从外部导入已知有效的方法论。但在 CodeCoder 的真实使用中,generate_skill 很少被调用;更多使用的是 generate_prompt → 试用 → promote_prompt 的渐进路径。
7.6 SourceInfo 溯源
每个 Skill 和 Prompt 文件在 Registry 中都附带 SourceInfo:
struct SourceInfo {
source: Source, // Agent | User | Imported
created_at: Instant,
agent_reason: Option<String>, // agent 生成时的 prompt 或理由
}
enum Source {
Agent { session_id: String },
User,
Imported { url: Option<String> },
}
SourceInfo 的用途是溯源——当读者在 skills/ 看到一个文件时,他可以去查"这个 Skill 是什么时候生成的、由谁生成的、基于什么理由生成的"。
例如:
---
source: Agent
session_id: "cc-session-20260715-a3b2c1"
reason: "在修复三个类似的 bug 后发现这些 bug 共享同一个根因模式"
---
# Debug Causal Chain
遇到 bug 时按这个顺序……
SourceInfo 本身不是数字签名——它只是一个普通的 metadata 文件头。它不能防止伪造(用户可以直接编辑文件头),但它在协作环境中提供了一个"为什么这个 Skill 会出现在这里"的可追溯上下文。
7.7 Skill 源码示例
以下是一个真实 Skill 文件的简化版,展示程序性知识的写作风格和结构:
---
name: debug-causal
source: Agent
session_id: "cc-session-20260710-9f8e7d"
reason: "三次遇到同样的 panic 模式后,总结了一个通用的 debug 流程"
---
# Debug Causal Chain
## 适用场景
遇到 panic、测试失败、或者意料之外的错误输出时。
## 步骤
### 1. 复现
写一个最小输入来触发这个 bug。
### 2. 锁定
用 git bisect 找到引入 bug 的 commit。
### 3. 验证
确认根因不是其他地方引入的。
### 4. 修复
写最少代码修。不重构、不优化——只修。
### 5. 对照
提交前确认修复不破坏已有测试。跑一遍测试套件。
## 不适用
- 性能退化:不适用本流程,用 `perf-debug` skill
- 已知 bug:如果根因已经在因果树中,直接在 `reason` 工具中更新状态
这个 Skill 文件的关键特征:
- 前置条件(适用场景/不适用)帮助 agent 判断何时该用这个 Skill
- 步骤按顺序编号,每个步骤一行
- 每步附带具体的操作,而不是"修复"两个字——"修"后面跟了具体说明
- 不适用条件防止 agent 在不合适的场景下套用错误的流程
ADR 深度阅读
从无溯源到 SourceInfo
CodeCoder 最初的 Skill 系统不包含任何溯源信息。Skill 文件只是一个 .md 文件,没有头部 metadata,没有谁创建、没有为什么创建。
问题在于:当一个 skills/ 目录增长到多个文件时,维护者无法判断"这个 Skill 是 agent 生成的还是人工编写的"、"为什么会有这个 Skill"、"它现在还有用吗"。
SourceInfo 的引入改变了这个问题。每个 Skill 文件在生成时自动附加头部 metadata 段,记录来源(Agent / User / Imported)、创建时间、agent 生成时的理由。这不是一个严格的验证机制(头部 metadata 可以被编辑),但它提供了一个可追溯的上下文——特别是当 agent 生成 Skill 时,它会在"reason"字段中记录自己的动机。
一个实际例子:agent 审查了三次 Python 代码,三次发现 import 顺序问题。它生成一个草稿,在 reason 字段中写"三次审查中发现了相同的 import 顺序问题"。后来人工审查这个文件时,维护者能理解"为什么这个 Skill 存在"。如果维护者觉得 import 顺序不是问题,他可以直接删除这个文件,同时理解"这是一个 agent 从实践中总结的模式,不是设计文档中的硬性规定"。
下一章进入 Capability 的执行环境——OneShot、OnDemand、Persistent 三种生命周期,以及 Shell、Wasm、Docker 三种沙箱。