跨端流式差异
核对日期:2026-08-26。各端基础库对分块传输的支持会变,上线前按目标版本复核。
1. 定义与边界
跨端流式差异是指同一套 Agent 网关事件,在 H5、微信小程序、支付宝小程序、App 上不能共用一套传输实现。差异来自运行时:有无 fetch 流、有无 EventSource、是否允许自定义 header、合法域名、TLS、后台挂起。
本文件给协议矩阵和降级。事件契约仍用 流式协议与中断.md 的 AgentUiEvent。不要为每个端发明一套业务事件。
2. 为什么重要
只在 Chrome 里用 fetch + ReadableStream 调通,不代表小程序能用:
- 微信、支付宝没有浏览器
EventSource。 - 小程序请求受合法域名、TLS、内容安全策略限制。
- 部分基础库不支持分块响应,只能一次返回或轮询。
- App WebView 与原生
uni.request能力不一致,条件编译写错会静默退回非流式。
把 H5 实现直接 wx.request 包一层,是跨端 Agent 最常见的假流式。
3. 核心机制
交互层分三截:
业务 UI(卡片 / 审批 / 产物)
↑ 同一套 AgentUiEvent
传输适配器(H5 fetch 流 / 微信分块 / 轮询)
↑ 同一套 run API
Agent 网关
传输适配器可以换;卡片状态机不能换。
4. 架构模式
| 端 | 推荐传输 | 次选 | 不要用 |
|---|---|---|---|
| H5(现代浏览器) | fetch POST + 解析 SSE | WebSocket(需强打断时) | 把 token 放 query 的 EventSource |
| React Native / 常规 App WebView | 与 H5 相同,先测流 | 原生 SSE 库 | 假设 WKWebView 与 Chrome 一致 |
| 微信小程序 | wx.request({ enableChunked: true }) | 短轮询 GET /runs/:id/events?after= | EventSource、浏览器 fetch 流 |
| 支付宝小程序 | 确认基础库后的分块 / 流式 HTTP | 短轮询 | 直接复用微信 API |
| uni-app | 条件编译到上述实现 | 各端降级表 | 一套 uni.request 冒充全端流式 |
EventSource 即使在 H5 也不适合作为生产 Agent 默认:只支持 GET,鉴权与 JSON body 都别扭。
5. 工程实现
5.1 传输适配器接口
interface AgentStreamClient {
start: (input: { goal: string; sessionId: string }) => Promise<void>;
stop: () => void;
onEvent: (handler: (event: { type: string; runId?: string }) => void) => void;
}
interface ResumeCursor {
runId: string;
afterSeq: number;
}
H5、微信、轮询各自实现 AgentStreamClient,页面只依赖接口。
5.2 微信小程序分块
微信侧用 enableChunked 收二进制 / 文本块,自行按 SSE 帧切分。不要假设块边界等于事件边界。
function startWechatAgentRun(options) {
const requestTask = wx.request({
url: 'https://api.example.com/agent/runs',
method: 'POST',
enableChunked: true,
header: {
'Content-Type': 'application/json',
Authorization: options.token,
},
data: { goal: options.goal, sessionId: options.sessionId },
fail: options.onError,
});
let buffer = '';
requestTask.onChunkReceived((res) => {
const chunk = decodeUtf8(res.data);
buffer += chunk;
const frames = buffer.split('\n\n');
buffer = frames.pop() || '';
frames.forEach((frame) => options.onFrame(frame));
});
return {
stop() {
requestTask.abort();
},
};
}
约束:
- 域名必须配在 request 合法域名里,且 HTTPS。
enableChunked依赖基础库版本,低版本走轮询。- 不要在小程序里读
document/window上的流 API。 - 工具卡片和审批页用原生组件,不要用
web-view加载未隔离的模型 HTML。
5.3 支付宝小程序
支付宝与微信的请求对象、分块回调、abort 方法都不同。用 my.request 及当前基础库文档核对是否支持分块;不支持就用同一套 afterSeq 轮询。禁止 wx.* 直接套到支付宝。
uni-app 里必须条件编译:
// #ifdef MP-WEIXIN
export { startWechatAgentRun as startAgentRun };
// #endif
// #ifdef MP-ALIPAY
export { startAlipayAgentRun as startAgentRun };
// #endif
// #ifdef H5
export { startH5AgentRun as startAgentRun };
// #endif
5.4 轮询降级
async function pollEvents(cursor: ResumeCursor, signal: AbortSignal): Promise<void> {
const url = `/api/agent/runs/${cursor.runId}/events?after=${cursor.afterSeq}`;
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`poll_http_${response.status}`);
}
}
轮询间隔建议 1–2 秒,带抖动;服务端必须支持 afterSeq 幂等续传。写工具仍只执行一次,见 ../09-Agent工程化/幂等性设计.md。
5.5 App 与后台
- iOS / 安卓把 App 切后台可能切断长连接;回到前台用
run_id续事件,不要自动重发 goal。 - WKWebView 对流式与 cookie 分区与 Safari 接近,需真机测,不能只在 Chrome DevTools 过关。
- 原生 App 若自建 SSE,TLS 证书校验不要在调试包里长期关闭。
6. 生产实践
| 实践 | 说明 |
|---|---|
| 能力探测 | 启动时探测分块是否可用,失败静默切轮询 |
| 统一心跳 | 小程序也要能识别「仍在推理」否则用户会反复点击发送 |
| 合法域名清单 | 模型厂商域名、网关域名、对象存储域名分开配置 |
| 包体积 | 流解析器放公共包,避免每页复制一套 buffer 逻辑 |
| 日志 | 端类型、基础库版本、传输模式(chunked / poll)写入 trace |
| 审批 | 小程序用全屏审批页,避免半屏 confirm |
7. 常见反模式
| 反模式 | 表现 | 后果 | 修正 |
|---|---|---|---|
| 假跨端 | uni.request 等完整 body | 小程序永远转圈或一次吐出 | 分端适配器 |
| 微信代码搬支付宝 | 直接 wx.request | 运行失败 | 分端 API |
| 轮询重跑 Agent | 每次 poll 新建 run | 重复写操作 | poll 只拉事件 |
| H5 示例当小程序文档 | 教 EventSource | 无法实现 | 本页矩阵 |
| web-view 渲染对话 | 用 H5 聊天嵌进小程序 | 登录态、注入、性能 | 原生卡片 |
| 忽略后台切断 | 切后台后当失败重试 | 重复提交 | 续 run_id |
8. 评测方法
| 指标 | 端 | 说明 |
|---|---|---|
| First Event | H5 / 微信 / 支付宝 | 首包是否在 SLA 内 |
| Chunk Integrity | 微信 onChunkReceived | 跨块拼接后事件是否可解析 |
| Fallback Rate | 全端 | 回退轮询的比例与是否预期内 |
| Abort Works | 全端 | 停止后服务端 span 结束 |
| Duplicate Write | 弱网 / 后台 | 重连不增加写工具次数 |
| Domain Fail | 小程序 | 合法域名误配导致的失败分类 |
没有真机与目标基础库,不能宣称小程序流式已完成。
9. 安全与治理
- 小程序 token 放 header 或安全存储,不要拼进 query。
- 所有端共用同一套数据权限;不能因为是小程序就减少审批。
web-view打开的外部页按不可信内容处理。- 轮询接口必须鉴权且按用户隔离
run_id。 - 日志里记录端类型,便于安全事件定位,但不记完整报文。
10. 权威资料
- 微信小程序
wx.request/enableChunked: https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html (核对日期:2026-08-26) - 支付宝小程序网络请求: https://opendocs.alipay.com/mini/api/owycmh (核对日期:2026-08-26)
- MDN
ReadableStream: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream (核对日期:2026-08-26) - MDN
EventSource: https://developer.mozilla.org/en-US/docs/Web/API/EventSource (核对日期:2026-08-26) - uni-app 条件编译: https://uniapp.dcloud.net.cn/tutorial/platform.html (核对日期:2026-08-26)
- Vercel AI SDK stopping streams: https://ai-sdk.dev/docs/advanced/stopping-streams (核对日期:2026-08-26)