Langchain解锁LLM大语言模型的结构化输出能力(多种实现方案)

在 LangChain解锁LLM大语言模型的结构化输出能力:调用 with_structured_output() 方法 这篇博客中,我们了解了格式化LLM输出内容的必要性以及如何通过调用langchain框架中提供的 with_structured_output() 方法对LLM输出进行格式化(三种可选方式:基于 TypedDict 类(类型化字典)、JSON Schema(JSON 模式)和 Pydantic 类)。在本篇博客中,我们将进一步学习了解对模型输出进行结构化控制的其他方案,分别是 少样本示例引导(Few-shot prompting)、结构化方法指定(Specifying the method for structuring outputs)和 直接解析模型输出(Direct prompting and parsing)。

少样本示例引导(Few-shot prompting)

当我们希望规范LLM输出格式比较复杂的时候,模型的格式化输出可能不会如预期稳定,这个时候我们可以采用一个非常简单高效的方法对模型输出质量的稳定性进行控制,那就是 few-shot prompting(少样本提示是一种在自然语言处理中,通过借助少量示例来引导语言模型生成预期输出的技术)。

代码实现如下:

  1. 我们首先还是基于 Pydantic 的方式去定义LLM输出的格式,然后调用 .with_structured_output() 方法创建 structured_llm 变量;
from langchain_ollama import ChatOllama
from typing import Optional
from pydantic import BaseModel, Field
from langchain_core.prompts import ChatPromptTemplate

# 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"
    )

llm = ChatOllama(model = "llama3.1:latest", 
                 temperature = 0.8)
structured_llm = llm.with_structured_output(Joke)
  1. 接着,我们使用 ChatPromptTemplate 从消息列表中创建一个提示词模板,并且将几个符合输出格式的实例放入到 system prompt 中实现 few-shot prompt,然后再通过 | 管道操作将 prompt 传递给结构化的语言模型进行处理(以下代码中文注释由 DeepSeek-R1 生成);
