最近在做文档问答的Agent,用的LangGraph搭的流程。一开始直接拿用户原始问题去向量库检索,效果还行,但遇到多跳问题就抓瞎。后来加了一步让LLM拆解/改写用户query,生成几个子问题再分别去召回。结果发现改写后的词向量和原文档的匹配度有时很差,尤其当用户口语化太严重时,改出来的问题跟原始文档里的术语根本对不上,召回率掉得厉害。我也试过加HyDE,但生成假设性文档太慢,且长文档场景下容易引入噪声。想问问这块大家怎么平衡的?是应该让Agent先做意图分类再决定要不要改写,还是干脆用混合检索?或者有没有更好的query压缩策略?
RAG里Agent自己改写query后,召回结果反而变差了,是我姿势不对吗?
全部回复
共 6 条我之前也踩过类似的坑,后来发现问题往往出在“改写”这一步太自由了。不如先让Agent判断一下问题复杂度,简单问题直接原query召回,只有多跳或指代不清时才触发改写,能省不少事。另外你试试给改写加个约束——让它必须保留原问题里的关键实体词,再去做子查询,匹配度会稳很多。混合检索确实是个方向,但更建议优先调好向量化那层的文本预处理,毕竟口语化问题有时候靠同义词扩展就解决了,不一定要动query。
我之前也踩过类似的坑,后来发现核心问题在于改写query时丢了原始语境的“锚点”。现在我会让Agent先判断问题复杂度,简单问题直接原句检索,多跳问题才触发改写,而且改写时保留原问题里的关键实体词,只补充逻辑关系,这样召回率稳多了。
另外混合检索确实能兜底,但别迷信BM25,我试过在改写失败时自动回退到原始query的向量结果,再按得分融合排序,效果比单纯堆检索方式好。你试试看把改写门槛调高,别让LLM自由发挥。
HyDE我也用过,但对长文档确实噪声太大,不如把改写后的子问题再跟原问题做一次向量拼接,相当于用原问题校准一下语义方向,这招对口语化严重的情况挺管用。
混合检索试试吧,关键词保底加向量召回,改写query真不如原词多路查。
确实遇到过类似的情况,query改写这事儿真不是无脑上就行的。我后来试下来感觉,问题可能出在改写目标和检索目标是两套逻辑——LLM觉得它在做“语义等价转换”,但实际上向量空间里它生成的词很可能跟原文术语分布差很远,尤其口语化输入时更是这样。
我现在的做法是分了两步走,先做个轻量的意图分类,判断是不是真的需要多跳拆解,如果只是简单事实类问题就直接用原query检索,省得引入额外噪声。真要拆的话,我也不让Agent自由发挥,而是给它限定几个改写模板,比如必须保留原文里的关键名词和实体,这样能很大程度减少术语漂移。
另外你说的HyDE慢和噪声问题我也踩过坑,后来改成了“局部HyDE”——只对拆出来的子问题生成假设性答案,而且强制要求假设里带上原文可能出现的术语,而不是泛泛地描述。效果比全量HyDE好不少,但确实还是慢,所以我现在是混合检索兜底,向量召回加BM25交叉验证,改写query和原query都跑一遍,最后用reranker合并排序。
我倒想问问,你那个LangGraph流程里,Agent改写query的时候有没有让LLM参考一下文档目录或标题?我之前试过把文档结构喂进去,感觉改写出来的query会更贴近语料风格,但不太确定这个是不是普适的解法。
试试先做意图分类再决定改不改写,多跳才拆,简单问题直接原句检索,能少很多噪声。
我踩过一模一样的坑,后来发现让LLM改写query时得把原始query也保底召回一次,两路结果用RRF融合,改写翻车了也不至于崩。另外意图分类挺关键的,寒暄类或者简单事实型问题压根不用改写,硬拆反而丢信息。HyDE我基本只在术语密集的垂直领域用,通用场景确实不划算。你可以试试先做query难度判断,只对多跳问题走改写分支,单跳直接原query加BM25混检。