Prompt编排与会话历史
本篇承接 01_LangChain概述与核心架构 中的 Prompts 与 Memory 模块,也衔接 02_LangChain底层原理 中的消息类型体系与 Runnable 机制。前者解决”如何组织模型输入”,后者解决”如何在多轮对话中持续带入上下文”。这两部分在真实项目里通常不是分开的,而是一条连续的输入装配链路。
1 Prompt编排
1.1 ChatPromptTemplate 与 MessagesPlaceholder
进入工程实践后,ChatPromptTemplate 基本是默认选择。原因不是它”更高级”,而是它天然兼容消息对象:
SystemMessageHumanMessageAIMessageToolMessageMessagesPlaceholder
这意味着 Prompt 不再只是一个字符串模板,而是一个消息编排器。
# pip install langchain langchain-openai
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages([ ("system", "你是一位技术支持助手。"), MessagesPlaceholder(variable_name="history"), ("human", "{input}"),])这里的 MessagesPlaceholder 不是”把历史拼成一段文本”,而是预留一个消息列表插槽。运行时传入的仍然是结构化消息,而不是大字符串。
from langchain_core.messages import HumanMessage, AIMessage
prompt_value = prompt.invoke({ "history": [ HumanMessage(content="我在部署时遇到报错。"), AIMessage(content="请把报错信息贴出来。"), ], "input": "报错和数据库连接有关。",})核心理解MessagesPlaceholder 保留的是角色边界和消息结构。这使得后续的 Tool Calling、Tracing、History 持久化都还能继续使用消息级语义,而不是退化成纯文本拼接。
1.2 partial variables
有些 Prompt 变量在整条链里几乎不变,例如:
- 系统角色
- 输出语言
- 企业名称
- 统一的格式约束
这类变量适合做成 partial variables,提前绑定,避免每次调用都重复传入。
# pip install langchain
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([ ("system", "你是 {company} 的技术文档助手,请始终使用 {language} 回答。"), ("human", "{question}"),]).partial( company="OpenDocs", language="中文",)
messages = prompt.invoke({"question": "什么是向量数据库?"})这里的思路是:
- 稳定不变的上下文,用
.partial(...)预绑定 - 每次变化的业务输入,继续保留为运行时变量
1.3 动态 Few-Shot 与 ExampleSelector
固定 Few-Shot 示例适合教学和演示,但真实场景更常见的是:根据当前输入动态挑选示例。这时就要用 ExampleSelector。
常见选择器:
| 选择器 | 适用场景 |
|---|---|
LengthBasedExampleSelector | 控制提示词长度,避免示例过多撑爆上下文 |
SemanticSimilarityExampleSelector | 根据语义相似度挑选最相关示例 |
MaxMarginalRelevanceExampleSelector | 在相关性和多样性之间折中,避免示例高度重复 |
# pip install langchain langchain-openai langchain-community faiss-cpu
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplatefrom langchain_core.example_selectors import SemanticSimilarityExampleSelectorfrom langchain_openai import OpenAIEmbeddingsfrom langchain_community.vectorstores import FAISS
examples = [ {"question": "什么是 Python 装饰器?", "answer": "装饰器是用于包装函数的高阶函数。"}, {"question": "什么是事务隔离级别?", "answer": "事务隔离级别定义并发事务之间的可见性。"}, {"question": "什么是向量数据库?", "answer": "向量数据库用于存储和检索高维向量。"},]
selector = SemanticSimilarityExampleSelector.from_examples( examples=examples, embeddings=OpenAIEmbeddings(model="text-embedding-3-small"), vectorstore_cls=FAISS, k=2, input_keys=["question"],)
example_prompt = ChatPromptTemplate.from_messages([ ("human", "{question}"), ("ai", "{answer}"),])
few_shot_prompt = FewShotChatMessagePromptTemplate( example_selector=selector, example_prompt=example_prompt,)
final_prompt = ChatPromptTemplate.from_messages([ ("system", "你是一位资深技术讲师,请用简洁准确的方式回答。"), few_shot_prompt, ("human", "{question}"),])实战经验固定 Few-Shot 是”把所有例题都提前印在卷子上”;动态 Few-Shot 则是”根据当前题目,从题库里抽最像的两三道例题放进去”。后者更省 Token,也更贴近真实输入。
2 会话历史机制
2.1 BaseChatMessageHistory
BaseChatMessageHistory 是 LangChain 的会话历史抽象层。它不关心你把消息存在哪里,只规定最基本的行为:
- 读取消息:
messages/aget_messages() - 追加消息:
add_messages()/aadd_messages() - 清空消息:
clear()/aclear()
概念解析BaseChatMessageHistory 的定位类似 Repository Interface。上层链路只依赖”能否按会话拿回消息并追加消息”,而不直接依赖具体存储后端。
官方文档还特别强调了一点:优先使用批量 add_messages(),而不是反复调用单条 add_message() / add_user_message() / add_ai_message()。对数据库、Redis、远程 KV 来说,这能减少不必要的往返开销。
2.2 RunnableWithMessageHistory
RunnableWithMessageHistory 的作用是:给一条已有 Runnable 链补上会话历史能力。它会在调用前自动读取历史,在调用后自动回写新消息。
# pip install langchain langchain-openai
from langchain_openai import ChatOpenAIfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholderfrom langchain_core.chat_history import InMemoryChatMessageHistoryfrom langchain_core.runnables.history import RunnableWithMessageHistory
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
prompt = ChatPromptTemplate.from_messages([ ("system", "你是一个会记住上下文的助手。"), MessagesPlaceholder(variable_name="history"), ("human", "{input}"),])
chain = prompt | llmstore = {}
def get_session_history(session_id: str): if session_id not in store: store[session_id] = InMemoryChatMessageHistory() return store[session_id]
chain_with_history = RunnableWithMessageHistory( chain, get_session_history, input_messages_key="input", history_messages_key="history",)
config = {"configurable": {"session_id": "user_001"}}
resp1 = chain_with_history.invoke({"input": "我叫小明。"}, config=config)resp2 = chain_with_history.invoke({"input": "我叫什么名字?"}, config=config)这里有三个关键字段:
get_session_history(session_id):定义如何按会话获取 history backendinput_messages_key:指出当前轮用户输入所在字段history_messages_key:指出 Prompt 中承接历史消息的占位字段
2.3 session_id 与会话边界
session_id 不是 demo 里的小参数,而是上下文污染边界。
如果设计不好,常见问题包括:
- A 用户读到 B 用户的历史
- 同一用户不同线程误共享上下文
- 一个长期会话和一个临时问答串线
常见设计方式:
| 方案 | 含义 | 适用场景 |
|---|---|---|
user_id | 一个用户只有一段长期对话 | 极简个人助手 |
user_id:thread_id | 一个用户可以有多个会话线程 | 通用聊天产品 |
tenant_id:user_id:thread_id | 多租户 + 多线程 | 企业 SaaS |
易错避坑不要把 session_id 写死成 "default"。本地 demo 看不出问题,一旦接入真实流量,就会出现会话串线。
3 常见实现与选型
3.1 常见实现全景
基于官方当前文档和 API 参考,最常见的 ChatMessageHistory 实现可以先记住这几类:
| 实现 | 包 | 特点 | 适用场景 |
|---|---|---|---|
InMemoryChatMessageHistory | langchain_core | 零依赖,进程内存存储 | 本地学习、单进程 demo |
FileChatMessageHistory | langchain_community | 消息落文件,便于本地调试 | 小型工具、本地原型 |
SQLChatMessageHistory | langchain_community | 通用 SQL 持久化 | SQLite / MySQL / PostgreSQL 原型与中小规模服务 |
RedisChatMessageHistory | langchain_community | 高速 KV + TTL | 短期会话、在线聊天服务 |
PostgresChatMessageHistory | langchain_postgres | 强持久化、事务与索引能力更好 | 企业系统、长期归档、审计需求 |
版本说明旧的 langchain_community 版 PostgresChatMessageHistory 已被标记为废弃。当前官方推荐使用 langchain_postgres 包中的实现。
3.2 InMemoryChatMessageHistory
这是最简单的实现,优点也最明显:
- 不需要外部依赖
- 与
RunnableWithMessageHistory配合最直接 - 教学和 Notebook 体验很好
但它的限制同样明确:
- 进程重启即丢失
- 多实例部署无法共享
- 不适合长期恢复历史会话
因此它适合做默认教学实现,但不是生产环境的最终答案。
3.3 FileChatMessageHistory
FileChatMessageHistory 适合”比内存多一步持久化、但还不想引入数据库”的场景。
优点:
- 本地调试直观
- 容易肉眼查看和排查
- 适合命令行工具或单机原型
缺点:
- 并发能力弱
- 不适合高流量写入
- 目录结构、锁、清理策略都要自己考虑
如果你只是想在本地把消息存下来回看,它比自定义文件实现更省事。
3.4 SQLChatMessageHistory
SQLChatMessageHistory 是一个很值得补上的常见实现,因为它覆盖了很多”我想持久化,但还不想单独引入 Redis 或专门的 Postgres 包”的中间场景。
# pip install langchain-community sqlalchemy
from langchain_core.messages import HumanMessagefrom langchain_community.chat_message_histories import SQLChatMessageHistory
history = SQLChatMessageHistory( session_id="user_001", connection="sqlite:///chat_history.db",)
history.add_messages([ HumanMessage(content="你好,我想了解 LangChain。"),])
print(history.messages)它的优点:
- 使用通用 SQL 后端,迁移成本低
- 适合 SQLite 本地原型,也适合接更正式的 SQL 数据库
- 会话隔离天然适合做表结构设计
它的缺点:
- 读写性能通常不如 Redis 这种专门的 KV
- 要考虑连接管理、表结构、索引和迁移
3.5 RedisChatMessageHistory
RedisChatMessageHistory 很适合在线聊天服务里的短期会话上下文。
# pip install langchain-community redis
from langchain_community.chat_message_histories import RedisChatMessageHistory
history = RedisChatMessageHistory( session_id="user_001", url="redis://localhost:6379/0", ttl=3600,)它的优点:
- 读写快
- 天然适合按
session_id做隔离 ttl对短期对话场景很实用
它的缺点:
- 长期归档不如关系型数据库方便
- Redis key 命名、过期策略、容量规划都需要提前设计
一个很常见的工程方案是:
- Redis 存最近会话上下文
- SQL / Postgres 存长期审计与归档
3.6 PostgresChatMessageHistory
如果你已经明确需要:
- 长期保存会话
- 做审计 / 回放
- 建索引 / 做分析
- 与现有企业数据库体系集成
那 langchain_postgres 里的 PostgresChatMessageHistory 会比简单的 community 版 SQL 后端更像”正式生产方案”。
它适合:
- 企业 SaaS
- 审计要求强的业务系统
- 需要和其他业务表联动的场景
代价则是:
- 需要额外依赖与数据库维护
- 建表与连接管理更正式
- 配置成本高于 SQLite / Redis demo
选型建议会话历史没有唯一最佳后端,关键看目标:学习和原型 用 InMemory / File;短期在线上下文 倾向 Redis;长期持久化和审计 倾向 SQL / Postgres。
4 工程实践
4.1 持久化、TTL 与裁剪策略
会话历史真正进入生产后,问题不再是”能不能存”,而是:
- 存多久
- 按什么规则清理
- 是否要裁剪长消息
- 是否要对旧会话做摘要压缩
常见策略:
- 只保留最近 K 轮原始消息
- 把较旧消息做摘要
- 给 Redis key 设置 TTL
- 把长工具输出裁剪后再写入 history
4.2 RunnableWithMessageHistory 与 LangGraph State
这两个方案都能管理上下文,但关注点不同:
| 方案 | 更关注什么 | 适用场景 |
|---|---|---|
RunnableWithMessageHistory | 给一条 Runnable 链补上会话消息 | 聊天链、轻量问答、简单 Agent |
LangGraph State | 管理整张图中的全局状态 | 多步骤 Agent、审批流、复杂分支 |
优先使用 RunnableWithMessageHistory 的场景:
- 你已有一条 LCEL 链,只想让它支持多轮对话
- 你的状态几乎都围绕”消息历史”展开
- 你不需要复杂分支、断点恢复和多节点共享状态
优先使用 01_LangGraph入门 中 State 的场景:
- 还要保存工具结果、计划、审批状态、路由决策
- 需要
interrupt/ checkpoint / resume - 多个节点要读写同一份状态
4.3 常见错误
history_messages_key与 Prompt 中的MessagesPlaceholder名称不一致- 忘记传
configurable.session_id - 把
ChatMessageHistory当成长期用户画像存储
5 总结
一句话总结Prompt 编排解决”本轮输入如何组织”,会话历史解决”过去发生过什么如何持续带入”。把这两部分一起理解,才算真正掌握了 LangChain 中多轮对话输入链路的设计方式。
相关笔记