大家好,我是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
[code]var skillsProvider = new AgentSkillsProvider( skillPath: Path.Combine(Directory.GetCurrentDirectory(), "skills"), scriptRunner: SubprocessScriptRunner.RunAsync);Console.WriteLine("✅ AgentSkillsProvider 创建成功");Console.WriteLine("
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |