Skills

第九章介绍了 Microsoft Agent Framework 的基本原理,而 Skill 只是在它之上多套一层,把一个领域能力的说明书 + 参考资料 + 可执行动作打包成整体交给模型,模型照着说明书走,不用自己编排。

不过开发者本机的 skill 可能几十上百个,每个完整内容都很长,全塞进系统提示会 token 爆炸、注意力稀释。所以 MAF 框架设计了一个 AgentSkillsProvider,使得 skill 不像工具那样一次性全发给模型,而是用渐进式披露(progressive disclosure)

  • 模型一开始只看到每个 skill 的名字和一句话描述
  • 判断要用时,调 load_skill 工具取完整说明书
  • 说明书引用的资料/脚本,再调 read_skill_resource / run_skill_script 按需取用。
正在渲染 Mermaid 图表...


记住四点:

① skill 四种来源,统一抽象成 AgentSkill

AgentSkillsProvider 本质是个 AIContextProvider,每次 run 注入目录 + 三个工具;

③ 三个工具都是 AIFunction,包了 ApprovalRequiredAIFunction(默认要审批);

④ 加载是条装饰器流水线。


加载 skill 并跟模型对话

skill 有四种来源(文件、代码、类、MCP),但接到 agent 上的方式是统一的:把 skill 包成一个 AgentSkillsProvider(它本身是个 AIContextProvider),放进 ChatClientAgentOptions.AIContextProviders

一般来说,文件 skill 会安装到用户的 ~/.agents/skills 目录,每个子目录就是一个 skill:

image-20260724104955755


把整个目录加载进来,再塞给 Agent,就能对话了:

string skillsRoot = Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
    ".agents", "skills");

var skillsProvider = new AgentSkillsProvider(skillsRoot);

IChatClient chatClient = new OpenAIClient(
    credential: new ApiKeyCredential(key: "1234"),
    options: new OpenAIClientOptions { Endpoint = new Uri("http://127.0.0.1:1234/v1") })
    .GetChatClient(model: "qwen/qwen3.5-9b")
    .AsIChatClient();

AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
    ChatOptions = new() { Instructions = "你是一个智能助手." },
    AIContextProviders = [skillsProvider],
});

Console.WriteLine(await agent.RunAsync("列出所有技能"));
Console.WriteLine();

image-20260724112725579

