[对比学习LangChain和MAF-12]Agent Skills的不同实现方式
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/添加到构造函数中。接下来我们创建了一个针对OpenAIClient的ChatClientAgent,在调用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对象,并调用其UseFileSkills和UseFileScriptRunner方法定义Skill的目录和ScriptRunner。ScriptRunner指向的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的脚本,并且需要传入两个参数value和factor,于是又回复一个针对run_skill_script工具的函数调用,参数为Skill名称、脚本名称和脚本参数; - Agent调用工具来执行这个脚本,脚本执行完成后将结果作为工具结果返回给LLM;
- LLM接收到脚本的执行结果后,结合之前加载的Skill内容和资源内容,生成最终的回答文本;
如果你对AgentSkillsProvider的设计和实现原理,已经它支持的多种Skill的定时形式和来源,可以参阅我的文章AgentSkillsProvider:将Skills引入MAF
更多推荐




所有评论(0)