技能加载 —— 别什么都往 System Prompt 里塞

把你的大脑想象成一个书包。你是想把所有课本都背着上学,还是只带下节课要用的那一本?


System Prompt 的悲剧

你写了一个 Agent。它在 system prompt 里描述了整个公司的内部知识:

SYSTEM = """You are a coding agent.
Knowledge:
- Database schema: 150 tables including users, orders, products...
- API docs: 23 REST endpoints with request/response formats
- Deployment: K8s config, Dockerfile, CI/CD pipeline
- Coding standards: 47 rules about naming, testing, linting
- Architecture: microservices, event bus, message queue...
- ...总共约 8000 tokens 的知识"""

然后你问它:“帮我改一下用户头像上传的逻辑。”

为了回答这个简单的问题,模型被迫阅读了 8000 tokens 的无关知识,然后才找到真正需要的那 200 tokens。

这不是聪明。这是自虐。

System prompt 不是冰箱,你不应该把所有东西都塞进去保鲜。它是模型启动时加载到上下文里的内容——每一 token 都在和你的实际对话抢位置


s05 的解法:两层注入

s05 的解决方案优雅得离谱。就两层:

Layer 1 (cheap — ~100 tokens/skill):
  System prompt 只放技能名称和一行描述

Layer 2 (on demand — 完整内容):
  模型调用 load_skill("xxx") → 完整的技能文档注入进来

System Prompt 是这样写的:

Skills available:
  - pdf: Process PDF files and extract text content [file, document]
  - code-review: Review code for quality, security, and maintainability [code]

就这些。每个技能一行名字 + 一行描述,几十个 token 搞定。

然后模型在需要时调用加载:

TOOL_HANDLERS = {
    ...
    "load_skill": lambda **kw: SKILL_LOADER.get_content(kw["name"]),
}

get_content 返回的是完整的技能文档:

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>"

返回的内容类似:

<skill name="pdf">
  # PDF Processing
  
  This agent can process PDF files using the following approach:
  
  1. Use `pdftotext` to extract text content
  2. For scanned PDFs, use OCR via `tesseract`
  3. Extract tables using `camelot` or `tabula-py`
  
  Common commands:
  ```bash
  pdftotext document.pdf - | head -100
```

整个过程就是模型问"你有这方面的知识吗?"→ Harness 回答"有,给你"→ 知识作为 tool_result 进入上下文。


SkillLoader 的设计

s05 的 SkillLoader 读的是 skills/<name>/SKILL.md 文件,用的是 YAML frontmatter:

class SkillLoader:
    def __init__(self, skills_dir: Path):
        self.skills_dir = skills_dir
        self.skills = {}
        self._load_all()

    def _parse_frontmatter(self, text: str) -> tuple:
        match = re.match(r"^---\n(.*?)\n---\n(.*)", text, re.DOTALL)
        if not match:
            return {}, text
        meta = yaml.safe_load(match.group(1)) or {}
        return meta, match.group(2).strip()

所以 skills/pdf/SKILL.md 的文件结构是:

---
name: pdf
description: Process PDF files and extract text content
tags: file, document
---

# PDF Processing

Detailed instructions for handling PDF files...

三个字段:

  • name:技能名,模型调用 load_skill("pdf") 时用的名字
  • description:一行描述,放在 system prompt 里
  • tags:可选的标签,辅助分类(在描述里带上了)

文件名不重要,SKILL.md 才重要。 这种设计允许你按任何目录结构组织技能:

skills/
├── pdf/
│   └── SKILL.md
├── code-review/
│   └── SKILL.md
├── deployment/
│   ├── SKILL.md
│   └── templates/
│       └── ...

_load_all 用的是 rglob("SKILL.md"),所以技能可以嵌套在任意深度的目录里。


为什么这个模式有效?

两层注入之所以有效,是因为它和模型的工作方式高度吻合:

模型的注意力是有限的。 研究表明,Transformer 的注意力在长上下文中会"稀释"。你把 8000 tokens 的数据库 schema 放在 system prompt 里,模型在处理你的代码时,那 8000 tokens 就像背景噪音。

按需加载让模型知道自己有什么可用。 系统 prompt 里的技能列表就像是"目录"。模型看到目录,知道有 PDF 处理能力。当它遇到 PDF 文件时,它会主动调用 load_skill("pdf")

技能加载本身就是一次工具调用。 这意味着:

  • 技能内容作为 tool_result 进入上下文 → 紧挨着使用场景
  • 用完技能后,上下文压缩可以把旧技能内容替换掉
  • 不同任务可以用不同的技能组合

一个对比:知识泛滥 vs. 按需注入

没有技能系统:

User: "帮我解析这个 PDF 文件里的表格"
  → Agent 的 system prompt 里有 8000 tokens 的知识,
    包括 PDF 处理、数据库 schema、部署配置、编码规范……
  → 模型需要在 8000 tokens 的噪音里找到 PDF 处理的部分
  → 可能会漏掉关键细节

有技能系统:

User: "帮我解析这个 PDF 文件里的表格"
  → Agent 看到"pdf"这个技能名,调用 load_skill("pdf")
  → 完整的 PDF 处理指南进入上下文(tool_result)
  → 模型按照指南操作
  → 上下文里没有无关的数据库 schema 和部署配置

效率差距就是"在图书馆找一本书"和"书已经翻到你需要的页码"的差距。


这和 RAG 有什么区别?

你可能会问:“这不就是 RAG(检索增强生成)吗?”

对,底层思想是一样的——不要把所有知识都塞进 prompt,按需检索。但实现层面有两个关键区别:

1. 触发机制不同

  • RAG 通常是系统自动检索,在用户 query 之后、LLM 调用之前,从向量数据库里捞一段相关内容拼进 prompt
  • 这个 repo 的技能系统是模型主动触发——模型看到技能名,自己决定要不要加载

2. 知识被当成工具,而不是上下文的一部分

  • 在 RAG 里,检索到的文本被动地混在 prompt 里
  • 在这个系统里,load_skill 是一个工具调用,知识作为 tool_result 返回,模型主动消费它

区别的实质是:谁决定什么时候加载知识?

  • RAG:系统提前决定
  • Skill Loading:模型自己决定

又回到了那个核心思想:模型是司机。


结合其他 Session

技能加载和这个 repo 里的其他机制配合得天衣无缝:

s04 Subagent:父代理加载技能后,把任务和技能描述一起传给子代理

prompt = f"用以下方法处理这个PDF:\n{skill_content}\n\n文件:report.pdf"
run_subagent(prompt)

s06 上下文压缩:技能内容不会被压缩,因为它在 tool_result 里。但如果后续被 micro_compact 替换成了 [Previous: used load_skill],模型可以随时重新加载。

s07 Task System:创建任务时把需要的技能名写在任务描述里,Agent 认领任务后自动加载对应的技能。

这又是一个可组合性的例子。技能加载不是孤立的——它和其他机制一起,构成一个完整的 Agent 生态系统。


下集预告

s05 解决了"知识太多"的问题。s06 要解决的是另一个方向的问题——“对话太长”。

上下文总会满的——不管你怎么小心翼翼地管理 prompt,只要 Agent 一直工作,messages 就会一直增长。s06 的三层压缩策略,让 Agent 可以永远工作下去。

下一篇:Subagent —— 进程隔离就是上下文隔离

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