掘金 人工智能 6小时前
如何让大模型输出结构化数据
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文详细介绍了如何利用LangChain框架,让大型语言模型(LLM)输出结构化数据。文章首先阐述了结构化输出的重要性及其常见方法,包括提示工程、输出解析器和工具调用。随后,重点讲解了如何通过手动编写提示工程和使用LangChain的PydanticOutputParser来实现结构化输出,并提供了具体的代码示例。最后,对比了不同解决方案的优势,强调了PydanticOutputParser在类型安全、错误恢复、嵌套结构处理及工程化支持方面的优越性,并建议在生产环境优先使用。

✨ **结构化输出的重要性与方法**:结构化输出是指让大模型输出符合预定义格式(如JSON、表格)而非自由文本,这对于程序化处理和数据提取至关重要。实现方式包括提示工程、输出解析器和工具调用。

📝 **手动提示工程实现结构化输出**:通过精心设计的系统提示,明确要求模型输出特定格式(如JSON),并提供示例和字段约束。虽然直观,但易出现格式漂移、类型薄弱、维护成本高等问题。

📚 **LangChain PydanticOutputParser**:利用Pydantic模型定义数据结构,LangChain的PydanticOutputParser能自动生成格式指令,简化提示词编写,并提供类型安全和自动解析功能,能有效处理复杂嵌套结构,是生产环境的推荐方案。

🛠️ **模型函数调用与with_structured_output**:对于支持函数调用的模型,LangChain的`.with_structured_output()`方法能更简洁高效地实现结构化输出。它将Pydantic模型转换为函数描述,引导模型生成函数调用,再解析为对象,显著提升了可靠性和便捷性。

⚖️ **解决方案对比与建议**:与手动提示工程相比,PydanticOutputParser在类型安全、错误恢复、嵌套结构处理、提示词维护和工程化支持方面具有明显优势,建议生产系统优先采用,手动提示仅作为临时方案。

结构化输出是指让大模型(如GPT、LLama等)的输出结果符合预定义的格式(如 JSON、Pydantic 对象、表格等)而不是自由文本,这在需要程序化处理结果(如数据提取、后续自动化操作)时非常有用,一个常见的用例是从文本中提取数据以插入数据库或与其他下游系统一起使用。本文介绍一下如何使用 LangChain 框架让大模型输出结构化数据。

实现结构化输出的常见方法包括:

使用提示工程实现结构化输出

手动编写提示工程

使用提示工程实现结构化输出需要通过设计明确的系统提示(System Prompt),直接要求模型输出符合特定格式(如 JSON、表格等)的数据,而不是自由文本。这种方法依赖模型的理解能力,所以在模型输出数据后需要对数据进行后处理(可以使用langchain提供的输出解析器**BaseLLMOutputParser**的子类,后面会详细介绍)

以下是一些常见的方法和技巧:

1. 明确目标

在提问时明确要求模型输出结构化数据。例如:

2. 提供示例

给出一个输出示例,这样模型可以模仿格式。例如:

3. 指定字段

明确列出需要包含的字段,以及它们的数据类型。例如:

4. 分步引导

对于复杂结构,可以分步引导模型。先让模型输出一部分,再逐步完善。

5. 强制约束(如果API支持)

在API调用中,可以通过系统消息(system message)来设定输出格式。例如:

{  "role": "system",  "content": "不要解释,仅以JSON格式输出,包含name和age字段。"}

6. 后处理

如果模型输出不是完全结构化的,可以尝试用后处理(如正则表达式)来提取信息并转化为结构化的数据。

7. 调整温度参数

将温度(temperature)设置为0(或较低值),使输出更加确定,减少随机性,有助于稳定输出结构。

8. 使用模板

在提示词中提供模板,让模型填充内容。例如:

请按照以下模板输出:| 书名 | 作者 | 出版年份 ||------|------|----------|| <书名> | <作者> | <年份>   |

示例:

假设我们想让模型输出一本书的信息,包括书名、作者和出版年份,要求是JSON格式。
提问:

请以JSON格式输出《三体》这本书的信息,包含字段:title(字符串)、author(字符串)、year(整数)。

期望输出:

{  "title": "三体",  "author": "刘慈欣",  "year": 2008}

注意:

总结:

手动编写提示工程实现结构化输出的一般步骤

下面是一个手动编写提示词实现结构化输出的示例:

system_prompt = """根据用户的输入生成转账或者红包的金额和备注,请严格按照以下JSON格式输出下面是一些例子:示例一:输入: 张三说给李四转账1000元输出: {"money": 1000, "unit":"元","remark": "转给李四"}示例二:输入: 转账100元输出: {"money": 100, "unit":"元","remark": ""}"""user_input = "李雷给韩梅梅转账2000元"messages = [    {"role": "system", "content": system_prompt},    {"role": "user", "content": user_input}]

通过以上方法,可以大大提高模型输出结构化数据的概率和准确性。

手动编写提示工程的缺点总结

手动编写提示工程(Prompt Engineering)实现结构化输出虽然直观,但存在以下显著缺点:

问题类别具体问题详细说明示例/备注
1. 输出稳定性问题格式漂移模型可能在输出中添加解释性文字或前缀,破坏结构化格式"答案:{ \"name\": \"Alice\" }"
随机性即使设置 temperature=0,复杂任务仍可能生成非法格式输出缺少引号、括号不匹配等
边界案例处理差对空值、特殊字符、极端输入处理不佳,易导致格式错误null 被替换为 "N/A" 或遗漏
2. 开发维护成本高脆弱提示不同模型(如 GPT-4、Claude、Llama)对同一提示响应不一致,需单独调整提示需为每个模型定制优化
版本敏感模型更新后原有提示可能失效,需重新测试和修改OpenAI 模型微更新导致 JSON 失效
长提示难优化复杂结构提示常超 100 token,调试困难,影响可读性和维护性提示过长导致注意力分散
3. 类型控制薄弱无强制类型校验模型可能返回字符串而非数字、日期等预期类型"age": "二十八" 应为 28
格式错误无自愈无法自动修复非法 JSON(如尾逗号、未转义字符)["a","b",] 是非法 JSON
复杂结构难实现嵌套对象、联合类型、枚举等复杂 Schema 几乎无法稳定生成难以保证 { user: { profile: { ... } } } 的完整性
4. 错误处理缺失无重试机制解析失败后需外部逻辑触发重试,提示本身不具备容错能力需手动编写重试循环
无错误反馈模型不知道输出为何被拒绝,无法从错误中学习无法告知模型“缺少字段 email
异常处理全手动所有格式异常需用代码捕获并处理,增加开发负担try-except 块遍地开花
5. 扩展性限制多字段维护难字段超过 10 个时,提示词变得冗长且难以管理提示膨胀至数百 token
动态结构不支持无法根据输入内容动态调整输出结构(如条件字段、可选对象)输入为“公司”则输出产品列表,否则不输出
多语言适配复杂不同语言需重写整个提示规则以保持结构一致性中文提示需重新设计格式约束
6. 效率问题token 浪费每次请求都需重复传输完整的 Schema 描述,占用大量上下文每次都传 "输出严格JSON格式:..."
响应延迟模型需花费计算资源理解格式要求,而非专注于内容生成影响推理速度
解析开销需先提取文本再解析 JSON,无法直接获取结构化数据,增加后处理成本大响应体下解析耗时显著
7. 与工程实践脱节无版本控制提示词通常以字符串形式存在,难以进行 git diff、合并冲突管理等代码级操作修改提示无法追溯变更历史
测试困难缺乏对提示的有效单元测试机制,难以验证其在各种输入下的行为一致性无法像函数一样进行自动化测试
协作障碍非技术人员(如产品经理)难以参与结构设计,因提示词混合了逻辑与格式指令业务人员看不懂技术提示

何时仍需手动编写提示工程?

    原型验证阶段:快速验证概念简单格式输出:单层CSV/简单列表无LangChain环境:受限环境下的临时方案非技术用户:需避免代码依赖的场景

