最近在做一个内部知识库的 Agent,用的 LangChain + Chroma。单轮问答效果还行,但只要对话轮次超过 5 轮,或者用户一个问题里带了好几个条件,检索回来的 chunk 就经常跟之前的上下文打架。比如用户先问了“报销流程”,再问“那电子发票呢”,系统经常会去检索“电子发票”的通用知识,而不是结合上一轮提到的“报销”去过滤。我试过把历史记录全部塞进 retriever 的 query 里,结果检索结果反而更杂。也试过用 LLM 重写 query,但有时候改写完和原意差挺多。想知道大家是怎么处理这种多轮上下文与 RAG 检索的融合的?是切窗口、加 rerank,还是干脆换 GraphRAG?
RAG+Agent 结合时,上下文一长就乱答,有什么好办法吗?
全部回复
共 6 条这个问题我最近也踩过坑,后来把历史对话单独存了个短时记忆池,只取最近两轮的关键实体丢给query改写,比全量塞进去稳很多。另外rerank确实救大命,尤其你这种多条件混合的,先粗筛再精排能把冲突chunk压下去不少。不过你提到的graph思路我也在观望,感觉对强关联场景可能更对症,就是工程复杂度有点劝退。
你这个情况我太熟了,之前做客服问答Agent也踩过同样的坑。我个人觉得问题不一定全在retriever,而在于你把“历史对话”和“当前问题”混在一起去检索了,这会让向量空间变得很乱。我现在的做法是先用LLM做一次严格的query改写,但不会让它自由发挥,而是给一个固定的模板,比如“根据上一轮提到的[实体]和当前问题中的[条件],生成一个仅用于检索的短查询”,这样改写出来的东西不会跑偏。另外,关于多轮上下文,我试过按时间窗口切最近2-3轮,并且用重排模型(比如Cohere Rerank)去过滤掉和当前意图明显冲突的chunk,效果比单纯堆历史好很多。你提到的Graph方案我也在观望,但如果知识库本身没有强关系图谱,上Graph的改造成本可能有点高,不如先试试把每轮对话的摘要单独存一份,检索时用摘要+当前问题去匹配,而不是直接拿原始历史。还有个小细节,Chroma那边可以试试按metadata打上轮次标签,检索时强制过滤掉跟当前主题无关的会话段,这样能减少不少干扰。说到底,多轮RAG的核心是“让retriever只关心当下最相关的信息”,而不是把全部记忆都交给向量库,这个思路调整过来之后,乱答的概率会低很多。
我之前也踩过这个坑,光把历史记录拼进query确实会越拼越乱。后来改成只提取跟当前问题强相关的实体和意图,比如“电子发票”就自动补上“报销”这个限定词,效果好很多。窗口的话我试过切最后两轮,比全量塞进去靠谱,但前提是得有个轻量的意图分类把关键信息捞出来。另外rerank建议加上,尤其你们chunk一多,它能把跟当前意图真正相关的片段顶到前面,不然检索出来的东西自己就先打架了。
我之前也踩过这个坑,后来发现单纯塞历史记录确实会把检索带偏,因为无关的上下文噪音太多了。我的做法是把最近两轮对话压缩成一个“当前意图”的简短描述,再跟原始query拼起来去检索,效果比全量塞历史稳不少。另外rerank确实能救一手,尤其是你这种多条件混合的问题,先用粗召回再用交叉编码器精排,能过滤掉不少冲突的chunk。不过换Graph的话成本有点高,建议先把前两步调好再考虑。
试试把历史对话里跟当前问题相关的实体抽出来,拼到query里做强制过滤,比全塞进去干净多了。
试试把历史轮次按相关性单独过滤一遍再拼进query,比全塞进去干净不少,rerank倒不是必须的。
我们之前也踩过这坑,后来改成让LLM先判断历史里哪些信息跟当前问题有关,再带着这些关键词去检索,效果好很多。