资讯动态

基于RAG与Flask的劳动合同智能审查系统实战

发布时间:2026/10/14 7:05:17 来源:尧图企业网站定制
1. 为什么劳动合同审查值得用RAG重做一遍劳动合同这东西绝大多数人一辈子签不了几次但每一次都可能是几十万甚至上百万的权益博弈。我见过太多案例入职时随手签了字离职时才发现竞业限制条款写得像天罗地网或者违约金条款根本不符合法定上限。传统做法是找律师逐条看但律师按小时收费一份合同审下来少则几百多则上千普通打工人根本扛不住这个成本。大模型出来之后很多人第一反应是直接把合同丢给GPT问有没有坑。我实测过这条路走不通。原因很直接通用大模型对劳动法领域的细节掌握是泛泛而谈的它能告诉你注意竞业限制但说不清楚竞业限制补偿金不得低于离职前十二个月平均工资的30%这种硬性数字更不会主动去比对当地的具体裁审口径。它没有依据只有印象。RAG检索增强生成解决的正是这个问题。它的核心思路是不让模型凭记忆回答而是先从一套权威的法规库、判例库、合同范本库里检索出相关条文再让模型基于这些条文来分析和判断。这样一来每一条风险提示背后都有出处可信度完全不一样。我做的这个系统就是把这套思路落地成一个Flask应用。用户上传劳动合同PDF或Word系统自动解析文本、切分条款、检索相关法规、逐条比对风险点最后输出一份带风险等级和修改建议的审查报告。关键词覆盖劳动合同审查、RAG检索增强、Flask后端、条款风险识别、法规知识库这几个核心方向。适合谁来参考如果你是有Python基础、想做一个垂直领域RAG应用的开发者这篇内容能给你一套完整的落地路径如果你是法务或HR想理解这类系统背后的逻辑也能看明白它到底靠不靠谱、边界在哪里。下面我从架构设计讲到具体实现把踩过的坑和关键取舍都摊开说。2. 系统整体架构Flask在这个链路里到底承担什么2.1 为什么选Flask而不是FastAPI或Django先说选型。做RAG应用后端框架的选择其实就三个候选Flask、FastAPI、Django。我最后选Flask理由很实际。FastAPI确实异步性能好、自带文档但这个项目的瓶颈根本不在Web层。一次审查请求90%的时间花在文档解析、向量检索和LLM调用上Web框架那点开销可以忽略。FastAPI的异步优势在这里发挥不出来反而它的依赖注入体系对一个小团队来说增加了理解成本。Django太重了。它自带ORM、Admin、认证体系适合做内容管理系统但RAG应用的数据流是文件进、报告出不需要复杂的后台管理。用Django等于为了用它的一个功能背上一整套框架。Flask的好处是薄。路由、请求处理、返回响应就这三件事代码量少调试直观。我需要的是一个能把各个模块串起来的胶水层Flask正好。而且Flask的生态足够成熟文件上传、跨域、模板渲染都有现成扩展遇到问题搜一下就有答案。提示如果你的系统未来要支撑高并发比如做成SaaS给几百家企业用那FastAPI的异步能力会变得重要。但如果是内部工具或小规模服务Flask的开发效率优势更明显。2.2 四层架构的职责划分整个系统我拆成四层每层职责单一方便单独调试和替换。层级职责关键技术可替换性接入层接收上传、参数校验、返回报告Flask Werkzeug低解析层文档转文本、条款切分pdfplumber / python-docx中检索层向量化、相似度检索嵌入模型 向量库高生成层基于检索结果做风险分析LLM API高接入层就是Flask的路由和视图函数。用户POST一个文件过来我先把文件存到临时目录校验格式和大小然后交给解析层。这里有个细节文件校验不能只看扩展名得看文件头magic number否则有人把exe改成pdf传上来会出问题。解析层负责把PDF或Word变成纯文本再按条款切分。劳动合同有个特点它的结构相对固定一般就是甲方乙方信息、合同期限、工作内容、劳动报酬、社会保险、劳动纪律、合同解除、争议解决这几大块。切分的时候如果能识别这些章节标题比无脑按字数切效果好得多。检索层是RAG的核心。把切分好的条款逐条向量化去向量库里找最相关的法规条文和判例。这里嵌入模型的选择很关键后面单独讲。生成层拿到条款原文 检索到的法规这个组合构造Prompt发给LLM让它输出结构化的风险分析。这一层要控制好输出格式否则模型返回一堆散文前端没法渲染。2.3 数据流转的完整链路一次完整的审查请求数据是这样流动的用户上传劳动合同.pdfFlask接收存到/tmp/uploads/生成唯一任务ID解析层用pdfplumber提取文本得到约8000字的纯文本条款切分器按章节和段落切出约25个条款块每个条款块用嵌入模型转成768维向量拿每个向量去法规库检索Top-5相关条文组装Prompt条款原文 5条法规 分析指令调用LLM得到该条款的风险分析25个条款的分析汇总成报告Flask返回JSON前端渲染这个链路里第5到第8步是耗时大头。25个条款意味着25次嵌入调用加25次LLM调用。如果串行执行一份合同要等两三分钟。我的做法是把条款分批并发处理同时给前端加进度反馈让用户知道系统在干活而不是卡死了。3. 文档解析与条款切分脏活累活里的真功夫3.1 PDF解析的坑比想象中多劳动合同的PDF来源五花八门有的是Word直接导出的文字层完整有的是扫描件本质是图片还有的是拍照转PDF歪歪扭扭。这三种情况处理方式完全不同。文字层完整的PDF用pdfplumber直接提取就行效果很好。但要注意表格。劳动合同里经常有薪酬结构表、岗位职责表pdfplumber提取表格时会把单元格内容按行拼接导致基本工资和8000被拆到两行。我的处理是先用extract_tables()单独提取表格转成结构化数据再从正文里剔除表格区域避免重复。扫描件就得走OCR。我试过几个方案最后用的是PaddleOCR中文识别准确率在劳动合同这种印刷体上能到95%以上。但OCR有个问题它会把页眉页脚、页码也识别进来这些噪声会干扰后续的条款切分。我的做法是在OCR后加一层正则清洗把纯数字行、重复出现的短行过滤掉。注意不要对扫描件抱太高期望。如果合同本身拍得模糊OCR出来的文本错字连篇后续分析质量会断崖式下跌。我的系统会在解析后做一个文本质量评分低于阈值就提示用户重新上传清晰版本而不是硬着头皮分析。3.2 条款切分的两种策略对比切分策略直接决定检索质量。我试过两种方案。第一种是固定长度切分比如每500字一块重叠100字。这是最省事的做法但问题很明显它会把一个完整的条款从中间切断。比如乙方在合同期内不得从事与甲方有竞争关系的工作这句话如果被切成乙方在合同期内不得从事和与甲方有竞争关系的工作检索出来的法规就完全不对了。第二种是语义切分按条款的自然边界来切。劳动合同的条款边界很清晰通常以第X条、一、、1.这类标记开头。我用正则匹配这些标记把文本切成条款块。实测下来一份标准劳动合同能切出20到30个条款块每个块平均200到400字正好适合做检索单元。但语义切分也有边界情况。有些合同不用第X条这种标记而是用加粗的标题比如劳动报酬单独一行。这种情况就得结合字体信息来判断。pdfplumber能提取每个字符的字体和大小我可以把字号明显大于正文且独占一行的文本识别为标题。import re def split_contract_clauses(text): # 匹配第X条、一、、1.等条款标记 pattern r(?(?:第[一二三四五六七八九十百]条|[一二三四五六七八九十]、|\d[\.、])) parts re.split(pattern, text) # 过滤空块和过短的块 clauses [p.strip() for p in parts if len(p.strip()) 20] return clauses这段代码是简化版实际用的时候还要处理第X条出现在句子中间的情况以及条款嵌套一条下面有若干款。我的经验是宁可切得碎一点也不要让一个块里混进两个不相关的条款。因为检索是按块进行的块越纯净检索精度越高。3.3 条款分类让检索有的放矢切分完之后我会给每个条款打个标签比如薪酬类、竞业限制类、解除条件类、违约责任类。这个标签有什么用它能让检索更精准。举个例子竞业限制这个条款如果我不加标签直接拿原文去检索可能会检索到一些泛泛的合同义务条文。但如果我知道这是竞业限制类就可以在检索时限定只搜竞业限制相关的法规子集精度立刻提升。分类怎么做我用了一个轻量级方案关键词规则加小模型兜底。先定义每个类别的关键词表比如竞业限制类包含竞业、竞争、同业、限制这些词命中就归类。命中不了的用一个小型文本分类模型比如微调过的BERT来判断。规则覆盖80%的情况模型兜底剩下的20%成本和效果平衡得比较好。4. 检索层设计嵌入模型和向量库怎么选才不踩雷4.1 中文法律文本的嵌入模型选型嵌入模型是RAG的命门。选错了检索出来的东西驴唇不对马嘴后面LLM再强也救不回来。我对比过几个主流的中文嵌入模型。OpenAI的text-embedding-ada-002在通用语义上很强但对中文法律术语的理解一般而且调用有网络和成本问题。BGE系列BAAI General Embedding是国产里表现很稳的bge-large-zh在中文语义相似度任务上长期霸榜。还有一个是M3E在中文场景下也不错。我最后选了bge-large-zh-v1.5。理由有三第一它对中文法律文本的语义捕捉准确我拿竞业限制补偿金和离职后经济补偿这两个概念测试它能正确识别出语义相关但表述不同的情况第二它支持本地部署不依赖外部API数据不出内网这对处理劳动合同这种敏感文件很重要第三它的向量维度是1024检索速度和精度平衡得不错。提示嵌入模型不是越大越好。bge-large是1024维bge-base是768维bge-small是512维。维度越高精度越好但检索越慢。劳动合同审查这种场景条款数量不多几十条用large完全扛得住。但如果你要处理成千上万份合同做批量分析base可能更合适。4.2 向量库的轻量级选择向量库我选了Chroma。原因很简单这个项目的法规库规模不大几千条法规条文几万条判例摘要总共也就几万个向量。这种量级用FAISS或Chroma都绰绰有余没必要上Milvus或Pinecone这种重型方案。Chroma的优势是开箱即用。它可以直接嵌入Flask应用用本地文件持久化不需要单独起服务。代码就几行import chromadb client chromadb.PersistentClient(path./legal_db) collection client.get_or_create_collection( namelabor_law, metadata{hnsw:space: cosine} ) # 添加法规条文 collection.add( documents[竞业限制期限不得超过二年...], metadatas[{source: 劳动合同法, article: 第二十四条}], ids[law_024] ) # 检索 results collection.query( query_texts[竞业限制最多几年], n_results5 )Chroma默认用余弦相似度这对文本检索是合适的。它的HNSW索引在几万向量规模下检索延迟在毫秒级完全满足实时交互需求。但Chroma有个坑它的持久化文件在并发写入时可能损坏。我的做法是法规库的写入只在系统初始化时做一次运行期间只读不写。如果法规库需要更新就停服务、重建索引、再启动避免并发写的问题。4.3 检索策略不只是向量相似度纯向量检索有个问题它擅长语义匹配但对精确的关键词匹配不够敏感。比如用户合同里写甲方有权单方解除向量检索可能返回一堆关于解除权的条文但漏掉了明确提到单方解除的那一条。我的解决方案是混合检索向量检索和关键词检索各跑一遍然后合并结果。关键词检索用BM25算法它对精确匹配很有效。两路结果用RRFReciprocal Rank Fusion融合公式很简单RRF_score Σ 1/(k rank_i)其中k通常取60rank_i是文档在第i路检索中的排名。这个融合方法不需要调参效果稳定。实测下来混合检索比纯向量检索的召回率提升了约15%。对于竞业限制、违约金、试用期这些有明确法律术语的概念关键词检索能兜住向量检索漏掉的情况。另外我还加了一层法规优先级过滤。法规库里的条文来源不同有《劳动合同法》这种上位法也有地方性的裁审纪要。检索时我会给上位法更高的权重因为它们的效力等级更高。这个权重不是硬编码的而是存在metadata里检索后按权重重新排序。5. Prompt工程与风险判定让模型说人话、说准话5.1 Prompt的结构设计RAG的Prompt不是随便写写就行的。我踩过的最大坑是早期Prompt太简单模型经常自由发挥编造一些法规库里没有的条文。后来我把Prompt结构固定成四段式第一段是角色设定你是一名劳动法领域的合同审查专家只依据提供的法规条文进行分析不得引用未提供的法条。第二段是法规上下文以下是与该条款相关的法规条文[检索结果]第三段是待审条款以下是需要审查的合同条款[条款原文]第四段是输出指令请分析该条款是否存在法律风险按以下JSON格式输出{risk_level, risk_type, analysis, suggestion, legal_basis}这个结构的关键在于只依据提供的法规这句话。没有这句话模型会调用它训练时的记忆可能引用已经废止的法规或者编造条文。加上之后模型的输出被约束在检索结果范围内可信度大幅提升。5.2 风险等级的判定逻辑风险等级我分了四级高、中、低、无。判定逻辑不是让模型拍脑袋而是有一套规则。高风险条款内容直接违反法律强制性规定。比如竞业限制期限超过二年、试用期超过六个月、约定女职工不得结婚生育。这类条款在法律上无效一旦发生争议企业必输。中风险条款内容不违法但明显对劳动者不利或者表述模糊容易引发争议。比如甲方可根据经营需要调整乙方工作岗位这种单方调岗条款法律上不是绝对无效但实践中容易产生纠纷。低风险条款表述基本合规但存在优化空间。比如工资于每月15日发放法律没规定具体日期但建议明确遇节假日提前发放。无风险标准合规条款无需修改。这套规则我写成了一个配置表每条规则对应一个正则或关键词匹配。模型在分析时我会把命中的规则作为提示放进Prompt引导它往正确的方向判断。这样既利用了模型的语义理解能力又保证了判定的稳定性。风险等级判定依据典型条款处理建议高违反强制性规定竞业限制超2年条款无效必须修改中显失公平或表述模糊单方调岗条款建议协商明确低合规但可优化工资发放日期可选优化无标准合规标准工时制无需处理5.3 输出格式的稳定性控制LLM输出JSON是个老大难问题。即使你在Prompt里明确要求只输出JSON模型还是可能给你加一段好的以下是分析结果的前缀或者在JSON外面包一层markdown代码块。我的处理是三层保险。第一层Prompt里用few-shot示例给一两个输入输出对让模型模仿格式。第二层用正则从模型输出里提取JSON部分忽略前后缀。第三层如果提取失败用json.loads尝试解析失败就触发重试重试时把错误信息也放进Prompt让模型纠正。import json import re def parse_llm_output(text): # 尝试直接解析 try: return json.loads(text) except json.JSONDecodeError: pass # 尝试提取代码块中的JSON match re.search(r(?:json)?\s*(\{.*?\})\s*, text, re.DOTALL) if match: try: return json.loads(match.group(1)) except json.JSONDecodeError: pass # 尝试提取第一个完整的JSON对象 match re.search(r\{.*\}, text, re.DOTALL) if match: try: return json.loads(match.group(0)) except json.JSONDecodeError: pass return None实测下来这套组合拳能把JSON解析成功率从70%提升到98%以上。剩下2%的失败案例系统会标记为需人工复核而不是直接报错。注意不要用eval()来解析LLM输出哪怕你觉得内容可控。LLM输出本质上是不可信输入用eval等于开了个后门。json.loads是安全的解析失败就重试不要图省事。6. 实测中的性能瓶颈与优化手段6.1 并发处理从三分钟到三十秒前面提到串行处理25个条款要两三分钟。用户等这么久体验很差。我的优化思路是并发。但并发不是无脑开线程。LLM API通常有速率限制开太多并发会被限流。我的做法是用线程池池大小设为5到8根据API的QPS限制调整。每个条款的处理是一个独立任务提交到线程池用concurrent.futures收集结果。from concurrent.futures import ThreadPoolExecutor, as_completed def review_contract(clauses): results [] with ThreadPoolExecutor(max_workers6) as executor: future_to_clause { executor.submit(review_single_clause, c): c for c in clauses } for future in as_completed(future_to_clause): clause future_to_clause[future] try: result future.result(timeout30) results.append(result) except Exception as e: results.append({clause: clause, error: str(e)}) return results这里有个细节as_completed返回的顺序是不确定的所以每个结果里必须带上原始条款的标识最后按原始顺序重新排列否则报告里的条款顺序会乱。并发之后一份合同的处理时间从三分钟降到了三十秒左右。瓶颈从LLM调用转移到了嵌入计算因为嵌入模型是本地跑的GPU资源有限。如果嵌入也变成瓶颈可以考虑把嵌入也做成批量推理一次算多个条款的向量。6.2 缓存避免重复劳动法规库是固定的同一批法规条文会被反复检索。每次检索都重新计算嵌入向量是浪费。我的做法是在系统启动时把法规库所有条文预计算好向量存进Chroma。检索时直接用查询向量去比对不需要重新编码法规。另外如果用户上传的是同一份合同通过文件哈希判断可以直接返回上次的审查结果不需要重新跑一遍。这个缓存在实际使用中命中率不低因为很多人会反复上传同一份合同做对比。6.3 超时与降级策略LLM调用偶尔会超时或返回错误。我的策略是单个条款的处理超时设为30秒超时就标记为分析超时建议人工复核不阻塞整个报告生成。如果超过一半的条款都超时说明API可能出问题了系统会返回一个服务暂时不可用的提示而不是给用户一份残缺的报告。这种降级策略的核心思想是宁可给用户一个部分完成的明确状态也不要给一个看起来完成了但实际有问题的结果。劳动合同审查涉及法律权益误导比不分析更糟糕。7. 这套系统的能力边界与我的实操心得7.1 它不能替代律师但能帮你省下第一轮筛选必须说清楚这个系统不是律师的替代品。它能做的是初筛——把明显有问题的条款标出来把合规的条款放过去让律师或法务只需要聚焦在那几个高风险点上。这能省下大量时间但最终的法律判断还是得由专业人士来做。我实测过几十份真实合同系统对硬性违法条款的识别准确率很高比如试用期超长、竞业限制超期、违约金约定违法这些基本不会漏。但对软性风险的判断就没那么准了比如甲方可根据经营需要调整工作岗位这种条款到底算不算风险不同地区的裁审口径不一样系统只能给出存在争议的提示没法给出确定结论。7.2 法规库的维护是长期工作RAG系统的效果一半取决于模型一半取决于知识库。法规库不是建一次就完事的法律法规会修订新的司法解释会出台地方裁审口径会变化。我现在的做法是每季度更新一次法规库更新时重新跑一遍嵌入和索引。更新法规库有个坑新旧法规的冲突。比如某条法规被修订了旧版本还在库里检索时可能同时返回新旧两个版本模型就懵了。我的处理是给每条法规加一个生效日期和失效日期字段检索时过滤掉已失效的条文。这个字段在入库时就要维护好不能等出问题了再补。7.3 给想复现这套系统的朋友几个实在建议第一先从小的法规库开始。不要一上来就想把所有的劳动法规、司法解释、地方纪要都塞进去。先放《劳动合同法》和《劳动法》这两部核心法律把链路跑通再逐步扩充。知识库越大检索噪声越多调优越难。第二嵌入模型和LLM可以分开选。嵌入模型用本地的BGE保证数据不出内网LLM可以用API因为生成质量对API的依赖更大。两者解耦各自替换不影响对方。第三一定要做人工评估。系统跑出来的结果找几份真实合同人工核对一遍看看哪些判对了、哪些判错了、哪些漏判了。这个评估过程比任何自动化指标都重要它能告诉你系统的真实短板在哪里。第四前端体验别忽视。用户上传合同后如果界面一直转圈没有任何反馈他会以为系统卡死了。加一个进度条显示正在解析文档、正在检索法规、正在分析第8/25条体验完全不一样。这个改动成本很低但用户感知很强。我在实际部署中发现真正影响用户是否愿意用这套系统的不是模型有多强而是它能不能在我等得起的的时间内给我一个我能看懂的结论。技术再先进如果输出是一堆专业术语堆砌的散文用户还是不会用。把风险等级、修改建议、法律依据这三样东西清晰地呈现出来比追求模型参数量重要得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