在LangChain中,聊天模型提供了额外的功能:结构化输出。一种使聊天模型以结构化格式(例如JSON)进行响应的技术。例如,可能希望将模型输出存储在数据库中,并确保输出符合数据库模式。这种需求激发了结构化输出的概念,其中可以指示模型使用特定的输出结构进行响应。
这样做的核心原因是:从"字符串"到"对象"的范式转换。想象一下,在没有这个功能之前,我们调用聊天模型得到的是一个AIMessage,其内容是一个字符串。
model = ChatOpenAI()
response = model.invoke("告诉我关于苹果公司的最新消息。")
print(response.content)
# 输出: "苹果公司于昨⽇发布了新款iPhone...其股价上涨了2%..."
这个字符串对人类很友好,但对程序不友好。如果我们想从这段文本中提取出"公司名"和"股价变化"并用在后续逻辑中,则需要编写复杂且容易出错的解析代码。
聊天模型的with_structured_output 方法则允许我们预先定义一个期望的数据结构,并要求大模型必须按照这个结构返回信息。
一、定义输出结构
with_structured_output()
要想使用结构化输出能力,LangChain提供了一种方法.with_structured_output(),该方法需要先定义输出结构,然后执行通过 .with_structured_output()得到的Runnable实例。
步骤如下(伪代码)代码块
#1.定义输出结构
schema = {"foo":"bar"}
#2.绑定schema,其实是生成支持结构化返回的 Runnable 实例
model_with_structure = model.with_structured_output(schema)
#3.执行
structured_output = model_with_structure.invoke(user_input)
这是获得结构化输出的最简单、最可靠的方法。此方法将输出结构作为参数输入,返回一个类似model的Runnable。不同之处在于执行Runnable后的输出结果,输出的不是字符串或消息,而是输出与给定输出结构相对应的对象。
该 输出结构 可以指定为 TypedDict 类、JSON Schema或 Pydantic类。如果使用 TypedDict 或JSON Schema,则Runnable将返回一个字典,如果使用 Pydantic类,则将返回一个Pydantic对象。
参数解析
# with_structured_output() 方法定义:
def with_structured_output(
schema: dict[str, Any] | type[_BM] | type | None = None,
*,
method: Literal['function_calling', 'json_mode', 'json_schema'] = 'json_schema',
include_raw: bool = False,
strict: bool | None = None,
**kwargs: Any,
) -> Runnable[PromptValue | str | Sequence[BaseMessage | list[str] | tuple[str, str] | str | dict[str, Any]], dict | _BM]:
schema,支持三种类型:
dict[str, Any]:手写 JSON Schema 字典type[_BM]:Pydantic 模型类(最常用),自动提取 schemaNone:不指定(极少用)
method:
json_schema(默认):原生支持 JSON Schema,推荐现代模型(OpenAI gpt-4o、DeepSeek 等)function_calling:用工具调用(function call)机制返回结构化数据,兼容性更广,老模型首选json_mode:仅强制返回合法 JSON 字符串,不会校验字段是否符合 schema,弱约束
include_raw: bool = False
False(默认):直接返回解析好的 Pydantic 对象 /dictTrue:返回字典,包含raw(模型原始返回)、parsed(解析后对象)、parsing_error解析异常信息,方便调试
strict: bool | None = None
开启严格模式:严格校验 JSON Schema,不允许额外字段 。OpenAI 的 json_schema 模式专用。
strict=True:不允许输出 schema 以外的字段strict=False:允许附加额外字段- 默认为 None,则不会将 strict 参数传递给模型。
返回值解析
该方法返回Runnable(LangChain 可运行组件,可以放进 LCEL 管道,调用.invoke()执行)。
将来执行时:
-
如果
include_raw=False(默认)且 schema是Pydantic类 ,则 Runnable 会输出 Pydantic 对象。 否则 Runnable 输出一个字典。 -
如果 include_raw=True (调试模式),则 Runnable 输出一个带有键的字典:
- 'raw' :BaseMessage对象,大模型原始完整返回消息,里面包含原始 content /tool_calls
- parsed' :如果出现解析错误,则为 None,否则类型取决于如上所述的 schema 。
- 'parsing_error' :OptionalBaseException
1.1 返回 Pydantic 对象
我们可以设置执行Runnable后的输出结果指定为Pydantic类,这将返回一个Pydantic对象。当收到模型的响应后,LangChain会提取出代表Pydantic参数的 JSON对象,并用Pydantic模型对其进行解析和验证,将这个验证后的JSON转换为一个可用的Pydantic对象实例返回。如下所示:
# 定义⼤模型
model = ChatOpenAI(model="gpt-4o-mini")
# 定义输出结构:Pydantic 类
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='小明在KTV唱歌,唱到一半,朋友突然冲过来把麦克风抢走了。小明很委屈:"我唱得好好的,你干嘛抢我麦?"朋友说:"你唱得是挺好,但你再唱下去,隔壁就要报警了。"小明不服:"为什么?"朋友说:"因为你唱的是《忐忑》,人家以为咱们在拆迁。"'
rating=7
还支持嵌套输出:
class Joke(BaseModel):
"""给⽤⼾讲⼀个笑话。"""
setup: str = Field(description="这个笑话的开头")
punchline: str = Field(description="这个笑话的妙语")
rating: Optional[int] = Field(default=None, description="从1到10分,给这个笑话评分")
class Data(BaseModel):
"""获取关于笑话的数据。"""
jokes: List[Joke]
structured_model = model.with_structured_output(Data)
result = structured_model.invoke("分别讲⼀个关于唱歌和跳舞的笑话")
print(result)
#jokes=[
# Joke(setup='为什么唱歌比赛里,麦克风总是一脸委屈?', punchline='因为它不是被跑调,就是被高音"暴击"。', rating=8),
# Joke(setup='为什么跳舞的地板从来不抱怨?', punchline='因为它早就习惯了被"踩"出节奏感。', rating=8)
#]
1.2 返回 TypedDict
什么叫
TypedDict?TypedDict 是 Python 类型提示工具,专门给字典做类型标注,用来声明字典里有哪些 key、每个 key 对应的值类型。
⚠️重点:它只是静态类型提示,运行时不会做校验(和 Pydantic BaseModel 不一样!)
定义TypedDict
最清晰、最常用的定义方式,就是类似于定义一个类(Python 3.8+)
from typing import TypedDict
class User(TypedDict):
name: str
age: int
email: str
is_active: bool = True # 默认值(这个写法代表该字段可选)
对于它非常重要的一个能力就是捕捉键名拼写错误与类型错误。
# 类型检查器会捕获这些错误
bad_user: User = {
"name": "Dave",
"age": "forty", # 错误:应该是int
"emial": "dave@example.com" # 错误:拼写错误
}
因此,我们也可以设置执行Runnable后的输出结果指定为TypedDict类,这将返回一个字典,且输出后,会根据设定进行验证。
# 定义输出结构: 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}
让我们加⼊ include_raw=True ,再来看看效果:
structured_model = model.with_structured_output(Joke, include_raw=True)
#{'raw': AIMessage(content='{\n "setup": "小明去KTV唱歌,为什么全场都给他鼓掌?",\n "punchline": "因为他终于唱完了。",\n "rating": 7\n}', additional_kwargs={'parsed': None, 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 158, 'prompt_tokens': 184, 'total_tokens': 342, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 112, 'rejected_prediction_tokens': None, 'text_tokens': 158}, 'prompt_tokens_details': {'audio_tokens': None, 'cache_write_tokens': None, 'cached_tokens': 0, 'text_tokens': 184}}, 'model_name': 'qwen3.8-max', 'system_fingerprint': None, 'id': 'chatcmpl-f0a6d0b2-91a6-919c-a1d9-8dd93fbf29f0', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None}, id='run--01a13474-b56a-7d71-81e4-b67253dba6d6-0', usage_metadata={'input_tokens': 184, 'output_tokens': 158, 'total_tokens': 342, 'input_token_details': {'cache_read': 0}, 'output_token_details': {'reasoning': 112}}),
'parsed': {'setup': '小明去KTV唱歌,为什么全场都给他鼓掌?', 'punchline': '因为他终于唱完了。', 'rating': 7},
'parsing_error': None}
可以看到 include_raw=True:返回字典,包含 raw(模型原始返回)、parsed(解析后对象)、parsing_error 解析异常信息,方便调试
1.3 返回 JSON
还可以让聊天模型直接返回JSON,只不过为了声明JSON,我们需要定义JSON schema
json_schema = {
"title": "joke",
"description": "给⽤⼾讲⼀个笑话。",
"type": "object",
"properties": {
"setup": {
"type": "string",
"description": "这个笑话的开头",
},
"punchline": {
"type": "string",
"description": "这个笑话的妙语",
},
"rating": {
"type": "integer",
"description": "从1到10分,给这个笑话评分",
"default": None,
},
},
"required": ["setup", "punchline"],
}
structured_model = model.with_structured_output(json_schema)
result = structured_model.invoke("给我讲⼀个关于唱歌的笑话")
print(result)
#{'setup': '小明问老师:"我唱歌怎么样?"', 'punchline': '老师说:"你唱得很投入,就是调没跟上。"', 'rating': 7}
1.4 有选择的输出
创建具有联合类型属性(Union)的父模式,以使⽤ Pydantic 为例(其他同理)
class Joke(BaseModel):
"""给⽤⼾讲⼀个笑话。"""
setup: str = Field(description="这个笑话的开头")
punchline: str = Field(description="这个笑话的妙语")
rating: Optional[int] = Field(default=None, description="从1到10分,给这个笑话评分")
class ConversationalResponse(BaseModel):
"""以对话的⽅式回应。待⼈友善,乐于助⼈。"""
response: str = Field(description="对⽤⼾查询的会话响应")
class FinalResponse(BaseModel):
final_output: Union[Joke, ConversationalResponse]
structured_model = model.with_structured_output(FinalResponse)
result = structured_model.invoke("给我讲⼀个关于唱歌的笑话")
print(result)
#final_output=Joke(setup='为什么歌⼿总是带着梯⼦?', punchline='因为他们想要达到更⾼的⾳调!', rating=7)
result = structured_model.invoke("你好")
print(result)
#final_output=ConversationalResponse(response='你好!很高兴见到你。有什么我可以帮你的吗?')
二、 结构化输出------实用场景
2.1 作为信息提取器
class Person(BaseModel):
"""⼀个⼈的信息。"""
# 注意:
# 1. 每个字段都是 Optional "可选的" ------ 允许 LLM 在不知道答案时输出 None。
# 2. 每个字段都有⼀个 description "描述" ------ LLM使⽤这个描述。
# 有⼀个好的描述可以帮助提⾼提取结果。
name: Optional[str] = Field(default=None, description="这个⼈的名字")
hair_color: Optional[str] = Field(default=None, description="如果知道这个⼈头发的颜⾊")
skin_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(content="你是⼀个提取信息的专家,只从⽂本中提取相关信息。如果您不知道要提取的属性的值,属性值返回null"),
HumanMessage(content="史密斯⾝⾼6英尺,⾦发。")
]
result = structured_model.invoke(messages)
print(result)
#name='史密斯' hair_color='⾦发' skin_color=None height_in_meters='1.83'
2.2 与工具结合使用
注意,使用聊天模型原生的工具搭配结构化输出并不好用!!!这里只是了解下其能力。更好的用法见LangGraph Agent 能力。
2.2.1 方式1:使用 with_structured_output()的tools参数
# 结构输出对象
class SearchResult(BaseModel):
"""结构化搜索结果。"""
query: str = Field(description="搜索查询")
findings: str = Field(description="调查结果摘要")
@tool
def web_search(query: str) -> str:
"""在⽹上搜索信息。
Args:
query: 搜索查询
"""
return "西安今天多云转⼩⾬,⽓温18-23度,东南⻛2级,空⽓质量良好"
structured_search_model = model.with_structured_output(
SearchResult,
tools=[web_search],
strict=True,
include_raw=True,
)
result = structured_search_model.invoke("搜索当前最新的西安的天⽓")
print(result)
#{'raw': AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_c1543b34d5b04fdaa5d1e188', 'function': {'arguments': '{"query": "西安天气 今天 最新预报"}', 'name': 'web_search', 'parsed_arguments': {'query': '西安天气 今天 最新预报'}}, 'type': 'function', 'index': 0}, {'id': 'call_bf6b4e9d546549489c3a0857', 'function': {'arguments': '{"query": "西安 未来三天天气预报 气温"}', 'name': 'web_search', 'parsed_arguments': {'query': '西安 未来三天天气预报 气温'}}, 'type': 'function', 'index': 1}], 'parsed': None, 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 88, 'prompt_tokens': 343, 'total_tokens': 431, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 22, 'rejected_prediction_tokens': None, 'text_tokens': 88}, 'prompt_tokens_details': {'audio_tokens': None, 'cache_write_tokens': None, 'cached_tokens': 0, 'text_tokens': 343}}, 'model_name': 'qwen3.8-max', 'system_fingerprint': None, 'id': 'chatcmpl-757a1f1b-a6d7-9525-a82d-6803e6e7c850', 'service_tier': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run--01a13489-48fa-7e10-a82c-f1e8a96e2a2f-0', tool_calls=[{'name': 'web_search', 'args': {'query': '西安天气 今天 最新预报'}, 'id': 'call_c1543b34d5b04fdaa5d1e188', 'type': 'tool_call'}, {'name': 'web_search', 'args': {'query': '西安 未来三天天气预报 气温'}, 'id': 'call_bf6b4e9d546549489c3a0857', 'type': 'tool_call'}], usage_metadata={'input_tokens': 343, 'output_tokens': 88, 'total_tokens': 431, 'input_token_details': {'cache_read': 0}, 'output_token_details': {'reasoning': 22}}),
# 'parsed': None,
# 'parsing_error': None}
可以看到,它并不能直接帮我们输出想要的SearchResult搜索结果,而是只返回了AIMessage(工具调用信息)
实际上,with_structured_output方法只是让模型知道有哪些工具可以调用,但是并不会自动执行工具。要获得工具执行后的结果并整合到最终的结构化输出中,我们需要手动处理工具调用,然后将工具返回的结果再次传递给模型。见方式2。
2.2.2 方法2:拆解能力,单独依次执行
当需要同时使用结构化输出和其他工具时,需要注意顺序,不要弄反:
1.首先绑定工具
2.其次添加结构化输出
# 结构输出对象
class SearchResult(BaseModel):
"""结构化搜索结果。"""
query: str = Field(description="搜索查询")
findings: str = Field(description="调查结果摘要")
@tool
def web_search(query: str) -> str:
"""在⽹上搜索信息。
Args:
query: 搜索查询
"""
# 模拟搜索返回结果
return "西安今天多云转⼩⾬,⽓温18-23度,东南⻛2级,空⽓质量良好"
# ⼀次性绑定⼯具和结构化输出
model_with_tools =model.bind_tools([web_search]).with_structured_output(SearchResult)
# 执⾏查询
messages = [HumanMessage("搜索当前最新的西安的天⽓")]
result = model_with_tools.invoke(messages)
print(result)
# query='西安的天⽓' findings='当前西安的天⽓情况:预计多云,⽓温在18°C到25°C之间,有轻微的⻛,空⽓湿度适中,⽆降⽔。'
从打印结果可以发现,它也没有去调用工具。而是聊天模型直接给出了结果。让我们改造下:
# 结构输出对象
class SearchResult(BaseModel):
"""结构化搜索结果。"""
query: str = Field(description="搜索查询")
findings: str = Field(description="调查结果摘要")
@tool
def web_search(query: str) -> str:
"""在⽹上搜索信息。
Args:
query: 搜索查询
"""
# 模拟搜索返回结果
return "西安今天多云转⼩⾬,⽓温18-23度,东南⻛2级,空⽓质量良好"
# ⼿动将⼯具结果加⼊消息列表
model_with_tool = model.bind_tools([web_search])
messages = [
HumanMessage("搜索当前最新的西安的天⽓")
]
ai_msg = model_with_tool.invoke(messages)
messages.append(ai_msg)
# ⼿动将ToolMessahe加⼊消息列表
for tool_call in ai_msg.tool_calls:
tool_msg = web_search.invoke(tool_call)
messages.append(tool_msg)
structured_search_model = model_with_tool.with_structured_output(SearchResult)
result = structured_search_model.invoke(messages)
print(result)
#query='西安天⽓' findings='西安今天多云转⼩⾬,⽓温18-23度,东南⻛2级,空⽓质量良好。'
这次的结果符合我们的预期。但依旧很麻烦,因为其实我们实际上调用了两次模型:
第一次调用模型时,模型返回了工具调用,然后我们执行工具,将工具结果加入消息列表。
第二次调用时,我们使用结构化输出SearchResult的模型,来处理整个消息列表(包括工具调用的结果),然后输出结构化结果。
有没有只手动调用一次(实际上允许调用两次模型)就能获取到结构化输出的结果呢?将来在学习
LangGraph部分的Agent 就可以做到这件事。