☰
LangChain 之 【聊天模型的结构化输出】(Field、Annotated)
2026/10/12 19:28:38 网站建设 项目流程

目录

1.从 invoke 到 with_structured_output

with_structured_output

2.结构化输出方式

2.1. 返回 Pydantic 对象(推荐)

Field 的本质:它是一个“元数据工厂函数”

2.2. 返回 TypedDict

Annotated

2.3. 返回 JSON Schema

2.4. 选择输出

3.示例

3.1. 信息提取器

3.2. 与工具(Tools)结合


我们常常面临一个尴尬的场景:模型返回了一段流畅自然的文本,但我们需要从中提取出精确的字段,存入数据库或用于后续逻辑。于是,正则表达式、字符串切割、异常处理……一套“手工解析”组合拳下来,确实难绷
LangChain 的 with_structured_output 方法应运而生,它让聊天模型直接返回结构化的数据对象(如 JSON、Pydantic 模型或 TypedDict)

1.从 invoke 到 with_structured_output

from langchain_openai import ChatOpenAI model = ChatOpenAI(model='deepseek-v4-flash') response = model.invoke('告诉我关于苹果公司的最新消息') print(response.content) #输出:你好!关于苹果公司的最新消息....

这段文本对人类友好,对程序却是“灾难”。要提取“公司名”和“股价变化”,只能写复杂的解析逻辑。
with_structured_output允许我们预先定义数据结构,并强制模型按此结构返回。使用起来分三步:

  1. 定义输出结构(Pydantic、TypedDict 或 JSON Schema)
  2. 绑定结构到模型,生成新的 Runnable
  3. 调用该 Runnable,获得结构化结果

with_structured_output

with_structured_output是一个包装器(Wrapper),接收一个 Pydantic 模型(或 JSON Schema),重构模型的 invoke / ainvoke 方法,强制 LLM 的输出必须符合该 Schema,并直接返回解析好的 Python 对象。

