从零实现 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 的完整说明,但不需要 pdf 和 deploy 的全文。
更好的设计是两层加载:
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 压缩成几句话:
- Skills 是按需加载的知识包,不是外部工具本身。
- system prompt 里只放技能索引,避免把所有知识都塞进上下文。
- 模型需要某个技能时,通过
load_skill把完整正文加载进来。 load_skill是工具,Skill 是工具返回的知识内容。- Skills 更偏"知识注入",MCP 更偏"能力接入"。
这套机制的本质,是让 Agent 不必一开始就背完整本手册。
它只需要先知道目录,等任务真的需要时,再把对应章节翻出来。