Skip to content

第 7 章 Skill Loading —— 按需注入知识

Agent 的知识注入只有两种模式:前置塞进 system prompt 白白浪费 token,或者通过 tool_result 按需加载——后者在 token 效率、注意力精度、可扩展性三个维度上全面碾压前者。

7.1 知识加载的两种模式

前置加载:system prompt 塞满一切

最直觉的方案是把所有领域知识、工作流程、编码规范全部写进 system prompt。这在技能少于 3 个时可以工作,但很快就会撞墙:

Token 爆炸:假设 10 个 skill,每个 2000 token,光技能描述就消耗 20000 token。对于一个 200K 上下文窗口的模型,这看起来只占 10%,但 system prompt 是每轮对话都要重复发送的——成本线性增长。

注意力稀释:Transformer 的注意力机制在处理长 system prompt 时存在固有问题——位于中间的指令被遵循的概率显著低于开头和结尾("lost in the middle" 效应)。塞进 10 个 skill 的完整内容,等于告诉模型"一切都重要",结果就是一切都不重要。

僵化:新增一个 skill 就要修改 system prompt,所有会话都受影响,包括那些完全用不到这个 skill 的会话。

按需加载:tool_result 注入

核心思路是两层分离:

mermaid
graph TB
    subgraph L1["System Prompt (Layer 1 — 始终存在)"]
        direction TB
        S1["You are a coding agent."]
        S2["Skills available:<br>- git: Git workflow helpers<br>- test: Testing best practices"]
        S1 --- S2
    end

    S2 -->|"模型调用 load_skill('git')"| L2

    subgraph L2["tool_result (Layer 2 — 按需加载)"]
        direction TB
        T1["&lt;skill name='git'&gt;<br>完整的 Git workflow 指令...<br>Step 1: ...<br>&lt;/skill&gt;"]
    end

    L1 -.-|"~100 tokens/skill"| NOTE1[" "]
    L2 -.-|"~2000 tokens"| NOTE2[" "]

    style NOTE1 fill:none,stroke:none
    style NOTE2 fill:none,stroke:none

Layer 1 只放技能名称和一句话描述,每个 skill 仅消耗约 100 token。Layer 2 的完整内容通过 tool_result 注入,只有模型判断当前任务需要时才加载。

这带来三个根本性优势:

  1. Token 效率:10 个 skill 的 Layer 1 总共约 1000 token,而非 20000。只在需要时才付出完整内容的代价。
  2. 注意力精准:通过 tool_result 注入的内容紧邻模型当前的推理上下文,不会被 system prompt 的其他内容淹没。
  3. 可扩展:新增 skill 只需在磁盘上添加一个文件,不需要修改任何代码。

7.2 Skill 文件的设计

SKILL.md 文件格式

每个 skill 是一个目录,包含一个 SKILL.md 文件,使用 YAML frontmatter 存储元数据:

text
skills/
  pdf/
    SKILL.md
  code-review/
    SKILL.md
  skill-creator/
    SKILL.md
    scripts/
      init_skill.py
    references/
      openai_yaml.md
    assets/
      skill-creator.png

SKILL.md 文件结构:

markdown
---
name: skill-creator
description: Guide for creating effective skills
metadata:
  short-description: Create or update a skill
---

# Skill Creator

This skill provides guidance for creating effective skills.

## About Skills

Skills are modular, self-contained folders that extend the agent's
capabilities by providing specialized knowledge, workflows, and tools.
...

frontmatter 中的 namedescription 用于 Layer 1(system prompt 里的一行摘要)。--- 分隔符之后的全部内容是 Layer 2(通过 tool_result 按需注入的完整技能体)。

