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. 如何测试这类系统

可靠性测试不应只覆盖“模型成功返回”这一条路径。可以从四个层次建立测试:

  1. 单元测试:结构化输出解析、SSE 分帧、字段规范化和内容清洗;
  2. 适配器测试:正常流式响应、空响应、非法事件、上游错误和中途断流;
  3. 业务测试:状态流转、重复请求、失败恢复、版本回退和存储降级;
  4. 端到端测试:发起任务、消费完整事件流、校验结果落库,再重新读取确认。

Mock 适配器在这里非常有价值。它提供稳定、可重复的输出,使测试不依赖外部模型的费用、网络和随机性。

10. 总结

一个可靠的 AI 应用,并不是简单地在业务系统中加入一次模型调用,而是由多个确定性环节包裹一个不确定性环节。

模型负责生成能力,编排层负责业务流程,适配器负责隔离供应商差异,校验层负责结果可靠性,持久化层负责状态恢复,流式接口则负责把过程及时反馈给用户。

当这些边界清晰之后,模型可以替换,协议可以调整,存储实现可以切换,下游输出也可以扩展,而整个系统不必因为某一处变化而重新设计。这才是 AI 能力从“接口演示”走向“工程系统”的关键一步。

posted @ 2026-09-27 20:45  fitch_liu  阅读(6)  评论(0)    收藏  举报