👋 欢迎阅读

🏠个人主页: 愿旖旎
📘专栏传送门: 算法专栏
💻当前学习内容:LangChain
📑 目录
一、前置讲解
在进入正文前,先花 10 秒了解本篇会反复用到的核心概念,带着印象去读正文,学习效率更高。
🧠 前置知识一:结构化输出
让模型按你定义的结构(JSON/Pydantic)返回数据 ,是程序对接模型的关键。
📐 前置知识二:三种结构定义方式
Pydantic / TypedDict / JSON Schema,决定了返回的是对象还是字典。
⚡ 前置知识三:流式传输
用 stream/astream 逐块输出结果 ,显著改善实时体验。
🗨️ 前置知识四:消息(Messages)
System/Human/AI/Tool 四种消息,是聊天模型的输入输出格式。
🧮 前置知识五:上下文窗口与历史管理
多轮对话靠"重发历史",trim/filter/merge 是管理历史的三个工具。
二、聊天模型:结构化输出
在 LangChain 中,聊天模型提供了额外的功能:结构化输出 。这是一种使聊天模型以结构化格式 (例如 JSON )进行响应的技术。例如,可能希望将模型输出存储在数据库中,并确保输出符合数据库模式。
这样做的核心原因 是:从**"字符串"** 到**"对象"** 的范式转换 。想象一下,在没有这个功能之前,我们调用聊天模型得到的是一个 AIMessage ,其内容是一个字符串:
python
model = ChatOpenAI()
response = model.invoke("告诉我关于苹果公司的最新消息。")
print(response.content)
# 输出: "苹果公司于昨日发布了新款iPhone...其股价上涨了2%..."
这个字符串 对人类很友好,但对程序 不友好。如果我们想从这段文本中提取出 "公司名" 和 "股价变化" 并用在后续逻辑中,则需要编写复杂且容易出错 的解析代码(例如,使用正则表达式 )。聊天模型的 with_structured_output 方法则允许我们预先定义 一个期望的数据结构 ,并要求大模型必须按照这个结构返回信息。
2.1 with_structured_output()
在 LangChain 中,要使用结构化输出 能力,提供了 .with_structured_output() 方法。该方法需要先定义输出结构 ,然后执行通过 .with_structured_output() 得到的 Runnable 实例。步骤如下(伪代码):
python
# 1. 定义输出结构
schema = {"foo": "bar"}
# 2. 绑定 schema,其实是生成支持结构化返回的 Runnable 实例
model_with_structure = model.with_structured_output(schema)
# 3. 执行
structured_output = model_with_structure.invoke(user_input)
这是获得结构化输出 的最简单、最可靠的方法。此方法将输出结构 作为参数输入,返回一个类似 model 的 Runnable 。不同之处在于执行 Runnable 后的输出结果,输出的不是字符串 或消息 ,而是与给定输出结构 相对应的对象。
三种输出结构定义方式:
| 方式 | 写法 | 返回结果 |
|---|---|---|
| TypedDict | class Xxx(TypedDict) |
字典 |
| JSON Schema | Python 字典描述结构 | 字典 |
| Pydantic | class Xxx(BaseModel) |
Pydantic 对象 |
函数参数:
| 参数 | 说明 |
|---|---|
| schema | 输出结构:JSON、TypedDict、Pydantic、OpenAI 函数/工具 |
| method | LLM 的生成方法 :json_schema(默认)、function_calling、json_mode |
| include_raw | False(默认)仅返回解析结果,出错抛异常;True 返回含 "raw"/"parsed"/"parsing_error" 的字典 |
| strict | True 保证输出与 schema 完全匹配;False 不验证;None(默认)不传递 |
| tools | 绑定到模型的工具列表(要求 method=json_schema、strict=True、include_raw=True) |
| kwargs(Any) | 附加参数,直接传递给 bind() |
method 三种取值:
| method | 说明 |
|---|---|
| json_schema(默认) | 使用 OpenAI 的结构化输出 API |
| function_calling | 使用 OpenAI 的工具调用(DeepSeek 需指定此方式) |
| json_mode | 使用 OpenAI 的 JSON mode(须在提示中说明输出格式) |
返回值 :返回一个 Runnable 实例。
-
如果 include_raw=False :schema 是 Pydantic 类则输出 Pydantic 对象 ,否则输出字典。
-
如果 include_raw=True :输出含三个键的字典 ------
'raw'(原始 BaseMessage)、'parsed'(解析结果,出错为 None)、'parsing_error'(解析异常,无则 None)。
2.2 Pydantic 对象
将执行 Runnable 后的输出结果 指定为 Pydantic 类,将返回一个 Pydantic 对象。当收到模型的响应后,LangChain 会提取出代表 Pydantic 参数的 JSON 对象,并用 Pydantic 模型对其进行解析和验证 ,最终将这个验证后的 JSON 转换为一个可用的 Pydantic 对象实例返回。
python
import os
from langchain_openai import ChatOpenAI
from typing import Optional
from pydantic import BaseModel, Field
# ========== 1. 初始化模型 ==========
model = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"), # Key 从环境变量读
base_url="https://api.deepseek.com", # DeepSeek 接口地址
temperature= 1.5
)
# ========== 2. 定义输出结构(Pydantic 类) ==========
# 告诉模型"你必须按这个结构返回":字段、类型、描述
class Joke(BaseModel):
"""给用户讲一个笑话。"""
setup: str = Field(description="这个笑话的开头")
punchline: str = Field(description="这个笑话的妙语")
rating: Optional[int] = Field(default=None, description="从1到10分,给这个笑话评分")
# ========== 3. 把模型包装成"结构化输出"模型 ==========
# 关键:必须指定 method="function_calling"!
# 默认的 json_schema 方式 DeepSeek 不支持
# function_calling = 模型按"工具调用"的方式返回 JSON,DeepSeek 支持
structured_model = model.with_structured_output(Joke, method="function_calling")
# ========== 4. 调用并取结果 ==========
result = structured_model.invoke("给我讲一个关于唱歌的笑话")
print(result) # 返回的是 Joke 对象

