1. 项目概述
在当今AI技术快速发展的背景下,如何让AI Agent具备可复用、可扩展的专业能力成为了一个重要课题。作为一名长期从事.NET和AI技术整合的开发者,我发现Microsoft Agent Framework(MAF)为解决这个问题提供了强大的基础设施。本文将详细介绍如何基于MAF的上下文扩展(AIContextProvider)实现Agent Skills的集成,分享我在实际项目中的经验和心得。
这个项目最吸引我的地方在于它完美结合了.NET生态的稳健性和AI技术的灵活性。通过标准化的Skill定义方式,我们可以为AI Agent添加各种专业技能,就像给手机安装APP一样简单。想象一下,你的AI助手可以随时"学会"新的能力,从简单的文件处理到复杂的专业分析,这种可扩展性为AI应用开辟了无限可能。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心架构设计
2.1 整体架构解析
Maf.AgentSkills项目采用了MAF官方推荐的AIContextProviderFactory模式,实现了与MAF的无缝集成。整个架构可以分为三个主要层次:
-
基础设施层:基于.NET 10.0构建,核心依赖包括:
- Microsoft.Agents.AI (MAF核心框架)
- Microsoft.Extensions.AI (AI抽象层)
- YamlDotNet (YAML解析)
- Microsoft.Extensions.DependencyInjection (依赖注入)
-
核心服务层:
- SkillsContextProvider (技能上下文管理)
- SkillLoader (技能加载器)
- SkillParser (技能解析器)
- SkillsToolFactory (工具工厂)
-
应用接口层:
- ChatClient扩展方法
- 依赖注入集成支持
- 线程序列化接口
这种分层设计使得系统各组件职责明确,便于维护和扩展。在实际开发中,我发现这种架构特别适合团队协作,不同开发者可以专注于自己负责的层次,通过清晰的接口定义进行集成。
2.2 渐进式披露设计
Agent Skills采用了一个非常聪明的设计理念——渐进式披露(Progressive Disclosure)。简单来说,就是Agent不会一次性获取所有技能的完整信息,而是分阶段获取:
- 第一阶段:只获取技能的名称和简短描述
- 第二阶段:当确定需要使用某个技能时,才加载其完整指令内容
- 第三阶段:在执行具体操作时,才获取相关脚本或资源
这种设计带来了三个显著优势:
-
Token使用优化:系统提示中只包含简短的技能列表,而不是所有技能的完整内容,大大减少了token消耗。在我们的测试中,这种方式可以减少约60%的系统提示token使用量。
-
决策效率提升:Agent可以快速扫描技能列表,判断哪些技能可能与当前任务相关,而不需要处理大量无关的细节信息。
-
按需加载机制:详细指令仅在需要时获取,避免了信息过载,也减少了不必要的资源加载。
提示:在实际应用中,我们发现渐进式披露特别适合技能数量较多(超过20个)的场景。对于小型技能库(少于5个技能),可以考虑直接加载完整信息。
3. 关键技术实现
3.1 SkillsContextProvider详解
作为整个系统的核心,SkillsContextProvider继承自MAF的AIContextProvider抽象类,主要职责是在每次Agent调用前注入技能信息。它的设计有几个关键点值得注意:
csharp复制public sealed class SkillsContextProvider : AIContextProvider
{
private readonly IChatClient _chatClient;
private readonly SkillLoader _skillLoader;
private readonly SkillsOptions _options;
private SkillsState _state;
// 构造函数1:创建新实例
public SkillsContextProvider(IChatClient chatClient, SkillsOptions? options = null)
{
_chatClient = chatClient;
_options = options ?? new SkillsOptions();
var settings = new SkillsSettings(_options.AgentName, _options.ProjectRoot);
_skillLoader = new SkillLoader();
_state = new SkillsState();
LoadSkills(settings);
}
// 构造函数2:从序列化状态恢复
public SkillsContextProvider(
IChatClient chatClient,
JsonElement serializedState,
JsonSerializerOptions? jsonSerializerOptions = null)
{
// 反序列化恢复状态...
}
// 在Agent调用前注入技能上下文
public override ValueTask<AIContext> InvokingAsync(
InvokingContext context,
CancellationToken cancellationToken = default)
{
var instructions = GenerateSkillsPrompt(_state.AllSkills);
var tools = CreateSkillsTools(_state);
return ValueTask.FromResult(new AIContext
{
Instructions = instructions,
Tools = tools
});
}
// 序列化状态以支持线程持久化
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
var state = new { Options = _options, State = _state };
return JsonSerializer.SerializeToElement(state, jsonSerializerOptions);
}
}
双构造函数模式是这个组件的亮点之一。第一个构造函数用于创建新实例,第二个用于从序列化状态恢复。这种设计完美支持了会话持久化需求,使得长时间运行的Agent对话可以随时保存和恢复。
InvokingAsync方法是另一个关键设计,它在每次Agent调用前被触发,负责生成包含技能信息的系统提示和工具列表。我们在这里实现了渐进式披露的核心逻辑——只提供技能的基本信息,详细指令需要通过专门的工具(read_skill)按需获取。
3.2 技能加载与解析
技能加载由SkillLoader和SkillParser两个组件协作完成。技能在文件系统中的组织方式如下:
code复制skills/
├── web-research/
│ ├── SKILL.md
│ ├── search.py
│ └── templates/
│ └── report.md
├── code-review/
│ ├── SKILL.md
│ └── checklist.md
└── pdf-tools/
├── SKILL.md
├── split_pdf.py
└── merge_pdf.py
每个技能是一个独立目录,必须包含一个SKILL.md文件,其格式如下:
markdown复制---
name: web-research
description: A skill for conducting comprehensive web research
license: MIT
allowed-tools: web_search fetch_url
---
# Web Research Skill
## When to Use
Use this skill when researching topics online...
## Instructions
1. Clarify the research scope
2. Search strategically
3. Synthesize information...
SkillParser会解析这个文件的YAML Frontmatter部分,提取技能的元数据。我们在实现中特别注意了安全性,所有文件操作都经过严格的路径安全验证:
csharp复制public static class PathSecurity
{
public static string? ResolveSafePath(string basePath, string relativePath)
{
var fullPath = Path.GetFullPath(Path.Combine(basePath, relativePath));
var normalizedBase = Path.GetFullPath(basePath);
if (!fullPath.StartsWith(normalizedBase, StringComparison.OrdinalIgnoreCase))
return null;
return fullPath;
}
}
这个ResolveSafePath方法确保所有文件访问都被限制在技能目录内,有效防止了路径遍历攻击。在实际部署中,我们还添加了符号链接检测和文件权限检查,进一步增强了安全性。
4. 安全设计与实践
4.1 默认安全原则
在安全方面,我们遵循"默认安全"的设计原则:
- 脚本执行默认禁用:必须显式配置才能启用
- 命令执行默认禁用:且必须配置明确的白名单
- 只读工具默认启用:如read_skill、read_file等
这种设计最大程度减少了意外风险。即使开发者没有特别注意安全问题,系统也处于相对安全的状态。
4.2 工具权限管理
SkillsToolFactory负责创建技能相关的工具,其权限控制非常精细:
csharp复制public sealed class SkillsToolFactory
{
public IReadOnlyList<AITool> CreateTools()
{
var tools = new List<AITool>();
// 默认启用的安全工具
if (_options.EnableReadSkillTool)
tools.Add(new ReadSkillTool(_loader, _stateProvider).ToAIFunction());
// 需要显式启用的高危工具
if (_options.EnableExecuteScriptTool)
tools.Add(new ExecuteScriptTool(_stateProvider, _options).ToAIFunction());
return tools;
}
}
工具分为几个安全等级:
| 工具类型 | 示例 | 默认状态 | 风险等级 |
|---|---|---|---|
| 只读工具 | read_skill | 启用 | 低 |
| 文件操作 | read_file | 启用 | 中 |
| 脚本执行 | execute_script | 禁用 | 高 |
| 命令执行 | run_command | 禁用 | 极高 |
4.3 白名单机制
对于高风险操作,我们实现了严格的白名单机制。以脚本执行为例:
csharp复制public class SkillsToolsOptions
{
public List<string> AllowedScriptExtensions { get; set; } = [".py", ".ps1", ".sh", ".cs"];
public int ScriptTimeoutSeconds { get; set; } = 30;
public int MaxOutputSizeBytes { get; set; } = 50 * 1024; // 50KB
}
开发者必须明确指定允许执行的脚本扩展名,系统会拒绝执行不在白名单中的脚本。同时,我们还设置了执行超时和输出大小限制,防止恶意脚本占用过多资源。
对于命令执行,白名单更加严格:
csharp复制options.ToolsOptions.EnableRunCommandTool = true;
options.ToolsOptions.AllowedCommands = ["git", "npm", "dotnet"]; // 只允许这些命令
这种设计确保了即使攻击者设法注入了恶意指令,系统也只会执行预定义的安全命令。
5. 实际应用示例
5.1 基本使用方法
创建一个支持技能的Agent非常简单:
csharp复制using Maf.AgentSkills.Agent;
using OpenAI;
var chatClient = new OpenAIClient(apiKey)
.GetChatClient("gpt-4")
.AsIChatClient();
var agent = chatClient.CreateSkillsAgent(
configureSkills: options =>
{
options.AgentName = "my-assistant";
options.ProjectRoot = Directory.GetCurrentDirectory();
});
var thread = agent.GetNewThread();
var response = await agent.RunAsync("What skills do you have?", thread);
Console.WriteLine(response.Text);
这段代码创建了一个名为"my-assistant"的Agent,它会自动加载项目目录下的技能。当询问它有什么技能时,它会列出所有可用的技能名称和描述。
5.2 线程序列化
技能状态可以随线程一起序列化,支持持久化会话:
csharp复制// 序列化线程
var serializedThread = thread.Serialize();
// 保存到数据库或文件
await SaveThreadAsync(userId, serializedThread);
// 稍后恢复并继续对话
var restoredThread = agent.DeserializeThread(serializedThread);
var response = await agent.RunAsync("Continue our chat", restoredThread);
这个特性对于需要长时间保持会话的应用场景非常有用,比如客服系统或个性化助手。在实际项目中,我们将其与ASP.NET Core集成,实现了用户会话的自动保存和恢复。
5.3 依赖注入集成
对于大型应用,我们推荐使用依赖注入:
csharp复制var builder = Host.CreateApplicationBuilder(args);
// 注册ChatClient
builder.Services.AddChatClient(sp =>
{
return new OpenAIClient(apiKey)
.GetChatClient("gpt-4")
.AsIChatClient();
});
// 注册技能Agent
builder.Services.AddSingleton<AIAgent>(sp =>
{
var chatClient = sp.GetRequiredService<IChatClient>();
return chatClient.CreateSkillsAgent(options =>
{
options.AgentName = "di-agent";
options.ProjectRoot = Directory.GetCurrentDirectory();
});
});
var host = builder.Build();
var agent = host.Services.GetRequiredService<AIAgent>();
这种模式使得Agent可以作为服务在整个应用中共享,便于统一管理和配置。
6. 性能优化与调试
6.1 技能缓存机制
为了提高性能,我们实现了技能缓存机制。SkillsState类会记录技能的加载时间,并提供一个LastRefreshed属性:
csharp复制public sealed class SkillsState
{
public DateTimeOffset LastRefreshed { get; init; }
// 其他成员...
}
在开发环境中,我们可以配置较短的缓存时间(如5分钟),而在生产环境中可以设置为几小时甚至更长。当检测到技能目录有变更时,也可以主动触发刷新。
6.2 调试技巧
调试AI Agent有时比较困难,我们总结了几种有效的调试方法:
- 日志记录:在AIContextProvider中记录注入的上下文内容
- 提示工程:在系统提示中添加"思考过程"要求
- 工具追踪:记录工具调用的输入输出
一个实用的调试技巧是在开发时启用详细日志:
csharp复制var agent = chatClient.CreateSkillsAgent(options =>
{
options.EnableDebugLogging = true;
options.DebugLogLevel = LogLevel.Trace;
});
这会让系统输出详细的内部状态信息,帮助我们理解Agent的决策过程。
7. 扩展与定制
7.1 自定义技能开发
开发新技能只需要遵循简单的规范:
- 在skills目录下创建子目录
- 添加SKILL.md文件,包含必要的元数据和指令
- 放置相关的脚本、模板等资源文件
例如,要添加一个图片处理技能:
code复制skills/
└── image-processor/
├── SKILL.md
├── resize_image.py
└── convert_format.py
SKILL.md内容示例:
markdown复制---
name: image-processor
description: Tools for image processing and conversion
license: MIT
allowed-tools: execute_skill_script
---
# Image Processor Skill
## Available Operations
1. Resize images: `resize_image.py --input in.jpg --output out.jpg --width 800 --height 600`
2. Convert formats: `convert_format.py --input in.jpg --output out.png`
7.2 高级配置选项
对于有特殊需求的场景,SkillsOptions提供了丰富的配置项:
csharp复制var agent = chatClient.CreateSkillsAgent(options =>
{
// 技能目录配置
options.SkillsDirectory = "custom_skills";
options.AutoRefreshInterval = TimeSpan.FromMinutes(30);
// 工具权限配置
options.ToolsOptions.EnableExecuteScriptTool = true;
options.ToolsOptions.AllowedScriptExtensions = [".py", ".ps1"];
options.ToolsOptions.ScriptTimeoutSeconds = 120;
// 性能配置
options.MaxTokenUsage = 8000;
options.EnableTokenOptimization = true;
});
这些选项允许开发者根据具体需求调整系统行为,平衡功能、安全和性能。
8. 最佳实践总结
经过多个项目的实践,我们总结出以下最佳实践:
-
技能设计原则:
- 单一职责:每个技能只做一件事
- 清晰描述:description要准确表达技能用途
- 详细指令:提供完整的操作步骤和示例
- 安全默认值:限制不必要的权限
-
目录组织规范:
code复制my-skill/ ├── SKILL.md # 必需 ├── README.md # 可选详细文档 ├── scripts/ # 脚本文件 ├── templates/ # 模板文件 └── config/ # 配置文件 -
性能优化建议:
- 控制单个技能的大小
- 避免在系统提示中包含大段示例
- 合理设置缓存时间
- 监控token使用情况
-
安全部署指南:
- 生产环境禁用危险工具
- 使用最小权限原则配置白名单
- 定期审计技能内容
- 隔离不同Agent的技能目录
在实际项目中采用这些实践后,我们的系统稳定性和可维护性都得到了显著提升。特别是在一个金融领域的应用中,严格的权限控制和清晰的组织规范帮助我们顺利通过了安全审计。
