组件化设计:接口即契约
当代码的主要读者从「人」变成「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 接口,不依赖具体的 InMemoryStore 或 OpenVikingStore。
3. 关注点隔离
每个组件只关心自己的事:
- Agent.Memory 只关心"怎么存数据",不关心数据是什么
- AiAgent.OpenViking 只关心"怎么用 LLM 做记忆提取",不关心谁在调用
- AiAgent 只定义接口,不关心谁来实现
这就是"接口即契约"的真正含义——接口定义了组件之间的沟通协议,组件之间通过接口通信,不知道也不关心对方内部怎么实现的。
组件化 vs 微服务
很多人会把组件化和微服务搞混,但它们是两个维度的东西:
| 组件化 | 微服务 | |
|---|---|---|
| 粒度 | 功能模块(一个记忆存储组件) | 业务域(一个用户服务) |
| 通信方式 | 进程内方法调用(接口) | 进程间网络调用(RPC/HTTP) |
| 部署方式 | 一起部署 | 独立部署 |
| 拆分依据 | 关注点分离 | 业务边界 |
| 治理成本 | 低(编译时检查) | 高(服务发现/熔断/限流) |

组件化解决的是"软件功能层面的复用和替换问题",微服务解决的是"业务架构层面的独立部署和扩展问题"。
用 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 友好的架构原则。