
去年年底帮一个做批量诉讼的律师朋友处理了一桩头疼事手上一次性进来三十几个民间借贷案件每个案子都要出起诉状、证据清单、律师函格式要求还都一致。用Word复制粘贴改了整整两天中间还漏改了一处被告名字差点闹出大问题。后来我实在看不过去花了一个周末写了套Python自动化法律文书撰写的脚本把Excel里的案件信息一键生成起诉状和律师函半小时干完原本两天的工作量。这套东西的价值不在于“懂法律”而在于把法律文书里那层高度结构化的壳子拆开用Python的字符串处理、模板渲染、数据校验能力去批量复现它。今天就把这套方案的完整思路、核心代码和踩坑记录都整理出来给同样被批量文书折磨的律师、法务和技术爱好者做个参考。在动手写代码之前我想先聊一个更底层的问题法律文书到底能不能自动化我的结论是能但必须清楚它的边界。法律文书在很多人眼里是严肃、专业、充满人类判断力的产物但实际上它的构成远比想象中更“机械”。一份标准合同里有固定的条款顺序一份起诉状有固定的段落结构一份律师函有固定的抬头、事实描述、法律依据和落款格式。这些固定结构本质上就是一套模板语法而案件信息、当事人信息、金额、日期这些变量就是填充模板的数据源。把“模板结构”和“可变数据”分离再用Python把它们组合起来这就是自动化撰写的底层逻辑。当然这不意味着AI或脚本能替代律师的专业判断它的定位是“高效生产初稿、人工负责终审”这个边界想清楚了后面所有技术选型才不会跑偏。1. 法律文书自动化的价值边界什么能自动化什么不能1.1 法律文书里的“结构化”究竟指什么很多人一听到“法律文书自动化”第一反应是觉得不靠谱——法律条文的理解、事实认定的逻辑、证据链的构建这些难道也能用脚本解决确实解决不了但这些并不是法律文书的全部。我拆解了大量真实文书之后发现一份典型的法律文书可以分成三个层次第一个层次是固定表达。比如起诉状里的“原告XX男汉族19XX年X月X日出生住XX联系方式XXX诉讼请求一、XX二、XX。事实与理由……”这些行文结构、起承转合、法律用语习惯在同一个律所、同一个业务领域里几乎是一模一样的。第二个层次是变量数据。当事人的姓名、身份证号、住址、联系方式、合同编号、借款金额、日期、逾期天数、约定利率等这些信息每份文书都不同但它们在文中的位置是固定的。第三个层次才是真正需要专业判断的内容比如对事实的法律定性、证据的组织逻辑、诉求金额的计算方式选择等。前两个层次大概占一篇文书的70%到85%而且高度机械化。自动化脚本能做的就是把第一个层次固化成模板把第二个层次从数据源比如Excel、数据库甚至是用户填写的表单中读取并填充进去第三个层次则通过预设规则辅助生成初稿最后由律师把关。想清楚这个模型之后我对“什么能自动化、什么不能自动化”的判断就非常清楚了。1.2 三类法律文书的自动化可行性分档根据结构化的程度我把常见的法律文书分成了三档每一档的自动化难度和策略完全不同文书类型典型示例结构化程度自动化策略高度模板化劳动合同、授权委托书、商标续展申请、房屋租赁合同极高变量集中模板渲染 数据填充即可几乎不需要业务逻辑半结构化民事起诉状、律师函、答辩状、上诉状较高但包含计算逻辑和条件分支模板渲染 规则引擎金额计算、日期计算、条件判断低结构化法律意见书、辩护词、尽职调查报告低高度依赖案件事实与专业判断自动化只做素材整理与初稿骨架核心内容靠人工我最初踩过最大的坑就是试图把低结构化的文书也做成全自动结果模板写了上百个变量到了实际使用的时候还是各种“意外情况”维护成本比手写还高。后来调整策略高度模板化的文书全力自动化半结构化的文书重点做“初稿自动生成 人工修订”低结构化的文书只自动拼装封面、目录、基本信息和素材附件。这个价值边界想清楚之后整个项目才真正跑起来。1.3 自动化的真正收益不是替代人而是消灭重复劳动如果只是偶尔写一份起诉状手动用Word排一下也很快不值得写脚本。自动化真正的价值体现在批量场景批量合同续签、批量案件立案、批量发送律师函、批量生成债权转让通知。在这些场景里每份文书虽然细节不同但结构和流程高度一致人工处理面临两个致命问题一是效率低二是容易出错。最典型的就是复制粘贴时漏改当事人姓名或金额这种错误在法律文书里是致命的。用Python做自动化之后数据源只有一个每次生成文书都从同一个数据源读取谁的名字、多少钱、什么日期都是程序填进去的只要数据源是对的文书就一定是对的。这就把“人肉复制粘贴出错”的概率降到了零。从这个角度说自动化的收益不只是省时间更是降低了错误率这是法律行业最看重的价值。2. 整体架构设计数据层、模板层、生成层如何分工2.1 一个稳定的三层次架构整套系统的代码结构并不复杂但架构设计必须清晰否则一旦文书类型增加代码会迅速腐化。我采用的方案是标准的三层架构数据层、模板层、生成层。数据层负责管理所有案件变量。包括当事人信息、案件事实、金额数据、日期数据、约定条款等。这套系统里我用Excel作为数据源因为律师团队的日常习惯就是在Excel里维护案件台账不需要额外开发管理界面。如果业务复杂也可以换成SQLite、MySQL或者直接接一个表单页面但核心原则是一致的数据与文书分离。模板层负责存放所有法律文书的Word模板。模板用docxtpl的Jinja2语法编写里面写死固定的表达用变量占位符表示可变内容用条件语句控制不同情况的展示。生成层是Python脚本它读取数据层的案件信息渲染模板层的docx文件最终输出一份完整的法律文书。2.2 为什么选docx作为载体而不是PDF或纯文本我一开始也考虑过用纯文本文档或直接生成PDF但最终选了docx原因有三个。第一法律行业的交付和归档习惯就是Word文档法院立案系统、合同管理系统、客户沟通场景都默认收docx生成PDF再转回Word反而多一道工序。第二docx本质上是XML文件通过python-docx库可以精确控制页面布局、字体、字号、缩进、页眉页脚、表格样式这些格式要求对法律文书来说极其重要纯文本根本做不到。第三docx模板的修改成本极低律师在Word里修改模板后直接保存就能生效不需要技术人员的介入。在这个架构下模板维护和代码开发被彻底解耦了。律师想改文书措辞打开Word改模板就行技术想加功能改Python脚本就行。两者互不干扰这是整个项目能够长期存活的关键。2.3 各模块的职责边界别让代码越界管业务架构设计里特别重要的一条原则是Python代码不应该硬编码任何法律术语、条款文字或固定表达。所有文书正文内容都应该在模板层解决代码只负责处理数据读取、逻辑判断、文件生成和命名这类技术操作。举个例子起诉状里“诉讼请求”的固定开头语“判令被告……”这段文字应该写在模板里而不是写在代码里。这样做的原因是法律语言的修改权必须还给法律专业人士程序员不应该也没能力去维护法律词汇库。同样模板里也不应该出现复杂的业务计算逻辑。金额计算、利息计算、期限推算这种需要规则的事情都应该放在生成层用Python函数去完成然后把计算结果作为变量传给模板。模板里只做简单的变量渲染和条件判断不承载计算职责。这个分工一旦明确模块边界就非常清晰后续扩展新文书类型的时候只需新增一个模板文件、适配数据解析逻辑就够了生成层基本不用动。3. 核心实现docxtpl模板引擎与结构化数据模型3.1 为什么选择python-docx配合docxtplPython操作Word文档有两个最常用的库python-docx负责从零创建和修改docx文件docxtpl则是在python-docx基础上提供了模板渲染能力。纯用python-docx去逐段、逐句的拼接文书不是不行但代码会非常啰嗦而且文书版本一变代码就要跟着大改。docxtpl的思路完全不同它把Word文档变成类似Django/Jinja2的模板文件直接在Word里写{{ 变量名 }}然后用Python传入一段字典数据一次性渲染出最终文档。这套设计非常贴合法律文书的迭代场景因为法律文书的行文结构经常微调用Word模板维护这些调整成本最低。安装过程很简单用pip一次性装齐需要的库即可pip install python-docx docxtpl pandas openpyxl其中pandas和openpyxl是为了读取Excel案件台账用的如果数据源是别的形式这两个库可以按需替换。我建议无论如何先把python-docx和docxtpl装上这两个是核心依赖。3.2 模板文件里怎么写变量和条件docxtpl使用的是Jinja2模板语法在Word里的写法和在HTML里几乎一样。核心语法有三类我需要重点说明。第一类是最基础的变量替换在Word中直接输入双花括号加变量名比如“{{ plaintiff_name }}”渲染时会被替换成实际数据。第二类是条件判断用{% if 条件 %}和{% endif %}包起来比如只有存在担保人时才显示担保人段落。第三类是循环渲染主要用于证据清单、诉讼请求列表这种“数量不固定”的模块用{% for item in items %}循环输出。诉讼请求 {% for req in claims %} {{ loop.index }}. {{ req }} {% endfor %} 事实与理由 原告{{ plaintiff_name }}与被告{{ defendant_name }}于{{ contract_date }}签订了《借款合同》合同编号为{{ contract_no }}借款金额为人民币{{ amount }}元约定借款期限为{{ loan_term }}个月。合同签订后原告依约支付了借款但被告自{{ overdue_date }}起未能按期还款。 {% if has_guarantor %} 担保人{{ guarantor_name }}对上述债务承担连带保证责任。 {% endif %}这里有几个细节值得注意{{ loop.index }}是Jinja2内置的循环序号变量可以自动生成“1.”、“2.”这种序号省去了在数据端预编写序号的麻烦。还有Word模板里的花括号字符需要直接在Word中输入不要用复制粘贴的HTML转义字符否则会被识别为普通文本而不是模板变量。3.3 数据模型的定义与校验别让脏数据毁掉一份文书模板搞定之后紧接着要考虑的是数据模型。我的建议是在Python里为每类文书定义一个数据类用类型注解声明每个字段的类型同时加入校验逻辑。原因是法律文书对数据的准确性要求极高一个空值或格式错误的日期直接渲染到文书里后果非常严重。from dataclasses import dataclass from datetime import date dataclass class LitigationCase: case_no: str plaintiff_name: str plaintiff_id: str plaintiff_address: str defendant_name: str defendant_address: str contract_no: str contract_date: date principal: float # 借款本金 annual_rate: float # 年利率小数形式如0.12表示12% overdue_start: date # 逾期起始日 filing_date: date # 起诉日期 def validate(self) - list[str]: errors [] if not self.plaintiff_name or not self.defendant_name: errors.append(当事人姓名不能为空) if self.principal 0: errors.append(借款本金必须大于0) if self.contract_date self.filing_date: errors.append(合同签订日期必须早于起诉日期) if self.overdue_start self.contract_date: errors.append(逾期起始日不能早于合同签订日) return errors在这种设计下读取Excel数据后先构造数据对象再调用validate方法做一轮整体校验发现异常数据就停下来人工排查绝不让坏数据流进模板。这个步骤可能看起来有些繁琐但在批量生成场景里它就是救命的保险丝。我遇到过Excel里日期列被Excel自动转成了另一种格式或者金额列里出现了文本描述这些都是肉眼很难在一堆数据里发现的但程序校验可以在几秒钟内把它们全揪出来。3.4 渲染并生成符合格式要求的Word文档数据校验通过之后的渲染反而很简单from docxtpl import DocxTemplate def generate_complaint(case: LitigationCase, template_path: str, output_path: str) - None: doc DocxTemplate(template_path) interest_days (case.filing_date - case.overdue_start).days interest_amount round(case.principal * case.annual_rate / 365 * interest_days, 2) total_amount round(case.principal interest_amount, 2) context { case_no: case.case_no, plaintiff_name: case.plaintiff_name, plaintiff_id: case.plaintiff_id, plaintiff_address: case.plaintiff_address, defendant_name: case.defendant_name, defendant_address: case.defendant_address, contract_no: case.contract_no, contract_date: case.contract_date.strftime(%Y年%m月%d日), principal: f{case.principal:,.2f}, interest: f{interest_amount:,.2f}, total_amount: f{total_amount:,.2f}, interest_days: interest_days, claims: [ f判令被告向原告偿还借款本金人民币{case.principal:,.2f}元, f判令被告向原告支付逾期利息人民币{interest_amount:,.2f}元暂计算至{case.filing_date.strftime(%Y年%m月%d日)}, 判令被告承担本案全部诉讼费用, ], } doc.render(context) doc.save(output_path)这样一份民事起诉状的初稿就生成了。这里我把诉讼请求中的金额计算结果也做成了列表变量传给模板模板里用循环输出好处是以后诉讼请求条数有增减时只需要改Python函数里的列表模板完全不用动。4. 条款级自动生成把计算规则写进文书里4.1 从变量填充到规则联动为什么需要规则引擎前面讲的模板渲染本质上还是“变量替换”只要数据齐全就能跑。但法律文书里还有一类更复杂的场景它不能直接读取数据而是要根据业务规则先计算出结果再决定文书怎么写。典型的例子包括逾期利息的计算、违约金的起算日、保证期间的届满日、对不同逾期天数适用不同违约责任条款等。这些计算和判断如果都塞在代码的业务逻辑里会越写越乱。我采用的做法是把这些规则封装成独立的Python函数形成一个小型规则引擎。它的输入是案件数据结构输出是供模板使用的计算字段。这样模板只对外展示计算结果规则修改时只动函数互不干扰。4.2 利息计算、违约金计算与大写金额转换的实现法律文书中金额涉及“人民币大写转小写”和“计算逾期利息”的需求非常高频。我提供了一个比较完整的小工具函数集def amount_to_rmb_upper(amount: float) - str: units [, 拾, 佰, 仟, 万, 拾, 佰, 仟, 亿] digits 零壹贰叁肆伍陆柒捌玖 amount_str f{amount:.2f} int_part, dec_part amount_str.split(.) if int(int_part) 0: result 零元 else: result n len(int_part) zero_flag False for i, ch in enumerate(int_part): pos n - i - 1 digit int(ch) if digit 0: if pos % 4 0 and pos 0: if not zero_flag: result 零 result units[pos] zero_flag False else: zero_flag True continue if zero_flag: result 零 zero_flag False result digits[digit] units[pos] result 元 jiao int(dec_part[0]) fen int(dec_part[1]) if jiao 0 and fen 0: result 整 else: if jiao 0: result digits[jiao] 角 elif fen 0: result 零 if fen 0: result digits[fen] 分 return result def calc_overdue_interest(principal: float, annual_rate: float, start_date: date, end_date: date) - float: days (end_date - start_date).days return round(principal * annual_rate / 100 / 365 * days, 2) def calc_penalty(principal: float, daily_rate: float, overdue_days: int) - float: return round(principal * daily_rate * overdue_days, 2)以逾期利息为例计算逻辑看起来简单但真正用的时候有几个细致的决策点年利率是按360天折算还是按365天折算司法实践中各地法院的习惯并不完全统一起算日是合同约定的还款日次日还是催告后届满日这个细节会影响最终金额。我的建议是不要把这些口径硬编码死在工具函数里而是做成参数由模板调用时按具体案件的需求传入。比如有的模板需要年利率除以365有的要除以360工具函数只要接收对应参数即可。4.3 条件分支让模板根据案件状态“自我表达”除了计算函数规则引擎里还包含一组条件判断逻辑用于决定文书中某些段落是否出现。一个常见的场景是保证责任。如果案件中有担保人文书里要专门写担保人的身份信息、保证方式、保证期间如果没有担保人这段就要完全隐藏。这个判断可以在模板中用Jinja2的{% if %}实现也可以由Python函数先把这个布尔值计算好传给模板。context { has_guarantor: len(case.guarantors) 0, guarantor_name: case.guarantors[0].name if case.guarantors else , guarantee_type: 连带责任保证 if case.guarantee_is_joint else 一般保证, guarantee_period_end: calc_guarantee_period_end(case), }我的经验是如果只是单纯的“有/无”判断放在模板里更直观如果判断条件本身涉及多步计算比如判断保证期间是否已届满结合了合同签订日、保证方式、是否约定保证期间等多个因素那就在Python里算好只给模板传一个布尔值和最终日期这样模板语言保持极简法律顾问维护模板时也不用理解Python逻辑。5. 完整实战从Excel案件表到批量起诉状5.1 实战场景设定理论说了一堆还是落地最重要。我来完整梳理一遍这套系统在真实场景里的使用方法。假设现在收到了一个批量案件的Excel台账路径是“案件清单.xlsx”里面每一行是一条案件记录列包括原告姓名、原告身份证号、被告姓名、被告身份证号、借款合同编号、合同签订日期、借款本金、年利率、逾期起始日、起诉日期等。我要做的就是从这份Excel出发为每一行生成一份对应的民事起诉状docx文件。5.2 数据读取、清洗与类型转换Excel读进来之后最先要处理的是数据类型。Excel中的日期列经常被自动识别为datetime类型读取出来是Timestamp对象直接传给docxtpl渲染会变成一串带时分的字符串极不友好。金额列可能出现千分位分隔符、货币符号或者干脆是文本格式。这些都需要统一转换。import pandas as pd from pathlib import Path from datetime import datetime def load_cases_from_excel(path: str) - list[LitigationCase]: df pd.read_excel(path, engineopenpyxl) df df.fillna() cases [] for _, row in df.iterrows(): case LitigationCase( case_nostr(row.get(案号, )).strip(), plaintiff_namestr(row.get(原告姓名, )).strip(), plaintiff_idstr(row.get(原告身份证号, )).strip(), plaintiff_addressstr(row.get(原告住址, )).strip(), defendant_namestr(row.get(被告姓名, )).strip(), defendant_addressstr(row.get(被告住址, )).strip(), contract_nostr(row.get(合同编号, )).strip(), contract_date_parse_date(row.get(合同签订日期)), principalfloat(row.get(借款本金, 0) or 0), annual_ratefloat(row.get(年利率, 0) or 0), overdue_start_parse_date(row.get(逾期起始日)), filing_datedatetime.now().date(), ) errors case.validate() if errors: print(f数据异常已跳过: {row.get(案号, )}: {; .join(errors)}) continue cases.append(case) return cases def _parse_date(value): if isinstance(value, datetime): return value.date() if isinstance(value, str): for fmt in (%Y-%m-%d, %Y/%m/%d, %Y年%m月%d日): try: return datetime.strptime(value.strip(), fmt).date() except ValueError: continue raise ValueError(f无法解析日期: {value})fillna()这个操作容易被忽略但很关键。Excel里未填写的单元格读进来是NaN如果直接拼进文书里会渲染成“nan”或者“None”这在法律文书中完全不能接受。先填充空字符串再后面判断是否需要拼接这个字段逻辑会清晰很多。同时validate方法会在数据异常时中断这一条记录的生成并打印提示避免批量跑到一半才发现前面生成的文书是错的。5.3 批量生成与文件命名规范数据解析完成之后批量生成就非常直接了。遍历案件列表对每个案件调用前面写好的generate_complaint函数输出文件路径按照统一规则命名原告_被告_案由_日期.docx这样文件管理器里按名称排序检索起来非常方便。from pathlib import Path def batch_generate_complaints(cases: list[LitigationCase], output_dir: str) - None: out_dir Path(output_dir) out_dir.mkdir(parentsTrue, exist_okTrue) template_path templates/民事起诉状模板.docx for case in cases: filename f{case.plaintiff_name}诉{case.defendant_name}_民间借贷纠纷_{case.filing_date.strftime(%Y%m%d)}.docx output_path out_dir / filename generate_complaint(case, template_path, str(output_path)) print(f已生成: {filename})这里有个细节文件名里尽量不使用“/”、“\”、“:”等特殊符号Windows文件系统不支持这些字符。常规做法是用中文“诉”和“_”分隔既清晰又安全。另外输出目录按日期建子文件夹是一个不错的习惯比如“output/2024-06-30/”这样以后回溯是哪一批生成的一目了然。5.4 加入进度反馈与错误处理批量生成到几百份的时候控制台没有任何输出是让人很慌的。我一般在循环里加进度计数器每生成十份打印一次进度。生成过程中还可能遇到某个模板文件损坏、某个案件数据导致渲染异常这些都需要做好try/except包裹保证单条失败不影响整体流程同时把失败的案件信息保存到日志里。from loguru import logger logger.add(batch_generate.log, rotation10 MB) def batch_generate_with_log(cases, output_dir): total len(cases) for idx, case in enumerate(cases, start1): try: batch_generate_complaints([case], output_dir) logger.info(进度: {}/{} 完成 - {}, idx, total, case.case_no) except Exception as e: logger.error(生成失败 - {}: {}, case.case_no, str(e))6. 落地过程中躲不开的坑格式、编码与合规底线6.1 模板渲染后的格式问题字体、缩进、表格列宽docxtpl有个老问题模板里设置好的字体在渲染后有时会跑偏。尤其是中文字体python-docx默认处理中文时如果模板里没有显式声明中文字体“宋体”或“仿宋”渲染出来的文档在另一台电脑上打开时可能变成默认字体。解决方式是在模板中提前设置好正文样式的中文字体并且明确指定字符集。还有表格列宽的问题。docxtpl对模板中表格的列宽处理并不稳定渲染后经常出现列宽被自动撑开的情况。最稳妥的办法是在模板里先把列宽锁死或者渲染后用python-docx在代码里重新显式设置列宽。我自己更倾向于后者因为代码可控不依赖模板编辑者的操作习惯。from docx import Document from docx.shared import Cm def fix_table_widths(doc_path: str, widths_cm: list[float]) - None: doc Document(doc_path) for table in doc.tables: for row in table.rows: for idx, width_cm in enumerate(widths_cm): if idx len(row.cells): row.cells[idx].width Cm(width_cm) doc.save(doc_path)6.2 编码问题的“水土不服”这个问题多发生在Windows环境下。Excel文件默认可能使用GBK编码读取pandas时如果不加处理会乱码而Python的默认字符串编码是UTF-8两者混杂时容易出幺蛾子。我统一的做法是读入数据后立刻做一次规范化强制转成Python的str类型在保存文件路径和文件名时也全部用字符串拼接不通过bytes做任何中转。这样能规避90%的编码问题。另一个常见的坑是Excel中的“手机号”或“身份证号”被识别为科学计数法。Excel对超过11位的数字会自动变成科学计数法显示比如身份证号会变成“110101199003076538”可能被读成“1.10101E17”。遇到这个字段读取时一定要把该列转换为字符串类型df[原告身份证号] df[原告身份证号].astype(str).str.replace(.0, , regexFalse)6.3 合规底线自动化生成不等于律师签发最后这一点是我特别想强调的。法律文书自动化的合规底线在于脚本生成的是初稿不是最终意见。任何一篇由Python生成的文书在对外提交之前都必须经过执业律师的人工审核、确认和签字。这不是技术功能的局限而是法律执业的基本规则。自动化工具的价值在于把律师从重复劳动里解放出来让他们把时间花在真正需要专业判断的地方而不是试图替代律师的专业职责。我在模板页脚里会固定生成一行小字“本文书由自动化系统辅助生成需经承办律师审核并签署后方可对外使用。”这个提示既能提醒使用者也是一种责任界定的体现。系统设计时也建议在生成流程里加入一个“审核环节”律师在AI或者工具生成初稿后必须在流程系统中点击“确认无误”该文书才会被标记为正式版本这个管理闭环对项目的长期可持续性非常重要。6.4 关于docx元数据清理的一个小细节很多人会忽略这个细节但律师行业特别在意。用python-docx或docxtpl生成的docx文件在文件属性里可能残留系统用户名、组织名、最后保存者等元数据。这些信息如果不清理发给对方之后对方在Word“文档信息”里就能看到是谁生成的、在哪台机器上生成的这在一些对保密性要求高的场景里可能引起不必要的麻烦。生成之后用python-docx打开把core_properties里的author和last_modified_by清空或者设置成承办律师自己的名字是一种稳妥的做法def clean_docx_metadata(doc_path: str, author: str 承办律师): doc Document(doc_path) doc.core_properties.author author doc.core_properties.last_modified_by author doc.save(doc_path)我实际把这套方案交付给律师朋友使用之后最大的体会是不要把自动化项目做成一个“写了就完事”的一次性脚本而是要把它当成一个会长期运转的“小系统”来维护。模板会随着司法实践变化而调整Excel台账字段会随着团队习惯变化而增减利率口径、管辖规则也会有更新。所以我在代码里特意把模板路径、数据列名、利率口径这些都做成了配置项切换业务场景时只改配置不改代码。另外一个经验是给律师团队用的时候最好在项目根目录放一个简单的一键运行脚本比如双击“生成全部文书.bat”或运行一行命令。不要让使用者去理解Python虚拟环境、依赖安装这些事那不是他们的职责。把复杂留给开发把简单留给用户这个工具才能真正在团队里活下来。这套方案目前已经跑了大半年累计生成了几百份文书帮团队省下的时间相当可观。如果你也有类似的需求不管是合同、律师函、起诉状还是其他法律文书完全可以照着这个思路动手试一下。核心要诀记住一句话结构化的模板归Word管变化的业务数据归Excel管规则判断和批量生成归Python管各归其位就不会乱。