结构化输出与流式响应

本节目标

安全处理 schema 约束输出与 SSE 增量事件,理解“HTTP 已成功”和“业务结果已完整”之间的差别。

结构化输出流水线

  1. 选择供应商当前支持的 schema 子集。
  2. 发请求前在本地验证 schema 本身。
  3. 处理正常完成、拒绝、安全拦截、输出截断和 API 错误。
  4. 对完成内容再次解析与 schema 校验。
  5. 执行业务语义校验与权限判断。

JSON 模式若只保证“有效 JSON”,不能替代 schema。即便 schema 合规,金额可能来自错误行、引用可能不存在、动作也可能未授权。

Tool 参数与最终结构化回答是两份合同

API family最终结构化回答Tool/function 参数关键差异
OpenAI Responsestext.formatfunction tool 的 parameters + strictResponses 与 Chat Completions 的 strict 省略行为不同
Anthropic Messages原生请求为 output_config.format;SDK messages.parse(output_format=...) 是便利层input_schema;strict tool 另设 strict: trueSDK helper 可能转换原 schema,需同时记录原始与实际发送版本
Gemini Interactions v1response_format,JSON 为 type=text、mime_type=application/json + schemafunction 的 parameters没有 OpenAI function tool 的 strict 字段

截至 2026-07-21,OpenAI Responses function tool 若省略 strict,服务端会先尝试把 schema 规范化为 strict;无法规范化时才回退 non-strict,并在响应 tool 上显示 strict:false。Chat Completions 仍是省略即 non-strict。生产合同不要依赖这段隐式分支:显式设置 strict,若为 true 则按当前要求令对象 additionalProperties:false 且所有 properties 都列入 required,再做本地 schema 和语义校验。

Anthropic structured output 的 SDK helper 可以把部分 SDK 类型/schema 特性转换成 API 接受的形状,因此审计时不能只保存调用方原 schema;还要记录 adapter/SDK 版本与实际发送 schema。三家在 refusal、max_tokens/截断、安全拦截或 API error 时都可能没有满足最终 schema 的业务答案,解析器必须先看终止状态,再看结构。

流式响应是什么

许多 LLM API 使用 Server-Sent Events(SSE)发送带类型的事件。客户端不是不断拼接任意 HTTP 字符,而是先由 SSE decoder 处理 UTF-8、行与 event/data/id framing,再按供应商协议处理开始、内容增量、工具参数增量、完成和错误事件。事件类型可能扩展,应记录未知类型并按兼容策略处理。

一个稳健状态机至少有:

INIT -> STARTED -> STREAMING -> COMPLETED
                         \-> FAILED

只有收到合法终止事件并完成校验才是成功。连接断开、超时或流中 error 不能把已显示的半段文本当完整答案。工具参数 JSON 也常被拆成多个增量;必须等对应内容块结束后再解析,不能对半截 JSON 执行工具。

本库项目使用 response.started、response.text.delta、response.finished 和 response.failed 作为 canonical 内部事件。它们不是 OpenAI、Anthropic 或 Google 的原始事件名。真实 adapter 必须按所用 API 版本把原始事件映射进来;遇到新的未知事件时应显式记录和失败/兼容处理,不能猜测其语义。

不要把三种测试层混成一种:raw SSE fixture 验证 framing,SDK-object fixture 验证固定 SDK 解码,application projection 验证 adapter 与业务状态机。本库 OpenAI/Gemini Provider fixture 是手工 typed-sse-projection,Anthropic fixture 是 wire-sse-envelope-projection;它们都不是抓取的真实网络字节。

有界组装与明确终止

流消费者必须限制事件数、单个 delta、累计文本/参数、JSON 深度和集合大小。否则一个永不终止或持续发送小片段的流会让内存无限增长。限制触发属于可审计的协议失败:保存 request/operation ID、已消费数量和受控 partial 元数据,关闭 stream,不把它伪装成成功,也不直接执行任何 provisional tool call。

终止权威必须按 API family 单独定义。HTTP 200、首事件、内容块结束,甚至“某个函数参数已拼完”都不等于整轮完成;只有 Provider 合法 terminal 与本地校验共同通过,调用才可进入宿主 execution ledger。消费者取消或 deadline 到期时还要显式关闭 stream context,停止读取并不保证 socket 或计费已停止。

流中断后的重试通常会开始一次新的生成,文本可能不同。不要把“上一次半段 + 新请求半段”直接拼接成一个答案。若供应商提供明确的恢复标识和协议,按当前文档实现;否则丢弃或标记旧 partial,并把外部工具副作用放在完整终止与校验之后。

UI 与安全

流式能降低首字节等待感,却增加内容审核、撤回、顺序和断线恢复难度。高风险输出可先在服务端缓冲并审核;若实时显示,UI 要明确“生成中/未完成”,错误时保留状态而不是伪装成功。

练习与自测

为事件 start, delta("hel"), delta("lo"), completed 写组装状态机,再测试 start, delta, error 和缺少 completed。自测:HTTP 状态为 200 后收到流内错误时,为什么只检查状态码会误判?

掌握检查

  • JSON 语法、schema、业务语义和动作授权依次校验,拒绝与截断是独立状态。
  • 我按 typed event 和状态机处理流,不对原始字节或半截工具参数执行操作。
  • HTTP 200、首个 delta 和 UI 已显示文字都不代表业务完成。
  • 流中断的 partial 被标记失败;重新生成时不会与旧 partial 静默拼接。
  • canonical 事件与供应商原始事件分开,未知事件不会默认当作可忽略成功。
  • raw SSE、SDK typed object 和 application projection 各有测试,不把 fixture parity 当 live conformance。
  • 事件、单 delta、累计 buffer 与 JSON 资源都有上限;越界会关闭 stream 并留下失败证据。

下一步

进入 超时、错误、限流与重试。完成可靠客户端内核后,再用 三家 Provider 合同测试对照三家当前 Reference/SDK 事件投影、身份关联和终态门禁;不要把本章 canonical 事件或离线 projection 当成供应商 wire 协议。

参考资料