文章目录
-
- [零基础入门 LangChain 与 LangGraph(五):核心组件上篇------消息、提示词模板、少样本与输出解析](#零基础入门 LangChain 与 LangGraph(五):核心组件上篇——消息、提示词模板、少样本与输出解析)
- [一、进一步理解 LangChain 为什么叫"框架"](#一、进一步理解 LangChain 为什么叫“框架”)
-
- [1.1 调模型很简单,但把模型调用写"规范"并不简单](#1.1 调模型很简单,但把模型调用写“规范”并不简单)
- [1.2 两类组件](#1.2 两类组件)
- 二、消息(Messages):聊天模型真正读懂的,不是一段字符串,而是一串带角色的消息
-
- [2.1 为什么 LangChain 不直接只收字符串?](#2.1 为什么 LangChain 不直接只收字符串?)
- [2.2 先把最底层概念记住:角色 + 内容 + 元数据](#2.2 先把最底层概念记住:角色 + 内容 + 元数据)
- [2.3 LangChain 为什么还要再包一层自己的消息类型?](#2.3 LangChain 为什么还要再包一层自己的消息类型?)
- [2.4 LangChain 里最常见的五种消息类型](#2.4 LangChain 里最常见的五种消息类型)
- [2.5 一次最基本的消息调用,到底长什么样?](#2.5 一次最基本的消息调用,到底长什么样?)
-
- [1. `[]` 表示列表](#1.
[]表示列表) - [2. 每一项都不是普通字符串,而是消息对象](#2. 每一项都不是普通字符串,而是消息对象)
- [3. `invoke(messages)` 表示把整段消息序列送进模型](#3.
invoke(messages)表示把整段消息序列送进模型)
- [1. `[]` 表示列表](#1.
- [2.6 多轮对话的本质,不是"模型记住了你",而是"历史消息又发了一遍"](#2.6 多轮对话的本质,不是“模型记住了你”,而是“历史消息又发了一遍”)
- [2.7 `BaseMessage`:所有消息的共同父类,](#2.7
BaseMessage:所有消息的共同父类,)
- 三、历史消息为什么必须管理?因为上下文窗口永远是有限的
-
- [3.1 所谓"多轮对话",本质上是在和上下文窗口做资源分配](#3.1 所谓“多轮对话”,本质上是在和上下文窗口做资源分配)
- [3.2 `trim_messages`:先掌握最常用的一招,消息裁剪](#3.2
trim_messages:先掌握最常用的一招,消息裁剪) -
- [`max_tokens`:最多允许保留多少输入 token](#
max_tokens:最多允许保留多少输入 token) - `strategy="last"`:优先保留后面的消息
- `include_system=True`:系统消息尽量保留
- `start_on="human"`:裁剪后从用户消息开始
- [`max_tokens`:最多允许保留多少输入 token](#
- [3.3 为什么裁剪不是简单地"从前面砍掉"这么粗暴?](#3.3 为什么裁剪不是简单地“从前面砍掉”这么粗暴?)
- [3.4 除了按 token 裁剪,还可以按消息条数裁剪](#3.4 除了按 token 裁剪,还可以按消息条数裁剪)
- [3.5 `filter_messages`:不是所有消息都该送进模型](#3.5
filter_messages:不是所有消息都该送进模型) - [3.6 `merge_message_runs`:连续同类消息太多时,把它们合并掉](#3.6
merge_message_runs:连续同类消息太多时,把它们合并掉) - [3.7 我怎么看待"消息历史封装类"这件事](#3.7 我怎么看待“消息历史封装类”这件事)
- [四、提示词模板(Prompt Template):真正可维护的提示词,绝不是字符串拼接](#四、提示词模板(Prompt Template):真正可维护的提示词,绝不是字符串拼接)
-
- [4.1 为什么不建议手写整段 Prompt](#4.1 为什么不建议手写整段 Prompt)
- [4.2 `PromptTemplate`:最基础的字符串模板](#4.2
PromptTemplate:最基础的字符串模板) - [4.3 `ChatPromptTemplate`:真正适合聊天模型的模板](#4.3
ChatPromptTemplate:真正适合聊天模型的模板) - [4.4 模板一旦接上模型和解析器,链就真正闭环了](#4.4 模板一旦接上模型和解析器,链就真正闭环了)
- [4.5 `MessagesPlaceholder`:当我不想只填字符串,而是想把"消息列表"塞进模板里](#4.5
MessagesPlaceholder:当我不想只填字符串,而是想把“消息列表”塞进模板里)
- 五、少样本提示(Few-shot):不是告诉模型答案,而是教它"按这个方式回答"
-
- [5.1 为什么需要](#5.1 为什么需要)
- [5.2 最简单的 Few-shot:用例子告诉模型,新的符号应该怎么理解](#5.2 最简单的 Few-shot:用例子告诉模型,新的符号应该怎么理解)
- [5.3 `FewShotChatMessagePromptTemplate`:把示例真正变成"聊天消息"](#5.3
FewShotChatMessagePromptTemplate:把示例真正变成“聊天消息”) - [5.4 当 Few-shot 和最终问题拼在一起时,模型才真正"学会该怎么答"](#5.4 当 Few-shot 和最终问题拼在一起时,模型才真正“学会该怎么答”)
- [5.5 少样本提示特别适合哪几类任务?](#5.5 少样本提示特别适合哪几类任务?)
- [六、示例选择器(Example Selector):示例一多,就不能无脑全塞进 Prompt 里](#六、示例选择器(Example Selector):示例一多,就不能无脑全塞进 Prompt 里)
-
- [6.1 Few-shot 的下一个现实问题:样本太多怎么办?](#6.1 Few-shot 的下一个现实问题:样本太多怎么办?)
- [6.2 按长度选:`LengthBasedExampleSelector`](#6.2 按长度选:
LengthBasedExampleSelector) - [6.3 按语义选:`SemanticSimilarityExampleSelector`](#6.3 按语义选:
SemanticSimilarityExampleSelector) - [6.4 按 MMR 选:相关,还要尽量不重复](#6.4 按 MMR 选:相关,还要尽量不重复)
- [6.5 N-gram 重叠:更偏表层文本相似](#6.5 N-gram 重叠:更偏表层文本相似)
- [七、输出解析器(Output Parser):模型的回复,本来是文本;但程序真正想要的,通常不是文本](#七、输出解析器(Output Parser):模型的回复,本来是文本;但程序真正想要的,通常不是文本)
-
- [7.1 为什么输出解析器会成为核心组件,而不是附属工具?](#7.1 为什么输出解析器会成为核心组件,而不是附属工具?)
- [7.2 `StrOutputParser`:最简单,但也是最常用的一个](#7.2
StrOutputParser:最简单,但也是最常用的一个) - [7.3 `PydanticOutputParser`:当我想要的不是字符串,而是一个结构化对象](#7.3
PydanticOutputParser:当我想要的不是字符串,而是一个结构化对象) - [7.4 `get_format_instructions()` 为什么这么关键?](#7.4
get_format_instructions()为什么这么关键?) - [7.5 `JsonOutputParser`:我不一定要对象类,但我可能至少想要 JSON](#7.5
JsonOutputParser:我不一定要对象类,但我可能至少想要 JSON) - [7.6 输出解析器和 `with_structured_output()` 到底怎么选?](#7.6 输出解析器和
with_structured_output()到底怎么选?) - [7.7 理解LangChain的框架化](#7.7 理解LangChain的框架化)
- 八、本篇总结
零基础入门 LangChain 与 LangGraph(五):核心组件上篇------消息、提示词模板、少样本与输出解析
💬 开篇说明 :前面几篇已经把模型接起来了,也看到了工具调用、结构化输出、流式传输这些能力。到这一步,我们其实已经能"调用模型"了,但还远远谈不上真正理解 LangChain。因为从这一节开始,我才真正进入 LangChain 的核心地带------组件(Components)。
👍 这一篇要解决的问题:为什么 LangChain 要把一次普通的模型调用,拆成消息、提示词模板、少样本、输出解析器这些看起来很碎的部分?这些东西到底是不是在"增加复杂度"?还是说,它们其实是在帮我们把 AI 应用开发这件事工程化?
🚀 这一篇的目标:写完之后,我至少要真正吃透四件事:
- 为什么聊天模型的输入输出,本质上是"消息"而不是一段普通字符串
- 为什么提示词模板不是语法糖,而是可复用的输入接口
- 为什么少样本提示可以显著提升模型输出质量
- 为什么输出解析器能把"聊天结果"变成"程序可直接使用的数据"
这一篇我会先把 模型调用前后最核心的这半边组件体系 讲透。下一篇再进入另外半边:文档、Embedding、向量存储、检索器,以及真正把 RAG 走通。
一、进一步理解 LangChain 为什么叫"框架"
1.1 调模型很简单,但把模型调用写"规范"并不简单
如果只是临时问模型一句话,其实非常简单:
python
response = model.invoke("你好,请介绍一下你自己")
到这一步,谁都能写。
但真正一做应用,问题就来了。
因为现实里的需求很快就会变成下面这种样子:
- 既要传系统设定,也要传用户问题
- 既要保留多轮历史,也要避免上下文爆炸
- 既要让提示词可复用,又不能每次手写一大段
- 既要让模型按固定格式输出,又不能靠人工肉眼去拆字符串
- 既要支持简单问答,也要支持复杂链路里的中间结果传递
你会发现,这时候我需要的已经不是"再调用一次模型",而是:
把模型的输入、组织、约束、输出,统一抽象成一套稳定的组件体系。
1.2 两类组件
第一类:模型调用前后的"输入输出组织层"
这一类就是这篇要讲的内容,有一些之前也提过很多次了,本篇真正从零系统讲解一遍。
- Messages
- Prompt Template
- Few-shot Prompting
- Example Selectors
- Output Parsers
它们的核心问题是:
怎么把模型调用组织得更规范、更稳定、更容易复用。
第二类:外部知识接入层
这一类是下一篇的内容:
- Document
- Document Loader
- Text Splitter
- Embedding
- Vector Store
- Retriever
- RAG
它们要解决的是:
模型本身不知道的知识,我该怎么接进来。
这样一分,你就会发现所谓"核心组件"并不是散乱 API,而是在搭两层基础设施:
模型输入如何组织
消息
提示词模板
少样本示例
模型输出如何落地
输出解析器
模型外部知识如何接入
文档
切分
Embedding
向量存储
检索器
、
二、消息(Messages):聊天模型真正读懂的,不是一段字符串,而是一串带角色的消息
2.1 为什么 LangChain 不直接只收字符串?
如果我刚开始学大模型,很容易有一个朴素认知:
输入就是一句话,输出就是一句话。
这个理解不能说错,但它只适合最简单的场景。
一旦进入聊天模型,我很快就会发现:
模型其实并不是只看"文本内容",它还很在意:
- 这句话是谁说的
- 它在整段对话里处在什么位置
- 前面有没有系统约束
- 这是不是工具执行结果
- 这是不是模型的中间增量输出
所以,聊天模型真正接收的,不只是一个字符串,而是一串有角色、有顺序、有上下文关系的消息序列。
这也是为什么 LangChain 没有停留在"字符串输入"这一层,而是抽象出了一套统一消息体系。
2.2 先把最底层概念记住:角色 + 内容 + 元数据
一条消息最核心的三部分,其实就是:
- Role(角色)
- Content(内容)
- Metadata(元数据)
如果用最容易理解的方式来看:
- Role 决定"这句话是谁说的"
- Content 决定"这句话具体说了什么"
- Metadata 决定"这条消息还带了哪些附加信息"
例如 OpenAI 风格的消息列表,通常长这样:
python
[
{"role": "user", "content": "Hello, how are you?"},
{"role": "assistant", "content": "I'm doing well."},
{"role": "user", "content": "Can you tell me a joke?"}
]
这里最重要的不是 JSON 格式本身,而是:
聊天模型靠角色来理解对话结构。
也就是说,模型不是只看"字",还看"这是谁说的话"。
2.3 LangChain 为什么还要再包一层自己的消息类型?
答案其实很简单:
因为不同模型提供商的消息格式并不完全一致,LangChain 需要一层统一抽象。
所以 LangChain 在自己的体系里,把常见消息统一成了几种标准类型。
2.4 LangChain 里最常见的五种消息类型
这是我们之前已经讲过的了:
| 消息类型 | 对应角色 | 作用 |
|---|---|---|
SystemMessage |
system |
给模型设定行为规则、身份、语气、上下文 |
HumanMessage |
user |
表示用户输入 |
AIMessage |
assistant |
表示模型的完整回复 |
AIMessageChunk |
assistant |
表示模型流式输出中的"增量片段" |
ToolMessage |
tool |
表示工具执行结果返回给模型的消息 |
这五种类型里,前面三种最好理解:
- 系统消息:告诉模型"你应该怎么做"
- 用户消息:用户的问题
- AI 消息:模型的回答
后面两种则是 LangChain 真正体现"框架感"的地方:
AIMessageChunk:把流式输出也纳入统一消息体系ToolMessage:把工具执行结果也纳入统一消息体系
所以你会发现,LangChain 并不是只在封装聊天,而是在封装:
一切和模型交互有关的输入输出单元。
2.5 一次最基本的消息调用,到底长什么样?
先看一个最基础的例子:
python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
model = ChatOpenAI(model="gpt-4o-mini")
messages = [
SystemMessage(content="你是一个讲解清晰的编程老师"),
HumanMessage(content="请解释一下什么是 TCP 三次握手")
]
result = model.invoke(messages)
result.pretty_print()
如果你没有 Python 基础,可以先只看三个点:
1. [] 表示列表
这里的 messages = [...],表示我把多条消息按顺序放进一个列表里。
2. 每一项都不是普通字符串,而是消息对象
SystemMessage(...) 和 HumanMessage(...) 本质上是在创建两条不同角色的消息。
3. invoke(messages) 表示把整段消息序列送进模型
不是只送最后一句用户问题,而是把整段上下文一起发过去。
这个地方特别关键,因为它会直接影响你对多轮对话的理解。
2.6 多轮对话的本质,不是"模型记住了你",而是"历史消息又发了一遍"
模型并不是真的在后台长期记住了你,它只是每次都重新看了一遍你发过去的历史消息。
很多人第一次聊天时会有一种错觉:
"模型记住我了。"
其实从程序角度看,真实发生的事情通常像这样:
python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage
model = ChatOpenAI(model="gpt-4o-mini")
messages = [
HumanMessage(content="Hi! I'm Bob"),
AIMessage(content="Hello Bob! How can I help you today?"),
HumanMessage(content="What's my name?")
]
result = model.invoke(messages)
result.pretty_print()
模型之所以知道"你的名字是 Bob",不是因为它真的把你持久记下来了,
而是因为你把前面的对话历史又一起发给它了。
所以从这个角度说,聊天模型的"记忆"其实本质上是:
历史消息管理问题。
一旦这个认知建立起来,后面你再看:
- 上下文窗口
- 历史消息裁剪
- 消息过滤
- 持久化记忆
这些东西就都不抽象了。
2.7 BaseMessage:所有消息的共同父类,
从使用角度看,你不需要天天去研究 BaseMessage 的源码。
但从框架理解角度看,我觉得知道它的存在非常重要。
因为它说明了一个本质问题:
系统消息、用户消息、AI 消息、工具消息,本质上都属于"消息"这个统一抽象。
它们只是角色不同、用途不同,但底层都共享一些共同字段,比如:
content:消息正文additional_kwargs:额外负载数据response_metadata:响应元数据type:消息类型name:消息名称id:消息唯一标识
在实际开发里,你最常会接触的是:
contentidresponse_metadata
例如:
content用来拿文本内容response_metadata里会有 token 使用情况id有时候用于追踪消息
LangChain 对消息的设计,是面向统一编排的,而不是面向一次性聊天的。
三、历史消息为什么必须管理?因为上下文窗口永远是有限的
3.1 所谓"多轮对话",本质上是在和上下文窗口做资源分配
上一节已经说了,多轮对话的本质是把历史消息重新发给模型。
问题来了:
如果历史越来越长,会发生什么?
答案很直接:
输入会越来越大,直到碰到上下文窗口限制。
模型不是无限内存。
你每次发进去的内容,都会占用上下文空间。
而这个空间里装的不只是用户输入,还包括:
- 系统消息
- 历史对话
- 工具结果
- 最终模型输出预留空间
所以你很快就会发现,多轮对话根本不是"存一下历史就行了",而是一个典型的资源管理问题。
如果我用系统编程里熟悉的视角来类比:
- 上下文窗口就像一个固定大小的缓冲区
- 历史消息就是不断往里追加的数据
- 一旦不管理,迟早溢出
所以后面 LangChain 提供的一系列消息处理工具,本质上都在解决:
有限上下文里,哪些消息该保留,哪些该裁掉。
3.2 trim_messages:先掌握最常用的一招,消息裁剪
当历史消息太长时,最常见的处理就是裁剪。
一个典型示例大概是这样:
python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, trim_messages
model = ChatOpenAI(model="gpt-4o-mini")
messages = [
SystemMessage(content="you're a good assistant"),
HumanMessage(content="hi! I'm bob"),
AIMessage(content="hi!"),
HumanMessage(content="I like vanilla ice cream"),
AIMessage(content="nice"),
HumanMessage(content="whats 2 + 2"),
AIMessage(content="4"),
HumanMessage(content="thanks"),
AIMessage(content="no problem!"),
HumanMessage(content="having fun?"),
AIMessage(content="yes!"),
HumanMessage(content="What's my name?"),
]
trimmer = trim_messages(
max_tokens=65,
strategy="last",
token_counter=model,
include_system=True,
allow_partial=False,
start_on="human",
)
trimmed = trimmer.invoke(messages)
print(trimmed)
这段代码看起来参数很多,但你真正要理解的只有几件事。
max_tokens:最多允许保留多少输入 token
这是裁剪的硬限制。
strategy="last":优先保留后面的消息
也就是越新的消息越重要。
这很合理,因为在聊天里,通常最近几轮最相关。
include_system=True:系统消息尽量保留
系统消息通常决定模型的行为边界,所以一般不建议随便丢。
start_on="human":裁剪后从用户消息开始
这能保证消息结构更自然,不容易出现"开头直接是一段模型回复"的奇怪情况。
3.3 为什么裁剪不是简单地"从前面砍掉"这么粗暴?
因为聊天消息是有结构的。
你不能随便把一半内容切掉,否则可能会导致:
- 开头只剩一条 AI 回复,没有用户问题
- 工具结果还在,但对应的工具调用消息不在了
- 系统设定丢了,模型行为突然变化
- 一段本来有因果关系的对话被切得前后断裂
所以一个好的裁剪,不是单纯减长度,而是要尽量保留"对话结构的有效性"。
这也是为什么 LangChain 会提供一些额外约束,例如:
- 开头尽量是
HumanMessage - 结尾尽量落在用户问题或工具结果上
- 系统消息尽量保留
- 不要把工具消息孤零零留在外面
这背后其实体现了一种非常工程化的思路:
裁剪历史不是为了省空间本身,而是为了在有限空间里保住最有价值的语义结构。
3.4 除了按 token 裁剪,还可以按消息条数裁剪
有时候我不想按 token 精确控制,只想粗暴地控制:
最多保留最近 N 条消息
这时就可以把 token_counter 换成 len。
python
trimmer = trim_messages(
max_tokens=10,
strategy="last",
token_counter=len,
include_system=True,
allow_partial=False,
start_on="human",
)
这里虽然参数名还是 max_tokens,但当 token_counter=len 时,它实际上控制的是消息数量。
这种方式更简单,但也更粗糙。
适合什么场景?
- 快速实验
- 对 token 精度没那么敏感
- 历史消息本身长度比较均匀
但真到生产化一点的场景,我还是更建议按 token 来看,因为真正影响上下文窗口的是 token,而不是消息条数。
3.5 filter_messages:不是所有消息都该送进模型
有时候,我维护了一大串消息列表,但并不想把所有内容都喂给模型。
这时就需要"筛"。
例如:
python
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, filter_messages
messages = [
SystemMessage("你是一个聊天助手", id="1"),
HumanMessage("示例输入", id="2"),
AIMessage("示例输出", id="3"),
HumanMessage("真实输入", id="4"),
AIMessage("真实输出", id="5"),
]
filtered = filter_messages(messages, include_types="human")
print(filtered)
这个场景在实际开发里特别常见。
比如:
- 我只想取出用户输入做分析
- 我只想剔除某些示例消息
- 我只想根据
id去掉某段不该继续参与推理的历史
所以 filter_messages 的本质,不是一个小工具,而是一种更细粒度的消息治理能力。
3.6 merge_message_runs:连续同类消息太多时,把它们合并掉
还有一种情况也很常见:
同一类消息连续出现很多条,比如:
- 两条连续的
SystemMessage - 两条连续的
HumanMessage - 两条连续的
AIMessage
有些模型对这种格式不太友好,或者你自己就希望上下文更紧凑。
这时可以用 merge_message_runs 把连续同类消息合并成一条。
python
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, merge_message_runs
messages = [
SystemMessage("你是一个聊天助手。"),
SystemMessage("你总是以简洁风格回答。"),
HumanMessage("为什么要用 LangChain?"),
HumanMessage("为什么要用 LangGraph?"),
]
merged = merge_message_runs(messages)
print(merged)
合并后的效果,本质上就是把连续同类型消息的 content 拼在一起。
这背后的思路很像日志压缩或请求合并:
在不破坏语义的前提下,让消息上下文更规整、更紧凑。
3.7 我怎么看待"消息历史封装类"这件事
总的来讲,这一节我们讲的这类能力,只是一种理解多轮对话原理的过渡方案 。
因为只要应用稍微复杂一点,问题就会变成:
- 多用户怎么隔离?
- 多会话怎么持久化?
- 重启后状态还在不在?
- 历史消息如何裁剪、汇总、压缩?
这些问题,已经不只是"存个列表"这么简单了。
所以这一节我更想抓住的重点还是:
消息历史不是附属品,它是聊天应用最核心的状态之一。
而 LangChain 把它先抽象成消息系统,就是为了后面更复杂的状态管理打基础。
四、提示词模板(Prompt Template):真正可维护的提示词,绝不是字符串拼接
4.1 为什么不建议手写整段 Prompt
最开始写 Prompt,谁都会这么干:
python
question = "请介绍上海的历史"
下一次又写:
python
question = "请介绍北京的历史"
再下一次:
python
question = "请介绍西安的历史"
短期看没问题,但只要任务一重复,你就会发现这种方式特别难维护。
因为真正固定下来的,不是"上海""北京""西安",而是这句话的结构:
bash
请介绍 {city} 的历史
这时候,提示词模板的价值就出来了:
把固定结构和变化内容分开。
4.2 PromptTemplate:最基础的字符串模板
最基础的模板长这样:
python
from langchain_core.prompts import PromptTemplate
prompt_template = PromptTemplate.from_template(
"Translate the following into {language}"
)
result = prompt_template.invoke({"language": "Chinese"})
print(result)
这里最关键的点是:
- 模板里有一个占位符
{language} - 调用时传一个字典,把
language这个变量补进去 - 最后得到一个已经填充好的 Prompt
所以 PromptTemplate 的核心意义就是:
把一次性字符串,变成可反复实例化的输入接口。
4.3 ChatPromptTemplate:真正适合聊天模型的模板
如果说 PromptTemplate 更像纯字符串模板,
那 ChatPromptTemplate 则更适合聊天模型,因为它不是拼一段长文本,而是直接构造一组消息。
例如:
python
from langchain_core.prompts import ChatPromptTemplate
prompt_template = ChatPromptTemplate([
("system", "Translate the following into {language}."),
("user", "{text}")
])
messages_value = prompt_template.invoke({
"language": "Chinese",
"text": "what is your name?"
})
messages = messages_value.to_messages()
print(messages)
这段代码特别重要,因为它说明了一件事:
提示词模板最终不是非得产出字符串,它也可以直接产出消息序列。
也就是说,ChatPromptTemplate 和消息系统之间其实是无缝衔接的。
工作流程可以理解成这样:
模板变量
ChatPromptTemplate
消息列表
聊天模型
AIMessage
所以 ChatPromptTemplate 不是"PromptTemplate 的升级版"那么简单,
它本质上是在把模板系统 直接接到消息系统上。
4.4 模板一旦接上模型和解析器,链就真正闭环了
看一个非常经典的组合:
python
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
model = ChatOpenAI(model="gpt-4o-mini")
prompt_template = ChatPromptTemplate([
("system", "Translate the following into {language}."),
("user", "{text}")
])
parser = StrOutputParser()
chain = prompt_template | model | parser
result = chain.invoke({
"language": "Chinese",
"text": "what is your name?"
})
print(result)
把这个链理解成:
bash
输入变量 -> 生成消息 -> 调用模型 -> 解析结果
这时候你就会发现,提示词模板已经不只是"方便写 Prompt",而是在正式承担:
链路起点的输入适配器角色。
4.5 MessagesPlaceholder:当我不想只填字符串,而是想把"消息列表"塞进模板里
有时候,模板里插入的不是一个普通变量,而是一整段消息历史。
这时就会用到 MessagesPlaceholder。
python
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage
prompt_template = ChatPromptTemplate([
("system", "你是一个聊天助手"),
MessagesPlaceholder("msgs")
])
messages_to_pass = [
HumanMessage(content="中国首都是哪里?"),
AIMessage(content="中国首都是北京。"),
HumanMessage(content="那法国呢?")
]
formatted_prompt = prompt_template.invoke({"msgs": messages_to_pass})
print(formatted_prompt)
这里的关键不是语法,而是能力变化:
普通模板只能插变量值,
而 MessagesPlaceholder 允许我在模板的某个位置插入一整段消息历史。
这意味着:
- 系统设定可以固定在最前面
- 历史消息可以动态插在中间
- 当前问题可以继续拼在后面
从能力上看,这就已经非常接近真正的聊天应用了。
五、少样本提示(Few-shot):不是告诉模型答案,而是教它"按这个方式回答"
5.1 为什么需要
有一类任务
- 需要固定格式输出
- 需要某种特殊风格
- 需要某种特定推理方式
- 需要按我给的标签体系归类
这时候,你光写一段规则,效果往往不如直接给它几个例子。
这就是 Few-shot 的核心思想:
不是只下命令,而是给模型示范。
5.2 最简单的 Few-shot:用例子告诉模型,新的符号应该怎么理解
比如我定义一个奇怪符号 🦜,让模型猜:
bash
What is 2 🦜 9?
如果我不解释,模型根本不知道这个符号是什么意思。
但如果我先给它两个例子:
python
examples = [
{"input": "2 🦜 2", "output": "4"},
{"input": "2 🦜 3", "output": "5"},
]
模型大概率能从例子里推断出:
bash
🦜 ≈ 加号
于是新问题就能答对。
这就是少样本提示最朴素的力量:
不是让模型背答案,而是通过样本归纳任务模式。
5.3 FewShotChatMessagePromptTemplate:把示例真正变成"聊天消息"
在 LangChain 里,少样本不是简单拼几行文本进去,而是可以被组织成标准消息。
例如:
python
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate
examples = [
{"input": "2 🦜 2", "output": "4"},
{"input": "2 🦜 3", "output": "5"},
]
example_prompt = ChatPromptTemplate([
("human", "{input}"),
("ai", "{output}"),
])
few_shot_prompt = FewShotChatMessagePromptTemplate(
example_prompt=example_prompt,
examples=examples,
)
print(few_shot_prompt.invoke({}).to_messages())
这里最重要的不是 API 名字长,而是它在做什么:
example_prompt负责规定单个示例怎么排版examples负责提供多组样本FewShotChatMessagePromptTemplate负责把这些样本实例化成真正的消息列表
5.4 当 Few-shot 和最终问题拼在一起时,模型才真正"学会该怎么答"
一个典型写法通常是:
python
from langchain_core.prompts import ChatPromptTemplate
final_prompt = ChatPromptTemplate([
("system", "你是一个神奇的数学奇才。"),
few_shot_prompt,
("human", "{input}"),
])
然后调用:
python
result = final_prompt.invoke({"input": "What is 2 🦜 9?"})
少样本提示本质上是在给模型构造"任务上下文"。
也就是说,模型不是突然变聪明了,
而是因为你在当前这次输入里,先给了它足够强的任务范式。
这和我们写函数时先给接口示例、写测试样例,其实有点像。
你不是在改变函数本身,而是在明确这次任务的约束边界。
5.5 少样本提示特别适合哪几类任务?
我自己总结下来,最适合 Few-shot 的通常就是下面几类:
-
格式要求很死的任务
比如必须输出固定 JSON 字段
-
风格要求很强的任务
比如仿某种写作语气
-
标签体系要稳定的任务
比如情感倾向分析、信息抽取
-
推理方式要被"示范"的任务
比如一步步推理、先拆问题再回答
六、示例选择器(Example Selector):示例一多,就不能无脑全塞进 Prompt 里
6.1 Few-shot 的下一个现实问题:样本太多怎么办?
假设我已经有 100 组示例。
那是不是把 100 组全塞进提示词里就最好?
当然不是。
因为示例越多,问题就越明显:
- 上下文更贵
- 推理更慢
- 噪声更多
- 甚至可能把模型搞糊涂
所以真正实用的思路不是"示例越多越好",而是:
每次只选最合适的那一部分示例。
LangChain 给这个问题做了专门抽象,这就是 Example Selector。
6.2 按长度选:LengthBasedExampleSelector
最直接的思路就是:
在上下文允许范围内,尽可能多放示例;
如果当前输入很长,那就少放一点。
这就是长度选择器的思想。
它适合什么场景?
- 你最担心的是上下文长度
- 示例质量比较均匀
- 你不想引入 Embedding 和向量库
这种方法不一定最聪明,但足够直接、稳定,而且实现成本低。
6.3 按语义选:SemanticSimilarityExampleSelector
更聪明的方法是:
当前输入和哪些示例"语义最像",就优先选哪些。
这时候就要用到 Embedding 和向量相似度。
例如你当前输入是:
bash
worried
系统可能会自动选出和"情绪"最接近的示例,而不是乱选一个天气类词语。
这种方法比按长度更聪明,因为它开始真正考虑任务相关性。
但它也意味着:你已经不再只是写 Prompt 了,而是在引入下一层能力------Embedding 检索。
6.4 按 MMR 选:相关,还要尽量不重复
如果只按"最相似"来选,可能会出现另一个问题:
选出来的几个示例全都很像,信息重复严重。
这时可以用 MMR(最大边际相关性) 的思路:
- 既要和当前输入相关
- 又要保证示例之间不要太像
如果只用一句话理解 MMR,那就是:
相关性和多样性同时要。
这个思路特别像检索系统里常见的"去冗余"策略。
与其给模型 5 个几乎重复的例子,不如给它 3 个覆盖面更好的例子。
6.5 N-gram 重叠:更偏表层文本相似
还有一种选择方式是看 n-gram overlap,也就是文本表层片段的重叠程度。
这种方法比较适合什么?
- 句子形式很关键
- 文本相似性更依赖词面
- 不一定需要真正深层语义
但如果任务更偏"意思像不像",那通常还是语义相似方案更自然。
- 简单路线:按长度、按 n-gram
- 更智能路线:按语义相似度、按 MMR
七、输出解析器(Output Parser):模型的回复,本来是文本;但程序真正想要的,通常不是文本
7.1 为什么输出解析器会成为核心组件,而不是附属工具?
到这里,一个非常现实的问题就出现了:
模型回答得再好,如果输出还是一大段自然语言,程序其实不太好直接用。
比如我真正需要的可能是:
- 一个字符串
- 一个 JSON 对象
- 一个 Pydantic 对象
- 一个日期
- 一个枚举值
- 一个列表
这时候,如果还让我手动去拆字符串、写正则,那整条链路就会变得非常脆弱。
所以输出解析器存在的意义其实特别明确:
把模型输出从"聊天文本"提升成"程序数据"。
这不是锦上添花,而是让 LLM 应用真正能进入工程链路的关键一步。
7.2 StrOutputParser:最简单,但也是最常用的一个
先看最简单的:
python
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
model = ChatOpenAI(model="gpt-4o-mini")
chain = model | StrOutputParser()
result = chain.invoke("写一首夏天的短诗")
print(result)
这里的作用很直接:
- 模型原本返回的是
AIMessage StrOutputParser()帮我把它变成纯文本字符串
这一步看起来简单,但特别常用。
因为很多时候,我根本不想拿一整个消息对象,我只想拿文本结果。
所以 StrOutputParser 可以理解成:
把"消息对象层"降到"纯字符串层"。
7.3 PydanticOutputParser:当我想要的不是字符串,而是一个结构化对象
如果我想要的结果更像程序里的对象,比如一个笑话对象:
setuppunchlinerating
那字符串就不够了。
这时可以这样写:
python
from typing import Optional
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import PromptTemplate
class Joke(BaseModel):
setup: str = Field(description="笑话的开头")
punchline: str = Field(description="笑话的包袱")
rating: Optional[int] = Field(default=None, description="1到10分")
parser = PydanticOutputParser(pydantic_object=Joke)
prompt = PromptTemplate(
template="Answer the user query.\n{format_instructions}\n{query}\n",
input_variables=["query"],
partial_variables={"format_instructions": parser.get_format_instructions()},
)
model = ChatOpenAI(model="gpt-4o-mini")
chain = prompt | model | parser
result = chain.invoke({"query": "给我讲一个关于唱歌的笑话"})
print(result)
这段代码第一次看确实会有点长,但本质只是在做三件事:
- 先定义我想要的结果结构
- 再把这个结构转成模型能读懂的格式约束
- 最后让解析器把结果还原成对象
这一步一旦理解了,你就会突然意识到:
原来模型不只是会"回答问题",它还可以被组织成一个"对象生成器"。
这对后面做信息抽取、结构化问答、表单填写、配置生成都非常重要。
7.4 get_format_instructions() 为什么这么关键?
在上面那段代码里,最容易被忽略的一行其实是这个:
python
parser.get_format_instructions()
它的作用不是执行解析,而是:
告诉模型:你应该按什么格式返回结果。
也就是说,输出解析器其实做了两件事:
- 前置阶段:生成格式约束,塞进 Prompt
- 后置阶段:拿到模型结果后,解析成目标对象
所以它不是单纯的后处理器,而是同时参与了:
- 输出格式约束
- 结果结构解析
这就是为什么它会成为核心组件,而不是"收尾小工具"。
7.5 JsonOutputParser:我不一定要对象类,但我可能至少想要 JSON
如果希望模型老老实实返回 JSON。
这时就可以用 JsonOutputParser。
python
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import PromptTemplate
parser = JsonOutputParser()
prompt = PromptTemplate(
template="Answer the user query.\n{format_instructions}\n{query}\n",
input_variables=["query"],
partial_variables={"format_instructions": parser.get_format_instructions()},
)
model = ChatOpenAI(model="gpt-4o-mini")
chain = prompt | model | parser
result = chain.invoke({"query": "给我讲一个关于唱歌的笑话"})
print(result)
7.6 输出解析器和 with_structured_output() 到底怎么选?
这一点特别容易混淆,所以我想单独说清楚。
两者都能帮我拿到结构化结果,但侧重点不一样。
如果更强调链式组合
那我更倾向于:
python
prompt | model | parser
因为输出解析器天生就适合放进 LCEL 链里。
如果更强调"直接从模型拿结构化对象"
那 with_structured_output() 会更方便。
所以我现在的理解是:
with_structured_output()是模型能力绑定。
输出解析器是链路组件
两者不是谁替代谁,而是看你当前是从"模型"视角出发,还是从"链"视角出发。
7.7 理解LangChain的框架化
因为到这里,LangChain 已经不只是"调模型"了,而是在做一件非常框架化的事情:
输入变量
PromptTemplate
ChatModel
OutputParser
程序可直接使用的数据
这条链意味着什么?
意味着模型输出不再停留在"人看得懂",
而是开始进入"程序也能稳定消费"。
一旦这一步打通,后面的能力就全都能串上来了:
- 信息抽取
- 对象构造
- API 入参生成
- 自动填表
- 配置文件生成
- 结果落库
八、本篇总结
这一篇我真正想讲透的,不是零散的类名,而是 LangChain 在"模型调用前后"到底做了哪些统一抽象。
核心结论:
-
消息系统解决的是聊天模型输入输出的统一表达问题。
角色、内容、元数据、工具结果、流式片段,全都被纳入一套消息抽象。
-
多轮对话本质上不是模型真的记住了我,而是历史消息被再次送进模型。
这意味着历史消息管理天然就是聊天应用的核心状态问题。
-
提示词模板解决的是输入复用与结构化组织问题。
从
PromptTemplate到ChatPromptTemplate,再到MessagesPlaceholder,本质上都是在把"手写 Prompt"提升成"可编排输入接口"。 -
少样本提示解决的是规则描述不如示例直观的问题。
它不是多给几个答案,而是在当前上下文里示范"这类任务该怎么做"。
-
示例选择器解决的是示例太多时如何自动挑选的问题。
从长度、语义相似度、MMR 到 n-gram,本质上都在做"示例检索"。
-
输出解析器解决的是模型输出如何稳定落地为程序数据的问题。
从字符串,到 JSON,再到对象,LangChain 在这里正式把"聊天结果"推进成"工程结果"。
如果用一句话总结这一篇,那就是:
模型调用不是一句 prompt 加一次 invoke,而是一整套输入组织、任务示范和输出落地的工程体系。
💬 下一篇承接方向 :从这里往下,就要进入 LangChain 另外半边真正和 RAG 强相关的组件了:
Document、文档加载器、文本切分器、Embedding、向量存储、检索器。也就是说,下一篇会正式回答一个最关键的问题:模型不知道的知识,我到底该怎么接给它?
这也是从"会调模型"走向"会做知识型 AI 应用"的关键一步。🚀