Skip to content
LlamaParse 文档版本 跟随上游 源快照 a1d6b0c0 更新于 2026-10-16

需要从上几百份简历里抽结构化信息时,处理简历册会很耗时。这份 notebook 演示如何搭一个智能体,用 LlamaAgent Workflows、LlamaSplit 与 LlamaExtract 自动处理简历册。这个智能体会:

  1. 上传 PDF 文档到 LlamaCloud
  2. 切分 文档为若干逻辑段(简历 vs. 课程目录/索引页)
  3. 从每份简历抽取结构化数据
  4. 用 LlamaIndex workflow 编排整个流程

获取简历册

本例用的是 NYU 数学-金融全日制简历册。你可以从这里下载:

📥 下载简历册

继续之前,请先把该文件在本地存为 resume_book.pdf:

终端窗口

curl -fL -o resume_book.pdf https://math-finance.cims.nyu.edu/wp-content/uploads/2024/09/Updated-Sept-Full-Time-Book-9.11.24.pdf

概述

这套流程用到两个关键的 LlamaCloud 服务:

  • LlamaSplit:把文档页面按类型归类(简历、课程页、封面页等)
  • LlamaExtract:用 AI 从文档里抽取结构化数据

先安装所需的依赖。

pip install "llama-cloud>=2.8" requests llama-index-workflows
import os
from getpass import getpass

if "OPENAI_API_KEY" not in os.environ:
    os.environ["OPENAI_API_KEY"] = getpass("OPENAI_API_KEY")
if "LLAMA_CLOUD_API_KEY" not in os.environ:
    os.environ["LLAMA_CLOUD_API_KEY"] = getpass("LLAMA_CLOUD_API_KEY")

第 1 步:把文件上传到 LlamaCloud

在处理文档之前,得先把它上传到 LlamaCloud。这会给我们一个 file_id,供其他 LlamaCloud API 使用。

LlamaCloud 客户端提供了便捷的 upload_file() 方法,它负责上传并返回含文件 ID 的元数据。

from llama_cloud import LlamaCloud, AsyncLlamaCloud

client = AsyncLlamaCloud(api_key=os.getenv("LLAMA_CLOUD_API_KEY"))

# Update this path to where you saved the resume book
pdf_path = "resume_book.pdf"  # or "/content/resume_book.pdf" in Colab

uploaded_file = await client.files.create(file=pdf_path, purpose="extract")

file_id = uploaded_file.id
print(f"✅ File uploaded: {file_id}")

第 2 步:把文档按类别切分

现在用 LlamaCloud 的 Split API 自动对页面分类。当一份文档混着多种内容时,这很有用。

我们定义这些类别:

  • resume:各候选人的单页简历
  • curriculum:列出项目课程的总体学生课程页
  • cover_page:封面页或标题页(视文档结构而定,可选)

Split API 会用 AI 分析每一页并归入合适的类别。这会创建一个异步运行的任务,因此我们需要轮询结果。

# Split the document and wait for completion
# Each category needs a name and a description that helps the AI
# understand what content belongs to that category
response = await client.beta.split.split(
    categories=[
        {
            "name": "resume",
            "description": "A resume page from an individual candidate containing their professional information, education, and experience",
        },
        {
            "name": "curriculum",
            "description": "The overall student curriculum page listing the program curriculum",
        },
        {
            "name": "cover_page",
            "description": "Cover page, title page, or introductory page of the resume book",
        },
    ],
    document_input={"type": "file_id", "value": file_id},
)

# Print the splitting results
print(f"Split job completed with status: {response.status}")
print()

if response.result:
    print(f"📊 Total segments found: {len(response.result.segments)}")
    for i, segment in enumerate(response.result.segments, 1):
        pages = segment.pages
        if len(pages) == 1:
            page_range = f"Page {pages[0]}"
        else:
            page_range = f"Pages {min(pages)}-{max(pages)}"

        print(f"\nSegment {i}:")
        print(f"   Category: {segment.category}")
        print(f"   {page_range} ({len(pages)} pages)")
        print(f"   Confidence: {segment.confidence_category}")

第 3 步:定义抽取 schema 并抽取数据

LlamaExtract 是一个从文档抽取结构化数据的服务。我们用它从每位候选人的简历里抽出信息。

抽取器会用一份 Pydantic schema 来定义我们希望抽取的数据结构。

我们定义了一份 Pydantic schema(ResumeSchema),描述要从每份简历里抽出的数据结构:

  • 候选人姓名
  • 联系方式(邮箱、电话)
  • 教育经历(学位、学校、时间)
  • 工作经历(公司、职位、时间、描述)
  • 技能(技术技能、编程语言等)
  • 其他信息(证书、语言等)

抽取配置里指定了:

  • tier:用 agentic 以获得最高质量的抽取
  • target_pages:只从特定页抽取(例如 “5” 表示抽第 5 页那份简历)
  • confidence_scores:在结果里带上置信度得分

随后我们创建一个抽取任务并轮询直到完成。

from pydantic import BaseModel, Field
from typing import List, Optional