2.3 TypedDict
先了解 一下 TypedDict ,它用于为字典对象 提供精确的、结构化的类型提示 。它允许我们指定一个字典中应该有哪些键 ,以及每个键对应的值的类型 。最清晰、最常用的定义方式,就是类似于定义一个类(Python 3.8+):
python
from typing import TypedDict
class User(TypedDict):
name: str
age: int
email: str
is_active: bool = True # 默认值
这有什么⽤呢?对于它⾮常重要的⼀个能⼒就是捕捉键名拼写错误与类型错误。
同样,我们也可以将执行 Runnable 后的输出结果 指定为 TypedDict 类,这将返回一个字典 ,且输出后,会根据 TypedDict 的定义进行验证:
python
import os
from langchain_openai import ChatOpenAI
from typing import Optional
from typing_extensions import Annotated, TypedDict
# ========== 1. 初始化模型 ==========
model = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"), # Key 从环境变量读
base_url="https://api.deepseek.com", # DeepSeek 接口地址
temperature=1.5, # 温度高一点,讲笑话更有创意(范围 0~2)
)
# ========== 2. 定义输出结构(TypedDict) ==========
# TypedDict = 只描述"结构长什么样"的字典类型,比 Pydantic 更轻量
class Joke(TypedDict):
"""给用户讲一个笑话。"""
setup: Annotated[str, ..., "这个笑话的开头"]
punchline: Annotated[str, ..., "这个笑话的妙语"]
rating: Annotated[Optional[int], None, "从1到10分,给这个笑话评分"]
# ========== 3. 把模型包装成"结构化输出"模型 ==========
# 关键:必须指定 method="function_calling"!
# 默认的 json_schema 方式 DeepSeek 不支持(400 报错)
structured_model = model.with_structured_output(Joke, method="function_calling")
# ========== 4. 调用并取结果 ==========
result = structured_model.invoke("给我讲一个关于跳舞的笑话")
print(result) # 返回的是【字典】dict(不是对象!)
print("开头:", result["setup"]) # 字典用 [键名] 取值(注意!不是 .字段名)

让我们加入 include_raw=True,再来看看效果:
可以看到:raw 是模型原始响应(含 tool_calls),parsed 是解析后的字典,parsing_error 无错误时为 None。
2.4 返回 JSON
还可以让聊天模型 直接返回 JSON ,只不过为了声明 JSON,我们需要定义 JSON Schema:
python
import os
from langchain_openai import ChatOpenAI
# ========== 1. 初始化模型 ==========
model = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"), # Key 从环境变量读
base_url="https://api.deepseek.com", # DeepSeek 接口地址
temperature=1.5, # 温度高一点,讲笑话更有创意(范围 0~2)
)
# ========== 2. 定义输出结构(JSON Schema 字典) ==========
# 这是结构化输出的第三种写法:直接用 JSON Schema 描述结构
# 结构说明:
# title = 结构名字
# description = 整体描述(模型参考)
# type = 顶层是对象 object
# properties = 字段列表:每个字段写 类型 + 描述
# required = 必填字段清单(没列进去的就是可选的)
json_schema = {
"title": "joke",
"description": "给用户讲一个笑话。",
"type": "object",
"properties": {
"setup": {
"type": "string",
"description": "这个笑话的开头",
},
"punchline": {
"type": "string",
"description": "这个笑话的妙语",
},
"rating": {
"type": "integer",
"description": "从1到10分,给这个笑话评分",
"default": None, # 可选字段(不在 required 里)
},
},
"required": ["setup", "punchline"], # 只有 setup 和 punchline 是必填
}
# ========== 3. 包装成结构化输出 ==========
# 关键:必须指定 method="function_calling"!
# 默认的 json_schema 方式 DeepSeek 不支持(400 报错)
structured_model = model.with_structured_output(json_schema, method="function_calling")
# ========== 4. 调用并取结果 ==========
result = structured_model.invoke("给我讲一个关于唱歌的笑话")
print(result) # 返回字典:{'setup': ..., 'punchline': ..., 'rating': ...}

