提取标准 OCR 遗漏的图表数据:Elastic Agent Builder 和 LlamaParse 在一个管道中

作者:来自 Elastic Jeffrey Rengifo

构建一个端到端流水线,该流水线从复杂 PDF文档 中提取结构化数据(包括图表中的数值),并将其导入 Elasticsearch ,支持使用 ES|QL 的 agent 查询就绪。

Agent Builder 现在已正式发布。通过注册 Elastic Cloud Trial 开始使用,并查看 Agent Builder 文档 这里。


企业级文档很难处理。包含多栏表格、扫描页面、混合布局以及嵌入图表的 PDF 会让简单的文本提取失效。标准处理流水线会遗漏这些文档中隐藏的结构化数据,这意味着你的 agent 在基于不完整上下文进行推理。大多数 agent 框架使用基础 OCR 工具处理 PDF,它们在纯文本文档中表现尚可,但在更复杂场景下表现不足。

为了解决这个问题,你需要一个系统,能够从这些文档中准确提取结构化数据,并自动将其索引,以便 agent 使用。Elastic Agent Builder 和 LlamaParse Extract API 的组合覆盖了数据处理与推理两个环节。LlamaParse Extract 负责文档复杂性处理:它将 schema 定义的提取模型应用到原始 PDF 上,并返回结构化 JSON。Elastic Agent Builder 负责编排:通过 Elastic Workflows 调用 LlamaParse Extract API,将结果写入 Elasticsearch,并让 agent 能够通过 Elasticsearch Query Language (ES|QL) 或语义查询对这些数据进行推理。

本文将讲解完整实现流程,从 schema 定义到可运行的 agent。完整代码可在配套 notebook 中查看 companion notebook。

何时应该使用 LlamaParse Extract 而不是 LlamaParse?

当你需要返回结构化的 JSON 具体字段时,应使用 LlamaParse Extract。当你需要完整文档内容用于 RAG pipeline 时,应使用 LlamaParse。

LlamaCloud 提供两种文档处理工具。LlamaParse 将文档转换为 LLM-ready 格式(如 Markdown),保留完整文档布局。它用于 RAG(Retrieval Augmented Generation)pipeline,当你希望将整个文档内容送入向量库或 LLM 上下文窗口时使用。LlamaParse Extract 则相反,它基于开发者定义的 schema,只返回你指定的字段,并以校验过的 JSON 形式输出。它还可以提取图表、图形以及文档中的视觉元素数据,这是标准文本解析完全无法覆盖的能力。

LlamaParse / LlamaParse Extract
输出格式 Markdown(完整文档) 结构化 JSON(基于 schema 定义的字段)
最适合场景 RAG pipelines,LLM 上下文窗口 搜索索引、数据库、类型化字段
是否支持图表/图形 否 是
输入 PDF PDF + 开发者定义的 schema

对于这个用例来说,Extract 是更好的选择。我们需要具体的数值(GDP 占比百分比、投资增长率)以及叙述性总结,并将其作为类型化的 Elasticsearch 字段进行索引。schema 会告诉提取模型具体要寻找什么,包括那些只存在于 PDF 可视化图表中的数值。

如果你的用例是将完整文档内容输入到 RAG pipeline,那么 LlamaParse 是正确工具。如果你需要用于搜索索引或数据库的结构化记录,尤其是当数据存在于图表或表格中时,应使用 Extract。

LlamaParse Extract 和 Elastic Agent Builder 流水线做了什么?

这个流水线的工作方式如下:用户发送一个问题,并附带一个 PDF 的 URL。agent 会调用一个 workflow 工具,将 PDF 上传到 LlamaParse,通过 LlamaParse Extract API 执行基于 schema 的结构化提取,然后把结构化结果索引到 Elasticsearch 中。

然后 agent 会使用 ES|QL 对该索引进行查询,以回答这个问题。

为了演示这一点,我们使用 世界银行全球经济展望(2026年1月) 报告作为源文档。该 agent 将能够直接从该 PDF 中回答关于前沿市场、经济指标以及政策建议的问题。

如何为 LlamaParse Extract 构建一个 Elastic Workflows 工具

Workflow tool 在 Elastic Agent Builder 中作为基于 YAML 定义的自动化运行。当被触发时,它会执行六个步骤。关于将 Agent Builder 和 Workflows 连接起来 的通用流程,该文章有更详细的搭建说明。这里我们重点讲 LlamaParse Extract 的特定集成方式。

  1. 上传 PDF: 从提供的 URL 将 PDF 上传到 LlamaCloud。该 workflow 接收 pdf_url 作为输入;你的 LlamaCloud API key、project ID 和 configuration ID 作为常量进行配置。

  2. 创建提取任务: 使用已保存的配置,在已上传文件上启动 Extract v2 任务。

  3. 等待: 暂停 15 秒,给 LlamaParse Extract 留出处理文档的时间。

  4. 轮询直到完成: 在 while 循环中每 10 秒轮询一次提取状态,直到状态变为 COMPLETED,最多循环 19 次(约 3 分钟上限)。

  5. 索引: 将 PDF 中所有提取字段作为单个 Elasticsearch 文档写入。

  6. 验证: 使用 document_id 以及查询 term 查询 在 Elasticsearch 中执行搜索,以确认文档已成功索引。

前置条件

使用 Elastic Agent Builder 实现 LlamaParse Extract:分步说明

下面的代码展示了实现的关键步骤。完整版本(包含所有细节)请参考配套 notebook companion notebook。

配置环境

复制代码
import os
import json
from dotenv import load_dotenv

load_dotenv()

ELASTICSEARCH_URL = os.getenv("ELASTICSEARCH_URL")
ELASTICSEARCH_API_KEY = os.getenv("ELASTICSEARCH_API_KEY")
LLAMA_CLOUD_API_KEY = os.getenv("LLAMA_CLOUD_API_KEY")
KIBANA_URL = os.getenv("KIBANA_URL")

定义提取 schema

LlamaParse Extract 是基于 schema 驱动的。你需要定义一个 Pydantic 模型,用来描述希望提取的字段,而 LlamaParse Extract 会基于该模型从原始 PDF 中引导提取。这正是它在复杂文档中保持可靠性的关键。与其 "期望" LLM 找到正确的值,不如明确告诉它需要找什么。

如上所述,agent 需要处理两类问题:

  • 结构化问题: "2025 年前沿市场 GDP 占比是多少?" 这类问题需要可用 ES|QL 过滤的数值字段。

  • 探索性问题: "前沿市场的主要风险是什么?" 这类问题需要可进行语义检索的叙述性文本字段。

这个 schema 只捕捉支持这两类问题所需的最小信息集合。description 字段不是文档说明,而是对提取模型的指令,因此越具体,提取质量越高。其中两个字段(frontier_market_gdp_share_pct 和 frontier_market_investment_growth_2020s_pct)直接来自报告中的柱状图(图 A 和图 C),这些数据是标准文本提取工具无法捕获的。

复制代码
class EconomicReportSummary(BaseModel):
    report_title: str = Field(description="Full title of the report")
    publication_date: str = Field(
        description="Publication date in YYYY-MM format, e.g. '2026-01'"
    )
    frontier_market_gdp_share_pct: float = Field(
        description="Frontier markets' share of global GDP in 2025 as a percentage, "
                    "extracted from Figure ES.A bar chart"
    )
    frontier_market_investment_growth_2020s_pct: float = Field(
        description="Average annual per capita investment growth for frontier markets "
                    "in the early 2020s (2020-24), extracted from Figure ES.C bar chart, as a percentage"
    )
    executive_summary: str = Field(
        description="Concise summary of the report's main findings and conclusions"
    )
    key_vulnerabilities: str = Field(
        description="Main vulnerabilities and risks facing frontier markets, as a paragraph"
    )
    policy_recommendations: str = Field(
        description="Key policy recommendations for frontier market policymakers, as a paragraph"
    )

创建 Extract 配置

LlamaExtract v2 用 "已保存配置(saved configurations)" 替代了旧的 extraction agents,本质上是一个可复用的参数集合,用来将你的 schema 与 extraction tier 绑定在一起。首先获取你的 project ID,然后通过 POST 创建配置。保存打印出来的两个 ID,因为你会在 workflow YAML 中用到它们。

复制代码
import requests

