Agent 为什么需要 Skills:别把所有知识都塞进 system prompt

从零实现 Agent Harness 系列 · 第 04 篇

这一篇讲 Skills:一种让 Agent 按需加载专业知识和操作流程的机制。

它要解决的问题很直接:真实 Agent 不可能把所有领域知识都长期塞进 system prompt。

前言

一个真实 Agent 往往要会很多事:

  • 怎么做 code review
  • 怎么写测试
  • 怎么处理 PDF
  • 怎么排查线上问题
  • 怎么按团队规范提交 PR
  • 怎么构建一个 MCP server

最直接的做法,是把这些说明全部写进 system prompt。但这很快会出问题。

如果用户只是让 Agent review 一段代码,模型其实不需要同时背着 PDF 处理说明、部署说明、MCP server 编写规范和数据库迁移规则。这些无关内容会带来几个问题:

  • 占上下文
  • 增加 token 成本
  • 稀释注意力
  • 让系统提示越来越难维护

所以更合理的方式是:

text 复制代码
先告诉模型"有哪些技能"
等模型真的需要某个技能时
再把完整技能说明加载进上下文

这就是 Skill Loading。

一句话说:

Skills 是一种按需加载知识的机制:system prompt 里只放技能索引,完整技能正文等需要时再加载。

一、Skills 要解决什么问题

Skills 很容易和 Tool 混在一起,但二者不是一回事。

Tool 更像动作按钮,回答"系统能执行什么动作?"; Skill 更像任务说明书,回答"做这类任务时,应该遵循什么流程和判断标准?"。

比如 read_file 能读取文件,code-review 则告诉模型 review 时先找 bug、风险和缺失测试,输出时 findings 优先,引用文件和行号,不要把总结放在最前面。

所以可以这样区分:

text 复制代码
Tool:让 Agent 能做动作
Skill:让 Agent 知道怎么把动作做对

二、为什么不能把所有 Skill 都塞进 system prompt

system prompt 适合放稳定、通用、每次都要遵守的规则。

比如:

text 复制代码
你是一个 Agent
工作前先理解当前任务
不要破坏用户已有上下文或改动
优先使用最合适的工具和资料

但它不适合塞大量可选领域知识。

假设有三个技能:

text 复制代码
pdf:处理 PDF 文件
code-review:做代码审查
deploy:部署服务

用户只问:

text 复制代码
帮我 review 这个 PR

这时模型需要 code-review 的完整说明,但不需要 pdfdeploy 的全文。

更好的设计是两层加载:

text 复制代码
Layer 1:轻量索引
  放进 system prompt
  只包含技能名和一句话简介

Layer 2:完整正文
  通过 load_skill 工具返回
  只有模型主动请求时才进入上下文

这有点像人类查资料:

text 复制代码
先看目录
需要哪章
再翻哪章

三、Skills 如何按需加载

3.1 SKILL.md 里写什么

一个技能通常可以写成一份 SKILL.md

它可以包含两部分:

markdown 复制代码
---
name: code-review
description: Review code for bugs, regressions, and missing tests.
tags: [python, review]
---

When reviewing code, lead with findings.
Focus on bugs, risks, and missing tests.
Reference file paths and line numbers when possible.

上面的 --- 区块叫 frontmatter,用来放元数据。

正文部分才是真正给模型看的技能说明。

程序会把它拆成:

text 复制代码
meta:技能名、简介、标签
body:完整技能正文

这样就能做到:

text 复制代码
system prompt 里只放 meta
模型需要时再加载 body

3.2 加载链路怎么走

Skill Loading 的运行链路可以分成四步。

第一步,启动时扫描技能目录。

程序会找到所有 SKILL.md,解析 frontmatter,并建立一个 registry:

python 复制代码
{
    "code-review": {
        "description": "Review code for bugs...",
        "body": "When reviewing code...",
    }
}

第二步,把技能索引放进 system prompt。

模型一开始看到的不是全文,而是:

text 复制代码
Skills available:
- code-review: Review code for bugs and risks.
- pdf: Process PDF files.
- mcp-builder: Build MCP servers and clients.

第三步,模型判断是否需要某个技能。

比如用户说:

text 复制代码
帮我 review 这次改动

模型看到有 code-review,于是调用:

text 复制代码
load_skill({"name": "code-review"})