2.5 使用场景:信息提取器
python
import os
from langchain_openai import ChatOpenAI
from typing import Optional
from pydantic import BaseModel, Field
from langchain_core.messages import HumanMessage, SystemMessage
# ========== 1. 初始化模型)==========
model = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
# ========== 2. 定义输出结构(Pydantic 类) ==========
# 信息提取场景的关键技巧:
# 1. 每个字段都用 Optional ------ LLM 不知道答案时返回 None(不硬编)
# 2. 每个字段都有 description ------ LLM 根据描述去文本里找信息
class Person(BaseModel):
"""一个人的信息。"""
name: Optional[str] = Field(default=None, description="这个人的名字")
hair_color: Optional[str] = Field(default=None, description="如果知道这个人头发的颜色")
skin_color: Optional[str] = Field(default=None, description="如果知道这个人的肤色")
height_in_meters: Optional[float] = Field(default=None, description="以米为单位的高度")
# ========== 3. 包装成结构化输出 ==========
# 关键:必须指定 method="function_calling"!默认方式 DeepSeek 不支持(400)
structured_model = model.with_structured_output(schema=Person, method="function_calling")
# ========== 4. 准备消息:SystemMessage 教规则 + HumanMessage 给原文 ==========
messages = [
SystemMessage(content="你是一个提取信息的专家,只从文本中提取相关信息。"
"如果您不知道要提取的属性的值,属性值返回null"),
HumanMessage(content="史密斯身高6英尺,金发。"),
]
# ========== 5. 调用并取结果 ==========
result = structured_model.invoke(messages)
print(result) # Person 对象:name='史密斯' hair_color='金发' skin_color=None ...
print("名字:", result.name) # 对象用 .字段名 取值
print("身高(米):", result.height_in_meters)

三、聊天模型:流式传输
流式处理 对于基于 LLM 的应用程序能够响应最终用户 至关重要。它通过逐步显示输出 ,即使在完整的响应准备就绪之前,流式传输也能显著改善用户体验 。我们之前直接使用 invoke 的调用方式属于非流式传输 ,现象是聊天模型直接返回全量内容。若模型思考时间较长,则我们等待的时间就越长。
| 方式 | 现象 | 比喻 |
|---|---|---|
| invoke | 等模型全部生成完,一次性返回全量内容 | 像收快递,等半天一次拿到 |
| stream | 模型生成一个字就给一个字 | 像看直播,逐字出现 |
3.1 stream() 同步流式传输
在 LangChain 聊天模型中,可以使用其 .stream() 方法,来同步生成流式响应的效果。聊天模型的 .stream() 方法返回一个迭代器 ,该迭代器在生成输出时同步产生输出消息块 。可以使用 for 循环实时处理每个块。
python
import os
from langchain_openai import ChatOpenAI
# ========== 1. 初始化模型 ==========
model = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"), # Key 从环境变量读
base_url="https://api.deepseek.com", # DeepSeek 接口地址
)
# ========== 2. 流式输出 ==========
# 和 invoke 的区别:
# invoke = 等模型全部生成完,一次性返回(像收快递,等半天一次拿到)
# stream = 模型生成一个字就给你一个字(像看直播,逐字出现)
chunks = []
for chunk in model.stream("讲一个笑话"):
chunks.append(chunk) # ① 收集每一块(后面可以拼成完整文本)
print(chunk.content, end="|", flush=True) # ② 实时打印:end="|" 不换行,flush=True 强制立即显示
print() # 循环结束后换行
print("分块数量:", len(chunks)) # 看看一段话被切成了几块
print("完整内容:", "".join(c.content for c in chunks)) # 把块拼回完整文本
我们得到了一个叫做 AIMessageChunk 的东西,它代表 AIMessage 的一部分,也就是消息块:
python
print(chunks[0] + chunks[1] + chunks[2] + chunks[3] + chunks[4])

