vlm.md
← 所有食谱 · 结构化输出 · 高级

嵌套结构数据提取

从包含层级结构的图片(组织架构图、嵌套表格、多层级表单、树形图)中提取完整的嵌套 JSON,保留层级关系。

2026/4/30 · vlm.md · 推荐模型: GPT-4oClaude 3.5 Sonnet

场景

图片中包含层级结构信息,agent 需要将其提取为保留层级关系的嵌套 JSON。典型场景:

  • 组织架构图:CEO → VP → Director → Manager 多级汇报关系
  • 嵌套表格:主表头下有子表头,行合并表示分组
  • 多层级表单:Section → SubSection → Field 三级结构
  • 树形图/思维导图:节点有任意深度的子节点

挑战:模型需要同时理解视觉位置关系(缩进、连线、合并单元格)和语义层级关系。

推荐模型

模型适用场景
GPT-4o最强的视觉-语义联合理解,处理复杂组织架构图首选
Claude 3.5 Sonnet对嵌套 JSON 格式输出更稳定,层级深时误差更小

复杂嵌套结构(>3 层)建议用 GPT-4o;输出格式稳定性要求高时用 Claude 3.5 Sonnet。

Prompt 模板

方案 A:直接输出嵌套 JSON(适合层级 <= 3 层)

请从图片中提取层级结构数据,以嵌套 JSON 格式返回。最大嵌套深度为 3 层。

每个节点的结构:
{
  "id": "唯一标识符(字母+数字)",
  "name": "节点名称",
  "attributes": {},  // 节点附带的其他属性(职位、部门等)
  "children": []     // 子节点列表,叶节点为空数组
}

如果层级超过 3 层,请将第 3 层以下的内容平铺合并到第 3 层节点的 attributes 中。
只输出 JSON,不要有任何多余文字。

方案 B:扁平化 + parent_id(适合任意深度,推荐用于组织架构)

请从图片中提取层级结构数据,以扁平 JSON 数组返回(每个节点一条记录,通过 parent_id 表示层级)。

每条记录的结构:
{
  "id": "节点唯一 ID(如 node_1, node_2)",
  "name": "节点名称",
  "parent_id": "父节点 ID,根节点为 null",
  "level": 层级深度(根节点为 0),
  "attributes": {}  // 职位、部门、人数等附加信息
}

返回格式:{"nodes": [...]}
只输出 JSON,不要有任何多余文字。

代码示例

import base64
import json
from pathlib import Path
from typing import Optional, Any
from openai import OpenAI
from pydantic import BaseModel

client = OpenAI()


# 方案 B:扁平化节点模型(推荐用于任意深度结构)
class FlatNode(BaseModel):
    id: str
    name: str
    parent_id: Optional[str] = None
    level: int
    attributes: dict[str, Any] = {}


class FlatTree(BaseModel):
    nodes: list[FlatNode]


def extract_nested_structure(
    image_path: str,
    strategy: str = "flat",  # "flat" 或 "nested"
    max_retries: int = 3,
) -> FlatTree | dict:
    """
    从图片中提取嵌套层级结构。

    strategy="flat"   : 扁平化 + parent_id,适合任意深度
    strategy="nested" : 直接嵌套 JSON,适合层级 <= 3
    """
    image_data = base64.b64encode(Path(image_path).read_bytes()).decode()
    suffix = Path(image_path).suffix.lower().lstrip(".")
    mime_type = {"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png"}.get(
        suffix, "image/jpeg"
    )

    if strategy == "flat":
        prompt = """请从图片中提取层级结构数据,以扁平 JSON 数组返回(每个节点一条记录,通过 parent_id 表示层级)。

每条记录的结构:
{
  "id": "节点唯一 ID(如 node_1, node_2)",
  "name": "节点名称",
  "parent_id": "父节点 ID,根节点为 null",
  "level": 层级深度(根节点为 0),
  "attributes": {}
}

返回格式:{"nodes": [...]}
只输出 JSON,不要有任何多余文字。"""
    else:
        prompt = """请从图片中提取层级结构数据,以嵌套 JSON 格式返回。最大嵌套深度为 3 层。

每个节点:{"id": "唯一ID", "name": "名称", "attributes": {}, "children": [子节点]}

如果层级超过 3 层,将深层内容合并到第 3 层节点的 attributes 中。
只输出 JSON,不要有任何多余文字。"""

    messages = [
        {"role": "system", "content": "你是层级结构数据提取专家,严格按 JSON 格式输出。"},
        {
            "role": "user",
            "content": [
                {"type": "image_url", "image_url": {"url": f"data:{mime_type};base64,{image_data}"}},
                {"type": "text", "text": prompt},
            ],
        },
    ]

    from pydantic import ValidationError

    last_error: Exception | None = None

    for attempt in range(max_retries):
        response = client.chat.completions.create(
            model="gpt-4o",
            response_format={"type": "json_object"},
            messages=messages,
            max_tokens=2048,
        )

        raw = response.choices[0].message.content

        try:
            data = json.loads(raw)

            if strategy == "flat":
                return FlatTree.model_validate(data)
            else:
                return data  # 嵌套结构直接返回 dict

        except (json.JSONDecodeError, ValidationError) as e:
            last_error = e
            messages.append({"role": "assistant", "content": raw})
            messages.append(
                {
                    "role": "user",
                    "content": (
                        f"输出校验失败(第 {attempt + 1} 次):{e}。"
                        "请修正后重新输出完整 JSON。注意 parent_id 必须指向已存在的节点 id,根节点的 parent_id 为 null。"
                    ),
                }
            )

    raise RuntimeError(f"经过 {max_retries} 次重试仍失败。最后错误:{last_error}")


