真正做过 LLM 开发的都知道,虽然理论上 LLM 可以生成结构化的数据,但是真正生成的时候经常出错。怎么稳定的让 LLM 返回结构化的数据?

LinkedIn 的做法如下:

 https://www.linkedin.com/blog/engineering/generative-ai/musings-on-building-a-generative-ai-product

YAML 和 JSON 是两种常用的数据序列化格式,它们各有优势和用途。大多数 LLM 在训练中都使用了 YAML 和 JSON 来进行结构化输出。

LinkedIn 选择了 YAML,因为它比 JSON 更简洁,因此消耗的 Token 更少

使用 YAML 而不是 JSON

选择 YAML 作为数据交换格式相对于 JSON 的一个主要优势在于它的人类可读性灵活性,这些特性使得 YAML 在某些方面具有更高的容错率

以下是几个关键点来解释为什么在某些情况下 YAML 可能比 JSON 更容错:

1、注释支持

YAML 支持在文件中直接添加注释,这有助于开发人员解释或标记代码,增加了代码的可读性和维护性。

这种可注释的特性使得开发者可以在配置文件中留下有用的说明,有助于避免错误的配置和理解配置的意图。

YAML

# 这是一个主机配置项
host: localhost
port: 8080 # 后面可以直接加注释
  • 1.
  • 2.
  • 3.

YAML 除了注释行,还允许在同一行中加注释和在冒号后直接跟值,使得配置更加简洁;

JSON

