LLM结构化输出:让AI返回JSON而不是废话,我踩了4个坑

写了很多AI应用了------RAG、Agent、GraphRAG......有一个问题一直困扰我:AI的输出太不可控了。

比如我让Agent查订单状态,期望它返回一个JSON:

json 复制代码
{"order_id": "ORD123", "status": "已发货", "eta": "2025-06-20"}

实际返回的是:

复制代码
根据查询,您的订单ORD123目前已经发货啦!预计2025年6月20日送达,请耐心等待哦~

一段废话,代码根本解析不了。 后面还要把订单状态写进数据库、触发通知------全废了,因为拿不到结构化数据。

这不是个例。RAG的回答是自然语言,Agent的观察是自然语言,GraphRAG的查询结果是自然语言......整个AI应用的上下游全是自然语言,只有我写的代码需要结构化数据。

让LLM稳定输出结构化数据(JSON、表格等),是AI应用从"能用"到"生产可用"的关键一步。我踩了4个坑才搞明白这件事。


先说结论

维度 自然语言输出 结构化输出
可解析性 ❌ 需要正则/后处理 ✅ 直接json.loads()
下游处理 ❌ 写一堆if-else猜格式 ✅ 字段确定,类型确定
可靠性 ❌ 每次格式可能不同 ✅ Schema约束,格式固定
调试难度 😫 看不懂AI在说什么 😎 JSON字段一目了然

用Java人的理解:自然语言输出 ≈ 返回Object,你不知道里面有什么,只能强转+判断;结构化输出 ≈ 返回OrderDTO,字段、类型全确定,直接用。

一句话:AI应用要上生产,结构化输出不是可选项,是必选项。


坑1:Prompt里写"请返回JSON",AI偏不配合

翻车现场

我一开始的方案很朴素------在Prompt里加一句"请以JSON格式输出":

ini 复制代码
from langchain_openai import ChatOpenAI
​
llm = ChatOpenAI(model="qwen-plus")
​
prompt = """请根据以下信息,返回订单状态的JSON。
​
订单信息:订单ORD123,已发货,顺丰SF123456,预计6月20日送达。
​
请以JSON格式输出,包含字段:order_id, status, tracking_number, eta。"""
​
result = llm.invoke(prompt)
print(result.content)

实际输出(10次运行,至少3种不同的"创意"格式):

json 复制代码
// 第1次:多了个解释
这是您的订单状态:
{
  "order_id": "ORD123",
  "status": "已发货",
  "tracking_number": "SF123456",
  "eta": "2025-06-20"
}
​
// 第2次:字段名不一致
{
  "orderId": "ORD123",
  "orderStatus": "已发货",
  "trackingNo": "SF123456",
  "estimatedDelivery": "2025-06-20"
}
​
// 第3次:嵌套结构
{
  "order": {
    "id": "ORD123",
    "current_status": "shipped"
  },
  "delivery": {
    "tracking": "SF123456",
    "eta": "2025-06-20"
  }
}
​
// 第4次:直接返回Markdown
```json
{
  "order_id": "ORD123",
  ...
}
python 复制代码
​
**4次输出,4种格式。** `json.loads()`能成功1次就算运气好。
​
### 根本原因
​
**Prompt不是合同,AI没有义务遵守。** "请返回JSON"只是请求,不是约束。LLM是语言模型,它擅长生成自然语言,不擅长严格遵守格式规范。
​
### 正确做法:用LLM的结构化输出功能(不要自己解析)
​
```python
from pydantic import BaseModel
from langchain_openai import ChatOpenAI
​
# 1. 用Pydantic定义输出Schema
class OrderStatus(BaseModel):
    order_id: str
    status: str
    tracking_number: str
    eta: str
