合同图像审查助手

作者:鹿Sir法律合规v1

审查合同图片,识别风险条款并给出修改建议。当用户需要对合同图片进行风险识别(高风险/中风险/低风险/无风险)、判断合同类型、分析风险条款与原因、获取条款修改建议时使用。基于 Qwen3-VL + OpenVINO 本地推理,输出结构化 JSON 结果。触发词:合同审查、合同图片、风险条款、合同风险识别、合同分析。

下载量
429
点赞
104
价格
免费

技能文档

---
name: want595-contract-review-skill
description: 审查合同图片,识别风险条款并给出修改建议。当用户需要对合同图片进行风险识别(高风险/中风险/低风险/无风险)、判断合同类型、分析风险条款与原因、获取条款修改建议时使用。基于 Qwen3-VL + OpenVINO 本地推理,输出结构化 JSON 结果。触发词:合同审查、合同图片、风险条款、合同风险识别、合同分析。
title: 合同图像审查助手
category: 法律合规
---

# 合同图像审查助手

使用 OpenVINO 运行 Qwen3-VL 多模态模型,对合同图片进行自动审查:识别合同类型、评定风险等级、列出风险条款与原因,并给出修改建议,输出结构化 JSON 结果。

## 技能工作流

### 步骤1:环境前置检查

确认以下条件满足:

- 虚拟环境已激活:`source ~/modelscope-workshop/ov_workshop/bin/activate`
- 模型目录存在:`~/modelscope-workshop/lab1-multimodal-vlm/Qwen3-VL-4B-Instruct-int4-ov/`
- 模型不存在时先下载:

```python
from pathlib import Path
from modelscope import snapshot_download
model_dir = Path("~/modelscope-workshop/lab1-multimodal-vlm/Qwen3-VL-4B-Instruct-int4-ov").expanduser()
if not model_dir.exists():
    snapshot_download("snake7gun/Qwen3-VL-4B-Instruct-int4-ov", local_dir=str(model_dir))
```

### 步骤2:加载模型

```python
import os
from optimum.intel.openvino import OVModelForVisualCausalLM
from transformers import AutoProcessor

model_dir = os.path.expanduser("~/modelscope-workshop/lab1-multimodal-vlm/Qwen3-VL-4B-Instruct-int4-ov")

model = OVModelForVisualCausalLM.from_pretrained(model_dir, device="AUTO")

min_pixels = 256 * 28 * 28
max_pixels = 1280 * 28 * 28
processor = AutoProcessor.from_pretrained(
    model_dir, min_pixels=min_pixels, max_pixels=max_pixels, fix_mistral_regex=True
)
```

### 步骤3:执行合同审查推理

核心函数接收合同图片路径,输出结构化审查结果:

```python
import json
import re
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Dict, List

from PIL import Image

RISK_LEVELS = ["高风险", "中风险", "低风险", "无风险"]


def _extract_json(text: str) -> Dict:
    """优先解析模型输出中的 JSON;失败时返回空字典。"""
    text = text.strip()
    candidate = text
    match = re.search(r"\{[\s\S]*\}", text)
    if match:
        candidate = match.group(0)
    try:
        return json.loads(candidate)
    except Exception:
        return {}


def _normalize_risk(raw: str) -> str:
    """将模型输出的风险等级归一化到标准四等级。"""
    raw = (raw or "").strip()
    for r in RISK_LEVELS:
        if r in raw:
            return r

    keyword_map = {
        "高": "高风险",
        "严重": "高风险",
        "中": "中风险",
        "一般": "中风险",
        "低": "低风险",
        "轻微": "低风险",
        "无": "无风险",
        "安全": "无风险",
        "合规": "无风险",
    }
    for k, v in keyword_map.items():
        if k in raw:
            return v
    return "中风险"


def _normalize_list(raw_items) -> List[str]:
    """将模型输出的列表规范化为字符串列表。"""
    if isinstance(raw_items, list):
        return [str(item) for item in raw_items if item]
    if isinstance(raw_items, str):
        items = re.split(r"[;;\n]", raw_items)
        return [item.strip().lstrip("0123456789.、") for item in items if item.strip()]
    return []


def review_contract(image_path: Path, user_note: str = "") -> Dict:
    """合同审查入口:输入合同图片路径,输出结构化审查结果。"""
    prompt = f"""
你是一个专业的合同审查助手。请根据图片中的合同内容进行审查分析。
风险等级只能从以下四类中选择一项:{', '.join(RISK_LEVELS)}。
用户补充信息:{user_note or '无'}

请严格输出 JSON(不要额外文字),格式如下:
{{
  "contract_type": "合同类型,如劳动合同、租赁合同、买卖合同等",
  "risk_level": "高风险|中风险|低风险|无风险",
  "risk_clauses": ["风险条款1", "风险条款2"],
  "risk_reasons": ["风险原因1", "风险原因2"],
  "suggestions": ["修改建议1", "修改建议2"],
  "summary": "综合审查意见,不超过100字"
}}
""".strip()

    messages = [
        {
            "role": "user",
            "content": [
                {"type": "image", "image": str(image_path)},
                {"type": "text", "text": prompt},
            ],
        }
    ]

    inputs = processor.apply_chat_template(
        messages,
        tokenize=True,
        add_generation_prompt=True,
        return_dict=True,
        return_tensors="pt",
    )

    output_ids = model.generate(**inputs, max_new_tokens=512, do_sample=True)
    generated_trimmed = [out[len(inp):] for inp, out in zip(inputs.input_ids, output_ids)]
    text = processor.batch_decode(
        generated_trimmed,
        skip_special_tokens=True,
        clean_up_tokenization_spaces=False,
    )[0].strip()

    data = _extract_json(text)
    contract_type = str(data.get("contract_type", "未识别"))
    risk_level = _normalize_risk(str(data.get("risk_level", "")))
    risk_clauses = _normalize_list(data.get("risk_clauses", []))
    risk_reasons = _normalize_list(data.get("risk_reasons", []))
    suggestions = _normalize_list(data.get("suggestions", []))
    summary = str(data.get("summary", "暂无综合审查意见。"))

    # 若没有识别到风险条款但风险等级为高/中,补充提示
    if not risk_clauses and risk_level in ("高风险", "中风险"):
        risk_clauses = ["未识别到具体风险条款,但合同整体存在风险"]
    if not risk_reasons and risk_level in ("高风险", "中风险"):
        risk_reasons = ["建议由专业律师进一步审查"]
    if not suggestions and risk_level in ("高风险", "中风险"):
        suggestions = ["建议咨询专业律师进行详细审查"]

    return {
        "contract_type": contract_type,
        "risk_level": risk_level,
        "risk_clauses": risk_clauses,
        "risk_reasons": risk_reasons,
        "suggestions": suggestions,
        "summary": summary,
        "raw_output": text,
    }


def review_contract_image(image: Image.Image, user_note: str = "") -> Dict:
    """接受 PIL Image 对象的包装函数。"""
    if image is None:
        return {
            "contract_type": "未识别",
            "risk_level": "中风险",
            "risk_clauses": [],
            "risk_reasons": ["未上传合同图片"],
            "suggestions": ["请先上传合同图片再审查。"],
            "summary": "未上传合同,无法审查。",
            "raw_output": "",
        }

    with NamedTemporaryFile(suffix=".png", delete=False) as f:
        tmp_path = Path(f.name)
    image.save(tmp_path)

    try:
        result = review_contract(tmp_path, user_note)
    finally:
        if tmp_path.exists():
            tmp_path.unlink()

    return result
```

### 步骤4:使用与解读结果

```python
# 对本地合同图片审查
result = review_contract(Path("contract.jpg"))
print(json.dumps(result, ensure_ascii=False, indent=2))

# 带补充信息审查,结果会侧重对应条款
result = review_contract(Path("contract.jpg"), user_note="重点关注违约责任")
```

输出字段说明:

| 字段            | 类型         | 说明                                                         |
| --------------- | ------------ | ------------------------------------------------------------ |
| `contract_type` | string       | 合同类型:`劳动合同` / `租赁合同` / `买卖合同` / `服务合同` 等 |
| `risk_level`    | string       | 风险等级:`高风险` / `中风险` / `低风险` / `无风险`          |
| `risk_clauses`  | list[string] | 识别到的风险条款列表                                         |
| `risk_reasons`  | list[string] | 风险原因列表                                                 |
| `suggestions`   | list[string] | 修改建议列表                                                 |
| `summary`       | string       | 综合审查意见(不超过 100 字)                                |
| `raw_output`    | string       | 模型原始输出文本                                             |

