法不净空,觉无性也。

第7章 程序性知识——Skill 系统与草稿晋升

2026.07.29

Agent 需要知道"怎么做"——但"怎么做"和"知道什么"是两类知识,需要不同的载体。


模式层

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_skillskills/ 中找不到时,回退到 prompts/ 目录。prompts/ 存放草稿状态的 Skill——那些尚未正式化但值得试用的知识。

草稿不会在启动时自动注入 system prompt。use_skillprompts/ 加载的内容与从 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 三种沙箱。