LangChain设计与实现-第13章-记忆与会话管理

7 阅读16分钟

第13章 记忆与会话管理

本书章节导航


引言

大语言模型本身是无状态的 -- 每次调用都是独立的,模型不会"记住"之前的对话内容。要构建一个能够进行连贯对话的 AI 应用,开发者必须在应用层实现记忆机制,将历史上下文注入到每次调用的输入中。

LangChain 的记忆系统经历了两代设计。第一代以 BaseMemory 为基础,通过 ConversationBufferMemoryConversationSummaryMemory 等类提供了丰富的记忆策略,与 Chain 体系深度集成。第二代以 RunnableWithMessageHistory 为核心,建立在 LCEL 和 BaseChatMessageHistory 之上,提供了更灵活、更现代的会话管理方式。

在最新的源码中,第一代记忆系统的所有类已被标记为 @deprecated,但它们的设计思想和实现模式依然值得深入研究。理解这些设计模式不仅有助于维护使用旧 API 的项目,更重要的是,它们揭示了 AI 应用中记忆管理的核心挑战和解决思路。

:::tip 本章要点

  • BaseMemory 定义了 load_memory_variables / save_context / clear 三个核心抽象方法
  • ConversationBufferMemory 存储完整对话历史,ConversationSummaryMemory 通过 LLM 压缩历史为摘要
  • ConversationTokenBufferMemory 按 token 上限裁剪历史,ConversationEntityMemory 提取和维护实体知识
  • VectorStoreRetrieverMemory 通过向量检索实现语义相关的记忆检索
  • ChatMessageHistory 是消息持久化的抽象接口,支持多种存储后端
  • RunnableWithMessageHistory 是现代推荐方案,通过包装 Runnable 自动管理消息历史 :::

13.1 BaseMemory:记忆的抽象基础

13.1.1 核心抽象

BaseMemory 定义在 langchain_classic/base_memory.py 中,是所有记忆类的抽象基类:

class BaseMemory(Serializable, ABC):
    """Abstract base class for memory in Chains."""

    model_config = ConfigDict(arbitrary_types_allowed=True)

    @property
    @abstractmethod
    def memory_variables(self) -> list[str]:
        """The string keys this memory class will add to chain inputs."""

    @abstractmethod
    def load_memory_variables(self, inputs: dict[str, Any]) -> dict[str, Any]:
        """Return key-value pairs given the text input to the chain."""

    async def aload_memory_variables(self, inputs: dict[str, Any]) -> dict[str, Any]:
        return await run_in_executor(None, self.load_memory_variables, inputs)

    @abstractmethod
    def save_context(self, inputs: dict[str, Any], outputs: dict[str, str]) -> None:
        """Save the context of this chain run to memory."""

    async def asave_context(self, inputs, outputs) -> None:
        await run_in_executor(None, self.save_context, inputs, outputs)

    @abstractmethod
    def clear(self) -> None:
        """Clear memory contents."""

    async def aclear(self) -> None:
        await run_in_executor(None, self.clear)

三个核心方法形成了完整的记忆生命周期:

flowchart LR
    subgraph "Chain 执行流程"
        A["prep_inputs"] --> B["_call"]
        B --> C["prep_outputs"]
    end

    subgraph "Memory 生命周期"
        LOAD["load_memory_variables(inputs)"] --> INJECT["注入到 Chain 输入"]
        SAVE["save_context(inputs, outputs)"] --> STORE["保存到存储"]
    end

    A -->|调用| LOAD
    INJECT -->|返回给| A
    C -->|调用| SAVE
  • memory_variables:声明此记忆类将向 Chain 输入中添加哪些键。Chain 基类用它来区分哪些输入键来自用户,哪些来自记忆。
  • load_memory_variables(inputs):根据当前输入加载相关的记忆内容。返回的字典将被合并到 Chain 的输入中。
  • save_context(inputs, outputs):在 Chain 执行完成后保存当前轮次的上下文。

注意异步方法全部通过 run_in_executor 提供默认实现,这使得同步记忆类自动获得异步支持,尽管性能不如原生异步实现。

13.1.2 Memory 与 Chain 的集成机制

回顾 Chain 基类的 prep_inputsprep_outputs 方法(第11章详述),Memory 的集成是完全透明的:

