Langchain-Chatchat 数据库模型解析:KnowledgeFileModel 与 FileDocModel 实现知识文件与向量文档的双层管理
导读
在 Langchain-Chatchat 的 RAG(检索增强生成)链路中,知识库文件从「上传入库」到「向量化检索」需要一套可靠的元数据登记机制。本文深入解析项目 knowledge_file 与 file_doc 两张核心表的 ORM 模型 —— KnowledgeFileModel(知识文件档案)与 FileDocModel(文件-向量文档关系表),并结合源码讲解字段语义、版本更新策略、__repr__ 调试表示,以及围绕它们实现的增删查改 repository 函数。读完本文,你将完整理解 Langchain-Chatchat 是如何用关系型数据库管理知识文件生命周期、并在文件与其切分后的向量文档之间建立映射的。
模型概览:两张表如何分工
知识库向量化的产物最终要落到两类东西上:原始物理文件(PDF、DOCX、TXT 等,位于知识库目录中)和切分后写入向量库的 Document。Langchain-Chatchat 用两个 SQLAlchemy ORM 模型分别登记它们的元数据:
| ORM 类 | 对应数据库表 | 登记内容 | 核心源码位置 |
|---|---|---|---|
KnowledgeFileModel |
knowledge_file |
物理文件的档案信息(文件名、大小、版本、切分数量等) | knowledge_file_model.py |
FileDocModel |
file_doc |
文件与向量库 Document 的一对多映射 | 同上文件 |
两个模型都继承自 Base(declarative_base() 生成的声明式基类),表引擎绑定在 Settings.basic_settings.SQLALCHEMY_DATABASE_URI 指定的数据库上。可以这样理解两者的关系:一张 knowledge_file 记录对应磁盘上的一个文件;一份文件经文本切分后产生 N 个向量 Document,就会在 file_doc 中写入 N 条记录。
KnowledgeFileModel:知识文件的「身份证」
KnowledgeFileModel 定义于 knowledge_file_model.py,表名为 knowledge_file。它登记了文件从「入库」到「切分」全过程所需的全部静态信息与状态信息。
字段与类型约束
| 属性 | 列类型 | 默认值 | 含义 |
|---|---|---|---|
id |
Integer,主键,自增 |
— | 知识文件唯一标识 ID |
file_name |
String(255) |
— | 文件名 |
file_ext |
String(10) |
— | 文件扩展名,如 .pdf、.txt |
kb_name |
String(50) |
— | 所属知识库名称 |
document_loader_name |
String(50) |
— | 文档加载器名称(如 PDFLoader) |
text_splitter_name |
String(50) |
— | 文本分割器名称(如 SpacyTextSplitter) |
file_version |
Integer |
1 |
文件版本号 |
file_mtime |
Float |
0.0 |
文件最后修改时间(Unix 时间戳,秒) |
file_size |
Integer |
0 |
文件大小(字节) |
custom_docs |
Boolean |
False |
是否使用自定义切分文档 |
docs_count |
Integer |
0 |
切分后产生的文档数量 |
create_time |
DateTime |
func.now() |
记录创建时间 |
从列约束可看出几个工程要点:
- 类型即契约:
file_ext限制在 10 字符内、file_size以字节为单位的整型、file_mtime用Float存 Unix 时间戳。在更新文件信息时,务必按这些类型与约束传参,避免因类型不匹配(如把大小传成字符串)或违反约束导致数据写坏。 - 默认值由模型兜底:新文件版本从
1起步、file_size默认0、custom_docs默认False、create_time由数据库函数func.now()自动填充,业务代码无需手动维护。
与真实文件对象 KnowledgeFile 的对应
KnowledgeFileModel 的字段并非凭空而来,它们大多直接取自项目中的 KnowledgeFile 运行时对象。KnowledgeFile 在构造时通过 os.path.splitext 解析出扩展名、通过 get_LoaderClass(self.ext) 按扩展名推断 document_loader_name、并以 Settings.kb_settings.TEXT_SPLITTER_NAME 作为 text_splitter_name;其 get_mtime()(os.path.getmtime)与 get_size()(os.path.getsize)则直接对应模型的 file_mtime、file_size 两列。这说明该表是磁盘文件系统元数据在关系库中的镜像,为上层检索与统计提供不受文件系统限制的稳定查询入口。
FileDocModel:文件到向量文档的映射关系
FileDocModel 定义于同一文件 knowledge_file_model.py,表名为 file_doc。它的作用是把「逻辑文件名」与「向量库中的真实 Document ID」关联起来。
字段结构
| 属性 | 列类型 | 默认值 | 含义 |
|---|---|---|---|
id |
Integer,主键,自增 |
— | 唯一标识符 |
kb_name |
String(50) |
— | 文档所属知识库名称 |
file_name |
String(255) |
— | 文档对应的原始文件名 |
doc_id |
String(50) |
— | 向量库中文档的 ID |
meta_data |
JSON |
{} |
文档附加元数据 |
meta_data 的 JSON 语义
meta_data 是整张表中最灵活的字段。它以 JSON 列类型存储任意结构化附加信息(如 {"author": "张三", "year": "2021"}),使得后续可按任意元数据键做过滤检索。由于 Base 创建的 engine 显式注册了 json_serializer=lambda obj: json.dumps(obj, ensure_ascii=False),中文元数据也能被正确序列化而不被转义。
doc_id 通常形如 c3392f0c-xxxx 这样的字符串。需要注意它在 knowledge_file_repository.py 中会被显式 int(_id[0]) 转换为整型返回,因此向量库侧的 Document ID 在使用时应当与 file_doc.doc_id 保持一致的编码格式。
模型不是孤立存在的:repository 层的完整操作链
模型(Model)只是表结构的定义,真正读写它们的是 knowledge_file_repository.py 中的一组 repository 函数。全部函数都通过 @with_session 装饰器自动获得数据库会话,会话由 session.py 中的 with_session 统一管理:进入时开启 session_scope() 上下文、提交事务,异常时自动 rollback 并抛出。以下按「文件档案」与「文档映射」两类梳理。
围绕 KnowledgeFileModel 的操作
count_files_from_db(kb_name):统计某知识库的文件数量,用.filter(KnowledgeFileModel.kb_name.ilike(kb_name))后.count()。list_files_from_db(kb_name):列出某知识库所有文件名,返回[file_name, ...]列表。file_exists_in_db(kb_file):按file_name与kb_name精确判断文件是否已在库中(ilike匹配,返回布尔值)。add_file_to_db(kb_file, docs_count, custom_docs, doc_infos):核心入库函数,逻辑分支清晰(见下节)。delete_file_from_db(kb_file):删除单条文件记录,同时联动删除其全部file_doc映射,并将所属知识库的file_count减一。delete_files_from_db(knowledge_base_name):级联删除整个知识库下的所有knowledge_file与file_doc记录,并把KnowledgeBaseModel.file_count归零。get_file_detail(kb_name, filename):按文件定位记录并组装成包含全部字段的字典;不存在时返回空{}。
其中 delete_file_from_db 与 delete_files_from_db 展示了项目中最典型的级联删除约束:为避免出现「向量库中有文档、但档案表已无记录」或反之的脏数据,删除文件时总是成对清理 KnowledgeFileModel 与 FileDocModel,并同步维护知识库表的 file_count 计数,保证跨表数据一致性。这些计数与存在性查询正是 kb_service/base.py 中 file_exists_in_db、list_files_from_db、count_files_from_db 等服务方法的后端实现。
围绕 FileDocModel 的操作
list_file_num_docs_id_by_kb_name_and_file_name(kb_name, file_name):按知识库与文件名查出全部doc_id并转成整数列表。list_docs_from_db(kb_name, file_name=None, metadata={}):返回形如[{"id": str, "metadata": dict}, ...]的文档列表;支持按file_name过滤,还支持把metadata字典逐键下钻为FileDocModel.meta_data[k].as_string() == str(v)的 SQL 条件,即按元数据键过滤向量文档。add_docs_to_db(kb_name, file_name, doc_infos):批量把doc_infos(形如[{"id": str, "metadata": dict}])逐条构造成FileDocModel实例加入会话。源码中对doc_infos is None的防御分支提示读者:真实调用链中可能出现该参数为None的边界场景,调用方应自行保证传入非空。delete_docs_from_db(kb_name, file_name=None):删除符合条件的映射,并返回被删除的文档信息列表。
add_file_to_db 的版本自增策略
add_file_to_db 是理解整个文件生命周期管理的关键,其逻辑位于 knowledge_file_repository.py:
if existing_file:
existing_file.file_mtime = mtime
existing_file.file_size = size
existing_file.docs_count = docs_count
existing_file.custom_docs = custom_docs
existing_file.file_version += 1
else:
# 构造新 KnowledgeFileModel 并 kb.file_count += 1
即:同一知识库下重复上传同名文件不会新建记录,而是更新修改时间、大小、切分文档数并让 file_version 自增;首次入库则新建记录并累加知识库 file_count。无论新建还是更新,最后都会调用 add_docs_to_db 写入本次切分产生的文档映射。配合模型层 file_mtime(默认 0.0)、file_version(默认 1)的默认值设计,可以判断:file_version > 1 即代表该文件经历过二次向量化,非常适合作为缓存失效、增量更新的判断依据。
这些操作在哪里被触发
从源码结构看,这些 repository 函数并非仅被模型文档调用,而是贯穿整个知识库管理链路:
- 知识库服务层 kb_service/base.py 的
file_exists_in_db、list_files_from_db、count_files_from_db、list_docs_from_db等方法直接委托给上述函数; - 向量库服务(如 milvus_kb_service.py)在删除向量时使用
list_file_num_docs_id_by_kb_name_and_file_name反查doc_id列表; - API 层 kb_doc_api.py 在向用户返回文件详情时调用
get_file_detail,将模型字段整理成可读字典。
这意味着:每当用户在 WebUI 或 API 中上传文件、删除文件、列出文件或查看文件详情,背后最终都会落到 knowledge_file / file_doc 两张表的读写上。
repr:面向调试的字符串表示
两个模型都实现了 Python 特殊方法 __repr__,用于生成对象的「官方字符串表示」,在调试与日志中最常用。它们不接收额外参数,返回的字符串按固定模板拼接关键字段:
KnowledgeFileModel.__repr__(见 knowledge_file_model.py)覆盖 8 个字段:id、file_name、file_ext、kb_name、document_loader_name、text_splitter_name、file_version、create_time。例如:
<KnowledgeFile(id='1', file_name='example.pdf', file_ext='.pdf', kb_name='DefaultKB', document_loader_name='PDFLoader', text_splitter_name='SpacyTextSplitter', file_version='1', create_time='2023-04-01 12:00:00')>
FileDocModel.__repr__ 则覆盖 id、kb_name、file_name、doc_id、meta_data 五个字段,例如:
<FileDoc(id='1', kb_name='知识库1', file_name='文件1.pdf', doc_id='doc123', metadata='{"author": "张三", "year": "2021"}')>
两点使用提示:其一,__repr__ 输出应能快速反映对象状态但保持简洁,便于在 logger 或交互式终端中一眼定位问题对象;其二,由于 meta_data 是 JSON 类型,repr 中打印的是其字符串形态,肉眼核对时要注意多层引号的转义。
实践注意点与字段维护建议
综合模型定义与 repository 源码,使用这两个模型时有几点需要特别注意:
- 更新信息时同步维护版本与时间字段:
file_version、file_mtime、file_size、docs_count之间是强关联的 —— 文件内容一旦变化,mtime/size 必然变化,随之应触发一次重新切分与版本号自增。若只更新其中一列,将破坏数据的准确性与一致性。 - 删除必须成对进行:不要只删
knowledge_file记录而遗留file_doc映射,否则检索时可能出现「查得到 doc_id 却查不到来源文件」的幽灵数据;项目实现中delete_file_from_db/delete_files_from_db已示范了正确的级联清理方式。 - 查询统一走 repository 而非手写 SQL:项目为两张表封装了成套函数(存在性判断、计数、列出、详情、增删),且统一处理了会话提交与回滚。业务代码直接调用 knowledge_file_repository.py 即可避免 Session 泄漏与事务边界混乱。
- 注意
custom_docs的语义:该字段标记「是否使用自定义 docs」。当用户在界面上手工维护某文件的切分结果时,该字段会被置真,后续向量化逻辑需据此决定是否沿用系统默认切分流程。
关联阅读
- 模型定义全文:knowledge_file_model.py
- 数据库引擎与
Base声明:base.py - 会话管理与
@with_session装饰器:session.py - 全部增删查改 repository 函数:knowledge_file_repository.py
- 运行时文件对象
KnowledgeFile的定义与 mtime/size 读取逻辑:utils.py - 知识库服务层调用这些函数的入口:base.py
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00