注意:上面加载的 skill 只有指令和资料、没有脚本,所以构造函数第二个参数(脚本运行器)省略了。一旦 skill 含脚本(scripts/*.py 之类),就必须传脚本运行器,否则模型调 run_skill_script 时会因没有 runner 报错。下一节演示带脚本的完整链路。


实际上首次请求时,AgentSkillsProvider 只会给大模型提供 load_skill、read_skill_resource、run_skill_script 三个工具,消耗的 token 极少,只有大模型觉得需要加载 skill 列表时,AgentSkillsProvider 才会读取这些 skill 的信息,实现动态加载。


使用 skill 并执行脚本

上一节只是让模型看" skill 目录、读读说明书。要真正执行 skill 里的脚本,需要两样东西:

① skill 带脚本(如 scripts/convert.py);

② 提供一个脚本运行器委托。


先看一个最小 skill 的目录结构(skills/unit-converter/):

image-20260724151916106


skills/unit-converter/
├── SKILL.md                 # name: unit-converter,说明怎么用
└── scripts/
    └── convert.py           # 实际干活的脚本:python convert.py --value 26.2 --factor 1.60934
# scripts/convert.py , 读命令行参数,算乘法,输出 JSON
import argparse, json
parser = argparse.ArgumentParser()
parser.add_argument("--value", type=float, required=True)
parser.add_argument("--factor", type=float, required=True)
args = parser.parse_args()
print(json.dumps({"value": args.value, "factor": args.factor, "result": round(args.value * args.factor, 4)}))

MAF 不带任何脚本解释器,文件脚本的执行完全委托给你提供的 AgentFileSkillScriptRunner 委托。下面这个实现按扩展名选解释器、起子进程执行。

也就是说,如果需要执行 unit-converter 这个 skill 里面的 .py 脚本,需要我们实现一个脚本执行器。


public static class SubprocessScriptRunner
{
    public static async Task<object?> RunAsync(
        AgentFileSkill skill, AgentFileSkillScript script, JsonElement? arguments,
        IServiceProvider? serviceProvider, CancellationToken cancellationToken)
    {
        // 按扩展名选解释器
        string? interpreter = Path.GetExtension(script.FullPath) switch
        {
            ".py" => "python", ".js" => "node", ".sh" => "bash", ".ps1" => "pwsh", _ => null,
        };

        var startInfo = new ProcessStartInfo
        {
            FileName = interpreter ?? script.FullPath,
            RedirectStandardOutput = true, RedirectStandardError = true,
            UseShellExecute = false, CreateNoWindow = true,
            WorkingDirectory = Path.GetDirectoryName(script.FullPath)!,
        };
        if (interpreter is not null) startInfo.ArgumentList.Add(script.FullPath);

        // 模型传来的 arguments 是 JSON 字符串数组,逐个转成命令行参数
        if (arguments is { ValueKind: JsonValueKind.Array } arr)
            foreach (var e in arr.EnumerateArray())
                startInfo.ArgumentList.Add(e.GetString()!);

        using var process = new Process { StartInfo = startInfo };
        process.Start();
        Task<string> outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
        Task<string> errorTask = process.StandardError.ReadToEndAsync(cancellationToken);
        await process.WaitForExitAsync(cancellationToken);

        string output = await outputTask, error = await errorTask;
        if (!string.IsNullOrEmpty(error)) output += $"\n[stderr]\n{error}";
        if (process.ExitCode != 0) output += $"\n[退出码 {process.ExitCode}]";
        return string.IsNullOrWhiteSpace(output) ? "(无输出)" : output.Trim();
    }
}

有了 runner,加载时把它传给 provider,就能端到端跑通了。

这里的案例完全放通脚本执行权限,后续我们可以按需审批。

// skill 含脚本,必须传脚本运行器
var skillsProvider = new AgentSkillsProvider(
    Path.Combine(AppContext.BaseDirectory, "skills"),
    SubprocessScriptRunner.RunAsync);

IChatClient chatClient = new OpenAIClient(
    credential: new ApiKeyCredential(key: "1234"),
    options: new OpenAIClientOptions { Endpoint = new Uri("http://127.0.0.1:1234/v1") })
    .GetChatClient(model: "qwen/qwen3.5-9b")
    .AsIChatClient();

AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
    ChatOptions = new() { Instructions = "你是一个智能助手." },
    AIContextProviders = [skillsProvider],
})
    .AsBuilder()
    .UseToolApproval(new ToolApprovalAgentOptions
    {
        // 全放行,让模型能直接 load_skill 和 run_skill_script
        AutoApprovalRules = [AgentSkillsProvider.AllToolsAutoApprovalRule],
    })
    .Build();

AgentResponse response = await agent.RunAsync("一个马拉松是 26.2 英里,等于多少公里?");
foreach (var msg in response.Messages)
    foreach (var c in msg.Contents)
    {
        if (c is FunctionCallContent fcc)
            Console.WriteLine($">> 调用工具 {fcc.Name}({string.Join(", ", fcc.Arguments?.Select(kv => $"{kv.Key}={kv.Value}") ?? [])})");
        else if (c is FunctionResultContent frc)
            Console.WriteLine($"<< 工具结果: {frc.Result}");
    }
Console.WriteLine(response.Text);

本地模型兼容:上面的 demo 直接对接 OpenAI / Azure OpenAI 时能跑通。但是笔者接入的是本地的 LM Studio 本地模型引擎,它们的模型 chat template 通常要求每条请求至少有一条 user 消息,而函数执行引擎在工具调用循环的后续轮次发出的 messages 只有 assistant(tool_call) + tool(result),没有 user,本地模型会报 No user query found in messages.(HTTP 400)。官方云服务能容忍,本地引擎不能。

解决办法是给 IChatClient 套一个补 user 消息的装饰器(记住第一次的 user 消息,缺了就补回去)。这个装饰器和 skill 无关,是本地模型的通用兼容处理,用云服务时不需要:

public sealed class EnsureUserMessageChatClient(IChatClient inner) : DelegatingChatClient(inner)
{
    private string? _lastUserText;

    public override Task<ChatResponse> GetResponseAsync(
        IEnumerable<ChatMessage> messages, ChatOptions? options, CancellationToken ct = default)
        => InnerClient.GetResponseAsync(EnsureUser(messages), options, ct);

    private IEnumerable<ChatMessage> EnsureUser(IEnumerable<ChatMessage> messages)
    {
        var list = messages as List<ChatMessage> ?? messages.ToList();
        if (list.FirstOrDefault(m => m.Role == ChatRole.User) is { Text: var t and not null and not "" } u)
            _lastUserText = t;
        if (list.Count > 0 && !list.Any(m => m.Role == ChatRole.User) && _lastUserText is not null)
            list.Insert(0, new ChatMessage(ChatRole.User, _lastUserText));
        return list;
    }
}
// 用法:IChatClient chatClient = new EnsureUserMessageChatClient(openAiChatClient);

第一轮运行时,会注入一些系统提示词和 load_skill、read_skill_resource、run_skill_script 三个工具。可以看到,MAF 框架会自动在 System 角色内容里面注入一段提示词。并且第一轮对话时不会默认推送这些 Skills,而是由 AI 决定要不要读取 skills 列表。

下面的请求结构已被简化。

{
  "messages": [
    {
      "role": "system",
      "content": "你是一个智能助手.\nYou have access to skills containing dom... <Truncated in logs> ...sted.\nOnly load what is needed, when it is needed."
    },
    {
      "role": "user",
      "content": "一个马拉松是 26.2 英里,等于多少公里?"
    }
  ],
  "model": "qwen/qwen3.5-9b",
  "tools": [
    {
      "type": "function",
      "function": {
        "description": "Loads the full content of a specific skill",
        "name": "load_skill",
      }
    },
    {
      "type": "function",
      "function": {
        "description": "Reads a resource associated with a skill, such as references, assets, or dynamic data.",
        "name": "read_skill_resource",
      }
    },
    {
      "type": "function",
      "function": {
        "description": "Runs a script associated with a skill.",
        "name": "run_skill_script"
    }
  ],
  "tool_choice": "auto"
}

第一轮 AI 要求调用 load_skill 获取本地 skills 列表。

2026-07-24 15:31:23  [INFO]
 [qwen/qwen3.5-9b] Start to generate a tool call...
2026-07-24 15:31:23  [INFO]
 [qwen/qwen3.5-9b] Tool name generated:  load_skill

然后 AI 模型确认本地的 Skills 后,单独加载 unit-converter

 {
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "",
        "reasoning_content": "用户想知道将26.2英里换算成公里。这是一个单位换算任务。\n\n根据可用技能,有一个 `unit-converter` 技能用于单位换算。我需要:\n1. 首先使用 `load_skill` 来获取该技能的说明和指引\n2. 然后可能使用 `read_skill_resource` 读取相关的资源\n3. 如果需要使用脚本运行转换,则使用 `run_skill_script`\n\n让我先加载这个技能来了解具体的使用说明。\n",
        "tool_calls": [
          {
            "type": "function",
            "id": "z75mwila62QwDpJCBkAz5XnmOuKE7OTa",
            "function": {
              "name": "load_skill",
              "arguments": "{\"skillName\":\"unit-converter\"}"
            }
          }
        ]
      }
    }
  ]
}

下一轮对话中,Agent 加载该技能并把技能信息推送给 AI 模型。

{
  "messages": [
    {
      "role": "system",
      "content": "你是一个智能助手.\nYou have access to skills containing dom... <Truncated in logs> ...sted.\nOnly load what is needed, when it is needed."
    },
    {
      "role": "user",
      "content": "一个马拉松是 26.2 英里,等于多少公里?"
    },
    {
      "role": "assistant",
      "tool_calls": [
        {
          "id": "z75mwila62QwDpJCBkAz5XnmOuKE7OTa",
          "type": "function",
          "function": {
            "name": "load_skill",
            "arguments": "{\r\n  \"skillName\": \"unit-converter\"\r\n}"
          }
        }
      ]
    },
    {
      "role": "tool",
      "tool_call_id": "z75mwila62QwDpJCBkAz5XnmOuKE7OTa",
      "content": "\"---\\nname: unit-converter\\ndescription: \\\"单位换算。当用... <Truncated in logs> ...meters_schema>\\n  </script>\\n</available_scripts>\""
    }
  ],
  "model": "qwen/qwen3.5-9b",
  "tools": [
    {
      "type": "function",
      "function": {
        "description": "Loads the full content of a specific skill",
        "name": "load_skill",
      }
    },
    {
      "type": "function",
      "function": {
        "description": "Reads a resource associated with a skill, such as references, assets, or dynamic data.",
        "name": "read_skill_resource",
      }
    },
    {
      "type": "function",
      "function": {
        "description": "Runs a script associated with a skill.",
        "name": "run_skill_script",
    }
  ]
}

AI 模型清楚这个 skills 结构后,开始使用 run_skill_script 调用脚本。

2026-07-24 15:31:29  [INFO]
 [qwen/qwen3.5-9b] Model generated a tool call.
2026-07-24 15:31:29  [INFO]
 [qwen/qwen3.5-9b] Model generated tool calls:  [run_skill_script(skillName="unit-converter", scriptName="scripts/convert.py", arguments=["--value","26.2","--factor","...")]

最终 AI 模型根据脚本执行结果,返回最后的回答,该 Agent 的问题通过多轮对话后自动结束。

image-20260724154124992


加载文件 skill

一个 skill 会占用一个目录,每个 skill 必须有一个 SKILL.md,可选 scripts/references/ 子目录。

skills/unit-converter/
├── SKILL.md                 ← frontmatter + 正文
├── references/conversion-table.md   ← 自动发现为 resource
└── scripts/convert.py        ← 自动发现为 script
---
name: unit-converter
description: Convert between common units using a multiplication factor. ...
---
## Usage
1. First, review `references/conversion-table.md` to find the correct factor
2. Run the `scripts/convert.py` script with `--value <number> --factor <factor>`


AgentFileSkillsSource 扫描磁盘有几条硬规则:

  1. 深度上限 2 层MaxSkillDirectorySearchDepth),超过不下钻。
  2. 遇到 SKILL.md 就停,该目录是 skill 根,子目录(references/scripts/)属于这个 skill,子目录就算还有 skill 也会被忽略。
  3. frontmatter 的 name 必须等于目录名skills/unit-converter/ SKILL.md 必须是 name: unit-converte ,否则会被拒。这是路径校验的一环。
  4. 按扩展名白名单发现.md .json .yaml .yml .csv .xml .txt,脚本 .py .js .sh .ps1 .cs .csx,可在 AgentFileSkillsSourceOptions 改。
  5. 路径安全Path.GetFullPath 规范化 + StartsWith 包含检查 + 符号链接检测,挡路径穿越(skill 常来自第三方,是真实攻击面)。

skill 的头部是一段 YAML 格式的内容,但是 AgentFileSkillsSource 解析 skill 的头部属性时,frontmatter 的解析并不是使用 YAML 库,是手写正则表达式解析,只支持标量、引号、块标量(|/>)、metadata: 缩进块,所以 SKILL.md 的头部别用太花哨的 YAML 特性。

另外 AgentFileSkillsSource 带 5 秒超时防 ReDoS。


脚本执行整体委托给一个 AgentFileSkillScriptRunner 委托,MAF 不带解释器,所以上一个案例中,我们定义了一个 SubprocessScriptRunner 执行脚本,其实现了该委托结构:

public delegate Task<object?> AgentFileSkillScriptRunner(
    AgentFileSkill skill, AgentFileSkillScript script, JsonElement? arguments,
    IServiceProvider? serviceProvider, CancellationToken cancellationToken);

加载代码 skill

skill 可以不存在实体,也就是不存在目录和文件或真实资源,我们可以在 MAF 框架中使用内存动态构造一个 skill。

var unitConverterSkill = new AgentInlineSkill(
    name: "unit-converter",
    description: "Convert between common units using a multiplication factor. ...",
    instructions: "1. Review the conversion-table resource ...\n2. Use the convert script ...")
    .AddResource("conversion-table", """# Conversion Tables ...""")          // 静态值
    .AddResource("conversion-policy", () => $"...{DateTime.UtcNow:O}...")    // 委托,每次现算
    .AddScript("convert", (double value, double factor) =>                   // 委托脚本
        JsonSerializer.Serialize(new { value, factor, result = Math.Round(value * factor, 4) }));

// 直接传 skill 实例构造 provider,再塞进 agent
var skillsProvider = new AgentSkillsProvider(unitConverterSkill);
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
    AIContextProviders = [skillsProvider],
});


代码 skill 的脚本就是普通 C# 委托,在进程内执行,不像文件 skill 要起子进程,所以不用传 scriptRunner,也不用怕跨平台解释器问题。模型调 run_skill_script 时,框架直接调你的委托。


代码 skill 没有 markdown 文件GetContentAsync 返回的是运行时合成的 XML:

<name>unit-converter</name>
<description>...</description>
<instructions>...</instructions>
<available_resources>
  <resource name="conversion-table" description="..."/>
</available_resources>
<available_scripts>
  <script name="convert" description="...">
    <parameters_schema>{"type":"object","properties":{"value":{"type":"number"}...}}</parameters_schema>
  </script>
</available_scripts>

加载类 skill

MAF 提供了一个 AgentClassSkill<T> 抽象,用于实现代码 skill,也就是通过一个类型实现一个 skill,这样我们可以通过 nuget 分发 skill,或者在代码中内置一个 skill。

internal sealed class TemperatureConverterSkill : AgentClassSkill<TemperatureConverterSkill>
{
    public override AgentSkillFrontmatter Frontmatter { get; } = new("temperature-converter", "...");
    protected override string Instructions => "1. Review the temperature-conversion-formulas resource ...";

    [AgentSkillResource("temperature-conversion-formulas")]
    [Description("Formulas for converting between ...")]
    public string ConversionFormulas => "...";

    [AgentSkillScript("convert-temperature")]
    [Description("Converts a temperature value ...")]
    private static string ConvertTemperature(double value, string from, string to) => "...";
}

// 和代码 skill 一样,实例化后传给 provider
var skillsProvider = new AgentSkillsProvider(new TemperatureConverterSkill());


MAF 框架用反射发现标了特性的成员,运行时和代码 skill 完全一样,底层都是 AgentInlineSkillResource/AgentInlineSkillScript。脚本同样是进程内执行的 C# 方法,不需要 scriptRunner。


使用 MCP 库加载 Skills

前面三种 skill(文件、代码、类)都在本地定义。第四种 MCP skill 把 skill 的分发搬到网络上,skill 的内容仍然是一份 SKILL.md,只是它存放在某个 MCP 服务器上,客户端通过 MCP 协议按需取回。

也就是通过 MCP 协议读取服务器的 skill。


一个团队或平台可以把一批 skill 集中托管在一个 MCP 服务器上,多个 agent、多个进程连上去就能用同一套 skill,不用每个项目复制一份 SKILL.md。skill 的更新只在服务器做一次,所有客户端下次加载就生效。

这样好处是,团队内不需要每个人电脑本地都拉取这些 skill,而且使用网络进行读取,避免本地电脑的 skill 版本落后、需要同步,减少出现问题的几率。


下面是官方示例的服务器端,它把一个 unit-converter skill 暴露成两个 resource:

// 服务器端:用 [McpServerResource] 把 SKILL.md 和目录暴露成 resource
[McpServerResourceType]
internal sealed class SkillResources
{
    // 目录:列出所有 skill。AgentMcpSkillsSource 就是从这里发现 skill 的
    [McpServerResource(UriTemplate = "skill://index.json", Name = "Skill Index", MimeType = "application/json")]
    public static string GetIndex() => """
        {
            "skills": [
                {
                    "name": "unit-converter",
                    "type": "skill-md",                       // 类型:按需读 SKILL.md
                    "description": "Convert between common units ...",
                    "url": "skill://unit-converter/SKILL.md"  // 说明书的地址
                }
            ]
        }
        """;

    // 说明书:skill 的正文,和文件 skill 的 SKILL.md 完全一样的格式
    [McpServerResource(UriTemplate = "skill://unit-converter/SKILL.md", MimeType = "text/markdown")]
    public static string GetSkillMd() => """
        ---
        name: unit-converter
        description: Convert between common units using a multiplication factor. ...
        ---
        ## Usage
        When the user requests a unit conversion, use these factors: ...
        """;
}


客户端只要连上这个 MCP 服务器,调 UseMcpSkills,剩下的发现和加载全自动:

// 客户端:连接 MCP 服务器(这里把当前程序当成子进程启动,它跑 --server 模式)
await using McpClient client = await McpClient.CreateAsync(
    new StdioClientTransport(new()
    {
        Command = "dotnet", Arguments = [thisAssemblyPath, "--server"],
    }));

var skillsProvider = new AgentSkillsProviderBuilder()
    .UseMcpSkills(client)     // 读 skill://index.json,把每个 skill-md 条目变成 AgentMcpSkill
    .Build();

AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
    AIContextProviders = [skillsProvider],
});

加载后,对模型和上层完全透明,AgentMcpSkill 和文件 skill 一样实现了 AgentSkill,模型照样看到目录、照样调 load_skill / read_skill_resource。区别只在内容从哪来:

  • load_skill 时,AgentMcpSkill.GetContentAsync 才发 resources/read 请求去服务器取 SKILL.md(懒加载,首次取回后缓存,AgentMcpSkill.cs:63-82);
  • read_skill_resource 时,把资源名拼到 skill 根 URI 上(如 skill://unit-converter/references/table.md),再 resources/readAgentMcpSkill.cs:91-113)。


index 里每个 skill 条目有个 type 字段,目前支持两种:

type含义怎么取内容
skill-md标准 skill,正文是一份 SKILL.md按需 resources/read 取 SKILL.md
archive整个 skill 打包成压缩文件下载并解压到本地目录(有大小/数量限制防压缩炸弹)


其它 type 会被跳过并记日志。


审批

三个工具都包了 ApprovalRequiredAIFunction默认全要审批(skill 正文/脚本常来自第三方)。如果不配,模型一调 load_skill 就会被拦下,agent 返回空文本。


MAF 框架有两条预置自动审批规则:

ReadOnlyToolsAutoApprovalRule  // 只放行 load_skill + read_skill_resource,脚本仍审批
AllToolsAutoApprovalRule       // 三个全放行


方式一:自动放行,通过 UseToolApproval 配规则,匹配的工具直接批准。前面"使用 skill 并执行脚本"那节用的就是 AllToolsAutoApprovalRule

AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
    AIContextProviders = [skillsProvider],
})
    .AsBuilder()
    .UseToolApproval(new ToolApprovalAgentOptions
    {
        // 只放行读类(load_skill / read_skill_resource),run_skill_script 仍要人工批准
        AutoApprovalRules = [AgentSkillsProvider.ReadOnlyToolsAutoApprovalRule],
    })
    .Build();


这两条对非 skill 工具返回 false,不误伤别的 provider 的工具,可和其它规则混用。


方式二:手动响应审批,脚本这类高危操作不想自动放行,就在循环里拿到 ToolApprovalRequestContent、人工判断后回复。未放行的审批请求会出现在 response.Messages 里。:

AgentSession session = await agent.CreateSessionAsync();
AgentResponse response = await agent.RunAsync("把 26.2 英里换算成公里", session);

// 找出所有待审批的工具调用,人工决定批准与否
List<ToolApprovalRequestContent> approvals = response.Messages
    .SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();

while (approvals.Count > 0)
{
    List<ChatMessage> replies = approvals.ConvertAll(req =>
    {
        var toolCall = (FunctionCallContent)req.ToolCall;
        Console.WriteLine($"是否批准执行 {toolCall.Name}?(Y/N)");
        bool ok = Console.ReadLine()?.Trim().Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false;
        return new ChatMessage(ChatRole.User, [req.CreateResponse(ok)]);   // 把批准/拒绝结果发回去
    });

    response = await agent.RunAsync(replies, session);    // 带着审批结果继续
    approvals = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
}

Console.WriteLine(response.Text);


req.CreateResponse(true/false) 把你的决定包成一条 user 消息发回去,agent 据此继续执行(批准)或换路(拒绝)。配合 ReadOnlyToolsAutoApprovalRule,就实现了"读类自动、脚本人工"的常见策略。

审批机制本身在第八章(ToolApprovalAgentApprovalRequiredAIFunction)已经讲过,这里只讲 skill 提供的两条预置规则。