class Chain(RunnableSerializable):
    memory: BaseMemory | None = None

    def prep_inputs(self, inputs):
        if not isinstance(inputs, dict):
            _input_keys = set(self.input_keys)
            if self.memory is not None:
                _input_keys = _input_keys.difference(self.memory.memory_variables)
            inputs = {next(iter(_input_keys)): inputs}
        if self.memory is not None:
            external_context = self.memory.load_memory_variables(inputs)
            inputs = dict(inputs, **external_context)
        return inputs

    def prep_outputs(self, inputs, outputs, return_only_outputs=False):
        self._validate_outputs(outputs)
        if self.memory is not None:
            self.memory.save_context(inputs, outputs)
        if return_only_outputs:
            return outputs
        return {**inputs, **outputs}

当一个 Chain 被传入单个字符串而非字典时,prep_inputs 会智能地推断哪个输入键是用户提供的(排除 Memory 提供的变量后剩余的那个)。这是一种"最少惊讶原则"的体现。

13.2 BaseChatMemory:对话记忆的中间层

BaseChatMemoryBaseMemory 之上增加了 ChatMessageHistory 的集成:

class BaseChatMemory(BaseMemory, ABC):
    chat_memory: BaseChatMessageHistory = Field(
        default_factory=InMemoryChatMessageHistory
    )
    output_key: str | None = None
    input_key: str | None = None
    return_messages: bool = False

    def _get_input_output(self, inputs, outputs):
        if self.input_key is None:
            prompt_input_key = get_prompt_input_key(inputs, self.memory_variables)
        else:
            prompt_input_key = self.input_key
        if self.output_key is None:
            if len(outputs) == 1:
                output_key = next(iter(outputs.keys()))
            elif "output" in outputs:
                output_key = "output"
                warnings.warn(...)
            else:
                raise ValueError(f"Got multiple output keys: {outputs.keys()}")
        else:
            output_key = self.output_key
        return inputs[prompt_input_key], outputs[output_key]

    def save_context(self, inputs, outputs):
        input_str, output_str = self._get_input_output(inputs, outputs)
        self.chat_memory.add_messages([
            HumanMessage(content=input_str),
            AIMessage(content=output_str),
        ])

    def clear(self):
        self.chat_memory.clear()
classDiagram
    class BaseMemory {
        <<abstract>>
        +memory_variables: list~str~
        +load_memory_variables(inputs) dict
        +save_context(inputs, outputs)
        +clear()
    }
    class BaseChatMemory {
        <<abstract>>
        +chat_memory: BaseChatMessageHistory
        +output_key: str | None
        +input_key: str | None
        +return_messages: bool
        +save_context(inputs, outputs)
        +clear()
    }
    class ConversationBufferMemory {
        +memory_key: str
        +buffer: Any
        +load_memory_variables(inputs)
    }
    class ConversationSummaryMemory {
        +buffer: str
        +llm: BaseLanguageModel
        +predict_new_summary(messages, existing_summary)
    }
    class ConversationTokenBufferMemory {
        +llm: BaseLanguageModel
        +max_token_limit: int
    }
    class ConversationEntityMemory {
        +llm: BaseLanguageModel
        +entity_store: BaseEntityStore
        +entity_cache: list~str~
    }

    BaseMemory <|-- BaseChatMemory
    BaseChatMemory <|-- ConversationBufferMemory
    BaseChatMemory <|-- ConversationSummaryMemory
    BaseChatMemory <|-- ConversationTokenBufferMemory
    BaseChatMemory <|-- ConversationEntityMemory

BaseChatMemory 的关键设计包括:

  1. 自动推断输入输出键:当 input_keyoutput_key 未设置时,会自动推断。如果有多个输出键但包含 "output",会发出警告并使用 "output"
  2. return_messages 开关:控制 load_memory_variables 返回的是消息对象列表还是格式化的字符串。Chat Model 通常需要消息列表,而 Text LLM 需要字符串。
  3. 委托给 ChatMessageHistory:实际的消息存储委托给 chat_memory 属性,实现了记忆策略与存储后端的分离。

13.3 ConversationBufferMemory:完整历史保留

最简单直接的记忆策略 -- 保留完整的对话历史:

class ConversationBufferMemory(BaseChatMemory):
    human_prefix: str = "Human"
    ai_prefix: str = "AI"
    memory_key: str = "history"

    @property
    def buffer(self) -> Any:
        return self.buffer_as_messages if self.return_messages else self.buffer_as_str

    @property
    def buffer_as_str(self) -> str:
        return get_buffer_string(
            self.chat_memory.messages,
            human_prefix=self.human_prefix,
            ai_prefix=self.ai_prefix,
        )

    @property
    def buffer_as_messages(self) -> list[BaseMessage]:
        return self.chat_memory.messages

    @property
    def memory_variables(self) -> list[str]:
        return [self.memory_key]

    def load_memory_variables(self, inputs):
        return {self.memory_key: self.buffer}

