最近做了一件事给某个文档处理工具加上了“AI智能排版”功能。用户把任何来源的杂乱文本丢进来——网页复制的碎片、OCR识别出的断行、会议录音转写稿、PDF硬导出的残文本——系统自动完成字体调整、行距设置、段落切分最后生成一份排版规范的文档。这篇文章把整个实现过程拆开讲清楚方案怎么选型、每个模块怎么设计、参数怎么算、实际踩了哪些坑。如果你正在做自动化文档生成、知识库内容整理或者单纯想把手动排版这件事从日常工作里踢出去这篇里的实操代码、参数表和排错思路可以直接照着用。1. 方案选型为什么最终选了 AI 规则双引擎1.1 先搞清楚“杂乱文本”到底乱在哪在动手写第一行代码之前我先花了不少时间收集真实输入样本。因为“杂乱”这个词太模糊了不同来源的文本脏点完全不一样不把输入样本摸清楚后面所有设计都是空中楼阁。我收集到的高频输入大致分四类。第一类是网页复制文本表面上看着还行实际上夹杂大量首尾空白、不间断空格、多余换行偶尔还混着没被过滤掉的HTML标签。第二类是OCR识别文本断行位置完全由图片上的视觉换行决定一个段落被硬生生拆成七八行而且错别字、标点丢失很常见。第三类是语音转写文本口语化严重经常一整段没有一个句号换行完全随机。第四类是从PDF直接复制出来的文本换行按纸张宽度切分复制到记事本里就是一堆碎行。这些输入有一个共同特征视觉排版信息和语义结构信息双重混乱。换行符不再代表段落边界标点缺失导致句子边界模糊标题和正文的视觉差异在纯文本里完全丢失。正则清洗只能解决表面字符问题没法理解哪些句子属于同一段、哪句话适合做标题。这是我把大语言模型引入核心决策链的根本原因——规则负责确定性处理AI负责语义理解各干各自擅长的事。1.2 纯规则方案的三个硬伤在决定用大模型之前我确实先试过纯规则方案而且试得相当认真。当时的思路很直接用正则去重字符、按句号分句、按空行分段。效果在“干净”的文本上确实不错但一遇到真实场景就崩。第一个硬伤是非结构化文本没法分。录音转写稿经常一整屏没有一个标点规则根本不知道该在哪里断句强行按字数切分又会把完整语义切开。第二个硬伤是OCR文本的边界丢失。视觉上明明是两个段落但转成纯文本后段间没有任何标记规则只能把它们合并成一大坨。第三个硬伤是规则维护成本爆炸。每接入一种新输入来源就要补一批新规则规则之间互相干扰修好A场景搞坏B场景最后反而不如人工排版效率高。绕过这些问题也不是完全不行代价是要维护一个几百条规则的异常库还得持续跟进新场景。我评估后放弃了这条路决定把“语义判断”这类开放问题交给大模型把“字符处理”“样式计算”“格式输出”这类确定性工作留给本地代码。理由很简单模型负责理解代码负责执行精确操作双方出错概率都能压到最低。1.3 双引擎架构的整体链路设计最终架构分四层每层职责单一、接口清晰可以独立测试、独立替换。第一层是预处理层负责文本清洗与标准化比如去掉多余空白、统一标点、修复编码问题这一步完全不做语义理解用正则和字符映射就够了。第二层是语义分析层把清洗后的文本交给大模型让它输出段落边界、标题层级、列表项、引用块等结构化信息。第三层是样式决策层主要是一个纯函数输入是语义结构加风格模板输出是每个块级元素的字体、字号、行距、缩进、段前段后距等具体参数。第四层是渲染输出层把样式参数写入DOCX、PDF或HTML对应的字段。四层之间用标准JSON数据衔接。预处理层输出纯文本语义分析层输出结构JSON样式决策层输出样式JSON渲染层消费样式JSON。这样每一层都能单独跑、单独测、单独替换底层实现。比如今天用大模型A做语义分析明天换更强的模型B只要输出格式不变其他层完全不用动。这个设计在后期排查问题时帮我省了大量时间。2. 四个核心模块的实现细节2.1 文本清洗与标准化清洗层是所有后续处理的地基脏字符不搞干净语义分析层的输入质量就没法保证。我按处理顺序把它拆成三小步。第一步是字符级清理。这里要处理的不只是普通空格还包括不间断空格(\u00A0)、全角空格(\u3000)、制表符、零宽字符。这些字符肉眼看不见但会给后续的分段和样式计算带来干扰。我直接用正则配合字符映射表统一替换。第二步是编码修复。从老系统导出的文本经常出现乱码字符比如把Unicode字符错误解码成Latin-1再编码的情况特征是一批乱七八糟的拉丁字符。我写了一个基于常见错误编码顺序的自动检测函数实测能修复大部分场景。第三步是行尾处理。把所有\r\n和\r统一成\n然后合并连续空行把三行以上连续换行压缩成一行。清洗层有个原则宁少勿多。不要在这里做任何可能损失语义的操作比如不要删掉看似“多余”的换行因为那些换行可能是唯一的段落线索留给语义分析层去判断更安全。注意清洗层禁止直接删除所有换行符再重新分段。我最初犯过这个错误以为语义分析可以完全无视原始换行结果模型在长文本上分段经常跑偏因为原始换行位置包含重要的视觉边界信息删掉之后信息就永久丢失了。2.2 段落边界判断规则预切 模型精调段落切分是整个排版功能的核心也是我迭代次数最多的模块。单靠大模型直接对全文分段长文本容易漏切而且输出不稳定。单靠规则又搞不定口语文本。最终方案是两层配合。第一层是规则预切。先利用标点和原始换行做粗粒度切分。具体逻辑是遇到句号、问号、感叹号后若跟换行或连续两个换行则视为段落边界如果原文本身空行密集则优先信任空行作为边界信号。这一步把文本切成“候选块”每个候选块几十到几百字不等。第二层是模型精调。把预切好的候选块编号让大模型判断哪些相邻块应该合并、哪些应该进一步拆分并给每个块标注类型正文、一级标题、二级标题、列表项、引用等。模型精调的Prompt我反复调了很多版后面在实操章节会给出完整模板。这里先讲一个关键设计让模型输出JSON而不是直接输出排版后的文本。JSON输出可以精确定位每个段落的边界后续样式层能拿到结构化数据做计算。如果让模型直接输出Markdown或HTML表格对不齐、列表层级乱、样式参数不可控排出来的文档远达不到“美观”标准。预切加精调的配合效果非常明显。预切把大问题缩小成小问题模型不需要通读全文就能做局部判断上下文窗口压力小稳定性也上来了。实测下来一段两千字的口语转写文本纯模型方案段落切分准确率大概七成加上规则预切后能到九成以上。2.3 字体、字号、行距的计算逻辑样式决策是“美观”的直接来源也是最容易被低估的部分。很多人以为字体随便选、行距设个1.5倍就行实际做出来还是丑因为中英文混排、标题层级、段落关系这些细节没有一套可计算、可复用的参数体系。我设计了一套以“风格模板 基础基数”为核心的样式计算方案。每一个风格模板定义一组基础参数比如正文字号基数、标题缩放比例、行距倍数、首行缩进量、颜色方案。样式层拿到语义结构后按规则给每个元素算具体数值。以中文技术文档风格为例正文用思源宋体字号12pt行距按字号乘1.6计算也就是19.2pt段前距0pt、段后距6pt首行缩进2字符。一级标题用思源黑体加粗字号取正文字号的1.5倍即18pt行距1.4倍即25.2pt段前距12pt、段后距6pt。二级标题字号1.25倍即15pt其余参数类推。列表项正文同字号但缩进量增加段后距压缩到3pt。这里有个中文排版的细节必须提首行缩进的2字符指的是两个汉字宽度不是两个空格。在DOCX里要用段落属性设置首行缩进在HTML里可以用相对单位直接用空格模拟缩进在后续编辑和转格式时会出问题。行距还有一个经验值纯中文文本行距乘数可以稍小1.5到1.6倍比较舒服但中英混排文本要放大到1.7到1.8倍因为拉丁字母的上升部与下降部占用的垂直空间比汉字大行距太小会显得拥挤。这个差异是我在实际对比测试里用肉眼和标尺双重验证过的不是拍脑袋。元素字体字号(pt)行距(pt)段前/段后(pt)缩进一级标题思源黑体 Bold1825.212 / 60二级标题思源黑体 Medium152110 / 50正文思源宋体 Regular1219.20 / 62字符列表项思源宋体 Regular1219.20 / 3左缩进12pt引用块思源宋体 Italic1219.26 / 6左缩进18pt这套参数表看着简单背后是一个计算约定所有行距都是字号乘以固定倍率所有段距和字号按比例关联保证不同模板之间切换时整体视觉密度一致。如果把每个参数都当作独立变量手工调整换模板时会累死而且很容易调出比例失调的版面。2.4 标题层级的自动识别标题识别直接决定文档的骨架。一个排版美观的文档前提是层级关系清晰。我在实现中把标题识别分成两条路。第一条路是规则优先。输入文本中如果保留了原始排版线索比如“一、”“1.1”“第一章”这类显式编号或者OCR文本中字号明显偏大的离散行规则可以直接给出高置信度判断。这些信号可靠时不需要模型介入省调用、省成本、速度也快。第二条路是模型兜底。显式编号不存在时让大模型从语义和句式特征判断比如短句、概括性强、独立成行、后面跟长段落等特征结合起来推断标题身份。这里要在Prompt里明确告诉模型拿不准时宁可按正文处理不要把太多普通句子误判成标题。误判成标题的代价比漏判大得多因为标题样式会打乱正文的阅读节奏。实际处理中我会把标题和正文分开建模标题作为独立的块级元素不参与正文段落合并识别出的标题再根据编号层级或语义权重映射到一、二、三级标题。如果原文没有层级线索默认所有标题降一级处理避免过度嵌套造成的视觉混乱。3. 完整实操流程与代码落地3.1 环境准备与依赖选型这个功能我用Python实现主要依赖是四个库openai或其他大模型SDK负责语义分析python-docx负责生成Word文档weasyprint负责HTML转PDFpydantic负责校验和解析模型输出的JSON。选python-docx而不是直接操作宿主机Word自动化是因为python-docx跨平台、无依赖、对样式控制足够细而Word的COM接口在Windows之外没法跑不适用于做服务。环境安装其实没什么特别pip一把梭就行。但有两个细节值得注意。第一weasyprint对系统字体依赖很大服务器上一定要装好中文字体否则生成的PDF中文全是方框。我在部署时就踩过这个坑后来在初始化脚本里强制安装了几个开源中文字体。第二请使用虚拟环境隔离依赖大模型的SDK版本升级很频繁隔离环境能避免版本冲突把整个服务搞挂。3.2 清洗层代码实现清洗层的代码不长但每一步都有用意的。核心函数如下import re def clean_text(raw: str) - str: # 统一换行符 text raw.replace(\r\n, \n).replace(\r, \n) # 替换不可见空白字符 text text.replace(\u00A0, ).replace(\u3000, ) text text.replace(\t, ) # 去掉零宽字符 text re.sub(r[\u200B-\u200D\uFEFF], , text) # 压缩连续空白为单个空格 text re.sub(r[ \t], , text) # 压缩连续空行最多保留一个空行 text re.sub(r\n{3,}, \n\n, text) return text.strip()这段代码的核心决策是合并连续空行时保留一个空行而不是全部删掉。前面说过空行是段落边界的重要线索完全抹掉会让后续预切分失去一个高置信信号。压缩到只剩一个空行既保留边界信息又不会让格式炸开。字符级清理顺序也讲究先处理换行符再处理空白字符最后压缩多余空行。如果顺序颠倒比如先压缩空格再统一换行可能在Windows换行符的\r\n中被空格压缩逻辑误伤把行结构破坏掉。3.3 模型调用与结构化输出设计模型调用是语义分析层的主战场。这里最核心的设计是Prompt模板和JSON Schema约束两者缺一不可。先看Prompt模板。我经过多轮调整最终稳定在下面的结构。前端是身份与任务说明中间是输入文本末尾是输出格式要求与注意点SYSTEM_PROMPT 你是一名专业的文档结构分析器。 你的任务分析用户提供的原始文本将其切分为语义连贯的段落并识别每个段落的类型。 输出要求 1. 只输出JSON不要输出任何解释性文字。 2. JSON结构为 {blocks: [{id: 0, type: paragraph|heading1|heading2|heading3|list_item|quote, text: 段落原文}], ..} 3. 相邻的短句如果语义连贯合并成一个段落。 4. 独立成行、概括性强、后接长段落的短句判断为标题。 5. 拿不准类型时一律按paragraph处理。 6. 不要修改文本内容text字段必须与原文一致。 def analyze_structure(text: str): resp client.chat.completions.create( modelyour-llm-model, messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: text}, ], response_format{type: json_object}, temperature0, ) content resp.choices[0].message.content parsed json.loads(content) # 生产环境用 pydantic 校验字段 return parsed[blocks]这里有三个实践要点必须展开讲。temperature设成0是关键。排版场景不需要创造性模型每一次输出都要尽可能稳定温度越高越容易出现分段位置漂移。第二response_format里的json_object约束不是所有模型服务都支持如果不支持就在System Prompt里反复强调“只输出JSON”并在代码里做异常重试。第三我明确要求text字段必须与原文一致不许模型改写内容这个约束能防止模型自作主张修错别字、改语序导致排版工具变成一个文本篡改工具。接下来是分块策略。模型有上下文长度限制不要一次性把几千上万字的文本全塞进去。我会先按清洗层留下的空行做成粗块再把超过一定长度的大块按句子边界二次切分每块控制在1200到1500字左右。每个块独立调用模型分析最后合并结果并重新编号。这个策略还有个意外收获单块分析失败时只需重试那一块整个任务的失败成本大幅降低。3.4 文档导出的关键实现样式计算出来之后导出到具体格式是最后一步。我主要实现了两个导出目标DOCX和PDF。DOCX用python-docx实现。关键点是设置中文字体时必须同时设置eastAsia字体否则Word会把中文显示成默认字体所有字体设计全部白费from docx import Document from docx.shared import Pt from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.oxml.ns import qn def apply_paragraph_style(paragraph, style: dict): pf paragraph.paragraph_format pf.line_spacing Pt(style[line_spacing]) pf.space_before Pt(style[space_before]) pf.space_after Pt(style[space_after]) pf.first_line_indent Pt(style[first_line_indent]) for run in paragraph.runs: run.font.size Pt(style[font_size]) run.font.name style[font_name] run.element.rPr.rFonts.set(qn(w:eastAsia), style[font_name])上面代码里那个qn(w:eastAsia)就是中文字体的关键设置没有这一行run.font.name只管西文字体中文渲染时乖乖认回默认宋体标题黑体、正文思源宋体的设定瞬间破功。PDF导出我用的是weasyprint先把样式转成CSS然后渲染HTML。好处是CSS对排版控制力强media print规则能实现分页控制。但要注意weasyprint对CSS的支持有自己的范围flex布局有时会出问题稳妥起见用普通的块级元素加margin控制间距。4. 常见问题与排查实录4.1 编码与乱码问题乱码问题最早出现在老系统导出的文本上。特征很典型原文里的中文全部变成类似“且的字符。这是UTF-8字节被错误按Latin-1解码导致的。排查方法很简单先把乱码文本encode成latin-1再decode成utf-8如果能还原出可读中文就说明是编码误判在清洗层前置修复即可。还有一类编码问题是输入文本混着多种编码比如前半段是GBK、后半段是UTF-8。这种情况单靠字符串解码函数修不干净我采取的方案是按长度分批检测乱码密度对高乱码密度批次单独尝试候选编码列表。说实话这个方案不算完美但对内部数据处理场景足够用。注意编码修复要写成一个独立的工具函数不要塞进清洗主流程。因为不是所有输入都需要编码修复草率地全量转换反而会破坏已经正常的UTF-8文本。4.2 分段数量失控怎么调分段最常出问题的有两种表现过度分段和不足分段。过度分段是指文本被切成几十个碎片段落每个段落就一两句话。不足分段则是整页文本最终只分出三五段每段几十行。排查时先定位是预切问题还是模型问题方法是查看中间输出。我会在系统里加一个调试开关打印出预切后的候选块数量和合并后的最终块数量。如果预切阶段碎片化严重说明清洗层把空行和换行处理得过狠需要保留更多原始换行信号如果预切正常但模型合并过多就要在Prompt里加强“不要把不同主题的段落合并”这个约束或者把temperature调得更低。还有一种情况是输入文本本身就没有清晰边界比如纯口语转写。这时我会放松对分段的完美要求只保证主题切换处能分段这比强行把所有句子都归并成整齐段落更自然。4.3 长文档的性能与截断问题处理一万字以上的长文档时最直接的问题是模型上下文窗口不够。分块策略能解决窗口问题但带来了新麻烦分块边界正好横跨一个段落时模型可能把半截段落识别成完整段落边界附近的标题识别也会失误。我的解决办法是块间重叠。每个块与相邻块重叠100到150字重叠区域的结构分析结果以后块为准。这样能保证分块边界处的段落完整性。实测重叠区域对模型输出稳定性的提升非常明显首尾衔接处的分段准确率从八成五提高到了九成五。性能方面的另一个瓶颈是PDF渲染。weasyprint在渲染几十页文档时速度尚可但遇到上百页时内存占用会飙升。我的缓存策略是相同输入文本不做重复渲染按内容哈希缓存生成结果实测在高频重复请求场景下能节省接近一半的服务资源。4.4 样式计算中的冲突处理样式冲突主要集中在两类。第一类是同一元素被多个规则命中比如一个段落既有“首行缩进”规则又有“列表项缩进”规则。我的处理是给样式规则定义优先级列表项样式优先级高于普通正文样式引用块样式优先级高于列表项按优先级链覆盖绝不合并计算。第二类是字号缩放后行距出现小数导致不同段落行距在视觉上微微不齐。这个在DOCX里表现不明显但在PDF里肉眼可辨处理方式是行距结果四舍五入到0.5pt的倍数确保视觉均匀。还有一种冲突不太起眼但遇到过一次才明白标题紧跟在表格或图片后面时标题的段前距会被前面的块级元素吞掉。我后来在渲染层强制给每个标题元素加一个不可压缩的段前距用样式中的“keep_with_next”属性把标题和它后面的第一个段落绑定避免标题变成某一页的最后一行。5. 实测结果与效果对比5.1 测试集怎么设计为了客观评估排版效果我建了一个不算大但覆盖很全的测试集一共60篇文档每类输入来源15篇。网页复制类、OCR类、语音转写类、PDF导出类各占四分之一。每篇文档都标注了人工整理的“标准答案结构”包括段落边界、标题层级、列表范围等。评估时不光看分段准确率还要看实际排版效果。因为即使结构都对样式参数选得不合适文档看起来依然丑。我引入了一个主观评分维度找了几位平时经常处理文档的人不看原文本只看排版后的效果从“信息层级清晰”和“视觉舒适度”两个角度打分。机器评准确率人眼评美观度两者结合评判整体质量。5.2 数据结果与观察最终跑完测试集结构层面的数据是这样段落边界判断的F1值在网页类和PDF导出类文本上最高能到0.93左右OCR类文本稍低大概0.88语音转写类文本最低只有0.82。标题识别的准确率整体在0.9上下语音转写类文本的标题识别最不稳因为口语文本几乎没有独立成行的概括句模型容易把普通陈述句误判成标题。视觉效果方面几位评测人一致的反馈是排版后的文档“看起来像正经文档了”层级分明、标题醒目、段落疏密得当。但也有一个共性意见自动生成的标题字体偏大、偏“重”在正文内容较长时会有一种压迫感。后来我把标题字号基数从1.5倍下调到1.35倍视觉反馈明显改善。这也印证了一件事样式参数需要靠真实用户反馈迭代单靠排版理论计算不出最优值。OCR类文本的错别字问题也在这轮测试中暴露出来。因为我在Prompt里明确要求模型不改写内容OCR错别字会原样保留。从排版角度看这没问题但从文档质量角度看错别字确实影响观感。我在后续版本里增加了一个可配置的“错字校对”选项默认关闭用户需要时再打开。这样既不会影响排版工具的“忠实原文”原则又给用户多一个选择。6. 个人实操心得与后续扩展这轮开发做完我最大的体会是AI排版这类功能成败的关键反而不在AI模型本身而在AI前后的工程化处理。文本清洗质量、结构化输出的设计、样式参数的计算规则、渲染层的细节处理这些看似“传统”的环节直接决定了最终效果。大模型只是其中一个部件而且是可以随时替换的部件——从这家的模型换成别家的只要结构化输出约定不变整套系统照常运转。另一个体会是文档美观是个高度主观的标准代码只能保证“规整”不能保证“好看”。规整是参数化的、可以计算校验的但好看需要对版式风格有审美经验。所以我的系统里把所有样式都做成了可配置模板不同用户、不同场景之间可以切换不同的风格包。我强烈建议后续有需要的人这么做把样式决策做成独立的配置层让使用方自己调整参数而不是把样式写死在代码里。最后分享一个实际的扩展思路这个排版能力完全可以作为“文档预处理”模块接入更大的内容流水线。比如知识库入库前先用它清洗统一格式再切分向量化或者作为编辑器插件用户写完草稿一键排版。排版功能单独存在的价值有限一旦嵌入内容生产链路成为入口环节价值会大得多。我自己下一步计划做的是把这套能力包装成标准服务对外只暴露“文本进、文档出”的接口让上层应用按需调用。