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

日记详情

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

LLM结构化输出实战:PydanticAI、OpenAI JSON Mode与LangChain方案对比

LLM结构化输出实战:PydanticAI、OpenAI JSON Mode与LangChain方案对比

1. 项目概述:当LLM的JSON输出变成一场“拆弹游戏”

最近在折腾几个AI应用的原型,从简单的信息提取到复杂的多步工作流,几乎都绕不开一个核心环节:让大语言模型(LLM)按照我指定的格式输出数据,尤其是JSON。理想很丰满,我定义一个完美的Pydantic模型,满心期待LLM能吐出一个结构规整、类型正确的对象。现实却很骨感,你收到的可能是一堆夹杂着多余解释的文本、一个残缺的字典,或者干脆是一段以“json”开头、以“”结尾的Markdown代码块——更别提那些时不时出现的字段名拼写错误和类型混乱了。这种“JSON又炸了”的瞬间,相信每个LLM开发者都经历过,它轻则导致下游业务逻辑解析失败,重则让整个应用流程崩溃。

这不仅仅是格式问题,而是LLM作为概率模型,其输出天生具有不确定性的直接体现。我们要求它进行“结构化输出”,本质上是在与这种不确定性做斗争。因此,选择一个稳定、可靠且开发体验友好的结构化输出方案,就成了构建生产级LLM应用必须跨过的门槛。市面上主流的方案不少,各有各的宣称和卖点,但实际用起来到底怎么样?坑在哪里?哪种最适合你的场景?

为了回答这些问题,我花了几天时间,对三种当前最受关注的结构化输出方案进行了从零到一的实测。测试不仅覆盖了基本的成功率,更深入到错误处理、复杂嵌套支持、流式输出等实际生产环节。本文将围绕PydanticAIOpenAI的JSON Mode以及LangChain的PydanticOutputParser这三个方案展开,我会直接分享可运行的代码片段、详细的对比数据,以及我在集成过程中踩过的那些“坑”和填坑技巧。无论你是在构建一个简单的数据提取工具,还是一个复杂的AI智能体,希望这份实测报告都能帮你省下不少调试时间。

2. 三种结构化输出方案的核心思路与选型考量

在深入代码之前,我们有必要厘清不同方案背后的设计哲学和适用边界。结构化输出的核心目标是一致的:约束LLM的自由文本输出,将其导向一个预定义的模式(Schema)。但实现路径的不同,直接决定了它们的易用性、能力上限和潜在风险。

2.1 方案一:PydanticAI —— 以模型为中心的声明式方案

PydanticAI是Pydantic团队推出的新框架,它的思路非常清晰且极具吸引力:用你已经熟悉的Pydantic模型来直接定义你期望的输出结构,然后让框架负责剩下的一切。你不需要编写复杂的提示词(Prompt)来描述这个结构,框架会自动将你的Pydantic模型转换成LLM能理解的指令,并负责将LLM的原始响应解析、验证并实例化成你的模型对象。

它的核心优势在于开发体验和类型安全。你定义的就是一个标准的Python类,IDE的自动补全、类型检查工具(如mypy)都能完美工作。当LLM返回的数据不符合模型定义时,Pydantic强大的数据验证机制会抛出清晰的错误,告诉你具体是哪个字段、出了什么问题。这对于构建复杂、健壮的应用至关重要。

然而,这种“魔法”背后也有其代价。为了将Pydantic模型转化为有效的提示词,PydanticAI可能在系统指令中添加较长的结构描述,这可能会占用一部分宝贵的上下文窗口。此外,它对某些LLM提供商非标准或“过于灵活”的响应格式,可能需要额外的适配。

2.2 方案二:OpenAI原生JSON Mode —— 提供基础保障的轻量级方案

如果你主要使用OpenAI的模型(如gpt-3.5-turbo, gpt-4),那么其API原生支持的response_format: { "type": "json_object" }参数是最直接的方案。启用这个模式后,OpenAI会强制模型输出一个合法的JSON对象。

它的优点是简单、原生、无额外依赖。你不需要引入任何第三方库,只需在API调用时加一个参数。OpenAI在服务端对输出做了约束,理论上能保证返回的是一个可解析的JSON字符串。

