第 25 期 | 长程记忆:在 Graph 中保存长期偏好
各位未来的 AI 架构师们,大家好!我是你们的老朋友,AI 技术导师。欢迎来到《LangGraph 多智能体专家课》第 25 期。
我们已经构建了一个相当强大的「AI 万能内容创作机构」。Planner 运筹帷幄,Researcher 博览群书,Writer 妙笔生花,Editor 更是我们内容的最后一道防线。但你有没有遇到这样的情况:同一个客户,每次都要重新交代他的品牌口吻、偏好术语、甚至是他个人对“幽默感”的独特定义?这就像你每次去理发店,都得从头开始解释你喜欢什么发型一样,效率低下,用户体验极差。
今天,我们要解决的就是这个痛点——给我们的 AI 编辑 Agent 装上“长程记忆”,让它能记住用户的历史偏好,真正做到“懂你”。这将是你的 AI 机构从“能干活”到“会来事儿”的关键一步。
🎯 本期学习目标
在本期课程中,你将不仅仅是学习技术,更要理解如何用技术解决实际业务问题。具体来说,你将收获:
- 理解长程记忆的核心价值: 为什么在多智能体系统中,短期记忆(Graph State)不足以支撑复杂的用户交互,长程记忆如何提升用户体验和系统效率。
- 掌握 LangGraph 中长程记忆的构建范式: 学习如何巧妙结合 VectorStore(向量数据库)来存储非结构化的用户偏好,并利用 LangGraph 的 Checkpointers 来管理会话状态和记忆指针。
- 实战 AI 编辑 Agent 的个性化记忆: 为我们的 Editor Agent 打造一个能够“记住”用户特定口吻、风格和历史反馈的工作流,让它成为一个真正贴心的“私人编辑”。
- 识别并规避长程记忆的常见陷阱: 深入探讨记忆膨胀、记忆冲突、检索精度等高阶问题,并提供实用的解决方案。
准备好了吗?让我们一起给 AI 装上“记忆芯片”!
📖 原理解析
在之前的课程中,我们主要依赖 LangGraph 的 Graph State 来维护智能体之间的短期对话和任务上下文。这就像人类的“工作记忆”,处理当前正在进行的任务。然而,一旦一个会话结束,或者需要跨越多个会话来记住用户的个性化需求,短期记忆就捉襟见肘了。
想象一下我们的 AI 编辑 Agent。一个客户可能喜欢幽默风趣的口吻,另一个则偏爱严谨专业的风格。如果每次编辑任务开始时,Editor Agent 都要重新“学习”这些偏好,那它就永远无法成为一个真正高效且个性化的服务者。这就是为什么我们需要引入“长程记忆”。
长程记忆,顾名思义,是能够持久化存储,并在需要时被检索和利用的信息。在 LangGraph 的语境下,我们通常通过两种机制的组合来实现它:
VectorStore(向量数据库): 这是一个存储非结构化数据(如文本、图片、音频等)并能进行语义检索的数据库。我们将用户的个性化偏好、品牌风格指南、历史反馈等信息,以文本的形式嵌入(Embed)成向量,然后存储在 VectorStore 中。当需要时,我们可以根据当前任务或用户ID进行语义相似性搜索,检索出最相关的偏好信息,将其作为上下文注入到 LLM 的提示词中。这解决了“记忆什么”和“如何检索”的问题。
Checkpointers(检查点): LangGraph 内置的
Checkpointer机制允许我们将 Graph 的当前状态持久化到数据库(如 SQLite)中。它主要用于恢复中断的会话,或者在多次迭代中保持状态的连贯性。在本期中,我们将利用 Checkpointer 来保存每个用户的会话 ID(thread_id),以及可能指向 VectorStore 中特定记忆条目的索引或摘要。这解决了“记忆在哪里”和“如何关联”的问题。
核心思想是: Checkpointer 负责记住“这是谁的会话,以及他上次进展到哪了”,而 VectorStore 则负责存储“这个用户的所有长期偏好和历史经验”。当一个编辑任务到来时,Graph 首先通过 Checkpointer 识别用户,然后利用这个用户ID去 VectorStore 中检索其专属的长期偏好,最后将这些偏好作为额外上下文,指导 Editor Agent 进行个性化编辑。如果用户对编辑结果不满意并提供了反馈,这些反馈又可以被结构化或非结构化地添加到 VectorStore 中,从而“教会”AI 编辑 Agent 下次做得更好。
AI 编辑 Agent 的长程记忆工作流:
- 任务接收: AI Content Agency 接收到新的内容编辑请求,并附带
user_id和content_to_edit。 - 记忆检索(Retrieve Preferences):
- Graph 首先通过
Checkpointer识别当前thread_id对应的user_id。 - 使用
user_id作为查询条件,从VectorStore中检索该用户的所有历史偏好、风格指南和之前的反馈。 - 将检索到的偏好信息整合成一个清晰的指令集。
- Graph 首先通过
- 个性化编辑(Editor Agent):
- 将原始内容和检索到的个性化偏好注入到 Editor Agent 的 LLM 提示词中。
- LLM 根据这些指令进行内容润色和优化。
- 用户反馈(Process Feedback):
- 如果用户对编辑结果提供反馈(例如:“太正式了,要更活泼一点!”)。
- Graph 接收反馈,并将其处理(可以是由另一个 LLM 总结,或者直接存储)。
- 将处理后的反馈作为新的记忆条目,添加到
VectorStore中,并与user_id关联。
- 记忆更新:
Checkpointer自动保存 Graph 的最新状态,包括任何可能更新的记忆指针或摘要。
通过这个循环,我们的 AI 编辑 Agent 将不再是“一次性”的工具,而是能够随着每次交互而不断学习和适应的“私人助理”。
Mermaid 图解核心架构
让我们通过一个 Mermaid 图来直观地理解这个流程:
graph TD
A[用户提交编辑请求] --> B{识别用户/会话ID}
B -- thread_id --> C(LangGraph Checkpointer)
C -- user_id --> D[检索用户长期偏好]
D -- 查询 --> E(VectorStore: 偏好/反馈)
E -- 检索结果 --> D
D -- 注入上下文 --> F[Editor Agent (LLM)]
F --> G[生成编辑内容]
G --> H{用户审核/反馈?}
H -- 是 --> I[处理用户反馈]
I -- 更新记忆 --> E
I --> G'[[编辑完成/再次编辑]]'
H -- 否 --> J[完成编辑,交付]
subgraph LangGraph 工作流
B --- F
F --- I
end
style C fill:#f9f,stroke:#333,stroke-width:2px
style E fill:#ccf,stroke:#333,stroke-width:2px
style D fill:#afa,stroke:#333,stroke-width:2px
style I fill:#afa,stroke:#333,stroke-width:2px图例解析:
- 用户提交编辑请求 (A): 工作的起点,包含内容和用户标识。
- 识别用户/会话ID (B): LangGraph 内部机制,通过
configurable: thread_id识别当前会话。 - LangGraph Checkpointer (C): 持久化会话状态,保存
user_id或其关联信息,确保会话连贯性。 - 检索用户长期偏好 (D): 这是一个 LangGraph 节点,负责根据
user_id从 VectorStore 中获取相关记忆。 - VectorStore: 偏好/反馈 (E): 我们的长程记忆库,存储所有用户的个性化偏好、风格指南和历史反馈的向量表示。
- Editor Agent (LLM) (F): 我们的核心编辑智能体,接收原始内容和检索到的个性化偏好进行编辑。
- 生成编辑内容 (G): Editor Agent 的输出。
- 用户审核/反馈? (H): 模拟用户对编辑结果的判断。
- 处理用户反馈 (I): 另一个 LangGraph 节点,负责接收用户反馈,并将其转化为可存储的记忆,更新 VectorStore。
- 完成编辑,交付 (J): 工作流的终点。
这个架构清晰地展示了 Checkpointer 和 VectorStore 如何协同工作,为我们的智能体提供强大的长程记忆能力。
💻 实战代码演练 (Agency 项目中的具体应用)
好了,理论说得再多,不如撸起袖子干一场。现在,我们将把上述原理转化为代码,为我们的 AI Content Agency 里的 Editor Agent 注入长程记忆。我们将使用 Python 和一些流行的 LangChain/LangGraph 组件。
1. 环境准备与依赖安装
首先,确保你的环境已安装必要的库:
pip install -U langchain-openai langgraph langchain_community chromadb tiktoken
2. 代码实现
import os
import sqlite3
from typing import TypedDict, Annotated, List, Literal
from langchain_core.messages import BaseMessage, HumanMessage
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter # 用于处理大文本
from langgraph.graph import StateGraph, END
from langgraph.checkpoint import MemorySaver, SqliteSaver # 用于持久化状态
# 设置你的 OpenAI API 密钥
# 建议通过环境变量设置,例如:export OPENAI_API_KEY="sk-..."
# os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY" # 请替换为你的真实密钥或通过环境变量设置
# 确保API密钥已设置
if not os.getenv("OPENAI_API_KEY"):
raise ValueError("请设置环境变量 OPENAI_API_KEY")
print("--- 环境准备完成,API 密钥已加载 ---")
### 1. 定义 Graph 状态 ###
# TypedDict 用于定义 LangGraph 的状态模式,确保类型安全和清晰
class AgentState(TypedDict):
"""
定义我们的多智能体工作流中的共享状态。
这个状态会在所有节点之间传递。
"""
content: str # 待编辑的原始内容 (Original content to be edited)
user_id: str # 用户的唯一标识符,用于检索个性化偏好 (Unique user ID for retrieving preferences)
preferences: str # 从 VectorStore 检索到的用户偏好 (User preferences retrieved from VectorStore)
edited_content: str # AI 编辑后的内容 (Content after AI editing)
feedback: str # 用户对编辑内容的反馈 (User feedback on the edited content)
# 可以添加一个历史消息列表,用于短期对话上下文,但本期侧重长程记忆
# chat_history: List[BaseMessage]
print("--- AgentState 定义完成 ---")
### 2. 初始化 VectorStore (长程记忆库) ###
# 我们使用 ChromaDB 作为向量数据库,用于存储用户偏好和历史反馈。
# 它支持本地持久化,方便演示。
# 初始化 OpenAI 嵌入模型,用于将文本转换为向量
embeddings = OpenAIEmbeddings()
# 初始化 ChromaDB。我们将数据持久化到本地文件系统,以便跨会话保持记忆。
# persist_directory 是存储数据库文件的路径
VECTOR_DB_PATH = "./chroma_db_editor_memory"
vectorstore = Chroma(embedding_function=embeddings, persist_directory=VECTOR_DB_PATH)
# 模拟一些初始的用户偏好文档
# 真实场景中,这些数据可能来自客户的品牌指南、风格手册或初期沟通
user_preferences_data = [
{"user_id": "client_A", "preference": "口吻:活泼、幽默、简洁。避免冗长和过于正式的表达。", "doc_id": "pref_A_1"},
{"user_id": "client_A", "preference": "关键词:创新、前沿技术、未来趋势。多用比喻和类比。", "doc_id": "pref_A_2"},
{"user_id": "client_B", "preference": "口吻:专业、严谨、细节丰富。强调数据支持和逻辑推理。", "doc_id": "pref_B_1"},
{"user_id": "client_B", "preference": "关键词:市场分析、投资回报、风险管理、合规性。避免主观判断。", "doc_id": "pref_B_2"},
{"user_id": "client_C", "preference": "口吻:平易近人、通俗易懂。避免行业术语和复杂句式。目标读者是普通大众。", "doc_id": "pref_C_1"},
{"user_id": "client_C", "preference": "关键词:健康生活、美食、旅行、亲子教育。内容要积极向上。", "doc_id": "pref_C_2"},
]
# 检查 VectorStore 是否已填充,避免重复添加
# 这是一个简单的检查,更严谨的做法是检查 doc_id 是否存在
existing_docs = vectorstore.get()
if not existing_docs["ids"]: # 如果数据库是空的,则填充
for data in user_preferences_data:
vectorstore.add_texts(
texts=[data["preference"]],
metadatas=[{"user_id": data["user_id"], "doc_id": data["doc_id"]}]
)
print(f"--- VectorStore 已初始化并填充 {len(user_preferences_data)} 条初始用户偏好。---")
else:
print(f"--- VectorStore 已存在 {len(existing_docs['ids'])} 条数据,跳过初始填充。---")
### 3. 定义 Agent (LLM) 和 Graph 节点 ###
# 初始化 ChatOpenAI 模型,作为我们的编辑大脑
llm = ChatOpenAI(model="gpt-4o", temperature=0.5) # 温度设低一点,让编辑更稳定
# --- Graph 节点 1: 检索用户偏好 ---
def retrieve_preferences(state: AgentState):
"""
根据 user_id 从 VectorStore 中检索用户的长期偏好和历史反馈。
"""
user_id = state["user_id"]
if not user_id:
print("警告:未提供 user_id,将使用通用编辑模式。")
return {"preferences": "无特定用户偏好。请以通用、专业且清晰的风格编辑。"}
# 构造查询,尝试检索与该用户相关的偏好
# 可以在这里加入更复杂的查询逻辑,例如结合当前内容类型进行检索
query = f"用户 {user_id} 的内容创作偏好、风格要求以及历史反馈。"
# 执行相似性搜索,并过滤出与当前 user_id 匹配的文档
# LangChain 的 Chroma 默认不支持直接在 similarity_search 中过滤 metadata
# 所以我们先检索,再在代码中过滤,或者使用 Chroma 的高级查询接口(如果需要)
# 为了简化演示,我们假设检索结果中包含了足够多的相关信息,并会通过LLM进行提炼
# 实际生产中,更好的做法是使用 Chroma 的 `similarity_search_with_score` 或 `similarity_search_by_vector`
# 并结合 `where` 参数进行过滤,但这里为了演示,我们先检索再手动过滤
# 检索所有相关文档,然后手动过滤
results = vectorstore.similarity_search_with_score(query, k=5) # 检索前5个最相关的
# 过滤出真正属于当前 user_id 的偏好
filtered_preferences = []
for doc, score in results:
if doc.metadata.get("user_id") == user_id:
filtered_preferences.append(doc.page_content)
# print(f" - 匹配偏好 (Score: {score:.2f}): {doc.page_content[:50]}...")
preferences_str = "\n".join(filtered_preferences)
if not preferences_str:
preferences_str = "无特定用户偏好。请以通用、专业且清晰的风格编辑。"
print(f"--- 未检索到用户 {user_id} 的特定偏好,使用通用模式。---")
else:
print(f"--- 成功检索到用户 {user_id} 的偏好:\n{preferences_str}\n---")
return {"preferences": preferences_str}
# --- Graph 节点 2: AI 编辑器 ---
def editor_agent_node(state: AgentState):
"""
AI 编辑器节点,根据原始内容和检索到的用户偏好进行编辑。
"""
content = state["content"]
preferences = state["preferences"]
user_id = state["user_id"]
prompt = f"""
你是一位经验丰富、洞察力敏锐的资深内容编辑。
你的任务是为用户 {user_id} 润色和优化以下内容。
请务必严格遵循以下用户个性化偏好和风格要求进行编辑。
这些偏好是用户长期积累的,代表了他们的品牌声音和期望。
--- 用户偏好和风格要求 ---
{preferences}
---
原始