三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

大语言模型结构化输出实战:从Pydantic到Function Calling的数据提取指南

大语言模型结构化输出实战:从Pydantic到Function Calling的数据提取指南

1. 项目概述:从“文本对话”到“数据接口”的范式转变

如果你已经用了一段时间的大语言模型(LLM),比如 ChatGPT 或者 Claude,你肯定经历过这种抓狂时刻:你问它“帮我分析一下这份合同里的关键条款和截止日期”,它确实给你洋洋洒洒写了一大段,格式还挺漂亮。但当你兴冲冲地想把这些信息自动录入到你的合同管理系统或者日历里时,傻眼了——你得一个字一个字地从那段文本里把“甲方”、“付款金额”、“2024年12月31日”这些关键信息给抠出来。这个过程费时费力还容易出错,本质上,你是在用“人眼”和“人脑”去解析 AI 生成的“非结构化”文本,这完全违背了我们使用 AI 提升效率的初衷。

这正是“结构化输出与数据提取”要解决的核心痛点。这个项目标题听起来有点技术化,但它的目标非常直接:让 AI 不再仅仅是一个“聊天机器人”,而是变成一个能直接返回规整、标准数据的“数据接口”。想象一下,你问 AI “明天北京的天气如何?”,它不再回复“明天北京晴转多云,气温15-25度,南风3-4级”,而是直接返回一个 JSON 对象:{“city”: “北京”, “date”: “2024-10-27”, “weather”: “晴转多云”, “temp_low”: 15, “temp_high”: 25, “wind”: “南风3-4级”}。你的程序拿到这个 JSON,可以直接解析、存储、触发后续流程,整个过程全自动化。

这背后的技术,正是当前 AI 应用开发,特别是基于 LangChain、LlamaIndex 等框架构建 AI Agent 或自动化工作流时,必须掌握的核心技能。它关乎可靠性、可集成性和生产效率。本章,我们就来彻底拆解如何利用 Pydantic、函数调用(Function Calling)等工具,驯服 AI 这匹“野马”,让它乖乖地输出我们想要的、机器可读的数据格式。

2. 核心需求与价值:为什么“结构化”如此重要?

在深入技术细节之前,我们必须先搞清楚,为什么费这么大劲让 AI 输出结构化数据?这不仅仅是“看起来整齐”那么简单,它关乎整个 AI 应用落地的成败。

2.1 从“信息展示”到“流程集成”的跨越

传统的人机对话,AI 的输出终点是人的眼睛和大脑。信息以自然语言呈现,人类阅读理解后,再手动进行下一步操作。而结构化输出,意味着 AI 的输出终点是另一个程序、数据库或 API。它实现了从“信息展示层”到“业务逻辑层”的无缝衔接。

一个典型场景是智能客服工单自动创建

  • 非结构化(旧模式):用户描述问题:“我的账户无法登录,提示密码错误,账号是 example@email.com”。AI 客服回复:“已收到您关于账户 example@email.com 登录失败的问题,初步判断可能是密码错误,建议您尝试重置密码或联系管理员。” 客服人员需要阅读这条回复,手动在工单系统选择问题类型(登录问题)、填写用户邮箱、摘要描述,然后创建工单。
  • 结构化(新模式):同样是用户的描述,AI 在回复的同时,同步输出一个结构体:{“ticket_type”: “登录故障”, “user_email”: “example@email.com”, “priority”: “中”, “summary”: “用户反馈密码错误导致无法登录”}。这个结构体可以通过 webhook 直接调用工单系统的创建接口,工单瞬间自动生成,客服人员只需处理后续跟进。效率提升不是一点半点。

2.2 提升数据处理的准确性与一致性

自然语言充满歧义和变体。“五千块”、“5k”、“5000元”指的是同一个金额。“下周一下午”可能因上下文而异。当 AI 以自由文本回答时,后续程序需要写非常复杂的正则表达式或 NLP 规则来提取和归一化这些信息,鲁棒性极差。

