最近在项目里做了一些 LLM 长文本生成的实践,踩了不少坑。本来想直接调通用 API 一把梭,结果发现真正交付的时候问题一大堆。

这篇笔记把我在长文本生成场景下总结的几个工程化思路整理出来,给同样在做类似业务的同行参考。文章里的代码示例都跑过,但具体业务里需要自己适配。

涉及到的技术栈主要是 Python + 主流大模型 API(OpenAI / Claude / DeepSeek 这套),没有偏向任何具体厂商。


一、问题的起点:为什么直接 prompt 写长文不靠谱?

很多人的第一反应是:

response = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "帮我写一篇 1 万字的 XXX 报告"}],
    max_tokens=8000
)

然后期待 LLM 一次性吐出一份高质量的长文。

实际跑下来,至少会撞到这几堵墙:

1. 输出截断

绝大多数模型的单次响应都有上限。即使把 max_tokens 拉满,到了某个 token 数也会被强制截断。真正能稳定生成的连续文本,通常在 2000-4000 字左右,再往上就开始不稳定。

2. 上下文一致性衰减

当生成内容超过 3000 字以后,模型对前文的"记忆"会显著衰减。表现是:术语前后不统一、人物/概念定义飘忽、章节之间的逻辑断层。

这个问题用 attention 可视化可以看得很清楚——前文的 attention 权重会随着距离指数下降。

3. 事实性幻觉

让模型直接生成涉及"客观事实"的内容(数据、引用、案例),模型会以非常自信的语气编造不存在的内容。这在写作类任务里是致命的。

我自己测过,让 GPT-4 写一段关于某个细分行业的报告,里面给出的数据来源、报告名称,70% 是不存在的。模型并不知道自己在胡说,但它会一本正经地把这些信息组装成看起来合理的句子。


二、工程化思路:把"生成长文"拆成"生成短文 + 拼接"

我最后落地的方案是放弃"一次性生成",改成分段生成 + 上下文窗口管理

核心代码结构大致是这样:

def generate_long_text(topic, sections, reference_context):
    """
    分段生成长文,每段保留前文摘要作为上下文锚点
    """
    full_text = []
    prev_summary = ""
    
    for i, section in enumerate(sections):
        # 构造当前段落的 prompt,带上前文摘要 + 真实参考资料
        prompt = build_section_prompt(
            topic=topic,
            current_section=section,
            prev_summary=prev_summary,
            references=reference_context  # 关键:外部资料注入
        )
        
        section_text = call_llm(prompt)
        full_text.append(section_text)
        
        # 生成本段摘要,作为下一段的上下文
        prev_summary = summarize(section_text, max_words=200)
    
    return "\n\n".join(full_text)

这个结构里有几个关键设计点:

设计点 1:先有大纲,再有内容

不要让 LLM 自己决定"接下来写什么"。把整篇文章的章节结构(sections)显式定义出来,让 LLM 严格按章节生成。

我后来发现,大纲这一步其实可以让 LLM 自己先生成一版,然后人工 review 调整,再用调整后的大纲去驱动正文生成。这样既利用了 LLM 的发散能力,又保证了人工控制。

设计点 2:滚动摘要作为上下文锚点

每生成一段后,把这段内容压缩成 ~200 字的摘要,作为下一段生成的输入。这样既绕开了上下文窗口的限制,又保持了前后逻辑的连贯性。

def summarize(text, max_words=200):
    prompt = f"""请用不超过 {max_words} 字概括以下内容,
    重点保留:核心观点、关键术语、已经使用的论据。
    
    原文:{text}
    """
    return call_llm(prompt, max_tokens=300)

设计点 3:参考资料显式注入(对抗幻觉)

要解决幻觉问题,最有效的方法不是 prompt 调优,而是给模型一个真实的资料库

我的做法是预先准备好相关资料(论文摘要、行业数据、权威报告片段),存在向量数据库里。生成每段内容时,先根据 section 主题做检索,把相关资料作为 context 注入到 prompt:

def build_section_prompt(topic, current_section, prev_summary, references):
    # 从向量库检索本段相关资料
    relevant_refs = vector_db.search(
        query=current_section, 
        top_k=5
    )
    
    refs_text = "\n".join([f"- {r.content}" for r in relevant_refs])
    
    return f"""
    主题:{topic}
    本段标题:{current_section}
    前文摘要:{prev_summary}
    
    可用的参考资料(只能基于这些资料写,不要编造):
    {refs_text}
    
    请基于以上资料撰写本段内容,约 800-1200 字。
    如果资料不足以支持某个论点,请明确说"资料不足",
    不要编造数据或来源。
    """

最后这句 prompt 很关键 —— "资料不足请明确说出来,不要编造"。这一句话能让幻觉率显著下降,亲测有效。


