获取 API 密钥

100k 长上下文实战:续写、总结、分段、预算

上下文 100,000 token 听起来很宽裕,真用起来才发现:一部长篇小说放不进去,一份几十页的文档加上要求的输出也会逼近上限。这篇把长上下文当成一笔预算来管理:怎么粗估 token,怎么给输出留位置,长文怎么分段总结,小说怎么用滑动窗口加滚动大纲续写。代码用 Python,思路适用于任何语言。

更新于

要点

  • 100,000 是 prompt 加 completion 的总和,不是单独的输入上限。
  • 中文 token 数只能粗估,约每字 1 到 1.5 个,最终以响应里的 usage 为准。
  • 长文总结用分段加合并,小说续写用最近章节原文加滚动大纲。
  • max_tokens 默认 2048、最大 16,000,写长内容前先算预算再设置。

先把上限算清楚

三个数字决定一切:

  • 上下文总量 100,000 token,prompt 与 completion 合计。
  • max_tokens 默认 2048,单次请求最大 16,000。
  • 请求体不超过 8 MB,这个上限对纯文本基本碰不到,真正先到顶的是 token。

预算公式很简单:prompt 可用 = 100000 - max_tokens。你打算让模型一次输出 4000 token,prompt 最多就只剩 60,000。超出时接口返回 400,不会替你自动截断。所以“把整本书塞进去再说”不是策略,只是赌运气。

另一个常见误解:max_tokens 设得越大越保险。其实它是预留的输出空间,设 16,000 就意味着 prompt 只能有 48,000。按需设置,别无脑拉满。

举个算账的例子,假设你有一份 30 万字的访谈整理稿要总结。按每字 1.3 token 粗估,约 39 万 token,是上限的六倍多,无论如何都不能整份送进去。再假设单次摘要要 800 token 的输出,那每块输入最多 63,000,实际再留一成余量,也就五万多。但这还不是最优解,块太大时模型对中段内容的关注会变弱,所以下面的做法是把块切小,宁可多调几次。

token 估算:近似算法

没有本地分词器时,可以用下面的经验值做粗估。以下均为近似,并非精确值,不同文本差异可达一到两成:

文本类型近似换算
中文正文约每字 1 到 1.5 token
英文约每 4 个字符 1 token,或每词 1.3 token 左右
代码、JSON、符号密集文本比普通文本更耗 token,建议按偏保守的高值估

一个够用的粗估函数如下,并带上留余量的预算计算:

CONTEXT = 100000

def est_tokens(text: str) -> int:
    """粗估:中文按每字 1.3 token,其余字符按每 4 个字符 1 token。仅为近似。"""
    zh = sum(1 for c in text if "\u4e00" <= c <= "\u9fff")
    other = len(text) - zh
    return int(zh * 1.3 + other / 4) + 1

def max_prompt_budget(max_tokens: int, margin: float = 0.1) -> int:
    """给定计划的 max_tokens,返回 prompt 最多能用多少 token(留 margin 余量)。"""
    return int((CONTEXT - max_tokens) * (1 - margin))

# 例:计划让模型写 3000 token,prompt 预算
print(max_prompt_budget(3000))   # 54900

校准方法:随便取一段典型输入发一次请求,读响应里的 usage.prompt_tokens,和你的估算对比,算出自己业务数据的真实倍率,然后把系数改成实测值。估算只用来决定“要不要分段”,不用来对账,对账看 usage。

长文档总结:分段加合并

文档超过预算时,标准做法是 map-reduce:先分段各自概括,再把概括合并成总摘要。注意以下细节:

  1. 按段落边界切,不要按固定字数硬切,否则会把句子拦腰截断。
  2. 每块预算留足,建议不超过上限的三分之一,给指令和输出留空间。
  3. 分段摘要要求保留人名、数字、关键事件,否则合并时信息已经丢了。
  4. 摘要的摘要仍然太长时,再递归一层。
import os
from openai import OpenAI

client = OpenAI(base_url="https://api.wushenchaapi.com/v1", api_key=os.environ["API_KEY"])

def split_paragraphs(text, budget):
    """按段落切块,每块估算不超过 budget token。"""
    chunks, cur, used = [], [], 0
    for p in text.split("\n"):
        t = int(len(p) * 1.3) + 1          # 中文粗估
        if used + t > budget and cur:
            chunks.append("\n".join(cur))
            cur, used = [], 0
        cur.append(p)
        used += t
    if cur:
        chunks.append("\n".join(cur))
    return chunks

def ask(prompt, max_tokens=800):
    r = client.chat.completions.create(
        model="uncensored",
        temperature=0.3,
        max_tokens=max_tokens,
        messages=[{"role": "user", "content": prompt}],
    )
    return r.choices[0].message.content

