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 结构化输出,理解其底层FunctionCallingProgram与LLMTextCompletionProgram两套执行引擎的分发逻辑,并学会在需要时直接调用或子类化这些预测类来获得更低级的控制能力。
从一个需求说起:为什么需要 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 正确解析数据时,你可以在提示词层面补充额外方向。text与company_name是提示词模板中的两个变量,以关键字参数形式传入。
注意该方法签名(见 llm.py):第一个参数output_cls是目标 Pydantic 类,第二个参数prompt是PromptTemplate,此外还可选传入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_predict | L307 | 同步、一次性获取完整 Pydantic 对象 |
astructured_predict | L374 | 异步(await llm.astructured_predict(...)) |
stream_structured_predict | L461 | 同步流式,返回生成器,逐个产出部分填充的 Pydantic 对象 |
astream_structured_predict | L539 | 异步流式 |
流式变体适用于需要边生成边展示结果的场景。其核心机制位于 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的显式覆盖(OPENAI、FUNCTION、LLM、LM_FORMAT_ENFORCER等模式),其中LM_FORMAT_ENFORCER需要额外安装llama-index-program-lmformatenforcer包。
FunctionCallingProgram:函数调用路径(更可靠)
当 LLM 具备函数调用(function calling)API 时,走 function_program.py 中的FunctionCallingProgram,其流程为:
- 把 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)将其包装成一个函数工具; - 提示 LLM 并强制其使用该工具:
from_defaults中校验llm.metadata.is_function_calling_model必须为真,并通过tool_choice(默认强制该工具)与tool_required=True约束 LLM 只能调用这个工具;还支持allow_parallel_tool_calls以便一次返回多个对象; - 返回生成的 Pydantic 对象:从工具调用的参数中还原出
output_cls实例。
由于输出被约束为结构化工具调用,这一路径通常更可靠,也是默认优先选用的方案。
LLMTextCompletionProgram:纯文本路径(通用兜底)
当 LLM 是纯文本模型(无函数调用 API)时,走 llm_program.py 中的LLMTextCompletionProgram,其流程为:
- 输出 Pydantic Schema 为 JSON:由
PydanticOutputParser(pydantic.py)生成,其默认模板PYDANTIC_FORMAT_TMPL为Here's a JSON schema to follow: {schema} / Output a valid JSON object but do not repeat the schema.,并把该模板附加到用户提示词末尾; - 把 Schema 与数据一起发送给 LLM,并指示其按 Schema 格式返回:在
__call__(L98-L120)中,若 LLM 是对话模型则走chat并取message.content,否则走complete并取response.text; - 调用 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,直接实例化LLMTextCompletionProgram或FunctionCallingProgram并进一步定制行为。
直接使用 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.llm;prompt与prompt_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),仅供参考