MAF快速入门(19)给Agent Skill添加脚本执行能力

大家好,我是Edison。

最近我一直在跟着圣杰的《.NET+AI智能体开发进阶》课程学习MAF开发多智能体工作流,我强烈推荐你也上车跟我一起出发!

上一篇,我们初步学习了Agent Skill的基本概念以及在MAF中实现了一个Agent Skill的案例。本篇,我们来了解下Agent Skill如何集成脚本执行能力。

1 Agent Skill中的执行脚本

根据上一篇我们知道,Agent Skill就是大模型随时翻阅的说明文档,它还可以包含一些资源文件,而脚本就是其中的一种重要资源。

复制代码
skill/
├── SKILL.md    # 必需:指令 + 元数据
├── scripts/    # 可选:可执行代码
├── references/ # 可选:文档资料
└── assets/     # 可选:模板、资源文件

scripts子目录

这些脚本会被放在其所属skill目录下的scripts子目录下,不管它是BASH脚本,PowerShell脚本,又或者是Python/C#代码,只要是Agent所在客户/服务端能够执行的都可以。

总结,Skills不仅让Agent知道处理规则(SKILL.md + references),还能让Agent直接上手处理(scripts)

工具 vs 技能

工具(Tool)是一种能力,而技能(Skill)是一种知识。比如,自己开发一个PowerShellTools,给模型提供一些run shell的能力,我们通过注册到 ChatOptions.Tools 完成加载。

又如,自己定义一个系统运维Skill,给模型提供一些系统运维的知识,我们通过注册到AIContextProviders完成加载。

知识指导行为,而非工具限定行为。

2 快速开始:MAF中集成Shell执行能力

假设我们有这样一个需求:某企业开发了一个IT助手Agent,它运行在每个员工的笔记本电脑上,可以回答用户关于系统方面的一些问题并加以分析给出建议。

例如,很多员工发现自己的笔记本电脑最近越来越慢了,又或者发现内存一直高负载 等等,在以前员工会直接联系IT部的某位员工,而现在IT部将自身的运维经验封装为了一个Skill并通过Agent直接回复员工。

以终为始,我们的Agent应用的解决方案长这样子:这是一个.NET控制台应用程序,其中有一个skills目录,存放了系统运维领域的知识,这是IT部呕心沥血整理的"宝典"。

系统运维Skill

首先,我们创建这个SKILL.md,内容如下:

复制代码
---
name: system-ops
description: 系统运维诊断技能。适用于系统健康检查、磁盘空间分析、进程资源监控、故障排查等系统运维场景。包含可执行的诊断脚本。
---

# 系统运维(System Operations)

## 可用诊断脚本

以下脚本位于本技能的 `scripts/` 目录,可通过 `run_shell` 工具执行:

| 脚本 | 用途 | 执行命令 |
|------|------|--------|
| check-system-info.ps1 | 获取系统基本信息(OS、CPU、内存) | 
| check-disk-usage.ps1 | 检查磁盘使用情况和剩余空间 | 
| check-top-processes.ps1 | 查看 CPU/内存占用 Top 进程 |

## 运维检查流程

1. **基础检查**:先执行 `check-system-info.ps1` 获取系统概况
2. **针对性诊断**:根据用户问题,选择性执行磁盘或进程检查脚本
3. **分析报告**:综合脚本输出和故障排查指南,给出诊断结论和建议
4. **故障排查**:如需深入排查,参考 [references/troubleshooting-guide.md](references/troubleshooting-guide.md)

## 告警阈值

| 指标 | 正常 | 警告 | 严重 |
|------|------|------|------|
| CPU 使用率 | <70% | 70-90% | >90% |
| 内存使用率 | <80% | 80-95% | >95% |
| 磁盘使用率 | <70% | 70-90% | >90% |
| 单进程 CPU | <30% | 30-60% | >60% |

由于该Skill还定义了引用的资源,所以我们还需要添加:

(1)参考文件:故障自查指南

说明:这是一个markdown文件。

复制代码
# 故障排查指南

## CPU 持续高负载

1. 通过 `check-top-processes.ps1` 识别高占用进程
2. 判断是否为预期行为(如编译、数据处理)
3. 如为异常进程,建议:
   - 记录进程信息(PID、启动时间、命令行)
   - 联系应用负责人确认
   - 必要时可终止进程:`Stop-Process -Id <PID>`

## 内存不足

1. 检查物理内存使用率(通过 `check-system-info.ps1`)
2. 识别内存大户(通过 `check-top-processes.ps1`)
3. 常见原因:
   - 内存泄漏:进程内存持续增长
   - 缓存过多:数据库或应用缓存未限制
4. 缓解措施:
   - 重启内存泄漏进程
   - 调整应用缓存配置
   - 考虑扩容