LLAMA_CLOUD_BASE_URL = "https://api.cloud.llamaindex.ai"
llama_headers = {
    "Authorization": f"Bearer {LLAMA_CLOUD_API_KEY}",
    "Content-Type": "application/json",
}

# Fetch the project ID (uses the first available project)
projects_response = requests.get(
    f"{LLAMA_CLOUD_BASE_URL}/api/v1/projects",
    headers=llama_headers,
)

projects_response.raise_for_status()
PROJECT_ID = projects_response.json()[0]["id"]
print(f"Project ID: {PROJECT_ID}")

# Create a saved Extract v2 configuration with our schema
config_response = requests.post(
    f"{LLAMA_CLOUD_BASE_URL}/api/v1/beta/configurations",
    headers=llama_headers,
    params={"project_id": PROJECT_ID},
    json={
        "name": "global-economic-extractor",
        "parameters": {
            "product_type": "extract_v2",
            "data_schema": EconomicReportSummary.model_json_schema(),
            "extraction_target": "per_doc",
            "tier": "agentic",
        },
    },
)

config_response.raise_for_status()
CONFIGURATION_ID = config_response.json()["id"]
print(f"Configuration ID: {CONFIGURATION_ID}")

创建 Elasticsearch 索引

创建索引时,需要使用与 extraction schema 对应的 mapping。数值字段(例如 frontier_market_gdp_share_pct)使用 number 类型,以支持 ES|QL 结构化过滤。publication_date 字段使用 date 类型,以支持时间范围查询。叙述性字段(例如 executive_summary 和 key_vulnerabilities)使用 text 类型,用于 全文检索。标识类字段(例如 report_title)使用 keyword 类型。完整 mapping 定义可在 notebook 中查看。

构建 Agent Builder workflow 工具

将下面的 YAML 复制并粘贴到 Elastic UI 中的 Elasticsearch > Workflows > Create a new Workflow 。Elastic Workflows 文档 覆盖了完整 YAML schema 以及可用的 step types。

该 workflow 使用 LlamaCloud 的 REST API:通过 Files API(/api/v1/files/upload_from_url)从公开 URL 上传 PDF;并通过 Extract v2 API(/api/v2/extract)创建任务并轮询结果(/api/v2/extract/{id})。

复制代码
name: LlamaParse Extract Economic Report Processor
description: >
  Uploads a PDF from a URL to LlamaCloud, runs Extract v2,
  and indexes the structured results into Elasticsearch.
enabled: true

inputs:
  - name: pdf_url
    type: string
    description: Public URL of the PDF to process
    required: true

consts:
  indexName: economic-reports
  llamaBaseUrl: https://api.cloud.llamaindex.ai
  projectId: <YOUR_PROJECT_ID>
  configurationId: <YOUR_CONFIGURATION_ID>
  documentId: global-economic-prospects-jan-2026
  llamaCloudApiKey: llx-YOUR-API-KEY-HERE

triggers:
  - type: manual

