AGENT 教程 S05 技能系统(Skill)

S05 技能系统(Skill)教程

整体说明

承接前文 S01~S04 已有能力:基础Agent循环、工具路由+安全沙箱、会话待办规划、子代理上下文隔离。

本章核心:把专项领域知识从固定提示词中拆分,实现「轻量发现、按需加载」,解决提示词臃肿、Token 浪费问题。全程在原有代码基础上增量开发,主循环、已有工具逻辑无需改动。


一、提出问题

1. 现有能力回顾

到 S04 为止,Agent 已具备完整基础能力:

  • 自主调用各类工具执行操作;
  • 通过 todo 做会话内多步骤任务规划;
  • 利用子代理隔离局部任务上下文,避免主消息爆炸。

2. 现存痛点

不同任务需要不同专项知识/操作规范 ,例如:代码审查、Git 版本操作、框架搭建等。

如果按照传统写法,把所有领域知识、操作手册全部硬编码写入固定 system prompt,会产生两个严重问题:

  1. Token 严重浪费:大部分场景用不到的知识常驻提示词,增加接口开销、拉长上下文;
  2. 提示词臃肿混乱:全局规则、工具说明、各类专业知识混杂在一起,模型难以区分核心指令,执行逻辑出错。

3. 本章核心目标

  1. 设计技能(Skill) 体系,将「全局固定规则」和「可选专项知识」解耦;
  2. 分为两层设计:先展示轻量化技能目录 (让模型知道有哪些技能可用),需要时再加载完整技能内容
  3. 技能内容仅在当前任务需要时注入上下文,任务结束不常驻,从根源精简提示词;
  4. 守住教学边界:仅实现基础的「发现+按需加载」,不拓展多源加载、条件激活、技能参数化等复杂功能。

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 技能注册表

全局统一管理器,核心能力:

  1. 自动扫描本地技能目录,加载所有技能;
  2. 对外输出技能目录(供系统提示词使用);
  3. 根据技能名,读取并返回完整技能正文。

4. 易混淆概念区分(初学者必看)

很多人会把 Skill / Memory / CLAUDE.md 弄混,用场景快速区分:

  1. Skill 技能某一类任务的操作手册,按需加载,临时使用(例:代码审查步骤、Git 操作规范);
  2. Memory 记忆 :跨会话需要长期记住的事实、用户偏好(例:用户习惯用 Python、项目目录路径);
  3. CLAUDE.md全局永久规则,整个系统的基础约束,永远常驻提示词(例:工具使用总规则、安全限制)。

5. 新旧方案对比

方案 实现方式 优点 缺点
传统方案 所有技能正文全量写入 system prompt 实现简单 提示词臃肿、Token 浪费、模型易被冗余信息干扰
技能系统方案 常驻轻量目录 + 按需加载完整正文 精简提示词、节约 Token、逻辑清晰 需额外实现技能管理与加载工具

6. 初学者高频踩坑点

  1. 把所有技能正文常驻提示词:违背技能系统设计初衷,回到臃肿老问题;
  2. 技能目录描述过于简单:只写技能名、不写用途,模型无法判断何时该加载对应技能;
  3. 将技能当成强制全局规则:技能是「可选手册」,不是必须全程遵守的硬性规则;
  4. 混淆 Skill / Memory:把临时任务手册当成长期记忆,架构混乱;
  5. 一步到位做复杂加载逻辑:初学先实现"目录展示+单技能加载",不要叠加多源加载、条件激活等复杂功能。

三、解决问题(落地实现 + 完整可运行代码)

1. 整体实现思路

基于 S04 子代理代码 增量开发,原有:安全沙箱、基础工具、Todo待办、子代理、主循环 全部保留,仅新增 4 部分内容:

  1. 本地 skills 目录,存放多个技能文件;
  2. SkillRegistry 技能注册表类,扫描、解析、加载技能;
  3. 新增 load_skill 工具,用于触发技能加载;
  4. 修改系统提示词,嵌入轻量化技能目录

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. 运行步骤 & 效果说明

前置准备

  1. 替换代码中 DEEPSEEK_API_KEY 为你的有效密钥;
  2. 按照前文要求,创建 skills 目录和两个 SKILL.md 文件;
  3. 安装依赖:pip install openai

运行流程(以「代码审查」为例)

  1. 第一轮 :系统提示词展示轻量化技能目录,模型识别任务需要 code-review 技能;
  2. 调用工具 :执行 load_skill(name="code-review"),注册表读取完整技能手册;
  3. 注入上下文:技能完整正文作为工具结果写入消息列表;
  4. 执行任务:模型依据加载的审查清单,完成代码审查并输出报告;
  5. 任务结束:完整技能内容不会常驻,下一轮无关任务不会重复加载。

核心效果验证

  • 系统提示词只有技能名称和简介,无大段正文;
  • 仅在对应任务触发时,才加载完整技能内容,完美实现「按需加载」。

四、拓展练习(巩固知识点)

  1. 新增自定义技能 :在 skills 下新建文件夹和 SKILL.md,补充「Python脚本编写规范」技能,测试模型自动识别并加载;
  2. 优化技能解析 :拓展 SkillRegistry,支持更多格式的元数据;
  3. 权限控制:限制部分技能仅子代理可加载,区分父子代理的技能权限。

五、本章一句话总结

技能系统的核心不是新增一个工具,而是把「各类专项知识」从常驻提示词中剥离,拆分为「轻量目录发现 + 完整内容按需加载」,精简上下文、节约 Token,让 Agent 架构更灵活易扩展。

相关推荐
Mr -老鬼1 小时前
EasyClick 脚本开发,选哪个 AI 工具最靠谱?
人工智能·自动化·ai编程·easyclick
basketball6161 小时前
AI Infra 硬件体系与编程模型:13. CUDA编程基础:多流并行
人工智能
QiLinkOS1 小时前
极客与商业思维的融合实践(1)
c语言·数据库·c++·人工智能·算法·开源协议
赫媒派1 小时前
不是靠Prompt:31万行重构的Agent评测实战
人工智能
水如烟1 小时前
孤能子视角:摩尔定律、韬定律 vs “摩尔制造“、“韬部署”?
人工智能
jerryinwuhan1 小时前
路径规划相关论文
人工智能
程序员cxuan1 小时前
Fable 5 的系统提示词被人扒出来了,精彩,太精彩了。
人工智能·后端·程序员
天风之翼1 小时前
AI 全栈开发实战(6):向量检索与 RAG 问答—— Qdrant 检索、Re-rank、流式输出
人工智能
断春风1 小时前
Gemini 2.5 Flash Lite 高效落地实战指南
人工智能·ai编程