建议:对于生产系统,优先采用PydanticOutputParserOpenAI Function Calling或专用输出解析库。手动提示仅作为临时方案,其维护成本随系统复杂度呈指数级增长。

使用langchain实现结构化输出

在 langchain 中使用内置的Pydanticoutputparser实现从大模型的输出中解析出结构化的数据。

我们使用PydanticOutputParser的主要好处在于:

    结构化数据Pydantic模型可以定义我们期望输出的数据结构,包括字段名称、类型和描述,使得输出更加规范。类型安全Pydantic会自动进行类型检查,如果模型输出不符合定义,会抛出错误,便于我们捕获和处理异常。自动解析LangChainPydanticOutputParser可以将语言模型的输出自动解析成我们定义的Pydantic模型对象,简化了代码。提示词优化Parser可以根据Pydantic模型自动生成格式说明,减少手动编写提示词的工作量,并降低出错概率。

如何使用LangChainPydanticOutputParser实现模型结构化输出?

    定义Pydantic模型:描述期望输出的数据结构。初始化解析器:传入定义好的Pydantic模型到PydanticOutputParse(pydantic_object=模型)构建提示词:使用PydanticOutputParserget_format_instructions方法获取格式指令,并加入到提示词中。调用模型:将提示词输入模型,得到输出。解析输出:使用PydanticOutputParserparse方法解析模型的输出,得到结构化的数据对象。

下面是一个具体的示例,通过结构化输出一个主题的各个章节列表,包含复杂嵌套结构的处理:

1. 定义Pydantic模型

from pydantic import BaseModel, Fieldfrom langchain.output_parsers import PydanticOutputParser# 1. 定义数据结构模型 (Pydantic)class Section(BaseModel):    name: str = Field(        description="章节名称",    )    description: str = Field(        description="简要概述本章节涉及的主要主题和概念,不超过50字",    )# Pydantic 支持嵌套模型,轻松处理多层数据class Sections(BaseModel):    sections: List[Section] = Field(        description="报告的章节列表",    )

2. 初始化解析器

# 2. 初始化解析器parser = PydanticOutputParser(pydantic_object=Sections)# 查看生成的格式指令format_instructions = parser.get_format_instructions()print(format_instructions)

输出的promp内容如下(这个prompt是 PydanticOutputParser 根据提供的Pydantic模型自动生成的):

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": {"Section": {"properties": {"name": {"description": "章节名称", "title": "Name", "type": "string"}, "description": {"description": "简要概述本章节涉及的主要主题和概念", "title": "Description", "type": "string"}}, "required": ["name", "description"], "title": "Section", "type": "object"}}, "properties": {"sections": {"description": "报告的章节列表", "items": {"$ref": "#/$defs/Section"}, "title": "Sections", "type": "array"}}, "required": ["sections"]}```

可以看到该prompt按照前面手动编写提示工程的技巧:

在LangChain的PydanticOutputParser中,我们通过编程方式定义数据结构,然后自动生成格式指令,这样更简洁,且避免了手动编写复杂的Schema描述。

3.构建提示词(包含格式说明)

接下来需要将format_instructions的格式指令放在system_prompt中,如下所示:

topic = "创建关于 LLM 缩放定律的报告"system_prompt = f"""请生成报告结构,严格遵守以下要求:{format_instructions}不要添加任何额外文本,仅输出JSON。"""user_prompt = f"""这里是报告主题: {topic}"""messages = [    {"role": "system", "content": system_prompt},    {"role": "user", "content": user_prompt}]

4.调用模型

将提示词输入模型,得到输出。这里使用 litellm 进行模型调用,关于 litellm 的使用可以查看我前面的文章,调用模型的代码如下:

