- Published on
Agent Harness 工程:Skill 与按需加载
- Authors

- Name
- XiaoLeiJun
Agent Harness 工程:Skill 与按需加载
上一篇我们讲了 Subagent:主 Agent 负责规划、分派和整合,子 Agent 在干净上下文里完成局部任务,再把压缩后的结果交回主 Agent。
这一篇继续看一个最近很常见的概念:Skill。
现在很多 Agent 产品都会讲 Skill。比如:
- code review Skill;
- 写需求文档 Skill;
- 高考志愿分析 Skill;
- 数据分析 Skill;
- 前端页面实现 Skill;
- 测试用例生成 Skill。
名字听起来很新,但放到 Agent Harness 的工程视角里,Skill 并不神秘。
它本质上是在解决一个问题:
某些任务需要一套专门的背景知识、规范、流程和示例,但这些内容不应该永远塞在系统提示词里。
为什么需要 Skill
前几篇里,我们已经给 Agent 增加了很多能力:
TOOLS让模型知道有哪些工具;TOOL_HANDLERS把工具名映射到真实函数;- 权限和 Hook 管住工具调用;
- TodoWrite 维护任务计划;
- Subagent 隔离局部任务上下文。
但光有工具还不够。
比如你让 Agent 做 code review。如果没有额外说明,它大概率会按照通用代码审查习惯来检查:命名、复杂度、边界条件、测试覆盖。
这当然有用,但真实项目里的 code review 往往不只是这些。
它还需要知道:
- 这个仓库的代码规范;
- 哪些目录是核心业务;
- 哪些 API 不能随便改;
- 安全检查要重点看什么;
- 输出格式要符合团队习惯;
- 哪些自动化命令应该优先运行;
- 哪些问题是阻断合并,哪些只是建议。
这些内容可能已经写在团队文档里,也可能是某个领域的经验清单。问题是,它们动辄几千字甚至几万字。
如果把所有 Skill 的完整内容都放进系统提示词,会有三个问题。
第一,容易撑爆上下文窗口。一个 code review Skill、一个前端 Skill、一个测试 Skill、一个文档 Skill,全塞进去很快就变成提示词巨物。
第二,浪费 token。用户只是问“解释这个函数”,不需要加载完整 code review 规范。
第三,干扰模型注意力。太多无关说明会让模型在当前任务里抓不住重点,反而降低稳定性。
所以 Skill 的关键不是“把更多提示词塞给模型”,而是:把长期知识做成可发现、可按需加载的上下文。
Skill 的元信息常驻在系统提示词里,完整内容只有在任务匹配时才加载进当前上下文。
Skill 到底是什么
可以把 Skill 理解成一个小包。
这个包里通常包含:
| 部分 | 作用 |
|---|---|
name | Skill 的稳定名称 |
description | 什么时候应该使用这个 Skill |
SKILL.md 正文 | 具体规则、流程、示例、输出格式 |
| 附加文件 | 模板、脚本、参考资料、示例数据 |
| 可选工具约束 | 这个 Skill 推荐或禁止使用哪些工具 |
从 Harness 角度看,Skill 不是一个神奇的新运行时。
它更像是三种东西的组合:
- 一段可发现的元信息;
- 一份可按需加载的任务说明书;
- 一组可能被引用的本地资源。
这和工具不一样。
工具是 Agent 可以执行的动作,比如 read_file、edit_file、bash、run_subagent。Skill 更像是告诉 Agent:面对某类任务时,应该按什么标准、流程和格式来做。
当然,Skill 可以指导模型调用工具。比如 code review Skill 里可以写“先看 diff,再跑测试,再检查安全风险”。但 Skill 本身通常不是直接执行动作的函数。
一个 Skill 长什么样
一种常见目录结构是:
skills/
code-review/
SKILL.md
checklist.md
examples/
review-output.md
最小版本只需要一个 SKILL.md:
---
name: code-review
description: Use when the user asks to review code, find bugs, audit changes, or check a pull request.
---
# Code Review Skill
You are reviewing code for correctness, security, performance, maintainability, and tests.
## Workflow
1. Understand the change context.
2. Inspect the diff and related files.
3. Check correctness and edge cases.
4. Check security and data exposure risks.
5. Check performance risks.
6. Check whether tests cover the changed behavior.
7. Report findings first, ordered by severity.
## Review Checklist
- Security: injection, auth, authorization, secrets, unsafe commands.
- Correctness: null handling, race conditions, resource leaks, error paths.
- Performance: repeated expensive work, blocking operations, large allocations.
- Maintainability: naming, duplication, complexity, dead code.
- Testing: missing critical paths, weak assertions, untested edge cases.
## Output Format
### Findings
1. [severity] file:line - issue description
Impact: what can go wrong.
Fix: suggested change.
### Open Questions
- ...
### Summary
Short overall assessment.
开头的 frontmatter 很关键:
---
name: code-review
description: Use when the user asks to review code, find bugs, audit changes, or check a pull request.
---
name 和 description 是给 Agent 发现 Skill 用的。它们应该短、准、稳定。
正文则是给 Agent 使用 Skill 时读的。这里可以写更细的流程、规范、例子、输出格式,甚至引用同目录下的其它文件。
Skill 可以先从一个 SKILL.md 开始,后面再扩展模板、脚本和参考资料。
启动时只加载 Skill 元信息
现在开始实现。
第一步,扫描 skills/ 目录,读取每个 Skill 的 name 和 description。
教学版先用一个简单 frontmatter 解析器。真实项目里可以换成 YAML parser。
from dataclasses import dataclass
from pathlib import Path
from typing import Callable
SKILLS_DIR = WORKDIR / "skills"
@dataclass(frozen=True)
class SkillMeta:
name: str
description: str
path: Path
def parse_skill_file(path: Path) -> tuple[SkillMeta, str]:
text = path.read_text(encoding="utf-8")
if not text.startswith("---"):
raise ValueError(f"Skill file missing frontmatter: {path}")
try:
_, header, body = text.split("---", 2)
except ValueError as exc:
raise ValueError(f"Invalid skill frontmatter: {path}") from exc
fields: dict[str, str] = {}
for raw_line in header.strip().splitlines():
if ":" not in raw_line:
continue
key, value = raw_line.split(":", 1)
fields[key.strip()] = value.strip().strip("\"'")
name = fields.get("name", "").strip()
description = fields.get("description", "").strip()
if not name:
raise ValueError(f"Skill missing name: {path}")
if not description:
raise ValueError(f"Skill missing description: {path}")
return SkillMeta(name=name, description=description, path=path), body.strip()
然后扫描目录:
def discover_skills(skills_dir: Path = SKILLS_DIR) -> dict[str, SkillMeta]:
registry: dict[str, SkillMeta] = {}
if not skills_dir.exists():
return registry
for skill_file in sorted(skills_dir.glob("*/SKILL.md")):
try:
meta, _ = parse_skill_file(skill_file)
except Exception as exc:
print(f"[skill:skip] {skill_file}: {exc}")
continue
if meta.name in registry:
print(f"[skill:skip] duplicate skill name: {meta.name}")
continue
registry[meta.name] = meta
return registry
SKILL_REGISTRY = discover_skills()
这里注意,我们只把元信息放进 SKILL_REGISTRY。
也就是说,启动时不读取完整正文到系统提示词里。否则 Skill 多了以后,还是会回到“大提示词”问题。
更严格地说,教学代码为了少写一个解析函数,parse_skill_file() 会把正文一起解析出来,但 discover_skills() 只保留 meta,不会把 body 放进注册表或系统提示词。真实工程里如果 Skill 文件很大,可以再拆一个 parse_skill_meta(),只读取 frontmatter。
把 Skill 列表放进系统提示词
Agent 怎么知道有哪些 Skill?
这和工具很像。我们不需要把工具实现代码给模型,只需要把工具名、描述和参数结构给它。Skill 也是一样:先把 name 和 description 给模型。
def render_skill_catalog() -> str:
if not SKILL_REGISTRY:
return "No skills are available."
lines = ["Available skills:"]
for skill in SKILL_REGISTRY.values():
lines.append(f"- {skill.name}: {skill.description}")
return "\n".join(lines)
然后把 Skill 目录挂到系统提示词里:
SYSTEM_PROMPT = f"""
你是一个编程助手,工作在当前目录:{WORKDIR}。
你可以使用工具读取文件、修改文件、执行必要命令、维护 Todo,并在需要时加载 Skill。
{render_skill_catalog()}
Skill 使用规则:
1. 如果用户任务明显匹配某个 Skill,先调用 load_skill 加载该 Skill 的完整说明。
2. 只加载当前任务需要的 Skill,不要一次性加载所有 Skill。
3. 加载 Skill 后,优先遵循 Skill 中的流程、检查清单和输出格式。
4. 如果没有匹配 Skill,就按普通 Agent 流程完成任务。
"""
这一步之后,模型知道“有哪些 Skill”以及“大概什么时候用”。
但它还没有看到 SKILL.md 的完整内容。
完整内容要通过工具按需加载。
实现 load_skill 工具
接下来定义一个新工具:load_skill。
这个工具只做一件事:根据 skill 名称读取对应 SKILL.md 的正文,并返回给模型。
LOAD_SKILL_TOOL = {
"type": "function",
"function": {
"name": "load_skill",
"description": (
"Load the full instructions for a named skill. "
"Use this before performing a task that matches an available skill."
),
"parameters": {
"type": "object",
"properties": {
"skill_name": {
"type": "string",
"description": "The name of the skill to load.",
}
},
"required": ["skill_name"],
},
},
}
然后实现真实函数:
def run_load_skill(skill_name: str) -> str:
name = skill_name.strip()
meta = SKILL_REGISTRY.get(name)
if meta is None:
available = ", ".join(sorted(SKILL_REGISTRY)) or "none"
return f"Error: unknown skill {name}. Available skills: {available}"
loaded_meta, body = parse_skill_file(meta.path)
if loaded_meta.name != meta.name:
return f"Error: skill metadata changed while loading {name}"
result = f"""
# Skill: {meta.name}
Description: {meta.description}
{body}
""".strip()
return result[:20000]
这里有几个细节。
第一,只能加载注册表里的 Skill。模型不能随便传一个路径让 Harness 读文件。
第二,每次加载时重新解析一次文件。这样开发时修改 SKILL.md 后,可以在下一次加载时生效。真实服务里可以加缓存和版本号。
第三,返回结果做长度限制。Skill 也可能写得很长,不能无限制塞回上下文。
第四,load_skill 返回的是工具结果,所以会进入 messages。模型后续就能基于这份 Skill 继续工作。
加入工具注册表
和前几篇一样,把工具定义加入 TOOLS:
TOOLS = [
BASH_TOOL,
LIST_DIR_TOOL,
READ_FILE_TOOL,
WRITE_FILE_TOOL,
EDIT_FILE_TOOL,
TODO_WRITE_TOOL,
SUBAGENT_TOOL,
LOAD_SKILL_TOOL,
]
再把真实函数加入 TOOL_HANDLERS:
TOOL_HANDLERS: dict[str, Callable[..., str]] = {
"bash": bash,
"list_dir": list_dir,
"read_file": read_file,
"write_file": write_file,
"edit_file": edit_file,
"run_todo_write": run_todo_write,
"run_subagent": run_subagent,
"load_skill": run_load_skill,
}
Agent Loop 仍然不用改。
模型请求 load_skill,Harness 执行,工具结果写回 messages。后续模型就能读到 Skill 正文,再继续调用其它工具或给出最终回答。
这也是这个系列一直在强调的结构:新能力最好先变成工具或 Hook,而不是直接把主循环改乱。
权限策略怎么处理
load_skill 是一个读取类工具,但它和普通 read_file 不一样。
普通 read_file 读取用户指定路径,需要做路径安全检查。load_skill 只允许读取 SKILL_REGISTRY 里已经发现的 SKILL.md。
所以它可以默认放行:
def check_permission(tool_name: str, arguments: dict[str, Any]) -> PermissionResult:
if tool_name == "load_skill":
return PermissionResult(
"allow",
"load_skill reads a registered skill file only",
)
# 其他规则沿用前几篇
不过,如果 Skill 可以引用附加文件、运行脚本、调用外部服务,就要更谨慎。
可以先定几条边界:
load_skill只读skills/目录下的注册文件;- Skill 里引用的附加文件也必须在对应 Skill 目录内;
- Skill 不能绕过现有工具权限;
- Skill 推荐的命令仍然要走
bash权限策略; - Skill 推荐的文件修改仍然要走
write_file或edit_file权限策略。
换句话说,Skill 不是权限后门。它只是上下文和流程的按需加载机制。
Skill 和 Tool 的区别
到这里可以整理一下 Skill 和 Tool 的关系。
| 概念 | 主要作用 | 是否直接执行动作 |
|---|---|---|
| Tool | 给 Agent 一个动作能力 | 是 |
| Skill | 给 Agent 一套任务方法 | 通常不是 |
比如:
read_file是 Tool,因为它真的读取文件;edit_file是 Tool,因为它真的修改文件;run_subagent是 Tool,因为它真的启动一个子循环;code-review是 Skill,因为它告诉 Agent 如何审查代码;frontend-implementation是 Skill,因为它告诉 Agent 如何实现前端页面;prd-writing是 Skill,因为它告诉 Agent 如何组织需求文档。
当然,Skill 里可以写“先调用 git diff 看变更,再读取相关文件,再输出 Findings”。但真正执行动作的仍然是工具。
Skill 更像是一个任务说明书,而工具是执行动作的手。
Skill 和 Subagent 怎么配合
上一篇讲了 Subagent,这一篇讲 Skill。两者也可以组合。
主 Agent 可以先根据任务加载 Skill,再决定是否把局部任务交给子 Agent。
比如用户说:
帮我 review 最近一次提交。
主 Agent 可以:
- 根据系统提示词里的 Skill 目录发现
code-review; - 调用
load_skill("code-review"); - 根据 Skill 要求创建 Todo;
- 调用工具查看 diff 和相关文件;
- 如果 diff 很大,把“审查认证模块”交给只读 Subagent;
- 汇总子 Agent 结论;
- 按 Skill 要求输出 Findings。
也可以让子 Agent 自己拥有一小部分 Skill。
比如一个“测试分析子 Agent”只暴露 test-analysis Skill 和只读工具。这样它既有干净上下文,又有专门任务规范。
但还是同一个原则:不要把所有 Skill 都塞给所有 Agent。主 Agent、子 Agent、不同角色的 Agent,都应该只看到自己需要的 Skill 目录。
Skill 什么时候有用
Skill 适合这类场景:
- 任务有稳定流程;
- 输出格式需要固定;
- 需要长期维护一套规范;
- 需要引用较长的背景文档;
- 需要按领域经验检查细节;
- 不同任务需要不同工作方式。
例如 code review、PRD 写作、事故复盘、测试计划、数据分析报告、前端实现规范,都很适合做成 Skill。
它不适合所有任务。
如果用户只是问一个简单概念,或者让 Agent 改一个很小的问题,没必要加载一大份 Skill。Skill 是为“可复用的任务方法”准备的,不是为每次对话都加一层仪式。
常见坑
第一,把 Skill 当成系统提示词仓库。 如果只是把所有 Skill 正文拼到系统提示词里,那并没有解决问题,只是换了个目录组织大提示词。
第二,description 写得太含糊。 description: useful helper 这种描述对模型没有帮助。它应该写清楚“什么时候使用这个 Skill”。
第三,Skill 正文太空。 只写“你要认真 code review”没意义。Skill 应该提供流程、清单、输出格式和具体判断标准。
第四,Skill 里偷偷绕过权限。 Skill 可以建议调用工具,但不能让工具跳过 Harness 的权限检查。
第五,Skill 失控膨胀。 Skill 不是越长越好。常驻的是元信息,正文也应该尽量结构清晰,必要时拆成附加文件按需读。
小结
这一篇把 Skill 拆成了一个工程上可实现的机制:
- 用
skills/<name>/SKILL.md组织任务规范; - 在
SKILL.mdfrontmatter 里写name和description; - 启动时只扫描 Skill 元信息,构建
SKILL_REGISTRY; - 把 Skill 目录渲染进系统提示词,让模型知道有哪些 Skill;
- 用
load_skill工具按需读取完整 Skill 正文; - 把
load_skill加入TOOLS和TOOL_HANDLERS,复用原来的 Agent Loop; - 继续用权限策略和 Hook 保证 Skill 不能绕过 Harness。
到这里,Agent Harness 已经不只是有工具、计划和子代理,还开始有了可复用的任务方法库。
下一篇继续处理一个越来越绕不开的问题:上下文压缩。当 messages 越来越长,Skill、Todo、Subagent 结果、工具输出都在不断进入上下文时,Harness 应该如何保留关键事实、压缩历史、丢掉噪声,并让 Agent 在长任务里不迷路?