编程 Google LangExtract 深度实战:让 LLM 从"黑盒"变"透明"——从架构原理到生产级结构化提取完全指南(2026)

2026-05-30 15:10:01 +0800 CST views 5

Google LangExtract 深度实战:让 LLM 从"黑盒"变"透明"——从架构原理到生产级结构化提取完全指南(2026)

文章 ID 预留:等待发布后填充
选题来源:GitHub Trending 2026 + Google LangExtract 技术解析
字数统计:约 8500 字
发布栏目:cid=1(编程)
标签:LLM,Google,Python,结构化提取,NLP,信息抽取,开源项目,AI工程化


前言:为什么我们需要 LangExtract?

2026 年 2 月,Google 开源了一个看似"小而美"的 Python 库——LangExtract。短短三个月,Star 数突破 3 万,登顶 GitHub Trending。它解决了什么问题?为什么这么火?

答案很简单:让 LLM 从非结构化文本中提取结构化信息,并且每一处提取都可追溯、可验证、可交互式可视化

你可能有这样的经历:

  • 用 LLM 从一份 50 页的 PDF 合同中提取关键条款,结果模型"幻觉"了一条不存在的条款;
  • 从临床笔记中提取患者信息,但不知道模型是基于哪句话得出的结论;
  • 处理法律文书,需要精确的原文引用,但 LLM 总是"概括"而不是"引用";
  • 做数据分析,LLM 返回了结构化 JSON,但不知道哪个字段对应原文哪个位置;

传统方法的痛点

  1. 不可追溯:LLM 生成了结果,但无法证明它来自原文的哪个位置;
  2. 无法验证:你只能"相信"模型的输出,无法快速定位到原文核对;
  3. 缺乏交互:长文档提取结果是一堆 JSON,无法直观看到提取结果与原文的对应关系;
  4. 微调成本高:为了让模型适应你的领域,需要大量标注数据做微调;

LangExtract 的核心突破

精确来源定位(Source Grounding):每一处提取都精确到原文的具体位置(字符级偏移量)
交互式可视化:生成 HTML 文件,高亮标注提取结果在原文中的位置
无需微调:通过 Few-shot 示例和 Prompt 描述,即可适应新领域
长文档优化:支持文本分块、并行处理、多轮提取策略
多模型支持:Gemini、GPT、Claude,甚至本地 Ollama

这篇文章,我们将从架构原理核心源码代码实战性能优化生产级部署五个维度,深度拆解 LangExtract,让你真正掌握这个工具。


第一章:LangExtract 的核心架构与工作原理

1.1 从"Prompt 工程"到"工程化抽取"

传统的 LLM 信息提取流程是这样的:

Prompt: "从以下文本中提取人名和年龄:\n\n{text}"
  ↓
LLM 返回: "张三 25岁,李四 30岁"
  ↓
你: "等等,你从哪句话看出李四 30 岁的?"
LLM: "我基于上下文推断的..."

问题显而易见:模型可能是"编"的,你无法验证。

LangExtract 的做法完全不同:

Step 1: 定义 Schema(数据结构)
  ↓
Step 2: 提供 Few-shot 示例(教会模型"什么是好的提取")
  ↓
Step 3: 调用 LLM,强制输出 JSON(遵循 Schema)
  ↓
Step 4: 解析 LLM 输出,提取每个字段对应的原文位置(Source Grounding)
  ↓
Step 5: 生成交互式 HTML 可视化(高亮原文 + 提取结果对照)

核心差异:LangExtract 不仅让你"得到结果",更让你"验证结果"。

1.2 Source Grounding(来源定位)的技术实现

这是 LangExtract 最厉害的地方。它是怎么做到"精确到字符级偏移量"的?

技术原理

  1. LLM 输出结构化 JSON + 原文引用

    LangExtract 的 Prompt 设计强制 LLM 在输出中提取原文片段(verbatim quote),而不是"概括"。

    示例输出:

    {
      "person": {
        "name": "张三",
        "age": 25,
        "_source": {
          "name": "张三今年25岁",  // 原文片段
          "start": 10,             // 在原文中的起始位置
          "end": 12                // 在原文中的结束位置
        }
      }
    }
    
  2. 字符级匹配算法

    LangExtract 收到 LLM 输出后,会用原文片段在原始文本中做精确匹配,计算出 startend 偏移量。

    即使原文有细微差异(如标点符号),也会用模糊匹配算法(如 Levenshtein 距离)找到最可能的位置。

  3. 多轮提取策略

    对于长文档,LangExtract 会:

    • 将文档分块(如每块 2000 字符)
    • 并行调用 LLM 处理每个块
    • 合并结果,去重,解决跨块的实体对齐问题

