Day3:结构化输出

在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 模型类(最常用),自动提取 schema
  • None:不指定(极少用)

method:

  • json_schema(默认):原生支持 JSON Schema,推荐现代模型(OpenAI gpt-4o、DeepSeek 等)
  • function_calling:用工具调用(function call)机制返回结构化数据,兼容性更广,老模型首选
  • json_mode:仅强制返回合法 JSON 字符串,不会校验字段是否符合 schema,弱约束

include_raw: bool = False

  • False(默认):直接返回解析好的 Pydantic 对象 /dict
  • True:返回字典,包含 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 就可以做到这件事。

相关推荐
程序员清风2 小时前
最短路径算法:BFS、Dijkstra 与 Floyd
java·算法·宽度优先
派小心.2 小时前
小程序分享链路中来源参数的传递校验
大数据·前端·数据库·人工智能·小程序·数据分析
Είναι η κοπέλα2 小时前
向量数据库与嵌入模型:Chroma/FAISS/Milvus 选型
数据库·milvus·faiss
IvorySQL3 小时前
从轮询到等待——PostgreSQL WAIT FOR LSN 特性深度解析
数据库·postgresql
OceanBase数据库官方博客3 小时前
被 VLDB 2027接收OceanBase 最新论文让数据库基(技术解析与实践)
数据库·人工智能·oceanbase
智能直播3 小时前
RK3528 V4L2 框架 UVC 接口自动获取 H.264 / MJPEG / YUYV 格式:从内核采集到硬解分发
java·网络·实时音视频·视频编解码·h.264
171320330计算机毕设编程4 小时前
计算机毕业设计 基于YOLO改进算法的车辆特征识别系统设计与实现--lw
java·大数据·python·sql·算法
badhope4 小时前
Spring AI Alibaba停更:Java程序员的信任,还能被辜负几次?
java·spring
const_qiu4 小时前
第一章 1.1-1.2:基础模型调用与 TypeScript 入门 —— Java 开发者的视角
java·javascript·typescript