掘金 人工智能 15小时前
解构 Coze Studio:为 AI Agent 实现微型 DBaaS 的架构艺术
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入剖析了 Coze Studio 的 memory 模块,揭示了其如何为 AI Agent 提供结构化、可扩展且安全的长期记忆。Coze 摒弃了传统的 JSON 存储方式,而是为每个“记忆体”动态创建独立的物理数据库表,实质上构建了一套“数据库即服务”(DBaaS)系统。文章详细介绍了其核心设计,包括一对一的表映射、物理与逻辑字段的解耦,以及通过双表(草稿/线上)实现的安全迭代机制。此外,还探讨了如何通过应用层限制和多租户架构规避“库表爆炸”的风险,以及分层架构和事件驱动的解耦艺术,为构建健壮的 AI Agent 记忆系统提供了宝贵的工程实践。

💡 **动态物理表实现结构化记忆**:Coze memory 模块的核心设计是将每个 AI Agent 的“记忆体”映射为一张独立的后端物理 MySQL 表。这种方式避免了传统 JSON 字段存储的无模式问题,为记忆提供了严格的数据类型、索引和约束,实现了真正的结构化存储,为Agent进行精确高效的数据查询奠定了基础。同时,每个记忆体拥有独立的物理表,也带来了天然的数据隔离,简化了数据权限和访问控制。

🔗 **逻辑与物理解耦的字段抽象**:为了规避直接使用用户字段名带来的 SQL 注入、关键字冲突等风险,Coze 采用了一层巧妙的抽象。用户定义的逻辑字段名(如“商品名称”)不会直接作为物理表的列名,而是被映射为内部的物理名称(如 `f_1`),并自动添加 `bstudio_id`、`bstudio_connector_uid` 等四个系统标准字段,用于内部管理。这种设计实现了用户逻辑视图与底层物理实现的完全解耦,提供了极高的安全性和灵活性。

🛡️ **“草稿”与“线上”双版本安全迭代**:为解决“活的”数据表安全迭代的挑战,Coze 采用了“草稿”与“线上”的双表模式。系统在创建之初会生成两张完全相同的物理表,一张用于草稿环境,一张用于线上环境。开发者可以在隔离的“草稿”表中进行任意数据增删改查和表结构调整,而不影响线上服务。调试完成后,通过“发布”操作将草稿版本的数据和结构变更同步到线上表,实现安全可靠的版本迭代。

⚖️ **规避“库表爆炸”的双重保险**:为了控制动态建表可能带来的“库表爆炸”风险,Coze 实施了“双保险”策略。第一层是应用层限制,如每个 Bot 最多创建 3 个记忆数据库、每个表最多 20 个字段、每个表最多 10 万行。第二层是面向未来的多租户架构,通过 `SpaceID` 字段实现租户隔离,可以将动态创建的表分散到各个租户独立的数据库实例中,从宏观上控制表的总规模。

🔄 **分层架构与事件驱动的解耦**:Coze memory 模块遵循经典的分层架构(API、Application、Domain、Infrastructure)和依赖倒置原则,实现了高内聚、低耦合。Application 层作为协调者,Domain 层封装核心业务规则,Infrastructure 层负责具体数据库操作。此外,通过事件驱动机制,在创建数据库后发布 `ResourceDomainEvent`,让其他模块(如 Search 模块)异步订阅并创建全文索引,避免了模块间的直接调用和紧耦合,提升了系统的响应速度和可扩展性。

👋 大家好,我是十三!

在我之前的文章中,从宏观上领略了 Coze Studio 优雅的架构设计。今天,我们将深入其中的数据存储模块——memory

如何让 AI Agent 拥有长期、结构化且可扩展的记忆?一个简单的键值存储显然无法满足复杂的业务需求。我们希望 Agent 能记住一张商品信息表,并能对其进行精确查询;我们希望 Bot 的知识库可以随时安全地迭代,而不影响线上服务。

在探索 Coze 的源码时,我发现 memory 模块提供了一个极其精巧的解决方案。它没有采用传统的、在单一巨大表中通过 JSON 字段存储数据的方式,而是大胆地为每一个“记忆体”动态地创建了一张独立的物理数据库表。

这种设计,实质上是在应用内部为 AI Agent 实现了一套“数据库即服务”(Database-as-a-Service, DBaaS)。这背后究竟隐藏着怎样的“黑科技”?让我们一起深入源码,一探究竟!

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 通过一层巧妙的抽象规避了这些风险。

这种逻辑字段到物理字段的映射关系,以及系统自动添加的标准字段,可以用下图清晰地展示:

graph TD    subgraph "用户视角:逻辑模型 (商品信息)"        L["            <b>商品ID</b> (Number)<br/>            <b>商品名称</b> (Text)<br/>            <b>价格</b> (Float)<br/>        "]    end    subgraph "系统视角:物理表 (table_xyz)"        P["            <b>--- 系统字段 (自动添加) ---</b><br/>            <b>bstudio_id</b> (BIGINT, PK)<br/>            <b>bstudio_connector_uid</b> (VARCHAR)<br/>            <b>bstudio_connector_id</b> (VARCHAR)<br/>            <b>bstudio_create_time</b> (TIMESTAMP)<br/>            <br/>            <b>--- 用户字段 (映射后) ---</b><br/>            <b>f_1</b> (BIGINT)<br/>            <b>f_2</b> (TEXT)<br/>            <b>f_3</b> (DOUBLE)<br/>        "]    end    L -- "转换与映射" --> P

用户定义的字段名(如 product_name)并不会直接用作列名,而是被映射为一个内部的物理名称,如 f_1, f_2

