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

先决条件

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

通常有一个模型返回与特定模式匹配的输出很有用。一个常见的用例是从文本中提取数据以插入到数据库中或在某些其他下游系统中使用。本指南涵盖了从模型获取结构化输出的几种策略。

.with_structured_output() 方法

支持的模型

您可以在此处找到支持此方法的模型列表

这是获取结构化输出的最简单和最可靠的方法。with_structured_output() 方法针对提供本机 API 以用于结构化输出的模型实现,如工具/函数调用或 JSON 模式,并在幕后使用这些功能。

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

举个例子,让我们让一个模型生成一个笑话,并将设置和笑话分开:

  • OpenAI
  • Anthropic
  • Azure
  • Google
  • Cohere
  • FireworksAI
  • MistralAI
  • TogetherAI
  1. pip install -qU langchain-openai
  1. import getpass
  2. import os
  3. os.environ["OPENAI_API_KEY"] = getpass.getpass()
  4. from langchain_openai import ChatOpenAI
  5. llm = ChatOpenAI(model="gpt-3.5-turbo-0125")
  1. pip install -qU langchain-anthropic
  1. import getpass
  2. import os
  3. os.environ["ANTHROPIC_API_KEY"] = getpass.getpass()
  4. from langchain_anthropic import ChatAnthropic
  5. llm = ChatAnthropic(model="claude-3-sonnet-20240229")
  1. pip install -qU langchain-openai
  1. import getpass
  2. import os
  3. os.environ["AZURE_OPENAI_API_KEY"] = getpass.getpass()
  4. from langchain_openai import AzureChatOpenAI
  5. llm = AzureChatOpenAI(
  6. azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
  7. azure_deployment=os.environ["AZURE_OPENAI_DEPLOYMENT_NAME"],
  8. openai_api_version=os.environ["AZURE_OPENAI_API_VERSION"],
  9. )
  1. pip install -qU langchain-google-vertexai
  1. import getpass
  2. import os
  3. os.environ["GOOGLE_API_KEY"] = getpass.getpass()
  4. from langchain_google_vertexai import ChatVertexAI
  5. llm = ChatVertexAI(model="gemini-pro")
  1. pip install -qU langchain-cohere
  1. import getpass
  2. import os
  3. os.environ["COHERE_API_KEY"] = getpass.getpass()
  4. from langchain_cohere import ChatCohere
  5. llm = ChatCohere(model="command-r")
  1. pip install -qU langchain-fireworks
  1. import getpass
  2. import os
  3. os.environ["FIREWORKS_API_KEY"] = getpass.getpass()
  4. from langchain_fireworks import ChatFireworks
  5. llm = ChatFireworks(model="accounts/fireworks/models/mixtral-8x7b-instruct")
  1. pip install -qU langchain-mistralai
  1. import getpass
  2. import os
  3. os.environ["MISTRAL_API_KEY"] = getpass.getpass()
  4. from langchain_mistralai import ChatMistralAI
  5. llm = ChatMistralAI(model="mistral-large-latest")
  1. pip install -qU langchain-openai
  1. import getpass
  2. import os
  3. os.environ["TOGETHER_API_KEY"] = getpass.getpass()
  4. from langchain_openai import ChatOpenAI
  5. llm = ChatOpenAI(
  6. base_url="https://api.together.xyz/v1",
  7. api_key=os.environ["TOGETHER_API_KEY"],
  8. model="mistralai/Mixtral-8x7B-Instruct-v0.1",
  9. )

如果我们希望模型返回一个 Pydantic 对象,我们只需要传入所需的 Pydantic 类:

  1. from typing import Optional
  2. from langchain_core.pydantic_v1 import BaseModel, Field
  3. class Joke(BaseModel):
  4. """Joke to tell user."""
  5. setup: str = Field(description="The setup of the joke")
  6. punchline: str = Field(description="The punchline to the joke")
  7. rating: Optional[int] = Field(description="How funny the joke is, from 1 to 10")
  8. structured_llm = llm.with_structured_output(Joke)
  9. structured_llm.invoke("Tell me a joke about cats")
  1. Joke(setup='Why was the cat sitting on the computer?', punchline='To keep an eye on the mouse!', rating=None)

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

