Fragments of verbose memory

冗長な記憶の断片 - Web技術のメモをほぼ毎日更新

Jan 18, 2026 - 日記

LangExtract: 抽出結果に文字位置を付ける構造化データ抽出ライブラリ

LangExtract: 抽出結果に文字位置を付ける構造化データ抽出ライブラリ cover image

100社の有価証券報告書から「売上変動要因」を抽出したい。1000件の契約書から「解約条項」を探したい。こうした大量文書からの情報抽出は、従来は人手で行うしかありませんでした。

LLMの登場で「構造化データ抽出」が可能になりましたが、新たな問題が生まれました:「この情報、本当にその文書に書いてあったのか?」

LangExtract は、この問題を解決するGoogleのオープンソースPython ライブラリです。抽出結果に文字位置(CharInterval)を付けることで、「どこから抽出したか」を明示できます。

なぜ大量文書から情報を抽出したいのか

実例1:金融アナリストの業務

証券会社のアナリストは、100社以上の有価証券報告書を読んで業界レポートを作成します。

従来の方法:

1. PDFを開く
2. Ctrl+Fで「売上」を検索
3. 該当箇所を手作業でExcelにコピペ
4. 100社分繰り返す(数日かかる)

求められる出力:

| 企業名 | セグメント | 変動要因 | 金額 | 出典ページ |
|--------|-----------|---------|------|-----------|
| A社 | デジタル広告 | 新規顧客獲得 | +300億円 | p.23 |
| B社 | クラウド | 契約減少 | -50億円 | p.15 |

この作業、LLMで自動化できないか?

実例2:法務部門の契約管理

企業の法務部門は、数百件の契約書を管理しています。

よくある課題:

  • 「30日前通知が必要な契約はどれ?」
  • 「自動更新条項がある契約を全て洗い出したい」
  • 「解約ペナルティの金額を一覧化したい」

これも手作業では膨大な時間がかかります。

LLMによる構造化データ抽出

従来のライブラリ:Instructor

InstructorLangChain は、LLMの出力を構造化データに変換する人気ライブラリです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import instructor
from openai import OpenAI
from pydantic import BaseModel

client = instructor.from_openai(OpenAI())

class Company(BaseModel):
    name: str
    location: str

# 非構造化テキストから構造化データを抽出
text = "株式会社サイバーエージェントは東京都渋谷区に本社を置く。"
result = client.chat.completions.create(
    model="gpt-4",
    response_model=Company,
    messages=[{"role": "user", "content": f"Extract company info: {text}"}]
)

print(result)
# Company(name='株式会社サイバーエージェント', location='東京都渋谷区')

便利ですが、問題があります。

問題:「本当にその文書に書いてあったのか?」

Instructorの出力:

1
Company(name='株式会社サイバーエージェント', location='東京都渋谷区')

この情報、どこから来たのか?

  • 元テキストの何文字目から何文字目?
  • 本当に書いてあった?LLMが推測した?
  • 監査で「根拠を示せ」と言われたら?

答えられません。

出典ポインタの必要性

医療現場での例

カルテから投薬情報を抽出する場合:

1
2
# Instructorで抽出
result = {"drug": "アスピリン", "dosage": "100mg"}

医師の疑問:

  • 「100mg、本当にカルテに書いてあった?」
  • 「LLMが勝手に推測してない?」
  • 「医療ミスになったら誰が責任取る?」

必要なのは:

1
2
3
4
5
6
result = {
    "drug": "アスピリン",
    "dosage": "100mg",
    "source_position": "カルテ123-145文字目",
    "original_text": "アスピリン100mgを処方"
}

法務文書での例

契約書から解約条項を抽出する場合:

1
2
# Instructorで抽出
result = {"notice_period": "30日前"}

弁護士の疑問:

  • 「30日前、本当に契約書に書いてあった?」
  • 「訴訟になったら原文を提示できる?」
  • 「どのページのどの条項?」

必要なのは:

1
2
3
4
5
result = {
    "notice_period": "30日前",
    "source_position": "p.5, 1234-1256文字目",
    "original_text": "解約の場合は30日前までに書面で通知すること"
}

LangExtractの登場

LangExtractは、Googleがリリースしたオープンソースライブラリです。

最大の特徴:

  • 抽出結果に文字位置(CharInterval)を含める
  • 「どこから抽出したか」を明示できる
  • 医療・法務・監査など「根拠の明示」が必須の領域で使える

基本的な使い方

インストール

1
pip install langextract

コード例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import os
import langextract as lx
from langextract import data

# 抽出対象のテキスト
text = """株式会社サイバーエージェントは、東京都渋谷区に本社を置くインターネット広告代理店です。
代表取締役社長は藤田晋氏で、1998年に設立されました。
主な事業はAbemaTVやAmeba、広告事業などです。"""

# プロンプトと例を定義
prompt = """
テキストから企業名、人名、場所を抽出してください。
正確なテキストを使用し、言い換えないでください。
"""

examples = [
    data.ExampleData(
        text="トヨタ自動車の豊田章男社長は、愛知県豊田市の本社で記者会見を行いました。",
        extractions=[
            data.Extraction(
                extraction_class="企業名",
                extraction_text="トヨタ自動車",
            ),
            data.Extraction(
                extraction_class="人名",
                extraction_text="豊田章男",
            ),
            data.Extraction(
                extraction_class="場所",
                extraction_text="愛知県豊田市",
            ),
        ]
    )
]

# 抽出実行
result = lx.extract(
    text_or_documents=text,
    prompt_description=prompt,
    examples=examples,
    model_id="gemini-2.5-flash",
    api_key=os.environ.get('GOOGLE_API_KEY'),
    extraction_passes=1,
)

# 結果表示(出典ポインタ付き)
for extraction in result.extractions:
    print(f"クラス: {extraction.extraction_class}")
    print(f"テキスト: {extraction.extraction_text}")
    if extraction.char_interval:
        start = extraction.char_interval.start_pos
        end = extraction.char_interval.end_pos
        print(f"位置: {start}-{end}文字目")
        print(f"検証: '{text[start:end]}'")
        print("---")

実行結果

クラス: 企業名
テキスト: 株式会社サイバーエージェント
位置: 0-14文字目
検証: '株式会社サイバーエージェント'
---
クラス: 場所
テキスト: 東京都渋谷区
位置: 16-22文字目
検証: '東京都渋谷区'
---
クラス: 人名
テキスト: 藤田晋
位置: 56-59文字目
検証: '藤田晋'
---

重要なポイント:

  • char_interval.start_poschar_interval.end_pos で原文の位置を特定
  • text[start:end] で原文を即座に確認可能
  • 監査時に「この情報の根拠は?」に即答できる

実用例:有価証券報告書の分析

ユースケース

100社の有価証券報告書から「売上変動要因」を抽出し、データベースに保存して分析します。

コード例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import langextract as lx
from langextract import data
import psycopg2
from datetime import datetime

# 有価証券報告書のテキスト
yuho_text = """
【経営成績の分析】
当期の売上高は1,200億円(前期比15%増)となりました。
増収の主な要因は以下の通りです。

(1) デジタル広告事業
新規顧客の獲得により300億円の増収となりました。

(2) メディア事業
既存サービスの会員数減少により100億円の減収となりました。

(3) 為替影響
円安進行により50億円の増収効果がありました。
"""

# プロンプト定義
prompt = """
有価証券報告書から売上変動要因を抽出してください。
以下の情報を含めること:
- 事業セグメント名
- 変動理由
- 金額(増減を明示)

正確なテキストを使用し、言い換えないでください。
"""

# Few-shot例
examples = [
    data.ExampleData(
        text="クラウド事業は新規契約の増加により200億円の増収となりました。",
        extractions=[
            data.Extraction(
                extraction_class="売上変動要因",
                extraction_text="クラウド事業は新規契約の増加により200億円の増収となりました",
            ),
        ]
    )
]

# 抽出実行
result = lx.extract(
    text_or_documents=yuho_text,
    prompt_description=prompt,
    examples=examples,
    model_id="gemini-2.5-flash",
    api_key=os.environ.get('GOOGLE_API_KEY'),
    extraction_passes=2,  # 長文なので2パス
)

# PostgreSQLに保存(出典ポインタも記録)
conn = psycopg2.connect("dbname=analytics user=analyst")
cur = conn.cursor()

for extraction in result.extractions:
    cur.execute("""
        INSERT INTO sales_factors 
        (company_code, fiscal_year, extraction_text, 
         source_start, source_end, document_id, extracted_at)
        VALUES (%s, %s, %s, %s, %s, %s, %s)
    """, (
        "4751",  # サイバーエージェント
        2024,
        extraction.extraction_text,
        extraction.char_interval.start_pos if extraction.char_interval else None,
        extraction.char_interval.end_pos if extraction.char_interval else None,
        "yuho_4751_2024.pdf",
        datetime.now()
    ))

conn.commit()

データベースでの分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
-- 2024年に為替影響を受けた企業を抽出
SELECT company_code, extraction_text, source_start, source_end
FROM sales_factors
WHERE fiscal_year = 2024
  AND extraction_text LIKE '%為替%'
ORDER BY company_code;

-- 原文を確認したい場合
SELECT document_id, source_start, source_end, extraction_text
FROM sales_factors
WHERE id = 123;
-- → "yuho_4751_2024.pdfの123-156文字目を見てください"

従来のライブラリとの違い:

項目LangExtractInstructor
DB保存✅ 原文位置も保存可能⚠️ 抽出結果のみ
監査対応✅ 原文を即座に提示❌ 手動で探す必要
トレーサビリティ✅ 完全❌ なし

可視化機能

LangExtractは抽出結果をインタラクティブなHTMLで可視化できます。

1
2
3
4
5
6
7
# JSONL保存
lx.io.save_annotated_documents([result], output_name="result.jsonl", output_dir=".")

# HTML可視化
html = lx.visualize("result.jsonl")
with open("visualization.html", "w", encoding="utf-8") as f:
    f.write(html if isinstance(html, str) else html.data)

