LangChain 1.0 智能体开发(三):Agent 记忆管理——从短期对话到跨会话长期记忆

前言

上一篇讲了工具------Agent 与外部世界交互的接口。这篇讲记忆。

记忆是让 Agent 从"一次性问答机器"变成"能持续协作的伙伴"的关键。没有记忆的 Agent 每次都从零开始:它不记得你叫什么、不喜欢什么、上次聊到哪了。这种 Agent 用两次就会让人觉得"聪明但健忘",信任感很难建立。

但记忆管理远没有"把所有对话存下来"这么简单。LangChain 1.0 把记忆拆成了两个层次:短期记忆长期记忆 。这两者的分界线不是"存在内存里"还是"存在数据库里"------这是一个极其常见的误解。分界线在于数据的生命周期跟谁绑定

官方文档在短期记忆页面写得很清楚:

Short-term memory in LangGraph is part of the agent state, persisted by the checkpointer, and organized by thread_id.

(LangGraph 中的短期记忆是 Agent 状态的一部分,由检查点机制持久化,按 thread_id 组织。)

而长期记忆,官方在另一篇文档里说:

Long-term memory is built on the LangGraph store, organized as JSON documents by namespace and key, persisted across sessions, and commonly recalled using user_id.

(长期记忆建在 LangGraph Store 之上,以 JSON 文档形式按 namespace 加 key 组织,跨会话可召回,常用 user_id 作为 namespace。)

一个是 thread_id,一个是 user_id。这就是分界线。

这篇文章会把这件事从头到尾讲透:先厘清概念,再逐个拆解 Checkpointer、上下文裁剪、自定义 State、向量数据库、BaseStore 的 API 细节,最后给出生产环境的组合架构。


一、短期 vs 长期记忆:分界线到底在哪

1.1 一个普遍的误解

"内存 = 短期记忆,数据库 = 长期记忆"

这句话错在把存储介质等同于记忆类型 。事实上,PostgresSaver 把数据存在 PostgreSQL 数据库里,但它管理的仍然是短期记忆;InMemoryStore 把数据存在 Python 进程内存里,但它管理的是长期记忆。

存储介质(内存 / 数据库 / 向量库)解决的是"数据放在哪"的问题;记忆类型解决的是"数据活多久"的问题。两件事不该混在一起。

1.2 正确的分界标准

维度 短期记忆 长期记忆
生命周期 绑定到会话(thread)。会话结束,数据语义上失效 绑定到用户 / 业务实体。跨会话持久保留
组织方式 thread_id 隔离。不同 thread 互不可见 namespace(通常是 user_id)+ key 组织
检索模式 自动加载:传入 thread_id 时,框架自动恢复完整历史 主动检索:Agent 在运行时通过工具主动查询
数据形态 完整的消息列表(messages) 结构化档案(KV)、语义向量(Embedding)、用户偏好
典型载体 InMemorySaverPostgresSaver BaseStore(KV)、向量数据库(Chroma / Pinecone)
清理时机 会话结束后无意义(但持久化载体不会自动删) 需要主动管理:TTL 过期、手动清理、压缩归档

一句话总结:短期记忆跟着对话走,长期记忆跟着用户走。

1.3 为什么需要两层

只用短期记忆,Agent 每次新对话都是失忆状态。用户的偏好、习惯、历史决策全部丢失。

只用长期记忆,每次对话都得重新理解上下文。"我刚才说的那个""接着上面的"这类指代全靠用户重新解释。

两层配合,短期记忆维持当前对话的连贯,长期记忆积累跨对话的知识和偏好。一个让 Agent 会说人话,一个让 Agent 越用越懂你。


二、短期记忆管理:Checkpointer 机制

2.1 Checkpointer 是短期记忆的灵魂

不加 checkpointer 参数:Agent 是无状态的。每次 invoke 都是全新的开始,不记得上一轮说了什么。

加一行 checkpointer=memory:LangGraph 会在每一步执行后,把完整的 state(包括所有消息历史)序列化并存入 Checkpointer。当你再次 invoke 并传入同一个 thread_id 时,LangGraph 先去查"这个 ID 上次停在哪里",加载状态,把新消息 append 进去,然后继续运行。

复制代码
用户第一次发消息 → Agent 处理 → 状态存到 Checkpointer(thread_id 作为 key)
用户第二次发消息 → 加载旧状态 → 追加新消息 → Agent 带着完整上下文处理 → 再次存储

2.2 Thread ID:短期记忆的钥匙

thread_id 就是 Web 开发里的 Session ID。你需要为每个用户或每次对话生成一个唯一标识。不同 thread_id 之间的记忆完全隔离------用户 A 和用户 B 的对话互不可见。