## 磁盘空间不足

1. 通过 `check-disk-usage.ps1` 确认各盘使用率
2. 清理建议(按优先级):
   - 清理临时文件
   - 清理日志文件(保留近 7 天)
   - 清理 NuGet 包缓存:`dotnet nuget locals all --clear`
3. 长期方案:配置日志轮转、扩容磁盘

(2)脚本文件:PowerShell脚本

说明:这里主要在Windows系统中执行脚本,所以选择了PowerShell。

脚本1:获取系统基本信息

复制代码
# check-system-info.ps1 — 获取系统基本信息
Write-Host "=== 系统基本信息 ==="
Write-Host "计算机名: $env:COMPUTERNAME"
Write-Host "操作系统: $([System.Runtime.InteropServices.RuntimeInformation]::OSDescription)"
Write-Host "处理器架构: $([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)"
Write-Host "逻辑处理器数: $([Environment]::ProcessorCount)"

$os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue
if ($os) {
    $totalMemGB = [math]::Round($os.TotalVisibleMemorySize / 1MB, 2)
    $freeMemGB = [math]::Round($os.FreePhysicalMemory / 1MB, 2)
    $usedMemGB = [math]::Round($totalMemGB - $freeMemGB, 2)
    $memUsagePercent = [math]::Round(($usedMemGB / $totalMemGB) * 100, 1)
    Write-Host ""
    Write-Host "=== 内存信息 ==="
    Write-Host "总内存: ${totalMemGB} GB"
    Write-Host "已使用: ${usedMemGB} GB ($memUsagePercent%)"
    Write-Host "可用: ${freeMemGB} GB"
}

Write-Host ""
Write-Host "=== 系统运行时间 ==="
$uptime = (Get-Date) - (Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime
Write-Host "已运行: $($uptime.Days) 天 $($uptime.Hours) 小时 $($uptime.Minutes) 分钟"

脚本2:检查系统磁盘使用信息

复制代码
# check-disk-usage.ps1 — 检查磁盘使用情况
Write-Host "=== 磁盘使用情况 ==="
Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" | ForEach-Object {
    $totalGB = [math]::Round($_.Size / 1GB, 2)
    $freeGB = [math]::Round($_.FreeSpace / 1GB, 2)
    $usedGB = [math]::Round($totalGB - $freeGB, 2)
    $usagePercent = if ($totalGB -gt 0) { [math]::Round(($usedGB / $totalGB) * 100, 1) } else { 0 }

    $status = if ($usagePercent -gt 90) { "🔴 严重" }
              elseif ($usagePercent -gt 70) { "🟡 警告" }
              else { "🟢 正常" }

    Write-Host ""
    Write-Host "驱动器 $($_.DeviceID)"
    Write-Host "  总容量: ${totalGB} GB"
    Write-Host "  已使用: ${usedGB} GB ($usagePercent%)"
    Write-Host "  可用: ${freeGB} GB"
    Write-Host "  状态: $status"
}

脚本3:查看资源占用Top10信息

复制代码
# check-top-processes.ps1 — 查看资源占用 Top 进程
param(
    [int]$Top = 10
)
Write-Host "=== CPU 占用 Top $Top 进程 ==="
Get-Process | Sort-Object CPU -Descending | Select-Object -First $Top | 
    Format-Table -Property @{N='进程名';E={$_.ProcessName}}, 
                           @{N='PID';E={$_.Id}}, 
                           @{N='CPU(s)';E={[math]::Round($_.CPU, 2)}}, 
                           @{N='内存(MB)';E={[math]::Round($_.WorkingSet64/1MB, 1)}} -AutoSize |
    Out-String | Write-Host
Write-Host ""
Write-Host "=== 内存占用 Top $Top 进程 ==="
Get-Process | Sort-Object WorkingSet64 -Descending | Select-Object -First $Top |
    Format-Table -Property @{N='进程名';E={$_.ProcessName}}, 
                           @{N='PID';E={$_.Id}}, 
                           @{N='内存(MB)';E={[math]::Round($_.WorkingSet64/1MB, 1)}},
                           @{N='CPU(s)';E={[math]::Round($_.CPU, 2)}} -AutoSize |
    Out-String | Write-Host

自定义ShellTools

最近MAF发布了1.0.0-rc2,支持了Agent Skill,但其尚未开放原生的脚本执行能力,虽然在GitHub的demo中我已经看到了脚本执行的demo。但貌似已经revert了,因此我们再静静等候一阵。

这里,我们通过自定义一个ShellTools来实现PowerShell的脚本执行能力:

复制代码
public class ShellTools
{
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // run_shell --- 一个 Shell 工具做一切(含安全护栏)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