第四步,完整技能正文作为 tool result 进入上下文。

之后模型就可以按照这份技能说明继续工作。

完整链路是:

text 复制代码
启动时扫描 SKILL.md
  -> system prompt 注入技能索引
  -> 模型根据任务选择 load_skill
  -> 完整技能正文进入上下文
  -> 模型按技能说明执行任务

四、Skill Loading 在代码里怎么接入 Agent Loop

4.1 先扫描技能目录

这一节的代码主线其实很短,可以抓住四个点:

text 复制代码
SkillLoader:扫描技能
SYSTEM:注入技能索引
TOOLS:暴露 load_skill
agent_loop:执行 load_skill,并把结果放回 messages

核心类是 SkillLoader

python 复制代码
class SkillLoader:
    """扫描 skills/<name>/SKILL.md,并支持按需加载完整技能正文。"""

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

初始化时,它会扫描技能目录:

python 复制代码
def _load_all(self):
    if not self.skills_dir.exists():
        return

    for fp in sorted(self.skills_dir.rglob("SKILL.md")):
        text = fp.read_text()
        meta, body = self._parse_frontmatter(text)
        name = str(meta.get("name") or fp.parent.name)
        self.skills[name] = {
            "meta": meta,
            "body": body,
            "path": str(fp),
        }

这里最关键的是:

text 复制代码
meta 进入技能索引
body 暂时不进入 system prompt

4.2 再注入技能索引

代码里只把技能索引放进去:

python 复制代码
SYSTEM = f"""You are an Agent working in {WORKDIR}.
Use tools to solve tasks.
Use load_skill to access specialized knowledge before tackling unfamiliar topics.

Skills available:
{SKILL_LOADER.get_descriptions()}"""

get_descriptions() 返回的是轻量目录:

python 复制代码
def get_descriptions(self) -> str:
    if not self.skills:
        return "(no skills available)"

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

所以模型一开始看到的是:

text 复制代码
Skills available:
  - code-review: Review code for bugs...
  - pdf: Process PDF files...

而不是所有 SKILL.md 的完整正文。

4.3 再把 load_skill 暴露成工具

工具实现很简单:

python 复制代码
TOOL_HANDLERS = {
    "load_skill": lambda **kw: SKILL_LOADER.get_content(kw["name"]),
}

工具 schema 告诉模型可以按名字加载技能:

python 复制代码
{
    "type": "function",
    "function": {
        "name": "load_skill",
        "description": "Load specialized knowledge by skill name.",
        "parameters": {
            "type": "object",
            "properties": {
                "name": {
                    "type": "string",
                    "description": "Skill name to load.",
                },
            },
            "required": ["name"],
        },
    },
}

真正读取正文的是 get_content()

python 复制代码
def get_content(self, name: str) -> str:
    skill = self.skills.get(name)
    if not skill:
        available = ", ".join(self.skills.keys()) or "(none)"
        return f"Error: Unknown skill '{name}'. Available: {available}"

    body = skill["body"]
    path = skill["path"]
    return f"<skill name=\"{name}\" path=\"{path}\">\n{body}\n</skill>"

这里用 <skill ...> 标签包起来,是为了让模型更容易识别这段内容的边界。

4.4 最后接回 Agent Loop

循环本身没有大改:

python 复制代码
def agent_loop(messages: list):
    while True:
        response = client.chat.completions.create(
            model=model_name,
            messages=[{"role": "system", "content": SYSTEM}] + messages,
            tools=TOOLS,
            tool_choice="auto",
        )
        assistant_message = response.choices[0].message
        messages.append(assistant_message.model_dump(exclude_none=True))

        if not assistant_message.tool_calls:
            return

        results = []
        for tool_call in assistant_message.tool_calls:
            output = execute_tool_call(tool_call)
            results.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": str(output)[:50000],
            })

        messages.extend(results)

也就是说,load_skill 并没有发明新的 Agent Loop。

它只是让模型多了一种获取知识的方式:

text 复制代码
模型看到技能索引
  -> 判断需要某个技能
  -> 调用 load_skill
  -> 技能正文作为 tool result 进入 messages
  -> 下一轮模型基于这段正文继续工作

这就是 Skill Loading 最小实现的核心。

4.5 load_skill 为什么要做成工具

