尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

LLM结构化输出实战:从JSON Schema到高可靠Agent开发

LLM结构化输出实战:从JSON Schema到高可靠Agent开发 1. 项目概述从“自由发挥”到“精准交付”在LLM应用开发的深水区我们常常面临一个尴尬的局面模型的理解能力超群但输出的内容却像一匹脱缰的野马难以被下游程序精准捕获和处理。你满怀期待地向一个强大的语言模型提问“请列出当前用户张三的订单详情包括订单号、商品名称、数量和状态。” 模型可能会给你一段近乎完美的自然语言描述“用户张三目前有一个待发货的订单订单号是20240315001包含一件‘智能咖啡机’数量为1。” 对人类来说这清晰明了但对你的程序来说要从中准确提取出“订单号20240315001”、“状态待发货”这些字段你需要额外编写一套复杂且脆弱的正则表达式或文本解析逻辑稍有不慎就会因为模型换了一种说法比如“订单编号为20240315001”、“状态显示为等待发货”而导致解析失败。这正是“Agent结构化输出”要解决的核心痛点。它的目标不是限制模型的创造力而是为模型的创造力套上一个精准的“接口”。简单说就是强制要求LLM按照开发者预先定义好的JSON Schema数据模式来组织它的回答。这样一来模型的输出不再是自由、多变的自然文本而是变成了一个结构清晰、键值分明、类型严格的数据对象。下游的程序可以直接像调用一个函数、解析一个API响应那样使用这个JSON对象彻底告别文本解析的泥潭。这不仅仅是格式上的转变更是LLM从“聊天伙伴”升级为“可靠数据生产者”的关键一步是构建复杂、自动化AI工作流Agent的基石。2. 核心需求与价值解析为什么JSON Schema是刚需2.1 从自由文本到结构化数据的范式转变在传统的LLM使用中我们采用的是“提问-回答”的对话范式。这种范式下输出是开放的、非结构化的其价值在于信息的呈现和人类的阅读。然而当LLM需要与软件系统、数据库、其他API进行交互时这种开放性就成了障碍。结构化输出要求我们切换到“查询-数据”的范式将LLM视为一个能够理解复杂查询并返回标准化数据记录的“智能查询引擎”。这种转变带来了几个根本性的优势确定性输出的结构是预先可知的程序可以基于Schema进行静态类型检查甚至在运行前就能发现潜在的数据访问错误。可编程性JSON作为几乎全栈通用的数据交换格式可以被任何现代编程语言轻松反序列化为对象、字典或映射无缝集成到现有代码逻辑中。可验证性我们可以编写校验逻辑确保返回的数据不仅结构正确其内容如数值范围、字符串格式、枚举值也符合业务规则。2.2 驱动结构化输出的三大核心场景场景一AI Agent与工具调用Function Calling这是最经典和迫切的需求。一个AI Agent需要根据用户指令“查一下北京明天下午的天气然后如果下雨就提醒我带伞”去调用“查询天气”的工具。这个工具需要一个结构化的输入比如{“city”: “北京” “date”: “2024-03-16”}。如果让LLM自由描述它可能会说“帮我查询北京明天下午的天气情况”这需要额外的NLU模块来理解。而通过结构化输出我们可以直接定义Schema让LLM生成符合工具调用规范的参数对象实现“思考”与“执行”的无缝衔接。场景二数据抽取与信息标准化从冗长的合同文本、研究论文或客服对话记录中抽取公司名、金额、日期、责任条款等特定信息。自由文本输出可能需要人工复核而结构化输出可以强制模型将找到的每个实体归类到预设的字段中并以JSON数组等形式返回方便直接导入数据库或分析系统。场景三复杂决策与分类用户输入一段产品反馈我们需要模型同时判断情感倾向积极/消极/中立、问题类别功能/性能/售后、紧急程度高/中/低。自由文本可能需要解析多句话而结构化输出可以要求模型一次性返回一个如{“sentiment”: “negative” “category”: “performance” “urgency”: “high”}的对象极大简化后续的工单路由或预警逻辑。注意结构化输出并不意味着剥夺LLM的推理能力。恰恰相反它要求LLM在生成答案前先进行一轮“格式化思考”将内部推理过程对齐到外部数据结构上。这通常能促使模型进行更严谨的逻辑组织。3. 技术实现方案深度对比实现LLM的结构化输出并非只有一条路。不同的方案在实现难度、可靠性、成本和对模型的依赖程度上各有优劣。理解这些方案的内在机理是做出正确技术选型的前提。3.1 方案一提示词工程Prompt Engineering—— 轻量但脆弱这是最直观、门槛最低的方法。核心思想是在给模型的系统提示System Prompt或用户消息中清晰地用自然语言描述你期望的JSON格式。典型提示词示例你是一个数据提取助手。请始终以严格的JSON格式回复且只包含JSON不要有任何其他解释。 JSON必须包含以下字段 - “order_id”: (字符串 订单编号) - “items”: (数组 每个元素是一个对象包含 “name” (商品名) 和 “quantity” (数量)) - “total_amount”: (数字 总金额) - “status”: (字符串 必须是 “pending” “shipped” “delivered” 中的一个) 用户查询{user_query}优点零成本、通用性强无需调用特殊接口或依赖特定模型版本任何支持文本生成的LLM都可以尝试。快速原型验证在项目初期验证想法时非常高效。致命缺点与“坑”输出不稳定模型可能会在JSON前后添加解释性文字如“好的这是你要的JSON”破坏纯净的JSON结构导致解析失败。格式漂移即使模型输出了纯JSON也可能在细微处不符合规范例如使用单引号而非双引号在最后一个数组元素后多加一个逗号或者键名没有用引号括起来虽然JSON5允许但标准JSON解析器会报错。Schema遵守不严格模型可能忽略对枚举值status、数据类型total_amount应是数字而非字符串的约束。上下文浪费复杂的Schema描述会占用大量Tokens挤占本应用于理解任务和内容的上下文空间。实操心得提示词工程可以作为一个起点但绝不能用于生产环境的核心链路。它更适合对输出格式错误有一定容忍度的场景或者作为其他更可靠方案的“第一道请求过滤器”。3.2 方案二函数调用Function Calling与工具使用Tool Use—— 主流选择这是目前各大主流模型平台OpenAI GPT, Anthropic Claude, Google Gemini等官方主推且最成熟的方式。它不再是简单的文本指令而是模型API提供的一个原生功能。工作原理开发者向模型API发送请求时除了常规的对话消息还会附加一个tools或functions参数。这个参数是一个列表详细定义了可供模型调用的“工具”每个工具都有名称、描述和严格的parameters参数Schema这个Schema就是一个标准的JSON Schema对象。当模型认为需要调用某个工具来回答用户问题时它不会在常规的聊天内容中输出文本而是会在响应中返回一个特殊的结构指明它“想要调用”哪个工具以及调用这个工具时传入的、已经结构化好的参数。这个参数对象就是我们需要的结果。OpenAI API调用示例伪代码response client.chat.completions.create( modelgpt-4, messages[{role: user, content: “列出张三最近的订单”}], tools[{ “type”: “function” “function”: { “name”: “get_order_details” “description”: “获取用户的订单详情” “parameters”: { “type”: “object” “properties”: { “user_name”: {“type”: “string”} “max_results”: {“type”: “integer” “description”: “最多返回的订单数”} } “required”: [“user_name”] } } }] ) # 模型的响应中会包含一个 tool_calls 字段其中就有结构化的参数。优点高可靠性这是模型被专门训练过的能力输出严格遵守提供的Schema格式错误率极低。意图识别与结构化输出合一模型会自主判断是否需要调用工具以及调用哪一个将“理解用户意图”和“生成结构化参数”两个步骤合并更智能。生态完善LangChain、LlamaIndex等主流框架对其有深度集成开发便捷。注意事项并非所有模型都支持需要确认你使用的模型版本是否具备此功能。成本略高输入Tokens会因包含Schema而增加且通常调用此功能的模型本身定价也可能更高。思维链被隐藏模型直接给出结构化结果我们看不到它得出这个结果的推理过程不利于调试复杂任务。3.3 方案三输出结构化JSON Mode与语法约束Grammar—— 专精之道这是比函数调用更“纯粹”的结构化输出方案。它不涉及“工具调用”这个概念直接告诉模型“请用这个具体的JSON格式回答我”。OpenAI的JSON Mode在API调用中设置response_format{“type”: “json_object”}并必须在系统提示中明确要求模型输出JSON。这能极大提高模型输出纯JSON的概率但它不强制约束JSON的内部结构Schema模型输出的字段可能和预期不完全一致。Llama.cpp的Grammar约束这是一个更底层、更强大的功能。它允许你定义一个上下文无关文法例如使用GBNF格式来严格限定模型输出文本的每一个可能字符。你可以用这个文法来定义一个完整的JSON Schema强制模型生成的每一个字符都符合该Schema从而得到100%语法正确且结构匹配的JSON。本地部署的Llama系列模型常通过此方式实现极致可控的输出。优点极致可控Grammar从语法层面保证输出格式理论上可以实现零格式错误。直接了当对于不需要“工具调用”这一抽象层只需要固定格式输出的场景这种方式更直观。缺点使用复杂Grammar定义GBNF文法有学习成本且不是所有推理框架都支持。灵活性差Schema一旦定义难以在运行时动态改变。可能影响内容质量过于严格的格式限制有时会干扰模型组织语言内容的最佳方式。3.4 方案四后处理与“自我修正”Self-Correction—— 安全网无论采用哪种方案一个健壮的系统都应该有后处理层作为安全网。思路是尝试解析模型输出如果解析失败非JSON、格式错误则自动触发一个“修正”流程。实现方式捕获JSON解析异常然后将原始用户问题、模型出错的输出、期望的JSON Schema以及解析错误信息一并作为新的提示发送给模型通常是同一个模型也可以是更小、更快的模型要求它分析错误并输出修正后的、正确的JSON。示例修正提示你之前试图生成一个JSON但失败了。错误信息是Expecting property name enclosed in double quotes: line 1 column 2 (char 1) 请根据原始问题和以下Schema修正你的输出。 原始问题“列出张三的订单” 期望的Schema{...} // 这里放入详细的JSON Schema 你之前错误的输出{order_id: ‘123’ status: shipped} 请输出修正后的、符合Schema和JSON语法的纯净JSON价值这相当于给系统加了一道保险能挽回大部分因模型偶然“走神”导致的格式错误显著提升整体成功率。但它增加了额外的API调用和延迟应作为容错机制而非主要手段。4. 实战构建一个高可靠的订单信息提取Agent让我们结合一个具体案例将上述方案融会贯通。目标是构建一个Agent它能从用户随意的自然语言描述中提取出标准化的订单信息。4.1 步骤一定义业务数据Schema这是所有工作的基石。Schema的设计要精准反映业务需求并考虑扩展性。{ “$schema”: “http://json-schema.org/draft-07/schema#” “title”: “OrderInfo” “type”: “object” “properties”: { “order_id”: { “type”: “string” “description”: “平台订单编号通常由字母和数字组成” } “customer_name”: { “type”: “string” “description”: “收货人姓名” } “items”: { “type”: “array” “description”: “订单商品列表” “items”: { “type”: “object” “properties”: { “product_name”: { “type”: “string” } “sku_code”: { “type”: “string” } “quantity”: { “type”: “integer” “minimum”: 1 } “unit_price”: { “type”: “number” “minimum”: 0 } } “required”: [“product_name” “quantity”] } “minItems”: 1 } “total_price”: { “type”: “number” “minimum”: 0 “description”: “订单总价单位元” } “status”: { “type”: “string” “enum”: [“pending” “paid” “shipped” “delivered” “cancelled”] “description”: “订单当前状态” } “order_time”: { “type”: “string” “format”: “date-time” “description”: “下单时间ISO 8601格式” } } “required”: [“order_id” “items” “total_price” “status”] }4.2 步骤二选择与实现核心输出策略对于生产环境我们选择“函数调用”作为核心策略因为它提供了最佳的可靠性和智能意图识别的平衡。使用LangChain实现from langchain_openai import ChatOpenAI from langchain_core.pydantic_v1 import BaseModel Field from typing import List # 1. 使用Pydantic定义结构化模型LangChain会将其自动转换为OpenAI的tools schema。 class OrderItem(BaseModel): product_name: str Field(description“商品名称”) sku_code: str Field(None description“商品SKU编码可能没有”) quantity: int Field(… gt0 description“购买数量”) unit_price: float Field(None ge0 description“商品单价”) class OrderInfo(BaseModel): order_id: str Field(description“订单编号”) customer_name: str Field(None description“客户姓名”) items: List[OrderItem] Field(description“商品列表”) total_price: float Field(… ge0 description“订单总价”) status: str Field(description“订单状态” enum[“pending” “paid” “shipped” “delivered” “cancelled”]) order_time: str Field(None description“下单时间”) # 2. 创建模型并绑定结构化输出。 llm ChatOpenAI(model“gpt-4-turbo-preview”) structured_llm llm.with_structured_output(OrderInfo) # 3. 调用并获取结构化对象。 user_query “我昨天用名字‘张三’下的那个订单订单号好像是E20240315001买了两箱牛奶每箱50块总共100现在显示已发货了。” result: OrderInfo structured_llm.invoke(user_query) print(result.order_id) # 输出E20240315001 print(result.status) # 输出shipped print(result.items[0].quantity) # 输出2 # result 就是一个 OrderInfo 类的实例可以直接使用其属性。4.3 步骤三设计分层容错与后处理流程即使使用函数调用我们也要设计健壮的流程。import json import logging from tenacity import retry stop_after_attempt retry_if_exception_type class OrderExtractionAgent: def __init__(self llm): self.primary_llm llm.with_structured_output(OrderInfo) # 可以准备一个更小、更快的模型用于修正 self.correction_llm llm retry(stopstop_after_attempt(2) retryretry_if_exception_type((json.JSONDecodeError ValidationError))) def extract(self user_query: str) - OrderInfo: “““主提取流程包含重试和修正””” try: # 第一尝试使用主模型和结构化输出 return self.primary_llm.invoke(user_query) except (json.JSONDecodeError ValidationError) as e: # 捕获JSON解析或数据验证错误 logging.warning(f“首次解析失败: {e} 尝试修正。”) # 进入修正流程 return self._correct_and_extract(user_query str(e)) def _correct_and_extract(self user_query: str error_msg: str) - OrderInfo: “““修正流程将错误信息反馈给模型要求其重新生成正确JSON””” correction_prompt f“““ 你之前尝试根据用户查询生成一个‘OrderInfo’格式的JSON但失败了。 错误信息是{error_msg} 请严格遵循以下JSON Schema定义重新生成正确的答案。 只输出纯净的JSON不要有任何其他文字。 Schema定义 {OrderInfo.schema_json(indent2)} 用户查询{user_query} 正确的JSON输出 “““ corrected_response self.correction_llm.invoke(correction_prompt) # 假设修正模型返回的是文本我们需要解析 # 在实际中修正模型也可以使用结构化输出这里演示通用文本处理 try: data json.loads(corrected_response.content) return OrderInfo(**data) except Exception as final_e: logging.error(f“修正后仍然失败: {final_e}”) # 如果还是失败可以返回一个默认的错误结构或者向上抛出异常 raise RuntimeError(“无法从用户输入中提取有效的订单信息。”) from final_e # 使用Agent agent OrderExtractionAgent(llm) try: order agent.extract(“我的订单号12345还没发货吗”) print(f“订单状态: {order.status}”) except RuntimeError as e: print(f“提取失败: {e}”)5. 常见陷阱、调试技巧与性能优化在实际部署中你会遇到各种各样的问题。以下是一些从实战中总结的经验。5.1 典型问题与排查清单问题现象可能原因排查步骤与解决方案模型返回了文本而非工具调用。1. 系统提示词未清晰要求使用工具。2. 工具描述与用户问题不匹配模型认为无需调用。3. 模型能力不足。1. 在系统提示中强调“请使用提供的工具”。2. 优化工具的名称和描述使其更贴近业务场景。3. 尝试更换更强或更新版本的模型。工具调用参数类型错误如数字传成了字符串。Schema定义不够严格或清晰。1. 在Schema的description字段中明确类型如“必须是一个整数”。2. 使用Pydantic等库在后端进行强制类型验证和转换。模型“臆造”了Schema中不存在的字段。1. 提示词中包含了示例示例中有额外字段。2. 模型过度推理添加了它认为有用的信息。1. 清理提示词确保示例与Schema完全一致。2. 在系统提示中明确强调“仅输出Schema中定义的字段”。3. 使用JSON Mode或Grammar进行更严格的格式限制。对于模糊或信息不全的查询模型返回空值或错误值。模型在不确定时进行了“猜测”。1. 在Schema中为字段设置nullable: true或使用可选类型。2. 在业务逻辑中处理None值而不是完全依赖模型。3. 设计多轮对话让Agent主动询问缺失信息如“请问您的订单号是多少”。处理长文本或复杂Schema时性能下降、速度慢。1. 输入Tokens过多。2. 模型需要更长的时间“思考”复杂结构。1.精简Schema只保留最必要的字段。将复杂嵌套结构拆分成多个简单的工具调用。2.预处理输入先使用一个快速模型或规则提取相关段落再交给大模型做精确结构化。3.使用流式输出如果API支持使用流式响应以降低感知延迟。5.2 高级技巧提升输出质量与稳定性提供少量示例Few-Shot在系统提示中除了Schema提供1-2个高质量的输入输出示例。这能极大地引导模型理解你的具体格式和内容期望。示例必须是Schema的完美体现。温度Temperature参数调优对于需要高确定性的结构化输出任务将温度设置为0或一个很低的值如0.1以减少输出的随机性。Schema设计的艺术优先使用枚举enum对于状态、类型等字段尽可能使用枚举列表。这比让模型自由发挥一个字符串要可靠得多。描述description字段至关重要用清晰、无歧义的自然语言描述每个字段的含义和规则。这是模型理解你意图的主要来源。分层与复用对于复杂的结构定义子Schema并复用。这使提示更清晰也便于后续维护。验证与清洗管道永远不要100%信任模型的输出。在将结构化数据送入核心业务逻辑前建立一道验证管道。使用如jsonschema库进行模式验证使用pydantic进行数据解析和清洗如字符串修剪、默认值填充。这能拦截大部分潜在的数据质量问题。5.3 成本与延迟优化考量结构化输出尤其是函数调用会增加输入Tokens因为要传输Schema。在规模化应用时成本不容忽视。缓存Schema描述如果你的应用频繁使用固定的几个Schema可以考虑在客户端或服务端缓存转换好的工具定义字符串避免每次请求都重复生成。使用更高效的模型对于格式相对简单的任务可以尝试使用如gpt-3.5-turbo而非gpt-4进行结构化输出在效果可接受的前提下大幅降低成本。异步与批处理对于非实时性要求极高的场景可以将多个用户请求队列化进行批量的结构化提取处理摊薄每次调用的开销。让LLM返回精准的JSON本质上是将人类模糊的自然语言指令与计算机精确的结构化需求进行对齐。这个过程没有银弹需要根据你的具体场景在提示词工程、函数调用、语法约束和后处理之间找到最佳平衡点。从我个人的经验来看对于大多数严肃的生产应用以官方函数调用能力为核心辅以清晰的Schema设计、严谨的后端验证和巧妙的容错机制是当前最稳健、最高效的路径。它让LLM真正从一个“聪明的聊天者”变成了一个“可靠的数据流水线工人”为构建复杂、自动化的智能体Agent应用铺平了道路。
返回列表