Skip to main content
Open In ColabOpen on GitHub

如何从模型返回结构化数据

Prerequisites

本指南假设您熟悉以下概念:

让模型返回符合特定schema的输出通常非常有用。一个常见的用例是从文本中提取数据以插入数据库或与其他下游系统一起使用。本指南介绍了几种从模型获取结构化输出的策略。

The .with_structured_output() 方法

Supported models

你可以在这里找到支持此方法的模型列表

这是获取结构化输出的最简单和最可靠的方法。with_structured_output() 是为提供原生API以结构化输出的模型实现的,如工具/函数调用或JSON模式,并在底层利用这些功能。

该方法以模式作为输入,指定所需输出属性的名称、类型和描述。该方法返回一个类似模型的可运行对象,不同之处在于它输出与给定模式对应的对象,而不是输出字符串或消息。模式可以指定为TypedDict类、JSON Schema或Pydantic类。如果使用TypedDict或JSON Schema,则Runnable将返回一个字典;如果使用Pydantic类,则返回一个Pydantic对象。

举个例子,让我们让模型生成一个笑话,并将铺垫与笑点分开:

pip install -qU langchain-openai
import getpass
import os

if not os.environ.get("OPENAI_API_KEY"):
os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter API key for OpenAI: ")

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini")

Pydantic 类

如果我们希望模型返回一个Pydantic对象,我们只需要传入所需的Pydantic类。使用Pydantic的主要优势是模型生成的输出将被验证。如果缺少任何必填字段或任何字段类型错误,Pydantic将引发错误。

from typing import Optional

from pydantic import BaseModel, Field


# Pydantic
class Joke(BaseModel):
"""Joke to tell user."""

setup: str = Field(description="The setup of the joke")
punchline: str = Field(description="The punchline to the joke")
rating: Optional[int] = Field(
default=None, description="How funny the joke is, from 1 to 10"
)


structured_llm = llm.with_structured_output(Joke)

structured_llm.invoke("Tell me a joke about cats")
Joke(setup='Why was the cat sitting on the computer?', punchline='Because it wanted to keep an eye on the mouse!', rating=7)
tip

除了Pydantic类的结构之外,Pydantic类的名称、文档字符串以及参数的名称和提供的描述也非常重要。大多数情况下,with_structured_output使用的是模型的功能/工具调用API,你可以有效地认为所有这些信息都被添加到模型提示中。

TypedDict 或 JSON 模式

如果你不想使用Pydantic,明确不希望验证参数,或者希望能够流式传输模型输出,你可以使用TypedDict类来定义你的模式。我们可以选择使用LangChain支持的特殊Annotated语法,它允许你指定字段的默认值和描述。请注意,如果模型没有生成默认值,它不会自动填充,它仅用于定义传递给模型的模式。

Requirements
  • 核心: langchain-core>=0.2.26
  • 类型扩展:强烈建议从typing_extensions导入AnnotatedTypedDict,而不是从typing导入,以确保在Python版本之间行为一致。
from typing import Optional

from typing_extensions import Annotated, TypedDict


# TypedDict
class Joke(TypedDict):
"""Joke to tell user."""

setup: Annotated[str, ..., "The setup of the joke"]

# Alternatively, we could have specified setup as:

# setup: str # no default, no description
# setup: Annotated[str, ...] # no default, no description
# setup: Annotated[str, "foo"] # default, no description

punchline: Annotated[str, ..., "The punchline of the joke"]
rating: Annotated[Optional[int], None, "How funny the joke is, from 1 to 10"]


structured_llm = llm.with_structured_output(Joke)

structured_llm.invoke("Tell me a joke about cats")
{'setup': 'Why was the cat sitting on the computer?',
'punchline': 'Because it wanted to keep an eye on the mouse!',
'rating': 7}

同样地,我们可以传入一个JSON Schema字典。这不需要导入或类,并且非常清楚地记录了每个参数的文档,代价是稍微冗长一些。

