合同是企业数据里最"沉默"的一类——格式各异、条款嵌套、关键信息散落在几十页 PDF 里。传统做法靠人工审阅,速度慢、遗漏率高,规模化时更是噩梦。Doczy.ai™ 把生成式 AI 搬上 AWS,把非结构化合同批量转成结构化洞察,让审批、合规、风控流程可以真正自动化。这篇文章拆解它的思路,并给出一个你可以直接改造的 AWS 合同解析最小原型。
合同智能的核心难题
合同智能不是简单的"OCR + 关键词搜索"。真正的难点有三层:
- 语义提取:违约条款可能写在"第 8.3 条(b)款"里,措辞千变万化,关键词匹配覆盖不了。
- 关系推理:一份合同里甲方、担保方、受益方之间的关系需要跨段落理解。
- 批量一致性:1000 份合同提取出的"终止日期"必须落在同一个 schema 下,否则下游自动化无法消费。
Doczy.ai 的解法是用大模型做语义理解,用 AWS 基础设施做规模化编排,再用结构化 schema 做输出约束——三层问题各击一层。
AWS 上的架构拆解
根据 Doczy.ai 在 AWS 上的部署方式,核心组件可以归纳为:
| 组件 | AWS 服务 | 作用 |
|---|---|---|
| 文件摄入 | S3 + EventBridge | 合同上传即触发处理流水线 |
| 文档预处理 | Lambda / Textract | PDF → 文本,处理表格与签名区 |
| 语义提取 | SageMaker / Bedrock | 大模型按 schema 提取条款 |
| 结构化输出 | DynamoDB / OpenSearch | 存储为可查询的结构化记录 |
| 编排与监控 | Step Functions + CloudWatch | 串联全流程,失败自动重试 |
关键设计决策:用 Step Functions 做编排而非自写脚本。合同处理涉及多步调用(OCR → 分段 → LLM 提取 → 校验),Step Functions 提供可视化流程、内置重试和错误捕获,规模化时比自编排脚本可靠得多。
实践:搭建一个最小合同解析流水线
下面给出一个可以直接改造的 AWS 合同智能最小原型。它用 S3 上传触发 Lambda,Lambda 调用 Bedrock Claude 模型按预设 schema 提取合同关键信息,结果写入 DynamoDB。
1. DynamoDB 表 — 存储结构化输出
aws dynamodb create-table \
--table-name ContractIntelligence \
--attribute-definitions \
AttributeName=contractId,AttributeType=S \
AttributeName=uploadDate,AttributeType=S \
--key-schema \
AttributeName=contractId,KeyType=HASH \
AttributeName=uploadDate,KeyType=RANGE \
--billing-mode PAY_PER_REQUEST \
--region us-east-1
运行前确保你的 AWS CLI 已配置好凭证和区域。表名可按项目替换。
2. S3 Bucket + EventBridge 触发
# 创建 bucket
aws s3api create-bucket \
--bucket my-contract-intelligence-bucket \
--region us-east-1
# 添加 EventBridge 通知配置
aws s3api put-bucket-notification-configuration \
--bucket my-contract-intelligence-bucket \
--notification-configuration '{
"EventBridgeConfiguration": {}
}'
然后在 EventBridge 中创建规则,匹配 ObjectCreated 事件,目标指向下面创建的 Lambda。
3. Lambda 函数 — 调用 Bedrock 提取合同信息
import json
import boto3
import uuid
from datetime import datetime
s3_client = boto3.client("s3")
bedrock_client = boto3.client("bedrock-runtime")
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("ContractIntelligence")
EXTRACTION_SCHEMA = {
"contract_title": "合同标题",
"effective_date": "生效日期",
"expiration_date": "到期日期",
"counterparty": "对方当事人名称",
"contract_value": "合同金额及币种",
"termination_clause_summary": "终止条款摘要",
"key_obligations": "主要义务列表",
"risk_flags": "风险标记(如违约金、竞业限制等)",
}
def build_prompt(raw_text: str) -> str:
schema_desc = json.dumps(EXTRACTION_SCHEMA, ensure_ascii=False, indent=2)
return f"""你是一名合同分析专家。请从以下合同文本中提取关键信息,严格按照 JSON schema 输出。
只输出 JSON,不要添加任何解释文字。如果某字段在文本中找不到,填 null。
Schema:
{schema_desc}
合同文本:
{raw_text}"""
def lambda_handler(event, context):
for record in event.get("Records", []):
bucket = record["s3"]["bucket"]["name"]
key = record["s3"]["object"]["key"]
# 1. 从 S3 读取合同文本(假设已预处理为纯文本)
response = s3_client.get_object(Bucket=bucket, Key=key)
raw_text = response["Body"].read().decode("utf-8")
# 2. 调用 Bedrock Claude 3 Sonnet 提取
prompt = build_prompt(raw_text)
bedrock_response = bedrock_client.invoke_model(
modelId="anthropic.claude-3-sonnet-20240229-v1:0",
body=json.dumps({
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 4096,
"messages": [{"role": "user", "content": prompt}],
}),
contentType="application/json",
accept="application/json",
)
result_body = json.loads(bedrock_response["body"].read())
extracted_json = json.loads(result_body["content"][0]["text"])
# 3. 写入 DynamoDB
contract_id = str(uuid.uuid4())
upload_date = datetime.utcnow().strftime("%Y-%m-%d")
item = {
"contractId": contract_id,
"uploadDate": upload_date,
"sourceFile": key,
**extracted_json,
}
table.put_item(Item=item)
print(f"Contract {key} extracted → contractId={contract_id}")
return {"statusCode": 200, "body": json.dumps("Processed")}
部署前需要改动的地方:
- IAM 角色:Lambda 需要权限
s3:GetObject、bedrock:InvokeModel、dynamodb:PutItem。 - 如果源文件是 PDF/图片,在调用 LLM 前需要加一步 Textract 预处理,把文件转成纯文本。
EXTRACTION_SCHEMA按你的业务需求定制——加字段如"管辖法院""保密条款"即可,模型会按 schema 输出。
4. 用 Step Functions 编排完整流程(可选增强)
单 Lambda 适合原型。生产环境建议用 Step Functions 把 Textract → 分段 → LLM 提取 → 校验串起来,每步独立重试。下面是一个最小状态机定义的核心片段:
Comment: "Contract intelligence pipeline"
StartAt: ExtractText
States:
ExtractText:
Type: Task
Resource: arn:aws:states:::textract:startDocumentAnalysis
Parameters:
S3Bucket.$: "$.bucket"
S3Key.$: "$.key"
FeatureTypes:
- TABLES
- FORMS
Next: RunLLMExtraction
Retry:
- ErrorEquals:
- States.ALL
IntervalSeconds: 10
MaxAttempts: 3
RunLLMExtraction:
Type: Task
Resource: arn:aws:lambda:us-east-1:123456789012:function:ContractLLMExtract
Next: ValidateOutput
ValidateOutput:
Type: Task
Resource: arn:aws:lambda:us-east-1:123456789012:function:ValidateContractSchema
End: true
Catch:
- ErrorEquals:
- States.ALL
Next: LogValidationFailure
LogValidationFailure:
Type: Task
Resource: arn:aws:lambda:us-east-1:123456789012:function:LogFailure
End: true
把 123456789012 替换为你的 AWS 账号 ID,Lambda 函数名按实际部署填写。
落地时的取舍与风险
先做对的,再做快的:
- Schema 先行:不要先跑模型再想输出格式。先和业务方锁定 schema,再写 prompt。否则 1000 份合同提取出来字段名不一致,下游直接报废。
- 分段策略:长合同(>50 页)不要整篇丢给模型。按条款分段提取,再合并,准确率更高,token 成本也更可控。
- 校验层不可省:LLM 输出会有幻觉——比如凭空编造一个到期日期。加一个 Validate 步骤,用规则检查(日期格式、金额范围、必填字段非空),拦截明显错误后再入库。
成本与延迟的平衡:
- Bedrock Claude 3 Sonnet 在合同提取场景性价比最好;Haiku 更快更便宜但复杂条款容易漏。
- 批量处理时用 Provisioned Throughput 锁定容量,避免突发请求被限流。
- 冷合同(历史存档)可以离线批量跑;热合同(新签署需即时审阅)走实时触发,两者分开部署。
安全边界:
- 合同含高度敏感信息。Bedrock 在 AWS VPC 内运行,数据不出区域,但 S3 bucket 必须启用加密(SSE-KMS)和 bucket policy 限制访问。
- Lambda 日志默认写入 CloudWatch,确认日志级别不打印原始合同全文——只输出 contractId 和提取结果摘要。
检查清单:上线前逐项确认
| 项 | 状态 |
|---|---|
| 提取 Schema 已与业务方锁定并文档化 | ☐ |
| S3 bucket 启用 KMS 加密 + 访问策略 | ☐ |
| Lambda IAM 权限最小化(仅 s3/bedrock/dynamodb) | ☐ |
| CloudWatch 日志不输出合同原文 | ☐ |
| Step Functions 每步有 Retry + Catch | ☐ |
| 长合同分段逻辑已测试(>30 页) | ☐ |
| Validate 步骤覆盖必填字段 + 格式校验 | ☐ |
| Bedrock Provisioned Throughput 按峰值配置 | ☐ |
| 提取结果抽样人工复核流程已建立 | ☐ |
最后一项最容易忽略:没有人工抽样复核的 AI 合同智能,不值得信任。 每周抽 20 份让法务快速比对,持续修正 prompt 和 schema,这才是系统越跑越准的闭环。