结构化输出强制 AI 在生成时就必须进行“归一化”思考。你定义好字段amount的类型是float,那么无论用户说“五千”、“5千”还是“5000”,AI 在生成这个字段值时,都必须将其推理并转化为5000.0这个浮点数。你定义meeting_time的类型是datetime,AI 就必须把“下周一下午三点”解析成一个具体的 ISO 时间戳。这相当于把数据清洗和规范化的压力前移给了 AI,保证了输出数据“出生即标准”,极大降低了后续处理链路的复杂度。

2.3 赋能复杂、多步骤的智能体(Agent)工作流

在 LangChain 或 LangGraph 构建的 AI Agent 系统中,一个复杂的任务(比如“调研某公司并撰写投资报告”)会被拆解成多个子任务(搜索信息、分析财报、总结风险、生成报告)。这些子任务之间的信息传递,如果靠自然语言,会像一场“传话游戏”,信息在多次传递中极易失真、丢失关键数据。

结构化输出在这里扮演了“标准化数据总线”的角色。例如,负责“搜索信息”的 Agent 输出一个结构化的CompanyProfile对象,包含统一字段的营收、利润、人员规模。负责“分析财报”的 Agent 接收这个对象,进行分析后,再输出一个结构化的FinancialAnalysis对象。整个工作流的数据流转清晰、可靠,每个环节都可以对输入输出的数据结构进行严格校验(通过 Pydantic),确保了整个复杂系统运行的稳定性和可调试性。

3. 核心技术栈深度解析:Pydantic 与 Function Calling 如何协同

实现结构化输出,目前业界主流且最有效的方法,是结合Pydantic 模型定义与 LLM 的函数调用(Function Calling)能力。理解这两者如何协同工作,是掌握这项技术的关键。

3.1 Pydantic:不只是数据验证,更是“数据契约”

Pydantic 是一个 Python 库,它利用 Python 的类型注解(type hints)来进行数据验证和设置管理。在结构化输出的上下文中,它的核心价值在于为 AI 和我们自己,定义了一份清晰的“数据契约”。

这份契约明确了:

  1. 要输出哪些数据?通过定义模型的字段(Field)。
  2. 每个数据是什么类型?字符串(str)、整数(int)、浮点数(float)、布尔值(bool)、列表(list),甚至是嵌套的其他 Pydantic 模型。
  3. 每个数据有什么约束?字符串的最大/最小长度、数值的范围、是否可选、默认值是什么。
  4. 数据应该如何被解释?通过字段描述(description),用自然语言告诉 AI 这个字段的含义和填写要求。
from pydantic import BaseModel, Field from typing import List, Optional from datetime import date class Book(BaseModel): """书籍信息""" title: str = Field(description="书籍的名称") author: str = Field(description="书籍的作者,格式为‘姓氏,名字’") publication_year: int = Field(ge=1800, le=date.today().year, description="书籍的出版年份") genres: List[str] = Field(min_items=1, description="书籍所属的体裁列表,如['科幻', '小说']") isbn: Optional[str] = Field(None, pattern=r‘^\d{13}$‘, description="书籍的13位ISBN号,可能没有") # 这个 Book 类就是一份“契约”:AI,请按照这个格式给我返回一本书的信息。

实操心得:字段描述(description)至关重要!不要写得太简略。把它当作你在给一个实习生布置任务,要清晰、无歧义。好的描述能极大提高 AI 输出字段的准确率。例如,author字段的“格式为‘姓氏,名字’”就比单纯的“作者”要好得多。

3.2 Function Calling:让 LLM 学会“填空”

有了“契约”(Pydantic 模型),我们怎么让 LLM 遵守它呢?这就需要用到 LLM 的函数调用(Function Calling)工具调用(Tool Calling)能力。