json_schema = {
"title": "joke",
"description": "Joke to tell user.",
"type": "object",
"properties": {
"setup": {
"type": "string",
"description": "The setup of the joke",
},
"punchline": {
"type": "string",
"description": "The punchline to the joke",
},
"rating": {
"type": "integer",
"description": "How funny the joke is, from 1 to 10",
"default": None,
},
},
"required": ["setup", "punchline"],
}
structured_llm = llm.with_structured_output(json_schema)

structured_llm.invoke("Tell me a joke about cats")
{'setup': 'Why was the cat sitting on the computer?',
'punchline': 'Because it wanted to keep an eye on the mouse!',
'rating': 7}

在多个模式之间选择

让模型从多个模式中选择的最简单方法是创建一个具有联合类型属性的父模式。

使用 Pydantic

from typing import Union


class Joke(BaseModel):
"""Joke to tell user."""

setup: str = Field(description="The setup of the joke")
punchline: str = Field(description="The punchline to the joke")
rating: Optional[int] = Field(
default=None, description="How funny the joke is, from 1 to 10"
)


class ConversationalResponse(BaseModel):
"""Respond in a conversational manner. Be kind and helpful."""

response: str = Field(description="A conversational response to the user's query")


class FinalResponse(BaseModel):
final_output: Union[Joke, ConversationalResponse]


structured_llm = llm.with_structured_output(FinalResponse)

structured_llm.invoke("Tell me a joke about cats")
FinalResponse(final_output=Joke(setup='Why was the cat sitting on the computer?', punchline='Because it wanted to keep an eye on the mouse!', rating=7))
structured_llm.invoke("How are you today?")
FinalResponse(final_output=ConversationalResponse(response="I'm just a computer program, so I don't have feelings, but I'm here and ready to help you with whatever you need!"))

使用 TypedDict

from typing import Optional, Union

from typing_extensions import Annotated, TypedDict


class Joke(TypedDict):
"""Joke to tell user."""

setup: Annotated[str, ..., "The setup of the joke"]
punchline: Annotated[str, ..., "The punchline of the joke"]
rating: Annotated[Optional[int], None, "How funny the joke is, from 1 to 10"]


class ConversationalResponse(TypedDict):
"""Respond in a conversational manner. Be kind and helpful."""

response: Annotated[str, ..., "A conversational response to the user's query"]


class FinalResponse(TypedDict):
final_output: Union[Joke, ConversationalResponse]


structured_llm = llm.with_structured_output(FinalResponse)

structured_llm.invoke("Tell me a joke about cats")
{'final_output': {'setup': 'Why was the cat sitting on the computer?',
'punchline': 'Because it wanted to keep an eye on the mouse!',
'rating': 7}}
structured_llm.invoke("How are you today?")
{'final_output': {'response': "I'm just a computer program, so I don't have feelings, but I'm here and ready to help you with whatever you need!"}}

响应应与Pydantic示例中显示的响应相同。

或者,如果您的所选模型支持,您可以直接使用工具调用来让模型在选项之间进行选择。这涉及更多的解析和设置,但在某些情况下会带来更好的性能,因为您不必使用嵌套模式。有关更多详细信息,请参阅此操作指南

流处理

当输出类型为字典时(即当模式被指定为TypedDict类或JSON Schema字典时),我们可以从结构化模型中流式传输输出。

info

请注意,生成的是已经聚合的块,而不是增量。

from typing_extensions import Annotated, TypedDict


# TypedDict
class Joke(TypedDict):
"""Joke to tell user."""

setup: Annotated[str, ..., "The setup of the joke"]
punchline: Annotated[str, ..., "The punchline of the joke"]
rating: Annotated[Optional[int], None, "How funny the joke is, from 1 to 10"]


structured_llm = llm.with_structured_output(Joke)