# 定义系统提示信息
# 告知语言模型它的角色是一个滑稽的喜剧演员,专长是敲敲门笑话
# 并说明了笑话的结构,即需要包含设置(对 "Who's there?" 的回应)和最终笑点(对 "<设置> who?" 的回应)
# 还给出了几个关于不同主题的笑话示例,让语言模型了解输出的格式
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}}"""

# 使用 ChatPromptTemplate 从消息列表中创建一个提示模板
# 消息列表包含一个系统消息和一个用户消息
# 系统消息是上面定义的系统提示信息
# 用户消息使用占位符 {input},表示后续可以传入具体的用户输入,例如用户想要的笑话主题
prompt = ChatPromptTemplate.from_messages([("system", system), ("human", "{input}")])

# 将提示模板与结构化的语言模型进行组合
# 这里将 prompt 和 structured_llm 进行管道操作(|)
# 意味着先根据提示模板处理输入,然后将处理后的结果传递给结构化的语言模型进行处理
few_shot_structured_llm = prompt | structured_llm
  1. 接着我们调用模型,让LLM基于用户提问进行符合预期的结构化输出。
# 调用组合后的模型,传入用户输入 "what's something funny about woodpeckers"
# 表示用户想要一个关于啄木鸟的有趣笑话
# 模型会根据系统提示中的要求和示例,生成一个符合格式的敲敲门笑话
response = few_shot_structured_llm.invoke("what's something funny about woodpeckers")
print(response)
setup='Woodpecker' punchline="They're always drumming up some laughter!" rating=8

结构化方法指定(Specifying the method for structuring outputs)

其实还有一种调用 with_structured_output() 方法但只给 method 的参数传递变量的方式可以对LLM的输出进行结构化,我们先来看一下代码实现(代码中文注释由 Doubao-1.5-pro-32k 生成):

# 从 langchain_ollama 库中导入 ChatOllama 类
from langchain_ollama import ChatOllama

# 创建一个 ChatOllama 实例
# model 参数指定要使用的模型,这里使用的是 "llama3.1:latest" 模型
# temperature 参数控制生成文本的随机性,值越大越随机,这里设置为 0.8
llm = ChatOllama(model="llama3.1:latest", 
                 temperature=0.8)

# 为 llm 实例添加结构化输出功能
# 第一个参数传入 None,表示不使用自定义的schema
# method 参数指定使用 "json_mode" 方法,即输出为 JSON 格式
structured_llm = llm.with_structured_output(None, method="json_mode")

# 调用结构化的语言模型
# 传入的提示信息是 "Tell me a joke about cats, respond in JSON with `setup` and `punchline` keys"
# 意思是让模型讲一个关于猫的笑话,并以包含 `setup`(笑话的铺垫)和 `punchline`(笑话的笑点)键的 JSON 格式响应
structured_llm.invoke(
    "Tell me a joke about cats, respond in JSON with `setup` and `punchline` keys"
)

从以上代码可以注意到,虽然我们调用的依旧是 with_structured_output() 方法,但第一个参数我们传递的是 None,也就是我们并没有传入基于 TypedDict 类(类型化字典)、JSON Schema(JSON 模式)和 Pydantic 类 这三种方式声明的schema,而是通过制定 method="json_mode",加上在用户提问中特别说明的 “respond in JSON with setup and punchline keys”的方式实现对LLM输出内容的结构化。当然对于输出schema比较复杂的情况,这种方式的处理效果有待考量和验证。

直接解析模型输出(Direct prompting and parsing)

需要特别注意的是,并非所有模型都支持 .with_structured_output() 方法,因为并非所有模型都支持工具调用或 JSON 模式。那么对于这类模型,我们可以直接提示模型使用特定的格式,然后使用输出解析器从模型的原始输出中提取结构化的响应内容。以下结合代码介绍两种实现方法,分别是使用langchain 框架内置的PydanticOutputParser 和自定义解析器。

使用 PydanticOutputParser

以下示例使用langchain内置的 PydanticOutputParser 来解析LLM的输出,即通过提示词工程,直接将定义模型输出的Pydantic 模式添加到system prompt中进行实现(本质上就是提示词工程),然后再对LLM的输出内容进行后置解析的思路进行实现。

实现代码如下(中文注释由 DeepSeek-R1 生成):

# 导入 List 类型提示,用于类型注解,表明变量将是一个列表
from typing import List

# 从 langchain_core.output_parsers 模块导入 PydanticOutputParser 类
# 该类用于将文本输出解析为 Pydantic 模型实例
from langchain_core.output_parsers import PydanticOutputParser
# 从 langchain_core.prompts 模块导入 ChatPromptTemplate 类
# 该类用于创建聊天提示模板
from langchain_core.prompts import ChatPromptTemplate
# 从 pydantic 模块导入 BaseModel 和 Field 类
# BaseModel 是 Pydantic 模型的基类,Field 用于定义模型字段的元数据
from pydantic import BaseModel, Field


# 定义一个名为 Person 的 Pydantic 模型类
# 该类用于表示一个人的信息
class Person(BaseModel):
    """Information about a person."""
    # 定义 name 字段,为字符串类型
    # ... 表示该字段是必需的
    # description 为该字段提供描述信息
    name: str = Field(..., description="The name of the person")
    # 定义 height_in_meters 字段,为浮点数类型
    # 表示人的身高,单位为米
    # ... 表示该字段是必需的
    # description 为该字段提供描述信息
    height_in_meters: float = Field(
        ..., description="The height of the person expressed in meters."
    )


# 定义一个名为 People 的 Pydantic 模型类
# 该类用于表示文本中所有人物的识别信息
class People(BaseModel):
    """Identifying information about all people in a text."""
    # 定义 people 字段,为 Person 类型的列表
    people: List[Person]


# 创建一个 PydanticOutputParser 实例
# 将其 pydantic_object 属性设置为 People 类
# 用于将文本输出解析为 People 模型实例
parser = PydanticOutputParser(pydantic_object=People)

# 创建一个聊天提示模板实例
# 使用 from_messages 方法从消息列表中创建模板
prompt = ChatPromptTemplate.from_messages(
    [
        (
            # 系统消息,提示回答用户查询
            # 并将输出用 `json` 标签包裹
            # {format_instructions} 是一个占位符,将在后续填充解析指令
            "system",
            "Answer the user query. Wrap the output in `json` tags\n{format_instructions}"
        ),
        (
            # 用户消息,{query} 是一个占位符,将在后续填充用户的查询内容
            "human",
            "{query}"
        )
    ]
).partial(format_instructions=parser.get_format_instructions())

我们来看一下将 user query传入之后,构成的完整prompt内容是什么样的。

query = "Anna is 23 years old and she is 6 feet tall"
print(prompt.invoke({"query": query}).to_string())

prompt

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

最后我们基于 prompt, llmparser 通过管道创建一个链式调用:

# 以下代码注释由 Doubao-1.5-pro-32k 生成

# 使用 LangChain 提供的管道操作符 `|` 来创建一个链式调用。
# 这个链式调用的作用是将多个组件按顺序连接起来,形成一个处理流程。
# 具体来说,这个链式调用包含三个组件:prompt、llm 和 parser。
# 1. prompt:这是之前创建的聊天提示模板实例。它的作用是根据用户输入的查询内容和解析指令生成合适的提示信息。
# 2. llm:这是一个语言模型实例,例如 OpenAI 的 GPT 系列模型等。它接收来自 prompt 生成的提示信息,然后根据这个提示信息生成相应的文本输出。
# 3. parser:这是之前创建的 PydanticOutputParser 实例。它的作用是将 llm 生成的文本输出解析为 People 模型实例,方便后续对输出数据进行结构化处理。
# 最终,chain 就是一个包含了提示生成、语言模型调用和输出解析这三个步骤的处理链。
chain = prompt | llm | parser

# 调用 chain 的 invoke 方法来执行这个处理链。
# invoke 方法接收一个字典作为参数,字典中的键 "query" 对应的值就是用户输入的查询内容。
# 处理链会按照之前定义的顺序依次执行各个组件:
# 首先,prompt 会根据传入的查询内容和解析指令生成提示信息。
# 然后,这个提示信息会被传递给 llm,llm 基于提示信息生成文本输出。
# 最后,parser 会将 llm 生成的文本输出解析为 People 模型实例。
# 整个处理过程完成后,会返回经过解析后的结构化数据,方便后续的业务逻辑使用。
chain.invoke({"query": query})

打印查看格式化后的输出,完全符合 People 类中定义的schema。

People(people=[Person(name='Anna', height_in_meters=1.8288)])

自定义解析器

为了增加结构化LLM输出的灵活性,我们可以使用LangChain表达语言( LangChain Expression Language (LCEL)),来自定义prompt和解析器(通过创建一个函数的方式)。

import json
import re
from typing import List
from langchain_ollama import ChatOllama
from langchain_core.messages import AIMessage
from langchain_core.prompts import ChatPromptTemplate
from pydantic import BaseModel, Field


llm = ChatOllama(model = "llama3.1:latest", 
                 temperature = 0.8)

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,我们通过system prompt,告知LLM要将输出内容wrap在<json></json>标签中
prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "Answer the user query. Output your answer as JSON that  "
            "matches the given schema: <json>\n{schema}\n</json>. "
            "Make sure to wrap the answer in <json> and </json> tags",
        ),
        ("human", "{query}"),
    ]
).partial(schema=People.model_json_schema())

我们看一下传入 user query后完整的prompt内容:

query = "Anna is 23 years old and she is 6 feet tall"
print(prompt.format_prompt(query=query).to_string())

prompt

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

通过创建名为 extract_json 的函数自定义 parser:

# Custom parser
def extract_json(message: AIMessage) -> List[dict]:
    """Extracts JSON content from a string where JSON is embedded between <json> and </json> 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>(.*?)</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}")

