S05 技能系统(Skill)教程
整体说明
承接前文 S01~S04 已有能力:基础Agent循环、工具路由+安全沙箱、会话待办规划、子代理上下文隔离。
本章核心:把专项领域知识从固定提示词中拆分,实现「轻量发现、按需加载」,解决提示词臃肿、Token 浪费问题。全程在原有代码基础上增量开发,主循环、已有工具逻辑无需改动。
一、提出问题
1. 现有能力回顾
到 S04 为止,Agent 已具备完整基础能力:
- 自主调用各类工具执行操作;
- 通过
todo做会话内多步骤任务规划; - 利用子代理隔离局部任务上下文,避免主消息爆炸。
2. 现存痛点
不同任务需要不同专项知识/操作规范 ,例如:代码审查、Git 版本操作、框架搭建等。
如果按照传统写法,把所有领域知识、操作手册全部硬编码写入固定 system prompt,会产生两个严重问题:
- Token 严重浪费:大部分场景用不到的知识常驻提示词,增加接口开销、拉长上下文;
- 提示词臃肿混乱:全局规则、工具说明、各类专业知识混杂在一起,模型难以区分核心指令,执行逻辑出错。
3. 本章核心目标
- 设计技能(Skill) 体系,将「全局固定规则」和「可选专项知识」解耦;
- 分为两层设计:先展示轻量化技能目录 (让模型知道有哪些技能可用),需要时再加载完整技能内容;
- 技能内容仅在当前任务需要时注入上下文,任务结束不常驻,从根源精简提示词;
- 守住教学边界:仅实现基础的「发现+按需加载」,不拓展多源加载、条件激活、技能参数化等复杂功能。
4. 核心诉求总结
不再把所有知识一次性塞进提示词,做到 用什么技能,就加载什么技能。
二、分析问题
1. 核心名词通俗解读
| 名词 | 通俗解释 |
|---|---|
| Skill(技能) | 针对某一类专项任务的可复用操作手册,包含使用场景、执行步骤、注意事项(例:代码审查清单、Git 提交规范) |
| Discovery(发现) | 只展示技能轻量化元信息(名称+简短描述),体积极小,常驻在系统提示词中,让模型"知道有哪些技能" |
| Loading(加载) | 模型判断需要某份技能时,通过专用工具读取完整技能正文,临时注入上下文使用(会加载大量文本,属于"重操作") |
| Skill Registry(技能注册表) | 统一管理所有技能的容器,负责扫描技能文件、查询目录、加载完整内容,集中管控,避免代码散乱 |
2. 核心心智模型(两层架构)
整体流程
固定系统提示词(常驻)
├─ 全局身份/基础规则
├─ 工具说明
└─ 【轻量化技能目录】(仅名称+简介,所有技能都在这里展示)
↓
用户发起专项任务 → 模型匹配到对应技能 → 调用 load_skill 工具
↓
读取技能完整正文 → 以工具结果形式注入消息上下文
↓
模型依据加载后的完整技能手册执行任务
核心原则
- 常驻层:只放轻量信息(全局规则、技能目录),不占用大量 Token;
- 按需层:完整技能正文临时加载,任务完成后不长期留存。
3. 三大基础数据结构
(1)SkillManifest 技能元数据(目录条目)
轻量化信息,仅用于"发现阶段",结构极简:
python
{
"name": "code-review", # 技能名称(唯一标识)
"description": "代码审查检查清单" # 技能用途描述
}
(2)SkillDocument 完整技能文档
包含元数据 + 详细正文,是真正要加载的内容:
python
{
"manifest": {...}, # 上面的 SkillManifest 元数据
"body": "这里是完整的技能手册、步骤、规范等大段文本"
}
(3)SkillRegistry 技能注册表
全局统一管理器,核心能力:
- 自动扫描本地技能目录,加载所有技能;
- 对外输出技能目录(供系统提示词使用);
- 根据技能名,读取并返回完整技能正文。
4. 易混淆概念区分(初学者必看)
很多人会把 Skill / Memory / CLAUDE.md 弄混,用场景快速区分:
- Skill 技能 :某一类任务的操作手册,按需加载,临时使用(例:代码审查步骤、Git 操作规范);
- Memory 记忆 :跨会话需要长期记住的事实、用户偏好(例:用户习惯用 Python、项目目录路径);
- CLAUDE.md :全局永久规则,整个系统的基础约束,永远常驻提示词(例:工具使用总规则、安全限制)。
5. 新旧方案对比
| 方案 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| 传统方案 | 所有技能正文全量写入 system prompt | 实现简单 | 提示词臃肿、Token 浪费、模型易被冗余信息干扰 |
| 技能系统方案 | 常驻轻量目录 + 按需加载完整正文 | 精简提示词、节约 Token、逻辑清晰 | 需额外实现技能管理与加载工具 |
6. 初学者高频踩坑点
- 把所有技能正文常驻提示词:违背技能系统设计初衷,回到臃肿老问题;
- 技能目录描述过于简单:只写技能名、不写用途,模型无法判断何时该加载对应技能;
- 将技能当成强制全局规则:技能是「可选手册」,不是必须全程遵守的硬性规则;
- 混淆 Skill / Memory:把临时任务手册当成长期记忆,架构混乱;
- 一步到位做复杂加载逻辑:初学先实现"目录展示+单技能加载",不要叠加多源加载、条件激活等复杂功能。
三、解决问题(落地实现 + 完整可运行代码)
1. 整体实现思路
基于 S04 子代理代码 增量开发,原有:安全沙箱、基础工具、Todo待办、子代理、主循环 全部保留,仅新增 4 部分内容:
- 本地
skills目录,存放多个技能文件; SkillRegistry技能注册表类,扫描、解析、加载技能;- 新增
load_skill工具,用于触发技能加载; - 修改系统提示词,嵌入轻量化技能目录。
2. 第一步:规划本地文件目录
在代码同级目录,创建如下文件夹结构(纯文本文件,无需额外依赖):
项目根目录
├─ agent_workspace/ # S02 遗留的文件沙箱目录
├─ skills/ # 新增:技能总目录
│ ├─ code-review/ # 技能1:代码审查
│ │ └─ SKILL.md # 技能文件(元数据 + 正文)
│ └─ git-workflow/ # 技能2:Git 工作流
│ └─ SKILL.md
└─ main.py # 主代码文件
3. 第二步:编写技能文件(SKILL.md)
文件头部用 --- 包裹元数据(Frontmatter),后面写技能正文(初学者无需第三方解析库,代码手动分割)。
示例1:skills/code-review/SKILL.md
md
---
name: code-review
description: 代码审查专用清单,用于检查代码语法、注释、异常处理、代码规范
---
# 代码审查标准流程
1. 检查代码语法是否合法,有无明显报错;
2. 检查函数、类是否添加注释说明;
3. 检查是否包含异常捕获逻辑;
4. 检查变量命名是否符合Python规范;
5. 汇总所有问题,输出审查报告。
示例2:skills/git-workflow/SKILL.md
md
---
name: git-workflow
description: Git版本控制操作规范,包含分支创建、提交、推送步骤
---
# Git 标准工作流
1. 新建分支:git checkout -b 分支名
2. 查看变更:git status
3. 暂存文件:git add .
4. 提交代码:git commit -m "提交说明"
5. 推送远程:git push origin 分支名
4. 第三步:完整可运行代码(兼容DeepSeek + Windows编码 + 全历史功能)
python
from openai import OpenAI
import json
from pathlib import Path
import subprocess
import locale
# ===================== 1. 全局基础配置 =====================
DEEPSEEK_API_KEY = "你的DeepSeek API Key"
client = OpenAI(
api_key=DEEPSEEK_API_KEY,
base_url="https://api.deepseek.com",
timeout=30.0
)
MODEL = "deepseek-chat"
WORKDIR = Path("./agent_workspace")
WORKDIR.mkdir(exist_ok=True)
SKILLS_DIR = Path("./skills") # 技能根目录
# ===================== 2. 新增:技能注册表 SkillRegistry(S05核心) =====================
class SkillRegistry:
def __init__(self, skills_root: Path):
self.skills_root = skills_root
self.skill_data = {} # 存储所有技能:{技能名: 完整技能文档}
self._scan_skills()
def _scan_skills(self):
"""自动扫描所有SKILL.md文件,解析元数据和正文"""
# 遍历所有子文件夹下的SKILL.md
for skill_file in self.skills_root.rglob("SKILL.md"):
content = skill_file.read_text(encoding="utf-8")
# 简易解析 Frontmatter:以 --- 分割元数据和正文(初学者简化方案)
parts = content.split("---")
if len(parts) < 3:
continue
# 提取元数据
meta_lines = parts[1].strip().splitlines()
meta = {}
for line in meta_lines:
if ":" in line:
k, v = line.split(":", 1)
meta[k.strip()] = v.strip()
# 提取技能正文
body = parts[2].strip()
# 组装完整技能文档
skill_name = meta.get("name", skill_file.parent.name)
self.skill_data[skill_name] = {
"manifest": {
"name": skill_name,
"description": meta.get("description", "暂无描述")
},
"body": body
}
def get_skill_list(self) -> str:
"""输出轻量化技能目录(用于嵌入系统提示词)"""
if not self.skill_data:
return "暂无可用技能"
lines = ["可用技能列表:"]
for name, info in self.skill_data.items():
manifest = info["manifest"]
lines.append(f"- {name}:{manifest['description']}")
return "\n".join(lines)
def load_skill_body(self, skill_name: str) -> str:
"""根据技能名,加载完整技能正文"""
if skill_name not in self.skill_data:
return f"错误:未找到名为【{skill_name}】的技能"
return f"【{skill_name} 技能手册】\n{self.skill_data[skill_name]['body']}"
# 初始化全局技能注册表
SKILL_REGISTRY = SkillRegistry(SKILLS_DIR)
# ===================== 3. S02 原有:安全沙箱 + 基础工具(完全保留) =====================
def safe_path(file_path: str) -> Path:
full_path = (WORKDIR / file_path).resolve()
if not full_path.is_relative_to(WORKDIR):
raise PermissionError(f"禁止访问工作目录外文件:{file_path}")
return full_path
def run_bash(command: str) -> str:
try:
raw_out = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, timeout=10)
sys_encode = locale.getpreferredencoding(False)
return f"命令执行成功:\n{raw_out.decode(sys_encode, errors='replace')}"
except subprocess.CalledProcessError as e:
sys_encode = locale.getpreferredencoding(False)
return f"命令异常:{e.output.decode(sys_encode, errors='replace')}"
except Exception as e:
return f"命令失败:{str(e)}"
def run_read_file(path: str, limit: int = None) -> str:
try:
fp = safe_path(path)
content = fp.read_text(encoding="utf-8")
if limit:
content = "\n".join(content.splitlines()[:limit])
return f"【{path}内容】:\n{content}"
except Exception as e:
return f"读取失败:{str(e)}"
def run_write_file(path: str, content: str) -> str:
try:
fp = safe_path(path)
fp.write_text(content, encoding="utf-8")
return f"✅成功写入{path}"
except Exception as e:
return f"写入失败:{str(e)}"
def run_edit_file(path: str, old_text: str, new_text: str) -> str:
try:
fp = safe_path(path)
txt = fp.read_text("utf-8")
txt = txt.replace(old_text, new_text)
fp.write_text(txt, "utf-8")
return f"✅{path}文本替换完成"
except Exception as e:
return f"编辑失败:{str(e)}"
# ===================== 4. S03 原有:Todo待办管理类(完全保留) =====================
class TodoManager:
def __init__(self):
self.items = []
def update(self, items: list):
valid_items = []
in_progress_cnt = 0
if len(items) > 20:
raise ValueError("任务清单不能超过20项")
for item in items:
content = item.get("content", "").strip()
status = item.get("status", "pending")
if not content:
raise ValueError("任务内容不能为空")
if status not in ["pending", "in_progress", "completed"]:
raise ValueError("状态只能是pending/in_progress/completed")
if status == "in_progress":
in_progress_cnt +=1
valid_items.append({"content": content, "status": status})
if in_progress_cnt > 1:
raise ValueError("同一时间只能有1个in_progress任务")
self.items = valid_items
return self.render()
def render(self):
if not self.items:
return "暂无待办任务"
mark_map = {"pending":"[ ]", "in_progress":"[>]", "completed":"[x]"}
res = []
for task in self.items:
res.append(f"{mark_map[task['status']]} {task['content']}")
return "\n".join(res)
TODO_MGR = TodoManager()
# ===================== 5. S04 原有:子代理 run_subagent(完全保留) =====================
SUB_TOOL_HANDLER = {
"bash": run_bash,
"read_file": run_read_file,
"write_file": run_write_file,
"edit_file": run_edit_file,
"todo": lambda **kw: TODO_MGR.update(kw["items"])
}
SUB_TOOLS = [
{"type":"function","function":{"name":"bash","description":"执行终端命令","parameters":{"type":"object","properties":{"command":{"type":"string"}},"required":["command"]}}},
{"type":"function","function":{"name":"read_file","description":"读取文件","parameters":{"type":"object","properties":{"path":{"type":"string"},"limit":{"type":"integer"}},"required":["path"]}},
{"type":"function","function":{"name":"write_file","description":"写入文件","parameters":{"type":"object","properties":{"path":{"type":"string"},"content":{"type":"string"}},"required":["path","content"]}},
{"type":"function","function":{"name":"edit_file","description":"编辑文件","parameters":{"type":"object","properties":{"path":{"type":"string"},"old_text":{"type":"string"},"new_text":{"type":"string"}},"required":["path","old_text","new_text"]}},
{"type":"function","function":{"name":"todo","description":"更新子任务待办清单","parameters":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"content":{"type":"string"},"status":{"type":"string"}},"required":["content","status"]}}},"required":["items"]}}
]
def run_subagent(sub_prompt: str, max_turn=5) -> str:
sub_messages = [{"role":"user","content":sub_prompt}]
sub_sys = "你是文件调研子代理,完成任务后只输出精简调研总结"
turn = 0
while turn < max_turn:
resp = client.chat.completions.create(model=MODEL,messages=[{"role":"system","content":sub_sys}]+sub_messages,tools=SUB_TOOLS)
msg = resp.choices[0].message
sub_messages.append(msg.model_dump(exclude_none=True))
if not msg.tool_calls:
return f"【子代理调研总结】\n{msg.content}"
sub_res = []
for call in msg.tool_calls:
args = json.loads(call.function.arguments)
func = SUB_TOOL_HANDLER.get(call.function.name)
ret = func(**args) if func else f"未知工具{call.function.name}"
sub_res.append({"role":"tool","tool_call_id":call.id,"content":ret})
sub_messages.extend(sub_res)
turn +=1
return "【子代理超时总结】:子任务达到最大运行轮次,根据已有信息汇总"
# ===================== 6. 工具路由字典:新增 load_skill 工具(S05核心) =====================
TOOL_HANDLERS = {
"bash": run_bash,
"read_file": run_read_file,
"write_file": run_write_file,
"edit_file": run_edit_file,
"todo": lambda **kw: TODO_MGR.update(kw["items"]),
"task": lambda **kw: run_subagent(kw["prompt"]),
# S05 新增:加载技能工具
"load_skill": lambda **kw: SKILL_REGISTRY.load_skill_body(kw["name"])
}
# ===================== 7. 工具描述列表:新增 load_skill 说明 =====================
TOOLS = [
{"type":"function","function":{"name":"bash","description":"执行终端命令","parameters":{"type":"object","properties":{"command":{"type":"string"}},"required":["command"]}}},
{"type":"function","function":{"name":"read_file","description":"读取文件","parameters":{"type":"object","properties":{"path":{"type":"string"},"limit":{"type":"integer"}},"required":["path"]}},
{"type":"function","function":{"name":"write_file","description":"写入文件","parameters":{"type":"object","properties":{"path":{"type":"string"},"content":{"type":"string"}},"required":["path","content"]}},
{"type":"function","function":{"name":"edit_file","description":"编辑文件","parameters":{"type":"object","properties":{"path":{"type":"string"},"old_text":{"type":"string"},"new_text":{"type":"string"}},"required":["path","old_text","new_text"]}},
{"type":"function","function":{"name":"todo","description":"更新任务清单","parameters":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"content":{"type":"string"},"status":{"type":"string"}},"required":["content","status"]}}},"required":["items"]}},
{"type":"function","function":{"name":"task","description":"创建独立上下文子代理","parameters":{"type":"object","properties":{"prompt":{"type":"string"}},"required":["prompt"]}}},
# S05 新增:技能加载工具
{
"type": "function",
"function": {
"name": "load_skill",
"description": "加载指定技能的完整手册,传入技能名称,按需读取专项任务规范",
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "要加载的技能名称"}
},
"required": ["name"]
}
}
}
]
# ===================== 8. 系统提示词:嵌入【轻量化技能目录】(S05关键改动) =====================
# 把技能目录动态拼入提示词,只放简介,不放完整正文
PARENT_SYS = f"""
你是全能智能体,遵守以下规则:
1. 多步骤任务优先使用todo拆分计划;
2. 复杂调研任务调用task交给子代理处理;
3. 执行专项任务前,必须先调用load_skill加载对应技能手册;
4. 严格按照技能手册的步骤完成任务。
{SKILL_REGISTRY.get_skill_list()}
"""
# ===================== 9. 主循环(历史版本完全不变) =====================
def agent_loop(user_input):
state = {
"messages": [{"role": "user", "content": user_input}],
"turn_count": 1,
"planning_state": {"rounds_since_update": 0}
}
while True:
print(f"\n=====第{state['turn_count']}轮【父Agent】推理=====")
resp = client.chat.completions.create(
model=MODEL,
messages=[{"role":"system","content":PARENT_SYS}] + state["messages"],
tools=TOOLS
)
msg = resp.choices[0].message
state["messages"].append(msg.model_dump(exclude_none=True))
if not msg.tool_calls:
print("\n🎉最终回复:",msg.content)
return
tool_results = []
todo_used = False
for tool_call in msg.tool_calls:
func = tool_call.function
args = json.loads(func.arguments)
handler = TOOL_HANDLERS.get(func.name)
ret = handler(**args) if handler else f"未知工具:{func.name}"
if func.name == "todo": todo_used = True
tool_results.append({"role":"tool","tool_call_id":tool_call.id,"content":ret})
# 待办清单提醒逻辑
if todo_used:
state["planning_state"]["rounds_since_update"] = 0
else:
state["planning_state"]["rounds_since_update"] +=1
if state["planning_state"]["rounds_since_update"] >=3:
remind = "<reminder>连续3轮未更新待办清单,请调用todo刷新</reminder>"
tool_results.insert(0,{"role":"tool","tool_call_id":"remind_001","content":remind})
state["messages"].extend(tool_results)
state["turn_count"] +=1
# ===================== 10. 测试用例 =====================
if __name__ == "__main__":
# 测试1:触发代码审查技能加载
agent_loop("对当前项目做一次完整的代码审查")
# 测试2:触发Git工作流技能加载(注释上面一行,单独运行)
# agent_loop("告诉我标准的Git版本操作流程")
5. 运行步骤 & 效果说明
前置准备
- 替换代码中
DEEPSEEK_API_KEY为你的有效密钥; - 按照前文要求,创建
skills目录和两个SKILL.md文件; - 安装依赖:
pip install openai。
运行流程(以「代码审查」为例)
- 第一轮 :系统提示词展示轻量化技能目录,模型识别任务需要
code-review技能; - 调用工具 :执行
load_skill(name="code-review"),注册表读取完整技能手册; - 注入上下文:技能完整正文作为工具结果写入消息列表;
- 执行任务:模型依据加载的审查清单,完成代码审查并输出报告;
- 任务结束:完整技能内容不会常驻,下一轮无关任务不会重复加载。
核心效果验证
- 系统提示词只有技能名称和简介,无大段正文;
- 仅在对应任务触发时,才加载完整技能内容,完美实现「按需加载」。
四、拓展练习(巩固知识点)
- 新增自定义技能 :在
skills下新建文件夹和SKILL.md,补充「Python脚本编写规范」技能,测试模型自动识别并加载; - 优化技能解析 :拓展
SkillRegistry,支持更多格式的元数据; - 权限控制:限制部分技能仅子代理可加载,区分父子代理的技能权限。
五、本章一句话总结
技能系统的核心不是新增一个工具,而是把「各类专项知识」从常驻提示词中剥离,拆分为「轻量目录发现 + 完整内容按需加载」,精简上下文、节约 Token,让 Agent 架构更灵活易扩展。