第09章:上下文与记忆 (6)

3.3 在Agent运行图中访问长期记忆

我们可以在工具或中间件中访问长期记忆。

1 在工具中访问长期记忆

基于InMemoryStore

复制代码
from typing import NotRequired

from langchain.agents import AgentState, create_agent
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langgraph.prebuilt import ToolRuntime
from langgraph.store.memory import InMemoryStore
from dotenv import load_dotenv
from langgraph.store.postgres import PostgresStore

load_dotenv(override=True)
# ============================================================
# 1. 自定义 Agent 状态
# ============================================================

class CustomState(AgentState):
    """
    自定义 Agent 状态。

    在默认 AgentState 的基础上增加 user_id,
    用于标识当前用户,从而实现不同用户之间的长期记忆隔离。
    """

    user_id: NotRequired[str]


# ============================================================
# 2. 创建长期记忆存储
# ============================================================

store = InMemoryStore()


# ============================================================
# 3. 保存用户信息
# ============================================================

@tool(parse_docstring=True)
def save_user_info(
    name: str,
    runtime: ToolRuntime,
) -> str:
    """
    将用户信息保存到长期记忆中。

    Args:
        name: 用户名。
        runtime: 工具运行时,可以通过它访问当前 Agent 的状态和 Store。

    Returns:
        str: 保存结果。
    """

    # 长期记忆的命名空间
    namespace = ("users",)

    # 使用 user_id 作为长期记忆的 key,
    # 从而实现不同用户之间的数据隔离。
    key = runtime.state["user_id"]

    # 要保存的用户信息
    value = {
        "name": name,
    }

    # 写入长期记忆
    runtime.store.put(
        namespace,
        key,
        value,
    )

    return "saved"


# ============================================================
# 4. 获取用户信息
# ============================================================

@tool(parse_docstring=True)
def get_user_info(
    runtime: ToolRuntime,
) -> str:
    """
    从长期记忆中读取当前用户的信息。

    Args:
        runtime: 工具运行时,可以通过它访问当前 Agent 的状态和 Store。

    Returns:
        str: 当前用户的信息,如果不存在则返回 unknown。
    """

    # 与保存用户信息时使用相同的命名空间
    namespace = ("users",)

    # 获取当前用户 ID
    key = runtime.state["user_id"]

    # 从长期记忆中读取数据
    item = runtime.store.get(
        namespace,
        key,
    )

    # 如果存在用户信息,则返回保存的数据
    if item:
        return str(item.value)

    # 用户信息不存在
    return "unknown"


# ============================================================
# 5. 创建 Agent
# ============================================================

agent = create_agent(
    model="deepseek:deepseek-v4-pro",
    tools=[
        save_user_info,
        get_user_info,
    ],
    store=store,
    state_schema=CustomState,
    system_prompt=(
        "用户提及个人信息时及时记录,"
        "用户询问个人信息时尝试使用工具检索。"
    ),
)


# ============================================================
# 6. 第一个会话(线程)
# ============================================================

print("=" * 30, "-> 第一个会话(线程) <-", "=" * 30)

response1 = agent.invoke(
    {
        "messages": [
            HumanMessage("你好,很高兴认识你,我是小花")
        ],
        "user_id": "user-1",
    }
)

for msg in response1["messages"]:
    msg.pretty_print()


# ============================================================
# 7. 第二个会话(线程)
# ============================================================

print("=" * 30, "-> 第二个会话(线程) <-", "=" * 30)

response2 = agent.invoke(
    {
        "messages": [
            HumanMessage("我是谁")
        ],
        "user_id": "user-1",
    }
)

for msg in response2["messages"]:
    msg.pretty_print()

其中,CustomState扩展了 Agent 的标准状态。除了默认的 messages (历史消息列表)之外,还额 外增加了一个 user_id 字段。这样,Agent 在运行过程中随时随地都能知道当前和它说话的用户 ID 是 什么

============================== -> 第一个会话(线程) <- ==============================

================================ Human Message =================================

你好,很高兴认识你,我是小花

================================== Ai Message ==================================

Tool Calls:

save_user_info (call_00_Svus19LFM2OgOZtELi6G0298)

Call ID: call_00_Svus19LFM2OgOZtELi6G0298

Args:

name: 小花

================================= Tool Message =================================

Name: save_user_info

saved

================================== Ai Message ==================================

