在构建基于大模型(LLM)的对话系统时,开发者常会遇到一个核心问题:

随着对话轮数不断增加,输入上下文会无限膨胀,最终导致 Token 过载、模型报错或性能下降。

这意味着:我们不能无限制地把所有历史对话都塞给模型,而需要一个“高质量的上下文裁剪(Context Truncation)策略”。但 裁剪上下文并非简单删除旧消息,背后包含一整套原则、优先级和工程策略。

不合理的裁剪会导致:

  • 模型突然忘记风格和角色
  • 对话逻辑断裂(assistant 回答找不到 user 提问)
  • JSON 格式丢失、导致下游解析失败
  • 安全策略失效,触发潜在违规内容
  • 回答不稳定、不连贯

要构建一个可用于生产环境的对话系统,上下文裁剪算法就成为了不可或缺的一部分。


1. 为什么必须进行上下文裁剪?

LLM 存在最大输入 Token 限制。如果不断累积对话历史,就会出现:

  • 调用失败(超过 Token 上限)
  • 成本上升(Token 越多越贵)
  • 延迟增加(序列越长推理越慢)

因此:

必须对对话历史进行选择性保留,而不是全部提供给模型。


2. 上下文裁剪的核心目标

一个设计良好的裁剪算法,应同时满足三个目标:

① 保留关键语义信息,让模型“记得”对话内容

模型必须知道用户最近说了什么、当前任务是什么。

② 控制 Token 数量,保证不会超限

避免报错、控制成本、提升性能。

③ 保持对话结构完整,不让模型迷失上下文

例如不能出现:

assistant:好的,我继续说……
(但裁剪掉了触发这句话的 user 提问)

这种情况会导致模型输出混乱。


3. 日常开发中裁剪策略应该遵守哪些原则?

在笔者的日常开发中,裁剪策略一般遵循以下 3 条原则。

原则 1:system 消息必须永远保留

system 消息通常是这样:

你是一个医学知识图谱问答助手,所有回答必须简洁。

system 是模型行为的“根指令”,决定:

  • 角色
  • 任务
  • 风格
  • 限制(如禁止输出敏感内容)

丢掉 system = 丢掉整个对话的“性格与规则”。

原则 2:优先保留最新的对话

用户最近的提问和模型的最新回答是对本轮任务最重要的:

  • 任务上下文
  • 思考链条
  • 已经解释过的内容
  • 用户正在继续的追问

因此,从后往前保留消息(最新优先),从前往后删除(最旧优先) 是业内通用策略(OpenAI、LangChain、Dify 都遵循此策略)。

原则 3:裁剪后的对话必须从 user 开始

这是为了避免:

assistant: 那我继续上一段……
(找不到上一段,语义断裂)

所以裁剪后必须保证:

system(可选)
user(必须)
assistant
user
assistant
...

这保证上下文结构完整。


4. 对话上下文裁剪策略常用的实现思路

(1)预留 system

List<Map<String, Object>> truncatedMessages = new ArrayList<>();
int remainingTokens = maxInputTokens;
Map<String, Object> system = messages.get(0);
if ("system".equals(system.getOrDefault("role", ""))) {
    remainingTokens -= tokenCounter.countMessageTokens(system);
}

如果第 0 条是 system:

  • 先预留它的 Token
  • 不参与回收
  • 最后统一加回对话头部

作用:确保角色设定永不丢失。

(2)从对话末尾“回填”消息,直到达到 Token 限制

for (int i = messages.size() - 1; i >= 0; i--) {
    Map<String, Object> message = messages.get(i);
    int messageToken = tokenCounter.countMessageTokens(message);
    if (remainingTokens >= messageToken) {
        truncatedMessages.add(0, message);
        remainingTokens -= messageToken;
    } else {
        break;
    }
}

即:从最新的 assistant/user 开始往前选,有多少 Token 装多少。

这样可以保证:

  • 最新用户提问一定保留
  • 最新模型回答一定存在
  • 语义连续性最佳

(3)删除裁剪后开头的孤立 assistant 消息

Iterator<Map<String, Object>> iterator = truncatedMessages.iterator();
 while (iterator.hasNext()) {
     Map<String, Object> message = iterator.next();
     if (!"user".equals(message.getOrDefault("role", ""))) {
         iterator.remove(); // 安全删除当前元素
     } else {
         break;
     }
 }

裁剪可能导致:

assistant: 我刚才解释过……

因为前面的 user 被裁掉。
这会导致模型继续“自言自语”。

所以必须在裁剪后执行:

从头删除非 user 消息,直到遇到第一条 user。

(4)最后插入 system,构成完整上下文

if ("system".equals(system.getOrDefault("role", ""))) {
   truncatedMessages.add(0, system);
}

最终结构:

system (如果有)
user
assistant
user
assistant
...

5. 整体流程示意图

开始
是否包含system?
预留system token
从对话末尾回填消息
直到Token用完
清理开头非user消息
将system插回开头
生成裁剪后的对话输出

6. 工程价值

这段裁剪逻辑本质上解决了三个问题:

① 保证模型行为一致性:system 设定永不丢,风格和角色延续。

② 保证模型能理解当前对话:裁剪后从 user 开始,不会断链。

③ 保证信息密度最高:只保留最近对话,语义最相关。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