无状态 LLM 为什么需要外部记忆
了解每次 API 调用为何都从头开始,朴素地填充上下文如何导致令牌数量急剧膨胀,以及从简单到复杂的记忆策略设计空间。
无状态 LLM 为什么需要外部记忆 是 CoddyKit 上的免费 AI Engineering Academy 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 AI Engineering Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 AI Engineering Academy 课程共包含 4 节课。
LLM 默认没有记忆
对 LLM 的每次 API 调用都完全独立。模型只会接收并处理您在这一次请求中发送的消息,仅此而已。当您发起下一次调用时,模型不会记得之前的对话。这就像您在和一个没有短期记忆的人交谈。这种无状态性是有意设计的:它让模型更容易扩展和部署,但也将记忆负担转移到了您的应用上。
实践中的失忆问题
没有记忆时,聊天机器人无法完成基本的多轮任务。如果用户在第 1 轮说“我的名字是 Alice”,然后在第 3 轮问“我的名字是什么?”,模型会回答它不知道。每一轮看起来都像一段全新的对话。用户会对此感到非常沮丧。解决方案是让您的应用维护对话历史,并在每次 API 调用中包含这些历史记录。
# Naive stateless approach — model forgets everything
from openai import OpenAI
client = OpenAI()
def chat_stateless(user_message: str) -> str:
response = client.chat.completions.create(
model='gpt-4o-mini',
messages=[ # Only the new message — no history!
{'role': 'user', 'content': user_message}
]
)
return response.choices[0].message.content
chat_stateless('My name is Alice.') # Model: 'Hello Alice!'
chat_stateless('What is my name?') # Model: 'I do not know your name.'简单但不理想的做法:填入全部历史记录
最简单的记忆方式是将每一轮对话追加到不断增长的列表中,并在每次请求时发送整个列表。这种方式确实可行,但存在一个严重缺陷:上下文窗口的大小是有限的。128K 个令牌的窗口听起来很大,但包含代码片段的长篇客户支持聊天可能在几分钟内就耗尽它。发送全部历史记录还意味着要反复为相同的令牌付费。
messages = [] # grows with every turn
def chat_with_full_history(user_message: str) -> str:
messages.append({'role': 'user', 'content': user_message})
response = client.chat.completions.create(
model='gpt-4o-mini',
messages=messages # all history every time
)
reply = response.choices[0].message.content
messages.append({'role': 'assistant', 'content': reply})
return reply
# Works at first, but messages list grows unboundedly
# After 100 turns: could be 50,000+ tokens per request记忆策略的设计空间
记忆策略存在一个连续范围,每种策略都会在上下文保真度(能够记住多少历史记录)和令牌成本(每次请求使用多少令牌)之间进行不同取舍。从简单到复杂,这些策略依次包括:完整缓冲记忆、滑动窗口记忆、摘要记忆、实体记忆,以及基于向量的情景记忆。正确的选择取决于对话长度和预算。
对话历史的令牌成本
每次 API 调用的令牌成本取决于所有消息的总长度,包括输入(您的消息数组)和输出(模型的响应)。如果您在每次请求中都包含完整历史记录,令牌成本会随着对话长度呈平方增长:第 N 轮会发送之前的 N 条消息。一段每轮 200 个令牌、共 50 轮的对话,总共会发送 200+400+600+...+10,000 个令牌,输入令牌总数超过 250,000。
import tiktoken
def estimate_conversation_cost(
turns: int,
tokens_per_turn: int,
price_per_1k_input: float = 0.00015 # gpt-4o-mini
) -> float:
total_input_tokens = sum(
(i + 1) * tokens_per_turn # each turn sends all previous turns
for i in range(turns)
)
total_output_tokens = turns * tokens_per_turn
cost = (total_input_tokens / 1000) * price_per_1k_input
print(f'{turns} turns: {total_input_tokens:,} input tokens, ${cost:.4f}')
return cost
estimate_conversation_cost(50, 200)将对话历史存储在哪里
对话历史需要存储在Python 进程之外,这样才能在服务器重启后保留数据,并支持跨多个实例进行扩展。常见的存储后端包括:用于快速内存访问并支持 TTL 过期的 Redis(非常适合活跃会话)、用于持久化长期存储和分析的 PostgreSQL,以及用于无服务器自动扩展的 DynamoDB。LangChain 开箱即用地为这些后端提供连接器。
# Three storage options for conversation history
# 1. In-process dict (development only — lost on restart)
from langchain_core.chat_history import InMemoryChatMessageHistory
# 2. Redis (production — fast, TTL-based expiry)
from langchain_community.chat_message_histories import RedisChatMessageHistory
history = RedisChatMessageHistory(session_id='user-123', url='redis://localhost:6379')
# 3. PostgreSQL (production — durable, queryable)
from langchain_community.chat_message_histories import PostgresChatMessageHistory
history = PostgresChatMessageHistory(
session_id='user-123',
connection_string='postgresql://user:pass@localhost/db'
)会话 ID:区分不同用户对话
当您的应用服务于多个用户时,需要为每个对话维护独立的历史记录。session_id(通常是 UUID,或用户 ID 与对话 ID 的组合)用于标识每个请求应加载哪段历史记录。LangChain 的 RunnableWithMessageHistory 接受一个 get_session_history 函数,该函数接收会话 ID,并返回相应的历史记录对象。
import uuid
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.chat_history import InMemoryChatMessageHistory
store = {} # session_id -> history (use Redis in production)
def get_session_history(session_id: str) -> InMemoryChatMessageHistory:
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
# Create a new session for each user conversation
def new_session() -> str:
return str(uuid.uuid4())
alice_session = new_session()
bob_session = new_session()
# Alice and Bob's histories are completely independentRunnableWithMessageHistory 包装器
RunnableWithMessageHistory 是 LangChain 原生支持 LCEL 的记忆添加方式。它会包装您的链,在每次调用前自动加载历史记录,追加新的用户消息和 AI 响应,并将所有内容保存回存储中。您需要指定哪个输入键包含用户消息,以及哪个提示变量应接收历史记录。
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_messages([
('system', 'You are a helpful assistant.'),
MessagesPlaceholder(variable_name='history'), # history injected here
('human', '{input}'),
])
chain = prompt | ChatOpenAI(model='gpt-4o-mini') | StrOutputParser()
chain_with_memory = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key='input',
history_messages_key='history'
)调用启用记忆的链
调用使用 RunnableWithMessageHistory 封装的链时,需要传入一个包含 configurable: {session_id: ...} 的 config 字典。这会告诉封装器要加载哪个历史记录存储。链会处理其余所有工作:在调用前加载历史记录,将其注入提示词,并在收到响应后保存新的一轮对话。
session_id = 'user-alice-session-1'
# First message
response1 = chain_with_memory.invoke(
{'input': 'My name is Alice and I love Python.'},
config={'configurable': {'session_id': session_id}}
)
print(response1) # 'Hello Alice! Nice to meet you.'
# Second message — model now knows Alice's name and interest
response2 = chain_with_memory.invoke(
{'input': 'What is my name and what do I love?'},
config={'configurable': {'session_id': session_id}}
)
print(response2) # 'Your name is Alice and you love Python!'应避免的记忆故障模式
记忆实现中有三个常见错误:忘记隔离会话——为所有用户重复使用同一个历史记录对象,会导致不同对话之间泄露私密数据。忽略 TTL——无限期存储历史记录会填满数据库;应为不活跃会话设置过期时间。过度信任历史记录——用户可以注入虚假记忆(“我告诉过您我是管理员”),因此应根据真实数据源验证声明,而不能只依赖对话历史。
# Mistake 1: Shared history for all users
global_history = InMemoryChatMessageHistory() # BAD!
# Fix: per-session history
store = {} # keyed by session_id
# Mistake 2: No TTL on Redis history
# BAD: history = RedisChatMessageHistory(session_id=sid, url=url)
# Good: set TTL to 24 hours
history = RedisChatMessageHistory(
session_id=session_id,
url='redis://localhost',
ttl=86400 # 24 hours in seconds
)选择记忆策略
仅在确定总上下文始终不会超出限制的短对话中使用完整缓冲记忆。对于通用聊天机器人,使用滑动窗口(保留最近 N 轮对话)。当对话没有明确终点且可能非常长时,使用摘要记忆。当用户需要从很长对话的早期内容中找回特定事实时,使用向量记忆。接下来的课程将逐一介绍这些策略。
快速检查
测试您对无状态 LLM 为什么需要外部记忆的理解。
课程回顾
本课中您学到了:LLM 在设计上是无状态的——每次应用程序接口调用只能看到您在该请求中发送的内容;简单地填入完整历史记录会使令牌成本呈二次增长,并最终达到上下文限制;而 RunnableWithMessageHistory 是 LangChain 为任意后端(Redis、PostgreSQL)添加外部记忆的简洁方式,记忆按会话 ID 进行索引。接下来我们将探索具体的记忆策略:缓冲记忆和窗口记忆。
常见问题解答
「无状态 LLM 为什么需要外部记忆」课时是免费的吗?
是的 — 「无状态 LLM 为什么需要外部记忆」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 AI Engineering Academy 课程的其余内容,请升级到 CoddyKit PRO。 AI Engineering Academy 课程共包含 4 节课。
「无状态 LLM 为什么需要外部记忆」这节课中我会学到什么?
了解每次 API 调用为何都从头开始,朴素地填充上下文如何导致令牌数量急剧膨胀,以及从简单到复杂的记忆策略设计空间。 你通过在浏览器中直接运行的动手代码来练习 AI Engineering Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 AI Engineering Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 AI Engineering Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。
「无状态 LLM 为什么需要外部记忆」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 AI Engineering Academy 课中编写并运行代码吗?
能。每节 AI Engineering Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 无状态 LLM 为什么需要外部记忆
- 缓冲区记忆与窗口记忆
- 摘要记忆与令牌感知截断
- 在 Redis 与 PostgreSQL 中持久化聊天记录