3.2 异步相关概念
对于流式传输,通常我们可以选择异步调用。 先来了解下异步相关知识。
想象一个场景:你需要煮一壶水 ,同时还要给朋友发一条短信 。我们分别用同步(传统) 和异步 两种方式来完成,以此对比并引入协程 和事件循环的概念。
同步(阻塞)方式 :这就像是一个"死心眼"的人,做事必须一件一件来 ------先花 5 秒煮水(期间什么也不能做),水开后再花 2 秒发短信,总耗时 7 秒。
python
import time
def boil_water():
print("开始煮水...")
time.sleep(5) # 模拟阻塞等待5秒
print("水开了!")
def send_message():
print("开始发短信...")
time.sleep(2) # 模拟阻塞等待2秒
print("短信发送成功!")
def main():
boil_water() # 先花5秒煮水,期间什么也不能做
send_message() # 水开后再花2秒发短信
main()

异步方式:我们请出 asyncio、协程和事件循环。
先看三种并发模型的区别:
| 模型 | 原理 | 特点 |
|---|---|---|
| 多进程 | 利用多核 CPU 同时执行多个任务 | 各自独立内存,进程间通信麻烦 |
| 多线程 | 进程内多个子任务,等待时切换 | 调度由操作系统控制,需锁机制保护共享资源 |
| 协程 | 用户态"轻量级线程",调度由程序员/语言控制 | 上下文切换开销低,适合 I/O 密集、低计算量场景 |

协程 是一个特殊的函数 ,它可以在执行过程中暂停 ,并在稍后恢复 执行。它使用 async def 定义,并在需要暂停的地方使用 await:
python
import asyncio
# 定义协程
async def boil_water_async():
print("开始煮⽔...")
await asyncio.sleep(5) # 关键! await 表⽰"等待这个操作完成,但期间让事件循环去做别的事"
print("⽔开了!")
async def send_message_async():
print("开始发短信...")
await asyncio.sleep(2) # 同样,等待2秒,但让出控制权
print("短信发送成功!")
# 主程序(也是⼀个协程)
async def main():
# 创建两个任务,并交给事件循环去调度
task1 = asyncio.create_task(boil_water_async())
task2 = asyncio.create_task(send_message_async())
# 等待两个任务都完成
await task1
await task2
# 它负责创建事件循环,并将第⼀个协程(主程序)放⼊其中运⾏。
asyncio.run(main())

总耗时:5 秒 (因为两个任务的等待时间是并发的)。
通过使用 asyncio ,我们可以在单线程 中同时处理多个任务。一个在单线程内调度和管理 所有协程的核心机制,就是事件循环------它不停地检查哪些协程可以执行,哪些在等待。
| 概念 | 说明 |
|---|---|
| 协程 | 特殊的函数,可暂停/恢复 ,用 async def 定义、await 暂停 |
| asyncio.run() | 创建事件循环,运行指定的协程 |
| 事件循环 | 核心组件,调度和执行协程,检查任务状态并回调 |
3.3 astream() 异步流式传输
可以使用 .astream() 方法来异步生成 流式响应的效果,这专为非阻塞工作流而设计:
python
import os
import asyncio
from langchain_openai import ChatOpenAI
model = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
# astream = 异步版 stream;async for = 异步版 for
# 好处:等待网络时不阻塞程序,可以同时处理多个请求(适合并发场景)
async def async_stream():
print("=== 异步调用 ===")
async for chunk in model.astream("讲一个50字的笑话"):
print(chunk.content, end="", flush=True)
print()
loop = asyncio.new_event_loop() # 新建事件循环
loop.run_until_complete(async_stream()) # 在循环里运行异步函数
loop.close() # 用完关闭循环
3.4 自定义流式输出解析器
上面我们演示了如何让聊天模型 进行流式输出 。若此时我们希望修改上一步的输出样式 (例如从逐字输出改为逐句输出 ),同时保留流式处理 功能,那么我们需要在链 中使用生成器函数 ,即可完成自定义流式输出的能力。
还记得之前说过,聊天模型的 .stream() 方法返回的是一个迭代器 。那么我们实现的这些生成器 的签名应该是:IteratorInput -> IteratorOutput ;对于异步生成器 ,则为:AsyncIteratorInput -> AsyncIteratorOutput。
python
import os
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from typing import Iterator, List
model = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
parser = StrOutputParser() # 把模型的 AIMessage 转成纯字符串
# 自定义生成器:把流式输出按句号切成句子
# 输入:上一环节(parser)流过来的字符串块;输出:每攒够一个句子就 yield 出去
def split_into_list(input: Iterator[str]) -> Iterator[List[str]]:
buffer = "" # 攒字缓冲区
for chunk in input:
buffer += chunk
while "。" in buffer: # 缓冲里出现句号 = 攒够一个完整句子
stop_index = buffer.index("。")
yield [buffer[:stop_index].strip()] # 句号前的部分 = 一个句子,产出!
buffer = buffer[stop_index + 1:]
if buffer.strip(): # 最后剩下的尾巴(可能没以句号结尾)
yield [buffer.strip()]
# 链:模型 → 转字符串 → 按句号切句子
chain = model | parser | split_into_list
for chunk in chain.stream("写一份关于爱情的歌词,需要5句话,每句话用句号分割"):
print(chunk, end="|", flush=True) # 每个 chunk 是一个 [句子] 列表
3.5 深度探索流式传输:SSE 协议
HTTP 协议本身设计为无状态 的请求-响应模式,严格来说,无法做到服务器主动推送消息到客户端 。但通过 Server-Sent Events(服务器发送事件,简称 SSE) 技术可实现流式传输 ,允许服务器主动向浏览器推送数据流------服务器向客户端声明接下来要发送的是流消息(streaming),此时客户端不会关闭连接,会一直等待服务器发送新的数据流。

