Agent Skill 渐进式加载 —— 按需激活工具,告别 Prompt 膨胀

前置阅读:一个最简化的 AI Agent 核心实现 核心模块:SkillManager.py(115 行)+ 主循环集成代码


一、问题:工具多了怎么办?

基础 Agent 只有 search_webcalculate 两个工具,prompt 很小。但真实场景中,Agent 可能拥有几十上百个工具------文件读写、数据库查询、代码执行、邮件发送、日历管理......

直接把所有工具塞进 system prompt 会有什么后果?

问题 影响
Token 浪费 每次 LLM 调用都附带全部工具定义,无关工具白白消耗 token
注意力稀释 tool_choice 需要在大量工具中选,准确率下降
幻觉工具调用 LLM 可能调用不该用的工具
维护困难 改一个工具要改全局注册表

核心矛盾:Agent 需要很多能力,但每次对话只需要其中一小部分。


二、设计思路:工具 → Skill → 按需激活

scss 复制代码
传统方案                      渐进式方案
────────                    ──────────
全部工具常驻 prompt           只发 load_skill / unload_skill
  │                              │
  ├─ search_web                  ├─ load_skill    ← 元工具(始终可用)
  ├─ calculate                   ├─ unload_skill  ← 元工具(始终可用)
  ├─ read_file                   │
  ├─ write_file                  └─ 按需激活 ──→  ┌ search
  ├─ query_db                                       ├ calculator
  ├─ send_email                                     ├ file-ops
  ├─ ... (N 个工具)                                 └ ...
  └─ 全部塞进 LLM 请求               每次只发当前激活的工具

核心思想:把"选工具"这件事也交给 Agent 自己 。LLM 通过 load_skillunload_skill 两个元工具,自主决定什么时候加载 / 卸载哪些技能模块。


三、数据结构:Skill 是什么

python 复制代码
@dataclass
class Skill:
    """一个 Skill = 一组工具 + 一段领域知识"""
    name: str                         # 唯一标识,如 "calculator"
    description: str                  # 给 LLM 看的简介,"数学计算能力"
    tools: list[dict]                 # OpenAI tool 定义列表
    tool_map: dict[str, Callable]    # 工具名 → 实际函数
    system_prompt: str = ""           # 激活时追加到 system prompt

一个 Skill 打包了三个东西:

  1. 工具定义tools)------ LLM 需要知道"有哪些工具、什么参数"
  2. 工具实现tool_map)------ Host 端执行的函数
  3. 领域知识system_prompt)------ 这个技能该怎么用、有什么注意事项

示例:注册三个 Skill

python 复制代码
skills = SkillManager()

skills.register(Skill(
    name="search",
    description="互联网搜索能力",
    tools=[{"type": "function", "function": {"name": "search_web", ...}}],
    tool_map={"search_web": search_web},
    system_prompt="你拥有搜索能力。遇到不确定的事实性问题,请先搜索再回答。",
))

skills.register(Skill(
    name="calculator",
    description="数学计算能力",
    tools=[{"type": "function", "function": {"name": "calculate", ...}}],
    tool_map={"calculate": calculate},
    system_prompt="你拥有计算能力。遇到数学计算请调用 calculate 工具,不要心算。",
))

skills.register(Skill(
    name="file-ops",
    description="文件读写能力",
    tools=[
        {"type": "function", "function": {"name": "read_file", ...}},
        {"type": "function", "function": {"name": "write_file", ...}},
    ],
    tool_map={"read_file": read_file, "write_file": write_file},
    system_prompt="你拥有文件读写能力。操作文件前请确认路径正确。",
))

注册后,LLM 初始状态下看不到 search / calculator / file-ops 的具体工具,只能看到:

makefile 复制代码
可用工具:
  - load_skill  → 描述里包含 "可用技能: search, calculator, file-ops"
  - unload_skill

四、元工具:让 Agent 自己管理工具

4.1 元工具定义

python 复制代码
META_TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "load_skill",
            "description": "加载一个技能模块。可用技能: {available_skills}",
            "parameters": {
                "type": "object",
                "properties": {
                    "name": {"type": "string", "description": "要加载的技能名称"}
                },
                "required": ["name"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "unload_skill",
            "description": "卸载一个技能模块。当前已加载: {loaded_skills}",
            "parameters": {
                "type": "object",
                "properties": {
                    "name": {"type": "string", "description": "要卸载的技能名称"}
                },
                "required": ["name"],
            },
        },
    },
]

关键细节:{available_skills}{loaded_skills}每次查询时动态填充的占位符,让 LLM 始终知道可选什么、已经加载了什么。

4.2 动态填充

python 复制代码
def get_active_tools(self) -> list[dict]:
    """只返回当前激活 skill 的工具 + 元工具"""
    tools = []
    available = ", ".join(self._skills.keys())
    loaded = ", ".join(self._active) if self._active else "无"
    for mt in self.META_TOOLS:
        tool_def = json.loads(json.dumps(mt))
        tool_def["function"]["description"] = \
            tool_def["function"]["description"].format(
                available_skills=available, loaded_skills=loaded
            )
        tools.append(tool_def)
    for name in self._active:
        tools.extend(self._skills[name].tools)
    return tools

