.agents/issue/dataset-search-query-extension-latency-analysis.md
用户反馈:普通知识库搜索流程中,开启“问题优化”后搜索耗时明显变长,需要分析原因。
这里的“问题优化”对应当前代码里的 datasetSearchUsingExtensionQuery / query extension,不是 Deep RAG。普通搜索入口包括:
projects/app/src/pages/api/core/dataset/searchTest.tspackages/service/core/workflow/dispatch/dataset/search.tspackages/service/core/dataset/search/index.tspackages/service/core/dataset/search/defaultRecall/*/api/core/dataset/searchTest 在完成权限、余额、图片 key 校验后,构造 searchData 并进入普通搜索:
await defaultSearchDatasetData({
...searchData,
datasetSearchUsingExtensionQuery,
datasetSearchExtensionModel,
datasetSearchExtensionBg
});
如果启用 Deep RAG,则走 deepRagSearch,不在本文“普通搜索”范围内。
工作流节点会先从 userChatInput 或 datasetSearchInput 归一化出 textQueries / imageQueries,再进入同一个普通搜索入口:
await defaultSearchDatasetData({
...searchData,
datasetSearchUsingExtensionQuery,
datasetSearchExtensionModel,
datasetSearchExtensionBg,
userKey: externalProvider.openaiAccount
});
因此搜索测试和工作流节点的慢点基本共用,区别是工作流会携带历史记录 histories,问题优化 prompt 可能更长。
普通搜索未开启问题优化时,大致流程是:
searchDatasetData开启问题优化后,在第 1 步前新增串行前置链路:
datasetSearchQueryExtensionqueryExtensiongenerateCount = 10也就是说,开启后不是“多一次轻量改写”,而是“LLM 改写 + embedding 筛选 + 多 query 召回放大”。
defaultSearchDatasetData 里会先 await datasetSearchQueryExtension(...),之后才调用 searchDatasetData(...)。因此 LLM 改写耗时会完整叠加到搜索总耗时上,不能和后续召回并行抵消。
queryExtension 使用 createLLMResponse({ stream: true }) 获取完整 answerText 后才继续解析。即使底层是 stream,请求方仍需要等完整 JSON 数组返回才能进入下一步。
工作流场景还会把历史记录压缩后放进 prompt。历史越长,LLM 输入 token 越多,首 token 和完整输出耗时都可能增加。
queryExtension 默认 generateCount = 10,prompt 明确要求模型输出最多对应数量的 JSON 字符串数组。拿到候选后会调用 lazyGreedyQuerySelection。
lazyGreedyQuerySelection 会对 [原问题, ...候选问题] 一起调用 embedding。候选 10 条时,这里是 11 条 embedding 输入。
因此问题优化固定增加至少一次 LLM 调用和一次 embedding 调用。对小知识库或原始召回很快的场景,这两个前置调用会成为主要耗时。
datasetSearchQueryExtension 会把原问题和扩展问题拼到 queries 里,最多可能形成 4 条文本 query:
后续 searchDatasetData 使用扩展后的 textQueries。在 embedding 召回里,所有文本 query 会一起生成向量,然后每个向量都会调用一次 recallFromVectorStore。
如果搜索模式是混合检索,full-text 也会对每条 query 跑一次 Mongo text aggregate。虽然 embedding 召回和 full-text 召回之间是并行的,但每条链路内部的任务数已经被扩展 query 放大。
混合检索下:
问题优化把文本 query 从 1 条放大到最多 4 条后,候选集合更大,后续 RRF 融合、去重、相似度过滤和 token 统计都会增加工作量。
如果同时启用 rerank,reRankQuery 会变成原问题和扩展问题的多行拼接,rerank 文档来自文本召回结果去重后的候选集。扩展 query 越多,进入 rerank 的候选越可能变多,rerank 输入 token 和模型耗时也会增加。
搜索测试 API 返回 duration,这是从 API handler 开始到响应前的总耗时。query extension 结果里有 seconds,但当前 SearchDatasetTestResponseSchema 只返回 queryExtensionModel,没有把 queryExtensionResult.seconds、召回耗时、rerank 耗时分别返回。
这会导致现象上只能看到“搜索慢”,但无法直接判断慢在:
开启问题优化后耗时变长是当前链路设计的直接结果,主要原因是:
先不要直接改召回策略,建议加临时或正式分段日志验证实际瓶颈:
defaultSearchDatasetData 记录 query extension 总耗时、扩展 query 数、LLM seconds、query extension embeddingTokens。searchDatasetData 记录 image caption、multiQueryRecall、rerank、token filter 的分段耗时。multiQueryRecall 记录 text query 数、embedding task 数、full-text task 数。如果实际瓶颈集中在 query extension LLM,可以优先考虑减少候选数、换更快模型、加缓存或对短问题/无历史问题跳过扩展。
如果瓶颈集中在召回放大,可以考虑限制进入召回的扩展 query 数、按搜索模式动态调低每路 recall limit,或只让扩展 query 参与 embedding、不参与 full-text。
如果瓶颈集中在 rerank,可以考虑对 rerank 前候选数做上限裁剪,或让 rerank query 使用原问题而不是原问题 + 扩展问题的拼接文本。
packages/service/core/dataset/search/index.ts:普通搜索入口先 await query extension,再调用 searchDatasetData。packages/service/core/dataset/search/utils.ts:datasetSearchQueryExtension 把原问题和扩展问题合并后下发。packages/service/core/ai/functions/queryExtension.ts:默认生成 10 个候选,LLM 完成后再做 lazy greedy。packages/service/core/ai/hooks/useTextCosine.ts:lazy greedy 会对原问题和全部候选调用 embedding。packages/service/core/dataset/search/defaultRecall/embeddingRecall.ts:每个 query 向量都会触发一次 vector store recall。packages/service/core/dataset/search/defaultRecall/fullTextRecall.ts:每个文本 query 都会触发一次 Mongo text aggregate。packages/service/core/dataset/search/defaultRecall/rerank.ts:rerank 对文本召回候选去重后调用 rerank 模型。