    // 🛡️ 安全护栏 1:危险命令黑名单
    private static string[] dangerousPatterns = [
        "rm -rf /", "rm -rf /*",       // 删除根目录
        "sudo ",                        // 提权
        "shutdown", "reboot",           // 系统操作
        "> /dev/",                      // 设备写入
        ":(){ :|:& };:",                // Fork bomb
        "mkfs.",                        // 格式化
        "dd if=",                       // 磁盘覆写
        "format ",                      // Windows 格式化
        "del /f /s /q",                 // Windows 递归删除
    ];

    [Description("执行 Shell 命令。通过操作系统原生 Shell 执行命令(Windows 用 cmd,Linux/Mac 用 bash)。包含安全护栏:危险命令阻止、输出截断(50KB)、超时控制(60秒)。")]
    public static string RunShell(
        [Description("要执行的 Shell 命令。例如:'pwsh -File /path/to/script.ps1' 或 'dir'")] string command,
        [Description("命令执行的工作目录(可选)。如果不指定,使用当前目录。")] string? workingDirectory = null)
    {
        try
        {
            // 🛡️ 安全护栏 1:危险命令检查
            if (dangerousPatterns.Any(d => command.Contains(d, StringComparison.OrdinalIgnoreCase)))
            {
                return "❌ 安全拦截:检测到危险命令,已阻止执行。";
            }

            // 🔑 跨平台 Shell 适配:
            //   Windows → cmd /c "command"   (原生命令提示符)
            //   Linux/Mac → bash -c "command" (原生 Bash)
            //
            // 为什么不直接用 pwsh?
            //   SKILL.md 中的命令已经是 "pwsh -File ..."
            //   如果 RunShell 也用 pwsh -Command 包裹 → pwsh 套 pwsh(冗余嵌套)
            //   用原生 shell 分发 → pwsh -File 直接执行,零嵌套
            var isWindows = OperatingSystem.IsWindows();
            var processInfo = new ProcessStartInfo
            {
                FileName = isWindows ? "cmd" : "bash",
                Arguments = isWindows ? $"/c {command}" : $"-c \"{command.Replace("\"", "\\\"")}\"",
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                CreateNoWindow = true
            };

            // 设置工作目录
            if (!string.IsNullOrWhiteSpace(workingDirectory) && Directory.Exists(workingDirectory))
            {
                processInfo.WorkingDirectory = workingDirectory;
            }

            using var process = Process.Start(processInfo);
            if (process == null)
            {
                return "❌ 无法启动 Shell 进程";
            }

            var stdout = process.StandardOutput.ReadToEnd();
            var stderr = process.StandardError.ReadToEnd();

            // 🛡️ 安全护栏 3:超时控制(60秒)
            if (!process.WaitForExit(60_000))
            {
                process.Kill(entireProcessTree: true);
                return "❌ 命令执行超时(60秒),已强制终止。";
            }

            var result = new StringBuilder();
            if (!string.IsNullOrWhiteSpace(stdout))
            {
                result.AppendLine(stdout.Trim());
            }
            if (!string.IsNullOrWhiteSpace(stderr))
            {
                result.AppendLine($"⚠️ stderr: {stderr.Trim()}");
            }
            if (process.ExitCode != 0)
            {
                result.AppendLine($"⚠️ 退出码: {process.ExitCode}");
            }

            var output = result.Length > 0 ? result.ToString() : "(命令执行成功,无输出)";

            // 🛡️ 安全护栏 2:输出截断(50KB)
            const int maxOutputLength = 50_000;
            if (output.Length > maxOutputLength)
            {
                output = output[..maxOutputLength] + "\n... (输出已截断,超过 50KB 上限)";
            }

            return output;
        }
        catch (Exception ex)
        {
            return $"❌ 执行失败: {ex.Message}";
        }
    }
}

这个ShellTools还考虑了必要的安全性,对于一些危险命令,会主动进行阻止,还对超时60s的操作进行了命令挂起。

智能体调用Agent Skill

这里,让我们一步一步来实现在MAF中让智能体调用集成脚本执行的Skill。

(1)创建SkillsProvider:从文件中发现和加载Skills

复制代码
var skillsProvider = new FileAgentSkillsProvider(
    skillPath: Path.Combine(Directory.GetCurrentDirectory(), "skills"),
    options: new FileAgentSkillsProviderOptions
    {
        // 🔑 自定义提示词:引导模型加载技能后使用 run_shell 执行脚本
        SkillsInstructionPrompt = """
        你可以使用以下技能获取领域知识和操作指引。
        每个技能提供专业指令、参考文档和可执行脚本。

        <available_skills>
        {0}
        </available_skills>

        工作流程:
        1. 当用户任务匹配技能描述时,使用 `load_skill` 加载该技能的完整指令
        2. 技能指令中会标明可用脚本及其执行命令
        3. 使用 `run_shell` 工具执行技能中标注的命令
        4. 需要时使用 `read_skill_resource` 读取参考资料

        重要原则:先加载知识,再执行操作。
        """
    }
);
Console.WriteLine("✅ FileAgentSkillsProvider 创建成功(知识层)");
Console.WriteLine("📂 自动注册工具: load_skill, read_skill_resource");