你好呀,小花!很高兴认识你~我已经记住你的名字啦。有什么我可以帮你的吗?😊

============================== -> 第二个会话(线程) <- ==============================

================================ Human Message =================================

我是谁

================================== Ai Message ==================================

Tool Calls:

get_user_info (call_00_4SzCqDqy4HBmwXO9suQf5842)

Call ID: call_00_4SzCqDqy4HBmwXO9suQf5842

Args:

================================= Tool Message =================================

Name: get_user_info

{'name': '小花'}

================================== Ai Message ==================================

你叫小花。😊 有什么我可以帮你的吗?

两次invoke没有通过config串联为起来,是两个独立的会话,但第二个会话可以访问第一个会话写入长 期记忆的内容。

② 基于PostgresStore

复制代码
import os
from typing import NotRequired

from dotenv import load_dotenv
from langchain.agents import AgentState, create_agent
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langgraph.prebuilt import ToolRuntime
from langgraph.store.postgres import PostgresStore


# ============================================================
# 1. 加载环境变量
# ============================================================

load_dotenv(override=True)

DB_URL = os.getenv("DB_URL")

if not DB_URL:
    raise ValueError("DB_URL 未配置,请检查 .env 文件")


# ============================================================
# 2. 自定义 Agent 状态
# ============================================================

class CustomState(AgentState):
    """
    自定义 Agent 状态。

    在默认 AgentState 的基础上增加 user_id,
    用于标识当前用户,从而实现不同用户之间的长期记忆隔离。
    """

    user_id: NotRequired[str]


# ============================================================
# 3. 保存用户信息
# ============================================================

@tool(parse_docstring=True)
def save_user_info(
    name: str,
    runtime: ToolRuntime,
) -> str:
    """
    将用户信息保存到长期记忆中。

    Args:
        name: 用户名。
        runtime: 工具运行时,可以访问当前 Agent 的状态和 Store。

    Returns:
        str: 保存结果。
    """

    # 长期记忆的命名空间
    namespace = ("users",)

    # 获取当前用户 ID
    key = runtime.state["user_id"]

    # 要保存的用户信息
    value = {
        "name": name,
    }

    # 保存到 PostgreSQL Store
    runtime.store.put(
        namespace,
        key,
        value,
    )

    return "saved"


# ============================================================
# 4. 获取用户信息
# ============================================================

@tool(parse_docstring=True)
def get_user_info(
    runtime: ToolRuntime,
) -> str:
    """
    从长期记忆中读取当前用户的信息。

    Args:
        runtime: 工具运行时,可以访问当前 Agent 的状态和 Store。

    Returns:
        str: 用户信息。如果不存在则返回 unknown。
    """

    # 与保存时保持相同的命名空间
    namespace = ("users",)

    # 获取当前用户 ID
    key = runtime.state["user_id"]

    # 从 PostgreSQL Store 查询
    item = runtime.store.get(
        namespace,
        key,
    )

    if item:
        return str(item.value)

    return "unknown"


# ============================================================
# 5. 创建 PostgreSQL Store
# ============================================================

with PostgresStore.from_conn_string(DB_URL) as store:

    # 第一次使用时执行数据库初始化。
    # 会创建 Store 所需要的数据表和迁移信息。
    store.setup()

    # ========================================================
    # 6. 创建 Agent
    # ========================================================

    agent = create_agent(
        model="deepseek:deepseek-v4-pro",
        tools=[
            save_user_info,
            get_user_info,
        ],
        store=store,
        state_schema=CustomState,
        system_prompt=(
            "用户提及个人信息时及时记录,"
            "用户询问个人信息时尝试使用工具检索。"
        ),
    )

    # ========================================================
    # 7. 第一个会话(线程)
    # ========================================================

    print(
        "=" * 30,
        "-> 第一个会话(线程) <-",
        "=" * 30,
    )

    response1 = agent.invoke(
        {
            "messages": [
                HumanMessage(
                    "你好,很高兴认识你,我是小花"
                )
            ],
            "user_id": "user-1",
        }
    )

    for msg in response1["messages"]:
        msg.pretty_print()

    # ========================================================
    # 8. 第二个会话(线程)
    # ========================================================

    print(
        "=" * 30,
        "-> 第二个会话(线程) <-",
        "=" * 30,
    )

    response2 = agent.invoke(
        {
            "messages": [
                HumanMessage("我是谁")
            ],
            "user_id": "user-1",
        }
    )

    for msg in response2["messages"]:
        msg.pretty_print()