三、生成后的处理:同样需要工程化

很多人以为生成完了就结束了。实际项目里,后处理环节的工作量不亚于生成本身

我建议至少做这三件事:

1. 一致性检查

把生成的长文整体过一遍,让另一个 LLM 实例做"挑刺",主要检查:

def consistency_check(full_text):
    prompt = f"""请审阅以下长文,检查并列出:
    1. 前后定义不一致的术语
    2. 数据/事实自相矛盾的地方
    3. 论证逻辑跳跃或断裂的段落
    
    用 JSON 格式输出,每条问题包括位置(段落号)、问题类型、问题描述。
    
    原文:{full_text}
    """
    return call_llm(prompt, response_format="json")

这个步骤能发现至少一半的低级错误。

2. 风格统一

分段生成的副作用是:每一段可能有微妙的风格差异(用词、句式、语气)。最后过一遍统一的 polish prompt:

polish_prompt = """请重写以下段落,保持原意不变,但:
- 统一专业术语用法
- 调整为正式书面语
- 拆解过长的复合句
"""

3. AI 痕迹检测与改写(这一点最近做项目时绕不过去)

现在很多接收方(学术机构、客户)会用 AI 检测工具反向扫描内容。如果你的输出需要"看起来不像 AI 写的",单靠生成参数调整是不够的,需要做语义层面的改写。

这里有个比较有意思的现象:AI 生成的文本有相对稳定的"统计指纹" —— 高频连接词、固定句式偏好、过度规整的段落长度。要降低这种指纹,比较有效的做法是:

def humanize(text):
    prompt = f"""请改写以下文本,保持核心意思不变,但:
    - 打破固定句式,有意混用长短句
    - 去掉"综上所述"、"不可否认"等套话
    - 加入一些口语化但不失专业的过渡
    - 控制段落长度,不要全部一样长
    
    原文:{text}
    """
    return call_llm(prompt, temperature=0.9)  # 注意:temperature 要拉高

注意最后一行——这里 temperature 必须高一点,否则模型还是会输出概率最高的"标准 AI 句式",等于白改。


四、绕不开的话题:这类需求适合自己写还是用现成工具?

写到这里,可能有同学会问:我业务里就一个长文本生成需求,要自己搭这套基建吗?

我的建议是分情况:

  • 如果是给自己/小团队用,建议自己写。一是可以深度理解 LLM 的能力边界,二是后续要扩展功能(比如加 RAG、加格式化输出)也方便。
  • 如果是高频、强合规的业务(比如学术写作、法律文书、医疗报告),市面上已经有不少专门的垂直工具,集成了上述的分段生成、资料检索、AI 痕迹处理等模块。直接用现成工具比自己搭一套更划算。

我自己写代码的同时,也调研过几款做长文本生成的垂直产品,发现它们底层基本都是上面这套思路的工程化封装。所以如果你不想踩坑,直接用现成的也是一种选择。这部分我就不展开了,免得变成软文。


五、一些零散的踩坑笔记

最后把我踩过的几个坑列一下:

1. 不要用 system 角色塞太多上下文

我一开始把所有的参考资料、风格指南都塞到 system prompt 里。后来发现,当 system prompt 超过 1000 token 后,模型对其中后半段内容的遵循度会显著下降。最好是把关键约束放在 user prompt 的开头和结尾(首尾效应)。

2. 不同模型的"性格"差异比想象中大

同样的 prompt,GPT-4 倾向于结构化、规整;Claude 倾向于细腻、有"温度";DeepSeek 偏向"理性、直接"。生成长文时,最好选一个模型从头跑到尾,混着用会出现明显的风格断层。

3. 流式输出和非流式输出的差异

流式输出(stream=True)在用户体验上更好,但有个坑:流式输出时模型的"自我修正"能力会弱一些,因为它无法"回头改写"已经输出的内容。对质量要求高的场景,建议用非流式 + 完整重生成。

4. token 计算别用估算

写代码时不要用"中文字符 × 1.5 ≈ token"这种估算公式,不同 tokenizer 的差异很大。建议用模型官方提供的 tokenizer 库精确计算,特别是接近上下文上限的时候。

import tiktoken
encoding = tiktoken.encoding_for_model("gpt-4")
token_count = len(encoding.encode(text))

写在最后

LLM 现在很强,但远没到"一句 prompt 解决所有问题"的程度。用好它,本质上是个工程问题,而不是 prompt 问题

把长文本生成拆成可控的小步骤、显式注入真实资料、生成后做严格的质检——这些都是软件工程里最朴素的道理,套到 LLM 应用上一样成立。

希望对在做类似业务的同学有点参考价值。有不同实践经验的也欢迎在评论区交流。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