Agent Skills是可移植的指令、脚本和资源包,赋予Agent专门化的能力和领域专业知识。Skill遵循开放规范,并采用渐进式披露模式,因此Agent只需在需要时加载所需的上下文。Agent Skills可用于:

  • 打包领域专业知识:将专业知识(费用政策、法律流程、数据分析流程)封装成可重用、可移植的软件包;
  • 扩展代理能力:在不更改核心指令的情况下,赋予代理新的能力;
  • 确保一致性:将多步骤任务转换为可重复、可审计的工作流程;
  • 实现互操作性:在不同的兼容Agent Skills的产品中重用相同的Skill;

Agent Skills在不同个Agent开发框架中的实现可能会有所不同,但它们的核心理念是相似的。接下来我们就来看看Agent Skills分别在LangChain和MAF中的编程方式和实现原理。

1. LangChain

虽然Deep Agents利用SkillsMiddleware提供了针对Agent Skills的实现,但是为了让读者更好地理解Agent Skills的概念和实现原理,我们利用一个简单的实例来演示如何采用最朴素的方式来实现Agent Skills。

1.1 利用系统提示词和工具实现Agent Skills

我们首先定义如下同名类型来描述一个Skill:

class Skill(TypedDict):
    name:str
    description:str
    content:str

一个Agent可以同时绑定多一个Skill,如下这个skills列表标识所有可用的Skill。简单起见,其中仅仅包含一个唯一的名为translator的Skill,这个Skill的功能是将中文古典诗词精确翻译成地道的英文。content字段中包含了这个Skill的详细指令。

skills = [
    {
        "name": "translator",
        "description": "能够将中文古典诗词精确翻译成地道的英文",
        "content": """\
## 详细指令

- 1. 尽量能够**押韵**;
- 2. 提供**3种**翻译变体;
- 3. 保留原诗的**禅意**。
"""
    }
]

Agent Skills实现的所谓渐进式披露体现在:加载所有Skill的元数据,并作为系统提示词的一部分,让LLM知晓目前它拥有的Skill;并提供一个用于加载Skill的工具,当LLM需要使用某个Skill时,可以调用这个工具来加载对应的内容。如下所示的系统提示词和Skill加载工具函数的定义。

system_prompt = """、
You have access to a tool `load_skill` that can load the content of a skill by its name. When you need to use a skill, call the tool with the appropriate skill name to get its content and then follow the instructions in the skill content to complete the task.

## Available Skills:
"""

@tool
def load_skill(skill_name:str) -> str:
    """Load the content of a skill by its name."""
    for skill in skills:
        if skill["name"] == skill_name:
            return skill["content"]
    return f"Specified skill '{skill_name}' is not found."

我们根据上面定义的Skill加载工具和系统提示词调用create_agent函数来创建一个Agent,并调用其ainvoke方法来请求它将一首中文古典诗词翻译成英文。为了展示整个推理的流程,我们在调用ainvoke方法后遍历返回结果中的消息,并将它们逐条打印出来。

import asyncio
from typing import TypedDict
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain.tools import tool
from dotenv import load_dotenv

load_dotenv()

agent = create_agent(
    model=ChatOpenAI(model="gpt-5.2-chat"),
    tools=[load_skill],
    system_prompt=system_prompt
)

async def main():
    result = await agent.ainvoke(input={
        "messages": [
            {"role": "user", "content": "把这句诗翻译成英文:千江同一月,万户尽皆春。千江有水千江月,万里无云万里天。"}
        ]
    })
    for message in result["messages"]:
        message.pretty_print()

asyncio.run(main())

输出:

================================ Human Message =================================

把这句诗翻译成英文:千江同一月,万户尽皆春。千江有水千江月,万里无云万里天。
================================== Ai Message ==================================
Tool Calls:
  load_skill (call_hHEFcJ9cHkuMgSxy5DR4aHDq)
 Call ID: call_hHEFcJ9cHkuMgSxy5DR4aHDq
  Args:
    skill_name: translator
================================= Tool Message =================================
Name: load_skill

## 详细指令

- 1. 尽量能够**押韵**;
- 2. 提供**3种**翻译变体;
- 3. 保留原诗的**禅意**。

================================== Ai Message ==================================

下面给出 **3 种英文译本**,都力求 **押韵、保留禅意**,但在风格与自由度上各有侧重。

---

### **译本一|偏忠实、意境清澈**

