首页 > 基础资料 博客日记
MAF快速入门(21)RC5引入的Script运行能力
2026-04-03 09:00:08基础资料围观2次
大家好,我是Edison。
最近我一直在跟着圣杰的《.NET+AI智能体开发进阶》课程学习MAF开发多智能体工作流,我强烈推荐你也上车跟我一起出发!
上一篇,我们了解下.NET 10新推出的File-Based App模式,它和MAF一起可以形成一个强大的“王牌组合”。而就在这两天,MAF 1.0.0-rc5发布了,之前github demo中的agent skills demo也可以运行了!有了script执行能力,skill的拼图终于完整了,我也相信GA版本就快到来了!
1 RC5的新变化
在RC5之前,我们使用Skills的方式还主要是静态知识注入,一方面解决如何让Agent知道领域知识,另一方面让Agent通过渐进式披露策略降低Token消耗。而至于script能力,MAF一直为开放,而前面的内容,圣杰也给出了一个思路。但是到了RC5,官方实现来了,别激动,请接住!
Skill的定位变化
有了script的运行能力,skill从 静态知识包 开始走向 可执行能力包,其新增的接口 run_skill_script 就是来执行script的入口。
根据之前的了解,现在AgentSkillProvider就能有三个接口来构建Skill了:
-
load_skill
-
read_skill_resource
-
run_skill_script
因此,我们对Agent Skills的定义也会更加完善:
即 Agent Skill = 指令 + 资源 + 脚本 共同组成的可移植能力包
MAF Skills的4层架构
从MAF对Skills的实现来看,它做了较多的抽象 和 工程化,基本可以拆分为4层,如下图所示:

- 1. 对象层:定义 Skill 是什么
- 2. Source 层:定义 Skill 从哪里来
- 3. Decorator 层:定义 Skill 怎么过滤、去重、组合
- 4. Provider 层:定义 Skill 怎么进入 Agent 运行时
更多地解析推荐大家阅读圣杰的《MAF悄悄更新到RC5,Agent Skills的脚本运行能力Ready了》,这里我就不再重复了,下面我们具体看看DEMO。
Code as Skill : 代码定义Skill
在RC5中,支持在代码中定义Skill,而不再限制于目录中的markdown文档,在Source层,它叫做 AgentInMemorySkillsSource。下面也会给出一个例子展示这个代码定义Skill的模式。
2 快速开始:单位转换器
这里我们直接来看官方团队给出的案例:单位转换器。虽然,这个案例有点脱了裤子放屁的意思,但是足够简单和覆盖知识点。
我们希望创建一个单位转换的Agent,能够通过查询skill及其相关计算规则 和 运行脚本 回复用户关于单位转换的结果。
老规矩,我们先写Skill的内容:SKILL.md,references 和 scripts。
SKILL.md
首先,我们创建这个SKILL.md,内容如下:
--- name: unit-converter description: 使用乘法换算系数在常见单位之间进行转换。在需要将英里/公里、磅/千克等单位互相换算时使用。 --- ## 使用方法 当用户请求单位换算时: 1. 先查看 `references/conversion-table.md`,找到正确的换算系数 2. 使用 `--value <数值> --factor <系数>` 运行 `scripts/convert.py` 脚本(例如:`--value 26.2 --factor 1.60934`) 3. 输出内容需要清晰地展示换算系数、换算过程和换算结果,并同时标明换算前后的两个单位
reference: 转换公式表
其次,我们创建一个conversion-table.md,用于告诉Agent转换的系数和公式:
# Conversion Tables(换算表) Formula(公式): **result = value × factor(结果 = 数值 × 系数)** > Note(说明): > - `From` / `To` 列请保持英文(miles, kilometers, pounds, kilograms),便于在工具参数/代码中稳定引用。 > - 中文列仅用于阅读理解。 | From | To | Factor | From (中文) | To (中文) | |------------|------------|----------|------------ |----------| | miles | kilometers | 1.60934 | 英里 | 千米/公里 | | kilometers | miles | 0.621371 | 千米/公里 | 英里 | | pounds | kilograms | 0.453592 | 磅 | 千克/公斤 | | kilograms | pounds | 2.20462 | 千克/公斤 | 磅 |
scripts: 可执行脚本
这里我们编写了一个python脚本 convert.py 来做一个简单的运算,虽然它太简单了:
# 单位换算脚本 # 使用乘法系数对数值进行换算:result = value × factor # # 用法: # python scripts/convert.py --value 26.2 --factor 1.60934 # python scripts/convert.py --value 75 --factor 2.20462 import argparse import json def main() -> None: parser = argparse.ArgumentParser( description="Convert a value using a multiplication factor.", epilog="Examples:\n" " python scripts/convert.py --value 26.2 --factor 1.60934\n" " python scripts/convert.py --value 75 --factor 2.20462", formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument("--value", type=float, required=True, help="The numeric value to convert.") parser.add_argument("--factor", type=float, required=True, help="The conversion factor from the table.") args = parser.parse_args() result = round(args.value * args.factor, 4) print(json.dumps({"value": args.value, "factor": args.factor, "result": result})) if __name__ == "__main__": main()
自定义脚本执行器
这里,官方定义了一个ScriptRunner,它会通过一个本地进程来执行Skill中的脚本,也就是上面的 convert.py 代码脚本。
internal static class SubprocessScriptRunner { /// <summary> /// Runs a skill script as a local subprocess. /// </summary> public static async Task<object?> RunAsync( AgentFileSkill skill, AgentFileSkillScript script, AIFunctionArguments arguments, CancellationToken cancellationToken) { if (!File.Exists(script.FullPath)) { return $"Error: Script file not found: {script.FullPath}"; } string extension = Path.GetExtension(script.FullPath); string? interpreter = extension switch { ".py" => "python3", ".js" => "node", ".sh" => "bash", ".ps1" => "pwsh", _ => null, }; var startInfo = new ProcessStartInfo { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, WorkingDirectory = Path.GetDirectoryName(script.FullPath) ?? ".", }; if (interpreter is not null) { startInfo.FileName = interpreter; startInfo.ArgumentList.Add(script.FullPath); } else { startInfo.FileName = script.FullPath; } if (arguments is not null) { foreach (var (key, value) in arguments) { if (value is bool boolValue) { if (boolValue) { startInfo.ArgumentList.Add(NormalizeKey(key)); } } else if (value is not null) { startInfo.ArgumentList.Add(NormalizeKey(key)); startInfo.ArgumentList.Add(value.ToString()!); } } } Process? process = null; try { process = Process.Start(startInfo); if (process is null) { return $"Error: Failed to start process for script '{script.Name}'."; } Task<string> outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken); Task<string> errorTask = process.StandardError.ReadToEndAsync(cancellationToken); await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); string output = await outputTask.ConfigureAwait(false); string error = await errorTask.ConfigureAwait(false); if (!string.IsNullOrEmpty(error)) { output += $"\nStderr:\n{error}"; } if (process.ExitCode != 0) { output += $"\nScript exited with code {process.ExitCode}"; } return string.IsNullOrEmpty(output) ? "(no output)" : output.Trim(); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { // Kill the process on cancellation to avoid leaving orphaned subprocesses. process?.Kill(entireProcessTree: true); throw; } catch (OperationCanceledException) { throw; } catch (Exception ex) { return $"Error: Failed to execute script '{script.Name}': {ex.Message}"; } finally { process?.Dispose(); } } /// <summary> /// Normalizes a parameter key to a consistent --flag format. /// Models may return keys with or without leading dashes (e.g., "value" vs "--value"). /// </summary> private static string NormalizeKey(string key) => "--" + key.TrimStart('-'); }
可以看到,在该Runner中定义了一些基本的校验规则,然后就通过启动一个本地进程去执行脚本。
主文件C#代码
这里,我们还是一步一步来看:
Step1. 创建SkillsProvider
var skillsProvider = new AgentSkillsProvider( skillPath: Path.Combine(Directory.GetCurrentDirectory(), "skills"), scriptRunner: SubprocessScriptRunner.RunAsync); Console.WriteLine("✅ AgentSkillsProvider 创建成功"); Console.WriteLine("📂 自动注册工具: load_skill, read_skill_resource, run_skill_script"); Console.WriteLine();
Step2. 创建Agent:
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions { Name = "UnitConverterAgent", ChatOptions = new() { Instructions = "你是一个专业的AI助手,负责帮助用户实现单位的转换。", }, // 🔑 知识层:通过 AIContextProviders 注入 Skills AIContextProviders = [skillsProvider], }); Console.WriteLine("✅ Agent 创建成功"); Console.WriteLine();
Step3. 测试Agent:
var session = await agent.CreateSessionAsync(); Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); Console.WriteLine($"开始测试:基于 File-Based Skills"); // 中文问题:英里 -> 公里 var question1 = "马拉松比赛的距离26.2 英里是多少公里?"; Console.WriteLine($"👤 用户: {question1}"); Console.WriteLine(); var response1 = await agent.RunAsync(question1, session); Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); Console.WriteLine($"🤖 Agent: {response1.Text}"); Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); Console.WriteLine(); // 英文问题:磅 -> 千克 var question2 = "How many pounds is 75 kilograms?"; Console.WriteLine($"👤 用户: {question2}"); Console.WriteLine(); var response2 = await agent.RunAsync(question2, session); Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); Console.WriteLine($"🤖 Agent: {response2.Text}"); Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); Console.WriteLine();
测试效果如下图:

可以看到,无论是中文还是英文,它都严格按照skill中的要求,输出了换算系数、换算过程 和 最终结果。
3 快速开始:代码定义Skills
这里我们再来看看官方的第二个DEMO:通过代码定义Skills,这里就会用到 AgentInlineSkill 这个新增对象。
var unitConverterSkill = new AgentInlineSkill( name: "unit-converter", description: "使用乘法换算系数在常见单位之间进行转换。在需要将英里/公里、磅/千克等单位互相换算时使用。", instructions: """ 当用户请求单位换算时,请使用本 Skill。 1. 查看 `conversion-table` 资源,找到正确的换算系数。 2. 查看 `conversion-policy` 资源,了解取整和格式化规则。 3. 使用 `convert` 脚本,并传入从表中查到的数值和系数。 """) // 1. 静态资源: conversion tables .AddResource( "conversion-table", """ # 换算表 公式: **结果 = 数值 × 系数** | From | To | Factor | |-------------|-------------|----------| | miles | kilometers | 1.60934 | | kilometers | miles | 0.621371 | | pounds | kilograms | 0.453592 | | kilograms | pounds | 2.20462 | """) // 2. 动态资源: conversion policy (运行时计算) .AddResource("conversion-policy", () => { const int Precision = 4; return $""" # 换算策略 **小数位数:** {Precision} **格式:** 始终同时显示原始值、换算后值以及单位 **生成时间:** {DateTime.UtcNow:O} """; }) // 3. 代码脚本: convert by C# code .AddScript("convert", (double value, double factor) => { double result = Math.Round(value * factor, 4); return JsonSerializer.Serialize(new { value, factor, result }); }); var skillsProvider = new AgentSkillsProvider(unitConverterSkill);
可以看到,MAF释放的信号很明确:Skill不只是磁盘中的markdown文件!Code as Skill!
那么,这类型的动态skill适合哪些场景呢?
答案:它特别适合那些只存在于运行时的信息,比如:
- 当前租户配置;
- 当前区域;
- 实时配额;
- 当前系统状态;
- 动态生成的业务规则。
这些信息如果硬要放到磁盘中形成文件,反而不自然。
4 小结
本文介绍了RC5引入的令人激动的script执行能力,有了script执行能力,Agent Skill才变得更加完整,也从静态知识包 开始走向 可执行能力包,Agent Skill = 知识 + 资源 + 脚本。
由此也可见,MAF对Skill的理解也在不断地进化,开始往软件工程层面迈进!
不过,要真的走上生产环境,还需要在应用层面考虑安全性 和 扩展性方面的内容,毕竟企业级应用还是得控制风险!
示例源码
GitHub: https://github.com/EdisonTalk/MAFD
参考资料
圣杰,《.NET + AI 智能体开发进阶》(推荐指数:★★★★★)
Microsoft Learn,《Agent Framework Tutorials》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!
标签:
相关文章
最新发布
- .NET 高级开发 | i18n 原理、实现一个 i18n 框架
- MAF快速入门(22)声明式Agent实战
- AI新时代真的来了,不管愿意还是不愿意
- 多日努力,想法思维终成硕果。这是必须要被记住的一天。AGIASI宗师的地位,毋容置疑!方案有三种,每种都应该是可行的。世界的震动,巨震,我想起来,还是会有点害怕,,,
- AI 资讯日报 | 2026 年 04 月 08 日
- 都是微软亲儿子,WPF凭啥干不掉WinForm?这3个场景说明白了
- 到底是我在上班,还是 OpenClaw 在上班?
- Postgres - Listen/Notify构建轻量级发布订阅系统
- 代码之外周刊(第 172 期):自控力的真相:不是忍耐,是喜欢
- 干货!Ubuntu 快速部署 Cloudreve :打造全功能私有云盘

