很多 AI 应用刚上线时调用正常,运行一段时间后却开始出现 context_length_exceededmaximum context lengthprompt is too long,或者在没有明确错误码的情况下直接返回 400。

常见现象包括:Dify 工作流第一次执行成功,连续对话后失败;Cursor 加入多个文件后回答突然变短;Cherry Studio 聊了几十轮后响应变慢;知识库检索内容一多,接口开始超时。

这类问题和 API Key 是否正确通常没有关系。真正需要检查的是:一次请求最终送进模型的输入有多大,以及给输出预留了多少空间。

本文不绑定具体模型或服务商,而是从 OpenAI 兼容接口的通用请求结构出发,说明如何计算上下文预算、裁剪历史消息、限制知识库片段,并给出 curl、Python 和 Node.js 示例。
在这里插入图片描述

一、context_length_exceeded 到底表示什么

在这里插入图片描述

大模型每次生成答案时,只能在一个有限的上下文窗口中处理内容。这个窗口不只包含用户刚输入的问题,还可能包含:

  • 系统提示词;
  • 历史对话;
  • 当前用户消息;
  • 工具定义与 JSON Schema;
  • 知识库检索结果;
  • 代码文件、网页或文档内容;
  • 图片等多模态输入;
  • 为本次回答预留的输出 Token。

可以把一次请求的预算简化为:

系统提示词
+ 历史消息
+ 当前问题
+ 工具定义
+ 检索片段
+ 附件内容
+ 预留输出
<= 模型上下文窗口

假设模型支持的上下文窗口为 C,输入估算为 I,希望最多生成 O 个 Token,那么应满足:

I + O <= C - 安全余量

工程上通常还会留出一定余量,用来吸收分词差异、工具调用参数和框架自动注入的内容。

不同模型使用的分词器可能不同,同一段中文、英文、代码或 JSON 产生的 Token 数也不相同,因此不能简单把“字符数”当成“Token 数”。

二、为什么短问题也会触发上下文超限

用户最后只输入“继续”,请求仍可能超限,因为真正发送的并不只有这两个字。
在这里插入图片描述

1. 框架自动携带全部聊天记录

不少聊天应用会把历史消息逐轮追加到 messages。单轮请求只有几百 Token,累计几十轮后就可能超过模型窗口。

2. 知识库一次返回过多片段

RAG 应用中,top_k 越大并不一定越准确。如果每个片段都很长,又没有去重和重排,检索结果可能占据大部分上下文,反而挤压当前问题和输出空间。

3. 工具定义和结构化输出被低估

函数调用、MCP 工具或复杂 JSON Schema 也会进入上下文。工具越多、参数说明越长,请求的固定开销越大。

4. 附件被重复加入

同一份代码、合同或报告可能同时出现在系统提示词、知识检索结果和用户附件里,内容重复但 Token 会重复计算。

5. 最大输出设置过大

部分接口会同时检查输入与最大输出预算。即使实际只想生成几百 Token,如果参数预留过大,也可能使总预算超过窗口。

6. 切换模型后仍沿用旧配置

不同模型的上下文窗口和最大输出上限可能不同。切换模型时如果没有同步调整历史长度、检索数量和输出上限,就会出现“原模型正常,新模型报错”。

三、先用 curl 建立一个可控的最小请求

排查工具或工作流之前,先直接调用接口。这样可以排除客户端自动拼接历史记录、附件和隐藏提示词的影响。
在这里插入图片描述

先设置环境变量:

export AI_API_KEY="替换为你的API_Key"
export AI_BASE_URL="填写你的OpenAI兼容接口Base_URL"
export AI_MODEL="替换为准确模型ID"

发送只包含一条消息的请求:

curl --request POST "$AI_BASE_URL/chat/completions" \
  --header "Authorization: Bearer $AI_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "model": "'"$AI_MODEL"'",
    "messages": [
      {"role": "user", "content": "用一句话解释上下文窗口"}
    ],
    "temperature": 0.2,
    "max_tokens": 128
  }'

