上下文内容的滑动清理

普通用户一般不需要清理上下文,可以忽略本节内容。

在多轮对话中,随着对话的持续,上下文(Messages)会不断增长,最终可能超过大模型的上下文长度限制,导致 API 调用失败。

要解决这个问题,我们需要先掌握两个方法。

TrimMessages方法

TrimMessages方法用于按轮次清理上下文,只保留最近的若干轮对话。其语法为:

TrimMessages(n)

n:整数型,要保留的最近轮数,在此之前的对话内容会被删除。

这里的一轮,指从一条 user 消息开始,到下一次出现 user 消息之前的所有消息(包括 assistant 回答、工具调用请求和工具执行结果)。TrimMessages 会以轮为单位整段保留或删除,不会拆散工具调用链。

提示:

1、TrimMessages不会删除系统提示词,只删除用户对话,所以对于需要长期保留的内容,可以放在系统提示词中(
SystemPrompt属性)

2、不能频繁清理上下文,否则会有很严重的副作用(后面会讲述),比较合适的做法是当上下文长度达到一个阈值后才进行清理,那么如何来估算阈值呢,这就要用到EstimateTokenCount方法。

EstimateTokenCount方法

EstimateTokenCount方法用于粗略计算指定字符串的Token数,其语法为:

语法:

EstimateTokenCount(content,cnWeight, enWeight)

content: 要计算token数的字符串
cnWeight:可选参数,用于指定一个中文字符对应的token数,默认为0.6
enWeight:可选参数,用于指定一个英文文字符对应的token数,默认为0.3

大模型的上下文长度是以token为单位的,而不是以字符数为单位的,例如Deepseek最大上下文长度为1M,就是1M个token,而不是1M个字符数。

根据DeepSeek官方文档,token与字符的换算关系为:

反过来算:1M token 大约对应 1.67M 个中文字符,或 3.33M 个英文字符。

可以根据不同的模型调整cnWeight和enWeight的值,对于DeepSeek来说,一个中文字符大约0.6个token,一个英文字符大约0.3个token,这是一个偏保守(安全)的预估值,据此计算出来的token数通常比实际的token数大。

修改发送按钮代码

之前我们设计了多个AI对话框,为了确保持续对话过程中,上下文长度不会超过限制,可以修改发送按钮的代码,将发送请求的代码改为:

' ---- 4. 发送请求 ----
e.Sender.Enabled =
False
Dim
body As String = caller.GetRequestBody() '获取请求体
if
caller.EstimateTokenCount(body) > 900 * 1000 '如果请求体长度大于900K
    caller.TrimMessages(10)
'清理上下文,保留最近10轮对话内容,其余内容会从上下文删除
End
If
Await caller.GetResultAsync() '请不要漏掉Await关键词
e.Sender.Enabled = True

这里假定大模型上下文长度是1MToken,当上下文长度超过900K时开始清理上下文,留出100K上下文是给新一轮对话使用的。

如果你希望在不超过上下文长度限制的情况下,尽可能保留更多轮次的对话(以提高对话质量), 同时保留的上下文长度也不希望太长(避免频繁清理),可以将发送请求的代码改为:

' ---- 4. 发送请求 ----
e.Sender.Enabled =
False
Dim
body As String = caller.GetRequestBody() '获取请求体
Dim
n As Integer = 50 '最多保留50轮旧对话
Dim
preserveTokens As Integer = 300 * 1000 '最多保留300KToken的上下文
If
caller.EstimateTokenCount(body) > 900 * 1000 Then'如果请求体长度大于900K,开始清理上下文
    While caller.EstimateTokenCount(body) > preserveTokens '每次清理后保留的旧上下文不超过300K
        caller.TrimMessages(n)
'清理上下文,保留最近n轮对话内容,其余内容会从上下文删除
       
If n = 1 Then
           
Exit While
       
End If
        n = Math.Max(1, n - 5)
        body = caller.GetRequestBody()
'再次获取请求体
   
End While
End
If
Await
caller.GetResultAsync() '请不要漏掉Await关键词
e.Sender.Enabled =
True

这里将上下文的长度大约限在300K到900K之间,且最多保留50轮对话,大家可根据需要自行调整相关数值

为什么不能频繁清理上下文