最后我们基于 prompt, llm 和 自定义用来解析大模型输出内容的parser,通过管道创建一个链式调用:

chain = prompt | llm | extract_json
chain.invoke({"query": query})

调用 chain 后得到的结构化输出如下:

[{'$defs': {'Person': {'description': 'Information about a person.',
    'properties': {'name': {'description': 'The name of the person',
      'title': 'Name',
      'type': 'string'},
     'height_in_meters': {'description': 'The height of the person expressed in meters.',
      'title': 'Height In Meters',
      'type': 'number'}},
    'required': ['name', 'height_in_meters'],
    'title': 'Person',
    'type': 'object'}},
  'description': 'Identifying information about all people in a text.',
  'properties': {'people': {'items': {'$ref': '#/$defs/Person'},
    'title': 'People',
    'type': 'array'}},
  'required': ['people'],
  'title': 'People',
  'type': 'object',
  'people': [{'name': 'Anna', 'height_in_meters': 1.8288}]}]

但以上并非预期的输出,预期输出应该如下:

[{'people': [{'name': 'Anna', 'height_in_meters': 1.8288}]}]

至于原因目前还不清楚,初步猜测是因为选用的 llama3.1:latest 模型因为量化程度较高导致对带输出格式说明的system prompt的理解力不够;当然也有可能是官方文档里提供的示例代码有些问题。(我在本地第一次运行的时候是报错的,对代码稍微调整了一下才fix了报错的问题)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/982304.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于ANTLR4的大数据SQL编辑器解析引擎实践|得物技术

