Skip to content
This page has been auto-translated and may contain errors.View in English

结构化输出

docs.scrimba.com

首先,是问题的本质。结构化输出是让模型返回代码可以直接读取的数据的实践——你可以对其中的字段做判断,而不必手动解析一段文本。本章讲的是如何让这一点可靠。

到目前为止,模型返回的是文本,当由人类阅读时没问题。但软件无法对段落做太多处理。要在代码中基于模型的答案采取行动,你需要的是数据:一个你可以读取的字段、一个你可以做判断的值、一条你可以保存的记录。

这个转变虽然很小,但它改变了下游的一切。一旦模型返回 { "sentiment": "negative" } 而不是关于客户似乎不满意的句子,你的代码就可以路由工单、更新仪表板或发送警报。有趣的部分是你如何让模型在只会预测文本的情况下生成干净的数据,答案揭示了一个你没有意识到的杠杆。

模型返回文本。你的代码需要数据:一个要读取的字段、一个要做判断的值、一条要存储的记录。本章是这两者之间的桥梁,而转变是停止希望模型格式化得很好,开始限制它能输出什么。

这里有一个真正的杠杆,而不是提示词技巧。来自大语言模型如何工作的同一采样循环可以被引导,使输出被强制成你想要的形状,这将一个模糊的文本生成器变成了其余系统可以依赖的组件。

返回散文的模型是一个你无法组合的组件。一旦它的输出流向另一个系统,你需要具有已知形状的数据,你需要获得该数据的失败模式是你能看到并处理的模式。

本章关于将生成限制到一个模式以及这在延迟方面的代价、它不能为你买到的东西(正确的值),以及它如何在边界处失败。底层的机制与驱动工具使用的机制相同:工具调用是结构化输出,用函数签名代替模式。

请求 JSON

第一直觉是在提示词中请求:"回复为 JSON",这是软件用来在花括号内用字段和值传递数据的纯文本格式。试试看,它通常可以工作,这正是陷阱所在。模型仍然只是在预测可能的文本,有时可能的文本是一个代码栏、在数据前的友好"好的,给你"或在它后面的尾部注释。任何这些都会破坏读取 JSON 的代码。友善地请求会偏向预测朝向 JSON;它不会强制实现。

所以提供商给你一个真正的杠杆。使用 response_format 打开 JSON 模式的作用不仅仅是请求:它限制了模型,使语法以有效的 JSON 形式返回。

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": '提取城市和温度。回复为 JSON: { "city": string, "tempC": number }.'},
        {"role": "user", "content": "现在北京大约是 18 度。"},
    ],
    response_format={"type": "json_object"},  # 将输出约束为有效的 JSON 语法
)

data = json.loads(response.choices[0].message.content)
print(data["city"], data["tempC"])  # "北京" 18

response_format={"type": "json_object"} 使输出成为有效的 JSON 语法,所以 json.loads 不会被分散的散文阻碍。你仍然需要在提示词中描述想要的字段,因为 JSON 模式承诺有效的 JSON,而不是你心目中的特定字段。它排除了"根本不是 JSON",而不是"形状错误的 JSON"。它仍然不能承诺一个:如果回复在中途被截断,你可能会收到一个半 JSON 对象,所以这是可靠的语法,而不是在每种情况下的硬保证。

Juno请求 JSON 仅在提示词中请求 JSON 是不稳定的,因为模型只预测可能的文本,可能将其包装在围栏或打乱的内容中,破坏解析。将 response_format 设置为 JSON 模式会约束语法,使其以有效的 JSON 形式返回。它不承诺字段匹配你要求的,在中途截断的回复仍然可能作为半对象到达,所以在提示词中描述形状并检查返回的内容。

本能是在提示词中写"回复为 JSON",其中 JSON 是软件传递字段和值的数据格式。它通常可以工作,这正好是问题所在:通常不是契约。模型预测可能的文本,可能的文本有时包括代码栏、"好的!"前言或尾部注释,所有这些都会破坏 json.loads

JSON 模式是第一个真正的杠杆。将 response_format 设置为 JSON 对象类型会约束生成,使语法成为有效的 JSON,而不是碰巧看起来像它的散文。

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": '提取城市和温度。回复为 JSON: { "city": string, "tempC": number }.'},
        {"role": "user", "content": "现在北京大约是 18 度。"},
    ],
    response_format={"type": "json_object"},  # 将输出约束为有效的 JSON 语法
)

data = json.loads(response.choices[0].message.content)
print(data["city"], data["tempC"])  # "北京" 18

这里 response_format 的形状是 OpenAI 的;其他提供商在他们自己的字段下公开同样的想法,所以移植概念,而不是字面键。JSON 模式约束语法,永远不约束字段形状。你在提示词中描述字段,模型可以自由地重命名、删除或不同地嵌套它们,同时仍然发出有效的 JSON。

还有一个裂缝:JSON 模式仅在响应完成时承诺格式良好的 JSON。如果你的 max_tokens 天花板在对象中间被命中,你会得到截断的、无法解析的输出,所以解析仍然需要一个保护。

Juno请求 JSON JSON 模式约束语法,使模型停止将数据包装在围栏和打乱内容中,但它永远不钉住字段,所以模型仍然可以重命名或删除它们。response_format 键是 OpenAI 的形状;其他提供商用不同的方式拼写它。而且承诺仅适用于完成的响应,所以被 max_tokens 截断的回复到达时无法解析。无论如何都要保护解析。

提示词中的"回复为 JSON"是一个建议,建议在负荷下失败:这里是代码栏,那里是前言,当模型感到乐于助人时是尾部注释,每一个都是生产中的 json.loads 异常。JSON 模式(这里 response_format={"type": "json_object"})用生成语法的约束替换建议,所以返回的字节解析为 JSON。

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": '提取城市和温度。回复为 JSON: { "city": string, "tempC": number }.'},
        {"role": "user", "content": "现在北京大约是 18 度。"},
    ],
    response_format={"type": "json_object"},  # 将输出约束为有效的 JSON 语法
)

data = json.loads(response.choices[0].message.content)
print(data["city"], data["tempC"])  # "北京" 18

准确地说明这约束了什么。JSON 模式钉住语法,而不是模式,且仅适用于完成的响应。模型仍然选择字段名,可以省略必需字段,并可以任意嵌套。

该保证以生成完成为条件:如果模型在对象中间遇到 max_tokens,你会得到一个不是有效 JSON 的截断字符串,这意味着 JSON 模式不移除的一个失败模式是你的重试路径最需要处理的那个。显示的 response_format 键是 OpenAI 的;等效的在其他提供商上位于不同的名称下,所以将概念视为可移植的,将字面字段视为不可移植的。要获得形状保证,你需要一个模式,这是下一部分。

Juno请求 JSON JSON 模式在完成的响应上约束语法,仅此而已:模型仍然命名、省略和嵌套字段,就像它喜欢的那样,一个被 max_tokens 杀死的生成会交给你截断的、无法解析的输出。那个截断情况是你的错误路径必须拥有的,因为模式不会。response_format 形状是 OpenAI 的,所以移植想法,而不是键。要获得形状,你需要一个模式。

使用模式锁定形状

JSON 模式保持语法有效,但模型仍可能重命名字段、删除一个或不同地嵌套内容。要消除那个摇晃,给它一个**模式**:字段和类型的精确描述,输出必须具有的形式,模型被要求填充的表单。使用 strict: true,输出然后保证与该形式匹配。

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "从消息中提取联系人信息。"},
        {"role": "user", "content": "嗨,我是李明,可以通过 [email protected] 联系我。"},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "contact",
            "strict": True,  # 精确执行模式
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "email": {"type": "string"},
                },
                "required": ["name", "email"],
                "additionalProperties": False,
            },
        },
    },
)

contact = json.loads(response.choices[0].message.content)
# {"name": "李明", "email": "[email protected]"}

当模型只预测令牌时,模式如何成为硬保证?因为提供商限制它被允许预测的令牌。在每一步,模型仍然对所有可能的下一个令牌进行排名,但系统会删除会破坏你的模式的令牌,模型从剩下的中选择。

如果模式说下一个字段必须是 email,会开始任何其他字段的令牌会在模型选择之前被从表上删除。不合形状的令牌被删除,所以模型不能漂离形状。

有一个值得了解的较软的效果。你选择的字段名称在模型预测值时成为它读取的内容的一部分,所以一个清晰的名称引导更好的答案。被要求填充 tempC,模型倾向于摄氏度数;被要求填充 value,它的依据远少。清楚地命名字段是部分指令,部分模式。

Juno使用模式锁定形状 模式是模型必须填充的表单,使用 strict: true 它得到精确的字段和类型,因为系统删除任何会破坏形状的令牌,然后模型才选择。那是执行,而不是礼貌的请求。也清楚地命名你的字段,因为字段名称是模型在预测值时读取的上下文。我花了一段时间才相信像 tempC 这样的名称会做真实的工作。

JSON 模式钉住语法但让模型可以自由地重命名、删除或重新嵌套字段。一个模式关闭了那个差距:字段和类型的精确描述,使用 strict: true 输出保证与之匹配。

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "从消息中提取联系人信息。"},
        {"role": "user", "content": "嗨,我是李明,可以通过 [email protected] 联系我。"},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "contact",
            "strict": True,  # 精确执行模式
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "email": {"type": "string"},
                },
                "required": ["name", "email"],
                "additionalProperties": False,
            },
        },
    },
)

contact = json.loads(response.choices[0].message.content)
# {"name": "李明", "email": "[email protected]"}

该机制是**受限解码**:应用于采样步骤的令牌修剪。在每个位置,模型对所有令牌进行其常规排名,然后系统屏蔽任何会使输出停止与模式匹配的令牌,模型从幸存者中采样。如果语法说下一个令牌必须开启 email 字段,会开始任何其他内容的令牌在绘制之前被清零。模型不能漂离形状,因为不合形状的移动被从棋盘上删除,而不是在提示词中劝阻。

两个实用的句柄。字段名称是指令,所以为你想要的值命名它们。tempC 将模型引导到摄氏度,其中 value 让它猜测,字段上的描述更硬引导。并且相同的约束执行 enum(允许值的固定列表)或数字范围,这就是使分类可靠的原因:在标签位置,只有你列出的标签在掩码中存活,所以模型不能创造一个不在你列表上的类别。

Juno使用模式锁定形状 使用 strict: true,受限解码在模型采样之前屏蔽会破坏模式的每个令牌,所以形状被执行,而不是请求。靠它:字段名称和描述是指令,所以命名 tempC 而不是 value 模型填充得更好。相同的掩码执行 enum,这就是分类成立的原因,模型不能发明一个不在你列表上的标签。

JSON 模式让你获得可解析的字节;一个模式让你获得已知的形状。使用 strict: true 输出保证与你声明的字段集和类型匹配,这是可以盲目索引到的数据和必须防御性探测的数据之间的区别。

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "从消息中提取联系人信息。"},
        {"role": "user", "content": "嗨,我是李明,可以通过 [email protected] 联系我。"},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "contact",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "email": {"type": "string"},
                },
                "required": ["name", "email"],
                "additionalProperties": False,
            },
        },
    },
)

contact = json.loads(response.choices[0].message.content)

该机制是**受限解码**,你想要从头理解它,因为它的代价不是免费的。模型在每一步对其词汇表中的每个令牌发出概率。受限解码将你的模式编译成语法(哪些令牌序列是合法的规则集),然后在每一步构建一个掩码,将每个非法令牌的概率设置为零,所以采样只能着陆于一个迄今为止保持输出有效的令牌。形状由构造强制,而不是通过请求。

这个编译不是免费的,它表现为延迟。针对新模式的第一次调用支付一次性代价来构建语法,所以针对新鲜模式的冷启动延迟比稳定状态高。保持你的模式稳定和重用,而不是按请求生成,否则你会一遍又一遍地支付该编译。

additionalProperties: false 和完整 required 列表不是装饰:它们是让系统直接拒绝额外或缺失字段的东西,在支持模式驱动拒绝的提供商上,它们也是让模型提供一个干净的方式返回结构化拒绝而不是格式错误的对象。删除它们,你扩大了什么算作有效,这与你选择严格模式的原因相反。

一些生产现实。严格支持不均匀:一些提供商执行真实语法,其他人近似它或仅支持 JSON 模式的子集(无 pattern,有限嵌套),所以测试你的提供商实际上采用了什么,而不是假设规范。部分和流式输出是尴尬的情况:当令牌流式传输时,你持有一个句法不完整的对象,所以要么缓冲到完成后才解析,要么使用能容忍半构建结构的增量解析器。这是与工具使用相同的机制:函数的参数模式以相同的方式被约束,所以这里的一切都直接转移到使工具调用可靠。

Juno使用模式锁定形状 受限解码将你的模式编译为语法,并在采样之前零出每个非法令牌,所以形状由构造强制。那个编译对新模式的第一次调用造成延迟代价,所以重用模式而不是每请求创造一个。保持 additionalProperties: false 和完整 `required` 列表,它们是执行拒绝和在某些提供商上干净结构化拒绝的东西。严格支持不同,流式传输给你半对象,相同的掩码驱动工具调用参数,所以你在这里学到的一切都有两倍的回报。

仍然要验证

模式约束形状,但无论如何都将输出视为来自外部世界的数据。形状可以有效,而内容是错误的:提取的电子邮件实际上是一个打字错误,模型猜测的数字,字段留空因为输入不包含它。而调用仍然可以以普通方式失败,比如被 max_tokens 截断并作为截断的 JSON 到达。防御性解析:有效的形状不是正确的值。

python
import json

def parse_contact(raw):
    try:
        data = json.loads(raw)
        if not data.get("name") or not data.get("email"):
            return None  # 存在但为空
        return data
    except json.JSONDecodeError:
        return None  # 不是有效的 JSON(例如,截断的回复)

contact = parse_contact(response.choices[0].message.content)
if not contact:
    pass  # 处理失败:重试、重新询问或显示友好错误

这是来自大语言模型如何工作的**幻觉**课程以新的服装出现:形状正确不会使值正确。模式保证你得到一个 name 字段;它不保证名字是正确的。在解析周围加一个 try/except 加上值的检查,是针对格式良好但仍然错误的回复的便宜保险。

Juno仍然要验证 模式保证形状,永远不保证值的真相,所以有效的记录仍然可以持有错误或空值,调用仍然可能到达截断。将解析包装在 try/except 中并在信任之前检查值。提前决定验证失败时会发生什么,重试或友好错误,所以稍后不会很乱。

严格模式保证形状,不保证内容。输出可以匹配模式且仍然是错误的:一个看起来合理的电子邮件实际上是打字错误,一个为填充必需字段而发明的数字,一个空字符串,其中输入曾经包含什么东西。而解析可能在响应在 max_tokens 上截断时完全失败。所以你验证,每一次。

python
import json

def parse_contact(raw):
    try:
        data = json.loads(raw)
    except json.JSONDecodeError:
        return None, "unparseable"  # 截断或格式错误

    if not data.get("name") or not data.get("email"):
        return None, "empty_field"  # 存在但空白
    if "@" not in data["email"]:
        return None, "bad_email"  # 形状可以,值不行
    return data, None

contact, error = parse_contact(response.choices[0].message.content)
if error == "unparseable":
    pass  # 重试一次,可能是截断:提高 max_tokens 或缩短输入
elif error:
    pass  # 记录值级别失败并回退

该模式是一个**防御性解析**,区分失败模式,因为它们需要不同的处理。一个 JSONDecodeError 通常意味着截断,所以修复是提高 max_tokens 或缩小输入并重试。一个空的或超范围的值是内容失败,不是格式的,所以重试相同调用很少有帮助;记录它并回退。

分离"不能解析"来自"解析但错误",因为修复不同。这联系回幻觉:受限解码删除了格式错误输出失败模式并完全保留了错误值的那一个。

Juno仍然要验证 严格模式钉住形状,不钉住内容,所以每一次验证:记录可以格式良好且持有打字错误、猜测或空白。分割失败,因为 JSONDecodeError 通常意味着截断(提高 max_tokens 或缩小输入并重试)而坏值是内容未命中,重试相同调用不会修复。受限解码删除了格式错误输出问题并保留了错误值问题。

受限解码关闭了格式错误输出失败模式,关闭了没有其他的。形状保证;内容不是。一个输入永远不支持的必需字段用自信的猜测填充,一个数字落在任何合理范围之外,一个电子邮件解析为字符串但是打字错误。

格式保证本身在截断上失效。所以验证不是可选的卫生,它是抓住模式结构不能的那一层。

python
import json

def parse_contact(raw, retry):
    try:
        data = json.loads(raw)
    except json.JSONDecodeError:
        return None, "truncated"  # 不完整生成,不是内容问题

    if not data.get("name") or "@" not in data.get("email", ""):
        return None, "invalid_value"  # 形状持有,值没有
    return data, None

contact, failure = parse_contact(raw, retry=False)
if failure == "truncated":
    pass  # 提高 max_tokens 或修剪输入,然后重试调用
elif failure == "invalid_value":
    pass  # 修复路径:重新询问,将坏输出反馈,或路由到回退

三样东西将一个堅實的验证器与吞掉一切的 try/except 分开:

  • 分类失败:截断是一个通过以更多 max_tokens 重试修复的预算问题,而无效值是盲目重试会重现的内容问题。
  • 为值级别情况构建一个**修复路径**,而不仅仅是回退。将无效输出反馈回去,带有修复它的指令,通常在一个额外调用中恢复它,这比失败请求便宜,但上限重试,所以持续错误的输入不能循环。
  • 界定代价:每次重试是另一个计费调用和另一轮延迟,所以重试预算是设计的一部分,不是事后想法。

验证值,而不仅仅是解析。这是应用到结构化数据的幻觉包含故事:受限解码给你一个干净信封,并对里面什么说什么都不是,所以信封正好是团队停止验证并获伤的地方。模式验证、范围和语义检查以及有界修复循环是三个层,它们抓住不同的东西:模式抓住形状,检查抓住无意义值,修复循环恢复可挽救的。

Juno仍然要验证 严格模式删除格式错误输出并完全保留错误值,所以验证内容,而不仅仅是解析。分类失败:截断是预算修复(更多 max_tokens,重试),无效值是盲目重试只会重现的内容未命中,所以构建一个修复路径,将坏输出反馈回去,并上限重试,所以坏输入不能循环你的账单。干净信封正好是人们停止检查并获伤的地方。

两个模式:分类和提取

大多数结构化输出工作是两个形状之一。

**分类**将输入排序到固定标签集中的一个。模式中的 enum 将输出约束到恰好那些标签,使用相同的令牌修剪技巧:在标签位置,只有允许的值可以被预测,所以模型不能发明一个不在你列表上的类别。

python
# 分类的模式片段
{"type": "string", "enum": ["billing", "technical", "general"]}

提取从自由文本中拉出特定字段,比如前面的联系人示例:一个名字、日期、金额、产品名称列表。分类和提取一起覆盖实际 AI 特征的大部分:路由支持工单、标记内容、将电子邮件转变为记录、阅读收据。两者都将一个模糊的文本生成器转变成你的代码可以构建的可靠组件。

Juno两个模式:分类和提取 分类将输入排序到固定标签集中的一个,使用 enum 锁定,所以模型不能发明一个不在你列表上的类别。提取从自由文本中拉出命名字段到记录。两者都将一个模糊的文本生成器转变成可靠组件,它们一起涵盖大量实际 AI 特征。

几乎你用结构化输出构建的一切是两个形状之一,命名它们帮助你选择正确的模式。

**分类**将输入映射到一个固定集中的一个标签。enum 承载整个工作:受限解码在标签位置掩码除你列出的标签以外的每个令牌,所以模型返回其中之一或什么都不返回。

python
# 分类:来自封闭集的一个标签
{"type": "string", "enum": ["billing", "technical", "general"]}

提取从自由文本拉出命名字段到记录,联系人和工单示例。两者组合:一个单一的模式可以同时分类和提取,这是大多数实际特征。封闭集得到 enum;开放值得到一个类型化字段。

有意识地做的决定:当一个值真的是固定集(状态、类别、优先级),使用 enum 使模型不能漂离,当它是开放的(一个名字、一个总结),使用一个纯类型化字段并接受你足够信任值来验证它。混淆它们,一个 enum 其中真实世界有更多情况,或一个自由字段其中一个封闭集会有抓住错误,是这些特征安静地表现不好的地方。

Juno两个模式:分类和提取 两个形状覆盖大多数这个:分类映射到一个封闭集中的一个标签(enum 做执行),提取拉出命名字段到记录,一个模式可以同时做两个。有意识地选择:对固定集 enum,所以模型不能漂离,对开放值纯类型化字段,你将验证。一个缺少真实世界情况的 enum 是错标签的安静来源。

结构化输出崩陷为两个模式,区别是设计杠杆,不仅仅是词汇。分类映射输入到一个封闭集中的一个标签,由受限解码减少到你值之间的硬选择的 enum 执行。提取从自由文本拉出类型化字段。大多数生产模式组合它们。

python
# 分类:受限解码仅在标签位置允许这些令牌
{"type": "string", "enum": ["billing", "technical", "general"]}

杠杆是你在哪里绘制封闭集边界,因为 enum 是有尖锐边缘的正确性保证。一个值,实际上是有界的(状态、优先级)属于 enum,所以模型不能发出任何不在清单上的东西,那打开了下游代码可以依赖的检查。

但 enum 即使没有任何适合时也强制选择:如果实际输入不匹配任何标签,模型被约束来选择最接近的错误的,而不是告诉你它不合适。一个没有逃脱舱门的 enum 把"不合适"变成自信错标签。所以对于任何面向混乱真实输入的分类器,添加一个显式**逃脱舱门**,一个 otherunknown 成员,并在它上读作信号以路由到别处。

相同的谨慎适用于信心:如果决定依赖标签,让模型也发出信心或简短理由字段,所以一个边界线调用是可见的,而不是洗入一个干净看的 enum 值。这些是与工具使用相同的本能,其中模型从一个封闭集中选择一个工具,你面对相同的"如果没有任何一个适合"问题。

Juno两个模式:分类和提取 分类是一个由 enum 执行的封闭集,提取是来自自由文本的类型化字段,enum 是一个有尖锐边缘的保证:它即使什么都不合适也强制选择,洗"不匹配"成自信错标签。添加 otherunknown 成员并路由它,在决定依赖标签时发出信心字段。与从一个封闭集中选择一个工具的工具使用相同形状。

在实践中

将混乱的支持电子邮件变成结构化工单,在一个模式中组合分类和提取:

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "将支持电子邮件变成工单。"},
        {"role": "user", "content": email_text},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "ticket",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "category": {"type": "string", "enum": ["billing", "technical", "general"]},
                    "urgency": {"type": "string", "enum": ["low", "medium", "high"]},
                    "summary": {"type": "string"},
                },
                "required": ["category", "urgency", "summary"],
                "additionalProperties": False,
            },
        },
    },
)

ticket = json.loads(response.choices[0].message.content)
# {"category": "billing", "urgency": "high", "summary": "上个月的订阅被重复扣费。"}

一个调用以两种方式分类电子邮件并提取总结,返回你的代码可以路由和存储的记录,形状保证且值仍然值得检查。到此为止,一切都是流向文本。接下来你给模型一个完全不同的感觉:在嵌入,文本变成数字,你可以按意义比较,搜索和处理你自己文档的基础。

Juno在实践中 一个带严格模式的调用可以以两种方式分类电子邮件,并在一次调用中提取总结,返回你的代码可以路由和存储的记录。形状保证;值仍然值得检查。工具使用稍后依赖相同的技巧:模型发送到工具的参数是结构化输出。

一个真实特征通常在一个模式中组合两个模式。这里支持电子邮件变成工单:两个分类和一个提取在一个调用中。

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "将支持电子邮件变成工单。"},
        {"role": "user", "content": email_text},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "ticket",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "category": {"type": "string", "enum": ["billing", "technical", "general"]},
                    "urgency": {"type": "string", "enum": ["low", "medium", "high"]},
                    "summary": {"type": "string"},
                },
                "required": ["category", "urgency", "summary"],
                "additionalProperties": False,
            },
        },
    },
)

ticket = json.loads(response.choices[0].message.content)
# {"category": "billing", "urgency": "high", "summary": "上个月的订阅被重复扣费。"}

两个 enum 被约束硬,所以 categoryurgency 总是可路由的。summary 是一个开放字段,所以它是在你信任它前要验证的。那个分割,执行的标签加检查的自由文本字段,是大多数提取特征的形状。接下来,嵌入给模型一个不同的感觉:文本变成数字,你可以按意义比较,搜索和处理你自己文档的基础。

Juno在实践中 一个严格模式可以以两种方式分类和在一个调用中提取总结,返回你可以路由和存储的记录。enum 被执行所以标签安全地行动;开放 summary 是要验证的字段。那个锁定标签和检查自由文本混合是提取特征的日常形状。

日常生产形状是一个复合:在一个或两个轴上分类并提取一个自由文本字段,都在一个模式和一个调用中。

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "将支持电子邮件变成工单。"},
        {"role": "user", "content": email_text},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "ticket",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "category": {"type": "string", "enum": ["billing", "technical", "general"]},
                    "urgency": {"type": "string", "enum": ["low", "medium", "high"]},
                    "summary": {"type": "string"},
                },
                "required": ["category", "urgency", "summary"],
                "additionalProperties": False,
            },
        },
    },
)

ticket = json.loads(response.choices[0].message.content)
# {"category": "billing", "urgency": "high", "summary": "上个月的订阅被重复扣费。"}

强制跨字段是不均匀的,那是设计要点。enum 是硬保证,所以 categoryurgency 可以安全地路由,不进一步检查。summary 是无约束字符串,所以它完全是一个幻觉或歪曲的值可以隐藏在干净模式后面的地方,字段在验证通行证中赚取。两个执行标签与一个开放 summary 字段,也是最暴露于截断的模式:一个长生成的总结是你运入 max_tokens 的东西,将整个对象变成不可解析,所以大小预算对自由文本字段,而不是标签。

这是与工具使用相同的机制:工具调用是模型对函数模式发出结构化参数,以相同的方式约束,所以验证、修复和 enum 逃脱舱门本能在这里直接转移到使代理可靠。从这里,嵌入将模型转变到一个不同的模式:文本作为向量你按意义比较,搜索和在你自己文档中基础的基础。

Juno在实践中 一个严格模式,两个执行 enum 和一个开放 summary:标签可以安全地路由,自由文本字段是幻觉值隐藏的地方,长生成变成 `max_tokens` 截断的地方,所以特别预算和验证它。这是与工具使用相同的机制,参数对函数模式约束,所以这里的每个习惯、验证、修复、enum 逃脱舱门,直接进入代理。