import litellmfrom typing import Union, List, Dict, Optionalmodel_config_dict = {    "deepseek-chat": {        "model": "openai/deepseek-chat",        "base_url": "https://api.deepseek.com/v1",        "api_key": os.getenv("DEEPSEEK_API_KEY"),    },}def llm_chat( messages: Union[str, List[Dict[str, str]]], model: str="deepseek-chat",  tools: Optional[List] = None, stream=False):    print(f'调用模型:{model}')    model_config = model_config_dict.get(model)    if isinstance(messages, str):        messages = [{"role": "user", "content": messages}]    response = litellm.completion(        model=model_config.get('model'),        base_url=model_config.get('base_url'),        api_key=model_config.get('api_key'),        messages=messages,        tools=tools,        stream=stream,    )    return  response.choices[0].messageresponse = llm_chat(messages)print(response.content)

调用模型输出结果如下:

```json{  "sections": [    {      "name": "引言",      "description": "介绍LLM(大型语言模型)的基本概念和缩放定律的重要性。"    },    {      "name": "LLM缩放定律的定义",      "description": "详细解释LLM缩放定律的含义及其在模型性能预测中的作用。"    },    {      "name": "缩放定律的数学基础",      "description": "探讨缩放定律背后的数学原理和关键公式。"    },    {      "name": "实证研究",      "description": "总结关于LLM缩放定律的实证研究结果和主要发现。"    },    {      "name": "应用与影响",      "description": "分析缩放定律在实际LLM开发中的应用及其对行业的影响。"    },    {      "name": "未来研究方向",      "description": "提出关于LLM缩放定律未来可能的研究方向和挑战。"    },    {      "name": "结论",      "description": "总结报告的主要观点和缩放定律在LLM发展中的重要性。"    }  ]}
#### 5.解析输出从前面可以看到模型输出的结果不是一个存粹的json字符串,包含特殊符号````json`,所以还需要使用`PydanticOutputParser`的 `parse`方法解析模型的输出,得到结构化的数据对象,代码如下:```python# 5. 使用PydanticOutputParser的 parse 方法解析模型的输出content = response.contentresult: Sections = parser.parse(content)print(result)

输出结果如下:

sections=[Section(name='引言', description='介绍LLM(大型语言模型)的基本概念和缩放定律的重要性。'), Section(name='LLM缩放定律的定义', description='详细解释LLM缩放定律的含义及其在模型性能预测中的作用。'), Section(name='缩放定律的数学基础', description='探讨缩放定律背后的数学原理和关键公式。'), Section(name='实证研究', description='总结关于LLM缩放定律的实证研究结果和主要发现。'), Section(name='应用与影响', description='分析缩放定律在实际LLM开发中的应用及其对行业的影响。'), Section(name='未来研究方向', description='提出关于LLM缩放定律未来可能的研究方向和挑战。'), Section(name='结论', description='总结报告的主要观点和缩放定律在LLM发展中的重要性。')]

使用 pydanticmodel_dump_json() 查看json字符串

json_result = result.model_dump_json()print(json_result)

输出结果如下:

{    "sections": [        {            "name": "引言",            "description": "介绍LLM(大型语言模型)的基本概念和缩放定律的重要性。"        },        {            "name": "LLM缩放定律的定义",            "description": "详细解释LLM缩放定律的含义及其在模型性能预测中的作用。"        },        {            "name": "缩放定律的数学基础",            "description": "探讨缩放定律背后的数学原理和关键公式。"        },        {            "name": "实证研究",            "description": "总结关于LLM缩放定律的实证研究结果和主要发现。"        },        {            "name": "应用与影响",            "description": "分析缩放定律在实际LLM开发中的应用及其对行业的影响。"        },        {            "name": "未来研究方向",            "description": "提出关于LLM缩放定律未来可能的研究方向和挑战。"        },        {            "name": "结论",            "description": "总结报告的主要观点和缩放定律在LLM发展中的重要性。"        }    ]}

parse方法源码分析

查看 PydanticOutputParser.parse()方法的源码,看看它到底帮我们做了什么,首先使用正则表达式提取json字符串:

_json_markdown_re = re.compile(r"```(json)?(.*)", re.DOTALL)# Try to find JSON string within triple backticksmatch = _json_markdown_re.search(json_string)# If no match found, assume the entire string is a JSON string# Else, use the content within the backticksjson_str = json_string if match is None else match.group(2)

