开篇:为什么消息是 LangChain 的"原子单位"
在 LangChain 的世界里,一切对话都是消息列表的输入与输出。聊天模型接收 list[Message],返回一条 AIMessage;Agent 的循环本质是往消息列表里追加"工具调用"和"工具结果";提示词模板最终渲染出的也是消息列表;对话历史管理操作的还是消息列表。可以说:把"消息"学透,LangChain 就懂了一半。
本讲义两大主线:
① 消息(Messages)——类型、格式、字段、content_blocks、历史管理与优化;
② 提示词模板(Prompt Templates)——机制演进、ChatPromptTemplate、MessagesPlaceholder 与高级特性。
消息在 LangChain 体系中的位置
消息列表是贯穿"提示词模板 → 模型 → 工具循环 → 历史记忆"的通用数据结构

1.消息的类型:七种角色,各司其职
所有消息类都继承自 BaseMessage,位于 langchain_core.messages。每条消息 = 角色(role)+ 内容(content)+ 元数据。角色描述"谁在说话",标准角色有四种:user / assistant / system / tool。

1.1 四大主力消息:示例代码
from langchain_core.messages import (
HumanMessage, AIMessage, SystemMessage, ToolMessage
)
# ① 系统消息:设定模型"人设"与行为规则,通常放消息列表第一位
sys = SystemMessage(content="你是一名严谨的 Python 助手,回答必须附带可运行代码。")
# ② 用户消息:终端用户的输入
human = HumanMessage(content="帮我写一个快速排序")
# ③ AI 消息:模型回复。若模型决定调用工具,tool_calls 字段会有值
ai = AIMessage(
content="我先查看当前环境的 Python 版本",
tool_calls=[{
"name": "get_python_version", # 要调用的工具名
"args": {}, # 调用参数
"id": "call_001", # 本次调用的唯一 id
}],
)
# ④ 工具消息:工具执行结果。tool_call_id 必须对应上面的 id
tool = ToolMessage(
content="Python 3.13.12",
tool_call_id="call_001", # ← 与 AIMessage.tool_calls[0]["id"] 配对
name="get_python_version",
)
messages = [sys, human, ai, tool]
print(messages[0].type) # system
print(messages[1].type) # human
print(messages[2].tool_calls) # [{name, args, id}]
黄金配对法则:带
tool_calls的 AIMessage 与对应的 ToolMessage 必须成对出现。大多数模型要求:ToolMessage 只能出现在含 tool_calls 的 AIMessage 之后,否则会报"dangling tool call(悬空工具调用)"错误。这也是历史裁剪时最容易踩的坑(第 06 节详述)。
1.2 AIMessageChunk:流式场景的"积木"
流式输出时,模型返回的不是一条完整 AIMessage,而是一串 AIMessageChunk。每个 chunk 只带增量内容,可以用 + 运算符把碎片聚合回完整消息——这正是流式打字机效果的底层机制。
for chunk in model.stream([HumanMessage("讲个程序员冷笑话")]):
print(chunk.content, end="", flush=True) # 打字机效果
# 聚合:把所有 chunk 合并成一条完整 AIMessage
full = chunk1 + chunk2 + chunk3 + ...
print(full.content) # 完整回复文本
print(full.tool_calls) # 聚合后的工具调用
1.3 一图看懂一次"工具调用"的消息流转

2. 消息的格式:三种写法,殊途同归
同一个"用户说你好",LangChain 允许三种写法。它们最终都会被规范化为消息类实例。写法选择建议:快速原型用元组,跨厂商兼容用 OpenAI 字典,需要完整元数据(id、name、tool_calls)用消息类。
2.1 写法一:消息类(最完整,生产首选)
from langchain_core.messages import HumanMessage
msg = HumanMessage(
content="你好,请介绍一下你自己",
name="alice", # 可选:区分多说话人(群聊场景常用)
id="msg-001", # 可选:手动指定 id,历史管理时能精确增删
)
model.invoke([msg])
2.2 写法二:元组(最简洁,原型首选)
# (role, content) 二元组,role 用 "system"/"user"/"ai"/"human"/"tool" 均可
messages = [
("system", "你是一个翻译助手"),
("user", "把'人工智能'翻译成英文"),
("ai", "Artificial Intelligence"),
("user", "那'深度学习'呢?"),
]
model.invoke(messages) # 框架自动转成 System/Human/AI/HumanMessage
2.3 写法三:OpenAI 格式字典(最通用,兼容存量代码)
# OpenAI 风格 {"role": ..., "content": ...},几乎所有聊天模型都接受
messages = [
{"role": "system", "content": "你是一个天气助手"},
{"role": "user", "content": "上海天气怎么样?"},
]
model.invoke(messages)
# 三种写法还能混用,LangChain 会统一规范化:
mixed = [
{"role": "system", "content": "你是助手"}, # 字典
("user", "你好"), # 元组
HumanMessage("再问一个问题"), # 消息类
]
| 写法 | 可读性 | 能力 | 典型场景 |
|---|---|---|---|
| 消息类实例 | 中 | 完整:id、name、tool_calls、多模态内容块 | 生产代码、历史管理、工具循环 |
| (role, content) 元组 | 高 | 受限:无法附加 id / 内容块 | 原型、教学示例、few-shot 样例 |
| OpenAI 字典 | 高 | 较全:可写 role/content,多模态需 content 列表 | 兼容 OpenAI SDK 存量代码、跨框架迁移 |
版本提示:v1 中导入路径统一为
from langchain.messages import ...;旧代码里常见的
from langchain.schema import ...仍然兼容(它只是langchain_core.messages的别名),但新项目建议统一用langchain_core.messages或langchain.messages。
3. 消息对象字段说明:从 BaseMessage 到各子类
所有消息共享 BaseMessage 的基础字段;各子类再扩展自己的专属字段。官方把字段分为"标准化字段"(跨厂商一致)和"原始字段"(厂商特有),这决定了哪些字段可以放心依赖。
3.1 BaseMessage 公共字段
| 字段 | 标准化? | 类型 | 说明 |
|---|---|---|---|
| content | 原始(各厂不同) | str 或 list(内容块) | 消息内容。绝大多数情况是字符串;多模态时是内容块列表(见第 04 节) |
| type | 标准化 | str | 类型标识:"human" / "ai" / "system" / "tool" |
| id | 标准化 | str | None | 唯一标识。模型生成的消息自带 id;历史增删改全靠它 |
| name | 部分支持 | str | None | 可选,区分同角色多说话人(如多人群聊中的两个 user) |
| additional_kwargs | 原始 | dict | 厂商附加数据(如 Anthropic 的 thinking 块信息) |
| response_metadata | 原始 | dict | 响应元数据:模型名、logprobs、token 用量等 |
3.2 AIMessage 专属字段(重点)
| 字段 | 标准化? | 说明 |
|---|---|---|
| tool_calls | 标准化 ⭐ | 模型决定调用的工具列表,每项含 name / args / id。Agent 的"发动机" |
| invalid_tool_calls | 标准化 | 解析失败的 tool_calls(容错时用于诊断) |
| usage_metadata | 标准化 ⭐ | token 用量:input_tokens / output_tokens / total_tokens——成本核算与历史裁剪的依据 |
3.3 ToolMessage 专属字段
| 字段 | 说明 |
|---|---|
| tool_call_id | ⭐ 必填。对应 AIMessage.tool_calls 里的 id,模型靠它把"结果"和"请求"配对 |
| name | 产生该结果的工具名 |
| artifact | 工具执行的"附件"(如图像、大 JSON)——用于记录追踪,不会发给模型 |
3.4 动手打印一条真实消息
from langchain_core.messages import HumanMessage
ai_msg = model.invoke([HumanMessage("用一句话介绍 LangChain")])
print(ai_msg) # 看整体结构
print(ai_msg.content) # "LangChain 是一个用于构建 LLM 应用的框架…"
print(ai_msg.type) # "ai"
print(ai_msg.id) # "run-8f3a…" 自动生成
print(ai_msg.usage_metadata)
{'input_tokens': 12, 'output_tokens': 34, 'total_tokens': 46}
print(ai_msg.response_metadata.get("model_name"))
'gpt-4o-mini'
实战意义:做成本统计就遍历历史里的
usage_metadata;做对话回放/审计就记录每条消息的id;做群聊机器人就利用name。字段不是摆设,每个都有生产用途。
4. 拓展:消息属性 content 与 content_blocks
content 字段有两种形态:纯字符串(绝大多数场景)和内容块列表(多模态 / 工具调用 / 思考过程)。当你需要"一段文字 + 一张图"或解析模型的结构化输出时,就必须理解内容块。新版 LangChain 提供了 content_blocks 属性,把各家厂商的原始格式规范化为统一的内容块类型,是处理混合内容的推荐入口。

4.1 多模态输入:文本 + 图片
from langchain_core.messages import HumanMessage
# 写法 A:内容块列表(多模态标准写法)
msg = HumanMessage(content=[
{"type": "text", "text": "这张图里是什么动物?"},
{"type": "image_url", "image_url": {"url": "https://example.com/cat.jpg"}},
])
# 写法 B:图片传 base64(本地文件场景)
import base64, pathlib
img = base64.b64encode(pathlib.Path("cat.jpg").read_bytes()).decode()
msg_local = HumanMessage(content=[
{"type": "text", "text": "图中是什么?"},
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64," + img}},
])
response = model.invoke([msg]) # 需要选用支持视觉的模型
4.2 读取模型输出:content vs content_blocks
读纯文本回复用 .content(它可能是字符串,也可能是厂商自定块列表);要统一处理混合内容(如 Anthropic 的 thinking 块、工具调用块)用 .content_blocks,它总是返回标准化的块列表。
ai_msg = model_with_tools.invoke("北京今天天气怎么样?")
# ① content:最快的文本出口(简单场景)
print(ai_msg.content) # "我来查一下天气…"
# ② content_blocks:规范化的块列表(复杂场景)
for block in ai_msg.content_blocks:
print(block.type) # "text" / "tool_call" / "thinking" …
if block.type == "text":
print(block.text) # 块内文本
# ③ 工具调用还是走标准入口 tool_calls(最成熟、最常用)
for tc in ai_msg.tool_calls:
print(tc["name"], tc["args"]) # get_weather {'city': '北京'}
content:最常用的文本入口
特点:
- 类型:
str或list(内容块列表) - 绝大多数场景下是纯字符串
- 多模态/工具调用时可能是厂商自定的块列表
content_blocks:标准化内容块入口(推荐)
特点:
- 总是返回标准化的块列表
- 统一处理各家厂商的异构格式(如 Anthropic 的 thinking 块、工具调用块)
- 是处理混合内容的推荐入口
选型建议
- 日常取文本 →
.content; - 工具调用 →
.tool_calls; - 处理思考链 / 多模态输出 / 跨厂商统一解析 →
.content_blocks。三者分工明确,别混着用。
5. 对话历史管理实战:让机器人"记住"上下文
聊天模型本身无状态——每次调用都像失忆。所谓"记忆",就是把历史消息列表和新一轮输入一起发给模型。LangGraph 官方方案 = add_messages reducer(累积历史)+ checkpointer(持久化)+ thread_id(区分会话)。
5.1 最朴素的记忆:自己攒消息列表
from langchain_core.messages import HumanMessage
history = [] # 自己维护一个列表,就是最原始的"记忆"
def chat(user_input):
history.append(HumanMessage(content=user_input))
response = model.invoke(history) # 把全部历史喂给模型
history.append(response) # 把回复也记入历史
return response.content
chat("你好!我叫小明,是一名数据工程师")
chat("我叫什么名字?") # "你叫小明" —— 模型从历史里"记得"了
这段代码能跑,但重启即失忆、无并发隔离、无持久化。生产需要 LangGraph 的三件套:
5.2 官方方案:MessagesState + Checkpointer + thread_id ⭐重点
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.checkpoint.memory import MemorySaver
# MessagesState 是官方预置 State,等价于:
# class MessagesState(TypedDict):
# messages: Annotated[list[AnyMessage], add_messages]
def call_model(state: MessagesState):
response = model.invoke(state["messages"]) # 历史已自动在 state 里
return {"messages": response} # 返回值经 reducer 自动追加
builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_edge(START, "call_model")
builder.add_edge("call_model", END)
# checkpointer:每个 super-step 结束后持久化整个 state
graph = builder.compile(checkpointer=MemorySaver())
config = {"configurable": {"thread_id": "user-alice-001"}} # 会话隔离
graph.invoke({"messages": [("user", "你好!我叫小明")]}, config)
graph.invoke({"messages": [("user", "我叫什么?")]}, config)
# "你叫小明" —— 历史由 checkpointer 自动恢复
# 随时检视线程内全部消息
graph.get_state(config).values["messages"]
5.3 add_messages reducer 的三条规则(面试高频)
| 你返回的消息 | reducer 行为 | 典型用途 |
|---|---|---|
| 无 id 的新消息 | 直接追加到列表末尾 | 普通聊天轮次 |
| 有 id 且与已有消息 id 相同 | 替换该条消息(原位覆盖) | 改写上一轮回答、修正内容 |
| RemoveMessage(id=...) | 删除指定 id 的消息 | 历史瘦身、敏感内容清除 |
生产建议:MemorySaver 存在内存里,重启丢失、多进程不共享——生产请换 SqliteSaver(单机)或 PostgresSaver(服务化),代码一行即换。thread_id 的设计粒度建议"一个用户一个会话一个 id",天然实现并发隔离。
6. 对话历史优化实战:裁剪、摘要与删除
历史越长,token 越贵、延迟越高、还会撞上下文窗口上限。官方给出三种优化武器:trim_messages(滑窗裁剪)、摘要压缩(summary)、RemoveMessage(精确删除)。

6.1 trim_messages
trim_messages 通常出现在大型语言模型(LLM) 的对话管理或消息处理上下文中,尤其是在像 LangChain、LlamaIndex、AutoGen 等 AI 框架中。它的核心作用是管理对话历史或消息列表的长度,以避免超出模型的最大上下文窗口(Token 限制)。
下面我为你详细解读:
6.1.1 为什么需要 trim_messages?
- Token 限制(Token Limit):每个 LLM 模型(如 GPT-4、Claude、Gemini)都有一个固定的最大上下文长度(例如 8K、32K、128K Tokens)。如果对话历史太长,一次性发送给模型,就会超出限制,导致错误(如“context length exceeded”)。
- 成本与性能:消息越长,API 调用成本越高,响应速度越慢。对于长对话,保留所有历史并非必要,尤其是早期、不重要的内容。
- 关注点聚焦:模型对最近的消息更敏感(“近因效应”)。保留太多早期信息反而可能干扰模型对当前任务的判断。
6.1.2 trim_messages 通常做什么?
它会根据你设定的策略,自动删除或截断消息列表中的一部分内容,确保总长度(Token 数)不超过一个阈值。
常见的策略包括:
- 保留最近的消息:从消息列表末尾开始保留,删除最旧的消息。这是最常用的策略。
- 保留系统消息:系统提示(System Prompt)通常很重要,
trim_messages会确保它不被删除,只裁剪用户和助手的对话。 - 按 Token 数裁剪:精确计算每条消息的 Token 数,直到总 Token 数低于设定上限。
- 保留关键消息:更高级的策略会分析消息重要性,保留对当前对话最关键的部分(较少见,实现复杂)。
6.1.3 一个简单的例子
假设你有一个消息列表:
[系统消息:你是AI助手]
[用户:你好]
[助手:你好!有什么可以帮你?]
[用户:...长篇对话内容...]
[助手:...长篇回答...]
[用户:今天的天气怎么样?] <-- 当前问题
如果没有 trim_messages,当对话很长时,你可能会把整个历史发给模型,导致超出 Token 限制。
使用 trim_messages 后(假设保留最近5条消息)
[系统消息:你是AI助手] <-- 保留
[用户:...长篇对话内容...] <-- 被裁剪掉
[助手:...长篇回答...] <-- 被裁剪掉
[用户:今天的天气怎么样?] <-- 保留
结果: 只保留了系统消息和最近的用户问题,删除了中间的长篇对话,确保总长度可控。
6.1.4 在不同框架中的具体用法(以 LangChain 为例)
from langchain_core.messages import trim_messages
trimmed = trim_messages(
messages,
max_tokens=1000, # 裁剪后总 token 预算
strategy="last", # "last"=保最新(常用) / "first"=保最旧
token_counter=model, # 计数器:传模型最准;传 len 按消息条数
include_system=True, # SystemMessage 永远保留在开头
allow_partial=False, # True 时允许把边界消息截断一半
start_on="human", # 裁剪后首条必须是 human(Llama 3.1 等要求)
end_on=None, # 约束末条角色(可选)
)
| 参数 | 说明 | 实战要点 |
|---|---|---|
| strategy | "last" 从尾往前保留;"first" 从头保留 | 聊天场景几乎总用 "last" |
| token_counter | 模型实例 / callable / "len_char" | 传模型实例最准确(各厂分词不同) |
| include_system | 系统消息是否豁免 | 通常 True——人设不能丢 |
| start_on | 裁剪后首条角色约束 | 默认 "human";Llama 3.1 等模型强制要求 human 开头 |
| allow_partial | 允许截断边界消息 | 长文档 RAG 场景可开;聊天场景关 |
在 LangChain 中,trim_messages 是一个直接可用的函数,通常用于修剪消息列表。
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, trim_messages
from langchain_openai import ChatOpenAI
messages = [
SystemMessage(content="你是一个乐于助人的助手。"),
HumanMessage(content="你好,我想了解AI。"),
AIMessage(content="当然可以!AI是人工智能的简称..."),
HumanMessage(content="请详细解释一下。"),
AIMessage(content="好的,AI包括机器学习、深度学习等..."),
HumanMessage(content="今天天气怎么样?"),
]
# 使用 trim_messages 修剪
trimmed = trim_messages(
messages,
max_tokens=50, # 最大Token数
strategy="last", # 保留最近的消息
token_counter=ChatOpenAI(model="gpt-4o"), # 使用模型来计算Token数量
# 可选:保留系统消息
include_system=True,
)
print(trimmed)
[SystemMessage(content="你是一个乐于助人的助手。"), # 保留系统消息
HumanMessage(content="今天天气怎么样?")] # 保留最近的用户消息
裁剪的两种接入姿势
# 姿势 A:临时裁剪,不动 state(历史完整保留,可回溯审计)——官方推荐起步
def call_model(state: MessagesState):
messages = trim_messages(
state["messages"], max_tokens=1000, strategy="last",
token_counter=model, include_system=True, start_on="human",
)
return {"messages": model.invoke(messages)}
# 姿势 B:裁剪并物理删除(state 里真的没有了,省存储)
from langchain_core.messages import RemoveMessage
def call_model_hard(state: MessagesState):
trimmed = trim_messages(state["messages"], max_tokens=1000,
strategy="last", token_counter=model)
response = model.invoke(trimmed)
keep_ids = {m.id for m in trimmed} # 留下的 id 集合
removed = [RemoveMessage(id=m.id) for m in state["messages"]
if m.id not in keep_ids] # 被裁掉的 → 删除指令
return {"messages": removed + [response]} # 先删旧,再追加新
6.1.5 注意事项
- Token 计算准确性:不同模型(如 GPT-4、Claude)的 Tokenizer 不同,你需要使用对应模型的 Tokenizer 来精确计算,否则可能导致裁剪过多或过少。
- 系统消息保护:如果系统消息很重要,务必设置
include_system=True或类似参数,避免被错误删除。 - 消息结构完整性:裁剪后,你可能会失去一些对话上下文,导致模型无法理解当前问题的背景。因此,
trim_messages通常用于长对话,而非短对话。 - 不是万能的:对于非常长的对话,仅仅裁剪旧消息可能不够,你还需要考虑使用摘要(Summarization) 或向量数据库检索来保留关键信息。
6.1.6 总结
trim_messages 是一个用于管理 LLM 对话历史长度的实用工具/函数。它通过自动删除列表中较早的消息,确保总 Token 数不超过模型限制,同时尽量保留对当前对话最重要的信息(通常是最近的消息和系统提示)。
如果你是在某个具体框架(如 llama_index、chainlit)或代码中看到这个函数,欢迎告诉我具体上下文,我可以为你进一步分析。
6.2 摘要压缩:summary 字段 + 条件边 + RemoveMessage
6.2.1 核心概念拆解
-
摘要压缩:指将较长的对话历史浓缩成一段简短的摘要(Summary),从而节省Token空间,同时保留关键信息。这是长对话系统的核心优化手段。
-
summary字段:通常是一个字符串变量,用来存储当前对话的摘要内容。它会被持续更新,代替原始对话历史传给大模型。 -
条件边(Conditional Edge):在状态机或图(Graph)结构中,条件边表示从当前节点到下一个节点的跳转不是固定的,而是根据某个条件判断后决定走哪条路径。例如:“如果对话长度超过阈值,走‘压缩分支’,否则走‘正常分支’”。
-
RemoveMessage:这是一个具体的操作指令,通常指删除部分或全部原始消息对象(如
HumanMessage、AIMessage)。在摘要压缩后,删除这些原始消息可以释放上下文空间,只保留summary字段。
步骤1:判断是否需要压缩
- 条件边检查当前对话的Token数量或消息条数。
- 条件1:Token数 < 阈值(如4000) → 走 正常回答路径,保留原始消息。
- 条件2:Token数 ≥ 阈值 → 走 压缩路径。
步骤2:执行压缩(进入压缩节点)
- 调用大模型,基于原有的
summary(如果有)和最近一轮的对话内容,生成新的、更长的summary。 - 例如:输入
[旧摘要 + 用户问题 + 助手回答]→ 输出新的摘要。
步骤3:清理历史(RemoveMessage操作)
- 执行
RemoveMessage:从上下文列表中删除所有原始消息(或只保留最近几轮的关键消息)。 - 只保留
summary字段(以及极少数必要的消息,比如系统提示)。
步骤4:后续对话
- 后续的对话,大模型只能看到
summary+ 最新的用户输入,而不是整段历史。
6.2.2 为什么要这样设计?(核心优点)
-
突破Token限制:LLM有固定的上下文窗口(如128K、200K),但长对话会快速耗尽。摘要压缩让对话可以无限持续。
- 保留核心信息:相比直接截断(只保留最近N轮),摘要压缩能保留早期的关键信息(如用户偏好、已确认的约定)。
- 提高回答质量:因为上下文变短了,模型注意力更集中,回答更准确,不容易被大量无关历史干扰。
- 降低成本:输入Token数量减少,直接降低API调用费用(尤其是对于Token计费的模型)。
6.2.3 实际应用场景举例
场景:一个长期陪伴的客服/个人助理机器人
- 用户:“帮我订一张明天去北京的机票。”
- 助手:“好的,需要您提供身份证号。”
- 用户:“我的身份证号是 123456。”
- ...很多轮对话后...
- 此时上下文已经很长,触发条件边。
- 模型:
- 生成摘要:
用户要求订明天去北京的机票,已提供身份证号123456。 - 删除原始消息(RemoveMessage)。
- 只保留这个摘要。
- 生成摘要:
- 用户继续问:“我的航班是否确认了?”
- 模型(基于摘要):
根据之前的记录,您已要求订明天去北京的机票并提供了身份证号,但尚未确认具体航班,请问您有偏好的航空公司或时间吗?—— 它没有丢失早期信息。
# 状态机中的条件边
def should_compress(state):
if len(state["messages"]) > 10 or total_tokens(state["messages"]) > 4000:
return "compress"
else:
return "normal_reply"
# 压缩节点中的逻辑
def compress_node(state):
# 1. 生成新的摘要
new_summary = llm.invoke(f"压缩以下对话为摘要:{state['summary']}\n新轮次:{state['messages'][-2:]}")
# 2. 更新summary字段
state["summary"] = new_summary
# 3. 执行RemoveMessage
state["messages"] = [] # 或保留最近1轮
return state
总结一句话:
“摘要压缩:summary 字段 + 条件边 + RemoveMessage” 是一种智能对话记忆管理策略:通过条件边判断何时需要压缩,用 summary 字段保存压缩后的核心信息,再用 RemoveMessage 删除原始冗长消息,从而让模型在有限上下文内高效运行。
from typing import Annotated, TypedDict
from langchain_core.messages import HumanMessage, SystemMessage, RemoveMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
# ① State:消息之外,多一个 summary 字段
class State(TypedDict):
messages: Annotated[list, add_messages]
summary: str
# ② 阈值判断:超过 6 条就去压缩
def should_continue(state: State):
return "summarize_conversation" if len(state["messages"]) > 6 else END
# ③ 摘要节点:生成/递增摘要 + 只保留最近 2 条
def summarize_conversation(state: State):
summary = state.get("summary", "")
if summary: # 已有摘要 → 让模型"扩写"它(增量式,不丢旧摘要)
instruction = ("已有对话摘要:" + summary +
"\n请结合以上新消息,扩展并更新摘要:")
else:
instruction = "请总结以上对话的关键信息:"
response = model.invoke(state["messages"] + [HumanMessage(instruction)])
delete_messages = [RemoveMessage(id=m.id) for m in state["messages"][:-2]]
return {"summary": response.content, "messages": delete_messages}
# ④ 普通回复节点:把摘要注入 system prompt,让模型"带着记忆"
def call_model(state: State):
summary = state.get("summary", "")
if summary:
sys = SystemMessage(content="此前对话摘要:" + summary)
msgs = [sys] + state["messages"]
else:
msgs = state["messages"]
return {"messages": model.invoke(msgs)}
# ⑤ 组装:conversation → (超阈值?) → summarize
wf = StateGraph(State)
wf.add_node("conversation", call_model)
wf.add_node(summarize_conversation)
wf.add_edge(START, "conversation")
wf.add_conditional_edges("conversation", should_continue)
wf.add_edge("summarize_conversation", END)
graph = wf.compile(checkpointer=MemorySaver())
6.3 RemoveMessage
RemoveMessage 是 LangChain 中用于精确删除消息历史的核心工具,主要用于对话历史管理中的物理删除操作
6.3.1 什么是 RemoveMessage?
RemoveMessage 是一个消息移除指令——它不是一条对话消息,而是一个"删除标记"。当它被返回给 add_messages reducer 时,reducer 会找到对应 id 的消息并从列表中彻底删除。
核心定位: 对话历史优化三大武器之一(与 trim_messages 和摘要压缩并列)
6.3.2 使用方法
1. 基本用法:删除指定消息
from langchain_core.messages import RemoveMessage
# 删除一条指定 id 的消息
removed = RemoveMessage(id="msg-001")
2.批量删除:配合列表推导
# 删除所有旧消息,只保留最近 2 条
delete_messages = [RemoveMessage(id=m.id) for m in state["messages"][:-2]]
return {"messages": delete_messages}
3.清空整个会话
from langgraph.graph.message import REMOVE_ALL_MESSAGES
def reset(state):
return {"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES)]}
6.3.3 add_messages reducer 的三条规则
| 你返回的消息 | reducer 行为 | 典型用途 |
|---|---|---|
| 无 id 的新消息 | 直接追加到列表末尾 | 普通聊天轮次 |
| 有 id 且与已有消息 id 相同 | 替换该条消息(原位覆盖) | 改写上一轮回答 |
| RemoveMessage(id=...) | 删除指定 id 的消息 | 历史瘦身、敏感内容清除 |
6.3.4 实战场景:摘要压缩中的应用
def summarize(state: State):
# 生成新摘要...
resp = model.invoke(state["messages"] + [HumanMessage(instruction)])
# 删除所有旧消息,只保留最近 2 条
delete = [RemoveMessage(id=m.id) for m in state["messages"][:-2]]
return {"summary": resp.content, "messages": delete}
6.3.5 三种历史优化策略对比
| 策略 | 成本 | 记忆完整性 | 实现复杂度 |
|---|---|---|---|
| trim_messages(滑窗裁剪) | 低 | 保留最近N条 | 低 |
| 摘要压缩 + RemoveMessage | 中 | 高(保留全部语义) | 中 |
| RemoveMessage(精确删除) | 低 | 由你控制 | 低 |
6.3.6 避坑指南(三大注意事项)
1.消息必须有 id
# 模型生成的消息自带 id,但手动构造时需指定
HumanMessage(content="你好", id="msg-001") # ✅ 正确
HumanMessage(content="你好") # ❌ 无 id,无法被 RemoveMessage 定位
2. tool_calls 配对必须同进同退
# 带 tool_calls 的 AIMessage 与对应的 ToolMessage 必须成对删除
# 只删一个会导致 "dangling tool call" 错误
3.物理删除不可恢复
# 重要业务建议先落库再删
# 生产环境可以考虑:先存档到数据库,再 RemoveMessage
6.3.7 与其他策略的协作
RemoveMessage 通常与 trim_messages 搭配使用,有两种接入姿势:
姿势 A:临时裁剪(不删 state,可回溯审计)
def call_model(state):
messages = trim_messages(state["messages"], max_tokens=1000, ...)
return {"messages": model.invoke(messages)} # state 里的历史还在
姿势 B:裁剪并物理删除(省存储)
def call_model_hard(state):
trimmed = trim_messages(state["messages"], max_tokens=1000, ...)
response = model.invoke(trimmed)
keep_ids = {m.id for m in trimmed}
removed = [RemoveMessage(id=m.id) for m in state["messages"] if m.id not in keep_ids]
return {"messages": removed + [response]} # 先删旧,再追加新
总结
RemoveMessage 是 LangChain 中精确删除消息历史的标准化工具,通过 id 定位目标消息,与 add_messages reducer 配合实现物理删除。它是实现对话历史优化(摘要压缩、敏感内容清除、会话重置)的关键组件,使用时需注意消息必须有 id、tool_calls 配对同进同退。
7. 综合实战:一个完整的多轮对话聊天机器人
把前六节串起来:消息类 + 历史管理 + 摘要压缩 + 提示词模板,做一个带持久记忆、自动压缩历史、多会话隔离的聊天机器人。这几十行代码就是生产级 chatbot 的骨架。

from typing import Annotated, TypedDict
from langchain_core.messages import HumanMessage, SystemMessage, RemoveMessage, trim_messages
from langchain_core.prompts import ChatPromptTemplate
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import MemorySaver
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-4o-mini") # 按需换成任意厂商
# ── ① 用 ChatPromptTemplate 固定"人设"(而不是每次手拼字符串)──
persona_prompt = ChatPromptTemplate.from_messages([
("system",
"你是智能助手小灵。当前时间:{now}。"
"回答风格:简洁、中文、必要时给例子。"),
("placeholder", "{history}"), # 历史消息插入点
("human", "{input}"), # 本轮新输入
])
class State(TypedDict):
messages: Annotated[list, add_messages]
summary: str
# ── ② 回复节点:历史裁剪 + 摘要注入 ──
def call_model(state: State):
history = trim_messages( # 上下文瘦身(姿势 A:不动 state)
state["messages"][:-1], # 历史不含本轮新输入
max_tokens=3000, strategy="last",
token_counter=model, include_system=True, start_on="human",
)
summary = state.get("summary", "")
if summary:
history = [SystemMessage("此前对话摘要:" + summary)] + history
user_text = state["messages"][-1].content
msgs = persona_prompt.invoke({"now": "2026-08-28",
"history": history, "input": user_text})
return {"messages": model.invoke(msgs)}
# ── ③ 摘要节点:超 8 条消息就压缩,保留最近 2 条 ──
def maybe_summarize(state: State):
return "summarize" if len(state["messages"]) > 8 else END
def summarize(state: State):
old = state.get("summary", "")
ask = ("已有摘要:" + old + "\n请结合新消息更新摘要:" if old
else "请总结以上对话要点:")
resp = model.invoke(state["messages"] + [HumanMessage(ask)])
delete = [RemoveMessage(id=m.id) for m in state["messages"][:-2]]
return {"summary": resp.content, "messages": delete}
# ── ④ 组装并编译 ──
g = StateGraph(State)
g.add_node("call_model", call_model)
g.add_node("summarize", summarize)
g.add_edge(START, "call_model")
g.add_conditional_edges("call_model", maybe_summarize)
g.add_edge("summarize", END)
bot = g.compile(checkpointer=MemorySaver())
# ── ⑤ 跑起来:一个 thread_id 一个会话 ──
cfg = {"configurable": {"thread_id": "demo-001"}}
for text in ["你好!我叫小明,做数据分析的", "我叫什么?做什么工作?"]:
out = bot.invoke({"messages": [("user", text)]}, cfg)
print("小灵:", out["messages"][-1].content)
小灵: 你好!记住啦,小明,数据分析方向
小灵: 你叫小明,是一名数据分析师。
这个骨架的可扩展点:换 PostgresSaver 即持久化上线;
这个骨架的可扩展点:换 PostgresSaver 即持久化上线;把 model 绑上 tools 就升级为 Agent;把 persona 换成 create_agent 的 system_prompt 参数即接入 v1 新 API。消息与模板学扎实后,向上的每一步都只是"组装"。
8.提示词模板:从字符串拼接走向工程化
Prompt Template = 带变量的提示词"函数"。输入一个字典(变量名 → 值),输出 PromptValue(既能当字符串用,也能当消息列表用)。它解决三个工程问题:复用(一处定义处处调用)、解耦(提示词与代码分离)、组合(可串进 LCEL 管道)。
8.1 String PromptTemplate:单字符串模板
from langchain_core.prompts import PromptTemplate
# 变量用 {var} 占位(默认 f-string 语法)
prompt = PromptTemplate.from_template("讲一个关于{topic}的笑话")
value = prompt.invoke({"topic": "程序员"})
print(value.to_string()) # "讲一个关于程序员的笑话"
# 直接接进 LCEL 管道:模板 → 模型
chain = prompt | model
chain.invoke({"topic": "程序员"}) # 输出 AIMessage
8.2 为什么不直接用 f-string?
| 维度 | f-string / 字符串拼接 | PromptTemplate |
|---|---|---|
| 变量校验 | 拼错变量名运行时才炸 | 自动推断 input_variables,缺参即报错 |
| 输出形态 | 只有字符串 | PromptValue:可 to_string() 也可 to_messages() |
| 管道组合 | 无法直接接 | 管道 | 标准 Runnable,可 | model、| parser |
| 生态互通 | 无 | LangSmith 追踪、LangHub 共享、序列化 |
9.提示词机制演进:五代范式
提示词写法的演进史,就是 LLM 应用工程化的缩影。理解这条时间线,你就能一眼识别任何教程/代码的新旧(这是学 LangChain 最重要的元技能之一)。

| 你看到的代码特征 | 属于哪个时代 | 建议 |
|---|---|---|
LLMChain(prompt=..., llm=...)、prompt.format_prompt() | 旧 0.0 链式时代 | 过时 改用 prompt | model |
ConversationBufferMemory、agent=AgentType.ZERO_SHOT… | 旧 AgentExecutor 时代 | 已弃用 改用 LangGraph / create_agent |
ChatPromptTemplate.from_messages([...]) + MessagesPlaceholder | 现代 LCEL | 主流范式,继续用 |
create_agent(model, tools, system_prompt=...) | v1 Agent 时代 | 当前官方推荐 |
这是一个非常专业且深刻的话题。“提示词机制演进:五代范式”通常指的是大语言模型(LLM) 时代,人类如何通过设计输入指令来引导模型行为的技术演变过程。
目前业界公认的“五代范式”主要划分如下,我为你梳理了核心逻辑、代表方法和典型示例:
第一代范式:原始提示(Zero-Shot Prompting)
- 核心逻辑:“直接问”。完全不给出示例,仅靠模型自身的预训练知识来回答问题。
- 特点:简单、最自然,但对复杂任务(如逻辑推理、格式转换)的准确率较低。
- 典型示例:
用户:请翻译“Hello”为中文。
模型:你好。
第二代范式:少样本提示(Few-Shot Prompting)
- 核心逻辑:“给样例”。在提示词中提供几个输入-输出的示例,让模型模仿模式。
- 特点:大幅提升了特定格式(如JSON、表格)和特定任务(如情感分析)的准确性。
- 典型示例:
用户:句子:这个电影太棒了! -> 情感:正面
句子:服务态度很差。 -> 情感:负面
句子:今天天气一般。 -> 情感:
第三代范式:思维链提示(Chain-of-Thought, CoT)
- 核心逻辑:“教推理”。要求模型在给出最终答案之前,展示中间推理步骤。
- 特点:显著提升了数学、逻辑、常识推理等复杂问题的成功率。催生了“零样本CoT”(即“让我们一步步思考”)。
- 典型示例:
用户:小明有5个苹果,妈妈又给了他3个,然后他吃掉了2个。他还剩几个?让我们一步步思考。
模型:一开始有5个,加上3个是8个,吃掉2个,所以还剩6个。
第四代范式:工具增强与结构化提示(Tool-Augmented & Structured Prompting)
- 核心逻辑:“给工具 + 定格式”。模型不再孤立回答问题,而是被赋予调用外部工具(如计算器、搜索引擎、Python代码解释器)的能力,并且提示词本身结构高度模块化(如System Prompt + User Prompt + 约束条件)。
- 特点:解决了LLM的“幻觉”和“计算错误”问题,实现了可验证的交互。代表如GPT-4的Plugins、Code Interpreter。
- 典型示例:
用户:请计算2的10次方,并使用Python来计算。
模型:import math; result = math.pow(2, 10);结果是1024。
第五代范式:系统级提示与人格化(System-Level Prompting & Persona)
- 核心逻辑:“定义身份 + 自主规划”。通过复杂的System Prompt定义模型的角色设定、行为边界、知识范围、输出格式,甚至情感语气。模型具备一定的自主Agent能力,能拆解复杂任务并分步执行。
- 特点:这是目前最前沿的范式,用于构建个性鲜明的AI助手、专家系统或角色扮演。例如,“你是一个资深律师,回答问题必须基于中国民法典,且必须分点罗列法律依据”。同时也是ReAct(Reason + Act) 框架和AutoGPT等Agent的基础。
- 典型示例:
系统提示:你是一位古代侠客,性格豪爽,说话要带三分江湖气,擅长用比喻,但回答必须准确。
用户:我今天工作压力很大。
模型:哈哈,江湖路远,哪能无风?兄台今日之困,正如剑客遇上了玄铁重剑,挥之不动,却不得不挥。不妨先饮一杯,歇歇气,明日再战。
业界共识(Harrison Chase 多次演讲中的观点):提示词工程的重心正从"措辞技巧"转向"给模型什么信息、什么工具"——工具的 name/description/参数 schema 本质上就是提示词的一部分。这也是官方把 prompt、tools、memory 统一收进 create_agent 的原因。
总结与深层洞察
表格
| 范式 | 核心关键词 | 解决的问题 | 典型应用场景 |
|---|---|---|---|
| 第一代 | 零样本 | 简单问答 | 知识问答、文本生成 |
| 第二代 | 少样本 | 格式模仿 | 文本分类、实体抽取 |
| 第三代 | 思维链 | 逻辑推理 | 数学题、多步推理 |
| 第四代 | 工具调用 | 外部知识/计算 | 数据分析、实时查询 |
| 第五代 | 系统人格 | 自主性/一致性 | 角色扮演、Agent、专家系统 |
这个演进过程的核心趋势是:
- 从“请求”到“编程”:提示词从一句简单的问话,变成了一个结构化的指令程序。
- 从“被动”到“主动”:模型从被动回答问题,进化到主动调用工具、规划步骤。
- 从“内容”到“身份”:重点从“输出什么内容”转向“以什么身份和逻辑来思考”。
如果你正在研究或应用这个框架,第五代范式的关键在于System Prompt的工程化——你需要像写一份产品需求文档一样,精确地定义模型的“人格”和“行为准则”。
10. ChatPromptTemplate 的使用:现代提示词的标准答案
ChatPromptTemplate = 消息序列的模板。每条消息自带角色,消息文本里可嵌变量。它是当今 LangChain 构建提示词的默认选择——因为聊天模型的世界里,提示词从来不是"一段话",而是"一组消息"。
10.1 基本用法:from_messages 三种元素
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个翻译助手,把用户输入翻译成{language}"), # 元组
MessagesPlaceholder("history"), # 占位符:整段历史
("user", "{text}"), # 元组
"补充:保持术语一致", # 裸字符串 = ("human", ...)
])
value = prompt.invoke({
"language": "英文",
"history": [HumanMessage("先做个自我介绍")],
"text": "人工智能正在改变世界",
})
print(value) # ChatPromptValue —— 消息列表形态
print(value.to_messages()) # [System, Human, Human, Human]
print(value.to_string()) # 也可以转成纯字符串(给旧 LLM 用)
# 直接接管道,一步到位:
chain = prompt | model
chain.invoke({"language": "英文", "history": [], "text": "你好世界"})
10.2 消息模板的等价写法
# 写法 A:元组(最常用,最简洁)——role 与类名均可:human/user、ai/assistant 等价
ChatPromptTemplate.from_messages([("human", "讲个{topic}笑话")])
# 写法 B:构造函数 + 消息序列(可含预置示范轮次,few-shot 用)
ChatPromptTemplate([
("system", "你是{role}"),
("human", "你好"),
("ai", "你好呀!"), # 预置一轮示范
("human", "{question}"),
])
10.3 MessagesPlaceholder:历史消息的"插槽"
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
# 显式写法:
prompt = ChatPromptTemplate.from_messages([
("system", "你是贴心助手"),
MessagesPlaceholder("history"),
("human", "{question}"),
])
# 简写等价形式:"placeholder" 元组
prompt2 = ChatPromptTemplate.from_messages([
("system", "你是贴心助手"),
("placeholder", "{history}"),
("human", "{question}"),
])
# optional=True:没传 history 也不报错(返回空列表)——可选历史场景
prompt3 = ChatPromptTemplate.from_messages([
("system", "你是助手"),
MessagesPlaceholder("history", optional=True),
("human", "{question}"),
])
# n_messages=2:最多只取历史的前 2 条(模板级截断)
MessagesPlaceholder("history", n_messages=2)
10.4 与 create_agent 配合(v1 风格)
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
agent = create_agent(
model=init_chat_model("openai:gpt-4o-mini"),
tools=[get_weather, search_web],
system_prompt="你是中文天气助手。查天气用 get_weather,"
"查资料用 search_web。回答附数据来源。", # 提示词已内建于 Agent
)
# create_agent 内部已处理消息历史循环,你只需给 system_prompt 与工具
11.提示词模板的高级特性
11.1 部分变量 partial:把"已知项"提前填死 ⭐重点
有些变量在构建时就已知(如当前日期、取好的检索片段),有些要到调用时才有(如用户输入)。partial 让前者提前固化,调用时只传后者——少传一个参数,少一分出错面。
from datetime import datetime
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", "当前日期是 {date}。你是{role}。"),
("human", "{question}"),
])
# 方式一:值已知,构建时固化
p1 = prompt.partial(role="财务分析师")
# 方式二:值未知,传 callable,调用时惰性求值(每次都取"现在")
p2 = prompt.partial(date=lambda: datetime.now().strftime("%Y-%m-%d"))
chain = p2 | model
chain.invoke({"role": "税务顾问", "question": "个税起征点是多少?"})
# date 由 lambda 自动补上,无需每次手动传
11.2 template_format:f-string 之外还有 Jinja2 / Mustache
默认 f-string({var})即可覆盖绝大多数场景。但当提示词里本身就要出现大量花括号(如让模型输出 JSON、写代码),转义写法 {{var}} 很痛苦——换模板语法更干净。
# 痛点:f-string 里嵌 JSON 示例,大括号全要双写
"""请按此格式输出:{{"name": "...", "score": 0}},用户:{user}"""
# 方案 A:mustache —— {{ }} 是变量,大括号不再冲突
ChatPromptTemplate.from_messages(
[("human", '输出 JSON:{"name": "..."},用户:{{user}}')],
template_format="mustache",
)
# 方案 B:jinja2 —— {{ var }} 是变量,支持 if/for 控制结构
ChatPromptTemplate.from_messages(
[("human", "用户 {{ user }}{% if detail %},请详细展开{% endif %}")],
template_format="jinja2",
)
11.3 Few-shot:模板里嵌示例(含消息级示例)
# 写法 A:静态写死在模板里(消息级 few-shot,对齐"情感分析"这类任务)
prompt = ChatPromptTemplate.from_messages([
("system", "判断评论情感,只输出 正面/负面"),
("human", "物流快得惊人,包装也用心!"),
("ai", "正面"),
("human", "第三次催了还没发货,客服已读不回。"),
("ai", "负面"),
("human", "{comment}"),
])
# 写法 B:动态注入示例列表(示例可存库、可检索后替换)
prompt2 = ChatPromptTemplate.from_messages([
("system", "判断评论情感,只输出 正面/负面"),
MessagesPlaceholder("examples"),
("human", "{comment}"),
])
prompt2.invoke({
"examples": [("human", "物美价廉"), ("ai", "正面")],
"comment": "包装破损,客服踢皮球",
})
11.4 LCEL 管道组合:模板是"一等公民" ⭐重点
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough, RunnableLambda
retriever = ... # 假设已有一个向量检索器
# 经典 RAG 管道:检索 → 塞模板 → 模型 → 解析
rag_prompt = ChatPromptTemplate.from_messages([
("system", "仅根据以下资料回答。\n\n{context}"),
("human", "{question}"),
])
chain = (
{"context": retriever | RunnableLambda(lambda docs: "\n".join(d.page_content for d in docs)),
"question": RunnablePassthrough()}
| rag_prompt # ← 模板接收 dict,输出 PromptValue
| model # ← 模型接收 PromptValue,输出 AIMessage
| StrOutputParser() # ← 解析器输出纯字符串
)
chain.invoke("LangChain 的消息有哪几种类型?")
组合的本质:LCEL 中每个 Runnable 接收"上一个的输出、产出下一个要的输入"。模板吃 dict 吐 PromptValue,模型吃 PromptValue 吐 AIMessage——PromptValue 就是模板与模型之间的"通用接口",这也是它存在的根本原因。
11.5 进度自检:你掌握模板了吗?

12. 重难点总结:六个最容易翻车的地方

571

被折叠的 条评论
为什么被折叠?