如果您不想使用 Pydantic,则还可以传入 JSON Schema 字典。在这种情况下,响应也是一个字典:

  1. json_schema = {
  2. "title": "joke",
  3. "description": "Joke to tell user.",
  4. "type": "object",
  5. "properties": {
  6. "setup": {
  7. "type": "string",
  8. "description": "The setup of the joke",
  9. },
  10. "punchline": {
  11. "type": "string",
  12. "description": "The punchline to the joke",
  13. },
  14. "rating": {
  15. "type": "integer",
  16. "description": "How funny the joke is, from 1 to 10",
  17. },
  18. },
  19. "required": ["setup", "punchline"],
  20. }
  21. structured_llm = llm.with_structured_output(json_schema)
  22. structured_llm.invoke("Tell me a joke about cats")

在多个模式之间进行选择

让模型从多个模式中选择的最简单方法是创建一个父类 Pydantic,该类具有一个 Union 类型的属性:

  1. from typing import Union
  2. class ConversationalResponse(BaseModel):
  3. """以对话方式回应。友善而有帮助。"""
  4. response: str = Field(description="对用户查询的对话回应")
  5. class Response(BaseModel):
  6. output: Union[Joke, ConversationalResponse]
  7. structured_llm = llm.with_structured_output(Response)
  8. structured_llm.invoke("给我讲个关于猫的笑话")
  1. Response(output=Joke(setup='为什么猫坐在电脑上?', punchline='为了盯着老鼠呀!', rating=8))
  1. structured_llm.invoke("你今天怎么样?")
  1. Response(output=ConversationalResponse(response="我只是一个数字助手,所以我没有感觉,但我在这里,随时为您提供帮助。您今天需要我如何帮助您?"))

或者,您可以直接使用工具调用,让模型在选项之间进行选择,如果您选择的模型支持的话。这涉及一些更复杂的解析和设置,但在某些情况下,由于不必使用嵌套模式,可以实现更好的性能。请参阅此操作指南获取更多详情。

流式输出

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

信息

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

  1. structured_llm = llm.with_structured_output(json_schema)
  2. for chunk in structured_llm.stream("给我讲个关于猫的笑话"):
  3. print(chunk)
  1. {}{'setup': ''}{'setup': '为什么'}{'setup': '为什么猫'}{'setup': '为什么猫坐'}{'setup': '为什么猫坐在'}{'setup': '为什么猫坐在电脑'}{'setup': '为什么猫坐在电脑上'}{'setup': '为什么猫坐在电脑上?'}{'setup': '为什么猫坐在电脑上?', 'punchline': ''}{'setup': '为什么猫坐在电脑上?', 'punchline': '因为'}{'setup': '为什么猫坐在电脑上?', 'punchline': '因为它'}{'setup': '为什么猫坐在电脑上?', 'punchline': '因为它想'}{'setup': '为什么猫坐在电脑上?', 'punchline': '因为它想'}{'setup': '为什么猫坐在电脑上?', 'punchline': '因为它想要'}{'setup': '为什么猫坐在电脑上?', 'punchline': '因为它想要'}{'setup': '为什么猫坐在电脑上?', 'punchline': '因为它想要看'}{'setup': '为什么猫坐在电脑上?', 'punchline': '因为它想要看'}{'setup': '为什么猫坐在电脑上?', 'punchline': '因为它想要看着'}{'setup': '为什么猫坐在电脑上?', 'punchline': '因为它想要看着'}{'setup': '为什么猫坐在电脑上?', 'punchline': '因为它想要看着老'}{'setup': '为什么猫坐在电脑上?', 'punchline': '因为它想要看着老'}{'setup': '为什么猫坐在电脑上?', 'punchline': '因为它想要看着老鼠'}{'setup': '为什么猫坐在电脑上?', 'punchline': '因为它想要看着老鼠!'}{'setup': '为什么猫坐在电脑上?', 'punchline': '因为它想要看着老鼠!', 'rating': 8}

少样本提示

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

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

  1. from langchain_core.prompts import ChatPromptTemplate
  2. system = """你是一个风趣的喜剧演员。你的特长是敲门笑话。\返回一个包含设置(回应“谁在那儿?”的回答)和最终妙语(对“<设置>谁?”的回答)的笑话。这是一些笑话的例子:example_user: 告诉我一个关于飞机的笑话example_assistant: {{"setup": "为什么飞机永远不累?", "punchline": "因为它们有休息的翅膀!", "rating": 2}}example_user: 再告诉我一个关于飞机的笑话example_assistant: {{"setup": "货物", "punchline": "货物‘嗡嗡’,但飞机‘嗖嗖’!", "rating": 10}}example_user: 现在关于毛毛虫example_assistant: {{"setup": "毛毛虫", "punchline": "毛毛虫真的很慢,但看我变成蝴蝶,独领风骚!", "rating": 5}}"""
  3. prompt = ChatPromptTemplate.from_messages([("system", system), ("human", "{input}")])
  4. few_shot_structured_llm = prompt | structured_llm
  5. few_shot_structured_llm.invoke("关于啄木鸟有什么有趣的事情")