steps:
  # Upload PDF from URL to LlamaCloud
  - name: upload_pdf
    type: http
    with:
      url: "{{ consts.llamaBaseUrl }}/api/v1/files/upload_from_url"
      method: PUT
      headers:
        Authorization: "Bearer {{ consts.llamaCloudApiKey }}"
        Content-Type: application/json
      body: |
        {
          "url": "{{ inputs.pdf_url }}"
        }

  # Create the Extract v2 job using our saved configuration
  - name: create_extraction_job
    type: http
    with:
      url: "{{ consts.llamaBaseUrl }}/api/v2/extract?project_id={{ consts.projectId }}"
      method: POST
      headers:
        Authorization: "Bearer {{ consts.llamaCloudApiKey }}"
        Content-Type: application/json
      body: |
        {
          "file_input": "{{ steps.upload_pdf.output.data.id }}",
          "configuration_id": "{{ consts.configurationId }}"
        }

  - name: wait_for_extraction
    type: wait
    with:
      duration: "15s"

  # Poll every 10s until status is COMPLETED (max ~3 min)
  - name: poll_until_done
    type: while
    condition: 'not steps.poll_get.output.data.status : "COMPLETED"'
    max-iterations:
      limit: 19
      on-limit: fail
    steps:
      - name: poll_wait
        type: wait
        with:
          duration: "10s"
      - name: poll_get
        type: http
        with:
          url: "{{ consts.llamaBaseUrl }}/api/v2/extract/{{ steps.create_extraction_job.output.data.id }}?project_id={{ consts.projectId }}"
          method: GET
          headers:
            Authorization: "Bearer {{ consts.llamaCloudApiKey }}"
            Accept: application/json

  - name: index_extracted_data
    type: elasticsearch.index
    with:
      index: "{{ consts.indexName }}"
      id: "{{ consts.documentId }}"
      document:
        report_title: "{{ steps.poll_get.output.data.extract_result.report_title }}"
        publication_date: "{{ steps.poll_get.output.data.extract_result.publication_date }}"
        frontier_market_gdp_share_pct: "{{ steps.poll_get.output.data.extract_result.frontier_market_gdp_share_pct }}"
        frontier_market_investment_growth_2020s_pct: "{{ steps.poll_get.output.data.extract_result.frontier_market_investment_growth_2020s_pct }}"
        executive_summary: "{{ steps.poll_get.output.data.extract_result.executive_summary }}"
        key_vulnerabilities: "{{ steps.poll_get.output.data.extract_result.key_vulnerabilities }}"
        policy_recommendations: "{{ steps.poll_get.output.data.extract_result.policy_recommendations }}"
      refresh: wait_for

  - name: verify_document
    type: elasticsearch.search
    with:
      index: "{{ consts.indexName }}"
      query:
        term:
          _id: "{{ consts.documentId }}"

用你自己的 llamaCloudApiKey、projectId 和 configurationId 更新 consts 部分。

与 Agent Builder 连接

保存 workflow 之后,使用 Agent Builder Kibana API 或 UI 创建两个工具和一个 agent。

复制代码
import requests

headers = {
    "Authorization": f"ApiKey {ELASTICSEARCH_API_KEY}",
    "kbn-xsrf": "true",
    "Content-Type": "application/json",
}

WORKFLOW_ID = "workflow-abcabc-0073-4a08-98a8-werwer"  # Copy from UI after creating the workflow

# Create the workflow tool
workflow_tool_payload = {
    "id": "run_llamaextract_workflow",
    "type": "workflow",
    "description": (
        "Triggers the LlamaParse Extract extraction workflow. "
        "Use this tool to extract structured data from a PDF URL and index it into Elasticsearch. "
        "Requires only the public URL of the PDF to process."
    ),
    "tags": ["llama-extract", "workflow"],
    "configuration": {
        "workflow_id": WORKFLOW_ID,
    },
}

response = requests.post(
    f"{KIBANA_URL}/api/agent_builder/tools",
    headers=headers,
    json=workflow_tool_payload,
)

print(f"Workflow tool: {response.status_code}")

# Create the structured indicators query tool
structured_query_payload = {
    "id": "query_structured_indicators",
    "type": "esql",
    "description": (
        "Query structured economic indicators using exact filters on numeric fields. "
        "Use this for questions like 'Which reports show frontier market GDP share below 5%?' "
        "or 'Show investment growth for reports published after 2025-01'."
    ),
    "tags": ["economic-data", "llama-extract"],
    "configuration": {
        "query": (
            "FROM economic-reports "
            "| WHERE frontier_market_gdp_share_pct <= ?max_gdp_share "
            "| KEEP report_title, publication_date, "
            "frontier_market_gdp_share_pct, frontier_market_investment_growth_2020s_pct "
            "| SORT publication_date DESC "
            "| LIMIT 10"
        ),
        "params": {
            "max_gdp_share": {
                "type": "double",
                "description": "Maximum frontier market GDP share percentage to filter by",
            }
        },
    },
}

response = requests.post(
    f"{KIBANA_URL}/api/agent_builder/tools",
    headers=headers,
    json=structured_query_payload,
)

print(f"Structured query tool: {response.status_code}")

