个人主页: > for_ever_love__ <(欢迎各位大佬莅临😊)
其他栏目: > 大模型开发从0到1 <
其他栏目: > iOS项目总结大全 <
其他栏目: > 我想学python了 <
其他栏目: > iOS UI <
文章目录
- [LangChain 结构化输出:让模型稳定吐出符合 schema 的 JSON](#LangChain 结构化输出:让模型稳定吐出符合 schema 的 JSON)
-
- 一、先看看不用它会怎样
- [三、三种 schema 定义方式](#三、三种 schema 定义方式)
-
- [3.1 Pydantic(最常用)](#3.1 Pydantic(最常用))
- [3.2 TypedDict(我个人更推荐)](#3.2 TypedDict(我个人更推荐))
- [3.3 JSON Schema(跨语言场景)](#3.3 JSON Schema(跨语言场景))
- [四、method 参数:三种实现机制](#四、method 参数:三种实现机制)
- 五、include_raw:调试救命参数
- [六、接进 LCEL 链](#六、接进 LCEL 链)
-
- [6.1 抽取多条:包一层列表](#6.1 抽取多条:包一层列表)
- [6.2 可选字段:让模型能说"不知道"](#6.2 可选字段:让模型能说"不知道")
- 七、实战:从合同文本抽结构化字段
- 八、常见坑
- 九、小结
LangChain 结构化输出:让模型稳定吐出符合 schema 的 JSON
让模型"返回 JSON"听起来简单,做起来全是坑:
有时候包在 ```json 代码块里,有时候键名对不上,
有时候多一段解释文字,有时候干脆少一个字段。
这篇讲
with_structured_output怎么用、三种模式怎么选,以及为什么我推荐 TypedDict 而不是 Pydantic。
一、先看看不用它会怎样
最常见的"土办法":
python
prompt = "请分析下面这句话的情感,返回 JSON:{'sentiment': ..., 'score': ...}\n文本:xxx"
resp = llm.invoke(prompt)
print(resp.content)
你会得到各种意外:
好的,分析结果如下:
```json
{"sentiment": "positive", "score": 0.9}
希望对你有帮助!
或者:
{"情感": "正面", "分数": 0.9} # 键名被翻译了
或者:
{"sentiment": "positive"} # 少了一个字段
然后你得写一堆正则、容错、重试。**这些麻烦 `with_structured_output` 全帮你处理了。**
## 二、上手
```python
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
class Sentiment(BaseModel):
"""情感分析结果"""
sentiment: str = Field(description="情感倾向:positive / negative / neutral")
score: float = Field(description="置信度,0~1 之间", ge=0, le=1)
keywords: list[str] = Field(description="体现情感倾向的关键词")
llm = ChatOpenAI(model="gpt-4o-mini")
structured_llm = llm.with_structured_output(Sentiment)
result = structured_llm.invoke("这家餐厅的牛排太好吃了,就是上菜有点慢")
print(type(result)) # <class 'Sentiment'>
print(result.sentiment) # positive
print(result.score) # 0.85
print(result.keywords) # ['牛排好吃', '上菜慢']
返回的直接是 Python 对象 ,不是字符串。类型安全,IDE 能补全,
下游代码可以直接 .score 用,不用再 json.loads。
三、三种 schema 定义方式
3.1 Pydantic(最常用)
python
from pydantic import BaseModel, Field
class Product(BaseModel):
name: str = Field(description="商品名称")
price: float = Field(description="单价(元)")
tags: list[str] = Field(description="标签列表", default_factory=list)
优点:自带校验 。定义了 ge=0, le=1,模型传 1.5 会抛校验错误,
你能立刻发现而不是等到线上。
3.2 TypedDict(我个人更推荐)
python
from typing_extensions import TypedDict, Annotated
class Product(TypedDict):
"""商品信息"""
name: Annotated[str, ..., "商品名称"]
price: Annotated[float, ..., "单价(元)"]
tags: Annotated[list[str], ..., "标签列表"]
为什么不直接用 Pydantic?
因为 Pydantic 的 BaseModel 有个"顺序陷阱":
想在描述里引用前面定义的字段,在某些写法下会出问题;
而且 Pydantic 的校验错误有时会把模型输出的小瑕疵放大成硬失败。
TypedDict 是纯类型声明,没有运行时校验,也就不会因为一点小偏差直接报错 ,
配合 Annotated 的三段式(类型, 默认值/..., 描述)语义非常清晰。
什么时候必须用 Pydantic? 需要运行时校验、需要嵌套复杂约束、
或者下游要复用这个 model 做序列化时。其余场景 TypedDict 更轻。
3.3 JSON Schema(跨语言场景)
python
schema = {
"title": "Product",
"type": "object",
"properties": {
"name": {"type": "string", "description": "商品名称"},
"price": {"type": "number", "description": "单价(元)"},
"tags": {"type": "array", "items": {"type": "string"}, "description": "标签"},
},
"required": ["name", "price"],
}
structured_llm = llm.with_structured_output(schema)
适合 schema 存在配置文件里、或由其他系统生成的场景。
四、method 参数:三种实现机制
with_structured_output(schema, method=...) 是关键参数。
| method | 机制 | 支持度 | 特点 |
|---|---|---|---|
function_calling |
用 tool calling 模拟 | OpenAI、大部分主流模型 | 最通用 |
json_schema |
模型原生的结构化输出 | OpenAI 新模型 | 最严格,保证 100% 符合 schema |
json_mode |
只强制输出合法 JSON | 部分模型 | 只保证是 JSON,不保证符合 schema |
python
# 默认会自动选,也可以显式指定
structured_llm = llm.with_structured_output(Product, method="function_calling")
怎么选?
- 模型支持原生
json_schema(如 gpt-4o 系列)→ 用它,最稳; - 不支持 →
function_calling,兼容性最好; - 只有在前两者都不可用时才用
json_mode,它只保证"是合法 JSON",
字段对不对它不管,你还得自己校验。
判断方法:直接试试,报错就换。
五、include_raw:调试救命参数
python
structured_llm = llm.with_structured_output(Product, include_raw=True)
result = structured_llm.invoke("iPhone 15 售价 5999 元")
print(result)
# {
# 'raw': AIMessage(content='', tool_calls=[...]),
# 'parsed': Product(name='iPhone 15', price=5999.0, tags=[]),
# 'parsing_error': None
# }
解析失败时 parsed 是 None、parsing_error 有内容 ,
而 raw 保留模型原始输出。
排查"为什么又解析失败了"的时候,这个参数能省你半小时。
生产环境建议先开着跑一段时间 ,收集几次 parsing_error,
确认稳定后再关掉。
六、接进 LCEL 链
结构化输出返回的还是 Runnable,可以直接链起来:
python
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", "你是信息抽取助手。从文本中抽取商品信息,不要编造。"),
("human", "{text}"),
])
chain = prompt | llm.with_structured_output(Product)
result = chain.invoke({"text": "这款 MacBook Pro 16G+512G 售价 12999 元"})
print(result.name, result.price)
6.1 抽取多条:包一层列表
python
from pydantic import BaseModel, Field
class ProductList(BaseModel):
"""商品列表"""
products: list[Product] = Field(description="抽取到的所有商品")
chain = prompt | llm.with_structured_output(ProductList)
不要直接传 list[Product] ------包一层带描述的容器模型,
模型的抽取完整度会明显更好,也方便你加"总数""备注"等字段。
6.2 可选字段:让模型能说"不知道"
python
from typing import Optional
class Product(BaseModel):
name: str = Field(description="商品名称")
price: Optional[float] = Field(description="单价,文本中没提到则为 null", default=None)
明确告诉模型"没有就填 null" ,比让它硬编一个数字好得多。
这是我反复强调的原则------给模型留"不知道"的出口,它才不会编。
七、实战:从合同文本抽结构化字段
python
from typing import Optional
from typing_extensions import TypedDict, Annotated
from langchain_core.prompts import ChatPromptTemplate
class ContractInfo(TypedDict):
"""合同关键信息"""
party_a: Annotated[str, ..., "甲方名称,原文照抄"]
party_b: Annotated[str, ..., "乙方名称,原文照抄"]
amount: Annotated[Optional[float], ..., "合同金额(元),未提及则为 None"]
sign_date: Annotated[Optional[str], ..., "签订日期,YYYY-MM-DD 格式,未提及则为 None"]
has_penalty: Annotated[bool, ..., "是否包含违约金条款"]
prompt = ChatPromptTemplate.from_messages([
("system",
"你是合同信息抽取助手。\n"
"只抽取文本中明确出现的信息,没有提到的一律填 None,绝对不要推测。\n"
"金额只填数字,不要带货币单位和千分位。"),
("human", "合同文本:\n{text}"),
])
chain = prompt | llm.with_structured_output(ContractInfo)
info = chain.invoke({"text": contract_text})
print(info["party_a"], info["amount"], info["has_penalty"])
几个 prompt 技巧,效果差别很大:
- "原文照抄"------避免模型自作主张规范化公司名;
- "没有提到的一律填 None"------给"不知道"的出口;
- 格式举例 (
YYYY-MM-DD、只填数字)------减少格式抖动; - "不要带货币单位"------明确边界。
八、常见坑
| 现象 | 原因 | 解法 |
|---|---|---|
| 报"不支持 structured output" | 模型不支持该 method | 换 method="function_calling" |
| 字段值被翻译/改名 | schema 描述用的中文 | 描述用中文没问题,检查是不是模型幻觉,加"原文照抄" |
| Pydantic 校验报错 | 模型传了越界值 | 放宽约束,或换 TypedDict |
| 嵌套结构抽不全 | 直接用了 list[X] |
包一层容器模型 |
| 偶尔返回 None | 解析失败 | 开 include_raw=True 看 raw |
| 长文本抽取质量下降 | 上下文太长 | 先切片,逐片抽取再合并 |
九、小结
with_structured_output直接返回 Python 对象,告别正则和容错代码;- schema 三种写法:Pydantic(要校验)、TypedDict(推荐,轻量)、JSON Schema(跨语言);
method优先级:json_schema(最严格)>function_calling(最通用)>json_mode(兜底);- 调试开
include_raw=True,能看到 raw / parsed / parsing_error 三部分; - 抽多条时包一层容器模型 ,别直接传
list[X]; - 可选字段用
Optional+ "没有就填 None",给模型留说不知道的出口。
下一篇讲 Agent 入门------把工具调用变成自动的多步循环。