在构建企业级 AI Agent 时,一个常见的难题是如何让 Agent 掌握大量专业知识,同时又不会在每次对话中都消耗昂贵的上下文。想象一下,如果你的公司有 50 个业务领域,每个领域有 2000 字的专业文档,把所有内容都塞进系统提示词显然不可行。

这就引出了"渐进式披露"(Progressive Disclosure)的设计模式——Agent 启动时只知道"有什么技能",当用户提出具体问题时,再按需加载对应的完整知识。这种方法由 Anthropic 普及,核心思想是将信息分为三个层次:元数据、核心内容、详细资源。

文件系统即技能仓库

我们的实现将每个技能设计为一个独立的文件夹,遵循 Anthropic Agent Skills 规范。以数据分析技能为例,它的目录结构是这样的:

skills/data-analysis/
├── skill.json              # 元数据:名称、版本、描述
├── instructions.md         # 核心指令:告诉 Agent 如何使用这个技能
├── resources/              # 资源文件:示例数据、模板文档
│   └── examples/
└── scripts/                # 可执行脚本:Python/Shell/JS
    ├── calculate_stats.py
    └── generate_report.py

在这里插入图片描述

skill.json 存放元数据,用于 Agent 快速判断这个技能是做什么的:

{
  "name": "data-analysis",
  "version": "1.0.0",
  "description": "数据分析专家技能,擅长处理 CSV、JSON 数据,生成统计报告和可视化",
  "tags": ["data", "analysis", "visualization"]
}

instructions.md 包含完整的操作指南,比如如何处理数据、有哪些最佳实践。resources/ 目录可以放任何参考材料——示例数据、模板文档、FAQ 等。而 scripts/ 则是这个技能的独特能力:Agent 可以调用这些脚本执行实际计算。

AgentSkill 和 SkillsLoader实现技能加载

整个技能系统的核心是 AgentSkill 类,它封装了单个技能的完整结构:

class AgentSkill:
    def __init__(self, skill_path: Path):
        self.path = skill_path
        self.metadata = self._load_metadata()
        self.instructions = self._load_instructions()
        self.resources = self._load_resources()
        self.scripts = self._load_scripts()

初始化时会自动加载四个部分:元数据、指令、资源和脚本。元数据加载很简单,就是读取 JSON 文件:

def _load_metadata(self) -> dict[str, Any]:
    metadata_file = self.path / "skill.json"
    if metadata_file.exists():
        with open(metadata_file, "r", encoding="utf-8") as f:
            return json.load(f)
    return {}

指令加载支持多种文件名,按优先级查找:

def _load_instructions(self) -> str:
    for filename in ["instructions.md", "README.md", "prompt.md"]:
        instruction_file = self.path / filename
        if instruction_file.exists():
            with open(instruction_file, "r", encoding="utf-8") as f:
                return f.read()
    return ""

资源加载会递归遍历 resources/ 目录,将所有文件内容读入内存:

def _load_resources(self) -> dict[str, str]:
    resources = {}
    resources_dir = self.path / "resources"

    if resources_dir.exists():
        for file in resources_dir.rglob("*"):
            if file.is_file():
                rel_path = file.relative_to(resources_dir)
                try:
                    with open(file, "r", encoding="utf-8") as f:
                        resources[str(rel_path)] = f.read()
                except UnicodeDecodeError:
                    pass
    return resources

脚本加载则只识别 Python、Shell、JavaScript 三种类型:

def _load_scripts(self) -> dict[str, Path]:
    scripts = {}
    scripts_dir = self.path / "scripts"

    if scripts_dir.exists():
        for file in scripts_dir.glob("*"):
            if file.is_file() and file.suffix in [".py", ".sh", ".js"]:
                scripts[file.stem] = file
    return scripts

SkillsLoader 类负责管理所有技能的加载和查询:

class SkillsLoader:
    def __init__(self, skills_dir: str = "skills"):
        self.skills_dir = Path(skills_dir)
        self.skills: dict[str, AgentSkill] = {}

    def load_all(self) -> list[AgentSkill]:
        if not self.skills_dir.exists():
            return []

        for skill_path in self.skills_dir.iterdir():
            if skill_path.is_dir() and not skill_path.name.startswith("."):
                skill = AgentSkill(skill_path)
                self.skills[skill.name] = skill
        return list(self.skills.values())

轻量级摘要减少上下文负担

关键的设计点在于:Agent 启动时只获取技能的轻量级摘要,而不是全部内容。to_metadata_summary() 方法生成一个简洁的描述:

def to_metadata_summary(self) -> str:
    summary = f"- {self.name}: {self.description}"
    if self.scripts:
        script_names = ", ".join(self.scripts.keys())
        summary += f"\n  可执行脚本: {script_names}"
    return summary

生成的摘要大概是这样:

- data-analysis: 数据分析专家技能,擅长处理 CSV、JSON 数据,生成统计报告和可视化
  可执行脚本: calculate_stats, generate_report
- baking-master: 烘焙专家技能,提供面包、蛋糕、点心的制作技巧和配方指导
- internet-jargon: 互联网黑话翻译专家,精通各种职场黑话、互联网术语的翻译和生成

这个摘要只占用大约 50 个 tokens,让 Agent 知道"有什么技能可用"。而 to_full_prompt() 方法则生成完整内容,包括指令、资源和脚本说明:

def to_full_prompt(self) -> str:
    prompt_parts = [
        f"# Skill: {self.name}",
        f"Version: {self.version}",
        "",
        self.description,
        "",
        "## Instructions",
        self.instructions,
    ]

    if self.resources:
        prompt_parts.extend(["", "## Available Resources"])
        for name, content in self.resources.items():
            prompt_parts.append(f"### {name}")
            prompt_parts.append(f"```\n{content}\n```")

    if self.scripts:
        prompt_parts.extend(["", "## Available Scripts"])
        for script_name in self.scripts.keys():
            prompt_parts.append(f"- {script_name}: 调用 execute_skill_script('{self.name}', '{script_name}', args...)")

    return "\n".join(prompt_parts)

完整内容可能有 500 个 tokens,但只在需要时才加载。

使用工具连接 Agent 和 Skills

Agent 通过两个 LangChain 工具与技能系统交互:

@tool
def load_skill_details(skill_name: str) -> str:
    """按需加载指定技能的完整内容"""
    loader = get_skills_loader()
    skill = loader.get_skill(skill_name)
    if skill:
        return skill.to_full_prompt()
    return f"Skill '{skill_name}' not found."

@tool
def execute_skill_script(skill_name: str, script_name: str, *args: str) -> str:
    """执行技能中的脚本"""
    loader = get_skills_loader()
    skill = loader.get_skill(skill_name)
    if not skill:
        return f"Skill '{skill_name}' not found."
    return skill.execute_script(script_name, *args)

第一个工具用于加载技能的完整指令和资源,第二个工具用于执行脚本。这两个工具被注册到 Agent 的工具列表中。

Agent 创建时的系统提示注入

在创建 Agent 时,我们将技能摘要注入到系统提示中:

async def create_agent_with_mcp(...):
    skills_loader = get_skills_loader()
    loaded_skills_info = skills_loader.get_loaded_skills_info()

    tools = list(tools) + [load_skill_details, execute_skill_script]

    if skills_loader.skills:
        skills_summary = skills_loader.get_metadata_summary()
        system_prompt = f"""{base_prompt}

## 你的专业技能

你拥有以下专业技能,需要时可以调用 load_skill_details 工具获取完整内容:

{skills_summary}

当用户的问题与某个技能相关时,先调用 load_skill_details 加载该技能的详细指令,然后按照指令执行任务。"""
    else:
        system_prompt = base_prompt

    agent = create_agent(llm, tools, system_prompt=system_prompt)
    return agent, loaded_skills_info

这样 Agent 在启动时就知道"我有什么技能",但不知道"技能的具体内容"。

实际运行流程

当用户提出问题时,整个流程是这样的:

用户问:“帮我计算 10, 20, 30 的统计信息”

Agent 首先看到系统提示中的技能摘要,判断这个问题属于数据分析领域,于是调用 load_skill_details("data-analysis")

工具返回完整内容,包括 instructions.md 中的操作指南和 scripts/ 中的可用脚本列表。Agent 发现有一个 calculate_stats 脚本可以处理这个请求,于是调用 execute_skill_script("data-analysis", "calculate_stats", "10", "20", "30")

脚本执行逻辑在 AgentSkill 类中实现:

def execute_script(self, script_name: str, *args) -> str:
    if script_name not in self.scripts:
        return f"错误: 脚本 '{script_name}' 不存在"

    script_path = self.scripts[script_name]

    try:
        if script_path.suffix == ".py":
            cmd = ["python3", str(script_path)] + list(args)
        elif script_path.suffix == ".sh":
            cmd = ["bash", str(script_path)] + list(args)
        elif script_path.suffix == ".js":
            cmd = ["node", str(script_path)] + list(args)
        else:
            return f"错误: 不支持的脚本类型 {script_path.suffix}"

        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=30
        )

        if result.returncode == 0:
            return result.stdout
        else:
            return f"错误: {result.stderr}"

    except subprocess.TimeoutExpired:
        return "错误: 脚本执行超时(30秒)"
    except Exception as e:
        return f"错误: {str(e)}"

脚本 calculate_stats.py 的实际内容是这样的:

#!/usr/bin/env python3
import sys
import json

def calculate_stats(numbers):
    if not numbers:
        return {"error": "没有数据"}

    numbers = [float(n) for n in numbers]

    return {
        "count": len(numbers),
        "sum": sum(numbers),
        "mean": sum(numbers) / len(numbers),
        "min": min(numbers),
        "max": max(numbers),
    }

if __name__ == "__main__":
    numbers = sys.argv[1:]
    result = calculate_stats(numbers)
    print(json.dumps(result, ensure_ascii=False, indent=2))

执行后返回 JSON 格式的统计结果:

{
  "count": 3,
  "sum": 60.0,
  "mean": 20.0,
  "min": 10.0,
  "max": 30.0
}

Agent 将这个结果整理后返回给用户,整个对话只加载了需要的技能,没有浪费上下文。

基于文件系统的设计带来了几个好处:一是团队成员可以独立开发维护各自领域的技能,只需在 skills/ 目录下创建新文件夹;二是技能更新后重启 Agent 即可生效,不需要修改代码;三是可以轻松添加几十上百个技能而不会影响性能,因为启动时只加载元数据。

这种架构特别适合大型企业的知识管理场景。比如一家电商公司,可以有"订单处理"“库存管理”“客户服务”"物流跟踪"等几十个技能,每个部门负责维护自己的技能内容,而中心 Agent 通过渐进式加载按需调用,既保证了专业性,又控制了成本。

参考资料

  • https://www.bilibili.com/video/BV1yRPWzqEhL/
  • https://docs.langchain.com/oss/python/langchain/multi-agent/skills-sql-assistant#optional-track-loaded-skills-and-enforce-tool-constraints
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