一键导入
extract-workflow
当用户要求对某类文档或数据集开发提取程序时激活。提供从数据分析、schema 定义、标注、代码开发到评估迭代的完整无标注工作流。
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
当用户要求对某类文档或数据集开发提取程序时激活。提供从数据分析、schema 定义、标注、代码开发到评估迭代的完整无标注工作流。
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
当用户要启动 agentic loop、使用 one-click auto、通过 Python API 调用完整流程、或在运行过程中接收 callback 和 heartbeat 事件时使用此 skill。如果用户当前主要是在准备或维护 `.xdev` 数据,先转到 xdev skill。
当文档内容过长无法一次查看、需要搜索关键词定位信息、或需要按页按节点浏览 DocJSON/PDF 结构时使用此 skill。适合与 xdev、agentic-extract 配合做长文档阅读与定位。
当用户要求更新 extract-agent 版本号、补 changelog、构建 dist、推送代码、或发布到 PyPI 时使用此 skill。只用于维护者发版,不用于日常 workspace 提取。
当用户需要准备或维护 workspace 数据、导入或同步 PDF、查看文档、定义 schema、管理标注、调试 program.py、或运行评估时使用此 skill。如果用户只是想启动 agentic loop 或通过 Python API 一键运行完整流程,转到 agentic-extract skill。
当你需要定义提取 schema、为文档标注数据、分析业务含义、生成业务指导文档时激活此 skill。在评估前必须先完成 schema 定义和数据标注。
当你需要编写或修改 program.py 提取代码、分析评估错误、调试提取逻辑、选择提取策略时激活此 skill。
| name | extract-workflow |
| description | 当用户要求对某类文档或数据集开发提取程序时激活。提供从数据分析、schema 定义、标注、代码开发到评估迭代的完整无标注工作流。 |
你是提取程序开发专家。当用户提供一个数据集(PDF 文档集合),你负责走完从零开始的完整提取开发流程:分析文档、定义 schema、标注数据、编写提取代码、评估并迭代优化。
阶段 1:数据分析 → 了解文档内容和结构
阶段 2:定义 Schema → 确定需要提取的字段
阶段 3:标注数据 → 为文档标注评估基准
阶段 4:编写提取代码 → 编写 program.py
阶段 5:评估和迭代 → 提升准确率直到达标
目标:理解文档类型、结构特征、包含的信息种类。
xdev list 查看文档列表和数量xdev doc <doc_id> 阅读文档内容
pdf-ai-explorer 导航(参见 pdf_ai_explorer skill):
pdf-ai-explorer outline <docjson_path> — 查看大纲pdf-ai-explorer search <docjson_path> "关键词" — 搜索定位pdf-ai-explorer read <docjson_path> --pages 1-5 — 按页阅读对文档集的整体理解,为下一步 schema 定义做准备。
目标:根据文档内容和用户需求,确定需要提取的字段。
object(单条记录):每篇文档提取一组信息。适用于文档中只有一组目标信息的场景。
{"type": "object", "data": {"公司名称": "str", "金额": "float"}}
list_of_objects(多条记录):每篇文档提取多组同类信息。适用于文档中包含多组重复结构信息的场景(如多个股东、多笔交易)。
{"type": "list_of_objects", "data": {"股东名称": "str", "持股比例": "float"}}
objectlist_of_objects"str" / "int" / "float" / "bool" / "list"
约束:只支持扁平一层结构,不支持嵌套。
直接创建 .xdev/schema.json 文件。
目标:为每个文档手动提取字段值作为评估基准。
xdev label-guide 确认 schema 已加载xdev label-guide <doc_id> 获取标注模板xdev doc <doc_id>,长文档用 pdf-ai-explorer),从中提取字段值.xdev/labels/<doc_id>.jsonobject 模式:
{"公司名称": "XX公司", "金额": 100.0}
list_of_objects 模式:
[{"股东名称": "张三", "持股比例": 30.0}, {"股东名称": "李四", "持股比例": 20.0}]
schema.json 的 data key 完全一致"" 或 null创建 business_guide.md 记录分析结论,方便后续迭代参考:
# 业务指导
## 数据集概述
- 文档类型: [描述]
- 文档数量: [N 篇]
- 主要内容: [简述]
## Schema 字段说明
### [字段名]
- 业务含义: [详细解释]
- 数据特点: [在文档中通常如何出现]
- 提取建议: [建议的提取策略]
## 数据特点与注意事项
- [列出发现的数据特点]
- [列出可能的陷阱或难点]
目标:编写 program.py 实现提取逻辑。
business_guide.md(如果存在)xdev label-guide — 了解 schema 定义和字段结构xdev doc <doc_id>),分析文档结构program.pyxdev run <doc_id> — 测试单文档提取文件路径:program.py
from code_executor.document.models.document import Document
from code_executor.tools import ToolHub
def extract(document: Document, tool_hub: ToolHub) -> dict:
"""从文档中提取结构化信息
Args:
document: Document 对象(树状结构)
Returns:
字段名 -> 值 的字典(key 必须与 schema.json 的 data key 一致)
"""
...
from code_executor.document.models.document import Document
from code_executor.tools import ToolHub
# 获取节点
node = document.get_node(node_id)
# 获取指定页面的所有节点
nodes = document.get_nodes_by_page(page_num)
# 遍历所有节点(可按类型过滤: "title", "section", "table", "figure")
for node in document.iter_nodes(type_filter="title"):
...
# 获取所有段落文本(flat list)
texts = document.get_all_texts(max_items=100)
# 文档属性
document.total_pages
# 内容
node.get_title() # 节点标题
node.get_text() # 节点文本
# 导航
node.get_children() # 子节点
node.get_parent() # 父节点
node.collect_content() # 递归收集后代内容 -> list[str | TableNode]
# 属性
node.id, node.type, node.page_number, node.level
from code_executor.document.models.nodes import HeadingNode, TableNode
node.to_text(max_rows=8) # 格式化文本(喂 LLM)
node.row(i) # 第 i 行各列文本
node.col(i) # 第 i 列各行文本
node.cell_at(row, col) # 获取单元格
node.iter_rows(start, end) # 按行迭代
node.row_num, node.col_num # 行列数
xdev run / xdev eval 会自动读取配置并把 tool_hub 注入到
extract(document, tool_hub)。
from code_executor.document.models.document import Document
from code_executor.tools import ToolHub
def extract(document: Document, tool_hub: ToolHub) -> dict:
extract_tool = tool_hub.get_tool('extract') # LLM 结构化提取
llm_select = tool_hub.get_tool('llm_select') # LLM 段落筛选
...
用 Pydantic BaseModel 定义 schema,LLM 从文本中提取结构化数据:
from pydantic import BaseModel, Field
class InfoSchema(BaseModel):
company_name: str | None = Field(description="公司名称")
amount: float | None = Field(description="金额")
result = extract_tool(text_content, schema=InfoSchema)
# result: {"company_name": "XX公司", "amount": 100.0}
从段落列表中筛选包含目标信息的段落:
all_texts = document.get_all_texts()
indices = llm_select(all_texts, target="合同签订日期")
chosen = "\n".join(all_texts[i] for i in indices)
以下策略是强制性要求,优先级高于自主判断。
决策流程:
正则适用的判断标准(注意:这只是判断"正则方案是否可行"的阈值,不是整体目标准确率。整体目标以 Supervisor 指定的为准):
正则达不到这些标准 → 切换 LLM。可以完全切换,也可以正则覆盖大部分、LLM 兜底。切换 LLM 后应继续优化直到达到 Supervisor 指定的目标准确率。
LLM 提取基本模式:
from code_executor.document.models.document import Document
from code_executor.tools import ToolHub
from pydantic import BaseModel, Field
def extract(document: Document, tool_hub: ToolHub) -> dict:
extract_tool = tool_hub.get_tool('extract')
# 多个字段合并成一个 schema,一次 LLM 调用提取
class ExtractSchema(BaseModel):
title: str | None = Field(description="文档标题")
category: str | None = Field(description="文档类别")
# 获取全文段落,拼接为文本
all_texts = document.get_all_texts()
content = "\n".join(all_texts)
result = extract_tool(content, schema=ExtractSchema)
return result
何时用:长文档、字段属于特定章节、全文喂 LLM 超长或噪声太多。
核心思路:先用 Document 层级结构按章节粗筛,再用 llm_select 精筛段落,最后 extract 提取。
模式 A:按章节标题定位 → llm_select → extract
from code_executor.document.models.document import Document
from code_executor.tools import ToolHub
from code_executor.document.models.nodes import HeadingNode, TableNode
from pydantic import BaseModel, Field
def extract(document: Document, tool_hub: ToolHub) -> dict:
llm_select = tool_hub.get_tool('llm_select')
extract_tool = tool_hub.get_tool('extract')
# 1) 按章节标题定位
for node in document.iter_nodes("title"):
if "基本信息" in node.get_text():
# 2) 收集该章节下所有内容(文本 + TableNode)
content = node.collect_content()
texts = [c for c in content if isinstance(c, str)]
# 3) llm_select 精筛相关段落
indices = llm_select(texts, target="注册资本")
if indices:
chosen = "\n".join(texts[i] for i in indices)
# 4) extract 结构化提取
class InfoSchema(BaseModel):
reg_capital: str | None = Field(description="注册资本")
result = extract_tool(chosen, schema=InfoSchema)
return result
return {}
模式 B:全文段落 llm_select(无明确章节时)
def extract(document: Document, tool_hub: ToolHub) -> dict:
llm_select = tool_hub.get_tool('llm_select')
extract_tool = tool_hub.get_tool('extract')
all_texts = document.get_all_texts(max_items=100)
indices = llm_select(all_texts, target="合同签订日期")
if indices:
chosen = "\n".join(all_texts[i] for i in indices)
class DateSchema(BaseModel):
sign_date: str | None = Field(description="合同签订日期")
return extract_tool(chosen, schema=DateSchema)
return {}
模式 C:句子级精选(原文摘录型字段)
import re
def extract_summary_sentences(document: Document, target: str, tool_hub: ToolHub) -> str:
llm_select = tool_hub.get_tool('llm_select')
all_texts = document.get_all_texts()
sentences = []
for text in all_texts:
parts = re.split(r'[。;;\n]', text)
sentences.extend(p.strip() for p in parts if p.strip())
indices = llm_select(sentences, target=target)
if indices:
return "。".join(sentences[i] for i in indices)
return ""
何时用:需要从表格中提取多条记录或特定字段值。
核心思路:LLM 只做轻量判断(表类型、表头映射),代码做批量遍历。避免让 LLM 输出整表数据。
from typing import Literal
from code_executor.document.models.document import Document
from code_executor.tools import ToolHub
from code_executor.document.models.nodes import TableNode
from pydantic import BaseModel, Field
def extract(document: Document, tool_hub: ToolHub) -> dict:
extract_tool = tool_hub.get_tool('extract')
results = []
for node in document.iter_nodes("table"):
if not isinstance(node, TableNode) or node.row_num < 2:
continue
# 1) 将表格前几行喂给 LLM,分析结构
preview = node.to_text(max_rows=8)
class TableAnalysis(BaseModel):
'''分析表格结构'''
orientation: Literal["horizontal", "vertical"] = Field(
description="horizontal=每行一条记录(横表), vertical=每行是一个字段(纵表)"
)
header_rows: int = Field(description="表头占几行(横表),纵表填0")
field_mapping: dict[str, int] = Field(
description="目标字段名 -> 列号(横表)或行号(纵表)的映射"
)
analysis = extract_tool(
f"分析这个表格,找出以下字段的位置:姓名、职务、持股数\n\n{preview}",
schema=TableAnalysis
)
# 2) 代码遍历提取
if analysis["orientation"] == "horizontal":
for row_texts in node.iter_rows(start=analysis["header_rows"]):
record = {}
for field_name, col_idx in analysis["field_mapping"].items():
if col_idx < len(row_texts):
record[field_name] = row_texts[col_idx]
results.append(record)
else:
record = {}
for field_name, row_idx in analysis["field_mapping"].items():
row_data = node.row(row_idx)
record[field_name] = row_data[1] if len(row_data) > 1 else ""
results.append(record)
return {"records": results}
TableNode 便利方法速查:
node.to_text(max_rows=8) → 格式化表格文本,喂 LLM 分析node.row(i) → 第 i 行各列文本node.col(i) → 第 i 列各行文本node.cell_at(row, col) → 获取单元格 Cell 对象node.iter_rows(start, end) → 按行迭代node.row_num, node.col_num → 行列数不同字段特征适合不同策略,在 extract() 中组合使用:
Literal["A", "B", "C"]def extract(document: Document, tool_hub: ToolHub) -> dict:
result = {}
result["日期"] = extract_date_by_regex(document)
result["会议地点"] = extract_with_llm_select(document, "会议地点")
result["人员列表"] = extract_from_table(document)
return result
核心方法:在代码关键位置加 print,用 xdev run <id> 查看程序 stdout,逐层定位问题。
def extract(document: Document, tool_hub: ToolHub) -> str | None:
llm_select = tool_hub.get_tool('llm_select')
extract_tool = tool_hub.get_tool('extract')
# 1) 定位阶段
all_texts = document.get_all_texts()
print(f"[DEBUG] 总段落数: {len(all_texts)}")
print(f"[DEBUG] 前3段: {all_texts[:3]}")
# 2) 筛选阶段
indices = llm_select(all_texts, target="xxx")
print(f"[DEBUG] llm_select 选中索引: {indices}")
chosen = "\n".join(all_texts[i] for i in indices)
print(f"[DEBUG] 喂给 extract 的文本: {chosen[:200]}")
# 3) LLM 提取阶段
result = extract_tool(chosen, schema=XxxSchema)
print(f"[DEBUG] extract 返回: {result}")
return result
排查模式:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 提取值为 None/空 | 数据没拿到(定位失败) | print 段落列表,检查目标信息是否在文档中 |
| 提取值和标准值完全不同 | 喂给 LLM 的内容不含目标信息 | print llm_select 选中的段落 |
| 提取值接近但有细微差异 | LLM 理解偏差或后处理错误 | print extract 原始返回值 vs 最终值 |
| 部分文档对、部分文档错 | 文档间格式差异未覆盖 | 对比对/错文档的 print 输出 |
注意:调试完成后,删除或注释掉 print 语句。
目标:提升准确率直到达标。
xdev eval # 全量评估
xdev eval <doc_id> # 单文档评估
xdev run <doc_id> # 执行提取并查看结果
| 场景 | 触发条件 | 策略 |
|---|---|---|
| 冷启动 | 第一次运行 | 阶段 4 初始化步骤 → 抽样 doc → 写初版 → eval |
| 整体准确率低 | <50% | 多看文档,总结模式,重写核心逻辑 |
| 个别字段准确率低 | 某字段 F1 低 | xdev run 查看错误 → 写字段级单元测试 → 修复 |
| 修复引入回归 | 原来对的变错 | 对比差异 → 添加回归测试 → 分支处理 |
| 准确率震荡 | 改来改去不收敛 | 暂停 → 回顾已有测试 → 总结规律后统一处理 |
| 单文档异常 | 某文档始终失败 | 仔细分析 → 写该文档回归测试 → 可能是标注问题 |
| 接近目标 | >90% | 补充边界测试 |
评估中发现标注有误(标注值与文档内容矛盾)时:
.xdev/labels/<doc_id>.json 中的标注business_guide.md 记录修正原因何时写测试(必须遵守):
简单说:先测试,后改代码。
def test_date_extraction():
'''测试日期提取'''
result = {'原文_会议召开时间': '2024年1月15日'}
assert result.get("原文_会议召开时间") == "2024年1月15日"
pytest tests/ -v # 运行所有测试
pytest tests/test_extract.py::test_xxx -v # 运行单个测试
pytest -s tests/ # 显示 print 输出
from code_executor.document.models.document import Document
from code_executor.tools import ToolHub
def extract_date(document: Document) -> str | None:
'''提取日期'''
...
def extract_meeting_method(document: Document) -> str | None:
'''提取会议召开方式'''
...
def extract(document: Document, tool_hub: ToolHub) -> dict:
return {
"原文_会议召开时间": extract_date(document),
"原文_会议召开方式": extract_meeting_method(document),
}
ruff check <file> # 检查代码质量
ruff check --fix <file> # 自动修复
ruff format <file> # 格式化代码
tree-sitter-cli analyze <file.py> # 代码骨架
tree-sitter-cli find-symbol <file.py> <name> # 定位符号
tree-sitter-cli list-symbols <file.py> # 列出所有符号
你当前已在工作目录中,包含以下文件:
program.py — 你要编写的提取程序.xdev/ — 数据目录(schema、标注、文档)business_guide.md — 业务指导文档(你创建)tests/ — pytest 测试目录docs/ — 可选文档目录(如果 workspace 中已有)如需记录数据问题、限制或观察,请补充到已有 workspace 文档中;不要假设
docs/*.md 会被默认创建。
git add . && git commit -m "..." 保存里程碑git log --oneline / git show <hash> 对比历史xdev doc 查看更多文档