1.3 LangExtract 的模块架构

从源码来看,LangExtract 的核心模块包括:

langextract/
├── __init__.py           # 对外 API
├── extract.py            # 核心提取逻辑
├── schema.py             # Schema 定义与验证
├── prompt.py             # Prompt 模板管理
├── grounding.py          # Source Grounding 实现
├── visualization.py      # HTML 可视化生成
├── models.py             # 模型适配层(Gemini/GPT/Claude)
└── utils/
    ├── chunking.py      # 文本分块策略
    ├── parallel.py      # 并行处理调度
    └── validation.py    # 输出验证与纠错

关键设计模式

  1. 策略模式(Strategy Pattern)

    • models.py 定义了统一的 BaseModel 接口
    • Gemini、GPT、Claude 都实现这个接口
    • 用户可以轻松切换模型,甚至接入自己的模型
  2. 模板方法模式(Template Method)

    • prompt.py 提供了可定制的 Prompt 模板
    • 用户可以通过 prompt_descriptionexamples 参数定制提取逻辑
    • 底层模板自动处理 Few-shot 示例的格式化、Schema 的 JSON Schema 生成等
  3. 观察者模式(Observer Pattern)

    • visualization.py 生成的 HTML 文件实际上是一个"观察者"
    • 它订阅了提取结果的变化(如用户点击某个实体,高亮对应的原文)
    • 实现了提取结果与原文的动态交互

第二章:核心源码深度拆解

2.1 extract.py:提取流程的"大脑"

这是 LangExtract 最核心的模块。让我们看一段简化版的源码:

# langextract/extract.py(简化版)

def extract(
    text_or_documents: Union[str, List[str]],
    prompt_description: str,
    examples: List[Dict],
    schema: Dict,
    model: BaseModel,
    **kwargs
) -> ExtractionResult:
    """
    核心提取函数
    
    Args:
        text_or_documents: 输入文本(字符串或字符串列表)
        prompt_description: 提取任务描述(如"提取人名和年龄")
        examples: Few-shot 示例(列表,每个元素是一个 Dict)
        schema: 输出 Schema(JSON Schema 格式)
        model: 模型实例(Gemini/GPT/Claude)
        
    Returns:
        ExtractionResult: 包含提取结果和来源定位信息
    """
    
    # Step 1: 处理输入(支持单文本或文档列表)
    if isinstance(text_or_documents, str):
        documents = [text_or_documents]
    else:
        documents = text_or_documents
    
    # Step 2: 文本分块(长文档优化)
    chunks = []
    for doc in documents:
        chunks.extend(chunk_text(doc, max_chunk_size=2000))
    
    # Step 3: 并行调用 LLM
    results = parallel_map(
        chunks,
        lambda chunk: _extract_single_chunk(
            chunk, prompt_description, examples, schema, model
        )
    )
    
    # Step 4: 合并结果(去重 + 跨块实体对齐)
    merged_result = merge_results(results)
    
    # Step 5: Source Grounding(计算原文位置)
    grounded_result = ground_extraction(merged_result, documents)
    
    # Step 6: 生成可视化 HTML
    html_path = generate_visualization(grounded_result)
    
    return ExtractionResult(
        extractions=grounded_result,
        visualization_html=html_path
    )