python 复制代码
config_user_a = {"configurable": {"thread_id": "session_user_A"}}
config_user_b = {"configurable": {"thread_id": "session_user_B"}}
# 两者的记忆完全隔离

2.3 InMemorySaver:开发环境首选

最简单的 Checkpointer 实现,数据存在 Python 进程的内存字典里。进程重启后数据丢失。

python 复制代码
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver

load_dotenv(override=True)

# ==================== 定义工具 ====================

@tool
def get_user_info(name: str) -> str:
    """查询用户信息,返回姓名、年龄和爱好。
    
    Args:
        name: 用户姓名,如"陈明"、"张三"
    """
    user_db = {
        "陈明": {"age": 28, "hobby": "旅游、滑雪、喝茶"},
        "张三": {"age": 32, "hobby": "编程、阅读、电影"},
    }
    info = user_db.get(name)
    if not info:
        return f"未找到用户 {name} 的信息"
    return f"姓名: {name}, 年龄: {info['age']}岁, 爱好: {info['hobby']}"

# ==================== 加载模型 ====================

model = ChatOpenAI(
    model="gpt-4o-mini",
    temperature=0,
)

# ==================== 创建带短期记忆的 Agent ====================

memory = InMemorySaver()  # 内存级检查点

agent = create_agent(
    model=model,
    tools=[get_user_info],
    system_prompt="你是一个友好的助手,记住用户提到的个人信息。",
    checkpointer=memory,  # ← 这一行启用短期记忆
)

# ==================== 测试多轮对话 ====================

config = {"configurable": {"thread_id": "user_123"}}

# 第一轮:用户自我介绍
r1 = agent.invoke(
    {"messages": [{"role": "user", "content": "你好,我叫陈明,很高兴认识你!"}]},
    config=config
)
print(f"AI: {r1['messages'][-1].content}")

# 第二轮:测试记忆
r2 = agent.invoke(
    {"messages": [{"role": "user", "content": "你还记得我叫什么名字吗?"}]},
    config=config  # 同一个 thread_id → 自动携带上下文
)
print(f"AI: {r2['messages'][-1].content}")
# 预期输出:当然记得,你叫陈明!

# 第三轮:查看当前记忆状态
state = agent.get_state(config)
print(f"当前记忆中共有 {len(state.values['messages'])} 条消息")

# 第四轮:新会话(不同 thread_id)→ 无记忆
config_new = {"configurable": {"thread_id": "user_456"}}
r3 = agent.invoke(
    {"messages": [{"role": "user", "content": "我们之前聊过吗?"}]},
    config=config_new
)
print(f"新会话 AI: {r3['messages'][-1].content}")
# 预期输出:我们之前没有聊过,这是我们的第一次对话。

2.4 PostgresSaver:生产环境持久化

InMemorySaver 开发够用,但生产环境需要数据持久化。PostgresSaver 把检查点存到 PostgreSQL,进程重启不丢失,支持多实例共享。

但请注意:即使数据存到了数据库,它仍然是短期记忆。 原因:

  • 作用域限制 :只检索和加载当前 thread_id 的数据
  • 生命周期语义:数据属于"本次会话",即使物理上没删
  • 无跨会话检索:新会话中无法自动访问旧会话数据(除非手动指定旧 thread_id)
python 复制代码
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain.agents import create_agent
from langgraph.checkpoint.postgres import PostgresSaver
from psycopg_pool import ConnectionPool

load_dotenv(override=True)

# ==================== 数据库配置 ====================

DB_URI = os.getenv("DATABASE_URL", "postgresql://user:pass@localhost:5432/agent_db")

# ==================== 定义工具 ====================

@tool
def get_user_info(name: str) -> str:
    """查询用户信息,返回姓名、年龄和爱好。
    
    Args:
        name: 用户姓名
    """
    user_db = {
        "陈明": {"age": 28, "hobby": "旅游、滑雪、喝茶"},
        "张三": {"age": 32, "hobby": "编程、阅读、电影"},
    }
    info = user_db.get(name)
    if not info:
        return f"未找到用户 {name} 的信息"
    return f"姓名: {name}, 年龄: {info['age']}岁, 爱好: {info['hobby']}"

# ==================== 模型 ====================

model = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# ==================== 使用 Postgres 持久化 ====================

