☰
LangChain 会话裁剪实测:为什么当前问题也被裁掉了
2026/10/14 9:51:22 网站建设 项目流程

版权与内容来源声明
本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容,均在附表 A 中标注来源;引用官方原文保持原样,不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准,标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式,也不对任何收益结果作承诺。转载请注明出处。

第 1 章 先说结论:裁剪完,第一眼看「首条是什么消息」

对话越聊越长,消息列表越堆越大,某一天请求发出去,接口会回你一句「超出上下文长度」。上下文窗口(模型单次能接收的内容上限,通常按 token 计)不是无限的,于是你需要在发送前把历史消息截短一点。LangChain 里干这件事的函数叫trim_messages,直译就是「裁剪消息」。

裁剪本身不难,难的是——剪完之后那份列表,模型还能不能看懂。

1.1 一个很容易被忽略的后果

本文要讲的核心现象是:把trim_messages用默认参数跑一遍,系统的角色设定会被整条扔掉,裁剪结果的第一条会变成 AI 的消息。模型收到的就不是「一份带系统指令的对话」,而是一段从 AI 的话开始的、没头没尾的聊天记录。

顺便分清两个词:AI 消息是模型自己之前说过的话,系统消息承载你写给模型的角色设定与规则。前者是模型的台词,后者是模型的剧本。

更极端的一种情况在标题里:预算压到极小的时候,只剩下系统提示,用户当前问的这句话被丢掉了。你以为是「保留最重要的」,实际保留的是最没用的那一条。

这两件事都不是我推的,是本机跑出来的。下面所有结论都对应一段可复现的记录。

1.2 本文实测的环境与做法

本文所有数值来自本机脚本work/2026-10-14_trim_messages实测.py(日志work/2026-10-14_trim_messages实测.log)。全部本地计算,不调用任何模型接口,没有 API key 也能复现。

项目值
Python3.13.14
langchain-core1.6.6
API key无(全部本地计算)
实测脚本work/2026-10-14_trim_messages实测.py
日志work/2026-10-14_trim_messages实测.log

做法是:先造一段带角色设定的对话历史——1 条系统提示 + 9 条对话消息(5 条人类消息、4 条 AI 消息),共 10 条;再用同一份历史、只改trim_messages的参数,跑 7 种配置,逐条看「保住了几条」和「首条是什么类型的消息」。这段历史长这样,后面所有结论都是拿它跑出来的:

fromlangchain_core.messagesimporttrim_messages,HumanMessage,AIMessage,SystemMessage msgs=[SystemMessage("你是 AI 应用开发助教,回答要给出可验证的判据。"),HumanMessage("我在做 RAG,文档切完 1200 块。"),AIMessage("先确认召回:只看向量相似度不够,要看命中位置。"),HumanMessage("那 rerank 要加吗?"),AIMessage("先量基线:不加 rerank 时的命中率。"),HumanMessage("基线怎么量?"),AIMessage("准备 30 条真实问题,标出应命中的块。"),HumanMessage("好了,然后呢?"),AIMessage("看两列:候选里有没有正确块、正确块排第几。"),HumanMessage("现在我想把问题问得更长一点。"),]

最后那条人类消息,就是「用户此刻真正想问的东西」;它在不在裁剪结果里,是第 4 章那个坑的关键。

为什么偏偏盯「首条类型」这个指标?因为主流聊天模型对输入格式有一个隐含要求:对话历史要么以人类消息开头,要么以「系统消息 + 人类消息」开头。这是 LangChain 官方 API 文档里明说的。第一条错了,后面全对也没用。

第 2 章 先弄清四个旋钮:trim_messages 的参数和默认值

看别人的排错记录容易晕,是因为默认值都藏在函数签名里。先把参数摊开,后面所有现象都能对上号。

2.1 七个参数,人话版

trim_messages的参数并不多,但每一个都会改变「剪完之后剩什么」。按官方 API 文档(截至 2026-10-14)的记录,签名里的可选项与默认值如下表。

参数作用(人话)官方默认值
max_tokens裁完之后,整份消息最多允许占多少 token必填,无默认
token_counter用谁去数 token(函数、模型,或字符串 “approximate”)必填
strategy从尾部往前留,还是从头部往后留“last”
include_system是否保住第 0 条的系统提示False
start_on结果从哪种类型的消息开始None
end_on结果在哪种类型的消息收尾None
allow_partial允许不允许把一条消息切成半截False

这里几个词第一次出现,各给一句解释:

  • token:模型处理文本的最小单位,大致可以理解成「一个词或半个词」,中文里通常 1 个字只占零点几个 token。
  • token_counter:数 token 的工具。传"approximate"表示用内置的快速近似算法,不追求精确。
  • strategy:裁剪方向。"last"是保最近、丢最早的(聊天场景通常用这个),"first"是保最早、丢最近的。
  • include_system:系统提示(SystemMessage)里通常写着「你是谁、按什么规矩回答」,这条丢了,模型的人格设定就没了。

2.2 官方文档自己给了「推荐配置」

值得注意的是,官方 API 文档并没有把这些默认值当成推荐值,它明确写了两条意图:

  1. 大多数聊天模型期望对话从「人类消息」或「系统消息 + 人类消息」开始 —— 要达成这一点,请设置start_on='human'。
  2. 如果原始历史里有系统提示,新的历史通常应该把它保留下来,因为系统提示承载着给模型的特殊指令 —— 要达成这一点,请设置include_system=True。

也就是说,该开的东西默认是关的。默认值给的是一套「最少假设」的行为,不是一套「能直接上线」的行为。这就是第 3 章那张对照表里,第一行会出问题的原因。默认值本身没有错,它要服务的是所有场景;问题只在于聊天场景下,关键参数默认是关的。

第 3 章 七种配置实测对照:问题出在哪一行

先把完整对照表放上来,再逐行解释。表里的「首条类型」就是我们第 1 章说的那个验收指标。

3.1 七种配置 × 裁剪结果

实测条件统一为:同一份 10 条消息的历史,token_counter="approximate",除表中写明的参数外不动其他值;预算为 40 token。

配置保留条数首条类型系统提示是否保住
默认(strategy=‘last’,include_system=False)4AIMessage被整条裁掉
include_system=True4SystemMessage保住
include_system=True + start_on=‘human’4SystemMessage保住
strategy=‘first’ + include_system=True——抛出取值错误,直接中断
strategy=‘first’(不带 include_system)4SystemMessage保住
max_tokens=1 + include_system=True + start_on=‘human’1SystemMessage保住,但当前问题被丢
end_on=‘ai’,且整段没有 AI 消息0无(空列表)不适用,静默返回空

这张表最有价值的地方,是把「看起来差不多的几组参数」分成三种命运:正常、报错、静默出错。逐行看。

3.2 第一行:为什么默认配置会把系统提示扔掉

第一行是默认调用,也就是很多人第一次写代码时的样子:

out=trim_messages(msgs,max_tokens=40,token_counter="approximate")print(len(out),out[0].__class__.__name__)# 4 AIMessage

日志里这一行的结果是:保留 4 条,首条是AIMessage,内容是一句「准备 30 条真实问题,标出应命中的块。」——这是对话中段的一句 AI 回复。系统提示那条SystemMessage不见了。

为什么会这样?strategy='last'的语义是「从尾部往前凑够 max_tokens」,它一路往回收,收到第 0 条时,include_system=False意味着第 0 条不享受任何豁免,于是跟着一起被切掉。默认值在这里没有兜底,它只是「按字面执行」。

后果很直接:模型收到的第一句话是 AI 说的,它会把这理解成「对话已进行到一半」,你的角色设定、输出格式要求、禁止事项全部失效。

3.3 第二、三行:打开 include_system 就对了

同一份数据,只加一个参数:

out=trim_messages(msgs,max_tokens=40,token_counter="approximate",include_system=True,start_on="human")print(len(out),out[0].__class__.__name__)# 4 SystemMessage

结果仍然是 4 条,但首条变成了SystemMessage,系统提示一句话都没丢。第三行再叠一个start_on='human'(结果必须从人类消息开始),实测结果与第二行完全一致。

这里有个官方文档里的细节能解释它:start_on的说明里写着,当include_system=True时,start_on不作用于第 0 条的系统消息。所以「系统消息 + 人类消息」这个合法开头不会被start_on='human'破坏。这一点让人放心:两个参数不打架。

另外留意一个容易看走眼的细节:第二行和第三行保留的都是 4 条,跟第一行留下的 4 条在内容上也高度重叠,但第一行丢的是系统提示,后两行丢的是一段普通对话。所以「保留条数」这个指标本身说明不了问题——留了 4 条不代表留对了 4 条,必须配合「首条类型」一起看。

不过start_on的官方说明里也写了「只应在strategy='last'时指定」——这类限制在第 4 章会咬人。

第 4 章 三个必须单独讲的坑

第 3 章那张表里,有三行不是「结果不同」,而是「行为性质不同」——一个是直接中断,一个是静默丢数据,一个是静默返回空。排障时最耗时间的往往不是报错,而是后两种:它们不会在代码里留下显眼痕迹,只表现为「结果不太对」,而「不太对」在聊天场景里太容易被误判成「模型状态问题」。

4.1 strategy=‘first’ 叠加 include_system=True:直接中断

很多人会想:「我既想保住系统提示,又想保住最早那几轮设定,那就strategy='first'加include_system=True呗。」这条路走不通,实测直接抛出取值错误:

配置:strategy="first" 同时带上 include_system=True 结果:抛出取值错误(ValueError) 转述:include_system 参数只在 strategy='last' 时有效

注意措辞是「只在 strategy=‘last’ 时有效」,不是「不建议」。这是硬校验,不符合就中断。所以如果你需要从头部保留(比如保留开头的设定、丢掉中间),就只能放弃include_system,不能再指望它帮你保系统提示。实测里单独用strategy='first'(不带include_system)时,保留的是前 4 条、且首条恰好就是系统提示——但这是「因为系统提示本来就在最前面」,不是被你保下来的,两者含义完全不同:一旦你的历史结构变了,它就不一定还在。

4.2 max_tokens=1:只剩系统提示,当前问题被丢掉

这是标题里那半句的来源。把预算压到 1 token:

out=trim_messages(msgs,max_tokens=1,token_counter="approximate",include_system=True,start_on="human")print(len(out),out[0].__class__.__name__)# 1 SystemMessage

结果只有 1 条,就是那条SystemMessage。用户当前那句「现在我想把问题问得更长一点。」——被丢掉了。

这一步很反直觉:直觉上「保系统提示」是对的,但include_system=True意味着系统提示优先占用预算,预算太小时它就把用户当前问题挤掉了。

值得警惕的是:这个结果不会报错。请求照样发出去、模型照样回你,只是内容跟这轮问的没关系——表现为「答非所问」,而日志里是一个完全正常的 200 响应。

4.3 end_on=‘ai’ 但没有 AI 消息:静默返回空列表

第三种最阴。end_on的作用是「结果在指定类型的消息处收尾」。实测给了一段只有两条人类消息、没有任何 AI 消息的历史,再要求end_on='ai':没有抛错,也没有警告,函数平静地返回了 0 条——一个空列表。

为什么会这样?按官方对end_on的说明,指定类型之后的消息一律忽略;这段历史里 AI 消息一次都没出现,「最后一次出现」无从谈起,收尾位置落到开头之前,列表就被清空了。这个推导来自官方语义,而「返回空列表」是本机实测确认的现象。

麻烦的是后续影响:下游没有校验就会拿空列表去拼请求;写了if trimmed:之类的判断,则会悄悄走进兜底分支——你看到的是一次降级,却找不到降级的原因。

后两种坑的共同点是:它们不打断流程。第一种坑(抛错)反倒是最安全的,因为它逼你当场修。

第 5 章 计数口径:max_tokens 到底该给多少

前文反复出现「40 token」「1 token」,这些数怎么算出来的?max_tokens是裁剪的尺子,尺子不准,结论就会偏。

5.1 approximate 的中英文字符/token 比

实测调用count_tokens_approximately(就是token_counter="approximate"背后那个函数),得到下面这组口径。

文本样本字符数估算 token约合字符/token
中文(含中文标点,重复拼接)13638约 3.58
英文(重复拼接)25267约 3.76
短中文「模型上线前先量基线」97—
短英文「before go live measure the baseline」3513约 2.69

大样本上,中文大约 3.58 个字符折算 1 个 token,英文大约 3.76 个字符折算 1 个 token。两者很接近,都在 3.5~3.8 这个区间——这正是近似算法「不追求精确、追求快」的体现。

5.2 短句上的偏差值得注意

真正要记的是那行短中文:「模型上线前先量基线」一共 9 个字符,估算结果是 7 个 token。按 3.58 的比例,9 个字符本应只要约 2.5 个 token,实际却给了 7。

原因不难理解:近似计数会为每条消息加上固定的前后缀开销,短句里这部分占比被放大,所以「越短越不准」。这条现象的实用含义是——如果你按字符数去除以 3.58 来反推 token,短消息会严重低估。官方文档也提到,"approximate"这个快捷方式是给「不在关键路径上、不需要精确计数」的场合用的(截至 2026-10-14)。

5.3 那把尺子该怎么定

给一条可执行的做法,别把max_tokens当成一个凭感觉填的数字:

  1. 先确认模型侧的上下文上限,再给它留出输出预算(模型回复也要占额度),把剩下的当作输入预算。
  2. 输入预算里,再留一段安全余量给「估算偏差」——特别是历史里短消息多的时候,approximate会偏高或偏低。
  3. 用你真实的对话数据跑一遍,打印裁剪前后的条数与首条类型,看有没有被削掉关键消息。

再补一个实操细节:max_tokens的语义是「裁剪后整份列表的总量上限」,不是「本轮新增消息的上限」,也不等于模型侧的上下文上限,它只约束历史这一段。直接把模型的上下文上限填进max_tokens,等于把输出空间也当成了历史预算。

会话裁剪的参数速查与断言清单:本章的计数口径和第 6 章的检查清单可以合成一页放在手边,改代码时对着核一遍。放在资料包里,扫码即可获取:

第 6 章 会话裁剪的检查清单(可直接照做)

前面讲的是「会出什么事」,这一章讲「怎么保证不出事」。核心思路只有一句:裁剪不能只调用,必须断言。

6.1 裁剪后必须断言的五件事

断言项为什么必须断言不通过时怎么办
结果非空end_on与消息类型不匹配会静默返回空列表检查end_on是否和实际会出现的消息类型一致,或干脆不传
首条是 SystemMessage 或 HumanMessage模型期望对话以此开头,否则角色设定丢失打开include_system=True,并加start_on='human'
末条是 HumanMessage 或 ToolMessage对话应以「待回答的一方」收尾用end_on=('human','tool')收口
末条包含用户本轮输入预算过小时当前问题会被挤掉先扣掉本轮输入的长度,再给历史留预算
保留条数与预期相符参数组合互相覆盖时条数会异常打印条数,与上一次对比

这五条里,第 2 条和第 4 条就是本文开头那两个现象的直接对策。

6.2 什么时候必须打开 include_system

不是所有场景都需要系统提示,但下面这些场景必须开:

  • 你的系统提示里写了输出格式(比如「只返回 JSON」)——关了它,解析器大概率要失败。
  • 系统提示里写了角色边界与禁止事项——关了它,拒答规则一起消失。
  • 你用start_on='human'来规范开头——此时不开include_system,系统提示必然被切掉。

反过来说,如果这段历史根本没有系统提示(比如纯工具调用流水),开不开都一样,此时更该关注的是末条类型。

6.3 怎么避免「静默空列表」

三个动作,成本很低:

  1. 裁剪后立刻断言非空,并且让断言带上一句人话提示,而不是让它抛一个没有上下文的错误。
  2. 如果用了end_on,先问自己一句:这段历史里一定会出现这种类型的消息吗?不一定就别传。
  3. 在日志里固定打印「裁剪前条数 / 裁剪后条数 / 首条类型」三件套。静默类问题只能靠这三个数看出来。