def summarize_long(text, budget=12000):
    parts = split_paragraphs(text, budget)
    partial = [ask("用不超过 200 字概括下面这段,保留人名和关键事件:\n\n" + p) for p in parts]
    merged = "\n".join(f"第{i+1}部分:{s}" for i, s in enumerate(partial))
    return ask("下面是分段摘要,请合并成一份 400 字以内的整体摘要:\n\n" + merged, max_tokens=900)

这里 temperature 取 0.3,总结类任务要稳定。每段调用互相独立,可以并发,但注意每把 key 每分钟 300 次请求的限制,几十段的文档完全不会碰到。

块的大小怎么定,没有标准答案。经验上,一万到一万五千 token 一块,兼顾了细节保留和调用次数。拿一份样本分别用 5000、12000、25000 三种块大小跑一遍,对比摘要里漏掉的关键事实数量,就能选出适合你文档类型的值。合同、技术文档这类信息密度高的材料,块要更小;对话记录这类冗余多的,可以更大。

小说续写:滑动窗口加滚动大纲

写到第十几章时,全文已经放不下,也没必要全放。思路是两层记忆:

  • 近景:最近两三章原文,保证文风、对话节奏和场景细节连贯。
  • 远景:更早章节压缩成大纲,保留人物关系、伏笔和未解决的线索。

每写完一章,就让模型把这一章概括成三五行追加进大纲;大纲本身超过 3000 token 时,再整体压缩一次。

def continue_story(chapters, outline, new_hint, keep_last=3):
    """chapters: 已写章节列表。只带最近 keep_last 章原文,更早的用 outline(滚动大纲)代替。"""
    recent = "\n\n".join(chapters[-keep_last:])
    prompt = (
        f"【全书大纲(早期章节摘要)】\n{outline}\n\n"
        f"【最近章节原文】\n{recent}\n\n"
        f"【下一章要求】\n{new_hint}\n\n请写下一章,约 2000 字。"
    )
    return ask(prompt, max_tokens=4000)

伏笔是最容易丢的东西。建议在大纲里单列“未回收的伏笔”一栏,续写时明确要求本章处理其中一条。人物名、地名也列成固定清单放在 system 里,避免滑出窗口后模型自己改名。

续写时还有一个容易被忽视的问题:风格漂移。模型会越写越贴近最近几章,如果前面章节的风格和后来不一致,就会放大。办法是在 system 里写一段“文风样本”,挑一段你最满意的原文放进去,作为固定锚点,不随窗口滑动。

max_tokens 与截断

回答被截断有两种情形,要分清:

  1. finish_reason 为 length:撞上了你设置的 max_tokens。解决办法是调大 max_tokens,或者让模型分段写,“写到这里先停,我说继续再写”。
  2. 请求直接 400:prompt 加 max_tokens 已超过 100,000。解决办法是缩短输入或调小 max_tokens。

一个实用的预算对照:

任务建议 max_tokensprompt 可用(约)
摘要、抽取80063,200
单章续写(约 2000 字)400060,000
万字级长输出16000(上限)48,000

长输出同时受“响应时间”影响,建议配合流式输出,边生成边显示。

续写被截断时,不要简单把半截内容丢给模型说“继续”。更稳的做法是把已生成的部分作为 assistant 消息放回 messages,再加一条 user 消息“从上一句话之后接着写,不要重复”,这样衔接最自然。注意这一步会让 prompt 变长,要重新核对预算。

长上下文上线清单

  • 每次请求前用估算函数算 prompt,超预算就走分段分支。
  • 每次响应记录 usage,持续校准估算系数。
  • system 与大纲放在内容最前,指令里的关键要求在最后重申一遍。
  • 检查 finish_reason,是 length 就告警或续写。
  • 历史对话设保留上限,超限时丢弃最旧的,不要等接口报 400。

相关内容:提示词结构见 Prompt 写法,报错处理见错误码排障手册,计价见价格页。

最后提醒一点:长上下文不等于长记忆。模型每次请求都是从零读你给的内容,不会记得上一次调用的任何事,需要连续性就必须你自己把历史带上。

常见问题

100k 上下文包含输出吗?

包含。上限是 prompt 和 completion 的总和,所以 max_tokens 设得越大,能放进去的输入就越少。

中文一个字到底是几个 token?

只能粗估,大致每字 1 到 1.5 个,随文本不同有浮动。最准确的办法是读响应里的 usage 字段,用实测值校准。

超过上限会自动截断输入吗?

不会,请求会返回 400。需要你在调用前自己分段或丢弃旧内容。

单次最多能生成多长?

max_tokens 默认 2048,单次最大 16,000。更长的内容请分多次生成,并用大纲或摘要保持连贯。

只需填写表单即可获取密钥

创建账户,复制密钥,修改 Base URL。配置就是这么简单。