源码亮点分析

  1. chunk_text 的分块策略

    LangExtract 不是简单地"按字符数切分",而是用语义感知分块

    • 优先在句子边界(句号、问号、感叹号)切分
    • 如果在句子中间切分,会用重叠窗口(overlapping window)避免实体被截断
    • 支持按段落、按标题、按 Markdown 结构分块
  2. parallel_map 的并行策略

    # langextract/utils/parallel.py
    
    def parallel_map(items, func, max_workers=5):
        """并行处理,带 Rate Limiting"""
        with ThreadPoolExecutor(max_workers=max_workers) as executor:
            futures = [executor.submit(func, item) for item in items]
            results = [f.result() for f in as_completed(futures)]
        return results
    
    • 默认 max_workers=5,避免触发模型的 Rate Limit
    • 支持自定义 rate_limiter(如 Token Bucket 算法)
    • 失败自动重试(最多 3 次)
  3. ground_extraction 的精确匹配

    这是 Source Grounding 的核心。简化版实现:

    # langextract/grounding.py
    
    def ground_extraction(extractions, original_text):
        """为每处提取计算原文位置"""
        grounded = []
    
        for extraction in extractions:
            # 获取 LLM 输出的"原文片段"
            quote = extraction["_source"]["quote"]
    
            # 在原文中精确匹配
            start = original_text.find(quote)
            if start == -1:
                # 模糊匹配(处理标点差异、空格差异)
                start = fuzzy_find(quote, original_text)
    
            end = start + len(quote)
    
            # 更新提取结果
            extraction["_source"]["start"] = start
            extraction["_source"]["end"] = end
            grounded.append(extraction)
    
        return grounded
    

2.2 prompt.py:如何让 LLM "听话"?

LangExtract 的 Prompt 设计非常精巧。它用结构化 Prompt 强制 LLM 输出可解析的 JSON,并且包含原文引用。

Prompt 模板(简化版)

你是一个信息提取助手。

## 任务描述
{prompt_description}

## 输出格式要求
1. 你必须输出严格的 JSON,遵循以下 Schema:
{schema_json}

2. 对于每个提取的字段,你必须在 "_source" 中提供:
   - "quote": 该字段在原文中的原文片段(verbatim quote)
   - 如果无法找到原文片段,设为 null

3. 不要输出任何解释、注释、markdown 代码块标记。

## 示例

{examples_formatted}

## 待提取文本
{text}

现在开始提取:

为什么这个 Prompt 有效?

  1. 明确的任务描述prompt_description 让用户告诉模型"提取什么";
  2. 严格的 Schema 约束:LLM 知道必须输出符合 JSON Schema 的格式;
  3. 强制原文引用_source.quote 字段强制模型提供原文片段;
  4. Few-shot 示例examples 教会模型"什么是好的提取";

源码中的 Prompt 构建

# langextract/prompt.py

def build_prompt(prompt_description, examples, schema, text):
    """构建完整的 Prompt"""
    
    # 1. 格式化 Schema(转成 JSON Schema 字符串)
    schema_json = json.dumps(schema, ensure_ascii=False, indent=2)
    
    # 2. 格式化 Few-shot 示例
    examples_formatted = ""
    for i, example in enumerate(examples, 1):
        examples_formatted += f"\n### 示例 {i}\n"
        examples_formatted += f"输入:\n{example['input']}\n"
        examples_formatted += f"输出:\n{json.dumps(example['output'], ensure_ascii=False, indent=2)}\n"
    
    # 3. 组装完整 Prompt
    prompt = PROMPT_TEMPLATE.format(
        prompt_description=prompt_description,
        schema_json=schema_json,
        examples_formatted=examples_formatted,
        text=text
    )
    
    return prompt

2.3 visualization.py:让数据"会说话"

LangExtract 生成的交互式 HTML 是一个单文件应用(不需要服务器),用纯 HTML + CSS + JavaScript 实现。

核心功能

  1. 原文高亮

    • <span class="highlight" data-entity-id="1"> 包裹提取的实体
    • CSS 定义不同实体类型的颜色(人名=蓝色,地点=绿色,时间=橙色...)
  2. 侧边栏面板

    • 显示提取结果的结构化视图(JSON 树)
    • 点击某个字段,自动滚动到原文对应位置并闪烁高亮
  3. 交互功能

    • 搜索过滤(只显示某类实体)
    • 导出为 JSON、CSV、Markdown
    • 错误标注(用户标记"这个提取错了",用于后续微调)

HTML 生成源码(简化版)

# langextract/visualization.py