API 参考:ChatPromptTemplate

  1. {'setup': '啄木鸟', 'punchline': "啄木鸟敲敲门,但不用担心,它们从来不指望你开门!", 'rating': 8}

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

  1. from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
  2. examples = [
  3. HumanMessage("告诉我一个关于飞机的笑话", name="example_user"),
  4. AIMessage(
  5. "",
  6. name="example_assistant",
  7. tool_calls=[
  8. {
  9. "name": "joke",
  10. "args": {
  11. "setup": "为什么飞机永远不累?",
  12. "punchline": "因为它们有休息的翅膀!",
  13. "rating": 2,
  14. },
  15. "id": "1",
  16. }
  17. ],
  18. ),
  19. # 大多数工具调用模型希望在具有工具调用的 AIMessage 之后跟随一个 ToolMessage。
  20. ToolMessage("", tool_call_id="1"),
  21. # 有些模型还希望在任何 ToolMessage 之后跟随一个 AIMessage,
  22. # 因此您可能需要在这里添加一个 AIMessage。
  23. HumanMessage("再告诉我一个关于飞机的笑话", name="example_user"),
  24. AIMessage(
  25. "",
  26. name="example_assistant",
  27. tool_calls=[
  28. {
  29. "name": "joke",
  30. "args": {
  31. "setup": "货物",
  32. "punchline": "货物‘嗡嗡’,但飞机‘嗖嗖’!",
  33. "rating": 10,
  34. },
  35. "id": "2",
  36. }
  37. ],
  38. ),
  39. ToolMessage("", tool_call_id="2"),
  40. HumanMessage("现在关于毛毛虫", name="example_user"),
  41. AIMessage(
  42. "",
  43. tool_calls=[
  44. {
  45. "name": "joke",
  46. "args": {
  47. "setup": "毛毛虫",
  48. "punchline": "毛毛虫真的很慢,但看我变成蝴蝶,独领风骚!",
  49. "rating": 5,
  50. },
  51. "id": "3",
  52. }
  53. ],
  54. ),
  55. ToolMessage("", tool_call_id="3"),
  56. ]
  57. system = """你是一个风趣的喜剧演员。你的特长是敲门笑话。 \ 返回一个包含设置(回应“谁在那儿?”的回答) \ 和最终妙语(对“<设置>谁?”的回答)的笑话。"""
  58. prompt = ChatPromptTemplate.from_messages(
  59. [("system", system), ("placeholder", "{examples}"), ("human", "{input}")]
  60. )
  61. few_shot_structured_llm = prompt | structured_llm
  62. few_shot_structured_llm.invoke({"input": "鳄鱼", "examples": examples})

API 参考:AIMessage | HumanMessage | ToolMessage

  1. {'setup': '鳄鱼', 'punchline': "鳄鱼‘回头见’,但一会儿,它就变成了鳄鱼!", 'rating': 7}

有关在使用工具调用时进行少样本提示的更多信息,请参阅此处

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

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

JSON 模式

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

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

  1. structured_llm = llm.with_structured_output(Joke, method="json_mode")
  2. structured_llm.invoke(
  3. "给我讲个关于猫的笑话,以 JSON 格式返回带有 `setup` 和 `punchline` 键"
  4. )
  1. Joke(setup='为什么猫坐在电脑上?', punchline='因为它想要看着老鼠!', rating=None)

直接提示和解析模型

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

使用 PydanticOutputParser

以下示例使用内置的 PydanticOutputParser 来解析通过提示匹配给定 Pydantic 模式的聊天模型的输出。请注意,我们直接从解析器的方法向提示中添加了 format_instructions

  1. from typing import List
  2. from langchain_core.output_parsers import PydanticOutputParser
  3. from langchain_core.prompts import ChatPromptTemplate
  4. from langchain_core.pydantic_v1 import BaseModel, Field
  5. class Person(BaseModel):
  6. """人员信息。"""
  7. name: str = Field(..., description="人员姓名")
  8. height_in_meters: float = Field(
  9. ..., description="人员身高(以米为单位)"
  10. )
  11. class People(BaseModel):
  12. """文本中所有人员的标识信息。"""
  13. people: List[Person]
  14. # 设置解析器
  15. parser = PydanticOutputParser(pydantic_object=People)
  16. # 提示
  17. prompt = ChatPromptTemplate.from_messages(
  18. [
  19. (
  20. "system",
  21. "回答用户的查询。将输出格式化为 `json` 标签中的内容\n{format_instructions}",
  22. ),
  23. ("human", "{query}"),
  24. ]
  25. ).partial(format_instructions=parser.get_format_instructions())

