结构化输出与流式响应
本节目标
安全处理 schema 约束输出与 SSE 增量事件,理解“HTTP 已成功”和“业务结果已完整”之间的差别。
结构化输出流水线
- 选择供应商当前支持的 schema 子集。
- 发请求前在本地验证 schema 本身。
- 处理正常完成、拒绝、安全拦截、输出截断和 API 错误。
- 对完成内容再次解析与 schema 校验。
- 执行业务语义校验与权限判断。
JSON 模式若只保证“有效 JSON”,不能替代 schema。即便 schema 合规,金额可能来自错误行、引用可能不存在、动作也可能未授权。
Tool 参数与最终结构化回答是两份合同
| API family | 最终结构化回答 | Tool/function 参数 | 关键差异 |
|---|---|---|---|
| OpenAI Responses | text.format | function tool 的 parameters + strict | Responses 与 Chat Completions 的 strict 省略行为不同 |
| Anthropic Messages | 原生请求为 output_config.format;SDK messages.parse(output_format=...) 是便利层 | input_schema;strict tool 另设 strict: true | SDK helper 可能转换原 schema,需同时记录原始与实际发送版本 |
| Gemini Interactions v1 | response_format,JSON 为 type=text、mime_type=application/json + schema | function 的 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 协议。
参考资料
- OpenAI:Structured Outputs(访问于 2026-07-21)
- OpenAI:Function calling(Responses 与 Chat Completions 的
strict默认差异,访问于 2026-07-21) - OpenAI:Migrate to Responses—Update streaming consumers(访问于 2026-07-21)
- OpenAI:Streaming API responses(访问于 2026-07-21)
- Anthropic:Structured outputs(访问于 2026-07-21)
- Anthropic:Streaming Messages(访问于 2026-07-21)
- Anthropic:API errors(流式 200 后仍可能出错,访问于 2026-07-21)
- Google:Structured output(访问于 2026-07-21)
- Google:Interactions API v1 reference(访问于 2026-07-21)
- Google:Interactions streaming(访问于 2026-07-21)
- WHATWG:Server-sent events