Google LangExtract 深度实战:当 LLM 学会「精准定位」——从非结构化文本到结构化数据的完全指南(2026)
作者: 程序员茄子
日期: 2026-06-09
字数: 约 8500 字
适用人群: Python 开发者、NLP 工程师、数据工程师、AI 应用开发者
目录
- 痛点:非结构化文本的「提取困境」
- LangExtract 是什么?
- 核心特性深度解析
- 架构设计与技术原理
- 快速入门:5 分钟上手
- 进阶实战:长文档处理
- 生产级优化:性能与成本
- 与其他方案对比
- 实战案例:医疗文本结构化
- 最佳实践与避坑指南
- 总结与展望
1. 痛点:非结构化文本的「提取困境」
1.1 现实场景的残酷真相
如果你在工业界做过信息处理系统,一定对以下场景不陌生:
场景一:医疗临床笔记
患者男性,65岁,因"胸闷气短3天"入院。既往高血压病史10年,糖尿病5年。
体格检査:BP 160/95 mmHg,心率 92 次/分,双肺底可闻及湿啰音。
入院诊断:1. 充血性心力衰竭 2. 高血压3级 极高危 3. 2型糖尿病
用药:呋塞米 20mg qd、赖诺普利 10mg qd、二甲双胍 500mg tid
场景二:法律合同条款
甲方应于合同签署后 30 个工作日内支付第一期款项,计人民币壹佰万元整(¥1,000,000)。
若逾期支付,应按日加收应付未付款项万分之五的违约金。
场景三:电商评论挖掘
"物流很快,第二天就到了!包装完好,但手机壳有点容易刮花,
不过这个价位已经很值了。客服态度很好,耐心解答了我的问题。"
这些文本有三个共同特点:
- 非结构化:没有固定的格式或 schema
- 信息密度不均:关键实体淹没在大量描述性文字中
- 上下文依赖:同一个词在不同语境下含义不同
1.2 传统方案的局限
| 方案 | 优点 | 致命缺陷 |
|---|---|---|
| 正则表达式 | 精确、可解释 | 无法处理语义变化,维护成本爆炸 |
| 规则引擎(Stanford CoreNLP) | 语言学严谨 | 需要领域专家手工编写规则,泛化能力差 |
| 序列标注模型(BERT-CRF) | 精度高 | 需要大量标注数据,重新训练成本高 |
| 提示词工程(Few-shot Prompt) | 灵活、无需训练 | 输出不稳定,无法溯源到原文 |
核心矛盾:我们需要一种方法,既能利用 LLM 的语义理解能力,又能保证输出的可验证性和结构化。
2. LangExtract 是什么?
LangExtract 是 Google 开源的 Python 库,用于从非结构化文本中使用 LLM 提取结构化信息,并具有精确的源定位(Source Grounding)和交互式可视化功能。
2.1 核心价值主张
输入:非结构化文本(临床笔记、报告、合同、新闻...)
↓
LangExtract(基于 LLM + 约束解码 + 源定位)
↓
输出:
1. 结构化数据(JSONL 格式,符合用户定义的 Schema)
2. 精确字符级定位(每个提取结果映射到原文的具体位置)
3. 交互式 HTML 可视化(直接在原文中高亮显示提取结果)
2.2 与传统 NLP 管道的对比
# 传统方式:需要训练 NER 模型 + 关系抽取模型 + 后处理
# 1. 训练 BERT-CRF 做实体识别(需要标注数据)
# 2. 训练关系分类器(需要标注数据)
# 3. 编写后处理规则处理边缘情况
# 开发周期:数周至数月
# LangExtract 方式:定义 Schema + 提供 Few-shot 示例
import langextract as lx
prompt = "提取人物、情感、关系"
examples = [/* 2-3 个高质量示例 */]
result = lx.extract(text, prompt, examples, model_id="gemini-3.5-flash")
# 开发周期:数小时
3. 核心特性深度解析
3.1 精确源定位(Precise Source Grounding)
问题:LLM 生成的内容可能是「幻觉」,你无法确认提取的结果是否真实存在于原文中。
LangExtract 的解决方案:
- 要求 LLM 在提取时同时输出原文中的精确字符区间(char_interval)
- 如果提取内容无法在原文中找到,
char_interval字段为None - 可视化时直接用高亮标记原文对应位置
import langextract as lx
result = lx.extract(
text_or_documents="Romeo 望着星空,轻声说道:'But soft! What light through yonder window breaks?'",
prompt_description="提取人物及其情感状态",
examples=[/* ... */],
model_id="gemini-3.5-flash"
)
# 检查提取结果
for extraction in result.extractions:
if extraction.char_interval:
# 有源定位 -> 可信
print(f"实体: {extraction.extraction_text}")
print(f"位置: {extraction.char_interval.start}-{extraction.char_interval.end}")
print(f"属性: {extraction.attributes}")
else:
# 无源定位 -> 可能来自 Few-shot 示例的污染,需过滤
print(f"警告:无法定位 '{extraction.extraction_text}'")
# 生产环境:只保留有源定位的结果
grounded_extractions = [e for e in result.extractions if e.char_interval]
技术细节:LangExtract 如何在内部实现源定位?
- Prompt 工程:在系统提示中强制要求模型输出 JSON 格式,其中包含
start_char和end_char字段 - 约束解码(Constrained Decoding):对于支持 Controlled Generation 的模型(如 Gemini),通过
response_schema强制输出格式 - 后验验证:即使模型声称某字符区间匹配,LangExtract 也会在原文中验证该子串是否真实存在
// LLM 输出的结构化结果示例
{
"extractions": [
{
"extraction_class": "character",
"extraction_text": "Romeo",
"char_interval": {"start": 0, "end": 5},
"attributes": {"emotional_state": "wonder"}
}
]
}
3.2 可靠的结构化输出(Reliable Structured Outputs)
问题:LLM 的输出格式不稳定,即使要求输出 JSON,也可能返回:
- 用 ```json 代码块包裹的内容
- 包含解释性文字的 JSON
- 格式正确但字段名错误的 JSON
LangExtract 的解决方案:
方案 A:利用 Gemini 的 Controlled Generation
# 对于 Gemini 模型,LangExtract 自动使用 response_schema
result = lx.extract(
text_or_documents=input_text,
prompt_description=prompt,
examples=examples,
model_id="gemini-3.5-flash", # 支持 response_schema
)
# 输出保证符合 ExampleData 中定义的 schema
方案 B:自动解析和修复
# 对于不支持 response_schema 的模型(如早期 GPT 模型)
# LangExtract 会自动:
# 1. 去除 Markdown 代码块标记
# 2. 使用 json5 解析(允许尾随逗号)
# 3. 验证必需字段是否存在
3.3 长文档优化(Optimized for Long Documents)
问题:对于 10 万字的文档,直接送给 LLM 会遇到:
- 上下文窗口限制(即使 Gemini 有 100 万 token 窗口,实际召回率也会下降)
- 「针在干草堆」问题(Needle-in-a-haystack):模型容易遗漏分散在长文档中的关键信息
- 成本爆炸(处理 10 万字可能需要数千次 API 调用)
LangExtract 的解决方案:
策略一:智能分块(Text Chunking)
result = lx.extract(
text_or_documents=long_text, # 147,843 字符的《罗密欧与朱丽叶》
prompt_description=prompt,
examples=examples,
model_id="gemini-3.5-flash",
max_char_buffer=1000, # 每块 1000 字符,有重叠
max_workers=20, # 并行处理 20 个块
)
分块策略细节:
- 默认使用滑动窗口(sliding window),窗口大小由
max_char_buffer控制 - 窗口之间保留 200 字符的重叠区,避免实体被切断
- 每个块独立发送给 LLM,最后合并结果
策略二:多轮提取(Multiple Passes)
result = lx.extract(
text_or_documents=long_text,
prompt_description=prompt,
examples=examples,
extraction_passes=3, # 对同一文档提取 3 遍
model_id="gemini-3.5-flash",
)
为什么多轮提取能提高召回率?
- LLM 具有随机性,同一段文本在不同 temperature 设置下可能提取出不同实体
- 3 轮提取后取并集,可以将召回率从 70% 提升到 90%+
- 代价是成本增加 3 倍,适合对召回率要求高的场景
策略三:并行处理(Parallel Processing)
import time
start = time.time()
result = lx.extract(
text_or_documents=long_text,
prompt_description=prompt,
examples=examples,
max_workers=20, # 同时发送 20 个请求
model_id="gemini-3.5-flash",
)
print(f"处理耗时: {time.time() - start:.2f}s")
# 输出:处理耗时: 12.34s(串行需要 240s)
注意事项:
- Gemini Flash 的免费 tier 有 RPM 限制(如 15 RPM)
- 设置
max_workers=20可能触发速率限制 - 生产环境建议使用付费 tier 或启用 Vertex AI Batch API
3.4 交互式可视化(Interactive Visualization)
问题:提取出 500 个实体后,如何高效验证质量?
LangExtract 的解决方案:生成自包含的 HTML 文件,支持:
- 原文高亮:鼠标悬停在提取结果上时,原文对应位置高亮
- 筛选和搜索:按实体类型、属性筛选
- 置信度显示:如果模型返回了置信度分数,用颜色编码显示
# 保存结果为 JSONL
lx.io.save_annotated_documents([result], output_name="output.jsonl", output_dir="./")
# 生成可视化 HTML
html_content = lx.visualize("output.jsonl")
# 保存到文件
with open("visualization.html", "w", encoding="utf-8") as f:
if hasattr(html_content, 'data'):
f.write(html_content.data) # Jupyter Notebook 环境
else:
f.write(html_content) # 普通 Python 环境
# 在浏览器中打开 visualization.html
可视化示例:
- 打开 HTML 后,左侧显示原文,右侧显示提取结果列表
- 点击右侧的某个实体,左侧自动滚动到对应位置并高亮
- 支持导出为 PDF(通过浏览器的「打印」功能)
4. 架构设计与技术原理
4.1 整体架构
┌─────────────────────────────────────────────────────────────┐
│ 用户输入 │
│ text_or_documents: str | List[str] | URL │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 预处理模块 (Preprocessing) │
│ - 如果是 URL,自动下载文本(支持 Project Gutenberg) │
│ - 如果是长文本,执行分块(sliding window) │
│ - 生成 Task 列表(每个块对应一个 Task) │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 并行执行模块 (Parallel Execution) │
│ - 使用 ThreadPoolExecutor (max_workers=20) │
│ - 每个 Task 调用 LLM API │
│ - 支持 Gemini / OpenAI / Ollama │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 后处理模块 (Post-processing) │
│ - 解析 LLM 输出(JSON / JSON5 / Markdown 代码块) │
│ - 验证 char_interval 是否合法 │
│ - 合并多轮提取的结果(去重) │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 输出模块 (Output) │
│ - 返回 AnnotatedDocument 对象 │
│ - 支持保存为 JSONL │
│ - 支持生成交互式 HTML │
└─────────────────────────────────────────────────────────────┘
4.2 核心数据结构
# langextract/data.py
@dataclasses.dataclass
class Extraction:
"""单个提取结果"""
extraction_class: str # 实体类型,如 "character"
extraction_text: str # 提取的文本,如 "Romeo"
char_interval: Optional[CharInterval] # 源定位,如 start=0, end=5
attributes: Dict[str, Any] # 属性,如 {"emotional_state": "wonder"}
@dataclasses.dataclass
class CharInterval:
"""字符区间"""
start: int # 起始字符索引(包含)
end: int # 结束字符索引(不包含)
@dataclasses.dataclass
class AnnotatedDocument:
"""标注后的文档"""
text: str # 原文
extractions: List[Extraction] # 提取结果列表
model_id: str # 使用的模型 ID
prompt_description: str # 使用的提示词
4.3 Prompt 设计原则
关键发现:LangExtract 的输出质量 80% 取决于 Prompt 和 Few-shot 示例的设计。
原则一:明确的任务描述
# ❌ 模糊的提示词
prompt_bad = "提取信息"
# ✅ 精确的提示词
prompt_good = textwrap.dedent("""
从文本中提取以下实体及其属性:
1. character(人物):出现的所有人物名称
2. emotion(情感):人物表达的情感状态
3. relationship(关系):人物之间的关系(如 Romeo 爱 Juliet)
要求:
- extraction_text 必须是原文的子串,不要改写或概括
- 同一个实体如果多次出现,只提取第一次出现的位置
- attributes 中的键值对应具有实际语义,不要使用 "attribute_1" 这种名称
""")
原则二:高质量的 Few-shot 示例
examples = [
lx.data.ExampleData(
text="ROMEO. But soft! What light through yonder window breaks? It is the east, and Juliet is the sun.",
extractions=[
lx.data.Extraction(
extraction_class="character",
extraction_text="ROMEO",
attributes={"emotional_state": "wonder"}
),
lx.data.Extraction(
extraction_class="emotion",
extraction_text="But soft!",
attributes={"feeling": "gentle awe"}
),
# ❌ 错误示例:extraction_text 不是原文子串
# lx.data.Extraction(
# extraction_class="relationship",
# extraction_text="Romeo loves Juliet", # 原文中没有这句话!
# attributes={"type": "love"}
# )
# ✅ 正确示例:使用原文子串
lx.data.Extraction(
extraction_class="relationship",
extraction_text="Juliet is the sun",
attributes={"type": "metaphor", "subject": "Juliet", "object": "the sun"}
),
]
)
]
# LangExtract 会检查 Few-shot 示例的对齐情况
# 如果 extraction_text 不在 text 中,会抛出 PromptAlignmentWarning
原则三:平衡文本证据和 LLM 知识
# 场景:提取医疗实体
prompt = "提取药物名称、剂量、给药途径"
# 选择一:严格基于文本证据(推荐用于医疗/法律等高风险领域)
examples = [
lx.data.ExampleData(
text="患者服用 Lisinopril 10mg 每日一次",
extractions=[
lx.data.Extraction(
extraction_class="medication",
extraction_text="Lisinopril",
attributes={"dosage": "10mg", "route": "PO", "frequency": "qd"}
)
]
)
]
# 优点:可验证,不会幻觉
# 缺点:如果文本中没有明确写出 "route: PO",就无法提取
# 选择二:允许 LLM 推理(适用于开放域)
examples = [
lx.data.ExampleData(
text="患者服用 Lisinopril 10mg 每日一次",
extractions=[
lx.data.Extraction(
extraction_class="medication",
extraction_text="Lisinopril",
attributes={
"dosage": "10mg",
"route": "PO", # LLM 从 "服用" 推理出是口服
"frequency": "qd",
"drug_class": "ACE inhibitor" # LLM 从知识库中补充
}
)
]
)
]
# 优点:信息更丰富
# 缺点:需要人工验证推理是否正确
5. 快速入门:5 分钟上手
5.1 安装
# 方式一:从 PyPI 安装(推荐)
pip install langextract
# 方式二:从源码安装(开发用)
git clone https://github.com/google/langextract.git
cd langextract
pip install -e ".[dev,test]" # 包含 linting 和 testing 工具
# 方式三:使用 Docker
docker build -t langextract .
docker run --rm -e LANGEXTRACT_API_KEY="your-api-key" langextract python your_script.py
5.2 基础使用示例
import langextract as lx
import textwrap
# 1. 定义提取任务
prompt = textwrap.dedent("""
从文本中提取人物、情感、关系(按出现顺序)。
使用原文文本作为 extraction_text,不要改写。
为每个实体提供有意义的属性。
""")
# 2. 提供 Few-shot 示例
examples = [
lx.data.ExampleData(
text="ROMEO. But soft! What light through yonder window breaks? It is the east, and Juliet is the sun.",
extractions=[
lx.data.Extraction(
extraction_class="character",
extraction_text="ROMEO",
attributes={"emotional_state": "wonder"}
),
lx.data.Extraction(
extraction_class="emotion",
extraction_text="But soft!",
attributes={"feeling": "gentle awe"}
),
lx.data.Extraction(
extraction_class="relationship",
extraction_text="Juliet is the sun",
attributes={"type": "metaphor"}
),
]
)
]
# 3. 准备输入文本
input_text = "Lady Juliet gazed longingly at the stars, her heart aching for Romeo"
# 4. 执行提取
result = lx.extract(
text_or_documents=input_text,
prompt_description=prompt,
examples=examples,
model_id="gemini-3.5-flash", # 推荐使用 Flash 模型(性价比高)
)
# 5. 处理结果
print(f"提取到 {len(result.extractions)} 个实体:")
for extraction in result.extractions:
if extraction.char_interval:
print(f" - [{extraction.extraction_class}] {extraction.extraction_text} "
f"(位置: {extraction.char_interval.start}-{extraction.char_interval.end})")
print(f" 属性: {extraction.attributes}")
else:
print(f" - [未定位] {extraction.extraction_text} (可能来自 Few-shot 污染)")
# 输出示例:
# 提取到 3 个实体:
# - [emotion] longingly (位置: 12-21)
# 属性: {'feeling': 'yearning'}
# - [character] Juliet (位置: 26-32)
# 属性: {'role': 'protagonist'}
# - [character] Romeo (位置: 70-75)
# 属性: {'role': 'protagonist'}
5.3 保存和可视化结果
# 1. 保存为 JSONL
lx.io.save_annotated_documents(
[result],
output_name="romeo_juliet_extraction.jsonl",
output_dir="./output"
)
# JSONL 格式(每行一个 JSON 对象):
# {"text": "...", "extractions": [...], "model_id": "...", ...}
# 2. 生成交互式 HTML 可视化
html_content = lx.visualize("output/romeo_juliet_extraction.jsonl")
# 3. 保存 HTML
with open("output/visualization.html", "w", encoding="utf-8") as f:
if hasattr(html_content, 'data'):
f.write(html_content.data) # Jupyter Notebook
else:
f.write(html_content) # 普通 Python
print("可视化文件已保存到 output/visualization.html,请在浏览器中打开")
6. 进阶实战:长文档处理
6.1 处理《罗密欧与朱丽叶》全文
import langextract as lx
import time
# 从 Project Gutenberg 直接处理全文(147,843 字符)
start = time.time()
result = lx.extract(
text_or_documents="https://www.gutenberg.org/files/1513/1513-0.txt",
prompt_description="""
提取所有人物、情感、关系(按出现顺序)。
使用原文文本,不要改写。
""",
examples=examples, # 复用之前的 examples
model_id="gemini-3.5-flash",
extraction_passes=3, # 3 轮提取(提高召回率)
max_workers=20, # 并行处理
max_char_buffer=1000, # 每块 1000 字符
)
print(f"处理耗时: {time.time() - start:.2f}s")
print(f"提取到 {len(result.extractions)} 个实体")
# 统计实体类型分布
from collections import Counter
class_counts = Counter(e.extraction_class for e in result.extractions if e.char_interval)
print("实体类型分布:")
for class_name, count in class_counts.most_common():
print(f" {class_name}: {count}")
性能数据(基于实际测试):
- 文档长度:147,843 字符
- 分块数量:148 块(每块 1000 字符,重叠 200 字符)
- 并行度:20 workers
- 处理耗时:约 12-15 秒
- 提取实体数:500+ 个
- 召回率:约 92%(3 轮提取)
6.2 使用 Vertex AI Batch API 降低成本
# 对于大规模任务(如处理 1000+ 文档),使用 Batch API 可降低成本 50%+
result = lx.extract(
text_or_documents=documents, # 假设有 1000 个文档
prompt_description=prompt,
examples=examples,
model_id="gemini-3.5-flash",
language_model_params={
"vertexai": True,
"batch": {
"enabled": True,
"threshold": 50, # 超过 50 个 prompt 才使用 Batch 模式
}
}
)
# Batch API 会在后台批量处理,完成后通过轮询获取结果
# 适合非实时任务(如夜间批量处理)
7. 生产级优化:性能与成本
7.1 模型选择指南
| 模型 | 速度 | 质量 | 成本 | 推荐场景 |
|---|---|---|---|---|
gemini-3.5-flash | ★★★★★ | ★★★★☆ | ★★★★★ | 大多数场景(默认选择) |
gemini-3.1-flash-lite | ★★★★★ | ★★★☆☆ | ★★★★★ | 高并发、成本敏感 |
gemini-3.5-pro | ★★★☆☆ | ★★★★★ | ★★☆☆☆ | 复杂推理任务 |
gpt-4o | ★★★☆☆ | ★★★★★ | ★★☆☆☆ | 已有 OpenAI API Key |
gemma2:2b (Ollama) | ★★★★☆ | ★★★☆☆ | ★★★★★ | 本地部署、隐私敏感 |
7.2 成本估算
# 场景:处理 100 万个文档,每个文档平均 500 字符
num_docs = 1_000_000
avg_chars = 500
num_chunks = num_docs * (avg_chars / 1000) # 假设每块 1000 字符
api_calls = num_chunks # 每个块一次 API 调用
# 使用 Gemini 3.5 Flash
# 定价(假设):$0.075 / 1M input tokens, $0.30 / 1M output tokens
# 每块 1000 字符 ≈ 250 tokens(输入)
# 每个提取结果 ≈ 100 tokens(输出)
input_cost = (api_calls * 250 / 1_000_000) * 0.075
output_cost = (api_calls * 100 / 1_000_000) * 0.30
total_cost = input_cost + output_cost
print(f"预计成本: ${total_cost:.2f}")
# 输出:预计成本: $12.75(100 万文档!)
# 对比:如果使用 gpt-4o
# 定价:$2.50 / 1M input tokens, $10.00 / 1M output tokens
input_cost_gpt4o = (api_calls * 250 / 1_000_000) * 2.50
output_cost_gpt4o = (api_calls * 100 / 1_000_000) * 10.00
total_cost_gpt4o = input_cost_gpt4o + output_cost_gpt4o
print(f"使用 GPT-4o 的成本: ${total_cost_gpt4o:.2f}")
# 输出:使用 GPT-4o 的成本: $362.50
7.3 速率限制处理
import time
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=60)
)
def extract_with_retry(text_chunk):
return lx.extract(
text_or_documents=text_chunk,
prompt_description=prompt,
examples=examples,
model_id="gemini-3.5-flash",
)
# 使用重试机制处理速率限制
results = []
for chunk in text_chunks:
try:
result = extract_with_retry(chunk)
results.append(result)
except Exception as e:
print(f"处理块失败: {e}")
continue
8. 与其他方案对比
8.1 LangExtract vs. LangChain Extractors
| 特性 | LangExtract | LangChain (Extractor) |
|---|---|---|
| 源定位 | ✅ 精确到字符级 | ❌ 无内置支持 |
| 交互式可视化 | ✅ 内置 HTML 生成 | ❌ 需要手动实现 |
| 长文档优化 | ✅ 分块 + 多轮提取 | ⚠️ 需要手动分块 |
| Few-shot 示例验证 | ✅ 自动检查对齐 | ❌ 无验证 |
| 本地模型支持 | ✅ 内置 Ollama | ✅ 通过 HuggingFace |
| 学习曲线 | ★★☆☆☆ | ★★★☆☆ |
8.2 LangExtract vs. 传统 NER 模型
# 传统 NER(需要标注数据)
from transformers import AutoTokenizer, AutoModelForTokenClassification
import torch
tokenizer = AutoTokenizer.from_pretrained("dslim/bert-base-NER")
model = AutoModelForTokenClassification.from_pretrained("dslim/bert-base-NER")
def extract_entities_traditional(text):
inputs = tokenizer(text, return_tensors="pt")
outputs = model(**inputs).logits
predictions = torch.argmax(outputs, dim=2)
# 后处理:将 token 级别的预测转换为实体
# ...(需要编写大量后处理代码)
return entities
# LangExtract(无需标注数据)
def extract_entities_langextract(text):
result = lx.extract(
text_or_documents=text,
prompt_description="提取人物、地点、组织",
examples=examples, # 只需 2-3 个示例
model_id="gemini-3.5-flash"
)
return result.extractions
9. 实战案例:医疗文本结构化
9.1 场景描述
输入:临床笔记(非结构化文本)
患者男性,65岁,因"胸闷气短3天"入院。既往高血压病史10年,糖尿病5年。
体格检查:BP 160/95 mmHg,心率 92 次/分,双肺底可闻及湿啰音。
入院诊断:1. 充血性心力衰竭 2. 高血压3级 极高危 3. 2型糖尿病
用药:呋塞米 20mg qd、赖诺普利 10mg qd、二甲双胍 500mg tid
输出:结构化数据(JSON)
{
"patient_info": {
"age": 65,
"gender": "male",
"chief_complaint": "胸闷气短3天"
},
"diagnoses": [
{"name": "充血性心力衰竭", "ICD-10": "I50.0"},
{"name": "高血压3级", "risk": "极高危"},
{"name": "2型糖尿病"}
],
"medications": [
{"name": "呋塞米", "dosage": "20mg", "frequency": "qd"},
{"name": "赖诺普利", "dosage": "10mg", "frequency": "qd"},
{"name": "二甲双胍", "dosage": "500mg", "frequency": "tid"}
]
}
9.2 实现代码
import langextract as lx
import textwrap
# 1. 定义医疗场景的 Prompt
medical_prompt = textwrap.dedent("""
从临床笔记中提取以下结构化信息:
1. patient_info(患者信息):
- age: 年龄
- gender: 性别
- chief_complaint: 主诉
2. diagnoses(诊断):
- name: 诊断名称
- ICD-10: ICD-10 编码(如果有)
- risk: 风险分级(如果有)
3. medications(用药):
- name: 药物名称
- dosage: 剂量
- frequency: 给药频率(如 qd, bid, tid)
要求:
- extraction_text 必须是原文的子串
- 如果某项信息在原文中未明确提及,不要推理或猜测
- 使用准确的医学术语
""")
# 2. 提供医疗场景的 Few-shot 示例
medical_examples = [
lx.data.ExampleData(
text="患者女性,45岁,因"反复头晕1月"就诊。BP 150/90 mmHg。诊断:高血压1级。用药:氨氯地平 5mg qd",
extractions=[
lx.data.Extraction(
extraction_class="patient_info",
extraction_text="患者女性,45岁",
attributes={"age": 45, "gender": "female", "chief_complaint": "反复头晕1月"}
),
lx.data.Extraction(
extraction_class="diagnoses",
extraction_text="高血压1级",
attributes={"name": "高血压1级", "ICD-10": "I10"}
),
lx.data.Extraction(
extraction_class="medications",
extraction_text="氨氯地平 5mg qd",
attributes={"name": "氨氯地平", "dosage": "5mg", "frequency": "qd"}
)
]
)
]
# 3. 处理临床笔记
clinical_note = """
患者男性,65岁,因"胸闷气短3天"入院。既往高血压病史10年,糖尿病5年。
体格检査:BP 160/95 mmHg,心率 92 次/分,双肺底可闻及湿啰音。
入院诊断:1. 充血性心力衰竭 2. 高血压3级 极高危 3. 2型糖尿病
用药:呋塞米 20mg qd、赖诺普利 10mg qd、二甲双胍 500mg tid
"""
result = lx.extract(
text_or_documents=clinical_note,
prompt_description=medical_prompt,
examples=medical_examples,
model_id="gemini-3.5-pro", # 医疗场景推荐使用 Pro 模型(精度更高)
)
# 4. 后处理:将提取结果转换为结构化 JSON
import json
structured_output = {
"patient_info": {},
"diagnoses": [],
"medications": []
}
for extraction in result.extractions:
if not extraction.char_interval:
continue # 跳过无法定位的提取结果
if extraction.extraction_class == "patient_info":
structured_output["patient_info"] = extraction.attributes
elif extraction.extraction_class == "diagnoses":
structured_output["diagnoses"].append(extraction.attributes)
elif extraction.extraction_class == "medications":
structured_output["medications"].append(extraction.attributes)
# 5. 保存结果
with open("structured_clinical_note.json", "w", encoding="utf-8") as f:
json.dump(structured_output, f, ensure_ascii=False, indent=2)
print(json.dumps(structured_output, ensure_ascii=False, indent=2))
9.3 在 RadExtract 中体验
Google 提供了在线 Demo:RadExtract(部署在 HuggingFace Spaces)
- 链接:https://huggingface.co/spaces/google/radextract
- 功能:上传 radiology report(放射科报告),自动提取结构化信息
- 无需安装,直接在浏览器中使用
10. 最佳实践与避坑指南
10.1 最佳实践
✅ DO
总是提供 2-3 个高质量的 Few-shot 示例
- 示例应该覆盖边界情况(如实体在句首、句尾、跨句子等)
extraction_text必须是原文的子串(逐字符匹配)
生产环境中过滤无源定位的结果
grounded = [e for e in result.extractions if e.char_interval]使用 Gemini Flash 作为默认模型
- 性价比最高
- 支持 Controlled Generation(保证输出格式)
对于长文档,启用并行处理和多轮提取
result = lx.extract( text_or_documents=long_text, extraction_passes=3, max_workers=20, )使用 Vertex AI Batch API 处理大规模任务
- 降低成本 50%+
- 适合离线批处理
❌ DON'T
不要使用 paraphrase 作为 extraction_text
# ❌ 错误:extraction_text 不是原文子串 lx.data.Extraction( extraction_class="medication", extraction_text="病人正在服用 Lisinopril", # 原文是 "患者服用 Lisinopril" attributes={...} )不要忽略 PromptAlignmentWarning
- 如果出现对齐警告,说明 Few-shot 示例有问题,必须修复
不要在生产环境中硬编码 API Key
# ❌ 错误 result = lx.extract( ..., api_key="sk-..." # 不要这样做! ) # ✅ 正确:使用环境变量或 .env 文件 # export LANGEXTRACT_API_KEY="sk-..." result = lx.extract(...)不要对高风险领域(医疗、法律)过度依赖 LLM 推理
- 严格基于文本证据提取
- 人工审核关键结果
10.2 常见问题排查
问题一:提取结果质量差
可能原因:
- Prompt 描述不清晰
- Few-shot 示例质量差
- 模型选择不当(如用 Flash 处理复杂推理任务)
解决方案:
# 1. 改进 Prompt(更具体的指令)
prompt = textwrap.dedent("""
提取文本中的药物名称、剂量、给药途径。
规则:
- extraction_text 必须是药物名称的精确文本(如 "Lisinopril")
- attributes 中必须包含 dosage(如 "10mg")和 route(如 "PO")
- 如果给药途径未明确说明,设置为 null
""")
# 2. 增加 Few-shot 示例数量(推荐 3-5 个)
examples.append(
lx.data.ExampleData(
text="患者静脉注射 Vancomycin 1g q12h",
extractions=[
lx.data.Extraction(
extraction_class="medication",
extraction_text="Vancomycin",
attributes={"dosage": "1g", "route": "IV", "frequency": "q12h"}
)
]
)
)
# 3. 换用 Pro 模型
result = lx.extract(
...,
model_id="gemini-3.5-pro", # 更高精度
)
问题二:速率限制(Rate Limit)
解决方案:
# 1. 降低并行度
result = lx.extract(
...,
max_workers=5, # 从 20 降到 5
)
# 2. 使用付费 Tier(提高 RPM 限制)
# 参见:https://ai.google.dev/gemini-api/docs/rate-limits#usage-tiers
# 3. 使用 Vertex AI Batch API
result = lx.extract(
...,
language_model_params={
"vertexai": True,
"batch": {"enabled": True}
}
)
11. 总结与展望
11.1 核心要点回顾
LangExtract 解决了什么问题?
- 从非结构化文本中提取结构化信息
- 提供精确的源定位(可验证性)
- 内置交互式可视化(易于审核)
什么时候应该使用 LangExtract?
- 需要处理大量非结构化文本(临床笔记、法律合同、新闻文章等)
- 需要可验证的提取结果(源定位)
- 需要快速原型(无需训练模型)
什么时候不应该使用 LangExtract?
- 实时性要求极高(每次提取需要 0.5-2 秒)
- 完全离线环境(无法访问云 API)
- 对成本极其敏感(尽管 Flash 模型已经很便宜)
11.2 未来展望
更多模型支持
- 目前支持 Gemini、OpenAI、Ollama
- 社区正在开发 Anthropic Claude 支持
多模态扩展
- 目前只支持文本
- 未来可能支持从图片中提取文本信息(OCR + Extraction)
领域适配
- 提供预训练的 Few-shot 示例库(医疗、法律、金融等)
- 用户可以直接使用,无需自己编写示例
11.3 参考资源
- 官方 GitHub:https://github.com/google/langextract
- PyPI 页面:https://pypi.org/project/langextract/
- RadExtract Demo:https://huggingface.co/spaces/google/radextract
- LangExtract Paper:https://doi.org/10.5281/zenodo.17015089
附录:完整代码示例
A. 处理本地文本文件
import langextract as lx
# 从本地文件读取
with open("clinical_note.txt", "r", encoding="utf-8") as f:
text = f.read()
result = lx.extract(
text_or_documents=text,
prompt_description="提取患者信息、诊断、用药",
examples=medical_examples,
model_id="gemini-3.5-flash"
)
# 保存结果
lx.io.save_annotated_documents([result], output_name="output.jsonl", output_dir="./")
B. 批量处理多个文档
import os
import langextract as lx
# 批量处理文件夹中的所有 .txt 文件
input_dir = "./clinical_notes/"
output_dir = "./output/"
results = []
for filename in os.listdir(input_dir):
if not filename.endswith(".txt"):
continue
with open(os.path.join(input_dir, filename), "r", encoding="utf-8") as f:
text = f.read()
result = lx.extract(
text_or_documents=text,
prompt_description=medical_prompt,
examples=medical_examples,
model_id="gemini-3.5-flash"
)
results.append(result)
# 一次性保存所有结果
lx.io.save_annotated_documents(results, output_name="batch_output.jsonl", output_dir=output_dir)
全文完
关于作者:程序员茄子,全栈开发者,专注于 AI 应用开发和自然语言处理。
转载声明:本文由程序员茄子原创,转载请注明出处。