如果最小请求成功,而 Dify、Cursor 或桌面客户端失败,说明 API Key、Base URL 和基础模型调用大概率没有问题,应重点检查工具实际组装的上下文。

如果最小请求仍然失败,则先按状态码处理:

  • 401:检查认证;
  • 404:检查路径和模型名;
  • 429:检查限流与配额;
  • 5xx:检查上游服务。

不要把所有 400 都直接判断为上下文超限,应读取响应体中的 error.codeerror.typeerror.message

四、Python:估算消息体并从最旧对话开始裁剪

准确计算 Token 应优先使用目标模型对应的分词器。若当前服务没有提供分词器,可以先用保守估算做请求前保护,再结合接口返回的 usage.prompt_tokens 校准系数。

下面的示例完成三件事:保留系统提示词、始终保留最新用户请求、从最旧的普通对话开始删除。
在这里插入图片描述

示例中的估算函数不是精确分词器,但适合说明预算控制流程。

import json
import math
import os
import re
import requests


def estimate_tokens(text: str) -> int:
    """通用保守估算;生产环境应替换为目标模型分词器。"""
    cjk = len(re.findall(r"[\u4e00-\u9fff]", text))
    ascii_chars = len(re.findall(r"[\x00-\x7f]", text))
    other = max(0, len(text) - cjk - ascii_chars)
    return math.ceil(cjk * 1.5 + ascii_chars / 4 + other)


def message_tokens(message: dict) -> int:
    content = message.get("content", "")
    if not isinstance(content, str):
        content = json.dumps(content, ensure_ascii=False)

    return (
        6
        + estimate_tokens(message.get("role", ""))
        + estimate_tokens(content)
    )


def trim_messages(
    messages: list[dict],
    input_budget: int
) -> tuple[list[dict], int]:
    if not messages:
        return [], 0

    system_messages = [
        message
        for message in messages
        if message.get("role") == "system"
    ]

    normal_messages = [
        message
        for message in messages
        if message.get("role") != "system"
    ]

    # 最新消息必须保留,避免裁剪后丢失当前问题。
    kept = normal_messages[-1:] if normal_messages else []
    used = sum(
        message_tokens(message)
        for message in system_messages + kept
    )

    if used > input_budget:
        raise ValueError("系统提示词和当前问题已经超过输入预算")

    # 从近到远补回历史消息,超出预算时停止。
    for message in reversed(normal_messages[:-1]):
        cost = message_tokens(message)

        if used + cost > input_budget:
            break

        kept.insert(0, message)
        used += cost

    return system_messages + kept, used


def call_chat(messages: list[dict]) -> dict:
    context_window = int(os.environ["AI_CONTEXT_WINDOW"])
    reserved_output = int(os.getenv("AI_MAX_OUTPUT", "800"))
    safety_margin = max(
        512,
        math.ceil(context_window * 0.1)
    )
    input_budget = (
        context_window
        - reserved_output
        - safety_margin
    )

    trimmed, estimated_input = trim_messages(
        messages,
        input_budget
    )

    response = requests.post(
        os.environ["AI_BASE_URL"].rstrip("/")
        + "/chat/completions",
        headers={
            "Authorization":
                f"Bearer {os.environ['AI_API_KEY']}",
            "Content-Type": "application/json",
        },
        json={
            "model": os.environ["AI_MODEL"],
            "messages": trimmed,
            "max_tokens": reserved_output,
            "temperature": 0.2,
        },
        timeout=(5, 60),
    )

    response.raise_for_status()
    result = response.json()

    result["local_budget"] = {
        "estimated_input": estimated_input,
        "messages_before": len(messages),
        "messages_after": len(trimmed),
    }

    return result

这里要注意一个边界:不能只按消息条数裁剪。十条短对话可能比一条大型代码附件更小。

生产系统应统计真实 Token,并把附件、工具 Schema 和检索片段一起纳入预算。

五、不要只删除历史:先摘要,再保留关键事实

直接删除旧消息虽然简单,却可能丢失用户偏好、业务约束和前面已经确认的结论。
在这里插入图片描述