以 OpenAI 的 GPT 系列为例。传统的聊天补全(Chat Completion)API,你发送消息列表,它返回文本消息。而当你启用函数调用功能时,流程发生了变化:

  1. 定义工具(函数):你在 API 请求中,除了消息,还会附带一个tools参数,里面描述了一个或多个“工具”。每个工具都有名字、描述,以及最重要的——parameters。这个parameters就是一个符合 JSON Schema 格式的对象描述,它和我们的 Pydantic 模型 schema 在本质上是一回事。
  2. LLM 的决策:LLM 分析用户的请求和上下文。如果它认为需要调用某个工具来完成用户的请求,它就不会生成普通的文本回复,而是生成一个特殊的“工具调用”响应。
  3. 结构化响应:这个响应里包含了它决定调用的工具名称,以及一个arguments对象——这就是一个已经填充了具体值的、符合我们定义的parameters结构的 JSON!

关键点:LLM 本身并不真正执行函数。它只是根据你的描述,“理解”了这个函数是干什么的、需要什么参数,然后在认为合适的时候,生成一个符合参数格式的 JSON 数据。这个 JSON,就是我们的“结构化输出”。

3.3 LangChain 的集成:简化流程的利器

手动构造 OpenAI 的函数调用请求、处理响应比较繁琐。LangChain 提供了更高层的抽象,让这个过程变得异常简单。其核心组件是create_structured_output_runnablewith_structured_output方法。

它的工作原理是:

  1. 内部转换:LangChain 将你的 Pydantic 模型自动转换为 LLM 能理解的 JSON Schema(即函数调用的parameters)。
  2. 封装请求:它帮你构造好包含工具定义的 API 请求。
  3. 解析响应:它接收 LLM 返回的argumentsJSON,并利用 Pydantic 对其进行验证和解析,最终返回一个你的模型类的实例对象。
  4. 异常处理:如果 LLM 返回的 JSON 不符合模型定义,或者验证失败,LangChain 可以提供重试机制(通过RetryOutputParser等),自动将错误信息反馈给 LLM,让它重新生成。
from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import PydanticOutputParser llm = ChatOpenAI(model=“gpt-4o”, temperature=0) prompt = ChatPromptTemplate.from_template(“请从以下文本中提取书籍信息:{input_text}”) # 方法1:使用 with_structured_output (LangChain 较新版本推荐) structured_llm = llm.with_structured_output(Book) chain = prompt | structured_llm result: Book = chain.invoke({“input_text”: “我最近读了《三体》,刘慈欣写的,2008年出版的,科幻小说,ISBN是9787536692930。”}) print(result.title) # 输出:三体 print(result.dict()) # 输出结构化字典 # 方法2:使用 PydanticOutputParser (传统方法) parser = PydanticOutputParser(pydantic_object=Book) prompt_with_format = ChatPromptTemplate.from_messages([ (“system”, “你是一个信息提取助手。请严格按以下格式输出:{format_instructions}”), (“human”, “{input_text}”) ]) chain = prompt_with_format | llm | parser result = chain.invoke({ “format_instructions”: parser.get_format_instructions(), # 这里会自动生成模型格式说明 “input_text”: “...” })

注意事项temperature参数建议设置为 0 或接近 0。结构化输出追求的是确定性和准确性,而不是创造性。较高的温度值会增加输出的随机性,可能导致字段格式错误或产生幻觉(Hallucination),生成不存在的字段值。

4. 实战演练:构建一个多层级信息提取管道

理论说得再多,不如亲手实现一个。我们构建一个稍微复杂点的场景:从一个混合了公司介绍、产品信息和招聘需求的自由文本中,提取出结构化的数据。

4.1 定义复杂的数据模型

我们要提取三类信息:公司概况、产品列表、招聘岗位。它们之间存在关联,适合用嵌套模型来定义。