查看PostgresSQL数据表

打开Xshell客户端,查看表

新增两张Store相关的表 store 和 store_migrations 。

2 在中间件中访问长期记忆

① Node-style hooks中访问

以 before_model 为例,其钩子函数签名如下

复制代码
def before_model(
    self,
    state: StateT,
    runtime: Runtime[ContextT],
) -> dict[str, Any] | None:
    """
    在模型调用之前执行。

    Args:
        state: 当前 Agent 的状态数据。
        runtime: 当前运行时上下文,可用于获取 Context 等运行时信息。

    Returns:
        dict[str, Any] | None:
            - 返回字典:用于更新当前 State。
            - 返回 None:表示不修改当前 State。
    """
    pass

Runtime 定义如下

复制代码
@dataclass(**_DC_KWARGS)
class Runtime(Generic[ContextT]):
    # 当前这次 Agent / Graph 运行的静态上下文
    # 例如:user_id、数据库连接、租户信息等
    context: ContextT = field(default=None)  # type: ignore[assignment]

    """
    Static context for the graph run, like `user_id`, `db_conn`, etc.

    可以理解为:
    当前这次 Graph 运行过程中一直存在的"依赖信息"。

    例如:
        context = {
            "user_id": 1001,
            "db_conn": db
        }

    这些数据一般不会随着模型调用不断变化。
    """

    # Graph 运行过程中使用的 Store
    # 可以用于持久化数据、长期记忆等
    store: BaseStore | None = field(default=None)

    """
    Store for the graph run, enabling persistence and memory.

    可以理解为:
    Agent 的"长期存储"。

    例如可以保存:
        - 用户偏好
        - 用户历史信息
        - 长期记忆
        - 向量检索数据

    与 state 的区别:

        state  -> 当前这一次运行的数据
        store  -> 可以跨运行保存的数据
    """

    # 自定义流式输出函数
    # Agent 运行过程中,可以通过它向外部发送数据
    stream_writer: StreamWriter = field(
        default=_no_op_stream_writer
    )

    """
    Function that writes to the custom stream.

    可以理解为:
    一个"消息输出器"。

    在流式响应(stream)场景中,
    可以通过这个函数把中间结果发送给前端。

    如果没有提供,则使用 _no_op_stream_writer,
    即默认什么都不做。
    """

    # 当前 Thread 上一次运行返回的结果
    # 主要用于 Functional API + Checkpointer
    previous: Any = field(default=None)

    """
    The previous return value for the given thread.

    表示:
    当前 thread 上一次执行的返回值。

    注意:
    这个字段只有在:

        Functional API
        +
        Checkpointer

    的情况下才可用。
    """

    ...

所以,我们可以通过 runtime.store 在中间件中访问长期记忆

② Wrap-style hooks中访问

  1. wrap_model_call

钩子函数签名如下

复制代码
def wrap_model_call(
    self,
    request: ModelRequest[ContextT],
    handler: Callable[
        [ModelRequest[ContextT]],
        ModelResponse[ResponseT],
    ],
) -> ModelResponse[ResponseT] | AIMessage | ExtendedModelResponse[ResponseT]:
    # request:当前模型请求,包含消息、模型、工具等信息
    # handler:继续执行模型调用的处理函数
    # 返回值:模型响应、AI 消息或扩展模型响应

ModelRequest 定义如下

复制代码
@dataclass(init=False)
class ModelRequest(Generic[ContextT]):
    """
    Agent 的模型请求信息。

    Type Parameters:
        ContextT:
            Runtime 上下文的类型。
            如果没有指定,则默认为 None。
    """

    # 当前使用的聊天模型
    model: BaseChatModel

    # 当前消息列表,不包含 system message
    messages: list[AnyMessage]

    # 系统消息
    system_message: SystemMessage | None

    # 工具调用选择配置
    tool_choice: Any | None

    # 当前可用的工具列表
    tools: list[BaseTool | dict[str, Any]]

    # 结构化响应格式
    response_format: ResponseFormat[Any] | None

    # Agent 当前状态
    state: AgentState[Any]

    # 当前运行时上下文
    runtime: Runtime[ContextT]

    # 模型配置参数
    model_settings: dict[str, Any] = field(
        default_factory=dict
    )

