with_structured_output()方法介绍

一般聊天模型的输出都是聊天式的字符串,但如果我们希望能把模型的输出存储到数据库,或者载入到程序,么从普通字符串到结构化的输出就尤为重要。

此时我们可以使用模型的with_structured_output()方法让它输出由输入结构指定的数据结构。

它的定义如下

1
2
3
4
5
6
7
8
9
def with_structured_output(
self,
schema: _DictOrPydanticClass | None = None,
*,
method: Literal["function_calling", "json_mode", "json_schema"] = "json_schema",
include_raw: bool = False,
strict: bool | None = None,
**kwargs: Any,
) -> Runnable[LanguageModelInput, _DictOrPydantic]:

解释下关键参数:

  • schema:可以传字典,Json Schema或者Pydantic类,前两种会返回一个字典,最后一种会返回Pydantic类
  • method:表示LLM的生成方法
    • json_schema:默认值,表示使用OpenAI的结构化输出API
    • function_calling:使用OpenAI的工具调用
    • json_mode:使用OpenAI的Json mode
  • **include_raw**:
    • 如果为 False(默认),则仅返回解析的结构化输出。如果在模型输出解析过程中发生错误,则会引发错误。
    • 如果为 True,则将返回原始模型响应(BaseMessage)和解析的模型响应。如果在输出解析过程中发生错误,它也会被捕获并返回。最终输出始终是带有键 "raw""parsed""parsing_error" 的字典。
  • **strict**:
    • 如果为 True,保证模型输出与 schema 完全匹配。输入 schema 也将根据支持的 schema 进行验证。
    • 如果为 False,输入 schema 将不会验证,模型输出也不会被验证。
    • 如果为 None(默认),则不会将 strict 参数传递给模型。
  • **tools**:要绑定到聊天模型的工具列表。要求:method'json_schema'strict=Trueinclude_raw=True。则生成的 AIMessage 将在 "raw" 中包含工具调用。
  • **kwargs(Any)**:任何附加参数都直接传递给 bind()

示例:返回Pydantic类的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from langchain_openai import ChatOpenAI
from typing import Optional
from pydantic import BaseModel,Field

class JokeSchema(BaseModel):
"""给用户讲一个笑话"""

setup: str = Field(description="这个笑话的开头")
punchline:str = Field(description="这个笑话的妙语")
rating:Optional[int] = Field(description="这个笑话的打分,满分10分", default=None)

model = ChatOpenAI(model="gpt-4o-mini")
structured_model = model.with_structured_output(JokeSchema)

print(structured_model.invoke("给我讲一个关于牛顿与苹果的笑话"))
1
2
3
4
5
D:\program_software\AnaConda\envs\langChainP13\python.exe D:\codes\code_pycharm\langChainTool\output.py 
setup='你知道牛顿和苹果之间发生了什么搞笑的事情吗?' punchline='牛顿说:‘我不是想引力,我只想摘个苹果!’' rating=7

Process finished with exit code 0

示例:返回字典的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from langchain_openai import ChatOpenAI
from typing import Optional
from typing_extensions import TypedDict,Annotated

class JokeSchema(TypedDict):
"""给用户讲一个笑话"""
setup: Annotated[str,...,"这个笑话的开头"]
punchline: Annotated[str,...,"这个笑话的妙语"]
rating: Annotated[Optional[int],None, "从0到10给这个笑话的好笑程度打分"]


model = ChatOpenAI(model="gpt-4o-mini")
structured_model = model.with_structured_output(JokeSchema)

print(structured_model.invoke("给我讲一个关于牛顿与苹果的笑话"))
1
2
3
4
D:\program_software\AnaConda\envs\langChainP13\python.exe D:\codes\code_pycharm\langChainTool\output.py 
{'setup': '为什么牛顿和苹果之间的关系总是那么紧张?', 'punchline': '因为每次牛顿想要放松时,苹果总会让他想到重力!', 'rating': 8}

Process finished with exit code 0

可以看到此时模型的输出为字典类型,如果想要像之前一样输出原始内容,则可以在方法参数中添加include_raw=True,即:

1
2
3
4
5
model = ChatOpenAI(model="gpt-4o-mini")
structured_model = model.with_structured_output(JokeSchema, include_raw=True)

