第13章 记忆与会话管理
本书章节导航
- 前言
- 第1章 为什么需要理解 LangChain
- 第2章 架构总览
- 第3章 Runnable 与 LCEL 表达式语言
- 第4章 消息系统与多模态
- 第5章 语言模型抽象层
- 第6章 提示词模板引擎
- 第7章 输出解析与结构化输出
- 第8章 工具系统
- 第9章 文档加载与文本分割
- 第10章 向量存储与检索器
- 第11章 Chain 组合模式
- 第12章 回调与可观测性
- 第13章 记忆与会话管理 (当前)
- 第14章 Agent 架构与执行循环
- 第15章 工具调用与 Agent 模式
- 第16章 序列化与配置系统
- 第17章 Partner 集成架构
- 第18章 设计模式与架构决策
引言
大语言模型本身是无状态的 -- 每次调用都是独立的,模型不会"记住"之前的对话内容。要构建一个能够进行连贯对话的 AI 应用,开发者必须在应用层实现记忆机制,将历史上下文注入到每次调用的输入中。
LangChain 的记忆系统经历了两代设计。第一代以 BaseMemory 为基础,通过 ConversationBufferMemory、ConversationSummaryMemory 等类提供了丰富的记忆策略,与 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_inputs 和 prep_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:对话记忆的中间层
BaseChatMemory 在 BaseMemory 之上增加了 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 的关键设计包括:
- 自动推断输入输出键:当
input_key或output_key未设置时,会自动推断。如果有多个输出键但包含"output",会发出警告并使用"output"。 - return_messages 开关:控制
load_memory_variables返回的是消息对象列表还是格式化的字符串。Chat Model 通常需要消息列表,而 Text LLM 需要字符串。 - 委托给 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_prefix 和 ai_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 = 新摘要
关键设计点:
- 增量摘要:每次只将最新的两条消息(一轮对话)与现有摘要合并,而非重新摘要全部历史。这使得摘要成本为 O(1)(每轮固定成本)而非 O(n)。
- SummarizerMixin:摘要逻辑被提取为 Mixin,可以被其他记忆类复用(如
ConversationSummaryBufferMemory)。 - 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 | 内存字典 | 简单快速,进程重启后丢失 |
RedisEntityStore | Redis | 支持 TTL,适合分布式 |
UpstashRedisEntityStore | Upstash Redis | 无服务器 Redis |
SQLiteEntityStore | SQLite 文件 | 单机持久化 |
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
这种记忆策略的独特优势在于:
- 无上下文窗口限制:向量数据库可以存储无限量的对话历史
- 语义相关性:检索到的不是最近的对话,而是与当前话题最相关的对话
- 长期记忆:即使是很久之前的对话,只要语义相关就能被召回
注意 _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_message 和 add_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 | 现代替代方案 |
|---|---|
ConversationBufferMemory | RunnableWithMessageHistory + InMemoryChatMessageHistory |
ConversationSummaryMemory | RunnableWithMessageHistory + 自定义摘要逻辑 |
ConversationTokenBufferMemory | RunnableWithMessageHistory + 消息裁剪 Runnable |
ConversationEntityMemory | 自定义 Runnable 实现实体提取 |
VectorStoreRetrieverMemory | 将向量检索作为 Runnable 步骤集成 |
13.10.2 设计决策:为何弃用传统 Memory
传统 Memory 系统存在几个根本性的限制:
- 与 Chain 耦合:
BaseMemory的save_context(inputs, outputs)签名假设了 Chain 的字典输入输出模式,无法适用于接受消息列表的 Chat Model。 - 不支持工具调用:源码注释中明确指出
BaseChatMemory"does NOT support native tool calling capabilities for chat models and will fail SILENTLY"。 - 隐式的状态管理:Memory 在
prep_inputs/prep_outputs中被隐式调用,调试困难。 - 缺乏流式支持:传统 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 渐进式上下文管理
五种记忆策略代表了上下文管理的五个层次:
- 完整保留(Buffer):最简单,适合短对话
- 按量裁剪(TokenBuffer):控制成本,保留最近的上下文
- 摘要压缩(Summary):用 LLM 压缩历史,保留全局理解
- 实体提取(Entity):结构化知识管理,适合需要跟踪多个话题的场景
- 语义检索(VectorStore):无限容量的长期记忆,按相关性召回
13.12 小结
LangChain 的记忆系统展现了 AI 应用中会话状态管理的完整图景。BaseMemory 通过三个抽象方法(load_memory_variables、save_context、clear)定义了清晰的记忆契约,与 Chain 基类的 prep_inputs/prep_outputs 形成了无缝的集成。
五种具体的记忆实现覆盖了从简单到复杂的全部需求:ConversationBufferMemory 适合短对话,ConversationTokenBufferMemory 精确控制 token 预算,ConversationSummaryMemory 通过 LLM 实现了信息压缩,ConversationEntityMemory 维护了结构化的实体知识库,VectorStoreRetrieverMemory 实现了基于语义相似度的长期记忆检索。
存储层通过 BaseChatMessageHistory 和 BaseEntityStore 两个抽象实现了策略与存储的分离,支持从内存到 Redis、PostgreSQL、DynamoDB 等多种后端。
RunnableWithMessageHistory 代表了现代的会话管理方式,它基于 LCEL 的 Runnable 协议,通过配置化的 session 管理和自动的消息加载/保存,提供了比传统 Memory 更灵活、更透明的解决方案。尽管传统 Memory 类已被标记为弃用,但它们所体现的设计模式 -- 策略选择、存储分离、渐进式上下文管理 -- 在现代方案中依然适用。