一、背景 随着得物离线业务的快速增长&#xff0c;为了脱离全托管服务的一些限制和享受技术发展带来的成本优化&#xff0c;公司提出了大数据Galaxy开源演进项目&#xff0c;将离线业务从全托管且封闭的环境迁移到一个开源且自主可控的生态系统中&#xff0c;而离线开发治理套…

HTML第四节

一.复合选择器 1.后代选择器 注&#xff1a;1.后代选择器会选中后代所有的要选择的标签 2.儿子选择器 3.并集选择器 注&#xff1a;1.注意换行&#xff0c;同时选中多种标签 4.交集选择器 注&#xff1a;1.标签选择器放在最前面&#xff0c;例如放在类选择器的前面 2.两个选择…

数字后端培训实战项目六大典型后端实现案例

Q1:请教一个问题&#xff0c;cts.sdc和func.sdc在innovus用如何切换&#xff1f;在flow哪一步切换输入cts.sdc&#xff1f;哪一步切换到func.sdc&#xff0c;具体如何操作&#xff1f; 这个学员其实就是在问使用分段长clock tree时具体的flow流程是怎么样的&#xff1f;针对时…

linux 使用阿里云盘 阿里网盘

可参考&#xff1a;https://blog.csdn.net/qq_41174671/article/details/127310715 一、对文件<100M的可以使用如下方法&#xff1a; 1.临时使用阿里网盘 wget https://github.com/tickstep/aliyunpan/releases/download/v0.2.7/aliyunpan-v0.2.7-linux-amd64.zip unzip …

性能测试--大厂面试问题解析

性能测试作为软件开发中的关键环节&#xff0c;确保系统在高负载下仍能高效运行。面试中&#xff0c;相关问题常被问及&#xff0c;包括定义、类型、工具选择、规划与执行方法、常见瓶颈、敏捷环境下的处理、监控作用、虚拟用户数量确定、功能测试的区别、JMeter工作原理、HTTP…

hom_mat2d_to_affine_par 的c#实现

hom_mat2d_to_affine_par 的c#实现 背景&#xff1a;为课室贡献一个通用函数&#xff0c;实现halcon算子的同等效果&#xff0c;查询csdn未果&#xff0c;deepseek二哥与chtgpt大哥给不了最终程序&#xff0c;在大哥与二哥帮助下&#xff0c;最终实现同等效果。 踩坑&#xf…

前端跨域设置 withCredentials: true

在做登录认证的时候&#xff0c;会出现请求未登录的情况&#xff0c;查看请求头的时候发现并没有把登录时的cookie设置到第二次的请求头里面。查看资料才知道跨域请求要想带上cookie&#xff0c;必须要在ajax请求里加上 withCredentials: true 再次访问发现请求头可以携带cook…

【漫话机器学习系列】122.相关系数(Correlation Coefficient)

深入理解相关系数&#xff08;Correlation Coefficient&#xff09; 1. 引言 在数据分析、统计学和机器学习领域&#xff0c;研究变量之间的关系是至关重要的任务。我们常常想知道&#xff1a;当一个变量变化时&#xff0c;另一个变量是否也会随之变化&#xff1f;如果会&…

uniapp+微信小程序+地图+传入多个标记点显示+点击打开内置地图导航+完整代码

一、效果展示 二、完整代码 <template><view class"container"><map class"map-container" :latitude"latitude" :longitude"longitude" :markers"markers" :controls"controls" show-location m…

高效数据管理:WPF中实现带全选功能的DataGrid复选框列

