能力三层拆分实战

在 Tether 中,任何可扩展的系统能力(如文件系统、代码执行、Shell、模型适配)都严格遵循三角色拆分模式。本页以开发一个“代码沙箱执行器(CodeRunner)”为例,手把手演示如何拆分程序集与契约。

为什么要三层拆分?

很多系统习惯把接口、实现和工具代码揉在同一个项目里。这种结构在 Agent 场景下会带来两个严重问题:

  1. 替换实现成本极高:想把“本地进程执行”换成“远程执行世界”(云端 microVM、SSH 主机等),必须把整个包推倒重写。
  2. 循环依赖与依赖膨胀:上层工具只想要一个简单接口,却不得不引入底层的重型第三方依赖(如 Docker SDK 或 PInvoke 库)。

Tether 通过强制三角色模式解决这一问题:

┌────────────────────────────────────────────────────────┐
│  Tether.CodeRunner (Seam)                              │
│  零依赖 · 纯接口与数据契约                                │
└────────────────────────────────────────────────────────┘
            ▲                           ▲
            │ (实现接口)                 │ (依赖接口)
┌───────────────────────────┐ ┌───────────────────────────┐
│ Tether.CodeRunner.Local   │ │ Tether.CodeRunner.Tools   │
│ (Provider) 本地进程实现     │ │ (Consumer) 大模型 Tool 暴露│
└───────────────────────────┘ └───────────────────────────┘

第一步:定义 Seam(Service Definition)

创建 src/Tether.CodeRunner/Tether.CodeRunner.csproj。这个程序集没有任何项目依赖,连 Cordis 都不引用:

namespace Tether.CodeRunner;

/// <summary>
/// 代码执行结果
/// </summary>
public sealed record ExecutionResult(
    int ExitCode,
    string StandardOutput,
    string StandardError,
    TimeSpan Elapsed);

/// <summary>
/// 代码执行服务契约 (Seam)
/// </summary>
public interface ICodeRunnerService
{
    ValueTask<ExecutionResult> ExecuteAsync(
        string language, 
        string code, 
        CancellationToken cancellation = default);
}

第二步:实现 Provider(服务提供者)

创建 src/Tether.CodeRunner.Local/,引用 Cordis 与 Tether.CodeRunner。

编写插件,向 Context 注册服务实例:

using Cordis;
using Tether.CodeRunner;

namespace Tether.CodeRunner.Local;

public sealed class LocalCodeRunnerPlugin : Plugin
{
    protected override void Apply(Context context)
    {
        var service = new LocalProcessCodeRunner();

        // 向上下文提供服务;Provide 返回的注册已绑定当前作用域,插件卸载时自动移除
        context.Provide<ICodeRunnerService>(service);
    }
}

internal sealed class LocalProcessCodeRunner : ICodeRunnerService
{
    public async ValueTask<ExecutionResult> ExecuteAsync(
        string language, 
        string code, 
        CancellationToken cancellation = default)
    {
        // 本地进程执行逻辑(实际工程中调用 Tether.Subprocess)
        return new ExecutionResult(0, "输出结果", string.Empty, TimeSpan.FromMilliseconds(50));
    }
}

第三步:编写 Consumer(工具暴露给大模型)

创建 src/Tether.CodeRunner.Tools/,引用 Cordis、Tether.Core.Contracts 与 Tether.CodeRunner(绝不引用 Local 实现包)。

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

namespace Tether.CodeRunner.Tools;

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

        var runTool = AIFunctionFactory.Create(
            [Description("在沙箱中执行指定语言的代码片段")]
            async (
                [Description("编程语言,例如 csharp, python, bash")] string language,
                [Description("要执行的代码")] string code,
                CancellationToken cancellation
            ) =>
            {
                var result = await runner.ExecuteAsync(language, code, cancellation);
                return new
                {
                    exitCode = result.ExitCode,
                    stdout = result.StandardOutput,
                    stderr = result.StandardError
                };
            },
            name: "execute_code",
            description: "运行代码并捕获标准输出与退出码");

        context.Effect(tools.Register(runTool).Dispose);
    }
}

成果与验证:零成本替换 Provider

现在,在配置文件中组合你的能力:

# 本地开发模式
plugins:
  - { id: tools, name: tools }
  - { id: code-runner-local, name: code-runner-local }
  - { id: code-runner-tools, name: code-runner-tools }

当部署到生产环境需要 Docker 容器沙箱时,只需编写 Tether.CodeRunner.Docker,在 YAML 中把 Tether.CodeRunner.Local 换成 Tether.CodeRunner.Docker 即可:

# 生产容器沙箱模式
plugins:
  - { id: tools, name: tools }
  - { id: code-runner-docker, name: code-runner-docker }  # 仅替换实现包,Tools 和上层业务无需任何改动!
  - { id: code-runner-tools, name: code-runner-tools }

下一步

在 GitHub 上编辑此页