为agent实现渐进式Skills能力的思考和实践
本文介绍了企业级AI Agent中"渐进式披露"技能知识的设计模式。通过将技能信息分层存储(元数据、核心内容、详细资源),系统启动时仅加载轻量级摘要(约50 tokens),在用户具体提问时才按需加载完整技能内容。每个技能采用标准化目录结构存储,包含元数据文件、操作指南、资源文件和可执行脚本。文章详细说明了AgentSkill类的实现方法,包括元数据加载、指令加载、资源加载和脚
在构建企业级 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
更多推荐



所有评论(0)