跳转到内容

架构设计

4 篇包含标签 "架构设计" 的文章

SQLite 分片方案实战:三种分片策略的深度对比

SQLite 分片方案实战:三种分片策略的深度对比

Section titled “SQLite 分片方案实战:三种分片策略的深度对比”

当单文件 SQLite 遇到并发瓶颈,我们该如何破局?本文分享 HagiCode 项目中三种不同场景下的 SQLite 分片方案,帮你理解如何选择合适的分片策略。

全民制作人们大家好,我是 HagiCode 制作人俞坤。

在构建高性能应用时,单文件 SQLite 数据库会碰到很现实的问题。用户量和数据量一上来,这些状况就会排队找上门:

  • 写入操作开始排队,响应时间肉眼可见地变长
  • 查询性能随数据增长往下掉
  • 多线程访问时频繁出现 “database is locked” 错误

很多人第一反应是:要不要直接迁移到 PostgreSQL 或者 MySQL?这波操作虽然能解决问题,但部署复杂度会直线上升。有没有更轻量的方案?

答案是:分片。说到底,工程问题还是要回到工程方法里解决,通过将数据分散到多个 SQLite 文件,可以显著提升并发能力和查询性能,同时保持 SQLite 的轻量级特性。

本文分享的方案来自我们在 HagiCode 项目中的实践经验。作为一个 AI 代码助手项目,HagiCode 需要处理大量的对话消息、状态持久化和事件历史记录。正是在解决这些实际问题的过程中,我们总结出了三种不同场景下的分片方案。

工欲善其事,必先利其器,但这些”器”怎么用,还得看具体的”事”是什么。

我们的代码仓库在 github.com/HagiCode-org/site,欢迎感兴趣的朋友深入了解。

经过对 HagiCode 代码库的分析,我们发现了三种针对不同业务场景的 SQLite 分片方案:

  1. Session Message 分片存储:AI 对话消息存储,特点是高频写入、基于 Session 的隔离查询
  2. Orleans Grain 分片存储:分布式框架状态持久化,特点是跨节点访问、需要确定性路由
  3. Hero History 分片存储:游戏化系统历史事件记录,特点是事件溯源、需要迁移兼容

虽然业务场景不同,但三者都遵循相同的核心设计原则:

  • 确定性路由:直接从业务 ID 计算分片,无需元数据表
  • 透明访问:上层通过统一接口操作,不感知分片存在
  • 独立存储:每个分片是完全独立的 SQLite 文件
  • 并发优化:WAL 模式 + busy_timeout 降低锁竞争

很多人会问:为什么不搞一套通用的分片方案?这个问题问得很实在,我们直接上结论:工程上没有万能方案,只有最贴合当前业务场景的方案。接下来我们深入对比这三种方案的具体实现。

方面Session MessageOrleans GrainHero History
分片数量256 (16²)10010
命名规则16 进制 (00-ff)10 进制 (00-99)10 进制 (0-9)
存储目录DataDir/messages/DataDir/orleans/grains/DataDir/hero-history/
文件名模式{shard}.dbgrains-{shard}.db{shard}.db

为什么分片数量差异这么大?这取决于业务特点。换句话说,模型会说,工具会变,工作流会升级,但工程上的基本盘一直都在那里:你得先搞清楚自己要解决什么问题。

  • Session Message 使用 256 个分片,因为对话消息的写入频率最高,需要更多的分片来分散负载
  • Orleans Grain 使用 100 个分片,平衡了并发性能和管理复杂度
  • Hero History 只用 10 个分片,因为历史事件写入频率较低,且需要考虑迁移成本

路由算法是分片方案的核心,决定了数据如何分布到各个分片。三种方案使用了不同的路由策略:

// Session Message: GUID 后两位 16 进制
var normalized = Guid.Parse(sessionId.Value).ToString("N").ToLowerInvariant();
return normalized[^2..]; // 取末两位 16 进制字符
// Orleans Grain: 提取数字后两位取模
var digits = ExtractDigits(grainId); // 提取所有数字
var lastTwoDigits = (digits[^2] * 10) + digits[^1];
return lastTwoDigits % shardCount;
// Hero History: 末位字符 ASCII 值取模
return heroId[^1] % 10;

设计思路解析

  • Session Message 的 ID 是 GUID,转换为 16 进制后取末两位,可以得到均匀分布的 256 个分片
  • Orleans Grain 的 ID 格式不统一,可能包含字母和数字,所以提取所有数字后取模
  • Hero History 的 ID 是字符串,直接用末位字符的 ASCII 值取模,简单但分布可能不够均匀

关键点:无论使用哪种算法,都必须保证同一 ID 永远映射到同一分片。这是分布式系统中最基本的要求,否则会导致数据不一致。说到底,路由不稳定,一切努力都是零。

方面Session MessageOrleans GrainHero History
初始化时机按需懒加载启动时全量并行初始化按需懒加载
并发控制Lazy<Task> 防重复初始化Parallel.ForEachAsyncLazy<Task> 防重复初始化

为什么 Orleans Grain 选择启动时全量初始化?

因为 Orleans 是分布式框架,Grain 可能被调度到任意节点。如果在运行时才发现分片文件不存在,会导致请求失败。启动时全量初始化虽然会延长启动时间,但能确保运行时的稳定性。能跑起来只是开始,能维护下去才算本事。

懒加载的优势

对于 Session Message 和 Hero History,使用懒加载可以减少启动时间,只有在真正需要访问某个分片时才创建文件和初始化 Schema。使用 Lazy<Task> 可以防止并发初始化时的竞态条件。这个设计看着简单,但在真实项目里能省掉很多不必要的麻烦。

三种方案的 Schema 设计反映了各自的业务特点:

Session Message

  • 支持 Event Sourcing 模式(事件表 + 快照表)
  • 包含消息内容块子表(MessageContentBlocks)
  • 具有压缩和压缩标记字段,支持后续优化

Orleans Grain

  • 最简设计:单表 GrainState
  • JSON 序列化存储状态
  • ETag 乐观并发控制

Hero History

  • 时间线查询优化索引
  • DedupeKey 唯一约束防重复
  • 支持多种事件类型和状态

从这些设计中可以看出,Schema 设计应该紧密贴合业务需求,而不是追求通用性。Orleans Grain 的简单设计正是因为它只需要存储序列化后的状态,不需要复杂的查询能力。这波不是玄学,是工程。别急着把名字起得太大,先看看这东西能不能在团队里活过两个迭代。

三种方案都使用了相同的 SQLite 并发优化配置:

PRAGMA journal_mode=WAL; -- 写前日志模式
PRAGMA synchronous=NORMAL; -- 降低持久化开销
PRAGMA busy_timeout=5000; -- 5秒忙等待
PRAGMA foreign_keys=ON; -- 外键约束

WAL 模式的优势

传统的回滚日志模式在写入时会产生锁竞争,而 WAL 模式允许读写并发进行。这在大数据量场景下可以显著提升性能。很多人不知道这个配置,其实它比你想的要重要得多。

synchronous=NORMAL 的权衡

设置为 FULL 可以保证最高安全性,但会显著降低性能。NORMAL 模式在安全性和性能之间取得了平衡,对于大多数应用来说是合适的选择。这个配置不需要纠结太久,NORMAL 就够了。

基于对 HagiCode 三种方案的分析,我们可以总结出以下决策矩阵:

高吞吐量场景 → 更多分片(如 Message 用 256)
简单维护性 → 较少分片(如 Hero History 用 10)
数字 ID 为主 → 取模算法(Orleans Grain)
GUID 为主 → 16 进制后缀(Session Message)
字符串 ID → ASCII 取模(Hero History)

分片数量选择的经验值

  • 太少(< 10):并发提升有限,分片意义不大
  • 太多(> 1000):文件管理复杂,连接池开销大
  • 经验值:10-100 个分片适用于大多数场景
  • 极高并发场景:可以考虑 256 个分片

这事你要是只看演示,确实容易上头;可一旦进了生产环境,账就得一笔一笔算清楚。很多问题不是不能做,只是没把代价算明白。

public interface IShardResolver<TId>
{
string ResolveShardKey(TId id);
}
// 16 进制分片(适用于 GUID)
public class HexSuffixShardResolver : IShardResolver<string>
{
private readonly int _suffixLength;
public HexSuffixShardResolver(int suffixLength = 2)
{
_suffixLength = suffixLength;
}
public string ResolveShardKey(string id)
{
var normalized = id.Replace("-", "").ToLowerInvariant();
return normalized[^_suffixLength..];
}
}
// 数字取模分片(适用于纯数字 ID)
public class NumericModuloShardResolver : IShardResolver<long>
{
private readonly int _shardCount;
public NumericModuloShardResolver(int shardCount)
{
_shardCount = shardCount;
}
public string ResolveShardKey(long id)
{
return (id % _shardCount).ToString("D2");
}
}
public class ShardedConnectionFactory<TOptions>
{
private readonly ConcurrentDictionary<string, Lazy<Task>> _initializationTasks = new();
private readonly TOptions _options;
private readonly IShardSchemaInitializer _initializer;
public ShardedConnectionFactory(
TOptions options,
IShardSchemaInitializer initializer)
{
_options = options;
_initializer = initializer;
}
public async Task<TDbContext> CreateAsync(string shardKey, CancellationToken ct)
{
var connectionString = BuildConnectionString(shardKey);
// 使用 Lazy<Task> 防止并发初始化
var initTask = _initializationTasks.GetOrAdd(
connectionString,
_ => new Lazy<Task>(() => InitializeShardAsync(connectionString, ct))
);
await initTask.Value;
return CreateDbContext(connectionString);
}
private async Task InitializeShardAsync(string connectionString, CancellationToken ct)
{
await _initializer.InitializeAsync(connectionString, ct);
}
private string BuildConnectionString(string shardKey)
{
var shardPath = Path.Combine(_options.BaseDirectory, $"{shardKey}.db");
return $"Data Source={shardPath}";
}
private TDbContext CreateDbContext(string connectionString)
{
// 根据具体的 ORM 创建 DbContext
return Activator.CreateInstance(typeof(TDbContext), connectionString) as TDbContext;
}
}
public class SqliteShardInitializer : IShardSchemaInitializer
{
public async Task InitializeAsync(string connectionString, CancellationToken ct)
{
await using var connection = new SqliteConnection(connectionString);
await connection.OpenAsync(ct);
// 并发优化配置
await connection.ExecuteAsync("""
PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
PRAGMA busy_timeout=5000;
PRAGMA foreign_keys=ON;
""");
// 创建表结构
await connection.ExecuteAsync("""
CREATE TABLE IF NOT EXISTS Entities (
Id TEXT PRIMARY KEY,
CreatedAt TEXT NOT NULL,
UpdatedAt TEXT NOT NULL,
Data TEXT NOT NULL,
ETag TEXT
);
""");
// 创建索引
await connection.ExecuteAsync("""
CREATE INDEX IF NOT EXISTS IX_Entities_CreatedAt
ON Entities(CreatedAt DESC);
CREATE INDEX IF NOT EXISTS IX_Entities_UpdatedAt
ON Entities(UpdatedAt DESC);
""");
}
}

1. 路由稳定性

路由算法必须保证同一 ID 永远映射到同一分片。避免使用随机或时间相关的计算,也不要在算法中引入可变参数。

2. 分片数量选择

分片数量应该在设计阶段确定,后期修改非常困难。需要考虑:

  • 当前和未来的并发量
  • 单个分片的管理成本
  • 数据迁移的复杂度

3. 迁移考虑

Hero History 方案展示了完整的迁移路径:

  1. 新建分片存储基础设施
  2. 实现迁移服务将主库数据复制到分片
  3. 验证迁移后查询兼容性
  4. 切换读写路径到分片
  5. 清理主库旧表

设计分片方案时就需要考虑未来的迁移需求。Talk is cheap. Show me the code,但光有代码还不够,你还得有完整的迁移路径。一次成功不叫体系,持续成功才叫体系。

4. 监控与运维

  • 监控各分片的大小分布,及时发现数据倾斜
  • 设置告警检测分片热点,避免单个分片成为瓶颈
  • 定期检查 WAL 文件大小,防止磁盘空间占用过多
  • 建立分片健康检查机制

5. 测试覆盖

  • 测试边界条件(空 ID、特殊字符、超长 ID)
  • 验证路由确定性,确保同一 ID 总是映射到同一分片
  • 并发写入压力测试,验证锁竞争得到有效缓解
  • 迁移测试,确保数据完整性和一致性

通过对比 HagiCode 项目中的三种 SQLite 分片方案,我们可以看到:

  1. 没有万能的解决方案:不同业务场景需要不同的分片策略
  2. 核心原则是通用的:确定性路由、透明访问、独立存储、并发优化
  3. 设计要面向未来:考虑迁移路径和运维成本

