先确认上下文上限再喂长文档——Gemini 4 Argon 的输入边界

文章导读
把整篇长报告一次性丢给 Gemini 4 Argon 之前,先确认两件事:这次调用允许的上下文总量是多少,单次回复允许生成多少。前者决定 prompt、历史消息、工具返回和输出加起来能放多大,后者决定摘要或答案本身会不会中途写停。这两个数字不落实,后面所有“模型答得不对”的判断都缺少依据。
📋 目录
  1. 一 在官方文档里定位上下文上限与输出上限两个数字,并记录出处
  2. 二 用递增长度的输入做边界探测,观察是报错还是静默截断
  3. 三 把长文档切成带重叠的分块,并给每块保留原文位置标记
  4. 四 先分块摘要再汇总,避免汇总阶段再次超限
  5. 五 在请求里加超限处理分支,捕获长度类错误后自动降级为分块流程
A A

把整篇长报告一次性丢给 Gemini 4 Argon 之前,先确认两件事:这次调用允许的上下文总量是多少,单次回复允许生成多少。前者决定 prompt、历史消息、工具返回和输出加起来能放多大,后者决定摘要或答案本身会不会中途写停。这两个数字不落实,后面所有“模型答得不对”的判断都缺少依据。

长文档进模型前,先把上下文上限和输出上限当成两个必须查证的参数:前者决定一次请求能放多长的原文与历史,后者决定摘要或答案能写多长。文档没写清就先留空,用递增长度输入探测,确认是明确报错还是静默截断。稳妥路径是切成带重叠和位置标记的分块,先逐块摘要再汇总,并在请求外层保留长度错误的降级分支。

在官方文档里定位上下文上限与输出上限两个数字,并记录出处

上下文上限和输出上限影响的东西不一样。上下文上限影响你能把多长的原文、多少轮历史和多少工具返回塞进同一次请求;输出上限影响模型这一次能写出多少字,长文档摘要若要求“逐节展开”,往往先撞输出上限,表现是回答写到一半停住,而不是抛错。查证时按这个顺序做:先在官方文档里找 context window、max input tokens 与 max output tokens 这两类字段,记下模型名、文档页面标题、字段所在小节和查询日期,写进项目里的 notes;同时记清限额单位是 token 还是字符,它决定后面的切分代码按什么单位计算。

文档没写清,或者你走的是第三方网关,就不要凭印象用。可以先按最保守的假设把流程跑通,再用边界探测把实际数字补回来,并在记录里把结论标注成“探测推断”而非官方依据。同一模型在不同接入点、不同参数下的可用上限可能不同,需要结合环境确认。

用递增长度的输入做边界探测,观察是报错还是静默截断

边界探测要回答的不是“最多能塞多少”,而是“超了之后是什么表现”。两种情况差别很大:明确超限会返回错误,常见的是 400、413、422 一类状态码,或错误消息里出现 token、length、too long 等字样;静默截断则返回正常状态码,但请求前部的内容被丢掉,模型基于不完整的文档给结论,表面一切正常。后者才是长文档场景里最容易误判的情况。

先确认上下文上限再喂长文档——Gemini 4 Argon 的输入边界

下面的骨架是通用的:把 call_model 换成你自己的请求函数,按长度阶梯构造输入,在填充串首尾各放一个唯一标记,让模型原样回显,再打印状态码、返回字符数和首尾标记是否命中。

MARK_HEAD = "HEAD-7f3a"
MARK_TAIL = "TAIL-9c1b"

def make_input(n_chars, filler="x"):
    body = filler * max(0, n_chars - len(MARK_HEAD) - len(MARK_TAIL))
    return MARK_HEAD + body + MARK_TAIL

def probe(steps):
    for n in steps:
        text = make_input(n)
        try:
            resp = call_model(
                model="你的模型名",
                prompt=text + "\n请原样回显首尾两个标记",
            )
            out = resp.get("text", "")
            print("chars=%s status=%s out_len=%s finish=%s head_ok=%s tail_ok=%s" % (
                n, resp.get("status"), len(out), resp.get("finish_reason"),
                MARK_HEAD in out, MARK_TAIL in out))
        except Exception as e:
            print("chars=%s error=%s msg=%s" % (n, type(e).__name__, e))

probe([1000, 4000, 16000, 64000, 256000, 1000000])

阶梯从小到大等比放大,一旦出现明确报错或者 tail_ok 为 False 就停下,前一个通过的长度就是这类请求的可用边界。探测本身也占输出,用“只回显两个标记”这种短任务作为返回内容,能减少输出上限对判断的干扰。接入层不返回 finish_reason 时,就靠首尾标记和返回字符数判断:head_ok 为真而 tail_ok 为假,通常说明内容在中途被截断。

