打造本地智能文档处理栈

APPLICATION Jan 18, 2025

在这个 LLM 新时代,银行和金融机构处于劣势,因为前沿模型几乎不可能在本地使用,因为它们有硬件要求。然而,银行数据的敏感性带来了严重的隐私问题,尤其是当这些模型仅作为云服务提供时。为了应对这些挑战,组织可以转向本地或小型语言模型 (SLM) 设置以将数据保留在内部,避免敏感信息的潜在泄露。这种方法允许你利用高级 LLM(本地或使用最少的外部调用),同时确保严格遵守 GDPR、HIPAA 或各种财务指令等法规。

本文展示了如何通过结合以下各项来构建完全本地的文档智能解决方案:

  • ExtractThinker — 一个开源框架,用于协调 LLM 的 OCR、分类和数据提取管道
  • Ollama — 一个用于 Phi-4 或 Llama 3.x 等语言模型的本地部署解决方案
  • DoclingMarkItDown — 灵活的库,用于处理文档加载、OCR 和布局解析

无论你是在严格的保密规则下操作、处理扫描的 PDF,还是只是想要高级的基于视觉的提取,这个端到端栈都可以在你自己的基础架构内提供安全、高性能的管道。

1、选择正确的模型(文本与视觉)

在构建文档智能栈时,首先要确定你需要纯文本模型还是支持视觉的模型。纯文本模型通常是本地解决方案的首选,因为它们广泛可用且限制较少。但是,支持视觉的模型对于高级拆分任务至关重要,尤其是当文档依赖于视觉提示(如布局、配色方案或不同格式)时。

在某些情况下,你可以将不同的模型配对用于不同的阶段。例如,较小的 moondream 模型(0.5B 参数)可能处理拆分,而 Phi-4 14B 模型管理分类和提取。许多大型机构更喜欢部署单个更强大的模型(例如 70B 范围内的 Llama 3.3 或 Qwen 2.5)来涵盖所有用例。如果你只需要以英语为中心的 IDP,那么可以简单地使用 Phi4 完成大多数任务,并保留一个轻量级的 moondream 模型以备不时之需。这完全取决于你的具体要求和可用的基础设施。

2、处理文档:MarkItDown 与 Docling

对于文档解析,两个流行的库脱颖而出:

MarkItDown:

  • 更简单、直接,受到 Microsoft 的广泛支持
  • 非常适合不需要多个 OCR 引擎的直接基于文本的任务
  • 易于安装和集成

Docling:

  • 更先进,具有多 OCR 支持(Tesseract、AWS Textract、Google Document AI 等)
  • 非常适合扫描工作流程或从图像 PDF 中进行强大的提取
  • 详细的文档,灵活适用于复杂的布局

ExtractThinker 可让你根据需要(简单的数字 PDF 或多引擎 OCR)切换 DocumentLoaderMarkItDownDocumentLoaderDocling

3、运行本地模型

尽管 Ollama 是一种流行的本地托管 LLM 工具,但现在有几种本地部署解决方案可以与 ExtractThinker 无缝集成:

  • LocalAI — 一个在本地模仿 OpenAI API 的开源平台。它可以在消费级硬件(甚至仅限 CPU)上运行 LLM,例如 Llama 2 或 Mistral,并提供一个简单的连接端点。
  • OpenLLM — BentoML 的一个项目,通过与 OpenAI 兼容的 API 公开 LLM。它针对吞吐量和低延迟进行了优化,适用于本地和云,并支持各种开源 LLM。
  • Llama.cpp — 一种运行具有高级自定义配置的 Llama 模型的较低级别方法。非常适合精细控制或 HPC 设置,尽管管理起来更复杂。

Ollama 通常是首选,因为它易于设置且 CLI 简单。但是,对于企业或 HPC 场景,Llama.cpp 服务器部署、OpenLLM 或 LocalAI 等解决方案可能更合适。只需将本地 LLM 端点指向代码中的环境变量或基本 URL,即可将所有这些都与 ExtractThinker 集成。

4、处理小上下文窗口

在使用上下文窗口有限的本地模型时(例如,~8K 个标记或更少),管理以下两点至关重要:

4.1 拆分文档

为了避免超出模型的输入容量,延迟拆分是理想的选择。不是一次提取整个文档:

  • 逐步比较页面(例如,第 1-2 页,然后是第 2-3 页),确定它们是否属于同一子文档。
  • 如果它们确实存在,则将它们放在一起以备下一步使用。如果不存在,则开始新的片段。
  • 这种方法节省内存,并且只需一次加载和分析几个页面即可扩展到非常大的 PDF。

注意:当你有更高的令牌限额时,拼接是理想的选择;对于有限的窗口,分页是首选。

4.2 处理部分响应

对于较小的本地模型,如果提示很大,则每个响应也存在截断的风险。 PaginationHandler 通过以下方式优雅地解决了这个问题:

  • 将文档的页面拆分为单独的请求(每个请求一页)。
  • 最后合并页面级结果,如果页面在某些字段上不一致,则可选择冲突解决方案。

注意:当有更高的令牌限额时,拼接是理想的选择;对于有限的窗口,分页是首选。

4.3 快速示例流程

  • 惰性拆分 PDF,使每个块/页面保持在模型的限制以下。
  • 跨页面分页:每个块的结果分别返回。
  • 将部分页面结果合并到最终的结构化数据中。

这种最小方法可确保你永远不会超出本地模型的上下文窗口——无论是在输入 PDF 的方式还是在处理多页响应的方式。

5、ExtractThinker:构建栈

下面是一个最小的代码片段,展示了如何集成这些组件。首先,安装 ExtractThinker

pip install extract-thinker

5.1 文档加载器

如上所述,我们可以使用 MarkitDown 或 Docling。

from extract_thinker import DocumentLoaderMarkItDown, DocumentLoaderDocling

# DocumentLoaderDocling or DocumentLoaderMarkItDown
document_loader = DocumentLoaderDocling()

5.2 定义合约

我们使用基于 Pydantic 的合约来指定我们要提取的数据的结构。例如,发票和驾驶执照:

from extract_thinker.models.contract import Contract
from pydantic import Field

class InvoiceContract(Contract):
    invoice_number: str = Field(description="Unique invoice identifier")
    invoice_date: str = Field(description="Date of the invoice")
    total_amount: float = Field(description="Overall total amount")

class DriverLicense(Contract):
    name: str = Field(description="Full name on the license")
    age: int = Field(description="Age of the license holder")
    license_number: str = Field(description="License number")

5.3 分类

如果你有多种文档类型,请定义分类对象。你可以指定:

  • 每个分类的名称(例如“发票”)。
  • 描述。
  • 它映射到的合约。
from extract_thinker import Classification

TEST_CLASSIFICATIONS = [
    Classification(
        name="Invoice",
        description="This is an invoice document",
        contract=InvoiceContract
    ),
    Classification(
        name="Driver License",
        description="This is a driver license document",
        contract=DriverLicense
    )
]

5.4 将所有内容放在一起:本地提取过程

下面,我们创建一个使用我们选择的 document_loader 和本地模型(Ollama、LocalAI 等)的提取器。然后我们构建一个流程,在单个管道中加载、分类、拆分和提取。

import os
from dotenv import load_dotenv

from extract_thinker import (
    Extractor,
    Process,
    Classification,
    SplittingStrategy,
    ImageSplitter,
    TextSplitter
)

# Load environment variables (if you store LLM endpoints/API_BASE, etc. in .env)
load_dotenv()

# Example path to a multi-page document
MULTI_PAGE_DOC_PATH = "path/to/your/multi_page_doc.pdf"

def setup_local_process():
    """
    Helper function to set up an ExtractThinker process
    using local LLM endpoints (e.g., Ollama, LocalAI, OnPrem.LLM, etc.)
    """

    # 1) Create an Extractor
    extractor = Extractor()

    # 2) Attach our chosen DocumentLoader (Docling or MarkItDown)
    extractor.load_document_loader(document_loader)

    # 3) Configure your local LLM
    #    For Ollama, you might do:
    os.environ["API_BASE"] = "http://localhost:11434"  # Replace with your local endpoint
    extractor.load_llm("ollama/phi4")  # or "ollama/llama3.3" or your local model
    
    # 4) Attach extractor to each classification
    TEST_CLASSIFICATIONS[0].extractor = extractor
    TEST_CLASSIFICATIONS[1].extractor = extractor

    # 5) Build the Process
    process = Process()
    process.load_document_loader(document_loader)
    return process

def run_local_idp_workflow():
    """
    Demonstrates loading, classifying, splitting, and extracting
    a multi-page document with a local LLM.
    """
    # Initialize the process
    process = setup_local_process()

    # (Optional) You can use ImageSplitter(model="ollama/moondream:v2") for the split
    process.load_splitter(TextSplitter(model="ollama/phi4"))

    # 1) Load the file
    # 2) Split into sub-documents with EAGER strategy
    # 3) Classify each sub-document with our TEST_CLASSIFICATIONS
    # 4) Extract fields based on the matched contract (Invoice or DriverLicense)
    result = (
        process
        .load_file(MULTI_PAGE_DOC_PATH)
        .split(TEST_CLASSIFICATIONS, strategy=SplittingStrategy.LAZY)
        .extract(vision=False, completion_strategy=CompletionStrategy.PAGINATE)
    )

    # 'result' is a list of extracted objects (InvoiceContract or DriverLicense)
    for item in result:
        # Print or store each extracted data model
        if isinstance(item, InvoiceContract):
            print("[Extracted Invoice]")
            print(f"Number: {item.invoice_number}")
            print(f"Date: {item.invoice_date}")
            print(f"Total: {item.total_amount}")
        elif isinstance(item, DriverLicense):
            print("[Extracted Driver License]")
            print(f"Name: {item.name}, Age: {item.age}")
            print(f"License #: {item.license_number}")

# For a quick test, just call run_local_idp_workflow()
if __name__ == "__main__":
    run_local_idp_workflow()

6、隐私和 PII:云中的 LLM

并非每个组织都可以(或想要)运行本地硬件。有些组织更喜欢高级的基于云的 LLM。如果是这样,请记住:

  • 数据隐私风险:将敏感数据发送到云会引发潜在的合规性问题。
  • GDPR/HIPAA:法规可能会完全限制数据离开你的场所。
  • VPC + 防火墙:你可以在私有网络中隔离云资源,但这会增加复杂性。

注意:许多 LLM API(例如 OpenAI)确实提供 GDPR 合规性。但如果你受到严格监管或希望自由切换提供商,请考虑本地或屏蔽云方法。

6.1 PII 屏蔽

一种强大的方法是构建 PII 屏蔽管道。Presidio 等工具可以在发送到 LLM 之前自动检测和编辑个人标识符。这样,你既可以保持模型无关性,又可以保持合规性。

7、结束语

通过将 ExtractThinker 与本地 LLM(例如 Ollama、LocalAI 或 OnPrem.LLM)和灵活的 DocumentLoader(Docling 或 MarkItDown)相结合,你可以从头开始构建安全的本地文档智能工作流。如果监管要求要求完全隐私或最少的外部调用,此技术栈会将你的数据保留在内部,而不会牺牲现代 LLM 的功能。


原文链接:Building an On-Premise Document Intelligence Stack with Docling, Ollama, Phi-4

汇智网翻译整理,转载请标明出处

Tags