如果你的项目正在使用 SQLite,并且开始遇到并发瓶颈,希望这篇文章能为你提供一些思路。不需要急着迁移到重量级数据库,有时候合适的分片方案就能解决问题。

当然,分片不是银弹。在选择分片方案之前,先确保:

  • 你已经优化了单表查询性能
  • 你已经使用了合适的索引
  • 你已经启用了 WAL 模式

只有在这些优化都做完之后,仍然存在性能瓶颈时,才考虑引入分片。你能把简单的事情做好,这本身就是一种能力。

很多话讲一遍不如做一遍,接下来就让工程结果自己发声。

感谢您的阅读,如果您觉得本文有用,欢迎点赞、收藏和分享支持。 本内容采用人工智能辅助协作,最终内容由作者审核并确认。

打造 AI 冒险团:HagiCode 多 Agent 协作配置实战

打造 AI 冒险团:HagiCode 多 Agent 协作配置实战

Section titled “打造 AI 冒险团:HagiCode 多 Agent 协作配置实战”

在现代软件开发中,单一 AI Agent 已经难以满足复杂需求。如何让来自不同公司的多个 AI 助手在同一项目中协同工作?本文将分享 HagiCode 项目在实际开发中探索出的多 Agent 协作配置方案。

相信很多开发者都有过这样的经历:项目中引入了 AI 助手辅助编程,效率确实提高了。但随着需求越来越复杂,一个 AI Agent 开始不够用了——你想让它同时处理代码审查、文档生成、单元测试等多个任务,结果往往是顾此失彼,输出质量参差不齐。

更头疼的是,当你尝试引入多个 AI 助手时,问题就变得更复杂了。每个 Agent 有自己的配置方式、API 接口和执行逻辑,彼此之间甚至会产生冲突。这就像一支球队,每个球员都很厉害,但没有人知道该怎么配合,结果踢得乱七八糟。

HagiCode 项目在开发过程中也遇到了同样的困扰。作为一个涉及前端 VSCode 扩展、后端 AI 服务、跨平台桌面客户端的复杂项目,我们在 2026-03 的当时版本 里需要同时对接来自不同公司的多个 AI 助手:Claude Code、Codex、CodeBuddy、iFlow 等等。如何让它们在同一项目中和谐共处、发挥各自特长,成了必须解决的关键问题。

其实这也罢了,毕竟谁愿意每天跟一群打架的 AI 打交道呢。

本文分享的方案,正是我们在 HagiCode 项目中实际踩坑、实际优化出来的多 Agent 协作配置实践。如果你也在为多 AI 助手协作而头疼,相信这篇文章会给你一些启发。或许吧,毕竟每个人的情况都不一样。

HagiCode 是一个 AI 代码助手项目,采用多 AI 引擎协同工作的”冒险团”模式。项目地址:github.com/HagiCode-org/site

本文分享的多 Agent 配置方案,正是 HagiCode 能够在复杂项目中保持高效开发的核心技术之一。也没什么特别的,就是把一群 AI 变成一支能打配合的冒险团而已。

从”单打独斗”到”团队协作”

Section titled “从”单打独斗”到”团队协作””

在 HagiCode 项目早期,我们也尝试过只用一个 AI Agent 来处理所有任务。很快我们就发现,这种方式存在明显的瓶颈:不同的任务需要不同的能力侧重点,有的任务需要更强的上下文理解能力,有的则需要更精准的代码修改能力。一个 Agent 很难在所有方面都表现出色。

这让我们意识到,必须让多个 Agent 协同工作。但问题是,如何让不同公司的 AI 产品在同一个项目中和平共处?我们需要解决几个核心问题:

  1. 配置管理复杂性:每个 Agent 有不同的配置方式、API 接口和执行模式
  2. 通信协议统一:需要一种标准化的方式让不同 Agent 之间进行数据交换
  3. 任务分工协调:如何合理分配任务,让每个 Agent 发挥特长

带着这些问题,我们开始设计 HagiCode 的多 Agent 架构。其实也没那么复杂,只是想明白了而已。

经过多次迭代,我们最终确定的架构是这样的:

┌─────────────────────────────────────────────────────────────────┐
│ AIProviderFactory │
│ (工厂模式统一管理所有 AI Provider) │
├─────────────────────────────────────────────────────────────────┤
│ ClaudeCodeCli │ CodexCli │ CodebuddyCli │ IFlowCli │
│ (Anthropic) │ (OpenAI) │ (智谱 GLM) │ (智谱) │
└─────────────────────────────────────────────────────────────────┘

核心思路是:通过统一的 Provider 接口,让不同的 AI Agent 可以被同一套代码管理。同时使用工厂模式动态创建和配置这些 Provider,确保系统的扩展性和灵活性。

这就像生活中的分工,每个人都有自己的角色,只是这里把这种分工变成了代码架构而已。

根据 HagiCode 项目的实际使用经验,我们为每个 Agent 分配了不同的职责:

Agent提供商模型主要用途
ClaudeCodeCliAnthropicglm-5-turbo生成技术方案和Proposal
CodexCliOpenAI/Zedgpt-5.4执行精准的代码修改
CodebuddyCli智谱glm-4.7优化提案描述和文档
IFlowCli智谱glm-4.7归档提案和历史记录(当时配置;当前仅历史兼容)
OpenCodeCli--通用代码编辑
GitHubCopilotMicrosoft-辅助编程和代码补全

这种分工的背后逻辑是:每个 Agent 都有自己擅长的领域。Claude Code 在理解和分析复杂需求方面表现出色,所以让它负责前期的方案设计;Codex 在代码修改方面更精准,适合处理具体的实现任务;CodeBuddy 性价比高,用来优化文档再合适不过。

毕竟适合自己的才是最好的,条条大路通罗马,只是有的路好走一点,有的路稍微曲折一点罢了。

要让不同的 AI Agent 能够被统一管理,首先需要定义一套统一的接口。HagiCode 中定义了这个接口:

public interface IAIProvider
{
// 统一的 Provider 接口
Task<IAIProvider?> GetProviderAsync(AIProviderType providerType);
Task<IAIProvider?> GetProviderAsync(string providerName, CancellationToken cancellationToken);
}

这个接口看起来很简单,但它是整个多 Agent 系统的基石。通过统一的接口,我们可以无视底层是哪个公司的 AI 产品,都以相同的方式进行调用。

其实这就是把复杂的事情简单化了,毕竟简单才是美。

有了统一的接口,接下来就是如何创建这些 Provider 实例。HagiCode 使用了工厂模式:

private IAIProvider? CreateProvider(AIProviderType providerType, ProviderConfiguration config)
{
return providerType switch
{
AIProviderType.ClaudeCodeCli =>
ActivatorUtilities.CreateInstance<ClaudeCodeCliProvider>(_serviceProvider, Options.Create(config)),
AIProviderType.CodebuddyCli =>
ActivatorUtilities.CreateInstance<CodebuddyCliProvider>(_serviceProvider, Options.Create(config)),
AIProviderType.CodexCli =>
ActivatorUtilities.CreateInstance<CodexCliProvider>(_serviceProvider, Options.Create(config)),
AIProviderType.IFlowCli =>
ActivatorUtilities.CreateInstance<IFlowCliProvider>(_serviceProvider, Options.Create(config)),
_ => null
};
}

这里用到了依赖注入的 ActivatorUtilities.CreateInstance,它可以在运行时动态创建 Provider 实例,并且自动注入依赖项。这种设计的好处是:新增一个 Agent 类型时,只需要添加对应的 Provider 类,然后在工厂方法中加一个 case 分支即可,完全不需要修改现有代码。

这也罢了,毕竟谁愿意每次加新功能都要改一堆旧代码呢。

为了让配置更灵活,我们还实现了类型映射机制:

public static AIProviderTypeExtensions
{
private static readonly Dictionary<string, AIProviderType> _typeMap = new(
StringComparer.OrdinalIgnoreCase)
{
["ClaudeCodeCli"] = AIProviderType.ClaudeCodeCli,
["CodebuddyCli"] = AIProviderType.CodebuddyCli,
["CodexCli"] = AIProviderType.CodexCli,
["IFlowCli"] = AIProviderType.IFlowCli,
// ...更多类型映射
};
}

这个映射表的作用是将字符串形式的 Provider 名称转换为枚举类型。这样一来,配置文件就可以使用直观的字符串名称,而代码内部则使用类型安全的枚举进行处理。

毕竟配置这东西,越直观越好,谁愿意记一堆复杂的代码呢。

实际使用时,只需要在 appsettings.json 中配置即可:

AI:
Providers:
Providers:
ClaudeCodeCli:
Enabled: true
Model: glm-5-turbo
WorkingDirectory: /path/to/project
CodebuddyCli:
Enabled: true
Model: glm-4.7
CodexCli:
Enabled: true
Model: gpt-5.4
IFlowCli:
Enabled: true
Model: glm-4.7

每个 Provider 都可以独立配置开关、模型版本、工作目录等参数。这种设计既保证了灵活性,又便于管理和维护。

其实配置文件就像人生的选项,你可以选择开启或关闭某些功能,只是代码里的选择更容易后悔罢了。

有了统一的技术架构,接下来就是如何让多个 Agent 协同工作了。HagiCode 设计了一套任务流转机制,让不同的 Agent 处理不同阶段的任务:

提案创建 (用户)
[Claude Code] ──生成提案──▶ 提案文档
│ │
│ ▼
│ [Codebuddy] ──优化描述──▶ 优化后提案
│ │
│ ▼
│ [Codex] ──执行修改──▶ 代码变更
│ │
│ ▼
└───────────────▶ [iFlow] ──归档──▶ 历史记录

这种分工的好处是:每个 Agent 只需要专注于自己擅长的任务,不需要”什么都会”。Claude Code 负责从无到有生成提案,Codebuddy 负责把提案描述得更清晰,Codex 负责把提案变成实际的代码变更,iFlow 则负责把这些变更归档保存。

其实这就像生活中的团队合作,每个人都有自己的角色,合起来才能完成一件大事。只是这里的团队成员是 AI 而已。

在实际运行中,我们总结了以下几点经验:

1. Agent 选择策略很重要

不是随便分配任务,而是要根据每个 Agent 的特长来分配:

  • 提案生成:使用 Claude Code,因为它有更强的上下文理解能力
  • 代码执行:使用 Codex,因为它在代码修改方面更精准
  • 提案优化:使用 Codebuddy,因为它的性价比高
  • 归档存储:使用 iFlow,因为它稳定可靠

毕竟让合适的人做合适的事,这是千古不变的道理。

2. 配置隔离确保稳定性

每个 Agent 的配置独立管理,支持环境变量覆盖,工作目录也相互独立。这样一来,一个 Agent 的配置出错不会影响到其他 Agent。

这就像生活中的界限,每个人都有自己的空间,互不干扰才能和谐共处。

3. 错误处理机制

单个 Agent 失败不应该影响整体流程。我们实现了降级策略:当某个 Agent 执行失败时,系统可以自动切换到备用方案,或者直接跳过该步骤继续执行后续任务。同时,完整的日志记录也便于事后排查问题。

毕竟谁也不能保证永远不会出错,关键是怎么处理错误。这就像人生,总会遇到挫折,重要的是怎么走出来。

4. 监控与可观测性

通过 ACP 协议(我们自定义的通信协议,基于 JSON-RPC 2.0),可以追踪每个 Agent 的执行状态。会话隔离确保了并发安全,动态缓存则优化了性能表现。

毕竟看不见的东西最容易出问题,有点监控总好过两眼一抹黑。

采用这套多 Agent 协作配置后,HagiCode 项目的开发效率有了明显提升。具体表现在:

  1. 任务处理能力翻倍:以前一个 Agent 需要同时处理多种任务,现在可以并行处理,吞吐量翻倍不止
  2. 输出质量更稳定:每个 Agent 只专注于自己擅长的任务,输出结果的一致性和质量都更高
  3. 维护成本降低:统一的接口和配置管理,让整个系统更容易维护和扩展
  4. 新增 Agent 简单:如果要接入新的 AI 产品,只需要实现接口、添加配置,不需要修改核心逻辑

这套方案不仅解决了 HagiCode 自身的问题,也证明了多 Agent 协作确实是一种可行的架构选择。

其实效果还挺明显的,只是过程有点折腾罢了。

本文分享了 HagiCode 项目在多 Agent 协作配置方面的实践经验。核心要点包括:

  1. 标准化接口:通过 IAIProvider 统一不同 Agent 的行为,让代码可以无视底层是哪个公司的产品
  2. 工厂模式:使用 ActivatorUtilities.CreateInstance 动态创建 Provider 实例,支持运行时配置和依赖注入
  3. 协议统一:ACP 协议实现 Agent 间的标准化通信,基于 JSON-RPC 2.0 的双向通信机制
  4. 任务分流:合理分配任务给不同的 Agent,让它们各展所长,而不是试图让一个 Agent 做所有事情