for chunk in structured_llm.stream("Tell me a joke about cats"):
print(chunk)
{}
{'setup': ''}
{'setup': 'Why'}
{'setup': 'Why was'}
{'setup': 'Why was the'}
{'setup': 'Why was the cat'}
{'setup': 'Why was the cat sitting'}
{'setup': 'Why was the cat sitting on'}
{'setup': 'Why was the cat sitting on the'}
{'setup': 'Why was the cat sitting on the computer'}
{'setup': 'Why was the cat sitting on the computer?'}
{'setup': 'Why was the cat sitting on the computer?', 'punchline': ''}
{'setup': 'Why was the cat sitting on the computer?', 'punchline': 'Because'}
{'setup': 'Why was the cat sitting on the computer?', 'punchline': 'Because it'}
{'setup': 'Why was the cat sitting on the computer?', 'punchline': 'Because it wanted'}
{'setup': 'Why was the cat sitting on the computer?', 'punchline': 'Because it wanted to'}
{'setup': 'Why was the cat sitting on the computer?', 'punchline': 'Because it wanted to keep'}
{'setup': 'Why was the cat sitting on the computer?', 'punchline': 'Because it wanted to keep an'}
{'setup': 'Why was the cat sitting on the computer?', 'punchline': 'Because it wanted to keep an eye'}
{'setup': 'Why was the cat sitting on the computer?', 'punchline': 'Because it wanted to keep an eye on'}
{'setup': 'Why was the cat sitting on the computer?', 'punchline': 'Because it wanted to keep an eye on the'}
{'setup': 'Why was the cat sitting on the computer?', 'punchline': 'Because it wanted to keep an eye on the mouse'}
{'setup': 'Why was the cat sitting on the computer?', 'punchline': 'Because it wanted to keep an eye on the mouse!'}
{'setup': 'Why was the cat sitting on the computer?', 'punchline': 'Because it wanted to keep an eye on the mouse!', 'rating': 7}

少样本提示

对于更复杂的模式,向提示中添加少量示例非常有用。这可以通过几种方式完成。

最简单且最通用的方法是在提示中的系统消息中添加示例:

from langchain_core.prompts import ChatPromptTemplate

system = """You are a hilarious comedian. Your specialty is knock-knock jokes. \
Return a joke which has the setup (the response to "Who's there?") and the final punchline (the response to "<setup> who?").

Here are some examples of jokes:

example_user: Tell me a joke about planes
example_assistant: {{"setup": "Why don't planes ever get tired?", "punchline": "Because they have rest wings!", "rating": 2}}

example_user: Tell me another joke about planes
example_assistant: {{"setup": "Cargo", "punchline": "Cargo 'vroom vroom', but planes go 'zoom zoom'!", "rating": 10}}

example_user: Now about caterpillars
example_assistant: {{"setup": "Caterpillar", "punchline": "Caterpillar really slow, but watch me turn into a butterfly and steal the show!", "rating": 5}}"""

prompt = ChatPromptTemplate.from_messages([("system", system), ("human", "{input}")])

few_shot_structured_llm = prompt | structured_llm
few_shot_structured_llm.invoke("what's something funny about woodpeckers")
API Reference:ChatPromptTemplate
{'setup': 'Woodpecker',
'punchline': "Woodpecker you a joke, but I'm afraid it might be too 'hole-some'!",
'rating': 7}

当结构化输出的基础方法是工具调用时,我们可以将示例作为显式工具调用传入。您可以在API参考中检查您使用的模型是否使用了工具调用。

from langchain_core.messages import AIMessage, HumanMessage, ToolMessage

examples = [
HumanMessage("Tell me a joke about planes", name="example_user"),
AIMessage(
"",
name="example_assistant",
tool_calls=[
{
"name": "joke",
"args": {
"setup": "Why don't planes ever get tired?",
"punchline": "Because they have rest wings!",
"rating": 2,
},
"id": "1",
}
],
),
# Most tool-calling models expect a ToolMessage(s) to follow an AIMessage with tool calls.
ToolMessage("", tool_call_id="1"),
# Some models also expect an AIMessage to follow any ToolMessages,
# so you may need to add an AIMessage here.
HumanMessage("Tell me another joke about planes", name="example_user"),
AIMessage(
"",
name="example_assistant",
tool_calls=[
{
"name": "joke",
"args": {
"setup": "Cargo",
"punchline": "Cargo 'vroom vroom', but planes go 'zoom zoom'!",
"rating": 10,
},
"id": "2",
}
],
),
ToolMessage("", tool_call_id="2"),
HumanMessage("Now about caterpillars", name="example_user"),
AIMessage(
"",
tool_calls=[
{
"name": "joke",
"args": {
"setup": "Caterpillar",
"punchline": "Caterpillar really slow, but watch me turn into a butterfly and steal the show!",
"rating": 5,
},
"id": "3",
}
],
),
ToolMessage("", tool_call_id="3"),
]
system = """You are a hilarious comedian. Your specialty is knock-knock jokes. \
Return a joke which has the setup (the response to "Who's there?") \
and the final punchline (the response to "<setup> who?")."""

