Skip to content

组件化设计:接口即契约

当代码的主要读者从「人」变成「AI」,接口的意义也在悄然改变。

组件化的本质

前面两章分别讨论了业务层的扁平化改造和组件服务的标准化接口。现在我们把视角拉高,看一个更根本的设计思想——组件化

组件化的核心是:面向接口编程,而不是面向实现编程。

组件定义
  └── 接口(契约):声明"我能做什么"
      └── 实现(细节):封装"我怎么做的"
          └── 模块(装配):按需加载不同的实现

这个思想在 C# 中得到了最好的体现。C# 的 DI 容器天生支持模块化装配——你可以把接口定义在一个程序集里,把实现放在另一个程序集里,然后在启动时决定加载哪个实现。

一个具体的例子:记忆存储组件

以 Components 项目中的 IMemoryStoreProvider 接口为例。它声明了记忆存储的契约:

csharp
// AiAgent.Interfaces —— 接口定义
public interface IMemoryStoreProvider
{
    Task<string> CreateSessionAsync(string? userId = null, CancellationToken ct = default);
    Task AddMessageAsync(string sessionId, ChatMessage message, CancellationToken ct = default);
    Task<IReadOnlyList<ChatMessage>> GetHistoryAsync(string sessionId, CancellationToken ct = default);
    Task AddFactsAsync(string sessionId, IEnumerable<string> facts, CancellationToken ct = default);
    Task<int> CompactAsync(string sessionId, int keepRecent = 10, CancellationToken ct = default);
    // ...
}

这个接口有两个完全不同的实现,各自独立成一个工程:

实现一:Agent.Memory(内存实现)

Agent.Memory/
├── AgentMemoryModule.cs           ← 模块入口:注册 InMemoryStore
├── Implementations/
│   └── InMemoryStore.cs           ← 基于 ConcurrentDictionary,进程重启丢失
└── Engine/
    └── SimpleCompactionEngine.cs  ← 简单截断压缩
csharp
// AgentMemoryModule.cs —— 模块入口
public static class AgentMemoryModule
{
    public static void RegisterAgentMemory(this IServiceCollection services, IConfiguration configuration)
    {
        services.TryAddSingleton<IMemoryStoreProvider, InMemoryStore>();
    }
}

实现二:AiAgent.OpenViking(高级实现)

AiAgent.OpenViking/
├── OpenVikingModule.cs            ← 模块入口:注册 OpenVikingStore
├── Implementations/
│   └── OpenVikingStore.cs         ← L0/L1/L2 分层上下文 + URI 寻址 + 全文搜索
└── Engine/
    ├── CompactionEngine.cs        ← LLM 智能压缩(可降级)
    └── MemoryExtractor.cs         ← LLM 记忆提取管道
csharp
// OpenVikingModule.cs —— 模块入口
public static class OpenVikingModule
{
    public static void RegisterOpenViking(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddSingleton<IMemoryStoreProvider>(sp =>
        {
            var chatClient = sp.GetService<IChatClient>();
            return new OpenVikingStore(chatClient);
        });
    }
}

这两个实现实现了同一个接口,但能力天差地别:

能力Agent.Memory(InMemory)AiAgent.OpenViking
数据持久化无(进程重启丢失)有(分层上下文存储)
语义搜索不支持(降级返回空)支持(子串匹配 + 相关度排序)
记忆管道不支持(降级返回空)支持(提取 → 去重 → 归档)
智能压缩不支持(简单截断)支持(LLM 摘要压缩,可降级)
依赖可选依赖 IChatClient

组件化的核心价值

1. 独立测试

csharp
// 测试 Agent.Memory
[Test]
public void InMemoryStore_Should_Create_Session()
{
    // Arrange
    IMemoryStoreProvider store = new InMemoryStore();

    // Act
    var sessionId = await store.CreateSessionAsync();

    // Assert
    Assert.IsNotNull(sessionId);
}