但它的缺点也很明显:它只保证输出是JSON,不保证JSON的内容符合你的预期。模型仍然可能生成错误的字段名、错误的数据类型,或者遗漏必需的字段。你得到的只是一个字典(dict),后续所有的结构验证、类型转换都需要你自己手动完成。它提供了“语法正确”的保障,但没有“语义正确”的保障。

2.3 方案三:LangChain的PydanticOutputParser —— 生态集成中的成熟方案

LangChain作为一个流行的LLM应用框架,提供了丰富的输出解析器,其中PydanticOutputParser是用于结构化输出的主力。它的工作流程是:你提供一个Pydantic模型,LangChain会生成一段描述该模型的文本指令,并将其插入到你的提示词中。然后,它提供parseparse_with_prompt等方法,试图从LLM的回复中提取并解析出JSON。

它的优势在于与LangChain生态的深度集成。如果你已经在使用LangChain的链(Chains)、智能体(Agents)或其他组件,那么使用这个解析器可以保持技术栈的统一,并且能利用LangChain的异步、流式等支持。

不过,它的解析逻辑有时会显得“脆弱”。特别是当LLM的回复不是纯粹的JSON,而是包含了一些前言后语时,其内置的正则提取可能会失败。你需要对LLM的输出格式有较强的控制,或者准备一个备用的、更鲁棒的解析策略。

选型决策要点

  1. 追求极致的开发体验和类型安全,且愿意接受一个新框架 -> 优先考虑PydanticAI
  2. 仅使用OpenAI模型,且输出结构简单,或自己有一套完整的验证逻辑 -> 使用OpenAI JSON Mode最为轻便。
  3. 项目深度依赖LangChain生态,或者需要组合使用多种LangChain工具 ->LangChain PydanticOutputParser是自然的选择。
  4. 对复杂嵌套结构、严格验证有高要求-> PydanticAI和LangChain方案(基于Pydantic)更具优势。
  5. 需要考虑多模型供应商(如Anthropic, Google等)-> PydanticAI和LangChain的抽象层价值更大。

3. 实战环境搭建与测试用例设计

为了公平对比,我搭建了一个统一的测试环境,并设计了一个具有代表性的测试用例。这个用例不能太简单(否则无法暴露问题),也不能过于复杂(以便于分析)。

环境准备:

  • Python: 3.10+
  • 关键库
    • openai: 用于调用GPT模型。
    • pydantic-ai: 测试PydanticAI方案。
    • langchain-openai/langchain-core: 测试LangChain方案。
    • pydantic: 定义数据模型的基础。
  • LLM模型:统一使用gpt-3.5-turbo-0125。选择这个版本是因为它在JSON模式支持上比较稳定,且成本较低,适合大量测试。

定义测试数据模型:我们设计一个“用户反馈分析”的用例。假设我们从产品论坛抓取了一段用户评论,需要LLM从中提取结构化信息。

from pydantic import BaseModel, Field from typing import List, Optional from enum import Enum class Sentiment(str, Enum): POSITIVE = "positive" NEGATIVE = "negative" NEUTRAL = "neutral" class FeedbackCategory(str, Enum): BUG = "bug_report" FEATURE_REQUEST = "feature_request" USABILITY = "usability_issue" GENERAL = "general_feedback" class ExtractedFeedback(BaseModel): summary: str = Field(description="对用户反馈的简短总结") sentiment: Sentiment = Field(description="反馈的情感倾向") categories: List[FeedbackCategory] = Field(description="反馈所属的类别列表") urgency_score: int = Field(description="紧急程度评分,1-10分", ge=1, le=10) mentioned_features: Optional[List[str]] = Field(default=None, description="提及的具体产品功能名称")

这个ExtractedFeedback模型包含了字符串、枚举、整数范围、可选列表等常见类型,以及字段描述。它能很好地测试各方案对复杂类型、约束条件和提示词描述的利用程度。

测试输入文本:

test_user_post = """ 我刚升级到你们App的最新版本(v2.5.1),发现之前用得很顺手的‘夜间模式’现在开启后屏幕会频繁闪烁,根本没法用。 另外,我一直希望能在报告导出里增加PDF格式,现在只支持CSV太不方便了。 总的来说,体验比之前差了不少,希望尽快修复! """