get_buffer_string 将消息列表格式化为字符串,格式如:

Human: 你好
AI: 你好!有什么可以帮助你的?
Human: 今天天气怎么样?
AI: 我无法直接获取天气信息...

human_prefixai_prefix 允许自定义角色标签,这在多语言场景中很有用。

适用场景:对话轮次较少(不超过模型上下文窗口的场景)。随着对话增长,完整历史会消耗越来越多的 token 预算。

13.4 ConversationSummaryMemory:摘要压缩

当对话变长时,ConversationSummaryMemory 通过 LLM 将历史压缩为摘要:

class SummarizerMixin(BaseModel):
    human_prefix: str = "Human"
    ai_prefix: str = "AI"
    llm: BaseLanguageModel
    prompt: BasePromptTemplate = SUMMARY_PROMPT
    summary_message_cls: type[BaseMessage] = SystemMessage

    def predict_new_summary(self, messages, existing_summary):
        new_lines = get_buffer_string(
            messages, human_prefix=self.human_prefix, ai_prefix=self.ai_prefix
        )
        chain = LLMChain(llm=self.llm, prompt=self.prompt)
        return chain.predict(summary=existing_summary, new_lines=new_lines)

class ConversationSummaryMemory(BaseChatMemory, SummarizerMixin):
    buffer: str = ""
    memory_key: str = "history"

    def save_context(self, inputs, outputs):
        super().save_context(inputs, outputs)
        self.buffer = self.predict_new_summary(
            self.chat_memory.messages[-2:], self.buffer
        )

    def load_memory_variables(self, inputs):
        if self.return_messages:
            buffer = [self.summary_message_cls(content=self.buffer)]
        else:
            buffer = self.buffer
        return {self.memory_key: buffer}

    def clear(self):
        super().clear()
        self.buffer = ""
sequenceDiagram
    participant User as 用户
    participant Chain as Chain
    participant Memory as ConversationSummaryMemory
    participant LLM as LLM (摘要)

    User->>Chain: 第 N 轮输入
    Chain->>Memory: load_memory_variables
    Memory-->>Chain: {history: "已有的对话摘要"}

    Chain->>Chain: 执行业务逻辑

    Chain->>Memory: save_context(inputs, outputs)
    Memory->>Memory: 先保存消息到 chat_memory
    Memory->>LLM: predict_new_summary(最后2条消息, 已有摘要)
    LLM-->>Memory: 更新后的摘要
    Memory->>Memory: self.buffer = 新摘要

关键设计点:

  1. 增量摘要:每次只将最新的两条消息(一轮对话)与现有摘要合并,而非重新摘要全部历史。这使得摘要成本为 O(1)(每轮固定成本)而非 O(n)。
  2. SummarizerMixin:摘要逻辑被提取为 Mixin,可以被其他记忆类复用(如 ConversationSummaryBufferMemory)。
  3. summary_message_cls:摘要以 SystemMessage 的形式返回,这在 Chat Model 中会被放置在消息序列的开头,提供全局上下文。

from_messages 工厂方法支持从已有消息历史构建摘要记忆:

@classmethod
def from_messages(cls, llm, chat_memory, *, summarize_step=2, **kwargs):
    obj = cls(llm=llm, chat_memory=chat_memory, **kwargs)
    for i in range(0, len(obj.chat_memory.messages), summarize_step):
        obj.buffer = obj.predict_new_summary(
            obj.chat_memory.messages[i : i + summarize_step], obj.buffer
        )
    return obj

13.5 ConversationTokenBufferMemory:按 token 裁剪

当需要精确控制 token 用量时,ConversationTokenBufferMemory 按 token 上限从前向后裁剪历史消息:

class ConversationTokenBufferMemory(BaseChatMemory):
    human_prefix: str = "Human"
    ai_prefix: str = "AI"
    llm: BaseLanguageModel
    memory_key: str = "history"
    max_token_limit: int = 2000

    def save_context(self, inputs, outputs):
        super().save_context(inputs, outputs)
        buffer = self.chat_memory.messages
        curr_buffer_length = self.llm.get_num_tokens_from_messages(buffer)
        if curr_buffer_length > self.max_token_limit:
            pruned_memory = []
            while curr_buffer_length > self.max_token_limit:
                pruned_memory.append(buffer.pop(0))
                curr_buffer_length = self.llm.get_num_tokens_from_messages(buffer)
flowchart TD
    SAVE["save_context 被调用"] --> ADD["添加新消息到 chat_memory"]
    ADD --> CALC["计算当前总 token 数"]
    CALC --> CHECK{超过 max_token_limit?}
    CHECK -->|否| DONE[结束]
    CHECK -->|是| POP["移除最早的消息"]
    POP --> RECALC["重新计算 token 数"]
    RECALC --> RECHECK{仍超过限制?}
    RECHECK -->|是| POP
    RECHECK -->|否| DONE

这种策略的核心优势是 精确的 token 预算控制。不同于按消息数量裁剪(可能导致 token 数量波动很大),它直接使用 LLM 的 tokenizer 计算真实 token 数,确保不会超出模型的上下文窗口。

需要注意的是,llm.get_num_tokens_from_messages 方法的准确性取决于具体的 LLM 实现。不同模型的 tokenizer 不同,因此这里传入的 llm 参数应该与实际使用的模型一致。

13.6 ConversationEntityMemory:实体级知识管理

ConversationEntityMemory 是最复杂的记忆实现,它使用 LLM 从对话中提取命名实体并维护每个实体的摘要:

class ConversationEntityMemory(BaseChatMemory):
    llm: BaseLanguageModel
    entity_extraction_prompt: BasePromptTemplate = ENTITY_EXTRACTION_PROMPT
    entity_summarization_prompt: BasePromptTemplate = ENTITY_SUMMARIZATION_PROMPT
    entity_cache: list[str] = []
    k: int = 3  # 用于实体提取的最近消息对数
    chat_history_key: str = "history"
    entity_store: BaseEntityStore = Field(default_factory=InMemoryEntityStore)

    @property
    def memory_variables(self) -> list[str]:
        return ["entities", self.chat_history_key]

load_memory_variables 做了两件事:提取实体名称,并查询每个实体的现有摘要:

def load_memory_variables(self, inputs):
    chain = LLMChain(llm=self.llm, prompt=self.entity_extraction_prompt)
    buffer_string = get_buffer_string(
        self.buffer[-self.k * 2 :],
        human_prefix=self.human_prefix, ai_prefix=self.ai_prefix,
    )
    output = chain.predict(history=buffer_string, input=inputs[prompt_input_key])

    if output.strip() == "NONE":
        entities = []
    else:
        entities = [w.strip() for w in output.split(",")]

    entity_summaries = {}
    for entity in entities:
        entity_summaries[entity] = self.entity_store.get(entity, "")

    self.entity_cache = entities

    if self.return_messages:
        buffer = self.buffer[-self.k * 2 :]
    else:
        buffer = buffer_string

    return {self.chat_history_key: buffer, "entities": entity_summaries}

save_context 为每个缓存的实体生成更新后的摘要:

def save_context(self, inputs, outputs):
    super().save_context(inputs, outputs)
    buffer_string = get_buffer_string(self.buffer[-self.k * 2 :], ...)

    chain = LLMChain(llm=self.llm, prompt=self.entity_summarization_prompt)
    for entity in self.entity_cache:
        existing_summary = self.entity_store.get(entity, "")
        output = chain.predict(
            summary=existing_summary, entity=entity,
            history=buffer_string, input=inputs[prompt_input_key]
        )
        self.entity_store.set(entity, output.strip())
flowchart TD
    subgraph "load_memory_variables"
        L1[获取最近 k 轮对话] --> L2["LLM 提取实体名称"]
        L2 --> L3{提取到实体?}
        L3 -->|是| L4["从 entity_store 查询每个实体的摘要"]
        L3 -->|否| L5["entities = 空字典"]
        L4 --> L6["返回 {history, entities}"]
        L5 --> L6
    end

    subgraph "save_context"
        S1["保存消息到 chat_memory"] --> S2["遍历 entity_cache 中的每个实体"]
        S2 --> S3["查询现有摘要"]
        S3 --> S4["LLM 生成更新后的摘要"]
        S4 --> S5["写入 entity_store"]
    end

13.6.1 BaseEntityStore 与存储后端

实体存储通过 BaseEntityStore 抽象,支持多种后端:

class BaseEntityStore(BaseModel, ABC):
    @abstractmethod
    def get(self, key: str, default: str | None = None) -> str | None: ...
    @abstractmethod
    def set(self, key: str, value: str | None) -> None: ...
    @abstractmethod
    def delete(self, key: str) -> None: ...
    @abstractmethod
    def exists(self, key: str) -> bool: ...
    @abstractmethod
    def clear(self) -> None: ...

LangChain 提供了四种内置实现:

实现类存储位置特点
InMemoryEntityStore内存字典简单快速,进程重启后丢失
RedisEntityStoreRedis支持 TTL,适合分布式
UpstashRedisEntityStoreUpstash Redis无服务器 Redis
SQLiteEntityStoreSQLite 文件单机持久化

Redis 实现支持自动过期(TTL),默认实体在创建后 24 小时过期,每次读取延长 3 天:

class RedisEntityStore(BaseEntityStore):
    ttl: int | None = 60 * 60 * 24      # 1 天
    recall_ttl: int | None = 60 * 60 * 24 * 3  # 3 天

    def get(self, key, default=None):
        return self.redis_client.getex(
            f"{self.full_key_prefix}:{key}", ex=self.recall_ttl
        ) or default or ""

这种"读取即延期"的设计巧妙地实现了 LRU(最近最少使用)语义:经常被提及的实体持续存活,长期不被提及的实体自动过期清除。

13.7 VectorStoreRetrieverMemory:语义检索记忆

VectorStoreRetrieverMemory 与前面的记忆类有本质区别 -- 它不是按时间顺序保留对话,而是将每轮对话存入向量数据库,然后根据当前输入的语义相似度检索相关的历史片段:

class VectorStoreRetrieverMemory(BaseMemory):
    retriever: VectorStoreRetriever = Field(exclude=True)
    memory_key: str = "history"
    input_key: str | None = None
    return_docs: bool = False
    exclude_input_keys: Sequence[str] = Field(default_factory=tuple)

    def load_memory_variables(self, inputs):
        input_key = self._get_prompt_input_key(inputs)
        query = inputs[input_key]
        docs = self.retriever.invoke(query)
        return self._documents_to_memory_variables(docs)

    def save_context(self, inputs, outputs):
        documents = self._form_documents(inputs, outputs)
        self.retriever.add_documents(documents)

    def _form_documents(self, inputs, outputs):
        exclude = set(self.exclude_input_keys)
        exclude.add(self.memory_key)
        filtered_inputs = {k: v for k, v in inputs.items() if k not in exclude}
        texts = [f"{k}: {v}" for k, v in list(filtered_inputs.items()) + list(outputs.items())]
        page_content = "\n".join(texts)
        return [Document(page_content=page_content)]
flowchart TD
    subgraph "save_context"
        SC1["过滤掉排除的输入键"]
        SC2["将输入和输出合并为文本"]
        SC3["创建 Document 对象"]
        SC4["添加到 VectorStore"]
        SC1 --> SC2 --> SC3 --> SC4
    end

    subgraph "load_memory_variables"
        LM1["获取当前输入文本"]
        LM2["用输入文本作为查询"]
        LM3["从 VectorStore 检索相似文档"]
        LM4["返回相关历史片段"]
        LM1 --> LM2 --> LM3 --> LM4
    end

这种记忆策略的独特优势在于:

  1. 无上下文窗口限制:向量数据库可以存储无限量的对话历史
  2. 语义相关性:检索到的不是最近的对话,而是与当前话题最相关的对话
  3. 长期记忆:即使是很久之前的对话,只要语义相关就能被召回

注意 _form_documents 方法中的 exclude_input_keys 设计 -- 它允许排除某些输入键(如 Memory 自身注入的 history),避免在存储时产生自引用的循环。

13.8 ChatMessageHistory:消息持久化层

13.8.1 BaseChatMessageHistory

所有记忆策略的底层消息存储都通过 BaseChatMessageHistory 接口访问:

class BaseChatMessageHistory(ABC):
    messages: list[BaseMessage]

    async def aget_messages(self) -> list[BaseMessage]:
        return await run_in_executor(None, lambda: self.messages)

    def add_user_message(self, message: HumanMessage | str) -> None:
        if isinstance(message, HumanMessage):
            self.add_message(message)
        else:
            self.add_message(HumanMessage(content=message))

    def add_ai_message(self, message: AIMessage | str) -> None:
        if isinstance(message, AIMessage):
            self.add_message(message)
        else:
            self.add_message(AIMessage(content=message))

    def add_message(self, message: BaseMessage) -> None:
        if type(self).add_messages != BaseChatMessageHistory.add_messages:
            self.add_messages([message])
        else:
            raise NotImplementedError(
                "add_message is not implemented. Implement add_message or add_messages."
            )

    def add_messages(self, messages: Sequence[BaseMessage]) -> None:
        for message in messages:
            self.add_message(message)

    @abstractmethod
    def clear(self) -> None:
        """Remove all messages from the store."""

add_messageadd_messages 之间存在一种巧妙的互递归关系:

  • 如果子类实现了 add_messages(批量添加),add_message 会委托给它
  • 如果子类没有实现 add_messages,默认实现会逐个调用 add_message

这种设计鼓励子类优先实现批量接口 add_messages(减少 IO 往返),同时保证了向后兼容性。

13.8.2 InMemoryChatMessageHistory

内存实现是最简单的参考实现:

class InMemoryChatMessageHistory(BaseChatMessageHistory, BaseModel):
    messages: list[BaseMessage] = Field(default_factory=list)

    def add_message(self, message: BaseMessage) -> None:
        self.messages.append(message)

    def clear(self) -> None:
        self.messages = []

13.8.3 存储后端生态

langchain_community 包中提供了大量的 ChatMessageHistory 实现,覆盖了主流的存储后端:

flowchart TD
    BASE[BaseChatMessageHistory]
    BASE --> INMEM[InMemoryChatMessageHistory]
    BASE --> REDIS[RedisChatMessageHistory]
    BASE --> PG[PostgresChatMessageHistory]
    BASE --> MONGO[MongoDBChatMessageHistory]
    BASE --> DDB[DynamoDBChatMessageHistory]
    BASE --> FS[FileChatMessageHistory]
    BASE --> SQL[SQLChatMessageHistory]
    BASE --> COSMOS[CosmosDBChatMessageHistory]
    BASE --> FIREBASE[FirestoreChatMessageHistory]

每种实现针对其存储后端的特性做了优化。例如 Redis 实现使用列表结构存储消息序列,支持自动过期;PostgreSQL 实现使用表结构,支持复杂查询和索引;DynamoDB 实现利用分区键实现按 session_id 高效查询。

13.9 RunnableWithMessageHistory:现代方式

13.9.1 设计动机

传统 Memory 系统与 Chain 紧耦合,无法直接用于 LCEL 管道。RunnableWithMessageHistory 解决了这个问题 -- 它是一个 Runnable 包装器,可以为任何 Runnable 自动管理消息历史。

class RunnableWithMessageHistory(RunnableBindingBase):
    get_session_history: GetSessionHistoryCallable
    input_messages_key: str | None = None
    output_messages_key: str | None = None
    history_messages_key: str | None = None
    history_factory_config: Sequence[ConfigurableFieldSpec]

13.9.2 初始化过程

构造函数构建了一个精巧的 Runnable 管道:

def __init__(self, runnable, get_session_history, *,
             input_messages_key=None, output_messages_key=None,
             history_messages_key=None, history_factory_config=None, **kwargs):
    # 1. 构建历史加载链
    history_chain = RunnableLambda(
        self._enter_history, self._aenter_history
    ).with_config(run_name="load_history")

    # 2. 如果需要,将历史注入到输入字典的指定键
    messages_key = history_messages_key or input_messages_key
    if messages_key:
        history_chain = RunnablePassthrough.assign(
            **{messages_key: history_chain}
        ).with_config(run_name="insert_history")

    # 3. 为 runnable 附加退出监听器(保存历史)
    runnable_sync = runnable.with_listeners(on_end=self._exit_history)
    runnable_async = runnable.with_alisteners(on_end=self._aexit_history)

    # 4. 组装完整管道
    bound = (
        history_chain
        | RunnableLambda(_call_runnable_sync, _call_runnable_async)
            .with_config(run_name="check_sync_or_async")
    ).with_config(run_name="RunnableWithMessageHistory")
flowchart LR
    subgraph "RunnableWithMessageHistory 内部管道"
        A["_enter_history: 加载历史消息"]
        B["RunnablePassthrough.assign: 注入到 history_messages_key"]
        C["被包装的 Runnable 执行"]
        D["_exit_history: 保存输入和输出消息"]
    end

    INPUT[用户输入] --> A --> B --> C --> OUTPUT[输出]
    C -.->|"on_end 监听器"| D

13.9.3 历史加载与保存

def _enter_history(self, value, config):
    hist = config["configurable"]["message_history"]
    messages = hist.messages.copy()

    if not self.history_messages_key:
        # 没有独立的历史键:将输入消息追加到历史消息后面
        input_val = value if not self.input_messages_key else value[self.input_messages_key]
        messages += self._get_input_messages(input_val)
    return messages

def _exit_history(self, run, config):
    hist = config["configurable"]["message_history"]

    inputs = load(run.inputs, allowed_objects="all")
    input_messages = self._get_input_messages(inputs)

    if not self.history_messages_key:
        # 去除已有的历史消息,避免重复
        historic_messages = config["configurable"]["message_history"].messages
        input_messages = input_messages[len(historic_messages):]

    output_val = load(run.outputs, allowed_objects="all")
    output_messages = self._get_output_messages(output_val)
    hist.add_messages(input_messages + output_messages)

_exit_history 中的去重逻辑很重要:当 history_messages_key 未设置时,输入消息是历史消息 + 当前消息的组合。保存时需要去除历史部分,只保存当前轮次的新消息。

13.9.4 Session 管理

_merge_configs 方法处理 session 的创建和查找:

def _merge_configs(self, *configs):
    config = super()._merge_configs(*configs)
    expected_keys = [field_spec.id for field_spec in self.history_factory_config]
    configurable = config.get("configurable", {})

    if len(expected_keys) == 1:
        message_history = self.get_session_history(configurable[expected_keys[0]])
    else:
        message_history = self.get_session_history(
            **{key: configurable[key] for key in expected_keys}
        )
    config["configurable"]["message_history"] = message_history
    return config

默认情况下,get_session_history 接受一个 session_id 参数。通过 history_factory_config 可以自定义参数(如 user_id + conversation_id):

# 默认:单一 session_id
chain_with_history = RunnableWithMessageHistory(
    chain, get_session_history,
    input_messages_key="question",
    history_messages_key="history",
)
chain_with_history.invoke(
    {"question": "..."},
    config={"configurable": {"session_id": "abc123"}}
)

# 自定义:user_id + conversation_id
chain_with_history = RunnableWithMessageHistory(
    chain, get_session_history,
    input_messages_key="question",
    history_messages_key="history",
    history_factory_config=[
        ConfigurableFieldSpec(id="user_id", annotation=str, ...),
        ConfigurableFieldSpec(id="conversation_id", annotation=str, ...),
    ],
)
chain_with_history.invoke(
    {"question": "..."},
    config={"configurable": {"user_id": "user1", "conversation_id": "conv1"}}
)

13.9.5 输入输出类型的智能处理

RunnableWithMessageHistory 支持多种输入输出格式的自动适配:

def _get_input_messages(self, input_val):
    if isinstance(input_val, dict):
        if self.input_messages_key:
            key = self.input_messages_key
        elif len(input_val) == 1:
            key = next(iter(input_val.keys()))
        else:
            key = "input"
        input_val = input_val[key]

    if isinstance(input_val, str):
        return [HumanMessage(content=input_val)]
    if isinstance(input_val, BaseMessage):
        return [input_val]
    if isinstance(input_val, (list, tuple)):
        return list(input_val)

def _get_output_messages(self, output_val):
    # 类似的逻辑,但处理 AI 消息
    if isinstance(output_val, str):
        return [AIMessage(content=output_val)]
    if isinstance(output_val, BaseMessage):
        return [output_val]
    # ...

这种多态处理使得 RunnableWithMessageHistory 可以包装几乎任何类型的 Runnable:直接传入消息列表的聊天模型、接受字典输入的 Chain、返回字符串的简单管道等。

13.10 从传统 Memory 到现代方案的迁移

13.10.1 迁移策略对照

传统 Memory现代替代方案
ConversationBufferMemoryRunnableWithMessageHistory + InMemoryChatMessageHistory
ConversationSummaryMemoryRunnableWithMessageHistory + 自定义摘要逻辑
ConversationTokenBufferMemoryRunnableWithMessageHistory + 消息裁剪 Runnable
ConversationEntityMemory自定义 Runnable 实现实体提取
VectorStoreRetrieverMemory将向量检索作为 Runnable 步骤集成

13.10.2 设计决策:为何弃用传统 Memory

传统 Memory 系统存在几个根本性的限制:

  1. 与 Chain 耦合BaseMemorysave_context(inputs, outputs) 签名假设了 Chain 的字典输入输出模式,无法适用于接受消息列表的 Chat Model。
  2. 不支持工具调用:源码注释中明确指出 BaseChatMemory "does NOT support native tool calling capabilities for chat models and will fail SILENTLY"。
  3. 隐式的状态管理:Memory 在 prep_inputs/prep_outputs 中被隐式调用,调试困难。
  4. 缺乏流式支持:传统 Memory 无法与流式输出协同工作。

RunnableWithMessageHistory 通过以下方式解决了这些问题:

  • 基于 BaseChatMessageHistory,直接操作消息对象
  • 使用 Runnable 监听器(on_end)保存上下文,与流式兼容
  • 通过 RunnableConfig 管理 session,显式而透明
  • 支持任意输入输出格式的自动适配

13.10.3 LangGraph 的进一步演进

在 LangChain 的最新生态中,LangGraph 提供了更加强大的状态管理能力。LangGraph 的 StateGraph 可以管理任意复杂的对话状态,包括消息历史、中间变量、分支条件等。这代表了 AI 应用状态管理从"记忆注入"向"状态图"的范式转变。

13.11 设计模式总结

13.11.1 策略模式

不同的记忆策略(Buffer/Summary/TokenBuffer/Entity/VectorStore)是策略模式的典型应用。它们都实现了 BaseMemory 接口,可以互换使用而不影响 Chain 的逻辑。

13.11.2 存储分离

记忆策略与存储后端的分离是一个优秀的设计。BaseChatMemory 通过 chat_memory: BaseChatMessageHistory 属性将消息存储委托出去。ConversationEntityMemory 通过 entity_store: BaseEntityStore 将实体存储委托出去。这使得同一种策略可以搭配不同的存储后端。

flowchart TD
    subgraph 记忆策略层
        BM[ConversationBufferMemory]
        SM[ConversationSummaryMemory]
        TBM[ConversationTokenBufferMemory]
        EM[ConversationEntityMemory]
    end

    subgraph 存储抽象层
        CMH[BaseChatMessageHistory]
        ES[BaseEntityStore]
    end

    subgraph 存储实现层
        INMEM[InMemory]
        REDIS[Redis]
        PG[PostgreSQL]
        SQLITE[SQLite]
    end

    BM --> CMH
    SM --> CMH
    TBM --> CMH
    EM --> CMH
    EM --> ES

    CMH --> INMEM
    CMH --> REDIS
    CMH --> PG
    ES --> INMEM
    ES --> REDIS
    ES --> SQLITE

13.11.3 渐进式上下文管理

五种记忆策略代表了上下文管理的五个层次:

  1. 完整保留(Buffer):最简单,适合短对话
  2. 按量裁剪(TokenBuffer):控制成本,保留最近的上下文
  3. 摘要压缩(Summary):用 LLM 压缩历史,保留全局理解
  4. 实体提取(Entity):结构化知识管理,适合需要跟踪多个话题的场景
  5. 语义检索(VectorStore):无限容量的长期记忆,按相关性召回

13.12 小结

LangChain 的记忆系统展现了 AI 应用中会话状态管理的完整图景。BaseMemory 通过三个抽象方法(load_memory_variablessave_contextclear)定义了清晰的记忆契约,与 Chain 基类的 prep_inputs/prep_outputs 形成了无缝的集成。

五种具体的记忆实现覆盖了从简单到复杂的全部需求:ConversationBufferMemory 适合短对话,ConversationTokenBufferMemory 精确控制 token 预算,ConversationSummaryMemory 通过 LLM 实现了信息压缩,ConversationEntityMemory 维护了结构化的实体知识库,VectorStoreRetrieverMemory 实现了基于语义相似度的长期记忆检索。

存储层通过 BaseChatMessageHistoryBaseEntityStore 两个抽象实现了策略与存储的分离,支持从内存到 Redis、PostgreSQL、DynamoDB 等多种后端。

RunnableWithMessageHistory 代表了现代的会话管理方式,它基于 LCEL 的 Runnable 协议,通过配置化的 session 管理和自动的消息加载/保存,提供了比传统 Memory 更灵活、更透明的解决方案。尽管传统 Memory 类已被标记为弃用,但它们所体现的设计模式 -- 策略选择、存储分离、渐进式上下文管理 -- 在现代方案中依然适用。