AI-Chain

Google LangExtract:把 LLM 抽取結果釘回原文,讓非結構化文字真正可驗證

LangExtract 不只讓 LLM 輸出結構化資料,還把每筆 extraction 對齊回來源文字的 character span,並透過 JSONL 與互動式 HTML 支援人工核對。本文拆解它的資料模型、長文件管線、provider 架構與實際上手方式。

分享:
Google LangExtract:把 LLM 抽取結果釘回原文,讓非結構化文字真正可驗證

Google LangExtract:把 LLM 抽取結果釘回原文,讓非結構化文字真正可驗證

很多 LLM 應用的第一個 demo 都是「把一段文字丟進模型,請它回傳 JSON」。真正進入產品後,問題才開始浮現:這個欄位是從哪一句話抽出來的?模型是不是把 few-shot 範例誤當成輸入內容?長文件切段之後,跨段落的關係還在不在?如果使用者不能回到原文核對,結構化輸出看起來再漂亮,也很難成為可靠的資料管線。

我這次在 GitHub 高星專案掃描中選到 Google 的 langextract。截至本次查核,它已經超過 3.8 萬顆星,且最近仍有更新;專案採 Apache 2.0,定位是一個 Python library,不是單純的 prompt 範例或資源清單。它的核心價值也很清楚:用 LLM 從非結構化文字抽取結構化資訊,同時把每個 extraction 對應回來源文字的精確 character span,再輸出可互動檢視的 HTML。

這個設計讓 LangExtract 不只是「讓模型吐 JSON」,而是把抽取結果、來源證據和人工審查放在同一條工作流裡。我的判斷是:如果你的 AI 應用需要處理長文件、醫療紀錄、客服對話、法務文件或研究資料,真正值得研究的不是它能不能抽出欄位,而是它能不能讓每個欄位都保留可追溯的證據。

先講結論:LangExtract 解的是可追溯性,不是幻覺的終結

LangExtract 最有辨識度的設計有四個層次:

  1. 用自然語言指令與 few-shot examples 定義抽取任務,不需要先微調模型。
  2. 把長文件切成可處理的 chunks,再平行送出推論,必要時可用多個 extraction passes 增加召回率。
  3. 把模型回傳的 extraction 對齊回原文,每筆資料可帶 char_intervalalignment_statusattributes 等資訊。
  4. 把 JSONL 與互動式視覺化接起來,讓人可以在原文脈絡中檢查結果,而不是只看一張扁平表格。

這四層組合起來,才是它和一般「LLM + JSON schema」範例的差異。Schema 可以限制輸出的形狀,卻不等於輸出有證據;LangExtract 把「資料長什麼樣」和「資料在來源哪裡」一起處理。

但這裡要先劃清界線:source grounding 只能回答「這段文字能不能在輸入中找到」,不能保證模型推導出的 attribute 一定是真的。README 也明確提醒,推論品質仍會受模型、prompt、範例和任務複雜度影響。對醫療、法律或金融場景來說,它是可審查性基礎,不是自動核准器。

它的資料模型,為什麼比一個 JSON dictionary 更實用?

在 LangExtract 的核心資料模型裡,一筆 Extraction 至少包含 extraction_classextraction_text,還可以帶上 char_intervalalignment_statusextraction_indexgroup_indexdescriptionattributes。其中最關鍵的是 char_interval:它使用來源文字的起訖位置表示 extraction 的範圍,起點包含、終點不包含。

這個決定帶來三個實務好處:

  • 可以反查證據:前端不用猜測哪句話支持某個欄位,直接用字元區間標亮原文。
  • 可以區分未對齊結果:如果模型從 few-shot example 借用了文字,而該文字不在真正輸入裡,系統可能無法定位,char_interval 會是 None。這比悄悄把錯誤資料塞進資料庫更安全。
  • 可以保留關係與屬性:例如抽取一個藥物名稱,再用 attributes 放 dosage、route 或 frequency;或者抽取人物與關係,讓後續資料處理不必重新解析一段自然語言。

這也是我認為 LangExtract 適合做「人機協作資料整理」的原因。模型先提出候選,系統保留證據,人再依照原文快速驗證。它不是把人工審查拿掉,而是把人工審查從「重新閱讀整份文件」縮小成「確認已標出的片段」。

從 prompt 到視覺化:一條完整的抽取管線

1. 用 examples 定義輸出行為

LangExtract 的 examples 不是裝飾,它們同時示範抽取類別、原文片段和 attributes。官方文件特別要求 extraction_text 儘量逐字出現在 example 的 text 中,並且依照原文出現順序排列;如果範例有 paraphrase、重疊或順序錯誤,系統會出現 prompt alignment 警告。

這個要求看似嚴格,實際上很合理。若你希望後面能把結果對回原文,example 本身就不能先破壞「文字證據 ↔ 結構化欄位」的對應。換句話說,LangExtract 把資料標註習慣往 prompt 設計前移了。

2. 長文件先切塊,再推論

官方範例提供 max_char_buffermax_workersextraction_passes 等參數。切塊是為了控制每次送進模型的上下文大小;平行處理可以提高吞吐量;多次 extraction pass 則是用額外成本換取更高召回率。

