人工智能(AI)Agent(智能体)经常需要调用外部能力:查邮件、读文件、生成报告、执行脚本、调用接口。问题在于,很多托管型 AI 助手把这些能力封装在聊天界面里,开发者只能“使用结果”,很难控制技能如何被发现、如何匹配、何时加载上下文、脚本又是怎样被执行的。
OpenSkills 解决的就是这个问题:把 AI Agent 的技能系统做成一个可读、可改、可集成的开源框架。一个技能可以用一个 SKILL.md 文件描述,参考资料放在 references/ 目录,脚本放在 scripts/ 目录,然后由 Agent 按需加载和调用。
仓库:https://github.com/twwch/OpenSkills
协议:Apache 2.0
技术栈:Python 3.10+
OpenSkills 解决什么问题
假设一名工程师一年里有 50 多封周报邮件,年底需要整理年度复盘。人工处理通常要经历这些步骤:
- 打开邮箱并搜索所有周报;
- 逐封复制内容;
- 提取项目成果、技术成长、团队贡献;
- 套用年度复盘模板;
- 调整格式并保存成文档。
这类任务很适合交给 AI Agent,但普通聊天式工具有几个限制:
| 问题 | 影响 |
|---|---|
| 技能逻辑不可见 | 不知道模型为什么调用某个能力,也不知道上下文加载了什么 |
| 难以复用 | 只能在特定聊天产品里用,不能直接嵌入自己的 Python 项目 |
| 难以定制 | 公司邮箱、内部模板、私有脚本很难接入 |
| 难以优化 | 邮件搜索慢、脚本超时、权限隔离等问题不好处理 |
| 安全边界不清晰 | Agent 自动执行脚本时,必须知道脚本在哪跑、能访问什么 |
OpenSkills 的思路是把“技能”变成工程资产。开发者可以定义技能元数据、系统指令、参考资料、可执行脚本和依赖声明,然后让 Agent 在明确规则下完成匹配和调用。
三层渐进式加载:只在需要时加载更多信息
OpenSkills 的核心设计是三层渐进式信息披露。它不会一开始就把所有技能、所有指令、所有参考文档全部塞进大语言模型(LLM,Large Language Model)的上下文,而是按阶段加载。
flowchart TD
A[用户请求] --> B[Layer 1: Metadata]
B --> C{匹配到相关技能?}
C -- 否 --> D[普通对话或返回无法匹配]
C -- 是 --> E[Layer 2: Instruction]
E --> F{是否需要更多资源?}
F -- 需要模板/资料 --> G[加载 References]
F -- 需要执行能力 --> H[执行 Scripts]
G --> I[LLM 生成结果]
H --> I
I --> J[返回用户]
三层分别承担不同职责:
| 层级 | 加载时机 | 内容 | 作用 |
|---|---|---|---|
| Layer 1:Metadata | 始终加载 | 技能名称、描述、版本、触发词、标签 | 快速发现和匹配技能 |
| Layer 2:Instruction | 匹配到技能后加载 | SKILL.md 正文指令 | 告诉 Agent 这个技能如何执行 |
| Layer 3:Resources | 条件加载 | 参考资料、模板、脚本 | 提供外部上下文和真实执行能力 |
这种设计适合技能数量较多的场景。比如有 100 个技能,每个技能又带有多个参考文档,如果全量加载,会浪费内存、拉长启动时间,还会占用 LLM 上下文窗口。渐进式加载只先读取元数据,等匹配到候选技能后再加载对应指令,参考资料则根据规则或模型判断继续加载。
一个技能就是一个 SKILL.md
OpenSkills 的技能定义以 Markdown 为核心。文件开头是 YAML 配置,后面是给 Agent 的执行指令。
以“从周报生成年度复盘”为例,目录可以这样组织:
weekly-report-to-annual/
├── SKILL.md
├── scripts/
│ ├── fetch_emails.py
│ └── save_report.py
├── references/
│ └── annual-review-template.md
└── demo.py
SKILL.md 可以这样写:
---
name: weekly-report-to-annual
description: 从周报邮件生成年度复盘报告
version: 1.0.0
triggers:
- 年度总结
- 周报汇总
- annual review
scripts:
- name: fetch_emails
path: scripts/fetch_emails.py
description: 从 IMAP 服务器获取邮件
- name: save_report
path: scripts/save_report.py
description: 保存报告到本地
references:
- path: references/annual-review-template.md
mode: always
description: 年度复盘报告模板
---
# 周报年度复盘技能
你是一个年度复盘助手,需要根据周报内容生成结构化报告。
## 执行步骤
1. 如果用户提供了邮箱配置,调用 [INVOKE:fetch_emails] 获取周报。
2. 分析周报内容,提取以下信息:
- 重要项目和成果
- 技术成长和学习
- 遇到的挑战和解决方案
- 团队协作和贡献
3. 按照 references 中的年度复盘模板生成报告。
4. 调用 [INVOKE:save_report] 保存到本地。
几个字段很关键:
| 字段 | 作用 |
|---|---|
name | 技能唯一名称 |
description | 技能用途说明,用于匹配 |
triggers | 触发词,比如“年度总结”“周报汇总” |
scripts | 可执行脚本列表 |
references | 参考资料、模板、规范 |
[INVOKE:script_name] | 在指令中声明脚本调用点 |
[INVOKE:fetch_emails] 不是普通文本,它表示 Agent 在执行技能时可以调用名为 fetch_emails 的脚本。脚本的输入输出通常用 JSON(JavaScript Object Notation,轻量数据交换格式)承载,这样 Agent 和脚本之间的边界比较清楚。
周报生成年度复盘的完整流程
用户只需要表达目标,Agent 负责匹配技能、取邮件、分析内容、套模板、保存报告。
sequenceDiagram
participant U as 用户
participant A as OpenSkills Agent
participant S as weekly-report-to-annual 技能
participant E as fetch_emails.py
participant L as LLM
participant W as save_report.py
U->>A: 生成张三的 2025 年度复盘报告
A->>A: 根据 Metadata 匹配技能
A->>S: 加载 SKILL.md 指令
A->>S: 加载年度复盘模板
A->>E: 调用脚本读取周报邮件
E-->>A: 返回 50 封周报内容
A->>L: 交给 LLM 提取成果和经验
L-->>A: 返回结构化报告
A->>W: 保存 Markdown 文件
W-->>A: 返回文件路径
A-->>U: 返回报告内容和保存结果
使用高级 API(应用程序编程接口)时,代码可以很短:
import asyncio
from openskills import create_agent
async def main():
agent = await create_agent(
skill_paths=["./examples/weekly-report-to-annual"],
api_key="your-api-key",
model="gpt-4",
)
response = await agent.chat("""
帮我生成张三的 2025 年度复盘报告。
从飞书邮箱获取所有标题包含“周报|张三”的邮件。
""")
print(response.content)
asyncio.run(main())
最终输出可以是这样的 Markdown 报告片段:
# 张三-2025 年度复盘报告
## 1. 目标复盘
| 年度目标 | 达成情况 | 后续行动 |
|---|---|---|
| API 网关性能优化 | QPS 从 2000 提高到 8000 | 继续压测并优化连接池 |
| 订单系统微服务化 | 拆分出用户、订单、支付、库存 4 个服务 | 扩展到更多业务模块 |
| 云原生能力建设 | Kubernetes 落地到生产环境 | 深入服务网格和可观测性 |
## 2. 工作亮点
- API 网关重构:通过连接池优化和对象复用,把 QPS(每秒查询数)从 2000 提高到 8000。
- 订单系统微服务化:部署时间从 30 分钟缩短到 5 分钟。
- 支付系统重构:支付成功率从 95% 提高到 99.2%。
## 3. 经验与教训
1. 性能优化要先定位瓶颈,再做针对性修改。
2. 微服务拆分应按业务边界划分,而不是按技术层次拆分。
3. 第三方支付接口需要提前设计超时、重试和降级机制。
这个例子体现了 OpenSkills 的价值:LLM 负责理解、抽取、归纳和写作,脚本负责确定性的数据获取和文件保存,模板负责约束输出结构。
脚本如何和 Agent 通信
技能脚本本质上是普通 Python 程序。它从标准输入读取 JSON 参数,从标准输出返回 JSON 结果。
下面是一个简化版邮件获取脚本,使用 IMAP(Internet Message Access Protocol,邮件访问协议)连接飞书邮箱。真实工程里还需要补充邮件正文解析、重试、日志脱敏和异常处理。
#!/usr/bin/env python3
"""从飞书邮箱获取周报邮件"""
import imaplib
import json
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
IMAP_HOST = "imap.feishu.cn"
IMAP_PORT = 993
def list_folders(email: str, password: str) -> list[str]:
mail = imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT)
mail.login(email, password)
status, data = mail.list()
mail.logout()
if status != "OK":
return []
folders = []
for item in data:
line = item.decode("utf-8", errors="ignore")
folder = line.split(' "/" ')[-1].strip('"')
folders.append(folder)
return folders
def search_folder_with_own_connection(
folder: str,
email: str,
password: str,
keyword: str,
year: int,
) -> tuple[str, list[dict]]:
mail = imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT)
mail.login(email, password)
try:
mail.select(folder, readonly=True)
# 示例搜索条件,真实场景可按邮件服务器能力调整
status, message_ids = mail.search(None, "ALL")
if status != "OK":
return folder, []
results = []
for msg_id in message_ids[0].split():
status, msg_data = mail.fetch(msg_id, "(RFC822)")
if status != "OK":
continue
# 这里省略邮件解析逻辑
# 可解析 Subject、Date、From、Body 等字段
parsed = parse_email(msg_data)
if keyword in parsed["subject"] and str(year) in parsed["date"]:
results.append(parsed)
return folder, results
finally:
mail.logout()
def parse_email(msg_data) -> dict:
"""示例占位函数:真实实现需要解析 MIME 邮件内容"""
return {
"subject": "",
"date": "",
"body": "",
}
def fetch_emails_from_feishu(
email: str,
password: str,
keyword: str,
year: int,
) -> list[dict]:
folders = list_folders(email, password)
all_emails = []
with ThreadPoolExecutor(max_workers=5) as executor:
futures = {
executor.submit(
search_folder_with_own_connection,
folder,
email,
password,
keyword,
year,
): folder
for folder in folders
}
for future in as_completed(futures):
folder, emails = future.result()
all_emails.extend(emails)
return all_emails
if __name__ == "__main__":
input_data = json.loads(sys.stdin.read())
emails = fetch_emails_from_feishu(
email=input_data["email"],
password=input_data["password"],
keyword=input_data.get("keyword", "周报|张三"),
year=input_data.get("year", 2025),
)
print(json.dumps({
"total": len(emails),
"emails": emails,
}, ensure_ascii=False))
这里有一个工程细节:不要多个线程共享同一个 imaplib.IMAP4_SSL 连接。IMAP 连接通常不是线程安全对象,多个文件夹并发搜索时,每个线程单独创建连接更稳妥。
邮件搜索从串行改为并发后,18 个文件夹可以按 5 个线程分批处理。这样做主要解决两个问题:
| 串行搜索的问题 | 并发搜索的处理方式 |
|---|---|
| 单连接操作时间过长 | 多连接分摊搜索任务 |
| 搜索到一半可能被服务器断开 | 每个文件夹独立连接,失败范围更小 |
| 总耗时随文件夹数量线性增长 | 文件夹较多时总耗时更短 |
这种优化只有在技能逻辑可控时才容易完成。如果邮件获取能力被封装在黑盒里,开发者很难调整连接策略、并发数和错误重试方式。
OpenSkills 的核心模块
OpenSkills 的内部结构大致可以分为技能管理、模型定义、LLM 集成、沙箱执行和高级 Agent API。
openskills/
├── core/
│ ├── manager.py # SkillManager:技能管理
│ ├── matcher.py # 智能匹配
│ ├── parser.py # SKILL.md 解析
│ └── executor.py # 脚本执行
│
├── models/
│ ├── metadata.py # Layer 1:元数据
│ ├── instruction.py # Layer 2:指令
│ └── resource.py # Layer 3:资源
│
├── llm/
│ ├── openai_compat.py # OpenAI 兼容接口
│ └── prompt_builder.py # 提示词构建
│
├── sandbox/
│ ├── client.py # 沙箱 HTTP 客户端
│ └── executor.py # 沙箱执行器
│
└── agent.py # SkillAgent 高级 API
整体调用关系可以抽象成这样:
flowchart LR
U[用户输入] --> A[SkillAgent]
A --> M[SkillManager]
M --> P[Parser 解析 SKILL.md]
M --> R[Matcher 匹配技能]
A --> L[LLM Client]
A --> X[Script Executor]
X --> B[Sandbox]
B --> X
X --> A
L --> A
A --> U
SkillManager 适合做底层控制,SkillAgent 适合快速接入业务。
两种使用方式:低级 API 和高级 API
如果需要完全控制技能发现、匹配、加载和脚本执行,可以使用低级 API。
import asyncio
from pathlib import Path
from openskills import SkillManager
async def main():
manager = SkillManager([Path("./skills")])
# 发现技能,只加载 Metadata
await manager.discover()
# 根据用户输入匹配技能
skills = manager.match("生成年度报告")
skill = skills[0]
# 手动加载 Instruction
await manager.load_instruction(skill.name)
# 手动加载 References
await manager.load_references(skill.name)
# 手动执行脚本
result = await manager.execute_script(
skill.name,
"fetch_emails",
{
"email": "user@example.com",
"password": "use-secret-manager",
"keyword": "周报|张三",
"year": 2025,
},
)
print(result)
asyncio.run(main())
如果想让 Agent 自动选技能、自动加载资源、自动执行脚本,可以使用高级 API。
import asyncio
from openskills import create_agent
async def main():
agent = await create_agent(
skill_paths=["./skills"],
api_key="your-key",
auto_select_skill=True,
auto_execute_scripts=True,
use_sandbox=True,
)
response = await agent.chat("帮我生成年度报告")
print(response.content)
print(f"使用的技能: {response.skill_used}")
asyncio.run(main())
两种方式的取舍:
| 使用方式 | 适合场景 | 代价 |
|---|---|---|
| 低级 API | 需要审计每一步、手动控制资源加载、接入复杂业务流 | 代码量更多 |
| 高级 API | 快速做 Agent 应用、技能较规范、执行流程相对固定 | 需要配置好权限和自动执行策略 |
沙箱执行:不要让 Agent 直接跑在宿主机上
脚本执行是 AI Agent 系统里风险最高的部分。OpenSkills 支持通过 AIO Sandbox 做隔离执行,避免脚本直接运行在宿主进程里。
启动沙箱容器:
docker run --rm -p 8080:8080 ghcr.io/agent-infra/sandbox:latest
创建 Agent 时启用沙箱:
agent = await create_agent(
skill_paths=["./skills"],
api_key="your-key",
use_sandbox=True,
sandbox_base_url="http://localhost:8080",
auto_execute_scripts=True,
)
沙箱常见能力包括:
| 能力 | 说明 |
|---|---|
| 容器隔离 | 脚本不直接在宿主进程执行 |
| 依赖安装 | 可安装 Python、npm 或系统依赖 |
| 文件同步 | 支持上传输入文件和下载结果文件 |
| 超时控制 | 避免脚本无限运行 |
| 资源限制 | 控制 CPU、内存等资源使用 |
技能依赖可以在 SKILL.md 中声明:
dependency:
python:
- pandas>=2.0.0
- requests>=2.31.0
npm:
- mermaid@latest
system:
- apt-get install -y imagemagick
沙箱不是万能保险。生产环境还需要限制网络访问、控制文件挂载、管理密钥注入方式,并记录脚本执行日志。尤其是邮箱密码、API Key 这类敏感信息,不应该写死在 SKILL.md 或脚本里,应该从密钥管理服务或环境变量读取。
Reference 的三种加载模式
references/ 目录用于放模板、规范、案例、术语表等材料。OpenSkills 支持三种加载模式:
references:
- path: references/guidelines.md
mode: always
description: 必读规范
- path: references/financial-handbook.md
mode: explicit
condition: 当讨论涉及财务、预算、成本时
- path: references/examples.md
mode: implicit
description: 示例参考
三种模式的区别:
| 模式 | 何时加载 | 适合放什么 |
|---|---|---|
always | 技能被选中后始终加载 | 输出模板、强约束规范 |
explicit | 满足明确条件时加载 | 财务手册、合规规则、特定业务说明 |
implicit | 由 LLM 判断是否有帮助 | 示例、背景资料、可选说明 |
如果只是把文件放进 references/ 目录,OpenSkills 也可以自动发现:
my-skill/
├── SKILL.md
└── references/
├── template.md
├── examples.md
└── appendix/
└── glossary.md
自动发现适合简单技能;当某些资料必须加载或只能在特定条件下加载时,最好显式写进 SKILL.md。
多 LLM 支持
OpenSkills 不绑定某一家模型服务,而是通过 OpenAI 兼容接口接入不同 LLM。
from openskills.llm import OpenAICompatClient
# OpenAI
client = OpenAICompatClient(
api_key="sk-...",
model="gpt-4",
)
# Azure OpenAI
client = OpenAICompatClient(
api_key="...",
base_url="https://xxx.openai.azure.com/",
model="gpt-4",
)
# Ollama 本地模型
client = OpenAICompatClient(
base_url="http://localhost:11434/v1",
model="llama3.2",
api_key="ollama",
)
只要服务提供 OpenAI 兼容接口,DeepSeek、Together AI、Groq 等模型平台也可以按类似方式接入。
CLI 工具
CLI(命令行工具)适合在开发阶段检查技能是否能被发现、是否能匹配、格式是否正确。
# 列出所有技能
openskills list
# 查看技能详情
openskills show weekly-report-to-annual
# 验证技能格式
openskills validate ./my-skill/
# 测试匹配
openskills match "生成年度报告"
这些命令适合放进开发流程里。比如提交新技能前先跑 validate,避免 YAML 缩进错误、脚本路径错误或 reference 路径不存在。
5 分钟创建一个翻译技能
一个最小技能不一定需要脚本,只写 SKILL.md 就能工作。
创建目录:
mkdir my-translator
cd my-translator
编写 SKILL.md:
---
name: smart-translator
description: 智能翻译,自动识别源语言和目标语言
version: 1.0.0
triggers:
- 翻译
- translate
- 翻成
---
# 智能翻译技能
你是一个专业的翻译助手。
## 翻译原则
1. 自动识别源语言和目标语言。
2. 保持原文的语气和风格。
3. 避免生硬的机翻腔。
4. 技术术语需要准确。
## 执行步骤
1. 识别源语言。
2. 识别目标语言;如果用户未指定,默认在中文和英文之间互译。
3. 翻译内容。
4. 检查术语、语气和格式。
调用这个技能:
import asyncio
from openskills import create_agent
async def main():
agent = await create_agent(
skill_paths=["./my-translator"],
api_key="your-key",
)
response = await agent.chat("""
翻译成英文:
这个项目让 AI Agent 的技能调用不再是黑盒。
""")
print(response.content)
asyncio.run(main())
可能输出:
This project makes AI Agent skill invocation transparent.
这个例子没有 scripts/,也没有 references/,但已经具备一个技能的基本形态:可发现、可匹配、可执行。
渐进式加载的性能表现
在一个测试配置中,技能库包含 50 个技能,每个技能有 10 个 reference 文档。全量加载和渐进式加载的结果如下:
| 方案 | 发现耗时 | 内存占用 | 匹配耗时 |
|---|---|---|---|
| 全量加载 | 8.5s | 450MB | 1.2s |
| OpenSkills 渐进式加载 | 0.3s | 25MB | 0.8s |
| 相对差异 | 约 28 倍 | 约 18 倍 | 约 1.5 倍 |
差异主要来自加载策略。全量加载会把所有技能说明和参考资料一起读入;渐进式加载只在技能匹配后读取相关指令,并且把 reference 的加载延后到真正需要的时候。
技能越多、reference 越大,这种差异越明显。只有几个小技能时,收益不会那么夸张,但结构上仍然更清晰。
适合和不适合的场景
| 场景 | 是否适合 | 原因 |
|---|---|---|
| 企业内部工作流自动化 | 适合 | 可以接入私有脚本、模板、知识库 |
| 多技能 Agent 平台 | 适合 | Metadata 匹配和渐进式加载能减少上下文浪费 |
| 需要审计技能调用过程 | 适合 | 技能定义、脚本路径、reference 条件都可见 |
| 一次性聊天任务 | 不一定需要 | 直接用普通对话成本更低 |
| 强确定性业务流程 | 谨慎使用 | 核心判断逻辑最好由代码和规则控制,LLM 只做理解和生成 |
| 未审查的第三方脚本 | 不适合直接执行 | 必须先做代码审查、权限限制和沙箱隔离 |
使用时容易踩的坑
-
不要把密钥写进技能文件
邮箱密码、API Key、数据库连接串应该走环境变量、密钥管理服务或运行时参数。 -
脚本输出要稳定
Agent 依赖脚本返回的 JSON。如果脚本有时返回日志、有时返回 JSON,解析会变得困难。日志建议写到标准错误,结构化结果写到标准输出。 -
Reference 不要无限堆
所有资料都放进always会重新制造“全量加载”问题。模板和硬规则用always,场景资料用explicit或implicit。 -
自动执行脚本要有权限边界
auto_execute_scripts=True很方便,但生产环境必须配合沙箱、白名单、超时和资源限制。 -
LLM 适合归纳,不适合替代校验
年度报告里的数字、项目名、日期最好来自脚本和结构化数据。LLM 可以负责整理表达,但关键字段要能追溯来源。 -
并发不是越大越好
邮箱、数据库、第三方接口通常有连接数限制。并发数应该按服务端限制和错误率调节,比如从 3~5 个线程开始压测。
OpenSkills 的核心价值不在于“让 Agent 能调用脚本”这一件事,而在于把技能调用拆成可工程化管理的几个环节:定义、发现、匹配、加载、执行和隔离。这样一来,AI Agent 不再只是一个聊天窗口,而可以成为可集成、可审计、可优化的业务组件。