Codex 的 skill 目录更丰富,除了 SKILL.md 还可以包含:

  • scripts/:可执行脚本(Python、Shell),Agent 可以直接调用
  • references/:参考文档,如 API 文档、升级指南
  • assets/:图标、图片等资源
  • agents/:Agent 配置文件(如 openai.yaml

这种结构的设计哲学是:skill 不仅是一段文字指令,它是一个自包含的工具包,Agent 可以在加载 skill 后直接使用其中的脚本和参考资料。

技能发现 → 技能加载 → 注入 tool_result

完整的三步流程:

Step 1:启动时扫描。SkillLoader 在初始化时递归扫描 skills 目录下所有 SKILL.md 文件,解析 YAML frontmatter,提取 name 和 description:

python
class SkillLoader:
    def __init__(self, skills_dir: Path):
        self.skills = {}
        for f in sorted(skills_dir.rglob("SKILL.md")):
            text = f.read_text()
            meta, body = self._parse_frontmatter(text)
            name = meta.get("name", f.parent.name)
            self.skills[name] = {"meta": meta, "body": body}

关键细节:如果 frontmatter 中没有 name 字段,自动使用父目录名作为 skill 名称。这是一个合理的 fallback——目录结构本身就是 skill 的标识。

Step 2:生成 Layer 1 描述。将所有 skill 的名称和描述拼接成一个紧凑的列表,注入 system prompt:

python
def get_descriptions(self) -> str:
    lines = []
    for name, skill in self.skills.items():
        desc = skill["meta"].get("description", "No description")
        lines.append(f"  - {name}: {desc}")
    return "\n".join(lines)

Step 3:模型按需调用 load_skillload_skill 就是一个普通的 tool,返回完整 skill body 包裹在 <skill> 标签中:

python
def get_content(self, name: str) -> str:
    skill = self.skills.get(name)
    if not skill:
        return f"Error: Unknown skill '{name}'. Available: {', '.join(self.skills.keys())}"
    return f'<skill name="{name}">\n{skill["body"]}\n</skill>'

整个链路的注册代码极简——load_skillbashread_file 等工具并列,没有任何特殊处理:

python
TOOL_HANDLERS = {
    "bash":       lambda **kw: run_bash(kw["command"]),
    "read_file":  lambda **kw: run_read(kw["path"], kw.get("limit")),
    "load_skill": lambda **kw: SKILL_LOADER.get_content(kw["name"]),
}

Codex 的 Skill 系统设计

Codex 的 skill 系统在教学版基础上增加了几个生产级特性:

内嵌系统 Skill。Codex 在编译时通过 include_dir! 宏将默认 skill 文件嵌入二进制中:

rust
const SYSTEM_SKILLS_DIR: Dir =
    include_dir::include_dir!("$CARGO_MANIFEST_DIR/src/assets/samples");

这些内嵌 skill 在启动时被释放到 $CODEX_HOME/skills/.system/ 目录。通过指纹机制(对所有文件内容计算 hash)决定是否需要更新:如果磁盘上已有的 skill 指纹与编译时嵌入的一致,就跳过写入,避免每次启动都做 I/O。

rust
fn embedded_system_skills_fingerprint() -> String {
    let mut items = Vec::new();
    collect_fingerprint_items(&SYSTEM_SKILLS_DIR, &mut items);
    items.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));

    let mut hasher = DefaultHasher::new();
    SYSTEM_SKILLS_MARKER_SALT.hash(&mut hasher);
    for (path, contents_hash) in items {
        path.hash(&mut hasher);
        contents_hash.hash(&mut hasher);
    }
    format!("{:x}", hasher.finish())
}

默认内嵌的 skill 包括 skill-creator(创建新 skill 的向导)、skill-installer(从 GitHub 安装社区 skill)和 openai-docs(API 文档参考)。这意味着 Codex 开箱即有"创建 skill"和"安装 skill"的能力——skill 系统可以自我扩展。

Skill 设计原则。Codex 的 skill-creator SKILL.md 中总结了几个核心原则:

  1. Context window 是公共资源——skill 与 system prompt、对话历史、其他 skill 的元数据共享上下文窗口。默认假设模型已经很聪明,只添加模型不具备的知识。
  2. 自由度分级——根据任务的脆弱性设定不同精度等级:高自由度(文字指令)、中等自由度(伪代码+参数)、低自由度(精确脚本,几乎没有可调参数)。如果操作容易出错且一致性至关重要,就用低自由度的精确脚本。
  3. 验证完整性——可以通过子 Agent 来验证 skill 是否在真实任务上有效。