*One moon is shared by a thousand streams,  
In ten thousand homes, the breath of spring.  
Where there is water, there the moon is found;  
For miles unclouded, heaven knows no bound.*

---

### **译本二|偏诗性、节奏与押韵更强**

*A thousand rivers hold the selfsame moon,  
Ten thousand doors awake to spring in bloom.  
Where water flows, the moon appears anew;  
For miles of sky, no cloud obscures the blue.*

---

### **译本三|偏禅意、语言更简洁空灵**

*One moon reflected in a thousand streams,  
Spring enters every home, as if in dreams.  
Each river bears the moon just as it is;  
Ten thousand miles of sky—no cloud, no trace.*

---

如果你愿意,我也可以提供  
- **更偏现代自由诗的版本**  
- **更贴近佛教禅宗语境的译法**  
- 或 **严格对仗、四行四拍的英诗形式**

由于系统提示词中明确告诉LLM调用load_skill工具加载与当前任务相关的Skill来获取指导指令,并且提供可可用Skill的元数据信息,所以LLM并不会按照自己的理解来翻译这首诗,而是会调用load_skill工具来加载translator这个Skill的内容,并严格按照其中的指令来生成三个不同风格的英文译本。

1.2 直接使用Deep Agents

Deep Agents的create_deep_agent函数利用内置的SkillsMiddleware提供了内置的Agent Skills的支持。由于它还提供FileSystemMiddleware构建了一个抽象的文件系统,Agents Skills就建立在这个文件系统上,这意味着我们可用任何一种支持的文件来存储Skill的内容。这里我们直接使用本地的物理文件系统,并将定义Skill的skill.md文件保存./translator/目录下。skill.md文件的内容如下:

---
name: translator
description: 将中文古典诗词精确翻译成地道的英文
---

## 详细指令

- 1. 尽量能够**押韵**;
- 2. 提供**3种**翻译变体;
- 3. 保留原诗的**禅意**。

使用Deep Agents将使我们的程序变得异常简单。如下面的代码片段所示,我们在调用create_deep_agent函数时,直接将包含Skill定义的目录路径./skills/添加到skills参数中即可。剩下的程序逻辑与之前的示例完全一样。

from deepagents import create_deep_agent
from deepagents.backends import FilesystemBackend
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
import asyncio

load_dotenv()

agent = create_deep_agent(
    model=ChatOpenAI(model= "gpt-5.2-chat"),
    backend= FilesystemBackend(root_dir=".", virtual_mode=True),
    skills=["/skills/"],
)
       
async def main():
    result = await agent.ainvoke(input={
        "messages": [
            {"role": "user", "content": "把这句诗翻译成英文:千江同一月,万户尽皆春。千江有水千江月,万里无云万里天。"}
        ]
    })
    for message in result["messages"]:
        message.pretty_print()

asyncio.run(main())

输出:

================================ Human Message =================================

把这句诗翻译成英文:千江同一月,万户尽皆春。千江有水千江月,万里无云万里天。
================================== Ai Message ==================================
Tool Calls:
  read_file (call_BCVSN8iELvYiJej6YTukOJdb)
 Call ID: call_BCVSN8iELvYiJej6YTukOJdb
  Args:
    file_path: /skills/translator/SKILL.md
    limit: 200
================================= Tool Message =================================
Name: read_file

     1  ---
     2  name: translator
     3  description: 将中文古典诗词精确翻译成地道的英文
     4  ---
     5
     6  ## 详细指令
     7
     8  - 1. 尽量能够**押韵**;
     9  - 2. 提供**3种**翻译变体;
    10  - 3. 保留原诗的**禅意**。
================================== Ai Message ==================================

**译文一(偏直译,温和平实,尾韵统一)**  
A single moon shines on a thousand streams,  
In ten thousand homes, spring softly gleams.  
Wherever there is water, the moon appears;  
Where clouds are absent, the sky stretches clear.

**译文二(偏诗性,节奏与禅意并重)**  
One moon is shared by a thousand rivers wide,  
Ten thousand doors—each filled with spring inside.  
A thousand streams hold a thousand moons in view;  
Ten thousand miles of sky, all empty blue.

**译文三(偏凝练哲思,强调禅境)**  
The moon is one, yet mirrored in all streams;  
Spring enters every home, as in a dream.  
Where water flows, the moon is never lost;  
Where clouds are gone, the heavens span unblocked.