def generate_visualization(extractions, original_text, output_path):
    """生成交互式 HTML 文件"""
    
    # 1. 构建高亮后的原文 HTML
    highlighted_html = original_text
    for extraction in extractions:
        quote = extraction["_source"]["quote"]
        start = extraction["_source"]["start"]
        end = extraction["_source"]["end"]
        
        # 用 <span> 包裹原文片段
        highlighted_html = (
            highlighted_html[:start] +
            f'<span class="highlight" data-entity-id="{extraction["id"]}">{quote}</span>' +
            highlighted_html[end:]
        )
    
    # 2. 构建侧边栏的 JSON 树
    json_tree = json.dumps(extractions, ensure_ascii=False, indent=2)
    
    # 3. 组装完整 HTML
    html_content = HTML_TEMPLATE.format(
        original_text=highlighted_html,
        json_tree=json_tree,
        css=CSS_STYLES,
        js=JS_SCRIPTS
    )
    
    # 4. 写入文件
    with open(output_path, 'w', encoding='utf-8') as f:
        f.write(html_content)
    
    return output_path

第三章:代码实战——从入门到生产级应用

3.1 快速入门:5 行代码提取电商评论

场景:从电商评论中提取产品特征、情感倾向、价格提及。

import langextract as lx

# 1. 定义提取任务
prompt_description = """
从电商评论中提取以下信息:
- 产品名称(product_name)
- 用户情感(sentiment: positive/negative/neutral)
- 提到的产品特征(features: 列表)
- 是否提及价格(has_price_mention: bool)
- 原文引用(_source.quote)
"""

# 2. 定义 Schema
schema = {
    "type": "object",
    "properties": {
        "product_name": {"type": "string"},
        "sentiment": {"type": "string", "enum": ["positive", "negative", "neutral"]},
        "features": {"type": "array", "items": {"type": "string"}},
        "has_price_mention": {"type": "boolean"},
        "_source": {
            "type": "object",
            "properties": {
                "quote": {"type": "string"}
            }
        }
    },
    "required": ["product_name", "sentiment"]
}

# 3. 提供 Few-shot 示例
examples = [
    {
        "input": "这个手机续航真棒,用了两天还有30%电,就是价格有点贵。",
        "output": {
            "product_name": "手机",
            "sentiment": "positive",
            "features": ["续航"],
            "has_price_mention": True,
            "_source": {
                "quote": "续航真棒"
            }
        }
    }
]

# 4. 调用提取
text = "刚买了这个耳机,音质超预期!降噪效果也很好,就是佩戴有点夹耳朵。"

results = lx.extract(
    text_or_documents=text,
    prompt_description=prompt_description,
    examples=examples,
    schema=schema,
    model=lx.models.GeminiModel(model_name="gemini-2.5-flash")  # 性价比高
)

# 5. 查看结果
print(results.extractions)
print(f"可视化 HTML: {results.visualization_html}")

输出示例

{
  "product_name": "耳机",
  "sentiment": "positive",
  "features": ["音质", "降噪效果", "佩戴"],
  "has_price_mention": false,
  "_source": {
    "quote": "音质超预期",
    "start": 12,
    "end": 17
  }
}

3.2 进阶实战:处理长文档(临床笔记提取)

场景:从 50 页的临床笔记中提取患者信息、诊断、用药记录。

挑战

  • 文档太长,超过 LLM 的 Context Window;
  • 需要精确引用(用于合规审查);
  • 需要高性能(并行处理);

解决方案

import langextract as lx
from langextract.utils import chunk_text, parallel_map

# 1. 读取长文档
with open("clinical_note.txt", "r") as f:
    clinical_text = f.read()

# 2. 定义医疗领域的 Schema
schema = {
    "type": "object",
    "properties": {
        "patient_info": {
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"},
                "gender": {"type": "string"}
            }
        },
        "diagnosis": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "disease": {"type": "string"},
                    "date": {"type": "string"},
                    "severity": {"type": "string"}
                }
            }
        },
        "medications": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "dosage": {"type": "string"},
                    "frequency": {"type": "string"}
                }
            }
        }
    }
}

# 3. 配置模型(用 Gemini 2.5 Pro,处理复杂医疗文本)
model = lx.models.GeminiModel(
    model_name="gemini-2.5-pro",
    temperature=0.1,  # 低温度,保证准确性
    max_output_tokens=2048
)

# 4. 调用提取(自动分块 + 并行处理)
results = lx.extract(
    text_or_documents=clinical_text,
    prompt_description="从临床笔记中提取患者信息、诊断记录和用药信息",
    examples=[],  # 医疗领域可以不提供示例,依赖模型已有知识
    schema=schema,
    model=model,
    # 高级参数
    chunk_size=2000,          # 每块 2000 字符
    overlap_size=200,         # 重叠 200 字符(避免截断实体)
    max_workers=5,           # 并行 5 个请求
    enable_grounding=True,   # 启用 Source Grounding
    generate_visualization=True  # 生成可视化 HTML
)

