AI RAG 知识库
1. 系统定位
AI Wiki 为业务问答场景提供知识检索能力:
- 业务方把飞书文档、表格、PDF、Markdown、Office、HTML、TXT、Excel 或 CSV 等内容放进知识库。
- 系统读取源文档,把内容切成适合检索的知识片段。
- 每个片段生成 Embedding 向量,写入 Milvus;片段原文和业务元数据写入 MySQL。
- 用户提问时,系统把问题转成向量,从指定知识库召回候选片段,再经过状态、标签、Rerank 和相关性阈值处理。
- 当前接口主要返回知识片段;上游的模型服务可以继续使用这些片段生成最终回答,但“召回结果”和“最终答案”是两个阶段。
整体处理链路如下:
源文档
-> 结构化解析
-> 文本切分
-> 向量化
-> MySQL 保存可管理信息 + Milvus 保存可相似度检索信息
-> 用户问题召回相关片段
-> 返回片段给上游回答链路
2. 业务背景
2.1 使用场景与问题
在客服、运营、内部助手等场景里,知识通常分散在不同地方:
- 飞书文档中有规则和流程;
- 表格中有商品、渠道、配置或价格信息;
- PDF、Word、Markdown 中有制度、说明书和历史沉淀;
- 同一类知识可能由多个团队维护,更新时间和内容结构也不一致。
直接把所有文档拼到 Prompt 中会遇到几个问题:
- 文档数量大,模型上下文无法容纳全部内容;
- 一次请求读取全部文档成本高、延迟高;
- 相似内容多,容易把不相关规则带进回答;
- 文档更新后,旧内容可能继续被使用;
- 回答无法稳定追溯到具体文档和片段。
AI Wiki 在业务文档和模型问答之间增加了一层知识索引。文档经过解析、切分和向量化后可以按问题检索,同时保留来源和管理关系。
2.2 业务数据模型
业务数据分为三层:
| 对象 | 业务含义 | 主要职责 |
|---|---|---|
wiki_item |
一个知识库 | 组织一组文档,并配置 Embedding、Rerank、阈值和发布状态 |
wiki_document |
知识库中的一篇源文档 | 代表飞书文档、PDF、CSV、Markdown 等原始知识来源 |
wiki_document_chunk |
源文档切出的一个知识片段 | 保存可编辑、可展示、可追溯的文本和向量关联信息 |
这三个对象的层级关系是:
一个知识库 wiki_item
└── 多篇源文档 wiki_document
└── 多个知识片段 wiki_document_chunk
这里的“知识库”是业务边界,也是后续向量检索的隔离边界。wiki_item 不只是一个页面名称,它决定了召回范围、模型配置和 Milvus Collection。
3. 数据对象
3.1 wiki_item:知识库根作用域
wiki_item 负责描述一个知识库的整体配置,典型信息包括:
- 知识库自身的
id; - 使用哪一个 Embedding 模型;
- 使用哪一个 Rerank 模型;
- 相关性阈值
relevance_threshold; - 发布状态;
- 自动更新策略;
- 是否允许通过标签限制文档范围。
对检索链路而言,最重要的是 wiki_item.id:
wiki_item.id = 12
-> Milvus Collection = wiki_doc_12
wiki_item.id 用于确定检索所属的知识库。具体命中的源文档由 doc_id 标识。
3.2 wiki_document:源文档实体
wiki_document 表示知识库里的具体源文档,通常包含:
id:文档逻辑 ID;wiki_item_id:归属的知识库;- 文档来源 URL 或本地文件信息;
- 文档类型;
- 切片类型和切片参数;
- 处理状态;
- 更新和发布信息。
文档处理状态定义为:
0 = 待处理
1 = 处理中
2 = 完成
3 = 失败
wiki_document.id 是后续批量删除、重建和回查该文档所有切片的关键条件。
3.3 wiki_document_chunk:可管理的知识片段
一篇文档会被切成多个 chunk。每个 chunk 同时服务于向量检索和业务管理,MySQL 中保存:
- 切片表行主键
id; doc_id;wiki_item_id;- 文档内顺序
chunk_index; - 展示或检索使用的
content; - 表格等场景下的完整结构
original_content; - 向量记录关联键
milvus_id; - 有效状态
status。
MySQL 中的 chunk 是业务可管理实体:编辑、查看、审核、溯源都依赖它。Milvus 中的向量记录则更偏向检索索引。
3.4 wiki_document_tag:召回范围约束
标签用于在检索前缩小文档范围。指定知识库后,系统会先得到符合条件的 allowedDocIds,再对 Milvus 返回的候选结果做文档过滤。
这说明标签过滤当前主要由 MySQL 完成,Milvus 负责向量相似度搜索,不负责完整的业务标签权限判断。
4. 文档入库与索引构建
文档处理由后台任务负责。本章按执行顺序说明文档解析、切分、向量化和落库过程。
4.1 建库与召回总架构
建库和查询形成两条对应链路:
建库:document content -> chunk -> chunk vector
查询:query text -> query vector -> 相似 chunk
建库和查询必须使用兼容的 Embedding 模型和相同向量维度。当前实现都读取 wiki_item.model_select,向量维度固定为 1024。
4.2 文档处理流程
该方法以一篇文档为处理单位。它先加载知识库配置并更新文档状态,再根据文档类型选择解析和切片方式。切片完成后准备 Milvus Collection,清理该文档的旧数据,最后执行向量化和双存储写入。
处理过程分为十个阶段:
| 阶段 | 处理动作 | 处理结果 |
|---|---|---|
| 1. 校验上下文 | 检查 Milvus 组件并查询知识库 | 得到知识库和模型配置 |
| 2. 计算切片大小 | 文档未配置切片大小时按模型补默认值 | v2 为 2048,其他为 8192 |
| 3. 更新状态 | 把文档状态改为处理中 | 后台任务可以识别正在处理的文档 |
| 4. 选择解析器 | 飞书正文、本地正文、表格走不同分支 | 正文字符串或表格行 |
| 5. 生成切片 | 正文按配置切分,表格按行生成 | 切片文本及位置信息 |
| 6. 记录总数 | 更新文档的切片总数 | 文档记录包含本次切片数量 |
| 7. 准备 Collection | 检查知识库对应的 Collection | Milvus Collection 可以写入 |
| 8. 清理旧版本 | 按文档 ID 删除 Milvus 和 MySQL 旧数据 | 为本次全量重建腾出位置 |
| 9. 向量化与落库 | 分批生成 Embedding,先 Milvus 后 MySQL | 向量记录和业务切片 |
| 10. 完成或失败 | 成功时标记完成,异常时标记失败 | 文档进入最终处理状态 |
设计说明与实际执行存在两处差异,以下内容以实际执行为准:
- 实际 Collection 名称是
wiki_doc_{wikiId},因此是一个知识库一个 Collection,不是一个文档一个 Collection。 - 实际
DASHSCOPE_MAX_BATCH_SIZE = 10,不是部分旧注释写的 25。
4.3 文档解析分支
解析路线由 URL 和 docType 共同决定:
正文解析器最终都输出一个 String content。这一步会丢失一部分源文件结构,例如 HTML 标签、PDF 页面布局和 Word 样式。后面的切分器主要面对纯文本,不理解复杂版面。
4.4 chunkSize 配置与长度单位
当文档没有设置 chunkSize,代码根据 Embedding 模型补默认值:
if (chunkSize == 0) {
if ("text-embedding-v2".equals(modelSelect)) {
chunkSize = 2048;
} else {
chunkSize = 8192;
}
}
代码中存在两种长度计算方式:
- 普通正文切分使用 Java
String.length()和substring(),本质上按字符下标处理; - 表格会先用
TokenizerUtils.countTokensWithSpecialTokens()计算 token 数; - 表格超限后仍使用
substring(0, chunkSize)截断,又回到了字符下标。
chunkSize 在正文和表格处理中分别对应字符长度和 token 限制。中文、英文、特殊字符和不同 tokenizer 下,两种长度并不严格相等。
4.5 正文切分策略
所有正文切分统一经过同一套路由:
系统根据 chunkType 选择切分策略,最后过滤空白切片,并保留每个切片在原字符串中的起止位置。
4.5.1 chunkType = 1:固定长度滑窗
核心计算是:
end = min(start + chunkSize, content.length)
chunk = content[start, end)
nextStart = start + chunkSize - overlapSize
例如:
chunkSize = 1000
overlapSize = 100
chunk 0 = [0, 1000)
chunk 1 = [900, 1900)
chunk 2 = [1800, 2800)
相邻切片共享 100 个字符,可以降低答案刚好跨越边界时的漏召概率。代价是重复文本增加,Embedding 调用量和向量数量也会增加。
当前方法没有看到对 overlapSize >= chunkSize 的显式校验。若配置不合法,start 可能无法正常向后推进。
4.5.2 chunkType = 2:按换行聚合
算法先执行:
String[] lines = content.split("\n");
然后按原顺序追加每一行。当“当前 chunk 长度 + 下一行长度”超过 maxChunkSize 时,先提交当前 chunk,再从下一行开始新 chunk。
示例:
maxChunkSize = 100
第 1 行 40 字符
第 2 行 35 字符 -> 当前约 75
第 3 行 50 字符 -> 加入后会超过 100
chunk 0 = 第 1 行 + 第 2 行
chunk 1 = 第 3 行
这种方式更适合段落式规则文档。当前算法不会继续拆分单个超长行,所以一行本身超过 maxChunkSize 时,仍可能形成超长 chunk。
4.5.3 chunkType = 3:按 Markdown 一级标题
核心正则是:
content.split("(?=^# )", Pattern.MULTILINE)
它只识别行首的一级标题:
# 退换货范围
# 特殊材质说明
## 二级标题 不会独立触发切分。这个策略能保留完整章节语义,但当前实现没有再按 chunkSize 做二次切分,所以一个很长的一级标题章节会整体成为一个 chunk。
4.5.4 chunkType = 4:自定义分隔符
算法先通过:
content.split(Pattern.quote(delimiter))
按业务配置的分隔符拆成多个 part,再按 maxChunkSize 聚合。拼装时会把分隔符加回当前 chunk,尽量保留原文结构。
适用场景包括:
- 用
---分隔的问答条目; - 用固定业务标记分隔的规则块;
- 已由上游预处理好的标准文本。
与换行策略类似,如果单个 part 自身已经超过最大长度,当前实现不会继续拆分它。
4.5.5 默认策略
未知的 chunkType 会回退到:
chunkSize = 1000
overlapSize = 100
这里的默认值直接写在路由方法中,不使用文档原本的 chunkSize 和 overlapSize。
4.6 表格切片规则
飞书表格、Excel 和 CSV 使用相同的业务建模方式:跳过表头,一行生成一个 chunk。
以下表格配置用于说明 field_index 的作用:
| 字段 | 值 | field_index |
|---|---|---|
| 品类 | 鞋类 | 1 |
| 问题类型 | 开胶 | 1 |
| 处理规则 | 穿着后需判断人为损伤 | 1 |
| 内部负责人 | 售后组 A | 0 |
生成的数据是:
content =
"品类:鞋类,问题类型:开胶,处理规则:穿着后需判断人为损伤"
original_content =
{
"品类":"鞋类",
"问题类型":"开胶",
"处理规则":"穿着后需判断人为损伤",
"内部负责人":"售后组 A"
}
两份内容的用途不同:
content只拼接field_index = 1的字段,用于生成 Embedding 和 Rerank;original_content保存全部已配置字段,用于召回后的完整展示;- 表格 chunk 的
start_position和end_position都设置为行序号。
field_index 会直接影响表格召回。未进入 content 的字段即使保存在 original_content,也不会参与当前的向量语义表达。
4.7 Embedding 处理流程
切分完成后进入向量化阶段,执行结构如下:
4.7.1 分批与并发
当前固定参数为:
ExecutorService = Executors.newFixedThreadPool(10)
DASHSCOPE_MAX_BATCH_SIZE = 10
如果文档有 36 个切片,会产生 4 个异步任务:
任务 1:chunk 0 - 9
任务 2:chunk 10 - 19
任务 3:chunk 20 - 29
任务 4:chunk 30 - 35
每个任务调用 getEmbeddingsBatch,请求参数为:
TextEmbeddingParam.builder()
.model(wikiItem.getModelSelect())
.texts(batchChunks)
.parameter("dimension", 1024)
.build();
如果建库侧模型值为空,getEmbeddingsBatch 会回退到 text-embedding-v4。响应中的 textIndex 用于把每个向量映射回批次中的原始 chunk。
4.7.2 chunk 与双存储的关联
对批次内每一个 chunk:
globalChunkIndex = batchStartIndex + j;
chunkId = doc.getId() + "_" + globalChunkIndex;
vector = vectorResults.get(j);
随后按以下顺序写入:
第一步:Milvus.insertVector(
collection = wiki_doc_{wikiId},
doc_id = docId,
chunk_id = chunkId,
vector = 1024维 FloatVector
)
第二步:MySQL.insert(
doc_id = docId,
wiki_item_id = wikiId,
chunk_index = globalChunkIndex,
content = chunk,
original_content = 表格原始 JSON 或 null,
milvus_id = chunkId,
vectorized = 1,
status = 1
)
当前向量记录按 chunk 逐条写入向量库,业务切片按任务批次批量写入 MySQL。
4.7.3 批次异常处理
批次内部有两层异常处理:
- MySQL 批量插入失败后只记录日志;
- 整个批次失败后也只记录日志。
批次异常会被记录并结束当前任务。外层看到任务结束,不等于所有 chunk 都成功写入。随后文档状态仍可能被更新为完成。
生产排查需要同时校验以下三项:
wiki_document.total_chunks
MySQL 实际有效 chunk 数
Milvus 中该 doc_id 的向量数
4.8 文档重建与数据替换
新切片生成完、Collection 准备好之后,系统按文档 ID 清理旧向量和旧切片,再写入本轮数据。当前采用文档级全量替换:
生成新 chunks
-> 删除旧 Milvus vectors
-> 删除旧 MySQL chunks
-> 写入新 vectors 和 chunks
它的优点是逻辑简单,不需要计算新旧 chunk 差异。代价是删除和新写入之间存在空窗期;如果新一轮 Embedding 或写库失败,该文档可能没有完整的可用版本。
5. ID 关系设计
5.1 ID 之间的关系
各层 ID 的关联关系如下:
wiki_item.id
└── wiki_document.wiki_item_id
└── wiki_document.id / doc_id
└── wiki_document_chunk.doc_id
└── Milvus.doc_id
wiki_document.id + chunkIndex
└── chunkId = docId + "_" + chunkIndex
├── wiki_document_chunk.milvus_id
└── Milvus.chunk_id
每个 ID 的职责不同:
| ID | 所在位置 | 解决的问题 |
|---|---|---|
wiki_item.id / wiki_item_id |
知识库、文档、切片关系 | 这条知识属于哪个知识库 |
doc_id / wiki_document.id |
文档、切片、Milvus | 这条知识来自哪篇源文档 |
wiki_document_chunk.id |
MySQL | 这条切片在业务表中的行主键是什么 |
chunkIndex / chunk_index |
文档内部 | 这条切片在原文中的顺序是什么 |
chunkId |
入库关联键 | 如何在 MySQL 和 Milvus 之间定位同一个切片 |
milvus_id |
MySQL | MySQL 保存的向量记录关联键 |
Milvus id |
Milvus | 向量库内部的物理主键,当前为 auto id |
5.2 wiki_item_id 的职责
doc_id 只能说明“来自哪篇文档”,无法单独说明“属于哪个知识库”。同一个业务系统里,文档可能被重新导入、复制到不同知识库,或者不同知识库存在同名文档。
wiki_item_id 主要承担三类作用:
- 业务归属:建立
知识库 -> 文档 -> 切片的层级关系。 - 检索隔离:根据知识库选择对应的 Milvus Collection。
- 配置选择:根据知识库读取 Embedding、Rerank 和相关性阈值。
当前 Collection 命名为:
wiki_doc_{wikiId}
例如:
wiki_item.id = 12
Milvus Collection = wiki_doc_12
当前采用“一个知识库一个 Collection,Collection 内通过 doc_id 区分文档”。历史设计说明中曾出现“每个文档独立集合”的表述,实际运行方式以知识库维度为准。
5.3 ID 关联示例
以下数字用于说明 ID 关系,不代表线上真实数据:
知识库:
wiki_item.id = 12
业务名称 = 商品售后知识库
Milvus Collection = wiki_doc_12
源文档:
wiki_document.id = 345
wiki_document.wiki_item_id = 12
文档名称 = 鞋类售后规则 2026-09
文档中的第 7 个切片:
chunkIndex = 7
chunkId = "345_7"
wiki_document_chunk.id = 9812
wiki_document_chunk.doc_id = 345
wiki_document_chunk.wiki_item_id = 12
wiki_document_chunk.milvus_id = "345_7"
Milvus 中对应的记录:
id = 550001 # Milvus auto id,物理主键
doc_id = 345
chunk_id = "345_7"
vector = [...]
查询返回 chunk_id = "345_7" 后,系统可以在 MySQL 中按 milvus_id = "345_7" 找到 wiki_document_chunk.id = 9812,再拿到完整文本、原始表格内容和文档归属。
Milvus 负责“找得像不像”
MySQL 负责“这条内容是谁、属于哪里、展示什么”
6. MySQL 与 Milvus 存储分工
6.1 两种存储的职责
当前 Milvus Collection 的核心字段是:
id Int64 auto primary key
doc_id Int64
chunk_id VarChar
vector FloatVector
默认检索配置为:
dimension = 1024
indexType = IVF_FLAT
metricType = COSINE
nlist = 1024
nprobe = 10
两种存储的职责如下:
| 能力 | MySQL | Milvus |
|---|---|---|
| 保存完整文本 | 负责 | 不作为主存储 |
| 保存表格原始 JSON | 负责 | 不负责 |
| 保存知识库、文档、状态、标签 | 负责 | 不负责 |
| 处理标签范围和业务状态 | 负责 | 不负责 |
| 保存 Embedding 向量 | 通过关联信息间接管理 | 负责 |
| 按向量相似度 Top-K 搜索 | 不适合 | 负责 |
| 作为最终展示数据来源 | 负责 | 返回关联键和距离 |
6.2 向量数据与业务数据
向量适合检索,不适合承担完整业务数据职责:
- 向量本身不能直接展示原文;
- 向量不能表达文档处理状态和发布状态;
- 标签、文档名称、来源 URL 等字段需要结构化管理;
- 业务排查需要知道召回片段属于哪个知识库、哪篇文档、哪一行。
查询结果需要经过向量召回和 MySQL 回查:
问题向量
-> Milvus 返回 chunk_id / doc_id / score
-> MySQL 查询有效 chunk
-> 按 allowedDocIds 过滤
-> 按 sort_model 调用 Rerank
-> 返回 content、original_content、docId、chunk 行 ID、score
6.3 写入顺序与一致性
当前一个批次大致采用:
1. 生成向量
2. 插入 Milvus
3. 构建 WikiDocumentChunk
4. 插入 MySQL
MySQL 和 Milvus 是两个独立存储。即使业务代码存在事务边界,常规数据库事务也不能天然覆盖 Milvus,因此“先 Milvus、后 MySQL”可能产生部分成功:
- Milvus 已经有向量,但 MySQL 没有对应的 chunk;
- MySQL 写入失败,下一次查询只能拿到无法完整回查的候选;
- 删除或重建过程中,两个存储短时间内状态不一致。
后续如果需要更强的一致性保障,可以增加幂等写入、补偿任务、状态表、版本号或一致性校验。
7. 在线召回流程
在线入口为知识切片召回接口。
7.1 召回主链路
7.2 请求参数
GetSliceRequest 的核心结构为:
{
"query": "麂皮鞋穿过一次后开胶,能否按质量问题退货?",
"size": 10,
"wikiIds": [12],
"wikiTags": {
"in": ["售后规则"],
"notIn": ["历史版本"]
}
}
参数含义:
| 参数 | 技术作用 |
|---|---|
query |
同时用于生成查询向量和 Rerank |
size |
每个知识库的 Milvus topK,也是最终结果的 limit |
wikiIds |
决定读取哪些知识库配置,以及搜索哪些 wiki_doc_{wikiId} |
wikiTags.in |
查询带有指定标签的文档 ID |
wikiTags.notIn |
排除带有指定标签的文档 ID |
7.3 召回处理步骤
第一步:确定知识范围
- 校验传入的
wikiIds; - 查询对应的知识库;
- 根据文档完成状态和标签条件得到
allowedDocIds。
没有标签条件时,MySQL 查询条件包括:
wiki_item_id IN (wikiIds)
is_deleted = false
status = 2
此时产物可能是:
allowedDocIds = [345, 346, 351]
第二步:生成查询向量
- 每个知识库可以有自己的
model_select; - 对每个知识库分别调用文本向量化服务;
- 请求只包含一个 query,维度固定为
1024; - 失败时返回空列表,该知识库会被跳过。
建库和查询的对称关系如下:
建库:
chunk content
-> wiki_item.model_select
-> 1024维 chunk vector
查询:
request.query
-> 同一知识库的 wiki_item.model_select
-> 1024维 query vector
如果同一个 Collection 中混入不同模型产生的向量,即使维度相同,向量空间也可能不兼容。因此切换 model_select 后,需要重新向量化该知识库中的历史文档。
第三步:Milvus 初召回
- 选择
wiki_doc_{wikiId}; - 使用
COSINE距离和默认nprobe = 10搜索 Top-K; - 返回
doc_id、chunk_id和相似度分数。
Milvus 请求的核心形态是:
collectionName = wiki_doc_12
data = [queryVector]
outputFields = [doc_id, chunk_id]
metricType = COSINE
topK = request.size
searchParams = { nprobe: 10 }
Milvus 返回结果会被转成:
milvusIdToScore = {
"345_7": 0.86,
"351_2": 0.81,
"346_5": 0.74
}
当请求包含多个知识库时,每个知识库各执行一次 Top-K,再把所有结果合并到同一个 milvusIdToScore。
第四步:MySQL 回查
- 通过
milvus_id IN (...)找到业务切片; - 只保留
chunk.status = 1的有效切片; - 拿到原文、原始内容、文档 ID 和 MySQL chunk 行 ID。
对应关系为:
Milvus.chunk_id = "345_7"
↓
MySQL.wiki_document_chunk.milvus_id = "345_7"
↓
content / original_content / doc_id / chunk表主键
第五步:文档范围过滤
- 使用前面得到的
allowedDocIds再过滤; - 标签逻辑在这一层生效;
- 这样可以避免“向量相似,但不属于本次允许范围”的内容进入结果。
当前 allowedDocIds 没有作为 Milvus filter 下推。实际顺序是:
整个知识库 Collection 做 Top-K
-> MySQL 回查
-> allowedDocIds 过滤
假设 size = 3,Milvus 前三名全部来自被标签排除的文档,而允许文档的最佳片段排在第 4 名,那么过滤后结果为 0,第 4 名也不会被补召回来。
Milvus Top-3:
1. doc 900,score 0.93,被排除
2. doc 901,score 0.91,被排除
3. doc 902,score 0.89,被排除
4. doc 345,score 0.87,允许,但未进入 Top-3
过滤结果:空
标签条件较严格时,当前 size 同时影响最终数量和候选召回深度。可选优化包括把 doc_id 过滤下推到 Milvus,或者使用大于最终返回数的候选池。
第六步:Rerank 重排
当前配置支持的 Rerank 模型包括:
ops-bge-reranker-largergte-rerank-v2
Rerank 对初召回候选重新计算问题与片段的相关度。向量召回负责候选覆盖,Rerank 负责候选排序和精度。
两种模型的调用路径是:
sort_model |
服务 | 写回的分数 |
|---|---|---|
ops-bge-reranker-larger |
OpenSearchRerankService |
score.getScore() |
gte-rerank-v2 |
TextRerankService |
result.getRelevanceScore() |
当前召回流程没有“直接使用向量分数并正常返回”的通用分支:
- 模型是上述两种之一且服务可用,进入 Rerank;
- Rerank 调用过程中抛异常时,代码记录日志并保留当前候选;
- 模型不支持或对应服务不可用时,进入
else并抛出异常。
另外,MySQL 的 IN 查询没有显式 ORDER BY,不能假设回查结果仍保持 Milvus 顺序。Rerank 成功时会按 Rerank 响应顺序重建 finalChunks;Rerank 异常并回退时,filteredChunks 的原始顺序需要运行时验证。
第七步:阈值与数量控制
- 使用第一个知识库的
relevance_threshold; - 代码先对
finalChunks.limit(topK),再逐项判断score >= relevanceThreshold; - 返回片段内容和来源字段。
构建响应时:
original_content 有值
-> flattenJsonToKeyValue(original_content)
original_content 为空
-> content
最终 SliceItem 只包含:
{
"content": "召回后的文本或打平后的表格行",
"docId": 345,
"score": 0.91,
"id": 9812
}
这里的 id 是 MySQL wiki_document_chunk.id,不是 Milvus auto id,也不是 chunkId。
7.4 检索层与回答层
知识切片召回接口负责返回上下文,为上游大模型提供输入。最终答案涉及的 Prompt 组装、引用格式、流式输出和模型生成逻辑属于后续回答链路。
完整问答链路分为两段:
检索层:问题 -> 候选知识片段
生成层:问题 + 知识片段 -> 大模型最终回答
故障排查时可以分别确认:
- “没有召回正确知识”;
- “召回正确,但模型没有正确使用知识”。
8. 商品售后知识库示例
8.1 业务数据
示例知识库名称为“商品售后知识库”,包含:
- 《鞋类售后规则 2026-09》:Markdown 文档;
- 《特殊材质处理表》:Excel;
- 《品牌退换货补充规则》:飞书文档。
用户提问:
一双麂皮鞋穿过一次后发现开胶,还能按质量问题退货吗?
8.2 入库过程
以《鞋类售后规则 2026-09》为例:
wiki_item.id = 12
wiki_document.id = 345
文档配置为按一级标题切分,其中一个片段如下:
chunkIndex = 7
content = "麂皮鞋出现开胶时,需要结合穿着次数、开胶位置和是否存在人为损伤判断..."
chunkId = "345_7"
入库时:
MySQL:
wiki_document_chunk.id = 9812
wiki_document_chunk.wiki_item_id = 12
wiki_document_chunk.doc_id = 345
wiki_document_chunk.chunk_index = 7
wiki_document_chunk.milvus_id = "345_7"
wiki_document_chunk.status = 1
Milvus:
Collection = wiki_doc_12
doc_id = 345
chunk_id = "345_7"
vector = Embedding(content)
8.3 查询过程
用户问题进入知识切片召回流程后:
- 根据
wikiIds = [12]确定只搜索商品售后知识库; - 把问题生成查询向量;
- 在
wiki_doc_12中初召回多个候选; - 通过
chunk_id回查 MySQL; - 丢弃已失效切片或不在标签范围内的文档;
- 使用 Rerank 判断“麂皮鞋、穿过一次、开胶、质量问题”这组语义的相关度;
- 过滤低于阈值的候选,返回最相关的规则片段。
接口返回面向业务使用的文档、切片和相关性信息:
知识库:12
源文档:345
业务切片:9812
关联键:345_7
片段内容:麂皮鞋出现开胶时,需要结合穿着次数...
相关性分数:...
上游回答服务再基于这些片段组织最终答复,并可以把来源文档展示给用户。
9. 实现边界与风险
以下内容来自当前实现梳理,生产环境仍需结合运行数据、数据库和 Milvus 实例验证。
9.1 双存储可能出现部分成功
Milvus 和 MySQL 不属于同一个常规事务边界。当前写入顺序为 Milvus 在前、MySQL 在后,失败时可能出现孤儿向量或缺少向量关联的 MySQL 记录。
文档处理流程内部捕获了最外层异常,并在失败时更新文档状态。事务边界和实际提交结果需要结合运行环境验证。无论 MySQL 事务是否生效,都无法自动回滚已经写入 Milvus 的向量。
排查建议:
按 wikiId / docId 对比:
Milvus.doc_id + chunk_id
MySQL.wiki_document_chunk.doc_id + milvus_id
9.2 重建期间可能暂时没有可用数据
如果重建流程先删除旧向量和旧切片,再写入新数据,且新数据处理失败,旧版本可能已经不存在。对于线上知识库,重建最好具备版本切换或失败回滚能力。
9.3 异步任务的成功状态需要运行时确认
异步批次会捕获并记录异常,MySQL 批量插入也有独立的异常处理。任务结束不代表所有切片都已经完整写入两个存储。不能只根据最终完成状态就下结论。
9.4 chunkId 当前没有版本号
当前 chunkId 由:
docId + "_" + chunkIndex
构成。这足以支持当前的 MySQL-Milvus 回查,但在并发重建、历史版本保留、灰度切换和回滚场景中,可能需要增加文档版本或索引版本。
9.5 多知识库召回的配置选择需要确认
当前召回逻辑涉及多个知识库时,Rerank 模型和阈值使用 validWikiItems.get(0) 的配置。需要确认产品设计是否要求“多个知识库共用第一项配置”,还是每个知识库应该分别使用自己的配置。
9.6 标签过滤发生在 Top-K 之后
allowedDocIds 没有下推到 Milvus。被排除文档仍会占用 Top-K 名额,严格标签条件下可能出现允许文档有相关片段,但返回结果不足甚至为空。
9.7 切片长度单位和边界校验不统一
- 正文使用字符长度,表格先计算 token 再按字符截断;
overlapSize >= chunkSize没有显式防护;- 换行、一级标题、自定义分隔符策略都可能产生超过
chunkSize的单个 chunk。
这些边界会影响 Embedding 请求是否超限,以及不同语言文档的切片粒度是否一致。
9.8 Rerank 回退路径不完整
召回流程只接受两个明确的 sort_model。模型不支持或服务不可用时会抛出异常;调用过程中异常虽然会保留候选,但 MySQL 回查结果没有显式排序,不能确认仍按 Milvus 分数顺序返回。
9.9 DDL 与数据库索引
本文未覆盖完整建表 DDL,因此以下信息不能仅凭业务对象说明确认:
- 是否存在物理外键;
milvus_id是否有唯一索引;- 字段真实长度;
- 线上字段类型和字符集;
- 删除、更新时是否有数据库级联约束。
这些内容应通过实际数据库 schema 或发布配置补充验证。
10. 配套架构图
配套图示可以结合本文一起讲解:




分享时可重点结合最后两张图:
- 数据库关系图说明业务对象如何关联;
- ID 关系图说明 MySQL 和 Milvus 如何通过
chunkId互相找到同一个知识片段。
11. 总结
wiki_item是知识库根作用域,wiki_item_id决定业务归属、检索隔离和模型配置。wiki_document表示源文档,wiki_document_chunk表示源文档拆出来的可管理知识片段。doc_id解决“来自哪篇文档”,chunk_id解决“文档中的哪一个片段”。- MySQL 保存业务事实和展示内容,Milvus 保存向量检索所需的数据。
- Milvus 返回候选关联键,MySQL 回查完整内容和业务元数据。
- 标签和文档状态属于业务过滤,应在召回链路中明确区分向量搜索与业务过滤。
- 召回结果是上游生成答案的输入,不能直接等同于最终答案。
- 生产验证应覆盖双存储一致性、重建失败处理、异步异常传播、切片长度单位、过滤召回深度和多知识库配置策略。

浙公网安备 33010602011771号