2694 字
13 分钟
Prompt编排与会话历史

Prompt编排与会话历史#

本篇承接 01_LangChain概述与核心架构 中的 PromptsMemory 模块,也衔接 02_LangChain底层原理 中的消息类型体系与 Runnable 机制。前者解决”如何组织模型输入”,后者解决”如何在多轮对话中持续带入上下文”。这两部分在真实项目里通常不是分开的,而是一条连续的输入装配链路。


1 Prompt编排#

1.1 ChatPromptTemplate 与 MessagesPlaceholder#

进入工程实践后,ChatPromptTemplate 基本是默认选择。原因不是它”更高级”,而是它天然兼容消息对象:

  • SystemMessage
  • HumanMessage
  • AIMessage
  • ToolMessage
  • MessagesPlaceholder

这意味着 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, FewShotChatMessagePromptTemplate
from langchain_core.example_selectors import SemanticSimilarityExampleSelector
from langchain_openai import OpenAIEmbeddings
from 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 ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.chat_history import InMemoryChatMessageHistory
from 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 | llm
store = {}
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)

这里有三个关键字段:

  1. get_session_history(session_id):定义如何按会话获取 history backend
  2. input_messages_key:指出当前轮用户输入所在字段
  3. 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 实现可以先记住这几类:

实现特点适用场景
InMemoryChatMessageHistorylangchain_core零依赖,进程内存存储本地学习、单进程 demo
FileChatMessageHistorylangchain_community消息落文件,便于本地调试小型工具、本地原型
SQLChatMessageHistorylangchain_community通用 SQL 持久化SQLite / MySQL / PostgreSQL 原型与中小规模服务
RedisChatMessageHistorylangchain_community高速 KV + TTL短期会话、在线聊天服务
PostgresChatMessageHistorylangchain_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 HumanMessage
from 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 中多轮对话输入链路的设计方式。


相关笔记