返回博客

成本优化指南

裁剪聊天历史以降低成本:滑动窗口还是摘要

限制你每轮重发的内容:保留最近几条消息或一段简短摘要。50 轮对话的 token 算术和一个 Python 辅助函数。

作者:CheapTokens Team发布于 5 分钟阅读

封面:用 trim_history 辅助函数让长对话保持低成本
本页内容
  1. 第 50 轮实际发送了什么
  2. 方案 1:滑动窗口
  3. 方案 2:旧轮次的摘要
  4. 三种方案的成本
  5. 该选哪一个

你的支持机器人已经聊了 50 轮,每一条新消息都比上一条更贵。API 不记忆任何内容,所以你的代码每次都要重发整个对话。解决办法是限制你发送的内容:保留系统提示和最近几轮(滑动窗口),或者把旧轮次换成一段简短摘要。下面是 50 轮对话的算术,以及一个可以直接复制的辅助函数。

DeepSeek 的多轮对话指南(api-docs.deepseek.com,2026 年 11 月 1 日查阅)说得很直白:服务器不记录先前请求的上下文,所以客户端每次调用都要发送完整的 messages 列表。CheapTokens 上的 OpenAI 兼容格式也是同样的工作方式。如果这对你来说是新的,请先看为什么长对话更贵和messages 数组如何工作。

第 50 轮实际发送了什么#

假设你运行一个支持机器人,系统提示为 400 个 token。在典型的一轮中,用户写大约 60 个 token,机器人回答大约 190 个。一个完整轮次会给历史增加 250 个 token。这些是假设:请用你自己响应中 usage 字段的数字替换它们。

在第 50 轮,请求携带系统提示、49 个先前轮次和新消息:400 + 49 × 250 + 60 = 12,710 个输入 token,以得到 190 个 token 的回答。把整个对话加起来。系统提示和新消息每次都会发送:50 × 460 = 23,000。重复的历史增加 250 × 1,225 = 306,250。合计:23,000 + 306,250 = 329,250 个输入 token,对应 50 × 190 = 9,500 个输出 token。

每个请求中的历史每轮增加一轮,所以总量随对话长度的平方增长。从 50 轮增加到 100 轮,重复历史部分大约翻两番。

自己试试试一试:如果你只发送系统提示、最近 4 轮和新消息,第 50 轮会携带多少输入 token?显示答案

1,460(400 + 4 × 250 + 60),而完整历史是 12,710。同样的问题,同样的回答长度,输入只是其中一小部分。

方案 1:滑动窗口#

思路:始终发送系统提示,丢弃除最近 N 轮之外的所有内容。你在自己的存储中保留完整历史,只裁剪你发送的那份副本。

python
def trim_history(messages, keep_pairs=4):
    """System prompt + the last keep_pairs turns + the new user message."""
    system = [m for m in messages if m["role"] == "system"]
    rest = [m for m in messages if m["role"] != "system"]
    return system + rest[-(keep_pairs * 2 + 1):]

在你追加用户的新消息之后调用它。使用 keep_pairs=4 时,就是 4 对 user/assistant 加上新消息,共 9 条消息,而且切片总是从一条 user 消息开始。下面是它接入真实调用的样子:

python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.cheaptokens.dev/v1",
    api_key=os.environ["CHEAPTOKENS_API_KEY"],
)
MODEL = "deepseek-ai/DeepSeek-V4-Flash-0731"

history = [{"role": "system", "content": "You are a concise support assistant."}]

def ask(text):
    history.append({"role": "user", "content": text})
    resp = client.chat.completions.create(
        model=MODEL,
        messages=trim_history(history),
        max_tokens=500,
    )
    answer = resp.choices[0].message.content
    history.append({"role": "assistant", "content": answer})
    print(resp.usage.prompt_tokens, resp.usage.completion_tokens)
    return answer

print 这一行是有用的:观察第一个数字。不裁剪时它每轮都会上升。使用窗口后,一旦对话超过第 4 轮,它就不再增长。

Python 辅助函数 trim_history:系统提示、最近 4 轮、新消息
本文中的裁剪辅助函数

用数字来说:从第 5 轮起,每个请求是 400 + 4 × 250 + 60 = 1,460 个输入 token。前四轮仍然正常增长,合计 3,340。整个对话:3,340 + 46 × 1,460 = 70,500 个输入 token,大约是完整历史发送的 329,250 的五分之一。输出保持 50 × 190 = 9,500。

第 50 轮请求:完整历史对比 4 轮窗口
根据上述假设,第 50 轮请求的 token 构成

在代码中,整个改动就是调用中的一个参数:

比较

一行改动

之前

python
messages=history

之后

python
messages=trim_history(history)