可以看出输出的内容与之前的示例基本一致,唯一的差别体现在:

  • Deep Agents调用了一个名为read_file的工具(这是FileSystemMiddleware提供的工具)来读取存储Skill内容的文件,而之前的示例中我们直接定义了一个load_skill工具函数来加载Skill内容;
  • 前面演示的实例使用自定义的load_skill工具函数来加载Skill内容;

由此我们不难看出Agent Skills的实现原理:

  • Agent初始化的时候建在所有可用Skill的元数据,格式化后称为系统提示词的一部分,让LLM知晓目前它拥有的Skill;
  • 提供一个用于加载Skill的工具,当LLM需要使用某个Skill时,可以调用这个工具来加载对应的内容;
  • 由于Skill的内容是通过工具函数动态加载的,所以Skill提供的指令实际上是存放在对话历史之中;

可能有读者会好奇,既然Skill的内容是存放在对话历史中的,那么它会不会因为针对对话历史的压缩机制导致Skill被剔除呢?答案是:会,而且这也正是我们希望的。因为LLM的不确定性决定了对于某个加载的Skill是否被使用是无法预知的,为了避免无用的Skill内容占用对话历史窗口的空间,针对对话历史的压缩正好达到了我们的目的。由于并没有限制同一个Skill的重复加载,所以当LLM需要使用某个Skill时,它完全可以再次调用工具函数来加载这个Skill的内容。

如果对SkillsMiddleware的设计和实现原理感兴趣,可以参阅我的文章通过SkillsMiddleware将Skill引入LangChain

2. MAF

Agent Skills和RAG一样,同样属于对Agent输入增强的范畴,因此在MAF中Agent Skills同样是通过AIContextProvider来实现的,这个将Agent Skills引入MAF的AIContextProvider类型为AgentSkillsProvider

2.1 利用AgentSkillsProvider引入Agent Skills

AgentSkillsProvider支持Skill的多种定义形式和来源,这里我们也采用常规操作,将上面定义的skill.md文件保存项目根目录下的./skills/目录下。在如下的演示程序中,我们创建了一个AgentSkillsProvider,并将包含Skill定义的目录路径./skills/添加到构造函数中。接下来我们创建了一个针对OpenAIClientChatClientAgent,在调用AsAIAgent扩展方法时,我们提供了一个ChatClientAgentOptions对象,并将之前创建的AgentSkillsProvider添加到AIContextProviders属性中。接下来我们提供相同提示词调用创建的Agent,并打印输出结果。

using dotenv.net;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Responses;
using System.ClientModel;

DotEnv.Load();