这种设计不仅解决了”多 Agent 打架”的问题,还通过冒险团的任务流转机制,实现了开发流程的自动化和专业化。

如果你也在考虑引入多个 AI 助手,希望本文能给你一些参考。当然,每个项目的情况不同,具体方案还需要根据实际情况调整。毕竟没有放之四海而皆准的方案,适合自己的才是最好的。

美的事物或人,不一定要占有,只要她还是美的,自己好好看着她的美就好了。技术方案也是如此,适合自己的,就是最好的…

打造 AI 冒险团:HagiCode 多 Agent 协作配置实战

打造 AI 冒险团:HagiCode 多 Agent 协作配置实战

Section titled “打造 AI 冒险团:HagiCode 多 Agent 协作配置实战”

在现代软件开发中,单一 AI Agent 已经难以满足复杂需求。如何让来自不同公司的多个 AI 助手在同一项目中协同工作?本文将分享 HagiCode 项目在实际开发中探索出的多 Agent 协作配置方案。

相信很多开发者都有过这样的经历:项目中引入了 AI 助手辅助编程,效率确实提高了。但随着需求越来越复杂,一个 AI Agent 开始不够用了——你想让它同时处理代码审查、文档生成、单元测试等多个任务,结果往往是顾此失彼,输出质量参差不齐。

更头疼的是,当你尝试引入多个 AI 助手时,问题就变得更复杂了。每个 Agent 有自己的配置方式、API 接口和执行逻辑,彼此之间甚至会产生冲突。这就像一支球队,每个球员都很厉害,但没有人知道该怎么配合,结果踢得乱七八糟。

HagiCode 项目在开发过程中也遇到了同样的困扰。作为一个涉及前端 VSCode 扩展、后端 AI 服务、跨平台桌面客户端的复杂项目,我们需要同时对接来自不同公司的多个 AI 助手:Claude Code、Codex、CodeBuddy、iFlow 等等。如何让它们在同一项目中和谐共处、发挥各自特长,成了必须解决的关键问题。

其实这也罢了,毕竟谁愿意每天跟一群打架的 AI 打交道呢。

本文分享的方案,正是我们在 HagiCode 项目中实际踩坑、实际优化出来的多 Agent 协作配置实践。如果你也在为多 AI 助手协作而头疼,相信这篇文章会给你一些启发。或许吧,毕竟每个人的情况都不一样。

HagiCode 是一个 AI 代码助手项目,采用多 AI 引擎协同工作的”冒险团”模式。项目地址:github.com/HagiCode-org/site

本文分享的多 Agent 配置方案,正是 HagiCode 能够在复杂项目中保持高效开发的核心技术之一。也没什么特别的,就是把一群 AI 变成一支能打配合的冒险团而已。

从”单打独斗”到”团队协作”

Section titled “从”单打独斗”到”团队协作””

在 HagiCode 项目早期,我们也尝试过只用一个 AI Agent 来处理所有任务。很快我们就发现,这种方式存在明显的瓶颈:不同的任务需要不同的能力侧重点,有的任务需要更强的上下文理解能力,有的则需要更精准的代码修改能力。一个 Agent 很难在所有方面都表现出色。

这让我们意识到,必须让多个 Agent 协同工作。但问题是,如何让不同公司的 AI 产品在同一个项目中和平共处?我们需要解决几个核心问题:

  1. 配置管理复杂性:每个 Agent 有不同的配置方式、API 接口和执行模式
  2. 通信协议统一:需要一种标准化的方式让不同 Agent 之间进行数据交换
  3. 任务分工协调:如何合理分配任务,让每个 Agent 发挥特长

带着这些问题,我们开始设计 HagiCode 的多 Agent 架构。其实也没那么复杂,只是想明白了而已。

经过多次迭代,我们最终确定的架构是这样的:

┌─────────────────────────────────────────────────────────────────┐
│ AIProviderFactory │
│ (工厂模式统一管理所有 AI Provider) │
├─────────────────────────────────────────────────────────────────┤
│ ClaudeCodeCli │ CodexCli │ CodebuddyCli │ IFlowCli │
│ (Anthropic) │ (OpenAI) │ (智谱 GLM) │ (智谱) │
└─────────────────────────────────────────────────────────────────┘

核心思路是:通过统一的 Provider 接口,让不同的 AI Agent 可以被同一套代码管理。同时使用工厂模式动态创建和配置这些 Provider,确保系统的扩展性和灵活性。

这就像生活中的分工,每个人都有自己的角色,只是这里把这种分工变成了代码架构而已。

根据 HagiCode 项目的实际使用经验,我们为每个 Agent 分配了不同的职责:

Agent提供商模型主要用途
ClaudeCodeCliAnthropicglm-5-turbo生成技术方案和Proposal
CodexCliOpenAI/Zedgpt-5.4执行精准的代码修改
CodebuddyCli智谱glm-4.7优化提案描述和文档
IFlowCli智谱glm-4.7归档提案和历史记录
OpenCodeCli--通用代码编辑
GitHubCopilotMicrosoft-辅助编程和代码补全

这种分工的背后逻辑是:每个 Agent 都有自己擅长的领域。Claude Code 在理解和分析复杂需求方面表现出色,所以让它负责前期的方案设计;Codex 在代码修改方面更精准,适合处理具体的实现任务;CodeBuddy 性价比高,用来优化文档再合适不过。

毕竟适合自己的才是最好的,条条大路通罗马,只是有的路好走一点,有的路稍微曲折一点罢了。

要让不同的 AI Agent 能够被统一管理,首先需要定义一套统一的接口。HagiCode 中定义了这个接口:

public interface IAIProvider
{
// 统一的 Provider 接口
Task<IAIProvider?> GetProviderAsync(AIProviderType providerType);
Task<IAIProvider?> GetProviderAsync(string providerName, CancellationToken cancellationToken);
}

这个接口看起来很简单,但它是整个多 Agent 系统的基石。通过统一的接口,我们可以无视底层是哪个公司的 AI 产品,都以相同的方式进行调用。

其实这就是把复杂的事情简单化了,毕竟简单才是美。

有了统一的接口,接下来就是如何创建这些 Provider 实例。HagiCode 使用了工厂模式:

private IAIProvider? CreateProvider(AIProviderType providerType, ProviderConfiguration config)
{
return providerType switch
{
AIProviderType.ClaudeCodeCli =>
ActivatorUtilities.CreateInstance<ClaudeCodeCliProvider>(_serviceProvider, Options.Create(config)),
AIProviderType.CodebuddyCli =>
ActivatorUtilities.CreateInstance<CodebuddyCliProvider>(_serviceProvider, Options.Create(config)),
AIProviderType.CodexCli =>
ActivatorUtilities.CreateInstance<CodexCliProvider>(_serviceProvider, Options.Create(config)),
AIProviderType.IFlowCli =>
ActivatorUtilities.CreateInstance<IFlowCliProvider>(_serviceProvider, Options.Create(config)),
_ => null
};
}

这里用到了依赖注入的 ActivatorUtilities.CreateInstance,它可以在运行时动态创建 Provider 实例,并且自动注入依赖项。这种设计的好处是:新增一个 Agent 类型时,只需要添加对应的 Provider 类,然后在工厂方法中加一个 case 分支即可,完全不需要修改现有代码。

这也罢了,毕竟谁愿意每次加新功能都要改一堆旧代码呢。

为了让配置更灵活,我们还实现了类型映射机制:

public static AIProviderTypeExtensions
{
private static readonly Dictionary<string, AIProviderType> _typeMap = new(
StringComparer.OrdinalIgnoreCase)
{
["ClaudeCodeCli"] = AIProviderType.ClaudeCodeCli,
["CodebuddyCli"] = AIProviderType.CodebuddyCli,
["CodexCli"] = AIProviderType.CodexCli,
["IFlowCli"] = AIProviderType.IFlowCli,
// ...更多类型映射
};
}

这个映射表的作用是将字符串形式的 Provider 名称转换为枚举类型。这样一来,配置文件就可以使用直观的字符串名称,而代码内部则使用类型安全的枚举进行处理。

毕竟配置这东西,越直观越好,谁愿意记一堆复杂的代码呢。

实际使用时,只需要在 appsettings.json 中配置即可:

AI:
Providers:
Providers:
ClaudeCodeCli:
Enabled: true
Model: glm-5-turbo
WorkingDirectory: /path/to/project
CodebuddyCli:
Enabled: true
Model: glm-4.7
CodexCli:
Enabled: true
Model: gpt-5.4
IFlowCli:
Enabled: true
Model: glm-4.7

每个 Provider 都可以独立配置开关、模型版本、工作目录等参数。这种设计既保证了灵活性,又便于管理和维护。

其实配置文件就像人生的选项,你可以选择开启或关闭某些功能,只是代码里的选择更容易后悔罢了。

有了统一的技术架构,接下来就是如何让多个 Agent 协同工作了。HagiCode 设计了一套任务流转机制,让不同的 Agent 处理不同阶段的任务:

提案创建 (用户)
[Claude Code] ──生成提案──▶ 提案文档
│ │
│ ▼
│ [Codebuddy] ──优化描述──▶ 优化后提案
│ │
│ ▼
│ [Codex] ──执行修改──▶ 代码变更
│ │
│ ▼
└───────────────▶ [iFlow] ──归档──▶ 历史记录

这种分工的好处是:每个 Agent 只需要专注于自己擅长的任务,不需要”什么都会”。Claude Code 负责从无到有生成提案,Codebuddy 负责把提案描述得更清晰,Codex 负责把提案变成实际的代码变更,iFlow 则负责把这些变更归档保存。

其实这就像生活中的团队合作,每个人都有自己的角色,合起来才能完成一件大事。只是这里的团队成员是 AI 而已。

在实际运行中,我们总结了以下几点经验:

1. Agent 选择策略很重要

不是随便分配任务,而是要根据每个 Agent 的特长来分配:

  • 提案生成:使用 Claude Code,因为它有更强的上下文理解能力
  • 代码执行:使用 Codex,因为它在代码修改方面更精准
  • 提案优化:使用 Codebuddy,因为它的性价比高
  • 归档存储:使用 iFlow,因为它稳定可靠

毕竟让合适的人做合适的事,这是千古不变的道理。

2. 配置隔离确保稳定性

每个 Agent 的配置独立管理,支持环境变量覆盖,工作目录也相互独立。这样一来,一个 Agent 的配置出错不会影响到其他 Agent。

这就像生活中的界限,每个人都有自己的空间,互不干扰才能和谐共处。

3. 错误处理机制

单个 Agent 失败不应该影响整体流程。我们实现了降级策略:当某个 Agent 执行失败时,系统可以自动切换到备用方案,或者直接跳过该步骤继续执行后续任务。同时,完整的日志记录也便于事后排查问题。

毕竟谁也不能保证永远不会出错,关键是怎么处理错误。这就像人生,总会遇到挫折,重要的是怎么走出来。

4. 监控与可观测性

通过 ACP 协议(我们自定义的通信协议,基于 JSON-RPC 2.0),可以追踪每个 Agent 的执行状态。会话隔离确保了并发安全,动态缓存则优化了性能表现。

毕竟看不见的东西最容易出问题,有点监控总好过两眼一抹黑。

采用这套多 Agent 协作配置后,HagiCode 项目的开发效率有了明显提升。具体表现在:

  1. 任务处理能力翻倍:以前一个 Agent 需要同时处理多种任务,现在可以并行处理,吞吐量翻倍不止
  2. 输出质量更稳定:每个 Agent 只专注于自己擅长的任务,输出结果的一致性和质量都更高
  3. 维护成本降低:统一的接口和配置管理,让整个系统更容易维护和扩展
  4. 新增 Agent 简单:如果要接入新的 AI 产品,只需要实现接口、添加配置,不需要修改核心逻辑

这套方案不仅解决了 HagiCode 自身的问题,也证明了多 Agent 协作确实是一种可行的架构选择。

其实效果还挺明显的,只是过程有点折腾罢了。

本文分享了 HagiCode 项目在多 Agent 协作配置方面的实践经验。核心要点包括:

  1. 标准化接口:通过 IAIProvider 统一不同 Agent 的行为,让代码可以无视底层是哪个公司的产品
  2. 工厂模式:使用 ActivatorUtilities.CreateInstance 动态创建 Provider 实例,支持运行时配置和依赖注入
  3. 协议统一:ACP 协议实现 Agent 间的标准化通信,基于 JSON-RPC 2.0 的双向通信机制
  4. 任务分流:合理分配任务给不同的 Agent,让它们各展所长,而不是试图让一个 Agent 做所有事情

这种设计不仅解决了”多 Agent 打架”的问题,还通过冒险团的任务流转机制,实现了开发流程的自动化和专业化。

