先把上限算清楚
三個數字決定一切:
- 上下文總量 100,000 token,提示詞與補全合計。
max_tokens預設 2048,單次請求最大 16,000。- 請求體不超過 8 MB,這個上限對純文本基本碰不到,真正先到頂的是 token。
預算公式很簡單:prompt 可用 = 100000 - max_tokens。你打算讓模型一次輸出 4000 token,prompt 最多就只剩 60,000。超出時接口返回 400,不會替你自動截斷。所以「把整本書塞進去再說」不是策略,只是賭運氣。
另一個常見誤解:max_tokens 設得越大越保險。其實它是預留的輸出空間,設 16,000 就意味著提示詞只能有 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:先分段各自概括,再把概括合併成總摘要。注意以下細節:
- 按段落邊界切,不要按固定字數硬切,否則會把句子攔腰截斷。
- 每塊預算留足,建議不超過上限的三分之一,給指令和輸出留空間。
- 分段摘要要求保留人名、數字、關鍵事件,否則合併時資訊已經丟了。
- 摘要的摘要仍然太長時,再遞歸一層。
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,總結類任務要穩定。每段呼叫互相獨立,可以並行,但注意每把金鑰每分鐘 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 與截斷
回答被截斷有兩種情形,要分清:
finish_reason為length:撞上了你設定的 max_tokens。解決辦法是調大 max_tokens,或者讓模型分段寫,「寫到這裡先停,我說繼續再寫」。- 請求直接 400:提示詞加 max_tokens 已超過 100,000。解決辦法是縮短輸入或調小 max_tokens。
一個實用的預算對照:
| 任務 | 建議 max_tokens | 提示詞可用(約) |
|---|---|---|
| 摘要、抽取 | 800 | 63,200 |
| 單章續寫(約 2000 字) | 4000 | 60,000 |
| 萬字級長輸出 | 16000(上限) | 48,000 |
長輸出同時受「回應時間」影響,建議搭配串流輸出,邊生成邊顯示。
續寫被截斷時,不要簡單地把半截內容丟給模型說「繼續」。更穩的做法是把已生成的部分作為 assistant 訊息放回 messages,再加一條 user 訊息「從上一句話之後接著寫,不要重複」,這樣銜接最自然。注意這一步會讓提示詞變長,要重新核對預算。
長上下文上線清單
- 每次請求前用估算函數算提示詞,超預算就走分段分支。
- 每次回應記錄 usage,持續校準估算係數。
- system 與大綱放在內容最前,指令裡的關鍵要求在最後重申一遍。
- 檢查 finish_reason,是 length 就告警或續寫。
- 歷史對話設保留上限,超限時丟棄最舊的,不要等介面報 400。
相關內容:提示詞結構見 Prompt 寫法,報錯處理見錯誤碼排障手冊,計價見價格頁。
最後提醒一點:長上下文不等於長記憶。模型每次請求都是從零讀你給的內容,不會記得上一次呼叫的任何事,需要連續性就必須你自己把歷史帶上。