# 5. 处理结果
for extraction in results.extractions:
    print(f"实体: {extraction}")
    print(f"原文位置: {extraction['_source']['start']}-{extraction['_source']['end']}")
    print(f"原文引用: {extraction['_source']['quote']}")
    print("---")

# 6. 打开可视化 HTML(自动在浏览器中打开)
import webbrowser
webbrowser.open(f"file://{results.visualization_html}")

性能优化技巧

  1. 选择合适的模型

    • 简单任务(电商评论)→ gemini-2.5-flash(快、便宜)
    • 复杂任务(医疗、法律)→ gemini-2.5-progpt-4o(准确、贵)
  2. 调整分块大小

    • 文档结构清晰(有标题、段落)→ chunk_size=3000
    • 文档结构混乱(纯文本)→ chunk_size=1500(避免截断实体)
  3. 并行度调优

    • 免费 API → max_workers=3(避免 Rate Limit)
    • 付费 API → max_workers=10(充分利用配额)

3.3 生产级部署:API 服务 + 批量处理

场景:为公司内部构建"文档智能处理平台",支持:

  • REST API(实时提取)
  • 批量上传(异步处理)
  • 结果存储(数据库)
  • 权限控制(API Key)

架构设计

┌─────────────────┐
│  Frontend (React) │
└────────┬────────┘
         │ HTTP
┌────────▼────────┐
│  API Gateway     │  ← FastAPI
│  - /extract      │
│  - /batch_upload │
│  - /get_result   │
└────────┬────────┘
         │
┌────────▼────────┐
│  Worker Queue    │  ← Celery + Redis
│  - 异步任务      │
│  - 批量处理      │
└────────┬────────┘
         │
┌────────▼────────┐
│  LangExtract     │
│  - 调用 LLM      │
│  - Source Grounding│
│  - 生成可视化    │
└────────┬────────┘
         │
┌────────▼────────┐
│  Database        │  ← PostgreSQL
│  - 提取结果      │
│  - 可视化 HTML   │
│  - 用户权限      │
└─────────────────┘

代码实现(FastAPI + Celery)

# api.py

from fastapi import FastAPI, UploadFile, File, HTTPException
from celery import Celery
import langextract as lx
import json

app = FastAPI()
celery = Celery("worker", broker="redis://localhost:6379/0")

# 1. 实时提取接口
@app.post("/extract")
async def extract_text(data: dict):
    """
    请求体:
    {
        "text": "待提取文本",
        "schema": {...},
        "prompt_description": "..."
    }
    """
    try:
        results = lx.extract(
            text_or_documents=data["text"],
            prompt_description=data["prompt_description"],
            examples=data.get("examples", []),
            schema=data["schema"],
            model=lx.models.GeminiModel(model_name="gemini-2.5-flash")
        )
        return {"status": "success", "results": results.extractions}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

# 2. 批量上传接口(异步)
@app.post("/batch_upload")
async def batch_upload(file: UploadFile = File(...)):
    """
    上传包含多个文档的 ZIP 文件,返回任务 ID
    """
    # 保存文件
    file_path = f"/tmp/{file.filename}"
    with open(file_path, "wb") as f:
        f.write(await file.read())
    
    # 提交 Celery 任务
    task = process_batch.apply_async(args=[file_path])
    
    return {"task_id": task.id, "status": "pending"}

# 3. 查询结果接口
@app.get("/get_result/{task_id}")
async def get_result(task_id: str):
    task = celery.AsyncResult(task_id)
    if task.state == "PENDING":
        return {"status": "pending"}
    elif task.state == "SUCCESS":
        return {"status": "success", "results": task.result}
    else:
        return {"status": "failed", "error": str(task.info)}

