OpenAI

ChatGPT API 输出字数不达标:分段生成与续写拼接的实用方案

编辑部阅读约 6 分钟

调用 ChatGPT API 生成说明文档时,在 prompt 里反复强调字数往往无效,模型仍会在一两千字处自行结束。本文解释 max_tokens、temperature、finish_reason 与长度的真实关系,给出按大纲分段生成、续写拼接、程序校验自动补写三套可落地的做法,并附提示词技巧与调用检查清单。

用 API 批量生成说明文档时,一个很常见的落差是:prompt 里把大纲、每节要点、返回格式都写得清清楚楚,还特意强调“内容尽量详细,至少三千字”,结果返回的正文只有一两千字就结束了。调 temperature、调 top_p、把 max_tokens 调大,甚至换更长的上下文模型,都不见明显改善。

这个问题不是参数没调对,而是由模型的生成方式决定的。下面把概念、原因和几种可落地的做法分开说清楚。

先分清几个容易混淆的概念

参数 / 字段 实际作用 与长度的关系
max_tokens 单次回复最多能生成多少 token 是上限不是目标,设大不会让模型写得更长;设小反而会被硬截断
temperature / top_p 控制采样的随机性 与长度无直接关系,调高只会让内容更发散
stop 自定义停止序列 设置不当会让回复提前结束
finish_reason 结束原因 stop 表示模型认为写完了,length 表示撞到了 max_tokens

关键在最后一行。如果返回的 finish_reason 是 stop,说明模型自己判断“这段回答已经结束”,此时在同一轮对话里再怎么要求加长,它也很难推翻自己刚刚做出的结束决定。如果 finish_reason 是 length,那才是真的被上限截断,需要调大 max_tokens 或减少其他占用。

另外,中文字数和 token 不是一一对应:一个汉字通常对应一到两个 token,具体取决于分词方式。模型本身也不具备可靠的字数统计能力,它对“三千字”没有可执行的感知,只会依据训练中形成的篇幅习惯作答。所以用“至少 3000 个中文字”甚至“按 UTF-8 字节统计不少于 N”去约束,模型会照抄这个要求、也会信誓旦旦地说自己写够了,但输出长度并不会真的改变。

为什么强调字数基本无效

模型对长度的控制是“软”的。它能理解方向性描述,比如“展开说明”“多举几个例子”“每个要点单独成段”,这类指令对应的是它熟悉的写作模式。而“精确到某个字数”属于计数任务,是它不擅长的部分。反复强调字数,最常见的结果是在结尾多出一句“以上约三千字”,正文篇幅原地不动。

有效的思路只有两条:把长度翻译成模型能执行的结构约束,以及把长文档拆成多次请求。

方案一:按大纲分段生成再拼接

这是最可控的做法,思路是把“写一篇长文”换成“写若干篇短文”。

  1. 第一次请求只让它输出大纲,最好是结构化格式(Markdown 标题列表或 JSON 数组),便于程序解析。
  2. 对大纲里的每一个小节单独发起一次请求,只写这一节,并在 prompt 中给出该节的最低篇幅要求。
  3. 所有小节生成完毕后拼接,最后单独做一次统一的润色和去重——注意这一步只允许改语句,不允许压缩篇幅。

单节请求的 prompt 可以这样写:

你是一名技术文档撰写者。请只写下面这一小节的内容。

小节标题:{section_title}
所属文档大纲:{outline}

要求:
1. 分 3 个以上自然段,每段不少于 5 句。
2. 必须覆盖以下要点,每个要点至少展开一段:{points}
3. 不要写其他小节,不要写全文总结,不要重复标题。

用“段数 × 每段句数”“要点必须逐个展开”来代替“至少 N 字”,模型执行起来明显更稳定。单节篇幅建议控制在几百字量级,一节太长又会退回到原来那个问题。

方案二:续写模式及其坑

如果只是差一点点,最省事的办法是续写:把已经生成的内容作为 assistant 消息回传,再发一条“继续,从断点处接着写,不要重复已经写过的内容”。多轮结果拼接起来即可。