下面这个封装把上面的断言都收进去了。它没有在本机跑过,是按前面的实测结论写的,你可以直接拿去改成自己的版本。

⚠️代码待验证

deftrim_with_guard(messages,max_tokens,token_counter="approximate"):trimmed=trim_messages(messages,max_tokens=max_tokens,token_counter=token_counter,strategy="last",include_system=True,# 保住系统提示start_on="human",# 结果从人类消息开始)asserttrimmed,"裁剪结果为空:检查 max_tokens 过小,或 end_on 与消息类型不匹配"first=trimmed[0].__class__.__name__assertfirstin("SystemMessage","HumanMessage"),f"裁剪后首条是{first},角色设定可能已丢失"asserttrimmed[-1].__class__.__name__=="HumanMessage","末条不是人类消息,模型可能答非所问"returntrimmed

第 7 章 本文和「消息模板」那篇不是一回事

写到这,有必要把本文和本号前一篇 LangChain 文章区分清楚,免得你按错的那篇去改代码。

7.1 两篇测的对象完全不同

对比项消息模板那篇本篇
测的对象消息模板 / 变量填充会话历史裁剪
关心的函数模板类与提示词构造trim_messages
判据变量有没有正确填进去裁剪后首条是什么类型的消息
典型症状提示词搬进代码后失效系统提示被裁掉 / 当前问题被裁掉
修复方向改模板与变量传递改裁剪参数并加断言

一句话概括:那篇解决「怎么把提示词拼对」,这篇解决「拼好之后怎么在长度限制下留住该留的」。两篇都会碰到「模型不听话」,但根因和改法在完全不同的地方。

7.2 落地建议:把裁剪当成一个有契约的函数

从实测看,trim_messages更像一个「忠实执行指令的工具」,不会替你想「系统提示该不该留」,也不会替你判断「结果是不是空的」。落地时建议:

  • 不要到处散着调trim_messages,封装成一个函数,把本文第 6 章的断言写进去;参数定下来后加注释说明为什么这么配,否则下一个人很容易「顺手」删掉include_system=True。
  • 把「裁剪前条数 / 裁剪后条数 / 首条类型」打进日志,作为长期监控项。

裁剪策略还应该可观测:一旦某轮响应开始答非所问,你可以先从日志确认「这一轮的首条类型是什么」,很快就能把方向缩到裁剪这一段,而不用去猜模型本身是不是不稳定。

7.3 小结:三条可以直接背下来的结论

  1. 默认配置(strategy='last'、include_system=False)会把系统提示整条裁掉,结果首条变成 AI 消息。
  2. include_system=True才能保住系统提示,且结果首条是系统消息;把它和strategy='first'叠加会直接抛出取值错误。
  3. 预算压到极小时只剩系统提示、当前问题被丢;end_on与消息类型不匹配则不报错、返回空列表——两种都是静默出错,必须靠断言兜住。

转行做 AI 应用开发的路径图:如果你是从别的技术岗转过来的,这份路线图标出了「会话与上下文管理」这一类工程基本功该在哪个阶段补,以及哪些可以先跳过。放在资料包里,扫码即可获取:

附表 A:本文引用事实与出处对照表

事实出处本文位置
trim_messages 的参数、默认值与语义(strategy 默认 ‘last’、include_system 默认 False、start_on/end_on 默认 None、allow_partial 默认 False)LangChain API Reference,LangChain 官方,检索入口 python.langchain.com(实际跳转至 reference.langchain.com/python/langchain-core/messages/utils/trim_messages),截至 2026-10-14第 2 章 表 2
官方建议:多数聊天模型期望历史以「人类消息」或「系统消息 + 人类消息」开头,需设置 start_on=‘human’同上第 2 章 2.2
官方建议:若原历史含系统消息,新历史通常应保留,需设置 include_system=True同上第 2 章 2.2
官方说明:include_system 只在 strategy=‘last’ 时有效;start_on 也只应在 strategy=‘last’ 时指定同上第 2 章 2.1、第 3 章 3.3、第 4 章 4.1
官方说明:include_system=True 时 start_on 不作用于第 0 条系统消息同上第 3 章 3.3
官方说明:token_counter 传 “approximate” 即使用 count_tokens_approximately,适合不要求精确计数的路径同上第 5 章 5.2
默认配置裁剪后保留 4 条、首条为 AIMessage、系统提示被整条裁掉本机实测(work/2026-10-14_trim_messages实测.py;langchain-core 1.6.6 / Python 3.13.14)第 3 章 3.2
include_system=True 裁剪后保留 4 条、首条为 SystemMessage、系统提示保住本机实测(同上)第 3 章 3.3
include_system=True 叠加 start_on=‘human’ 结果与上一行一致本机实测(同上)第 3 章 3.3
strategy=‘first’ 叠加 include_system=True 抛出取值错误(信息:include_system 参数只在 strategy=‘last’ 时有效)本机实测(同上)第 3 章 表 1、第 4 章 4.1
strategy=‘first’ 不带 include_system 时保留前 4 条(含 SystemMessage)本机实测(同上)第 4 章 4.1
max_tokens=1 + include_system=True + start_on=‘human’ 时只剩 1 条 SystemMessage,用户当前问题被丢本机实测(同上)第 3 章 表 1、第 4 章 4.2
end_on=‘ai’ 但整段没有 AI 消息时不抛错、返回空列表本机实测(同上)第 3 章 表 1、第 4 章 4.3
近似计数口径:中文 136 字符 → 38 token(约 3.58 字符/token);英文 252 字符 → 67 token(约 3.76 字符/token)本机实测(同上)第 5 章 5.1
短中文「模型上线前先量基线」9 字符 → 估 7 token;短英文 35 字符 → 13 token本机实测(同上)第 5 章 5.2

附表 B:术语速查表

术语一句话解释在本文哪里用到
LangChain一套搭建大模型应用的开发框架,把模型调用、提示词、工具、记忆等拆成可组合的模块全文
langchain-coreLangChain 的基础抽象层,消息类型与 trim_messages 都在这里第 1 章 1.2
trim_messages按 token 数或消息数把对话历史截短的函数第 1 章起
token模型处理文本的最小单位,中文里通常 1 个字只占零点几个 token第 2 章 2.1
token_counter数 token 的工具,可传函数、模型,或字符串 “approximate”第 2 章 2.1
SystemMessage系统提示,承载角色设定与输出规则,一般在历史第 0 条第 2 章 2.1
HumanMessage / AIMessage分别代表用户说的话与模型说的话第 2 章 2.1
strategy裁剪方向:‘last’ 保最近,‘first’ 保最早第 2 章 2.1
include_system是否保住第 0 条系统提示,默认关闭第 2 章 2.1
start_on / end_on规定结果从哪种消息开始、在哪种消息收尾第 2 章 2.1
allow_partial是否允许把一条消息切成半截再保留第 2 章 2.1
approximate内置的快速近似计数方式,快但不精,短消息偏差较大第 5 章
断言(assert)在代码里写下一句「这里必须是这个样子」,不满足就立刻中断第 6 章

写在最后:这篇用到的资料

写这篇文章时,把相关的官方文档和源码又翻了一遍,顺手也整理了几份配套的东西:

  • 大模型学习路线图:从零基础到能自己动手做 Agent,按阶段说明每一步该学什么、哪些可以先跳过
  • 《LangChain + LangGraph + MCP 智能体开发实战》视频课:7 个模块,从私有化部署、Embedding+RAG 到 MCP+Agent 全流程
  • AI 大模型知识库(在线可查):Agent Skills 从入门到落地、Claude Skills 完全指南等专题,按目录浏览即可
  • 640 套 AI 大模型行业报告 + 经典 PDF 书籍:看行业落地案例和别人怎么做的时候用得上
  • 大模型零基础到精通教学视频:跟着敲一遍,比只读文档快得多

资料是我自己整理的,放在下面这个码上,扫码即可获取:






添加时备注「AI」,优先通过。

资料按「先路线、再动手、最后查漏」的顺序整理好了,建议先看学习路线那一份,照着它挑一条适合自己当前基础的路径再往下看。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询