​
# 2. 用with_structured_output让LLM自动遵守
llm = ChatOpenAI(model="qwen-plus")
structured_llm = llm.with_structured_output(OrderStatus)
​
# 3. 调用------返回的直接就是Pydantic对象
result = structured_llm.invoke(
    "订单ORD123,已发货,顺丰SF123456,预计6月20日送达。"
)
​
print(type(result))   # <class 'OrderStatus'>
print(result.order_id)    # ORD123
print(result.status)      # 已发货
print(result.eta)          # 2025-06-20

不需要自己解析JSON,不需要写正则,LLM直接返回Pydantic对象。 with_structured_output会自动:

  1. 把你的Schema转换成LLM能理解的约束
  2. 让LLM输出符合Schema的数据
  3. 自动解析成Pydantic对象

支持结构化输出的模型:

模型 支持方式 可靠性
通义千问 qwen-plus with_structured_output ⭐⭐⭐⭐⭐
OpenAI gpt-4o with_structured_output ⭐⭐⭐⭐⭐
Ollama qwen2.5 with_structured_output(部分支持) ⭐⭐⭐
Ollama llama3 不支持,需手动解析 ⭐⭐

坑2:复杂嵌套结构,AI字段填错或漏填

翻车现场

需求升级:订单查询不只返回状态,还要返回商品列表、收货地址等嵌套结构:

python 复制代码
from pydantic import BaseModel
from typing import Optional
​
class ProductItem(BaseModel):
    name: str
    quantity: int
    price: float
​
class ShippingAddress(BaseModel):
    province: str
    city: str
    detail: str
​
class OrderDetail(BaseModel):
    order_id: str
    status: str
    products: list[ProductItem]
    shipping_address: ShippingAddress
    tracking_number: Optional[str] = None  # 可选字段

测试时发现3个问题:

python 复制代码
问题1:quantity返回了字符串"2"而不是整数2
问题2:price返回了"约200元"而不是数字200.0
问题3:tracking_number为空时返回了"暂无"而不是None

嵌套越深,类型出错概率越高。 AI分不清"字符串2"和"整数2",也分不清"暂无"和null

正确做法:Schema里加字段描述 + Optional类型

ini 复制代码
from pydantic import BaseModel, Field
from typing import Optional
​
class ProductItem(BaseModel):
    name: str = Field(description="商品名称")
    quantity: int = Field(description="购买数量,必须为整数,如:2")
    price: float = Field(description="单价,必须为数字,如:199.9,不要带货币符号")
​
class ShippingAddress(BaseModel):
    province: str = Field(description="省份,如:浙江省")
    city: str = Field(description="城市,如:杭州市")
    detail: str = Field(description="详细地址")
​
class OrderDetail(BaseModel):
    order_id: str = Field(description="订单号,格式:ORD+数字")
    status: str = Field(description="订单状态:待支付/已支付/已发货/已完成/已取消")
    products: list[ProductItem] = Field(description="商品列表")
    shipping_address: ShippingAddress = Field(description="收货地址")
    tracking_number: Optional[str] = Field(
        default=None,
        description="物流单号,未发货时为null,不要填'暂无'等文字"
    )

关键改进:

改进 作用 示例
Field(description=...) 告诉AI每个字段应该填什么 "必须为整数,如:2"
Optional[str] 明确标记可选字段 tracking_number未发货时为None
default=None 给可选字段默认值 避免AI自己编一个值
description里给示例 AI照着示例填,出错率降50% "格式:ORD+数字"

用Java人的理解:Field(description=...) ≈ Swagger的@ApiModelProperty注释------不仅定义了类型,还说明了取值范围和格式。AI就是你的"自动填充器",描述越清楚填得越准。


坑3:AI返回了"合理"但"错误"的值------幻觉不是只发生在文本里

翻车现场

结构化输出解决了解析问题,但引入了一个新问题:AI会编造符合Schema但不符合事实的数据。

python 复制代码
class OrderStatus(BaseModel):
    order_id: str
    status: str
    tracking_number: Optional[str] = None
    eta: Optional[str] = None
​
result = structured_llm.invoke(
    "帮我查一下订单ORD999的状态"  # ORD999根本不存在
)
print(result)
# OrderStatus(order_id='ORD999', status='已取消', tracking_number='SF888999', eta='2025-06-25')