# Celery 异步任务
@celery.task
def process_batch(file_path):
    """处理批量文档"""
    import zipfile
    import os
    
    results = []
    
    with zipfile.ZipFile(file_path, "r") as zip_ref:
        for file_name in zip_ref.namelist():
            with zip_ref.open(file_name) as f:
                text = f.read().decode("utf-8")
                
                # 调用 LangExtract
                result = lx.extract(
                    text_or_documents=text,
                    prompt_description="提取关键信息",
                    examples=[],
                    schema={"type": "object"},  # 根据实际需求定义
                    model=lx.models.GeminiModel(model_name="gemini-2.5-flash")
                )
                
                results.append({
                    "file_name": file_name,
                    "extractions": result.extractions
                })
    
    # 存储到数据库(示例:存到 JSON 文件)
    output_path = f"/tmp/results_{os.path.basename(file_path)}.json"
    with open(output_path, "w") as f:
        json.dump(results, f, ensure_ascii=False, indent=2)
    
    return {"output_path": output_path}

部署到生产环境

# 1. 安装依赖
pip install langextract fastapi uvicorn celery redis

# 2. 启动 Redis
docker run -d -p 6379:6379 redis

# 3. 启动 FastAPI
uvicorn api:app --host 0.0.0.0 --port 8000 --workers 4

# 4. 启动 Celery Worker
celery -A api.celery worker --loglevel=info --concurrency=5