API 参考:PydanticOutputParser | ChatPromptTemplate

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

  1. query = "Anna 今年 23 岁,身高 6 英尺"
  2. print(prompt.invoke(query).to_string())
  1. System: 回答用户的查询。将输出格式化为 `json` 标签中的内容
  2. 输出应格式化为符合以下 JSON 模式的 JSON 实例。
  3. 例如,对于模式 {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]},对象 {"foo": ["bar", "baz"]} 是模式的格式良好实例。对象 {"properties": {"foo": ["bar", "baz"]}} 不是格式良好的实例。
  4. 下面是输出模式:
  5. {"description": "文本中所有人员的标识信息。", "properties": {"people": {"title": "People", "type": "array", "items": {"$ref": "#/definitions/Person"}}}, "required": ["people"], "definitions": {"Person": {"title": "Person", "description": "人员信息。", "type": "object", "properties": {"name": {"title": "Name", "description": "人员姓名", "type": "string"}, "height_in_meters": {"title": "Height In Meters", "description": "人员身高(以米为单位)", "type": "number"}}, "required": ["name", "height_in_meters"]}}}
  1. Human: Anna 今年 23 岁,身高 6 英尺

现在让我们调用它:

  1. chain = prompt | llm | parser
  2. chain.invoke({"query": query})
  1. People(people=[Person(name='Anna', height_in_meters=1.8288)])

要了解更多关于使用输出解析器进行结构化输出的提示技术,请参阅此指南

自定义解析

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

  1. import json
  2. import re
  3. from typing import List
  4. from langchain_core.messages import AIMessage
  5. from langchain_core.prompts import ChatPromptTemplate
  6. from langchain_core.pydantic_v1 import BaseModel, Field
  7. class Person(BaseModel):
  8. """人员信息。"""
  9. name: str = Field(..., description="人员姓名")
  10. height_in_meters: float = Field(
  11. ..., description="人员身高(以米为单位)"
  12. )
  13. class People(BaseModel):
  14. """文本中所有人员的标识信息。"""
  15. people: List[Person]
  16. # 提示
  17. prompt = ChatPromptTemplate.from_messages(
  18. [
  19. (
  20. "system",
  21. "回答用户的查询。将输出格式化为 `json` 标签中的内容\n```json\n{schema}\n```。"
  22. "确保将答案包裹在 ```json 和 ``` 标签中",
  23. ),
  24. ("human", "{query}"),
  25. ]
  26. ).partial(schema=People.schema())
  27. # 自定义解析器
  28. def extract_json(message: AIMessage) -> List[dict]:
  29. """从包含在字符串中的 `json` 标签之间的 JSON 中提取 JSON 内容。
  30. 参数:
  31. text (str): 包含 JSON 内容的文本。
  32. 返回:
  33. list: 提取的 JSON 字符串列表。
  34. """
  35. text = message.content
  36. # 定义正则表达式模式以匹配 JSON```python
  37. blocks
  38. pattern = r"```json(.*?)```"
  39. # 在字符串中找到模式的所有非重叠匹配
  40. matches = re.findall(pattern, text, re.DOTALL)
  41. # 返回匹配的 JSON 字符串列表,去除任何前导或尾随空格
  42. try:
  43. return [json.loads(match.strip()) for match in matches]
  44. except Exception:
  45. raise ValueError(f"解析失败:{message}")

API 参考:AIMessage | ChatPromptTemplate

这是发送给模型的提示:

  1. query = "Anna 今年 23 岁,身高 6 英尺"
  2. print(prompt.format_prompt(query=query).to_string())
  1. System: Answer the user query. Output your answer as JSON that matches the given schema: ```json
  2. {'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']}}}
  3. ```. 确保将回答包裹在 ```json and ``` 标签之间
  4. Human: Anna is 23 years old and she is 6 feet tall

现在让我们调用它:

  1. chain = prompt | llm | extract_json
  2. chain.invoke({"query": query})
  1. [{'people': [{'name': 'Anna', 'height_in_meters': 1.8288}]}]