ORD999这个订单根本不存在,但AI没有报错,反而编造了一整套"看起来合理"的数据!

这比自然语言幻觉更危险------因为JSON格式是对的,下游代码能正常解析,错误数据就悄无声息地写进了数据库。

解决方案:在Schema里加"不确定"选项 + 后端校验

ini 复制代码
from enum import Enum
​
class QueryStatus(str, Enum):
    found = "found"           # 查到了
    not_found = "not_found"   # 没查到
    error = "error"           # 查询出错
​
class OrderQueryResult(BaseModel):
    """订单查询结果------包含查询状态,避免AI编造数据"""
    query_status: QueryStatus = Field(
        description="查询状态:found=查到订单, not_found=订单不存在, error=查询出错"
    )
    order_id: Optional[str] = Field(
        default=None,
        description="订单号,仅当query_status=found时填写"
    )
    status: Optional[str] = Field(
        default=None,
        description="订单状态,仅当query_status=found时填写"
    )
    message: str = Field(
        description="查询结果说明,如:'订单不存在'、'查询成功'、'系统繁忙请稍后重试'"
    )
​
# 测试:查不存在的订单
result = structured_llm.invoke("帮我查一下订单ORD999的状态")
# OrderQueryResult(query_status='not_found', order_id=None, status=None, message='订单ORD999不存在')
​
# 测试:查存在的订单
result = structured_llm.invoke("帮我查一下订单ORD123的状态")
# OrderQueryResult(query_status='found', order_id='ORD123', status='已发货', message='查询成功')

关键设计:

设计 作用
query_status枚举 强制AI先判断"查没查到",而不是直接编数据
可选字段+default=None 没查到时字段为空,不会编造
message字段 AI可以用自然语言解释,但结构化数据不含幻觉

后端校验同样不能省:

python 复制代码
def process_order_query(result: OrderQueryResult):
    """后端处理:结构化输出也要校验"""
    if result.query_status == QueryStatus.found:
        # 二次校验:真的去数据库查一下这个订单是否存在
        db_order = db.get_order(result.order_id)
        if not db_order:
            return {"error": "AI返回了订单数据,但数据库中不存在该订单"}
​
        # 校验AI返回的状态是否和数据库一致
        if result.status != db_order.status:
            return {"error": f"AI返回状态为{result.status},但数据库为{db_order.status}"}
​
        return {"order": result}
​
    elif result.query_status == QueryStatus.not_found:
        return {"message": result.message}
​
    else:
        return {"error": result.message}

结构化输出的幻觉比自然语言幻觉更危险,因为它更隐蔽。 自然语言幻觉你一眼就能看出来不对劲,JSON幻觉格式正确、字段齐全、值看起来合理------你根本不会怀疑。

用Java人的理解:前端传过来的参数你一定会校验,对吧?AI的结构化输出也一样------永远不要信任AI返回的数据,即使格式是对的。 该做的数据库校验一个都不能少。


坑4:流式输出 + 结构化输出,鱼和熊掌怎么兼得

翻车现场

产品提了个需求: "AI回复的时候能不能像ChatGPT一样一个字一个字蹦出来?但数据还得是JSON格式的。"

问题来了:

  • 流式输出:LLM一个token一个token返回,输出不完整时JSON无法解析
  • 结构化输出:必须等LLM输出完整JSON才能解析

两个需求天然矛盾。

解决方案:分离"展示内容"和"结构化数据"

python 复制代码
from pydantic import BaseModel, Field
from typing import Optional
​
class StreamResponse(BaseModel):
    """流式响应:展示内容+结构化数据分离"""
    display_text: str = Field(
        description="给用户看的自然语言回复,可以包含emoji和格式化"
    )
    data: Optional[dict] = Field(
        default=None,
        description="结构化数据,供程序处理。如果本次回复没有需要程序处理的数据,则为null"
    )