model.with_structured_output( schema=Data, # 必填:Pydantic 类或 JSON Schema 字典 include_raw=False, # 关键!下文详解 )

返回⼀个类似model的 Runnable。不同之处在于执⾏ Runnable 后的输出结果,输出的不是字符串或消息 ,⽽是输出与给定输出结构相对应的对象

include_raw默认 False,直接返回 Pydantic 对象(如 Data 实例)。如果你设为 True,返回的是一个字典,包含三个键:

  • raw:LLM 返回的原始 AIMessage 对象(包含 Token 消耗、响应头等元数据)
  • parsed:解析成功的 Pydantic 对象(如果验证通过)
  • parsing_error:如果 Pydantic 验证失败,这里会存放异常信息(None 表示成功)
# 调试神器:既拿数据,又看 Token 消耗 structured_model = model.with_structured_output(Data, include_raw=True) result = structured_model.invoke("讲个笑话") print(result['parsed']) # 正常业务数据 print(result['raw'].response_metadata['token_usage']) # 计费详情

2.结构化输出方式

2.1. 返回 Pydantic 对象(推荐)

我们可以设置执⾏ Runnable 后的输出结果指定为 Pydantic 类,这将返回⼀个 Pydantic 对象。
当收到模型的响应后,LangChain 会提取出代表 Pydantic 参数的 JSON 对象,并⽤ Pydantic 模型
对其进⾏解析和验证,将这个验证后的 JSON 转换为⼀个可⽤的 Pydantic 对象实例返回。如

from langchain_openai import ChatOpenAI from pydantic import BaseModel, Field from typing import Optional model = ChatOpenAI(model="gpt-4o-mini") class Joke(BaseModel): """给用户讲一个笑话。""" setup: str = Field(description="这个笑话的开头") punchline: str = Field(description="这个笑话的妙语") rating: Optional[int] = Field(default=None, description="从1到10分给这个笑话评分") structured_model = model.with_structured_output(Joke) result = structured_model.invoke("给我讲一个关于唱歌的笑话") print(result) # 输出: setup='为什么歌手总是喜欢在洗手间里唱歌?' punchline='因为那里有很好的回音和灵感!' rating=7

BaseModel 是 Pydantic 库的基类。继承它意味着 Joke 变成了数据模型类,而不仅仅是普通的 Python 类。Pydantic 在运行时会对类属性进行类型校验(Type Validation)。如果 LLM 返回的 rating 是字符串 "ten",Pydantic 会抛出验证错误(或尝试转换,取决于配置)

返回的 result 是一个 Joke 实例,可直接访问属性。Pydantic 还支持嵌套结构:

class Data(BaseModel): jokes: List[Joke] structured_model = model.with_structured_output(Data) result = structured_model.invoke("分别讲一个关于唱歌和跳舞的笑话") # result.jokes 是一个列表

Field 的本质:它是一个“元数据工厂函数”

Field是一个普通函数。它返回一个 FieldInfo 对象。当你写 setup: str = Field(description='开头') 时,Pydantic 并不会立刻把它赋值给 setup,而是把这个 FieldInfo 对象存储在类的内部属性 __fields__(或 model_fields)中。这个对象包含了该字段所有的校验规则和描述

  • Field 参数详解

(1) 校验约束

如果大模型(或用户)传入了非法数据,Pydantic 会直接抛出ValidationError,避免脏数据进入业务逻辑

参数名作用你的场景示例
gt/ge大于(>) / 大于等于(>=)rating: int = Field(ge=1, le=10)(强制1-10分)
lt/le小于(<) / 小于等于(<=)同上
min_length/max_length字符串/列表的最小/最大长度setup: str = Field(min_length=5)(笑话开头至少5个字)
pattern正则表达式匹配code: str = Field(pattern=r'^A\d{3}$')
multiple_of数值的倍数score: int = Field(multiple_of=2)

(2) 文档与描述(给 AI 和开发者看,直接影响 LLM 的输出质量)

参数名作用
description最核心。生成 JSON Schema 时填入description字段,OpenAI 会把它当做人设提示(System Prompt)来理解字段含义。
title字段的显示名称,若未填写则默认使用变量名(如setup)。
examples极具价值。给大模型提供“满分示例”。LLM 看到例子后,会模仿格式返回。
deprecated布尔值。标记该字段是否已弃用,会在 Schema 中生成提示,告诉 AI 尽量别用。

(3) 行为控制(默认值与别名)

参数名作用
default设定固定默认值(如rating: int = Field(default=5))。若 LLM 没给,就用 5。
default_factory传入一个无参函数,用于生成动态默认值(如datetime.now)。注意:default和default_factory只能二选一。
alias设置字段的“别名”。例如前端传参叫"妙语",但 Python 变量想叫punchline,用alias="妙语"做映射。
frozen布尔值。设为True后,该字段在实例化后禁止修改,相当于只读属性。

2.2. 返回 TypedDict

TypedDict⽤于为字典对象提供精确的、结构化的类型提⽰。它允许我们指定⼀个字典中应该有哪些键,以及每个键对应的值的类型。定义方式与 Pydantic 类似,但返回的是普通字典,并捕捉键名拼写错误与类型错误

from typing import Optional from typing_extensions import Annotated, TypedDict class Joke(TypedDict): setup: Annotated[str, ..., "这个笑话的开头"] punchline: Annotated[str, ..., "这个笑话的妙语"] rating: Annotated[Optional[int], None, "从1到10分给这个笑话评分"] structured_model = model.with_structured_output(Joke) result = structured_model.invoke("给我讲一个关于唱歌的笑话") print(result) # {'setup': '...', 'punchline': '...', 'rating': 7}

若希望获取原始响应(包含 token 用量、finish_reason 等元数据),可开启 include_raw=True:

structured_model = model.with_structured_output(Joke, include_raw=True) result = structured_model.invoke("给我讲一个关于唱歌的笑话") # result 是一个字典,包含 'raw'(原始消息)、'parsed'(解析后的对象)和 'parsing_error'

Annotated

知识点一句话总结
Annotated是什么类型注解的“附加信息容器”,解释器忽略,第三方库读取。
为什么用解决类型检查器误报、实现校验规则复用、叠加多个校验器。
Pydantic 中怎么用Annotated[类型, Field(...)]代替直接Field。
LangChain 场景必须用Field(description=...)包裹,不能只用字符串。
核心记忆Annotated不改变类型,只附加元数据;必填性由default决定,与Optional无关。

它的语法是 Annotated[类型, 元数据1, 元数据2, ...]

  • 第一个参数(类型):告诉类型检查器(如 Pyright、Mypy)这个变量是什么类型(如 str、int)
  • 后续参数(元数据):可以放任意对象(如 Field(...)、校验器、或者你自己写的类),仅供库(如 Pydantic)在运行时读取,Python 解释器本身会完全忽略它们

关键区别:

# 旧写法(直接绑定 Field) setup: str = Field(description="开头") # 新写法(用 Annotated 包裹 Field) setup: Annotated[str, Field(description="开头")]

直接绑定 Field 会“污染”类属性(赋值),而 Annotated 把元数据剥离到类型注解区域,让类型检查器和数据校验器各司其职,互不干扰

2.3. 返回 JSON Schema

当不想引入 Pydantic 或 TypedDict 时,可以直接提供 JSON Schema 字典:

json_schema = { "title": "joke", "type": "object", "properties": { "setup": {"type": "string", "description": "笑话开头"}, "punchline": {"type": "string", "description": "笑话妙语"}, "rating": {"type": "integer", "description": "评分", "default": None} }, "required": ["setup", "punchline"] } structured_model = model.with_structured_output(json_schema) result = structured_model.invoke("给我讲一个关于唱歌的笑话") # 返回字典

2.4. 选择输出

有时,模型需要根据用户输入返回不同类型的响应(比如询问笑话则返回 Joke,普通问候则返回 ConversationalResponse)。可以使用 Union 类型:

from pydantic import BaseModel, Field from typing import Union class Joke(BaseModel): setup: str punchline: str rating: Optional[int] = None class ConversationalResponse(BaseModel): response: str = Field(description="对用户查询的会话响应") class FinalResponse(BaseModel): final_output: Union[Joke, ConversationalResponse] structured_model = model.with_structured_output(FinalResponse) result1 = structured_model.invoke("给我讲一个笑话") # result1.final_output 是 Joke 实例 result2 = structured_model.invoke("你好") # result2.final_output 是 ConversationalResponse 实例

3.示例

3.1. 信息提取器

结构化输出最常见的应用是信息提取。比如从一段描述中提取人物属性:

class Person(BaseModel): name: Optional[str] = Field(default=None, description="名字") hair_color: Optional[str] = Field(default=None, description="头发颜色") height_in_meters: Optional[str] = Field(default=None, description="身高(米)") structured_model = model.with_structured_output(Person) messages = [ SystemMessage("你是提取信息专家,不知道的属性返回 null"), HumanMessage("史密斯身高6英尺,金发。") ] result = structured_model.invoke(messages) print(result) # name='史密斯' hair_color='金发' height_in_meters='1.83'

关键点:

  • 所有字段设为 Optional,让模型在不确定时返回 None,避免强行编造。
  • 为每个字段提供详细的 description,这对提取准确率影响很大。

3.2. 与工具(Tools)结合

(分两步走):

  1. 第一轮:绑定工具,让模型决定是否调用,并手动执行工具,将结果添加到消息历史
  2. 第二轮:将包含工具结果的消息列表传给带有结构化输出的模型,得到最终结构化结果
# 1. 绑定工具,获得工具调用请求 model_with_search = model.bind_tools([web_search]) messages = [HumanMessage("搜索西安天气")] ai_msg = model_with_search.invoke(messages) messages.append(ai_msg) # 2. 手动执行工具 for tool_call in ai_msg.tool_calls: tool_msg = web_search.invoke(tool_call) messages.append(tool_msg) # 3. 现在用结构化输出模型处理完整消息历史 structured_model = model.with_structured_output(SearchResult) result = structured_model.invoke(messages)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询