1. 项目缘起:当AI项目需要“记忆”时
最近在折腾几个AI应用项目,从简单的智能客服到复杂的多模态内容生成平台,我发现一个绕不开的痛点:状态管理。这里的“状态”不是指前端组件的状态,而是指AI应用在运行过程中产生的、需要跨会话、跨任务甚至跨项目持久保存的数据。比如,一个AI对话助手需要记住用户的历史偏好;一个AI绘画工具需要保存用户自定义的风格模板;一个RAG(检索增强生成)系统需要维护不断更新的向量知识库。
这些数据,我们姑且称之为AI项目的“记忆”。它们的特点是:价值高、增长快、访问频繁、结构多样。传统的解决方案,比如直接写文件到服务器本地、或者用单一的关系型数据库(如MySQL)来存,很快就遇到了瓶颈。文件管理混乱,难以分布式部署;关系型数据库在面对非结构化的向量数据、大段的对话历史JSON时,不仅性能堪忧,Schema设计也成了噩梦。
于是,我开始寻找一种能够统一管理这些“记忆”的系统。它需要像保险库(Vault)一样安全、可靠,又能灵活容纳各种形态的“资产”。这就是我着手设计和实现这个“Vault跨项目持久化存储系统”的初衷。它不是一个具体的、像Redis或MinIO那样的开源软件,而是一套基于现有成熟组件构建的、针对AI应用场景优化的存储架构方案。简单说,我想打造一个专属于AI项目的“数据中枢”,让不同的AI应用都能方便地存、取、管理自己的关键状态数据。
2. 核心需求拆解:AI项目存储的四大挑战
在动手之前,我花了大量时间分析AI项目对存储的真实需求。我发现,它们主要集中在以下四个方面,这也是Vault系统需要解决的核心挑战。
2.1 数据类型异构:从JSON到向量
AI项目产生的数据五花八门。粗略分类就有:
- 结构化元数据:用户ID、会话ID、任务状态、时间戳等。这类数据适合用关系型数据库。
- 非结构化内容:对话的纯文本历史、AI生成的报告、用户上传的文档原文。这类数据通常以JSON、XML或纯文本形式存在,对全文检索有要求。
- 向量嵌入:这是AI时代的特产。通过Embedding模型将文本、图像等内容转换成的浮点数向量,用于相似性检索。它们通常是几百甚至上千维的数组,需要专门的向量数据库进行高效近似最近邻搜索。
- 二进制文件:AI生成的图片、音频、视频,或是用户上传的原始素材。这类数据体积可能很大,需要对象存储服务。
一个统一的存储系统,必须能优雅地处理这几种类型的数据,并建立它们之间的关联。
2.2 访问模式复杂:低延迟读取与高吞吐写入
AI应用的交互特性决定了其访问模式复杂。
- 对话型应用:要求极低的读取延迟(毫秒级),因为每次用户提问都需要立刻获取上下文历史。但写入压力相对较小,主要是追加新的对话轮次。
- 批处理任务:如模型训练数据预处理、大规模文档向量化。这类任务要求高吞吐的批量写入能力,对实时性要求不高。
- 检索增强场景:用户提问时,需要先在海量向量中做快速检索(高并发、低延迟的读),再将检索结果与问题一起送入大模型。这涉及向量数据库的读和对象存储/数据库的元数据读。
系统需要针对不同的数据类型和访问模式,匹配不同的底层存储引擎,并在逻辑上提供一个统一的入口。
2.3 数据关联与溯源:给AI的“思考”加上注释
AI的决策过程需要可解释。这意味着我们不仅要知道它输出了什么,还要知道这个输出是基于哪些数据(“记忆”)产生的。例如:
- 一次回答引用了哪几份文档?
- 生成的图片是依据哪几个风格模板融合而成的?
- 本次推荐的依据是用户哪一段历史行为?
因此,存储系统需要维护数据之间的关联关系。当存入一份新数据(如一个向量)时,需要能方便地记录它来源于哪个原始文件、哪个处理任务。这要求存储层具备数据谱系管理的基本能力,通常通过唯一的标识符(如UUID)和关联表来实现。
2.4 跨项目共享与隔离
在团队开发中,我们可能有多个AI项目(如客服机器人、内容审核系统、内部知识库)。这些项目之间,有些数据是希望共享的(例如,公司级的通用知识库向量),有些数据则必须严格隔离(例如,不同客户的对话数据)。Vault系统需要提供灵活的数据命名空间(Namespace)或租户(Tenant)隔离机制,在保证安全性的前提下,避免数据孤岛,促进有价值数据的复用。
3. 架构设计与技术选型:构建AI的数据保险库
基于以上需求,我设计了一套分层、解耦的Vault系统架构。其核心思想是:统一门面,异构存储,索引关联。
[AI 应用 A] [AI 应用 B] [AI 应用 C] | | | +---------+---------+ | [Vault 统一服务层 (API Gateway + 元数据管理)] | +-------+-------+-------+ | | | [关系型数据库] [向量数据库] [对象存储] (PostgreSQL) (Milvus) (MinIO) | | | (元数据、关系) (向量数据) (原始文件)3.1 统一服务层:Vault-API
这是整个系统的“大脑”和“门面”。它对外提供一组统一的RESTful API或gRPC接口,定义了几个核心操作:
PUT /vault/entity:存储一个实体(如一份文档、一条对话)。请求体中包含元数据、文本内容,并可选择是否同步生成向量。GET /vault/entity/{id}:根据ID获取实体完整信息。POST /vault/search:执行混合搜索。可以传入文本进行向量相似性检索,也可以传入结构化条件进行过滤。PUT /vault/relation:建立两个实体之间的关系(如“文档A包含段落B”)。
技术选型:我选择了Go语言来编写Vault-API。原因在于Go的并发性能优异,非常适合作为高并发的API网关;其静态编译、部署简单的特性也符合云原生趋势。Web框架选用Gin,轻量且高效。这一层不直接持久化数据,而是作为协调者。
3.2 元数据存储:PostgreSQL + JSONB
所有实体的核心信息(ID、类型、创建时间、所属项目/租户)以及实体之间的关系,都需要一个可靠的、支持事务的存储。我选择了PostgreSQL。
- 结构化查询优势:对于租户隔离、按时间范围查询、状态统计等需求,SQL的强大表现力无可替代。
- JSONB字段:这是关键。对于非结构化的元数据(如文档的作者、标签、自定义属性),我们可以直接存入JSONB字段。PostgreSQL支持对JSONB进行索引和部分查询,在保持灵活性的同时,提供了不错的查询性能。这样,我们就不需要为每类实体都设计固定的表结构。
- 数据关联:通过额外的“关系表”,可以清晰地记录实体之间的各种联系(如“引用”、“来源”、“版本”等),为数据溯源打下基础。
表结构示例:
CREATE TABLE entities ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id VARCHAR(64) NOT NULL, -- 租户隔离 type VARCHAR(32) NOT NULL, -- 实体类型,如 'document', 'conversation', 'image' metadata JSONB, -- 非结构化元数据 content_hash VARCHAR(64), -- 指向对象存储中文件内容的哈希 vector_id VARCHAR(128), -- 指向向量数据库中对应向量的ID created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_entities_tenant_type ON entities(tenant_id, type); CREATE INDEX idx_entities_metadata ON entities USING GIN (metadata); -- 为JSONB创建GIN索引 CREATE TABLE relations ( id SERIAL PRIMARY KEY, src_entity_id UUID NOT NULL REFERENCES entities(id) ON DELETE CASCADE, dst_entity_id UUID NOT NULL REFERENCES entities(id) ON DELETE CASCADE, relation_type VARCHAR(32) NOT NULL, -- 关系类型,如 'contains', 'generated_from' properties JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );3.3 向量数据存储:Milvus
这是为AI能力量身定制的部分。当Vault-API接收到需要向量化的文本时,它会调用集成的Embedding模型(如OpenAI的text-embedding-ada-002,或开源的BGE、Sentence-Transformer)生成向量,然后将向量存入Milvus。
- 选型理由:对比了Pinecone(云服务,贵)、Qdrant(Rust编写,性能好)和Weaviate(内置多模态),我最终选择了Milvus。原因在于其成熟度高(已进入2.0稳定版本),社区活跃,支持分布式部署,并且与云原生生态(Kubernetes)结合得很好。它专门为大规模向量检索设计,性能指标出色。
- 数据同步:在Milvus中创建一个Collection(类似表),其Schema包含向量字段和用于过滤的标量字段(如
entity_id,tenant_id)。存入向量时,必须包含对应的entity_id,这样才能与PostgreSQL中的元数据记录关联起来。Milvus支持通过tenant_id进行分区,天然实现多租户隔离。
3.4 原始文件存储:MinIO
对于图片、PDF、音频等原始二进制文件,对象存储是最佳选择。我选择了开源的MinIO,它兼容Amazon S3协议,可以私有化部署,性能和可靠性都经过验证。
- 存储逻辑:文件上传后,MinIO返回一个唯一的名字(或路径)。我们在PostgreSQL的
entities表中,通过content_hash或一个object_key字段来记录这个位置。通常,我们会以tenant_id/entity_id/file_name的格式来组织存储路径,实现逻辑上的隔离。 - 成本与性能:对象存储适合存放大文件,成本低于数据库,并且可以通过CDN加速访问。MinIO也支持版本控制,对于需要追溯文件变更的场景很有用。
4. 核心流程实现:一次完整的“记忆”存取
理论说再多,不如看一次完整的流程。假设一个AI文档问答应用,需要处理用户上传的一份PDF手册,并将其内容纳入知识库。
4.1 数据入库流程
- 应用调用:AI应用调用
PUT /vault/entity,将PDF文件、元数据(如标题、作者)和租户信息发送给Vault-API。 - 文件存储:Vault-API先将PDF文件上传至MinIO,获得一个
object_key(如tenant_abc/doc_12345/original.pdf)。 - 元数据记录:在PostgreSQL的
entities表中插入一条新记录,类型为document,content_hash字段记录文件哈希,object_key字段记录MinIO路径,其他元数据存入metadataJSONB字段。插入后获得一个生成的UUID,假设为doc-uuid-1。 - 内容处理与向量化(异步):Vault-API触发一个异步任务。这个任务会: a. 从MinIO下载PDF。 b. 使用PyPDF2或pdfplumber等库进行文本提取。 c. 使用LangChain的
RecursiveCharacterTextSplitter将长文本分割成语义连贯的短片段(如500字一段)。 d. 对每个文本片段,调用Embedding模型生成向量。 e. 将每个文本片段作为新的entity(类型为text_chunk)存入PostgreSQL,并建立它与父文档(doc-uuid-1)的contains关系。 f. 将每个文本片段对应的向量,连同其entity_id和tenant_id,批量插入Milvus的对应Collection。 - 状态更新:异步任务完成后,更新父文档
entity的状态为processed。
注意:步骤4的异步化至关重要。PDF解析和向量化可能是耗时操作,绝不能阻塞API的同步响应。我使用Redis作为任务队列,配合Celery或直接使用Go的协程池来实现。API只需快速响应“已接收”,后续处理在后台完成。
4.2 数据检索流程
当用户提问“如何重启设备?”时,AI应用需要从知识库中查找相关信息。
- 问题向量化:应用先将问题文本发送给Vault-API的专用端点,或直接使用相同的Embedding模型在本地生成向量。
- 混合检索:应用调用
POST /vault/search,传入问题向量和过滤条件(如tenant_id=tenant_abc,type=text_chunk)。 - Vault-API协调: a.向量检索:将问题向量和过滤条件发给Milvus,执行近似最近邻搜索(ANN),返回最相似的K个(比如5个)向量片段的
entity_id列表和相似度分数。 b.元数据补全:用这些entity_id列表,去PostgreSQL中批量查询对应的完整元数据,包括它们所属的原始文档信息(通过关系表查询)。 c.内容获取:根据需要,再根据元数据中的object_key,从MinIO获取原始文档或文本片段内容。 - 返回结果:Vault-API将检索到的文本片段、来源文档信息、相似度分数等打包,返回给AI应用。应用将这些信息作为上下文,与大模型提示词组合,生成最终答案。
这个流程实现了“向量检索找位置,关系数据库补信息,对象存储取内容”的高效协作。
5. 部署、运维与踩坑实录
设计很美,落地很痛。将这套系统部署到生产环境,并让多个AI项目接入,过程中遇到了不少典型问题。
5.1 部署拓扑与网络考量
我采用Docker Compose进行开发环境部署,生产环境则使用Kubernetes。
- 关键点:PostgreSQL、Milvus、MinIO、Redis(队列)和Vault-API服务之间的网络延迟必须尽可能低。它们会在一次请求中频繁通信。在K8s中,我将它们部署在同一个节点或具有高速网络连接的可用区内,并使用Service进行内部发现。
- 资源隔离:Milvus和PostgreSQL比较吃内存和CPU。需要根据数据量预估资源,并设置合理的requests和limits。特别是Milvus,其索引构建过程是计算密集型。
5.2 数据一致性挑战
这是分布式系统永恒的话题。在我们的架构中,一个实体的信息被拆到了三个地方:元数据在PostgreSQL,向量在Milvus,文件在MinIO。如何保证它们的一致性?
- 最终一致性是主流选择:我们接受在极短时间窗口内,数据可能不一致。例如,PDF文件已存入MinIO,PostgreSQL记录已生成,但向量化任务还在队列中未执行。此时检索,可能找不到该文档的向量。
- 采用状态机管理:在PostgreSQL的
entities表中增加一个status字段,如pending,processing,processed,failed。应用检索时,可以过滤掉非processed状态的数据。Vault-API在返回数据时,也可以明确告知应用该条数据的准备状态。 - 补偿与重试机制:对于失败的异步任务(如向量化失败),需要有监控和告警,并设计自动重试或手动触发的补偿任务。我使用Redis存储失败任务的信息,并有一个后台守护进程定期扫描并重试。
5.3 向量数据库的调优陷阱
Milvus性能虽好,但配置不当就是灾难。
- 索引类型选择:Milvus支持IVF_FLAT、HNSW等多种索引。HNSW查询速度快,但建索引慢、内存占用高;IVF_FLAT是均衡之选。对于千万级以下的数据量,HNSW是不错的选择。关键是要在插入数据前就创建好索引,否则先插入大量数据再建索引会阻塞很久。
nprobe参数:在搜索时,nprobe参数控制搜索的广度。值越大,精度越高,但速度越慢。需要通过实际查询进行压测,找到一个精度和延迟的平衡点。我们的经验是,对于大多数问答场景,nprobe在16到64之间通常足够。- 分区管理:我们按
tenant_id进行分区。但要注意,如果某个租户的数据量暴涨,可能会造成分区不均。需要定期监控,或者采用按时间+租户的复合分区策略。
5.4 安全与权限控制
跨项目存储,安全是生命线。
- API鉴权:Vault-API本身需要强认证。我集成了JWT(JSON Web Token)。每个AI项目(客户端)持有自己的密钥,用于生成访问令牌。令牌中包含了租户(
tenant_id)信息。 - 租户数据隔离:这是最核心的。所有数据库查询和存储操作,都必须显式带上
tenant_id过滤条件。在Vault-API层,从JWT令牌中解析出当前请求的tenant_id,并将其作为所有下游查询(PostgreSQL的WHERE子句、Milvus的过滤表达式、MinIO的路径前缀)的强制条件。绝对不能在代码中出现任何不带租户过滤的“全表扫描”。 - MinIO存储桶策略:为每个租户创建独立的存储桶(Bucket),或者使用前缀策略,并通过MinIO的STS(安全令牌服务)或预设策略,实现桶级别的权限控制,防止租户越权访问他人文件。
6. 价值与展望:不止于存储
这套Vault系统运行一段时间后,其价值超出了最初的“持久化存储”范畴。
- 降低了AI应用开发复杂度:应用开发者不再需要关心数据存哪里、怎么关联。他们只需要调用简单的API,就能实现复杂的“记忆”功能。团队可以更专注于AI模型和业务逻辑本身。
- 形成了可复用的AI数据资产:所有项目的“记忆”被集中管理后,我们发现了数据复用的巨大潜力。A项目标注好的高质量数据,经过脱敏后可以用于B项目的模型微调。通用知识库向量对所有项目开放,避免了重复建设。
- 为AI运维提供抓手:通过分析Vault中存储的对话历史、用户反馈,我们可以更直观地评估AI模型的效果,发现bad case,进行定向优化。存储的数据成为了迭代AI能力的燃料。
当然,系统还有很大的演进空间。例如,我正在考虑引入Apache Kafka作为数据变更的流式管道。任何数据的增删改,都通过事件发出。这样,其他系统(如实时推荐、审计日志、数据仓库)可以订阅感兴趣的事件,实现更松耦合的生态。另一个方向是增强数据治理功能,比如自动化的数据生命周期管理(冷热分层、定期归档)、数据质量监控等。
构建这样一个系统,就像为AI项目打造了一个坚实的“数字地基”。它不直接产生智能,但所有智能的涌现,都离不开对“记忆”的高效组织与利用。在AI工程化的道路上,这类基础设施的完善程度,往往决定了应用能走多远、多稳。