​
# 使用:先流式输出展示内容,最后输出结构化数据
async def stream_chat(question: str):
    """流式聊天:自然语言实时流式,结构化数据最后返回"""
​
    # 第一步:流式输出自然语言回复
    stream = llm.astream(
        f"用户问题:{question}\n请先给出自然的文字回复。"
    )
​
    display_text = ""
    async for chunk in stream:
        token = chunk.content
        display_text += token
        yield {"type": "text", "content": token}  # 前端实时显示
​
    # 第二步:流式结束后,输出结构化数据
    structured_result = structured_llm.invoke(
        f"基于刚才的回复,提取结构化数据。\n用户问题:{question}\n回复内容:{display_text}"
    )
    yield {"type": "data", "content": structured_result.model_dump()}

前端处理:

ini 复制代码
// 前端代码示例
const eventSource = new EventSource('/api/chat/stream');
​
eventSource.onmessage = (event) => {
    const msg = JSON.parse(event.data);
​
    if (msg.type === 'text') {
        // 实时显示文字
        appendToChat(msg.content);
    } else if (msg.type === 'data') {
        // 结构化数据,触发业务逻辑
        handleStructuredData(msg.content);
    }
};

效果:

css 复制代码
用户:查一下订单ORD123到哪了
​
AI(逐字显示):📦 您的订单ORD123已发货!
顺丰快递单号SF123456,预计6月20日送达。
​
[同时,后端收到结构化数据]
{
  "display_text": "📦 您的订单ORD123已发货!...",
  "data": {
    "order_id": "ORD123",
    "status": "已发货",
    "tracking_number": "SF123456",
    "eta": "2025-06-20"
  }
}

展示用自然语言(流式体验好),处理用结构化数据(可靠可控)。 两者分离,各取所长。

方案 流式体验 数据可靠性 适用场景
纯自然语言流式 ❌ 难解析 纯聊天场景
纯结构化输出 ❌ 等完整输出 批处理/API场景
分离式 推荐:生产级AI应用

用Java人的理解:这和前后端分离一个道理------HTML给用户看,JSON给程序处理。AI输出也可以分离:自然语言给前端展示,结构化数据给后端处理。


完整代码:结构化输出的订单查询AI

python 复制代码
"""
LLM结构化输出:订单查询AI
依赖:pip install langchain langchain-openai pydantic
"""
from enum import Enum
from pydantic import BaseModel, Field
from typing import Optional
from langchain_openai import ChatOpenAI
​
# ============ Schema定义 ============
​
class QueryStatus(str, Enum):
    found = "found"
    not_found = "not_found"
    error = "error"
​
class ProductItem(BaseModel):
    name: str = Field(description="商品名称")
    quantity: int = Field(description="购买数量,必须为整数")
    price: float = Field(description="单价,数字,不带货币符号")
​
class OrderDetail(BaseModel):
    order_id: str = Field(description="订单号")
    status: str = Field(description="订单状态:待支付/已支付/已发货/已完成/已取消")
    products: list[ProductItem] = Field(description="商品列表")
    tracking_number: Optional[str] = Field(default=None, description="物流单号,未发货为null")
    eta: Optional[str] = Field(default=None, description="预计到达时间,未发货为null")
​
class OrderQueryResult(BaseModel):
    """带查询状态的结果------避免AI编造数据"""
    query_status: QueryStatus = Field(
        description="查询状态:found/not_found/error"
    )
    order: Optional[OrderDetail] = Field(
        default=None,
        description="订单详情,仅query_status=found时有值"
    )
    message: str = Field(description="查询结果说明")
​
# ============ 使用 ============
​
llm = ChatOpenAI(model="qwen-plus")
structured_llm = llm.with_structured_output(OrderQueryResult)
​
def query_order(question: str) -> dict:
    """查询订单,返回结构化结果"""
    result = structured_llm.invoke(
        f"用户问题:{question}\n\n"
        f"已知数据:\n"
        f"- 订单ORD123:已发货,含2件商品(iPhone 16 x1 单价6999、AirPods x1 单价1299),"
        f"顺丰SF123456,预计6月20日送达\n"
        f"- 订单ORD456:待支付,含1件商品(MacBook Pro x1 单价12999),未发货\n"
        f"- 其他订单号均不存在"
    )
