1. 项目背景
- 本项目希望完成一个面向私有知识库问答场景的 Agent 系统。涉及到:
- Agent
- RAG
- Tool
- Knowledge Base
- Conversation Memory
2. 系统设计
2.1 用户角色
- 系统有两类用户,需要考虑用户、权限、会话、消息、知识库
1)普通用户
主要完成:
- 登录
- 聊天
- 知识库问答
- 工具调用
- 查看历史会话
- 删除自己的会话
2)管理员
额外功能:
- 上传知识库文档
- 查看知识库
- 删除文档
2.2 数据流
- 系统围绕两条链路运行
1)用户问答链路
1 | 用户问题 |
2)文档入库链路
1 | 管理员上传文档 |
2.3 数据库设计
2.3.1 向量数据库设计
- 向量数据库作为系统里面的一个功能,需要提前设计好,他还会影响后面的数据库设计。
1)文档分块策略
- 采用三级分块策略:
1 | L1 大块 |
- 检索使用小的 L3,因为小块更容易精准匹配问题。但是最终回答可能需要更完整的上下文,因此要能够根据小块索引到大块(父块),只需要向量化L3,这个决定直接影响后面的数据库设计。
- 举例: 假设三级分块策略是:
1 | L1: chunk_size=2400, overlap=400 |
我有一个文档,以页为单位进行切分,假设第一页长度是4000,它会被切成两个L1:(0-2400)(2000-4000)。然后再把每个L1切成L2,得到:(0-1600)(1400-2400)(2000-3600)(3400-4000)。然后每个L2再切成L3。这里涉及到两个字段:chunk_id和chunk_idx。这个例子切分后得到的顺序:
1 | L1-0 |
这两个字段的编号逻辑:
1 | L1-0 |
2)检索策略
- 采用Hybrid Retrieval:
1 | Dense Retrieval |
3)存储结构
- 使用Milvus作为向量数据库,结合项目背景,设计如下Collection字段:
| 字段 | 类型 | 作用 |
|---|---|---|
id |
INT64 | 主键 |
dense_embedding |
FLOAT_VECTOR | 语义向量,1024 维 |
sparse_embedding |
SPARSE_FLOAT_VECTOR | BM25 使用的稀疏表示 |
text |
VARCHAR(65535) | L3 Chunk 原始文本 |
filename |
VARCHAR(255) | 来源文件名 |
file_type |
VARCHAR(50) | PDF、Word、Excel 等 |
file_path |
VARCHAR(1024) | 原始文件路径 |
page_number |
INT64 | 所在页 |
chunk_idx |
INT64 | Chunk 顺序 |
chunk_id |
VARCHAR(512) | 当前 Chunk 唯一 ID |
parent_chunk_id |
VARCHAR(512) | 对应 L2 Chunk ID |
root_chunk_id |
VARCHAR(512) | 对应 L1 Chunk ID |
chunk_level |
INT64 | Chunk 层级,这里正常为 3 |
2.3.2 关系数据库设计
1)用户表 users
| 字段 | 类型 | 约束/作用 |
|---|---|---|
id |
Integer | 主键 |
username |
String(100) | 唯一、索引、不能为空 |
password_hash |
String(255) | 保存密码 Hash,不保存明文密码 |
role |
String(20) | 用户角色,默认 user |
created_at |
DateTime | 创建时间 |
2)会话表 chat_sessions
| 字段 | 类型 | 约束/作用 |
|---|---|---|
id |
Integer | 主键 |
user_id |
Integer | 外键 |
session_id |
String(120) | 前端/业务使用的会话 ID |
metadata_json |
JSON | 会话扩展信息 |
updated_at |
DateTime | 最近更新时间 |
created_at |
DateTime | 创建时间 |
- 同时建立唯一约束:
1 | (user_id, session_id) UNIQUE |
原因是:不同用户可以存在相同的 session_id,但是同一个用户不能存在两个相同 session。
3)聊天消息表 chat_messages
每个 Session 包含多条 Message:
1 | Session |
| 字段 | 类型 | 约束/作用 |
|---|---|---|
id |
Integer | 主键 |
session_ref_id |
Integer | 外键,指向 chat_sessions.id |
message_type |
String(20) | human、ai、system 等 |
content |
Text | 消息正文 |
timestamp |
DateTime | 消息时间 |
rag_trace |
JSON,可空 | 保存该 AI 回答的 RAG 执行轨迹 |
- 当 Session 被删除时,对应消息也会被删除
1 | ChatSession |
rag_trace记录:
1 | 检索了哪些内容 |
4)父级 Chunk 表 parent_chunks
存 L1/L2
| 字段 | 类型 | 作用 |
|---|---|---|
chunk_id |
String(512) | 主键 |
text |
Text | Parent Chunk 完整文本 |
filename |
String(255) | 来源文件 |
file_type |
String(50) | 文件类型 |
file_path |
String(1024) | 文件路径 |
page_number |
Integer | 页码 |
parent_chunk_id |
String(512) | 上一级 Chunk ID |
root_chunk_id |
String(512) | L1 Chunk ID |
chunk_level |
Integer | 1 或 2 |
chunk_idx |
Integer | Chunk 顺序 |
updated_at |
DateTime | 更新时间 |
这就是数据库设计与 Auto-merging RAG 发生联系的地方。
2.3.3 Redis 缓存设计
- 频繁读取的数据需要缓存到内存
1)聊天历史
用户每发一条消息,Agent 都可能需要获取当前 Session 的历史:
1 | 用户发送消息 |
会产生很多重复数据库访问。因此缓存每个用户的对话记录,key定义为:
1 | supermew:chat_messages:{user_id}:{session_id} |
2)会话列表
用户打开左侧历史会话列表时,需要查询当前用户的所有session:
1 | chat_sessions:{user_id} |
因此定义一个key保存这个用户所有 Session 的摘要:
1 | supermew:chat_sessions:{user_id} |
3)Parent Chunk
一次查询查询出来很多的L3,然后为了扩展上下文,需要进一步多次查询L2/L1,如果多个L3都属于同一个L2,那就会把这个L2重复多次查询。因此缓存(key里面的id是L2/L1):
1 | parent_chunk:{chunk_id} |
3. 代码实现
3.1 数据存储模块
包括PostgreSQL、Redis 和 Milvus。
3.1.1 文件结构设计
backend/
env.py:加载项目环境变量。
backend/infra/
database.py:PostgreSQL 连接与初始化。engine:数据库引擎。SessionLocal:数据库 Session 工厂。Base:ORM 基类。init_db():初始化数据表。
cache.py:Redis 缓存封装。RedisCache:统一管理缓存读取、写入和删除。
backend/db/
models.py:定义 PostgreSQL 数据模型。User:用户信息。ChatSession:会话信息。ChatMessage:聊天消息及rag_trace。ParentChunk:保存 L1/L2 父级文本块。
backend/indexing/
milvus_client.py:Milvus 向量数据库封装。MilvusSettings:Milvus 配置。MilvusStore:L3 Chunk 的存储与检索,包含 Dense、BM25 和 Hybrid Retrieval。
parent_chunk_store.py:L1/L2 Parent Chunk 存储层。ParentChunkStore:负责 PostgreSQL 持久化及 Redis 缓存。
3.1.2 模块实现
backend/
1)env.py
1 | """从项目根目录加载 .env;由应用入口或独立脚本在 import 其它 backend 模块前调用一次。""" |
backend/infra/
1)database.py
1 | import os |
- 先根据
DATABASE_URL连接 PostgreSQL,再提供SessionLocal供后续代码执行数据库读写,并用Base统一管理db/models.py中定义的数据表;当调用init_db()时,会加载这些表定义并在 PostgreSQL 中创建尚不存在的表。
2)cache.py
1 | import json |
- 统一管理 Redis 缓存。需要缓存数据时,把数据转成 JSON 存进去;读取时再取出来恢复;支持删除单个缓存和批量删除缓存。为了避免不同功能的缓存互相冲突,所有 key 前面统一加
supermew:,缓存默认 5 分钟过期。如果 Redis 出问题,直接忽略,不影响主程序继续运行。
backend/db/
1)models.py
1 | from datetime import datetime |
- 定义关系数据库表结构
backend/indexing/
1)milvus_client.py
Milvus 的统一操作层。后面的代码不直接调用 Milvus,而是通过这个文件完成建库、写入、查询和检索。这个文件需要提供如下功能:
- 保存 Milvus 配置
- 读取配置环境
- 提供一个连接Milvus的客户端,用的时候连接,用完关闭
- 条件过滤处理
- 定义Milvus操作:
- 操作执行函数用于统一执行
- Collection 初始化与创建
- Collection 是否存在检查
- L3 Chunk 数据写入
- 条件查询
- 查询全部数据
- 根据
chunk_id查询指定 Chunk - Dense 向量检索
- Dense + BM25 + RRF 混合检索
- 删除指定数据
- 删除整个 Collection
保存 Milvus 配置
- 读取环境配置,创建实例
1 | QUERY_MAX_LIMIT = 16384 # 一次查询最多返回多少条数据 |
连接Milvus的客户端
1 |
|
条件过滤处理
1 | def _normalize_filter(filter_expr: str) -> str: |
定义Milvus操作
1 | """Milvus 访问层:无状态 Store + 短生命周期 gRPC 连接(避免长期持有失效 channel)。""" |
2)parent_chunk_store.py
L1/L2 Parent Chunk 的统一存储层。后面的代码不直接分别操作 PostgreSQL 和 Redis,而是通过这个文件统一完成父级 Chunk 的写入、读取和删除。这个文件主要提供:
- 数据转换功能
- 数据库字段转成字典
- 增删改功能:
- 增/改 L1/L2 Chunk,同步 Redis
- 删 L1/L2 Chunk,同步 Redis
- 查询父级 Chunk,同步 Redis
- Redis功能:
- 生成Redis Key
1 | """父级分块文档存储(用于 Auto-merging Retriever)""" |
3.1.3 模块测试
test/1_test_storage.py
1 | from dataclasses import replace |
- 如果刚insert或delete,下一毫秒就做query可能Milvus 的插入/删除结果还没立即可见,因此测试代码中间做了flush。实际过程中可以在模块实现部分加上flush,但是这样会有额外开销;也可以创建Collection 时增加一致性配置,不过这样查询延迟会更高。考虑到我们的业务场景,
管理员上传文档->写入milvus->后续用户再来检索。通常不是“刚 insert 完,下一毫秒就必须 query”,所以没必要让所有正常 RAG 查询都承担 Strong 的额外等待。