开发了一个能自主查法规的合同审查 Agent,踩了 5 个深坑

0 阅读10分钟

一个问题:能不能让 AI 来做合同审查?不是那种"丢给ChatGPT帮我看一下"的粗糙版本,而是真正能查法规、找判例、看地方政策、算税务影响的专业审查。 于是有了"法眼"这个项目。三个月,从零搭了一个基于 ReAct Agent 的合同审查系统,覆盖 6 种合同类型、10 个工具、4 重知识库。这篇文章是完整的复盘——重点不在"我做了什么",在我踩过的坑和踩完之后的理解。

1.png 一、架构:从三步流水线到 ReAct Agent 1.0 最开始的版本 最简版本就三步: 合同全文 → 提取条款 → 风险分析 → 生成报告 每一步调一次 LLM,输出直接喂给下一步。能用,但有致命问题:只要某一步出错,整个流程崩掉。比如提取条款漏了一条关键内容,后面的分析就永远覆盖不到。

2.0 重构:ReAct Agent 受 Anthropic 和 OpenAI 的 Agent 模式启发,我重构成了 ReAct(Reasoning + Acting)循环。核心变化是——AI 自主决定每一步做什么: 第1轮:思考 → 调用 extract_clauses → 观察结果 第2轮:思考 → 调用 search_regulation("押金") → 观察结果 第3轮:思考 → 调用 analyze_single_clause → 观察结果 ... 第N轮:思考 → 调用 generate_final_report → 输出报告 上限 20 轮,实际一次完整审查通常在 12-15 轮。最关键的改变不是效率,是可追溯——每一轮调了什么工具、传了什么参数、返回了什么结果,全部可见。

11 个工具的设计:

  1. extract_clauses — 从合同全文提取结构化条款(10个类别)
  2. search_regulation — 检索内置法规库(民法典 + 司法解释 + 行政法规)
  3. search_case_law — 查找类似判例,了解法院怎么判
  4. check_local_policy — 查询5城地方政策(北京/上海/深圳/广州/成都)
  5. lookup_tax_rule — 查询税务规则(增值税/个税/印花税/租金专票)
  6. web_search — 联网搜索最新法规动态
  7. analyze_single_clause — 逐条三维评分(公平性/明确性/风险敞口)
  8. check_completeness — 检查合同缺了什么必要条款
  9. switch_perspective — 切换到对立视角再审视一遍 10.generate_final_report — 汇总五段式审查报告 11.self_reflection — 全局质量审核(一致性/覆盖性/评分合规检查)

二、知识库:不只是查法规 很多"AI合同审查"工具就做一件事——把合同丢给GPT,问"有什么风险"。三个致命问题: 第一,LLM 会编造法条(幻觉)。你没给它真实法规文本,它就自己编。第二,LLM 不知道最新的司法解释和地方法规。第三,LLM 不会考虑地方差异——北京的租赁政策和深圳完全不同。 法眼的解法是四重知识体系内置化——让 Agent 在分析前必须先查资料: 法规原文:民法典 + 劳动合同法 + 司法解释 + 行政法规 法院判例:押金纠纷 / 过高违约金 / 试用期违法 / 竞业限制 / 砍头息等52个判例要点 地方政策:北京 / 上海 / 深圳 / 广州 / 成都(备案、押金监管、税率差异) 税务规则:增值税 / 个税 / 印花税 / 租金专票 / 技术合同免税

Agent 分析条款时的逻辑链是:先查法规 → 对比判例 → 参考地方政策 → 算税务影响 → 再下结论。每一步检索都有原始资料支撑,不是模型自己编的。

检索技术栈是 RAG 混合检索:向量语义(ChromaDB cosine)+ 关键词全文扫描(字符覆盖度评分)+ RRF 融合去重。两路并行的好处是——向量路召回语义相近但字面不同的内容,关键词路兜底精确匹配,融合后取加权最佳。全链路从分块、嵌入、索引、检索到格式化都自己搭的,不是调一个 API 就完事。