核心特点:
| 特点 | 说明 |
|---|---|
| 基于 HTTP 协议 | 复用标准 HTTP/HTTPS,无需额外端口,兼容性好 |
| 单向通信机制 | 仅支持服务器 → 客户端的单向推送,客户端无法经同一连接回发 |
| 自动重连机制 | 连接中断时浏览器自动重连 (支持 retry 字段指定间隔) |
| 自定义消息类型 | 响应头设置 Content-Type: text/event-stream,持续推送事件流 |
数据格式:
服务端向浏览器发送 SSE 数据,需要设置必要的 HTTP 头信息:
python
Content-Type: text/event-stream;charset=utf-8
Connection: keep-alive
每一次发送的消息由若干个 message 组成,每个 message 之间由 \n\n 分隔。每个 message 内部由若干行组成:
python
[Field]: value\n
event: foo
data: a foo event
data: an unnamed event
event: end
data: a bar event
Field 取值:
| Field | 说明 |
|---|---|
| data(必需) | 数据内容 |
| event(非必需) | 自定义事件类型 ,默认是 message 事件 |
| id(非必需) | 数据标识符,相当于每条数据的编号 |
| retry(非必需) | 指定浏览器重新发起连接的时间间隔 |
除此之外,还可以有冒号(:) 开头的行,表示注释。
LangChain 流式传输流程分析 :LangChain 本身并不"创造"或"规定"一个底层的网络传输协议 ,而是依赖于其底层的大模型供应商 (如 OpenAI)和我们自身服务应用所使用的 Web 框架 的协议。因此,对于 LangChain 的流式传输 能力,本身是因为大模型供应商提供了流式传输能力,由 LangChain 进行调用后接收并处理成一个个的 AIMessageChunk 。当我们发起请求时,会在请求中设置 stream=True (_stream() 源码中的第一步),表示服务器将在生成响应时向客户端发送数据(SSE),此时 API 会保持 HTTP 连接打开,并以特定格式发送数据。
四、核心组件:消息与历史管理
4.1 消息(Messages)
消息是聊天模型中的通信单位 ,用于表示聊天模型的输入 和输出 ,以及可能与对话关联的任何其他上下文或元数据。
LLM 消息的三个组成部分:
| 组成 | 说明 |
|---|---|
| 消息角色(Role) | 区分对话中不同类型的消息,帮助模型了解如何响应 |
| 消息内容(Content) | 文本或多模态数据(图像/音频/视频),大多数模型以文本为主 |
| 其他元数据(Additional metadata) | 因模型而异的额外信息(时间戳、模型特定参数等) |
消息角色详解
4.2 LangChain 消息
LangChain 提供了一种统一的消息格式 ,可以跨聊天模型 使用,允许用户使用不同的聊天模型,而无需担心每个模型提供商使用的消息格式的具体细节:
python
# 不同厂商的模型,输入输出统一使用 LangChain 消息格式(模型名请以各厂商最新为准)
openai_model = init_chat_model("gpt-5.1", model_provider="openai")
deepseek_model = init_chat_model("deepseek-chat", model_provider="deepseek")
这些模型提供商 不同,但对于其输入 和输出 ,统一使用 LangChain 的消息格式 。它们都是 LangChain BaseMessage 的子类 ,全部作为 LangChain 聊天模型的输入 和输出。
| 消息类型 | 角色 |
|---|---|
| BaseMessage | 抽象基类,所有消息的父类 |
| SystemMessage | 系统角色,设定对话上下文(通常第一条) |
| HumanMessage | 用户角色,用户输入 |
| AIMessage | AI 角色,模型响应(可含工具调用) |
| ToolMessage | 工具角色,工具执行结果 |
BaseMessage 参数:
| 参数 | 说明 |
|---|---|
| content | 消息的字符串内容 |
| additional_kwargs | 其他有效载荷数据,AI 消息可能含工具调用 |
| response_metadata | 响应元数据(响应头、令牌计数、模型名称等) |
| type | 消息类型,用于反序列化时识别 |
| name | 消息的人类可读名称(可选) |
| id | 消息的可选唯一标识符 |
BaseMessage 方法:
| 方法 | 说明 |
|---|---|
| pretty_print() | 打印消息的漂亮表示 |
| pretty_repr(html=False) | 获取消息的漂亮表示(可选 HTML 格式化) |
| text() | 获取消息的文本内容 |
4.3 缓存历史消息(多轮对话)
我们常常能体验到与大型语言模型 进行连贯的多轮对话的便利性。但目前我们的系统还不支持此功能:
python
import os
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
model = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
# 第一次对话
result = model.invoke([HumanMessage(content="你好,我叫小明")])
result.pretty_print()
# 第二次对话
result = model.invoke([HumanMessage(content="我叫什么名字?")])
result.pretty_print()
python
================================== Ai Message ==================================
你好,小明!很高兴认识你!😊
================================== Ai Message ==================================
你好!我并不知道你的名字哦~ 😊
可以发现,聊天模型 并不认识我们,更别说支持更多轮的对话了。稍作修改,让我们将 AI 回复 给我们的响应,跟着新的用户消息 一起发给聊天模型试试:
python
import os
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage
model = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
# 记录消息:把完整对话历史(含 AI 上次的回复)都列出来,模型才能"记得"
messages = [
HumanMessage(content="你好,我叫小明"),
AIMessage(content="你好小明!有什么可以帮你的吗?"),
HumanMessage(content="我叫什么名字?"),
]
model.invoke(messages).pretty_print()
python
你刚才告诉我你叫**小明**呀!如果之后想改称呼或补充其他信息,随时告诉我哦~ 😄
从结果可知,只要将历史消息,重新发送给聊天模型,那么就可以实现多轮对话的功能。
4.4 管理历史消息
上下文窗口
管理历史消息 ,无非就是理解如何"管理","管理"也就是一些 CRUD(Create、Read、Update、Delete) 操作。在了解如何管理消息之前,需要先了解多轮对话的核心概念:上下文窗口 。上下文窗口可以理解为模型的"短期工作记忆区 ",即 LLM 在一次处理请求时,所能查看和处理的最大 Token 数量,它包含了:
-
用户的输入
-
大模型的输出
-
有时还包括系统指令(SystemMessage) 和对话历史
不同大模型支持的上下文窗口大小不同。
Token
在自然语言处理(NLP) 中,Token 是文本的基本单位 。它不是完全等同于一个单词或一个汉字,而是一个更细粒度的划分 。计算机无法直接理解文字 ,它需要将文本转换为数字(向量)。Tokenization(令牌化) 就是这个转换过程的第一步 ,将句子分解成模型可以理解和处理的碎片。
| 语言 | Token 估算 |
|---|---|
| 英文 | 1 个 Token ≈ 4 个字符 / 0.75 个单词;1000 Tokens ≈ 750 个英文单词 |
| 中文 | 1 个汉字 ≈ 1.5~2 个 Tokens;1000 Tokens ≈ 500~700 个汉字 |
举个例子 :上下文窗口 就像一个固定大小的工作台 ,Token 好比积木零件 ,大模型好比工匠 。工匠需要拼出作品,必须把所需的零件(输入的 Token) 放在工作台上,一边拼装(生成回复),一边把拼好的部分(输出的 Token) 也放在工作台上。整个过程(输入 + 输出)中,工作台上的所有积木(Tokens) 总数都不能超过工作台的最大容量(上下文窗口大小) 。如果最初的零件(输入) 太多,占满了工作台,工匠就没有空间进行拼装了,这时你就需要减少零件(精简输入)。