// 测试 OpenViking —— 完全独立,不依赖 Agent.Memory
[Test]
public void OpenVikingStore_Should_Store_Context()
{
    // Arrange
    IMemoryStoreProvider store = new OpenVikingStore(chatClient: null);

    // Act
    await store.StoreContextAsync(entry);
    var result = await store.GetContextAsync(entry.Uri);

    // Assert
    Assert.IsNotNull(result);
}

每个组件可以独立测试,不需要启动整个应用。接口就是天然的 Mock 边界——用 IMemoryStoreProvider 作为依赖注入,测试时直接 new 实现类,或者 Mock 接口。

2. 独立替换

csharp
// Program.cs —— 按配置加载不同的记忆组件
if (config["MemoryProvider"] == "OpenViking")
{
    services.RegisterOpenViking(config);    // 加载高级实现
}
else
{
    services.RegisterAgentMemory(config);   // 加载基础实现
}

切换实现只需要改一行配置,或者换一个 Register 调用。业务代码一行都不用改。 因为业务代码只依赖 IMemoryStoreProvider 接口,不依赖具体的 InMemoryStoreOpenVikingStore

3. 关注点隔离

每个组件只关心自己的事:

  • Agent.Memory 只关心"怎么存数据",不关心数据是什么
  • AiAgent.OpenViking 只关心"怎么用 LLM 做记忆提取",不关心谁在调用
  • AiAgent 只定义接口,不关心谁来实现

这就是"接口即契约"的真正含义——接口定义了组件之间的沟通协议,组件之间通过接口通信,不知道也不关心对方内部怎么实现的。

组件化 vs 微服务

很多人会把组件化和微服务搞混,但它们是两个维度的东西:

组件化微服务
粒度功能模块(一个记忆存储组件)业务域(一个用户服务)
通信方式进程内方法调用(接口)进程间网络调用(RPC/HTTP)
部署方式一起部署独立部署
拆分依据关注点分离业务边界
治理成本低(编译时检查)高(服务发现/熔断/限流)

组件化 vs 微服务

组件化解决的是"软件功能层面的复用和替换问题",微服务解决的是"业务架构层面的独立部署和扩展问题"。

用 C# 的 DI 容器可以很优雅地实现组件化,因为容器天生支持"按接口注册实现、按需加载"。Java 的 Spring 虽然也支持 DI,但在模块化装配上不如 C# 灵活——Spring 的组件通常需要依赖注入配置、自动扫描包路径等,做不到"一个组件一个入口 Register 方法"这样清晰的模块边界。

为什么组件化对 AI 友好

回到本书的核心主题——AI 作为代码的主要读者和写作者

组件化给 AI 带来了几个巨大的好处:

1. AI 只需要关心接口

csharp
// AI 看到这个接口,就知道能力边界
public interface IMemoryStoreProvider
{
    Task<string> CreateSessionAsync(string? userId, CancellationToken ct);
    Task AddMessageAsync(string sessionId, ChatMessage message, CancellationToken ct);
    Task<IReadOnlyList<ChatMessage>> GetHistoryAsync(string sessionId, CancellationToken ct);
}

AI 不需要知道 InMemoryStore 的 ConcurrentDictionary 怎么初始化,也不需要知道 OpenVikingStore 的 L0/L1/L2 怎么分层。它只需要知道接口声明了什么方法、参数是什么、返回什么。

2. AI 的 Token 消耗集中在契约上

写业务代码时,AI 只需要 import 接口,调用接口方法。不需要阅读实现类的全部代码——那些代码在另一个文件里,AI 不需要打开它。

对比传统做法:AI 需要读接口定义 → 读实现类逻辑 → 再回去写调用代码。组件化后,AI 只需要读接口定义。

3. 接口本身就是最好的约束规则

前面我们花了一整章讨论"统一规则对 AI 更友好"。组件化的接口就是最强有力的规则——它用类型系统声明了"你能做什么、不能做什么"。AI 在接口约束下生成代码,不可能"越界"。