def tree_to_nested(flat: FlatTree) -> dict:
    """将扁平化节点列表转换为嵌套树形结构(用于展示)。"""
    node_map = {n.id: {**n.model_dump(), "children": []} for n in flat.nodes}
    root = None

    for node in flat.nodes:
        if node.parent_id is None:
            root = node_map[node.id]
        else:
            parent = node_map.get(node.parent_id)
            if parent:
                parent["children"].append(node_map[node.id])

    return root or {}


def validate_tree_integrity(flat: FlatTree) -> list[str]:
    """校验扁平化树的完整性,返回错误列表。"""
    errors = []
    ids = {n.id for n in flat.nodes}

    for node in flat.nodes:
        if node.parent_id and node.parent_id not in ids:
            errors.append(f"节点 {node.id} 的 parent_id={node.parent_id} 不存在")

    roots = [n for n in flat.nodes if n.parent_id is None]
    if len(roots) == 0:
        errors.append("没有根节点(parent_id=null 的节点)")
    elif len(roots) > 1:
        errors.append(f"存在多个根节点:{[r.id for r in roots]}")

    return errors


if __name__ == "__main__":
    result = extract_nested_structure("org_chart.png", strategy="flat")

    # 校验树完整性
    errors = validate_tree_integrity(result)
    if errors:
        print("树结构校验警告:")
        for err in errors:
            print(f"  - {err}")

    # 转换为嵌套结构展示
    nested = tree_to_nested(result)
    print(json.dumps(nested, ensure_ascii=False, indent=2))

扁平化输出示例(组织架构图):

{
  "nodes": [
    {"id": "node_1", "name": "张总(CEO)", "parent_id": null, "level": 0, "attributes": {"title": "CEO"}},
    {"id": "node_2", "name": "李副总(CTO)", "parent_id": "node_1", "level": 1, "attributes": {"title": "CTO"}},
    {"id": "node_3", "name": "王总监(研发)", "parent_id": "node_2", "level": 2, "attributes": {"title": "研发总监", "headcount": 20}},
    {"id": "node_4", "name": "陈总监(测试)", "parent_id": "node_2", "level": 2, "attributes": {"title": "测试总监", "headcount": 8}},
    {"id": "node_5", "name": "赵总监(产品)", "parent_id": "node_1", "level": 1, "attributes": {"title": "产品总监"}}
  ]
}

踩坑记录

坑 1:层级过深时模型混淆父子关系

当嵌套超过 3 层时,模型经常把第 4、5 层节点的父节点搞错,出现”跳级”或”平级变父级”的错误。

解决方案:在 prompt 中明确限制最大深度为 3 层,超出部分平铺到 attributes;或使用扁平化方案(方案 B),用 parent_id 引用代替物理嵌套,模型出错率显著降低。

# 在 prompt 中加入深度限制
"如果层级超过 3 层,将第 3 层以下的内容合并到第 3 层节点的 attributes 中,不要继续嵌套。"

坑 2:组织架构图等递归结构——改用 parent_id 扁平化

组织架构图的层级是任意深度的,直接让模型输出嵌套 JSON 会导致:层级越深、错误越多。

更稳定的方案是让模型输出扁平 nodes 列表,每个节点只需记住自己的 parent_id,由代码在提取后重建树形结构。这样模型只需处理”局部关系”而不是”全局嵌套”。

坑 3:父子关系模糊时不要让模型猜测

图片中有时存在视觉歧义(节点间的连线不清晰、缩进不规则),模型会”猜”一个父节点。

解决方案:在 prompt 中加入明确指示:

如果某个节点的父节点不确定,将其 parent_id 设为 null,并在 attributes 中加入 "ambiguous_parent": true。
不要猜测不确定的层级关系。

然后在代码中对带 "ambiguous_parent": true 的节点单独处理或人工审核:

ambiguous = [n for n in result.nodes if n.attributes.get("ambiguous_parent")]
if ambiguous:
    print(f"需要人工确认的节点:{[n.name for n in ambiguous]}")