var model = Environment.GetEnvironmentVariable("MODEL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var endpoint = Environment.GetEnvironmentVariable("OPENAI_URL")!;

var agentSkillsProvider = new AgentSkillsProvider(["./skills"]);
var agent = new OpenAIClient(
        credential: new ApiKeyCredential(key: apiKey),
        options: new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
    .GetChatClient(model:model)
    .AsIChatClient()
    .AsAIAgent(options: new ChatClientAgentOptions {  AIContextProviders = [agentSkillsProvider] });

var response = await agent.RunAsync(message: "把这句诗翻译成英文:千江同一月,万户尽皆春。千江有水千江月,万里无云万里天。");
Console.WriteLine(response.Text);

输出:

下面给出 **三种英文译本**,力求在意象、禅意与英文诗性之间取得平衡,并尽量保留一定的韵律。

---

### **Version I · Classical & Balanced**

*One moon is mirrored by a thousand streams,*
*In ten thousand homes, all dwell in spring.*
*Wherever there is water, there shines the moon;*
*Wherever clouds are gone, the sky is boundless blue.*

---

### **Version II · More Poetic, Stronger Rhythm**

*A single moon shared by a thousand rivers,*
*A thousand homes, all steeped in spring.*
*Each stream that holds water holds the moon;*
*Each mile without clouds reveals the sky.*

---

### **Version III · Zen-like & Minimalist**

*One moon, a thousand rivers.*
*One spring, ten thousand homes.*
*Where there is water, the moon appears;*
*Where there are no clouds, the sky is endless.*

---

如果你希望:
- **更偏现代自由诗**
- **更严格押韵**
- 或 **更偏佛教禅宗语感(如英译禅诗风格)**

可以告诉我,我可以再为你细调一个版本。

2.2 一个更加完整的例子

接下来我们演示一个同时涉及资源和脚本的Agent Skills的例子,我们编写了一个用于执行单位转换的Skill,并将其命名为unit-converter。此Skill所在目录结构如下:

skills/
└── unit-converter/
    ├── SKILL.md
    ├── references
    |   └──conversion-table.md
    └── scripts
        └──convert.py

SKILL.md文件、conversion-table.md文件和convert.py文件的内容如下:

---
name: unit-converter
description: Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.
---

Use this skill when the user asks to convert between units.

- 1. Review the **references/conversion-table.md** resource to find the correct factor.
- 2. Use `--value <value> --factor <factor>` options to run the **scripts/convert.py** script.
- 3. Present the result clearly with both units.
# Conversion Tables

Formula: **result = value × factor**

| From       | To         | Factor   |
|------------|------------|----------|
| miles      | kilometers | 1.60934  |
| kilometers | miles      | 0.621371 |
| pounds     | kilograms  | 0.453592 |
| kilograms  | pounds     | 2.20462  |
import argparse,json

def main():
    parser = argparse.ArgumentParser(description='Unit Converter')
    parser.add_argument('--value', type=float, help='The value to convert')
    parser.add_argument('--factor', type=float, help='The conversion factor')
    args = parser.parse_args()
    result = args.value * args.factor
    print(json.dumps({"result": result,"value": args.value, "factor": args.factor}))

if __name__ == "__main__":
    main()

如下所示的是完整的演示程序。我们利用AgentSkillsProviderBuilder来构建一个AgentSkillsProvider对象,并调用其UseFileSkillsUseFileScriptRunner方法定义Skill的目录和ScriptRunnerScriptRunner指向的RunAsync方法会启动一个子进程来执行Python脚本,并将脚本的输出作为结果返回。最后我们创建了一个Agent对象,并调用RunAsync方法来运行这个Agent,传入一个需要单位转换的消息。

using dotenv.net;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using System.ClientModel;
using System.Diagnostics;
using System.Text.Json;

DotEnv.Load();

var model = Environment.GetEnvironmentVariable("MODEL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var endpoint = Environment.GetEnvironmentVariable("OPENAI_URL")!;

var agentSkillsProvider = new AgentSkillsProviderBuilder()
    .UseFileSkills(["./skills"])
    .UseFileScriptRunner(new AgentFileSkillScriptRunner(RunAsync))
    .Build();
var agent = new OpenAIClient(
        credential: new ApiKeyCredential(key: apiKey),
        options: new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
    .GetChatClient(model: model)
    .AsIChatClient()
    .AsAIAgent(options: new ChatClientAgentOptions { AIContextProviders = [agentSkillsProvider] });

var response = await agent.RunAsync(message: "一公斤有几磅?");
var inex = 1;
foreach(var message in response.Messages)
{
    Console.WriteLine($"\n{new string('-', 40)}Message {inex++}{new string('-', 40)}");
    PrintMessage(message);
}

static async Task<object?> RunAsync(
    AgentFileSkill skill,
    AgentFileSkillScript script,
    AIFunctionArguments args,
    CancellationToken cancellationToken)
{
    var psi = new ProcessStartInfo("python3")
    {
        RedirectStandardOutput = true,
        UseShellExecute = false,
    };
    psi.ArgumentList.Add(Path.Combine(skill.Path, script.FullPath));
    foreach (var (k, v) in args)
    {
        if (v is not null)
        {
            psi.ArgumentList.Add($"--{k}");
            psi.ArgumentList.Add(v.ToString()!);
        }
    }
    using var process = Process.Start(psi)!;
    string output = await process.StandardOutput.ReadToEndAsync();
    await process.WaitForExitAsync();
    return output.Trim();
}

static void PrintMessage(ChatMessage message)
{ 
    Console.WriteLine($"Role: {message.Role}");
    Console.WriteLine("Contents:");
    foreach(var content in message.Contents ?? [])
    {
        switch (content)
        {
            case FunctionCallContent call:
                Console.WriteLine($"{new string(' ', 4)}FunctionCall");
                Console.WriteLine($"{new string(' ', 8)}Name:  {call.Name}");
                Console.WriteLine($"{new string(' ', 8)}CallId: {call.CallId}");
                if (call.Arguments is not null)
                {
                    Console.WriteLine($"{new string(' ', 8)}Arguments");
                    foreach (var (k, v) in call.Arguments)
                    {
                        Console.WriteLine($"{new string(' ', 12)}{k} = {v}");
                    }
                }
                break;
            case FunctionResultContent result:
                Console.WriteLine($"{new string(' ', 4)}FunctionResult:");
                string resultString;

                if (result.Result is JsonElement jeResult && jeResult.ValueKind == JsonValueKind.String)
                {
                    resultString = jeResult.GetString()!;
                }
                else 
                {
                    resultString = $"\"{result}\"";
                }

                foreach (var line in resultString.Split(Environment.NewLine))
                {
                    Console.WriteLine($"{new string(' ', 8)}{line}");
                }
                break;
            case TextContent text:
                Console.WriteLine($"{new string(' ', 4)}Text: {text.Text}");
                break;
        }
    }
}

由于我们提出的是一个很常规的问题,为了验证Agent是否通过我们提供的Skill对问题作答,我们将整个过程涉及的对话历史输出来啊。程序之后会输出如下所示的七个消息。

----------------------------------------Message 1----------------------------------------
Role: assistant
Contents:
    FunctionCall
        Name:  load_skill
        CallId: call_8gfwzx1ybDJc7i9NYJeDfmhq
        Arguments
            skillName = unit-converter

----------------------------------------Message 2----------------------------------------
Role: tool
Contents:
    FunctionResult:
        ---
        name: unit-converter
        description: Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.
        ---

        Use this skill when the user asks to convert between units.

        - 1. Review the **references/conversion-table.md** resource to find the correct factor.
        - 2. Use `--value <value> --factor <factor>` options to run the **scripts/convert.py** script.
        - 3. Present the result clearly with both units.

----------------------------------------Message 3----------------------------------------
Role: assistant
Contents:
    FunctionCall
        Name:  read_skill_resource
        CallId: call_sjcJBsl4ihmpY6sEMIjD1dfK
        Arguments
            skillName = unit-converter
            resourceName = references/conversion-table.md

----------------------------------------Message 4----------------------------------------
Role: tool
Contents:
    FunctionResult:
        # Conversion Tables

        Formula: **result = value × factor**

        | From       | To         | Factor   |
        |------------|------------|----------|
        | miles      | kilometers | 1.60934  |
        | kilometers | miles      | 0.621371 |
        | pounds     | kilograms  | 0.453592 |
        | kilograms  | pounds     | 2.20462  |

----------------------------------------Message 5----------------------------------------
Role: assistant
Contents:
    FunctionCall
        Name:  run_skill_script
        CallId: call_VRIBhSYxFmX2OkVKiwA7YOqu
        Arguments
            skillName = unit-converter
            scriptName = scripts/convert.py
            arguments = {"value":1,"factor":2.20462}

----------------------------------------Message 6----------------------------------------
Role: tool
Contents:
    FunctionResult:
        {"result": 2.20462, "value": 1.0, "factor": 2.20462}

----------------------------------------Message 7----------------------------------------
Role: assistant
Contents:
    Text: 1 公斤 ≈ **2.20462 磅**。

通常也可以近似记为:**1 公斤 ≈ 2.2 磅**。

从上面的输出可以看出Agent内部的执行流程:

  • Agent首先调用LLM,后者回复一个消息,并携带一个针对load_skill工具的函数调用,参数为我们定义的Skill的名称unit-converter
  • Agent接收到这个消息后,识别出这是一个函数调用,于是调用对应的工具来加载这个Skill的内容,并将内容作为工具结果返回给LLM;
  • LLM接收到Skill的内容后,发现涉及一个命名为references/conversion-table.md的资源,于是又回复一个针对read_skill_resource工具的函数调用,参数为Skill名称和资源名称;
  • Agent调用工具来读取这个资源的内容,并将内容作为工具结果返回给LLM;
  • LLM接收到资源的内容后,发现需要运行一个命名为scripts/convert.py的脚本,并且需要传入两个参数valuefactor,于是又回复一个针对run_skill_script工具的函数调用,参数为Skill名称、脚本名称和脚本参数;
  • Agent调用工具来执行这个脚本,脚本执行完成后将结果作为工具结果返回给LLM;
  • LLM接收到脚本的执行结果后,结合之前加载的Skill内容和资源内容,生成最终的回答文本;

如果你对AgentSkillsProvider的设计和实现原理,已经它支持的多种Skill的定时形式和来源,可以参阅我的文章AgentSkillsProvider:将Skills引入MAF

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