prompt = ChatPromptTemplate.from_messages(
[("system", system), ("placeholder", "{examples}"), ("human", "{input}")]
)
few_shot_structured_llm = prompt | structured_llm
few_shot_structured_llm.invoke({"input": "crocodiles", "examples": examples})
{'setup': 'Crocodile',
'punchline': 'Crocodile be seeing you later, alligator!',
'rating': 6}

有关使用工具调用时的少量提示的更多信息,请参见这里

(高级) 指定结构化输出的方法

对于支持多种输出结构方式的模型(即它们同时支持工具调用和JSON模式),您可以使用method=参数指定要使用的方法。

JSON mode

如果使用JSON模式,您仍然需要在模型提示中指定所需的模式。您传递给with_structured_output的模式仅用于解析模型输出,它不会像工具调用那样传递给模型。

要查看您使用的模型是否支持JSON模式,请检查其在API参考中的条目。

structured_llm = llm.with_structured_output(None, method="json_mode")

structured_llm.invoke(
"Tell me a joke about cats, respond in JSON with `setup` and `punchline` keys"
)
{'setup': 'Why was the cat sitting on the computer?',
'punchline': 'Because it wanted to keep an eye on the mouse!'}

(高级) 原始输出

LLMs 在生成结构化输出方面并不完美,尤其是在模式变得复杂时。你可以通过传递 include_raw=True 来避免引发异常并自己处理原始输出。这将改变输出格式,使其包含原始消息输出、parsed 值(如果成功)以及任何产生的错误:

structured_llm = llm.with_structured_output(Joke, include_raw=True)

structured_llm.invoke("Tell me a joke about cats")
{'raw': AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_f25ZRmh8u5vHlOWfTUw8sJFZ', 'function': {'arguments': '{"setup":"Why was the cat sitting on the computer?","punchline":"Because it wanted to keep an eye on the mouse!","rating":7}', 'name': 'Joke'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 33, 'prompt_tokens': 93, 'total_tokens': 126}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_4e2b2da518', 'finish_reason': 'stop', 'logprobs': None}, id='run-d880d7e2-df08-4e9e-ad92-dfc29f2fd52f-0', tool_calls=[{'name': 'Joke', 'args': {'setup': 'Why was the cat sitting on the computer?', 'punchline': 'Because it wanted to keep an eye on the mouse!', 'rating': 7}, 'id': 'call_f25ZRmh8u5vHlOWfTUw8sJFZ', 'type': 'tool_call'}], usage_metadata={'input_tokens': 93, 'output_tokens': 33, 'total_tokens': 126}),
'parsed': {'setup': 'Why was the cat sitting on the computer?',
'punchline': 'Because it wanted to keep an eye on the mouse!',
'rating': 7},
'parsing_error': None}

直接提示和解析模型输出

并非所有模型都支持.with_structured_output(),因为并非所有模型都支持工具调用或JSON模式。对于这些模型,您需要直接提示模型使用特定格式,并使用输出解析器从原始模型输出中提取结构化响应。

使用 PydanticOutputParser

以下示例使用内置的PydanticOutputParser来解析聊天模型的输出,该模型被提示匹配给定的Pydantic模式。请注意,我们正在从解析器的方法中直接将format_instructions添加到提示中:

from typing import List

from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate
from pydantic import BaseModel, Field


class Person(BaseModel):
"""Information about a person."""

name: str = Field(..., description="The name of the person")
height_in_meters: float = Field(
..., description="The height of the person expressed in meters."
)


class People(BaseModel):
"""Identifying information about all people in a text."""

people: List[Person]


# Set up a parser
parser = PydanticOutputParser(pydantic_object=People)