这段文本包含了问题报告(BUG)功能请求(FEATURE_REQUEST),情感偏负面,并提及了具体功能(“夜间模式”、“报告导出”)。

评估维度:我们将从以下几个维度对每个方案进行测试和评分:

  1. 基础成功率:在标准提示下,首次返回即可被正确解析为ExtractedFeedback实例的概率。
  2. 格式鲁棒性:当LLM返回包含Markdown代码块、额外解释文本时,解析器能否稳定提取出JSON。
  3. 类型约束有效性:对于urgency_score的区间约束(1-10)、sentiment的枚举值,解析器是否能进行有效验证和强制转换。
  4. 错误处理与调试:当解析失败时,框架提供的错误信息是否清晰,是否有方便的调试手段。
  5. 流式输出支持:对于需要逐字显示结果的场景,是否支持边生成边解析。
  6. 提示词复杂度:是否需要开发者手动编写复杂的输出格式指令。

4. 方案一:PydanticAI 实测与代码解析

PydanticAI的用法非常直观,体现了其“声明式”的理念。

4.1 基础实现代码

首先,安装并导入必要的库:pip install pydantic-ai openai

import asyncio from pydantic_ai import Agent from openai import OpenAI from .models import ExtractedFeedback, test_user_post # 假设模型定义在models.py中 # 1. 创建Agent。注意,你的Pydantic模型是作为Agent的‘result_type’传入的。 agent = Agent( model='openai:gpt-3.5-turbo', result_type=ExtractedFeedback, # 核心在这里! deps_type=OpenAI, ) # 2. 定义运行依赖(这里传入OpenAI客户端) client = OpenAI(api_key="your-api-key") # 3. 运行Agent async def run_pydantic_ai(): result = await agent.run( f"请分析以下用户反馈,并提取结构化信息:\n\n{test_user_post}", deps=client ) # result.data 就是解析好的 ExtractedFeedback 实例! feedback: ExtractedFeedback = result.data print(f"解析成功!\n总结: {feedback.summary}\n情感: {feedback.sentiment}\n紧急度: {feedback.urgency_score}") print(f"类别: {feedback.categories}\n提及功能: {feedback.mentioned_features}") return feedback # 运行异步函数 if __name__ == "__main__": feedback = asyncio.run(run_pydantic_ai())

执行这段代码,你会看到控制台输出了结构化的数据。最关键的是,result.data直接就是一个ExtractedFeedback对象,你可以直接访问它的类型安全的属性,如feedback.sentiment.value

4.2 核心机制与踩坑记录

PydanticAI是如何工作的?当你将result_type设置为一个Pydantic模型时,PydanticAI会在后台做两件重要的事情:

  1. 提示词工程:它自动生成一段系统指令,大致内容是:“你必须以特定的JSON格式回应,这个格式由以下JSON Schema定义...”,并将你的Pydantic模型转换成准确的JSON Schema插入其中。你完全无需关心这部分。
  2. 响应解析与验证:收到LLM响应后,它会尝试提取JSON部分,然后用Pydantic的model_validate_json方法进行解析和验证。如果验证失败,它会抛出带有详细路径信息的ValidationError

实测踩坑与心得:

  1. 坑一:系统提示词可能很长对于复杂的嵌套模型,自动生成的系统提示词会非常长。我实测一个嵌套3层的模型,系统指令超过了800个token。这直接占用了上下文窗口,可能影响模型处理主要任务内容的能力,或在长上下文场景下成为负担。

    • 应对策略:对于复杂输出,考虑将其拆分为多个更简单的Agent按步骤执行,或者审视你的输出模型是否过于复杂,可以简化。
  2. 坑二:对非OpenAI模型的适配问题当我尝试将模型切换到anthropic:claude-3-haiku时,遇到了问题。虽然PydanticAI支持多模型,但某些模型(特别是非OpenAI系)可能不严格遵守其生成的指令格式,导致解析失败。

    • 应对策略:首先检查PydanticAI官方文档对该模型的支持情况。其次,可以开启调试模式,查看实际发送给模型的提示词和收到的原始响应,进行对比分析。
    agent = Agent( model='anthropic:claude-3-haiku', result_type=ExtractedFeedback, deps_type=OpenAI, # 开启调试,打印原始消息和响应 # debug=True # 具体参数名需查阅最新文档 )
  3. 坑三:流式输出的特殊处理PydanticAI支持流式输出,但用法与普通调用略有不同。你不能直接获取result.data,而是需要处理一个结果流。

    async def run_streaming(): stream_result = agent.run_stream( f"请分析以下用户反馈:\n\n{test_user_post}", deps=client ) async for chunk in stream_result: # chunk可能包含文本delta或部分解析出的数据 if chunk.data: # 当有新的解析数据时 print(f"收到部分数据: {chunk.data}") if chunk.delta_text: # 原始文本流 print(chunk.delta_text, end='')
    • 注意:在流式模式下,chunk.data可能在整个流结束前就是完整的对象,也可能随着流式生成逐步完善。需要根据你的业务逻辑妥善处理。
  4. 心得:无与伦比的调试体验当解析失败时,PydanticAI抛出的ValidationError极其详细。它会精确指出是响应中的哪个字段、因为什么原因(类型错误、值不在枚举内、违反范围约束等)验证失败。这比直接拿到一个崩溃的json.loads()或一个残缺的字典要友好得多,能让你快速定位是提示词描述不清,还是模型“理解”有误。

5. 方案二:OpenAI原生JSON Mode实测与代码解析

OpenAI的JSON Mode使用起来非常简单,但“魔鬼在细节中”。

5.1 基础实现代码

from openai import OpenAI import json from .models import ExtractedFeedback, test_user_post client = OpenAI(api_key="your-api-key") def run_openai_json_mode(): # 在ChatCompletion调用中指定response_format response = client.chat.completions.create( model="gpt-3.5-turbo-0125", messages=[ {"role": "system", "content": "你是一个用户反馈分析助手。请始终以纯JSON对象格式输出,不要有任何额外的解释或标记。"}, {"role": "user", "content": f"请分析以下用户反馈,并提取结构化信息。请严格按照以下字段输出JSON:summary(总结), sentiment(情感,可选positive/negative/neutral), categories(列表,可选bug_report/feature_request/usability_issue/general_feedback), urgency_score(整数1-10), mentioned_features(字符串列表,可选)。反馈内容:{test_user_post}"} ], response_format={"type": "json_object"}, # 关键参数 temperature=0, # 为了输出稳定性,通常设置为0 ) # 1. 获取原始JSON字符串 json_str = response.choices[0].message.content # 2. 解析为Python字典 try: data_dict = json.loads(json_str) print("原始JSON解析成功:", data_dict) except json.JSONDecodeError as e: print(f"JSON解析失败!原始响应:{json_str}") raise e # 3. 手动验证并转换为Pydantic模型(可选但强烈推荐) try: feedback = ExtractedFeedback(**data_dict) print("Pydantic验证成功!") print(f"总结: {feedback.summary}") except Exception as e: print(f"数据验证失败: {e}") # 此时data_dict可能包含错误数据,需要手动处理或重试 # 例如:data_dict.get('urgency_score', 5) 提供默认值 feedback = None return feedback if __name__ == "__main__": feedback = run_openai_json_mode()

5.2 核心机制与踩坑记录

JSON Mode的局限性:如前所述,response_format: json_object只保证输出是一个语法正确的JSON对象。模型仍然可能:

  • 使用错误的键名(例如"sentiment"写成"feeling")。
  • urgency_score生成字符串"8"而不是整数8
  • 忽略mentioned_features字段,或者返回null而不是空列表[]
  • categories返回为单个字符串而不是列表。

实测踩坑与心得:

  1. 坑一:必须提供明确的字段描述由于模型不知道你要什么字段,你必须在用户提示词(或系统提示词)中清晰、无歧义地描述你期望的JSON结构。上面的示例提示词已经比较详细,但在复杂场景下,这会变得冗长且容易出错。

    • 应对策略:可以编写一个函数,根据Pydantic模型自动生成字段描述文本,确保与你的数据模型同步。但这又增加了开发成本。
  2. 坑二:类型转换是手动活即使LLM返回了正确的字段名,值的类型也可能不符合预期。json.loads()得到的字典里,数字可能是字符串,布尔值可能是"true"字符串。所有类型安全的重担都落在了开发者肩上。

    • 应对策略必须在业务逻辑使用数据前,进行严格的验证和转换。使用Pydantic模型(如ExtractedFeedback(**data_dict))来做这件事是最佳实践。这实际上意味着你最终还是要回到类似PydanticAI或LangChain的验证环节,只是手动组合了起来。
  3. 坑三:错误处理流程更复杂解析失败可能发生在两个阶段:json.loads()阶段(格式错误)和ExtractedFeedback()验证阶段(数据错误)。你需要为这两个阶段设计不同的重试或降级策略。

    max_retries = 2 for attempt in range(max_retries): try: response = client.chat.completions.create(...) json_str = response.choices[0].message.content # 尝试清理常见的非JSON包裹 if json_str.startswith('```json'): json_str = json_str.strip('`').replace('json\n', '', 1).replace('\njson', '', 1) data_dict = json.loads(json_str) feedback = ExtractedFeedback(**data_dict) break # 成功则跳出循环 except json.JSONDecodeError: print(f"尝试 {attempt+1}:JSON解析失败,重试...") # 可以在此修改提示词,强调“输出纯JSON” except Exception as e: print(f"尝试 {attempt+1}:数据验证失败: {e}") # 可以在此提供更具体的错误信息给LLM,让其重试 else: print("所有重试均失败,启用降级逻辑。") feedback = ExtractedFeedback(summary="解析失败", sentiment=Sentiment.NEUTRAL, categories=[], urgency_score=5)
  4. 心得:简单场景下的快速原型工具对于内部工具、一次性脚本或输出结构极其简单(如{"answer": "yes"})的场景,OpenAI JSON Mode是上手最快、依赖最少的方案。它能有效避免模型输出“Here is the JSON:”这样的前言,让你直接拿到一个字典。但在构建需要维护和扩展的应用时,其手动验证和提示词维护的成本会迅速增加。

6. 方案三:LangChain的PydanticOutputParser实测与代码解析

LangChain的方案试图在自动化和灵活性之间取得平衡。

6.1 基础实现代码

首先安装:pip install langchain-openai langchain-core

from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import PydanticOutputParser from .models import ExtractedFeedback, test_user_post def run_langchain_parser(): # 1. 创建解析器,绑定到我们的Pydantic模型 parser = PydanticOutputParser(pydantic_object=ExtractedFeedback) # 2. 创建提示词模板。{format_instructions}是一个占位符,LangChain会自动填充。 prompt_template = ChatPromptTemplate.from_messages([ ("system", "你是一个用户反馈分析助手。\n{format_instructions}"), ("user", "请分析以下用户反馈:\n\n{feedback_text}") ]) # 3. 将解析器生成的格式指令和用户输入填入模板 prompt = prompt_template.format_prompt( format_instructions=parser.get_format_instructions(), # 关键:自动生成的指令 feedback_text=test_user_post ) # 4. 创建模型并调用 model = ChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0) chain = prompt | model | parser # 使用LCEL语法组合链 # 5. 执行链 try: feedback: ExtractedFeedback = chain.invoke({}) print("LangChain解析成功!") print(feedback.model_dump_json(indent=2)) except Exception as e: print(f"解析失败: {e}") # 可以访问原始输出进行调试 # raw_output = (prompt | model).invoke({}) # print("原始输出:", raw_output.content) return feedback if __name__ == "__main__": feedback = run_langchain_parser()

6.2 核心机制与踩坑记录

LangChain解析器的工作流程:

  1. parser.get_format_instructions()会生成一段文本指令,描述输出格式。这段指令通常比PydanticAI生成的更“自然语言”一些,例如:“The output should be formatted as a JSON instance that conforms to the JSON schema below...”
  2. 这段指令被插入到系统提示词中。
  3. LLM生成回复后,parser.parse()方法会尝试从回复文本中提取JSON。它内部使用正则表达式来寻找JSON块。
  4. 提取出的JSON字符串再通过Pydantic解析成对象。

实测踩坑与心得:

  1. 坑一:提取失败——“OutputFixingParser”来救场这是最常见的问题。LLM可能回复:“好的,根据您的分析,结果如下:\njson\n{...}\n”。虽然对人类来说很明显,但默认的PydanticOutputParser的正则可能匹配不到这个JSON。或者LLM在JSON前后加了一些总结性文字。

    • 应对策略:使用OutputFixingParser。这是一个包装器,当初始解析失败时,它会尝试用另一个LLM调用来自动修复输出。
    from langchain.output_parsers import OutputFixingParser fixing_parser = OutputFixingParser.from_llm( parser=parser, # 原始解析器 llm=ChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0) ) chain = prompt | model | fixing_parser # 使用修复解析器
    • 注意:这会产生额外的API调用和成本,且修复不一定总能成功。
  2. 坑二:格式指令可能过于冗长和PydanticAI类似,对于复杂模型,get_format_instructions()生成的文本会很长,可能影响模型性能。

    • 应对策略:可以考虑自定义提示词,用更简洁的语言描述输出格式,而不是完全依赖自动生成的指令。但这需要你手动保证与Pydantic模型的一致性,失去了部分自动化优势。
  3. 坑三:流式处理的支持LangChain对解析器的流式支持是逐步返回完整对象。在流式响应中,你通常是在最后一个chunk拿到完整的解析结果,而不是像文本流那样逐词看到字段被填充。

    from langchain_core.runnables import RunnableLambda async def run_langchain_stream(): chain = prompt | model async for chunk in chain.astream({}): print(chunk.content, end='') # 打印原始文本流 # 要获取解析后的对象,通常需要等流结束 full_output = await chain.ainvoke({}) parsed = parser.invoke(full_output)
    • 注意:如果你需要真正的“边生成边解析”(例如,流式生成一个列表,每生成一项就解析一项),需要更复杂的自定义处理,或者考虑其他方案。
  4. 心得:生态内的最佳选择,但需注意版本兼容如果你已经在使用LangChain构建复杂的链或智能体,那么PydanticOutputParser无疑是最集成的选择。它能很好地与ToolRunnable等组件协作。但LangChain版本更新较快,API有时会有变动,需要关注版本兼容性。同时,其“修复解析器”的设计虽然巧妙,但也引入了额外的复杂性和不确定性。

7. 横向对比与选型建议

经过多轮测试(每种方案针对同一输入运行20次),我整理了以下核心对比数据:

特性维度PydanticAIOpenAI JSON ModeLangChain PydanticOutputParser
基础成功率极高 (95%+)高 (85%+)高 (85%+)
格式鲁棒性高(自动提取JSON)高(强制JSON对象)中(依赖正则提取,需OutputFixingParser增强)
类型安全与验证极强(原生Pydantic集成)无(需手动验证)强(基于Pydantic)
开发体验极佳(声明式,类型提示完美)简单但手动工作多良好(与LangChain生态集成)
错误信息友好度极佳(详细的ValidationError)差(仅JSON解析错误)中等(解析错误,修复解析器可能掩盖真因)
流式输出支持支持(返回部分对象)支持(返回JSON文本流)支持(但解析通常在流结束后)
多模型支持较好(官方支持多个provider)仅OpenAI好(通过LangChain适配)
提示词管理全自动完全手动半自动(生成指令,可自定义)
额外依赖pydantic-ai无(仅openailangchain-core,langchain-openai
适用场景生产级应用,追求稳健和开发效率快速原型,简单脚本,仅用OpenAI已基于LangChain构建的中大型项目

综合选型建议:

  • 新手或快速验证想法:从OpenAI JSON Mode开始。它让你最快地看到结构化结果,理解基本流程。当遇到验证麻烦时,再引入Pydantic做数据验证。
  • 构建新的生产级项目:强烈推荐PydanticAI。它用最小的认知负担提供了最完整的解决方案,从提示词生成到解析验证全部自动化,错误信息极其友好,能大幅提升开发效率和代码健壮性。虽然它是一个较新的框架,但其背后的Pydantic团队保证了质量和可持续性。
  • 现有LangChain项目集成:继续使用LangChain PydanticOutputParser,并搭配OutputFixingParser来提高鲁棒性。迁移到PydanticAI可能带来不必要的重构成本。
  • 需要复杂流式交互:仔细评估需求。如果需要在token生成过程中就实时更新结构化数据,可能需要更底层的自定义实现,或者关注PydanticAI和LangChain在此方面的最新进展。

8. 进阶技巧与常见问题排查

无论选择哪种方案,在实际项目中都会遇到一些共性问题。这里分享几个进阶技巧和排查清单。

技巧一:给模型“举例子”(Few-Shot Prompting)对于特别复杂或容易出错的输出结构,在提示词中提供1-2个清晰的输入输出示例,能显著提升模型输出的准确率和一致性。这在所有方案中都适用。

# 在系统或用户提示词中加入示例 few_shot_prompt = """ 请根据用户反馈提取信息。输出必须是有效的JSON。 示例: 输入:“登录按钮点了没反应,已经试了三次了。” 输出:{{"summary": "用户报告登录按钮无响应", "sentiment": "negative", "categories": ["bug_report"], "urgency_score": 8, "mentioned_features": ["登录按钮"]}} 现在请分析以下反馈: {feedback_text} """

技巧二:设置更低的Temperature对于结构化输出任务,将temperature参数设置为0或接近0(如0.1),可以极大减少输出的随机性,提高格式稳定性。

技巧三:分而治之处理复杂输出如果单个输出模型非常复杂(例如包含多个嵌套列表和可选字段),可以考虑将其拆分为多个连续的子任务。先用一个LLM调用决定需要提取哪些部分,再分别调用其他LLM或使用同一个LLM分步提取。这比要求模型一次性生成一个庞大而完美的JSON成功率更高。

常见问题排查清单:

当你遇到解析失败时,可以按照以下步骤排查:

  1. 检查原始响应:无论用哪个方案,第一步永远是打印或记录LLM返回的原始响应内容。很多问题一看便知(比如多了Markdown代码块符号)。

    # 通用方法:在解析前打印 raw_content = response.choices[0].message.content print("原始响应:", raw_content)
  2. 验证JSON语法:将原始响应复制到一个在线JSON验证器(如jsonlint.com)中,检查是否是合法的JSON。如果不是,问题出在LLM没有遵守指令。

  3. 审查提示词指令:检查你发送给LLM的关于输出格式的指令是否清晰、无歧义。是否明确要求“输出纯JSON,不要有任何额外文本”?对于枚举字段,是否列出了所有可能的值?

  4. 简化模型测试:如果复杂模型失败,尝试创建一个仅包含1-2个简单字段(如summarysentiment)的临时Pydantic模型进行测试。如果简单模型能成功,说明问题出在复杂结构的描述或模型的理解能力上。

  5. 启用重试与降级:对于生产环境,永远不要假设一次调用就能成功。实现一个带有指数退避的重试机制。在多次重试失败后,要有降级策略,例如返回一个包含错误信息的默认对象,或者将任务转入人工审核队列。

  6. 关注Token使用:过长的格式指令会消耗上下文窗口。使用tiktoken等库估算一下你的提示词+格式指令的token数量,确保它不会挤占留给实际任务内容的空间。

一个典型的错误处理增强示例:

import tenacity from tenacity import retry, stop_after_attempt, wait_exponential @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10)) def robust_extract_with_pydantic_ai(text: str, max_tokens=500) -> ExtractedFeedback: """一个带有重试和降级的稳健提取函数""" try: result = await agent.run( f"请分析以下用户反馈:{text}", deps=client, model_settings={'max_tokens': max_tokens} # 控制输出长度 ) return result.data except Exception as e: print(f"解析失败,进行重试。错误: {e}") raise # 触发重试 # 重试耗尽后,Tenacity会抛出RetryError,需要在调用方处理 # 调用方 try: feedback = await robust_extract_with_pydantic_ai(test_user_post) except tenacity.RetryError: print("所有重试均失败,使用降级结果。") feedback = ExtractedFeedback( summary="[解析失败] " + test_user_post[:100], sentiment=Sentiment.NEUTRAL, categories=[FeedbackCategory.GENERAL], urgency_score=5 )

最终,选择哪种结构化输出方案,是技术决策,也是权衡。没有银弹,只有最适合你当前团队、技术栈和项目阶段的选择。我的个人体会是,在经历了无数次“JSON又炸了”的调试之后,像PydanticAI这样将类型安全贯穿始终的方案,带来的心智负担减轻和开发效率提升,对于严肃的长期项目而言,价值远超其学习成本和早期可能遇到的小麻烦。它让你能更专注于业务逻辑本身,而不是没完没了地处理数据格式的边角情况。

← 返回列表