目录 引言项目结构与准备工作XAML布局设计后台代码实现视图模型设计总结引言 在许多应用场景中,我们需要在表格中选择多行数据进行批量操作,如删除、导出等。为了提高用户体验,通常会在表格的第一列添加一个复选框,允许用户逐个或批量选择数据项。本文将详细介绍如何在 WP…

一周学会Flask3 Python Web开发-SQLAlchemy简介及安装

锋哥原创的Flask3 Python Web开发 Flask3视频教程&#xff1a; 2025版 Flask3 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili SQLAlchemy是Python编程语言下的一款开源软件。提供了SQL工具包及对象关系映射&#xff08;ORM&#xff09;工具&#xff0c;…

《Linux C 智能 IO 矩阵:输入输出的自适应数据流转》

1. 标准库IO简介 标准库IO特点&#xff1a;通过操作系统提供的接口&#xff08;API&#xff09;和操作系统进行交互。&#xff08;接近100个函数&#xff09; 1.1. IO的过程 操作系统&#xff1a;向上为用户提供操作接口&#xff0c;向下为统筹控制硬件。 操作系统的组成&#…

【大模型理论篇】--Mixture of Experts架构

Mixture of Experts&#xff08;MoE&#xff0c;专家混合&#xff09;【1】架构是一种神经网络架构&#xff0c;旨在通过有效分配计算负载来扩展模型规模。MoE架构通过在推理和训练过程中仅使用部分“专家”&#xff08;子模型&#xff09;&#xff0c;优化了资源利用率&#x…

智能云呼叫中心如何升级企业服务?带来新商机

当某国际零售品牌通过部署云呼叫中心将客服响应效率提升73%时&#xff0c;这场由云计算掀起的客户服务革命已悄然渗透到企业运营的毛细血管。在数字化转型的深水区&#xff0c;云呼叫中心正从"成本中心"蜕变为"价值引擎"&#xff0c;推动企业构建差异化的服…

java+jvm笔记

JUC synchornized原理&#xff08;java锁机制&#xff09;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 升级顺序&#xff1a; 无锁偏向锁&#xff0c;只有一个线程来访问轻量级锁&#xff0c;有两个线程交替访问重锁&#xff0c;两个及以上线…

CES Asia 2025增设未来办公教育板块,科技变革再掀高潮

作为亚洲消费电子领域一年一度的行业盛会&#xff0c;CES Asia 2025&#xff08;第七届亚洲消费电子技术贸易展&#xff09;即将盛大启幕。今年展会规模再度升级&#xff0c;预计将吸引超过500家全球展商参展&#xff0c;专业观众人数有望突破10万。除了聚焦人工智能、物联网、…

Sass 模块化革命:深入解析 @use 语法,打造高效 CSS 架构

文章目录 前言use 用法1. 模块化与命名空间2. use 中 as 语法的使用3. as * 语法的使用4. 私有成员的访问5. use 中with默认值6. use 导入问题总结下一篇预告&#xff1a; 前言 在上一篇中&#xff0c;我们深入探讨了 Sass 中 import 语法的局限性&#xff0c;正是因为这些问题…

AI-Deepseek + PPT

01--Deepseek提问 首先去Deepseek问一个问题&#xff1a; Deepseek的回答&#xff1a; 在汽车CAN总线通信中&#xff0c;DBC文件里的信号处理&#xff08;如初始值、系数、偏移&#xff09;主要是为了 将原始二进制数据转换为实际物理值&#xff0c;确保不同电子控制单元&…

解锁前端表单数据的秘密旅程:从后端到用户选择!✨

&#x1f604; 解锁前端表单数据的秘密旅程&#xff1a;从后端到用户选择&#xff01;✨ 嘿&#xff0c;技术爱好者们&#xff01;&#x1f44b; 你有没有在开发中遇到过这样的困惑&#xff1a;表单里的数据&#xff08;比如图片附件、识别点 ID&#xff09;从哪儿来的&#x…

【Linux】进程间通信 续

目录 管道的原理&#xff08;匿名管道&#xff09; 核心原理 站在内核的角度看管道的本质 接口 创建管道文件 代码示例 管道的特征 管道读写端的四种情况 管道的应用场景 命令行的管道。 使用管道实现进程池 初始化 控制子进程 退出 命名管道 命名管道的理解 …