跳至内容

实验 03 — 工具

目标: 使用以下项目,以模型可调用的真实 C# 函数替换静态提示内容: CopilotTool.DefineTool 以及 SessionConfig.Tools.

时间: 约 20 分钟

先决条件: 实验 02 已完成。

步骤 1 — 为何需要工具

在实验 02 中,您将零售数据放入系统消息,让助手的回答有所依据。这适用于小型示例,但有三个问题:

  1. 内容是静态的 — 它只知道您最初粘贴的信息
  2. 模型必须猜测哪些事实重要
  3. 每项事实都会消耗令牌,即使回答根本不需要该事实

工具会改变问题的处理方式。您不再需要指望提示中包含正确数据,而是注册一个 C# 函数。模型会判断何时需要该函数、要求 SDK 调用、接收结果,再撰写最终答案。

步骤 2 — 查看工具结构

打开 ToolsSample.cs 并找到 GetCustomerTotal.

Copilot SDK 工具一开始只是一个普通的 C# 方法:

[Description("Gets the total amount a given retail customer has spent.")]
private static string GetCustomerTotal(
    [Description("Customer identifier, for example C003")] string customerId)
{
    ...
}

最重要的部分是 [Description] 元数据,来源为 System.ComponentModel.

这些描述就是模型的 API 文件。如果方法描述模糊,模型可能找不到工具;如果参数描述模糊,模型可能传入错误值。请像为其他开发人员记录公用 API 一样编写描述。

步骤 3 — 注册工具

CopilotTool.DefineTool(GetCustomerTotal) 会将方法转换为 AIFunction。接着,将该函数赋给会话配置:

var modelId = await ModelPicker.PickAsync(client, requestedModelId);
var totalTool = CopilotTool.DefineTool(GetCustomerTotal);

var config = new SessionConfig
{
    Model = modelId,
    Streaming = false,
    Tools = [totalTool]
};

Tools = [totalTool] 正是“模型只能生成文本”与“模型可以要求主机应用程序执行实际工作”之间的关键区别。 ModelPicker 会优先选择 claude-haiku-4.5;如果您的账户无法使用该模型,则会回退到其他可用模型。您也可以通过以下参数覆盖选择: --model <id>.

步骤 4 — 执行

dotnet run --project src/AgentOrchestrator/samples/SdkLabs -- tools

预期输出:

== Lab 03: tools ==

Model: claude-haiku-4.5
Prompt: How much has customer C003 spent in total?


Assistant: 
  [tool] GetCustomerTotal(C003) -> $1,700.00

Assistant: Customer C003 has spent a total of **$1,700.00** across 2 transactions.

⚠️ 请注意第一个空白 Assistant: 行。这是真实行为:助手消息事件会在工具调用附近触发,早于最终自然语言答案的组合。

步骤 5 — 跟踪实际发生的情况

执行作业包含四个操作部分:

  1. 提示要求查询客户的消费总额: C003
  2. 模型判断已注册工具是回答问题的正确方式
  3. SDK 会调用 C# 方法,生成 [tool]
  4. 工具结果会被送回模型,再由模型撰写最终答案

[tool] GetCustomerTotal(C003) -> $1,700.00 这一行并非模拟输出;它来自真正的 GetCustomerTotal 方法,表明 SDK 正在处理模型发起的工具调用。

步骤 6 — 进行实验

尝试不存在的客户,例如 C999。工具会返回普通字符串而不是抛出异常,以处理该情况:

Prompt = "How much has customer C999 spent in total? Use the available tool."

重新执行示例,确认助手报告找不到任何交易。

然后试试不需要零售数据的提示词:

Prompt = "In one short sentence, define average order value."

模型应该直接回答。由于不需要查询客户, [tool] 行不应出现。

步骤 7 — 使用权限控制工具执行

SDK 还公开了进程内权限钩子:

OnPermissionRequest = (request, invocation) =>
{
    return Task.FromResult(PermissionDecision.ApproveOnce());
}

签名如下:

Func<PermissionRequest, PermissionInvocation, Task<PermissionDecision>>

决策来自 GitHub.Copilot.Rpc.PermissionDecision,包括 ApproveOnce() 以及 Reject(string feedback)。另外也有内置快捷键: PermissionHandler.ApproveAll,适用于允许所有请求的示例。

⚠️ 在 GitHub Copilot SDK v1.0.9 中,此权限决策 API 标示为实验性。使用 GitHub.Copilot.Rpc.PermissionDecision 会引发构建错误 GHCP001 ,除非该内容遭到隐藏。示例项目特意在此位置进行此处理: SdkLabs.csproj:

<NoWarn>$(NoWarn);GHCP001</NoWarn>

⚠️ 将此钩子用作强制执行点时,务必格外谨慎。根据我们的测试,该处理程序从未被调用:即使处理程序拒绝每个请求,命令仍会执行,因为主机 CLI 已预先授予工具权限。只有当主机把决策交给 SDK 时,OnPermissionRequest 才会介入。请务必在您的环境中验证其行为,再将它作为控制措施。

请参阅 PermissionsSample.cs 获取参考代码。如需 Shell 钩子治理替代方案,请参阅 扩展实验:治理钩子

CopilotToolOptions.SkipPermission 也可以在适合您的主机时,让工具不参与权限流程。

✅ 检查点

您现在可以说明:

  • [x] 了解为何工具优于将动态数据塞入系统消息
  • [x] 如何 [Description] 属性会引导工具选择与参数设置
  • [x] 如何 CopilotTool.DefineTool 会将 C# 方法注册为 AIFunction
  • [x] 如何 SessionConfig.Tools 会让模型可以使用该函数
  • [x] 了解为何权限钩子必须在实际执行的主机中验证

💡 加分练习

添加第二个工具,用于返回客户购买过的商品类别,例如 Electronics 以及 Fashion 用于 C003。请为方法及其参数提供精确的 [Description] 属性,并将它注册在以下项目旁: GetCustomerTotal,随后询问:

Which categories has customer C003 bought from, and how much have they spent?

确认模型调用一个工具、两个工具,或直接回答: