Dify+自建PDF渲染服务:实现原格式翻译的完整方案 干过这活儿的都知道把一份 PDF 原封不动翻译成另一种语言跟把里面的文字抠出来翻一下完全是两码事。文字翻译充其量是“信息搬运”原格式翻译是“排版重建”。尤其当你面对的是合同、产品手册、技术白皮书或者带图表混排的学术 PDF里面夹杂着页眉页脚、表格框线、跨页段落、特殊字体一旦格式乱了这套文档基本就等于废了。Dify 本身是个好用的 LLM 应用编排平台知识库、工作流、Agent 这些能力都很成熟但如果你天真地以为「在 Dify 里拖一个翻译节点把 PDF 喂进去就能吐出一份排版一模一样的文件」那大概率会摔跟头。Dify 擅长的是把文本交给大模型处理而 PDF 的版式还原需要的是另一套工程手段。这篇文章我就把完整的实现思路拆开讲先分析原格式翻译的技术卡点再给出一套「Dify 编排 自建 PDF 渲染服务」的组合方案最后把我在实际项目中踩过的坑和优化经验一并交代清楚。适合已经在用 Dify、想做文档翻译工具但又不想只停留在文本层面的人参考。1. 先搞清楚「原格式翻译」到底卡在哪三种 PDF 类型对应三种处理逻辑1.1 文字版 PDF、扫描版 PDF、混合版 PDF 的底层差异不是所有 PDF 都长一个样。你在决定技术路线之前第一件事就是把 PDF 分成三类因为它们的处理方式完全不同。第一类是文字版 PDF最典型的就是 Word、LaTeX 导出的 PDF。这种文件天生带有文本层PDF 解析器可以直接提取出里面的字符、字体、字号、坐标位置。理论上讲拿到这些信息后把原文替换成译文再按原坐标渲染回去版式就能保留。第二类是扫描版 PDF本质上是图片套了一层 PDF 外壳。你看到的每一页都是一张扫描图没有文本层鼠标选中不了文字CtrlF 也搜不到。这种 PDF 必须先做 OCR 识别出文字和位置再走翻译和回填流程难度直接上一个台阶。第三类是混合版 PDF里面既有文字层又有图片、表格、矢量图形常见于产品图册、学术论文、财报。这类文档的翻译不能只动文字还要保证图文相对位置不跑偏。判断方法很简单打开 PDF用鼠标框选一段文字能选中就是文字版选中不了但页面上有内容大概率是扫描版部分能选中部分选不中就是混合版。我见过不少团队一上来就买各种商业 PDF 翻译工具的 API结果遇到扫描版照样抓瞎原因就是没做分类处理。这个分类工作必须在你的处理流程里做成自动预检而不是靠人工肉眼判断。1.2 版式还原的三个核心指标坐标、度量、字体原格式翻译的本质是把译文文本按原文的坐标、字号、字体渲染到新 PDF 里。这里面的三个核心指标一个都不能少。坐标是最直观的PDF 的页面坐标系以左下角为原点单位是磅point1 磅等于 1/72 英寸。每段文字的左上角坐标、每行的基线位置都可以从 PDF 的 content stream 里解析出来。翻译后的文本能不能待在原来的位置就看坐标对齐做得好不好。度量是很容易被忽视的「隐形杀手」。同一个字号下英文和中文的字符宽度完全不同。比如 Hello 和 你好同样是 12pt 字体Hello 占的宽度大约是 30 个磅值而 你好 只占 24 磅左右。如果你直接把译文按原文位置渲染不考虑语言切换带来的度量变化结果就是长文本溢出边框短文本留出大片空白。字体的问题更隐蔽。原文用的是 Helvetica、Times New Roman、思源宋体你的系统里不一定装了对应字体或者译文的字符集比如中文繁简体在原字体里根本不存在。渲染时一旦找不到合适的字形PDF 阅读器就会用替代字体显示整个版式直接崩掉。我早期做第一版工具时就是把坐标和字体拿对了忽略了度量差异结果英文翻成中文后原本一行放得下的标题溢出到下一行表格列宽全乱了。后来学乖了每次渲染前都要做一次「文本宽度预计算」用目标语言的字体度量去模拟排版。1.3 Dify 在其中的角色定位与边界Dify 在整个方案里承担的是「大脑」的角色负责解析用户上传的文件、调用大模型翻译、管理多轮对话上下文、编排整个业务流程。它不负责也不擅长 PDF 的像素级渲染。Dify 的文档提取器能把 PDF 里的文本抽出来这是它的强项。但文档提取器拿到的是纯文本加简单的 markdown 结构没有坐标、字号、字体这些版式信息。如果你用 Dify 内置的知识库或者文档工具来跑 PDF 翻译输出的结果只能是「文字对了格式全丢」。所以我给出的方案是一个组合拳Dify 负责任务编排与翻译自建一个 PDF 处理微服务负责解析与渲染。Dify 通过自定义工具节点调用这个微服务两边通过 JSON 传递版式信息和译文文本。这样既发挥了 Dify 的流程编排能力又弥补了它在版式处理上的短板。这套架构还有一个好处PDF 渲染服务独立部署可以用 PyMuPDF、pdf-lib 这类底层库自由操控Dify 升级不影响它反过来你想换掉 Dify 换别的编排平台渲染服务也能直接用。2. 选型我为什么放弃「Dify 内置节点硬扛」改用「编排 微服务」组合2.1 Dify 内置能力做 PDF 翻译的三个明显短板先说结论Dify 走内置流程确实能完成「PDF → 文本 → 翻译 → 输出译文」的闭环但做不出原格式效果。第一个短板就是文档提取器丢版式信息。Dify 内部做 PDF 解析时本质上也是调用文本抽取逻辑输出的是纯文本和 markdown。你拿到的内容是按阅读顺序排好的文字但哪句话在页面的哪个位置、字号多大、什么颜色全都没有。没有这些信息还原版式就是空谈。第二个短板是「代码节点」能力受限。Dify 的工作流里虽然有代码执行节点但运行环境是受限的沙箱你没办法在代码节点里直接装 PyMuPDF更没法把生成的 PDF 文件传回工作流再返回给用户。代码节点更适合处理 JSON 数据转换、调用 HTTP 接口不适合做重量级的文件渲染。第三个短板是长文档处理。一份 50 页的 PDF全部塞进一次 LLM 调用上下文窗口大概率会爆掉就算不爆翻译质量也会因为文本太长而下降。虽然 Dify 有迭代码节点可以做切片处理但切片后的上下文如何衔接、术语如何保持一致这些都需要额外设计。2.2 微服务方案的架构设计与职责划分我最终落地的架构分成了三块各司其职负责接收 Dify 请求的 API 服务用 FastAPI 写暴露三个接口/parse_pdf解析 PDF 返回版式 JSON/render_pdf接收译文 JSON 渲染出新的 PDF/health健康检查。负责 PDF 底层解析与渲染的引擎基于 PyMuPDFfitz实现。解析时用page.get_text(dict)拿到按块block、行line、字符span组织的文本层级连同坐标、字体、字号、旋转角度等信息序列化成 JSON。Dify 侧的工作流编排接收用户的 PDF 上传调用工具节点请求解析服务获取版式 JSON再通过迭代码节点把文本按 block 切片交给 LLM 翻译最后把翻译好的 JSON 回调给渲染服务生成 PDF 返回。这个方案的好处是职责清晰。解析和渲染是纯技术活交给专门的库和服务处理翻译是智能化活交给 LLMDify 在其中做流程串接和状态管理。任何一块出问题都能快速定位和替换。2.3 为什么最终选了 PyMuPDF 而不是 pdfplumber 或 pdf-libPDF 处理的 Python 库我基本都试过一遍pdfplumber、pdfminer.six、PyMuPDF、pikepdf甚至前端侧的 pdf-lib、pdf.js 也调研过。最终在解析端选的是 PyMuPDF渲染端也是 PyMuPDF。PyMuPDF 的解析能力是真强。page.get_text(dict)返回的数据结构里从页面page到文本块block到文本行line到字符跨度span每一层都带有完整的坐标和样式信息。关键是它速度快一个 30 页的 PDF 解析耗时基本在毫秒到秒级而 pdfplumber 遇到复杂版面经常会卡住或者在某些页面上崩溃。渲染方面PyMuPDF 支持直接在页面指定坐标处insert_textbox还能通过insert_font加载自定义字体文件处理中文字体缺失的问题非常好用。它甚至能操作原有的 PDF 对象复制原页面背景、图片、矢量图形然后在上面覆盖新文本。pdfplumber 解析文本坐标也很好但它没有渲染能力通常要搭配 reportlab 用。pdf-lib 是 JavaScript 库适合前端纯浏览器方案但要在 Dify 服务端集成还得搭一套 Node 环境增加不必要的复杂度。选型的结论是一套技术栈打通解析和渲染两端能少很多因数据结构对接产生的 bug。PyMuPDF 在这两端都是最强的选择。3. 核心链路拆解从 PDF 解析到坐标级翻译回填的完整流程3.1 第一步解析 PDF 生成带版式的中间 JSON解析是整个链路的地基这一步做得不扎实后面全是空中楼阁。我的解析服务接收到 PDF 文件后会用 PyMuPDF 逐页读取。核心代码类似这样import fitz # PyMuPDF def parse_pdf(pdf_path: str) - dict: doc fitz.open(pdf_path) pages_json [] for page_idx, page in enumerate(doc): page_dict { page_number: page_idx 1, width: page.rect.width, height: page.rect.height, blocks: [] } # 提取文本块及样式信息 text_dict page.get_text(dict) for block in text_dict[blocks]: if block[type] ! 0: # 0 代表文本块1 代表图片块 continue block_info { bbox: list(block[bbox]), # [x0, y0, x1, y1] lines: [] } for line in block[lines]: line_info { bbox: list(line[bbox]), text: , spans: [] } for span in line[spans]: span_info { text: span[text], font: span[font], size: round(span[size], 2), flags: span[flags], color: span[color], bbox: list(span[bbox]), origin: list(span[origin]) } line_info[spans].append(span_info) line_info[text] span[text] block_info[lines].append(line_info) page_dict[blocks].append(block_info) pages_json.append(page_dict) return {pages: pages_json}段落处理時有一個关键细节解析出来的 block 只是 PDF 内部的文本块并不等于视觉上的「段落」。同一段落可能被拆成多个 block一个 block 里也可能包含多个不同样式的文本。我会额外做一层聚合把字体字号相同、行距接近、上下文衔接的相邻行合并成段落。这一步纯粹是启发式规则但能显著提升后续翻译和渲染的效果。3.2 第二步Dify 工作流里的文本切片与翻译策略解析服务把 JSON 返回给 Dify 工作流后接下来的核心问题就是怎么把长文本切成适合 LLM 翻译的小块同时保证术语和上下文一致。我的做法是分两个维度控制切片逻辑维度和长度维度。逻辑维度指尽量按段落边界切保持语义完整长度维度指单个片段的字符数控制在 1000~1500 字之间。硬切会破坏语义过长会导致质量下降两者取平衡。Dify 的迭代码节点可以接收解析 JSON在里面把每个页面的文本做二次分组按「段落区块」输出成一批翻译子任务。每个子任务包含一个block_id用于后续渲染时定位和source_text原始文本内容。提示这里的block_id必须带上页码前缀比如page_3_block_2因为渲染是逐页面做的跨页定位靠完整 ID 才能唯一匹配。翻译节点我配置的是系统自带的 LLM 节点模型选的是上下文窗口较大、翻译质量稳定的平衡型模型。Prompt 的设计有个讲究要告诉模型「这是 PDF 文档片段请保持专业术语准确保留原文的换行意图不要给译文添加额外解释」同时把术语表作为上下文注入。这里分享一个实测有效的技巧把页面标题和页眉页脚单独拎出来翻译不跟正文混在一起。页眉页脚通常是公司名、页码、章节名这类短文本跟正文一起翻译容易产生语义干扰。我通常会在解析时就把footer或header标记出来翻译时单独处理。3.3 第三步译后渲染回填——按坐标塞回译文并处理宽度溢出翻译完成后Dify 把「译文 JSON」回调给渲染服务。渲染服务的核心逻辑是按原文的坐标系把译文渲染到指定的位置。def render_pdf(original_pdf_path: str, translated_json: dict, output_path: str): doc fitz.open(original_pdf_path) for page_idx, page in enumerate(doc): # 获取该页的所有 block 译文映射 block_mappings translated_json[pages][page_idx][blocks] for block_map in block_mappings: block_id block_map[block_id] bbox block_map[bbox] translated_text block_map[translated_text] font_name block_map[font] font_size block_map[font_size] # 覆盖原文本区域先画一个白色遮罩再写入译文 # 如果原文有背景色需要先读取原背景并对齐填充 rect fitz.Rect(bbox) page.draw_rect(rect, colorNone, fill(1, 1, 1)) # 白色覆盖 # 写入译文自动换行 textbox_rect fitz.Rect(bbox[0], bbox[1], bbox[2], bbox[3] 20) page.insert_textbox( textbox_rect, translated_text, fontnamechina-s, # 中文字体需要先注册 fontsizefont_size, lineheight1.4, align0 ) doc.save(output_path)实际渲染要比这复杂不少有几个细节必须处理一是原文遮罩。直接覆盖文本会跟旧文字叠在一起但如果把整个 bbox 涂白又可能盖掉图片或背景色。我的做法是先判断 bbox 区域是否与图片重叠不重叠的才涂白重叠的改用更细粒度的「逐行遮罩」。二是文本溢出。中英文长度差异导致译文可能比原文长insert_textbox会返回放不下剩余文本的标志。需要动态评估「当前 block 的译文行数 × 行高」是否超出原 bbox 高度超了就把溢出部分合并到下一 block 的开头。这个过程类似文字排版里的「流式重排」。三是段间距。原文 block 之间本身有间距翻译后如果不对齐视觉上会显得段落粘连或过于松散。可以引入一个「文本高度相似度」指标当译文的预估高度跟原文 bbox 高度偏差超过 30% 时按比例调整行距。3.4 追加链路表格、图片区域的特殊处理表格和图片是原格式翻译里最容易翻车的地方。表格的问题在于它的单元格结构不会因为你替换了文字就自动适应图片尤其是含文字的信息图里面的文字需要额外走 OCR 或直接跳过。表格处理上我目前的做法是解析时识别出表格区域用page.find_tables()或基于线条的启发式检测把表格范围内的文本 block 标记为table_cell类型。翻译时保持单元格粒度渲染时按原单元格矩形写入。遇到长表格允许单元格内文本自动换行并把行高做相应扩展——这在文字版 PDF 里能做但在扫描版里基本做不了。图片内文字是另一个大坑。纯文字版 PDF 里的图片插图如果只是装饰性的直接保留原图不动就行但如果是含流程说明、数据标注的图里面的文字没法直接用 PyMuPDF 翻译。我的方案是把这类图片先抽取出来送到 OCR 服务识别文字翻译后再用图像处理把译文合成回去。这一步成本较高通常只对重要文档开启。4. Dify 工作流里具体怎么编排节点配置与参数调优实录4.1 工作流节点清单与连接关系我在 Dify 中创建的是一个「工作流」类型应用不是 Chatflow。因为文档翻译是一次性任务不需要多轮对话工作流更适合这种确定性流程。具体节点编排如下表节点名称节点类型作用关键配置开始节点开始接收用户上传的 PDF 文件文件变量pdf_file解析 PDF工具节点HTTP调用自建服务的/parse_pdf接口请求方式 POST返回 JSON切片分组代码节点把解析 JSON 按段落切分成翻译子任务输入parse_result输出batch_list翻译循环迭代节点逐批调用 LLM 翻译循环变量batch_list翻译 LLMLLM 节点调用大模型翻译文本模型、Prompt、术语表汇总译文代码节点把各批翻译结果合并成渲染用 JSON输入各批translated_batch渲染 PDF工具节点HTTP调用自建服务的/render_pdf接口请求方式 POST返回文件输出节点结束把渲染好的 PDF 返回给用户文件变量output_pdf连接关系很简单基本是线性链路中间插了一个迭代节点做分批处理。这里要重点说迭代节点的配置技巧。4.2 迭代节点与 LLM 节点的参数配置经验Dify 的迭代节点默认按数组逐项处理每轮迭代都能拿到当前项数据。我给它配置的输入是一个包含多个翻译子任务的数组每个子任务包含block_id和source_text。迭代节点里挂上 LLM 节点每轮翻译一个段落。这里要注意一个 Dify 的具体限制迭代节点的输出是数组格式所有轮次的结果会聚合在一起。汇总代码节点需要把每轮输出的block_id和translated_text重新组织成一个以page_index为维度的嵌套 JSON再传给渲染服务。LLM 节点的参数我建议这样设置温度Temperature翻译任务建议 0.1~0.2低温度能有效减少自由发挥和漏译。最大 Token根据切片长度动态设置一般设 2000 左右避免单轮输出被截断。Prompt 模板明确角色、任务、输入格式、输出格式并要求只输出译文、不加解释。我的 Prompt 模板参考如下你是一名专业的多语言文档翻译专家。请将用户提供的 PDF 文档片段从 {source_lang} 翻译成 {target_lang}。 要求 1. 保持原文的专业术语一致性优先使用术语表中的译法。 2. 保留原文中的数字、单位、专有名词、型号信息。 3. 不要添加任何解释性文字、注释或格式标记如 Markdown。 4. 输出只包含译文文本。 原文片段 {source_text}术语表可以通过 Dify 的「变量」或「数据集」方式注入。我的经验是用一个知识库存储术语表在 LLM 节点前加一个「知识检索」节点把术语词组拉出来直接塞进 Prompt。这样做的好处是术语表方便动态更新不需要改工作流。4.3 文件上传与返回的两种实现路径Dify 工作流里处理文件上传和返回有「表单上传」和「HTTP 回调」两种常见路径我在不同项目里都用过优缺点很明显。第一种是表单上传路径适合对安全要求不高的内部工具。用户在应用前端上传 PDFDify 把它作为变量传给工具节点自建服务接收文件后返回 JSON渲染完成后 Dify 把 PDF 作为输出返回给用户。链路短用 Postman 都能调试。第二种是 HTTP 回调路径适合跨网络、跨系统调用的场景。自建服务解析完 PDF 后不直接同步返回大 JSON同步返回容易超时而是先返回一个task_idDify 工作流轮询或者自建服务翻译完成后主动 POST 回调 Dify 的 API把结果推回来。这里有一个非常实用的生产级经验千万不要用同步 HTTP 请求去送一个 50MB 的大 PDF。轻则超时重则网关直接断连。生产环境我都是用异步任务队列简单场景 Redis RQ 就够任务提交后 Dify 工作流先记录task_id渲染完成后服务端回调 Dify再触发后续节点。Dify 工作流本身可以配置「等待」节点配合回调实际体验很顺畅。5. 实战中踩过的坑与优化经验中英文宽度差异到扫描件 OCR5.1 中英文宽度差异导致的文本溢出与我的解决方案这是我在做原格式翻译时遇到的第一个大坑也是被问得最多的一个问题。中文是方块字一个字符基本占「1em」的宽度英文是比例字体i和W的宽度天差地别。当原文是英文时解析得到的每个 span 文本宽度是靠英文字体度量计算的翻译成中文后同样字号下中文字符的总宽度往往大于原文英文宽度。举个例子原文 Configuration 在 12pt 下宽度约 27 磅翻译成「配置」两个字宽度约 24 磅看着差不多。但如果是 Internationalization 这种长词翻译成「国际化」三个字宽度可能只有原来的一半不到渲染后右侧会空出一大片。反过来中文原文翻译成英文时问题更严重「数据处理」四个字 12pt 下宽度约 48 磅翻译成 data processing 宽度约 78 磅直接超过原 block 宽度要么溢出边框要么被insert_textbox自动换行导致高度增加挤压下面的内容。我的解决手段是「宽度感知的自适应字号」渲染前先按目标语言字体计算译文总宽度如果超过原文 bbox 宽度的 90%先尝试缩小字号以 0.5pt 为步长最小不低于原字号的 80%如果缩小后还不够就允许文本在 bbox 内自动换行同时把后续 block 的 y 坐标向下平移以让位。实际效果测试下来单行标题场景下缩小字号的方案最稳妥多行正文场景下自动换行 下移补偿更自然。两者可以结合标题区域优先缩字号正文区域优先换行补偿。5.2 字体缺失与中文回填乱码的处理中文字体缺失是另一个高频问题。PyMuPDF 默认的china-s字体其实是内置的基础 CJK 字体能显示中文但字形偏细看起来像宋体。如果你的原文用的是黑体或楷体渲染出来字体风格不一致一眼就能看出是「翻译过的文件」。更严重的情况是某些 PDF 原文使用的字体只包含拉丁字符集翻译成中文后字形文件里根本没对应的中文字符直接用原字体渲染会得到乱码或空白。我的处理方式是单独维护一个字体映射表解析时把原文的字体名记录下来渲染时根据目标语言映射到对应字体文件。比如原文的Helvetica-Bold在中文场景下映射到思源黑体 Bold在日文场景下映射到Noto Sans JP Bold。字体文件会在服务启动时预加载进 PyMuPDF 的字体管理器。代码大致是这样FONT_MAP { (Helvetica, zh): fonts/SourceHanSansSC-Regular.otf, (Helvetica-Bold, zh): fonts/SourceHanSansSC-Bold.otf, (Times-Roman, zh): fonts/SourceHanSerifSC-Regular.otf, } def get_font_for_span(original_font: str, target_lang: str) - str: return FONT_MAP.get((original_font, target_lang), fonts/NotoSansCJK-Regular.ttc)这事的成本主要在字体文件本身。思源黑体一个 OTF 动辄十几 MB但这是保证版式还原质量绕不开的投入。实测下来用思源黑体替代 Helvetica 做中文渲染视觉风格最接近原文现代感用思源宋体替代 Times New Roman则更适合学术文档。5.3 扫描版 PDF 的 OCR 链路与 Dify 的配合方式扫描版 PDF 的处理逻辑和前面对照不同因为根本没有文本层可提取。我单独跑了一套 OCR 链路先转图片PyMuPDF 的page.get_pixmap()再调 PaddleOCR 做检测识别。PaddleOCR 输出的结果同样带坐标信息每个文本行有一个四点框我会把这些检测结果转换成一个跟文字版 PDF 结构类似的 JSON包括bbox取外接矩形、text、置信度。之后走 Dify 工作流的流程跟文字版完全一样。但有几个扫描件特有的问题要处理好OCR 误识别扫描质量差、倾斜、光照不均都会导致识别错误。建议在 OCR 前先做图像预处理包括灰度化、二值化、倾斜校正。PaddleOCR 自带的angle_cls方向分类器一定要开能显著降低倒置文本的概率。回填位置偏差OCR 给出的坐标是有误差的渲染时译文不能直接按原文坐标写需要以原文图片为底先把坐标微调对齐再用白色遮罩覆盖原文写译文。扫描件的背景不是纯白扫描文档通常有底色或噪点涂白遮罩会露出一块明显的白斑。解决办法是把遮罩颜色改成从原图对应区域采样的平均背景色。OCR 链路在 Dify 里的实现也不复杂Dify 知识库里自带文档解析但对扫描版 PDF 的 OCR 支持依赖底层部署效果参差不齐。我更推荐在自建服务里把 OCR 做完再把结构化结果抛给 Dify 翻译这样的可控性最强。5.4 长文档翻译的成本控制与质量一致性一份上百页的 PDF按段落切片后可能生成几百个翻译子任务。如果不做任何优化Token 消耗会非常可观而且术语一致性很难保证——同一个词在第一页翻译成「接口」到第三十页可能变成「界面」。成本控制方面我常用的三个手段第一是「相似文本去重」。文档里常有重复出现的段落比如产品手册里的安全警告、页脚免责声明。解析时对段落文本做哈希相同文本只翻译一次渲染时复制结果到所有出现的位置。这个优化在手册类文档上能省 30% 以上的 Token。第二是「低价值区域跳过」。页眉页脚、纯数字页码、URL 这些内容可以直接跳过或只翻译必要部分。尤其是 URL 和邮箱地址翻译了反而惹麻烦。第三是「缓存」。高频文档重复翻译时比如同一产品的多语言手册迭代可以用 Dify 的知识库或者外部 Redis 做翻译缓存按「源语言 目标语言 文本哈希」做键命中直接取结果。术语一致性方面除了前面说的术语表注入还有一个技巧在切片分组时把包含术语密度较高的技术段落优先翻译翻译结果回填到术语表缓存里后续段落翻译时把已确定的术语译法作为 few-shot 示例注入 Prompt。这相当于一种「增量术语对齐」实测能让术语一致性提高不少。6. 效果验证与扩展思路从单语还原到双语对照再到批处理6.1 一套可复用的质量评估方法翻译类项目最怕「自我感觉良好」。我做了一套简单的质量评估方法每次上线前必须过一遍。第一层是版式完整性检查。用 PyMuPDF 对比原 PDF 和译后 PDF 的页面数、每页文本 block 数量、图片和表格的坐标位置。如果译后 PDF 的 block 数量跟原文差异超过 5%说明有段落合并或拆分异常需要人工复查。第二层是文本回译检查。从译后 PDF 里提取文本翻译回源语言跟原文做相似度对比。相似度低于阈值的段落大概率是翻译出错或漏译。这一步可以用 LLM 做「回译 打分」但成本较高建议只在抽样段落里做。第三层是视觉抽样检查。按页码抽样把原文和译文 PDF 每页渲染成图片人工对比版式还原度。重点抽查目录页、表格密集页、含图片混排页。信息密度高的场景最好用截图对比工具做像素级 diff能快速发现坐标偏移。6.2 双语对照模式一层原文一层译文的层叠技巧除了「直接替换原文」的原格式翻译我还做过「双语对照」模式就是在原文下方追加一行译文或者把译文书签叠加在原页面上。这种模式对学习材料、产品对照手册特别有用。实现方式是在渲染时把每个 block 的高度动态扩展为原来的两倍原文写在原位置译文写在原文正下方字号减 2~3pt颜色用灰色。后续 block 的 y 坐标统一向下平移保证不重叠。这个模式对「原格式」的定义更强不但要保留原有排版还要保证加了译文后整体版式仍协调。实测时需要特别关注列表符号、项目编号的排列以及跨页段落的衔接。6.3 从单文件工具到批处理管线的演进路径单文件处理跑通后自然会想到批量处理一批 PDF。批处理对架构有一个硬性要求Dify 工作流要支持并发控制避免同时翻译大量文件把 LLM API 的配额打爆。我的做法是加一个「排队 限流」层自建服务的任务队列里设置并发上限比如同时最多 3 个翻译任务Dify 工作流按队列顺序提交完成后通过回调通知。Dify 侧可以做一个「批次任务」表记录每个文件的处理状态前端展示进度条。批处理的另一个需要考虑的点是文件命名和归档。我通常会要求原文文件名带上语言标签如user_manual_en.pdf这样输出文件名可以自动生成user_manual_zh.pdf。归档目录按项目和时间分文件夹方便后续追溯。最后说一个后续可以扩展的方向把整个能力跟 RAG 结合。翻译完成的文档可以直接回流到 Dify 的知识库不仅保留原格式还能成为高质量的多语言检索语料做跨语言的问答或摘要。这个链路一旦打通PDF 原格式翻译就从「一次性工具」变成了「知识资产沉淀管道」价值就完全不一样了。