//`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"),},}}

这种设计将用户定义的“逻辑视图”和底层的“物理实现”完全解耦,提供了极高的安全性和灵活性。这个大胆的设计固然灵活,但这样动态建表,数据库不会爆炸吗?

风险与权衡:如何驾驭“库表爆炸”这匹野马?

任何不谈权衡的架构设计都是“耍流氓”。“动态建表”这个方案如果缺少约束,无疑会成为一场灾难。Coze 通过一个“双保险”策略,完美地驾驭了这匹野马。

第一层保险:明确粒度与应用层限制

首先,“记忆体”的粒度并非单条记忆,而是用户在界面上创建的一个完整的“记忆数据库”。动态建表的操作仅在此刻发生,频率很低。

更重要的是,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 实体通过 DraftIDOnlineID 来维护这两个版本的关联关系。

// `api/model/crossdomain/database/database.go:117-144`*type Database struct {ID          int64// ...DraftID     *int64OnlineID    *int64// ...}

一个逻辑上的“记忆数据库”是如何通过元数据关联到两张独立的物理表,以及“发布”操作的实质,可以通过下图清晰地展现:

graph TD    subgraph "用户视角"        MemoryDB[记忆数据库: 商品信息]    end    subgraph "物理实现 (MySQL)"        OnlineTable(线上物理表<br>table_1001)        DraftTable(草稿物理表<br>table_1002)    end        subgraph "元数据表"        Meta(元数据记录<br>ID: db_abc<br>OnlineID: table_1001<br>DraftID: table_1002)    end    MemoryDB -- 读写 --> Meta    Meta -- 线上环境读 --> OnlineTable    Meta -- 开发环境读写 --> DraftTable    DraftTable -- "发布操作: 数据与结构同步" --> OnlineTable

这个设计的巧妙之处在于:

3. 架构之美:一次请求的优雅之旅

Coze memory 模块的实现,是经典分层架构和依赖倒置原则的优秀范例。让我们跟随一次“创建记忆”的请求,看看它是如何在清晰的层次间优雅地流转的。

调用链示意图:

graph TD    subgraph "API Layer"        A["table.AddDatabaseRequest"]    end    subgraph "Application Layer"        B["DatabaseApplicationService"]    end    subgraph "Domain Layer"        C["Database Service Interface"]    end    subgraph "Infrastructure Layer"        D["RDB Interface"]    end    subgraph "Database"        E["MySQL Database"]    end    A -->|"convert"| B    B -->|"d.DomainSVC.CreateDatabase()"| C    C -->|"physicaltable.CreatePhysicalTable()"| D    D -->|"mysqlService.CreateTable()"| E

这种设计使得系统高度解耦,核心业务逻辑稳定,易于测试和维护,并且未来可以方便地替换底层技术栈(例如,将 MySQL 更换为 PostgreSQL)。

4. 解耦的艺术:事件驱动的“广而告之”

ApplicationService 中,我们注意到一个细节:在成功创建数据库后,它会通过 eventbus 发布一个 ResourceDomainEvent 事件。

这是一个非常巧妙的异步解耦设计。memory 模块在完成自己的核心职责后,只是“广而告之”有一个新的记忆被创建了,它并不关心谁需要这个消息,也不需要等待后续处理完成。

系统的其他模块(例如 search 模块)可以订阅此类事件。当监听到新记忆创建的事件后,search 模块就可以在后台异步地为这张新表的数据创建全文检索引擎的索引,以便用户未来可以进行模糊搜索。

这种模式避免了模块间的直接调用和紧耦合,提升了系统的响应速度和整体可扩展性。

这个异步的事件发布与订阅流程,可以用下面的时序图清晰地展示出来:

sequenceDiagram    participant AppService as Application Service<br>(memory module)    participant EventBus as Event Bus    participant SearchModule as Search Module<br>(subscriber)    participant SearchIndex as Search Index    AppService->>+EventBus: Publish(ResourceDomainEvent)    Note right of AppService: "广而告之" - 我已完成创建,<br>不关心谁在监听。        EventBus-->>-SearchModule: Notify(event)    Note left of SearchModule: 监听到事件,开始异步处理。    SearchModule->>+SearchIndex: Update Index    SearchIndex-->>-SearchModule: OK

通过对 Coze memory 模块的源码进行深度挖掘,我们发现它远非一个简单的信息存储单元。它是一个设计精良、功能完备、企业级的“数据库即服务”系统,其核心设计思想和工程实践非常值得我们学习:

    动态物理表:为每个记忆实例创建独立物理表,实现了真正的结构化、高性能和数据隔离。版本化管理:通过“草稿-线上”双表模式,为 AI 应用开发提供了安全可靠的迭代和发布工作流。分层与抽象:经典的四层架构和依赖倒置原则,构建了高内聚、低耦合、易于维护的系统。异步与解耦:利用事件驱动机制,实现了模块间的异步通信,提升了系统的可扩展性。

Coze 的这一实现,为我们展示了如何为复杂的 AI Agent 构建一个既灵活又健壮的长期记忆系统,是后端架构设计的一次精彩演绎。


👨‍💻 关于十三Tech

资深服务端研发工程师,AI 编程实践者。
专注分享真实的技术实践经验,相信 AI 是程序员的最佳搭档。
希望能和大家一起写出更优雅的代码!

📧 联系方式569893882@qq.com
🌟 GitHub@TriTechAI
💬 VX:TriTechAI(备注:十三 Tech)

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Coze AI Agent Memory 数据库 架构设计 DBaaS
相关文章