Agent 长期记忆

重点介绍Agent长期记忆,短期记忆使Agent能够在单次会话中维持对话的连贯性,而长期记忆则赋予了Agent跨会话学习和积累知识的能力。这样Agent能够构建持久的用户画像,形成经验库,并持续优化自身行为,从而提供真正个性化、智能化的服务。

长期记忆介绍

长期记忆是一种用于存储用户特定信息或应用级数据的系统,其核心特点是跨越会话和线程共享。与短期记忆局限于单一线程(thread_id)不同,长期记忆中的数据可以被任何时间、在任何线程中召回。其存储范围(作用域)被定义在自定义的命名空间中,而非单个线程ID内。

image-20260715143828249

在LangChain框架中,长期记忆通过存储(Store)组件来实现,Store允许你将记忆保存为JSON文档,并通过命名空间(namespace)和键(Key)进行组织和管理,便于后续的检索、更新与删除。

  • 命名空间(namespace):类似于文件夹,用于对记忆进行逻辑分组(例如,按应用场景划分)。
  • 键(key):命名空间内每个文档的唯一标识符。

Store存储支持基本的put(写入)、get(读取)、delete(删除)、search(搜索)操作,如下案例中演示基于内存store存储的基本操作。

from langgraph.store.memory import InMemoryStore

# 初始化一个内存存储store
store = InMemoryStore()

# 定义命名空间:通常包含用户ID和上下文
namespace = ("user1", "preferences")

# 写入一个记忆
store.put(
    namespace,
    "fruit", # 键
    {"likes": ["苹果", "香蕉"], "dislikes": ["橙子"]} # 值 (JSON文档)
)

print("=== 读取 fruit 记忆 ===")
# 读取记忆
memory = store.get(namespace, "fruit")
print(memory)

# 写入另一个记忆
store.put(
    namespace,
    "chat", # 键
    {"language": "中文", "emotion": "高兴"} # 值 (JSON文档)
)
print("=== 读取 chat 记忆 ===")
# 读取记忆
memory = store.get(namespace, "chat")
print(memory)

print("=== 搜索所有记忆 ===")
# 搜索所有记忆
memories = store.search(namespace)
print(memories)

# 更新记忆
store.put(
    namespace,
    "fruit", # 键
    {"likes": ["橘子", "葡萄"], "dislikes": ["草莓"]} # 值 (JSON文档)
)

print("=== 读取更新后的 fruit 记忆 ===")
# 读取更新后的记忆
memory = store.get(namespace, "fruit")
print(memory)

以上代码运行结果如下:

image-20260715143957854

以上代码注意点如下:

  1. 命名空间(namespace)格式为元组格式,建议包含用户ID和上下文特征,这样的层级化命名空间可以更好隔离数据、避免命名空间冲突。
  2. 键(key):在命名空间中唯一的一个key,store中put对应key的值后,后续再put相同的key的值,会覆盖当前key的值。
  3. 键(key)对应的value的值是一个字典结构,该字典中 k,v 内容由用户定义。
  4. store.put 方法可以传入命名空间(元组类型)、key(字符串类型)、value(字典类型)三个参数。
  5. store.get方法可以指定namespace和key参数获取对应的value值;store.search方法可以指定namespace参数获取该命名空间中所有的value。

Agent中使用长期记忆方式

长期记忆的核心作用是实现跨会话、跨节点的数据共享。在 LangChain 中,单个 Agent 通过将 Store注入到不同会话(thread_id)中,实现跨会话状态持久化。Agent中设置长期记忆的方式和短期记忆类似,长期记忆可以存在内存或者数据库中,下面重点介绍在Agent中如何使用长期记忆。

此外,长期记忆更多使用在 LangGraph 构建的工作流中,流中不同节点通过共享的 Store访问同一份长期记忆。关于LangGraph中长期记忆在后续进行介绍。

使用内存存储长期记忆

在测试环境中,通常使用内存型的 InMemoryStore 对象,这种方式简单易用但程序重启后数据会丢失。

如下代码中使用内存存储长期记忆,该案例中创建好 InMemoryStore 后,首先向该对象中存储一些数据作为长期记忆,后续在工具中获取上下文Context中用户信息,根据不同用户查询不同的长期记忆信息,实现在相同/不同的thread_id中读取相同长期记忆内容。

from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.memory import InMemoryStore
from langchain.tools import tool, ToolRuntime

from pydantic import BaseModel

from init_llm import deepseek_llm


class UserContext(BaseModel):
    user_id: str

# 1. 初始化存储并预先存入一些长期记忆
store = InMemoryStore()
checkpointer = InMemorySaver()

# 预先在长期记忆中存储一些用户信息
store.put(
    ("users",),  # 命名空间:用户数据
    "user_123",  # 键:用户ID
    {"name": "张三", "age": 28, "city": "北京", "hobby": "编程、阅读"}  # 值:用户信息
)
store.put(
    ("users",),
    "user_456",
    {"name": "李四", "age": 32, "city": "上海", "hobby": "旅游、摄影"}
)


# 2. 定义读取长期记忆的工具
@tool
def get_user_info(runtime: ToolRuntime[UserContext]) -> str:
    """
    从长期记忆中获取当前用户的信息。

    Returns:
        str: 用户的详细信息
    """
    # 从runtime中获取store和context
    store = runtime.store
    user_id = runtime.context.user_id

    # 从长期记忆中读取用户信息
    user_data = store.get(("users",), user_id)

    if user_data:
        # 将字典格式化为字符串
        info = user_data.value
        return f"用户信息:姓名-{info['name']}, 年龄-{info['age']}, 城市-{info['city']}, 爱好-{info['hobby']}"
    else:
        return "未找到该用户的信息。"


# 3. 创建带有长期记忆读取工具的Agent
agent = create_agent(
    model=deepseek_llm,
    tools=[get_user_info],
    checkpointer=checkpointer,
    store=store,
    context_schema=UserContext
)

# 4. 演示:在不同线程中读取相同的长期记忆
print("=== 长期记忆的跨线程读取 ===")

# 线程1:用户123询问自己的信息
print("线程1 - 用户123询问信息:")
result1 = agent.invoke(
    {"messages": [{"role": "user", "content": "我的个人信息是什么?"}]},
    config={"configurable": {"thread_id": "thread_1"}},
    context=UserContext(user_id="user_123")  # 用户ID决定访问哪个长期记忆
)
print(f"Agent回复: {result1['messages'][-1].content}\n")

# 线程2:另一个线程,相同的用户
print("线程2 - 相同的用户,不同的线程:")
result2 = agent.invoke(
    {"messages": [{"role": "user", "content": "再告诉我一次我的信息"}]},
    config={"configurable": {"thread_id": "thread_2"}},  # 线程ID不同
    context=UserContext(user_id="user_123")  # 但用户ID相同
)
print(f"Agent回复: {result2['messages'][-1].content}\n")

# 线程3:另一个用户
print("线程3 - 用户456询问信息:")
result3 = agent.invoke(
    {"messages": [{"role": "user", "content": "我的信息是什么?"}]},
    config={"configurable": {"thread_id": "thread_3"}},
    context={"user_id": "user_456"}  # 用户ID不同,访问不同的长期记忆
)
print(f"Agent回复: {result3['messages'][-1].content}")

以上代码运行结果如下:

image-20260715144240144

以上代码注意:Agent创建中通过Store参数指定长期记忆对象,使用长期记忆时,可以同时使用短期记忆(checkpointer指定),也可以不使用短期记忆,但一般两者会同时使用。

使用数据库存储长期记忆

在生产环境中,推荐使用数据库支持的Store,以确保数据的持久化和多实例部署的支持。

如下案例中使用mysql数据库来存储长期记忆,完成此案例需要提前安装好mysql数据库(默认已经安装mysql8),并且进行数据库创建和安装必要python依赖,具体如下:

1) 在mysql中创建langchain_db数据库

#进入mysql navicate客户端,创建mysql数据库langchain_db
create database langchain_db;

2) 在当前python环境中安装如下依赖

#安装必要依赖
conda activate langchain_v1.2
python -m pip install langgraph-checkpoint-mysql==3.0.0 pymysql==1.1.2 cryptography==46.0.3
python -m pip install aiomysql==0.3.2 asyncmy==0.2.11

使用mysql数据库存储长期记忆代码如下:

from langchain.agents import create_agent
from langgraph.checkpoint.mysql.pymysql import PyMySQLSaver
from langchain.tools import tool, ToolRuntime
from langgraph.store.mysql import PyMySQLStore
from pydantic import BaseModel
from utils.llm_util import deepseek_llm
from urllib.parse import quote_plus

class UserContext(BaseModel):
    user_id: str

# 1. 定义读取长期记忆的工具
@tool
def get_user_info(runtime: ToolRuntime[UserContext]) -> str:
    """
    从长期记忆中获取当前用户的信息。

    Returns:
        str: 用户的详细信息
    """
    # 从runtime中获取store和context
    store = runtime.store
    user_id = runtime.context.user_id

    # 从长期记忆中读取用户信息
    user_data = store.get(("users",), user_id)

    if user_data:
        # 将字典格式化为字符串
        info = user_data.value
        return f"用户信息:姓名-{info['name']}, 年龄-{info['age']}, 城市-{info['city']}, 爱好-{info['hobby']}"
    else:
        return "未找到该用户的信息。"
password = quote_plus("Root#*123")
# 配置 MySQL 连接
DB_URI = (
    f"mysql+pymysql://root:{password}"
    "@localhost:3306/langchain_db?charset=utf8mb4"
)

with (
    PyMySQLSaver.from_conn_string(DB_URI) as checkpointer,
    PyMySQLStore.from_conn_string(DB_URI) as store
):
    # 自动创建checkpointer数据库表(首次运行)
    checkpointer.setup()
    # 2. 自动创建store数据库表(首次运行)
    store.setup()

    # 3. 预先在长期记忆中存储一些用户信息
    store.put(
        ("users",),  # 命名空间:用户数据
        "user_123",  # 键:用户ID
        {"name": "张三", "age": 28, "city": "北京", "hobby": "编程、阅读"},  # 值:用户信息
    )
    store.put(
        ("users",),
        "user_456",
        {"name": "李四", "age": 32, "city": "上海", "hobby": "旅游、摄影"},
    )

    # 4. 创建带有长期记忆读取工具的Agent
    agent = create_agent(
        model=deepseek_llm,
        tools=[get_user_info],
        checkpointer=checkpointer,
        store=store,
        context_schema=UserContext
    )

    # 5. 演示:在不同线程中读取相同的长期记忆
    print("=== 长期记忆的跨线程读取 ===")

    # 线程1:用户123询问自己的信息
    print("线程1 - 用户123询问信息:")
    result1 = agent.invoke(
        {"messages": [{"role": "user", "content": "我的个人信息是什么?"}]},
        config={"configurable": {"thread_id": "thread_1"}},
        context=UserContext(user_id="user_123")  # 用户ID决定访问哪个长期记忆
    )
    print(f"Agent回复: {result1['messages'][-1].content}\n")

    # 线程2:另一个线程,相同的用户
    print("线程2 - 相同的用户,不同的线程:")
    result2 = agent.invoke(
        {"messages": [{"role": "user", "content": "再告诉我一次我的信息"}]},
        config={"configurable": {"thread_id": "thread_2"}},  # 线程ID不同
        context=UserContext(user_id="user_123")  # 但用户ID相同
    )
    print(f"Agent回复: {result2['messages'][-1].content}\n")

    # 线程3:另一个用户
    print("线程3 - 用户456询问信息:")
    result3 = agent.invoke(
        {"messages": [{"role": "user", "content": "我的信息是什么?"}]},
        config={"configurable": {"thread_id": "thread_3"}},
        context=UserContext(user_id="user_456")  # 用户ID不同,访问不同的长期记忆
    )
    print(f"Agent回复: {result3['messages'][-1].content}")

以上代码运行结果如下:

image-20260715150047739

运行代码后进入到Mysql数据库中可以看到对应的数据库表和数据:

image-20260715150506819

以上代码需要注意如下几点:

  1. “with PyMySQLStore.from_conn_string(DB_URI) as store:”通过连接字符串DB_URI创建与MySQL数据库的持久化连接,用于管理长期记忆的存储。

  2. 使用mysql存储长期记忆需要提前在数据库中创建对应的数据库,然后代码首次运行执行“store.setup()”(首次运行需要,首次运行后可以不再执行该代码)会自动在该数据库中创建对应数据库表。

  3. 也可以使用其他数据库进行短期记忆的持久化存储,例如使用postgresql存储,需要安装“pip install langgraph-checkpoint-postgres==3.0.4”,具体代码参考:https://docs.langchain.com/oss/python/langchain/short-term-memory#in-production

  4. 持久化存储支持的数据库可以通过 https://pypi.org/search/?o=&q=langgraph-checkpoint&page=2 查看,搜索 langgraph-checkpoint-* 查看对应需要安装的依赖和使用方式。

    image-20260715150748757

工具中实现长期记忆读写

长期记忆的核心使用场景是在工具中进行读写操作。LangChain通过ToolRuntime将store和context注入到工具函数中,使工具能够访问和修改长期记忆。如下案例中通过工具使用长期记忆记录用户偏好。

from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.memory import InMemoryStore
from langchain.tools import tool, ToolRuntime
from typing import TypedDict, Literal

from pydantic import BaseModel, Field

from init_llm import deepseek_llm
import uuid


class UserContext(BaseModel):
    user_id: str

# 定义工具输入的类型
class UserPreference(BaseModel):
    category: Literal["color", "food", "music"] = Field(description=" 用户偏好类别,必须是 'color', 'food', 'music' 中的一个")
    preference: str = Field(description="具体偏好内容,如'红色'、'中国美食'等")


# 1. 初始化存储
store = InMemoryStore()
checkpointer = InMemorySaver()


# 2. 定义写入长期记忆的工具
@tool(args_schema=UserPreference)
def save_user_preference(category: str, preference: str, runtime: ToolRuntime) -> str:
    """
    将用户偏好保存到长期记忆中。

    Args:
        category: 用户偏好类别,必须是 "color", "food", "music" 中的一个
        preference: 具体偏好内容,如'红色'、'中国美食'等
        runtime: ToolRuntime  # 包含长期记忆存储和上下文
    Returns:
        str: 操作结果描述
    """
    user_id = runtime.context.user_id

    # 创建命名空间:(user_id, "preferences")
    namespace = (user_id, "preferences")

    # 生成唯一记忆ID
    memory_id = str(uuid.uuid4())

    # 准备要保存的数据
    memory_value = {
        "category": category,
        "preference": preference,
    }

    # 保存到长期记忆
    runtime.store.put(namespace, memory_id, memory_value)

    return f"已成功保存你的{category}偏好:{preference}"


# 3. 定义读取用户偏好的工具
@tool
def get_user_preferences(runtime: ToolRuntime) -> str:
    """
    从长期记忆中获取用户特定类别的所有偏好。

    Returns:
        str: 用户的偏好列表
    """
    user_id = runtime.context.user_id
    namespace = (user_id, "preferences")

    # 搜索该命名空间下的所有记忆
    memories = runtime.store.search(namespace)

    if not memories:
        return f"您还没有保存过偏好"

    print("memories:", memories)

    # 格式化所有偏好为字符串列表
    preferences_list = []
    for mem in memories:
        pref = mem.value
        preferences_list.append(f"- 种类:{pref['category']},偏好:{pref['preference']}")

    return f"你的偏好有:\n" + "\n".join(preferences_list)


# 4. 创建带有长期记忆读写工具的Agent
memory_agent = create_agent(
    model=deepseek_llm,
    tools=[save_user_preference, get_user_preferences],
    checkpointer=checkpointer,
    store=store,
    context_schema=UserContext
)

# 5. 演示:完整的长时期记忆读写流程
print("=== 完整演示:长期记忆的写入与跨线程读取 ===")

# 第一轮:用户保存颜色偏好(线程1)
print("第一轮(线程1):用户保存颜色偏好")
result1 = memory_agent.invoke(
    {"messages": [{"role": "user", "content": "请记住我喜欢的颜色是蓝色"}]},
    config={"configurable": {"thread_id": "thread1"}},
    context=UserContext(user_id="current_user")
)
print(f"Agent回复: {result1['messages'][-1].content}")

# 第二轮:用户保存食物偏好(同一线程)
print("第二轮(同一线程):用户保存食物偏好")
result2 = memory_agent.invoke(
    {"messages": [{"role": "user", "content": "我还喜欢的食物是意大利面"}]},
    config={"configurable": {"thread_id": "thread1"}},  # 同一线程
    context=UserContext(user_id="current_user")
)
print(f"Agent回复: {result2['messages'][-1].content}")

# 第三轮:在新线程中查询所有偏好
print("第三轮(新线程):查询我的所有偏好")
result3 = memory_agent.invoke(
    {"messages": [{"role": "user", "content": "告诉我我都喜欢什么颜色和食物"}]},
    config={"configurable": {"thread_id": "thread2"}},  # 新线程
    context=UserContext(user_id="current_user")  # 相同用户
)
print(f"Agent回复: {result3['messages'][-1].content}")


# 直接验证:从store中读取数据
print("=== 直接验证:从长期记忆存储中读取数据 ===")
# 读取颜色偏好
color_memories = store.search(("current_user", "preferences"))
print(f"长期记忆中存储的颜色偏好: {[m.value for m in color_memories if m.value['category'] == 'color']}")

# 读取食物偏好
food_memories = store.search(("current_user", "preferences"))
print(f"长期记忆中存储的食物偏好: {[m.value for m in food_memories if m.value['category'] == 'food']}")

代码运行结果如下:

image-20260715155602675

短期记忆和长期记忆区别总结

如下是短期记忆和长期记忆区别:

对比维度 短期记忆(Short-term Memory) 长期记忆(Long-term Memory)
作用域 线程/会话范围记忆 (Thread-scoped)。与单个会话线程 (thread_id) 绑定。 跨线程/会话记忆。存储在自定义命名空间 (namespace) 中,可被多个线程共享。
核心目的 保证单次对话的连贯性和上下文感知。 实现跨对话的个性化、知识积累和持续学习。
主要存储内容 对话的原始历史 (messages列表) 以及当前会话的状态数据。 从交互中提炼的结构化知识(如用户事实、行为经验、优化规则)。
管理组件 检查点 (Checkpointer),如 InMemorySaver、PyMySQLSaver。 存储 (Store),如 InMemoryStore、PostgresStore。
生命周期 随线程的创建而开始,随线程的销毁(或超时清理)而结束。 独立于任何特定线程,除非被显式删除,否则永久或长期存在。
访问方式 自动管理。Agent的状态在每个步骤后自动持久化到检查点,并在下次恢复。 手动控制。必须在工具(Tool) 或自定义逻辑中,通过代码显式地调用 store.put()或 store.get()。
典型应用场景 维持聊天上下文,让Agent记得用户在当前对话中刚说过的话。 记住用户的身份、偏好、历史行为。

记忆综合案例-电商客服助手

如下案例是一个智能电商客服助手案例,该助手具备如下能力:

  • 记住当前会话状态(短期记忆):用户正在查询的订单号。
  • 了解用户长期偏好(长期记忆):用户偏好的商品类型和商品名称。
  • 处理多轮复杂对话:通过消息摘要管理长对话上下文。
  • 工具调用:查询用户信息、查询订单信息、更新用户偏好、基于用户偏好给用户推荐商品。
  • 工具调用错误捕获:当工具调用错误时,通过中间件进行错误捕获,返回友好提示。

该案例中长期和短期记忆使用MySQL进行存储,需要在MySQL中创建对应的库:

#进入mysql navicate客户端,创建mysql数据库langchain_db
drop database langchain_db;
create database langchain_db;

在项目中安装必要依赖:

#安装必要依赖
conda activate langchain_v1.2
python -m pip install langgraph-checkpoint-mysql==3.0.0 pymysql==1.1.2 cryptography==46.0.3
python - m pip install aiomysql==0.3.2 asyncmy==0.2.11

案例代码如下:

"""
智能电商客服助手
功能:结合短期记忆、长期记忆、消息摘要,提供流式、个性化的客服服务。
前置准备:
1. 安装依赖: pip install langchain langgraph langgraph-checkpoint-mysql pymysql
2. 创建MySQL数据库: CREATE DATABASE langchain_memory_db;
"""
import uuid
import warnings
from typing import List, Optional
from pydantic import BaseModel, Field
from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import SummarizationMiddleware, wrap_tool_call
from langchain_core.tools import tool
from langchain_core.messages import ToolMessage
from langgraph.checkpoint.mysql.pymysql import PyMySQLSaver
from langgraph.store.mysql.pymysql import PyMySQLStore
from langgraph.prebuilt import ToolRuntime
from langgraph.types import Command
from init_llm import deepseek_llm

# 禁用Pydantic序列化警告
warnings.filterwarnings("ignore", category=UserWarning, module="pydantic.main")

# ========== 1. 定义Context 上下文 Schema==========
class UserContext(BaseModel):
    """定义调用Agent时传入的静态上下文信息"""
    user_id: str = Field(description="用户的唯一标识符")
    channel: str = Field(description="用户咨询渠道,如: APP, Web, 小程序")


# ========== 2. 定义自定义短期记忆状态 (继承AgentState) ==========
class CustomerSessionState(AgentState):
    """自定义短期记忆状态,用于管理单次会话中的动态信息"""
    current_order_id: str  # 用户当前正在查询的订单号


# ========== 3. 模拟订单数据库 数据 ==========
MOCK_DATABASE = {
    "orders": {
        "order001": {"order_id": "order001", "status": "已发货", "product": "智能手机",
                     "preference_context": "华为手机P70"},
        "order002": {"order_id": "order002", "status": "待支付", "product": "智能手表",
                     "preference_context": "Apple Watch Series 8"},
    }

}


# ========== 4. 定义工具 ==========
@tool
def get_user_info(runtime: ToolRuntime) -> str:
    """
    获取用户当前用户信息
    Args:
        runtime (ToolRuntime): 包含上下文信息的运行时环境
    Returns:
        str: 用户当前用户信息
    """
    print("get_user_info 中 runtime:", runtime)
    # 从上下文中获取当前用户ID
    current_user_id = runtime.context.user_id

    # 从上下文中获取用户咨询渠道
    user_channel = runtime.context.channel

    # 从状态中获取用户当前正在查询的订单号
    state = runtime.state
    if "current_order_id" in state:
        current_order_id = state["current_order_id"]
    else:
        current_order_id = "无"

    # 获取当前用户信息
    return f"用户ID: {current_user_id}, 咨询渠道: {user_channel}, 当前查询订单号: {current_order_id}"


@tool
def query_order_status(order_id: str, runtime: ToolRuntime) -> Command:
    """
    查询用户订单状态
    Args:
        order_id (str): 用户订单号
        runtime (ToolRuntime): 包含上下文信息的运行时环境
    Returns:
        Command: 包含更新操作的命令对象:状态中更新当前订单ID,并返回订单信息(状态、商品、用户偏好)
    """

    # 查询订单状态
    order_info = MOCK_DATABASE["orders"].get(order_id)

    if not order_info:
        return Command(
            update={
                "messages": [
                    ToolMessage(
                        content=f"错误:订单 [{order_id}] 不存在",
                        tool_call_id=runtime.tool_call_id
                    )
                ]
            }
        )

    updates = {
        "current_order_id": order_id,
        "messages": [
            ToolMessage(
                content=f"订单 [{order_id}] 状态: {order_info['status']}, 商品: {order_info['product']}。"
                        f"需要进行用户偏好更新,用户偏好: {order_info['preference_context']}",
                tool_call_id=runtime.tool_call_id
            )
        ]
    }

    return Command(update=updates)


@tool
def update_user_preference(category: str, liked_item: str, runtime: ToolRuntime) -> str:
    """
    更新用户长期偏好
    Args:
        category (str): 商品类别,如: 手机、配件
        liked_item (str): 用户喜欢的具体商品
        runtime (ToolRuntime): 包含上下文信息的运行时环境
    Returns:
        str: 确认更新结果
    """
    user_id = runtime.context.user_id
    namespace = (f"user_{user_id}", "preferences")

    key = str(uuid.uuid4())

    value_to_store = {
        "category": category,
        "liked_item": liked_item,
    }

    # 写入到长期记忆
    runtime.store.put(namespace, key, value_to_store)
    return f"已成功将您的偏好记录到长期记忆: 喜欢 {category} 类的 {liked_item}。"


@tool
def get_recommendation(runtime: ToolRuntime) -> str:
    """
    获取用户推荐商品
    Args:
        runtime (ToolRuntime): 包含上下文信息的运行时环境
    Returns:
        str: 包含推荐商品信息的字符串
    """
    user_id = runtime.context.user_id
    current_order = runtime.state.get("current_order_id", "未知订单")
    namespace = (f"user_{user_id}", "preferences")
    prefs = runtime.store.search(namespace)

    pref_list = []
    if prefs:
        for p in prefs[-3:]:  # 仅取最近3条偏好记录,[-3:] 表示取最后3条记录
            pref_list.append(f"{p.value.get('category')}({p.value.get('liked_item')})")

    return f"基于用户当前的订单 [{current_order}] 和长期偏好 {pref_list if pref_list else '无'},为用户推荐相关配件或类似风格商品。"


@wrap_tool_call
def handle_tool_errors(request, handler):
    """使用自定义消息处理工具执行错误"""
    try:
        return handler(request)
    except Exception as e:
        # 向模型返回自定义错误消息
        return ToolMessage(
            content=f"调用工具错误:请稍后重试,错误信息:({str(e)})",
            tool_call_id=request.tool_call["id"]
        )


# ========== 5. 创建Agent,控制台交互循环 ==========
DB_URI = "mysql+pymysql://root:123456@localhost:3306/langchain_db?charset=utf8mb4"

# 初始化MySQL存储 (短期记忆Checkpointer 和 长期记忆Store)
with (
    PyMySQLSaver.from_conn_string(DB_URI) as checkpointer,
    PyMySQLStore.from_conn_string(DB_URI) as store
):
    # 首次运行时自动建表
    checkpointer.setup()
    store.setup()

    # 创建Agent
    agent = create_agent(
        model=deepseek_llm,
        tools=[get_user_info, query_order_status, update_user_preference, get_recommendation],
        system_prompt="""
                        你是一个智能电商客服助手,具备回答用户咨询、获取用户信息、查询订单状态、更新用户偏好和推荐商品功能。"
                        获取用户信息请调用 get_user_info 工具。
                        查询订单状态请调用 query_order_status 工具,查询到订单状态后,还需要调用 update_user_preference 工具更新用户偏好。
                        更新用户偏好请调用 update_user_preference 工具。
                        获取推荐商品请调用 get_recommendation 工具。
                      """,
        checkpointer=checkpointer,
        store=store,
        state_schema=CustomerSessionState,
        context_schema=UserContext,
        middleware=[
            SummarizationMiddleware(
                model=deepseek_llm,
                summary_prompt="请总结以下对话内容:{messages}",
                trigger=("messages", 10),  # 每10条消息触发一次摘要
                keep=("messages", 5),  # 保留最后5条消息
            ),
            handle_tool_errors
        ],
    )

    # 控制台交互循环 (流式调用)
    print("=" * 50)
    print("智能电商客服助手")
    print("功能: 查询订单、更新偏好、获取推荐。")
    print("输入 'quit' 或 '退出' 结束对话。")
    print("=" * 50)

    # 初始化用户上下文
    user_context = UserContext(user_id="customer_001", channel="Web")
    # 会话线程ID
    config = {"configurable": {"thread_id": "session_01"}}

    # 对话循环
    while True:
        try:
            user_input = input("[你]: ").strip()

            if user_input.lower() in ['quit', 'exit', '退出', 'q']:
                print("客服助手: 感谢你的咨询,再见!")
                break

            # 过滤空输入
            if not user_input:
                continue

            # 准备输入消息
            input_data = {"messages": {"role": "user", "content": user_input}}

            print("[客服助手]: ")
            # 流式调用Agent
            for chunk in agent.stream(input_data, config=config, context=user_context):
                # print("chunk:", chunk)
                for step, data in chunk.items():  # 遍历dict的key-value对
                    # print("step:", step)
                    # print("data:", data)

                    # 只有当 step为model或者tools时,才打印消息
                    if step in ["model", "tools"]:
                        message = data["messages"][-1]
                        message.pretty_print()

        except Exception as e:
            print(f"调用过程中出现错误: {e}")

代码运行后,对于thread_id为session_01 时,进行如下对话:

给我查询订单 order001 信息
给我推荐一些商品
我喜欢 苹果电脑 尤其是 mac pro
查询我的信息
我还喜欢 索尼的xm5耳机
给我推荐一些商品

停止程序后,切换thread_id为session_02后,进行如下对话:

查询我的信息
给我推荐一些商品

可以看到长期记忆信息在不同的会话之间是共享的,而短期记忆只针对每个thread_id生效。

此外,以上代码运行中,会出现如下警告:

UserWarning: Pydantic serializer warnings:
  PydanticSerializationUnexpectedValue(Expected `none` - serialized value may not be as expected [field_name='context', input_value=UserContext(user_id='customer_001', channel='Web'), input_type=UserContext])
  return self.__pydantic_serializer__.to_python(

该警告是将Pydantic模型对象直接传递给了LangChain Agent的context参数,而LangChain内部在处理这个对象时,期望的是基本的Python数据类型(如字典),而不是Pydantic模型对象,导致了序列化警告。去除该警告,可以在项目开始导入:“warnings.filterwarnings(“ignore”, category=UserWarning, module=”pydantic.main”)”

--- 本文结束 The End ---