Langchain解锁LLM大语言模型的布局化输出本领(多种实现方案) ...

打印 上一主题 下一主题

主题 1004|帖子 1004|积分 3012

在 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(少样本提示是一种在自然语言处理中,通过借助少量示例来引导语言模型天生预期输出的技术)。
代码实现如下:

  • 我们首先照旧基于 Pydantic 的方式去定义LLM输出的格式,然后调用 .with_structured_output() 方法创建 structured_llm 变量;
  1. from langchain_ollama import ChatOllama
  2. from typing import Optional
  3. from pydantic import BaseModel, Field
  4. from langchain_core.prompts import ChatPromptTemplate
  5. # Pydantic
  6. class Joke(BaseModel):
  7.     """Joke to tell user."""
  8.     setup: str = Field(description="The setup of the joke")
  9.     punchline: str = Field(description="The punchline to the joke")
  10.     rating: Optional[int] = Field(
  11.         default=None, description="How funny the joke is, from 1 to 10"
  12.     )
  13. llm = ChatOllama(model = "llama3.1:latest",
  14.                  temperature = 0.8)
  15. structured_llm = llm.with_structured_output(Joke)
复制代码

  • 接着,我们使用  ChatPromptTemplate 从消息列表中创建一个提示词模板,而且将几个符合输出格式的实例放入到 system prompt 中实现 few-shot prompt,然后再通过 | 管道操作将 prompt 传递给布局化的语言模型进行处理(以下代码中文解释由 DeepSeek-R1 天生);
  1. # 定义系统提示信息
  2. # 告知语言模型它的角色是一个滑稽的喜剧演员,专长是敲敲门笑话
  3. # 并说明了笑话的结构,即需要包含设置(对 "Who's there?" 的回应)和最终笑点(对 "<设置> who?" 的回应)
  4. # 还给出了几个关于不同主题的笑话示例,让语言模型了解输出的格式
  5. system = """You are a hilarious comedian. Your specialty is knock - knock jokes. \
  6. Return a joke which has the setup (the response to "Who's there?") and the final punchline (the response to "<setup> who?").
  7. Here are some examples of jokes:
  8. example_user: Tell me a joke about planes
  9. example_assistant: {{"setup": "Why don't planes ever get tired?", "punchline": "Because they have rest wings!", "rating": 2}}
  10. example_user: Tell me another joke about planes
  11. example_assistant: {{"setup": "Cargo", "punchline": "Cargo 'vroom vroom', but planes go 'zoom zoom'!", "rating": 10}}
  12. example_user: Now about caterpillars
  13. example_assistant: {{"setup": "Caterpillar", "punchline": "Caterpillar really slow, but watch me turn into a butterfly and steal the show!", "rating": 5}}"""
  14. # 使用 ChatPromptTemplate 从消息列表中创建一个提示模板
  15. # 消息列表包含一个系统消息和一个用户消息
  16. # 系统消息是上面定义的系统提示信息
  17. # 用户消息使用占位符 {input},表示后续可以传入具体的用户输入,例如用户想要的笑话主题
  18. prompt = ChatPromptTemplate.from_messages([("system", system), ("human", "{input}")])
  19. # 将提示模板与结构化的语言模型进行组合
  20. # 这里将 prompt 和 structured_llm 进行管道操作(|)
  21. # 意味着先根据提示模板处理输入,然后将处理后的结果传递给结构化的语言模型进行处理
  22. few_shot_structured_llm = prompt | structured_llm
复制代码

  • 接着我们调用模型,让LLM基于用户提问进行符合预期的布局化输出。
  1. # 调用组合后的模型,传入用户输入 "what's something funny about woodpeckers"
  2. # 表示用户想要一个关于啄木鸟的有趣笑话
  3. # 模型会根据系统提示中的要求和示例,生成一个符合格式的敲敲门笑话
  4. response = few_shot_structured_llm.invoke("what's something funny about woodpeckers")
  5. print(response)
  6. 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 天生):
  1. # 从 langchain_ollama 库中导入 ChatOllama 类
  2. from langchain_ollama import ChatOllama
  3. # 创建一个 ChatOllama 实例
  4. # model 参数指定要使用的模型,这里使用的是 "llama3.1:latest" 模型
  5. # temperature 参数控制生成文本的随机性,值越大越随机,这里设置为 0.8
  6. llm = ChatOllama(model="llama3.1:latest",
  7.                  temperature=0.8)
  8. # 为 llm 实例添加结构化输出功能
  9. # 第一个参数传入 None,表示不使用自定义的schema
  10. # method 参数指定使用 "json_mode" 方法,即输出为 JSON 格式
  11. structured_llm = llm.with_structured_output(None, method="json_mode")
  12. # 调用结构化的语言模型
  13. # 传入的提示信息是 "Tell me a joke about cats, respond in JSON with `setup` and `punchline` keys"
  14. # 意思是让模型讲一个关于猫的笑话,并以包含 `setup`(笑话的铺垫)和 `punchline`(笑话的笑点)键的 JSON 格式响应
  15. structured_llm.invoke(
  16.     "Tell me a joke about cats, respond in JSON with `setup` and `punchline` keys"
  17. )