# Create the narrative text search tool
text_search_payload = {
    "id": "search_economic_narratives",
    "type": "esql",
    "description": (
        "Search narrative content in economic reports. "
        "Use this for open-ended questions like 'What are the main risks for frontier markets?' "
        "or 'What does the report recommend for policymakers?'."
    ),
    "tags": ["economic-data", "llama-extract"],
    "configuration": {
        "query": (
            "FROM economic-reports "
            "| WHERE MATCH(executive_summary, ?query) "
            "OR MATCH(key_vulnerabilities, ?query) "
            "OR MATCH(policy_recommendations, ?query) "
            "| KEEP report_title, executive_summary, key_vulnerabilities, policy_recommendations "
            "| LIMIT 5"
        ),
        "params": {
            "query": {
                "type": "keyword",
                "description": "The search query to find relevant narrative content",
            }
        },
    },
}

response = requests.post(
    f"{KIBANA_URL}/api/agent_builder/tools",
    headers=headers,
    json=text_search_payload,
)

print(f"Text search tool: {response.status_code}")

# Create the agent
agent_payload = {
    "id": "economic-report-analyst",
    "name": "Economic Report Analyst",
    "description": "Extracts and analyzes economic reports from PDFs using LlamaParse Extract and Elasticsearch.",
    "labels": ["economics", "llama-extract"],
    "configuration": {
        "instructions": (
            "You are an economic research assistant. You have three tools:\n"
            "1. run_llamaextract_workflow: Use this FIRST to extract and index data from a PDF.\n"
            "2. query_structured_indicators: Use this for structured questions that filter on "
            "numeric fields like GDP share or investment growth.\n"
            "3. search_economic_narratives: Use this for open-ended questions about risks, "
            "vulnerabilities, or policy recommendations.\n"
            "When presenting data, use clear formatting with bullet points or tables. "
            "Always cite the report title and publication date."
        ),
        "tools": [
            {
                "tool_ids": [
                    "run_llamaextract_workflow",
                    "query_structured_indicators",
                    "search_economic_narratives",
                ]
            }
        ],
    },
}

response = requests.post(
    f"{KIBANA_URL}/api/agent_builder/agents",
    headers=headers,
    json=agent_payload,
)

print(f"Agent: {response.status_code}")

workflow ID 可以在 URL 中找到:

复制代码
https://4622216ea8cd443ead5bef0a3de05135.us-central1.gcp.cloud.es.io/app/workflows/<WORKFLOW-ID>

通过 Agent Builder 测试 workflow

配置好 "经济报告分析员" agent 后,打开 Agent Builder 聊天窗口,并发送如下消息(请替换为你自己的实际 ID):

复制代码
Process this PDF and extract its data: <your-pdf-url> 

Once extracted, answer: What are the main vulnerabilities facing frontier markets and what policy recommendations does the report suggest to address them?

Agent 会首先调用 run_llamaextract_workflow,等待 workflow 完成,然后再使用 search_economic_reports 来检索并总结提取后的数据。

复制代码
Process this PDF and extract its data: https://diaphysial-rafael-unscrupulously.ngrok-free.dev/Markets-Executive-Summary.pdf

Once extracted, answer: What are the main vulnerabilities facing frontier markets and what policy recommendations does the report suggest to address them?

提示:在实际的使用中,https://raw.githubusercontent.com/Delacrobix/notebook-llamaindex-agent-builder/main/Markets-Executive-Summary.pdf 这个文件的路径可能导致失败。一种做法就是使用如下的命令在本地构建一个文件服务器:

  • python3 -m http.server 8000

  • ngrok http 8000

你可以参考 ngrok: deliver your apps, APIs, and AI on local and prod 来进行设置。我们可以得到一个公共的访问地址,比如:https://diaphysial-rafael-unscrupulously.ngrok-free.dev -> http://localhost:8000

结果:

复制代码
What is the frontier market GDP share in 2025?

结论:从原始 PDF 到 agent 可用数据

LlamaParse Extract 解决了文档理解问题,它可以从包含图表和表格的 PDF 中提取结构化、基于 schema 的数据。Elastic Agent Builder 解决了编排问题,将提取、索引以及查询串联成 workflow,并提供 agent 可按需调用的 ES|QL 工具。两者结合在一起,就弥合了企业原始文档与 agent 可用数据之间的鸿沟。

这一模式不仅适用于经济报告。任何具有明确结构的企业文档(合同、技术规范、财务报表)都可以通过 Pydantic schema 建模,并用同样的 pipeline 进行处理。

下一步

原文:PDF data extraction with LlamaParse and Elastic Agent Builder - Elasticsearch Labs