跳到主要内容

知识库(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
datasetSearchResultConcatRRF 融合算法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[]

摄入总入口是 createCollectionAndInsertDatacollection/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.2collection/controller.ts:142)——块之间留 20% 重叠,防止一句话正好被切在两块边界而两块都答不全。

3.2 训练模式:同一张队列,五种加工方式

拆完块要「加工」——但加工方式不止一种。FastGPT 用一个枚举 TrainingModeEnum 统一表达(packages/global/core/dataset/constants.ts:239):

mode干什么谁来算
chunk直接对块算 embedding 向量向量模型
qa先让 LLM 把块拆成一问一答,再向量化LLM(agent 模型)
autochunk + 自动补索引向量 + LLM
image对图片算图片向量图片 embedding 模型
imageParse / parse先调 VLM 把图/文件解析成文本,再回队列VLM

具体给哪个 mode,由 getTrainingModeByCollectioncollection/utils.ts)根据 collection 的训练类型、是否开图片索引、是否 Plus 版决定。注意 image/auto/imageParse 都要求 global.feConfigs?.isPlus——这些是商业版能力,社区版会回落到 chunk

3.3 入队:pushDataListToTrainingQueue

拆好的块通过 pushDataListToTrainingQueuetraining/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),走的是另一条更轻的入口 pushDatasetToParseQueuetraining/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)兜底清理。团队级的锁由 lockTrainingDataByTeamIdtraining/controller.ts:19)处理——比如团队 AI 点数用尽时,用一个 30 分钟的定时锁把该团队所有任务冻结,避免继续扣费。

3.5 collection 与 data:两级组织

摄入的产物落在两张表,是一个「文件 → 块」的父子关系:

粒度存什么文件
dataset_collections一份文档/一个来源来源、训练配置、原文 hashcollection/schema.ts
dataset_datas一个块q/a/imageId + indexes[](每条索引带自己的 dataIddata/schema.ts:15

dataset_datas 里最关键的是 indexes 数组(data/schema.ts:51):一个块可以有多条索引向量(比如 QA 模式下问题和答案各一条)。每条索引在向量库里是独立的一个点,各带一个 dataId。检索时向量库返回的是 dataId,需要再回查 dataset_datas 才能拿到完整的 q/a——这个「向量库出 id、Mongo 出内容」的两步走,是理解检索侧回查逻辑的前提。

此外还有一张 dataset_data_textsdata/dataTextSchema.ts)专门存全文索引 token,供 Mongo $text 全文搜索用。


4. 检索侧(重点):一次提问怎么找到最相关的块

这一半是同步的、每次提问都跑,也是 FastGPT RAG 工程含量最高的地方。主线:扩写 → 双路并发召回 → RRF 融合 → 重排/过滤

4.1 检索入口:defaultSearchDatasetData

入口 defaultSearchDatasetDatasearch/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 });

还有一个并列入口 deepRagSearchsearch/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 把原问题 + 对话历史改写成多条明确的检索词,补全指代、覆盖不同角度(主体/原因/方法/约束/示例…)。

datasetSearchQueryExtensionsearch/utils.ts:69)是这层的入口,真正调 LLM 的是 queryExtensionpackages/service/core/ai/functions/queryExtension.ts:108)。它有两个精华细节:

  1. 子模优化选词queryExtension.ts:271):LLM 先生成最多 10 条候选,再用 lazyGreedyQuerySelection(贪心子模选择,alpha: 0.3)挑出至多 3 条既相关、又互相不冗余的检索词。不是全都拿去搜,避免近义词扎堆浪费召回名额。

  2. 失败不阻断主链路search/utils.ts:110-128):扩写是 try/catch 包住的,LLM 挂了就退回原始 query 继续搜。检索的可用性不被 LLM 能力绑架。

去重时还有个小心思(search/utils.ts:97):比较两条 query 是否重复,先把标点和空格全删掉再 hash,但保留原文——只用于判重,不影响真正拿去 embedding 的文本。

4.3 多路召回:向量 + 全文,并发跑

扩写出的检索词交给 searchDatasetDatadefaultRecall/index.ts:35),它先算「每条链路取多少候选」再并发召回。

先定名额countRecallLimitdefaultRecall/utils.ts:13):

搜索模式embedding 名额full-text 名额
embedding(纯语义)1000
fullTextRecall(纯关键词)0100
mixedRecall(混合,默认推荐)8060

混合模式两路都多取一批,给后面的融合留素材;单一模式则把另一路名额设 0,直接跳过。

再并发召回multiQueryRecallmultiQueryRecall.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 }), // 全文路
]);

两条链路各自负责什么:

向量召回 embeddingRecallembeddingRecall.ts:134 —— 懂语义、抗换词。流程是:把每条检索词算成向量 → 到向量库 recallFromVectorStore 找最近邻(返回 dataId)→ 回查 dataset_datasdataset_collections 补齐 q/a 和来源(embeddingRecall.ts:200-240)。每条 query 内部先按块 id 去重,多条 query 之间再交给 RRF 合并(embeddingRecall.ts:295)。它还统一处理图片:文本 query 和图片描述都走 text embedding,原始图片只在向量模型支持图片时才走 image embedding(embeddingRecall.ts:39-104)。

全文召回 fullTextRecallfullTextRecall.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 是平滑常数,压低头部名次之间的差距,让靠后但被多路命中的块也有机会翻身。

这是全章最核心的算法,实现只有几十行,在 datasetSearchResultConcatpackages/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):

  • concatRecallListsresult.ts:43):等权(weight 全为 1)合并,用于「同一路的多条 query」之间。
  • concatWeightedRecallListsresult.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 + 块正文重新打一个更准的相关性分——但它是文本模型。

reRankSearchResultsrerank.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),先后有讲究:

  1. 同内容去重 removeDuplicateSearchResultsresult.ts:57):同一个块可能被文本、caption、图片向量多路命中。按 q+a 归一化后的 hash 去重,保留最靠前的。先去重,免得重复块白占后面的相似度和 token 预算。

  2. 相似度阈值 filterSearchResultsByScoreresult.ts:69):只在「用了 rerank」或「纯 embedding 模式」时才按阈值过滤——因为只有这两种情况下的分数(rerank 分 / 余弦分)才有绝对意义。混合模式不带 rerank 时不做阈值过滤(result.ts:80-91),usingSimilarityFilter 标 false。

  3. token 上限 filterDatasetDataByMaxTokensdefaultRecall/utils.ts:39):逐条累加 token,超过模型上下文上限就截断。但至少保留第一条utils.ts:63)——否则一条超长但高质量的命中会被全过滤掉,用户看到「无引用」,比返回一条可裁剪引用更难排查。

最后返回前,才把块内容里的内部图片 key 换成可预览 URL(defaultRecall/index.ts:174-177)——只在最终步做,避免带过期时间的动态 URL 污染中间的去重和融合。

4.7 检索结果如何回到工作流

检索不是裸调用,它是工作流里的一个节点。dispatchDatasetSearchdispatch/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 节点 dispatchDatasetConcatdispatch/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.deepRagHandlersearch/index.ts:74),真实实现是 Pro 版 /core/dataset/deepRagprojects/app/src/service/common/system/index.ts:71),克隆里看不到。社区版拿不到多轮迭代检索。

  • 图片索引 / QA / auto 训练模式要 Plus 版。 getTrainingModeByCollection 里带 global.feConfigs?.isPlus 判断,社区版这些模式会回落到普通 chunk。

  • 全文召回强依赖分词质量。 中文靠 jiebaSplitfullTextRecall.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.tscreateCollectionAndInsertData
数据入训练队列packages/service/core/dataset/training/controller.tspushDataListToTrainingQueue
文件解析入队packages/service/core/dataset/training/controller.tspushDatasetToParseQueue
训练队列表 + 排队索引packages/service/core/dataset/training/schema.tsMongoDatasetTraining, TrainingDataSchema
训练模式枚举packages/global/core/dataset/constants.tsTrainingModeEnum
块存储表packages/service/core/dataset/data/schema.tsMongoDatasetData, DatasetDataSchema
检索总入口 + deep RAG 转发packages/service/core/dataset/search/index.tsdefaultSearchDatasetData, deepRagSearch
查询扩展编排packages/service/core/dataset/search/utils.tsdatasetSearchQueryExtension
查询扩展 LLM 实现packages/service/core/ai/functions/queryExtension.tsqueryExtension, lazyGreedyQuerySelection
召回主流程(分层融合)packages/service/core/dataset/search/defaultRecall/index.tssearchDatasetData
多路并发调度packages/service/core/dataset/search/defaultRecall/multiQueryRecall.tsmultiQueryRecall
向量召回packages/service/core/dataset/search/defaultRecall/embeddingRecall.tsembeddingRecall, buildVectorRecallTasks
全文召回packages/service/core/dataset/search/defaultRecall/fullTextRecall.tsfullTextRecall
RRF 融合算法packages/global/core/dataset/search/utils.tsdatasetSearchResultConcat
融合封装 + 去重 + 阈值过滤packages/service/core/dataset/search/defaultRecall/result.tsconcatWeightedRecallLists, removeDuplicateSearchResults, filterSearchResultsByScore
召回名额 + token 上限过滤packages/service/core/dataset/search/defaultRecall/utils.tscountRecallLimit, filterDatasetDataByMaxTokens
只对文本重排packages/service/core/dataset/search/defaultRecall/rerank.tsreRankSearchResults
检索节点调度packages/service/core/workflow/dispatch/dataset/search.tsdispatchDatasetSearch
多库结果合并节点packages/service/core/workflow/dispatch/dataset/concat.tsdispatchDatasetConcat