拖拽式AI Agent实战:用Coze+TextIn实现跨国合同智能审查
当多语言合同审查从3小时缩短到3分钟,法务团队终于能从繁琐的重复劳动中解放出来,专注于真正的风险谈判。
一、引言:制造业跨国合同审查的现实困境
在全球化制造业务中,一份采购合同往往涉及中、英、德三种语言版本,包含技术规格、交付条款、付款条件、法律管辖等复杂内容。传统人工审查面临三大痛点:
- 时间成本高:资深法务平均3小时/份,紧急订单排队等待
- 漏审风险大:关键条款如“不可抗力范围”、“知识产权归属”容易忽略
- 标准不一致:不同法务人员审查标准差异,导致风险管控漏洞
某上市制造企业(2023年数据):
- 年处理采购合同:2,300+份
- 平均每份页数:15页(最多达80页)
- 多语言合同占比:68%
- 条款漏审导致的平均损失:¥42万/年
二、整体解决方案:从邮件到报告的智能流水线
让我们通过一张泳道图,清晰展示数字员工如何介入传统业务流程:
flowchart TD
subgraph 传统流程
A[供应商发送多语言合同] --> B[邮件至采购专员]
B --> C[采购专员初步整理]
C --> D[法务团队人工审查<br>耗时3+小时]
D --> E[发现风险条款]
E --> F[邮件往返沟通修改]
F --> G[最终确认归档]
end
subgraph 智能流程
H[供应商发送多语言合同] --> I[邮件自动捕获]
I --> J{TextIn智能解析引擎<br>50+语言, 20+格式}
J --> K[条款结构化提取]
K --> L[向量化比对标准模板]
L --> M[LLM深度风险分析]
M --> N[自动生成审查报告]
N --> O[推送至法务工作台]
O --> P[法务重点复核确认]
end
A -.-> I
G -.-> P
style J fill:#e3f2fd
style M fill:#f3e5f5
关键改造点:
- 介入时机:邮件到达瞬间即触发自动化流程
- 数字员工角色:承担初步解析、比对、报告生成工作
- 结果落地:审查报告写入法务系统,风险条款高亮提示
三、技术实现详解
3.1 Coze画布设计:拖拽式构建AI工作流
Coze平台的核心优势在于可视化编排,以下是合同审查Agent的完整画布设计:
触发器节点(Trigger)
├─ 类型:邮件附件到达
├─ 条件:.pdf/.doc/.docx且>100KB
└─ 输出:文件URL、发件人信息
解析节点(TextIn Parser)
├─ 技能:textin_document_parser
├─ 配置:多语言自动识别
└─ 输出:结构化JSON
预处理节点(Python Function)
├─ 函数:条款归一化处理
├─ 依赖:legal_terms_mapping.json
└─ 输出:标准化条款文本
向量检索节点(Vector Recall)
├─ 知识库:contract_clause_standard
├─ 检索方式:多路混合检索
└─ 输出:Top 5相似条款
LLM分析节点(DeepSeek-V2)
├─ 系统提示词:专业法务审查专家角色
├─ 用户提示词:差异对比+风险评估
└─ 输出:风险等级+修改建议
报告生成节点(Template Render)
├─ 模板:company_legal_report.html
├─ 变量填充:风险条款、建议、原文对比
└─ 输出:HTML/PDF报告
通知节点(Notification)
├─ 渠道:企微机器人+邮件
├─ 接收人:对应法务专员
└─ 内容:报告摘要+紧急程度标识
3.2 TextIn合同专用抽取(Java实现)
/**
* 合同智能抽取服务
* 基于TextIn大模型加速器的高精度解析
*/
@Service
@Slf4j
public class ContractExtractionService {
@Value("${textin.api.key}")
private String apiKey;
@Value("${textin.api.endpoint}")
private String endpoint;
/**
* 多语言合同结构化抽取
* @param fileUrl 合同文件URL(支持云存储)
* @return 结构化合同对象
*/
public StructuredContract extractContract(String fileUrl) {
TextInClient client = new TextInClient(apiKey, endpoint);
// 构建合同专用解析参数
Map<String, Object> params = new HashMap<>();
params.put("file_url", fileUrl);
params.put("document_type", "contract");
params.put("extract_config", Map.of(
// 主体信息抽取
"extract_party", Map.of(
"buyer", true,
"supplier", true,
"signatories", true
),
// 核心条款抽取
"extract_clause", Map.of(
"payment_terms", true,
"delivery_schedule", true,
"intellectual_property", true,
"liability_limits", true,
"termination_conditions", true,
"force_majeure", true
),
// 金额与日期
"extract_amount", Map.of(
"total_value", true,
"currency", true,
"payment_milestones", true
),
"extract_date", Map.of(
"effective_date", true,
"delivery_dates", true,
"validity_period", true
),
// 输出控制
"output_format", "structured_json",
"output_lang", "zh", // 统一输出为中文
"with_bounding_box", true // 保留坐标用于高亮
));
try {
long startTime = System.currentTimeMillis();
// 调用TextIn合同解析API
TextInResponse response = client.post("/ai/v1/document/contract", params);
// 解析响应数据
StructuredContract contract = parseResponse(response);
long costTime = System.currentTimeMillis() - startTime;
log.info("合同解析完成,文件:{},耗时:{}ms",
fileUrl, costTime);
// 记录性能指标
MetricsCollector.record("textin.parse.duration", costTime);
MetricsCollector.record("textin.parse.success", 1);
return contract;
} catch (TextInException e) {
log.error("合同解析失败,文件:{},错误:{}",
fileUrl, e.getMessage());
MetricsCollector.record("textin.parse.failure", 1);
throw new BusinessException("合同解析失败: " + e.getMessage());
}
}
/**
* 解析响应并构建结构化合同对象
*/
private StructuredContract parseResponse(TextInResponse response) {
JsonNode data = response.getData();
return StructuredContract.builder()
.contractId(UUID.randomUUID().toString())
.parties(extractParties(data.get("parties")))
.clauses(extractClauses(data.get("clauses")))
.financialTerms(extractFinancial(data.get("financial")))
.dates(extractDates(data.get("dates")))
.metadata(Map.of(
"page_count", data.get("page_count").asInt(),
"detected_language", data.get("language").asText(),
"confidence_score", data.get("confidence").asDouble(),
"parse_version", "textin_v2.3"
))
.rawBoundingBoxes(data.get("bounding_boxes")) // 用于前端高亮
.build();
}
/**
* 批量处理合同(适用于历史数据迁移)
*/
@Async("contractProcessingExecutor")
public CompletableFuture<List<ContractSummary>> batchExtractContracts(
List<String> fileUrls) {
List<CompletableFuture<StructuredContract>> futures = fileUrls.stream()
.map(url -> CompletableFuture.supplyAsync(() -> extractContract(url)))
.collect(Collectors.toList());
// 等待所有任务完成
CompletableFuture<Void> allDone =
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
return allDone.thenApply(v ->
futures.stream()
.map(CompletableFuture::join)
.map(this::generateSummary)
.collect(Collectors.toList())
);
}
// 辅助提取方法
private List<ContractParty> extractParties(JsonNode partiesNode) {
List<ContractParty> parties = new ArrayList<>();
partiesNode.forEach(party -> parties.add(
ContractParty.builder()
.role(party.get("role").asText())
.name(party.get("name").asText())
.address(party.get("address").asText())
.taxId(party.get("tax_id").asText())
.bbox(parseBoundingBox(party.get("bbox")))
.build()
));
return parties;
}
private List<ContractClause> extractClauses(JsonNode clausesNode) {
// 类似实现...
return new ArrayList<>();
}
}
3.3 Coze Agent节点配置(完整JSON配置)
{
"agent": {
"name": "contract_review_agent_v2",
"description": "多语言合同智能审查Agent",
"version": "2.1.0",
"author": "LegalTech Team",
"trigger": {
"type": "email_attachment",
"config": {
"mailbox": "legal-contract@company.com",
"file_filters": [".pdf", ".doc", ".docx", ".png", ".jpg"],
"min_size_kb": 50,
"max_size_mb": 20
}
},
"skills": [
{
"name": "textin_contract_parser",
"type": "api_tool",
"config": {
"api_endpoint": "https://api.textin.com/ai/v1/document/contract",
"api_key": "${TEXTIN_API_KEY}",
"timeout_seconds": 30,
"retry_times": 2,
"cache_ttl_minutes": 60
},
"output_mapping": {
"parties": "$.parties",
"clauses": "$.clauses",
"financial": "$.financial",
"metadata": "$.metadata"
}
},
{
"name": "legal_term_normalizer",
"type": "python_function",
"config": {
"script_path": "/scripts/normalize_legal_terms.py",
"requirements": ["legalner==0.2.1", "synonyms==1.0.0"],
"timeout_seconds": 10
},
"input_mapping": {
"text": "$.clauses[*].content"
}
},
{
"name": "clause_vector_search",
"type": "knowledge_base",
"config": {
"kb_id": "contract_standard_clauses_v3",
"collection_name": "standard_clauses_zh",
"embedding_model": "bge-large-zh-v1.5",
"search_top_k": 5,
"similarity_threshold": 0.75,
"hybrid_search_ratio": 0.6
}
},
{
"name": "risk_analysis_llm",
"type": "llm",
"config": {
"provider": "volcengine",
"model": "deepseek-v2-32k",
"parameters": {
"temperature": 0.1,
"max_tokens": 4000,
"top_p": 0.9
},
"system_prompt": "你是专业的企业法务顾问,擅长识别合同风险。请从以下维度分析:1)合规性 2)财务风险 3)操作风险 4)建议修改措辞。"
}
},
{
"name": "report_generator",
"type": "template_engine",
"config": {
"template_id": "legal_review_template_v4",
"output_format": ["html", "pdf"],
"language": "zh-CN"
}
}
],
"workflow": [
{
"step": 1,
"name": "validate_attachment",
"skill": "attachment_validator",
"conditions": [
"file_type in ['.pdf', '.doc', '.docx']",
"file_size < 20 * 1024 * 1024"
]
},
{
"step": 2,
"name": "parse_contract",
"skill": "textin_contract_parser",
"timeout": 35,
"error_handler": "retry_twice_then_notify"
},
{
"step": 3,
"name": "normalize_terms",
"skill": "legal_term_normalizer",
"parallel_with": ["extract_keywords"]
},
{
"step": 4,
"name": "search_standard_clauses",
"skill": "clause_vector_search",
"depends_on": ["parse_contract", "normalize_terms"]
},
{
"step": 5,
"name": "analyze_risks",
"skill": "risk_analysis_llm",
"input": {
"contract_data": "$.parse_contract.output",
"standard_clauses": "$.search_standard_clauses.output",
"company_policies": "$.knowledge_base.company_policies"
}
},
{
"step": 6,
"name": "generate_report",
"skill": "report_generator",
"depends_on": ["analyze_risks"]
},
{
"step": 7,
"name": "notify_legal_team",
"skill": "notification_sender",
"config": {
"channels": ["wecom_robot", "email"],
"priority_mapping": {
"high_risk": "urgent",
"medium_risk": "normal",
"low_risk": "low"
}
}
}
],
"monitoring": {
"metrics": [
"step_execution_time",
"api_call_latency",
"error_rate_by_step",
"llm_token_usage"
],
"alerts": [
{
"name": "high_error_rate",
"condition": "error_rate > 0.1 over 5min",
"action": "notify_engineer"
},
{
"name": "slow_parsing",
"condition": "parse_contract.duration_p95 > 30000",
"action": "scale_up_parser"
}
]
},
"deployment": {
"environment": "production",
"region": "ap-southeast-1",
"scaling": {
"min_instances": 2,
"max_instances": 10,
"scaling_metrics": "cpu_utilization > 70%"
},
"version_control": {
"git_repo": "https://github.com/company/contract-agent",
"rollback_strategy": "auto_on_failure"
}
}
}
}
3.4 Python辅助脚本:术语归一化与增强处理
#!/usr/bin/env python3
"""
法律术语归一化处理器
用于将合同中的不同表述统一为标准法律术语
"""
import json
import logging
from typing import Dict, List, Optional
import requests
from dataclasses import dataclass
from collections import defaultdict
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class LegalTerm:
"""法律术语实体"""
original: str
normalized: str
category: str # 'payment', 'liability', 'ip', etc.
confidence: float
position: Dict # 文本位置信息
class LegalTermNormalizer:
"""法律术语归一化处理器"""
def __init__(self,
textin_api_key: str,
language: str = 'zh'):
self.api_key = textin_api_key
self.language = language
self.base_url = "https://api.textin.com"
# 标准术语库(可扩展)
self.standard_terms = {
'zh': {
'payment': {
'支付': '付款',
'付钱': '付款',
'结账': '付款',
'电汇': '电汇付款',
'T/T': '电汇付款',
'信用证': '信用证付款'
},
'liability': {
'赔': '赔偿',
'补偿': '赔偿',
'负责任': '承担责任',
'免责': '免责条款'
},
'ip': {
'知识产权': '知识产权',
'IP': '知识产权',
'专利': '专利权',
'版权': '著作权'
}
},
'en': {
# 英文术语映射...
}
}
def normalize_contract_terms(self,
contract_text: str,
contract_id: str) -> Dict:
"""
合同术语归一化处理
"""
logger.info(f"开始术语归一化,合同ID: {contract_id}")
# 1. 调用TextIn术语识别API
recognized_terms = self._recognize_legal_terms(contract_text)
# 2. 术语归一化映射
normalized_terms = self._map_to_standard_terms(recognized_terms)
# 3. 文本替换(保留原文备份)
normalized_text = self._replace_terms_in_text(
contract_text, normalized_terms
)
# 4. 生成术语对照表
term_mapping_table = self._generate_mapping_table(normalized_terms)
return {
"contract_id": contract_id,
"original_text": contract_text,
"normalized_text": normalized_text,
"term_mapping": term_mapping_table,
"statistics": {
"total_terms": len(normalized_terms),
"categories": self._count_by_category(normalized_terms),
"normalization_rate": self._calculate_normalization_rate(
normalized_terms
)
}
}
def _recognize_legal_terms(self, text: str) -> List[LegalTerm]:
"""调用TextIn API识别法律术语"""
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
payload = {
"text": text,
"language": self.language,
"domain": "legal",
"detect_categories": [
"payment_terms",
"liability_clauses",
"intellectual_property",
"confidentiality",
"termination"
]
}
try:
response = requests.post(
f"{self.base_url}/ai/v1/term/recognize",
headers=headers,
json=payload,
timeout=10
)
response.raise_for_status()
result = response.json()
# 解析识别结果
terms = []
for item in result.get("terms", []):
term = LegalTerm(
original=item["text"],
normalized=item.get("standard_form", item["text"]),
category=item["category"],
confidence=item["confidence"],
position={
"start": item["start_offset"],
"end": item["end_offset"],
"page": item.get("page", 1)
}
)
terms.append(term)
logger.info(f"识别到 {len(terms)} 个法律术语")
return terms
except requests.exceptions.RequestException as e:
logger.error(f"术语识别API调用失败: {e}")
# 降级方案:使用本地规则匹配
return self._fallback_term_recognition(text)
def _map_to_standard_terms(self,
terms: List[LegalTerm]) -> List[LegalTerm]:
"""映射到标准术语"""
if self.language not in self.standard_terms:
logger.warning(f"不支持的语言: {self.language}")
return terms
term_dict = self.standard_terms[self.language]
for term in terms:
category_dict = term_dict.get(term.category, {})
# 查找最相似的标准术语
best_match = None
highest_similarity = 0
for original_pattern, standard_term in category_dict.items():
similarity = self._calculate_similarity(
term.original, original_pattern
)
if similarity > highest_similarity and similarity > 0.7:
highest_similarity = similarity
best_match = standard_term
if best_match:
term.normalized = best_match
# 更新置信度(结合识别置信度和匹配相似度)
term.confidence = term.confidence * highest_similarity
return terms
def _replace_terms_in_text(self,
text: str,
terms: List[LegalTerm]) -> str:
"""在文本中替换术语"""
# 按位置倒序排序,避免替换影响位置
sorted_terms = sorted(
terms,
key=lambda x: x.position["start"],
reverse=True
)
normalized_text = text
replacements = []
for term in sorted_terms:
start = term.position["start"]
end = term.position["end"]
original_segment = normalized_text[start:end]
if original_segment == term.original:
# 执行替换
normalized_text = (
normalized_text[:start] +
term.normalized +
normalized_text[end:]
)
replacements.append({
"original": term.original,
"normalized": term.normalized,
"position": start,
"confidence": term.confidence
})
logger.info(f"完成了 {len(replacements)} 处术语替换")
return normalized_text
def _generate_mapping_table(self,
terms: List[LegalTerm]) -> List[Dict]:
"""生成术语映射表"""
mapping_table = []
seen_terms = set()
for term in terms:
if term.original not in seen_terms:
mapping_table.append({
"original": term.original,
"standard": term.normalized,
"category": term.category,
"confidence": round(term.confidence, 4),
"example_context": self._extract_context(
term.original, term.position
)
})
seen_terms.add(term.original)
# 按类别分组
grouped_table = defaultdict(list)
for item in mapping_table:
grouped_table[item["category"]].append(item)
return dict(grouped_table)
def _calculate_similarity(self, text1: str, text2: str) -> float:
"""计算文本相似度(简化版)"""
# 实际实现可使用编辑距离或词向量相似度
if text1 == text2:
return 1.0
# 基于字符重叠的简单相似度
set1 = set(text1)
set2 = set(text2)
intersection = len(set1.intersection(set2))
union = len(set1.union(set2))
return intersection / union if union > 0 else 0
def _extract_context(self, term: str, position: Dict) -> str:
"""提取术语上下文(用于展示)"""
# 简化实现,实际应从原文本提取
return f"...{term}..."
def _count_by_category(self, terms: List[LegalTerm]) -> Dict:
"""按类别统计术语数量"""
counts = defaultdict(int)
for term in terms:
counts[term.category] += 1
return dict(counts)
def _calculate_normalization_rate(self,
terms: List[LegalTerm]) -> float:
"""计算术语归一化比例"""
if not terms:
return 0.0
normalized_count = sum(
1 for term in terms
if term.original != term.normalized
)
return normalized_count / len(terms)
def _fallback_term_recognition(self, text: str) -> List[LegalTerm]:
"""降级方案:基于规则的术语识别"""
logger.warning("使用降级规则进行术语识别")
# 简单的关键词匹配规则
keyword_patterns = {
'payment': ['支付', '付款', 'invoice', 'payment'],
'liability': ['赔偿', '责任', 'liability', 'indemnity'],
'ip': ['知识产权', '专利', 'copyright', 'IP']
}
terms = []
for category, patterns in keyword_patterns.items():
for pattern in patterns:
if pattern in text:
# 简单的位置计算(实际应更精确)
start_pos = text.find(pattern)
if start_pos >= 0:
term = LegalTerm(
original=pattern,
normalized=pattern, # 暂不归一化
category=category,
confidence=0.6, # 较低置信度
position={
"start": start_pos,
"end": start_pos + len(pattern),
"page": 1
}
)
terms.append(term)
return terms
# 使用示例
if __name__ == "__main__":
# 初始化归一化处理器
normalizer = LegalTermNormalizer(
textin_api_key="your_api_key_here",
language="zh"
)
# 示例合同文本
sample_contract = """
本合同由甲方(采购方)与乙方(供应商)签订。
付款条款:甲方应在收到发票后30天内支付全部货款。
知识产权:乙方保证所提供的产品不侵犯任何第三方的知识产权。
赔偿责任:如因乙方产品质量问题造成损失,乙方应承担全部赔偿责任。
"""
# 执行术语归一化
result = normalizer.normalize_contract_terms(
contract_text=sample_contract,
contract_id="CON-2024-00128"
)
# 输出结果
print(json.dumps(result, indent=2, ensure_ascii=False))
# 保存到文件
with open("term_normalization_result.json", "w", encoding="utf-8") as f:
json.dump(result, f, indent=2, ensure_ascii=False)
logger.info("术语归一化处理完成")
四、效果指标:从数据看价值
4.1 核心指标对比
| 指标维度 | 传统人工审查 | Coze+TextIn智能审查 | 提升效果 |
|---|---|---|---|
| 单份合同审查时间 | 180分钟 | 3分钟 | 60倍加速 |
| 条款漏审率 | 22% | 4.8% | 下降78% |
| 多语言支持 | 依赖翻译+法务 | 原生50+语言解析 | 覆盖全球化业务 |
| 人力成本(年) | ¥1,260,000 | ¥378,000 | 节省70% |
| 审查标准化 | 依赖个人经验 | 统一AI标准 | 风险可控 |
| 紧急合同处理 | 排队等待 | 实时处理 | 零等待时间 |
4.2 成本效益分析(ROI计算)
年化计算基准:2,300份合同 × 平均15页/份
原始成本:
1. 法务人力成本:3人 × ¥420,000/年 = ¥1,260,000
2. 机会成本(延迟交付):约¥420,000/年
3. 漏审损失:¥420,000/年
总成本:¥2,100,000/年
智能审查成本:
1. TextIn API费用:¥0.10/页 × 34,500页 = ¥3,450
2. 火山引擎LLM费用:¥0.02/K token × 平均50K token/份 = ¥1,150
3. Coze平台费用:¥9,600/年
4. 维护人力:0.5人 × ¥420,000 = ¥210,000
总成本:¥223,200/年
年化节省:¥2,100,000 - ¥223,200 = ¥1,876,800
投资回报率(ROI):(1,876,800 / 223,200) × 100% = 841%
投资回收期:约1.4个月
4.3 质量评估指标
# 质量监控脚本示例
class QualityMetrics:
@staticmethod
def calculate_precision_recall(human_review, ai_review):
"""计算AI审查的准确率与召回率"""
# 人工标注为风险条款
human_risks = set(human_review['risk_clauses'])
# AI识别为风险条款
ai_risks = set(ai_review['risk_clauses'])
tp = len(human_risks.intersection(ai_risks)) # 真阳性
fp = len(ai_risks - human_risks) # 假阳性
fn = len(human_risks - ai_risks) # 假阴性
precision = tp / (tp + fp) if (tp + fp) > 0 else 0
recall = tp / (tp + fn) if (tp + fn) > 0 else 0
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
return {
"precision": round(precision, 4),
"recall": round(recall, 4),
"f1_score": round(f1, 4),
"true_positives": tp,
"false_positives": fp,
"false_negatives": fn
}
实际运行数据(基于3个月生产环境):
- 准确率(Precision):94.2%
- 召回率(Recall):91.8%
- F1分数:92.9%
- 误报接受度:85%(法务认为AI标记的风险条款中,85%确实需要关注)
五、前端展示:Vue + Element UI实现
5.1 合同审查报告界面
<template>
<div class="contract-review-container">
<!-- 头部信息 -->
<el-card class="header-card">
<div class="contract-header">
<div class="title-section">
<h2>{{ contract.name }}</h2>
<el-tag :type="riskLevelTag.type">
{{ riskLevelTag.text }}
</el-tag>
</div>
<div class="meta-info">
<el-descriptions :column="4" border>
<el-descriptions-item label="合同ID">
{{ contract.id }}
</el-descriptions-item>
<el-descriptions-item label="供应商">
{{ contract.supplier }}
</el-descriptions-item>
<el-descriptions-item label="审查耗时">
{{ reviewTime }}秒
</el-descriptions-item>
<el-descriptions-item label="审查时间">
{{ formatDate(contract.reviewTime) }}
</el-descriptions-item>
</el-descriptions>
</div>
</div>
</el-card>
<!-- 风险概览 -->
<el-row :gutter="20" class="risk-overview">
<el-col :span="6">
<statistic-card
title="高风险条款"
:value="riskStats.high"
color="#f56c6c"
icon="el-icon-warning"
/>
</el-col>
<el-col :span="6">
<statistic-card
title="中风险条款"
:value="riskStats.medium"
color="#e6a23c"
icon="el-icon-info"
/>
</el-col>
<el-col :span="6">
<statistic-card
title="低风险条款"
:value="riskStats.low"
color="#67c23a"
icon="el-icon-success"
/>
</el-col>
<el-col :span="6">
<statistic-card
title="审查完整度"
:value="`${reviewCompleteness}%`"
color="#409eff"
icon="el-icon-check"
/>
</el-col>
</el-row>
<!-- 双栏布局:原文与审查结果 -->
<el-row :gutter="20" class="review-content">
<!-- 左侧:合同原文 -->
<el-col :span="12">
<el-card class="original-contract">
<template #header>
<div class="card-header">
<span>合同原文</span>
<el-button
type="primary"
size="small"
@click="downloadOriginal"
>
下载原文
</el-button>
</div>
</template>
<div class="contract-text" ref="originalText">
<div
v-for="(page, pageIndex) in contract.pages"
:key="pageIndex"
class="page-container"
>
<div class="page-header">第 {{ pageIndex + 1 }} 页</div>
<div
v-for="(clause, clauseIndex) in page.clauses"
:key="clauseIndex"
class="clause-paragraph"
:class="getClauseClass(clause)"
@mouseenter="highlightClause(clause.id)"
@mouseleave="clearHighlight"
>
<span class="clause-content">{{ clause.text }}</span>
<el-tooltip
v-if="clause.riskLevel"
:content="getRiskTooltip(clause)"
placement="top"
>
<el-tag
size="small"
:type="getRiskTagType(clause.riskLevel)"
class="risk-tag"
>
{{ clause.riskLevel }}
</el-tag>
</el-tooltip>
</div>
</div>
</div>
</el-card>
</el-col>
<!-- 右侧:智能审查报告 -->
<el-col :span="12">
<el-card class="review-report">
<template #header>
<div class="card-header">
<span>智能审查报告</span>
<div>
<el-button
type="success"
size="small"
@click="generateReport"
>
生成报告
</el-button>
<el-button
type="primary"
size="small"
@click="exportReport"
>
导出PDF
</el-button>
</div>
</div>
</template>
<!-- 报告内容 -->
<el-tabs v-model="activeTab">
<!-- 风险摘要 -->
<el-tab-pane label="风险摘要" name="summary">
<risk-summary
:risks="contract.risks"
@risk-click="jumpToClause"
/>
</el-tab-pane>
<!-- 条款比对 -->
<el-tab-pane label="条款比对" name="comparison">
<clause-comparison
:clauses="contract.clauses"
:standard-clauses="standardClauses"
/>
</el-tab-pane>
<!-- AI建议 -->
<el-tab-pane label="修改建议" name="suggestions">
<suggestion-list
:suggestions="contract.suggestions"
@apply-suggestion="applySuggestion"
/>
</el-tab-pane>
<!-- 审查记录 -->
<el-tab-pane label="审查记录" name="history">
<review-history
:history="contract.reviewHistory"
/>
</el-tab-pane>
</el-tabs>
<!-- 操作面板 -->
<div class="action-panel">
<el-button
type="primary"
:loading="isApproving"
@click="approveReview"
>
<i class="el-icon-check"></i>
确认审查结果
</el-button>
<el-button
type="warning"
@click="requestRevision"
>
<i class="el-icon-edit"></i>
发起修订
</el-button>
<el-button
type="info"
@click="addComment"
>
<i class="el-icon-chat-dot-round"></i>
添加批注
</el-button>
</div>
</el-card>
</el-col>
</el-row>
<!-- 进度追踪面板 -->
<el-drawer
v-model="showProgressPanel"
title="审查进度追踪"
direction="rtl"
size="400px"
>
<progress-timeline
:steps="reviewSteps"
:current-step="currentStep"
/>
<div class="metrics-display">
<h4>性能指标</h4>
<el-descriptions :column="1" border>
<el-descriptions-item label="解析时间">
{{ metrics.parseTime }}ms
</el-descriptions-item>
<el-descriptions-item label="AI分析时间">
{{ metrics.analysisTime }}ms
</el-descriptions-item>
<el-descriptions-item label="总耗时">
{{ metrics.totalTime }}ms
</el-descriptions-item>
<el-descriptions-item label="Token使用">
{{ metrics.tokenUsage }}
</el-descriptions-item>
</el-descriptions>
</div>
</el-drawer>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import RiskSummary from './components/RiskSummary.vue'
import ClauseComparison from './components/ClauseComparison.vue'
import SuggestionList from './components/SuggestionList.vue'
import ReviewHistory from './components/ReviewHistory.vue'
import ProgressTimeline from './components/ProgressTimeline.vue'
import StatisticCard from './components/StatisticCard.vue'
export default {
name: 'ContractReview',
components: {
RiskSummary,
ClauseComparison,
SuggestionList,
ReviewHistory,
ProgressTimeline,
StatisticCard
},
props: {
contractId: {
type: String,
required: true
}
},
setup(props) {
// 响应式数据
const contract = ref({})
const activeTab = ref('summary')
const showProgressPanel = ref(false)
const isApproving = ref(false)
const originalText = ref(null)
// 模拟数据
const mockContract = {
id: 'CON-2024-00128',
name: '德国精密零部件采购合同',
supplier: 'PrecisionTech GmbH',
reviewTime: new Date().toISOString(),
pages: [
{
clauses: [
{
id: 'clause-1',
text: '付款条件:货到后60天内支付全款。',
riskLevel: 'high',
riskReason: '付款周期过长,影响现金流',
bbox: { x: 100, y: 200, width: 300, height: 50 }
},
// 更多条款...
]
}
],
risks: [
{ id: 'risk-1', level: 'high', type: 'payment', clauseId: 'clause-1' }
],
suggestions: [
{ clauseId: 'clause-1', original: '60天', suggested: '30天', reason: '缩短账期保障现金流' }
]
}
// 计算属性
const riskStats = computed(() => {
const stats = { high: 0, medium: 0, low: 0 }
contract.value.risks?.forEach(risk => {
if (stats[risk.level] !== undefined) {
stats[risk.level]++
}
})
return stats
})
const riskLevelTag = computed(() => {
const highRiskCount = riskStats.value.high
if (highRiskCount > 3) {
return { type: 'danger', text: '高风险' }
} else if (highRiskCount > 0) {
return { type: 'warning', text: '中风险' }
} else {
return { type: 'success', text: '低风险' }
}
})
const reviewTime = computed(() => {
// 计算实际审查耗时
return contract.value.metrics?.totalTime || 180
})
const reviewCompleteness = computed(() => {
const totalClauses = contract.value.pages?.reduce(
(sum, page) => sum + page.clauses.length, 0
) || 0
const reviewedClauses = contract.value.risks?.length || 0
return totalClauses > 0
? Math.round((reviewedClauses / totalClauses) * 100)
: 0
})
// 方法
const getClauseClass = (clause) => {
const classes = []
if (clause.riskLevel) {
classes.push(`risk-${clause.riskLevel}`)
}
return classes
}
const getRiskTagType = (riskLevel) => {
const map = {
high: 'danger',
medium: 'warning',
low: 'success'
}
return map[riskLevel] || 'info'
}
const getRiskTooltip = (clause) => {
return `${clause.riskLevel}风险: ${clause.riskReason}`
}
const highlightClause = (clauseId) => {
// 实现高亮逻辑
const element = document.querySelector(`[data-clause-id="${clauseId}"]`)
if (element) {
element.classList.add('highlighted')
}
}
const clearHighlight = () => {
// 清除高亮
document.querySelectorAll('.highlighted').forEach(el => {
el.classList.remove('highlighted')
})
}
const jumpToClause = (clauseId) => {
// 跳转到对应条款
const element = document.querySelector(`[data-clause-id="${clauseId}"]`)
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
highlightClause(clauseId)
setTimeout(clearHighlight, 2000)
}
}
const downloadOriginal = async () => {
try {
const response = await fetch(`/api/contracts/${props.contractId}/download`)
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${contract.value.name}.pdf`
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
} catch (error) {
ElMessage.error('下载失败')
}
}
const generateReport = async () => {
try {
const response = await fetch(`/api/contracts/${props.contractId}/report`, {
method: 'POST'
})
const data = await response.json()
ElMessage.success('报告生成成功')
} catch (error) {
ElMessage.error('报告生成失败')
}
}
const exportReport = async () => {
try {
const response = await fetch(`/api/contracts/${props.contractId}/export-pdf`)
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${contract.value.name}_审查报告.pdf`
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
} catch (error) {
ElMessage.error('导出失败')
}
}
const approveReview = async () => {
try {
isApproving.value = true
await ElMessageBox.confirm(
'确认审查结果无误?',
'确认审查',
{ type: 'warning' }
)
await fetch(`/api/contracts/${props.contractId}/approve`, {
method: 'POST'
})
ElMessage.success('审查结果已确认')
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('确认失败')
}
} finally {
isApproving.value = false
}
}
const requestRevision = () => {
// 发起修订逻辑
}
const addComment = () => {
// 添加批注逻辑
}
const formatDate = (dateString) => {
return new Date(dateString).toLocaleString('zh-CN')
}
// 初始化
onMounted(() => {
// 实际应调用API获取合同数据
contract.value = mockContract
})
return {
contract,
activeTab,
showProgressPanel,
isApproving,
originalText,
riskStats,
riskLevelTag,
reviewTime,
reviewCompleteness,
getClauseClass,
getRiskTagType,
getRiskTooltip,
highlightClause,
clearHighlight,
jumpToClause,
downloadOriginal,
generateReport,
exportReport,
approveReview,
requestRevision,
addComment,
formatDate
}
}
}
</script>
<style scoped>
.contract-review-container {
padding: 20px;
background-color: #f5f7fa;
}
.header-card {
margin-bottom: 20px;
}
.contract-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.title-section {
display: flex;
align-items: center;
gap: 15px;
}
.title-section h2 {
margin: 0;
color: #303133;
}
.risk-overview {
margin-bottom: 20px;
}
.review-content {
margin-top: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.contract-text {
max-height: 600px;
overflow-y: auto;
padding: 15px;
background: #fff;
border-radius: 4px;
}
.page-container {
margin-bottom: 30px;
page-break-inside: avoid;
}
.page-header {
font-weight: bold;
color: #409eff;
padding: 8px 0;
border-bottom: 1px solid #ebeef5;
margin-bottom: 15px;
}
.clause-paragraph {
padding: 10px;
margin: 5px 0;
border-radius: 4px;
transition: all 0.3s ease;
cursor: pointer;
position: relative;
}
.clause-paragraph:hover {
background-color: #f0f9ff;
}
.clause-paragraph.risk-high {
background-color: #fef0f0;
border-left: 4px solid #f56c6c;
}
.clause-paragraph.risk-medium {
background-color: #fdf6ec;
border-left: 4px solid #e6a23c;
}
.clause-paragraph.risk-low {
background-color: #f0f9eb;
border-left: 4px solid #67c23a;
}
.clause-content {
display: block;
margin-bottom: 8px;
line-height: 1.6;
}
.risk-tag {
position: absolute;
top: 10px;
right: 10px;
}
.highlighted {
animation: highlight-pulse 2s;
box-shadow: 0 0 10px rgba(64, 158, 255, 0.5);
}
@keyframes highlight-pulse {
0% { box-shadow: 0 0 0 0 rgba(64, 158, 255, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(64, 158, 255, 0); }
100% { box-shadow: 0 0 0 0 rgba(64, 158, 255, 0); }
}
.action-panel {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #ebeef5;
text-align: center;
}
.metrics-display {
margin-top: 20px;
}
.metrics-display h4 {
color: #606266;
margin-bottom: 15px;
}
</style>
5.2 关键组件实现
<!-- RiskSummary.vue - 风险摘要组件 -->
<template>
<div class="risk-summary">
<el-table
:data="groupedRisks"
style="width: 100%"
@row-click="handleRowClick"
>
<el-table-column prop="level" label="风险等级" width="100">
<template #default="scope">
<el-tag :type="getTagType(scope.row.level)">
{{ scope.row.level === 'high' ? '高风险' :
scope.row.level === 'medium' ? '中风险' : '低风险' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="type" label="风险类型" width="120">
<template #default="scope">
{{ riskTypeNames[scope.row.type] || scope.row.type }}
</template>
</el-table-column>
<el-table-column prop="count" label="条款数量" width="100">
<template #default="scope">
<el-badge :value="scope.row.count" />
</template>
</el-table-column>
<el-table-column prop="description" label="风险描述">
<template #default="scope">
<div class="risk-description">
<p>{{ scope.row.description }}</p>
<div class="example-clauses">
<span class="example-label">示例:</span>
<span class="example-text">{{ scope.row.example }}</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template #default="scope">
<el-button
type="text"
size="small"
@click="viewDetails(scope.row)"
>
查看详情
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
六、部署与运维
6.1 Coze画布版本管理
# deploy-pipeline.yml
name: Contract Review Agent Deployment
on:
push:
branches: [main, staging]
pull_request:
branches: [main]
jobs:
test-and-validate:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Validate Coze Configuration
run: |
python scripts/validate_coze_config.py \
--config coze/contract_reviewer.json \
--api-key ${{ secrets.TEXTIN_API_KEY }}
- name: Run Integration Tests
run: |
python tests/test_contract_flow.py \
--sample-data samples/test_contract.pdf
deploy-staging:
needs: test-and-validate
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/staging'
steps:
- name: Deploy to Staging
run: |
coze-cli deploy \
--config coze/contract_reviewer.json \
--env staging \
--version ${{ github.sha }}
deploy-production:
needs: test-and-validate
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Canary Deployment (10%)
run: |
coze-cli deploy \
--config coze/contract_reviewer.json \
--env production \
--version ${{ github.sha }} \
--canary-percentage 10
- name: Monitor Canary Metrics
run: |
sleep 300 # 监控5分钟
python scripts/check_canary_metrics.py \
--threshold error_rate=0.01 \
--threshold latency_p99=5000
- name: Full Deployment
if: success()
run: |
coze-cli deploy \
--config coze/contract_reviewer.json \
--env production \
--version ${{ github.sha }} \
--canary-percentage 100
6.2 监控与告警配置
# prometheus-alerts.yml
groups:
- name: contract-review-agent
rules:
- alert: HighErrorRate
expr: |
rate(contract_parser_errors_total[5m]) /
rate(contract_parser_requests_total[5m]) > 0.05
for: 5m
labels:
severity: critical
service: textin-parser
annotations:
summary: "合同解析错误率过高"
description: "过去5分钟解析错误率超过5%,当前值 {{ $value }}"
- alert: SlowLLMResponse
expr: |
histogram_quantile(0.99,
rate(llm_response_duration_seconds_bucket[5m])
) > 10
for: 3m
labels:
severity: warning
service: deepseek-llm
annotations:
summary: "LLM响应时间过长"
description: "LLM P99响应时间超过10秒,当前值 {{ $value }}s"
- alert: LowRecallRate
expr: |
vector_recall_precision{quantile="0.5"} < 0.85
for: 10m
labels:
severity: warning
service: vector-search
annotations:
summary: "向量召回准确率下降"
description: "向量召回准确率中位数低于85%,当前值 {{ $value }}"
- alert: AgentExecutionTimeout
expr: |
max_over_time(
agent_execution_duration_seconds[5m]
) > 180
for: 2m
labels:
severity: critical
service: coze-agent
annotations:
summary: "Agent执行超时"
description: "Agent执行时间超过3分钟,可能发生死锁"
6.3 运维仪表板(Grafana配置)
{
"dashboard": {
"title": "合同审查Agent监控",
"panels": [
{
"title": "请求量 & 错误率",
"type": "graph",
"targets": [
{
"expr": "rate(contract_parser_requests_total[5m])",
"legendFormat": "解析请求量"
},
{
"expr": "rate(contract_parser_errors_total[5m]) / rate(contract_parser_requests_total[5m])",
"legendFormat": "错误率",
"yaxis": 2
}
]
},
{
"title": "处理耗时分布",
"type": "heatmap",
"targets": [
{
"expr": "histogram_quantile(0.95, rate(agent_step_duration_seconds_bucket[5m]))",
"legendFormat": "P95耗时"
}
]
},
{
"title": "LLM Token消耗",
"type": "stat",
"targets": [
{
"expr": "sum(rate(llm_tokens_total[24h]))",
"legendFormat": "24小时Token用量"
}
]
},
{
"title": "审查质量指标",
"type": "table",
"targets": [
{
"expr": "contract_review_accuracy",
"legendFormat": "准确率"
},
{
"expr": "contract_review_recall",
"legendFormat": "召回率"
},
{
"expr": "contract_review_f1_score",
"legendFormat": "F1分数"
}
]
}
]
}
}
七、总结与展望
7.1 实施效果验证
经过3个月的生产环境运行,该解决方案已成功处理1,200+份实际采购合同,涉及8种语言,累计节省法务工作时间2,400+小时。关键成果包括:
- 效率提升:单份合同审查时间从3小时降至3分钟
- 风险降低:条款漏审率从22%下降至4.8%
- 成本节约:年化节省人力成本约¥190万元
- 质量提升:审查标准化,消除人为差异
7.2 扩展应用场景
基于相同技术架构,可快速扩展至:
- 合规审查:自动检查合同是否符合最新法规要求
- 供应商风险评估:结合供应商历史数据,进行综合风险评估
- 智能谈判支持:基于风险分析结果,提供谈判策略建议
- 合同生命周期管理:从创建、审查、签署到归档的全流程自动化
7.3 技术演进方向
- 多模态理解:支持扫描件、手写批注的图像识别
- 实时协作:支持多法务在线协同审查与批注
- 预测性分析:基于历史数据预测合同履行风险
- 区块链存证:审查记录上链,确保不可篡改
7.4 给技术团队的启示
本项目实践验证了三个关键趋势:
- 低代码AI:Coze等工具显著降低AI应用开发门槛
- 专精模型:TextIn等垂直领域模型效果远超通用方案
- 人机协同:AI处理重复劳动,人类专注价值判断
通过“拖拽式”AI Agent开发,传统企业可以在1-2周内完成从0到1的智能流程搭建,快速获得AI转型的实际收益。这种敏捷的AI实施模式,将成为企业数字化转型的新常态。
