芥末
发布于 2026-01-30 / 0 阅读
0
0

OpenSkills 入门:用 SKILL.md 定义可控的 AI Agent 技能调用

人工智能(AI)Agent(智能体)经常需要调用外部能力:查邮件、读文件、生成报告、执行脚本、调用接口。问题在于,很多托管型 AI 助手把这些能力封装在聊天界面里,开发者只能“使用结果”,很难控制技能如何被发现、如何匹配、何时加载上下文、脚本又是怎样被执行的。

OpenSkills 解决的就是这个问题:把 AI Agent 的技能系统做成一个可读、可改、可集成的开源框架。一个技能可以用一个 SKILL.md 文件描述,参考资料放在 references/ 目录,脚本放在 scripts/ 目录,然后由 Agent 按需加载和调用。

仓库:https://github.com/twwch/OpenSkills
协议:Apache 2.0
技术栈:Python 3.10+

OpenSkills 解决什么问题

假设一名工程师一年里有 50 多封周报邮件,年底需要整理年度复盘。人工处理通常要经历这些步骤:

  1. 打开邮箱并搜索所有周报;
  2. 逐封复制内容;
  3. 提取项目成果、技术成长、团队贡献;
  4. 套用年度复盘模板;
  5. 调整格式并保存成文档。

这类任务很适合交给 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.5s450MB1.2s
OpenSkills 渐进式加载0.3s25MB0.8s
相对差异约 28 倍约 18 倍约 1.5 倍

差异主要来自加载策略。全量加载会把所有技能说明和参考资料一起读入;渐进式加载只在技能匹配后读取相关指令,并且把 reference 的加载延后到真正需要的时候。

技能越多、reference 越大,这种差异越明显。只有几个小技能时,收益不会那么夸张,但结构上仍然更清晰。

适合和不适合的场景

场景是否适合原因
企业内部工作流自动化适合可以接入私有脚本、模板、知识库
多技能 Agent 平台适合Metadata 匹配和渐进式加载能减少上下文浪费
需要审计技能调用过程适合技能定义、脚本路径、reference 条件都可见
一次性聊天任务不一定需要直接用普通对话成本更低
强确定性业务流程谨慎使用核心判断逻辑最好由代码和规则控制,LLM 只做理解和生成
未审查的第三方脚本不适合直接执行必须先做代码审查、权限限制和沙箱隔离

使用时容易踩的坑

  1. 不要把密钥写进技能文件
    邮箱密码、API Key、数据库连接串应该走环境变量、密钥管理服务或运行时参数。

  2. 脚本输出要稳定
    Agent 依赖脚本返回的 JSON。如果脚本有时返回日志、有时返回 JSON,解析会变得困难。日志建议写到标准错误,结构化结果写到标准输出。

  3. Reference 不要无限堆
    所有资料都放进 always 会重新制造“全量加载”问题。模板和硬规则用 always,场景资料用 explicitimplicit

  4. 自动执行脚本要有权限边界
    auto_execute_scripts=True 很方便,但生产环境必须配合沙箱、白名单、超时和资源限制。

  5. LLM 适合归纳,不适合替代校验
    年度报告里的数字、项目名、日期最好来自脚本和结构化数据。LLM 可以负责整理表达,但关键字段要能追溯来源。

  6. 并发不是越大越好
    邮箱、数据库、第三方接口通常有连接数限制。并发数应该按服务端限制和错误率调节,比如从 3~5 个线程开始压测。

OpenSkills 的核心价值不在于“让 Agent 能调用脚本”这一件事,而在于把技能调用拆成可工程化管理的几个环节:定义、发现、匹配、加载、执行和隔离。这样一来,AI Agent 不再只是一个聊天窗口,而可以成为可集成、可审计、可优化的业务组件。


评论