上下文清理的阈值设置,不仅关系到 API 调用能否成功,还直接影响成本。

阈值越小,清理越频繁,上下文长度越小,对话质量就会下降,大模型会更容易出现幻觉。

你可能觉得阈值越小,虽然会影响对话质量,但因为每次请求的上下文长度也小了,所以成本会下降,实际却不一定是这么回事。

因为DeepSeek 的上下文缓存机制(主流大模型大多支持类似的缓存功能),使得“大阈值”、“少清理”反而可能更省钱。

DeepSeek 的上下文缓存是自动开启的,命中规则基于前缀完整匹配。

在多轮对话中,Messages 是逐轮叠加的:第 n 轮请求包含了第 1 到 n-1 轮的全部历史,以及第 n 轮的新问题。对于 DeepSeek 的 API 来说:

命中缓存和未命中缓存,价格差异极大,以 deepseek-flash 模型、空闲时段为例:
 

计费项

价格(元 / 百万 Token)

输入(缓存命中)

0.02

输入(缓存未命中)

1

输出

4

缓存命中的输入价格仅为未命中的 1/50。在多轮对话中,随着轮次增加,历史上下文越来越长,但这部分“重复”的内容始终能以极低的价格命中缓存。你每一轮真正需要按标准价格付费的,只是本轮新增的提问和答案。

理解了缓存机制后,阈值设置就有了成本层面的考量:
 

策略

缓存命中

成本

阈值较高,尽量不清理

历史完整保留,每轮都能命中缓存

有可能更低——只有新增内容按标准价计费

阈值较低,频繁清理

历史被删除,前缀改变,缓存失效

有可能更高——清理后的第一次请求的全部内容都按未命中价计费

频繁的清理,不仅影响对话质量,还可能会增加成本,所以在实践中,一般是尽可能少清理或不清理,保留完整历史,以维持高缓存命中率。

本节示例中阈值为900K个Token,接近1.5M个中文字符,意味着大多数情况下不会触发清理,从而保护了缓存命中率,同时为保持对话质量,清理后保留了50轮对话(或300K)的上下文。

在 DeepSeek 的缓存机制下,保持上下文完整(高阈值或不清理)通常是成本最优的策略。TrimMessages 应被视为一个“防超限保险”,而非日常优化手段。阈值设置需要在“避免请求失败”和“维持缓存命中率”之间取得平衡。

注意上面关于成本的讨论,没有任何确定的结论,用的是"通常"、"可能",一切都以实际测试为准。

分析token消费

我们可以通过LLMCaller的ResultJObject属性,获取每次请求的完整信息,参考:获取更多信息

假定某次请求后通过下面的代码:

Output.Show(caller.ResultJObject.ToString())

获得返回的具体信息为:

{
  "id": "42f102d4-266a-40c5-a3d2-da17fcd62e8c",
  "object": "chat.completion",
  "created": 1791599109,
  "model": "deepseek-flash",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "85500
再加 5 得 855050,再减 1 最终等于 **8550490**。",
        "reasoning_content": "Result 8550490. Answer."
      },
      "logprobs": null,
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 1175,
    "completion_tokens": 34,
    "total_tokens": 1209,
    "prompt_tokens_details": {
      "cached_tokens": 1024
    },
    "completion_tokens_details": {
      "reasoning_tokens": 8
    },
    "prompt_cache_hit_tokens": 1024,
    "prompt_cache_miss_tokens": 151
  },
  "system_fingerprint": "aeb56401ca74e127821c4f9126dcb669"
}

通过上面的信息可以看到token消费的数据:

总 token 数:total_tokens = 1209

它由 输入(prompt) 和 输出(completion) 两部分组成:

输入:prompt_tokens = 1175

输出:completion_tokens = 34

1175 + 34 = 1209

输入消费的1175个token包括:

这个1175个token又分为缓存命中和缓存未命中:

prompt_cache_hit_tokens = 1024(命中缓存)

prompt_cache_miss_tokens = 151(未命中缓存)

1024 + 151 = 1175

缓存命中率 = 1024 / 1175 ≈ 87.1%,说明这次请求的大部分前缀(很可能是系统提示、工具定义和之前的对话历史)都被成功复用了。


本页地址:http://www.foxtable.com/webhelp/topics/7222.htm