
把七部《哈利波特》压成六句话这个挑战听起来像内容运营的标题党实际上是一个相当硬核的长文本压缩问题。七本书英文原文合计百万词量级六句话大概一百多字压缩比接近一比七千。任何做过长文档摘要的人都知道压缩比一旦超过某个阈值模型输出的就不再是摘要而是“看起来像摘要的东西”人物还在因果链断了主题被抹平成几句正确的废话。这篇文章不评测某个具体模型跑分而是拆一套可复用的长文本六句话摘要流水线怎么切块、怎么做层次归并、怎么约束输出句数、怎么评估压缩后信息有没有塌陷、怎么批量跑完整个书系、接口怎么调用、成本怎么算、哪里最容易翻车。同时拿“魔法故事为什么能风靡全球”当作一个真实的评估靶子——如果六句话之后读者仍然能感受到成长叙事、规则清晰的魔法系统、以及选择与代价这条暗线那这次压缩就是成功的如果只剩“一个男孩发现自己会魔法并打败了坏人”那就是失败的。这套流水线适合几类人做长文档摘要、会议纪要、论文速读的工程同学给 RAG 做前置压缩、想把百万字知识库变成可控上下文的知识库维护者以及做内容运营、需要把长内容改写成短卡片的自媒体作者。前面几节先给能力矩阵和边界后面直接进代码跑通分块、摘要、六句话归并、质量评估、批量任务和排错。1. 核心能力速览先把这个方案的能力边界摆清楚。它不是某个带安装包的开源项目而是一条可以挂到任意兼容 OpenAI 协议接口上的处理链文本进来六句话出去中间的分块、归并、句数约束、质量打分全部由你自己控制。能力项说明任务类型超长文本层次化摘要、定句数压缩、主题骨架提取输入规模单章节到百万词级书系均可取决于分块策略与上下文窗口输出约束固定 6 句可扩展为 3 句、10 句、要点卡片技术路径清洗 → 分块 → 块级摘要 → 层次归并 → 定句数重写 → 质量评估运行依赖Python 3.10、一个兼容 OpenAI 接口的推理服务或云端接口硬件门槛需按实际模型版本和推理参数测试本地跑大上下文模型对显存要求高小模型分块跑对显存友好是否支持 API支持流水线本身以函数调用形式组织可包成 HTTP 服务是否支持批量任务支持按章节或按书目并行需要自己做并发控制和失败重试适合场景长文速读、知识库前置压缩、内容改写、书籍/报告结构化归档不适合场景需要精确引用原文、需要逐字保真的法律与医学文本处理这里必须强调一句显存占用、单本处理耗时、token 成本这三项本文不给具体数字因为不同模型、不同量化精度、不同上下文长度差异极大任何写死的数字都会误导人。正确做法是在你自己的机器上按第 8 节的方法实测一遍再决定用哪档配置。2. 适用场景与使用边界先说什么情况适合用。第一类是长文档速读比如几十万字的行业报告、几本书的技术史、一部小说的全套剧情梳理人读不完模型可以先用层次摘要压到可读长度。第二类是知识库前置压缩把原始长文本压成结构化摘要再入向量库检索时命中率往往比直接切碎原文更高因为摘要层已经做完了一次语义聚合。第三类是内容改写把长文变成短卡片、把一本书变成一条推送本质也是定句数压缩。再说边界。定句数摘要天然会丢细节它保的是骨架不是血肉。如果你要做的是合同条款核对、病历摘要、法条比对这套东西只能当辅助最终必须以原文为准。六句话读完一本书读的是“这本书在讲什么”不是“这本书里每一处怎么写的”。把这两件事混为一谈是这类流水线最常见的误用。版权和合规这块必须单独拎出来讲。拿《哈利波特》做实验处理的应该是你自己合法持有的文本实验产物摘要、评估结果用于个人学习和技术研究没问题但不能把原文批量复制、拼接、二次分发也不能把整本书的摘要做成公开的替代性阅读产品。做批量任务时输入目录里的素材来源要可追溯输出目录要做好访问控制。涉及真人素材、未公开手稿、企业内部文档的先确认授权范围再决定要不要送进第三方接口。本地部署在这件事上有天然优势文本不出机器风险面小很多。还有一条容易被忽略的摘要结果不能当作事实来源。模型在压缩过程中会做合理推断也会做不合理推断。人物关系、时间线、因果归属这类信息如果后面要拿去做决策必须回原文校验。第 6 节会给一个可操作的抽检方法。3. 环境准备与前置条件环境这块给一份通用检查清单不写死版本号因为模型服务和依赖更新很快写死了反而容易踩坑。操作系统Linux 或 Windows 均可批处理建议 Linux路径和并发都好管。Python3.10 及以上虚拟环境隔离别和系统环境混用。推理后端本地推理服务或云端兼容接口二者用同一套调用代码只改 base_url。磁盘原始文本、中间摘要、最终结果分三个目录中间产物体积通常是原文的数倍。网络调云端接口要稳定批量任务建议加超时和重试。端口本地起服务默认端口容易撞车起之前先看占用。先把虚拟环境和依赖装上# 创建隔离环境避免污染系统 Python python -m venv venv # Linux / macOS 激活 source venv/bin/activate # Windows PowerShell 激活 # .\venv\Scripts\Activate.ps1 # 安装依赖版本按实际可用版本调整 pip install --upgrade pip pip install openai tqdm tenacityopenai只用来做协议客户端指向本地服务同样能用tqdm打进度条tenacity做重试。三个库足够撑起整条流水线不需要引入重型框架。然后是目录约定这一步看着无聊但批量跑书系的时候能救命workspace/ ├── config/ │ └── pipeline.json # 模型地址、分块参数、并发数 ├── inputs/ # 原始文本按书目/章节命名 │ └── book01/ │ ├── ch01.txt │ └── ch02.txt ├── chunks/ # 分块中间产物 ├── summaries/ # 块级与章节级摘要 ├── outputs/ # 最终六句话结果 └── logs/ # 每次运行的请求日志与错误日志命名规则建议book{序号}_ch{章节号}全程不换。中途换命名后面归并脚本就得写一堆兼容逻辑纯属给自己找麻烦。4. 部署与启动方式流水线本身没有“一键启动”这种说法它是脚本形态加一个可选的服务外壳。先给配置文件把可变参数全部外置后面调参只改 JSON不动代码。{ base_url: http://127.0.0.1:8000/v1, api_key: EMPTY, model: your-model-name, chunk_max_chars: 8000, chunk_overlap: 300, summary_temperature: 0.2, final_temperature: 0.3, max_concurrency: 4, timeout_seconds: 180, retry_times: 3 }几个参数值得单独解释。chunk_max_chars是字符数不是 token 数中英文差异很大中文 8000 字大概对应几千 token英文 8000 字符大概两千 token 上下实际要按你的模型上下文留足余量。chunk_overlap是块间重叠主要防止一个关键情节被切在两块边界上两边的摘要都没提。summary_temperature压到 0.2 左右是为了让块级摘要稳定final_temperature稍高一点让六句话读起来不那么僵硬。并发数别一上来拉满先用 2 到 4 试。如果要把流水线暴露成服务给别的系统调用一个轻量 HTTP 外壳就行下面是最小可用版本实际路径和端口按需改# server.py import json from fastapi import FastAPI from pydantic import BaseModel from pipeline import run_pipeline # 你的流水线入口函数 app FastAPI(titleLongText Six-Sentence Summarizer) class SummarizeRequest(BaseModel): text: str sentences: int 6 language: str zh app.post(/api/summarize) def summarize(req: SummarizeRequest): result run_pipeline( textreq.text, target_sentencesreq.sentences, languagereq.language, ) return {code: 0, sentences: result[sentences], usage: result[usage]}启动命令# 仅监听本机避免暴露到公网 uvicorn server:app --host 127.0.0.1 --port 7861 --workers 1--workers 1是有意的。多 worker 会让并发控制失效真正的并发应该在流水线内部用信号量管而不是靠多进程放大请求量。这一步做错批量任务很容易把后端服务打挂。5. 六句话摘要流水线实现这一节是核心。整条链路拆成四步清洗分块、块级摘要、层次归并、定句数重写。每一步都能单独测单独调不要一次写完整条链再调试出了问题根本定位不到哪一步。5.1 文本清洗与分块分块的第一原则是不要按固定字符数硬切。硬切会把一句话切成两半把一段对话拆成两个块摘要质量直接掉档。按段落切超过阈值再退化成按句子切。# pipeline.py import re from typing import List def clean_text(text: str) - str: 去掉多余空行、页眉页脚残留、连续空白。 text text.replace(\r\n, \n).replace(\r, \n) text re.sub(r[ \t], , text) text re.sub(r\n{3,}, \n\n, text) return text.strip() def split_sentences(text: str) - List[str]: 中英文混排的粗粒度断句够用即可。 parts re.split(r(?[。!?\.])\s, text) return [p.strip() for p in parts if p.strip()] def chunk_text(text: str, max_chars: int 8000, overlap: int 300) - List[str]: 按段落优先、句子兜底的策略切块带尾部重叠。 paragraphs [p.strip() for p in text.split(\n) if p.strip()] chunks: List[str] [] buf for para in paragraphs: if len(para) max_chars: # 超长段落先按句子拆开再逐句塞入 for sent in split_sentences(para): if len(buf) len(sent) 1 max_chars: buf f{buf}\n{sent} if buf else sent else: if buf: chunks.append(buf) buf sent[-max_chars:] continue if len(buf) len(para) 1 max_chars: buf f{buf}\n{para} if buf else para else: if buf: chunks.append(buf) buf para if buf: chunks.append(buf) # 加上下文重叠降低边界信息丢失 if overlap 0 and len(chunks) 1: overlapped [chunks[0]] for i in range(1, len(chunks)): tail chunks[i - 1][-overlap:] overlapped.append(tail \n chunks[i]) chunks overlapped return chunks重叠不是越大越好。重叠 300 字处理一本 30 万字的书会多出百分之几的重复计算量可以接受重叠 2000 字成本直接翻倍收益却很小。300 到 500 是个合理区间。5.2 块级摘要块级摘要的提示词要写得克制。不要让它一次吐太多东西目标只有一个把这一段里发生的事和涉及的人抽出来。# llm_client.py import os import json from openai import OpenAI CFG json.load(open(config/pipeline.json, encodingutf-8)) client OpenAI( base_urlos.getenv(LLM_BASE_URL, CFG[base_url]), api_keyos.getenv(LLM_API_KEY, CFG[api_key]), timeoutCFG[timeout_seconds], ) def chat(prompt: str, system: str , temperature: float 0.2) - str: messages [] if system: messages.append({role: system, content: system}) messages.append({role: user, content: prompt}) resp client.chat.completions.create( modelCFG[model], messagesmessages, temperaturetemperature, ) return resp.choices[0].message.content.strip()块级摘要的提示词模板CHUNK_SYSTEM 你是一名严谨的文本摘要工程师只提取文本中明确出现的信息不做推测。 CHUNK_PROMPT 请对下面这段文本做压缩摘要要求 1. 输出 3 到 5 个要点每个要点一句话 2. 保留人物、事件、因果、转折删掉描写性内容和重复信息 3. 不要评价不要抒情不要补充原文没有的内容 4. 用中文输出。 文本 {chunk} 注意第三条。不让模型“评价”是因为评价会污染后续归并——归并阶段需要的是一堆平铺的事实不是一堆观点。观点层留到最后六句话那一步再处理。5.3 层次归并与六句话重写归并是分层的。章节内的块摘要先归并成章节摘要章节摘要再归并成书摘要书摘要再归并成书系摘要。三层足够了再多一层收益递减。def merge_summaries(summaries: list[str], level: str chapter) - str: joined \n\n.join(f[片段{i1}]\n{s} for i, s in enumerate(summaries)) if len(joined) CFG[chunk_max_chars]: prompt f下面是同一{level}的多段要点摘要请合并成一段连续摘要 1. 去重合并重复信息 2. 保留时间推进和因果关系 3. 控制在 300 字以内。 {joined} return chat(prompt, systemCHUNK_SYSTEM, temperature0.2) # 超长则先两两归并递归压缩 mid len(summaries) // 2 left merge_summaries(summaries[:mid], level) right merge_summaries(summaries[mid:], level) return merge_summaries([left, right], level)最后一步定句数重写。这一步的提示词是全文最关键的地方写不好就前功尽弃。FINAL_PROMPT 下面是一部作品的完整摘要。请把它压缩成恰好 {n} 句话。 硬性要求 1. 必须是 {n} 句话多一句少一句都不行 2. 每句只承载一个核心信息点句间要有逻辑推进不要并列堆砌 3. 必须覆盖主角的核心处境、关键转折、代价或牺牲、最终结局 4. 保留专有名词不要用某个人一个地方这种模糊指代 5. 不要出现本书讲述了这个故事告诉我们这类元叙述 6. 不要评论作品好坏。 摘要正文 {summary} 第三条里的“代价或牺牲”不是随便加的。一步到位的爽文压缩出来是流水账而有分量的故事骨架里一定有“付出了什么”。这条约束能把流水账和真正的叙事梗概区分开。6. 功能测试与效果验证测试要分层做一章能不能压对决定了一本书能不能压对。不要跳过分层直接测整本书出了问题你连是哪一层坏的都不知道。6.1 单章摘要测试拿一章原文进来跑块级摘要加章节归并输出 300 字以内。判断成功的标准有三条出现的人名在原文里都真实存在事件顺序和原文一致没有出现原文里根本没写的情节。第三条是最容易翻车的模型在压缩时特别爱“顺手补全”把两个相似情节合并或者给一个模糊结局凭空加个收束。失败原因通常是分块太长。块内信息太多模型会自己挑重点挑着挑着就变形了。把chunk_max_chars降到 5000 左右再试一次多数情况能修好。6.2 单本摘要测试一本书的章节摘要喂进归并层输出 800 字左右的整书梗概。这一步要看的不是文字漂不漂亮是时间线有没有断。具体做法是把输出里的关键事件按顺序列出来和目录对一遍看有没有前后颠倒或者整段消失。常见问题是中段塌陷。前几章和后几章信息密度高中间部分容易被压成一句“经历了一系列冒险”。这是模型对“过渡段落”的天然歧视。解法是在归并提示词里显式要求“中间章节的关键转折不得省略”或者在归并时给每章摘要带一个序号标签让模型看得见进度。6.3 六句话输出测试七部书的书级摘要再归并一次得到书系级摘要最后走定句数重写。这一步要验三件事句数是否严格等于 6。大概率第一次不达标模型会输出 5 句或者 8 句。解法是把句数校验做成程序化检查不符合就带反馈重跑一次不要让模型自己数。六句之间有没有逻辑推进。好的六句是递进的处境、入场、规则、冲突、代价、落点。坏的六句是并列的六个事件读完不知道重点在哪。主题骨架有没有保住。这就是开头说的评估靶子。如果六句话之后“成长”“规则清晰的魔法体系”“选择与代价”这三条还立得住说明压缩是有效的如果六句话只是六个人名加六个动作那压缩失败了。6.4 稳定性与重复性测试同一份输入temperature设成 0 到 0.2连跑三次看输出差异。差异大说明提示词约束不够硬或者分块边界刚好切在关键情节上。实用做法是把三次结果做一次并集对比如果某个关键信息三次里只出现一次那它在原文里的权重很可能被低估了需要回头调分块。6.5 一个可自动化的抽检脚本人工读太慢写一个粗糙但有效的覆盖率检查能筛掉大部分事故import re def key_entities(text: str, top_k: int 30) - set: 极简实体抽取按出现频次取高频专有名词仅作抽检用。 tokens re.findall(r[\u4e00-\u9fa5]{2,4}, text) freq {} for t in tokens: freq[t] freq.get(t, 0) 1 ranked sorted(freq.items(), keylambda x: -x[1])[:top_k] return {w for w, _ in ranked} def coverage(source: str, summary: str) - float: 高频词在摘要中的命中率衡量信息保留程度。 ents key_entities(source) if not ents: return 0.0 hit sum(1 for e in ents if e in summary) return hit / len(ents)这个指标很糙会出现大量误报——高频词很多是虚词和常用词。但它在批量任务里当报警器足够用覆盖率突然从 0.6 掉到 0.2说明那一批任务的输入或提示词出问题了。真要精细评估得换成人名实体识别加人工抽检成本高很多。7. 接口 API 与批量任务接口调用没什么特别的标准 chat completions 就够。curl 版本先跑通再接代码curl -s http://127.0.0.1:7861/api/summarize \ -H Content-Type: application/json \ -d { text: 把清洗后的章节正文放在这里, sentences: 6, language: zh }Python 侧带重试的批量调用import json import time from pathlib import Path from concurrent.futures import ThreadPoolExecutor, as_completed from tenacity import retry, stop_after_attempt, wait_exponential from llm_client import chat, CFG from pipeline import clean_text, chunk_text LOG Path(logs/run.log) retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, min2, max20)) def safe_chunk_summary(chunk: str) - str: from pipeline import CHUNK_PROMPT, CHUNK_SYSTEM return chat(CHUNK_PROMPT.format(chunkchunk), systemCHUNK_SYSTEM, temperature0.2) def process_file(path: Path, out_dir: Path) - dict: raw clean_text(path.read_text(encodingutf-8, errorsignore)) chunks chunk_text(raw, CFG[chunk_max_chars], CFG[chunk_overlap]) results, errors [], 0 for i, c in enumerate(chunks): try: results.append(safe_chunk_summary(c)) except Exception as e: errors 1 with LOG.open(a, encodingutf-8) as f: f.write(f{path.name}#{i}\t{type(e).__name__}\t{e}\n) out_path out_dir / f{path.stem}.chunks.json out_path.write_text( json.dumps({source: path.name, chunks: results, errors: errors}, ensure_asciiFalse, indent2), encodingutf-8, ) return {file: path.name, chunks: len(chunks), ok: len(results), err: errors} def run_batch(in_dir: Path, out_dir: Path, workers: int 4): out_dir.mkdir(parentsTrue, exist_okTrue) files sorted(in_dir.glob(*.txt)) stats [] with ThreadPoolExecutor(max_workersworkers) as pool: futures {pool.submit(process_file, f, out_dir): f for f in files} for fut in as_completed(futures): try: stats.append(fut.result()) except Exception as e: stats.append({file: futures[fut].name, fatal: str(e)}) with LOG.open(a, encodingutf-8) as f: f.write(f[batch] {time.strftime(%Y-%m-%d %H:%M:%S)} {json.dumps(stats, ensure_asciiFalse)}\n) return stats批量任务的设计要点只有三条。第一中间产物必须落盘块级摘要写完就存 JSON不要等全部跑完再一起写——跑到一半挂了前面全白干。第二失败重试要区分错误类型超时和限流可以重试参数错误重试一百次也没用。第三并发数要小于等于后端服务能承受的并行请求数本地单卡服务开 8 并发经常不是变快而是集体超时。文件命名和输出结构建议固定成{书名}.chunks.json、{书名}.chapter.json、{书名}.final.json三级方便断点续跑。断点续跑的判断逻辑很简单输出文件存在且errors字段为 0 就跳过。8. 资源占用与性能观察这一节不编数字只给观察方法。因为同一段代码在 8G 显存和 24G 显存、在 4K 上下文和 128K 上下文的表现可以差出一个数量级。观察项一单块处理耗时。在日志里记录每次请求的起止时间跑十个块取中位数。如果中位数明显高于预期先看是不是并发把显存打满了触发换页之后速度会断崖式下跌。观察项二上下文利用率。分块大小是字符数模型吃的是 token。用分词器算一下你的块平均 token 数再对比模型的上下文上限留出百分之三十以上的余量。留太少长块会被截断摘要就少一段内容而你完全看不出来。观察项三总调用次数。一本 30 万字的书按 8000 字一块大概是 40 个块块级摘要 40 次加归并若干次单本就接近 50 次调用。七本加起来三百多次。这个数字决定了你的成本量级跑之前心里要有数别跑完才发现账单。观察项四CPU 推理和 GPU 推理的差异。这不只是速度差还有稳定性差。CPU 跑长上下文往往慢到没法批量但胜在显存零占用适合小规模验证GPU 跑得快但并发一高就 OOM适合串行加小并发。降低资源占用的几个实用手段把块调小、把归并层数减少、把中间摘要的字数上限压下来、对已经处理过的章节做结果缓存。第四条最有效——调提示词的时候反复重跑同一批数据缓存能省掉绝大部分重复开销。进程和端口这块也要盯。服务跑完没退出端口一直占着下次启动就报地址已占用。排查很简单# 查看端口占用Linux / macOS lsof -i :7861 # Windows 查看端口占用 netstat -ano | findstr :7861确认是残留进程再杀别上来就一把梭误杀别的服务很麻烦。9. 常见问题与排查方法问题现象可能原因排查方式解决方案依赖安装失败Python 版本过低或网络源不通看报错里的版本要求换国内镜像源升到 3.10pip install -i指定镜像连接接口报错base_url 少写/v1或服务未启动curl 直接打/v1/models探活补全路径先确认服务在跑输出句数不等于 6模型不擅长数数程序化统计句数并判断加句数校验不符合带反馈重跑一次摘要里出现原文没有的情节提示词约束太松抽检比对原文加“不得补充原文没有的内容”降温度中间章节被压缩成一句话归并时过渡段落被歧视看章节摘要的分布摘要带序号标签显式要求保留转折批量任务卡住并发超过后端承受能力看日志里是否大量超时并发降到 2加超时和重试显存不足 OOM块太大或并发太高观察单次请求的显存峰值降块大小并发设 1换量化版本端口被占用上次服务没退干净lsof -i或netstat -ano杀残留进程或换端口输出质量忽好忽坏分块边界随机性固定chunk_overlap降温度同输入跑三次比对差异中文摘要夹英文提示词没限定语言检查输出提示词里明确“用中文输出”排查有一个通用原则先定位到层。块级摘要出错和归并出错现象完全不同。块级出错是细节错、人名错归并出错是顺序错、整段消失。分不清是哪一层就逐层打印中间结果别猜。10. 最佳实践与使用建议第一条先用小样本把链路跑通。挑三章、一本书、一轮六句话三十分钟能验证完的事不要一上来就扔七本书进去跑一小时然后发现提示词写错了。第二条把提示词当代码管。版本号、改动原因、对应的评估结果都记下来。摘要这类任务提示词改一个词输出风格能差出很远不记录就没法回溯。第三条中间产物一定要落盘且带时间戳。调参阶段你会反复重跑没有缓存就是纯烧时间。第四条批量任务必须有日志和失败清单。跑完不检查errors字段等于没跑。哪怕只有一条失败也要单独重试不要整批重来。第五条接口服务默认只监听本机。真要对外提供加鉴权、加限流、加请求体大小限制别把一个裸接口暴露出去。第六条涉及有版权的文本、真实人物素材、企业内部文档时先确认授权范围再处理。摘要产物也不等于可以自由分发尤其是整本书级别的梗概公开发布前想清楚用途。第七条发布或商用前必须人工复核。定句数摘要这种强压缩模型很容易在最后一句硬凑一个“升华”读起来像模像样实际是编的。最后一句尤其要盯。回到开头那个问题六句话能不能读完《哈利波特》。技术上能但这六句话的价值不在“省了多少阅读时间”而在它逼着你去想——一部作品里到底哪些东西是不能丢的。是主角的处境是关键转折是选择的代价是规则的清晰度。这套流水线真正的用处是把“什么算重要”这件事从直觉变成可配置的参数你想保留情节就调事件覆盖率你想保留主题就调骨架约束你想保留风格就单独跑一遍文风迁移。同一套分块和归并逻辑换个提示词就能复用。如果你现在手上正好有一堆读不完的长文档建议先从一个章节开始把chunk_max_chars调到 5000跑出第一版六句话看看丢的是什么。丢的东西往往比留下的更能说明你的提示词哪里没写好。