然后使用下面的代码进行json字符串补全:

def parse_partial_json(s: str, *, strict: bool = False) -> Any:    """Parse a JSON string that may be missing closing braces.    Args:        s: The JSON string to parse.        strict: Whether to use strict parsing. Defaults to False.    Returns:        The parsed JSON object as a Python dictionary.    """    # Attempt to parse the string as-is.    try:        return json.loads(s, strict=strict)    except json.JSONDecodeError:        pass    # Initialize variables.    new_chars = []    stack = []    is_inside_string = False    escaped = False    # Process each character in the string one at a time.    for char in s:        new_char = char        if is_inside_string:            if char == '"' and not escaped:                is_inside_string = False            elif char == "\n" and not escaped:                new_char = (                    "\\n"  # Replace the newline character with the escape sequence.                )            elif char == "\\":                escaped = not escaped            else:                escaped = False        elif char == '"':            is_inside_string = True            escaped = False        elif char == "{":            stack.append("}")        elif char == "[":            stack.append("]")        elif char in {"}", "]"}:            if stack and stack[-1] == char:                stack.pop()            else:                # Mismatched closing character; the input is malformed.                return None        # Append the processed character to the new string.        new_chars.append(new_char)    # If we're still inside a string at the end of processing,    # we need to close the string.    if is_inside_string:        if escaped:  # Remoe unterminated escape character            new_chars.pop()        new_chars.append('"')    # Reverse the stack to get the closing characters.    stack.reverse()    # Try to parse mods of string until we succeed or run out of characters.    while new_chars:        # Close any remaining open structures in the reverse        # order that they were opened.        # Attempt to parse the modified string as JSON.        try:            return json.loads("".join(new_chars + stack), strict=strict)        except json.JSONDecodeError:            # If we still can't parse the string as JSON,            # try removing the last character            new_chars.pop()    # If we got here, we ran out of characters to remove    # and still couldn't parse the string as JSON, so return the parse error    # for the original string.    return json.loads(s, strict=strict)

完整的结构化输出代码

from pydantic import BaseModel, Fieldfrom langchain.output_parsers import PydanticOutputParserimport litellmfrom typing import Union, List, Dict, Optionalmodel_config_dict = {    "deepseek-chat": {        "model": "openai/deepseek-chat",        "base_url": "https://api.deepseek.com/v1",        "api_key": os.getenv("DEEPSEEK_API_KEY"),    },}def llm_chat( messages: Union[str, List[Dict[str, str]]], model: str="deepseek-chat",  tools: Optional[List] = None, stream=False):    print(f'调用模型:{model}')    model_config = model_config_dict.get(model)    if isinstance(messages, str):        messages = [{"role": "user", "content": messages}]    response = litellm.completion(        model=model_config.get('model'),        base_url=model_config.get('base_url'),        api_key=model_config.get('api_key'),        messages=messages,        tools=tools,        stream=stream,    )    return  response.choices[0].message# 1. 定义数据结构模型 (Pydantic)class Section(BaseModel):    name: str = Field(        description="章节名称",    )    description: str = Field(        description="简要概述本章节涉及的主要主题和概念",    )# Pydantic 支持嵌套模型,轻松处理多层数据class Sections(BaseModel):    sections: List[Section] = Field(        description="报告的章节列表",    )# 2. 初始化解析器parser = PydanticOutputParser(pydantic_object=Sections)# 查看生成的格式指令format_instructions = parser.get_format_instructions()print(format_instructions)# 3.构建提示词(包含格式说明)topic = "创建关于 LLM 缩放定律的报告"system_prompt = f"""请生成报告结构,严格遵守以下要求:{format_instructions}不要添加任何额外文本,仅输出JSON。"""user_prompt = f"""这里是报告主题: {topic}"""messages = [    {"role": "system", "content": system_prompt},    {"role": "user", "content": user_prompt}]print(messages)# 4.调用模型response = llm_chat(messages)content = response.contentprint(content)# 5. 使用PydanticOutputParser的 parse 方法解析模型的输出content = response.contentresult: Sections = parser.parse(content)print(result)json_result = result.model_dump_json()print(json_result)

