最近在搭一个基于Agent的RAG问答系统,用的LangGraph+向量库。遇到个很拧巴的问题:Agent拆解复杂问题成多个子查询时,每个子查询是独立检索好,还是把历史对话(比如用户之前的追问)也拼进去再检索?我试了两种,独立检索的结果碎片化严重,但全带上上下文又容易让向量检索跑偏,召回一些不相关的东西。目前我是在子查询前面加了个“根据历史对话,用户当前想问的是……”的前缀,效果时好时坏。有没有佬遇到过类似问题?你们是怎么处理这个上下文窗口和检索平衡的?还是说根本上就不该让Agent自己规划查询,直接用固定策略拆?求指点,调参调得有点麻了。
RAG里Agent规划出来的子查询,到底该不该带上历史对话上下文?
全部回复
共 107 条我现在的做法是拆两个阶段:规划子查询时带上下文,真正去检索的时候用改写后的独立查询。你那个“根据历史对话……”的前缀其实就是query rewriting的思路,但时好时坏大概率是因为改写不稳定,可以让模型把历史里指代消解掉再输出短查询。另外子查询之间也可以互相做去重,不然碎片化会更严重。固定策略拆也不是不行,但复杂追问场景下确实不如Agent灵活。
写得挺好,建议补充一些性能数据。
子查询独立检索+主查询带上下文做兜底召回,别硬拼历史,容易两头不讨好。
这个坑我也踩过,而且踩得挺深。后来我的做法是把“检索用的query”和“给LLM推理用的context”彻底分开处理——子查询本身保持相对干净,但会做一次轻量的指代消解,比如把“它”“这个”替换成历史对话里明确指向的实体,而不是把整段历史塞进检索语句里。你那个“根据历史对话,用户当前想问的是……”的前缀其实方向是对的,但它会让embedding混进太多噪声,相当于把一句清晰的查询稀释了。
我现在更倾向于让Agent在规划阶段就输出结构化的东西,比如每个子查询带上独立的standalone query加上它依赖的历史槽位,检索时只用standalone query,重排序阶段再把历史上下文喂给cross-encoder去校正相关性。这样碎片化的问题能缓解不少,因为rerank那步能帮你把带上下文的语义补回来。
另外别太迷信Agent自己规划查询,固定策略加少量动态分支往往更稳。可以试试先做query改写再拆子查询,拆完对每个子查询做一次自检:脱离历史能不能独立成立?不能就补全实体,能就别硬塞上下文。全带上历史确实容易跑偏,尤其是多轮追问里话题已经切换的时候,旧上下文会把向量拉到完全无关的簇里去。调参麻了很正常,这块本来就没有银弹,先固定住检索链路再慢慢调Agent的规划粒度会好受很多。
这个坑我也踩过,后来发现关键在于子查询的定位不同。如果子查询是依赖历史才能理解的指代型问题,比如“那它的延迟呢”,那必须带上下文改写;但如果是独立的事实型子查询,硬拼历史反而引入噪声向量。我现在做法是让Agent在规划时给每个子查询打个标记,区分“自包含”和“需改写”,只对后者做query rewriting,前者直接裸查。另外你说的前缀方案时好时坏,很可能是前缀本身把语义重心带偏了,向量模型对“根据历史对话”这种元话语并不敏感,反而稀释了核心词。可以试试把改写和检索拆成两步,改写用LLM做,检索时只喂改写后的干净query。至于要不要让Agent自己规划,我觉得复杂多跳问题还是得让它拆,但可以加个约束:子查询数量超过三个就回退到固定策略,防止它越拆越碎。
我一般让子查询独立检索,再拿原问题做一次全局召回兜底,反而比硬拼历史稳。
这个坑我也踩过,后来发现核心问题不在“带不带上下文”,而在于子查询的语义粒度是不是被污染了。你加的那个前缀其实是在做一次隐式的query rewrite,但LLM改写完可能把用户真正想找的关键实体给稀释掉了,向量检索最怕的就是这种语义漂移。我现在的做法是拆两层:Agent规划阶段只负责把问题拆成逻辑上独立的子问题,每个子问题必须能自洽地被人类读懂;然后在检索前单独跑一个轻量的rewrite步骤,只把指代词(“它”“这个”)替换成历史里的具体实体,不引入额外语义。这样独立检索不会碎片化,也不会因为整段历史拼进去而跑偏。另外你说的固定策略拆,其实要看问题类型,多跳推理类的问题Agent规划还是有优势的,但简单事实追问确实不如规则拆靠谱。调参调麻了很正常,这块目前就没有银弹,多试试把rewrite和retrieval解耦,可能会舒服很多。