生成されたHTMLをブラウザで開くと、元テキスト上で抽出箇所がハイライト表示されます。

イメージ:

株式会社サイバーエージェントは、東京都渋谷区に本社を置く...
^^^^^^^^^^^^^^^^^^^^^^^          ^^^^^^
[企業名]                          [場所]

これにより、「LLMが勝手に推測した情報」と「実際に文書に書いてあった情報」を一目で区別できます。

他のライブラリとの違い

Instructorとの使い分け

用途推奨ライブラリ
シンプルな構造化出力(API応答のパース等)Instructor
抽出根拠の明示が必要(医療・法務・監査)LangExtract

LangStructとの違い

LangStruct も同様にSource Groundingを提供しますが、最適化方法が異なります:

  • LangExtract: Few-shot例を手動で用意(制御性重視)
  • LangStruct: DSPy で自動最適化(効率重視)

モデルを乗り換える際、LangExtractは例を再調整する必要がありますが、LangStructは自動で再最適化されます。

機能比較表

機能LangExtractInstructorLangChain
Source Grounding✅ 文字位置まで❌ なし❌ なし
可視化✅ HTML内蔵❌ なし❌ なし
長文書対応✅ チャンク+並列❌ 基本なし⚠️ 要実装
日本語対応✅ UAX#29⚠️ 基本的⚠️ 基本的
Batch API✅ Vertex AI❌ なし❌ なし

長文書への対応

LangExtractは「Needle-in-a-Haystack問題」(長文書の中間部分に埋まった情報を見落とす問題)に対応しています。

チャンキング戦略

1
2
3
4
5
6
7
8
9
result = lx.extract(
    text_or_documents=long_document,
    prompt_description=prompt,
    examples=examples,
    model_id="gemini-2.5-flash",
    extraction_passes=3,      # 複数パスで精度向上
    max_workers=10,           # 並列処理数
    max_char_buffer=2000,     # チャンクサイズ
)

パラメータの意味:

  • extraction_passes: 1(高速) ~ 3(高精度)
  • max_workers: 並列度(API制限に注意)
  • max_char_buffer: 小さいほど精度向上、大きいほど高速

長文書を小さなチャンクに分割し、複数パスで抽出することで、単一LLM呼び出しでは見落としがちな情報も確実に拾えます。

日本語対応の実装

LangExtractはUnicode準拠のトークナイザー(UAX #29 )を内蔵しており、日本語の文境界検出に対応しています。

今回のテストでも、日本語テキストから正確に企業名・人名・場所を抽出できました。他の構造化出力ライブラリでは日本語の単語境界が正しく認識されないケースがありますが、LangExtractではその問題は発生しませんでした。

セットアップ

API Key取得

Google AI Studio APIを使うのが最適です:

  1. API Key取得: https://aistudio.google.com/app/apikey
  2. 無料枠: 15リクエスト/分まで無料
  3. 環境変数設定:
    1
    
    export GOOGLE_API_KEY='your-api-key'
    

OpenRouterでは動かない

試行錯誤の結果、OpenRouterの無料Geminiモデルでは動作しませんでした。

エラー内容:

openai.BadRequestError: Error code: 400 - 
{'error': {'message': "Invalid parameter: 'response_format' of type 'json_object' is not supported with this model."}}

原因:

  1. LangExtractのOpenAIプロバイダーは response_format: {type: "json_object"} を強制的に設定
  2. OpenRouterの無料Geminiモデルはこのパラメータをサポートしていない

OpenRouterで使う場合は、有料モデル(GPT-4、Claude等)が必要です。

ユースケース

適している用途

  1. 医療・法務文書: 抽出根拠の明示が必須
  2. 長文書処理: 契約書、論文、議事録など
  3. 監査・検証: 抽出結果の妥当性確認が必要
  4. 多言語対応: 日本語を含む非英語テキスト
  5. データベース化: 抽出結果を構造化データとして保存・分析

適していない用途

  • シンプルな構造化出力のみ(Instructorで十分)
  • 自動最適化が必要(LangStructが適切)
  • リアルタイム性重視(Batch APIは遅い)

まとめ

LangExtractは「LLMの抽出結果に文字位置を付ける」という、これまでのライブラリにはなかった機能を提供します。

本記事で説明した流れ:

  1. 構造化抽出の必要性: 大量文書からの情報抽出を自動化したい
  2. 従来ライブラリの問題: 「どこから抽出したか」が分からない
  3. 出典ポインタの重要性: 医療・法務・監査では根拠の明示が必須
  4. LangExtractの解決策: 文字位置まで記録してトレーサビリティを確保
  5. 実用例: データベースに保存して分析可能

特に医療・法務・監査など「根拠が重要」な領域では、この機能が決定的な差になります。Few-shotの例の質が抽出精度を決定するため、ドメイン特化の例を用意すれば、ファインチューニング不要で高精度な抽出が可能です。

日本語対応も良好で、今回のテストでは問題なく動作しました。OpenRouterの無料モデルでは動かない点は注意が必要ですが、Google AI Studioの無料枠で十分試せます。

興味のある方はぜひ試してみてください。

参考リンク