# Prompt
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"Answer the user query. Wrap the output in `json` tags\n{format_instructions}",
),
("human", "{query}"),
]
).partial(format_instructions=parser.get_format_instructions())

让我们看一下发送给模型的信息:

query = "Anna is 23 years old and she is 6 feet tall"

print(prompt.invoke({"query": query}).to_string())
System: Answer the user query. Wrap the output in `json` tags
The output should be formatted as a JSON instance that conforms to the JSON schema below.

As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}
the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.

Here is the output schema:
\`\`\`
{"description": "Identifying information about all people in a text.", "properties": {"people": {"title": "People", "type": "array", "items": {"$ref": "#/definitions/Person"}}}, "required": ["people"], "definitions": {"Person": {"title": "Person", "description": "Information about a person.", "type": "object", "properties": {"name": {"title": "Name", "description": "The name of the person", "type": "string"}, "height_in_meters": {"title": "Height In Meters", "description": "The height of the person expressed in meters.", "type": "number"}}, "required": ["name", "height_in_meters"]}}}
\`\`\`
Human: Anna is 23 years old and she is 6 feet tall

现在让我们调用它:

chain = prompt | llm | parser

chain.invoke({"query": query})
People(people=[Person(name='Anna', height_in_meters=1.8288)])

要深入了解如何使用输出解析器与提示技术来生成结构化输出,请参阅本指南

自定义解析

你也可以使用LangChain表达式语言(LCEL)创建一个自定义提示和解析器,使用一个普通函数来解析模型的输出:

import json
import re
from typing import List

from langchain_core.messages import AIMessage
from langchain_core.prompts import ChatPromptTemplate
from pydantic import BaseModel, Field


class Person(BaseModel):
"""Information about a person."""

name: str = Field(..., description="The name of the person")
height_in_meters: float = Field(
..., description="The height of the person expressed in meters."
)


class People(BaseModel):
"""Identifying information about all people in a text."""

people: List[Person]


# Prompt
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"Answer the user query. Output your answer as JSON that "
"matches the given schema: \`\`\`json\n{schema}\n\`\`\`. "
"Make sure to wrap the answer in \`\`\`json and \`\`\` tags",
),
("human", "{query}"),
]
).partial(schema=People.schema())


# Custom parser
def extract_json(message: AIMessage) -> List[dict]:
"""Extracts JSON content from a string where JSON is embedded between \`\`\`json and \`\`\` tags.

Parameters:
text (str): The text containing the JSON content.

Returns:
list: A list of extracted JSON strings.
"""
text = message.content
# Define the regular expression pattern to match JSON blocks
pattern = r"\`\`\`json(.*?)\`\`\`"

# Find all non-overlapping matches of the pattern in the string
matches = re.findall(pattern, text, re.DOTALL)

# Return the list of matched JSON strings, stripping any leading or trailing whitespace
try:
return [json.loads(match.strip()) for match in matches]
except Exception:
raise ValueError(f"Failed to parse: {message}")

以下是发送给模型的提示:

query = "Anna is 23 years old and she is 6 feet tall"

print(prompt.format_prompt(query=query).to_string())
System: Answer the user query. Output your answer as JSON that  matches the given schema: \`\`\`json
{'title': 'People', 'description': 'Identifying information about all people in a text.', 'type': 'object', 'properties': {'people': {'title': 'People', 'type': 'array', 'items': {'$ref': '#/definitions/Person'}}}, 'required': ['people'], 'definitions': {'Person': {'title': 'Person', 'description': 'Information about a person.', 'type': 'object', 'properties': {'name': {'title': 'Name', 'description': 'The name of the person', 'type': 'string'}, 'height_in_meters': {'title': 'Height In Meters', 'description': 'The height of the person expressed in meters.', 'type': 'number'}}, 'required': ['name', 'height_in_meters']}}}
\`\`\`. Make sure to wrap the answer in \`\`\`json and \`\`\` tags
Human: Anna is 23 years old and she is 6 feet tall

这是我们调用它时的样子:

chain = prompt | llm | extract_json

chain.invoke({"query": query})
[{'people': [{'name': 'Anna', 'height_in_meters': 1.8288}]}]

这个页面有帮助吗?