news 2026/9/12 8:24:47

LlamaIndex 结构化预测(Structured Prediction)实战指南:用 structured_predict 细粒度控制 LLM 输出

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LlamaIndex 结构化预测(Structured Prediction)实战指南:用 structured_predict 细粒度控制 LLM 输出

LlamaIndex 结构化预测(Structured Prediction)实战指南:用 structured_predict 细粒度控制 LLM 输出

【免费下载链接】llama_indexLlamaIndex is the document processing platform for AI项目地址: https://gitcode.com/GitHub_Trending/ll/llama_index

本文是 LlamaIndex 结构化数据抽取系列教程的第三篇,聚焦于比 Structured LLM 更底一层的structured_predictAPI。通过本文,你将掌握如何绕过自动提示词、直接向 LLM 传入自定义PromptTemplate完成 Pydantic 结构化输出,理解其底层FunctionCallingProgramLLMTextCompletionProgram两套执行引擎的分发逻辑,并学会在需要时直接调用或子类化这些预测类来获得更低级的控制能力。

从一个需求说起:为什么需要 structured_predict

在系列上一篇《使用结构化 LLM》中,我们通过llm.as_structured_llm(Invoice)创建了一个结构化 LLM,所有提示词的构造都由 LlamaIndex 自动完成。这在大多数场景下开箱即用,但当你希望对「LLM 如何被提示」拥有更细粒度的控制时——例如在提示词中加入业务规则、兜底逻辑或领域指令——structured_predict提供了更直接的途径。

structured_predict每一个 LLM 类上都具备的方法(定义于 llm.py),它接收一个 Pydantic 类和一个PromptTemplate作为参数,外加提示词模板中出现的任意变量作为关键字参数。与 Structured LLM 不同,提示词完全由你书写,LLM 的调用与输出解析则由 LlamaIndex 替你完成。

前置准备:模型、依赖与 Invoice 定义

我们沿用整个系列相同的示例:一张 Uber 电子发票(uber_receipt.pdf),以及同一个InvoicePydantic 模型(完整定义与 JSON Schema 展开见系列开篇的 结构化数据抽取入门):

from datetime import datetime from pydantic import BaseModel, Field class LineItem(BaseModel): """A line item in an invoice.""" item_name: str = Field(description="The name of this item") price: float = Field(description="The price of this item") class Invoice(BaseModel): """A representation of information from an invoice.""" invoice_id: str = Field( description="A unique identifier for this invoice, often a number" ) date: datetime = Field(description="The date this invoice was created") line_items: list[LineItem] = Field( description="A list of all the items in this invoice" )

依赖与环境(与上一篇一致):

  • pip install llama-index-core llama-index-llms-openai安装核心库与 OpenAI LLM(也可替换为其他 LLM 集成);
  • 设置环境变量OPENAI_API_KEY
  • pip install llama-index-readers-file以使用PDFReader(该读取器实现在 llama-index-readers-file 中)。

加载发票文本:

from llama_index.readers.file import PDFReader from pathlib import Path pdf_reader = PDFReader() documents = pdf_reader.load_data(file=Path("./uber_receipt.pdf")) text = documents[0].text

核心用法:向 LLM 直接调用 structured_predict

as_structured_llm不同,这次我们不再让 LlamaIndex 代写提示词,而是自己构造PromptTemplate,把「当发票 ID 缺失时如何兜底」这类业务规则直接写进提示词:

from llama_index.core.prompts import PromptTemplate prompt = PromptTemplate( "Extract an invoice from the following text. If you cannot find an invoice ID, use the company name '{company_name}' and the date as the invoice ID: {text}" ) response = llm.structured_predict( Invoice, prompt, text=text, company_name="Uber" )

这正是structured_predict的价值所在:当 Pydantic 模型本身不足以指导 LLM 正确解析数据时,你可以在提示词层面补充额外方向textcompany_name是提示词模板中的两个变量,以关键字参数形式传入。

注意该方法签名(见 llm.py):第一个参数output_cls是目标 Pydantic 类,第二个参数promptPromptTemplate,此外还可选传入llm_kwargs(透传给底层 LLM 调用的参数,如 temperature、max_tokens 等)。方法内部会先派发LLMStructuredPredictStartEvent,调用结束后派发LLMStructuredPredictEndEvent,并且会校验返回结果必须是BaseModel实例,否则抛出TypeError提示「LLM 未能产生有效的结构化输出」。

返回值与 JSON 序列化

as_structured_llm(...).complete(...)返回CompletionResponse不同,structured_predict的返回值直接就是 Pydantic 对象本身。因此无需访问.raw属性,可以直接调用 Pydantic 的model_dump_json()得到 JSON:

import json json_output = response.model_dump_json() print(json.dumps(json.loads(json_output), indent=2))