复制代码
从以上代码可以留意到,固然我们调用的仍旧是 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 天生):
  1. # 导入 List 类型提示,用于类型注解,表明变量将是一个列表
  2. from typing import List
  3. # 从 langchain_core.output_parsers 模块导入 PydanticOutputParser 类
  4. # 该类用于将文本输出解析为 Pydantic 模型实例
  5. from langchain_core.output_parsers import PydanticOutputParser
  6. # 从 langchain_core.prompts 模块导入 ChatPromptTemplate 类
  7. # 该类用于创建聊天提示模板
  8. from langchain_core.prompts import ChatPromptTemplate
  9. # 从 pydantic 模块导入 BaseModel 和 Field 类
  10. # BaseModel 是 Pydantic 模型的基类,Field 用于定义模型字段的元数据
  11. from pydantic import BaseModel, Field
  12. # 定义一个名为 Person 的 Pydantic 模型类
  13. # 该类用于表示一个人的信息
  14. class Person(BaseModel):
  15.     """Information about a person."""
  16.     # 定义 name 字段,为字符串类型
  17.     # ... 表示该字段是必需的
  18.     # description 为该字段提供描述信息
  19.     name: str = Field(..., description="The name of the person")
  20.     # 定义 height_in_meters 字段,为浮点数类型
  21.     # 表示人的身高,单位为米
  22.     # ... 表示该字段是必需的
  23.     # description 为该字段提供描述信息
  24.     height_in_meters: float = Field(
  25.         ..., description="The height of the person expressed in meters."
  26.     )
  27. # 定义一个名为 People 的 Pydantic 模型类
  28. # 该类用于表示文本中所有人物的识别信息
  29. class People(BaseModel):
  30.     """Identifying information about all people in a text."""
  31.     # 定义 people 字段,为 Person 类型的列表
  32.     people: List[Person]
  33. # 创建一个 PydanticOutputParser 实例
  34. # 将其 pydantic_object 属性设置为 People 类
  35. # 用于将文本输出解析为 People 模型实例
  36. parser = PydanticOutputParser(pydantic_object=People)
  37. # 创建一个聊天提示模板实例
  38. # 使用 from_messages 方法从消息列表中创建模板
  39. prompt = ChatPromptTemplate.from_messages(
  40.     [
  41.         (
  42.             # 系统消息,提示回答用户查询
  43.             # 并将输出用 `json` 标签包裹
  44.             # {format_instructions} 是一个占位符,将在后续填充解析指令
  45.             "system",
  46.             "Answer the user query. Wrap the output in `json` tags\n{format_instructions}"
  47.         ),
  48.         (
  49.             # 用户消息,{query} 是一个占位符,将在后续填充用户的查询内容
  50.             "human",
  51.             "{query}"
  52.         )
  53.     ]
  54. ).partial(format_instructions=parser.get_format_instructions())
复制代码
我们来看一下将 user query传入之后,构成的完备prompt内容是什么样的。
  1. query = "Anna is 23 years old and she is 6 feet tall"
  2. print(prompt.invoke({"query": query}).to_string())
复制代码
prompt
  1. System: Answer the user query. Wrap the output in `json` tags
  2. The output should be formatted as a JSON instance that conforms to the JSON schema below.
  3. As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}
  4. the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.
  5. Here is the output schema:
  6. ```
  7. {"$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"]}
  8. ```
  9. Human: Anna is 23 years old and she is 6 feet tall
复制代码
最后我们基于 prompt, llm 和 parser 通过管道创建一个链式调用:
  1. # 以下代码注释由 Doubao-1.5-pro-32k 生成
  2. # 使用 LangChain 提供的管道操作符 `|` 来创建一个链式调用。
  3. # 这个链式调用的作用是将多个组件按顺序连接起来,形成一个处理流程。
  4. # 具体来说,这个链式调用包含三个组件:prompt、llm 和 parser。
  5. # 1. prompt:这是之前创建的聊天提示模板实例。它的作用是根据用户输入的查询内容和解析指令生成合适的提示信息。
  6. # 2. llm:这是一个语言模型实例,例如 OpenAI 的 GPT 系列模型等。它接收来自 prompt 生成的提示信息,然后根据这个提示信息生成相应的文本输出。
  7. # 3. parser:这是之前创建的 PydanticOutputParser 实例。它的作用是将 llm 生成的文本输出解析为 People 模型实例,方便后续对输出数据进行结构化处理。
  8. # 最终,chain 就是一个包含了提示生成、语言模型调用和输出解析这三个步骤的处理链。
  9. chain = prompt | llm | parser
  10. # 调用 chain 的 invoke 方法来执行这个处理链。
  11. # invoke 方法接收一个字典作为参数,字典中的键 "query" 对应的值就是用户输入的查询内容。
  12. # 处理链会按照之前定义的顺序依次执行各个组件:
  13. # 首先,prompt 会根据传入的查询内容和解析指令生成提示信息。
  14. # 然后,这个提示信息会被传递给 llm,llm 基于提示信息生成文本输出。
  15. # 最后,parser 会将 llm 生成的文本输出解析为 People 模型实例。
  16. # 整个处理过程完成后,会返回经过解析后的结构化数据,方便后续的业务逻辑使用。
  17. chain.invoke({"query": query})
