知识库(RAG):从文档摄入到混合检索
30 秒导读: RAG(Retrieval-Augmented Generation,检索增强生成)就是「让模型答题前先去翻资料」。本章讲 FastGPT 的这套「资料系统」怎么运转——左半边把文档拆成小块、排队、向量化后存进库;右半边在用户提问时把问题扩写、同时跑向量搜索和关键词搜索、用 RRF 算法把两路结果融成一张榜、必要时再精排,最后把命中的片段回填给工作流。第 04 章讲模型拿到这些片段后怎么生成答案,本章只管把最相关的片段找出来。
本章接在 工作流引擎 和 AI 节点 之后:知识库检索本身也是工作流里的一个节点(dataset search node),它产出的「引用(quote)」会作为上下文喂给下游的 LLM 节点。
1. 这是什么(零基础也能懂)
-
一句话定义: 知识库子系统 = 一个「文档进、相关片段出」的检索引擎,让 AI 回答能引用你自己的资料而不是瞎编。
-
解决什么问题: 大模型不知道你公司的内部文档、产品手册、历史工单。RAG 的做法是:预先把这些资料切碎存好;提问时先检索出最相关的几段,连同问题一起塞给模型。模型于是「带着资料答题」。
-
两个阶段,缺一不可:
阶段 白话 什么时候发生 摄入(ingest) 把文档拆块、算向量、存进库 你上传/同步文档时,后台异步慢慢跑 检索(retrieval) 拿问题去库里捞最相关的块 用户每次提问时,实时同步跑 -
一句话直觉: 摄入像「给一屋子书做卡片索引」(慢、离线、一次性);检索像「拿着问题冲进图书馆,同时查卡片目录和全文,再把最像的几本抽出来」(快、在线、每次都做)。
-
FastGPT 的关键取舍: 它不押注单一检索方式,而是默认双路召回 + RRF 融合——向量搜索懂语义、全文搜索懂关键词,两者各有盲区,融合起来才稳。这是本章的重点。
2. 顶层全景(它大概怎么转)
先给一张两半边的总图。怎么读: 上半是离线摄入(左→右),下半是在线检索(左→右);两半在中间的 dataset_datas 库(存好的块 + 向量)会合——摄入往里写,检索从里读。
━━━━━━━━━━━━━━ 摄入侧(异步 / 离线)━━━━━━━━━━━━━━
上传文档
│
▼
① 拆块 rawText → chunks[](可含 QA 拆分/图片块)
│ createCollectionAndInsertData
▼
② 入训练队列 pushDataListToTrainingQueue → MongoDatasetTraining
│ (mode: chunk / qa / auto / image / parse)
▼
③ 后台 worker 拆QA / 调 VLM / 算 embedding 向量
│
▼
┌─────────────────────────────────────────┐
│ dataset_datas(块的 q/a/图 + 索引) │
│ + 向量库(vector)+ dataset_data_texts │──── 全文索引
└─────────────────────────────────────────┘
▲ │
│ 写 │ 读
━━━━━━━━━━━━━━ 检索侧(同步 / 在线)━━━━━━━━━━━━━━
用户提问
│
▼
④ 查询扩展 datasetSearchQueryExtension(LLM 改写出多条检索词)
│
▼
⑤ 多路召回 multiQueryRecall ──并发──┬─ embeddingRecall(向量/语义)
│ └─ fullTextRecall(关键词/jieba)
▼
⑥ RRF 融合 datasetSearchResultConcat(按名次加权合并成一张榜)
│
▼
⑦ 重排+过滤 reRankSearchResults(只对文本)→ 去重 → 相似度阈值 → token 上限
│
▼
⑧ 回填工作流 dispatchDatasetSearch → quoteQA → 下游 LLM 节点
各部件一句话职责:
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
createCollectionAndInsertData | 摄入总入口:拆块 + 建 collection + 入队 | packages/service/core/dataset/collection/controller.ts:41 |
pushDataListToTrainingQueue | 把 chunk 批量写进训练队列 | packages/service/core/dataset/training/controller.ts:73 |
MongoDatasetTraining | 训练队列表(worker 按 mode 排优先级拉取) | packages/service/core/dataset/training/schema.ts:17 |
MongoDatasetData | 存好的块:q/a、图片、索引 | packages/service/core/dataset/data/schema.ts:15 |
defaultSearchDatasetData | 检索总入口:编排扩写 + 分发召回 | packages/service/core/dataset/search/index.ts:18 |
multiQueryRecall | 并发调度向量 + 全文两路召回 | packages/service/core/dataset/search/defaultRecall/multiQueryRecall.ts:10 |
datasetSearchResultConcat | RRF 融合算法 | packages/global/core/dataset/search/utils.ts:5 |
reRankSearchResults | 只对文本召回做精排 | packages/service/core/dataset/search/defaultRecall/rerank.ts:55 |
dispatchDatasetSearch | 检索节点:把结果变成工作流引用 | packages/service/core/workflow/dispatch/dataset/search.ts:56 |
3. 摄入侧:文档怎么变成「可检索的块」
这一半是异步的,用户上传后不阻塞——真正的重活(拆 QA、调 VLM、算向量)交给后台 worker 慢慢跑。设计核心是一张训练队列表。
3.1 拆块:一份文档 → chunks[]
摄入总入口是 createCollectionAndInsertData(collection/controller.ts:41)。它做的第一步是把原始文本切成小块(chunk),块大小、重叠率等由 collection 的配置决定:
// 示意,非源码:拆块的核心思路
const chunks = await rawText2Chunks({
rawText, // 整份文档的纯文本
chunkSize, // 每块目标大小
overlapRatio: 0.2, // chunk 模式下块间重叠 20%,避免切断语义
});
// → [{ q: "第一段…" }, { q: "第二段…" }, …]
真实实现见 collection/controller.ts:117-169:文本走 rawText2Chunks,图片则每张图直接当作一个块({ imageId, indexes: [] })。重点看一处取舍:只有普通 chunk 模式才加 overlapRatio: 0.2(collection/controller.ts:142)——块之间留 20% 重叠,防止一句话正好被切在两块边界而两块都答不全。
3.2 训练模式:同一张队列,五种加工方式
拆完块要「加工」——但加工方式不止一种。FastGPT 用一个枚举 TrainingModeEnum 统一表达(packages/global/core/dataset/constants.ts:239):
| mode | 干什么 | 谁来算 |
|---|---|---|
chunk | 直接对块算 embedding 向量 | 向量模型 |
qa | 先让 LLM 把块拆成一问一答,再向量化 | LLM(agent 模型) |
auto | chunk + 自动补索引 | 向量 + LLM |
image | 对图片算图片向量 | 图片 embedding 模型 |
imageParse / parse | 先调 VLM 把图/文件解析成文本,再回队列 | VLM |
具体给哪个 mode,由 getTrainingModeByCollection(collection/utils.ts)根据 collection 的训练类型、是否开图片索引、是否 Plus 版决定。注意 image/auto/imageParse 都要求 global.feConfigs?.isPlus——这些是商业版能力,社区版会回落到 chunk。
3.3 入队:pushDataListToTrainingQueue
拆好的块通过 pushDataListToTrainingQueue(training/controller.ts:73)批量写进 MongoDatasetTraining 表。这个函数的两个看点:
看点一:按 mode 决定「一块最多多大、权重多少」(training/controller.ts:113-148):
// 示意,非源码:不同 mode 的入队参数
if (mode === 'chunk') maxToken = Infinity, weight = 向量模型权重;
if (mode === 'qa' || 'auto') maxToken = getLLMMaxChunkSize(llm), weight = 0;
if (mode === 'image'/'imageParse') maxToken = VLM 上下文, weight = 0;
// 超过 maxToken 的块直接丢弃(training/controller.ts:163)
weight 只有向量化的 chunk 模式非零——它后面会决定 worker 拉取优先级。
看点二:大数据量分段事务(training/controller.ts:171-248)。每批 500 条,每个事务最多 20 批(= 1 万条);超过 1 万就切成多个事务,每段用 retryFn 包起来重试。这是为了避免「一次性插 10 万条」把 MongoDB 事务撑爆。
对于还没解析成文本的文件(比如刚上传的 PDF),走的是另一条更轻的入口 pushDatasetToParseQueue(training/controller.ts:264)——它只创建一条 mode: parse 的记录,等 worker 先把文件解析成文本,再回头走上面的拆块入队流程。
3.4 队列表的排队模型
worker 怎么知道先干哪条?靠 MongoDatasetTraining 上的一个复合索引(training/schema.ts:130):
TrainingDataSchema.index({ mode: 1, retryCount: 1, lockTime: 1, weight: -1 })
读法:按 mode 分组 → retryCount 小的(快用完重试次数的)优先 → lockTime 早的(没被锁的)优先 → weight 大的优先。配合 lockTime 字段(schema.ts:52)实现「谁抢到锁谁执行」,retryCount 默认 5 次(schema.ts:56),expireAt 挂了 7 天 TTL(schema.ts:131)兜底清理。团队级的锁由 lockTrainingDataByTeamId(training/controller.ts:19)处理——比如团队 AI 点数用尽时,用一个 30 分钟的定时锁把该团队所有任务冻结,避免继续扣费。
3.5 collection 与 data:两级组织
摄入的产物落在两张表,是一个「文件 → 块」的父子关系:
| 表 | 粒度 | 存什么 | 文件 |
|---|---|---|---|
dataset_collections | 一份文档/一个来源 | 来源、训练配置、原文 hash | collection/schema.ts |
dataset_datas | 一个块 | q/a/imageId + indexes[](每条索引带自己的 dataId) | data/schema.ts:15 |
dataset_datas 里最关键的是 indexes 数组(data/schema.ts:51):一个块可以有多条索引向量(比如 QA 模式下问题和答案各一条)。每条索引在向量库里是独立的一个点,各带一个 dataId。检索时向量库返回的是 dataId,需要再回查 dataset_datas 才能拿到完整的 q/a——这个「向量库出 id、Mongo 出内容」的两步走,是理解检索侧回查逻辑的前提。
此外还有一张 dataset_data_texts(data/dataTextSchema.ts)专门存全文索引 token,供 Mongo $text 全文搜索用。
4. 检索侧(重点):一次提问怎么找到最相关的块
这一半是同步的、每次提问都跑,也是 FastGPT RAG 工程含量最高的地方。主线:扩写 → 双路并发召回 → RRF 融合 → 重排/过滤。
4.1 检索入口:defaultSearchDatasetData
入口 defaultSearchDatasetData(search/index.ts:18)只干「前置编排」——它自己不召回,而是先扩写查询,再把扩写结果转交给真正的召回主流程 searchDatasetData:
// 真实逻辑简化,见 search/index.ts:25-51
const query = textQueries.join('\n');
const { searchQueries, reRankQuery } = query
? await datasetSearchQueryExtension({ query, llmModel, ... }) // 扩写
: { searchQueries: [], reRankQuery: query };
const result = await searchDatasetData({ ...props, reRankQuery, textQueries: searchQueries });
还有一个并列入口 deepRagSearch(search/index.ts:74),它只是 global.deepRagHandler(data) 的转发。这个 handler 在 projects/app/src/service/common/system/index.ts:71 被绑成「POST 到 /core/dataset/deepRag」——属于商业版(Pro)闭源能力,克隆里看不到实现,社区版走上面的默认检索。工作流里的 datasetDeepSearch 开关且有文本 query 时才启用它(dispatch/dataset/search.ts:157)。
4.2 查询扩展:让「怎么解决」变成能搜的词
要解决的小问题: 用户爱说省略句。上文聊 Nginx,接一句「怎么下载」——直接拿「怎么下载」去搜,向量和关键词都抓不住重点。
思路: 用一个 LLM 把原问题 + 对话历史改写成多条明确的检索词,补全指代、覆盖不同角度(主体/原因/方法/约束/示例 …)。
datasetSearchQueryExtension(search/utils.ts:69)是这层的入口,真正调 LLM 的是 queryExtension(packages/service/core/ai/functions/queryExtension.ts:108)。它有两个精华细节:
-
子模优化选词(
queryExtension.ts:271):LLM 先生成最多 10 条候选,再用lazyGreedyQuerySelection(贪心子模选择,alpha: 0.3)挑出至多 3 条既相关、又互相不冗余的检索词。不是全都拿去搜,避免近义词扎堆浪费召回名额。 -
失败不阻断主链路(
search/utils.ts:110-128):扩写是 try/catch 包住的,LLM 挂了就退回原始 query 继续搜。检索的可用性不被 LLM 能力绑架。
去重时还有个小心思(search/utils.ts:97):比较两条 query 是否重复,先把标点和空格全删掉再 hash,但保留原文——只用于判重,不影响真正拿去 embedding 的文本。
4.3 多路召回:向量 + 全文,并发跑
扩写出的检索词交给 searchDatasetData(defaultRecall/index.ts:35),它先算「每条链路取多少候选」再并发召回。
先定名额(countRecallLimit,defaultRecall/utils.ts:13):
| 搜索模式 | embedding 名额 | full-text 名额 |
|---|---|---|
embedding(纯语义) | 100 | 0 |
fullTextRecall(纯关键词) | 0 | 100 |
mixedRecall(混合,默认推荐) | 80 | 60 |
混合模式两路都多取一批,给后面的融合留素材;单一模式则把另一路名额设 0,直接跳过。
再并发召回(multiQueryRecall,multiQueryRecall.ts:10)。这层的职责是「先统一算好 collection 过滤范围,再把同一份约束下发给两条链路」,保证两路看到的集合范围一致:
// 真实结构简化,见 multiQueryRecall.ts:31-74
const [forbidList, filterList] = await Promise.all([
getForbidCollectionIdList(...), // 被禁用的 collection
filterCollectionByMetadata(...), // 按 metadata/tag 过滤的 collection
]);
const [embeddingResults, fullTextResults] = await Promise.all([
embeddingRecall({ ..., limit: embeddingLimit, forbidList, filterList }), // 向量路
fullTextRecall({ ..., limit: fullTextLimit, forbidList, filterList }), // 全文路
]);
两条链路各自负责什么:
向量召回 embeddingRecall(embeddingRecall.ts:134) —— 懂语义、抗换词。流程是:把每条检索词算成向量 → 到向量库 recallFromVectorStore 找最近邻(返回 dataId)→ 回查 dataset_datas 和 dataset_collections 补齐 q/a 和来源(embeddingRecall.ts:200-240)。每条 query 内部先按块 id 去重,多条 query 之间再交给 RRF 合并(embeddingRecall.ts:295)。它还统一处理图片:文本 query 和图片描 述都走 text embedding,原始图片只在向量模型支持图片时才走 image embedding(embeddingRecall.ts:39-104)。
全文召回 fullTextRecall(fullTextRecall.ts:27) —— 懂精确关键词、抗专有名词。它用 Mongo 的 $text 全文索引,查询词先过 jiebaSplit 中文分词(fullTextRecall.ts:69),按 textScore 排序取前 N。只处理文本类 query(用户文本 + 图片 caption),原始图片进不来——Mongo 文本索引没法消费图片向量(fullTextRecall.ts:22-24 注释点明)。
4.4 RRF 融合:把两张榜合成一张
要解决的小问题: 向量搜索给的是余弦相似度(0~1),全文搜索给的是 BM25 文本分(几到几十)——量纲根本不能比。直接把分数相加是错的。
思路:RRF(Reciprocal Rank Fusion,倒数排名融合)——不看分数,只看名次。 一个块在某张榜里排第 rank 名,就贡献 1/(60+rank) 分;在多张榜里都靠前,累加起来就高。60 是平滑常数,压低头部名次之间的差距,让靠后但被多路命中的块也有机会翻身。
这是全章最核心的算法,实现只有几十行,在 datasetSearchResultConcat(packages/global/core/dataset/search/utils.ts:5):
// 真实核心,见 global/core/dataset/search/utils.ts:16-47
arr.forEach((item) => {
const weight = item.weight;
item.list.forEach((data, index) => {
const rank = index + 1;
const score = weight * (1 / (60 + rank)); // ← RRF 公式,与分数无关,只看名次
const record = map.get(data.id);
if (record) {
// 同一个块被多路命中:rrfScore 累加;同 type 的原始分取 max
map.set(data.id, { ...record, rrfScore: record.rrfScore + score });
} else {
map.set(data.id, { ...data, rrfScore: score });
}
});
});
// 最后按 rrfScore 降序排,得到融合榜
上层用两个薄封装调它(defaultRecall/result.ts):
concatRecallLists(result.ts:43):等权(weight 全为 1)合并,用于「同一路的多条 query」之间。concatWeightedRecallLists(result.ts:47):带权合并,且自动丢掉空列表和 0 权重,用于「不同语义来源」之间。
分层融合的顺序(defaultRecall/index.ts:109-159)——不是一锅烩,而是三层:
第1层:同一语义来源内融合
用户文本 = embeddingWeight × 文本向量 + (1-embeddingWeight) × 文本全文
图片描述 = embeddingWeight × caption向量 + (1-embeddingWeight) × caption全文
第2层:图片侧内部融合
图片结果 = 0.3 × 图片描述(caption) + 0.7 × 原始图片向量
第3层:文本侧 与 图片侧 融合
最终 = 1.0 × 文本结果 + (有文本时 0.7 / 纯图片时 1.0) × 图片结果
embeddingWeight 默认 0.5(向量和全文各占一半),用户可在检索节点调。设计意图很清楚:文本问题永远主导,图片作为视觉补充约束(defaultRecall/index.ts:148-159)。
4.5 重排:为什么只精排文本、图片仍走 RRF
要解决的小问题: RRF 只看名次,不看「块内容到底多贴题」。重排(rerank)模型能拿 query + 块正文重新打一个更准的相关性分——但它是文本模型。
reRankSearchResults(rerank.ts:55)的关键设计,直接看它上面那行注释(rerank.ts:52-53):
只对文本召回结果 rerank。图片召回仍通过 RRF 权重参与最终融合,避免图片向量结果被文本 rerank 误杀。
为什么? rerank 模型只会读文本。你把一个「图片向量命中的块」丢给它,它读不懂图,只会给个低分,等于把视觉相似的正确结果误杀。所以流水线把 rerank 卡在「文本侧融合之后、图文合并之前」(对照 defaultRecall/index.ts 的 Step 4 在 121-133 行、Step 6 图文合并在 148 行)——只精排文本,图片绕开 rerank,仍靠 RRF 权重参与最终融合。
重排本身也不是「一刀切换掉 RRF 结果」(rerank.ts:87-102):
// 示意:rerankWeight 控制「信 rerank 几分」
if (rerankWeight === 1) return reRankResults; // 完全信 rerank
return concatWeightedRecallLists([ // 否则再 RRF 融合一次
{ weight: 1 - rerankWeight, list: textRecallResults }, // 原 RRF 名次
{ weight: rerankWeight, list: reRankResults }, // rerank 名次
]);
rerankWeight 默认 0.5。rerank 调用失败会 catch 住退回原结果(rerank.ts:103-109),usingReRank 标回 false——又一处「增强失败不阻断主链路」。启用前提是开了开关、有 reRankQuery、且系统配了默认 rerank 模型(defaultRecall/index.ts:60)。
4.6 收尾三连:去重 → 相似度阈值 → token 上限
融合完还要过三道过滤,顺序是固定的(defaultRecall/index.ts:161-177),先后有讲究:
-
同内容去重
removeDuplicateSearchResults(result.ts:57):同一个块可能被文本、caption、图片向量多路命中。按q+a归一化后的 hash 去重,保留最靠前的。先去重,免得重复块白占后面的相似度和 token 预算。 -
相似度阈值
filterSearchResultsByScore(result.ts:69):只在「用了 rerank」或「纯 embedding 模式」时才按阈值过滤——因为只有这两种情况下的分数(rerank 分 / 余弦分)才有绝对意义。混合模式不带 rerank 时不做阈值过滤(result.ts:80-91),usingSimilarityFilter标 false。 -
token 上限
filterDatasetDataByMaxTokens(defaultRecall/utils.ts:39):逐条累加 token,超过模型上下文上限就截断。但至少保留第一条(utils.ts:63)——否则一条超长但高质量的命中会被全过滤掉,用户看到「无引用」,比返回一条可裁剪引用更难排查。
最后返回前,才把块内容里的内部图片 key 换成可预览 URL(defaultRecall/index.ts:174-177)——只在最终步做,避免带过期时间的动态 URL 污染中间的去重和融合。
4.7 检索结果如何回到工作流
检索不是裸调用,它是工作流里的一个节点。dispatchDatasetSearch(dispatch/dataset/search.ts:56)是这个节点的调度函数,它把节点参数翻译成检索调用,再把结果包装成工作流引用:
- 入口分流(
search.ts:157-180):开了datasetDeepSearch且有文本 query → 走deepRagSearch(Pro);否则走defaultSearchDatasetData。 - 计费拆项(
search.ts:183-291):向量、rerank、查询扩展、图片 caption、deep search 每一项分别按 token 计点,累加成totalPoints。 - 产出(
search.ts:294-332):quoteQA(给下游 LLM 节点当上下文的引用列表)+toolResponse(给工具调用循环用的 cites,见 AI 节点)。
多个知识库怎么合并? 靠 concat 节点 dispatchDatasetConcat(dispatch/dataset/concat.ts:21)。当工作流里挂了多个检索节点(比如查多个库),它把各节点的引用列表再用同一个 datasetSearchResultConcat(等权 RRF)合成一张榜,再按字符上限裁剪(concat.ts:30-42)。同一套 RRF 算法在两个层级复用:库内融合双路召回、库间融合多个检索节点。
5. 巧妙之处(可借鉴的技术)
-
RRF 用名次而非分数融合(
global/.../search/utils.ts:21):一行weight * (1/(60+rank))就绕开了「向量分和全文分量纲不可比」这个老大难。零训练、零调参,是异构检索融合的工业级默认解。 -
rerank 卡位在图文合并之前(
rerank.ts:52):识别出「文本 rerank 模型读不懂图」这个坑,把 rerank 精确限定在文本侧,图片绕开走 RRF。这种「按数据模态分治」的意识值得学。 -
所有增强都可失败降级:查询扩展(
search/utils.ts:125)、rerank(rerank.ts:103)、单张坏图(embeddingRecall.ts:83)——任何一个「锦上添花」的环节挂了,都退回主链路继续搜,绝不让增强能力拖垮基础检索的可用性。 -
token 过滤至少保 1 条(
defaultRecall/utils.ts:63):一个「宁可给一条超长引用、也不给零引用」的产品级判断,直接降低了线上「明明有资料却说找不到」的排查成本。 -
异步队列 + 复合索引排优先级(
training/schema.ts:130):把重活(QA 拆分、向量化、VLM 解析)全推到后台队列,用{mode, retryCount, lockTime, weight}索引 +lockTime抢锁实现多 worker 竞争消费,摄入不阻塞用户。
6. 边界与局限
-
deep RAG 是闭源的。
deepRagSearch只是转发到global.deepRagHandler(search/index.ts:74),真实实现是 Pro 版/core/dataset/deepRag(projects/app/src/service/common/system/index.ts:71),克隆里看不到。社区版拿不到多轮迭代检索。 -
图片索引 / QA / auto 训练模式要 Plus 版。
getTrainingModeByCollection里带global.feConfigs?.isPlus判断,社区版这些模式会回落到普通 chunk。 -
全文召回强依赖分词质量。 中文靠
jiebaSplit(fullTextRecall.ts:69),分词切错会直接影响$text命中;且全文索引只在 Mongo 内,扩展性受 Mongo$text能力限制。 -
RRF 的 60 是硬编码常数(
global/.../search/utils.ts:21),不随数据规模自适应;名次融合天然对「只在一路排第一、另一路完全没命中」的块不够友好。 -
相似度阈值不是万能开关。 混合模式不带 rerank 时根本不做阈值过滤(
result.ts:80)——用户以为设了 similarity 就会生效,实际要看模式和是否开 rerank。
7. 横向对比
同属 ai-agent-reference 货架,本章讲的是「Agent 的长期外部记忆怎么被检索」,与其它章的分工:
| 章 | 关切 | 和本章的关系 |
|---|---|---|
| 01 工作流数据模型 | 节点/边/变量 | 检索节点也是其中一个节点 |
| 02 对话管线 | completions → 调度 → SSE | 检索是管线里的一步 |
| 03 工作流引擎 | 调度内核 | dispatchDatasetSearch 由它驱动 |
| 04 AI 节点 | LLM 生成、工具循环 | 消费本章产出的引用 |
一句话边界:本章只负责「把最相关的片段找出来并回填」,片段进了模型之后怎么生成答案,是第 04 章的事。
8. 代码地图(导航索引)
| 主题 | 文件路径 | 关键符号 |
|---|---|---|
| 摄入总入口(拆块+建collection+入队) | packages/service/core/dataset/collection/controller.ts | createCollectionAndInsertData |
| 数据入训练队列 | packages/service/core/dataset/training/controller.ts | pushDataListToTrainingQueue |
| 文件解析入队 | packages/service/core/dataset/training/controller.ts | pushDatasetToParseQueue |
| 训练队列表 + 排队索引 | packages/service/core/dataset/training/schema.ts | MongoDatasetTraining, TrainingDataSchema |
| 训练模式枚举 | packages/global/core/dataset/constants.ts | TrainingModeEnum |
| 块存储表 | packages/service/core/dataset/data/schema.ts | MongoDatasetData, DatasetDataSchema |
| 检索总入口 + deep RAG 转发 | packages/service/core/dataset/search/index.ts | defaultSearchDatasetData, deepRagSearch |
| 查询扩展编排 | packages/service/core/dataset/search/utils.ts | datasetSearchQueryExtension |
| 查询扩展 LLM 实现 | packages/service/core/ai/functions/queryExtension.ts | queryExtension, lazyGreedyQuerySelection |
| 召回主流程(分层融合) | packages/service/core/dataset/search/defaultRecall/index.ts | searchDatasetData |
| 多路并发调度 | packages/service/core/dataset/search/defaultRecall/multiQueryRecall.ts | multiQueryRecall |
| 向量召回 | packages/service/core/dataset/search/defaultRecall/embeddingRecall.ts | embeddingRecall, buildVectorRecallTasks |
| 全文召回 | packages/service/core/dataset/search/defaultRecall/fullTextRecall.ts | fullTextRecall |
| RRF 融合算法 | packages/global/core/dataset/search/utils.ts | datasetSearchResultConcat |
| 融合封装 + 去重 + 阈值过滤 | packages/service/core/dataset/search/defaultRecall/result.ts | concatWeightedRecallLists, removeDuplicateSearchResults, filterSearchResultsByScore |
| 召回名额 + token 上限过滤 | packages/service/core/dataset/search/defaultRecall/utils.ts | countRecallLimit, filterDatasetDataByMaxTokens |
| 只对文本重排 | packages/service/core/dataset/search/defaultRecall/rerank.ts | reRankSearchResults |
| 检索节点调度 | packages/service/core/workflow/dispatch/dataset/search.ts | dispatchDatasetSearch |
| 多库结果合并节点 | packages/service/core/workflow/dispatch/dataset/concat.ts | dispatchDatasetConcat |