把长文档切成带重叠的分块,并给每块保留原文位置标记

确认边界后再切。块大小不要贴着上限取,通常按上限的几分之一留余量,把提示词、本块摘要输出和后面汇总阶段的空间一起算进去。切分优先按段落或标题边界收口,避免把一句话劈成两半;重叠用来兜住跨块句子和跨块表格,完全不重叠的切法在“某条结论横跨两页”时最容易漏。

位置标记字段建议至少包含 doc_id、chunk_index、total_chunks、char_start、char_end、section_title。有 char_start 和 char_end,摘要里出现的任何结论都能回到原文核对;有 chunk_index 和 total_chunks,才能判断拼接时有没有缺块。

先确认上下文上限再喂长文档——Gemini 4 Argon 的输入边界
def split_doc(text, doc_id, size=4000, overlap=300):
    chunks, start, idx, n = [], 0, 0, len(text)
    while start < n:
        end = min(start + size, n)
        cut = text.rfind("\n", start + int(size * 0.8), end)
        if cut != -1:
            end = cut
        chunks.append({
            "doc_id": doc_id,
            "chunk_index": idx,
            "char_start": start,
            "char_end": end,
            "section_title": guess_title(text, start),
            "text": text[start:end],
        })
        if end >= n:
            break
        start = end - overlap
        idx += 1
    total = len(chunks)
    for c in chunks:
        c["total_chunks"] = total
    return chunks

重叠长度通常取 200 到 400 字,或块长的 10% 左右,具体看文档密度:表格密集的可以调大,纯散文可以小一些。切完后抽查几处接缝,确认重叠区里没有整句丢失、也没有因为边界回退产生过短的碎块。

先分块摘要再汇总,避免汇总阶段再次超限

逐块摘要时,提示词要强制保留原文锚点,否则摘要阶段就会把数字、文件名、表名这类关键信息磨平。下面是一个可直接改的骨架。

你是文档摘要助手。下面是长文档的第 {i}/{n} 块。
原文位置:{section_title}(字符 {char_start}-{char_end})
请只输出以下字段,不要展开叙述:
- 本块主题:
- 关键结论(逐条,保留原文数字与专有名词):
- 出现的新实体(系统名/文件名/表名/人名):
- 与上一块可能重叠或冲突的点:
- 原文锚点:本块中最值得回看的 1-2 句原话
---- 块内容开始 ----
{chunk_text}
---- 块内容结束 ----

汇总阶段只喂摘要,不喂原文。合并前按 chunk_index 排序、按块号顺序拼接,避免语序错乱。如果摘要合起来估算长度仍接近上限,按这个顺序裁剪:先删过程描述和背景铺垫,保留结论、实体与锚点;仍然超,就把相邻几块先合并成中层摘要,再做最终汇总,而不是直接丢掉靠后的块——被丢掉的往往正是结论所在。

先确认上下文上限再喂长文档——Gemini 4 Argon 的输入边界

核对摘要有没有丢关键结论,可以从原文里挑出一批必现项:核心数字、系统名、表名、章节标题,直接在汇总文本里检索,缺失的回到对应的 char_start 与 char_end 附近重读那一块。这比通读整份汇总更省时间,也更容易定位是哪一步掉的。

在请求里加超限处理分支,捕获长度类错误后自动降级为分块流程

长文档请求不该因为一次长度错误就整批失败。在调用外面包一层,捕获长度类错误后自动走分块流程。错误类型不要只按状态码判断,不同接入方式给出的错误码和消息格式不一样,建议用“状态码加消息关键词”双重匹配,并保留原始错误文本便于事后核对。

def answer_long_doc(doc_text, question):
    try:
        return call_model(prompt=build_full_prompt(doc_text, question))
    except Exception as e:
        if not is_length_error(e):      # 状态码或消息关键词匹配
            raise
        log_event({
            "event": "length_error",
            "model": MODEL,
            "input_chars": len(doc_text),
            "error_type": getattr(e, "code", type(e).__name__),
            "raw_message": str(e)[:500],
            "fallback": "chunked_summary",
        })
        chunks = split_doc(doc_text, doc_id=DOC_ID)
        partials = [summarize(c) for c in chunks]
        merged = merge_summaries(partials)
        if estimate_tokens(merged) > SAFE_INPUT_LIMIT:
            merged = compress_summaries(merged)
        return call_model(prompt=build_final_prompt(merged, question))

降级发生时至少要打这些点:request_id、模型名与接入点、输入字符数或估算 token 数、chunk_count、error_type 与原始错误消息、finish_reason、fallback 名称、整体耗时。这些字段用来回答两个问题:有多少请求真的撞了上限,以及降级路径给出的结果和直连结果差在哪里。降级分支本身也需要定期手动跑一次,否则它可能在下一次故障时才第一次被执行。