深入解析:LangGraph长短期记忆实践

一、概述

        在langgraph体系中,短期记忆(short-term memory)是线程级别的,即在图编译的时候指定checkpointer,并在图调用的时候指定thread_id,短期记忆是线程内共享的,在实际的对话系统中,可以使用session_id作为thread_id,实现会话级别的记忆管理。

        而长期记忆(long-term memory)是跨thread_id的,即可以抽取每个会话的用户的基本信息、提问偏好等信息,作为每个会话共享的数据,更好的理解用户的问题与意图。具体在图调用的时候指定user_id(或者其他可唯一标识用户的键),在图调用过程中自定义的基于历史消息抽取长期记忆,在回答时召回长期记忆作为补充去调用大模型。

 二、环境说明与安装

为实现快速实现功能,聊天模型与embedding模型使用的是阿里云百炼平台,其他环境版本如下所示:

langchain==1.0.5
langchain-community==0.4.1
langgrap==1.0.2
langgraph-checkpoint-sqlite==3.0.0
pydantic==2.12.4
python-dotenv==1.2.1

三、短期记忆

短期记忆在langgraph中很容易实现,即定义有状态图,对话消息作为图状态的一部分。要求:

  • 在图编译(compile)的时候指定checkpointer
  • 在图调用(invoke或stream)的时候需要指定thread_id

3.1 代码实现

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, MessagesState, START
def say_hi(state: MessagesState):
    return {"messages": {"role":"human", "content":"Hi"}}
graph_builder = StateGraph(MessagesState)
graph_builder.add_node("say_hi", say_hi)
graph_builder.add_edge(START, "say_hi")
checkpointer = InMemorySaver()
graph = graph_builder.compile(checkpointer=checkpointer)
print(graph.invoke(
    {"messages": [{"role": "user", "content": "hi! i am Bob"}]},
    {"configurable": {"thread_id": "1"}},
))

运行结果:

{'messages': [HumanMessage(content='hi! i am Bob', additional_kwargs={}, response_metadata={}, id='f42aa618-4b40-4a6c-b78f-dfb325c868d4'), HumanMessage(content='Hi', additional_kwargs={}, response_metadata={}, id='44e332fa-370e-484b-9e02-6b2f155c8934')]}

3.2 短期记忆管理

大模型的上线文窗口是有限的,消息不能无限制的增长,所以消息的管理至关重要

3.2.1 消息截断(trim)

一种办法就是在调用大模型之前,从状态中获取历史消息,对历史消息进行截断,截断的单位是token,比如只保留最近128个token

from langchain_core.messages.utils import (
    trim_messages,
    count_tokens_approximately
)
def call_model(state: MessagesState):
    messages = trim_messages(
        state["messages"],
        strategy="last",
        token_counter=count_tokens_approximately,
        max_tokens=128,
        start_on="human",
        end_on=("human", "tool"),
    )
    response = model.invoke(messages)
    return {"messages": [response]}

3.2.2 消息删除(delete)

可以自定义删除消息,如保留最近3条消息

from langchain.messages import RemoveMessage
def delete_messages(state):
    messages = state["messages"]
    if len(messages) >= 5:
        # remove the earliest two messages
        return {"messages": [RemoveMessage(id=m.id) for m in messages[:2]]}

3.2.3 消息摘要(summarize)

修剪(trim)或删除(delete)消息的问题在于消息队列的清理而丢失信息,对历史消息进行归纳压缩可以解决这个问题,如有100条消息,让大模型对最早的70条消息进行归纳总结,仅保留最近30条消息,往往最近的消息更加重要

四、长期记忆

4.1 要点

  • 使用InMemorySaver作为会话状态存储后端
  • 使用SqliteStore作为长期记忆存储后端
  • extract_base_info函数抽取最近一条记录的用户基本信息
  • 在具体api操作层面,存储过程中namespace作为每个用户的“存储域”,namespace中的key是唯一的,如果key存在则更新value,如果不存在则新增一条记录。namespace + key可以唯一确定一条记录
  • 图示sqlite存储表结构(store表,如果store指定了embedding模型,则还有store_vectors表)
store表

4.2 代码实现

        通过设置两个不同的thread_id,模拟两个不同的会话,可以发现在第一个会话中抽取的长期记忆,在第二个会话通过召回补充特定的记忆能很好的回答用户的问题

import os
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
from langchain_community.embeddings import DashScopeEmbeddings
from langgraph.store.sqlite import SqliteStore
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import START, END, StateGraph, MessagesState
from pydantic import BaseModel, Field
from typing import List
from langchain_core.runnables import RunnableConfig
from langgraph.store.base import BaseStore
load_dotenv()
api_key = os.environ["API_KEY"]
# 1.模型定义
## 1.1 chat 模型
model = ChatTongyi(
    model="qwen3-32b",
    api_key=api_key,
    streaming=True,
    extra_body={  # 关键:通过此参数传递思考模式配置
        "enable_thinking": True
    }
)
## 1.2 embedding 模型
embeddings = DashScopeEmbeddings(
    model="text-embedding-v4",
    dashscope_api_key=api_key
)
# 2.基本信息抽取
## 2.1 抽取schema定义
class AttributeItem(BaseModel):
    """定义一个属性项,包含属性名和属性值"""
    name: str = Field(description="属性的名称")
    value: str = Field(description="属性的值")
class AttributeList(BaseModel):
    attributes: List[AttributeItem] = Field(description="属性项组成的列表")
## 2.2 抽取函数定义
def extract_base_info(state: MessagesState) -> AttributeList:
    chat_history = "\n".join([ele.content for ele in state.get("messages")])
    prompt = f"""
    你是一个用户信息抽取专家,请基于一下记录,抽取用户的基本信息,包括但不限于:
    姓名、年龄、家庭住址等信息
    {chat_history}
    """
    structure_model = model.with_structured_output(AttributeList)
    return structure_model.invoke(prompt)
# 3.图构建
with SqliteStore.from_conn_string("longterm_memory.db", index={"embed": embeddings}) as store:
    store.setup() # 第一次需要初始化
    def invoke_model(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
        # 获取最新一条信息
        newest_msg = state.get("messages")[-1].content
        # 获取配置,获取user_id,拼接namespace(每个用户一个namespace)
        user_id = config.get("configurable").get("user_id")
        namespace = ("memories", user_id)
        # 抽取基本信息,并保存到长期记忆
        attributes: AttributeList = extract_base_info(state)
        if attributes is not None:
            print(f"抽取的信息:{attributes.attributes}")
            for ele in attributes.attributes:
                # key要求在namespace里面唯一,namespace和key可以唯一确定一条数据
                # key存在,则会进行更新value,否则则插入
                if ele.name is not None and ele.value is not None:
                    store.put(namespace, key=ele.name, value=ele.value)
        # 召回长期记忆
        longterm_memories = store.search(namespace, query=newest_msg, limit=2)
        print(f"召回的信息:{longterm_memories}")
        longterm_memories = "\n".join([ele.value for ele in longterm_memories])
        # 调用模型时候拼接记忆
        system_prompt = f"""
        You are a helpful assistant talking to the user. User info: {longterm_memories}
        """
        response = model.invoke(
            [{"role": "system", "content": system_prompt}] + state["messages"]
        )
        return {"messages": response}
    # 图定义
    graph_builder = StateGraph(MessagesState)
    graph_builder.add_node("invoke_model", invoke_model)
    graph_builder.add_edge(START, "invoke_model")
    checkpointer = InMemorySaver()
    graph = graph_builder.compile(checkpointer=checkpointer, store=store)
    config = {
        "configurable": {
            "thread_id": "1",
            "user_id": "123"
        }
    }
    print(graph.invoke({
        "messages": [
            {"role": "user", "content": "我的名字是Alice,职业是钢琴家,现年28岁,喜欢在舞台上演奏、打羽毛球,你叫啥名字呢?"}
        ]
    }, config=config))
    print("=" * 50)
    config = {
        "configurable": {
            "thread_id": "2",
            "user_id": "123"
        }
    }
    print(graph.invoke({
        "messages": [
            {"role": "user", "content": "请问我的爱好是什么?"}
        ]
    }, config=config))

运行结果:

抽取的信息:[AttributeItem(name='姓名', value='Alice'), AttributeItem(name='年龄', value='28岁'), AttributeItem(name='职业', value='钢琴家'), AttributeItem(name='兴趣爱好', value='舞台上演奏、打羽毛球')]
召回的信息:[Item(namespace=['memories', '123'], key='兴趣爱好', value='舞台上演奏、打羽毛球', created_at='2025-11-08T12:34:52', updated_at='2025-11-08T12:34:52', score=0.5591988861560822), Item(namespace=['memories', '123'], key='年龄', value='28岁', created_at='2025-11-08T12:34:51', updated_at='2025-11-08T12:34:51', score=0.4479065537452698)]
{'messages': [HumanMessage(content='我的名字是Alice,职业是钢琴家,现年28岁,喜欢在舞台上演奏、打羽毛球,你叫啥名字呢?', additional_kwargs={}, response_metadata={}, id='3ded43c8-bc52-449d-bf62-2df0892c6166'), AIMessage(content='你好Alice!我是Qwen,很高兴认识你这位才华横溢的钢琴家~  作为音乐爱好者,每次听到你在舞台上演奏一 定都令人激动不已吧?听说你还喜欢打羽毛球,这种动静结合的生活方式真让人向往!有什么我可以帮你的吗?无论是讨论音乐还是运动心得,我都随时奉陪哦~ ', additional_kwargs={'reasoning_content': '好的,用户是Alice,28岁的钢琴家,喜欢在舞台上演奏和打羽毛球。她问我的名字,我需要先回应她的自我介绍,然后给出一个合适的名字。作为AI助手,我 需要保持友好和专业的态度,同时结合她的兴趣爱好来建立联系。\n\n首先,她的职业是钢琴家,说明她可能对音乐有很高的造诣,喜欢舞台表演,这可能意味着她享受在观众面前展示自己的才华。另外,她喜欢打羽毛球,这显示她可能喜欢运动,注重身体健康或者享受竞技的乐趣。结合这些信息,我应该选择一个既专业又亲切的名字,可能带有一些音乐或运动的元素,但又不能太复杂。\n\n接下来,我需要考虑如何回应她的名字和职业。可以表达对她职业的赞赏,比如提到她的钢琴演奏,或者询问她最近的演出情况。同时,可以提到打羽毛球,询问她的兴趣,这样能展示出我对她的关注,并且找到共同话题。\n\n在名字方面,我需要选一个容易记住且符合AI助手形象的名字。可能用一些音乐相关的词汇,比如Melody、Harmony,或者 运动相关的词汇,比如Court、Racket,但也要保持中立,不显得太刻意。比如“小乐”这样的名字,既简单又有音乐感,容易发音,也符合中文用户的习惯。\n\n然后,我需要确保回复自 然流畅,不显得生硬。比如,先回应她的名字和职业,然后介绍自己的名字,并表达愿意帮助的意愿。同时,可以加入一些表情符号或轻松的语气词,让对话更亲切。\n\n最后,检查是否有遗漏的信息,比如她提到的年龄和兴趣,是否需要进一步展开话题。比如,可以问她最近有没有新的演出计划,或者羽毛球打得怎么样,这样能促进进一步的交流,建立更紧密的联系。'}, response_metadata={'finish_reason': 'stop', 'request_id': '2b1f7ca1-ea62-497e-a313-2656644ddea1', 'token_usage': {'input_tokens': 73, 'output_tokens': 459, 'total_tokens': 532, 'output_tokens_details': {'reasoning_tokens': 379}}}, id='lc_run--439e1527-c0f3-46ed-8c7a-8ba8ccab9869')]}
==================================================
召回的信息:[Item(namespace=['memories', '123'], key='兴趣爱好', value='舞台上演奏、打羽毛球', created_at='2025-11-08T12:34:52', updated_at='2025-11-08T12:34:52', score=0.45809656381607056), Item(namespace=['memories', '123'], key='年龄', value='28岁', created_at='2025-11-08T12:34:51', updated_at='2025-11-08T12:34:51', score=0.2850668430328369)]
{'messages': [HumanMessage(content='请问我的爱好是什么?', additional_kwargs={}, response_metadata={}, id='e5759dc3-4214-432d-80fb-c493fb25edd3'), AIMessage(content='你的爱好是舞台上演奏和打羽毛球!这真的很棒呢~(*^▽^*) 舞台演奏让人感受到艺术的魅力,而羽毛球则是充满活力的运动,两者结合的你一定很有感染力吧?平时会参加演出或者打球比赛吗?', additional_kwargs={'reasoning_content': '好的,用户问的是“请问我的爱好是什么?”。首先,我需要回顾之前的对话历史。用户之前提供的信息是:28岁,爱好是舞台上演奏和打羽毛球。所以现在的问题是确认用户的爱好。\n\n接下来,我需要确保回答准确无误。用户的信息已经明确提到这两个爱好,所以直接引用即可。不过,用户可能希望得到更详细的回应,或者想进一步讨论这些爱好。比如,他们可能想分享更多关于舞台演奏的经历,或者询问如何提升羽毛球技巧。\n\n另外,考虑到用户28岁,可能处于职业和兴趣发展的阶段,可以适当加入一些鼓励的话,或者询问是否有相关目标。例如,是否在准备演出,或者有没有考虑参加羽毛球比赛。这样可以让对话更深入,也能展示关心。\n\n还要注意语气要友好、自然,避免机械化的回答。使用表情符号或轻松的语言会让用户感觉更亲切。同时,保持回答简洁,但留有进一步交流的空间,比如用问题结尾,邀请用户分享更多。\n\n最后,检查是否有遗漏的信息,确保没有误解用户的爱好。用户提到的是“舞台上演奏”,可能需要确认是指乐器演奏还是其他表演形式,但根据常见情况,可以默认是乐器演奏。如果有不确定的地方,可以礼貌地询问澄清,但这里用户已经明确给出,所以直接使用即可。'}, response_metadata={'finish_reason': 'stop', 'request_id': 'e4f6ed40-4c75-4ac6-82a7-6b437678a794', 'token_usage': {'input_tokens': 47, 'output_tokens': 336, 'total_tokens': 383, 'output_tokens_details': {'reasoning_tokens': 272}}}, id='lc_run--668a8720-1258-456a-a633-134f479dad65')]}

五、参考

Memory - Docs by LangChain

posted @ 2026-01-21 18:01  gccbuaa  阅读(4)  评论(0)    收藏  举报