class Education(BaseModel):
    degree: str = Field(description="Degree type (e.g., B.S., M.S., Ph.D.)")
    institution: str = Field(description="Name of the educational institution")
    field_of_study: Optional[str] = Field(None, description="Field of study or major")
    graduation_date: Optional[str] = Field(None, description="Graduation date or year")
    gpa: Optional[str] = Field(None, description="GPA if mentioned")

class WorkExperience(BaseModel):
    company: str = Field(description="Company or organization name")
    position: str = Field(description="Job title or position")
    start_date: Optional[str] = Field(None, description="Start date")
    end_date: Optional[str] = Field(None, description="End date (or 'Present' if current)")
    description: Optional[str] = Field(None, description="Job description or key responsibilities")

class ResumeSchema(BaseModel):
    name: str = Field(description="Full name of the candidate")
    email: Optional[str] = Field(None, description="Email address")
    phone: Optional[str] = Field(None, description="Phone number")
    location: Optional[str] = Field(None, description="Location or address")
    education: List[Education] = Field(description="List of educational qualifications")
    work_experience: List[WorkExperience] = Field(description="List of work experiences")
    skills: List[str] = Field(description="List of skills, programming languages, or technical competencies")
    certifications: Optional[List[str]] = Field(None, description="Certifications or licenses")
    languages: Optional[List[str]] = Field(None, description="Languages spoken")
    summary: Optional[str] = Field(None, description="Professional summary or objective")

import asyncio

job = await client.extract.create(
    file_input=file_id,
    configuration={
        "data_schema": ResumeSchema.model_json_schema(),
        "tier": "agentic",
        "confidence_scores": True,
        "target_pages": "5",
    },
)

while job.status not in ("COMPLETED", "FAILED", "CANCELLED"):
    await asyncio.sleep(2)
    job = await client.extract.get(job.id)

extracted_result = ResumeSchema.model_validate(job.extract_result)

查看抽取出的数据

来看看从文档里抽出了哪些数据。结果是一个与我们的 ResumeSchema 对应的字典。

extracted_result.model_dump()
{'name': 'Quanquan (Lydia) Chen',
 'email': 'q.chen@nyu.edu',
 'phone': '(201) 626-0959',
 'location': 'New York, NY',
 'education': [{'degree': 'M.S.',
   'institution': 'New York University',
   'field_of_study': 'Mathematics in Finance',
   'graduation_date': '12/24',
   'gpa': None},
  {'degree': 'B.S.',
   'institution': 'Zhejiang University',
   'field_of_study': 'Mathematics and Applied Mathematics',
   'graduation_date': '06/23',
   'gpa': None}],
 'work_experience': [{'company': 'Numerix',
   'position': 'Financial Engineering Intern',
   'start_date': '07/24',
   'end_date': 'Present',
   'description': 'Developed models (e.g., Black-Scholes, Heston, Bates), applied market data and wrote payoff scripts to price exotic instruments (e.g., barrier options, variance swaps, cliquets, corridors). Conducted calibrations for equity and FX models with pricing and Greeks, considered different cases (e.g., time-dependent yield, projection rate, day-count conventions) to ensure accuracy. Researched and applied pricing algorithms (e.g., backward Monte Carlo for American options) in literature review from academic papers on financial products pricing.'},
  {'company': 'Shenwan Hongyuan Securities Research Co., Ltd.',
   'position': 'Financial Engineering Intern',
   'start_date': '06/22',
   'end_date': '11/22',
   'description': 'Extracted fund data, manipulated and validated data through detecting outliers, dropping duplicates values, completed missing values with imputers, and reduce data dimensions. Applied PCA on portfolio, based on principal components and risk budgeting to build a new one, backtested it and obtained annualized return 7.16% and winning percentage nearly 85%. Anatomized low-cost fund data, summarized competitive advantages and background as well as business strategies of investment companies; researched other products, produced client reports.'}],
 'skills': ['Python (Pandas, Numpy, Scipy, Matplotlib, Sklearn)',
  'LaTeX',
  'Excel'],
 'certifications': None,
 'languages': ['English (fluent)', 'Mandarin (native)'],
 'summary': None}

第 4 步:搭一个 workflow 把一切自动化

现在我们把整个流程编排成一个 LlamaIndex Workflow

  1. split_document 步骤:

    • 上传文件
    • 创建切分任务
    • 轮询直到完成
    • 为每个段发出一个 ExtractResume 事件
  2. extract_resume 步骤:

    • 等所有段收集完毕(fan-in 汇聚模式)
    • 从每个 “resume” 段抽取数据
    • 返回全部已抽取的简历

workflow 的几个关键概念:

  • 事件:用自定义事件类型(ExtractResume)在步骤之间传递数据
  • fan-out / fan-in:split_document 步骤发出多个事件(每段一个),extract_resume 把它们全部收集后才继续
  • 上下文存储:用来记录我们预计要收集多少个段
  • 并行处理:多个抽取事件可以并发处理
import asyncio
from workflows import Workflow, step, Context
from workflows.events import StartEvent, StopEvent, Event

