找回密码
 立即注册
首页 业界区 业界 MAF快速入门(21)RC5引入的Script运行能力 ...

MAF快速入门(21)RC5引入的Script运行能力

布相 5 小时前
大家好,我是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.png


  • 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,内容如下:
  1. ---
  2. name: unit-converter
  3. description: 使用乘法换算系数在常见单位之间进行转换。在需要将英里/公里、磅/千克等单位互相换算时使用。
  4. ---
  5. ## 使用方法
  6. 当用户请求单位换算时:
  7. 1. 先查看 `references/conversion-table.md`,找到正确的换算系数
  8. 2. 使用 `--value <数值> --factor <系数>` 运行 `scripts/convert.py` 脚本(例如:`--value 26.2 --factor 1.60934`)
  9. 3. 输出内容需要清晰地展示换算系数、换算过程和换算结果,并同时标明换算前后的两个单位
复制代码
reference: 转换公式表

其次,我们创建一个conversion-table.md,用于告诉Agent转换的系数和公式:
  1. # Conversion Tables(换算表)
  2. Formula(公式): **result = value × factor(结果 = 数值 × 系数)**
  3. > Note(说明):
  4. > - `From` / `To` 列请保持英文(miles, kilometers, pounds, kilograms),便于在工具参数/代码中稳定引用。
  5. > - 中文列仅用于阅读理解。
  6. | From       | To         | Factor   | From (中文) | To (中文) |
  7. |------------|------------|----------|------------ |----------|
  8. | miles      | kilometers | 1.60934  | 英里        | 千米/公里 |
  9. | kilometers | miles      | 0.621371 | 千米/公里   | 英里     |
  10. | pounds     | kilograms  | 0.453592 | 磅          | 千克/公斤 |
  11. | kilograms  | pounds     | 2.20462  | 千克/公斤   | 磅       |
复制代码
scripts: 可执行脚本

这里我们编写了一个python脚本 convert.py 来做一个简单的运算,虽然它太简单了:
  1. # 单位换算脚本
  2. # 使用乘法系数对数值进行换算:result = value × factor
  3. #
  4. # 用法:
  5. #   python scripts/convert.py --value 26.2 --factor 1.60934
  6. #   python scripts/convert.py --value 75 --factor 2.20462
  7. import argparse
  8. import json
  9. def main() -> None:
  10.     parser = argparse.ArgumentParser(
  11.         description="Convert a value using a multiplication factor.",
  12.         epilog="Examples:\n"
  13.         "  python scripts/convert.py --value 26.2 --factor 1.60934\n"
  14.         "  python scripts/convert.py --value 75 --factor 2.20462",
  15.         formatter_class=argparse.RawDescriptionHelpFormatter,
  16.     )
  17.     parser.add_argument("--value", type=float, required=True, help="The numeric value to convert.")
  18.     parser.add_argument("--factor", type=float, required=True, help="The conversion factor from the table.")
  19.     args = parser.parse_args()
  20.     result = round(args.value * args.factor, 4)
  21.     print(json.dumps({"value": args.value, "factor": args.factor, "result": result}))
  22. if __name__ == "__main__":
  23.     main()
复制代码
自定义脚本执行器

这里,官方定义了一个ScriptRunner,它会通过一个本地进程来执行Skill中的脚本,也就是上面的 convert.py 代码脚本。
  1. internal static class SubprocessScriptRunner
  2. {
  3.     /// <summary>
  4.     /// Runs a skill script as a local subprocess.
  5.     /// </summary>
  6.     public static async Task<object?> RunAsync(
  7.         AgentFileSkill skill,
  8.         AgentFileSkillScript script,
  9.         AIFunctionArguments arguments,
  10.         CancellationToken cancellationToken)
  11.     {
  12.         if (!File.Exists(script.FullPath))
  13.         {
  14.             return $"Error: Script file not found: {script.FullPath}";
  15.         }
  16.         string extension = Path.GetExtension(script.FullPath);
  17.         string? interpreter = extension switch
  18.         {
  19.             ".py" => "python3",
  20.             ".js" => "node",
  21.             ".sh" => "bash",
  22.             ".ps1" => "pwsh",
  23.             _ => null,
  24.         };
  25.         var startInfo = new ProcessStartInfo
  26.         {
  27.             RedirectStandardOutput = true,
  28.             RedirectStandardError = true,
  29.             UseShellExecute = false,
  30.             CreateNoWindow = true,
  31.             WorkingDirectory = Path.GetDirectoryName(script.FullPath) ?? ".",
  32.         };
  33.         if (interpreter is not null)
  34.         {
  35.             startInfo.FileName = interpreter;
  36.             startInfo.ArgumentList.Add(script.FullPath);
  37.         }
  38.         else
  39.         {
  40.             startInfo.FileName = script.FullPath;
  41.         }
  42.         if (arguments is not null)
  43.         {
  44.             foreach (var (key, value) in arguments)
  45.             {
  46.                 if (value is bool boolValue)
  47.                 {
  48.                     if (boolValue)
  49.                     {
  50.                         startInfo.ArgumentList.Add(NormalizeKey(key));
  51.                     }
  52.                 }
  53.                 else if (value is not null)
  54.                 {
  55.                     startInfo.ArgumentList.Add(NormalizeKey(key));
  56.                     startInfo.ArgumentList.Add(value.ToString()!);
  57.                 }
  58.             }
  59.         }
  60.         Process? process = null;
  61.         try
  62.         {
  63.             process = Process.Start(startInfo);
  64.             if (process is null)
  65.             {
  66.                 return $"Error: Failed to start process for script '{script.Name}'.";
  67.             }
  68.             Task<string> outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
  69.             Task<string> errorTask = process.StandardError.ReadToEndAsync(cancellationToken);
  70.             await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
  71.             string output = await outputTask.ConfigureAwait(false);
  72.             string error = await errorTask.ConfigureAwait(false);
  73.             if (!string.IsNullOrEmpty(error))
  74.             {
  75.                 output += $"\nStderr:\n{error}";
  76.             }
  77.             if (process.ExitCode != 0)
  78.             {
  79.                 output += $"\nScript exited with code {process.ExitCode}";
  80.             }
  81.             return string.IsNullOrEmpty(output) ? "(no output)" : output.Trim();
  82.         }
  83.         catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
  84.         {
  85.             // Kill the process on cancellation to avoid leaving orphaned subprocesses.
  86.             process?.Kill(entireProcessTree: true);
  87.             throw;
  88.         }
  89.         catch (OperationCanceledException)
  90.         {
  91.             throw;
  92.         }
  93.         catch (Exception ex)
  94.         {
  95.             return $"Error: Failed to execute script '{script.Name}': {ex.Message}";
  96.         }
  97.         finally
  98.         {
  99.             process?.Dispose();
  100.         }
  101.     }
  102.     /// <summary>
  103.     /// Normalizes a parameter key to a consistent --flag format.
  104.     /// Models may return keys with or without leading dashes (e.g., "value" vs "--value").
  105.     /// </summary>
  106.     private static string NormalizeKey(string key) => "--" + key.TrimStart('-');
  107. }
复制代码
可以看到,在该Runner中定义了一些基本的校验规则,然后就通过启动一个本地进程去执行脚本。
主文件C#代码

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

相关推荐

您需要登录后才可以回帖 登录 | 立即注册