消息裁剪(trim_messages)
有了上下文窗口 和 Token 的认知,再来看多轮对话的实现原理,其实就是:
-
输入 = 系统消息 + 对话历史 + 最新用户问题
-
对于模型来说,并不真正"记忆",而是每次都将完整的上下文重新输入。
由于所有模型的上下文窗口大小 都是有限的,这意味着作为输入的 Token 数量也是有限的。如果累积了很长的消息历史记录,则需要管理 传递给模型的消息的长度 。trim_messages (LangChain 提供)可用于将聊天历史记录的大小 减小为指定的令牌计数 或指定的消息数量。
基于输入 Token 数的修剪:
python
import os
from langchain_openai import ChatOpenAI
from langchain_core.messages import (
HumanMessage, SystemMessage, AIMessage, trim_messages,
)
model = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
# 历史消息记录(一长串对话,含"你好,我叫小明"......"我叫什么名字?")
messages = [
SystemMessage(content="你是一个好助手"),
HumanMessage(content="你好,我叫小明"),
# ......(多轮对话历史)......
HumanMessage(content="我叫什么名字?"),
]
# 自定义 token 计数函数
# 原因:token_counter 直接传 model 会调用 get_num_tokens_from_messages(),
# 该方法对 deepseek-chat 未实现(报 NotImplementedError),
# 所以改为逐条用 get_num_tokens()(DeepSeek 支持)再求和
def token_counter(msgs):
return sum(model.get_num_tokens(m.content) for m in msgs)
# 使用 trim_messages 减少发送给模型的消息数量
trimmer = trim_messages(
max_tokens=65, # 修剪消息的最大令牌数
strategy="last", # 修剪策略:"last"(默认)从后往前;"first" 从前往后
token_counter=token_counter, # 用自定义函数数 token
include_system=True, # 始终保留初始系统消息
allow_partial=False, # 是否允许拆分消息的内容
start_on="human", # 确保第一条非系统消息始终是 human
)
chain = trimmer | model
print(chain.invoke(messages))
python
content='很抱歉,我不知道您的名字。'
可以发现一开始介绍"我叫什么"的消息被删减了,因此 AI 无法识别。 修剪聊天记录后,生成的聊天记录(输入)应该有效 ,但需遵循对话模式原则:

修剪后的对话必须遵循的原则:
| 原则 | 实现方式 |
|---|---|
| 以 HumanMessage 或 SystemMessage 开头,且后跟 HumanMessage | 设置 start_on="human" |
| 以 HumanMessage 或 ToolMessage 结尾 | 设置 ends_on=("human", "tool") |
| ToolMessage 必须出现在涉及工具调用的 AIMessage 之后 | --- |
| 若原历史存在 SystemMessage,新历史必须包含它 | 设置 include_system=True |
基于消息数的修剪:
除了基于 Token 的修剪,还可以通过设置 token_counter=len 来根据消息数量 修剪聊天记录。在这种情况下,max_tokens 将控制最大消息数(即保留最近 N 条消息)。

消息过滤(filter_messages)
在更复杂场景 下,我们可能使用消息列表 来跟踪状态,例如只将完整消息列表的子集 传递给模型调用,而非全部历史记录 。filter_messages 方法可以轻松地按类型 、ID 或名称过滤消息:
python
from langchain_core.messages import filter_messages
def filter_messages(
messages: Iterable[MessageLikeRepresentation],
include_types: Optional[Sequence[Union[str, type[BaseMessage]]]] = None, # 要包含的消息类型
exclude_types: Optional[Sequence[Union[str, type[BaseMessage]]]] = None, # 要排除的消息类型
include_names: Optional[Sequence[str]] = None, # 要包含的消息名称
exclude_names: Optional[Sequence[str]] = None, # 要排除的消息名称
**kwargs # 其他可选参数(如 include_ids、exclude_ids 等)
) -> List[BaseMessage]:
# 功能:
# 根据消息类型(如 system、human、ai)或消息名称过滤消息列表。
# 常用于从对话历史中筛选出特定角色的消息,以精准控制模型输入。
# 参数说明:
# include_types: 要包含的消息类型,可为字符串('system','human','ai')或 BaseMessage 子类。
# exclude_types: 要排除的消息类型。
# include_names: 要包含的消息名称(如 "example_user")。
# exclude_names: 要排除的消息名称。
# **kwargs: 其他可选参数(include_ids、exclude_ids、exclude_tool_calls 等),用法类似。
# 返回值:
# List[BaseMessage]:过滤后的消息列表。
python
from langchain_core.messages import (
HumanMessage, SystemMessage, AIMessage, filter_messages,
)
# 历史消息记录(带 id,方便按 id 筛选)
messages = [
SystemMessage("你是一个聊天助手", id="1"),
HumanMessage("示例输入", id="2"),
AIMessage("示例输出", id="3"),
HumanMessage("真实输入", id="4"),
AIMessage("真实输出", id="5"),
]
# 按类型进行筛选:只保留 human(HumanMessage) 类型
print(filter_messages(messages, include_types="human"))
# 结果:[HumanMessage(content='示例输入', ..., id='2'), HumanMessage(content='真实输入', ..., id='4')]
# 按类型+ID 进行筛选:保留 human 和 AI(AIMessage),但排除 id="3"
print(filter_messages(messages, include_types=[HumanMessage, AIMessage], exclude_ids=["3"]))
# 结果:[HumanMessage(content='示例输入', ..., id='2'), HumanMessage(content='真实输入', ..., id='4'),
# AIMessage(content='真实输出', ..., id='5')]
消息合并(merge_message_runs)
若消息列表 中存在连续相同类型 的消息,但某些模型不支持 传递相同类型的连续消息。对于这种情况,可以使用 merge_message_runs 方法轻松合并相同类型的连续消息:
python
rom langchain_core.messages import merge_message_runs, BaseMessage
def merge_message_runs(
messages: Iterable[BaseMessage], # 待合并的消息序列
chunk_separator: str = "\n", # 合并时连接消息内容的间隔符,默认换行
) -> List[BaseMessage]:
# 功能:
# 将连续的消息合并为一条消息。如果连续的多个消息具有相同的类型(如 HumanMessage、AIMessage 等),
# 则将其内容连接在一起,形成一条合并后的消息,以简化对话历史并减少 API 调用开销。
# 常见场景:将用户多轮连续输入合并为一个 HumanMessage,或将模型连续输出合并为一个 AIMessage。
# 参数说明:
# messages: 待合并的消息对象列表。
# chunk_separator: 合并时用于连接多条消息内容的间隔字符串,默认为换行符 "\n"。
# 返回值:
# List[BaseMessage]:合并后的消息列表(相邻同类型消息被合并)。
python
import os
from langchain_openai import ChatOpenAI
from langchain_core.messages import (
HumanMessage, SystemMessage, AIMessage, merge_message_runs,
)
model = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
# 历史消息记录(存在连续的同类型消息,会被合并)
messages = [
SystemMessage("你是一个聊天助手。"),
SystemMessage("你总是以笑话回应。"),
HumanMessage("为什么要使用 LangChain?"),
HumanMessage("为什么要使用 LangGraph?"),
AIMessage("因为当你试图让你的代码更有条理时,LangGraph 会让你感到"节点"是个好主意!"),
AIMessage("不过别担心,它不会"分散"你的注意力!"),
HumanMessage("选择LangChain还是LangGraph?"),
]
# 方式一:直接调用函数合并(不建链)
merged = merge_message_runs(messages) # 把连续同类型消息合并(结果存在 merged 里)
model.invoke(messages).pretty_print() # 直接调模型(合并与否,模型回答一样)
# 方式二:把合并器接进链里(Runnable 写法)
merger = merge_message_runs() # 不传 messages,先构造合并器(可复用,合并"连续相邻"的相同类型消息)
chain = merger | model # 链:先合并消息 → 再调模型
chain.invoke(messages).pretty_print() # 效果和方式一等价,但可以继续往后接其他环节

五、复盘(附答案)
💡 思考题
-
with_structured_output 是什么?它支持哪三种输出结构?用 DeepSeek 时必须指定哪个 method,为什么?
-
include_raw 和 strict 参数各有什么作用?
-
invoke 和 stream 有什么区别?AIMessageChunk 是什么?
-
同步、多进程、多线程、协程有什么区别?为什么说协程适合 I/O 密集场景?
-
为什么聊天模型"记不住"历史?多轮对话的实现原理是什么?trim_messages / filter_messages / merge_message_runs 各解决什么问题?
📝 答案
-
with_structured_output 是聊天模型的方法:传入输出结构 ,返回一个执行后输出"对象/字典"而非字符串的 Runnable。三种结构:Pydantic (返回对象)、TypedDict (返回字典)、JSON Schema (返回字典)。用 DeepSeek 必须指定 method="function_calling",因为默认的 json_schema 方式 DeepSeek 不支持(会 400 报错)。
-
include_raw=True :返回含
raw(原始 BaseMessage)、parsed(解析结果)、parsing_error(解析异常)三个键的字典,解析出错也被捕获而不抛异常;strict=True:保证模型输出与 schema 完全匹配,输入 schema 也会被验证。 -
invoke 等模型全部生成完,一次性返回全量内容(像收快递);stream 逐块返回,每块是一个 AIMessageChunk(AIMessage 的一部分/消息块),可实时显示或用 for 循环收集拼接。AIMessageChunk 支持相加合并成完整消息。
-
同步 :一件事做完再做下一件,等待时阻塞;多进程 :多核并行,进程间通信麻烦;多线程 :进程内多任务,OS 调度,需锁保护共享资源;协程 :用户态"轻量级线程",程序员/语言控制切换,上下文切换开销低 ,I/O 密集(如网络请求)时用
async def+await让事件循环去调度其他任务,适合并发等待场景。 -
聊天模型本身不记忆 ------每次调用只看到当次输入。多轮对话的原理是把完整历史(含 AI 历史回复)重新发送给模型 。三个工具:trim_messages 把过长历史裁剪到指定 Token 数/消息数(防止超出上下文窗口);filter_messages 按类型/ID/名称过滤出需要的消息子集;merge_message_runs 合并连续的同类型消息(有些模型不支持连续同类型消息)。
🎯 闭幕

从"结构化输出"到"流式传输",从"消息格式"到"历史管理"------这一篇帮你把 LangChain 的进阶用法走了一遍。
如果本文对你有帮助,欢迎:
👍 点赞 | ⭐ 收藏 | 👤 关注作者 | 💬 留言交流你的疑问或补充
你的每一次互动都是我继续更新的动力,我们下一篇见!🚀
