AI 应用如何设计可靠的流式生成架构
调用大模型接口通常并不复杂:提交一段输入,等待模型返回结果,再把结果展示给用户。真正困难的是,当这次调用进入真实业务系统后,我们如何把一个具有不确定性的模型,封装成可观察、可取消、可恢复、可替换的工程流程。
模型可能输出错误格式,生成过程可能持续几十秒,用户可能中途关闭页面,上游服务可能突然断流,不同供应商的接口协议也各不相同。本文从通用工程视角,讨论一套适用于内容生成、报告撰写、代码辅助等场景的流式 AI 应用架构。
1. 先划清边界:模型能力不等于业务系统
在架构设计中,首先要区分“模型能力”和“应用编排”。模型擅长生成、总结、改写和推理;应用系统则需要负责读取上下文、构造请求、管理任务状态、校验输出、保存结果以及处理异常。
一个较清晰的依赖方向可以表示为:
客户端
↓
接口层
↓
业务编排层
├─ 上下文构建
├─ 任务状态管理
├─ 输出校验
└─ 结果持久化
↓
模型适配层
├─ Provider A
├─ Provider B
└─ Mock Provider
这里最重要的一条原则是:业务代码不直接依赖某一家模型供应商的协议。否则,一次接口字段调整就可能影响路由、业务逻辑、前端事件处理和数据存储。
2. 用适配器统一不同模型的行为
不同模型服务的鉴权方式、请求字段、流式格式、错误结构和会话标识往往都不相同。适配器层的职责,就是把这些差异收敛为统一事件。
async function* run(options) {
yield { type: 'status', stage: 'thinking' };
yield { type: 'delta', text: '正在生成的内容' };
yield { type: 'done', content: '最终结果' };
}
上层只需要理解少数几类事件:
status:任务阶段发生变化;delta:收到增量内容;data:收到结构化业务结果;done:任务正常完成;error:任务失败。
这样,无论底层连接的是通用模型接口、企业内部智能体平台,还是用于测试的 Mock 实现,业务编排层都不需要改变。
3. 为什么生成任务适合使用 SSE
普通 HTTP 更适合短请求;WebSocket 适合高频双向通信;而大多数 AI 生成任务的核心需求,是客户端提交一次请求,服务端持续返回进度和内容。因此,Server-Sent Events(SSE)通常是更简单的选择。
一次典型事件序列如下:
status: thinking
status: generating
delta: 第一段内容
delta: 第二段内容
done: 生成完成
如果请求需要携带较大的 JSON 正文,可以使用 fetch + ReadableStream 解析 SSE,而不必局限于只支持 GET 的 EventSource。
SSE 落地时有四个细节容易被忽略:
- 返回
Content-Type: text/event-stream; - 关闭网关和反向代理的响应缓冲;
- 定期发送心跳,避免空闲连接被中间设备关闭;
- 客户端断开后,及时取消上游模型请求。
流式接口不能只考虑正常结束,还必须覆盖客户端断开、上游报错和服务端异常三种终止路径。
4. 不要直接相信模型输出
大模型是概率系统。即使提示词明确要求返回 JSON,它仍可能添加解释文字、Markdown 围栏,或者返回缺失字段和非法枚举值。因此,模型输出与业务存储之间必须存在一层确定性的处理流程。
原始输出
↓
提取结构化内容
↓
字段规范化
↓
业务规则校验
↓
安全清洗
↓
持久化
校验层至少应检查:
- 动作类型是否在白名单中;
- 必填字段是否存在;
- 字段类型和长度是否合法;
- 枚举值是否受支持;
- HTML 是否含脚本、事件属性或危险链接;
- 结果是否满足下游转换所需的结构约束。
可自动修正的问题可以修正并记录;影响语义的问题则应该明确失败,而不是把异常数据静默写入数据库。
5. 使用中间表示隔离生成与交付
让模型直接生成最终二进制文件,通常会让预览、修改、校验和多格式输出变得困难。更稳妥的方式,是先生成受约束的中间表示,再由确定性的渲染器生成最终产物。
模型
↓
中间表示(JSON / AST / 约束 HTML)
↓
校验与修正
↓
渲染器或转换器
↓
Web 页面 / PDF / Office 文档
中间表示带来的价值包括:前端可以即时预览,后端可以进行结构校验,同一份内容可以输出多种格式,而且将来替换模型时不必重写交付链路。
简而言之,模型负责生成语义和结构,渲染器负责生成最终格式。
6. 长任务需要取消、幂等与版本机制
AI 生成任务往往持续较长时间。用户可能重复点击、切换页面或关闭浏览器。如果客户端已经离开,而后端仍继续消耗模型额度和连接资源,就会造成隐蔽的浪费。
推荐把取消信号沿调用链向下传递:
客户端断开
↓
接口层检测连接关闭
↓
AbortController 发出取消信号
↓
模型适配器终止上游请求
↓
任务状态更新为 cancelled
此外,每个生成任务都应有明确状态机:
pending → running → succeeded
├→ failed
└→ cancelled
涉及批量修改时,还应考虑幂等键和版本快照。同一批请求重复执行时不能产生重复数据;生成结果覆盖原内容前,应允许用户恢复到操作前状态。
7. 故障降级必须是显式能力
在开发和演示环境中,系统可能需要在数据库不可用时切换到内存存储。合理的做法是让业务层依赖统一的数据访问门面,由持久化实现和内存实现遵守同一套接口。
业务服务
↓
数据访问门面
├─ 持久化数据库实现
└─ 内存实现
但“可以降级”不等于“静默降级”。系统应该明确展示当前存储状态,并记录降级原因。生产环境还应根据数据重要性决定是否禁止自动降级,避免用户误以为数据已经可靠保存。
8. 安全边界不能交给模型决定
AI 应用除了常规鉴权,还需要特别关注模型生成内容带来的风险:
- 模型密钥只保存在服务端,不能下发到浏览器;
- 模型生成的 HTML 必须经过白名单清洗;
- 外部图片和文件地址需要防范 SSRF;
- 请求体大小、生成时长和并发数都要有限制;
- 项目、会话和生成结果必须校验用户归属;
- 日志可以记录“是否携带凭证”,但不能记录凭证本身。
提示词约束只能改善输出倾向,不能替代服务端安全校验。
9. 如何测试这类系统
可靠性测试不应只覆盖“模型成功返回”这一条路径。可以从四个层次建立测试:
- 单元测试:结构化输出解析、SSE 分帧、字段规范化和内容清洗;
- 适配器测试:正常流式响应、空响应、非法事件、上游错误和中途断流;
- 业务测试:状态流转、重复请求、失败恢复、版本回退和存储降级;
- 端到端测试:发起任务、消费完整事件流、校验结果落库,再重新读取确认。
Mock 适配器在这里非常有价值。它提供稳定、可重复的输出,使测试不依赖外部模型的费用、网络和随机性。
10. 总结
一个可靠的 AI 应用,并不是简单地在业务系统中加入一次模型调用,而是由多个确定性环节包裹一个不确定性环节。
模型负责生成能力,编排层负责业务流程,适配器负责隔离供应商差异,校验层负责结果可靠性,持久化层负责状态恢复,流式接口则负责把过程及时反馈给用户。
当这些边界清晰之后,模型可以替换,协议可以调整,存储实现可以切换,下游输出也可以扩展,而整个系统不必因为某一处变化而重新设计。这才是 AI 能力从“接口演示”走向“工程系统”的关键一步。

浙公网安备 33010602011771号