第 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 注入
核心思路是两层分离:
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["<skill name='git'><br>完整的 Git workflow 指令...<br>Step 1: ...<br></skill>"]
end
L1 -.-|"~100 tokens/skill"| NOTE1[" "]
L2 -.-|"~2000 tokens"| NOTE2[" "]
style NOTE1 fill:none,stroke:none
style NOTE2 fill:none,stroke:noneLayer 1 只放技能名称和一句话描述,每个 skill 仅消耗约 100 token。Layer 2 的完整内容通过 tool_result 注入,只有模型判断当前任务需要时才加载。
这带来三个根本性优势:
- Token 效率:10 个 skill 的 Layer 1 总共约 1000 token,而非 20000。只在需要时才付出完整内容的代价。
- 注意力精准:通过
tool_result注入的内容紧邻模型当前的推理上下文,不会被 system prompt 的其他内容淹没。 - 可扩展:新增 skill 只需在磁盘上添加一个文件,不需要修改任何代码。
7.2 Skill 文件的设计
SKILL.md 文件格式
每个 skill 是一个目录,包含一个 SKILL.md 文件,使用 YAML frontmatter 存储元数据:
skills/
pdf/
SKILL.md
code-review/
SKILL.md
skill-creator/
SKILL.md
scripts/
init_skill.py
references/
openai_yaml.md
assets/
skill-creator.pngSKILL.md 文件结构:
---
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 中的 name 和 description 用于 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:
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:
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_skill。load_skill 就是一个普通的 tool,返回完整 skill body 包裹在 <skill> 标签中:
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_skill 和 bash、read_file 等工具并列,没有任何特殊处理:
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 文件嵌入二进制中:
const SYSTEM_SKILLS_DIR: Dir =
include_dir::include_dir!("$CARGO_MANIFEST_DIR/src/assets/samples");这些内嵌 skill 在启动时被释放到 $CODEX_HOME/skills/.system/ 目录。通过指纹机制(对所有文件内容计算 hash)决定是否需要更新:如果磁盘上已有的 skill 指纹与编译时嵌入的一致,就跳过写入,避免每次启动都做 I/O。
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 中总结了几个核心原则:
- Context window 是公共资源——skill 与 system prompt、对话历史、其他 skill 的元数据共享上下文窗口。默认假设模型已经很聪明,只添加模型不具备的知识。
- 自由度分级——根据任务的脆弱性设定不同精度等级:高自由度(文字指令)、中等自由度(伪代码+参数)、低自由度(精确脚本,几乎没有可调参数)。如果操作容易出错且一致性至关重要,就用低自由度的精确脚本。
- 验证完整性——可以通过子 Agent 来验证 skill 是否在真实任务上有效。
7.3 分层知识:CLAUDE.md / AGENTS.md
层级加载:全局 → 项目 → 目录
Claude Code 使用 CLAUDE.md 作为项目级指令文件,Codex 使用 .codex/config.toml 和 AGENTS.md。核心设计一致:知识按层级组织,从全局到局部逐层叠加。
以 Codex 的配置层级为例,优先级从低到高:
| 层级 | 来源 | 说明 |
|---|---|---|
| system | /etc/codex/config.toml | 系统管理员设定的全局约束 |
| user | $CODEX_HOME/config.toml | 用户的全局偏好 |
| project (tree) | 从 cwd 到项目根目录逐层查找 .codex/config.toml | 项目级配置 |
| project (repo) | $(git root)/.codex/config.toml | Git 仓库根级配置 |
| runtime | CLI 参数、UI 选择 | 会话级覆盖 |
Claude Code 的层级更直观:
| 层级 | 来源 | 说明 |
|---|---|---|
| 全局 | ~/.claude/CLAUDE.md | 用户的全局习惯和规则 |
| 全局规则 | ~/.claude/rules/*.md | 分文件组织的全局规则 |
| 项目 | 项目根目录 CLAUDE.md | 项目级约定 |
| 目录 | 子目录中的 CLAUDE.md | 目录级约定 |
多层配置合并机制
Codex 的配置合并是一套完整的 TOML 值递归合并系统。核心函数 load_config_layers_state 按顺序加载每一层,最终生成 ConfigLayerStack:
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 的处理方式:
- 项目配置总是被加载(解析并保留在 ConfigLayerStack 中)
- 但只有被标记为
trusted的项目,其配置才会生效 - 未信任的项目配置带有
disabled_reason,在 UI 中可见但不影响行为
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 完成这一转换:
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 启动时只读取顶层地图,按需加载深层内容:
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 的记忆系统,也采用了完全相同的渐进披露结构:
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 的核心设计原则:
- 层级依赖树——设定硬性规则,如
Types → Config → Repo → Service → Runtime → UI,低层不允许反向依赖高层。这不是写在文档里的"建议",而是 CI 中的自动化检查。 - 带修复指令的报错——这是最关键的设计。Linter 报错不能只说
Error: dependency violation,必须附带具体的修复路径:
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 可执行的规则。
- 文件大小/复杂度限制——对单文件行数、函数复杂度、依赖数量设上限。Agent 生成代码极快,不加限制会迅速产生 3000 行的 God Object。
"机器检查机器"的理念
AI 每天可能提交几百个 PR,人工 Review 不可能跟上。解决方案是用机器检查机器——自动化的验证闭环:
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)中提取结构化记忆:
- 从状态数据库中领取(claim)符合条件的 rollout 任务
- 过滤 rollout 内容,保留与记忆相关的 response item
- 将 rollout 发送给模型,并行处理(并发上限为 8)
- 模型返回结构化 JSON:
raw_memory(详细记忆)+rollout_summary(紧凑摘要)+rollout_slug(文件名标识) - 对生成的记忆内容执行密钥脱敏(redact secrets)
- 结果存回状态数据库
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——如果这只是一次性的随机查询、没有持久洞见的状态更新、或者常识性知识——则返回空值,不写入任何记忆。
高信号记忆被分为四类:
- 稳定的用户偏好——用户反复要求、纠正或打断 Agent 以执行的操作
- 高杠杆的过程知识——节省大量探索时间的快捷方式、失败防护、精确路径
- 可靠的任务路线图——真相存储在哪里、如何判断方向错误、什么信号应触发转向
- 持久的环境和工作流证据——稳定的工具习惯、仓库惯例、展示/验证期望
Phase 2:Global Consolidation(全局整合)
多次 Phase 1 运行后,积累了大量单会话的零散记忆。Phase 2 将它们整合为结构化的文件系统:
- 领取全局 Phase 2 任务(全局唯一锁,同一时刻只有一个 consolidation 在运行)
- 从状态数据库加载最新的 Stage 1 输出
- 同步本地文件系统:更新
rollout_summaries/和raw_memories.md - 生成 consolidation prompt,包含与上次成功运行的 diff(新增/保留/移除的记忆)
- 派生一个子 Agent 来执行实际的整合工作
Phase 2 的子 Agent 配置值得注意:
// 不生成记忆(防止递归)
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/——保留的详细会话摘要
记忆的读取路径
新会话启动时,记忆系统的读取路径也遵循渐进披露原则:
memory_summary.md的内容被截断到 5000 token 上限后注入 developer instructions- Agent 收到用户查询后,执行 "Quick Memory Pass":
- 从 memory summary 中提取任务相关关键词
- 搜索
MEMORY.md - 仅在 MEMORY.md 指向时,打开 1-2 个最相关的 rollout summary 或 skill
- 如果无命中,停止记忆查找,正常处理
- 整个记忆查找控制在 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 可以做增量更新而非全量重写,大幅降低成本。
- 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.md、memory_summary.md 和 skills/ 目录也一并删除——不留下陈旧的文件误导未来的 Agent。