这里有一个细节很关键:

text 复制代码
Skill 本身不是工具
load_skill 才是工具

Skill 是知识内容。

load_skill 是读取知识内容的动作。

模型看到的是一个工具 schema:

python 复制代码
{
    "name": "load_skill",
    "description": "Load specialized knowledge by skill name.",
    "parameters": {
        "type": "object",
        "properties": {
            "name": {
                "type": "string",
                "description": "Skill name to load.",
            },
        },
        "required": ["name"],
    },
}

当模型调用这个工具时,程序读取对应的 SKILL.md,然后把正文作为 tool result 返回。

可以理解成:

text 复制代码
load_skill 是模型打开某本说明书的动作
Skill 是那本说明书的内容

五、Skills 和 MCP 有什么区别

Skills 和 MCP 都在扩展 Agent,但方向不同。

Skills 解决的是:

text 复制代码
模型应该怎么思考和操作?

MCP 解决的是:

text 复制代码
外部系统有什么能力,模型应用怎么发现和调用?

可以这样对比:

text 复制代码
Skill:
  给模型补充知识、流程、规范
  通常是文档
  通过 load_skill 按需进入上下文

MCP:
  给模型应用接入外部工具、资源、提示模板
  通常是一个 server
  通过 MCP client 发现和调用

举个例子。

mcp-builder skill 可以告诉模型:

text 复制代码
写 MCP server 时要先定义 Tool / Resource / Prompt;
stdio server 的 stdout 只能输出协议消息;
工具列表变化时要考虑 list_changed 通知。

而 MCP server 本身负责暴露真实能力:

text 复制代码
tools/list
tools/call
resources/read

所以一句话区分:

text 复制代码
Skills 更偏知识注入
MCP 更偏能力接入

从工程上看,Skill Loading 的价值不在于代码复杂,而在于它把上下文管理做成了分层。

没有 Skills 时,Agent 很容易变成这样:

text 复制代码
所有规则都塞进 system prompt
每一轮都带着完整说明
上下文越来越长
模型越来越容易分心

有了 Skills 后,结构变成:

text 复制代码
system prompt:稳定规则 + 技能索引
tool result:按需加载的技能正文
history:当前会话已经加载过的技能

这样至少有三个好处:

  • 节省上下文
  • 降低无关信息干扰
  • 让领域知识可以独立维护和复用

真实系统里,还可以继续往前做:

  • 技能版本管理
  • 权限控制
  • 技能来源审核
  • token 预算管理
  • 避免重复加载
  • 技能使用审计

但最核心的模式就是:

text 复制代码
先索引
再按需加载
最后把知识留在当前上下文里

小结

如果把 Skills 压缩成几句话:

  1. Skills 是按需加载的知识包,不是外部工具本身。
  2. system prompt 里只放技能索引,避免把所有知识都塞进上下文。
  3. 模型需要某个技能时,通过 load_skill 把完整正文加载进来。
  4. load_skill 是工具,Skill 是工具返回的知识内容。
  5. Skills 更偏"知识注入",MCP 更偏"能力接入"。

这套机制的本质,是让 Agent 不必一开始就背完整本手册。

它只需要先知道目录,等任务真的需要时,再把对应章节翻出来。

相关推荐
JieE2122 天前
LeetCode 101. 对称二叉树|JS 递归 + 迭代双解法,彻底搞懂镜像判断
javascript·算法
JieE2122 天前
LeetCode 56. 合并区间|超清晰 JS 图解思路,面试高频区间题
javascript·算法·面试
Jack203 天前
HarmonyOS开发中错误处理策略:网络异常统一处理
算法
小小杨树3 天前
读懂色彩:拍照调色不再难
算法·计算机视觉·配色
JieE2123 天前
LeetCode 226. 翻转二叉树|JS 递归超详细拆解,二叉树入门经典题
javascript·算法
JieE2124 天前
LeetCode 104. 二叉树的最大深度|递归思路超详细拆解
javascript·算法
vivo互联网技术4 天前
CVPR 2026 | 全新强化学习框架 BeautyGRPO:重塑真实人像
算法·大模型·cvpr·影像
Darling噜啦啦4 天前
列表转树算法深度解析:从 Map 到 Reduce 的两种实现,面试高频考点
数据结构·算法·面试