LangChain 消息与提示词模板

开篇:为什么消息是 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

  1. Token 限制(Token Limit):每个 LLM 模型(如 GPT-4、Claude、Gemini)都有一个固定的最大上下文长度(例如 8K、32K、128K Tokens)。如果对话历史太长,一次性发送给模型,就会超出限制,导致错误(如“context length exceeded”)。
  2. 成本与性能:消息越长,API 调用成本越高,响应速度越慢。对于长对话,保留所有历史并非必要,尤其是早期、不重要的内容。
  3. 关注点聚焦:模型对最近的消息更敏感(“近因效应”)。保留太多早期信息反而可能干扰模型对当前任务的判断。

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 注意事项

  1. Token 计算准确性:不同模型(如 GPT-4、Claude)的 Tokenizer 不同,你需要使用对应模型的 Tokenizer 来精确计算,否则可能导致裁剪过多或过少。
  2. 系统消息保护:如果系统消息很重要,务必设置 include_system=True 或类似参数,避免被错误删除。
  3. 消息结构完整性:裁剪后,你可能会失去一些对话上下文,导致模型无法理解当前问题的背景。因此,trim_messages 通常用于长对话,而非短对话。
  4. 不是万能的:对于非常长的对话,仅仅裁剪旧消息可能不够,你还需要考虑使用摘要(Summarization) 或向量数据库检索来保留关键信息。

6.1.6 总结

trim_messages 是一个用于管理 LLM 对话历史长度的实用工具/函数。它通过自动删除列表中较早的消息,确保总 Token 数不超过模型限制,同时尽量保留对当前对话最重要的信息(通常是最近的消息和系统提示)。

如果你是在某个具体框架(如 llama_indexchainlit)或代码中看到这个函数,欢迎告诉我具体上下文,我可以为你进一步分析。

6.2 摘要压缩:summary 字段 + 条件边 + RemoveMessage

6.2.1 核心概念拆解

  • 摘要压缩:指将较长的对话历史浓缩成一段简短的摘要(Summary),从而节省Token空间,同时保留关键信息。这是长对话系统的核心优化手段。

  • summary 字段:通常是一个字符串变量,用来存储当前对话的摘要内容。它会被持续更新,代替原始对话历史传给大模型。

  • 条件边(Conditional Edge):在状态机或图(Graph)结构中,条件边表示从当前节点到下一个节点的跳转不是固定的,而是根据某个条件判断后决定走哪条路径。例如:“如果对话长度超过阈值,走‘压缩分支’,否则走‘正常分支’”。

  • RemoveMessage:这是一个具体的操作指令,通常指删除部分或全部原始消息对象(如 HumanMessageAIMessage)。在摘要压缩后,删除这些原始消息可以释放上下文空间,只保留 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
ConversationBufferMemoryagent=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、专家系统

这个演进过程的核心趋势是:

  1. 从“请求”到“编程”:提示词从一句简单的问话,变成了一个结构化的指令程序。
  2. 从“被动”到“主动”:模型从被动回答问题,进化到主动调用工具、规划步骤。
  3. 从“内容”到“身份”:重点从“输出什么内容”转向“以什么身份和逻辑来思考”。

如果你正在研究或应用这个框架,第五代范式的关键在于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. 重难点总结:六个最容易翻车的地方

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值