{
  "host": "localhost",
  "port": 8080
  // 这会导致错误,因为JSON不支持这样的注释
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

对比说明: YAML 允许在文件中直接添加注释,方便开发者记录重要信息或者暂时禁用某个配置项。而JSON格式不支持注释,试图添加注释会导致解析错误。

让LLM生成指定格式内容时,可以给出示例,YAML格式的示例中有注释,更不容易产生歧义。

2、不严格的语法

YAML 不要求所有数据结构元素(如列表和映射)强制使用逗号和括号分隔。

这种灵活性意味着在编写或编辑数据时,遗漏或错误添加分隔符不会立即导致解析错误,从而提高了容错能力。

YAML

animals:
  - dog
  - cat
  - rabbit
  • 1.
  • 2.
  • 3.
  • 4.

JSON

{
  "animals": ["dog", "cat", "rabbit"]
}
  • 1.
  • 2.
  • 3.

对比说明: YAML 中列表项前只需要一个短横线和一个空格,没有严格的逗号分隔;而JSON中的数组元素必须使用逗号分隔,漏写或多写逗号都会导致错误。

3、数据类型检测

YAML 自动检测数据类型,如整数、浮点数、字符串等。这意味着不需要显式声明类型,简化了数据的书写和阅读过程。

例如,你不需要用引号包围字符串,除非它包含可能会被解析为其他类型的字符。

YAML

integer: 5
float: 5.0
boolean: true
unquoted_string: hello world
quoted_string: "12345"  # 强制作为字符串
text: "He said, \"Hello, how are you?\"" # 有转义字符
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

YAML 自动推断数据类型,整数和浮点数不需要引号。

在 YAML 中,字符串常常不需要转义字符(除非包含特定的符号),这简化了复杂字符串的表示,减少了因转义错误导致的问题。

unquoted_string 中的文本不需要引号,除非像 quoted_string 那样要强制为字符串类型。

JSON

{
  "integer": 5,
  "float": 5.0,
  "boolean": true,
  "unquoted_string": "hello world",
  "quoted_string": "12345",
  "text": "He said, \"Hello, how are you?\""
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

JSON 中所有的字符串都必须用引号包围,无论内容。布尔值和数值不需要引号,但对于想要表示为字符串的数字,如 quoted_string,也必须用引号。

4、复杂结构的表示

YAML 通过缩进来表示层级结构,而不是使用大括号和方括号。这种基于缩进的结构通常更易于阅读和编辑,减少了因括号匹配错误导致的问题。

YAML

root:
  child1:
    child2: value
  • 1.
  • 2.
  • 3.

JSON

{
  "root": {
    "child1": {
      "child2": "value"
    }
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

对比说明: YAML 使用缩进来表达层级关系,更易读;JSON 使用花括号和逗号,层次复杂时容易出错,尤其是在括号匹配上。

上面这些例子可以看出:YAML在某些方面相对于JSON的高容错性和灵活性。

解决生成的 YAML 也不规范问题

尽管选用了YAML作为LLM结构化数据输出格式,也有成功率的挑战:

LinkedIn 统计:尽管大约 90% 的时间里 LLM 的响应包含了正确格式的参数,但仍有约 10% 的时间 LLM 会出错,常常输出不符合提供的架构,甚至不是有效的 YAML。

虽然这些错误对于人类来说很容易发现,但却会导致解析代码出错。10% 的错误率足够高,不能轻易忽视,针对这个问题,LinkedIn的解决方案是编写了一个内部防御性 YAML 解析器:

1、YAML 解析器

用日志记录常见的 YAML 错误,优化自己的 YAML 解析器,可以解析 LLM 返回的不规范的 YAML;

下面是一个python版本的yaml格式修正代码,可以根据自己实际的yaml错误做优化改进。

import yaml
import re

def preprocess_yaml(yaml_string):
    # 修复常见的缩进错误,确保子项至少比父项多两个空格
    lines = yaml_string.split('\n')
    processed_lines = []
    indent_level = 0
    for line in lines:
        stripped_line = line.lstrip()
        current_indent = len(line) - len(stripped_line)
        if ':' in stripped_line and stripped_line.endswith(':') and not stripped_line.endswith(': '):
            # 为冒号后确保有一个空格
            line = line.replace(':', ': ')
        if current_indent < indent_level and stripped_line:
            # 如果当前缩进小于预期缩进但非空行,则增加缩进
            line = '  ' * indent_level + stripped_line
        if stripped_line.endswith(':'):
            # 如果行以冒号结尾,则增加预期缩进级别
            indent_level = current_indent + 2
        processed_lines.append(line)
    return '\n'.join(processed_lines)

def parse_yaml(yaml_string):
    try:
        # 预处理 YAML 字符串以修正格式错误
        preprocessed_yaml = preprocess_yaml(yaml_string)
        # 尝试解析修正后的 YAML 字符串
        data = yaml.safe_load(preprocessed_yaml)
        print("YAML 解析成功:", data)
    except yaml.YAMLError as e:
        # 处理 YAML 解析错误
        print("YAML 解析错误:", e)
    except Exception as e:
        # 处理其他可能的异常
        print("其他错误:", e)

# 示例用的 YAML 字符串,包含一些格式错误
yaml_example = """
name: John Doe
age: 34
children:
  - name: Jane Doe
    age: 10
    - hobby: reading  # 示例的格式错误,应该更多缩进
"""

parse_yaml(yaml_example)
  • 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.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.

2、修复解析错误的Prompt

如果还是无法解析则将错误信息交给 LLM 修复,并且不断优化提示词,提升 LLM 修复的成功率;

下面是一个修复解析错误的Prompt示例:

"你有一段包含错误的 YAML 文本,其中可能包括不正确的缩进、缺失的冒号、多余的字符或其他语法问题。
请仔细检查以下 YAML 内容,并纠正所有格式错误,使其成为一个有效的 YAML 格式。
请确保所有键值对都正确分隔,并且列表项正确缩进。
完成后,请输出修正后的完整 YAML 文本。

错误的 YAML 内容:
<incorrect_yaml_content>
name: John Doe
age: 34
children:
  - name: Jane Doe
    age: 10
    - hobby: reading  # 示例的格式错误,应该更多缩进
</incorrect_yaml_content>

请修复以上内容,并提供一个有效的 YAML 结构。"
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

你可以根据实际情况,不断迭代优化这个Prompt。

通过上面步骤的努力,LinkedIn 最终结构化数据的错误率从 10% 下降到 0.01%。

总结

在开发过程中,使用LLM生成结构化数据时,经常会出现格式错误。

为了提高生成数据的准确性和稳定性,LinkedIn选择了YAML作为数据序列化格式,主要因为其简洁性和较低的Token消耗。YAML的优点包括较高的人类可读性、灵活性和容错性。它支持在文件中添加注释,不严格要求数据结构元素使用逗号和括号分隔,自动检测数据类型,且通过缩进来表示层级结构,这些特点使其在某些情况下比JSON更容错。

尽管如此,LLM生成的YAML有时仍然存在约10%的错误率。为应对此问题,LinkedIn开发了一个内部防御性YAML解析器,通过记录常见错误、优化解析器和不断调整提示词,将错误率显著降低至0.01%。