能力三层拆分实战
在 Tether 中,任何可扩展的系统能力(如文件系统、代码执行、Shell、模型适配)都严格遵循三角色拆分模式。本页以开发一个“代码沙箱执行器(CodeRunner)”为例,手把手演示如何拆分程序集与契约。
为什么要三层拆分?
很多系统习惯把接口、实现和工具代码揉在同一个项目里。这种结构在 Agent 场景下会带来两个严重问题:
- 替换实现成本极高:想把“本地进程执行”换成“远程执行世界”(云端 microVM、SSH 主机等),必须把整个包推倒重写。
- 循环依赖与依赖膨胀:上层工具只想要一个简单接口,却不得不引入底层的重型第三方依赖(如 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 }