关键词:聚合 API、多轮对话、上下文窗口、对话压缩、Token 成本、滑动窗口

做带记忆的 AI 助手,新手最容易踩的坑:把每一轮 user/assistant 都无脑 append 到一个 messages 列表里

问题有三个,而且会同时爆发:

  1. 成本飙升:上下文每轮累加,Token 消耗近似线性增长,长聊一天烧掉不少钱。
  2. 超窗口:再大的上下文也有上限,聊久了直接报错 context length exceeded
  3. 越聊越笨:堆满历史后,模型被早期无关信息干扰,回答质量反而下降。

解法就两条,本文用 TokenPortal 聚合 API(一个 base_url + 一个 Key 接 150+ 大模型,OpenAI 兼容)给出最小可运行实现:

滑动窗口(只留最近几轮)+ 摘要压缩(把老对话压成一段)。换模型零改动,摘要还能用更便宜的模型跑。


一、滑动窗口:只保留最近 N 轮

最简单也最常用。超过窗口就把最老的几轮"砍掉",只留 system + 最近 K 轮。

import os
from openai import OpenAI

# 控制台获取:https://tokenportal.ai (BASE_URL / API_KEY 以控制台为准)
BASE_URL = "https://api.tokenportal.ai/v1"          # 发布前替换为你的真实网关地址
API_KEY  = os.getenv("TP_API_KEY", "YOUR_TOKENPORTAL_KEY")

client = OpenAI(base_url=BASE_URL, api_key=API_KEY)

SYSTEM = "你是一个乐于助人的中文助手。"
KEEP_TURNS = 4          # 始终保留的最近对话轮数
CHAT_MODEL = "deepseek-v4-pro"   # 正式回答用强模型(模型 ID 以控制台为准)

history = []  # 仅存 user/assistant 轮次

def chat(user_msg: str) -> str:
    global history
    history.append({"role": "user", "content": user_msg})
    # 超出窗口:丢弃最老的部分
    if len(history) > KEEP_TURNS:
        history = history[len(history) - KEEP_TURNS:]
    resp = client.chat.completions.create(
        model=CHAT_MODEL,
        messages=[{"role": "system", "content": SYSTEM}] + history,
        temperature=0.7
    )
    reply = resp.choices[0].message.content
    history.append({"role": "assistant", "content": reply})
    return reply

滑动窗口干净、零额外开销,但有个代价:砍掉的历史就彻底丢了,模型会"忘了"前面聊过什么。


二、摘要压缩:把老对话"折叠"起来

要保留长程记忆,就在砍之前先总结。把超出窗口的旧轮次交给模型压成一段摘要,替换进上下文。这样模型既不被旧信息淹没,又不丢关键事实。

context_summary = ""   # 跨轮累积的对话摘要

def summarize(old_turns: list) -> str:
    # 摘要用与主回答相同的模型即可;想更省可换成 deepseek-v4-flash(以控制台模型广场为准)
    resp = client.chat.completions.create(
        model=CHAT_MODEL,
        messages=[
            {"role": "system", "content": "把对话压缩成一段中文摘要,保留关键事实与未决问题,不超过 120 字。"},
            {"role": "user",   "content": str(old_turns)}
        ],
        temperature=0
    )
    return resp.choices[0].message.content

def chat_with_summary(user_msg: str) -> str:
    global history, context_summary
    history.append({"role": "user", "content": user_msg})
    # 超出窗口:把最老的部分摘要掉,再裁剪
    if len(history) > KEEP_TURNS:
        head = history[:len(history) - KEEP_TURNS]
        context_summary = summarize(head)          # 覆盖式压缩,始终是最新摘要
        history = history[len(history) - KEEP_TURNS:]
    sys_msg = SYSTEM + (f"\n(此前对话摘要:{context_summary})" if context_summary else "")
    resp = client.chat.completions.create(
        model=CHAT_MODEL,
        messages=[{"role": "system", "content": sys_msg}] + history,
        temperature=0.7
    )
    reply = resp.choices[0].message.content
    history.append({"role": "assistant", "content": reply})
    return reply

要点:

  • 摘要是覆盖式的:每次只对"窗口外的最老部分"重新总结,不会无限累积。
  • 摘要模型可降级:摘要任务对能力要求低,换成更便宜更快的 deepseek-v4-flash 能再省一笔(ID 以控制台为准)。
  • 主回答用强模型:关键生成仍走 deepseek-v4-pro,质量不受影响。

三、换模型零改动

上面 CHAT_MODEL 换成 qwen3.7-max / glm-5.2 / kimi-k3 任意一个,对话管理逻辑一行不用改——聚合 API 抹平了各厂商 SDK 差异,你只管切模型。


四、落地建议

  1. 短对话(客服单次咨询):滑动窗口就够,KEEP_TURNS=4~6。
  2. 长对话(陪聊、咨询、带状态的 Agent):滑动窗口 + 摘要双保险。
  3. 成本敏感:摘要用便宜模型,主回答按"难的任务用强模型"分级调用。
  4. 可观测:每次请求后打印 resp.usage,把 Token 当"电费"记账(见我主页另一篇《给 AI 应用装一个 Token 电表》)。

五、小结

  • 多轮对话别无脑 append,否则又贵又容易超窗口。
  • 滑动窗口管"近因",摘要压缩管"长程记忆",两者可叠加。
  • 聚合 API 下换模型只改一个字段,上下文管理代码完全复用。

如果这篇对你有启发,来我主页看看更多聚合 API 实战。一个接口接 150+ 大模型,官方货源、正规可开票、稳定,代码不用为每个厂商重写。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