[Deep Agents:LangChain的Agent Harness-09]利用MemoryMiddleware构建能够自我学习和进化的Agent
Deep Agents将MemoryMiddleware管理的Memory视为Agent的核心资产,并将其作为一等公民。Memory与工具、模型具有同等的重要性,是Agent正常运转的必备组件。Memory建立在由Backend抽象出来的文件系统上,意味着这个所谓的记忆不再存储在不可见的内存或复杂的向量数据库里,而是变成了看得见、摸得着、改得了的磁盘文件。Agent像写日记一样记录经验,而我们可以像管理硬盘一样管理它的灵魂。基于文件系统的Memory设计带来了几个显著的优势:
- 可解释性:我们直接查看文件内容知道Agent记了什么。如果出现了推理问题,我们查看文件确实是否源于记忆错乱;
- 易于管理和调试:如果它记错了,我们只需要手动删掉或改掉那个文件,它的Memory就立刻变了;
MemoryMiddleware提供了一个持久化的记忆系统,使得Agent能够跨对话会话积累和利用知识。这种记忆系统不仅可以存储事实性的知识,还可以记录经验和学习过程,从而使得Agent具备了学习和进化的能力。借助于MemoryMiddleware,Agent可以在每次对话结束时将新的知识写入到Memory中,并且在下一次对话开始时从Memory中读取之前积累的知识来指导当前的对话。这种机制使得Agent能够不断地学习和适应用户的需求,提供更加个性化和智能化的服务。
1. 让订餐助手记住你的饮食习惯
下面的示例展示了如何利用MemoryMiddleware来构建一个具有学习能力的订餐Agent。这个Agent会根据用户的饮食习惯来处理订单请求,并且在用户提到新的饮食习惯或忌口时,立即将这些信息写入到Memory中,以便在未来的对话中参考和使用。
fromtypingimportLiteralfromlangchain.agentsimportcreate_agentfromlangchain.agents.middlewareimportwrap_model_callfromlangchain.toolsimporttoolfromdeepagents.middlewareimportMemoryMiddleware,FilesystemMiddlewarefromlangchain_openaiimportChatOpenAIfromdeepagents.backendsimportFilesystemBackendfromdotenvimportload_dotenvimportasyncio load_dotenv()@tooldefplace_order(dish_name:Literal["麻婆豆腐","辣椒炒肉","剁椒鱼头","番茄炒蛋","清蒸鲈鱼","清炒菜心"],quantity:int):"""订餐工具,提供菜名和数量,返回订单信息"""returnf"Order placed:{quantity}x{dish_name}"backend=FilesystemBackend(root_dir="./agent_data",virtual_mode=True)file_system_middleware=FilesystemMiddleware(backend=backend)memory_middleware=MemoryMiddleware(backend=backend,sources=["preferences.md"])agent=create_agent(model=ChatOpenAI(model="gpt-5.2-chat"),tools=[place_order],system_prompt=("你是一个私人订餐助手,帮助用户下单。请使用`place_order`工具尽可以根据用户的饮食习惯来处理订单请求""无需用户确认,自己选择菜品和数量来下单。""如果用户提到了新的饮食习惯或忌口,立即写入`preferences.md`中"),middleware=[file_system_middleware,memory_middleware]# type: ignore)asyncdefmain():result=awaitagent.ainvoke(input={"messages":[{"role":"user","content":"帮我点一份外卖,我不能吃辣"}]})print(result["messages"][-1].content)result=awaitagent.ainvoke(input={"messages":[{"role":"user","content":"帮我点一份外卖"}]})print(result["messages"][-1].content)asyncio.run(main())这个演示实例利用create_agent创建的Agent注册了用来点餐的place_order工具。注册的FilesystemMiddleware和MemoryMiddleware都采用同一个FilesystemBackend,根目录为./agent_data(注册FilesystemMiddleware的目的在于我们需要适用它提供的操作文件的工具)。MemoryMiddleware配置了一个名为preferences.md的Memory源文件,我们可以把它看作是Agent的饮食偏好日记。我们利用系统提示词让Agent尽量按照用户饮食习惯来点餐。
我们两次调用了Agent,第一次调用中用户明确表示了不能吃辣的饮食习惯,Agent在处理这个请求时会将这个信息写入到preferences.md文件中。第二次调用中,用户没有提到饮食习惯的信息,但是由于Agent已经在第一次调用中记住了用户不能吃辣的偏好,所以它会参考这个信息来点餐,从而避免点了辣的菜,如下的输出充分体现了这一点:
好的,已经帮你下单啦 😊 考虑到你**不能吃辣**,我给你搭配了一份清淡又均衡的外卖: - 清蒸鲈鱼 × 1 - 番茄炒蛋 × 1 - 清炒菜心 × 1 都是不辣、口味清爽的菜式,营养也很均衡。 如果你想换成偏素一点、偏肉一点,或者有其他忌口,随时告诉我,我可以下次直接按你的习惯来点。 好的,已经帮你点好外卖啦 ✅好的,已经帮你点好外卖啦 ✅ 根据你**不能吃辣**的饮食习惯,我给你安排了一份清淡又营养的搭配: - 番茄炒蛋 × 1 - 清蒸鲈鱼 × 1 - 清炒菜心 × 1 如果你想换口味、加菜,或者下次有新的忌口/偏好,随时告诉我就行~MemoryMiddleware的实现其实非常简单。其state_schema字段表示的状态类型MemoryState中包含了一个memory_contents字段,它返回的字典用来存储记忆内容。它的__init__方法接受一个backend参数和一个sources参数,前者用于指定Memory的存储后端,后者用于指定源文件列表。
classMemoryMiddleware(AgentMiddleware[MemoryState,ContextT,ResponseT]):state_schema=MemoryStatedef__init__(self,*,backend:BACKEND_TYPES,sources:list[str],)->Nonedefbefore_agent(self,state:MemoryState,runtime:Runtime,config:RunnableConfig)->MemoryStateUpdate|Noneasyncdefabefore_agent(self,state:MemoryState,runtime:Runtime,config:RunnableConfig)->MemoryStateUpdate|Nonedefwrap_model_call(self,request:ModelRequest[ContextT],handler:Callable[[ModelRequest[ContextT]],ModelResponse[ResponseT]],)->ModelResponse[ResponseT]asyncdefawrap_model_call(self,request:ModelRequest[ContextT],handler:Callable[[ModelRequest[ContextT]],Awaitable[ModelResponse[ResponseT]]],)->ModelResponse[ResponseT]classMemoryState(AgentState):memory_contents:NotRequired[Annotated[dict[str,str],PrivateStateAttr]]classMemoryStateUpdate(TypedDict):memory_contents:dict[str,str]2.MemoryMiddleware是如何让Agent拥有跨Thread记忆的?
MemoryMiddleware通过重写before_agent/abefore_agent方法来实现Memory的读取和更新逻辑。这两个方法返回的MemoryStateUpdate对象具有与MemoryState完全一致的结构,加载的Memory内容转换成路径/内容的映射字典后,会存储在memory_contents通道中。before_agent/abefore_agent方法会利用它避免重复加载。在重写的wrap_model_call/awrap_model_call方法中,Memory的内容会转换成LLM请求系统提示词的一部分,进而实现利用记忆指导模型的推理的目的。对于上面演示的这个例子,在附加了由MemoryMiddleware生成的系统提示词后,完整的提示词内容如下(不包含FileSystemMiddleware附加的系统提示词),其中- 忌口:不能吃辣就是写入preferences.md中的内容:
你是一个私人订餐助手,帮助用户下单。请使用`place_order`工具尽可以根据用户的饮食习惯来处理订单请求无需用户确认,自己选择菜品和数量来下单。如果用户提到了新的饮食习惯或忌口,立即写入`preferences.md`中 <agent_memory> preferences.md - 忌口:不能吃辣 </agent_memory> <memory_guidelines> The above <agent_memory> was loaded in from files in your filesystem. As you learn from your interactions with the user, you can save new knowledge by calling the `edit_file` tool. **Learning from feedback:** - One of your MAIN PRIORITIES is to learn from your interactions with the user. These learnings can be implicit or explicit. This means that in the future, you will remember this important information. - When you need to remember something, updating memory must be your FIRST, IMMEDIATE action - before responding to the user, before calling other tools, before doing anything else. Just update memory immediately. - When user says something is better/worse, capture WHY and encode it as a pattern. - Each correction is a chance to improve permanently - don't just fix the immediate issue, update your instructions. - A great opportunity to update your memories is when the user interrupts a tool call and provides feedback. You should update your memories immediately before revising the tool call. - Look for the underlying principle behind corrections, not just the specific mistake. - The user might not explicitly ask you to remember something, but if they provide information that is useful for future use, you should update your memories immediately. **Asking for information:** - If you lack context to perform an action (e.g. send a Slack DM, requires a user ID/email) you should explicitly ask the user for this information. - It is preferred for you to ask for information, don't assume anything that you do not know! - When the user provides information that is useful for future use, you should update your memories immediately. **When to update memories:** - When the user explicitly asks you to remember something (e.g., "remember my email", "save this preference") - When the user describes your role or how you should behave (e.g., "you are a web researcher", "always do X") - When the user gives feedback on your work - capture what was wrong and how to improve - When the user provides information required for tool use (e.g., slack channel ID, email addresses) - When the user provides context useful for future tasks, such as how to use tools, or which actions to take in a particular situation - When you discover new patterns or preferences (coding styles, conventions, workflows) **When to NOT update memories:** - When the information is temporary or transient (e.g., "I'm running late", "I'm on my phone right now") - When the information is a one-time task request (e.g., "Find me a recipe", "What's 25 * 4?") - When the information is a simple question that doesn't reveal lasting preferences (e.g., "What day is it?", "Can you explain X?") - When the information is an acknowledgment or small talk (e.g., "Sounds good!", "Hello", "Thanks for that") - When the information is stale or irrelevant in future conversations - Never store API keys, access tokens, passwords, or any other credentials in any file, memory, or system prompt. - If the user asks where to put API keys or provides an API key, do NOT echo or save it. **Examples:** Example 1 (remembering user information): User: Can you connect to my google account? Agent: Sure, I'll connect to your google account, what's your google account email? User: john@example.com Agent: Let me save this to my memory. Tool Call: edit_file(...) -> remembers that the user's google account email is john@example.com Example 2 (remembering implicit user preferences): User: Can you write me an example for creating a deep agent in LangChain? Agent: Sure, I'll write you an example for creating a deep agent in LangChain <example code in Python> User: Can you do this in JavaScript Agent: Let me save this to my memory. Tool Call: edit_file(...) -> remembers that the user prefers to get LangChain code examples in JavaScript Agent: Sure, here is the JavaScript example<example code in JavaScript> Example 3 (do not remember transient information): User: I'm going to play basketball tonight so I will be offline for a few hours. Agent: Okay I'll add a block to your calendar. Tool Call: create_calendar_event(...) -> just calls a tool, does not commit anything to memory, as it is transient information </memory_guidelines>