问题在于:窗口会遗忘。如果用户在第 40 轮问“我的订单号是多少?”,而该号码在第 3 轮,模型从未见过它。有两个低成本的修复方法。在你自己的代码中从对话里提取稳定事实(姓名、订单号、套餐),并把它们追加到系统提示中,窗口永远不会裁掉它们。或者添加摘要,也就是下一个方案。

方案 2:旧轮次的摘要#

不是丢弃旧轮次,而是压缩它们。每隔几轮,把刚掉出窗口的轮次加上当前摘要发送给模型,并要求生成一个新的简短摘要。然后把这个摘要放入系统消息。

python
def summarize(summary, old_messages):
    transcript = "\n".join(f"{m['role']}: {m['content']}" for m in old_messages)
    prompt = (
        f"Summary so far:\n{summary}\n\nNew turns:\n{transcript}\n\n"
        "Rewrite the summary in under 200 words. "
        "Keep names, numbers, decisions and open questions."
    )
    resp = client.chat.completions.create(
        model=MODEL,
        messages=[{"role": "user", "content": prompt}],
        max_tokens=400,
    )
    return resp.choices[0].message.content

def build_messages(history, summary, keep_pairs=4):
    system = history[0]["content"]
    if summary:
        system += "\n\nEarlier in this chat: " + summary
    recent = history[1:][-(keep_pairs * 2 + 1):]
    return [{"role": "system", "content": system}] + recent

摘要不是免费的:它是一次额外的模型调用。假设摘要约 300 个 token,每 5 轮刷新一次(50 轮中 9 次),每次刷新读取约 1,600 个 token(旧摘要加五轮加指令)并写入 300。为稳妥起见,从第 5 轮起在每个请求中都计入摘要(实际上它出现在第一次刷新之后,所以这是略微高估):400 + 300 + 4 × 250 + 60 = 1,760 个输入 token。

对话总计:主请求 3,340 + 46 × 1,760 = 84,300 个输入 token,摘要调用 9 × 1,600 = 14,400 个输入和 9 × 300 = 2,700 个输出。合计 84,300 + 14,400 = 98,700 个输入和 9,500 + 2,700 = 12,200 个输出 token。

摘要比单纯的窗口成本略高,作为回报,模型仍然知道第 3 轮发生了什么。

三种方案的成本#

下面是同样的 50 轮对话在 DeepSeek-V4-Flash 上,根据上述总量实时定价:

DeepSeek-V4-Flash 上的一次 50 轮对话
  • 完整历史(329,250 输入,9,500 输出)$0.0059
  • 窗口加摘要(98,700 输入,12,200 输出)$0.0027
  • 滑动窗口,4 轮(70,500 输入,9,500 输出)$0.002

价格来自实时价格表,所以当资费变化时数字会更新。排序不会变:窗口发送最少,摘要居中,完整历史最贵。

对于这样的对话,起步用的 CheapTokens 模型是 DeepSeek-V4-Flash。它有 380K 上下文窗口(50 轮对话即使不裁剪也放得下),每次回答最多 32,768 个输出 token,一天中任何时段都是固定价格,而且总体而言,把输入和输出加在一起计算,它比 OpenRouter 更便宜。模型 id 是 deepseek-ai/DeepSeek-V4-Flash-0731。如果你想在窗口方案上比较模型(平均每个请求 1,410 个输入 token,190 个输出,50 个请求),这里是实时表格:

模型 每次请求 50 次请求 OpenRouter,50 次请求
GLM-5.3-Flash $0.000036 $0.0018 $0.0032
DeepSeek-V4-Flash $0.00004 $0.002 $0.0063
MiniMax-M2.7 $0.000099 $0.005 $0.0594

每次请求 1,410 个输入 token 和 190 个输出 token。每 100 万 token 的价格为当前价格,会随我们的资费一同变动;OpenRouter 列显示相同工作负载的参考价格。

GLM-5.3-Flash 有 390K 上下文,MiniMax-M2.7 有 180K,对于裁剪过的对话来说足够了。当前价格在定价页面上,文档展示了如何发送第一个请求。

该选哪一个#

窗口是默认选择:三行代码,没有额外调用,没有额外故障模式。把它用于支持机器人、辅导老师和角色扮演,在这些场景中最近几轮承载对话,系统提示承载规则。当用户回溯很久以前说过的事情时,添加摘要,比如一个永不结束对话的个人助理。让窗口足够小,使模型仍能看到即时上下文;又足够大,能容纳一个完整的问答交换。到底能容纳多少是一个单独的限制,在什么是上下文窗口中有所介绍。

快速检查

用户在第 40 轮询问第 3 轮给出的订单号。你使用 4 轮窗口且没有摘要。会发生什么?

显示答案

答案: 模型看不到它,因为第 3 轮已从请求中裁掉

API 是无状态的。模型只能看到请求中的内容,所以你仍然需要的事实必须留在系统提示或摘要中。