from pydantic import BaseModel, Field, validator from typing import List, Optional from enum import Enum class JobType(str, Enum): FULL_TIME = “全职” PART_TIME = “兼职” INTERN = “实习” class Product(BaseModel): name: str = Field(description=“产品名称”) category: str = Field(description=“产品类别,如‘SaaS软件’、‘硬件设备’、‘咨询服务’”) description: Optional[str] = Field(None, description=“产品的简要描述”) class JobPosting(BaseModel): role: str = Field(description=“招聘职位名称,如‘后端开发工程师’”) type: JobType = Field(description=“职位类型”) department: Optional[str] = Field(None, description=“所属部门”) # 使用 validator 进行复杂校验 @validator(‘department’) def department_must_contain_keyword(cls, v): if v and ‘技术’ not in v and ‘研发’ not in v and ‘产品’ not in v: raise ValueError(‘部门名称应包含“技术”、“研发”或“产品”等关键词’) return v class CompanyExtraction(BaseModel): """从文本中提取的公司综合信息""" company_name: str = Field(description=“公司的全称”) core_business: str = Field(description=“公司的核心业务描述,一句话概括”) founding_year: Optional[int] = Field(None, ge=1900, description=“公司成立年份”) products: List[Product] = Field(default_factory=list, description=“公司的主要产品列表”) active_jobs: List[JobPosting] = Field(default_factory=list, description=“公司正在招聘的职位列表”) # 计算字段示例(不依赖LLM输出,由Pydantic后处理) @property def product_count(self) -> int: return len(self.products) @property def is_hiring(self) -> bool: return len(self.active_jobs) > 0

这个模型体现了几个高级技巧:

  1. 枚举类型(Enum)JobType限制了职位类型只能是我们定义的几种,LLM 必须从中选择,保证了数据的一致性。
  2. 嵌套模型CompanyExtraction包含了ProductJobPosting的列表,可以表达复杂的一对多关系。
  3. 验证器(validator):在JobPosting中,我们对department字段添加了自定义校验逻辑,确保部门名称符合一定的业务规则。注意:这个校验发生在 LLM 输出之后、Pydantic 解析之时。如果校验失败,会抛出ValidationError。我们可以捕获这个错误,并将其作为反馈让 LLM 重试。
  4. 计算属性(property)product_countis_hiring不是需要 LLM 填充的字段,而是基于已有数据计算得出的,展示了 Pydantic 模型作为数据容器的强大处理能力。

4.2 设计提示词(Prompt)与构建执行链

好的模型需要好的引导。我们需要设计一个清晰的系统提示词,告诉 LLM 它的角色和任务。

from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser, PydanticOutputParser import json llm = ChatOpenAI(model=“gpt-4”, temperature=0) # 方法:使用 with_structured_output structured_llm = llm.with_structured_output(CompanyExtraction) # 构建提示词模板 system_prompt = “““你是一个专业的商业信息提取助手。你的任务是从用户提供的文本中,精确地提取出关于公司的结构化信息。 请特别注意: 1. 对于‘成立年份’,如果文本中没有明确提及,请留空(null),不要猜测。 2. ‘产品’和‘招聘职位’可能有多项,请全部找出。 3. 所有提取的信息必须严格基于文本内容,不要添加任何文本中不存在的信息。 4. 如果文本中信息模糊或不完整,请在对应字段中留空或使用最合理的推断,但必须在最终输出的‘note’字段中说明(我们稍后为模型添加这个字段)。 ””” prompt_template = ChatPromptTemplate.from_messages([ (“system”, system_prompt), (“human”, “请分析以下文本并提取信息:\n{text}”) ]) # 构建链 extraction_chain = prompt_template | structured_llm # 测试文本 input_text = “““ 创新科技有限公司(InnovTech)成立于2015年,专注于企业级人工智能解决方案。我们目前主打两款产品:1. ‘智析’SaaS平台,提供数据智能分析服务;2. ‘守卫者’硬件安防系统。公司正处于快速发展期,现诚聘:后端开发工程师(全职,技术部)、机器学习实习生(实习,AI实验室)。 ””” try: result: CompanyExtraction = extraction_chain.invoke({“text”: input_text}) print(“提取成功!”) print(f“公司名称: {result.company_name}”) print(f“核心业务: {result.core_business}”) print(f“成立年份: {result.founding_year}”) print(f“产品数量: {result.product_count}”) for product in result.products: print(f“ - 产品: {product.name}, 类别: {product.category}”) print(f“是否在招聘: {result.is_hiring}”) for job in result.active_jobs: print(f“ - 职位: {job.role}, 类型: {job.type}, 部门: {job.department}”) # 转换为标准JSON,便于存储或传输 json_output = result.json(indent=2) print(“\nJSON输出:”, json_output) except Exception as e: print(f“提取过程中发生错误: {e}”) # 在这里可以加入重试逻辑

