最近在做AI Agent的项目,用RAG搭了一个知识库,Agent需要调用多个工具(比如先查数据库再调文档搜索)。但发现一个问题:当Agent连续调用两个工具时,第一次返回的结果在第二次对话中经常“消失”了,比如用户问“张三上个月销售额多少?他有哪些客户?”,Agent查到销售额后,第二次调用客户信息工具时,上下文里就看不到销售额的结果,导致回答不完整。用的是LangChain的AgentExecutor,也试了Memory,但效果不好。有没有大佬遇到过?是工具调用时的上下文拼接方式不对,还是需要自己维护一个临时记忆?求指点,感谢!
RAG里Agent调用多个工具时,上下文老是丢,怎么解决?
全部回复
共 144 条遇到过,这问题太典型了,AgentExecutor默认的memory只管对话历史,根本不管工具中间结果的拼接。你那个“查完销售额再查客户”的链条,本质是两步推理依赖,但LangChain默认把每次工具调用当成独立step,上下文只保留最后一条消息,前置结果自然就丢了。
我试过几个土办法,最靠谱的是自己维护一个临时变量池,比如在tool函数里把关键输出塞进一个全局dict,或者用一个自定义的CallbackHandler在每步结束后把结果强制注入到prompt模板里。还有个偷懒的招:把两个查询合并成一个tool,让Agent自己用SQL先查汇总再联表查客户,虽然丑但省事。
另外你检查下是不是用了ConversationBufferMemory,这玩意儿只管用户和AI的对话,工具内部的intermediate_steps根本没存进去。可以试试改用ConversationSummaryMemory,配合return_intermediate_steps=True,效果会好一些,但token消耗会变大。
还有个坑,有些模型对工具结果里的长文本特别容易“选择性遗忘”,特别是OpenAI的gpt-4-turbo,建议对工具返回做结构化压缩,比如只保留核心数值和名单,别把完整查询结果全塞回去。你现在用的什么模型?如果换Claude或者本地模型,行为可能完全不一样。
巧了,我上周刚踩完这个坑。你这个问题八成不是Memory的锅,而是AgentExecutor在中间步骤里对observation的处理方式导致的——它默认只把最终输出拼进下一轮prompt,中间工具返回的原始结果可能被截断或者压根没传进下一轮。我后来是直接把AgentExecutor换成LangGraph,把每个工具的输出显式存到state里,再手动控制下一步该读哪些字段,问题当场就没了。你要是暂时不想换框架,也可以试试在工具函数内部把返回结果顺便写进一个全局dict,然后在下个工具调用前强制把dict内容拼到prompt末尾,但这样确实比较hack。另外注意下工具返回的文本长度,如果超过模型上下文窗口,被静默丢弃也是常事,最好在工具里就做摘要。你用的是哪个模型?有些模型对多轮工具调用的指令遵循能力就是弱一些,换GPT-4或者Claude 3.5可能也会有改善。
我之前也踩过这个坑,LangChain的AgentExecutor在工具间传递上下文时确实有点“健忘”,主要是它默认只把当前这一步的observation塞给下一步,之前的结果如果没有显式塞回prompt,自然就丢了。你试过用ConversationBufferMemory,但那个更适合多轮对话,对工具调用的中间状态帮助不大,因为Agent的execution plan和memory是两套逻辑。我觉得最直接的办法是自己在agent的tools里维护一个全局的临时存储,比如用contextvars或者简单的dict,每个工具执行前把需要的历史结果作为参数传进去,然后返回时把关键信息追加到当前输入里。另一个思路是别依赖Agent自动拼接,改成手动控制流程:先调用第一个工具,拿到结果后,把结果格式化成一个临时“备忘”文本,再拼到第二次调用的user query里,相当于你把上下文硬编码进去。还有个小技巧,检查一下是不是用了ReAct的prompt模板,有时候输出解析器会把中间步骤截断,导致思考里虽然有但实际传给工具的参数没带上,可以在tool的description里明确要求“必须引用之前查询到的具体数值”。我后来干脆用LangGraph的StateGraph重写了流程,每个节点显式声明要读哪些state字段,再也没丢过,虽然代码量多了点,但比在AgentExecutor里折腾省心。你要是懒得重构,可以先试试把Memory换成Zep或者自写一个回调,在tool_start时把上次结果注入当前消息。
我之前也踩过这个坑,LangChain的AgentExecutor默认只把工具返回值塞进中间步骤,如果没开return_intermediate_steps或者没手动拼进下一轮prompt,第二次调用确实看不到前面的结果。你可以试试把每次工具输出显式追加到一个running summary里,再喂给下一轮。或者干脆换个思路,用LangGraph那种带状态的图结构,每个节点自己管理上下文,比硬怼Memory靠谱多了。