result = structured_model.invoke("给我讲一个关于牛顿与苹果的笑话")
print(result)
1
{'raw': AIMessage(content='{"setup":"牛顿为什么不吃苹果?","punchline":"因为他发明了重力,知道一口下去会重到下不来!","rating":8}', additional_kwargs={'parsed': None, 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 40, 'prompt_tokens': 189, 'total_tokens': 229, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_provider': 'openai', 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_51db84afab', 'id': 'chatcmpl-CdXGJHZn0jPCiOVYj8URpJ8mz4zvd', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--95fef42f-8a7b-4c0a-be90-3fa2b2a53e47-0', usage_metadata={'input_tokens': 189, 'output_tokens': 40, 'total_tokens': 229, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), 'parsed': {'setup': '牛顿为什么不吃苹果?', 'punchline': '因为他发明了重力,知道一口下去会重到下不来!', 'rating': 8}, 'parsing_error': None}

示例:返回JSon

为了返回Json,我们需要传入Json Schema

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from langchain_openai import ChatOpenAI

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"],
}

model = ChatOpenAI(model="gpt-4o-mini")
structured_model = model.with_structured_output(json_schema)

result = structured_model.invoke("给我讲一个关于牛顿与苹果的笑话")
print(result)

输出为json格式

1
2
3
4
D:\program_software\AnaConda\envs\langChainP13\python.exe D:\codes\code_pycharm\langChainTool\output.py 
{'setup': '牛顿在树下休息,突然一个苹果掉下来了,', 'punchline': '他抬头看看,说:‘我就知道,树上有个苹果有问题!’', 'rating': 8}

Process finished with exit code 0

从多种输出格式中选择

我们可以创建多种输出格式,并最后用Union类型聚合在一个类中传入给LLM,同时他会自动选择输出格式,参照下面的代码示例,特别的,此时最后一个用于聚合的类,不需要注释解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from langchain_openai import ChatOpenAI
from typing import Optional,Union
from pydantic import BaseModel,Field

class JokeSchema(BaseModel):
"""给用户讲一个笑话"""

setup: str = Field(description="这个笑话的开头")
punchline:str = Field(description="这个笑话的妙语")
rating:Optional[int] = Field(description="这个笑话的打分,满分10分", default=None)
class ConversationalSchema(BaseModel):
"""以聊天对话的方式回应,用闲聊的语气"""

response: str = Field(description="对用户问候的回应")

class FinalResponse(BaseModel):
final_output: Union[JokeSchema, ConversationalSchema]

model = ChatOpenAI(model="gpt-4o-mini")
structured_model = model.with_structured_output(FinalResponse)

print(structured_model.invoke("给我讲一个关于牛顿与苹果的笑话"))
print(structured_model.invoke("你好,你是谁"))

输出如下,可以看到有两种输出格式

1
2
3
4
5
D:\program_software\AnaConda\envs\langChainP13\python.exe D:\codes\code_pycharm\langChainTool\output.py 
final_output=JokeSchema(setup='牛顿为了研究重力,有一天在果园里,看到一个苹果掉下来,问:', punchline='你为什么掉下来?苹果回答:因为我不想被你研究了!', rating=8)
final_output=ConversationalSchema(response='你好!我是一个人工智能助手,随时准备帮助你解答问题或聊天。你有什么想知道的呢?')

Process finished with exit code 0

部分实用场景示例

1. 作为数据提取器

我们可以让LLM作为数据提取器,从文本中提取数据并返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from typing import Optional
from pydantic import BaseModel, Field
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI

model = ChatOpenAI(model="gpt-4o-mini")
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(schema=Person)
messages = [
SystemMessage(content="你是一个提取信息的专家,只从文本中提取相关信息。如果您不知道要提取的属性的值,属性值返回null"),
HumanMessage(content="史密斯身高6英尺,金发。")
]
result = structured_model.invoke(messages)
print(result)

messages.append(result)
result = structured_model.invoke("约翰身高1.8米,是一位长着白发的黑人老人")
print(result)

输出如下

1
2
3
D:\program_software\AnaConda\envs\langChainP13\python.exe D:\codes\code_pycharm\langChainTool\output.py 
name='史密斯' hair_color='金发' skin_color=None height_in_meters='1.83'
name='约翰' hair_color='白色' skin_color='黑色' height_in_meters='1.8'

可以看到我用不同的句子问了两次,都很好地提取到了信息

OutParser输出解析器介绍

之前我们使用的是接口,接下来我们介绍一下使用Runnable组件实现输出解析器

StrOutputParser解析文本输出

我们已经用过很多回这个输出解析器了,他能提取AIMessage中的主要文本,这里不多做介绍

PydanticOutputParser使用Pydantic类描述输出

它来自langchain_core.output_parsers.pydantic.PydanticOutputParser,初始化的时候需要按名称pydantic_object传入一个描述输出结构的Pydantic类,同时它还提供了invoke()方法和get_format_instructions() → str方法,其中后面的那个方法用于填充提示词模板

比如我们让LLM按模板输出一个笑话

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import PydanticOutputParser
from typing import Optional
from pydantic import BaseModel,Field
from langchain_core.prompts import PromptTemplate

class JokeSchema(BaseModel):
"""给用户讲一个笑话"""

setup: str = Field(description="这个笑话的开头")
punchline:str = Field(description="这个笑话的妙语")
rating:Optional[int] = Field(description="这个笑话的打分,满分10分", default=None)

parser = PydanticOutputParser(pydantic_object=JokeSchema)

prompt = PromptTemplate(
template="{query}\nAnswer in this following template\n{template}",
input_variables=["query"],
partial_variables={"template":parser.get_format_instructions()}
)

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

chain = prompt | model | parser
print(chain.invoke({"query":"给我讲一个关于牛顿与苹果的笑话"}))

其它输出

langchain还提供了其它描述输出的方法,使用起来没有太大差别:

  • Json解析器:JsonOutputParser
  • XML解析器:XMLOutputParser
  • Yaml解析器:YamlOutputParser
  • CSV解析器:CommaSeparatedListOutputParser
  • 枚举解析器:EnumOutputParser
  • ⽇期解析器:DatetimeOutputParser
    等等,更多类型参考这⾥