4.3 处理模糊、缺失与冲突信息

现实世界的文本很少是完美的。LLM 可能会遇到信息模糊(“几年前成立”)、缺失(没提产品)或冲突(文本前后矛盾)的情况。我们的结构化提取流程必须具备鲁棒性。

策略一:字段可选性与默认值如上例所示,将可能缺失的字段定义为Optional[...]并设置default=None。在 Pydantic 模型中,可以添加一个note字段来记录任何不确定性。

class RobustCompanyExtraction(CompanyExtraction): note: Optional[str] = Field(None, description=“记录提取过程中的任何不确定性、假设或文本中的矛盾之处”)

策略二:使用验证器提供默认逻辑对于模糊信息,可以在验证器里提供一些启发式逻辑,但需谨慎。

from pydantic import validator class RobustCompanyExtraction(CompanyExtraction): @validator(‘founding_year’, pre=True, always=True) def handle_vague_year(cls, v, values): if v is not None: return v # 如果LLM没提取到,这里可以尝试从文本其他部分推断,但最好还是留空 # 例如,检查文本是否有‘成立于十年前’之类的描述 # 这里为了安全,我们返回None return None

策略三:实现自动重试(Retry)机制当 LLM 的输出无法通过 Pydantic 验证时(比如类型错误、缺少必填字段),最有效的策略是让 LLM 重试。LangChain 提供了RetryOutputParser

from langchain.output_parsers import RetryWithErrorOutputParser from langchain_core.prompts import PromptTemplate # 1. 首先,定义一个基础解析器(会失败) parser = PydanticOutputParser(pydantic_object=CompanyExtraction) # 2. 定义重试解析器 retry_parser = RetryWithErrorOutputParser.from_llm( parser=parser, llm=llm, max_retries=2 # 最大重试次数 ) # 3. 构建一个包含错误反馈提示的链 prompt = PromptTemplate( template=“““请根据以下用户输入和之前的错误信息,重新生成正确的输出格式。\n 原始查询: {query}\n 上次解析错误: {error}\n 请只输出符合格式的JSON,不要有其他任何内容。\n 格式要求: {format_instructions}”””, input_variables=[“query”, “error”], partial_variables={“format_instructions”: parser.get_format_instructions()} ) retry_chain = prompt | llm | retry_parser # 使用这个 chain 进行调用,初始错误可以设为空字符串

实操心得:重试机制非常有用,但不宜设置过多次数(通常1-2次)。如果多次重试仍失败,很可能是指令不清、模型能力不足或任务本身过于复杂。此时,应该将错误和原始输入记录下来,进行人工分析,并考虑优化你的 Pydantic 模型定义或提示词。

5. 高级技巧与性能优化

掌握了基础流程后,我们来看看如何提升结构化提取的可靠性、效率和处理复杂情况的能力。

5.1 处理列表类型与不确定数量的项

提取列表(如List[Product])是一大挑战。LLM 可能漏项,也可能把非项目内容塞进来。提升列表提取质量的方法:

  1. 在字段描述中明确数量提示Field(description=“产品列表,请找出文本中提到的所有产品,可能有多项,也可能没有。”)
  2. 使用更具体的指令:在系统提示词中强调:“请仔细扫描全文,确保不遗漏任何提到的产品。”
  3. 后处理清洗:对提取出的列表,可以写一个简单的后处理函数,过滤掉名称过于模糊(如“各种服务”)或明显不符合产品定义的项。

5.2 利用思维链(Chain-of-Thought)提升复杂字段准确率

对于一些需要推理的字段,比如从“我们是一家有十年历史的公司”推断founding_year,可以引导 LLM 在输出结构化数据的同时,附带一个简单的推理过程。虽然我们最终只要结构化数据,但这个“思考空间”能提升准确性。这可以通过在提示词中要求 LLM “逐步思考”来实现,或者使用支持 JSON 模式且能保留“推理痕迹”的模型(如 Claude 3 系列)。

