零基础入门 LangChain 与 LangGraph(五):核心组件上篇——消息、提示词模板、少样本与输出解析

文章目录

    • [零基础入门 LangChain 与 LangGraph(五):核心组件上篇------消息、提示词模板、少样本与输出解析](#零基础入门 LangChain 与 LangGraph(五):核心组件上篇——消息、提示词模板、少样本与输出解析)
    • [一、进一步理解 LangChain 为什么叫"框架"](#一、进一步理解 LangChain 为什么叫“框架”)
    • 二、消息(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) 表示把整段消息序列送进模型)
      • [2.6 多轮对话的本质,不是"模型记住了你",而是"历史消息又发了一遍"](#2.6 多轮对话的本质,不是“模型记住了你”,而是“历史消息又发了一遍”)
      • [2.7 `BaseMessage`:所有消息的共同父类,](#2.7 BaseMessage:所有消息的共同父类,)
    • 三、历史消息为什么必须管理?因为上下文窗口永远是有限的
      • [3.1 所谓"多轮对话",本质上是在和上下文窗口做资源分配](#3.1 所谓“多轮对话”,本质上是在和上下文窗口做资源分配)
      • [3.2 `trim_messages`:先掌握最常用的一招,消息裁剪](#3.2 trim_messages:先掌握最常用的一招,消息裁剪)
      • [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 应用开发这件事工程化?

🚀 这一篇的目标:写完之后,我至少要真正吃透四件事:

  1. 为什么聊天模型的输入输出,本质上是"消息"而不是一段普通字符串
  2. 为什么提示词模板不是语法糖,而是可复用的输入接口
  3. 为什么少样本提示可以显著提升模型输出质量
  4. 为什么输出解析器能把"聊天结果"变成"程序可直接使用的数据"

这一篇我会先把 模型调用前后最核心的这半边组件体系 讲透。下一篇再进入另外半边:文档、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 先把最底层概念记住:角色 + 内容 + 元数据

一条消息最核心的三部分,其实就是:

  1. Role(角色)
  2. Content(内容)
  3. 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:消息唯一标识

在实际开发里,你最常会接触的是:

  • content
  • id
  • response_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 的通常就是下面几类:

  1. 格式要求很死的任务

    比如必须输出固定 JSON 字段

  2. 风格要求很强的任务

    比如仿某种写作语气

  3. 标签体系要稳定的任务

    比如情感倾向分析、信息抽取

  4. 推理方式要被"示范"的任务

    比如一步步推理、先拆问题再回答


六、示例选择器(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:当我想要的不是字符串,而是一个结构化对象

如果我想要的结果更像程序里的对象,比如一个笑话对象:

  • setup
  • punchline
  • rating

那字符串就不够了。

这时可以这样写:

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)

这段代码第一次看确实会有点长,但本质只是在做三件事:

  1. 先定义我想要的结果结构
  2. 再把这个结构转成模型能读懂的格式约束
  3. 最后让解析器把结果还原成对象

这一步一旦理解了,你就会突然意识到:

原来模型不只是会"回答问题",它还可以被组织成一个"对象生成器"。

这对后面做信息抽取、结构化问答、表单填写、配置生成都非常重要。


7.4 get_format_instructions() 为什么这么关键?

在上面那段代码里,最容易被忽略的一行其实是这个:

python 复制代码
parser.get_format_instructions()

它的作用不是执行解析,而是:

告诉模型:你应该按什么格式返回结果。

也就是说,输出解析器其实做了两件事:

  1. 前置阶段:生成格式约束,塞进 Prompt
  2. 后置阶段:拿到模型结果后,解析成目标对象

所以它不是单纯的后处理器,而是同时参与了:

  • 输出格式约束
  • 结果结构解析

这就是为什么它会成为核心组件,而不是"收尾小工具"。


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 在"模型调用前后"到底做了哪些统一抽象。

核心结论:

  1. 消息系统解决的是聊天模型输入输出的统一表达问题。

    角色、内容、元数据、工具结果、流式片段,全都被纳入一套消息抽象。

  2. 多轮对话本质上不是模型真的记住了我,而是历史消息被再次送进模型。

    这意味着历史消息管理天然就是聊天应用的核心状态问题。

  3. 提示词模板解决的是输入复用与结构化组织问题。

    PromptTemplateChatPromptTemplate,再到 MessagesPlaceholder,本质上都是在把"手写 Prompt"提升成"可编排输入接口"。

  4. 少样本提示解决的是规则描述不如示例直观的问题。

    它不是多给几个答案,而是在当前上下文里示范"这类任务该怎么做"。

  5. 示例选择器解决的是示例太多时如何自动挑选的问题。

    从长度、语义相似度、MMR 到 n-gram,本质上都在做"示例检索"。

  6. 输出解析器解决的是模型输出如何稳定落地为程序数据的问题。

    从字符串,到 JSON,再到对象,LangChain 在这里正式把"聊天结果"推进成"工程结果"。

如果用一句话总结这一篇,那就是:

模型调用不是一句 prompt 加一次 invoke,而是一整套输入组织、任务示范和输出落地的工程体系。


💬 下一篇承接方向 :从这里往下,就要进入 LangChain 另外半边真正和 RAG 强相关的组件了:Document、文档加载器、文本切分器、Embedding、向量存储、检索器。

也就是说,下一篇会正式回答一个最关键的问题:模型不知道的知识,我到底该怎么接给它?

这也是从"会调模型"走向"会做知识型 AI 应用"的关键一步。🚀

相关推荐
吃一根烤肠2 小时前
2026年4月AI大事件深度解读:大模型竞争进入“深水区“
人工智能
MOON404☾2 小时前
Chapter 002. 线性回归
算法·回归·线性回归
小陈工2 小时前
数据库Operator开发实战:以PostgreSQL为例
开发语言·数据库·人工智能·python·安全·postgresql·开源
慕涯AI2 小时前
Agent 30 课程开发指南 - 第21课
人工智能·python
源码之家2 小时前
计算机毕业设计:Python城市天气数据挖掘与预测系统 Flask框架 随机森林 K-Means 可视化 数据分析 大数据 机器学习 深度学习(建议收藏)✅
人工智能·爬虫·python·深度学习·机器学习·数据挖掘·课程设计
数智化管理手记2 小时前
零基础认知精益生产——核心本质与必避误区
大数据·数据库·人工智能·低代码·制造
故事和你912 小时前
洛谷-数据结构-1-3-集合3
数据结构·c++·算法·leetcode·贪心算法·动态规划·图论
用户5191495848452 小时前
Kubernetes kubeadm 集群部署与 CKA 实战指南
人工智能·aigc
幻风_huanfeng3 小时前
人工智能之数学基础:坐标下降法
人工智能·深度学习·计算机视觉·梯度下降法·坐标下降法