大家好,我是十三!欢迎来到十三Tech。
AI Agent 的"记忆"该怎么设计?简单的键值存储撑不起复杂查询,传统大宽表又缺乏扩展性。Coze Studio 的 memory 模块给了一个大胆而精巧的答案——为每一个"记忆体"动态创建独立的物理 MySQL 表。这本质上是在应用内部实现了一套微型 DBaaS(Database-as-a-Service)。它怎么保证安全?怎么管理版本?怎么避免"库表爆炸"?这篇文章拆开源码逐一回答。
1. 核心设计:一个记忆,一张物理表
Coze memory 模块最核心的设计,是把用户的每一个"记忆数据库"映射为一张后端的物理 MySQL 表——相当于给每个需要记忆的 Agent 配一个专属、量身定制的数据库。创建请求到达领域服务时的核心逻辑长这样:
// `domain/memory/database/service/database_impl.go:83-117`
func (d databaseService) CreateDatabase(ctx context.Context, req *CreateDatabaseRequest) (*CreateDatabaseResponse, error) {
// 1. 将用户定义的逻辑字段转换为物理表的列定义
fieldItems, columns := physicaltable.CreateFieldInfo(req.Database.FieldList)
// 2. 调用基础设施层,动态创建一张物理的草稿表
draftPhysicalTableRes, err := physicaltable.CreatePhysicalTable(ctx, d.rdb, columns)
// ...
// 3. 同样地,再创建一张物理的线上表
onlinePhysicalTableRes, err := physicaltable.CreatePhysicalTable(ctx, d.rdb, columns)
// ...
// 4. 在元数据表中记录这次创建,并关联两张物理表
tx := query.Use(d.db).Begin()
// ...
_, err = d.draftDAO.CreateWithTX(ctx, tx, draftEntity, draftID, onlineID, draftPhysicalTableRes.Table.Name)
onlineEntity, err = d.onlineDAO.CreateWithTX(ctx, tx, onlineEntity, draftID, onlineID, onlinePhysicalTableRes.Table.Name)
// ...
err = tx.Commit()
return &CreateDatabaseResponse{
Database: onlineEntity,
}, nil
}
两个直接收益:真正的结构化——记忆不再是无模式的 JSON,而是有严格数据类型、索引和约束的真实表,精确查询和高效扫描都成立;天然的数据隔离——每个记忆的数据在物理上完全独立,权限和访问控制被极大简化。
物理与逻辑的解耦:优雅的字段抽象
直接拿用户输入的字段名当物理列名会带来 SQL 注入、关键字冲突等一连串问题。Coze 用一层抽象规避这些风险:用户定义的字段名(如 product_name)被映射成内部物理名(如 f_1),每张动态表还自动加上四个系统标准字段。
映射规则极其简单:
//`domain/memory/database/internal/physicaltable/physical.go:120-122`
func GetFieldPhysicsName(fieldID int64) string {
return fmt.Sprintf("f_%d", fieldID)
}
每张动态表自动获得四个系统字段,用于内部管理:
// `domain/memory/database/internal/physicaltable/physical.go:89-113`
func getDefaultColumns() []*entity3.Column {
return []*entity3.Column{
{
Name: database.DefaultIDColName, // "bstudio_id"
DataType: entity3.TypeBigInt,
NotNull: true,
AutoIncrement: true,
},
{
Name: database.DefaultUidColName, // "bstudio_connector_uid"
DataType: entity3.TypeVarchar,
NotNull: true,
},
{
Name: database.DefaultCidColName, // "bstudio_connector_id"
DataType: entity3.TypeVarchar,
NotNull: true,
},
{
Name: database.DefaultCreateTimeColName, // "bstudio_create_time"
DataType: entity3.TypeTimestamp,
NotNull: true,
DefaultValue: ptr.Of("CURRENT_TIMESTAMP"),
},
}
}
这种设计把用户的"逻辑视图"和底层"物理实现"彻底解耦——用户字段名怎么取都不会撞 SQL 关键字,物理列名怎么变也不影响用户接口。
风险与权衡:如何驾驭"库表爆炸"
任何不谈权衡的架构都是耍流氓。动态建表如果没约束,就是一场灾难。Coze 用"双保险"驾驭这匹野马。
第一层保险:明确粒度 + 应用层硬限制。"记忆体"的粒度不是单条记忆,而是用户在界面上创建的一个完整的"记忆数据库"——动态建表只在创建记忆库时发生,频率极低。更关键的是代码里的硬性限制:
// `application/memory/database.go:51-62`
func (d *DatabaseApplicationService) GetModeConfig(...) (*table.GetModeConfigResponse, error) {
return &table.GetModeConfigResponse{
// ...
MaxTableNum: 3, // 每个 Bot 最多 3 个记忆数据库
MaxColumnNum: 20, // 每个表最多 20 个字段
MaxRowNum: 100000, // 每个表最多 10 万行
}, nil
}
每个 Bot 最多 3 个记忆库,从根本上杜绝了"单个 Bot 无限建表"的可能。
第二层保险:多租户架构兜底。几乎所有相关操作里都能看到 SpaceID 字段,这是多租户隔离的关键。对于大型公有云部署,Coze 可以为每个租户(团队或企业)分配独立的数据库实例,把动态表分散到各租户自己的库里,宏观上控制总规模。粒度限制解决"单点失控",多租户解决"全局爆炸"——两层加起来把风险压到了可控范围。
2. 安全的迭代:"草稿"与"线上"的双版本哲学
存储灵活性解决之后,下一个挑战是:怎么安全地迭代这些"活的"数据表?Coze 的答案是——为每次变更提供一个沙箱。系统在创建之初就生成两张结构完全相同的物理表:一张草稿、一张线上。Database 实体通过 DraftID 和 OnlineID 维护两者的关联:
// `api/model/crossdomain/database/database.go:117-144`*
type Database struct {
ID int64
// ...
DraftID *int64
OnlineID *int64
// ...
}
这套设计的收益很直接:开发者在草稿表里任意改数据、调结构,对正在服务的 Bot 毫无影响;调试完成后执行"发布"操作,把草稿表的数据和结构变更原子化同步到线上表,完成一次安全可靠的版本迭代。本质上是把软件工程里"开发-生产"环境分离的成熟实践,迁移到了数据表层面。
3. 架构之美:一次请求的优雅之旅
Coze memory 模块是经典分层架构 + 依赖倒置的优秀范例。跟随一次"创建记忆"的请求,能清楚看到它如何在四层之间流转:
Application 层扮演协调者,编排业务流程并处理应用级任务(如发布领域事件):
// application/memory/database.go
func (d *DatabaseApplicationService) AddDatabase(...) (*table.SingleDatabaseResponse, error) {
// ...
res, err := d.DomainSVC.CreateDatabase(ctx, convertAddDatabase(req))
// ...
err = d.eventbus.PublishResources(ctx, &searchEntity.ResourceDomainEvent{...})
// ...
}
Domain 层是业务灵魂,定义 Database 领域服务接口、封装核心规则,但完全不关心底层是 MySQL 还是其他数据库。Infrastructure 层提供 rdb.RDB 接口的具体实现(mysqlService),把领域层的业务需求(如"创建一个表")翻译成具体的 SQL 并执行——它的实现是通用的,能操作任何表,不是为特定业务写死的 DAO。
这种切分让核心业务逻辑稳定、易于测试,未来要换底层技术栈(MySQL → PostgreSQL)也只需要替换 infra 实现,业务代码一行不动。
4. 解耦的艺术:事件驱动的"广而告之"
ApplicationService 里有个值得拆开看的细节:成功创建数据库后,它会通过 eventbus 发布一个 ResourceDomainEvent 事件。这是一套精巧的异步解耦设计。
memory 模块完成核心职责后只是"广而告之"——不关心谁在听、也不等待后续处理。系统的其他模块(比如 search)订阅这类事件,监听到新记忆创建后在后台异步为这张新表的数据建全文检索索引,让用户后续可以做模糊搜索。这种发布-订阅模式避免了模块间的直接调用和紧耦合,提升了响应速度和整体可扩展性——新增一个订阅者不需要改动发布方一行代码。代价是引入了最终一致性,需要事件幂等和重试机制兜底。
总结
Coze memory 模块远不是一个简单的信息存储单元,它是一套设计精良、功能完备的企业级"数据库即服务"系统。核心可借鉴的设计有四条:动态物理表实现真正的结构化与数据隔离;逻辑-物理字段映射保证安全与灵活;草稿-线上双版本支持安全迭代;事件驱动实现模块解耦。这些做法不只适用于 AI Agent 记忆系统,对任何"用户可定义数据结构"的场景都有直接参考价值。
关于十三Tech
资深服务端研发工程师,AI 编程实践者。 专注分享真实的技术实践经验,相信 AI 是程序员的最佳搭档。 希望能和大家一起写出更优雅的代码!
联系方式:569893882@qq.com GitHub:@TriTechAI VX:TriTechAI(备注:十三 Tech)
