跳到主要内容

702 篇博文 含有标签「llm」

查看所有标签

生产环境中的结构化输出:如何用 LLM 生成可靠的 JSON

· 阅读需 11 分钟
Tian Pan
Software Engineer

大语言模型是文本生成器。你的应用程序需要数据结构。这两个事实之间的差距,正是生产环境中的错误滋生之地。

每个使用大语言模型构建产品的团队都会遇到这个瓶颈。模型在游乐场中表现出色——它返回的内容看起来像 JSON,字段大多正确,通常能通过 JSON.parse。然后你将其部署上线,你的解析层却在凌晨两点开始抛出异常。响应中多了一个逗号。或者是一个 Markdown 代码围栏。或者模型决定在 JSON 前面添加一段解释性文字。又或者它幻觉出一个字段名。

业界已经花了三年时间来解决这个问题。这就是目前解决方案的收敛点,以及仍然让团队头疼的问题。

三个成熟度级别

团队处理结构化输出的方式有着清晰的进展,每个级别都有一个实际的可靠性上限。

级别 1:提示工程。 你写道:“只用以下格式的有效 JSON 回复:”并展示一个示例。对于简单模式,这在 80-95% 的情况下有效。失败模式很微妙:模型在复杂提示中添加前言,在模式变长时将 JSON 包装在代码块中,或者静默地省略可选字段。你添加一个正则表达式清理步骤和一个 try/catch,然后说服自己这样没问题。

对于任何重要的应用来说,这都远不够好。95% 的解析成功率听起来很高,直到你有一个 10 步的智能体链:0.95^10 ≈ 0.60。十次智能体运行中,有六次会失败。这个数学计算是无情的。

级别 2:函数调用/工具使用。 所有主要提供商都暴露了一个 API,你可以在其中定义 JSON 模式,模型应该填充它。这能让你达到 95-99% 的可靠性。但问题是:模式是一个提示,而不是一个约束。模型将模式视为其上下文的一部分并学习遵循它——但解码过程中的任何环节都无法阻止它生成无效 token。提供商仍然可能返回格式错误的数据包,尤其是在处理复杂模式或边缘情况输入时。

级别 3:带有约束解码的原生结构化输出。 这是在数学上保证 100% 模式有效性的地方。推理引擎根据你的模式构建一个有限状态机,并在每个生成步骤中屏蔽无效 token。模型实际上无法生成无法解析的输出。OpenAI 结合 json_schemaresponse_format、Gemini 的 response_schema,以及 Outlines 等开源框架都使用了这种方法。

如果你正在构建任何需要可靠下游解析的东西——分类管道、智能体工具调用、数据提取——你都会需要级别 3。

约束解码的实际工作原理

理解其实现方式是值得的,因为它决定了你能使用和不能使用哪些模式。

在每个生成步骤中,模型都会在其整个词汇表(50,000 多个 token)上生成一个概率分布。通常,你会从该分布中采样。而使用约束解码时,你首先构建一个有限状态机,代表你的 JSON 模式中的每个有效路径。在采样之前,你会计算一个token 掩码:一个布尔向量,其中 false 意味着“给定 FSM 中的当前状态,这个 token 不能出现在这里”。你将这些 logits 置零,然后从剩余的 logits 中采样。

结果是:模型只能生成那些能够推进模式有效完成的 token。这不是后处理——它融入到每一个解码步骤中。

早期的实际开销曾是一个担忧。为复杂模式构建初始 FSM 可能需要 50-200 毫秒。但像 XGrammar(来自 MLC 团队)这样的引擎能够在每个 token 不到 40 微秒内完成 token 掩码生成,后续请求则重用缓存的 FSM,开销几乎为零。对于简单模式,延迟影响低于 5%。对于具有大型枚举集的深度嵌套模式,延迟可能达到 30-60%——这真正提示你需要简化你的模式了。

模式设计:团队常犯的错误

即使约束解码强制执行了语法有效性,糟糕的模式设计仍然会导致语义失败。以下是困扰大多数团队的模式:

将推理置于结论之前。 如果你的模式有一个 reasoning 字段和一个 classification 字段,请将 reasoning 放在前面。大语言模型从左到右生成 token。当模型在确定分类之前写出其推理时,它会产生更好的分类。如果你将答案字段放在前面,模型会在思考之前确定一个标签,然后才在推理字段中进行合理化。这听起来像大语言模型的一个怪癖,但它确实能持续地将准确性提高几个百分点。

扁平化你的模式。 嵌套是可靠性的大敌。OpenAI 的原生结构化输出最多支持 5 层嵌套和 100 个总属性。超出这个范围,语法编译时间会飙升,每个 token 的开销也会增加。更重要的是,即使有约束解码,具有 4 层以上深度嵌套的模式也表现出明显更高的错误率——模型有更多机会失去上下文。如果你的模式深度嵌套,请问问自己,这种嵌套是反映了实际的数据层次结构,还是仅仅是组织偏好。

描述每个字段。 Pydantic 的 Field(description=...) 值作为内联指令传递给模型。如果没有描述,模型将仅从字段名推断语义。confidence: float——它是 0-1 还是 0-100?status: str——有效值是什么?字段描述不是文档;它们是直接影响输出质量的提示指令。

明确处理可选性。 OpenAI 的结构化输出不支持你期望的那种可选字段。如果一个字段可以不存在,请将其建模为带有默认值 NoneOptional[str],而不是仅仅是 没有默认值的 str | None。提供商对这种区别处理不同,如果处理不当,会在运行时产生神秘的“无效模式”错误。

避免复杂模式。 带有复杂模式的正则表达式约束字段、具有多个分支的 oneOf 和递归模式会在 FSM 中造成组合爆炸。如果你需要“一个或多个与某个模式匹配的项”,请考虑将问题分解为多个顺序调用,而不是在单个模式中表达它。

实践中的服务商格局

每个主要的服务商都有不同的 API 接口,其抽象层无法清晰地在不同服务商之间转换。

OpenAI 提供了最成熟的实现。你可以使用 client.beta.chat.completions.parse() 搭配 Pydantic 模型,它能处理模式转换并返回一个类型化的 Python 对象。使用原始 JSON 模式的 response_format 方法也有效,但需要手动构建模式。.parse() 方法是正确的默认选择。

Anthropic 没有专用的结构化输出 API。其惯用模式是强制使用工具:将你的模式定义为一个工具,然后设置 tool_choice 强制模型调用它。如果没有 tool_choice: {type: "tool", name: "your_tool"},模型可能会选择根本不使用该工具。这并非约束解码——它仍然是 Level 2——但比提示工程要可靠得多。

Google Gemini 提供了带有约束解码的 response_schema,类似于 OpenAI 的方法。该 API 接受原始 JSON 模式而非 Pydantic 模型,所以你需要模式转换工具。

对于跨多个服务商工作的团队来说,Instructor 库抽象了这些差异。它在 OpenAI、Anthropic、Gemini 等平台之间提供了一个统一的 client.chat.completions.create(response_model=YourPydanticModel) 接口。Instructor 还能处理验证失败时的自动重试——如果模型返回了未能通过 Pydantic 验证的内容,它会附带错误信息重新提示并再次尝试。

验证三明治

即使在使用原生结构化输出时,也务必在其之上添加一个验证层。这并非多疑——这是为了防范语法约束无法捕捉的语义失败。

from openai import OpenAI
from pydantic import BaseModel, field_validator

class ClassificationResult(BaseModel):
reasoning: str
label: str
confidence: float

@field_validator("confidence")
def confidence_must_be_normalized(cls, v):
if not 0.0 <= v <= 1.0:
raise ValueError(f"confidence must be between 0 and 1, got {v}")
return v

@field_validator("label")
def label_must_be_valid(cls, v):
valid_labels = {"positive", "negative", "neutral"}
if v not in valid_labels:
raise ValueError(f"label must be one of {valid_labels}, got {v}")
return v

client = OpenAI()
result = client.beta.chat.completions.parse(
model="gpt-4o",
messages=[...],
response_format=ClassificationResult,
)

# result.choices[0].message.parsed is already a ClassificationResult
# but Pydantic validators run during construction, so they've already fired

模式强制结构。Pydantic 验证器强制语义。两者你都需要。

约束解码保证的是语法有效性,而非语义正确性。模型可以在一个浮点数字段中返回 confidence: 1.7 并满足模式。它也可以从模式的枚举中返回一个对于输入而言语义上错误的标签。验证器能捕捉前者;评估则捕捉后者。

智能体链中的结构化输出

在多步骤工作流中,可靠性计算会变得更糟。每个返回结构化数据的工具调用都是一个模式验证可能失败的步骤。通过 Instructor 的重试行为,失败会附带错误上下文进行重试——但重试会消耗 token 和延迟,而且某些故障模式会循环。

这里有两种有助于解决问题的模式:

在每个步骤中缩小你的模式。 不要将一个庞大复杂的模式贯穿于每个工具调用。在每个步骤中,只提取你需要用于下一步的数据。更小的模式具有更低的失败率和更少的开销。

每次调用都记录模式版本。 模式会演变,而错误往往源于模式变更未在所有地方传播。将模式版本与提示和响应一同记录。当出现问题时,你可以根据当时生效的模式重放精确的输入。

仍然存在的问题

约束解码解决了解析问题,而非建模问题。无论模式强制如何,一些故障模式依然存在:

幻觉枚举值。 如果你的模式允许 enum: ["gpt-4", "claude-3-5-sonnet", "gemini-2-0-flash"],而你添加了一个新模型却忘记更新模式,模型将被迫返回其中一个有效值——但它可能会自信地返回错误的值。模式约束并不能让模型准确;它们只是让模型可解析。

长链中的语义漂移。 在多步骤管道中,第 N 步的结构化输出会作为第 N+1 步的提示输入。意义上的错误(而非格式上的)会以解析检查无法检测到的方式累积。这时,评估和抽查比工具更重要。

调用者之间的模式不匹配。 在包含多个服务的生产系统中,调用服务中的模式定义与下游消费者期望的模式定义不一致是很常见的。将你的 Pydantic 模型视为事实的唯一来源,并将其作为一个包共享,而不是复制粘贴的字典。

默认应该是 Level 3

原生结构化输出的工程论点很简单:提示工程增加了重试复杂性,函数调用增加了验证复杂性,两者都增加了在凌晨 2 点调试起来令人烦恼的故障模式。带有 Pydantic 验证层的原生结构化输出为你提供了最强的保证,并消除了一整类生产事故。

工具链已成熟。XGrammar 使约束解码足够快,对于简单模式而言,延迟很少成为问题。Instructor 库消除了服务商特定的样板代码。在 2025 年,没有充分的理由使用 Level 1 解析交付新的 LLM 管道。

唯一的真正代价是模式设计规范。扁平模式、描述性字段、显式可选性、先推理后结论的顺序——这些都不是复杂的要求,但它们需要刻意为之。这种规范正是区分在演示中有效与在生产中可靠运行的 LLM 功能的关键。

Prompt Caching:将 LLM 成本降低 90% 的优化方案

· 阅读需 9 分钟
Tian Pan
Software Engineer

大多数基于 LLM 构建产品的团队都多付了 60%–90% 的费用。这并不是因为他们使用了错误的模型或提示词效率低下,而是因为他们在每次请求中都在重复处理相同的 Token。提示词缓存(Prompt caching)可以解决这个问题,且只需大约 10 分钟即可实现。然而,它仍然是生产级 LLM 系统中利用率最低的优化手段之一。

实际情况是:每次你向 LLM API 发送请求时,模型都会对提示词中的每一个 Token 运行注意力机制(Attention)。如果你的系统提示词(System prompt)有 10,000 个 Token,且每天处理 1,000 个请求,那么你每天仅为提示词中的静态部分(即永不变化的上下文)就要支付 1,000 万个 Token 的处理费用。提示词缓存会存储中间计算结果(即 Key-Value 注意力状态),以便后续请求可以完全跳过这部分工作。

生产环境中的 LLM 可观测性:工程师容易忽略的四个隐性故障

· 阅读需 12 分钟
Tian Pan
Software Engineer

大多数将 LLM 应用推向生产环境的团队,其日志设置常被误认为是可观测性。他们在数据库中存储提示词(prompt)和响应,在表格中跟踪 token 数量,并在 Datadog 中设置延迟告警。然而,当用户反馈聊天机器人已经连续两天给出错误回答时,没人能告诉你原因 —— 因为收集到的数据都没有告诉你模型是否真的正确。

传统监控回答的是“系统是否在线且速度多快?”而 LLM 可观测性回答的是一个更难的问题:“系统是否在做它应该做的事情,以及它在什么时候停止了这种正常行为?”当你的系统行为是概率性的、依赖上下文的,并且经常以不触发任何告警的方式出错时,这种区别就显得至关重要。

LLM 路由:如何停止为简单查询支付顶级模型的昂贵价格

· 阅读需 14 分钟
Tian Pan
Software Engineer

大多数团队都会遇到同样的拐点:LLM API 成本的增长速度超过了使用量的增长,而且每一个查询——无论是“总结这句话”还是“审计这个 2,000 行的代码库以查找安全漏洞”——都指向同一个昂贵的模型。解决方法不是挤压 prompt,而是路由。

LLM 路由意味着将每个请求引导至最适合该特定任务的模型。不是能力最强的模型,而是正确的模型——在成本、延迟和质量之间平衡,以满足查询的实际需求。如果做得好,路由可以在质量几乎不下降的情况下将 LLM 成本降低 50–85%。如果做得不好,它会产生隐性的质量倒退,直到用户流失你才会察觉。

这篇文章涵盖了其机制、权衡以及在生产环境中实际会出问题的地方。

生产级 LLM 应用的 Token 预算策略

· 阅读需 12 分钟
Tian Pan
Software Engineer

大多数团队发现他们上下文管理问题的方式都如出一辙:一个在演示中表现良好的生产级智能体,在对话进行 15 轮后开始出现幻觉。日志显示 JSON 格式正确,模型返回了 200 状态码,且没有人修改代码。变化的是累积效应——工具结果、检索到的文档和对话历史悄无声息地填满了上下文窗口,直到模型需要在 80,000 个相关性参差不齐的 Token 上进行推理。

上下文溢出(Context overflow)是显而易见的故障模式,但“上下文腐化”(context rot)则更具隐蔽性。研究表明,在达到限制之前,LLM 的性能就已经开始下降。随着上下文的增加,模型会出现“中间迷失”效应(lost-in-the-middle effect):注意力集中在输入的开头和结尾,而中间的内容则变得不可靠。埋藏在 30 轮对话中第 12 轮的指令可能会实际上消失。模型不会报错——它只是悄悄地忽略了它们。

LLM 应用压力测试:为什么 k6 和 Locust 会误导你

· 阅读需 13 分钟
Tian Pan
Software Engineer

你运行了负载测试。k6 报告平均延迟为 200ms,P99 延迟低于 800ms,在 50 个并发用户时错误率为零。你上线到了生产环境。不到一周,用户就开始反馈 8 秒的卡顿、连接中断以及流式输出中途 Token 预算耗尽。发生了什么?

测试之所以通过,是因为你衡量错了指标。传统的负载测试工具是为在几毫秒内返回完整响应的无状态 HTTP 端点设计的。LLM API 的行为与这些工具所建模的完全不同:它们在几秒钟内流式传输 Token,按 Token 而非请求计费,消耗的是 GPU 显存而非 CPU 线程,并且响应速度完全取决于缓存是否命中。一个对 /chat/completions 端点进行压测的 k6 脚本产生的数据看起来像是性能数据,但实际上几乎无法反映生产环境的真实情况。

评估与生产环境的差距:为什么测试套件的 92% 分数仅意味着 40% 的用户满意度

· 阅读需 12 分钟
Tian Pan
Software Engineer

你的团队花了三周时间构建了一个严谨的评估套件。它涵盖了各种边缘情况,包括对抗性示例。LLM 作为评测员(LLM-as-judge)在所有维度上的得分都达到了 92%。你发布了产品。

接着,客服工单接踵而至。用户反馈 AI “听不懂他们在问什么”。会话放弃率上升了 30%。满意度得分仅为 41%。

这种差距 —— 即评估表现与现实世界结果之间的鸿沟 —— 是当今生产级 AI 系统最常见的失败模式。这不是模型问题,而是衡量标准的问题。

生产级 AI 系统中的时序推理失效

· 阅读需 12 分钟
Tian Pan
Software Engineer