class ExtractResume(Event):
    file_path: str
    category: str
    pages: list[int]

class ResumeBookAgent(Workflow):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.client = AsyncLlamaCloud(api_key=os.getenv("LLAMA_CLOUD_API_KEY"))

        class ResumeSchema(BaseModel):
            name: str = Field(description="Full name of the candidate")
            email: Optional[str] = Field(None, description="Email address")
            phone: Optional[str] = Field(None, description="Phone number")
            location: Optional[str] = Field(None, description="Location or address")
            education: List[Education] = Field(description="List of educational qualifications")
            work_experience: List[WorkExperience] = Field(description="List of work experiences")
            skills: List[str] = Field(description="List of skills, programming languages, or technical competencies")
            certifications: Optional[List[str]] = Field(None, description="Certifications or licenses")
            languages: Optional[List[str]] = Field(None, description="Languages spoken")
            summary: Optional[str] = Field(None, description="Professional summary or objective")

        self.extract_schema = ResumeSchema
        self.categories = [
            {
                "name": "resume",
                "description": "A resume page from an individual candidate containing their professional information, education, and experience",
            },
            {
                "name": "curriculum",
                "description": "The overall student curriculum page listing the program curriculum",
            },
            {
                "name": "cover_page",
                "description": "Cover page, title page, or introductory page of the resume book",
            },
        ]

    @step
    async def split_document(self, ev: StartEvent, ctx: Context) -> ExtractResume:
        uploaded_file = await self.client.files.create(file=ev.file_path, purpose="extract")

        file_id = uploaded_file.id
        await ctx.store.set("file_id", file_id)
        print(f"✅ File uploaded: {uploaded_file.name}", flush=True)

        response = await self.client.beta.split.split(
            categories=self.categories,
            document_input={"type": "file_id", "value": file_id},
        )

        segments = response.result.segments if response.result else []
        await ctx.store.set("segments_count", len(segments))
        for segment in segments:
            ctx.send_event(ExtractResume(file_path=ev.file_path, category=segment.category, pages=segment.pages))

    @step
    async def extract_resume(self, ev: ExtractResume, ctx: Context) -> StopEvent:
        ready = ctx.collect_events(
            ev, [ExtractResume] * await ctx.store.get("segments_count")
        )
        if ready is None:
            return None
        extraction_result = []
        for event in ready:
            if event.category == "resume":
                file_id = await ctx.store.get("file_id")
                job = await self.client.extract.create(
                    file_input=file_id,
                    configuration={
                        "data_schema": self.extract_schema.model_json_schema(),
                        "tier": "agentic",
                        "target_pages": f"{min(event.pages)}-{max(event.pages)}",
                    },
                )
                while job.status not in ("COMPLETED", "FAILED", "CANCELLED"):
                    await asyncio.sleep(2)
                    job = await self.client.extract.get(job.id)
                extraction_result.append(job.extract_result)
        return StopEvent(result=extraction_result)

端到端跑一遍智能体,输出大致如下:

agent = ResumeBookAgent(timeout=1000)

resp = await agent.run(start_event=StartEvent(file_path="resume_book.pdf"))

✅ 文件已上传:resume_book.pdf 状态:pending(已用 0s) 状态:processing(已用 5s) 状态:processing(已用 10s) 状态:completed(已用 15s)

for resume in resp[1:3]:
    print(f"\n{'='*60}")
    print(f"Name: {resume.get('name', 'N/A')}")
    print(f"Education: {resume.get('education', 'N/A')}")
    print(f"Skills: {', '.join(resume.get('skills', []))}")
    print(f"{'='*60}")
============================================================
Name: Shengjun (James) Guan
Education: [{'degree': 'M.S.', 'institution': 'New York University', 'field_of_study': 'Mathematics in Finance', 'graduation_date': '12/24', 'gpa': None}, {'degree': 'B.S.', 'institution': 'Rose-Hulman Institute of Technology', 'field_of_study': 'Mathematics and Data Science', 'graduation_date': '05/23', 'gpa': None}]
Skills: Python, Java, R, MongoDB, NoSQL, MATLAB, Maple
============================================================

============================================================
Name: Shupeng (Wayne) Guan
Education: [{'degree': 'M.S.', 'institution': 'New York University', 'field_of_study': 'Mathematics in Finance', 'graduation_date': '01/25', 'gpa': None}, {'degree': 'B.S.', 'institution': 'University of Birmingham', 'field_of_study': 'Mathematics With Honours (First Class)', 'graduation_date': '07/23', 'gpa': None}, {'degree': 'B.S.', 'institution': 'Huazhong University of Science and Technology', 'field_of_study': 'Finance', 'graduation_date': '06/21', 'gpa': '3.8/4'}]
Skills: Python, R, MATLAB, SQL, LaTex

下一步

有了结构化的简历数据之后,你可以:

  • 按技能、学历或经验筛选候选人
  • 搜索特定的任职资格
  • 按岗位需求搭建候选人匹配系统
  • 生成报告,呈现候选人画像与资历

BytesRouter 是社区翻译站点,所有译文仅供参考,一切以官方英文原文为准。