如果你也在考虑引入多个 AI 劏手,希望本文能给你一些参考。当然,每个项目的情况不同,具体方案还需要根据实际情况调整。毕竟没有放之四海而皆准的方案,适合自己的才是最好的。

美的事物或人,不一定要占有,只要她还是美的,自己好好看着她的美就好了。技术方案也是如此,适合自己的,就是最好的…


如果本文对你有帮助,欢迎来 GitHub 给个 Star,您的支持是我们继续分享的动力。公测已开始,欢迎安装体验。


感谢您的阅读,如果您觉得本文有用,快点击下方点赞按钮,让更多的人看到本文。

本内容采用人工智能辅助协作,经本人审核,符合本人观点与立场。

HagiCode 平台的多 AI Provider 架构实践

HagiCode 平台的多 AI Provider 架构实践

Section titled “HagiCode 平台的多 AI Provider 架构实践”

本文分享了在 Orleans Grain 架构下,如何通过统一的 IAIProvider 接口集成 iflow 和 OpenCode 两个 AI 工具的技术方案,并详细对比了 WebSocket 和 HTTP 两种通信方式的实现差异。

其实也没什么特别的,就是做 HagiCode 的时候遇到了个挺实际的问题——用户想用不同的 AI 工具,这倒也不奇怪,毕竟每个人都有自己的习惯。有的喜欢 Claude Code,有的钟爱 GitHub Copilot,还有些团队用自己开发的工具。

最开始的方案也挺简单粗暴的,就是给每个 AI 工具写专门的对接代码。可后来问题就来了——代码里全是 if-else,改一个地方要到处测试,新工具接入还得重新写一堆逻辑,想想都觉得累。

后来我想明白了,不如做一个统一的 IAIProvider 接口,把所有 AI 提供者的能力都抽象出来。这样,不管底层用的是哪个工具,对上层来说都是一样的调用方式,岂不美哉?

最近项目要接入两个新工具:iflow 和 OpenCode。这两个都支持 ACP 协议,但通信方式不太一样——iflow 用 WebSocket,OpenCode 用 HTTP API。这也算是种考验吧,要在统一的接口下适配两种不同的通信模式,不过想想也挺有意思的。

本文分享的方案来自我们在 HagiCode 项目中的实践经验。HagiCode 是一个基于 Orleans Grain 架构的 AI 辅助开发平台,通过统一的 IAIProvider 接口与不同的 AI 提供者集成,让用户可以灵活选择自己喜欢的 AI 工具。

首先,定义了 IAIProvider 接口,把所有 AI 提供者需要实现的能力都抽象出来:

public interface IAIProvider
{
string Name { get; }
bool SupportsStreaming { get; }
ProviderCapabilities Capabilities { get; }
Task<AIResponse> ExecuteAsync(AIRequest request, CancellationToken cancellationToken = default);
IAsyncEnumerable<AIStreamingChunk> StreamAsync(AIRequest request, CancellationToken cancellationToken = default);
Task<ProviderTestResult> PingAsync(CancellationToken cancellationToken = default);
IAsyncEnumerable<AIStreamingChunk> SendMessageAsync(AIRequest request, string? embeddedCommandPrompt = null, CancellationToken cancellationToken = default);
}

这个接口有几个关键方法:

  • ExecuteAsync:执行一次性的 AI 请求
  • StreamAsync:流式获取响应,支持实时展示
  • PingAsync:健康检查,验证 provider 是否可用
  • SendMessageAsync:发送消息,支持嵌入式命令

IFlowCliProvider:基于 WebSocket 的实现

Section titled “IFlowCliProvider:基于 WebSocket 的实现”

iflow 使用 WebSocket 进行 ACP 通信,整体架构是这样的:

IFlowCliProvider → ACPSessionManager → WebSocketAcpTransport → iflow CLI
动态端口分配 + 进程管理

核心流程也挺简单:

  1. ACPSessionManager 负责创建和管理 ACP 会话
  2. WebSocketAcpTransport 处理 WebSocket 通信
  3. 动态分配一个端口,用 iflow —experimental-acp —port 启动 iflow 进程
  4. 通过 IAIRequestToAcpMapper 和 IAcpToAIResponseMapper 做请求/响应的转换

来看看核心代码:

private async IAsyncEnumerable<AIStreamingChunk> StreamCoreAsync(
AIRequest request,
string? embeddedCommandPrompt,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
// 解析工作目录
var resolvedWorkingDirectory = ResolveWorkingDirectory(request);
var effectiveRequest = ApplyEmbeddedCommandPrompt(request, embeddedCommandPrompt);
// 创建 ACP 会话
await using var session = await _sessionManager.CreateSessionAsync(
Name,
resolvedWorkingDirectory,
cancellationToken,
request.SessionId);
// 发送提示词
var prompt = _requestMapper.ToPromptString(effectiveRequest);
var promptResponse = await session.SendPromptAsync(prompt, cancellationToken);
// 接收流式响应
await foreach (var notification in session.ReceiveUpdatesAsync(cancellationToken))
{
if (_responseMapper.TryConvertToStreamingChunk(notification, out var chunk))
{
if (chunk.Type == StreamingChunkType.Metadata && chunk.IsComplete)
{
yield return chunk;
yield break;
}
yield return chunk;
}
}
}

这里有几个设计上的注意点,也算是一些小心得:

  • 用 await using 确保会话正确释放,避免资源泄漏,毕竟资源这东西,不用了就该放归自然
  • 流式响应通过 IAsyncEnumerable 返回,天然支持异步流
  • Metadata 类型的 chunk 判断是否完成,确保完整接收响应

OpenCodeCliProvider:基于 HTTP API 的实现

Section titled “OpenCodeCliProvider:基于 HTTP API 的实现”

OpenCode 用 HTTP API 方式提供服务,架构略有不同:

OpenCodeCliProvider → OpenCodeRuntimeManager → OpenCodeClient → OpenCode HTTP API
OpenCodeProcessManager → opencode 进程管理

OpenCode 的特点是用 SQLite 数据库持久化会话绑定关系,这样可以支持会话恢复和提示词响应恢复,这倒是挺贴心的设计:

private async Task<OpenCodePromptExecutionResult> ExecutePromptAsync(
AIRequest request,
string? embeddedCommandPrompt,
CancellationToken cancellationToken)
{
var prompt = BuildPrompt(request, embeddedCommandPrompt);
var resolvedWorkingDirectory = ResolveWorkingDirectory(request.WorkingDirectory);
var client = await _runtimeManager.GetClientAsync(resolvedWorkingDirectory, cancellationToken);
var bindingSessionId = request.SessionId;
var boundSession = TryGetBinding(bindingSessionId, resolvedWorkingDirectory);
// 尝试使用已绑定的会话
if (boundSession is not null)
{
try
{
return await PromptSessionAsync(
client,
boundSession,
BuildPromptRequest(request, prompt, CreatePromptMessageId()),
request.Model ?? _settings.Model,
cancellationToken);
}
catch (OpenCodeApiException ex) when (IsStaleBinding(ex))
{
// 会话已过期,移除绑定
RemoveBinding(bindingSessionId);
}
}
// 创建新会话
var session = await client.Session.CreateAsync(new OpenCodeSessionCreateRequest
{
Title = BuildSessionTitle(request)
}, cancellationToken);
BindSession(bindingSessionId, session.Id, resolvedWorkingDirectory);
return await PromptSessionAsync(client, session.Id, ...);
}

这个实现有几个亮点,或者说几个有趣的地方:

  • 会话绑定机制:同一个 SessionId 会复用 OpenCode 会话,避免重复创建,省得浪费资源
  • 过期处理:检测到会话过期时自动清理绑定,旧的不去,新的不来
  • 数据库持久化:通过 SQLite 存储绑定关系,重启后仍然有效,有些东西记住了就是记住了
方面IFlowCliProviderOpenCodeCliProvider
通信方式WebSocket (ACP)HTTP API
进程管理ACPSessionManagerOpenCodeProcessManager
端口分配动态端口无端口(使用 HTTP)
会话管理ACPSessionOpenCodeSession
持久化内存缓存SQLite 数据库
启动命令iflow —experimental-acp —portopencode
延迟更低(长连接)相对较高(HTTP 请求)

选择哪种方式主要看你的需求:WebSocket 适合实时性要求高的场景,HTTP API 则更简单、更容易调试。这就像选路一样,有的路快一点,有的路好走一点罢了。

先在配置文件里启用这两个 provider:

AI:
Providers:
IFlowCli:
Type: "IFlowCli"
Enabled: true
ExecutablePath: "iflow"
Model: null
WorkingDirectory: null
OpenCodeCli:
Type: "OpenCodeCli"
Enabled: true
ExecutablePath: "opencode"
Model: "anthropic/claude-sonnet-4"
WorkingDirectory: null
OpenCode:
Enabled: true
BaseUrl: "http://localhost:38376"
ExecutablePath: "opencode"
StartupTimeoutSeconds: 30
RequestTimeoutSeconds: 120
// 通过 Factory 获取 provider
var provider = await _providerFactory.GetProviderAsync(AIProviderType.IFlowCli);
// 执行 AI 请求
var request = new AIRequest
{
Prompt = "请帮我重构这个函数",
WorkingDirectory = "/path/to/project",
Model = "claude-sonnet-4"
};
// 获取完整响应
var response = await provider.ExecuteAsync(request, cancellationToken);
Console.WriteLine(response.Content);
// 或者用流式响应
await foreach (var chunk in provider.StreamAsync(request, cancellationToken))
{
if (chunk.Type == StreamingChunkType.ContentDelta)
{
Console.Write(chunk.Content);
}
}
// 通过 Factory 获取 provider
var provider = await _providerFactory.GetProviderAsync(AIProviderType.OpenCodeCli);
var request = new AIRequest
{
Prompt = "请帮我分析这个错误",
WorkingDirectory = "/path/to/project",
Model = "anthropic/claude-sonnet-4"
};
var response = await provider.ExecuteAsync(request, cancellationToken);
Console.WriteLine(response.Content);

在启动或使用前,可以先检查 provider 是否可用:

var iflowResult = await iflowProvider.PingAsync(cancellationToken);
if (!iflowResult.Success)
{
Console.WriteLine($"IFlow 不可用: {iflowResult.ErrorMessage}");
return;
}
var openCodeResult = await openCodeProvider.PingAsync(cancellationToken);
if (!openCodeResult.Success)
{
Console.WriteLine($"OpenCode 不可用: {openCodeResult.ErrorMessage}");
return;
}

两个 provider 都支持嵌入式命令,比如 /file:xxx 这样的命令:

var request = new AIRequest
{
Prompt = "分析这个文件的问题",
SystemMessage = "你是一个代码分析专家"
};
await foreach (var chunk in provider.SendMessageAsync(
request,
embeddedCommandPrompt: "/file:src/main.cs",
cancellationToken))
{
Console.Write(chunk.Content);
}

IFlow 用 WebSocket 长连接,所以资源管理要特别注意:

  • 用 await using 确保会话正确释放,不用了就放手
  • 取消操作会触发进程清理
  • ACPSessionManager 支持最大会话数限制

OpenCode 的进程管理相对简单,OpenCodeRuntimeManager 会自动处理,省心不少。

两个 provider 都有完善的错误处理:

  • IFlow 的错误通过 ACP 会话更新传播
  • OpenCode 的错误通过 OpenCodeApiException 抛出
  • 建议在调用方捕获并处理这些异常,毕竟错误总会发生的
  • IFlow 的 WebSocket 通信比 HTTP 有更低的延迟
  • OpenCode 的会话复用可以减少 HTTP 请求开销
  • Factory 的缓存机制可以避免重复创建 provider
  • 高并发场景下,要关注进程数和连接数的限制,别到时候撑不住了

启动时会验证可执行文件路径,但运行时也可能出问题。PingAsync 是个好工具,可以验证配置是否正确:

// 启动时检查
var provider = await _providerFactory.GetProviderAsync(providerType);
var result = await provider.PingAsync(cancellationToken);
if (!result.Success)
{
_logger.LogError("Provider {ProviderType} 不可用: {Error}", providerType, result.ErrorMessage);
}

本文分享了 HagiCode 平台在集成 iflow 和 OpenCode 两个 AI 工具时的技术方案。通过统一的 IAIProvider 接口,实现了对不同通信方式(WebSocket 和 HTTP)的适配,同时保持了上层调用的一致性。

核心思路其实挺简单的:

  1. 定义统一的接口抽象
  2. 对不同实现做适配层
  3. 通过工厂模式统一管理

这样扩展性就很好,以后有新的 AI 工具要接入,只需要实现 IAIProvider 接口就行,不用改动太多现有代码。想想也挺合理的,就像搭积木一样,有统一的接口,想怎么拼都行。

如果你也在做多 AI 工具的集成,希望本文对你有帮助。不过话说回来,技术这东西,能帮到人就好,其他的也不必太在意…


如果本文对你有帮助: