难用!OpenAI结构化输出

我开发 AI 应用程序越多,就越意识到 LinkedIn 和 TikTok 上的噪音并非来自真正开发 AI 应用程序的人。它来自想要成为影响者的人。

他们喜欢谈论 AI 的最新进展……但与此同时,他们自己从未尝试过。或者,他们可能已经尝试过最小的玩具示例,但还没有创建真正的生产用例。

我最近注意到的一个例子是 OpenAI 的结构化输出。尽管这个版本更像是一个错误修复,但它被认为是 AI 应用程序的重大事件。

OpenAI 已经有了函数调用,它迫使你提供非常冗长的 JSON 模式;它根本不起作用。无法保证响应符合模式;你最好在指令中乞求模型按照你希望的方式响应。

现在,OpenAI 声称通过结构化输出,他们已经解决了这个问题。

我不同意。

1、什么是结构化输出?

结构化输出是一种强制大型语言模型以特定格式响应的方法。据我所知,目前唯一支持的结构化输出是 JSON,尽管你可以想象未来会支持其他格式,如 CSV 或 SQL。

这个想法是,你为模型提供 JSON 模式,模型将只输出该格式的响应。

你可以想象这对 LLM 应用程序很重要。如果模型在响应中不包含必填字段,则可能会破坏整个应用程序。

例如,假设你使用 LLM 调用股票交易 API。你需要提供日期、股票代码以及你想从 API 接收的数据类型(资产负债表、现金流或衍生基本面)。如果模型不提供股票代码,API 调用会发生什么?

它会失败。

开发人员之前已经通过提示工程、应用程序层中的广泛验证逻辑和重试逻辑解决了这个问题。但现在,OpenAI 声称已经通过结构化输出解决了这个问题。

我在这里要大声说,

No,他们还没有真正解决这个问题。

2、结构化输出示例

在此示例中,我们定义了一个需要三个字段的 JSON 架构: “date”、 “ticker” 和 “dataType”。使用枚举将“dataType”字段限制为特定值。该架构(schema)还指定不允许使用其他属性:

{
  "title": "Example of Structured Outputs",
  "description": "This example demonstrates a JSON schema for stock data and a corresponding JSON output.",
  "schema": {
    "$schema": "http://json-schema.org/draft-07/schema#",
    "type": "object",
    "properties": {
      "date": {
        "type": "string",
        "format": "date"
      },
      "ticker": {
        "type": "string"
      },
      "dataType": {
        "type": "string",
        "enum": ["balanceSheet", "cashFlow", "derivedFundamentals"]
      }
    },
    "required": ["date", "ticker", "dataType"],
    "additionalProperties": false
  }
}

此 JSON 架构对应于以下 JSON:

{
  "date": "2024-08-12",
  "ticker": "AAPL",
  "dataType": "balanceSheet"
}

示例输出显示了符合此架构的响应的外观。此结构可确保进行 API 调用以检索股票数据所需的所有信息均已存在且格式正确。

使用结构化输出,其理念是语言模型将始终生成与此架构匹配的响应,从而防止出现字段缺失或数据类型不正确等问题,这些问题可能会破坏应用程序中的下游流程。

3、OpenAI的结构化输出

JSON 模式的确切规范定义明确。有些字段是必需的,有些是可选的。由于 OpenAI 选择使用 JSON 模式规范,因此可以合理地假设你可以通过提供语法有效的 JSON 模式来使用该 API。

然而,他们错了。OpenAI 似乎对他们认为有效的 JSON 模式有一堆任意规则。而且,即使你允许他们违反规则,并向你的应用程序引入奇怪的补丁逻辑以使用他们的 API,它仍然不起作用。至少不是你期望的那样。

下面是我测试它的方法。

4、方法论:我对结构化输出的疯狂用例

就我个人而言,我讨厌将配置逻辑与我的代码库耦合在一起。非技术用户可能想要创建一个提示并开始使用它;为什么他们需要知道如何编码?

因此,我构建了一个外部平台 NexusGenAI 来管理我的 LLM 提示。

NexusGenAI 的功能之一是在前端构建语法有效的 JSON 模式并将其附加到提示。然后在后端,我们将模式注入模型以获得响应。

因此,我已经构建了一个用于指定有效 JSON 模式的框架。将模式集成到 OpenAI 中应该是小菜一碟,对吧?

好吧,让我们进行测试。我使用以下函数向 OpenAI 发送请求:

private async chat(
  messages: OpenAiChatMessage[],
  userId: Id,
  model: string,
  temperature: number,
  func?: IJson
): Promise<OpenAiChatResponse> {
  const apiKey = process.env.OPENAI_API_KEY;
  if (!apiKey) {
    throw new Error("API key for OpenAI is missing");
  }
  const headers = {
    "Content-Type": "application/json",
    Authorization: `Bearer ${apiKey}`,
  };
  func = this.cleanSchemaAndSetProperties(func);
  const data: OpenAiChatRequest = {
    model: model,
    user: userId,
    messages,
    temperature: temperature,
    tools: func
      ? [
          {
            type: "function",
            fu

对于工具参数,我向 API 指定了以下模式(在前端配置):

{
  "name": "indicator",
  "description": "indicator",
  "parameters": "indicator",
  "title": "indicator",
  "type": "object",
  "properties": {
    "indicator": {"$ref": "#/definitions/indicator"},
    "definitions": {
      "indicator_value": {
        "type": "object",
        "properties": {"value": {"type": "number"}},
        "type": {"const": "Value", "type": "string"},
        "additionalProperties": false,
        "required": ["value", "type"]
      },
      "indicator_price": {
        "type": "object",
        "properties": {
          "type": {"const": "Price", "type": "string"},
          "targetAsset": {"$ref": "#/definitions/targetAsset"}
        },
        "additionalProperties": false,
        "required": ["type", "targetAsset"]
      },
      // ... 更多指标定义
    }
  },
  "additionalProperties": false,
  "required": ["indicator"],
  "strict": true
}

上面的模式代表一个“指标”对象。在我的应用程序中,指标就是任何可以计算为数字的东西,包括技术指标、基本指标、常量值和数学运算。

我首先要说的是,这个模式庞大而复杂。有递归定义、引用、常量、枚举等等。

但这不是正当的借口。尽我所能,我已经确认该模式在语法上是有效的。我使用了一个外部 JavaScript 库 React JSON Schema Form 来验证模式是否在前端呈现。我还要求所有不同的语言模型检查并确认它的有效性。

模式是有效的。

然而,尽管提供了有效的模式,让 API 工作却一直是一场噩梦!

3、我遇到的错误(到目前为止)

我遇到的大多数问题都很容易通过向应用程序层添加补丁逻辑来解决。虽然实现这个逻辑并不是什么大问题,但我最大的挫败感是完全有效的模式被 OpenAI 拒绝了。

以下是我在尝试生成具有结构化输出的响应时遇到的问题的非详尽列表。

  • 必须在所有嵌套对象上设置其他属性
{
  "error": {
    "message": "Invalid schema for function 'indicator': In context=(),'additionalProperties' is required to be supplied and to be false",
    "type": "invalid_request_error",
    "param": "tools[0].function.parameters",
    "code": "invalid_function_parameters"
  }
}

出于某种原因,如果你没有在架构中每个嵌套的 JSON 对象上明确设置 additionalProperties,OpenAI 将直接拒绝该请求。根据规范,这不是必填字段,并且在他们这边设置默认值并不困难。

  • 所有嵌套对象都必须设置“required”关键字
{
  "error": {
    "message": "Invalid schema for function 'indicator': In context=(),'required' is required to be supplied and to be an array including every key in properties. Missing 'value'",
    "type": "invalid_request_error",
    "param": "tools[0].function.parameters",
    "code": "invalid_function_parameters"
  }
}

与 additionalProperties 类似,OpenAI 要求我们设置“required”参数,即使根据规范它不是必需的。

  • oneOf 不允许出现在架构中
{
  "error": {
    "message": "Invalid schema for function \"indicator': In context=(),\"oneof' is not permitted",
    "type": "invalid_request_error",
    "param": "tools[0].function.parameters",
    "code": "invalid_function_parameters"
  }
}

OneOf” 是一个非常常见的参数,用于指定对象可以是几种类型之一。如果您是 Rust 狂热者,您可以将其视为使用 Rust 枚举。或者,如果您是 TypeScript 粉丝,请将其视为以下内容:

type Indicator = ValueIndicator | SimpleMovingAverageIndicator | ...

根据 JSON 规范,OneOf 绝对是一个有效参数。OpenAI 不允许您出于任何原因指定它。因此,我需要应用程序代码在运行时将其更改为“anyOf”。

  • anyOf 不能共享相同的第一个键
{
  "error": {
    "message": "Invalid schema: Objects provided via 'anyof' must not share identical first keys. Consider adding a discriminator key or rearranging the properties to ensure the first key is unique.",
    "type": "invalid_request_error",
    "param": null,
    "code": null
  }
}

这是我遇到的最新错误消息,我觉得这是一个彻头彻尾的错误。OpenAI 抱怨 anyOf 对象的第一个键不能相同。

我明白他们为什么有这条规则:他们想限制他们那边的逻辑量来确定要为 anyOf 对象使用哪种模式。

但据我所知,这不是符合规范的规则。这是 OpenAI 的规则。在玩了大约一个小时后,我终于找到了几种解决此限制的解决方案,包括:

  • 大大简化模式
  • 使用枚举来表示具有不同类型但相同属性的指标
  • 在所有 anyOf 引用的开头添加一个随机键
  • 重新排列键的顺序,以便鉴别器(在本例中为类型)始终位于顶部

4、结束语

由于输出可能会有所改进,我不会放弃更新我的 API 以使用结构化输出。我使用传统方式的验证和重试逻辑很丑陋(至少可以这么说)。我可以消除它的那一天就是我的应用程序代码质量提高 10 倍的那一天。

但是,我很生气,我被迫更新我的应用程序代码的很大一部分只是为了让这些结构化输出正常工作。规范的目的是让双方了解数据的格式。

如果你选择使用通用规范(如 JSON 规范),那么开发人员在输入有效的 JSON 模式时期望 API 能够正常工作似乎是合理的。否则,如果另一个 LLM 提供商(如 Anthropic)实现结构化输出,他们要么必须复制 OpenAI 的奇怪规则,要么应用程序需要单独的逻辑来为 LLM 指定模式。

至少,你拥有的所有奇怪规则都应该清楚地记录在表格中。错误消息应包含对文档的引用,并提供包含该规则的原因的解释。

不要说“阅读文档”。我已经阅读了(链接)。虽然许多这些决定(如必填字段和附加参数)确实在整个文档的零星位置明确指出,但其他问题(例如我在 anyOf 中遇到的错误)在互联网上根本找不到。

不可否认,我的用例相当复杂。我还没有看到很多人使用 UI 来创建相当复杂的 JSON 模式,然后将其输入到模型中。指定附加属性(如必需参数或附加参数)对他们来说相当容易,所以他们没有抱怨。

但无论如何,这有点奇怪!例如,JSON 规范是众所周知的,具有明确定义的可选字段和必填字段。语法有效的 JSON 模式被拒绝请求感觉很奇怪。更奇怪的是,关于如何避免这些错误的文档也同样稀少。

至少,这次经历让我大开眼界。我只在网上读到过这个新功能是自神经网络诞生以来人工智能领域最好的东西。每个人都在称赞 OpenAI 发布这个功能(再次强调,这本质上是他们发布的早期功能的一个错误修复),而我是唯一一个期望有效 JSON 规范以有效 JSON 输出响应的白痴。

哦,算了!尽管我在整篇文章中抱怨和咆哮了很多,但很明显,我不能没有它们。😃


原文链接:The long awaited feature from OpenAI, “Structured Outputs”, is broken

BimAnt翻译整理,转载请标明出处