一个自信地推荐已经缺货六个月产品的智能体;一个告诉用户查不到 20 分钟前下单记录的客服机器人;一个针对两年前已废弃的库 API 生成可正常运行代码的编程助手。这些并不是传统意义上的“幻觉”——模型只是在回忆曾经准确的信息。这是一种完全不同的失效模式,而且大多数团队还没有准备好如何检测或防御它。

这种区分至关重要,因为缓解措施根本不同。你无法通过提示词工程解决时效性问题。你也无法通过微调来解决——对过时的知识进行微调只会让问题变得更糟,而不是更好,因为模型会以更高的权威感表达过时的信息。随着模型在表达上变得越来越流利和自信,它们那些自信且错误的陈旧答案对用户来说变得更难察觉,而不是更简单。

生产级 AI 系统中的提示词版本控制与变更管理

· 阅读需 11 分钟
Tian Pan
Software Engineer

一个团队在客服提示词中增加了三个词,为了让它“更具对话感”。几小时内,结构化输出错误率激增,一条创收流水线停滞。工程师们花了将近一整天的时间调试基础设施和代码,才有人想到去检查提示词。没有版本历史。没有回滚机制。这三个词的修改是由一位产品经理直接在配置文件中内联完成的,他完全没理由认为这会有风险。

这是一个典型的生产环境提示词事故。类似的戏码在各种规模的公司中上演,其根源几乎总是一样的:提示词被视作临时配置,而不是软件。

LLM 应用的测试驱动开发:类比成立与失效之处

· 阅读需 12 分钟
Tian Pan
Software Engineer

一个团队使用 Claude 构建了一个 AI 研究助手。他们对 Prompt 进行了三周的迭代,向利益相关者演示了该助手,并满怀信心肠发布了它。两个月后,他们发现该助手在大约 30% 的输出中悄悄地产生虚假引用(幻觉)—— 这种失败模式之前没有人测试过,因为评估套件是在 Prompt 在演示中“感觉对了”之后才建立的。

这种模式是常态,而非例外。LLM 开发行业在很大程度上采用了测试驱动开发(TDD)的词汇 —— 评估(Evals)、回归套件、黄金数据集、LLM-as-judge —— 却忽略了 TDD 建立的最重要规则:在实现之前编写测试,而不是在实现之后。

以下是如何正确执行此操作的方法,以及 TDD 类比在哪些地方失效得非常严重,以至于字面上照搬它会让你的系统变得更糟。

生产环境中的 LLM API 韧性:速率限制、故障转移以及简单重试逻辑的隐藏成本

· 阅读需 13 分钟
Tian Pan
Software Engineer

2025 年中,一个构建多智能体(multi-agent)财务助手的团队发现其 API 开支从每周 127 美元飙升至 4.7 万美元。一个智能体循环——智能体 A 向智能体 B 寻求澄清,智能体 B 反过来询问智能体 A,以此类推——已经递归运行了 11 天。没有熔断机制(circuit breaker)拦截它,也没有及时触发预算报警。重试逻辑尽职地在每次超时后不断重试,使每一环节的失控成本不断叠加。

这不是一个关于模型质量的故事。这是一个关于分布式系统工程的故事——特别是关于大多数 LLM 应用开发者跳过的那部分,因为他们假设供应商会处理好这些。

事实上,他们并不会。

LLM 延迟分解:为什么 TTFT 和吞吐量是两个不同的问题

· 阅读需 13 分钟
Tian Pan
Software Engineer

大多数在 LLM 上构建应用的工程师都将延迟视为一个单一的刻度盘。他们调整一些参数——批处理大小(batch size)、量化级别(quantization level)或实例类型(instance type)——观察“它是否变快了”,然后就收工了。这在上线生产环境之前一直有效,直到你发现 p50 TTFT 看起来不错,而 p99 却超过了 3 秒,或者发现让吞吐量翻倍的优化不知为何却让单个用户感觉系统变慢了。

TTFT 和吞吐量(throughput)并不是同一个滑块的两端。它们是由根本不同的物理特性引起的,受不同瓶颈的影响,并由不同的技术修复。将它们视为可互换的是我在生产环境中看到的大多数 LLM 推理事故的根本原因。