使用支持函数调用的模型实现结构化输出

我们使用 LangChain 的 .with_structured_output() 方法来实现结构化输出,这是 LangChain 提供的一个高级抽象,它简化了从模型中获取结构化输出的过程。这个方法通常与支持工具调用或函数调用的模型一起使用。

为什么模型具有函数调用功能?

函数调用(或工具调用)功能是指大模型(如 OpenAI 的 GPT-4 等)能够根据用户请求和预定义的函数描述,输出一个符合函数参数的调用请求。这种能力使得模型能够与外部工具或API进行交互,从而执行具体的操作(如查询天气、计算等)或者返回结构化的数据。
模型之所以具有函数调用功能,是因为在训练过程中,它们接触到了包含函数调用示例的数据,或者通过微调(fine-tuning)来学习如何根据用户指令和函数描述生成正确的函数调用参数。在推理时,我们通过提供函数的描述(名称、描述、参数及其类型)来引导模型生成相应的调用。

使用示例

下面我们以 OpenAI 的模型(支持函数调用)和 Pydantic 模型为例,展示如何使用 .with_structured_output()

步骤 1:定义 Pydantic 模型

from pydantic import BaseModel, Fieldclass Person(BaseModel):    name: str = Field(description="The person's name")    age: int = Field(description="The person's age")    hobbies: list[str] = Field(description="The person's hobbies")

步骤 2:创建模型并绑定结构化输出

from langchain_openai import ChatOpenAI# 创建支持函数调用的模型实例model = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)# 使用 with_structured_output 绑定输出结构structured_model = model.with_structured_output(Person)

步骤 3:调用模型获取结构化输出

# 直接调用,返回的是 Person 实例result = structured_model.invoke("John Doe is a 30 year old who likes hiking and reading.")print(result)# 输出:name='John Doe' age=30 hobbies=['hiking', 'reading']

底层过程解析

    构造工具(函数)描述:在内部,with_structured_output 方法会根据 Person 模型生成一个 JSON Schema,然后创建一个 LangChain 工具(Tool)或函数(Function),其参数就是这个 JSON Schema。例如,在OpenAI的API中,你可以这样定义:
{  "name": "get_user_info",  "description": "获取用户信息",  "parameters": {    "type": "object",    "properties": {      "name": {        "type": "string",        "description": "用户的姓名"      },      "age": {        "type": "integer",        "description": "用户的年龄"      }    },    "required": ["name", "age"]  }}
    调用模型:当调用 invoke 时,模型会接收到:
      用户的消息(即输入的字符串)可用的工具列表(包含我们定义的这个虚拟工具)
    模型决策:模型会判断是否需要调用工具。在这个例子中,因为要求结构化输出,所以模型会返回一个工具调用请求,其中参数部分就是根据用户输入填充的 Person 数据。解析工具调用:LangChain 运行时捕获这个工具调用,提取参数,并将其转换为 Person 对象。

注意事项

总结

with_structured_output() 方法通过利用模型的函数调用能力,将输出约束在预定义的结构中,从而简化了从大语言模型获取结构化数据的过程。其核心在于将输出解析器(如Pydantic模型)转换为函数描述,引导模型生成结构化的函数调用参数,然后将其解析为对象。这种方法比手动编写提示词和解析文本输出更可靠、更简洁。

对比解决方案的优势

问题维度手动提示工程PydanticOutputParser/LangChain
类型安全❌ 无保障✅ 自动验证字段类型和范围
错误恢复❌ 需完全手动处理✅ 内置RetryOutputParser自动重试
嵌套结构⚠️ 极难实现✅ 原生支持嵌套模型/联合类型
提示词维护❌ 完全手动✅ 自动生成格式指令
多模型适配❌ 需为每个模型定制✅ 统一接口适配主流模型
工程化支持❌ 无✅ 集成测试/版本控制/CI/CD

参考文档

    python.langchain.com/docs/how_to…

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

LangChain 结构化输出 大模型 提示工程 Pydantic
相关文章