system_prompt_cot = “““你是一个信息提取助手。请按以下步骤工作: 1. 首先,仔细阅读文本,找出所有相关信息。 2. 然后,对于需要推断的字段(如成立年份),进行简要推理。 3. 最后,严格根据推理结果,生成最终的结构化JSON输出。 请将最终输出放在 ‘output’ 字段中,将你的简要推理步骤放在 ‘reasoning’ 字段中。 ””” # 然后定义一个包含 output 和 reasoning 字段的 Pydantic 模型来接收

5.3 批量处理与异步优化

当需要从大量文档中提取信息时,顺序调用 API 会非常慢。我们需要批量处理和异步编程。

import asyncio from langchain_openai import ChatOpenAI from langchain_core.output_parsers import PydanticOutputParser from typing import List async def extract_from_documents_async(doc_texts: List[str], model_class, max_concurrency: int = 5): “”“异步批量提取文档信息”“” llm = ChatOpenAI(model=“gpt-4o”, temperature=0, max_retries=2) structured_llm = llm.with_structured_output(model_class) prompt = ChatPromptTemplate.from_template(“提取信息:{text}”) chain = prompt | structured_llm semaphore = asyncio.Semaphore(max_concurrency) # 控制并发数,避免触发速率限制 async def process_one(text: str): async with semaphore: try: result = await chain.ainvoke({“text”: text}) return result except Exception as e: print(f“处理文本时出错: {e}”) return None tasks = [process_one(text) for text in doc_texts] results = await asyncio.gather(*tasks, return_exceptions=True) # 处理结果和异常 valid_results = [r for r in results if isinstance(r, model_class)] return valid_results # 使用示例 documents = [“文档1文本...”, “文档2文本...”, ...] # 假设有很多文档 extracted_data = asyncio.run(extract_from_documents_async(documents, CompanyExtraction, max_concurrency=10))

注意事项:批量调用时,务必关注 API 的速率限制(RPM, TPM)和成本。max_concurrency不宜设置过高。对于超大批量任务,可以考虑结合队列和分布式处理。

5.4 模型选择与成本考量

不是所有任务都需要 GPT-4。进行结构化输出时:

  • 高精度、复杂结构、强推理需求:选择能力最强的模型,如 GPT-4、Claude 3 Opus。它们对指令遵循和复杂格式的理解更好。
  • 中等复杂度、常规提取:GPT-3.5-Turbo、Claude 3 Haiku/Sonnet 通常是性价比之选,在大多数场景下表现足够好。
  • 简单、固定格式的提取:甚至可以考虑使用更小、更快的开源模型(通过 LangChain 集成),如果其指令跟随能力经过微调能满足要求,可以大幅降低成本。

一个实用的策略是“分层处理”:先用一个快而便宜的模型(如 GPT-3.5-Turbo)做初筛和简单提取,对于它置信度低或提取失败的案例,再用更强的模型(如 GPT-4)进行复核和精提取。

6. 常见问题排查与实战避坑指南

在实际操作中,你会遇到各种各样的问题。下面是我踩过坑后总结出来的“避坑清单”。

6.1 LLM 不按格式输出或返回无关内容

  • 症状:LLM 返回了纯文本解释,而不是 JSON;或者在 JSON 外面包裹了 Markdown 代码块标记(json ...);或者添加了额外的说明文字。
  • 根因:提示词指令不够强硬,或者模型“太有礼貌”,总想解释它在做什么。
  • 解决方案
    1. 强化系统指令:在系统提示词开头使用强有力的命令,如“你必须只输出 JSON 对象,不要有任何额外的解释、前言、后语或 Markdown 标记。”“你的响应有且仅有一个合法的 JSON 对象。”
    2. 使用 LangChain 的with_structured_output:这是最推荐的方法,因为它从机制上强制 LLM 走函数调用路径,极大降低了“乱说话”的概率。
    3. 后处理清洗:如果仍有杂音,可以在解析前用简单的字符串处理(如正则表达式r‘```json\n?(.*?)\n?```’)提取 JSON 部分。