接口是给 AI 看的最佳文档。它比任何注释都准确,比任何规范都强制。

组件化的工程结构

一个标准化的组件工程结构应该是这样的:

组件名/
├── XxxModule.cs               ← 模块入口(Register + Start/Stop 生命周期)
├── Implementations/           ← 接口实现
│   └── XxxServiceImpl.cs
├── Engine/                    ← 内部引擎(可选)
├── Models/                    ← 内部模型(可选)
├── Xxx.csproj                 ← 工程文件
└── README.md                  ← 组件说明(依赖、配置、用法)

模块入口 XxxModule.cs 是组件的唯一"暴露面",外部只通过这个入口与组件交互:

csharp
// 组件入口的标准模式
public static class XxxModule
{
    // 注册:将接口映射到实现
    public static void RegisterXxx(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddSingleton<IXxxService, XxxServiceImpl>();
    }

    // 启动(可选):初始化资源
    public static async Task StartAsync(this IServiceProvider provider) { ... }

    // 停止(可选):释放资源
    public static async Task StopAsync(this IServiceProvider provider) { ... }
}

这种结构无论对开发人员还是对 AI 都非常清晰:

  • 看到 XxxModule.cs,就知道这个组件提供了什么能力
  • AI看到 IXxxService 接口,就知道怎么调用

Qoder 工作区与组件化的天然契合

Qoder 的多工作区能力,与组件化设计高度匹配。

我们的实际项目结构就是一个例子:

D:\DEV\CODE\Automator\
├── Client/          ← 一个工作区(客户端应用)
├── Components/      ← 另一个工作区(所有组件)
│   ├── Agent.Memory/          ← 记忆存储组件
│   ├── AiAgent.OpenViking/    ← 高级记忆组件
│   ├── AiAgent/               ← 接口定义
│   ├── Workflow/              ← 工作流引擎
│   ├── Plugin/                ← 插件系统
│   ├── Video/                 ← 视频处理组件
│   ├── MCP/                   ← MCP 集成组件
│   ├── LLM/                   ← LLM 封装组件
│   └── ...

一个工作区对应一组组件,Qoder 在加载工作区时自动索引所有代码文件。这意味着:

1. AI 的上下文是组件级别的

当你在 Components 工作区里写代码,Qoder 只索引 Components 目录下的文件。AI 不会读到 Client 的代码,上下文窗口只分配给当前工作区的组件。这正是组件化的初衷——关注点隔离。

2. 切换工作区就是切换关注域

  • 开发 Client 业务 → 打开 Client 工作区,AI 只看到 Client 的代码
  • 开发 Components 组件 → 打开 Components 工作区,AI 只看到组件的代码
  • 两个工作区互不干扰,AI 的上下文不会被无关代码污染

3. 新组件即新目录

需要新增一个组件时,直接在 Components 目录下加一个子目录即可。Qoder 自动将其纳入索引范围,不需要额外配置。新增组件对 AI 来说就是新增一个可调用的模块——接口暴露什么,AI 就用什么,零成本。

对比传统的单体 IDE 工程:所有代码在一个 Solution/Project 里,AI 打开文件时上下文被整个工程塞满。组件化 + 工作区拆分后,AI 每次只面对一个组件或一组相关组件,Token 效率显著提升。

Qoder 的工作区模型,本质上是将组件化的物理边界映射为 AI 的上下文边界。


本章小结

组件化的本质是"接口即契约"。它把软件拆分为可独立开发、独立测试、独立替换的功能单元,每个单元通过接口暴露能力,通过模块入口完成装配。

组件化和微服务的区别在于:组件化是功能层面的,微服务是业务架构层面的。

对 AI 来说,组件化的最大价值是降低了理解成本——AI 只需要关心接口契约,不需要关心实现细节。Token 消耗集中在接口定义上,而不是实现代码上。


关联:下一章我们将从"面向人的架构"走向"面向 AI 的架构",总结一套面向 AI 友好的架构原则。