开发一个 Tool

在 Tether 中,暴露给大模型的工具(Tool)都是通过插件向 IToolRegistry 注册的。本篇通过一个 greet 工具示例,讲解如何在 C# 中定义工具、注入依赖并纳入生命周期管理。

核心机制

  • 工具模型:Tether 采用 Microsoft.Extensions.AI.AIFunction 作为工具的标准抽象。
  • Schema 推导:通过 AIFunctionFactory.Create(...),从 C# 方法签名、参数类型与 XML/参数特性自动生成模型可见的 JSON Schema。
  • 注册即回收:调用 tools.Register(tool) 返回 IDisposable,通过 context.Effect(registration.Dispose) 绑定到当前插件的作用域(Effect 只接受 Action/Func<ValueTask> 回调)。当插件卸载或重载时,工具自动从大模型视图中注销。

编写工具插件

创建一个继承 Plugin 基类的类,用 [Inject(...)] 特性声明依赖 IToolRegistry:

using System.ComponentModel;
using Cordis;
using Microsoft.Extensions.AI;
using Tether.Core;

namespace MyPlugins;

[Inject(typeof(IToolRegistry))]
public sealed class GreetToolPlugin : Plugin
{
    protected override void Apply(Context context)
    {
        var tools = context.Require<IToolRegistry>();

        // 定义供模型调用的委托与元数据
        var greetFunction = AIFunctionFactory.Create(
            [Description("根据姓名向用户打招呼")]
            (
                [Description("要打招呼的目标用户名")] string name,
                [Description("打招呼使用的语言,默认 zh-CN")] string? language = "zh-CN"
            ) =>
            {
                return language switch
                {
                    "en-US" => $"Hello, {name}!",
                    "ja-JP" => $"こんにちは、{name}さん!",
                    _ => $"你好,{name}!"
                };
            },
            name: "greet",
            description: "向指定的人发送问候语");

        // 注册工具并挂载到 effect 作用域
        context.Effect(tools.Register(greetFunction).Dispose);
    }
}

代码要点说明

  1. [Inject(typeof(IToolRegistry))]:Plugin 基类会读取该特性(Plugin.Inject 从 InjectAttribute 聚合),通知 Cordis 只有在 IToolRegistry 存在时才激活当前插件。
  2. [Description(...)]:参数特性中的描述会直接写入大模型看到的 Tool Parameter 描述中,指导模型如何填参;委托上的 [Description] 在没有显式传 description: 时会成为工具级描述(显式传入时以 description: 为准)。
  3. context.Effect(tools.Register(...).Dispose):这是 Cordis 的核心规则——Register 返回 IDisposable,而 Effect 只接受 Action/Func<ValueTask> 回调,所以要传 registration.Dispose(或先存成变量再传 registration.Dispose)。绝不要丢弃这个返回值,否则插件卸载时工具会残留并在后续调用中产生悬空引用。

配置并发与执行选项

默认情况下,工具是以 独占模式(Exclusive) 执行的,构成执行屏障。如果你的工具是只读的、无副作用且支持并发调用(如搜索、查表),可以通过 ToolRegistrationOptions 明确声明:

var options = new ToolRegistrationOptions
{
    // 当参数表明是只读操作时,允许与其他只读工具并发执行
    IsConcurrencySafe = args => true
};

context.Effect(tools.Register(greetFunction, scope: null, options).Dispose);

挂载与验证

将插件编译并在宿主或 YAML 组合中启用:

# composition.yml
plugins:
  - { id: tools, name: tools }   # Tether.Core.Tools 的 ToolRegistryPlugin,提供 IToolRegistry
  - { id: my-greet-tool, name: my-greet-tool }   # 你自己的插件 row(name 对应 BundleCatalog 注册名)

通过 CLI 运行:

dotnet run --project apps/Tether.Cli -- \
  --send "请使用 greet 工具向 Alice 打个招呼(用英文)"

大模型将生成如下工具调用并在获得返回值后组织回复:

{
  "name": "greet",
  "arguments": { "name": "Alice", "language": "en-US" }
}

下一步

在 GitHub 上编辑此页