所以,可以通过 request.runtime.store 访问长期记忆。

  1. wrap_tool_call

钩子函数签名如下

复制代码
def wrap_tool_call(
    self,
    request: ToolCallRequest,
    handler: Callable[
        [ToolCallRequest],
        ToolMessage | Command[Any],
    ],
) -> ToolMessage | Command[Any]:
    # request:当前工具调用请求
    # handler:继续执行工具调用的处理函数
    # 返回值:工具消息或 Command 指令

ToolCallRequest 定义如下

复制代码
@dataclass
class ToolCallRequest:
    """
    传递给工具调用拦截器的工具执行请求。

    Attributes:
        tool_call:
            模型输出的工具调用信息。
            包含工具名称、参数和调用 ID。

        tool:
            要执行的 BaseTool 实例。
            如果工具没有注册到 ToolNode,则为 None。

            当 tool 为 None 时,拦截器可以直接处理请求,
            此时不会进行工具验证。

            如果拦截器调用 execute(),
            则会进行工具验证。
            如果工具未注册,则会抛出异常。

        state:
            Agent 当前状态。
            可以是 dict、list 或 BaseModel。

        runtime:
            LangGraph 的运行时上下文。
            如果在 Graph 外部运行,则为 None。
    """

    # 模型生成的工具调用信息
    # 包含工具名称、参数和调用 ID
    tool_call: ToolCall

    # 当前需要执行的工具
    # 如果工具未注册到 ToolNode,则为 None
    tool: BaseTool | None

    # Agent 当前状态
    state: Any

    # LangGraph 运行时上下文
    # 可以通过 request.runtime.store 访问长期记忆
    runtime: ToolRuntime

ToolRuntime 定义如下

复制代码
@dataclass
class ToolRuntime(
    _DirectlyInjectedToolArg,
    Generic[ContextT, StateT],
):
    # Agent 当前的状态
    state: StateT

    # 当前运行的上下文
    # 例如:user_id、数据库连接等
    context: ContextT

    # LangChain Runnable 的运行配置
    config: RunnableConfig

    # 流式输出函数
    # 可以向外部发送自定义流式数据
    stream_writer: StreamWriter

    # 当前工具调用的 ID
    # 如果没有工具调用 ID,则为 None
    tool_call_id: str | None

    # 持久化存储
    # 可以用于保存和读取长期记忆
    store: BaseStore | None

所以,可以通过 request.runtime.store 访问长期记忆。

3.4 何时写入记忆

官方介绍了两种方式。

  1. 在主流程里写(hot path)

也就是:用户发消息,AI 一边回答,一边决定要不要记下来。

  • 优点: 立即生效 下一轮马上能用 用户可感知,透明
  • 缺点: 增加延迟 逻辑变复杂
  1. 在后台写(background)

就是先回答用户,记忆整理放到后台 异步 做

  • 优点: 主流程更快 记忆逻辑更独立 更适合批量整理
  • 缺点: 不能立刻生效 要决定多久整理一次 触发时机不好选

工程上通常这么选:

  • 用户偏好、账号资料 :可热路径写
  • 对话摘要、经验沉淀、行为分析 :更适合后台写
相关推荐
evans在进步16 分钟前
Java 常用设计模式入门:建造者、工厂、单例、外观与代理
java·python·设计模式
用户9314563556616 分钟前
接口幂等性设计:从原理到落地,一篇讲透
前端
柚yuzumi17 分钟前
别再猜 this:先看它属于谁,再看它指向谁
前端·javascript
YIAN20 分钟前
React + Zustand + JWT 前端权限体系完整实现:从登录鉴权到路由守卫全流程拆解
前端·react.js·架构
汉堡大王952724 分钟前
面试必考:手写代码 new 做了什么?从原理到实现全解析
前端·javascript·面试
sunburn-35 分钟前
Java堆(Heap)详解与实战教学
java·开发语言·数据结构·ide·算法
BillKu1 小时前
TypeScript中,字符串字面量联合类型(Union Type)、enum的用法说明
前端·javascript·typescript
jayson.h1 小时前
PDF 合并+添加页码 相关库、类、函数
开发语言·前端·python
qq_322762751 小时前
一个接口从能用到稳定,中间差的到底是什么
服务器·开发语言·lua·接口·fastapi·请求