更稳妥的做法是把上下文分成三层:

  1. 稳定事实:用户身份范围、项目约束、输出格式、已经确认的决定;
  2. 滚动摘要:较早对话压缩成结构化摘要;
  3. 近期原文:保留最近几轮完整消息和当前问题。

滚动摘要不应写成一段模糊文字,可以使用固定结构:

目标:当前要完成什么
已确认:已经达成的结论
约束:不能改变的条件
未解决:仍需处理的问题
引用:必要的文件名、字段名、错误码

每次上下文接近阈值时,更新旧摘要,而不是继续叠加多个摘要。摘要本身也需要版本和长度限制,否则它最终也会变成新的超长历史。

六、Dify 工作流怎么控制上下文

Dify 中出现上下文超限时,先检查 LLM 节点最终接收的内容,而不是只看用户输入框。
在这里插入图片描述

建议按下面的顺序定位:

  1. 检查系统提示词是否粘贴了大段固定文档;
  2. 检查是否开启了对话记忆,以及保留了多少历史内容;
  3. 检查知识检索节点的 top_k、片段长度和重排结果;
  4. 检查模板中是否重复引用了同一个变量;
  5. 检查工具定义、结构化输出 Schema 是否过长;
  6. 调低最大输出 Token,为输入留出空间;
  7. 在 LLM 节点前增加摘要或裁剪代码节点,并记录裁剪前后的长度。

知识库场景不要只追求更多片段。更合理的流程是:先召回,再去重和重排,只把与当前问题直接相关的少量片段交给模型。

对于长表格和长文档,可以先按章节生成摘要,再检索摘要对应的原文块。

七、Cursor 长对话和大文件怎么处理

Cursor 会对长对话和大型文件做摘要或压缩,但这不代表可以无限加入内容。

当文件显示为已压缩、显著压缩或未包含时,模型看到的内容可能已经不是完整原文。

实际使用中可以这样做:

  • 一个聊天只处理一个明确任务,需求变化较大时新建聊天;
  • 不要直接加入整个仓库,先限定目录、文件或函数;
  • 大文件只引用与问题相关的行和符号;
  • 把历史聊天中的关键结论整理成简短说明,再带入新会话;
  • 让模型先列出还缺哪些文件,再逐个补充;
  • 出现回答遗漏时,检查文件是否被压缩或未包含,而不是立刻重复发送全部代码。

“一次性提供所有上下文”看似省事,实际会降低有效信息密度。

对代码任务来说,入口函数、调用链、错误堆栈和相关配置通常比整个仓库更有价值。

八、Cherry Studio 的上下文数量和最大输出怎么设置

Cherry Studio 的对话设置中可以控制保留的上下文消息数量,也可以限制单次回答的最大 Token。
在这里插入图片描述

普通聊天可以先保留较少的最近消息;需要分步骤写作或连续推理时再适当增加。

如果出现长对话失败,可按以下步骤处理:

  1. 复制当前任务目标和已确认结论;
  2. 新建话题,把摘要作为第一条消息;
  3. 减少上下文消息数量;
  4. 将单次最大输出调整到实际需要的范围;
  5. 删除重复附件,只保留本轮需要的材料;
  6. 用短问题测试模型连通性,再逐步恢复必要上下文。

上下文数量是“消息条数”,不等于精确 Token。包含长代码、长文档或图片时,即使只保留几条消息,也可能占用很大空间。

九、Node.js 后端代理:在请求发出前统一做预算保护

如果多个前端、机器人和工作流共用一个 AI 接口,最好把上下文预算放到后端统一执行。
在这里插入图片描述

下面的 Express 示例限制消息体大小,并在发送前裁剪旧对话。

import express from "express";

const app = express();
app.use(express.json({ limit: "512kb" }));

function roughTokens(value) {
  const text = typeof value === "string"
    ? value
    : JSON.stringify(value);

  const cjk = (
    text.match(/[\u4e00-\u9fff]/g) || []
  ).length;

  const ascii = (
    text.match(/[\x00-\x7f]/g) || []
  ).length;

  return Math.ceil(
    cjk * 1.5
    + ascii / 4
    + (text.length - cjk - ascii)
  );
}