五、主循环集成:每次调用前动态组装

在上一篇的基础 Agent 循环中,tools 是固定的。引入 Skill 后,每次 LLM 调用前都要动态组装:

python 复制代码
for step in range(max_steps):
    # ← 每轮动态获取当前激活的工具
    active_tools = skills.get_active_tools()
    active_tool_map = skills.get_active_tool_map()

    # ← 动态拼装 system prompt(base + 激活 skill 的领域知识)
    system_prompt = build_system_prompt(base_system_prompt, skills)
    messages[0]["content"] = system_prompt

    response = client.chat.completions.create(
        model="your-model-name",
        messages=messages,
        tools=active_tools,      # ← 不是全部 TOOLS,而是按需的
        tool_choice="auto",
    )

完整交互流程

ini 复制代码
用户: "北京天气怎么样?顺便算 156*23"

Round 1:
  tools = [load_skill, unload_skill]
  LLM → load_skill("search")    # 先加载搜索能力

Round 2:
  tools = [load_skill, unload_skill, search_web]
  LLM → search_web("北京天气")  # 搜索

Round 3:
  tools = [load_skill, unload_skill, search_web]
  LLM → load_skill("calculator") # 需要计算,再加载

Round 4:
  tools = [load_skill, unload_skill, search_web, calculate]
  LLM → calculate("156*23")      # 计算

Round 5:
  LLM → 最终回答                  # 整合结果输出

会话恢复时自动恢复 Skill 状态

python 复制代码
if session_id:
    state = pm.load_session(session_id)
    for name in state.get("active_skills", []):
        skills.load(name)              # ← 恢复之前激活的 Skill

六、对比:无 Skill vs 有 Skill

维度 无 Skill(上一篇) 有 Skill(本篇)
工具数量 2 个,硬编码 任意多个,注册即用
Prompt 大小 随工具数量线性增长 只包含当前激活的工具
工具选择 LLM 在全部工具中选 先通过 load_skill 缩小范围
领域知识 写死在全局 system prompt 每个 Skill 自带,按需激活
可扩展性 加工具要改主循环代码 skills.register(Skill(...))
会话恢复 恢复 messages 恢复 messages + active_skills
工具发现 LLM 只能看到定义 LLM 通过 description 自主"浏览"

七、设计精要

7.1 三个不变原则

  1. 主循环不变 ------ 仍是「思考 → 行动 → 观察」的 ReAct 循环,只是 tools 和 prompt 每轮动态生成
  2. 工具执行不变 ------ TOOL_MAP[name](**args) 这行代码没有变,只是 map 的来源从全局变成了动态
  3. LLM 调用不变 ------ client.chat.completions.create 的参数结构完全一致

7.2 "元工具"是高杠杆设计

两个 meta-tool(load_skill / unload_skill)让 Agent 获得了一种自省能力------它能意识到"我现在需要什么能力",然后主动获取。这个模式可以无限扩展:

  • list_skills → 浏览所有可选技能
  • search_skills → 按关键词搜技能
  • skill_status → 查看当前技能树

7.3 为什么不用 MCP 动态发现

MCP(Model Context Protocol)的动态 tool discovery 需要在每次调用时拉取远端工具列表,而本方案是本地注册 + 按需激活

本方案 MCP dynamic
延迟 0(本地 dict 查询) 网络往返
上下文控制 精确到单个 tool 级别 tools/list 返回全部
领域知识 Skill.system_prompt 随激活注入 需另行处理

7.4 渐进式 Skill 的正确心态

渐进式不等于"先写个简单的,以后慢慢加"。它指的是:

  1. 用户侧:agent 的能力随对话进展逐步解锁,不是开场就 show hand
  2. 开发者侧:新能力以 Skill 为单位注册,不碰主循环代码
  3. LLM 侧:每次只看到当前需要的工具子集,注意力不被稀释

八、完整架构图

flowchart TB subgraph SR[Skill 注册层] S1[Skill: search] S2[Skill: calculator] S3[Skill: file-ops] end subgraph SM[SkillManager] SK[_skills: dict] AC[_active: set] end subgraph AL[Agent 循环层] GT[get_active_tools] GM[get_active_tool_map] GP[get_active_prompt] LOOP[LLM 调用] end subgraph PL[持久化层] ST[active_skills] end S1 -- register --> SK S2 -- register --> SK S3 -- register --> SK SK --> AC AC --> GT AC --> GM AC --> GP GT --> LOOP GM --> LOOP GP --> LOOP LOOP -- save_session --> ST ST -- 恢复时 load --> AC

全部代码增量:SkillManager 115 行 + MiniAgent 集成 ~20 行 + build_system_prompt 7 行 = < 150 行净增


推荐学习资源

  • 从零开始打造一个AI Agent CLI --- 手把手带你构建一个完整的 AI Agent 命令行工具,从工具调用、对话管理到发布上线的全流程实战。
  • AI Agents 开发实践 --- 涵盖 Agent 架构设计、记忆系统、多 Agent 协作等核心主题的实战教程,适合想深入 Agent 开发的工程师。
  • AI 全栈编程生存指南 --- AI 时代的全栈开发者生存法则,从 AI 辅助编程到底层原理,帮你建立不可替代的技术壁垒。