元数据的作用与管理
元数据是在分块之后、向量化之前加入的。分块完成后,你得到的是一个个纯文本块,这时候给每个块打上标签,记录它的来源、权限、位置等信息。
这些元数据会和文本内容一起,被送到向量数据库里存储。后续检索的时候,不仅可以根据文本相似度找到相关的块,还可以根据元数据做过滤、排序、引用生成等操作。
在没有元数据之前,一个 chunk 就是一段文本:
"自签收之日起 7 天内,商品未经使用且不影响二次销售的,消费者可申请七天无理由退货。"
加上元数据之后,它变成了这样:
{"content":"自签收之日起 7 天内,商品未经使用且不影响二次销售的,消费者可申请七天无理由退货。","metadata":{"doc_id":"doc_20240315_001","source_url":"https://docs.company.com/policy/return.pdf","file_name":"退货政策.pdf","title":"一、退货政策","page_number":3,"created_at":"2024-03-15T10:30:00Z","updated_at":"2024-03-15T10:30:00Z","department":"customer_service","access_roles":["employee","customer_service","manager"],"start_offset":0,"end_offset":58,"chunk_index":0}}
1. 文档标识类:doc_id、source_url、file_name
1.1 为什么需要文档标识
最基础的需求:知道这个 chunk 来自哪份文档。
用户问“报销流程是什么”,系统回答后,用户可能会追问“能把完整的报销文档发给我吗”。这时候如果 chunk 里记录了 source_url,系统就能直接返回文档链接。
另一个场景是去重和更新。假设你重新上传了一份更新后的《报销流程 v2.0》,系统需要把旧版本的所有 chunk 删掉。如果每个 chunk 都记录了 doc_id,就可以批量删除 doc_id = "报销流程 v1.0" 的所有块。
三个字段的分工:
doc_id:文档的唯一标识符,通常是系统生成的 ID,用于内部管理source_url:文档的访问地址,可以是 HTTP 链接、文件路径、或者内部系统的 URLfile_name:文档的原始文件名,方便人类阅读
2. 结构信息类:标题层级(H1/H2/H3)、章节编号、页码
很多文档是有层级结构的:一级标题、二级标题、三级标题……每个 chunk 往往属于某个章节。
记录结构信息有两个好处:
一是生成引用时更精确。比如用户问“试用期多久”,系统回答“3 个月”,如果 chunk 里记录了 title = "第二章 员工入职 > 2.1 试用期规定",就可以返回“依据:《员工手册》第二章 2.1 节”。这比只说“来源:员工手册”要清晰得多。
二是检索时可以利用结构信息做排序。比如用户问的是一个概述性问题,那一级标题下的 chunk 可能比三级标题下的更相关;如果问的是细节问题,就反过来。
2.2 如何从原始文档中提取标题层级
这取决于你的文档格式:
- Markdown 文档:标题层级是现成的(
#是 H1,##是 H2) - HTML 文档:可以解析
<h1><h2><h3>标签 - Word 文档:可以通过 Apache POI 读取样式信息,判断哪些段落是标题
- PDF 文档:比较麻烦,需要根据字体大小、加粗等特征推断标题,或者用专门的 PDF 解析库
一个简化的思路:在分块的时候,记录当前块属于哪个最近的标题。比如你按段落切块,切到第 5 段时,往前找最近的一个标题是“2.1 试用期规定”,就把这个标题记录到 chunk 的元数据里。
3. 时间版本类:创建时间、更新时间、生效时间、失效时间
3.1 为什么需要时间版本信息
企业的知识是会变化的。今年的报销政策和去年不一样,上个月的产品手册和这个月不一样。如果不记录时间信息,系统可能会把过时的内容返回给用户。
四个时间字段的含义:
created_at:这个 chunk 是什么时候创建的(通常是文档上传到系统的时间)updated_at:这个 chunk 最后一次更新是什么时候effective_date:这条规则从什么时候开始生效(比如“新版报销政策自 2024 年 4 月 1 日起执行”)expiration_date:这条规则什么时候失效(比如“本优惠活动截止到 2024 年 12 月 31 日”)
前两个(created_at、updated_at)是系统自动记录的,后两个(effective_date、expiration_date)通常需要从文档内容中提取,或者在上传文档时手动标注。
一个典型的应用场景:用户问“现在的报销额度是多少”,系统检索时可以过滤掉 expiration_date 早于当前日期的 chunk,只返回当前有效的规则。
4. 权限控制类:部门标签、角色标签、ACL(访问控制列表)
4.1 什么是 ACL,为什么需要它
ACL 是 Access Control List(访问控制列表)的缩写。简单说,就是一个列表,记录了“谁能访问这个资源”。
在 RAG 系统里,每个 chunk 可以带一个 ACL,记录哪些角色、哪些部门、哪些用户能看到这个块。检索的时候,系统会先检查当前用户的身份,然后只返回他有权限看的 chunk。
权限控制有几种常见的粒度:
- 基于角色(Role-Based):比如
access_roles: ["manager", "hr"],只有经理和 HR 能看 - 基于部门(Department-Based):比如
access_departments: ["finance", "legal"],只有财务部和法务部能看 - 基于用户(User-Based):比如
access_users: ["user_001", "user_002"],只有特定用户能看 - 基于敏感级别(Sensitivity-Based):比如
sensitivity_level: "public"/"internal"/"confidential"/"secret",不同级别的用户能看不同敏感度的内容
实际项目中,通常会组合使用。比如一个 chunk 标记为 sensitivity_level: "confidential",同时指定 access_departments: ["finance"],意思是“这是机密信息,只有财务部能看”。
检索时如何根据权限过滤
权限过滤通常发生在检索阶段。大多数向量数据库(如 Milvus、Qdrant、Weaviate)都支持元数据过滤,你可以在检索时加上过滤条件。
伪代码示例:
// 假设当前用户是财务部的普通员工String userRole ="employee";String userDepartment ="finance";
// 构造过滤条件:只检索用户有权限看的 chunkMap<String, Object> filter =newHashMap<>();
filter.put("access_roles", userRole);// 或者
filter.put("access_departments", userDepartment);
// 执行检索(具体 API 取决于你用的向量数据库)List<Document> results = vectorStore.search(
query,
topK,
filter // 传入过滤条件);
不同向量数据库的过滤语法不太一样,但核心思路是一致的:在向量相似度检索的基础上,叠加元数据过滤条件。
5. 位置追溯类:原文位置(start_offset、end_offset)、chunk_index
当你发现某个 chunk 的内容有问题,需要回到原文去修正时,位置信息就派上用场了。
start_offset 和 end_offset 记录了这个 chunk 在原文中的字符位置。比如 start_offset: 120, end_offset: 250,意思是这个 chunk 对应原文的第 120 到 250 个字符。
chunk_index 是这个 chunk 在所有 chunk 中的序号。比如一份文档被切成了 10 个 chunk,第一个 chunk 的 chunk_index 是 0,第二个是 1,以此类推。
有了这些信息,你可以:
- 快速定位到原文的具体位置,方便人工审核和修正
- 在展示答案时,高亮显示原文中的相关段落
- 分析相邻 chunk 的关系(比如检索到了 chunk 5,可以顺便看看 chunk 4 和 chunk 6 的内容)
业务自定义类:产品类型、业务线、优先级等
业务场景可能还有自己特殊的需求。
比如电商场景,你可能需要给 chunk 打上商品类目标签:
这样用户问“生鲜能退货吗”,系统可以优先检索 product_category: "fresh_food" 的 chunk。
元数据的三大核心应用场景
前面讲了元数据有哪些字段,现在看看这些字段在实际场景中怎么用。
1. 回答可引用:让 AI 的回答有据可查
用户问:“新员工试用期多久?”
系统回答:“新员工试用期为 3 个月。”
用户追问:“这个规定在哪份文档里?”
如果 chunk 里记录了文档来源和章节信息,系统可以这样回答:
新员工试用期为 3 个月。
依据:《员工手册》第二章“员工入职” > 2.1 节“试用期规定”,第 5 页
这就是回答可引用。用户不仅得到了答案,还知道答案的出处,可以点击链接查看完整的原文。
2. 权限过滤:不同员工看不同知识
公司的知识库里有各种文档:
- 产品部的需求文档(只有产品部和技术部能看)
- 人事部的薪酬政策(只有人事部和管理层能看)
- 财务部的预算数据(只有财务部和高管能看)
- 公共的员工手册(所有人都能看)
技术部的小李问:“公司的年终奖怎么算?”
如果不做权限过滤,系统可能会把人事部的内部文档返回给他,泄露敏感信息。
正确的做法:检索时根据小李的身份(技术部、普通员工),只返回他有权限看的 chunk。如果没有匹配的公开信息,就回答“抱歉,这个问题涉及内部信息,请咨询人事部”。
检索前根据用户角色过滤 chunk
3.回溯与纠错:发现错答能定位到源头
用户反馈:“系统告诉我报销需要贴发票,但实际上新流程已经改成线上提交了,不用贴纸质发票。”
技术团队需要:
- 1.找到返回错误信息的那个 chunk
- 2.定位到原始文档的具体位置
- 3.修正或删除这个 chunk
- 4.检查是否还有其他相关的过时 chunk 需要一起更新
如果 chunk 里记录了 doc_id、chunk_index、start_offset、created_at 等信息,这个过程就会简单很多。
实现思路:通过元数据定位问题 chunk,快速定位,精准修正,批量更新,版本管理
元数据设计的最佳实践
1. 元数据字段不是越多越好
问题在于:存储成本,维护成本,检索性能
一个实用的原则:只加对检索、过滤、展示有实际帮助的字段。
2. 元数据的粒度要和业务场景匹配
不同的业务场景,对元数据的粒度要求不一样。
场景 1:面向公众的产品帮助文档
- 不需要权限控制(所有人都能看)
- 不需要部门标签(没有部门概念)
- 需要文档标识和章节信息(方便引用)
推荐的元数据:
{"doc_id":"...","file_name":"...","title":"...","source_url":"..."}
场景 2:企业内部知识库
- 需要权限控制(不同部门看不同内容)
- 需要时间版本(知识会更新)
- 需要位置追溯(方便纠错)
推荐的元数据:
{"doc_id":"...","file_name":"...","title":"...","source_url":"...","access_departments":[...],"access_roles":[...],"created_at":"...","updated_at":"...","start_offset": ...,"end_offset": ...,"chunk_index": ...
}
场景 3:电商客服知识库
- 需要商品类目标签(不同类目的规则不同)
- 需要政策类型标签(退货、换货、物流等)
- 需要优先级(某些规则优先级更高)
推荐的元数据:
{"doc_id":"...","file_name":"...","product_category":"...","policy_type":"...","priority": ...,"effective_date":"...","expiration_date":"..."}
3. 元数据的维护成本要考虑进去
有些元数据是系统自动生成的(如 created_at、chunk_index、start_offset),维护成本低。
有些元数据需要人工标注(如 access_roles、product_category、effective_date),维护成本高。
如果你的知识库有几千份文档,每份文档都要人工标注十几个字段,这个工作量是不现实的。
一个折中的方案:分层标注。
- 文档级标注:在上传文档时,标注文档级的元数据(如
access_departments、doc_type),这些元数据会自动继承给文档下的所有 chunk - Chunk 级标注:系统自动生成 chunk 级的元数据(如
chunk_index、start_offset) - 按需标注:只对重要的、高频访问的文档做精细化标注(如
effective_date、priority)
这样可以在保证元数据质量的同时,把维护成本控制在可接受的范围内。
| 元数据字段 | 用途 | 适用场景 | 维护成本 | 优先级 |
|---|---|---|---|---|
| doc_id | 文档标识,用于批量管理 | 几乎所有场景 | 低(系统生成) | 必须 |
| file_name | 展示给用户,生成引用 | 几乎所有场景 | 低(系统生成) | 必须 |
| source_url | 提供原文链接 | 需要回溯原文的场景 | 低(系统生成) | 推荐 |
| title / h1_title / h2_title | 生成引用,展示章节信息 | 有结构的文档 | 中(需要解析) | 推荐 |
| page_number | 生成引用,定位原文 | PDF 等有页码的文档 | 中(需要解析) | 推荐 |
| created_at / updated_at | 版本管理,追踪变更 | 知识会更新的场景 | 低(系统生成) | 推荐 |
| effective_date / expiration_date | 过滤过时内容 | 有时效性的知识(政策、活动) | 高(需要人工标注) | 可选 |
| access_roles / access_departments | 权限过滤 | 企业内部知识库 | 高(需要人工标注) | 必须(如果有权限需求) |
| sensitivity_level | 权限过滤 | 企业内部知识库 | 中(可以按文档标注) | 推荐(如果有权限需求) |
| start_offset / end_offset | 定位原文,纠错 | 需要人工审核和修正的场景 | 低(系统生成) | 推荐 |
| chunk_index | 定位 chunk,分析相邻块 | 需要管理 chunk 的场景 | 低(系统生成) | 推荐 |
| product_category / policy_type 等 | 业务过滤和排序 | 有明确业务分类的场景 | 中到高(取决于分类复杂度) | 可选 |
使用建议:
- 必须:这些字段几乎所有场景都需要,优先实现
- 推荐:这些字段能显著提升用户体验或运维效率,建议实现
- 可选:这些字段针对特定场景,根据实际需求决定是否实现
从文本到向量之理解Embedding
2.1 同义词问题
“一周”和“七天”是同一个意思,“退”和“退货”是同一个动作,但关键词检索不知道。它只做字面匹配,不理解语义。日常对话中的表达关键词检索不能,因为书面
2.2 一词多义问题
同一个词在不同语境下意思完全不同。用户问“苹果的售后政策是什么”——这里的“苹果”是指 Apple 品牌,还是指水果?关键词检索不知道,它会把所有包含“苹果”的文本块都返回,水果类目的退货政策和 Apple 产品的保修政策混在一起。
再比如“充值”这个词,在游戏场景是充游戏币,在话费场景是充话费,在会员场景是充会员余额。关键词检索没法区分。
2.3 上下文理解问题
有些问题需要理解整句话的意思,而不是拆成单个关键词。
用户问:“我不想要了,但已经拆了包装。”
关键词检索拆出来的是“不想要”“拆了”“包装”,可能匹配到“包装材料说明”或者“拆箱指南”这种完全不相关的内容。但实际上用户想问的是“拆封商品能不能退货”。
用一句话概括 Embedding 的核心思想:把文本映射到一个高维空间中,让语义相近的文本在空间中距离相近。
真实的语言含义非常丰富,两个维度根本不够用。
实际的 Embedding 模型会用几百甚至上千个维度来表示一段文本。每个维度捕捉文本含义的一个方面——虽然我们没法直观地说出每个维度具体代表什么,但模型通过大量训练数据学会了怎么分配这些维度。
回到开头的例子:
- “七天无理由退货”的向量和“买了一周的东西还能退吗”的向量,在高维空间中距离很近
- “七天无理由退货”的向量和“物流配送时效说明”的向量,距离很远
有了向量表示,计算机就不用再做字面匹配了。它只需要比较两个向量之间的距离,距离近就是语义相关,距离远就是语义无关。
这就是 RAG 系统能做语义检索的根基。
几个关键特性:
- 输入长度有限制:每个模型都有最大输入 token 数(比如 512 或 8192 等等),超过会被截断。这也是为什么前面要做分块——把长文档切成短文本块,确保每个块都在模型的输入限制内
- 输出维度固定:同一个模型输出的向量维度是固定的。比如某个模型输出 1024 维,那不管你输入一个词还是一段话,输出都是 1024 个浮点数
- 同一模型内可比较:只有用同一个模型生成的向量才能互相比较。用模型 A 生成的向量和模型 B 生成的向量,没法直接算相似度
第三点非常重要。这意味着你在数据准备阶段用什么模型把 chunk 转成向量,检索阶段就必须用同一个模型把用户的 query 转成向量。换模型 = 所有向量要重新生成。
在看具体模型之前,先搞清楚选型时要看哪些指标:
| 指标 | 含义 | 为什么重要 |
|---|---|---|
| 向量维度 | 输出向量的浮点数个数 | 维度越高,表达能力越强,但存储和计算成本也越高 |
| 最大输入 token 数 | 单次能处理的最大文本长度 | 决定了你的 chunk 最大能有多长 |
| 中文效果 | 对中文文本的语义理解能力 | 中文场景必须关注,有些模型主要针对英文训练 |
| API 成本 | 每次调用的费用 | 大规模向量化时,成本差异会很明显 |
| 是否支持本地部署 | 能不能在自己的服务器上跑 | 涉及数据安全和隐私的场景,可能不允许数据出外网 |
| 模型 | 提供方 | 向量维度 | 最大输入 token | 中文效果 | 部署方式 | 备注 |
|---|---|---|---|---|---|---|
| text-embedding-3-small | OpenAI | 1536 | 8191 | 中等 | 仅云端 API | 性价比高,适合英文为主的场景 |
| text-embedding-3-large | OpenAI | 3072 | 8191 | 中等 | 仅云端 API | 维度更高,效果更好,成本也更高 |
| text-embedding-v3 | 阿里通义 | 1024/768 | 8192 | 优秀 | 云端 API | 中文效果好,支持多种维度输出 |
| BGE-large-zh | BAAI(智源) | 1024 | 512 | 优秀 | 本地部署/API | 开源模型,中文效果突出 |
| BGE-M3 | BAAI(智源) | 1024 | 8192 | 优秀 | 本地部署/API | 支持多语言、多粒度,综合能力强 |
| Qwen3-Embedding-8B | 阿里通义 | 4096 | 32768 | 优秀 | 本地部署/API | 最新一代,维度高,上下文窗口大 |
| GTE-large-zh | 阿里通义 | 1024 | 8192 | 优秀 | 本地部署/API | 中文基准测试表现好 |
维度是选模型时绑定的——你选了某个模型,维度就确定了(部分模型支持多种维度输出,但大多数是固定的)。
打个比方:维度就像描述一个人用了多少个特征。用 2 个特征(身高、体重)描述一个人,信息很有限,很多人会“撞衫”。用 100 个特征(身高、体重、肤色、发型、口音、走路姿势……)描述,区分度就高多了。但特征越多,记录和比较的成本也越高。
| 维度范围 | 适用场景 | 存储成本(100 万条) |
|---|---|---|
| 256~512 | 简单场景,文本短、类目少 | 约 1~2 GB |
| 768~1024 | 大多数生产场景的甜蜜点 | 约 3~4 GB |
| 1536~4096 | 对精度要求极高的场景 | 约 6~16 GB |
对于大多数中文 RAG 项目,768 到 1024 维是一个比较稳妥的选择。既能保证足够的语义区分度,存储和检索成本也在可控范围内。除非你的场景对精度有极致要求(比如法律条文检索、医疗知识库等),否则不需要上 3072 或 4096 维。
相似度计算:怎么判断两个向量“像不像”
文本变成向量之后,下一步就是比较两个向量之间的相似程度。用户输入一个 query,系统把它转成向量,然后和知识库里所有 chunk 的向量逐一比较,找出最相似的几个——这就是语义检索的核心流程。
那怎么比较两个向量“像不像”?这就涉及到相似度计算。
1. 余弦相似度——最常用的度量方式
余弦相似度(Cosine Similarity)是 Embedding 检索中最常用的相似度度量方式。
- 算两个向量的点积(对应位置的数字相乘,然后全部加起来)
- 算每个向量的模(每个数字的平方加起来,再开根号)
- 点积除以两个模的乘积
在实际的 RAG 系统中,通常会设一个相似度阈值(threshold),只返回相似度高于阈值的结果。
常见的做法:
- 阈值设 0.7:比较严格,只返回高度相关的结果,准确率高但可能漏掉一些相关内容
- 阈值设 0.5:比较宽松,召回率高但可能混入一些不太相关的内容
- 不设阈值,只取 Top-K:返回相似度最高的 K 个结果(比如 Top-5),不管分数多少
实际项目中,建议先用 Top-K(比如 K=5)+ 阈值 0.6 的组合策略:先取相似度最高的 5 个,再过滤掉低于 0.6 的。具体阈值需要根据你的数据和场景调试,没有放之四海而皆准的数字。
同样两段文本,用不同的模型算出来的相似度分数可能差很多。这是因为:
- 不同模型的训练数据不同,对语义的理解方式不同
- 不同模型的向量维度不同,表达能力不同
- 有些模型输出的向量已经做了归一化(长度为 1),有些没有
所以,相似度阈值不能跨模型套用。换了模型之后,阈值要重新调。

demo 里我们一次性把 5 个 chunk 传给 API,实际项目中可能有几万甚至几十万个 chunk。一次全传过去不现实(API 有请求大小限制),一个一个传又太慢。
2.1 分批处理
最基本的优化:把 chunks 分成固定大小的批次,逐批调用 API。
/**
* 分批向量化
*
* @param texts 所有待向量化的文本
* @param batchSize 每批的大小(建议 20~50)
* @return 所有文本的向量
*/publicList<double[]>embedInBatches(List<String> texts,int batchSize)throwsException{List<double[]> allEmbeddings =newArrayList<>();for(int i =0; i < texts.size(); i += batchSize){int end =Math.min(i + batchSize, texts.size());List<String> batch = texts.subList(i, end);System.out.printf("向量化进度:%d/%d%n", end, texts.size());List<double[]> batchEmbeddings =embed(batch);
allEmbeddings.addAll(batchEmbeddings);// 简单的限流:每批之间等一下,避免触发 API 的速率限制if(end < texts.size()){Thread.sleep(200);}}return allEmbeddings;}
2.2 并发控制
分批处理是串行的,如果 API 支持并发,可以用多线程加速:
importjava.util.concurrent.*;/**
* 并发批量向量化
*/publicList<double[]>embedConcurrently(List<String> texts,int batchSize,int maxConcurrency)throwsException{ExecutorService executor =Executors.newFixedThreadPool(maxConcurrency);List<Future<List<double[]>>> futures =newArrayList<>();for(int i =0; i < texts.size(); i += batchSize){int start = i;int end =Math.min(i + batchSize, texts.size());List<String> batch = texts.subList(start, end);
futures.add(executor.submit(()->embed(batch)));}List<double[]> allEmbeddings =newArrayList<>();for(Future<List<double[]>> future : futures){
allEmbeddings.addAll(future.get());}
executor.shutdown();return allEmbeddings;}
并发数不要设太高,一般 3~5 就够了。设太高容易触发 API 的速率限制(Rate Limit),反而更慢。
2.3 错误重试
网络请求难免会失败,加一个简单的重试机制:
/**
* 带重试的 embed 方法
*/publicList<double[]>embedWithRetry(List<String> texts,int maxRetries)throwsException{Exception lastException =null;for(int attempt =1; attempt <= maxRetries; attempt++){try{returnembed(texts);}catch(Exception e){
lastException = e;System.err.printf("第 %d 次调用失败:%s,%s%n",
attempt, e.getMessage(),
attempt < maxRetries ?"准备重试...":"已达最大重试次数");if(attempt < maxRetries){// 指数退避:第 1 次等 1 秒,第 2 次等 2 秒,第 3 次等 4 秒Thread.sleep(1000L*(1<<(attempt -1)));}}}thrownewRuntimeException("向量化失败,已重试 "+ maxRetries +" 次", lastException);}
向量化和元数据的关系
一句话概括:元数据不参与向量化,但和向量一起存入向量数据库。
打个比方:向量化就像给一本书拍了一张“语义照片”,元数据就是贴在照片背面的标签(作者、出版日期、分类等)。拍照的时候不需要看标签,但存档的时候标签要跟照片放在一起。
检索的时候,流程是这样的:
1.向量编码文本和 向量
2.ANN 检索Query向量
相似度计算 ,返回Top-K,按相似度降序,候选 chunks
3.元数据过滤根据条件得到最终结果
所以在存储阶段,每个 chunk 在向量数据库里的记录通常包含三部分:
| 字段 | 内容 | 说明 |
|---|---|---|
| id | chunk 的唯一标识 | 用于更新和删除 |
| vector | Embedding 模型输出的向量 | 用于相似度检索 |
| metadata | 元数据(JSON 格式) | 用于过滤和展示 |
大部分向量数据库(如 Milvus)还可以把原始文本也存进去,这样检索的时候不用再去别的地方取文本内容。
什么时候需要重新向量化
4.1 换了 Embedding 模型
不同模型生成的向量是不兼容的。模型 A 生成的向量和模型 B 生成的向量,维度可能不同,即使维度相同,语义空间也不一样,不能混在一起做相似度计算。
换模型 = 所有 chunk 重新向量化 + 向量数据库里的旧向量全部替换。
4.2 文档内容更新
如果某份文档的内容改了(比如退货政策从 7 天改成了 15 天),对应的 chunk 文本变了,向量自然也要重新生成。
实际操作中,通常的做法是:
- 1.根据
doc_id找到这份文档的所有旧 chunk - 2.删除旧 chunk 的向量
- 3.对更新后的文档重新分块、向量化、存入向量数据库
这就是为什么前一篇强调元数据里要有 doc_id——没有它,你很难知道哪些 chunk 属于同一份文档。
4.3 分块策略调整
如果你调整了分块的大小或重叠策略(比如从 500 字一块改成 300 字一块),chunk 的内容变了,向量也要重新生成。
这种情况通常意味着全量重新向量化,工作量比较大。所以分块策略最好在项目初期就确定下来,别频繁改。
4.4 模型升级
同一个模型的不同版本(比如 text-embedding-v2 升级到 text-embedding-v3),生成的向量也可能不兼容。升级前要看模型提供方的说明,确认新旧版本的向量是否兼容。
一个实用的建议:在向量数据库的元数据里记录
embedding_model和embedding_model_version,这样你随时知道每个向量是用哪个模型生成的,升级时也方便做灰度切换。

Comments NOTHING