输出示例(注意兜底逻辑生效:invoice_id被拼成了Uber-2024-10-10):

{ "invoice_id": "Uber-2024-10-10", "date": "2024-10-10T19:49:00", "line_items": [ {"item_name": "Trip fare", "price": 12.18}, {"item_name": "Access for All Fee", "price": 0.1}, ..., ], }

四个变体:同步、异步与流式

structured_predict并不是孤立的方法,它提供了一套覆盖同步/异步、普通/流式的完整变体(全部定义于 llm.py):

方法签名位置适用场景
structured_predictL307同步、一次性获取完整 Pydantic 对象
astructured_predictL374异步(await llm.astructured_predict(...)
stream_structured_predictL461同步流式,返回生成器,逐个产出部分填充的 Pydantic 对象
astream_structured_predictL539异步流式

流式变体适用于需要边生成边展示结果的场景。其核心机制位于 program/utils.py 的process_streaming_objects:流式解析时先通过create_flexible_model生成一个允许任意字段的FlexibleModel副本,随着 token 逐步到达不断尝试model_validate/model_validate_json解析,并借助_repair_incomplete_json修复残缺的 JSON(补全缺失的引号与花括号);解析出的部分对象按「有效字段数更多则替换」(num_valid_fields递归统计非 None 字段)的策略持续更新,最终再尝试把 FlexibleModel 转换回严格的output_cls。若设置了allow_parallel_tool_calls=True,还会返回对象列表而非单个对象。

Under the hood:两套底层执行引擎

structured_predict并不自己直接解析输出,而是根据所用 LLM 的能力分发到两个不同的 Program 类。分发逻辑集中在 program/utils.py 的 get_program_for_llm:

if pydantic_program_mode == PydanticProgramMode.DEFAULT: if llm.metadata.is_function_calling_model: return FunctionCallingProgram.from_defaults(...) else: return LLMTextCompletionProgram.from_defaults( output_parser=PydanticOutputParser(output_cls=output_cls), ...)

也就是说:默认模式下,判断依据是 LLM 元数据中的is_function_calling_model标志。同时该分发函数还支持PydanticProgramMode的显式覆盖(OPENAIFUNCTIONLLMLM_FORMAT_ENFORCER等模式),其中LM_FORMAT_ENFORCER需要额外安装llama-index-program-lmformatenforcer包。

FunctionCallingProgram:函数调用路径(更可靠)

当 LLM 具备函数调用(function calling)API 时,走 function_program.py 中的FunctionCallingProgram,其流程为:

  1. 把 Pydantic 对象转换为工具get_function_tool(L37-L64)调用output_cls.model_json_schema()生成 JSON Schema,并用FunctionTool.from_defaults(fn=model_fn, name=..., description=..., fn_schema=output_cls)将其包装成一个函数工具;
  2. 提示 LLM 并强制其使用该工具from_defaults中校验llm.metadata.is_function_calling_model必须为真,并通过tool_choice(默认强制该工具)与tool_required=True约束 LLM 只能调用这个工具;还支持allow_parallel_tool_calls以便一次返回多个对象;
  3. 返回生成的 Pydantic 对象:从工具调用的参数中还原出output_cls实例。

由于输出被约束为结构化工具调用,这一路径通常更可靠,也是默认优先选用的方案。

LLMTextCompletionProgram:纯文本路径(通用兜底)

当 LLM 是纯文本模型(无函数调用 API)时,走 llm_program.py 中的LLMTextCompletionProgram,其流程为:

  1. 输出 Pydantic Schema 为 JSON:由PydanticOutputParser(pydantic.py)生成,其默认模板PYDANTIC_FORMAT_TMPLHere's a JSON schema to follow: {schema} / Output a valid JSON object but do not repeat the schema.,并把该模板附加到用户提示词末尾;
  2. 把 Schema 与数据一起发送给 LLM,并指示其按 Schema 格式返回:在__call__(L98-L120)中,若 LLM 是对话模型则走chat并取message.content,否则走complete并取response.text
  3. 调用 Pydantic 的model_validate_json()解析PydanticOutputParser.parse先通过extract_json_str从文本中提取 JSON 片段,再执行self._output_cls.model_validate_json(json_str)

这一路径依赖 LLM 自己"按格式输出",可靠性明显低于函数调用路径,但所有基于文本的 LLM 都支持,因此是覆盖面最广的兜底方案。

直接调用预测类:更低级的控制

实践上structured_predict对任何 LLM 都应当开箱即用,但若你需要更低级的控制,完全可以绕过structured_predict,直接实例化LLMTextCompletionProgramFunctionCallingProgram并进一步定制行为。

直接使用 LLMTextCompletionProgram

下面这段代码与在无函数调用 API 的 LLM 上调用structured_predict完全等价,返回的同样是 Pydantic 对象:

from llama_index.core.program import LLMTextCompletionProgram from llama_index.core.prompts import PromptTemplate textCompletion = LLMTextCompletionProgram.from_defaults( output_cls=Invoice, llm=llm, prompt=PromptTemplate( "Extract an invoice from the following text. If you cannot find an invoice ID, use the company name '{company_name}' and the date as the invoice ID: {text}" ), ) output = textCompletion(company_name="Uber", text=text)

from_defaults的实现(llm_program.py)可以看到几个细节:llm缺省时回退到全局Settings.llmpromptprompt_template_str必须二选一;output_cls未指定时会从PydanticOutputParser中反推;未传output_parser时会自动构造PydanticOutputParser(output_cls=...)

子类化 PydanticOutputParser 定制解析逻辑

直接调用预测类的真正优势在于:你可以通过子类化PydanticOutputParser并覆写get_pydantic_object方法来自定义输出解析,例如为低能力(low-powered)LLM 编写更聪明的纠错解析:

from llama_index.core.output_parsers import PydanticOutputParser class MyOutputParser(PydanticOutputParser): def get_pydantic_object(self, text: str): # do something more clever than this return self.output_parser.model_validate_json(text) textCompletion = LLMTextCompletionProgram.from_defaults( llm=llm, prompt=PromptTemplate( "Extract an invoice from the following text. If you cannot find an invoice ID, use the company name '{company_name}' and the date as the invoice ID: {text}" ), output_parser=MyOutputParser(output_cls=Invoice), )

注意:PydanticOutputParser.__init__(pydantic.py)会把传入的output_cls保存在self._output_cls属性中,因此子类内部应通过self._output_cls.model_validate_json(...)而非self.output_parser访问目标模型(示例中的self.output_parser仅为演示骨架,实际覆写时请直接使用self._output_cls)。这一机制对于解析能力较弱、容易输出脏文本的小型模型尤为实用,你可以在这里加入去噪、字段重映射或基于规则的修补逻辑,而完全不影响 LLM 调用部分。

小结与下一步

structured_predict家族方法位于「Structured LLM(全自动提示)」与「底层 Program 调用(全手动)」之间的关键位置:它把提示词控制权交还给你,同时替你处理 Pydantic Schema 生成、工具转换与输出解析。从源码看,它的可靠性差异完全取决于底层引擎——函数调用路径(FunctionCallingProgram)与纯文本路径(LLMTextCompletionProgram)的分发逻辑清晰记录在 get_program_for_llm 中。

如果你还想进一步下探——例如在一次调用中同时抽取多个结构体、完全绕过 Program 抽象——请继续阅读系列的最后一篇:更低层的结构化数据调用。与之互补的内容还包括 结构化输入(使用RichPromptTemplate将输入格式化为 XML),以及开篇的 Pydantic 与 Schema 基础。

【免费下载链接】llama_indexLlamaIndex is the document processing platform for AI项目地址: https://gitcode.com/GitHub_Trending/ll/llama_index

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 8:22:56

ShopXO商城系统源码部署与二次开发全攻略

简介:一份基于PHP开发的开源电商系统ShopXO源码包,面向PHP开发者、电商二次开发人员及希望搭建商城的技术学习者。源码完整覆盖商品、订单、支付、购物车、用户、物流等核心模块,适合研读MVC分层结构、数据库设计、支付API对接、前端交互与安…

作者头像 李华
网站建设 2026/9/12 8:22:53

Xshell连接Linux云服务器并安装Nginx完整实操指南

最近入手了一台HoRain云的云服务器,想着把手上几个小项目都挂上去。第一步就是通过Xshell远程连接Linux,再把Nginx装起来。说实话,这俩操作对老手来说就是几分钟的事,但对刚入门的同学,坑还真不少:Xshell连…

作者头像 李华
网站建设 2026/9/12 8:21:29

从EasyExcel迁移到Apache POI:Java Excel底层控制实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 8:16:35

Karpathy式LLM工程实践:用CLAUDE.md构建可信AI协作系统

1. 项目概述:这不是一份“技能清单”,而是一份LLM时代工程师的生存地图你点开这个标题,大概率不是想查Andrei Karpathy的LinkedIn履历,也不是想背诵他讲过的某句金句。你真正想问的是:当一个像Karpathy这样亲手把神经网…

作者头像 李华
网站建设 2026/9/12 8:15:03

基于Matlab的手指手掌静脉识别实现与算法详解

简介:面向机器视觉课程创新实践,这份手指手掌静脉识别Matlab工程聚焦手部静脉图像预处理算法实验研究。项目完整覆盖静脉识别链路,对手指和手掌分别进行轮廓分割、感兴趣区域(ROI)截取、静脉纹理增强与分割&#xff0c…

作者头像 李华