function buildBudgetedMessages(messages, budget) {
  const system = messages.filter(
    (message) => message.role === "system"
  );

  const normal = messages.filter(
    (message) => message.role !== "system"
  );

  const latest = normal.length
    ? [normal.at(-1)]
    : [];

  const kept = [...latest];

  let used =
    roughTokens(system)
    + roughTokens(latest);

  if (used > budget) {
    return {
      messages: [],
      estimatedInputTokens: used,
      removedMessages: messages.length,
      overflow: true,
    };
  }

  for (
    let index = normal.length - 2;
    index >= 0;
    index -= 1
  ) {
    const cost = roughTokens(normal[index]);

    if (used + cost > budget) {
      break;
    }

    kept.unshift(normal[index]);
    used += cost;
  }

  return {
    messages: [...system, ...kept],
    estimatedInputTokens: used,
    removedMessages:
      messages.length
      - system.length
      - kept.length,
    overflow: false,
  };
}

app.post("/api/chat", async (req, res) => {
  const contextWindow = Number(
    process.env.AI_CONTEXT_WINDOW
  );

  if (
    !Number.isFinite(contextWindow)
    || contextWindow <= 0
  ) {
    return res.status(500).json({
      error: "CONTEXT_WINDOW_NOT_CONFIGURED",
    });
  }

  const maxOutput = Math.min(
    Number(req.body.maxOutput || 800),
    2000
  );

  const safetyMargin = Math.max(
    512,
    Math.ceil(contextWindow * 0.1)
  );

  const inputBudget =
    contextWindow
    - maxOutput
    - safetyMargin;

  const budgeted = buildBudgetedMessages(
    req.body.messages || [],
    inputBudget
  );

  if (budgeted.overflow) {
    return res.status(413).json({
      error: "CURRENT_REQUEST_TOO_LARGE",
      estimatedInputTokens:
        budgeted.estimatedInputTokens,
    });
  }

  const upstream = await fetch(
    `${process.env.AI_BASE_URL.replace(/\/$/, "")}`
      + "/chat/completions",
    {
      method: "POST",
      headers: {
        Authorization:
          `Bearer ${process.env.AI_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        model: process.env.AI_MODEL,
        messages: budgeted.messages,
        max_tokens: maxOutput,
        temperature: 0.2,
      }),
      signal: AbortSignal.timeout(60_000),
    },
  );

  const body = await upstream.json();

  if (!upstream.ok) {
    return res.status(502).json({
      error:
        body?.error?.code
        || "UPSTREAM_REJECTED",
      upstreamStatus: upstream.status,
    });
  }

  res.json({
    data: body,
    context: {
      estimatedInputTokens:
        budgeted.estimatedInputTokens,
      removedMessages:
        budgeted.removedMessages,
    },
  });
});

app.listen(3000);

这个示例只保留近期消息。生产环境还应加入滚动摘要、真实分词器、检索片段预算和工具 Schema 预算。

裁剪行为也应返回给调用方,避免用户以为模型仍然看到了完整历史。

十、常见报错排查表

现象 常见原因 优先处理
context_length_exceeded 输入与预留输出超过模型窗口 裁剪历史、检索片段和附件
prompt is too long 系统提示词或拼接后的 Prompt 过大 输出最终请求长度,检查重复变量
max_tokens 参数错误 最大输出超过模型允许范围 按模型文档降低输出上限
rate_limit / 429 请求频率或 Token 吞吐超过限制 降低并发并做退避,不要只裁剪历史
timeout 上下文过大、网络慢或模型处理时间长 先缩短输入,再检查分段超时
model_not_found 模型 ID 错误或无访问权限 核对模型名,与上下文长度分开处理
401 / invalid_api_key 认证头、API Key 或目标地址错误 用最小 curl 请求验证认证链
返回内容突然遗漏 文件或历史被工具压缩、摘要或移除 缩小任务范围并重新提供关键内容

十一、API Key 安全和日志记录

排查上下文问题时,经常需要记录请求信息,但不能把完整 API Key、用户文档和全部聊天内容直接写入日志。

建议记录:

  • 模型 ID;
  • 消息数量;
  • 估算与实际输入 Token;
  • 最大输出 Token;
  • 检索片段数量;
  • 附件数量与总长度;
  • 裁剪消息数量;
  • HTTP 状态码、错误码和请求 ID;
  • 耗时与重试次数。

不建议记录:完整 API Key、身份证号、手机号、合同原文、未脱敏代码凭据和完整用户提示词。

API Key 应保存在服务端环境变量或密钥管理系统中,不要放进浏览器前端或公开仓库。
在这里插入图片描述

十二、企业场景需要增加哪些控制

团队统一接入大模型时,不能把上下文控制完全交给每个前端。建议在网关或后端服务中统一实现:

  1. 按模型维护上下文窗口和最大输出配置;
  2. 为系统提示词、历史、知识检索、工具和输出分别分配预算;
  3. 对超长文档做分块、摘要和分层检索;
  4. 为每次裁剪保留可审计记录,但不保存敏感原文;
  5. 监控真实 prompt_tokens 与本地估算的偏差;
  6. 对不同业务设置独立上限,避免单个请求占满 Token 吞吐;
  7. 用固定测试集验证裁剪后是否丢失关键事实。

上下文越长,并不代表答案一定越好。

企业应用更关注的是有效上下文:内容相关、没有重复、来源可追踪,并且给输出和工具调用留出足够空间。
在这里插入图片描述

十三、FAQ

1. context_length_exceeded 可以通过重试解决吗?

通常不能。相同请求不做修改,重试仍会超过窗口。应先减少输入或输出预算,再重新发送。

2. 只降低 max_tokens 就够了吗?

不一定。如果历史、附件和检索片段已经占满窗口,仅降低输出可能仍然失败。需要先拆分各部分预算,找到占用最大的来源。

3. 中文字符数可以直接换算成 Token 吗?

不能精确换算。不同模型和分词器结果不同,代码、空格、标点和 JSON 也会消耗 Token。

字符估算只能做预警,生产环境应使用对应分词器或接口返回的 usage 数据校准。

4. RAG 的 top_k 越大越好吗?

不是。过多低相关片段会增加成本、挤占上下文,还可能让答案偏离问题。

应结合召回、去重、重排和片段长度共同设置。

5. Cursor 已经自动摘要,为什么还会遗漏代码?

摘要和文件压缩只能保留模型判断的重要结构,无法保证每一行都存在。

遇到具体缺陷时,应单独提供相关函数、调用位置、错误堆栈和测试结果。

6. 新建 Dify 会话后恢复正常,说明是什么问题?

通常说明历史记忆或累计变量占用了过多上下文。

还应检查知识检索和系统提示词,避免新会话运行一段时间后再次超限。

7. 长上下文和 rate_limit 是同一类限制吗?

不是。上下文窗口限制单次请求可处理的内容;Token 速率限制约束一段时间内的吞吐。

缩短请求可能同时缓解两者,但排查时应根据状态码和错误信息分别处理。

总结

在这里插入图片描述

context_length_exceeded 的核心不是“对话轮数太多”,而是一次请求中系统提示词、历史消息、当前问题、工具定义、检索片段、附件和预留输出的总 Token 超过了模型窗口。

可靠的处理顺序是:

  1. 先用 curl 验证最小请求;
  2. 输出最终消息体和各部分预算;
  3. 历史消息采用“稳定事实 + 滚动摘要 + 近期原文”的分层结构;
  4. 知识库内容经过召回、去重和重排;
  5. 在后端统一执行裁剪、日志和安全控制。

这样做不仅能减少上下文超限,也能降低无效 Token 消耗,并让 Dify、Cursor、Cherry Studio 和自建应用在长任务中保持可预测的行为。

参考文档

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