RAG做到后面,很多人会发现一个奇怪的现象:Embedding模型换了好几个,Chunk策略调了一轮又一轮,Prompt模板改得都快认不出来了,可检索质量始终卡在80分上不去。问题不在模型,也不在提示词,而是在索引构建之前那个最容易被忽略的环节——Schema。
Schema这个词听起来像是数据库工程师的专属名词,但在RAG链路里,它的含义要宽得多。它不光是关系表里的字段定义,还包括元数据字段设计、向量库中的数据组织形式、知识图谱中的本体定义。可以说,RAG项目的上限由Embedding决定,下限却由Schema兜着。这一节的内容,就是把“1.4 RAG中的Schema”这件事彻底讲透:它到底在RAG链路里起什么作用,怎么设计才能避免后面反复返工,以及实际项目中那些文档里不会告诉你的坑。
1. 为什么要单独谈RAG中的Schema:整体定位与设计思路
1.1 RAG链路里Schema到底卡在哪一环
先把RAG的标准链路铺开:文档解析、清洗、分块、元数据抽取、向量化、入库,然后是召回、重排、生成。很多人下意识地把注意力放在“向量化”和“召回”这两步,因为这两步最直观,效果好不好一眼就能看到。但实际操作下来,你会发现一个规律:索引阶段没想清楚的东西,到了检索阶段一定会以诡异的方式暴露出来。
举个例子。你辛辛苦苦把几万份文档切好块、灌进向量库,上线之后运营同学说“我要按部门过滤”,你打开代码一看,发现当初压根没存部门这个字段。这时候能怎么办?要么全量重新解析一遍文档重新灌索引,要么在前端硬筛——可你连字段都没有,拿什么筛?这就是典型的Schema设计缺位。
Schema在RAG链路里的位置,本质上是连接非结构化数据和结构化检索规则的桥梁。文档解析完了,要抽哪些元数据字段,字段类型是什么,允许的取值有哪些,哪些字段参与检索过滤,哪些字段只是展示用,这些问题在索引构建之前就应该全部定下来。RAG流程里,Schema决定了两个关键动作:一是写库时数据以什么形态落盘,二是读库时系统按什么规则取数。写库和读库的规则如果不一致,轻则过滤失效,重则整条检索链路都跑偏。
1.2 三种Schema的分类与选型
RAG场景中的Schema,按数据形态可以分成三类,这三类也是这一节后续内容的骨架。
第一类是元数据Schema,描述文档本身的属性。比如标题、作者、发布日期、所属部门、文档类型、权限级别、标签。它挂在每个Chunk上面,召回后用来做过滤和路由。这是最常见的形态,几乎所有RAG项目都要做。
第二类是关系表Schema,也就是传统数据库里的表结构定义。适用于“用户提问,系统查库返回结构化结果”的场景,通常配合Text-to-SQL一起用。用户问“上季度华东区销售额是多少”,系统把问题转成SQL,去数据库里查,再把结果交给模型回答。这个场景里,表Schema的质量直接决定SQL生成得准不准。
第三类是知识图谱Schema,也叫本体定义。适用于GraphRAG或基于知识图谱的问答场景,需要定义实体类型、属性、关系类型和约束。比如“某公司内部的智能问答系统,把人员、项目、文档做成一张图,回答跨实体的复杂问题”。
三类Schema的定位差异很大,选型时主要看业务问题和数据形态。为了直观,我整理了一个对比表格:
| Schema类型 | 核心对象 | 落地位置 | 主要价值 | 典型问题 |
|---|---|---|---|---|
| 元数据Schema | 文档属性字段 | 向量库的标量字段或独立元数据库 | 检索前过滤、多路路由 | 字段缺失、命名混乱、类型不匹配 |
| 关系表Schema | 表、列、类型、注释、外键 | 关系型数据库 | 精确查询、聚合计算 | 列名歧义、枚举缺失、注释不完整 |
| 知识图谱Schema | 实体、属性、关系、约束 | 图数据库或GraphRAG索引 | 多跳推理、全局问答 | 节点爆炸、关系冗余、实体合并困难 |
很多RAG项目并不是只用一种Schema。比如一个企业知识库,文档问答走元数据过滤,统计报表走关系表查询,跨部门人员关系走图谱问答,三条线并存。这个时候,三类Schema要有统一的设计规范,否则各自为政,后期整合成本非常高。
1.3 Schema先行原则:先定结构再灌数据
做过几次RAG落地之后,我有个很深的体会:RAG项目里,Schema设计应该先于Chunk策略和Embedding选型。不少人习惯先跑通Demo,Embedding模型随便选一个,Chunk大小拍脑袋定个500,向量库建个Collection就往里灌数据。Demo阶段没事,一上真实数据就崩——各种元数据对不上、过滤字段不存在、文档版本混乱。最后只能全部删库重建。
我把这个原则叫“Schema先行”。启动RAG项目时,第一周不急着写代码,先把下面几个问题确认完:
- 业务上需要按哪些维度过滤检索结果?哪些维度是高频的?
- 有没有多租户或权限隔离的需求?谁来定义数据可见范围?
- 文档需要区分版本吗?新旧版本冲突时保留哪个?
- 检索结果里需要展示哪些辅助信息?这些信息从哪里来?
这几个问题全部落到纸面上,形成一份Schema设计文档,再开始做解析和索引构建。后面所有环节都围绕这份Schema展开。别嫌这一步慢,后面省下来的返工时间起码是十倍。
2. 元数据Schema:检索前最有效的围栏
2.1 从业务问题反推字段,而不是堆字段
元数据Schema最常见的误区,是字段越多越好。文档里的信息全抽一遍,标题、作者、创建时间、修改时间、文件大小、页数、审批人、关键词……全塞进去。结果检索的时候,真正用到的就那么三五个字段,剩下的既占存储,又增加解析的出错概率。
正确的做法是从业务问题反推字段。你把用户可能会问的问题列一遍,逐个拆解这些问题落到检索上需要什么约束条件。比如“查2024年发布的安全规范”,就需要发布时间字段;“找A部门最近的培训材料”,就需要部门字段和发布日期字段;“这份文档能不能给我的合作伙伴看”,就需要权限字段。
我在项目里一般会列一个字段设计Checklist:
- 稳定性标识字段:文档的唯一ID,必须幂等。推荐用UUID或内容哈希,不要用自增ID。
- 业务维度字段:部门、项目、业务线、标签,按实际业务来定。
- 生命周期字段:发布日期、生效日期、失效日期,过滤时非常常用。
- 权限控制字段:可见范围、密级、访问控制列表,多租户场景必备。
- 审计追踪字段:上传人、最后修改人、版本号,排查问题的时候靠它们还原现场。
字段类型的选择也要动脑子。日期字段坚决不用字符串,不然“2024-1-5”和“2024/01/05”会同时存在,排序和范围过滤全部错乱。枚举字段要定死取值范围,不能今天写“部门A”明天写“A部门”。布尔字段命名要能看懂,is_confidential就比confidential_flag清晰得多。
2.2 一个可落地的元数据Schema示例
说个我们实际用过的简化版元数据Schema,字段不多,但覆盖了过滤、路由、权限三类核心场景。用字段表展示:
| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| doc_id | string | 是 | 文档唯一标识,使用内容哈希,重建时不会产生重复数据 |
| org_id | string | 是 | 组织/租户标识,多租户硬隔离字段 |
| category | string | 否 | 文档类别,如“制度”“规范”“FAQ” |
| tag | array | 否 | 标签列表,用于多维度交叉过滤 |
| owner | string | 否 | 文档负责人,用于“查某人负责的材料”这类需求 |
| access_level | int | 是 | 密级字段,0公开、1内部、2机密,过滤时按阈值控制 |
| version | string | 是 | 版本号,如“v1.2”,决定多版本文档的取舍逻辑 |
| published_at | date | 否 | 发布日期,时间范围过滤的核心字段 |
| created_at | timestamp | 是 | 入库时间,排查问题时定位数据批次 |
这个Schema里有两个字段是我特别想说明的。一是doc_id用哈希而不是自增ID,这样同一份文档更新后重新解析,只要内容没变,生成的doc_id就不变,去重逻辑会简单很多。二是access_level用数字而不用字符串,“机密”“秘密”“内部”这种中文枚举看起来直观,但实际过滤时“机密”和“秘密”谁级别高?很难比较。数字可以直接做范围过滤,大于阈值的一方直接不返回,省掉一层逻辑判断。
2.3 过滤下推与多租户隔离的实现
元数据字段设计好之后,怎么在检索时用起来,是另一个关键问题。这里有一个概念必须搞清楚:前置过滤还是后置过滤。
前置过滤是在向量检索之前,先用元数据条件缩小候选集,再去里面做向量相似度匹配。比如用户是A部门的,那就先在org_id='dept_A'的范围内做向量检索。后置过滤则是先向量召回一批结果,再在应用层把不符合元数据条件的Chunk删掉。
后置过滤写起来简单,但埋着一个大坑:向量召回只找了全局最近的Top K,压根没考虑过滤之后还剩多少。如果过滤条件很苛刻,比如真机上百万份文档里只有几十篇属于目标部门,全局Top 20可能全都不在目标部门里,过滤完后返回空结果。这就是典型的召回率崩塌。
正确做法是尽量把元数据过滤下推到向量数据库里。主流的向量数据库都支持在相似度搜索时附带标量过滤条件,在底层就完成候选集裁剪。实在做不到,那就把元数据过滤和向量检索合并到一个混合查询里,用BM25之类的稀疏检索补一刀,别一条路走到黑。元数据Schema不光是字段定义,还决定了过滤能不能在底层执行。设计字段时就要考虑这个字段在后端怎么建索引、怎么参与Filter,别设计出来一堆没法检索的字段。
多租户隔离是另一个元数据Schema的典型应用。最简单的方案是每个租户独立的Collection,互不干扰;但租户数量一多,管理成本和存储成本都会膨胀。更常见的做法是共享Collection,通过org_id字段做强制过滤,再把org_id纳入主键的一部分,防止跨租户的数据串味。
3. 结构化查询中的表Schema:让模型自己写SQL
3.1 Text-to-SQL在RAG中的位置
文档类RAG解决的是“知识在哪、答案是什么”的问题,但对于“上季度各区域销售额排名”“最近一个月故障工单数量变化趋势”这类需要精确计算的问题,文档检索就使不上劲了。这就要用到结构化的RAG:用户输入自然语言,大模型把它转成SQL,去数据库里执行查询,返回结果后组装答案。
这条链路里,表Schema就是大模型的“眼睛”。模型压根不知道你的数据库里有哪些表、每个表里有什么列、列里存的是什么枚举值,它只能看到你喂给它的定义。你喂什么,它就基于什么写SQL。喂一份含糊其辞的表结构,写出来的SQL大概率也是含糊的。
我见过最典型的翻车现场:业务表里存的是数字状态码0和1,模型不知道这回事,用户问“有多少订单已支付”,模型写了WHERE status = '已支付',数据库里根本没有这个值,直接报错或者返回空。问题不在模型,而在Schema没有把枚举值映射说清楚。表Schema设计里的每个细节,最后都会反映在SQL质量上。
3.2 自解释的DDL设计要点
为了让大模型准确生成SQL,表Schema设计首先要遵循一条核心原则:自解释。也就是不依赖额外文档,只看表结构就能理解每个字段的业务含义。
列名是关键。表里的列名,能写清楚绝对不缩写。product_name就是product_name,别整个prd_nm。虽然有些数据库表设计规范要求列名精简,但在RAG+Text-to-SQL场景里,语义明确的列名能显著降低模型误解。按区域聚合的表,列就写region、sales_amount,别写r、amt。
字段类型同样重要。金额字段用DECIMAL还是FLOAT,日期字段是DATE还是DATETIME,这些差别会直接影响SQL生成的写法。模型看到DECIMAL类型的金额,通常不会乱写字符串比较;看到FLOAT类型的字段,可能会写出不严谨的等值判断。尽量用精确类型,给模型明确的边界。
COMMENT注释是自解释设计里最核心的部分。大模型读取表信息,主要依赖列注释来理解字段业务含义。一份好的列注释,应该包含完整的枚举值列表和单位信息。比如状态字段的注释可以写成:订单状态,枚举值为pending(待支付)、paid(已支付)、shipped(已发货)、cancelled(已取消)。单位信息直接写在注释里,金额单位为人民币元,日期均指订单创建日期。
下面给一个示例DDL,这个表结构我们测下来是比较稳的:
CREATE TABLE sales_order ( order_id BIGINT PRIMARY KEY COMMENT '订单号', customer_name VARCHAR(128) COMMENT '客户名称,如北京某公司', region VARCHAR(64) COMMENT '销售区域,枚举值:华东、华南、华北、西南', product_category VARCHAR(64) COMMENT '产品大类,枚举值:家电、数码、家具、服饰', product_name VARCHAR(256) COMMENT '产品全称', order_amount DECIMAL(12,2) COMMENT '订单金额,单位:人民币元', status VARCHAR(16) COMMENT '订单状态,枚举值:pending(待支付)、paid(已支付)、shipped(已发货)、cancelled(已取消)、refunded(已退款)', order_date DATE COMMENT '下单日期,格式YYYY-MM-DD' );注意每个字段的注释里,我都刻意加了“单位”“格式”“枚举值”这类信息,这比干巴巴一句“订单日期”管用得多。模型在写SQL的时候,会把这些信息当作语料来推理。
3.3 把Schema喂给LLM的正确姿势
表Schema设计好之后,接下来是提示词里组织Schema的问题。直接把整个库的几十张表结构全塞给模型,既占Token又容易让模型迷路。正确做法是每次查询只提供相关的表结构,或者用“表结构+少量示例”的方式组织。
一个标准的系统提示词,至少要包含下面几块:
- 表结构信息:符合实际业务的DDL或简化字段清单
- 枚举值映射:字段注释里出现过还好,没有就得单独列一份
- 查询限制规则:只允许SELECT,禁止UPDATE/DELETE,必须带LIMIT
- 少量示例:同一个业务场景下2到3组问题转SQL的例子
示例的作用最容易被人低估。给模型一个参考范式,它倾向于模仿参考范式来写答案。用户问“上季度华东区销量前10的产品”,你给的示例里写过类似的问题怎么转化,模型就会照葫芦画瓢,而不是凭空发挥。
还有一个很重要的技巧:把公共的业务固定写法写到Schema旁边。比如“所有时间过滤条件统一用DATE(col)=...”这种规范,直接写进提示词里,能大幅减少时间条件写错的概率。用户问“最近三天”,不同模型对“最近三天”的理解不一样,有的写INTERVAL 3 DAY,有的写DATE_SUB(NOW(), INTERVAL 3 DAY),还有的直接把当前日期写死。你把这些规则在提示词里提前声明,就不会出现这种五花八门的写法。
3.4 外键、枚举与同义词的补全
表Schema里还有一种常见问题:业务用户说的话和表里的术语对不上。用户说“客户”,表里叫customer_name;用户说“商品”,表里叫product_name;用户说“买了多少”,表里叫order_amount。模型不一定能把这些同义词串起来。
解决这个问题,我用的办法是在Schema后面补一张同义词映射表。类似这样:
| 用户常用说法 | 标准字段/表名 | 使用方式 |
|---|---|---|
| 客户、顾客、买方 | customer_name | 用LIKE模糊匹配或等值过滤 |
| 销售额、卖了多少钱 | SUM(order_amount) | 聚合函数 |
| 订单状态、是否完成 | status | 枚举值过滤 |
| 这几周、最近一段时间 | order_date | 配合DATE条件过滤 |
这张映射表本身也要进提示词,让模型在写SQL之前先做一步“翻译对齐”。实测下来,加了这一步之后,SQL生成准确率提升非常明显。特别是涉及多个业务术语的场景,模型不再自己脑补字段名了。
外键关系也要在Schema里体现。两张表之间有关联,却没在提示词里说明,模型就不知道该用什么JOIN条件。最简单的方式是在字段注释里写明外键指向,比如customer_id字段注释后面加一句“关联customer表的id字段”。这一句话能省掉排查JOIN条件的半天时间。
4. 向量库中的Schema:字段类型、度量方式与索引参数
4.1 字段类型与主键设计
向量数据库是RAG项目最核心的存储组件,它的Schema设计直接影响检索效果和运维成本。不同向量数据库的结构不太一样,但核心概念类似:Collection(一个逻辑上的数据集合),里面定义字段类型和索引参数。
设计向量库Schema,第一件事是确定主键。强烈建议用业务幂等ID,也就是从业务上能唯一确定数据的ID,比如文档的doc_id加上Chunk序号:doc_7f3a_chunk_002。不要用自增数字当主键。你的数据如果经历了重新解析,自增ID会变,同一个Chunk会变成完全不同的ID,后面做增量更新、去重、关联引用记录时,全部对不上号。
第二件事是定义向量字段和标量字段的边界。向量字段存储Embedding,标量字段存储元数据。有一个小原则:参与过滤的字段、要在引用中展示的字段,必须拆成独立的标量字段;只是随数据带来、不太会用到的信息,可以合并成一个JSON字段存起来,别一个信息一个字段,搞出一大堆冗余列。
第三件事是向量维度。这个值取决于Embedding模型,比如有些模型输出768维,有些输出1024维,有些更狠,直接上3072维。维度在Collection创建后就固定下来了,原则上不能改。要换Embedding模型就要重建整个Collection,或者做双向量字段+别名切换。所以选Embedding模型的时候就要想清楚,别今天用A模型,下周换B模型,每次换模型都等于一次全量重建。
4.2 距离度量方式的选择逻辑
向量库Schema里另一个容易忽略的点是距离度量。常见的有欧氏距离L2、内积IP、余弦相似度Cosine。选哪个不是随缘的,要看你用的是什么Embedding模型以及向量是否归一化。
如果你的Embedding模型输出向量做了归一化,那Cosine和IP在数学上等价,选哪个都行,但主流向量库一般建议用IP,因为IP计算更快。如果你的Embedding向量没做归一化,Cosine相似度更稳妥,因为它不受向量长度影响,只关注方向。文本Embedding模型大多会做归一化,但多模态模型和部分开源模型不一定,需要确认一下再定。
索引参数也和度量方式有关。以常见的HNSW图索引为例,有两个关键参数:M控制每个节点的最大连接数,M越大,图越密集,召回率越高,但内存开销和构建耗时都会涨;efConstruction控制构建索引时的搜索宽度,越大索引质量越好,但构建时间越长。实际项目中M给16到32,efConstruction给100到200,属于一个比较折中的区间。线上检索时的efSearch参数可以在查询时动态调,先给个100跑通流程,再根据时延和召回率调。
这些参数听起来很枯燥,但它们确实会直接影响最终效果。我见过一个项目,全员默认参数一把梭,索引参数完全没调,结果召回率忽高忽低。后面把M从16调到32,efSearch从100调到200,效果立刻稳定下来。
4.3 分区、别名与多租户的实践
向量库Schema设计还牵涉到数据组织方式。多租户场景下,是在Collection里加一个租户字段做过滤,还是按租户拆Collection,或者用分区来做隔离,需要根据数据规模权衡。
租户数量少(几十个以内)、数据量悬殊不大,用租户字段加过滤是最便捷的。代码层面保证每次查询都带上租户过滤条件就行。但要注意,漏加过滤条件的代码只要发生一次,就可能造成跨租户数据泄露。这个风险要在查询封装层就堵死,写一个统一检索入口,强制注入租户条件。按租户拆Collection隔离效果最好,运维成本也随之上升。分区则是折中方案,数据物理隔离,但共享同一个集群。
还有一点值得重视:别名切换。向量库的Collection不能直接改名或改Schema,需要重建。一批新数据重新构建完索引之后,怎么切流量而不中断线上服务?用别名机制。Collection构建完成后,将别名从旧Collection指向新Collection,查询端只认识别名,就实现了无缝切换。这套操作在元数据Schema调整、Embedding模型升级、向量维度变更时都会用到,属于RAG运维的基本功。
5. 知识图谱RAG的Schema:本体设计决定图质量
5.1 GraphRAG为什么要先定义本体
GraphRAG这类做法最近讨论很多,思路是把文档实体抽取出来建成图,然后用社区检测、图遍历等方式做检索增强。但很多人第一次跑通GraphRAG流程后,会发现抽出来一堆乱七八糟的东西:有的节点是人,有的是组织,有的是地点,还有的是文档标题,关系也各式各样。图查询的结果让人完全没法用。
问题几乎都出在没定义本体。本体就是知识图谱的Schema,它规定了图里有哪些类型的实体、实体有哪些属性、实体之间能建立哪些类型的关系、每个关系连的实体类型是什么。没有这些约束,大模型在抽取时的自由度太大,它会把任何有语义关联的短语都抽成关系。加一层本体约束,等于给抽取过程上了紧箍咒,模型只能按照预定义的范畴来提取信息。
GraphRAG的效果,很大程度上不是由模型抽取能力决定的,而是由本体设计的质量决定的。本体设计得清晰,社区发现、实体消歧、多跳推理都会轻松很多。
5.2 一个本体Schema的示例
在项目里,我通常用“实体类型-属性-关系类型-约束”四个维度来描述本体Schema。下面是一个内部知识库的简化示例:
实体类型:
- Person(人员),属性包括id、name、title、department
- Organization(组织),属性包括id、name、industry、level
- Project(项目),属性包括id、name、status、start_date、end_date
- Document(文档),属性包括id、title、category、published_at
关系类型:
- Person - works_at -> Organization(人员就职于组织)
- Person - collaborates_with -> Person(人员之间协作关系)
- Person - participates_in -> Project(人员参与项目)
- Document - references -> Project(文档引用项目)
- Organization - cooperates_with -> Organization(组织间合作关系)
约束声明:
- 每个实体必须有id和name,没有name的实体直接丢弃
- 抽取时的实体重要度低于0.6的节点不建(阈值在抽取阶段控制)
- 一次抽取进程中,同名实体优先复用已有节点,而不是新建节点
- 关系只在本体定义的类型里选,不抽取本体之外的自由关系
这个约束列表特别重要,尤其是“同名实体复用”这一点。如果不加这条,同一家公司在不同文档里出现三次,就会被建成三个孤立节点,等要做跨文档推理时图是零散的,社区发现完全失去意义。
5.3 本体抽取任务的落地方式
本体定义好之后,怎么让它生效?做法是在抽取环节给大模型一个结构化抽取指令:告诉模型有哪些实体类型和关系类型,让模型从文本片段中抽取符合预定义类型的实体和关系,输出为JSON格式。一次抽取会话中,模型被要求严格按照给定的本体来找信息,不在本体范围内的信息就不抽。
实际操作中还有一个提升抽取质量的小技巧:在抽取提示词里加“否定示例”。比如明确告诉模型:“不要创建Organization类型的自我引用关系”、“不要将文档标题抽取为实体(标题只用来生成Document节点)”。这些负面约束能让模型少走弯路。做过几轮测试后,抽取结果的准确率提升非常明显。
图谱维护的问题同样绕不开。文档持续更新,意味着需要增量抽取,而不是每次全部重建。增量抽取时,新抽取的实体和旧图里的实体如何合并?最简单的方案是先用统一的实体名称做哈希匹配,匹配不上再借助模型的语义判断,再不行才创建新节点。规则比模型好调试,能用规则解决的就不动用模型。
6. 常见问题与排查技巧实录
6.1 元数据过滤失效:“过滤条件没生效”
现象是:查询时明明在代码里加了过滤条件,结果却还是返回了不符合条件的内容。或者反过来,原本应该有几条结果的,过滤完变成0条。
这一类问题90%出在字段名不一致。元数据写入时用的字段名和查询端Filter里写的字段名对不上,比如写入端存量数据用的是access_level,查询端Filter写的是confidential_level,自然永远过滤不到。另一个常见原因是数据本身缺字段,刚上线时的历史文档没有后来新增的字段,过滤条件一加,这些旧文档全部命中不了。
排查思路:先在向量库管理后台看一眼实际数据的字段分布,确认字段是否真的存在、类型是否匹配;再写一个空过滤条件的查询,打日志看原始返回结果里字段的值;最后看过滤条数和无过滤条数的差值,判断是过滤逻辑的问题还是索引数据的问题。这类问题最好在索引构建阶段就堵住,解析流程里加一步字段完整性校验,缺必填字段的文档直接进死信队列。
6.2 Text-to-SQL的SQL写错:“列名不存在”或“过滤值不对”
现象:模型生成的SQL执行时报列名不存在,或者能执行但结果明显不对。
列名不存在的常见原因是提示词里给的表结构和实际数据库不一致。有些情况下数据库表做过字段调整,但提示词里拷贝的还是旧DDL;有时候是表名对上号了但列名缩写太严重,模型没法理解。过滤值不对的常见原因则是枚举值映射缺失,也就是前面反复提过的status字段用中文而库里面存的是英文的问题。
我的排查顺序:先看提示词里实际生效的Schema是哪个版本,是不是线上最新的;把模型生成的SQL原样在数据库工具里跑一遍,看具体报错信息;对过滤值,执行一条SELECT DISTINCT看枚举值的真实分布,和提示词里列的枚举项比对。最后,为了防止这类问题反复出现,我给Schema做了一套版本号机制,DDL变更时同步更新提示词里的Schema片段,并且把版本号也写进去,模型端收到的Schema永远是当前版本。
6.3 向量库重建后查询无结果
现象:索引重建完成、别名切到新Collection之后,线上查询返回空结果或报错。
备选Collection没建好就切了流量是常见原因,或者重建过程中写入的字段类型和旧的不一致,新Collection允许一个字段为空,查询时过滤条件匹配不到任何数据。另一个坑是Embedding模型导致的数据跨代:旧向量用A模型,新索引用B模型,虽然都存进一个Collection,候选空间里的向量分布完全变样,查询质量剧烈波动。
排查时先把查询端临时改成不经过别名、直接指定Collection名,确认数据是否正常;再看字段类型和命名是否和查询代码完全一致;最后看重建批次的日志,确认Embedding模型是不是在重建过程中被换掉了。实际项目中我吃过一次亏:重建索引时顺手升级了Embedding模型,结果新旧向量混存,查出来的数据排名质量几乎不可用。后面就把Embedding模型和向量库Schema一起纳入版本管理,动用别名切换,新模型先建独立Collection跑A/B对比,验证没问题再切流量。
6.4 图谱节点爆炸与关系冗余
现象:GraphRAG上线后发现图谱里的节点和关系数量涨得离谱,几份文档抽出来的节点比文本句子还多,查询结果也两极分化。
节点爆炸通常是因为实体名称没有归一化。同一个机构在不同文档中可能有“某公司”、“某科技公司”、“某某科技股份有限公司”三种写法,模型不加约束时就会各建一个节点。关系冗余则是指抽取出了大量没有业务意义的关系,比如“Document - located_in -> 某个地址”这种对问答毫无帮助的边。
根治办法只有一个:本体约束加实体合并策略双管齐下。抽取指令里明确“同名归一:同一实体在不同上下文中出现时,合并为一个节点”;关系类型白名单外的全部丢弃;阈值控制重要度太低的实体不建图。有这两道防线,图谱规模一般能控制在初版预期的三分之一左右,质量也稳定很多。
关于Schema,几个实操经验
做过的RAG项目越多,越发现一个问题:凡是前期花时间把Schema理清楚的项目,后期基本不折腾;凡是急急忙忙建索引的项目,后面全在还债。元数据字段缺失让人返工,表Schema注释不全让SQL质量忽高忽低,图谱没定义本体让GraphRAG跑成笑话——这些坑我都踩过。
最后分享一个一直沿用的习惯:索引构建前先跑一个字段诊断脚本,统计每个元数据字段的覆盖率和取值分布。字段覆盖率低于95%的,说明解析链路有漏数据的情况,先去修解析逻辑再继续建索引;字段取值分布异常集中的也要警惕,可能是解析时字段映射串了。这一步只要十分钟,却能把索引阶段的隐性缺陷提前暴露出来,省下的排查时间远不止十分钟。
Schema这件事,放在RAG整个技术栈里并不显眼,它不会直接提升模型效果,但它决定了系统的上限和下限。Embedding强、Prompt好,救不了一个Schema混乱的工程化RAG系统。