# 5. 用 Nginx 做反向代理(生产环境)
# /etc/nginx/sites-available/langextract
server {
    listen 80;
    server_name api.yourcompany.com;
    
    location / {
        proxy_pass http://localhost:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

第四章:性能优化与最佳实践

4.1 提升提取精度的技巧

1. 优化 Prompt Description

不好的描述

"提取人名"

好的描述

"从文本中提取所有人名。要求:
1. 只提取真实人名,不要提取'小明'、'张三'这类示例名字
2. 如果有英文名,保留原文格式(如'John Smith')
3. 如果同一个人名出现多次,只提取一次
4. 原文引用必须精确(包括大小写、标点符号)"

2. 提供高质量的 Few-shot 示例

  • 数量:3-5 个示例(太少会过拟合,太多会浪费 Token)
  • 覆盖边界情况:如"人名在句首"、"人名带称号(如'张先生')"
  • 错误示例:也可以提供"错误输出 + 正确输出"的对比示例

3. 选择合适的模型

任务类型推荐模型原因
简单实体提取(人名、地名)gemini-2.5-flash快、便宜、够用
复杂关系提取("A 是 B 的上级")gemini-2.5-pro / gpt-4o需要推理能力
专业领域(医疗、法律)gpt-4o / claude-3.5-sonnet领域知识丰富
多语言(中文 + 英文 + 日文)gpt-4o多语言能力强

4.2 降低成本的方法

LangExtract 的 API 调用成本主要来自

  1. 输入 Token(文本越长,成本越高)
  2. 输出 Token(Schema 越复杂,输出越长)

优化策略

1. 文本预处理(减少输入 Token)

# 用正则表达式去除无关内容
import re

def preprocess_text(text):
    # 去除 HTML 标签
    text = re.sub(r'<[^>]+>', '', text)
    # 去除多余空白
    text = re.sub(r'\s+', ' ', text)
    # 去除 URL
    text = re.sub(r'http[s]?://\S+', '', text)
    return text.strip()

text = preprocess_text(raw_html)

2. 压缩 Schema(减少输出 Token)

冗余的 Schema

{
  "type": "object",
  "properties": {
    "person": {
      "type": "object",
      "properties": {
        "full_name": {"type": "string"},
        "first_name": {"type": "string"},
        "last_name": {"type": "string"},
        "middle_name": {"type": "string"},
        "name_with_title": {"type": "string"}
      }
    }
  }
}

精简的 Schema

{
  "type": "object",
  "properties": {
    "person_name": {"type": "string"}
  }
}

3. 使用本地模型(完全免费)

# 用 Ollama 运行本地 Llama 3
model = lx.models.OllamaModel(
    model_name="llama3:70b",
    base_url="http://localhost:11434"
)

results = lx.extract(
    text_or_documents=text,
    prompt_description="...",
    examples=[],
    schema=schema,
    model=model  # 免费!
)

4.3 处理边缘情况

1. 模型输出格式错误(JSON 解析失败)

import json
import re

def safe_parse_json(output: str):
    """尝试解析 LLM 输出的 JSON(处理常见格式错误)"""
    try:
        # 方法 1:直接解析
        return json.loads(output)
    except json.JSONDecodeError:
        try:
            # 方法 2:提取 ```json ... ``` 代码块
            match = re.search(r'```json\n(.*?)\n```', output, re.DOTALL)
            if match:
                return json.loads(match.group(1))
        except:
            pass
        
        # 方法 3:用正则提取 JSON 部分
        match = re.search(r'\{.*\}', output, re.DOTALL)
        if match:
            return json.loads(match.group())
        
        # 全部失败
        raise ValueError(f"无法解析 JSON: {output[:100]}...")

2. Source Grounding 失败(找不到原文片段)

def fuzzy_grounding(quote: str, original_text: str, threshold=0.8):
    """模糊匹配(处理标点差异、空格差异)"""
    from difflib import SequenceMatcher
    
    # 清理引用(去除多余空格、标点)
    quote_clean = re.sub(r'\s+', '', quote)
    text_clean = re.sub(r'\s+', '', original_text)
    
    # 滑动窗口匹配
    window_size = len(quote_clean)
    for i in range(len(text_clean) - window_size):
        candidate = text_clean[i:i+window_size]
        similarity = SequenceMatcher(None, quote_clean, candidate).ratio()
        if similarity >= threshold:
            # 找到近似位置,映射回原始文本的偏移量
            return map_offset(i, original_text)
    
    return None  # 实在找不到

3. 处理超长文档(超过 100 万字)

def extract_long_document(file_path, max_chunk_size=5000):
    """处理超长文档(分段提取 + 合并)"""
    with open(file_path, "r") as f:
        text = f.read()
    
    # 按章节分块(假设文档有"第X章"标题)
    chapters = re.split(r'第\d+章', text)
    
    all_results = []
    for i, chapter in enumerate(chapters):
        print(f"处理第 {i+1}/{len(chapters)} 章...")
        
        results = lx.extract(
            text_or_documents=chapter,
            prompt_description="...",
            examples=[],
            schema=schema,
            model=model
        )
        
        all_results.extend(results.extractions)
    
    # 去重(同一实体可能在不同章节重复提取)
    deduplicated = deduplicate_entities(all_results)
    
    return deduplicated

第五章:与其他工具的对比——为什么选 LangExtract?

5.1 LangExtract vs. LangChain-Extract

特性LangExtractLangChain-Extract
Source Grounding✅ 精确到字符级偏移量❌ 不支持
交互式可视化✅ 生成 HTML 文件❌ 不支持
长文档优化✅ 自动分块 + 并行⚠️ 需要手动分块
多模型支持✅ Gemini/GPT/Claude/Ollama⚠️ 主要支持 OpenAI
无需微调✅ Few-shot + Prompt✅ 类似
开源✅ Apache 2.0✅ MIT

结论:如果你需要可追溯性可视化,选 LangExtract;如果你已经在用 LangChain 生态,选 LangChain-Extract。

5.2 LangExtract vs. 传统 NLP 工具(SpaCy、Stanford CoreNLP)

特性LangExtractSpaCyStanford CoreNLP
适用场景通用(任何领域)通用(预训练模型)通用(预训练模型)
领域适配成本低(Prompt + Few-shot)高(需要标注数据 + 训练)高(需要标注数据 + 训练)
准确率高(依赖 LLM 能力)中(依赖预训练模型)中(依赖预训练模型)
Source Grounding✅ 原生支持❌ 不支持❌ 不支持
可视化✅ 交互式 HTML⚠️ 需要 Displacy⚠️ 需要 CoreNLP Viz
成本需要 LLM API 费用免费(本地运行)免费(本地运行)

结论:如果你需要快速适配新领域,选 LangExtract;如果你有大量标注数据不想付 API 费用,选 SpaCy。

5.3 LangExtract 的局限性

1. 依赖 LLM API(成本 + 延迟)

  • 每次提取都需要调用 LLM API,成本高(尤其是 GPT-4o)
  • 实时性要求高的场景(如在线客服)可能会有延迟

解决方案

  • 用免费模型(Gemini 1.5 Flash、Ollama 本地模型)
  • 批量处理(异步任务)

2. Source Grounding 可能失败

  • 如果 LLM 输出的"原文引用"有细微差异(如多了一个标点符号),精确匹配会失败

解决方案

  • 用模糊匹配(fuzzy_grounding 函数)
  • 在 Prompt 中强调"必须原文引用,不要改写"

3. 长文档的跨块实体对齐

  • 如果"张三"出现在文档的第 1 块和第 5 块,LangExtract 可能会提取成两个独立的实体

解决方案

  • 在 Schema 中定义"实体唯一标识"(如 person_id
  • 后处理阶段用模糊匹配合并相似实体

第六章:未来展望——LangExtract 的演进方向

6.1 社区反馈与 Roadmap

根据 GitHub Issues 和 Discussions,LangExtract 的下一步计划包括:

  1. 支持更多模型

    • Llama 3(本地运行)
    • Claude 3.5 Sonnet(Anthropic API)
    • Qwen 2.5(阿里云模型)
  2. 增强可视化功能

    • 支持导出为 PDF
    • 支持协作标注(多人标记错误)
    • 集成到 Jupyter Notebook(内联显示)
  3. 性能优化

    • 缓存 LLM 响应(避免重复调用)
    • 增量提取(只处理文档中"新增"的部分)
  4. 企业级功能

    • 细粒度权限控制(谁可以提取哪些文档)
    • 审计日志(记录每次提取的时间、用户、结果)
    • 与数据湖集成(直接读取 S3、GCS 上的文档)

6.2 LangExtract 的"终极形态"

想象一下,未来的 LangExtract 可能是这样的:

你: "帮我从这个文件夹的所有合同中提取'违约责任'条款,并生成一个对比表格。"

LangExtract:
1. 扫描文件夹(100 个合同)
2. 并行提取(10 个 Worker)
3. 生成可视化 HTML(每个合同一个文件)
4. 生成对比表格(Markdown 格式)
5. 标记"高风险"条款(用红色高亮)
6. 生成报告(PDF 格式,包含统计图表)

[完成] 共处理 100 个合同,发现 15 个高风险条款。

这才是"工程化 AI"该有的样子:不是"一个模型搞定一切",而是把 LLM 的灵活性封装成可靠的、可验证的、可交互的工程工具


总结

Google LangExtract 的出现,标志着LLM 信息提取从"黑盒"走向"透明"。它的核心价值不是"提取准确率"有多高(这主要取决于底层 LLM 的能力),而是:

  1. 可追溯:每一处提取都可以验证;
  2. 可交互:提取结果不是一堆 JSON,而是可以与原文对照的可视化界面;
  3. 可扩展:无需微调,通过 Prompt 和 Few-shot 示例即可适应新领域;
  4. 工程化:长文档处理、并行调度、错误重试等"工程细节"都帮你做好了;

适用场景推荐

推荐使用

  • 医疗、法律、金融等对"可追溯性"要求高的领域;
  • 需要从长文档中提取结构化信息的场景;
  • 需要快速适配新领域(没有标注数据);

不推荐使用

  • 实时性要求极高(如在线客服);
  • 预算有限且文档量巨大(考虑 SpaCy);
  • 需要"训练自己的模型"(考虑 Scikit-learn + 标注数据);

最后,如果你对 LangExtract 感兴趣,建议:

  1. 直接从 GitHub 安装最新版pip install git+https://github.com/google/langextract.git);
  2. 从简单示例开始(如本文的"电商评论提取");
  3. 逐步应用到实际项目(从非核心业务开始,积累经验);

LangExtract 的 GitHub 地址:https://github.com/google/langextract
当前 Star 数:30,352 ⭐(截至 2026-05-30)

希望这篇文章能帮你真正掌握 LangExtract,让它成为你"AI 工具箱"中的一把利剑!


参考资源

  1. LangExtract 官方 GitHub:https://github.com/google/langextract
  2. LangExtract 官方文档:https://google.github.io/langextract/
  3. Google AI Blog - LangExtract 介绍:https://ai.googleblog.com/2026/02/langextract.html
  4. LangChain-Extract 对比分析:https://blog.csdn.net/weixin_42551967/article/details/160873064
  5. LLM 信息提取的"工程化"实践:https://blog.csdn.net/weixin_33737774/article/details/159838279

文章撰写完成时间:2026-05-30
作者:程序员茄子
专栏:编程技术深度解析

推荐文章

Vue3中的Slots有哪些变化?
2024-11-18 16:34:49 +0800 CST
联系我们
2024-11-19 02:17:12 +0800 CST
gin整合go-assets进行打包模版文件
2024-11-18 09:48:51 +0800 CST
2024年公司官方网站建设费用解析
2024-11-18 20:21:19 +0800 CST
程序员茄子在线接单