GLM-5.3-FlashX 上下文接近上限被截断 / 先看请求里的历史轮数

文章导读
多轮对话里 GLM-5.3-FlashX 表现像“忘了前面说过的话”,先别急着归因到模型能力。更常见的情况是客户端或服务在构造请求时,把过长的历史按某种策略截掉了,模型拿到的输入里根本没有那段内容。判断顺序建议是:先看这次请求实际带了多少条历史消息、估算占用多少 token、距离上下文窗口还有多少余量;再看裁剪逻辑有没有误删关键轮次;最后才考虑用摘要或检索来压缩历史。
📋 目录
  1. Ⅰ 在请求日志里打印每次发送的历史消息条数和估算 token 数
  2. Ⅱ 检查历史消息裁剪逻辑是否误删关键轮次
  3. Ⅲ 用摘要或向量检索替换完整历史
  4. Ⅳ 在会话记录里标记被截断的位置
A A

多轮对话里 GLM-5.3-FlashX 表现像“忘了前面说过的话”,先别急着归因到模型能力。更常见的情况是客户端或服务在构造请求时,把过长的历史按某种策略截掉了,模型拿到的输入里根本没有那段内容。判断顺序建议是:先看这次请求实际带了多少条历史消息、估算占用多少 token、距离上下文窗口还有多少余量;再看裁剪逻辑有没有误删关键轮次;最后才考虑用摘要或检索来压缩历史。

适用场景:多轮对话中模型开始忽略早期约束、设定或工具返回结果,怀疑历史被截断。操作动作:在发请求前打印历史消息条数、字符数与估算 token 数,并记录每条消息是否真的被发送。验证方式:拿会话记录逐轮比对,找出第一条标记为未发送或被摘要替换的消息,看它是否正好对应模型“遗忘”的那部分内容。风险边界:估算 token 与真实用量有偏差,应以服务端返回的用量字段为准;不同部署的窗口上限与裁剪策略可能不同,需要结合环境确认。

在请求日志里打印每次发送的历史消息条数和估算 token 数

这一步只做一件事:让每次请求的真实输入可见。日志里建议至少落这些字段,字段名可以按自家规范改,但语义要保留。

{
  "trace_id": "a1b2c3",
  "model": "glm-5.3-flashx",
  "history_messages": 24,          // 实际发送的消息条数
  "history_turns": 11,             // 折算的对话轮数
  "prompt_chars": 18320,           // 所有 content 的字符数合计
  "est_tokens": 12600,             // 估算 token
  "max_context": 32768,            // 本环境配置的窗口上限
  "usage_ratio": 0.385,            // est_tokens / max_context
  "finish_reason": "stop",          // 服务端返回,用于判断是否被截断
  "server_prompt_tokens": 12480    // 服务端返回的真实用量,优先信这个
}

估算 token 不必追求精确,够用来判断量级即可。保守做法:中文按接近 1 字 1 token 计,英文与代码按 4 个字符 1 token 计,再对全量消息求和。usage_ratio 超过某个阈值(比如 0.8)时就该警惕,而不是等它撞到上限。如果环境里能装通用的 tokenizer 库,或模型接口本身提供 count_tokens 一类的计数入口,优先用它们;两者都拿不到时,字符数估算是可接受的兜底。同时关注返回里的 finish_reason:正常结束和被长度限制打断,含义完全不同。

检查历史消息裁剪逻辑是否误删关键轮次

排除完上限问题,接下来看裁剪策略本身。常见的几种:保留最近 N 轮;按 token 预算从最新往旧倒序保留;固定保留 system 消息加首轮加最近 N 轮;按角色区分,用户消息优先于助手消息。最容易出事的是第一种——它假设“越近越重要”,但用户在第一轮给出的格式约束、身份设定,以及中途某次工具调用的返回结果,往往正好被砍掉。

GLM-5.3-FlashX 上下文接近上限被截断 / 先看请求里的历史轮数
def trim(messages, budget):
    kept, used = [], 0
    for m in reversed(messages):          # 从最新往旧遍历
        c = estimate(m)
        if used + c > budget and kept:
            break                          # 预算不够就停,不半条塞进去
        if c > budget:                     # 单条就超预算的长消息
            m = shrink_single(m, budget)   # 单独截断或摘要,别整条丢弃
            c = estimate(m)
        kept.append(m)
        used += c
    kept.reverse()
    return kept

写这段逻辑时有两个坑值得盯一下:一是预算不够时直接 break,会把一条本来放得下的短消息也挡在外面,可以改成跳过当前条继续往前试;二是超长单条消息,如果只判断条数不看长度,容易出现“保留了 10 条,其中 1 条吃掉了全部预算”。改完后,把被丢弃的消息 id 一并打进日志,后面定位遗忘点会省很多事。

用摘要或向量检索替换完整历史

如果历史确实长到裁剪无法兼顾,就用摘要或检索来替代完整历史。通用骨架如下,替换项是摘要模型、检索条数 top_k 和归档存储,其余部分按业务调整即可。

GLM-5.3-FlashX 上下文接近上限被截断 / 先看请求里的历史轮数
def build_prompt(session, question, budget):
    recent = trim(session.messages, budget.recent)
    older = session.messages[: len(session.messages) - len(recent)]
    summary = session.summary or summarize(older)   # 摘要模型可替换
    hits = vector_search(question, session.archive, top_k=3)
    return [
        {"role": "system", "content": BASE_PROMPT + "\n【历史摘要】\n" + summary},
        {"role": "user",   "content": "【相关资料】\n" + "\n".join(hits)},
        *recent,
        {"role": "user",   "content": question},
    ]

拼接位置有讲究:历史摘要放 system 段,作为长期背景;检索命中的片段放最新一条用户提问之前,作为本轮临时证据;最近几轮原文照常保留,保证指代和追问不断线。验证方式是每次组完 prompt 后把摘要单独打出来人工看一遍,确认用户给的硬性约束还在里面;摘要一旦丢了约束,后面几轮会持续跑偏。检索这边要注意去重和长度上限,命中的片段加起来不能把留给最近几轮原文的预算挤光。

在会话记录里标记被截断的位置

最后一步是把上面几项拼成可对照的会话记录。每条消息都记清楚它有没有被发送、发送时占多少 token、累计到多少,以及没发送的原因。

{"session_id": "s-001", "turn": 12,
 "messages": [
   {"role": "system", "sent": true,  "est_tokens": 90,  "cum_tokens": 90,   "trim_reason": null},
   {"role": "user",   "sent": true,  "est_tokens": 42,  "cum_tokens": 132,  "trim_reason": null},
   {"role": "assistant", "sent": true, "est_tokens": 210, "cum_tokens": 342, "trim_reason": null},
   {"role": "user",   "sent": false, "est_tokens": 380, "cum_tokens": null, "trim_reason": "budget_exceeded"},
   {"role": "assistant", "sent": "summary", "est_tokens": 120, "cum_tokens": 462, "trim_reason": "replaced_by_summary"}
 ]}

有了这份记录,定位遗忘点就变成一次查询:翻到模型回复开始偏离的那一轮,往回看它依赖的那条消息是 sent: true 还是被标记为 budget_exceeded 或 replaced_by_summary。如果被丢的正是用户给出的约束,问题在裁剪策略;如果消息明明发送了、模型仍然没遵守,那就要往 prompt 组织方式或消息顺序上查。建议在调试期把这份记录按会话落盘,线上则只保留计数和标记位,避免日志里堆积大量原文。