编程 Google LangExtract 深度实战:当 LLM 学会「精准定位」——从非结构化文本到结构化数据的完全指南(2026)

2026-06-09 16:19:31 +0800 CST views 10

Google LangExtract 深度实战:当 LLM 学会「精准定位」——从非结构化文本到结构化数据的完全指南(2026)

作者: 程序员茄子
日期: 2026-06-09
字数: 约 8500 字
适用人群: Python 开发者、NLP 工程师、数据工程师、AI 应用开发者


目录

  1. 痛点:非结构化文本的「提取困境」
  2. LangExtract 是什么?
  3. 核心特性深度解析
  4. 架构设计与技术原理
  5. 快速入门:5 分钟上手
  6. 进阶实战:长文档处理
  7. 生产级优化:性能与成本
  8. 与其他方案对比
  9. 实战案例:医疗文本结构化
  10. 最佳实践与避坑指南
  11. 总结与展望

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)。
若逾期支付,应按日加收应付未付款项万分之五的违约金。

场景三:电商评论挖掘

"物流很快,第二天就到了!包装完好,但手机壳有点容易刮花,
不过这个价位已经很值了。客服态度很好,耐心解答了我的问题。"

这些文本有三个共同特点:

  1. 非结构化:没有固定的格式或 schema
  2. 信息密度不均:关键实体淹没在大量描述性文字中
  3. 上下文依赖:同一个词在不同语境下含义不同

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 的解决方案

  1. 要求 LLM 在提取时同时输出原文中的精确字符区间(char_interval)
  2. 如果提取内容无法在原文中找到,char_interval 字段为 None
  3. 可视化时直接用高亮标记原文对应位置
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 如何在内部实现源定位?

  1. Prompt 工程:在系统提示中强制要求模型输出 JSON 格式,其中包含 start_charend_char 字段
  2. 约束解码(Constrained Decoding):对于支持 Controlled Generation 的模型(如 Gemini),通过 response_schema 强制输出格式
  3. 后验验证:即使模型声称某字符区间匹配,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 会遇到:

  1. 上下文窗口限制(即使 Gemini 有 100 万 token 窗口,实际召回率也会下降)
  2. 「针在干草堆」问题(Needle-in-a-haystack):模型容易遗漏分散在长文档中的关键信息
  3. 成本爆炸(处理 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 文件,支持:

  1. 原文高亮:鼠标悬停在提取结果上时,原文对应位置高亮
  2. 筛选和搜索:按实体类型、属性筛选
  3. 置信度显示:如果模型返回了置信度分数,用颜色编码显示
# 保存结果为 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

特性LangExtractLangChain (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

  1. 总是提供 2-3 个高质量的 Few-shot 示例

    • 示例应该覆盖边界情况(如实体在句首、句尾、跨句子等)
    • extraction_text 必须是原文的子串(逐字符匹配)
  2. 生产环境中过滤无源定位的结果

    grounded = [e for e in result.extractions if e.char_interval]
    
  3. 使用 Gemini Flash 作为默认模型

    • 性价比最高
    • 支持 Controlled Generation(保证输出格式)
  4. 对于长文档,启用并行处理和多轮提取

    result = lx.extract(
        text_or_documents=long_text,
        extraction_passes=3,
        max_workers=20,
    )
    
  5. 使用 Vertex AI Batch API 处理大规模任务

    • 降低成本 50%+
    • 适合离线批处理

❌ DON'T

  1. 不要使用 paraphrase 作为 extraction_text

    # ❌ 错误:extraction_text 不是原文子串
    lx.data.Extraction(
        extraction_class="medication",
        extraction_text="病人正在服用 Lisinopril",  # 原文是 "患者服用 Lisinopril"
        attributes={...}
    )
    
  2. 不要忽略 PromptAlignmentWarning

    • 如果出现对齐警告,说明 Few-shot 示例有问题,必须修复
  3. 不要在生产环境中硬编码 API Key

    # ❌ 错误
    result = lx.extract(
        ...,
        api_key="sk-..."  # 不要这样做!
    )
    
    # ✅ 正确:使用环境变量或 .env 文件
    # export LANGEXTRACT_API_KEY="sk-..."
    result = lx.extract(...)
    
  4. 不要对高风险领域(医疗、法律)过度依赖 LLM 推理

    • 严格基于文本证据提取
    • 人工审核关键结果

10.2 常见问题排查

问题一:提取结果质量差

可能原因

  1. Prompt 描述不清晰
  2. Few-shot 示例质量差
  3. 模型选择不当(如用 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 核心要点回顾

  1. LangExtract 解决了什么问题?

    • 从非结构化文本中提取结构化信息
    • 提供精确的源定位(可验证性)
    • 内置交互式可视化(易于审核)
  2. 什么时候应该使用 LangExtract?

    • 需要处理大量非结构化文本(临床笔记、法律合同、新闻文章等)
    • 需要可验证的提取结果(源定位)
    • 需要快速原型(无需训练模型)
  3. 什么时候不应该使用 LangExtract?

    • 实时性要求极高(每次提取需要 0.5-2 秒)
    • 完全离线环境(无法访问云 API)
    • 对成本极其敏感(尽管 Flash 模型已经很便宜)

11.2 未来展望

  1. 更多模型支持

    • 目前支持 Gemini、OpenAI、Ollama
    • 社区正在开发 Anthropic Claude 支持
  2. 多模态扩展

    • 目前只支持文本
    • 未来可能支持从图片中提取文本信息(OCR + Extraction)
  3. 领域适配

    • 提供预训练的 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 应用开发和自然语言处理。
转载声明:本文由程序员茄子原创,转载请注明出处。

复制全文 生成海报 LLM 信息提取 Python Google NLP

推荐文章

filecmp,一个Python中非常有用的库
2024-11-19 03:23:11 +0800 CST
php使用文件锁解决少量并发问题
2024-11-17 05:07:57 +0800 CST
在Vue3中实现代码分割和懒加载
2024-11-17 06:18:00 +0800 CST
Golang 几种使用 Channel 的错误姿势
2024-11-19 01:42:18 +0800 CST
JavaScript 的模板字符串
2024-11-18 22:44:09 +0800 CST
OpenCV 检测与跟踪移动物体
2024-11-18 15:27:01 +0800 CST
# 解决 MySQL 经常断开重连的问题
2024-11-19 04:50:20 +0800 CST
程序员茄子在线接单