(2)创建Agent:注入SkillsProvider让其有Skill调用能力

复制代码
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
    Name = "SkillsBashAgent",
    ChatOptions = new()
    {
        Instructions = "你是一个专业的系统运维助手。请用中文回答所有问题。",
        // 🔑 能力层:仅注册一个 run_shell 工具
        Tools = [AIFunctionFactory.Create(ShellTools.RunShell)],
    },
    // 🔑 知识层:通过 AIContextProviders 注入 Skills
    AIContextProviders = [skillsProvider],
});
Console.WriteLine("✅ 技能 Agent 创建成功");

下面,我们就来测试一下这个Agent:

测试用例1

用户:帮我检查一下当前系统的整体健康状态,包括 CPU、内存和磁盘使用情况。

复制代码
Agent 会:
1. 识别属于 system-ops 领域 → load_skill("system-ops")
2. 从 SKILL.md 获取脚本执行命令
3. 依次调用 run_shell 执行诊断脚本
4. 根据告警阈值分析结果

测试代码如下:

复制代码
var session = await agent.CreateSessionAsync();
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("   🔍 测试 1:系统健康检查");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine();
var question1 = "帮我检查一下当前系统的整体健康状态,包括 CPU、内存和磁盘使用情况。";
Console.WriteLine($"👤 用户: {question1}");
Console.WriteLine();
var response1 = await agent.RunAsync(question1, session);
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine($"🤖 Agent: {response1.Text}");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine();
Console.WriteLine();

测试结果如下图所示:

测试用例2

危险脚本操作命令,验证Agent不会执行这些命令。

复制代码
var dangerousTestCases = new[]
{
    ("rm -rf /", "删除根目录"),
    ("sudo apt-get install malware", "提权操作"),
    ("shutdown -s -t 0", "关机命令"),
};
foreach (var (cmd, desc) in dangerousTestCases)
{
    var result = ShellTools.RunShell(cmd);
    Console.WriteLine($"❌ 测试: {desc}");
    Console.WriteLine($"   命令: {cmd}");
    Console.WriteLine($"   结果: {result}");
    Console.WriteLine();
}
// 测试正常命令
var normalResult = ShellTools.RunShell("Get-Date");
Console.WriteLine($"✅ 测试: 正常命令");
Console.WriteLine($"   命令: Get-Date");
Console.WriteLine($"   结果: {normalResult.Trim()}");
Console.WriteLine();
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("🛡️ 安全护栏验证完成:危险命令被正确拦截,正常命令正常执行。");

测试结果如下图所示:

可以看到,安全防护栏正常工作,防止危险命令被Agent执行。

3 小结

本文介绍了Agent Skill的脚本执行,脚本 和 工具的对比,最后通过一个MAF中Agent Skill 结合 PowerShell脚本执行 的系统运维案例介绍如何集成脚本执行,相信会对你有所帮助。

示例源码

GitHub: https://github.com/EdisonTalk/MAFD

参考资料

圣杰,《.NET + AI 智能体开发进阶》(推荐指数:★★★★★)

Microsoft Learn,《Agent Framework Tutorials


作者:爱迪生

出处:https://edisontalk.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

相关推荐
TechFind2 小时前
我用 OpenClaw 搭了一套运营 Agent,每天自动生产内容、分发、追踪数据——独立开发者的运营平替
人工智能·agent
jerrywus3 小时前
Claude Code vs. Codex:终极指南
chatgpt·agent·claude
草帽lufei3 小时前
再见小龙虾(OpenClaw),卸载指南
agent
Tzarevich4 小时前
从零手写一个“迷你版 Cursor”:让 AI 真正帮你写代码
langchain·node.js·agent
吴佳浩5 小时前
什么是算力?
人工智能·pytorch·llm
掉头发的王富贵5 小时前
OpenClaw的本地安装和快速使用
程序员·openai·agent
回家路上绕了弯5 小时前
OpenClaw 本地 AI 智能体全解析
后端·agent
charlex5 小时前
【陈同学】走进 AI Agent:从“对话框”到“自主智能体”
人工智能·agent
爱可生开源社区5 小时前
🧪 你的大模型实验室开张啦!亲手测出最懂你 SQL 的 AI
数据库·sql·llm