基于Gemini API的合同风险审查与法律AI工程实现
Google在AI应用层推出的Gemini Enterprise for Legal把Gemini大模型能力收拢到企业法务场景核心目标是自动化合同分析和法律研究。法律行业的信息密度高、术语严谨、容错率低通用聊天模式很难直接落地需要一套面向法务工作流的工程方案。这篇文章不讨论产品宣传只从技术实现角度拆解法律AI产品背后的能力模块并用Gemini API写一个最小可运行的合同风险审查工程然后说明法律研究场景中RAG检索增强、权限隔离、幻觉控制和审计日志的设计思路。适合正在做法律科技产品、法务信息化或者想把大模型引入合同审查流程的开发者阅读。1. 法律行业为什么需要专门面向法务场景的企业级AI1.1 通用大模型与企业法律版AI的差异通用大模型擅长对话、总结、翻译但法务场景要求的不只是流畅回答还包括结构化的合同条款、可溯源的法规依据、严格的权限控制和完整的操作审计。一个律师使用法律AI时系统不仅要给出答案还要告诉他答案从哪里来、凭什么这么判断、哪个版本的模型输出这段内容。Gemini Enterprise for Legal 这类产品本质上是在Gemini模型基础上叠加了企业级安全能力、法律场景工作流和行业知识库。与通用Gemini对话相比差异主要体现在几个维。对比维度通用Gemini面向法律的企业版方案输出目标自然语言对话结构化字段、条款级风险、引用来源知识来源模型参数记忆企业法规库、案例库、合同库增强权限控制按账号控制按租户、组织、合同库细分隔离审计要求普通日志提问人、合同ID、模型版本、审批记录适用场景写作、问答、多模态理解合同审查、法律研究、条款比对这个表格说明一个关键判断法律AI不只是换一套提示词而是要在模型外面加工程约束。模型负责语义理解系统负责流程可控。1.2 合同自动化和法律研究到底在解决什么企业法务团队的工作量大头集中在两类重复劳动。第一类是合同审查。一份合同从业务部门提交到法务审核涉及主体信息核对、金额条款确认、付款节点检查、违约责任评估、保密条款判断等。常规合同可能有几十个条款人工逐条读一遍需要数十分钟而且不同律师的判断标准还不一致。法律AI可以先做初步筛选和标记把高风险条款识别出来让律师把精力集中在真正需要判断的地方。第二类是法律研究。律师遇到一个具体问题时要在法规库、判例库、监管文件里找相关依据可能要翻阅几十份文档才能定位到几个关键条款。检索增强生成可以把“问题—检索—引用—回答”串起来让模型基于限定范围的文档作答避免凭记忆编造法条。Gemini Enterprise for Legal的产品化价值就是压缩这两类工作的耗时。对技术团队来说需要理解的是背后的四层技术链路长文档理解、信息抽取、检索增强、结果约束。1.3 法律AI落地的风险边界必须明确一点法律AI的输出不等于正式法律意见。合同审查涉及金额、违约责任、争议解决条款这些直接影响企业经济利益。模型可能漏掉某个条款也可能判断错风险等级因此人工复核环节不能省。落地时还要考虑律师保密义务和数据主权。合同文本、客户名称、交易金额都属于敏感数据不能随意进入公共模型上下文。企业版方案通常支持私有化部署或租户隔离但技术团队仍要在架构上做数据分级并在产品流程中强制加入审阅节点。注意法律AI生产环境必须保留人在回路的机制模型只能做预审和辅助最终审批权始终在法务人员手中。2. 拆解法律AI的技术主线理解、抽取、检索、约束2.1 第一层是长文档理解合同通常以PDF或Word形式存在可能是扫描件也可能有表格、页眉页脚、盖章。直接让模型读二进制文件不现实工程上要先做文档解析把PDF解析成纯文本再分块交给模型。Gemini模型本身支持多模态输入但在合同审查场景里建议先走文本抽取流程。原因是文本抽取后的内容可以记录、检索、审计也便于做不同模型之间的切换。扫描件需要先做OCR这一层如果做得不好后续抽取准确率会明显下降。from pypdf import PdfReader def extract_contract_text(pdf_path: str) - str: reader PdfReader(pdf_path) pages [page.extract_text() or for page in reader.pages] return \n.join(pages)这段代码先把PDF每一页转成字符串再用换行拼接。实际项目里会遇到扫描PDF没有文本层的情况这时要先用OCR服务识别再进入抽取流程。解析完成后建议把文本按合同唯一ID存入存储层方便后续多次调用避免每次审查都重复解析。2.2 第二层是信息抽取与结构化合同文本是自由格式同一个含义可能有不同表达方式模型的价值就在于把非结构化文本映射成结构化字段。常见的抽取目标包括合同主体、合同金额、付款条件、保密条款、违约责任、管辖法院、合同期限等。产品设计上不要只让模型输出一段总结而是要求它输出JSON。结构化输出便于下游系统写入工单、生成审查报告、标记高风险合同。官方文档中Gemini API支持通过response_mime_type指定JSON输出部分较新的模型还支持response_schema定义输出结构这对合同字段稳定抽取很有帮助。结构化输出的稳定性直接决定产品可用性。如果模型偶尔输出多余文字JSON解析就会失败导致流程中断。因此代码里必须有容错处理。2.3 第三层是检索增强生成法律研究场景不适合让模型凭记忆回答。原因有二一是模型训练数据有截止时间新法规、新司法解释覆盖不到二是即使模型答对了也没有可验证的来源律师无法判断依据是否可靠。检索增强生成的标准链路是把法规库和案例库切分为文本块离线做向量化用户提问后先从向量库检索相关块再把问题与检索结果一起交给模型要求模型只基于检索内容回答并标注引用来源。这样做的好处是回答有出处、知识库可更新、权限可按文档粒度控制。后面会单独用一个章节展开。2.4 第四层是结果约束与人工闭环模型输出之后还需要经过一道工程约束层包括JSON schema校验、风险等级映射、敏感信息检查、人工审批状态流转。最终写入合同审查系统的不只是模型输出还要附加模型版本、提示词版本、调用时间、审核人员等元数据。这套链路在最小Demo里可以是一个脚本但在企业产品中往往拆成多个服务。对技术团队来说最忌讳一开始就把所有能力耦合在一个函数里。3. 基于Gemini API实现合同风险审查最小可运行工程3.1 环境准备与依赖开始前先明确要使用Google Cloud上的Vertex AI和Gemini API因此需要准备一个可用的Google Cloud项目并在项目中启用Vertex AI服务。实际可用区域以账号权限和官方支持列表为准落地前要先确认本企业的网络策略和数据合规要求。环境项示例值说明Python版本3.10推荐3.10以上版本云项目your-gcp-project-idVertex AI所在项目区域us-central1模型可用区域以实际开通为准模型名称gemini-2.0-flash以账号可用模型列表为准Python包google-cloud-aiplatformGoogle Cloud AI平台SDK安装依赖pip install google-cloud-aiplatform pypdf注意模型名称在不同时间段、不同账号下可能不同代码里写的模型名只是示例。运行时如果报模型不存在要先去Vertex AI模型广场确认可用模型列表。3.2 初始化模型客户端初始化Vertex AI客户端并配置生成参数。关键参数是temperature法律场景建议设置较低的值比如0.2减少随机性让输出更稳定。response_mime_type设置为application/json要求模型按JSON格式回答。import vertexai from vertexai.generative_models import GenerativeModel, GenerationConfig PROJECT_ID your-gcp-project-id LOCATION us-central1 vertexai.init(projectPROJECT_ID, locationLOCATION) model GenerativeModel( gemini-2.0-flash, system_instruction[ 你是一名具有多年合同审查经验的法务助理。 你的任务是从用户提供的合同文本中提取关键条款并标记风险。 只能输出JSON不要输出多余解释。 ], ) generation_config GenerationConfig( temperature0.2, response_mime_typeapplication/json, )这样配置后每次调用模型都会使用同一套系统提示词不需要在每次请求里重复写。temperature越低输出越倾向于确定性结果适合合同审查、法律条文抽取这类要求严谨的任务。3.3 合同文本解析与分词块业务侧的合同以PDF居多先用pypdf完成文本抽取。如果合同很长超过模型上下文窗口需要先做切分。切分时不要按固定字符数硬切尽量按“合同编号”“第X条”“第X章”等结构边界切避免把一个条款从中间断开。def split_contract_text(text: str, max_len: int 4000): lines text.split(\n) chunks [] current [] current_len 0 for line in lines: if current_len len(line) max_len and current: chunks.append(\n.join(current)) current [] current_len 0 current.append(line) current_len len(line) 1 if current: chunks.append(\n.join(current)) return chunks这是一个按行累积、超过阈值就断开的分块函数。真实合同建议用条款标题作为切分锚点例如看到“第一条”“第二条”或“Chapter One”时开启新块这样能避免模型在抽取时错乱上下文。3.4 提示词模板与条款抽取合同抽取的提示词要非常明确。不仅要说“提取风险条款”还要告诉模型输出哪些字段、风险等级如何划分、JSON结构是什么样的。最好的做法是把JSON结构直接写在提示词里配合response_mime_type双保险。contract_review_prompt 请审查以下合同文本并输出JSON。 JSON字段定义 { contract_name: 合同名称或编号, parties: [甲方名称, 乙方名称], amount: 合同总金额若未明确则填null, payment_terms: 付款方式与时间节点的摘要, risks: [ { clause_title: 风险条款名称, risk_level: high | medium | low, description: 风险描述, suggestion: 修改建议 } ], missing_clauses: [建议补充但合同中缺失的条款名称] } 要求 1. 只输出JSON不要输出说明文字。 2. risk_level为high时必须属于重大利益条款、违约责任、争议解决或单方解除权。 3. 如果原文没有明确内容对应字段使用null或空数组。 4. 不要编造合同中不存在的条款。 合同文本 {contract_text} 提示词里写清楚了评分标准和字段结构模型输出的一致性会明显提升。特别是“不要编造合同中不存在的条款”这句话可以降低补充条款时的幻觉概率。3.5 调用模型并解析JSON调用模型时把提示词与合同文本一起传入。API返回后先去除可能的Markdown代码围栏再用json.loads解析。如果解析失败要有重试或降级策略。import json response model.generate_content( contract_review_prompt.format(contract_textcontract_text), generation_configgeneration_config, ) raw response.text.strip() if raw.startswith(json): raw raw.removeprefix(json).removesuffix().strip() try: result json.loads(raw) except json.JSONDecodeError as e: print(JSON解析失败:, e) print(原始输出:, raw) result None这里要强调generate_content返回的是模型生成文本不一定保证是纯JSON所以解析前要处理代码围栏和多余空白。JSON解析失败是合同审查工程最常见的错误之一建议把原始输出写入日志便于定位问题是出在提示词还是模型。3.6 运行验证与预期输出用一段模拟合同文本发起调用预期输出结构类似下面这样。以下仅为示意输出真实运行时输出内容会随合同文本和模型版本变化。{ contract_name: 软件开发服务合同, parties: [北京某科技有限公司, 上海某信息技术有限公司], amount: 人民币500000元, payment_terms: 合同签订后支付30%验收合格后支付70%, risks: [ { clause_title: 违约赔偿责任, risk_level: high, description: 违约金上限为合同总额的50%比例偏高, suggestion: 建议将违约金上限调整为合同总额的30%或按实际损失计算 }, { clause_title: 保密条款, risk_level: medium, description: 保密期限未明确约定, suggestion: 建议补充保密期限及保密信息返还或销毁要求 } ], missing_clauses: [知识产权归属条款] }验证时不要只看模型能不能返回JSON还要检查字段是否齐全、风险等级是否符合业务定义。可以把同一份合同跑多次观察输出是否稳定再用这个结果作为提示词优化的起点。4. 法律研究场景里的检索增强实现4.1 为什么法律研究不能只靠模型生成法律研究的本质是查找依据而不是让模型编造答案。模型在训练时形成的参数记忆无法保证覆盖最新法规也无法为每个答案提供页码和条文号。没有检索增强的问答系统在法律场景几乎不具备生产价值。检索增强生成的核心思路是把知识外置。回答时模型参考的不是自己的记忆而是事先构建好的法规库、案例库、内部知识库。这样做有三个直接好处回答可溯源、知识库可在不重新训练模型的情况下更新、权限控制可以精确到文档块。4.2 文档切分与向量化法规和判决书的结构与普通文章不同切分策略不能一刀切。以法规为例更好的方式是先解析章节层级再按“第X条”切块。判决书可以按“原告诉称”“被告辩称”“本院认为”“裁判结果”等模块切分。import re def split_regulation_by_article(text: str): pattern re.compile(r(第[一二三四五六七八九十百千0-9]条)) parts pattern.split(text) chunks [] for i in range(1, len(parts), 2): title parts[i] body parts[i1] if i1 len(parts) else chunks.append(title body.strip()) return chunks切块后需要对每个块做向量化并保存文档来源、标题、条文序号、所属法规等元数据。法律检索场景建议至少保存三样元数据法规名称、条文编号、来源文档ID。后续展示引用时才能告诉用户“这条结论来自哪部法规的第几条”。4.3 检索、重排序与答案生成检索阶段可以分两步。第一步用向量检索召回相关度高的文本块第二步用关键词或额外的重排序模型对召回结果排序。法律术语往往有固定表达方式比如“甲方”“违约金”“不可抗力”向量检索可能漏掉精确匹配的内容混合检索更适合。查询时最容易被忽略的是权限过滤。不同法务团队、不同客户可能只能访问各自的合同库和法规子集。向量检索必须把租户ID作为过滤条件写入查询参数否则会出现跨租户数据泄露。results vector_store.similarity_search( queryquery, k10, filter{ tenant_id: current_user.tenant_id, doc_type: regulation, }, )检索完成后把问题和检索到的文本块拼进提示词要求模型只依据给定文本回答并在答案中标注引用来源。如果检索结果为空模型必须回答“未检索到相关依据”不能自己编造。4.4 引用溯源与答案约束为了让引用可核对提示词里要给模型明确的输出格式例如要求回答中包含“【依据】”字段。最终结果还要经过后处理提取引用来源ID与合同审查结果一起写入审计日志。legal_research_prompt 你是法律研究助手。请根据参考文档回答问题。 参考文档 {context} 问题{question} 要求 1. 只依据参考文档回答不要使用自己的记忆。 2. 回答末尾列出依据来源格式为[source_id文档ID, 法规名, 条文号]。 3. 如果参考文档中未找到相关内容直接回答“未检索到相关依据”。 4. 不要编造法规名称和条文编号。 法律研究的成败不在模型本身而在知识库质量和检索质量。如果库里本身没有对应法规再强的模型也答不出来。因此知识库的更新机制比提示词更重要。5. 工程化落地中的权限、审计、幻觉与数据安全5.1 企业法务场景的权限隔离合同和法律文档通常按组织和项目隔离。产品层要有清晰的租户模型例如企业租户、部门、合同库三级结构。模型调用、知识检索、合同文本读取都必须带上租户上下文。向量存储的filter参数只是最后一道闸真正的数据隔离应在文档入库时就完成。给每个文档块写入tenant_id和acl_group字段检索时强制携带这些条件。同时不要把账户体系做在应用层就认为安全倒排索引和分片层面的隔离也要评估。层级隔离内容实现方式文档入库文本块打租户标签元数据字段检索查询过滤条件强制携带向量库filter模型调用账号级凭证与配额IAM角色控制应用接口按合同库授权访问RBAC权限模型5.2 幻觉控制与置信度提示合同条款抽取中模型可能在字段缺失时“脑补”一个值。工程手段是强制模型在不确定时返回null而不是猜测。可以在提示词里反复声明同时在系统提示中明确“不确定就填null”。更稳妥的做法是给每条风险建议增加confidence字段由法务人员根据经验判断是否采纳。这个字段既可以是模型自己给出的低置信度值也可以是系统规则映射。例如风险等级为high但模型描述含糊时把该风险强制标记为“待人工重点审核”。5.3 审计日志与模型版本管理法律AI若引发争议第一个被问到的就是“这个结论是哪个版本的模型得出的”。因此审计日志不能只记录答案还要记录完整的调用上下文。审计字段示例用途user_idzhang.lawyer追踪提问人contract_idCT20250312001关联具体合同model_versiongemini-2.0-flash-20250311模型版本核对prompt_versionlegal-review-v3提示词版本核对raw_output模型原始结果留证与问题分析reviewer_idli.lawyer人工复核人review_statusapproved审批状态凡是进入法律流程的结果建议把原始模型输出和最终人工审批结果都保存为不可变记录。这里所说的不可变不是指数据库不能删而是指系统在逻辑上不做静默覆盖。5.4 学习环境与生产环境的差异学习环境跑通一个脚本很快生产环境则要补齐多块短板。生产系统至少要考虑模型版本切换、异常重试、配额管理、敏感数据脱敏和回滚预案。模型版本升级不能直接全量切换应该先跑并行评审用历史合同的抽取结果做对比评估。合规层面涉及个人数据或商业秘密的内容要确认数据是否允许进入云端模型服务。如果企业合规不允许就需要评估私有化部署或专有云方案。不要等技术团队做完再补合规这个环节应该在项目启动前确定。6. 常见问题与排查路径6.1 JSON输出解析失败问题现象可能原因检查方式处理建议json.loads抛异常模型输出包含Markdown围栏或解释文字打印原始输出查看前后缀先去除围栏再做解析若仍失败重试一次字段缺失或为null提示词字段定义不够明确对比输出与JSON schema在提示词中补充枚举值和必填字段说明解析失败是所有大模型结构化场景的第一道坑。不要一上来就改模型参数先确认提示词里的JSON示例是否足够清楚再确认response_mime_type是否生效。6.2 合同文本超过上下文窗口长合同动辄几百页超出模型上下文窗口后直接报错或效果下降。处理方式不是硬截断而是先按条款结构切块再分段抽取最后汇总。分段抽取时要设计好重叠边界避免条款标题与正文被切到不同块里。6.3 模型调用返回权限或区域错误大模型服务是按区域部署的如果账号所在区域没有开通对应模型调用会返回PERMISSION_DENIED或模型不存在错误。这类问题通常与账号权限、项目配额、服务启用状态有关不要只盯着代码改参数要按这个顺序排查项目是否正确、服务是否启用、区域是否支持、账号是否有权限、配额是否充足。注意如果团队所在地区对特定云服务有合规限制项目启动前就要评估替代方案不要在运行期再处理访问问题。6.4 法律术语检索不准向量检索对语义近似的文本有效但法律术语有很强的精确性要求。“违约金”和“违约损害赔偿”在语义上接近但在特定场景下有区别。解决办法是维护法律同义词典在查询前做查询改写把用户口语化问题映射为规范法律术语再做检索召回。6.5 长文档中风险条款漏抽模型抽取时可能只关注最显眼的条款忽略隐藏在合同附件的风险。应对策略是把常见风险条款做成检查清单在提示词里让模型逐项确认例如是否包含知识产权、保密、竞业限制、自动续约、争议解决等条款。这样从“自由发现风险”变成“按清单确认风险”漏抽概率会下降。7. 最佳实践与可复用上线检查清单合同审查和法律研究的AI化不建议一开始就追求端到端全自动。最稳妥的路径是先把人工流程做成半自动让模型完成初筛律师做复核再用复核数据反哺提示词和检索策略。7.1 落地路线建议第一步先选一个高频、低风险的场景比如“采购合同的风险预审”把PDF解析、条款抽取、风险标记、人工复核跑通。第二步扩充条款类型和风险规则把法务的判断标准沉淀到提示词和评测集里。第三步再考虑法律研究、合同谈判辅助、多语言合同等高阶场景。7.2 上线前可复用检查清单[ ] 合同文本解析是否覆盖PDF扫描件、图片、表格等格式[ ] 抽取字段是否满足下游审批系统的最小数据要求[ ] 提示词是否明确“不确定时填null”和“不编造条文”[ ] JSON解析失败是否有重试和日志上报[ ] 向量库检索是否强制携带租户过滤条件[ ] 审计日志是否包含用户、合同ID、模型版本、提示词版本、原始输出[ ] 高风险条款是否强制进入人工复核[ ] 模型版本升级是否有并行评估机制[ ] 是否已确认数据合规要求和区域可用性7.3 扩展方向后续可扩展的方向至少有三个起草辅助基于合同模板和用户输入自动生成初稿谈判辅助对比甲乙双方合同版本并标出差异法规变动监测周期性拉取法规库更新内容并重新向量化确保检索结果不过期。技术团队最需要建立的一个习惯是每次合同审查结果都要收集人工修改记录。这些记录既是评测集也是提示词迭代的依据。法律AI优化到最后比的是谁能更快把律师的经验转化为可校验的规则和评测集而不是谁的模型参数更大。