复制代码
打印查看格式化后的输出,完全符合 People 类中定义的schema。
  1. People(people=[Person(name='Anna', height_in_meters=1.8288)])
复制代码
自定义剖析器

为了增长布局化LLM输出的灵活性,我们可以使用LangChain表达语言( LangChain Expression Language (LCEL)),来自定义prompt息争析器(通过创建一个函数的方式)。
  1. import json
  2. import re
  3. from typing import List
  4. from langchain_ollama import ChatOllama
  5. from langchain_core.messages import AIMessage
  6. from langchain_core.prompts import ChatPromptTemplate
  7. from pydantic import BaseModel, Field
  8. llm = ChatOllama(model = "llama3.1:latest",
  9.                  temperature = 0.8)
  10. class Person(BaseModel):
  11.     """Information about a person."""
  12.     name: str = Field(..., description="The name of the person")
  13.     height_in_meters: float = Field(
  14.         ..., description="The height of the person expressed in meters."
  15.     )
  16. class People(BaseModel):
  17.     """Identifying information about all people in a text."""
  18.     people: List[Person]
  19. # 自定义prompt,我们通过system prompt,告知LLM要将输出内容wrap在<json></json>标签中
  20. prompt = ChatPromptTemplate.from_messages(
  21.     [
  22.         (
  23.             "system",
  24.             "Answer the user query. Output your answer as JSON that  "
  25.             "matches the given schema: <json>\n{schema}\n</json>. "
  26.             "Make sure to wrap the answer in <json> and </json> tags",
  27.         ),
  28.         ("human", "{query}"),
  29.     ]
  30. ).partial(schema=People.model_json_schema())
复制代码
我们看一下传入 user query后完备的prompt内容:
  1. query = "Anna is 23 years old and she is 6 feet tall"
  2. print(prompt.format_prompt(query=query).to_string())
复制代码
prompt
  1. System: Answer the user query. Output your answer as JSON that  matches the given schema: <json>
  2. {'$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'}
  3. </json>. Make sure to wrap the answer in <json> and </json> tags
  4. Human: Anna is 23 years old and she is 6 feet tall
复制代码
通过创建名为 extract_json 的函数自定义 parser:
  1. # Custom parser
  2. def extract_json(message: AIMessage) -> List[dict]:
  3.     """Extracts JSON content from a string where JSON is embedded between <json> and </json> tags.
  4.     Parameters:
  5.         text (str): The text containing the JSON content.
  6.     Returns:
  7.         list: A list of extracted JSON strings.
  8.     """
  9.     text = message.content
  10.     # Define the regular expression pattern to match JSON blocks
  11.     pattern = r"<json>(.*?)</json>"
  12.     # Find all non-overlapping matches of the pattern in the string
  13.     matches = re.findall(pattern, text, re.DOTALL)
  14.     # Return the list of matched JSON strings, stripping any leading or trailing whitespace
  15.     try:
  16.         return [json.loads(match.strip()) for match in matches]
  17.     except Exception:
  18.         raise ValueError(f"Failed to parse: {message}")
复制代码
最后我们基于 prompt, llm 和 自定义用来剖析大模型输出内容的parser,通过管道创建一个链式调用:
  1. chain = prompt | llm | extract_json
  2. chain.invoke({"query": query})
复制代码
调用 chain 后得到的布局化输出如下:
  1. [{'$defs': {'Person': {'description': 'Information about a person.',
  2.     'properties': {'name': {'description': 'The name of the person',
  3.       'title': 'Name',
  4.       'type': 'string'},
  5.      'height_in_meters': {'description': 'The height of the person expressed in meters.',
  6.       'title': 'Height In Meters',
  7.       'type': 'number'}},
  8.     'required': ['name', 'height_in_meters'],
  9.     'title': 'Person',
  10.     'type': 'object'}},
  11.   'description': 'Identifying information about all people in a text.',
  12.   'properties': {'people': {'items': {'$ref': '#/$defs/Person'},
  13.     'title': 'People',
  14.     'type': 'array'}},
  15.   'required': ['people'],
  16.   'title': 'People',
  17.   'type': 'object',
  18.   'people': [{'name': 'Anna', 'height_in_meters': 1.8288}]}]
复制代码
但以上并非预期的输出,预期输出应该如下:
  1. [{'people': [{'name': 'Anna', 'height_in_meters': 1.8288}]}]
复制代码
至于原因目前还不清楚,开端猜测是由于选用的 llama3.1:latest 模型由于量化程度较高导致对带输出格式说明的system prompt的明白力不够;固然也有可能是官方文档里提供的示例代码有些问题。(我在当地第一次运行的时候是报错的,对代码稍微调解了一下才fix了报错的问题)

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

兜兜零元

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表