但续写有几个必须知道的限制:

  • 每续一次上下文都会变长,费用上升,且模型越往后越容易跑题、重复、堆砌套话;
  • 续写次数多了,内容会逐渐偏离最初的大纲和中心主题;
  • 拼接时要做去重,比较已有内容的末尾几段与新返回内容的开头,避免出现重复段落;
  • 每次续写请求里最好重申一次大纲和当前小节目标,抵消上下文漂移。

实践上,续写一到两次通常没问题;靠它连写十几次凑出长文,质量会明显下降。

方案三:程序校验长度并自动补写

不管用哪种方式生成,都应该由程序来判断长度是否达标,而不是相信模型的自我描述。基本循环是:生成 → 统计实际字符数 → 不足则发起扩写或续写请求 → 拼回原文 → 再次校验,并设置最大重试次数防止死循环。

import re

def char_count(text):
    # 只统计中文字符与英文单词,忽略空白和标点
    zh = len(re.findall(r'[\u4e00-\u9fa5]', text))
    en = len(re.findall(r'[a-zA-Z]+', text))
    return zh + en

def expand_if_needed(client, text, target, max_rounds=3):
    for _ in range(max_rounds):
        if char_count(text) >= target:
            break
        resp = client.chat.completions.create(
            model=MODEL,
            messages=[
                {"role": "system", "content": "你是技术文档编辑,只做扩写,不改事实、不删内容。"},
                {"role": "user", "content": (
                    f"下面这段内容偏短。请在不改变原意、不删除已有段落的前提下扩写,"
                    f"补充示例、边界情况和注意事项,至少新增两段。只输出扩写后的完整内容。\n\n{text}"
                )},
            ],
            max_tokens=MAX_TOKENS,
        )
        text = resp.choices[0].message.content
    return text

对已经成型的段落做“扩写”,比让它从头再写一遍更容易达到目标长度,因为模型只需在既有内容上添加,而不用重新规划结构。

提示词层面的几个技巧

  • 用可数任务替代模糊要求:“列举 5 个方面,每个方面单独一段展开”远比“写详细一点”可控。
  • 给出密度示例:把一段你觉得长度和详细程度合适的样文贴进去,说“每一节都写到这个密度”,比给数字更有效。
  • 禁止提前收尾:明确写出“不要写全文总结”“不要在末尾加结语”,模型很容易用一段总结草草结束。
  • 要求结构化输出:Markdown 分节或 JSON,程序可以据此检查哪一节缺失或过短,再单独补。
  • 把字数要求写成下限而非区间:区间会让模型倾向于取下限甚至更低。

调用层面的检查清单

  • max_tokens 是否留足余量,不要按预期字数卡死;
  • 是否误设了 stop 序列导致提前结束;
  • 历史消息是否过长,挤占了输出空间,也可能让模型倾向简短回答;
  • 模型与版本的长文能力、上下文窗口不同,具体以官方最新文档为准;
  • 客户端超时是否留够,长文生成可能耗时数十秒到几分钟,并要做好幂等重试;
  • 生成结果是否按实际字符数校验,而不是看模型自己的说法。

批量生成长文档通常是多轮请求、长时间运行的任务,本地网络抖动或机器休眠都可能导致中途失败。除了在脚本里做断点续跑,也可以把这类需要连续跑很久的生成任务放到一台长期在线的机器上执行,例如整机托管在机房、通过远程连接使用的独享 Mac 环境,网络稳定、可长期在线。这只是可选做法,单次生成或短文档完全没必要。

注意事项

  • 没有任何接口层面的参数能“保证精确字数”,所有方案都只是逼近,需要程序侧做校验和兜底。
  • 为凑长度要求模型“啰嗦没关系”,很容易换来重复表述和空话,可读性反而下降;长度应该服务于内容覆盖度,而不是反过来。
  • 分段生成的总成本高于一次性生成,请求数和重复上下文都会增加开销,批量任务前先估算预算。
  • 接口行为、上下文长度和计费规则会变化,落地前以官方最新说明为准。

小结

只差一点篇幅,续写一两次最省事;需要稳定产出三千字以上的结构化文档,按大纲分段生成加程序校验补写是更可靠的组合;prompt 里的字数要求只能当辅助,真正起作用的是结构约束、可数任务和多次请求的拆分。

ChatGPT APIPrompt 技巧长文本生成分段续写finish_reason