一、方案总览
核心解决的问题
针对法律、金融等行业合同、发票等非结构化文本数据,解决人工提取效率低、云端大模型数据泄露风险、纯大模型提取结果不可控/不可追溯三大核心痛点,实现:
-
100%本地闭环:合同文本、模型推理、数据存储全流程不出本地环境,运行阶段无外网依赖,满足强合规要求
-
精准结构化提取:基于LangExtract实现可追溯、格式可控的字段提取,从根源上杜绝大模型幻觉
-
毫秒级检索筛选:基于Elasticsearch 9.x实现结构化数据多条件筛选+全文检索,适配复杂业务查询场景
-
零API成本:无需申请任何云端大模型API,无调用次数、流量限制,可无限量处理文档
核心技术栈
| 组件 | 选型 | 核心作用 |
|---|---|---|
| 大语言模型 | DeepSeek-R1/DeepSeek-V3(本地部署) | 负责文本语义理解与关键信息提取,通过Ollama部署,提供OpenAI兼容的本地API接口 |
| 信息提取框架 | LangExtract | 封装标准化结构化提取逻辑,提供少样本学习、结果溯源、格式校验、可视化能力,解决纯大模型提取的不稳定性 |
| 存储与检索引擎 | Elasticsearch 9.x 本地部署 | 结构化数据持久化存储、全文检索、多维度筛选,支持海量合同数据的高并发查询 |
| 模型部署工具 | Ollama(新手推荐)/vLLM(高性能场景) | 一键部署本地DeepSeek模型,屏蔽复杂的模型配置,自动提供标准化API服务 |
适用场景
-
企业合同管理系统:批量合同信息提取、智能归档、多维度检索
-
金融信贷审核:发票、财报、信贷合同的关键信息自动提取与核验
-
法务合规场景:合同主体、期限、金额、适用法律的批量核查与风险预警
-
内网/物理隔离环境:无法访问外网的政府、国企、金融机构内部系统
-
其他非结构化文本处理:简历解析、病历提取、工单信息结构化等场景
前置环境要求
| 环境项 | 最低要求 | 推荐配置 |
|---|---|---|
| 操作系统 | Windows 10+/macOS 12+/Linux CentOS7+/Ubuntu20.04+ | 同左,64位稳定版系统 |
| Java环境 | JDK 17+(Elasticsearch 9.x 强制要求) | JDK 17 LTS 稳定版 |
| Python环境 | Python 3.10 ~ 3.12 | Python 3.11(兼容性最优) |
| 硬件(7B模型) | 16GB可用内存,CPU运行 | 16GB内存 + 8GB以上NVIDIA显存GPU |
| 硬件(14B模型) | 32GB可用内存 | 32GB内存 + 16GB以上NVIDIA显存GPU |
| 网络 | 部署阶段需外网拉取模型和安装包,运行阶段完全离线 | 同左 |
二、全流程环境部署(一步不缺,新手可直接照搬)
2.1 Elasticsearch 9.x 本地部署与初始化
核心目的:搭建本地存储与检索引擎,完成安全配置与凭证生成,为后续数据写入和查询做准备
2.1.1 下载与安装
-
打开Elasticsearch官方下载页,选择对应系统的9.x最新稳定版安装包
-
解压到无中文、无空格、无特殊字符的本地路径:
-
Windows示例:
D:\elasticsearch-9.3.0 -
macOS/Linux示例:
/opt/elasticsearch-9.3.0
-
2.1.2 启动前置配置
| 操作系统 | 必做配置 |
|---|---|
| Windows | 1. 右键以管理员身份运行终端 2. 临时关闭杀毒软件实时防护(避免误杀ES进程) |
| macOS | 1. 打开「系统设置→隐私与安全性」,允许ES的开发者权限 2. 终端执行sudo xattr -r -d com.apple.quarantine ES解压目录,解决文件无法打开的问题 |
| Linux | 1. 执行sudo sysctl -w vm.max_map_count=262144,解决内存映射不足的启动报错 2. 禁止用root用户直接启动ES,需新建普通用户并给ES目录授权 |
2.1.3 启动ES并获取核心凭证
-
打开终端,进入ES解压后的
bin目录 -
执行启动命令:
-
Windows:
elasticsearch.bat -
macOS/Linux:
./elasticsearch
-
-
启动成功后,终端会输出3组核心信息,必须复制保存:
-
elastic超级用户的初始密码(示例:
elastic: abc123def456) -
HTTPS访问地址(默认:
https://localhost:9200) -
根CA证书路径(默认:
ES解压目录/config/certs/http_ca.crt)
-
-
验证启动成功:浏览器访问
https://localhost:9200,输入用户名elastic和上述密码,能返回ES版本信息的JSON数据,即为启动正常
2.1.4 生成专属API密钥(安全必备)
ES 9.x 推荐使用API密钥做身份认证,避免代码中硬编码账号密码,生成方式:
-
新建终端窗口,进入ES的
bin目录 -
执行以下命令,生成有效期30天的API密钥:
Bash# Windows elasticsearch-api-key.bat create "langextract-es-key" --expiration 30d # macOS/Linux ./elasticsearch-api-key create "langextract-es-key" --expiration 30d -
执行后会输出一串API密钥字符串,必须复制保存,后续代码会用到
2.2 本地DeepSeek大模型部署
核心目的:一键部署本地DeepSeek模型,提供OpenAI兼容的API接口,实现完全离线的文本提取能力,彻底摆脱云端依赖
2.2.1 安装Ollama模型部署工具
Ollama是开源的本地大模型一键部署工具,屏蔽了复杂的模型配置、环境依赖问题,对新手极度友好:
-
打开Ollama官方下载页,选择对应系统的安装包
-
按提示完成安装,安装后Ollama会自动在后台运行,并开机自启
2.2.2 拉取并运行DeepSeek模型
-
打开新的终端窗口,执行以下命令拉取适配硬件的DeepSeek模型(推荐先试7B版本,平衡速度与准确率):
Bash# 首选:DeepSeek-R1:7B(推理能力强,适合信息提取任务,8GB显存/16GB内存可跑) ollama pull deepseek-r1:7b # 高配置可选:DeepSeek-R1:14B(准确率更高,16GB显存/32GB内存可跑) # ollama pull deepseek-r1:14b # 低配置可选:DeepSeek-V3:7B(速度更快,适合简单提取场景) # ollama pull deepseek-v3:7b -
验证模型是否正常运行:
Bash# 进入模型交互界面 ollama run deepseek-r1:7b # 输入测试问题:你好,请介绍一下自己 # 能正常回复说明模型运行成功,输入 /exit 退出交互界面 -
核心特性 :Ollama会自动在后台启动OpenAI兼容的API服务 ,默认地址为
http://localhost:11434/v1,后续LangExtract会通过这个地址调用本地模型,无需额外配置
2.2.3 验证本地API接口(提前避坑)
执行以下curl命令,验证本地API服务是否正常(确保Ollama在后台运行):
Bash
curl http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-r1:7b",
"messages": [{"role": "user", "content": "你好"}],
"stream": false
}'
能正常返回JSON格式的响应内容,说明API服务正常,后续代码可正常调用。
2.3 Python依赖安装
-
本地新建项目文件夹(示例:
contract-extraction),作为项目根目录 -
打开终端,进入项目根目录,执行以下命令安装所有依赖:
Bash# 升级LangExtract到最新版(必须,旧版本不支持自定义OpenAI兼容API) pip install --upgrade langextract -q # 安装其他核心依赖 pip install elasticsearch python-dotenv -q -
依赖包作用说明:
-
langextract:核心结构化提取框架,适配本地大模型 -
elasticsearch:ES官方Python客户端,用于和本地ES服务通信 -
python-dotenv:环境变量管理工具,避免密钥硬编码在代码中,提升安全性
-
三、核心工作原理详解(先懂原理,再做实操,少走90%的弯路)
3.1 整体工作闭环流程
Plain
本地合同文本文件 → LangExtract按预设规则调用本地DeepSeek提取结构化数据 → 结果校验与格式转换 → 批量写入Elasticsearch 9.x → 按需执行多条件筛选/全文检索/统计分析
全程数据不出本地环境,部署完成后可完全离线运行。
3.2 核心组件原理解释
3.2.1 LangExtract:解决纯大模型提取的核心痛点
LangExtract是Google开源的信息提取框架,底层基于大模型,但针对结构化提取场景做了深度优化,和纯大模型直接提取的核心差异如下:
| 纯大模型直接提取 | LangExtract + 本地DeepSeek |
|---|---|
| 输出格式不稳定,经常不按要求返回结构化数据 | 强制输出标准化的字段+值结构,格式100%可控 |
| 结果无法追溯,不知道数据来自原文哪个位置,极易出现幻觉 | 每个提取结果都绑定原文的字符位置,支持可视化溯源,从根源上杜绝编造数据 |
| 准确率完全依赖prompt,稳定性极差 | 支持少样本示例学习,给1-3个示例就能让模型对齐提取规则,准确率大幅提升 |
| 长文本处理能力弱,容易遗漏关键信息 | 原生支持长上下文、多轮提取补全、多进程并行处理 |
| 它的核心工作逻辑: |
-
你通过「prompt描述+少样本示例」,明确告诉它要提取的字段、格式规则、归一化要求
-
它调用本地DeepSeek模型,严格按规则从文本中提取对应内容,同时记录每个内容在原文中的字符位置
-
自动完成格式校验、数据归一化(比如日期统一为
MM/dd/yyyy、月份转天数、金额去符号) -
输出可追溯的结构化结果,支持HTML可视化校验
3.2.2 Elasticsearch 9.x 索引设计原理
ES的索引相当于MySQL的表,映射(mapping)相当于表结构,字段类型的选型直接决定了数据写入的成功率和查询效率,本方案的选型逻辑如下:
| 字段类型 | 核心特性 | 本方案选型规则 |
|---|---|---|
date |
支持时间范围筛选、排序,必须指定日期格式 | 合同签订日期、到期日期,统一指定格式为MM/dd/yyyy,和LangExtract的提取格式完全对齐 |
keyword |
不分词的精准匹配类型,支持精准筛选、聚合、排序 | 仅用于精准筛选的字段(服务类型、适用法律),不需要分词搜索 |
text + keyword |
双字段类型,既支持全文分词搜索,又支持精准筛选 | 合同双方主体名称(既支持模糊搜索,又支持按完整名称筛选) |
float |
浮点数值类型,支持数值范围筛选 | 付款金额,适配带小数的金额场景 |
integer |
整数数值类型,支持数值范围筛选,占用空间更小、查询更快 | 交付天数,均为整数场景 |
text |
全文分词类型,支持模糊搜索、语义匹配 | 原始合同文本,支持合同全文检索 |
3.2.3 本地DeepSeek调用逻辑
LangExtract原生支持OpenAI兼容的API接口,因此只需通过3个核心参数,即可无缝对接本地部署的DeepSeek模型,无需修改核心提取逻辑:
-
model_id:指定本地部署的模型名称(和Ollama拉取的模型名称完全一致) -
api_base:指定本地Ollama提供的API服务地址(默认http://localhost:11434/v1) -
api_key:本地模型无需真实密钥,填写任意占位符即可(示例:ollama-local-key)
四、完整代码实现(可直接复制运行,全流程闭环)
4.1 项目目录结构
先按以下结构创建文件夹和文件,确保代码能正常运行:
Plain
contract-extraction/
├── contracts/ # 存放所有合同txt文件
├── .env # 环境变量配置文件(密钥、路径)
└── main.py # 核心运行代码
4.2 环境变量配置文件(.env)
在项目根目录新建.env文件,粘贴以下内容,替换为你自己的配置:
Plain
# --------------------------
# Elasticsearch 9.x 配置
# --------------------------
# ES本地访问地址,默认https://localhost:9200
ELASTICSEARCH_URL=https://localhost:9200
# 你之前生成的ES API密钥
ELASTICSEARCH_API_KEY=你的ES API密钥
# ES根CA证书的绝对路径,在ES解压目录的config/certs/http_ca.crt
# Windows示例:ES_CA_CERTS_PATH=D:\\elasticsearch-9.3.0\\config\\certs\\http_ca.crt
# macOS/Linux示例:ES_CA_CERTS_PATH=/opt/elasticsearch-9.3.0/config/certs/http_ca.crt
ES_CA_CERTS_PATH=你的ES CA证书绝对路径
# --------------------------
# 本地DeepSeek模型配置
# --------------------------
# Ollama提供的OpenAI兼容API地址,默认无需修改
LANGEXTRACT_API_BASE=http://localhost:11434/v1
# 本地模型无需真实密钥,任意占位符即可
LANGEXTRACT_API_KEY=ollama-local-key
# 你拉取的DeepSeek模型名称,必须和ollama pull的名称完全一致
LANGEXTRACT_MODEL_ID=deepseek-r1:7b
4.3 核心运行代码(<main.py>)
在项目根目录新建main.py文件,粘贴以下完整代码,每一行都带详细注释,可直接运行:
Python
# --------------------------
# 1. 导入所需依赖库
# --------------------------
from elasticsearch import Elasticsearch, helpers
import os
from dotenv import load_dotenv
import langextract as lx
import glob
import json
# --------------------------
# 2. 加载环境变量 & 全局配置
# --------------------------
# 加载.env文件中的配置,避免密钥硬编码
load_dotenv()
# 全局配置,统一管理,后续修改仅需调整此处
INDEX_NAME = "contracts" # ES索引名称,相当于MySQL的表名
CONTRACTS_DIR = "./contracts" # 合同文件存放目录
NDJSON_FILE = "extraction_results.ndjson" # 提取结果保存文件
# 本地DeepSeek配置(从.env读取)
LANGEXTRACT_API_BASE = os.getenv("LANGEXTRACT_API_BASE")
LANGEXTRACT_API_KEY = os.getenv("LANGEXTRACT_API_KEY")
LANGEXTRACT_MODEL_ID = os.getenv("LANGEXTRACT_MODEL_ID")
# --------------------------
# 3. Elasticsearch客户端初始化 & 索引创建
# --------------------------
# 初始化ES客户端,和本地ES服务建立连接
es_client = Elasticsearch(
hosts=os.getenv("ELASTICSEARCH_URL"),
api_key=os.getenv("ELASTICSEARCH_API_KEY"),
ca_certs=os.getenv("ES_CA_CERTS_PATH"),
# 本地测试可临时关闭证书验证,生产环境必须开启
# verify_certs=False
)
# 验证ES连接是否正常
if not es_client.ping():
raise Exception("❌ ES连接失败,请检查地址、API密钥、证书路径是否正确")
print("✅ ES连接成功")
# 定义ES索引映射(表结构),严格和提取字段对齐
index_mapping = {
"mappings": {
"properties": {
"contract_date": {"type": "date", "format": "MM/dd/yyyy"},
"end_contract_date": {"type": "date", "format": "MM/dd/yyyy"},
"service_provider": {
"type": "text",
"fields": {"keyword": {"type": "keyword", "ignore_above": 256}}
},
"client": {
"type": "text",
"fields": {"keyword": {"type": "keyword", "ignore_above": 256}}
},
"service_type": {"type": "keyword"},
"payment_amount": {"type": "float"},
"delivery_time_days": {"type": "integer"},
"governing_law": {"type": "keyword"},
"raw_contract": {"type": "text"}
}
}
}
# 创建索引(仅当索引不存在时创建,避免重复创建报错)
if not es_client.indices.exists(index=INDEX_NAME):
es_client.indices.create(index=INDEX_NAME, body=index_mapping)
print(f"✅ 索引 {INDEX_NAME} 创建成功")
else:
print(f"ℹ️ 索引 {INDEX_NAME} 已存在,无需重复创建")
# --------------------------
# 4. LangExtract提取规则配置(核心,决定提取准确率)
# --------------------------
# 提取prompt:清晰、无歧义,明确提取规则、格式要求、禁止项
contract_prompt_description = """
从合同文本中提取以下结构化信息,严格遵守所有规则:
1. 必须提取的字段:contract_date、end_contract_date、service_provider、client、service_type、payment_amount、delivery_time_days、governing_law
2. 日期格式统一为 MM/dd/yyyy,例如2025年2月2日必须转为02/02/2025
3. 付款金额仅保留数字,去掉$符号、千分位逗号,例如$3,200必须转为3200
4. 交付时间统一转为天数,例如3个月转为90天,1年转为365天
5. 所有提取内容必须100%来自原文,禁止编造、补充任何原文没有的信息,找不到的字段留空
6. 按文本中信息出现的顺序提取
"""
# 少样本示例:给模型做示范,是提升提取准确率的核心,可根据你的合同格式补充更多示例
contract_examples = [
lx.data.ExampleData(
# 示例合同原文
text="Service Agreement dated March 10, 2024, between ABC Corp (Service Provider) and John Doe (Client) for consulting services. Payment: $5,000. Delivery: 30 days. Contract ends June 10, 2024. Governed by California law.",
# 预期提取结果,严格和prompt中的字段对齐
extractions=[
lx.data.Extraction(extraction_class="contract_date", extraction_text="03/10/2024"),
lx.data.Extraction(extraction_class="end_contract_date", extraction_text="06/10/2024"),
lx.data.Extraction(extraction_class="service_provider", extraction_text="ABC Corp"),
lx.data.Extraction(extraction_class="client", extraction_text="John Doe"),
lx.data.Extraction(extraction_class="service_type", extraction_text="consulting services"),
lx.data.Extraction(extraction_class="payment_amount", extraction_text="5000"),
lx.data.Extraction(extraction_class="delivery_time_days", extraction_text="30"),
lx.data.Extraction(extraction_class="governing_law", extraction_text="California"),
],
)
]
# --------------------------
# 5. 批量读取合同文件 & 调用本地DeepSeek提取数据
# --------------------------
# 匹配合同目录下所有txt格式的合同文件
contract_files = glob.glob(os.path.join(CONTRACTS_DIR, "*.txt"))
# 检查是否找到合同文件
if len(contract_files) == 0:
raise Exception(f"❌ 在 {CONTRACTS_DIR} 目录下未找到任何txt合同文件,请检查路径")
print(f"📄 找到 {len(contract_files)} 个合同文件")
# 批量提取合同数据
extraction_results = []
for file_path in contract_files:
filename = os.path.basename(file_path)
print(f"\n🔄 正在处理文件:{filename}")
# 读取合同文件内容,指定utf-8编码避免乱码
with open(file_path, "r", encoding="utf-8") as f:
contract_content = f.read()
# 调用本地DeepSeek模型执行提取
contract_result = lx.extract(
text_or_documents=contract_content,
prompt_description=contract_prompt_description,
examples=contract_examples,
# 核心参数:对接本地DeepSeek模型
model_id=LANGEXTRACT_MODEL_ID,
api_base=LANGEXTRACT_API_BASE,
api_key=LANGEXTRACT_API_KEY,
# 提取参数优化:降低温度,让输出更稳定,信息提取场景推荐0.05-0.1
temperature=0.1,
)
extraction_results.append(contract_result)
print(f"✅ {filename} 处理完成,提取到 {len(contract_result.extractions)} 个字段")
# 保存提取结果到NDJSON文件,方便后续写入ES和校验
lx.io.save_annotated_documents(extraction_results, output_name=NDJSON_FILE, output_dir=".")
print(f"\n💾 提取结果已保存到 {NDJSON_FILE}")
# 生成提取结果可视化HTML文件,可打开查看每个提取结果对应的原文位置,校验是否准确
html_content = lx.visualize(NDJSON_FILE)
with open("extraction_visualization.html", "w", encoding="utf-8") as f:
f.write(html_content.data)
print("📊 提取结果可视化文件已生成:extraction_visualization.html,打开即可校验提取准确率")
# --------------------------
# 6. 批量写入提取结果到Elasticsearch
# --------------------------
def build_es_documents(ndjson_file, index_name):
"""
构建ES批量写入的文档生成器
核心作用:做数据类型转换,解决字符串写入数值/日期字段的报错问题;用生成器减少内存占用
"""
with open(ndjson_file, "r", encoding="utf-8") as f:
for line in f:
doc = json.loads(line)
es_doc = {}
# 遍历提取结果,转换为ES文档的字段和值
for extraction in doc["extractions"]:
field_name = extraction["extraction_class"]
field_value = extraction["extraction_text"]
# 数据类型转换,严格和ES映射中的类型对齐
try:
if field_name == "payment_amount":
es_doc[field_name] = float(field_value)
elif field_name == "delivery_time_days":
es_doc[field_name] = int(field_value)
else:
es_doc[field_name] = field_value
except Exception as e:
print(f"⚠️ 字段 {field_name} 类型转换失败,值:{field_value},错误:{e}")
es_doc[field_name] = None
# 写入原始合同文本,支持全文检索
es_doc["raw_contract"] = doc["text"]
# 生成ES bulk API要求的格式
yield {"_index": index_name, "_source": es_doc}
# 执行批量写入
try:
success_count, error_list = helpers.bulk(
client=es_client,
actions=build_es_documents(NDJSON_FILE, INDEX_NAME),
chunk_size=100 # 批次大小,合同量大可调整
)
print(f"\n✅ 批量写入完成,成功写入 {success_count} 个文档")
if error_list:
print(f"⚠️ 写入错误详情:{error_list}")
except Exception as e:
print(f"❌ 批量写入失败,错误:{str(e)}")
# --------------------------
# 7. ES数据查询示例(覆盖常用业务场景)
# --------------------------
print("\n==================== 🔍 数据查询结果 ====================")
# 场景1:查询已过期、付款金额≥15000的合同
print("\n1. 已过期、付款金额≥15000的合同:")
try:
response = es_client.search(
index=INDEX_NAME,
source_excludes=["raw_contract"], # 排除原始合同文本,让结果更简洁
body={
"query": {
"bool": {
"filter": [
{"range": {"payment_amount": {"gte": 15000}}},
{"range": {"end_contract_date": {"lte": "now"}}},
]
}
}
}
)
print(f"匹配到 {response['hits']['total']['value']} 个合同:")
for hit in response["hits"]["hits"]:
print(json.dumps(hit["_source"], indent=4, ensure_ascii=False))
except Exception as e:
print(f"查询失败:{str(e)}")
# 场景2:按服务提供方精准查询
print("\n2. 服务提供方为GreenLeaf Landscaping Co.的合同:")
try:
response = es_client.search(
index=INDEX_NAME,
source_excludes=["raw_contract"],
body={
"query": {
"term": {"service_provider.keyword": "GreenLeaf Landscaping Co."}
}
}
)
print(f"匹配到 {response['hits']['total']['value']} 个合同:")
for hit in response["hits"]["hits"]:
print(json.dumps(hit["_source"], indent=4, ensure_ascii=False))
except Exception as e:
print(f"查询失败:{str(e)}")
# 场景3:全文检索合同内容中的关键词
print("\n3. 合同内容包含'landscaping'的合同:")
try:
response = es_client.search(
index=INDEX_NAME,
source_excludes=["raw_contract"],
body={
"query": {
"match": {"raw_contract": "landscaping"}
}
}
)
print(f"匹配到 {response['hits']['total']['value']} 个合同:")
for hit in response["hits"]["hits"]:
print(json.dumps(hit["_source"], indent=4, ensure_ascii=False))
except Exception as e:
print(f"查询失败:{str(e)}")
4.4 测试合同文件准备
在contracts文件夹中,新建contract1.txt文件,粘贴以下测试合同内容,也可以添加自己的合同文件:
Plain
This Contract Agreement ("Agreement") is made and entered into on February 2, 2025, by and between:
* Contractor: GreenLeaf Landscaping Co.
* Contractee: Robert Jenkins
Purpose: Garden maintenance and landscaping for private residence.
Terms and Conditions:
1. The Client agrees to pay the Service Provider the sum of $3,200 for the services.
2. The Service Provider agrees to provide landscaping and maintenance services for a period of 3 months.
3. This Agreement shall terminate on May 2, 2025.
4. This Agreement shall be governed by the laws of California.
5. Both parties accept the conditions stated herein.
Signed:
GreenLeaf Landscaping Co.
Robert Jenkins
五、运行与结果验证
5.1 前置服务检查
运行代码前,必须确保:
-
Elasticsearch服务正在后台运行,能正常访问
https://localhost:9200 -
Ollama服务正在后台运行,执行
ollama list能看到你拉取的DeepSeek模型 -
项目目录结构正确,
.env文件配置无误,contracts文件夹中有txt合同文件
5.2 运行代码
打开终端,进入项目根目录,执行以下命令运行完整代码:
Bash
python main.py
5.3 运行结果验证
运行完成后,你将得到以下结果,可逐一验证:
-
终端输出:依次输出ES连接状态、找到的合同文件、每个文件的提取进度、写入结果、3个场景的查询结果
-
提取结果文件 :项目根目录生成
extraction_results.ndjson,包含所有合同的结构化提取结果 -
可视化校验文件 :项目根目录生成
extraction_visualization.html,用浏览器打开即可看到每个提取结果对应的原文高亮位置,验证提取是否准确、有无幻觉 -
ES数据验证 :可通过Kibana或ES API查询
contracts索引,确认数据已成功写入,字段类型正确
六、全场景常见问题排查与避坑指南
6.1 Elasticsearch相关问题
| 问题现象 | 根因与解决方案 |
|---|---|
| ES连接失败 | 1. 检查ES服务是否正常启动 2. 检查API密钥是否正确、是否过期 3. 检查CA证书路径是否正确,Windows路径需用双反斜杠 4. 本地测试可临时开启verify_certs=False关闭证书验证 |
| 索引创建失败 | 1. 检查索引是否已存在,已存在需先删除再创建 2. 检查mapping语法是否正确,字段类型是否为ES支持的类型 |
| 数据写入报错 | 90%是类型不匹配:检查提取的字段值是否能转换为ES映射中的类型,比如日期格式是否和format一致,数值是否有非数字字符 |
| 查询不到数据 | 1. 确认数据已成功写入,终端输出成功写入的文档数>0 2. 检查字段名是否拼写正确,精准查询是否加了.keyword后缀 3. ES写入有1秒刷新间隔,可手动执行es_client.indices.refresh(index=INDEX_NAME)刷新 |
6.2 本地DeepSeek相关问题
| 问题现象 | 根因与解决方案 |
|---|---|
| LangExtract连接本地API失败 | 1. 检查Ollama是否在后台运行,浏览器访问http://localhost:11434能看到欢迎页 2. 检查api_base是否正确,必须带/v1后缀 3. 检查模型名称是否和ollama list中的名称完全一致 |
| 模型推理速度极慢 | 1. CPU运行:仅用7B模型,关闭其他占用内存的程序 2. GPU加速:安装CUDA和cuDNN,Ollama会自动识别NVIDIA GPU,速度提升5-10倍 3. 减少并发,单文件依次处理 |
| 提取准确率低、结果乱 | 1. 降低temperature到0.05-0.1,让输出更稳定 2. 增加2-3个贴合你合同格式的少样本示例 3. 优化prompt,把提取规则写得更明确,增加禁止编造的强调 4. 硬件允许的话,更换14B/32B更大的模型 |
| 模型加载失败、闪退 | 1. 检查可用内存是否足够,7B模型至少需要8GB可用内存 2. 清理Ollama缓存,重新拉取模型 3. 关闭其他占用内存的程序,释放系统资源 |
6.3 代码运行相关问题
| 问题现象 | 根因与解决方案 |
|---|---|
| 找不到合同文件 | 检查CONTRACTS_DIR路径是否正确,合同文件是否为.txt格式 |
| 依赖安装失败 | 1. 升级pip到最新版:pip install --upgrade pip 2. 更换国内pip镜像源:pip install 包名 -i https://pypi.tuna.tsinghua.edu.cn/simple 3. 检查Python版本是否在3.10~3.12之间 |
| 合同文本乱码 | 读取文件时指定encoding="utf-8",确保合同文件的编码为UTF-8 |
七、进阶优化方案
7.1 推理速度优化:vLLM部署DeepSeek
如果需要批量处理成百上千份合同,Ollama的速度可能无法满足需求,可使用vLLM部署DeepSeek模型,推理速度比Ollama快2-4倍,支持更高并发:
-
安装vLLM:
pip install vllm -
启动vLLM的OpenAI兼容API服务:
Bashvllm serve deepseek-ai/DeepSeek-R1-7B --port 8000 -
修改
.env文件中的配置:PlainLANGEXTRACT_API_BASE=http://localhost:8000/v1 LANGEXTRACT_MODEL_ID=deepseek-ai/DeepSeek-R1-7B
7.2 提取准确率优化
-
少样本优化:准备5-10份贴合你业务场景的标注合同示例,覆盖不同格式、不同边界场景,准确率可提升30%以上
-
多轮提取优化 :在
lx.extract中设置num_passes=2,让模型两轮提取补全遗漏的信息,提升召回率 -
模型微调:用10-50份标注数据,通过LLaMA Factory对DeepSeek做LoRA微调,适配特定行业的合同术语,提取准确率会大幅提升
-
规则校验:增加自定义后处理规则,比如日期合法性校验、金额大于0校验、主体名称非空校验,不符合规则的自动重新提取
7.3 功能扩展
-
多格式文件支持 :通过
pdfplumber、python-docx库,实现PDF、Word格式合同的文本自动提取,无需手动转为txt -
完全离线向量搜索:用本地开源Embedding模型(BGE-M3、M3E)生成合同文本的向量,结合ES 9.x的向量搜索能力,实现完全离线的语义级合同检索
-
多进程并行处理 :在批量提取时开启
num_workers多进程,同时处理多份合同,大幅提升批量处理效率 -
合同风险预警:在提取规则中增加风险条款提取,实现合同风险自动识别与预警