### 步骤5:(可选)启动交互式演示

```python
import gradio as gr

RISK_EMOJI = {"高风险": "🔴", "中风险": "🟡", "低风险": "🟢", "无风险": "✅"}


def contract_agent_demo(image, user_note):
    result = review_contract_image(image, user_note)
    risk_emoji = RISK_EMOJI.get(result["risk_level"], "⚪")

    clauses_text = "\n".join(f"  - {c}" for c in result["risk_clauses"]) if result["risk_clauses"] else "  无"
    reasons_text = "\n".join(f"  - {r}" for r in result["risk_reasons"]) if result["risk_reasons"] else "  无"
    suggestions_text = "\n".join(f"  - {s}" for s in result["suggestions"]) if result["suggestions"] else "  无"

    answer = (
        f"合同类型:{result['contract_type']}\n"
        f"风险等级:{risk_emoji} {result['risk_level']}\n\n"
        f"⚠️ 风险条款:\n{clauses_text}\n\n"
        f"📋 风险原因:\n{reasons_text}\n\n"
        f"💡 修改建议:\n{suggestions_text}\n\n"
        f"📝 综合意见:{result['summary']}"
    )
    return answer, result


with gr.Blocks(title="合同审查助手", theme=gr.themes.Soft()) as demo:
    gr.Markdown("# 合同审查助手\n上传合同图片,自动识别合同类型、分析风险条款并给出修改建议。")
    with gr.Row():
        image_input = gr.Image(type="pil", label="上传合同图片")
        with gr.Column():
            note_input = gr.Textbox(
                label="补充描述(可选)",
                placeholder="例如:劳动合同、租赁合同、关注违约条款等",
                lines=3,
            )
            review_focus = gr.CheckboxGroup(
                choices=["违约责任", "付款条款", "保密条款", "竞业限制", "争议解决", "知识产权"],
                label="重点关注(可选)",
            )
            run_button = gr.Button("开始审查", variant="primary")
    text_output = gr.Textbox(label="审查报告", lines=18)
    json_output = gr.JSON(label="结构化输出")

    run_button.click(contract_agent_demo, [image_input, note_input], [text_output, json_output])

demo.launch(share=False)   # 浏览器打开 http://127.0.0.1:7860
```

## 常见错误排查

| 错误                           | 原因               | 解决方法                                                     |
| ------------------------------ | ------------------ | ------------------------------------------------------------ |
| `FileNotFoundError: model_dir` | 模型未下载         | 确认模型目录路径正确,或运行步骤1的下载代码                  |
| JSON 解析失败返回空字典        | 模型输出格式不稳定 | `risk_level` 会归一化到 `中风险`,可调大 `max_new_tokens` 重试 |
| Gradio 端口占用                | 7860 端口被占用    | `demo.launch(server_port=7861)`                              |

## 注意事项

- 审查结果仅供初步参考,高风险合同务必由专业律师复核
- 图片清晰度直接影响识别效果,建议使用分辨率足够的扫描件或照片

使用说明

# 合同图像审查助手

上传合同图片,自动识别合同类型、评定风险等级、定位风险条款并给出修改建议。

## 能做什么

- 识别合同类型:劳动合同、租赁合同、买卖合同、服务合同等
- 风险评级:高风险 / 中风险 / 低风险 / 无风险 四级
- 列出风险条款与原因,逐条给出修改建议
- 输出结构化 JSON,便于接入自动化流程

## 何时使用

- 签约前想快速判断一份合同有没有坑
- 需要批量初筛合同扫描件时
- 想聚焦某类条款(违约责任、竞业限制、保密条款等)做重点审查时

## 快速上手

对本地合同图片执行审查:

```python
result = review_contract(Path("contract.jpg"), user_note="重点关注违约责任")
print(json.dumps(result, ensure_ascii=False, indent=2))
```

环境准备(本地推理,首次使用需下载模型)见 SKILL.md 的「技能工作流」章节。

## 注意事项

- 审查结果仅供初步参考,重要合同请交由专业律师复核
- 图片越清晰,识别效果越好

如何安装此技能?

访问技能市场,点击「安装」按钮,按提示将技能包放入 AI 编程助手的 skills 目录即可。

浏览技能市场

支持平台:Qoder · QoderWork · Claude · Codex 等 AI 编程助手