​
    # 后端校验
    if result.query_status == QueryStatus.found and result.order:
        # 实际项目:查数据库确认
        print(f"✅ 查到订单 {result.order.order_id}")
        print(f"   状态:{result.order.status}")
        print(f"   商品:{[p.name for p in result.order.products]}")
        print(f"   物流:{result.order.tracking_number}")
        return result.order.model_dump()
​
    elif result.query_status == QueryStatus.not_found:
        print(f"❌ 订单不存在:{result.message}")
        return {"error": result.message}
​
    else:
        print(f"⚠️ 查询出错:{result.message}")
        return {"error": result.message}
​
​
# ============ 测试 ============
​
if __name__ == "__main__":
    print("--- 测试1:查存在的订单 ---")
    query_order("ORD123到哪了?")
​
    print("\n--- 测试2:查不存在的订单 ---")
    query_order("帮我查订单ORD999")
​
    print("\n--- 测试3:查待支付的订单 ---")
    query_order("ORD456发货了吗?")

4个坑的总结

# 错误做法 正确做法 一句话
1 Prompt写"返回JSON"不可靠 自己写Prompt+手动解析 with_structured_output + Pydantic 别自己解析,用LLM的结构化输出能力
2 嵌套结构字段填错 Schema只写类型 Field(description=...) + 示例值 description是给AI看的接口文档
3 AI编造"合理"的错误数据 只看格式不校验内容 查询状态枚举 + 后端二次校验 结构化幻觉比自然语言幻觉更危险
4 流式输出和结构化输出矛盾 只选一个 分离展示内容和结构化数据 自然语言给前端,结构化给后端

Pydantic Schema设计的4条铁律

铁律 说明 反例
每个字段加description 告诉AI填什么、格式是什么 只有类型没有描述
可选字段用Optional+default AI不会编造不存在的值 必填但AI不知道填什么
用枚举限制取值范围 status只能从几个选项中选 status是自由文本,AI可能编造新状态
加查询状态字段 先判断"有没有"再填数据 AI不管有没有都编一整套数据

这些铁律的本质:把"信任AI"变成"约束AI"。 Java开发天然理解这个道理------你不会信任前端传来的参数不加校验就写库,对AI的结构化输出也一样。


从自然语言到结构化的演进

阶段 做法 适用场景
阶段1:Prompt里写"请返回JSON" 自己解析+正则 Demo、快速验证
阶段2:with_structured_output Pydantic自动解析 大多数生产场景
阶段3:+查询状态+后端校验 防幻觉+防编造 高可靠性场景
阶段4:展示/数据分离 流式+结构化兼得 用户交互场景

我的建议:直接从阶段2开始。 with_structured_output零成本接入,不用自己解析JSON。阶段3按需加,阶段4看产品需求。


你让AI返回结构化数据踩过什么坑?评论区聊聊 👇

相关推荐
Agent_大师2 小时前
WebSocket 行情重连成功,K线缺口不会自动消失
python
copyer_xyf2 小时前
FastAPI 如何连接 MySQL
后端·python
plainGeekDev4 小时前
Gson → kotlinx.serialization
android·java·kotlin
小bo波12 小时前
Java Swing 图形用户界面实验 —— 从算术练习到游戏开发的完整实践
java·课程设计·gui·游戏开发·扫雷·swing
咖啡八杯14 小时前
GoF设计模式——备忘录模式
java·后端·spring·设计模式
apocelipes16 小时前
常用编程语言和库的正则表达式性能对比
c语言·c++·python·性能优化·golang·开发工具和环境
用户83562907805117 小时前
使用 Python 在 PDF 中创建与管理书签
后端·python