# 使用连接池管理数据库连接
with ConnectionPool(
    conninfo=DB_URI,
    max_size=20,              # 最大连接数
    kwargs={"autocommit": True}
) as pool:
    
    checkpointer = PostgresSaver(pool)
    checkpointer.setup()  # 幂等操作:首次运行自动创建表结构
    
    agent = create_agent(
        model=model,
        tools=[get_user_info],
        system_prompt="你是一个友好的助手,记住用户提到的个人信息。",
        checkpointer=checkpointer,
    )
    
    # 使用方式与 InMemorySaver 完全一致
    config = {"configurable": {"thread_id": "prod_user_001"}}
    
    agent.invoke(
        {"messages": [{"role": "user", "content": "我是张三,请记住我的信息"}]},
        config=config
    )
    
    result = agent.invoke(
        {"messages": [{"role": "user", "content": "我是谁?"}]},
        config=config
    )
    print(f"AI: {result['messages'][-1].content}")
    # 即使重启程序,只要 thread_id 不变,记忆依然存在

2.5 两种 Checkpointer 对比

维度 InMemorySaver PostgresSaver
存储位置 Python 进程内存 PostgreSQL 数据库
持久化 进程重启后丢失 持久保存
性能 极高(纳秒级) 较高(毫秒级)
扩展性 单进程限制 支持多实例、高并发
适用环境 开发、测试 生产、分布式部署
核心定位 短期记忆 短期记忆(持久化版)

关键认知: 两者都是短期记忆。区别只在于"数据能不能扛住重启",不在于"是不是长期记忆"。


三、上下文裁剪:防止记忆撑爆 Token

3.1 问题

短期记忆会累积所有消息。随着对话进行,state["messages"] 可能包含几千条消息。直接全部传给 LLM 会:

  1. 烧钱------每轮都按全量 token 计费
  2. 超限报错------超过模型的上下文窗口(如 128K / 8K)直接报错
  3. 性能下降------上下文越长,模型的注意力越分散

3.2 解决思路

State 中保存 100% 的完整历史(用于审计和回溯),但传给 LLM 时只传最近的 N 条消息或 N 个 token。

这就是 trim_messages 的作用。

复制代码
State 中:[msg1, msg2, msg3, ..., msg100]     ← 完整保存
传给 LLM:[msg95, msg96, msg97, ..., msg100]   ← 只传最近的

3.3 trim_messages 核心参数

参数 类型 说明
messages list 待裁剪的消息列表
max_tokens int 允许的最大 token 数,超过则触发裁剪
token_counter Callable / str token 计数函数,可传 len(按条数)或自定义函数(精确计算 token)
strategy str "last" 保留最新消息,"first" 保留最早消息
include_system bool 是否保留 System 消息(通常必须保留)
allow_partial bool 是否允许部分消息(False 则保持消息完整性)
start_on str 裁剪后第一条消息的角色,"human" 确保以用户消息开始

3.4 精确 Token 计数

不同模型使用不同的 token 编码。tiktoken 是 OpenAI 官方的 token 编码库,支持精确计数。

python 复制代码
import tiktoken

def get_token_encoder(model_name: str = "gpt-4o-mini"):
    """获取 tiktoken 编码器实例"""
    try:
        encoding = tiktoken.encoding_for_model(model_name)
        return encoding
    except KeyError:
        # 不在映射表中的模型,使用默认编码
        return tiktoken.get_encoding("o200k_base")

# 模型与编码器对应关系:
# - gpt-4o, gpt-4o-mini: o200k_base
# - gpt-4-turbo: cl100k_base
# - text-davinci-003: p50k_base

TOKEN_ENCODER = get_token_encoder("gpt-4o-mini")

def count_tokens(messages):
    """精确计算消息列表的 token 总数"""
    total = 0
    for msg in messages:
        role_tokens = len(TOKEN_ENCODER.encode(msg.type))
        content_tokens = len(TOKEN_ENCODER.encode(msg.content))
        format_overhead = 4  # 每条约 4 个特殊 token(消息边界标记)
        total += role_tokens + content_tokens + format_overhead
    return total

3.5 实战:多轮对话 + 自动裁剪

python 复制代码
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, trim_messages
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
import tiktoken

load_dotenv(override=True)

# ==================== 配置 ====================

MAX_TOKENS = 2000  # 裁剪阈值(根据模型上下文窗口调整)
MODEL_NAME = "gpt-4o-mini"
TOKEN_ENCODER = tiktoken.encoding_for_model(MODEL_NAME)

def count_tokens(messages):
    """精确 token 计数"""
    total = 0
    for msg in messages:
        total += len(TOKEN_ENCODER.encode(msg.type))
        total += len(TOKEN_ENCODER.encode(msg.content))
        total += 4  # 格式开销
    return total

# ==================== 工具 ====================

@tool
def search_weather(city: str) -> str:
    """查询指定城市的天气信息。
    
    Args:
        city: 城市名称,如"北京"、"上海"
    """
    data = {"北京": "晴朗 25°C", "上海": "多云 28°C", "广州": "小雨 30°C"}
    return f"{city}:{data.get(city, '暂无数据')}"

# ==================== 模型 ====================

model = ChatOpenAI(model=MODEL_NAME, temperature=0)

# ==================== 创建带记忆的 Agent ====================

memory = InMemorySaver()
agent = create_agent(
    model=model,
    tools=[search_weather],
    system_prompt="你是一个简洁的助手,记住用户提到的信息。",
    checkpointer=memory,
)

# ==================== 裁剪并调用的封装函数 ====================

def invoke_with_trim(agent, user_input: str, config: dict):
    """在调用 Agent 前自动裁剪上下文"""
    
    # 1. 获取当前历史
    state = agent.get_state(config)
    history = state.values.get("messages", []) if state else []
    
    if history:
        current_tokens = count_tokens(history)
        print(f"  [裁剪前] {len(history)} 条消息, {current_tokens} tokens")
        
        # 2. 裁剪
        trimmed = trim_messages(
            history,
            max_tokens=MAX_TOKENS,
            token_counter=count_tokens,
            strategy="last",         # 保留最新消息
            include_system=True,     # 保留系统消息
            allow_partial=False,     # 保持消息完整性
            start_on="human",        # 裁剪后以用户消息开始
        )
        
        new_tokens = count_tokens(trimmed)
        print(f"  [裁剪后] {len(trimmed)} 条消息, {new_tokens} tokens")
    else:
        trimmed = []
    
    # 3. 追加新消息
    new_messages = trimmed + [HumanMessage(content=user_input)]
    
    # 4. 调用 Agent
    return agent.invoke({"messages": new_messages}, config=config)

# ==================== 测试多轮对话 ====================

config = {"configurable": {"thread_id": "trim_demo"}}

conversations = [
    "你好,我叫陈明",
    "帮我查一下北京天气",
    "上海呢?",
    "明天北京天气如何?",
    "我是谁来着?",  # 测试裁剪后是否还记得
]

for i, query in enumerate(conversations, 1):
    print(f"\n--- 第 {i} 轮 ---")
    print(f"用户: {query}")
    result = invoke_with_trim(agent, query, config)
    print(f"AI: {result['messages'][-1].content}")

start_on="human" 为什么重要? 如果裁剪后第一条消息是 AI 的回复(没有对应的用户问题),某些模型会感到困惑,可能输出一段莫名其妙的回答。这个参数确保裁剪后的对话总是以用户消息开始。


四、自定义 State:扩展短期记忆的维度

4.1 为什么需要扩展

默认的 AgentState 只有 messages 字段。但实际业务中,你需要在 Agent 执行过程中持久化更多上下文:

  • 用户身份user_id 用于权限控制和个性化
  • 用户偏好:主题、语言、通知方式
  • 执行状态:重试次数、当前步骤、错误计数
  • 审计信息:原始查询、创建时间

这些字段需要在 Agent 的所有步骤间共享(LLM 调用 → 工具调用 → 结果解析),不能只放在工具内部变量里。

4.2 用 TypedDict 扩展 AgentState

LangChain 1.0 推荐使用 TypedDict 而非 Pydantic。原因:

  • 性能更高(无序列化开销,在高频 API 调用场景优势明显)
  • 与 LangGraph 的状态管理系统原生兼容
  • 从 LangChain 1.0 开始,state_schema 必须是 TypedDict 类型,不再支持 Pydantic 和 dataclass
python 复制代码
from typing import TypedDict
from langchain.agents import AgentState

class CustomAgentState(AgentState):
    """扩展的 Agent 状态"""
    user_id: str            # 用户唯一标识
    preferences: dict       # 用户偏好
    visit_count: int        # 访问次数

4.3 实战:带用户偏好的 Agent

python 复制代码
import os
from typing import TypedDict
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import ToolMessage
from langchain.agents import AgentState, create_agent
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command

load_dotenv(override=True)

# ==================== 自定义 State ====================

class CustomAgentState(AgentState):
    user_id: str
    preferences: dict
    visit_count: int

# ==================== 模型 ====================

model = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# ==================== 工具:读写自定义 State ====================

@tool
def update_preference(theme: str, runtime: "ToolRuntime") -> Command:
    """更新用户的主题偏好设置。
    
    Args:
        theme: 主题名称,如"暗黑模式"、"浅色模式"
    """
    current_prefs = runtime.state.get("preferences", {})
    current_prefs["theme"] = theme
    
    return Command(update={
        "preferences": current_prefs,
        "messages": [
            ToolMessage(
                content=f"已成功将主题设置为:{theme}",
                tool_call_id=runtime.tool_call_id,
            )
        ],
    })

@tool
def get_greeting(runtime: "ToolRuntime") -> str:
    """根据用户偏好生成个性化问候。"""
    user_id = runtime.state.get("user_id", "访客")
    prefs = runtime.state.get("preferences", {})
    theme = prefs.get("theme", "默认")
    visit_count = runtime.state.get("visit_count", 0)
    return f"欢迎回来,{user_id}!当前主题:{theme},这是你的第 {visit_count} 次访问。"

# ==================== 创建 Agent ====================

memory = InMemorySaver()

agent = create_agent(
    model=model,
    tools=[update_preference, get_greeting],
    state_schema=CustomAgentState,  # ← 关键:传入自定义状态类型
    system_prompt="你是一个个性化助手,帮助用户管理偏好设置。",
    checkpointer=memory,
)

# ==================== 测试 ====================

config = {"configurable": {"thread_id": "custom_state_demo"}}

# 第一轮:传入自定义字段
r1 = agent.invoke({
    "messages": [{"role": "user", "content": "把主题设成暗黑模式"}],
    "user_id": "user_789",
    "preferences": {"language": "zh-CN"},
    "visit_count": 1,
}, config=config)
print(f"AI: {r1['messages'][-1].content}")

# 第二轮:读取状态
r2 = agent.invoke(
    {"messages": [{"role": "user", "content": "打个招呼"}]},
    config=config
)
print(f"AI: {r2['messages'][-1].content}")

# 查看完整状态
state = agent.get_state(config)
print(f"\n当前状态:")
print(f"  用户ID:{state.values.get('user_id')}")
print(f"  偏好:{state.values.get('preferences')}")
print(f"  消息数:{len(state.values['messages'])}")

4.4 TypedDict vs Pydantic:什么时候用哪个

场景 TypedDict Pydantic
Agent state_schema ✅ 最佳选择(LangChain 1.0 强制要求) ❌ 不再支持
context_schema ✅ 推荐 ✅ 可用
response_format ❌ 不支持 ✅ 必须用 BaseModel
API 请求体 ⚠️ 需手动校验 ✅ 原生验证
内部函数参数 ✅ 轻量有效 ⚠️ 略重

记住:持久化状态用 TypedDict,对外输出契约用 Pydantic。


五、长期记忆:向量数据库方案

长期记忆的第一种实现路径是向量数据库。适合非结构化的语义记忆------用户偏好、历史对话摘要、知识片段。

5.1 核心原理

复制代码
用户说"我喜欢草莓" → 文本向量化 → 存入向量数据库
用户问"我有什么忌口" → 查询向量化 → 语义相似度搜索 → 找到"草莓""花生过敏"

向量数据库的优势在于模糊匹配------不需要精确的关键词,语义相近就能找到。

5.2 主流向量数据库对比

数据库 特点 适用场景
Chroma 轻量级,支持本地持久化 开发、小规模应用
Pinecone 云原生,全托管 生产环境、大规模
Milvus 开源,高性能,支持大规模检索 企业级部署
Qdrant 高性能向量搜索引擎 需要丰富过滤条件的场景

5.3 实战:基于 Chroma 的长期记忆

python 复制代码
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage
from langchain_core.documents import Document
from langchain_chroma import Chroma
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver

load_dotenv(override=True)

# ==================== 初始化向量数据库 ====================

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

vector_store = Chroma(
    collection_name="agent_long_term_memory",
    embedding_function=embeddings,
    # persist_directory="./chroma_db"  # 取消注释启用本地持久化
)

# ==================== 定义记忆工具 ====================

@tool
def save_memory(content: str) -> str:
    """将重要信息保存到长期记忆中。
    
    当获知用户的喜好、职业、计划或其他长期有效的事实时调用。
    
    Args:
        content: 要保存的记忆内容,用一句话概括关键信息
    """
    doc = Document(
        page_content=content,
        metadata={"source": "user_interaction"}
    )
    vector_store.add_documents([doc])
    print(f"[记忆] 已保存:{content}")
    return "记忆已保存。"

@tool
def search_memory(query: str) -> str:
    """从长期记忆中搜索相关信息。
    
    当被问及关于用户过去的问题,或不确定答案时使用。
    
    Args:
        query: 搜索查询,描述你想查找的信息
    """
    results = vector_store.similarity_search(query, k=3)
    
    if not results:
        return "没有找到相关的记忆。"
    
    memory_content = "\n".join([f"- {doc.page_content}" for doc in results])
    return f"找到以下相关记忆:\n{memory_content}"

# ==================== 创建 Agent ====================

model = ChatOpenAI(model="gpt-4o", temperature=0)

SYSTEM_PROMPT = """你是一个拥有长期记忆的私人助手。

核心规则:
1. 用户告诉你关于他们自己的事实(名字、喜好、职业、忌口等)时,必须调用 save_memory 保存
2. 用户问你可能在记忆中存在的问题时,先调用 search_memory 查找
3. 普通闲聊不需要调用记忆工具

记忆格式要求:每条记忆用一句话概括,包含关键信息。
例如:"用户最喜欢的水果是草莓" 而不是 "用户说他喜欢吃草莓"
"""

checkpointer = InMemorySaver()

agent = create_agent(
    model,
    tools=[save_memory, search_memory],
    system_prompt=SYSTEM_PROMPT,
    checkpointer=checkpointer,
)

# ==================== 场景演示 ====================

# 场景 A:存入记忆(今天的对话)
print("--- 场景 A:用户告诉 Agent 喜好 ---")
config_a = {"configurable": {"thread_id": "session_today"}}

for chunk in agent.stream(
    {"messages": [HumanMessage(content="你好,记住我最喜欢草莓,而且对花生过敏。")]},
    config=config_a,
    stream_mode="values"
):
    pass
print(f"Agent: {chunk['messages'][-1].content}")

# 场景 B:新会话,短期记忆已清空,但长期记忆可跨会话访问
print("\n--- 场景 B:第二天(新 Session,短期记忆已清空)---")
config_b = {"configurable": {"thread_id": "session_tomorrow"}}

for chunk in agent.stream(
    {"messages": [HumanMessage(content="我想吃点零食,但我忘了有什么忌口,帮我查查?")]},
    config=config_b,
    stream_mode="values"
):
    pass
print(f"Agent: {chunk['messages'][-1].content}")
# 预期输出:Agent 调用 search_memory,找到"花生过敏"的记忆并提醒用户

六、长期记忆:BaseStore 结构化方案

向量数据库擅长语义检索,但不擅长精确的结构化存储。长期记忆的第二种路径是 BaseStore------LangGraph 提供的通用键值存储,专为结构化数据设计。

6.1 核心特性

  • 命名空间(Namespace) :层次化元组路径,类似文件系统目录

    python 复制代码
    namespace = ("users", "user_123", "preferences")
    # 对应逻辑路径:users/user_123/preferences
  • 核心操作put() 存储、get() 精确检索、search() 搜索、delete() 删除

  • 支持 TTL:自动过期清理

  • 跨线程访问 :不受 thread_id 限制

6.2 BaseStore vs 向量数据库

维度 BaseStore 向量数据库
存储内容 结构化字典(JSON 序列化) 非结构化文本(自动 Embedding)
检索方式 get() 精确匹配 + search() 搜索 similarity_search() 语义相似
查询灵活性 必须精确 key 或有限搜索 自然语言模糊查询
写入速度 < 1ms(内存版) 50-200ms(含 Embedding 计算)
更新成本 O(1) 直接覆盖 O(n) 需重新计算向量
适用场景 用户档案、偏好设置、配置信息 对话摘要、知识片段、模糊回忆

生产环境最佳实践是两者组合使用------BaseStore 存结构化档案,向量数据库存语义记忆。

6.3 实战:基于 PostgresStore 的跨线程记忆

这是最复杂的场景:用户 Alice 今天跟 Agent 聊天,关掉浏览器,第二天重新打开(新的 thread_id),Agent 依然记得她是谁。

python 复制代码
import os
import time
from typing import Annotated
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
from langchain.agents import AgentState, create_agent
from langgraph.checkpoint.postgres import PostgresSaver
from langgraph.store.postgres import PostgresStore
from langgraph.store.base import BaseStore
from langgraph.prebuilt import InjectedStore, InjectedState
from langgraph.types import Command
from psycopg_pool import ConnectionPool
from pydantic import BaseModel, Field

load_dotenv(override=True)

# ==================== 数据库配置 ====================

DB_URI = os.getenv("DATABASE_URL", "postgresql://user:pass@localhost:5432/agent_db")

# ==================== 模型 ====================

model = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# ==================== 自定义 State ====================

class CrossThreadState(AgentState):
    user_id: str  # 跨线程记忆的关键标识

# ==================== 结构化输出模型 ====================

class UserInfo(BaseModel):
    """从文本中提取的用户信息"""
    user_name: str = Field(description="用户的名字")
    additional_info: str = Field(description="关于用户的其他信息")

# ==================== 记忆工具 ====================

@tool
def remember_info(
    info: str,
    state: Annotated[dict, InjectedState()],
    store: Annotated[BaseStore, InjectedStore()],
) -> str:
    """将用户信息存入跨线程长期记忆。
    
    当用户告诉你他的名字、职业、偏好等信息时调用。
    
    Args:
        info: 要记忆的信息
    """
    # 用 LLM 提取结构化信息
    structured_llm = model.with_structured_output(UserInfo)
    try:
        extracted = structured_llm.invoke(f"从以下文本提取用户名和信息:{info}")
        user_id = extracted.user_name.lower() if extracted.user_name else state.get("user_id", "unknown")
        full_info = f"{extracted.user_name}: {extracted.additional_info}"
    except Exception:
        user_id = state.get("user_id", "unknown")
        full_info = info
    
    # 存入 BaseStore(namespace 设计:按用户 ID 隔离)
    namespace = (user_id, "profile")
    import uuid
    memory_id = str(uuid.uuid4())
    
    store.put(namespace, memory_id, {
        "info": full_info,
        "timestamp": "2025-01-01",  # 实际项目中用 datetime.now().isoformat()
        "source": "user_input",
    })
    
    return Command(update={
        "messages": [ToolMessage(content=f"已记住:{full_info}", tool_call_id=info)],
    })

@tool
def recall_info(
    query: str,
    state: Annotated[dict, InjectedState()],
    store: Annotated[BaseStore, InjectedStore()],
) -> str:
    """从跨线程记忆中检索用户信息。
    
    当用户问"你还记得我吗"或类似问题时调用。
    
    Args:
        query: 查询关键词
    """
    user_id = state.get("user_id")
    if not user_id:
        return "无法确定用户身份。"
    
    # 搜索该用户的所有记忆
    namespace_prefix = (user_id,)
    memories = store.search(namespace_prefix, limit=20)
    
    if not memories:
        return "没有找到相关记忆。"
    
    results = []
    for item in memories:
        info = item.value.get("info", "未知")
        results.append(f"- {info}")
    
    return f"找到 {len(results)} 条记忆:\n" + "\n".join(results)

# ==================== 创建 Agent ====================

with ConnectionPool(
    conninfo=DB_URI,
    max_size=20,
    kwargs={"autocommit": True}
) as pool:
    
    checkpointer = PostgresSaver(pool)
    store = PostgresStore(pool)
    
    checkpointer.setup()
    store.setup()
    
    agent = create_agent(
        model=model,
        tools=[remember_info, recall_info],
        state_schema=CrossThreadState,
        system_prompt="""你是一个具备跨线程记忆的助手。

规则:
1. 用户告诉你关于他们的信息时,调用 remember_info 保存
2. 用户问你还记得什么时,调用 recall_info 检索
3. 记忆是跨会话持久的,即使换了新对话也能记住""",
        store=store,           # ← 注入 BaseStore
        checkpointer=checkpointer,
    )
    
    # ==================== 测试跨线程记忆 ====================
    
    # Alice 第一次对话
    print("--- Alice 首次对话 ---")
    config_a1 = {"configurable": {"thread_id": "alice_session_001"}}
    for chunk in agent.stream(
        {"messages": [HumanMessage(content="我是 Alice,一名 Python 工程师,喜欢深度学习。")],
         "user_id": "alice"},
        config=config_a1,
        stream_mode="values"
    ):
        pass
    print(f"Agent: {chunk['messages'][-1].content}")
    
    # Alice 第二天重新打开(新 thread_id)
    print("\n--- Alice 第二天(新 Session)---")
    time.sleep(1)
    config_a2 = {"configurable": {"thread_id": "alice_session_002"}}
    for chunk in agent.stream(
        {"messages": [HumanMessage(content="你还记得我是谁吗?")],
         "user_id": "alice"},
        config=config_a2,
        stream_mode="values"
    ):
        pass
    print(f"Agent: {chunk['messages'][-1].content}")
    # 预期:Agent 调用 recall_info,从 PostgresStore 中找到 Alice 的信息
    
    # Bob 的对话(验证记忆隔离)
    print("\n--- Bob 的对话 ---")
    config_b = {"configurable": {"thread_id": "bob_session_001"}}
    for chunk in agent.stream(
        {"messages": [HumanMessage(content("我是 Bob,产品经理。")],
         "user_id": "bob"},
        config=config_b,
        stream_mode="values"
    ):
        pass
    print(f"Agent: {chunk['messages'][-1].content}")

关键设计: InjectedStore()InjectedState() 这两个注解让 Pydantic 在生成工具 Schema 时跳过这些参数------模型看不到 storestate,只知道 infoquery。但运行时 LangGraph 会自动注入真实对象。这是长期记忆工具的标准写法。


七、生产环境最佳实践

7.1 企业级组合架构

复制代码
┌────────────────────────────────────────────────────┐
│                    Agent 运行时                      │
├────────────────────────────────────────────────────┤
│                                                    │
│  ┌─────────────────┐     ┌──────────────────────┐  │
│  │   短期记忆层      │     │    长期记忆层           │  │
│  │                   │     │                      │  │
│  │  PostgresSaver    │     │  BaseStore(KV)       │  │
│  │  ├─ thread_id 隔离│     │  ├─ user_id 命名空间   │  │
│  │  └─ 完整对话历史   │     │  └─ 用户档案、偏好     │  │
│  │                   │     │                      │  │
│  │                   │     │  向量数据库(Semantic) │  │
│  │                   │     │  ├─ 对话摘要          │  │
│  │                   │     │  └─ 语义模糊检索      │  │
│  └─────────────────┘     └──────────────────────┘  │
│                                                    │
└────────────────────────────────────────────────────┘

身份标识 :除了 thread_id,必须传入 user_id。前者管短期记忆,后者管长期记忆。

7.2 记忆生命周期管理

策略 说明 实现方式
TTL 过期 自动清理过期记忆 BaseStore put() 时设置 TTL
手动清理 提供管理接口删除无用记忆 store.delete(namespace, key)
压缩归档 对历史记忆进行摘要压缩 定期用 LLM 把多条记忆压成一条
容量限制 防止记忆无限增长 每个 namespace 限制最大条目数

7.3 性能优化

  • 索引优化:为常用 namespace 建立索引
  • 缓存策略:热数据(最近活跃用户)放内存,冷数据放数据库
  • 批量操作 :批量 put() 减少 I/O 开销
  • 异步加载:长期记忆检索使用异步调用,不阻塞主循环

7.4 安全注意事项

  • 不同用户的记忆必须通过 namespace 严格隔离
  • 长期记忆中的敏感信息(PII)需要加密或脱敏
  • 记忆写入要有审计日志
  • 设置合理的记忆容量上限,防止滥用

核心要点总结

  1. 分界线是生命周期,不是存储介质。 短期记忆跟着 thread_id 走,长期记忆跟着 user_id 走。PostgresSaver 存在数据库里也是短期记忆。
  2. Checkpointer 是短期记忆的灵魂。 不加它,Agent 无状态;加了它,同一 thread_id 自动携带上下文。
  3. 上下文裁剪不能省。 trim_messages 配合精确的 tiktoken 计数,在 State 中保留完整历史、传给 LLM 时只传最近的。start_on="human" 防止裁剪后对话结构异常。
  4. 自定义 State 用 TypedDict。 LangChain 1.0 强制要求,不再支持 Pydantic。通过 ToolRuntimeInjectedState / InjectedStore 让工具读写状态。
  5. 长期记忆两条路。 向量数据库做语义检索(模糊、灵活),BaseStore 做结构化 KV 存储(精确、高效)。生产环境建议组合使用。
  6. 安全隔离是底线。 namespace 隔离、PII 脱敏、审计日志、容量限制,缺一不可。

参考资料

主题 官方文档
短期记忆、checkpointer、上下文压缩 Short-term memory
长期记忆、store、namespace/key 结构 Long-term memory
Agent 定义、create_agent 参数 Agents
工具定义、ToolRuntime Runtime
预置中间件(摘要、上下文裁剪) Prebuilt middleware
相关推荐
MSTcheng.1 小时前
KES 进了 K8s 之后运维归谁管?
数据库
知几蜗牛1 小时前
部署大模型别先选GPU,先回答你愿意承担多少运维
人工智能
知几蜗牛1 小时前
AI写了80万行Rust,最值得学的却是它花十倍精力读代码
人工智能
天云数据1 小时前
OPC保姆级指南:被优化的第四个月,我在图书馆里想好了开家公司
人工智能
知几蜗牛1 小时前
语音AI为什么总抢话?用VAD和打断机制做对实时对话
人工智能
fellow991 小时前
V100 的上下文极限:vLLM 卡 131K,llama.cpp 冲 230K
人工智能·自然语言处理
AgentMaster1 小时前
数据资产化落地难题:5款数据中台系统架构对比与实施记录
大数据·人工智能·算法
AIGCmagic社区1 小时前
LightNav-0:激发VLM空间智能,迈向通用具身导航
人工智能·具身智能·ai多模态
知几蜗牛1 小时前
AI每次提交都查漏洞,真正的升级是把证明链放进评审
人工智能