7.3 分层知识:CLAUDE.md / AGENTS.md

层级加载:全局 → 项目 → 目录

Claude Code 使用 CLAUDE.md 作为项目级指令文件,Codex 使用 .codex/config.tomlAGENTS.md。核心设计一致:知识按层级组织,从全局到局部逐层叠加

以 Codex 的配置层级为例,优先级从低到高:

层级来源说明
system/etc/codex/config.toml系统管理员设定的全局约束
user$CODEX_HOME/config.toml用户的全局偏好
project (tree)从 cwd 到项目根目录逐层查找 .codex/config.toml项目级配置
project (repo)$(git root)/.codex/config.tomlGit 仓库根级配置
runtimeCLI 参数、UI 选择会话级覆盖

Claude Code 的层级更直观:

层级来源说明
全局~/.claude/CLAUDE.md用户的全局习惯和规则
全局规则~/.claude/rules/*.md分文件组织的全局规则
项目项目根目录 CLAUDE.md项目级约定
目录子目录中的 CLAUDE.md目录级约定

多层配置合并机制

Codex 的配置合并是一套完整的 TOML 值递归合并系统。核心函数 load_config_layers_state 按顺序加载每一层,最终生成 ConfigLayerStack

mermaid
graph TB
    S["system layer<br>(最低优先级)"] --> U["user layer"]
    U --> P["project layer(s)<br>(可能有多个,从项目根到 cwd 逐层叠加)"]
    P --> R["runtime / CLI overrides<br>(最高优先级)"]

    style S fill:#e8e8e8,stroke:#999
    style U fill:#d4e6f1,stroke:#7fb3d8
    style P fill:#d5f5e3,stroke:#82e0aa
    style R fill:#fadbd8,stroke:#f1948a

合并规则:后加载的层覆盖先加载的层,但 requirements(安全约束)例外——先定义的 requirement 不能被后面的层覆盖。这确保了系统管理员设定的安全策略(如强制沙箱模式)不能被用户或项目配置绕过。

信任机制是关键安全设计。项目级配置来自第三方仓库,可能包含恶意指令。Codex 的处理方式:

  1. 项目配置总是被加载(解析并保留在 ConfigLayerStack 中)
  2. 但只有被标记为 trusted 的项目,其配置才会生效
  3. 未信任的项目配置带有 disabled_reason,在 UI 中可见但不影响行为
rust
fn project_layer_entry(
    trust_context: &ProjectTrustContext,
    // ...
    config: TomlValue,
    config_toml_exists: bool,
) -> ConfigLayerEntry {
    if config_toml_exists
        && let Some(reason) = trust_context.disabled_reason_for_dir(&layer_dir)
    {
        ConfigLayerEntry::new_disabled(source, config, reason)
    } else {
        ConfigLayerEntry::new(source, config)
    }
}

路径解析也值得注意。不同层的 config.toml 可能在不同目录,其中的相对路径(如 model_instructions_file = "./prompts.md")需要相对于各自所在目录解析,才能在合并后正确工作。Codex 通过序列化/反序列化 round-trip 完成这一转换:

rust
fn resolve_relative_paths_in_config_toml(
    value_from_config_toml: TomlValue,
    base_dir: &Path,
) -> io::Result<TomlValue> {
    let _guard = AbsolutePathBufGuard::new(base_dir);
    let Ok(resolved) = value_from_config_toml.clone()
        .try_into::<ConfigToml>() else {
        return Ok(value_from_config_toml);
    };
    drop(_guard);
    let resolved_value = TomlValue::try_from(resolved)?;
    Ok(copy_shape_from_original(&value_from_config_toml, &resolved_value))
}

AbsolutePathBufGuard 临时设置当前工作目录,使得 Deserialize 实现中的相对路径解析基于正确的 base_dir。copy_shape_from_original 确保 round-trip 过程中不会丢失 TOML 中未被 ConfigToml 结构体识别的自定义字段。

7.4 结构化知识的渐进披露

分层文档树:Agent 按需加载

将所有规范塞进一个巨大的 AGENTS.md,等于让 Agent 在 100KB 文档中玩"大海捞针"。正确做法是构建分层文档树,Agent 启动时只读取顶层地图,按需加载深层内容:

text
repo/
├── AGENTS.md          ← 顶层地图(~100 行),指向下方文档
├── docs/
│   ├── architecture/  ← 架构设计与分层规则
│   ├── domains/       ← 各业务域详细文档
│   ├── references/    ← 特定技术的快速参考
├── ai/
│   ├── conventions/   ← 全局约定(命名规范、日志要求)
│   ├── .plan/         ← 设计与执行计划
│   └── .modify/       ← 变更记录与实现说明

这就是 Progressive Disclosure(渐进式披露) 在 Agent 工程中的应用。顶层 AGENTS.md 充当路由表——它告诉 Agent"关于数据库的约定在 docs/domains/database.md,关于 API 设计规范在 docs/architecture/api-design.md"。Agent 根据当前任务类型,只加载相关的文档节点。

类比 Codex 的记忆系统,也采用了完全相同的渐进披露结构:

text
memories/
├── memory_summary.md       ← 始终注入 system prompt(紧凑导航)
├── MEMORY.md               ← 可搜索的知识条目索引
├── skills/<skill-name>/    ← 可复用的过程知识
│   └── SKILL.md
├── rollout_summaries/      ← 每次会话的详细记录
│   └── <rollout_slug>.md
└── raw_memories.md         ← Phase 1 原始提取(Phase 2 的输入)

memory_summary.md 始终被加载到 system prompt 中(有严格的 token 上限),充当导航入口。Agent 需要更详细的信息时,先搜索 MEMORY.md,再按需打开具体的 rollout summary 或 skill。

机械化架构约束 + 自定义 Linter

渐进披露解决了"Agent 如何找到知识"的问题。但知识找到后,Agent 会不会遵守?答案是:不能靠文字指令,要靠机械化约束。

自定义 Linter 的核心设计原则

  1. 层级依赖树——设定硬性规则,如 Types → Config → Repo → Service → Runtime → UI,低层不允许反向依赖高层。这不是写在文档里的"建议",而是 CI 中的自动化检查。
  2. 带修复指令的报错——这是最关键的设计。Linter 报错不能只说 Error: dependency violation,必须附带具体的修复路径:
python
ERROR: Service layer cannot directly import UI layer.
FIX: Use Providers interface injection instead.
     Move the import to the Providers layer and inject via constructor.

为什么这样设计?因为 Agent 遇到报错时的行为模式是:读取错误信息 → 尝试修复。如果错误信息不包含修复方向,Agent 会"自信地即兴发挥",可能用更糟糕的方式绕过约束。附带修复指令的报错,本质上是把"人类的架构品味"编码为 Agent 可执行的规则。

  1. 文件大小/复杂度限制——对单文件行数、函数复杂度、依赖数量设上限。Agent 生成代码极快,不加限制会迅速产生 3000 行的 God Object。

"机器检查机器"的理念

AI 每天可能提交几百个 PR,人工 Review 不可能跟上。解决方案是用机器检查机器——自动化的验证闭环:

text
Developer Agent → 提交 PR

Linter/CI → 自动检查架构约束、测试覆盖、命名规范
      ↓ (失败时附带修复指令)
Developer Agent → 根据报错自修复
      ↓ (通过)
Reviewer Agent → 独立的代码审查(跑测试、检查边界情况)
      ↓ (打回修改意见)
Developer Agent → 再次修改
      ↓ (双方达成一致)
人工 → 仅参与重大架构决策

这个流程中有两个关键机制:

死循环检测(Doom Loop Detection)——监听同一文件的编辑次数,超过 N 次后注入强制打断:"你已经尝试多次但未成功,请退一步,重新阅读错误日志并彻底更换思路。" 这防止 Agent 陷入"反复微调同一处代码"的死胡同。

PreCompletionChecklist——在 Agent 尝试声明"任务完成"之前,强制拦截,要求它必须先运行测试、构建或启动验证。不验证不放行。

7.5 持久化记忆系统

为什么需要持久化记忆

每个 Agent 会话都是无状态的——上一次会话中学到的用户偏好、踩过的坑、发现的仓库结构,在新会话开始时全部归零。用户不得不反复重复相同的指令:"我喜欢用 Tabs 而不是 Spaces"、"这个仓库的测试要用 pnpm test 不是 npm test"。

持久化记忆的目标:让 Agent 跨会话积累知识,减少用户的重复劳动

Codex 的两阶段记忆管道

Codex 实现了一个生产级的自动记忆系统,分为两个阶段:

Phase 1:Rollout Extraction(单会话提取)

每个会话结束后(或下次启动时),Phase 1 从历史会话记录(rollout)中提取结构化记忆:

  1. 从状态数据库中领取(claim)符合条件的 rollout 任务
  2. 过滤 rollout 内容,保留与记忆相关的 response item
  3. 将 rollout 发送给模型,并行处理(并发上限为 8)
  4. 模型返回结构化 JSON:raw_memory(详细记忆)+ rollout_summary(紧凑摘要)+ rollout_slug(文件名标识)
  5. 对生成的记忆内容执行密钥脱敏(redact secrets)
  6. 结果存回状态数据库

Phase 1 的模型提取使用的是轻量级模型(如 gpt-5.1-codex-mini),reasoning effort 设为 Low——提取记忆不需要深度推理,用小模型快速处理大量 rollout 更经济。

Phase 1 的 system prompt 中定义了严格的"最低信号门槛"(Minimum Signal Gate):

"Will a future agent plausibly act better because of what I write here?" 如果答案是 No——如果这只是一次性的随机查询、没有持久洞见的状态更新、或者常识性知识——则返回空值,不写入任何记忆。

高信号记忆被分为四类:

  1. 稳定的用户偏好——用户反复要求、纠正或打断 Agent 以执行的操作
  2. 高杠杆的过程知识——节省大量探索时间的快捷方式、失败防护、精确路径
  3. 可靠的任务路线图——真相存储在哪里、如何判断方向错误、什么信号应触发转向
  4. 持久的环境和工作流证据——稳定的工具习惯、仓库惯例、展示/验证期望

Phase 2:Global Consolidation(全局整合)

多次 Phase 1 运行后,积累了大量单会话的零散记忆。Phase 2 将它们整合为结构化的文件系统:

  1. 领取全局 Phase 2 任务(全局唯一锁,同一时刻只有一个 consolidation 在运行)
  2. 从状态数据库加载最新的 Stage 1 输出
  3. 同步本地文件系统:更新 rollout_summaries/raw_memories.md
  4. 生成 consolidation prompt,包含与上次成功运行的 diff(新增/保留/移除的记忆)
  5. 派生一个子 Agent 来执行实际的整合工作

Phase 2 的子 Agent 配置值得注意:

rust
// 不生成记忆(防止递归)
agent_config.memories.generate_memories = false;
// 无需审批
agent_config.permissions.approval_policy =
    Constrained::allow_only(AskForApproval::Never);
// 禁止派生更多子 Agent
let _ = agent_config.features.disable(Feature::SpawnCsv);
let _ = agent_config.features.disable(Feature::Collab);
let _ = agent_config.features.disable(Feature::MemoryTool);
// 仅本地写权限,无网络
let consolidation_sandbox_policy = SandboxPolicy::WorkspaceWrite {
    writable_roots,
    network_access: false,
    // ...
};

这个子 Agent 的权限被极度收窄:不能生成自己的记忆(防止递归)、不能派生其他 Agent、不能访问网络、只能写入 $CODEX_HOME 目录。它使用更强的模型(如 gpt-5.3-codex)和 Medium reasoning effort——整合需要更强的综合分析能力。

Phase 2 的输出是一套文件系统工件:

  • memory_summary.md——始终注入 system prompt 的紧凑导航
  • MEMORY.md——可搜索的知识条目索引
  • skills/<skill-name>/——从记忆中提炼的可复用过程知识
  • rollout_summaries/——保留的详细会话摘要

记忆的读取路径

新会话启动时,记忆系统的读取路径也遵循渐进披露原则:

  1. memory_summary.md 的内容被截断到 5000 token 上限后注入 developer instructions
  2. Agent 收到用户查询后,执行 "Quick Memory Pass":
    • 从 memory summary 中提取任务相关关键词
    • 搜索 MEMORY.md
    • 仅在 MEMORY.md 指向时,打开 1-2 个最相关的 rollout summary 或 skill
    • 如果无命中,停止记忆查找,正常处理
  3. 整个记忆查找控制在 4-6 步搜索以内

这个预算控制至关重要——如果 Agent 每次查询都深挖全部 rollout summary,记忆系统本身就会消耗过多的 token 和延迟。

自动记忆 vs 显式记忆

Codex 的记忆系统是完全自动的——用户不需要告诉 Agent "记住这个"。系统在后台异步运行 Phase 1 和 Phase 2,用户无感知。

Claude Code 的记忆系统则是显式的——用户通过命令(如 /memory add)或在 CLAUDE.md 中手动写入记忆。

两种模式各有取舍:

维度自动记忆(Codex)显式记忆(Claude Code)
用户负担零——后台自动运行需要用户主动管理
精度依赖模型判断"什么值得记"——可能记了不重要的,漏了重要的用户决定记什么——精度高
覆盖率每个会话都会被扫描——覆盖率高用户忘记记录就丢失——覆盖率依赖用户纪律
成本需要额外的模型调用(Phase 1 + Phase 2)零额外成本
安全性需要密钥脱敏、防止敏感信息泄露用户自控

生产系统中的最佳实践是两者结合:自动记忆捕捉用户可能忘记记录的模式和偏好,显式记忆处理用户明确知道重要的规则和约束。Claude Code 的 CLAUDE.md + rules/*.md 体系实际上就是显式记忆的分层实现——用户将长期有效的规则写入文件,Agent 在每次启动时自动加载。

记忆系统的协调细节

Codex 的记忆管道在工程上有几个精巧的协调机制:

Job Leasing——Phase 1 和 Phase 2 都使用数据库级别的任务租约(lease)。每个任务被领取后有 1 小时的租约期限,租约过期后其他 worker 可以重新领取。Phase 2 还有心跳机制(每 90 秒续约),防止长时间运行的 consolidation agent 失去任务所有权。

Watermark 追踪——Phase 2 通过水位线(watermark)追踪上次成功整合时的状态。只有当新的 Phase 1 输出超过水位线时("dirty" 状态),Phase 2 才会启动。这避免了无意义的重复整合。

选择差异(Selection Diff)——Phase 2 的 prompt 不是"整合所有记忆",而是精确描述"与上次相比,新增了什么、保留了什么、移除了什么"。这让 consolidation agent 可以做增量更新而非全量重写,大幅降低成本。

python
- selected inputs this run: 15
- newly added since last Phase 2: 3
- retained from last Phase 2: 12
- removed from last Phase 2: 2

Current selected Phase 1 inputs:
- [retained] thread_id=abc, rollout_summary_file=...
- [added] thread_id=def, rollout_summary_file=...

Removed from last selection:
- thread_id=xyz, rollout_summary_file=...

过期清理——Phase 1 启动前会先清理过期的 Stage 1 输出(按 max_unused_days 配置),Phase 2 整合时也只选择未过期的记忆。移除的记忆对应的 rollout summary 文件被从磁盘删除。如果所有记忆都被清理,MEMORY.mdmemory_summary.mdskills/ 目录也一并删除——不留下陈旧的文件误导未来的 Agent。