原始碼中的 Annotator 會把文件轉成 chunks,將 chunk 組成 prompt,交給 language model,再交給 resolver 將模型輸出解析與對齊回原文。多個 pass 的結果會合併;如果不同 pass 的 extraction 位置互相重疊,原始碼採用「較早 pass 優先」的策略,後續 pass 只補進不重疊的結果。

這個行為值得記進產品設計文件:extraction_passes=3 不是免費的「再跑兩次」,它可能提高召回率,也會增加模型請求、費用、延遲和去重判斷的複雜度。對大量文件,應先用小樣本比較單次與多次 pass 的 precision、recall 和成本,再決定預設值。

3. Provider factory 把模型選擇和抽取邏輯分開

目前專案用 provider factory 和 entry points 發現模型供應商。內建 provider 包含 Gemini、Ollama 和 OpenAI,ModelConfig 可以用 model_id、明確的 provider 以及 provider_kwargs 建立模型。這讓抽取程式不必把每個模型 API 的初始化方式硬編碼在業務邏輯裡。

對團隊來說,這個分層比「支援很多模型」更重要。你可以先用本機 Ollama 做資料格式和對齊測試,再切到雲端模型做品質或吞吐量評估;抽取任務的 examples、輸出處理和視覺化流程不需要整套重寫。

不過不同 provider 的能力並不完全相同。README 目前說明 Gemini 與 OpenAI 支援 output_schema 的情境,而 Ollama 尚不支援這個參數;使用 OpenAI-compatible endpoint 時,也可能需要明確指定 provider 和 base_url。因此不要把「同一個 model_id 呼叫方式」誤解成「所有 provider 的輸出保證相同」。

4. JSONL 不是終點,互動式 HTML 才是審查介面

抽取結果可以用 lx.io.save_annotated_documents 寫成 JSONL,再交給 lx.visualize 產生 self-contained HTML。這個 HTML 的價值不在於展示漂亮,而在於把大量 extraction 放回原文脈絡中。

對資料工程而言,JSONL 適合進入下游 pipeline;對分析師或領域專家而言,視覺化頁面適合快速檢查。兩者並存,讓同一份結果同時服務機器處理和人工 QA,而不是為了人工檢查另做一套資料格式。

如何開始:先用本機 Ollama 做一個可驗證任務

下面用官方支援的 Ollama provider 示範,避免把 API key 直接放在程式裡。前置條件是 Python 3.10 以上、已安裝 Ollama,並且有足夠記憶體執行你選擇的模型。

先建立隔離環境並安裝套件:

python -m venv .venv
source .venv/bin/activate
pip install langextract

啟動 Ollama 並準備一個模型:

ollama serve
ollama pull gemma2:2b

接著把任務縮小成「從一段客服紀錄中抽取產品、問題與嚴重度」。重點不是讓模型寫一篇摘要,而是要求它引用輸入中確實存在的文字:

import langextract as lx

prompt = """
Extract product names, reported problems, and severity in order of appearance.
Use exact text from the input. Do not paraphrase or overlap extractions.
Put a short normalized label in attributes when useful.
"""

examples = [
    lx.data.ExampleData(
        text="使用者回報 CloudBox 在同步相片時顯示逾時,影響程度為高。",
        extractions=[
            lx.data.Extraction(
                extraction_class="product",
                extraction_text="CloudBox",
                attributes={"normalized": "CloudBox"},
            ),
            lx.data.Extraction(
                extraction_class="problem",
                extraction_text="同步相片時顯示逾時",
                attributes={"normalized": "sync timeout"},
            ),
            lx.data.Extraction(
                extraction_class="severity",
                extraction_text="高",
                attributes={"normalized": "high"},
            ),
        ],
    )
]

text = "使用者說 CloudBox 登入正常,但同步相片仍逾時,這次影響程度為中。"
result = lx.extract(
    text_or_documents=text,
    prompt_description=prompt,
    examples=examples,
    model_id="gemma2:2b",
    model_url="http://localhost:11434",
)

# 只保留能定位回輸入文字的結果
for extraction in result.extractions:
    if extraction.char_interval:
        print(
            extraction.extraction_class,
            extraction.extraction_text,
            extraction.char_interval,
            extraction.attributes,
        )

這段程式的第一個驗證點不是「模型有沒有回傳三個欄位」,而是每個結果是否有 char_interval,以及 extraction_text 能否在輸入中被核對。若 Ollama 模型沒有產生穩定結果,先檢查服務是否真的在 localhost:11434、模型名稱是否已 pull,以及 examples 是否遵守逐字、非重疊、依序出現三個條件。

完成小文件測試後,再加入 JSONL 與 HTML:

lx.io.save_annotated_documents(
    [result], output_name="extraction_results.jsonl", output_dir="."
)
html = lx.visualize("extraction_results.jsonl")
with open("visualization.html", "w", encoding="utf-8") as file:
    file.write(html.data if hasattr(html, "data") else html)

對長文件,可以把 text_or_documents 改成官方支援的 URL 或文件集合,再逐步調整:

result = lx.extract(
    text_or_documents="https://www.gutenberg.org/files/1513/1513-0.txt",
    prompt_description=prompt,
    examples=examples,
    model_id="gemma2:2b",
    model_url="http://localhost:11434",
    extraction_passes=2,
    max_workers=4,
    max_char_buffer=1000,
)

我會建議先保守設定 max_workers,因為本機模型的吞吐量通常比 API 文件中的理論並行度更容易成為瓶頸;先量測記憶體、延遲和結果品質,再增加平行度。

三個最容易踩到的坑

坑一:把 grounding 當成 fact checking

char_interval 能證明抽取文字落在輸入中,不能證明輸入本身正確,也不能證明 attributes 的推論沒有偏差。例如模型可以把「高風險」這個詞從原文標出來,但「高風險」是否符合公司的風險定義,仍需要規則、資料庫或專家審核。

比較穩妥的資料表設計,是把 source_textchar_startchar_endextraction_classattributesreview_status 分開保存。不要只保存模型最後整理出的欄位,否則後續仍然要重新呼叫模型才能追查。

坑二:examples 寫得像摘要,結果就會失去可對齊性

如果 example 的 extraction_text 不是原文片段,或同一段文字有重疊的 extraction,模型可能學到不一致的標註習慣。README 提到的 prompt alignment 警告不是可以忽略的噪音,而是資料契約出了問題的訊號。

我的做法是先用一個很短的 example 跑通,再增加第二個 example 覆蓋邊界情況:同義詞、同一實體多次出現、相鄰實體、沒有任何符合項目的文件。每次只改一個變因,才知道品質變化來自 prompt、模型還是 resolver。

坑三:長文件的成本不只取決於字數

切塊、平行、多次 pass、context window 都會影響實際成本。extraction_passes 增加時,模型請求數和結果合併風險也可能增加;max_char_buffer 太小,跨句關係可能被切斷;max_char_buffer 太大,又可能使模型在一個 chunk 內漏掉細節。

正式導入前至少建立一個小型 benchmark:固定一批人工標註文件,記錄每個設定的 precision、recall、未對齊比例、平均延遲、token 或 API 成本。沒有這些數據時,不要只因為多 pass 看起來更「仔細」就把它設成預設值。

LangExtract 適合放在什麼位置?

它不是向量資料庫,也不是完整的 RAG framework,更不是取代 OCR、資料庫驗證或 workflow orchestration 的平台。它比較像一個「以 LLM 為核心、但把證據對齊與人工審查納入設計」的抽取層。

  • 和傳統 NER 比:LangExtract 更容易用 few-shot examples 調整任務,也能抽 attributes 與關係;代價是需要模型推論,穩定性、成本和延遲要管理。
  • 和直接要求 JSON 比:它多了來源區間、對齊狀態和視覺化;但 schema 或 provider 能力仍有差異,不能假設每個模型都提供同等強度的 constrained output。
  • 和 RAG 比:RAG 的重點是找回相關內容供生成使用;LangExtract 的重點是把內容轉成可追溯的結構化標註。兩者可以串接,例如先從合約抽取條款,再把帶證據的結果建立索引。
  • 和資料標註工具比:LangExtract 可以先做機器預標註與證據標記,讓人工審核更快;但高風險資料仍要保留人工覆核流程。

誰適合先試?

我會優先推薦給三類團隊:第一,手上有大量文字,但人工整理成本高;第二,不能接受只拿到沒有來源的 JSON;第三,希望在雲端模型與本機模型之間保留切換空間。客服工單、研究摘要、合約條款、醫療紀錄結構化、產品回饋分類,都是合理的試點。

不適合直接導入的情況也很明確:如果你的任務只需要非常穩定的固定格式轉換,傳統 parser 或規則可能更便宜;如果你要求每個推論 attribute 都具備外部世界的真值保證,單靠 LangExtract 不夠;如果文件包含敏感資料,又沒有完成資料傳輸、模型供應商和保存政策審查,不應只因為 API 很方便就上線。

另外,專案 README 明確提醒 LangExtract 並不是 Google 官方支援的產品;若用於健康相關應用,還要遵守其列出的相關使用條款。這些不是頁尾免責聲明而已,而是評估導入範圍時必須列入的治理條件。

我的判斷

LLM 抽取的下一個競爭點,不會只是「誰能輸出更漂亮的 JSON」,而是「誰能讓團隊更快相信、檢查並修正這份 JSON」。LangExtract 把 source grounding、chunking、provider abstraction 和 visualization 放在同一個 Python library 裡,讓開發者可以從一個小任務開始,逐步測量抽取品質,再決定要不要擴張到長文件與批次處理。

它不會消除模型錯誤,也不會替你完成領域驗證;但它把最常被忽略的證據鏈補上了。對 AI Chain 讀者來說,我認為最值得帶走的不是某一個 model_id,而是這個設計原則:任何會進入資料庫、決策流程或人工審查的 LLM 輸出,都應該盡量保留它在來源中的位置,以及人下一步要如何驗證它。


參考資料