6.2 字段值提取错误或产生幻觉(Hallucination)

  • 症状:文本中明明没有的信息,被 AI“编造”出来填入了字段。例如,文本没提成立年份,但模型输出了一个 2020。
  • 根因:模型倾向于“完成”任务,当信息缺失时,它可能基于训练数据中的模式进行猜测;或者字段描述不够清晰,导致模型理解偏差。
  • 解决方案
    1. 明确“留空”指令:在字段描述和系统提示中反复强调“如果文本中没有明确提及,请将该字段设置为null或留空”。
    2. 使用Optional类型:确保你的 Pydantic 模型将可能缺失的字段定义为可选,并设置合理的默认值(如None)。
    3. 提供负面示例(Few-Shot):在提示词中给出一个例子,展示当信息缺失时,对应字段应该输出null
    4. 降低temperature:如前所述,将其设为 0。

6.3 处理枚举(Enum)类型时,LLM 返回了不在枚举值中的内容

  • 症状:你定义了JobType枚举为[“全职”, “兼职”, “实习”],但 LLM 返回了“合同工”
  • 根因:LLM 可能没有严格约束在枚举范围内选择,或者你的枚举值未能覆盖所有实际情况。
  • 解决方案
    1. 在描述中明确枚举值Field(description=“职位类型,必须是‘全职’、‘兼职’或‘实习’中的一个。”)
    2. 使用 Pydantic 的validator进行修正:写一个验证器,尝试将常见变体映射到标准值。
      @validator(‘type‘, pre=True) def normalize_job_type(cls, v): if v in [“全职”, “full-time”, “Full Time”]: return JobType.FULL_TIME elif v in [“兼职”, “part-time”, “Part Time”]: return JobType.PART_TIME # ... 其他映射 else: # 如果无法映射,可以抛错,或返回一个默认值 raise ValueError(f“不支持的职位类型: {v}”)
    3. 考虑使用字符串类型+后处理:如果类别动态变化或难以穷举,可以先让 LLM 输出字符串,然后用自己的业务逻辑进行归类。

6.4 性能瓶颈与速率限制(Rate Limit)

  • 症状:批量处理时速度慢,或频繁收到429 Too Many Requests错误。
  • 解决方案
    1. 异步并发:如上文所示,使用asyncio和信号量控制并发数。
    2. 指数退避重试:实现重试逻辑时,加入随机延迟(如time.sleep(2**retry_count + random.random())),避免雪崩式重试。
    3. 缓存结果:对于相同的或极其相似的输入文本,可以将提取结果缓存起来(例如使用functools.lru_cache或 Redis),避免重复调用 API,节省成本和时间。
    4. 监控与告警:记录调用次数、耗时和错误率,设置阈值告警。

6.5 复杂文本中关系提取的挑战

  • 症状:当文本中实体关系复杂时(如“A 产品由 X 部门负责,B 产品由 Y 部门负责”),简单的扁平化列表提取可能丢失这种对应关系。
  • 解决方案:升级你的数据模型,使其能表达关系。
    class ProductWithOwner(BaseModel): name: str category: str owning_department: str # 明确关联部门 class CompanyExtractionAdvanced(BaseModel): company_name: str departments: List[str] = Field(description=“文中提到的所有部门列表”) products: List[ProductWithOwner] # 产品直接关联部门
    同时,在提示词中明确要求建立这种关联:“请提取产品信息,并指明每个产品由哪个部门负责。如果文中未明确说明,请将 owning_department 字段留空。”

结构化输出与数据提取,是将大语言模型从“玩具”变为“生产力工具”的关键一步。它消除了人机交互的最后一道手动屏障,让 AI 生成的数据可以直接流入下游系统,驱动自动化流程。掌握 Pydantic 与 Function Calling 的结合使用,并灵活运用提示工程、错误处理和性能优化技巧,你就能构建出强大、可靠的 AI 数据提取管道。

← 返回列表