2.png 三、踩坑实录:5个让我怀疑人生的Bug 这部分是本文最有价值的内容。每一个坑都花了大量时间排查和迭代。 坑1:qwen-plus 的 JSON 地狱(8轮迭代) 现象:模型返回 400 InternalError.Algo.InvalidParameter。 根因:qwen-plus 在 Function Calling 的 arguments 字段里生成非法 JSON——单引号代替双引号、中文引号未转义、尾部多余逗号。DashScope 服务端直接拒绝,错误甚至没传到我的代码里。 尝试过的解法(8轮): × 修复重试逻辑 → 重试照样失败 × 客户端 JSON 修复 → 修不了,请求在服务端就被拦截了 × 精简系统提示词 → 有改善但不稳定 × 纯文本兜底 → 报告质量缩水严重 最终方案:文本格式工具调用——用 <TOOL:name> 和 <ARGS:json> 标签绕过 DashScope 的 Function Calling 校验 + 会话存储规避大段 JSON 传参。 坑2:DeepSeek 的 Markdown 执念 切到 deepseek-v3.2 试了试——JSON 格式完美,但报告永远输出 Markdown(各种 # ## ** |)。报告模板明确要求纯文本(很多法务系统不支持 Markdown 渲染),改了 3 版提示词都拦不住。 最终切回 qwen-plus。教训:选模型不是看能力参数,是看它在你的具体任务上的"听话程度"。 坑3:提示词"铁律"污染报告 我在报告模板底部加了一行内部约束("禁止Markdown、禁止表格……"),结果模型把这行内部约束逐字输出到了用户报告里。 解决方案:把铁律移到 System Prompt 里,报告模板只留结构性模板。System Prompt 和 User Prompt 的作用域是两回事,这个坑让我彻底理解了它们的区别。 坑4:Agent自己"总结"覆盖工具结果 Agent 调用 generate_final_report 产生了完整的五段式报告,但模型在下一轮文本回复里又自己总结了一遍,导致最终输出是缩水版。 修复:last_report 无条件优先——只要 generate_final_report 执行过,最终输出一定是工具结果而不是模型回复。这是 Agent 工程里典型的边界处理。 坑5:13种合同类型回退到6种 一度把合同类型从5种扩展到13种,结果 qwen-plus 的 JSON 错误暴增。 原因很简单:系统提示词越长,模型越容易在格式上出错。最终回退到6种(加回借款合同),但每种的法条红线反而写得更细更准。 教训:少即是多。与其覆盖13种合同但每种审查粗糙,不如精选6种高频类型做到极致。

四、报告质量:那些看不见的打磨 生成报告看似简单,实际上花了很多功夫在细节上: · 去重逻辑:同一条款同时触犯多个风险点,只在最高风险段列出,其余段不列也不写"此处同上" · 评分自洽:综合风险评分必须与高/中风险条数成比例,不能出现"高风险条数多但综合分低" · 格式强约束:五段式纯文本模板,禁止表格、禁止Markdown、禁止免责声明 · 条款拆分:同一编号条款有多个独立风险点(如"扣除直接成本后分成,且可单方调整比例"),必须拆成多次独立分析

一份好的审查报告的标准不是"AI写得多",是"关键信息不遗漏,关键信息不重复"。

image_649013997562383.png 五、技术栈 模型层:阿里云百炼 qwen-plus(OpenAI兼容API) OCR:qwen-vl-plus(合同照片→文字) Agent层:ReAct循环(20轮上限)+ 11个工具 + Self-Reflection反思 + LangGraph版 + SSE流式输出 RAG层:向量语义 + 关键词全文扫描 + RRF融合(ChromaDB) Web层:Streamlit(Legal Editorial风格)+ PDF/DOCX原生解析 协议层:MCP 标准化工具接口(可被Claude Code等调用) 工程化:Docker容器化 + pre-commit自动质量检查 + 冒烟测试22个 兜底:JSON修复 + 文本格式工具调用 + 会话存储 + JSON错误三级兜底

六、LangGraph 改写:从手写到图编排 Agent 循环写完之后思考了一个问题: LangGraph,与手写的 ReAct 的区别在哪里? 于是我花了半天,用 LangGraph 自定义 StateGraph 把法眼的 Agent 循环完整重写了一遍。 三个核心概念: LangGraph 的抽象就三层:State(共享状态)、Node(处理节点)、Edge(流程边)。原来手写的 200 行 for/if 循环,用这三个概念重新组织: [START] → [Agent节点: LLM思考] ←→ [Tools节点: 执行工具] ↓(无工具调用, 结束) [END] Agent 节点每轮调用 LLM 决定下一步,Tools 节点执行具体工具并返回结果,条件边自动判断"继续调工具"还是"结束审查"。 改写结果: 维度 | 手写版 | LangGraph版| 流程编排代码 | ~200行 | ~50行| qwen JSON修复 | 保留 | 保留(一模一样) | | 文本模式兜底 | 保留 | 保留(一模一样) | | API重试 | 保留 | 保留(一模一样) | | 图可视化 | 无 | draw_mermaid_png() 输出决策拓扑图 | | 审查输出 | 基准 | 完全一致(同 temperature=0.0)| 现在项目代码里 main.py和 agent_langgraph.py两个版本并存,随时可对比——这就是最好的代码说服力。

七、持续打磨:四项工程化 + 两项技术深度改进 1.流式输出 原来的审查 UI 是轮询模式——每 0.5 秒检查一次后台日志,推算"大概进展到第几轮了"。改成了真正的 SSE 流式:LLMClient.stream_chat()→ Agent.run_stream()→ StreamingReviewRunner(Queue)→ 前端 for-event-loop。模型思考内容逐字流式出现,工具调用日志实时刷新。整条链路从 StringIO 轮询重构成了 Queue 事件驱动,但保留了原有的 JSON 修复、文本兜底、API 重试机制。 2.PDF/DOCX 原生解析 之前只支持 .txt 和图片 OCR。用 pypdf+ python-docx加了原生 PDF 和 DOCX 文本提取,90% 的合同文件格式都覆盖了。扫描件 PDF 仍然走 OCR,普通文档 PDF 直接提取文本。 3.RAG 混合检索重构 原来是"向量检索 → 失败则 key 名关键词匹配"。重构为真正的混合检索:向量路 + 关键词全文扫描路并行,RRF(Reciprocal Rank Fusion)融合去重后取 Top-K。关键词回退也从只匹配 dict key 改成了全文字符覆盖度评分,彻底解决了"搜民法典第585条找不到任何结果"的问题。 4.Self-Reflection 反思机制 受 Anthropic 的 Constitutional AI 启发,在 generate_final_report 之前插入了一个 self_reflection质量审核节点。检查一致性(同条款是否被给不同评级)、覆盖性(是否所有提取的条款都分析了)、评分合规性(三维度规则是否严格遵循)。如果审核不通过,Agent 会回到分析阶段修正,最多三轮反思后强制出报告。用时间换质量——一次审查原本约 4 分钟,加上反思后约 4.5-5 分钟。 5.Docker + pre-commit 工程化 加了 Dockerfile+ docker-compose.yml,支持 docker compose up -d 一键部署 Web + API 双服务。.env.example模板让新开发者复制即用。pre-commit hook 配置了 ruff 自动格式化 + lint,每次 git commit自动检查代码质量。 这 5项改动全部推到了 GitHub,22 个冒烟测试 4 秒全过,集成测试验证了报告完整性。

八、开源部署 项目已开源:github.com/Nakoem/faya… 接下来计划:

  1. 审查报告质量评估体系(Recall@K、MRR等指标)
  2. Agent决策流程的可视化监控
    1. 支持更多合同类型的插件化扩展 #AI应用 #Agent #合同审查 #开源项目