ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

LangGraph智能体记忆系统:从对话历史到向量数据库的完整实现

LangGraph智能体记忆系统:从对话历史到向量数据库的完整实现 1. 项目概述从“金鱼脑”到“记忆大师”的进化之路如果你最近在折腾AI Agent尤其是用LangGraph这类框架来构建多步骤的智能体工作流那你大概率遇到过这个让人头疼的问题Agent的“记忆”太差了。它就像一个只有七秒记忆的金鱼上一步刚跟你聊完需求下一步就忘了上下文要么重复提问要么给出完全跑偏的回应。这种体验足以让一个精心设计的智能体项目瞬间变得毫无实用价值。今天我们就来彻底解决这个问题用大约30分钟的时间深入LangGraph的核心讲透它的记忆管理机制让你的Agent从“健忘”的金鱼进化成拥有长期、结构化记忆的“大师”。LangGraph本身是一个基于图Graph来编排复杂Agent工作流的强大框架它把每个步骤节点和步骤间的流转边组织得井井有条。但框架本身并不自动解决“记忆”问题。默认情况下每个节点的执行都是相对孤立的状态State在节点间传递但如何持久化、如何检索、如何让Agent“记住”跨越多轮对话甚至多个会话的关键信息这需要我们手动设计和实现。这恰恰是构建实用Agent的“护城河”。本文将围绕这个核心痛点拆解LangGraph中记忆管理的三种核心范式对话历史记忆、实体记忆和摘要记忆并手把手带你实现一个融合了向量数据库的增强版记忆系统。我们会从原理讲起一直到代码实操和避坑指南目标是让你读完就能动手彻底告别Agent的“金鱼脑”。2. LangGraph记忆管理的核心设计思路在深入代码之前我们必须先理解LangGraph设计记忆系统时的底层逻辑。这不同于简单的变量存储而是一种与图计算紧密结合的状态管理哲学。2.1 状态State是记忆的载体LangGraph的核心是“状态”State。你可以把它理解为一个在整个工作流图中传递的共享字典dictionary。每个节点Node都可以读取和修改这个State。State里存放什么决定了Agent能“记住”什么。一个典型的用于对话的State可能长这样from typing import TypedDict, List, Annotated import operator class AgentState(TypedDict): messages: Annotated[List[str], operator.add] # 对话消息历史 user_query: str # 当前用户问题 context: List[str] # 检索到的相关背景信息 # 我们可以在这里添加更多的“记忆”字段 chat_history: Annotated[List[dict], operator.add] # 结构化对话历史 known_entities: dict # 已知的实体信息注意Annotated[List[str], operator.add]这个用法这是LangGraph的一个精妙设计。operator.add是一个归约器Reducer它定义了当多个节点并发修改同一个字段时虽然对话流通常是顺序的但LangGraph支持更复杂的图如何合并这些修改。这里用add意味着新的消息会被追加append到列表中而不是覆盖。这为记忆的累积性增长奠定了基础。2.2 记忆的粒度与生命周期设计记忆系统时我们需要考虑两个关键维度粒度和生命周期。粒度记忆是存储原始的每一条消息还是存储提取后的关键信息如实体、摘要原始消息最全但冗余且消耗大关键信息精炼但可能丢失细节。生命周期记忆是仅存在于当前一次图运行in-session还是需要持久化到数据库跨越多次会话cross-session前者简单后者复杂但更实用。LangGraph的灵活性在于它不强制你使用某种特定模式而是提供了一套机制主要是通过State和自定义节点/边让你可以根据应用场景自由组合。我们接下来要讲的三种模式就是对不同粒度和生命周期需求的具体实现方案。2.3 图结构对记忆流的影响记忆的读写发生在节点中。一个设计良好的LangGraph应用其节点功能应该是清晰的。例如你可能有一个retrieve_node专门从外部知识库获取信息一个generate_node调用LLM生成回复还有一个update_memory_node专门负责处理和更新记忆。记忆更新的时机也很重要是在生成回复后立即更新还是先经过一个验证节点这取决于你的业务逻辑。理解你的图结构是设计有效记忆管理的前提。3. 三种核心记忆模式详解与实现我们将逐一拆解三种最经典、最实用的记忆模式并提供可直接运行的代码示例。假设我们正在构建一个客户服务Agent它需要记住用户偏好、历史问题以及产品信息。3.1 模式一对话历史记忆Conversation Buffer Memory这是最简单直接的模式就是将整个对话的原始消息列表保存在State中。每次新的交互都将用户输入和AI回复追加到这个历史列表里并在下次生成时将整个历史或最近N条历史作为上下文提供给LLM。实现要点State定义如上文示例在State中定义一个messages或chat_history字段使用operator.add归约器。节点设计在负责调用LLM的节点通常是generate_node中从State中读取历史消息拼接到系统提示词或用户输入中。更新时机在LLM生成回复后在一个单独的节点或generate_node的内部将用户消息和AI消息成对地写入State的历史字段。代码示例from langgraph.graph import StateGraph, END from langchain_core.messages import HumanMessage, AIMessage import operator # 1. 定义包含对话历史的State class BasicState(TypedDict): chat_history: Annotated[list, operator.add] # 存储LangChain的Message对象 input: str output: str # 2. 定义节点函数 def call_llm(state: BasicState): from langchain_openai import ChatOpenAI # 示例需安装langchain-openai llm ChatOpenAI(modelgpt-4) # 构建包含历史的对话链 messages state[“chat_history”] [HumanMessage(contentstate[“input”])] response llm.invoke(messages) # 准备更新State new_messages [HumanMessage(contentstate[“input”]), AIMessage(contentresponse.content)] return {“chat_history”: new_messages, “output”: response.content} # 3. 构建图 graph_builder StateGraph(BasicState) graph_builder.add_node(“generate”, call_llm) graph_builder.set_entry_point(“generate”) graph_builder.add_edge(“generate”, END) graph graph_builder.compile()注意这种模式虽然简单但有两个致命缺点。一是上下文长度限制对话轮次一多就会超出LLM的上下文窗口。二是信息冗余无关的历史对话会干扰LLM当前的重点。因此它只适用于短对话或演示场景。3.2 模式二实体记忆Entity Memory这种模式更智能它不再记忆原始对话而是从中提取出关键实体如人名、产品名、日期、偏好等及其属性/关系存储在一个结构化的格式中如字典。下次交互时只将与当前查询相关的实体信息注入上下文。实现要点State定义在State中增加一个entity_store字段类型为dict。实体提取节点创建一个extract_entity_node使用一个专门的LLM调用或NER模型从最新的对话内容中识别并结构化实体信息。记忆更新逻辑将提取的新实体与entity_store中的旧实体进行智能合并例如更新某个用户的偏好设置而不是简单地追加。检索与注入在生成回复前有一个retrieve_entity_node它分析当前用户输入从entity_store中查找相关实体并将其作为背景信息提供给生成节点。代码示例关键部分class EntityState(TypedDict): input: str output: str chat_history: Annotated[list, operator.add] entity_store: dict # 例如{“user_preferences”: {“theme”: “dark”, “language”: “zh”}, “mentioned_products”: [“ProductA”]} def extract_entity_node(state: EntityState): # 使用LLM或规则从state[‘input’]和最近的chat_history中提取实体 # 这是一个简化的提示词示例 extraction_prompt f 从以下对话中提取关键实体信息并以JSON格式返回。重点关注用户偏好、提到的产品、日期、任务等。 对话{state[‘chat_history’][-4:] if state[‘chat_history’] else ‘无’} {state[‘input’]} 只返回JSON例如{{“user_preferences”: {{“budget”: “moderate”}}, “mentioned_products”: [“Laptop X1”]}} # 调用LLM进行提取... new_entities llm.invoke(extraction_prompt) # 假设llm返回了解析后的dict # 合并实体到存储中这里需要自定义合并逻辑如深度更新 current_store state.get(“entity_store”, {}) merged_store deep_merge(current_store, new_entities) # deep_merge需要自己实现 return {“entity_store”: merged_store} def generate_with_entity_node(state: EntityState): # 检索相关实体 relevant_entities retrieve_relevant_entities(state[“entity_store”], state[“input”]) # 将实体信息作为上下文加入提示词 enriched_prompt f”已知背景信息{relevant_entities}\n\n用户问题{state[‘input’]}” # 调用LLM生成回复... response llm.invoke(enriched_prompt) return {“output”: response.content}实操心得实体提取的准确性是关键。你可以使用更强大的模型如gpt-4或者结合预训练的NER模型。合并逻辑deep_merge也需要仔细设计比如对于列表类型的属性是追加还是去重对于字典是覆盖还是合并这些都需要根据业务语义来定。3.3 模式三摘要记忆Conversation Summary Memory这是解决长上下文问题的经典方法。它不保存全部历史而是维护一个不断演进的“对话摘要”。每次新交互后LLM都会根据旧摘要和最新对话生成一个新的、更新的摘要。实现要点State定义包含一个conversation_summary字符串字段。摘要更新节点这是一个核心节点。它接收当前的conversation_summary和最新的对话片段如最近两轮QA要求LLM输出一个新的、整合了所有信息的摘要。使用摘要在生成回复时将最新的conversation_summary作为主要的历史上下文提供给LLM而不是完整的原始历史。代码示例class SummaryState(TypedDict): input: str output: str conversation_summary: str # 核心滚动摘要 last_interaction: list # 可选存储最近一两轮原始对话用于生成摘要 def update_summary_node(state: SummaryState): current_summary state.get(“conversation_summary”, “对话尚未开始。”) # 假设last_interaction存储了[HumanMessage, AIMessage]格式的最近对话 recent_chat state.get(“last_interaction”, []) summary_prompt f””” 你是一个高效的对话摘要助手。 现有对话摘要{current_summary} 最新发生的对话内容{recent_chat} 请根据最新对话内容更新上面的对话摘要。确保摘要简洁包含关键事实、用户决策和待办事项。 直接输出更新后的摘要不要有其他内容。 “”” new_summary llm.invoke(summary_prompt).content # 清空或更新last_interaction为下一轮做准备 return {“conversation_summary”: new_summary, “last_interaction”: []} def generate_with_summary_node(state: SummaryState): context f”对话背景摘要{state[‘conversation_summary’]}\n\n当前用户问题{state[‘input’]}” response llm.invoke(context) # 将本轮交互存入last_interaction供下次更新摘要使用 new_interaction [HumanMessage(contentstate[‘input’]), AIMessage(contentresponse.content)] return {“output”: response.content, “last_interaction”: new_interaction}注意事项摘要记忆存在“信息损耗”。LLM在总结时可能会丢失一些细节特别是数字、特定名称等。因此它常与实体记忆结合使用摘要记录大致脉络实体记忆保存精确细节。4. 构建混合记忆系统集成向量数据库实现长期记忆单一模式往往不够。一个健壮的、生产级的Agent记忆系统通常是上述模式的混合体并引入外部存储如向量数据库来实现长期、可检索的语义记忆。4.1 系统架构设计我们的目标是构建这样一个系统短期工作记忆使用对话摘要来维持当前会话的连贯性避免上下文过长。中期结构化记忆使用实体记忆来精确记录用户偏好、重要事实等关键数据存储在内存或快速KV数据库中。长期语义记忆将对话中的重要片段Chunks嵌入成向量存入向量数据库如Chroma, Pinecone, Weaviate。当用户提到相关话题时通过语义搜索快速召回。State定义升级class AdvancedState(TypedDict): input: str output: str summary: str # 短期摘要 entities: dict # 中期实体存储 # 长期记忆通过向量数据库接口交互State中可能只存一个session_id用于关联 session_id: str retrieved_memories: list # 临时存放本次检索到的长期记忆片段4.2 关键节点实现记忆的写入与检索节点A记忆归档节点Memory Archival Node这个节点在每轮对话或一个会话结束后被触发。它的任务是将有价值的对话内容存入长期记忆。def archive_memory_node(state: AdvancedState): # 1. 判断哪些内容值得存档例如包含事实陈述、决策、用户明确说“记住这个”等 content_to_archive identify_valuable_content(state[‘summary’], state[‘entities’], state[‘chat_history’]) if not content_to_archive: return {} # 没有值得存档的直接跳过 # 2. 为内容生成嵌入向量 embeddings embedder.embed_documents(content_to_archive) # 3. 构建记忆对象包含元数据如时间戳、会话ID、实体标签 memory_objects [] for content, embedding in zip(content_to_archive, embeddings): memory_obj { “content”: content, “embedding”: embedding, “metadata”: { “session_id”: state[‘session_id’], “timestamp”: datetime.now().isoformat(), “related_entities”: list(state[‘entities’].keys())[:3] # 关联前几个实体 } } memory_objects.append(memory_obj) # 4. 批量插入向量数据库 vectorstore.add_documents(memory_objects) # 假设vectorstore已初始化 return {“archive_status”: “success”}节点B记忆检索节点Memory Retrieval Node这个节点在生成回复前被调用负责从长期记忆中寻找相关信息。def retrieve_memory_node(state: AdvancedState): # 1. 基于当前用户输入和对话摘要生成检索查询 # 单纯用用户输入可能不够结合摘要能提供更丰富的上下文 query f”{state[‘summary’]} {state[‘input’]}” # 2. 可选对查询进行重写或扩展以提高召回率 # rewritten_query query_rewriter_llm.invoke(f”为了从记忆库中找到相关信息请优化以下查询{query}”) # 3. 执行向量相似性搜索 retrieved_docs vectorstore.similarity_search(query, k3) # 返回最相关的3条记忆 # 4. 将检索到的记忆片段格式化存入State供生成节点使用 formatted_memories [f”- {doc.page_content} (来源: {doc.metadata[‘timestamp’]})” for doc in retrieved_docs] return {“retrieved_memories”: formatted_memories}4.3 图工作流编排现在我们将这些节点组装到一个完整的LangGraph工作流中builder StateGraph(AdvancedState) # 添加节点 builder.add_node(“process_input”, process_input_node) # 预处理输入 builder.add_node(“retrieve_memories”, retrieve_memory_node) # 检索长期记忆 builder.add_node(“generate_response”, generate_response_node) # 综合所有记忆生成回复 builder.add_node(“update_summary”, update_summary_node) # 更新短期摘要 builder.add_node(“extract_entities”, extract_entity_node) # 提取并更新实体 builder.add_node(“archive_memories”, archive_memory_node) # 归档到长期记忆 # 设置边定义执行流程 builder.set_entry_point(“process_input”) builder.add_edge(“process_input”, “retrieve_memories”) builder.add_edge(“retrieve_memories”, “generate_response”) # 生成回复后并行更新摘要、实体和归档如果需要 builder.add_edge(“generate_response”, “update_summary”) builder.add_edge(“generate_response”, “extract_entities”) builder.add_edge(“generate_response”, “archive_memories”) # 更新和归档完成后结束本轮 builder.add_edge(“update_summary”, END) builder.add_edge(“extract_entities”, END) builder.add_edge(“archive_memories”, END) # 编译图 graph builder.compile()这个工作流清晰地展示了信息流先检索已有的相关记忆再生成回答最后并行更新短期、中期记忆并选择性归档到长期记忆。5. 实战避坑指南与性能优化理论很美好但实际搭建时你会遇到一堆坑。下面是我从多个项目中总结出的血泪经验。5.1 常见问题与排查技巧问题1记忆混乱或相互污染现象Agent在回答用户A的问题时提到了用户B的信息。排查首先检查session_id是否在每次新的用户会话时都正确生成并传递。其次检查向量数据库的检索查询是否包含了足够唯一的上下文如session_id作为元数据过滤器。最后检查State的归约器Reducer逻辑确保没有错误地跨会话合并了数据。解决在检索长期记忆时强烈建议使用元数据过滤。例如vectorstore.similarity_search(query, k3, filter{“session_id”: state[‘session_id’]})。这能确保只检索当前会话相关的记忆。问题2实体提取不准或合并冲突现象用户说“我喜欢蓝色”但实体记忆里却记录成了“主题是蓝色”或者用户后来改成“喜欢绿色”但记忆里两者并存。排查检查实体提取的提示词Prompt是否足够清晰要求LLM输出稳定的JSON格式。检查deep_merge函数的逻辑对于用户偏好这类键值对通常应采用覆盖策略对于“提及产品”这类列表可能采用追加并去重的策略。解决为不同类型的实体设计不同的合并策略。可以引入一个“置信度”或“时间戳”字段在合并时保留最新的或置信度更高的信息。问题3摘要记忆的信息丢失严重现象对话进行到后面Agent完全忘记了早期讨论过的关键细节。排查摘要的提示词是关键。过于强调“简洁”会导致信息丢失。检查你提供给LLM用于生成摘要的“最新对话内容”是否包含了足够轮次的历史比如最近5轮而不是2轮。解决调整摘要提示词明确要求保留“关键事实、数字、决策和待办事项”。可以尝试一种“分层摘要”策略每10轮对话生成一个“章节摘要”然后维护一个由这些章节摘要组成的“总体概要”。问题4向量检索召回不相关的内容现象检索到的记忆片段与当前问题风马牛不相及。排查首先检查嵌入模型Embedding Model是否适合你的领域。通用模型在专业领域可能表现不佳。其次检查存入向量库的“内容块”Chunk是否大小合适。过大的块包含太多信息导致语义模糊过小的块可能缺乏上下文。解决考虑使用领域数据微调嵌入模型或尝试不同的开源/商业模型。优化分块策略尝试按语义如句子或段落而非固定长度分块。在检索时可以尝试“多查询检索”用LLM从用户问题生成多个不同角度的查询或“重排序”先用向量检索出较多结果再用一个更精细的模型重新排序以提高精度。5.2 性能与成本优化策略记忆归档的异步化archive_memory_node操作尤其是嵌入生成和数据库写入可能比较耗时会阻塞对话响应。在生产环境中应该将其改为异步任务。节点只负责将需要归档的数据放入一个消息队列如Redis Stream, RabbitMQ由后台工作进程消费队列并执行实际的归档操作。缓存检索结果对于频繁出现的相似查询其检索结果在一定时间内是稳定的。可以在retrieve_memory_node中加入缓存层如使用functools.lru_cache或外部缓存Redis键为查询文本的哈希值为检索到的记忆片段。设置合理的TTL生存时间。控制LLM调用成本实体提取、摘要更新、查询重写等都需要调用LLM是成本大头。可以采取以下措施设定频率阈值例如每3轮对话才更新一次摘要而不是每轮都更新。使用小模型对于摘要、实体提取这类对创造力要求不高的任务可以使用更便宜、更快的模型如gpt-3.5-turbo甚至专门的小型微调模型。批量处理如果支持将多个记忆处理任务批量发送给LLM API。向量数据库的索引优化随着记忆条目的增长检索速度会变慢。确保你的向量数据库使用了高效的索引如HNSW。定期清理过时或无用的记忆条目或者按时间分片存储。5.3 评估记忆系统的有效性如何知道你的记忆系统是否真的变“聪明”了不能只靠感觉需要设计一些评估方法人工评估设计一组多轮对话的测试用例让评估者判断Agent在后续轮次中是否能正确引用之前提到过的信息。自动化指标实体一致性检查在对话中提到的实体是否被正确记录并在后续被召回。事实留存率在对话早期植入几个关键事实在最后几轮提问看Agent能否回答正确。检索相关性对每次检索结果进行人工或模型打分评估其与当前问题的相关度。记忆管理不是一劳永逸的它需要根据你的Agent的具体应用场景进行持续迭代和调优。从简单的对话历史开始逐步引入实体、摘要最终集成向量数据库实现长期语义记忆这是一个复杂度逐步提升但能力也显著增强的过程。最关键的是理解每种模式解决的问题和带来的代价做出适合你当前阶段的选择。
返回列表