技术博客
易源合一流式接入点 3054-3:SSE 分块解析与选型取舍

易源合一流式接入点 3054-3:SSE 分块解析与选型取舍

作者: 万维易源
2026-09-15
易源合一流式SSE分块解析透传模式
# 易源合一流式接入点 3054-3:SSE 分块解析与选型取舍 > 接入点:3054-3 智能对话(流式) | 请求方式:POST | 返回格式:SSE 文本流(透传,无 showapi_res_body 封装) | 计费:5.5 厘/次,失败不扣费 | 最后实测核对:2026-09-15 ## 核心要点 - 3054-3 是**透传模式**,直接返回服务商原始数据流,没有 `showapi_res_body` 封装,也没有 `showapi_fee_num` 计费字段。 - 响应格式是 `id` / `event` / `data` 三个字段,块与块之间用空行分隔,`data` 里是一段 JSON 字符串,要再解一次。 - 实测天气、笑话、新闻三类意图都只收到**一个 `finish` 块**,没有观察到文档里描述的 `add` 增量分片。要不要用它,先按这个事实估。 ## 先看清它和 3054-1 的差别 | 项目 | 3054-1 智能对话 | 3054-3 智能对话(流式) | |------|----------------|---------------------| | 返回形态 | 一次性完整 JSON | SSE 文本流,`\n\n` 分块 | | 外层封装 | 有 `showapi_res_body` | 无,透传 | | 计费字段 | 返回 `showapi_fee_num` | 不返回 | | 请求参数 | `text` + 可选 `args_list` | 相同 | | 鉴权 | query 里的 `appKey` | 相同 | 请求体完全一样,差别只在响应怎么给你。所以选哪个不是「功能多少」的问题,是「你的前端要不要边收边渲染」的问题。 ## 响应格式逐字段拆 一次真实响应(2026-09-15,「帮我查找一下昆明今天的天气」): ``` id: 94fc2278-f037-4dc9-aa5a-1e754bb22a6a event: finish data: {"reply_msg":{"img_list":[],"audio_list":[],"text":"#### 昆明 26年09月15日的天气\n\n预计白天气温:24℃,多云,南风0-3级,夜间气温:17℃,多云,东风0-3级。\n\n","video_list":[]},"ret_code":0,"result":[...],"intent":{"name":"weather","args":{"area":"昆明","date":"2026-09-15"}}} ``` 三个字段的用途: | 字段 | 类型 | 说明 | |------|------|------| | `id` | String | 当前流式 id,本次实测为 UUID 格式,同一响应内多次出现时值相同 | | `event` | String | 事务类型:`add` 增量、`error` 异常、`finish` 结束、`interrupted` 中断 | | `data` | String | 内容是**一段 JSON 字符串**,结构等同于 3054-1 的 `showapi_res_body` | 注意 `data` 字段本身是字符串,不是嵌套对象。要拿到 `reply_msg.text`,得对它再做一次 `json.loads`。 `data` 里的结构与 3054-1 的 `showapi_res_body` 一致,包括 `reply_msg`、`ret_code`、`result`、`intent` 四块。这意味着两个接入点的解析代码可以复用,只在最外层解包方式上分叉。 ## 实测观察:只收到一个 finish 块 这一点在使用前必须知道。 用三类意图各调一次 3054-3,实测结果: | 输入 | `event` 序列 | 耗时 | |------|-------------|------| | 帮我查找一下昆明今天的天气 | 仅 1 个 `finish` | 1.58 秒 | | 讲个笑话 | 仅 1 个 `finish` | 1.57 秒 | | 今天有什么新闻 | 仅 1 个 `finish` | 2.00 秒 | 三次调用都没有出现 `add` 分片,数据是一次性给完的。 这说明**在这三类意图上,流式没有带来「边收边渲染」的效果**。它和 3054-1 的实际差别,只剩「响应格式不同」和「少了计费字段」。 如果你打算用 3054-3 提升首字延迟,建议先用你自己的目标意图做一轮验证:记录第一个非 `finish` 事件的到达时间,和 3054-1 的整体返回时间比一比。我这次的样本不足以支撑「流式一定更快」或「流式一定没用」的结论。 ## 解析代码 要兼容 `add` / `error` / `finish` / `interrupted` 四种事件,别只处理 `finish`。 ```python import json, requests APP_KEY = "YOUR_APPKEY" def ask_stream(text, timeout=60): """流式接入点:逐块解析,按事件类型分派。""" with requests.post( f"https://route.showapi.com/3054-3?appKey={APP_KEY}", data={"text": text}, stream=True, # 关键:不要一次性读完整响应 timeout=timeout, # 文档标注读超时 60s ) as resp: resp.raise_for_status() buffer = "" chunks = [] for raw in resp.iter_content(chunk_size=None, decode_unicode=True): if not raw: continue buffer += raw # 块与块之间用空行分隔 while "\n\n" in buffer: block, buffer = buffer.split("\n\n", 1) event = None payload = None for line in block.splitlines(): if line.startswith("id:"): pass elif line.startswith("event:"): event = line[6:].strip() elif line.startswith("data:"): payload = line[5:].strip() if event == "add": # 增量分片:data 本身是 JSON 字符串,可能是不完整的 try: chunks.append(json.loads(payload)) except json.JSONDecodeError: print("增量块暂不完整,跳过:", payload[:60]) elif event == "finish": body = json.loads(payload) return {"ok": body.get("ret_code") == 0, "text": body.get("reply_msg", {}).get("text", ""), "intent": body.get("intent", {}).get("name"), "result": body.get("result"), "chunks": chunks} elif event in ("error", "interrupted"): return {"ok": False, "event": event, "raw": payload} return {"ok": False, "reason": "流结束但未收到 finish"} print(ask_stream("帮我查找一下昆明今天的天气")) ``` ```javascript const APP_KEY = "YOUR_APPKEY"; async function askStream(text) { const res = await fetch(`https://route.showapi.com/3054-3?appKey=${APP_KEY}`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ text }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const reader = res.body.getReader(); const decoder = new TextDecoder("utf-8"); let buffer = ""; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); let idx; while ((idx = buffer.indexOf("\n\n")) !== -1) { const block = buffer.slice(0, idx); buffer = buffer.slice(idx + 2); const event = (block.match(/^event:\s*(.+)$/m) || [])[1]; const dataLine = (block.match(/^data:\s*([\s\S]*)$/m) || [])[1]; if (event === "finish") { const body = JSON.parse(dataLine); // data 是 JSON 字符串,需要再解一层 return { ok: body.ret_code === 0, text: body.reply_msg?.text, intent: body.intent?.name }; } if (event === "error" || event === "interrupted") { return { ok: false, event, data: dataLine }; } } } return { ok: false, reason: "未收到 finish 块" }; } ``` 两版代码里最容易写错的一处是 `data` 的二次解析。`data:` 后面那一长串是 JSON 文本,不是已经解析好的对象。 ## 什么时候值得用它 **值得用的场景只有一个前提:你需要把响应改成流式通道。** 比如你的后端到前端已经是 SSE 或 WebSocket,中间不想再转换成一次性 JSON;或者你要接语音合成,希望在文本产出过程中就开始合成。这种情况下 3054-3 的响应格式正好对上,省一层转换。 **不值得用的场景更常见:** - 你要靠 `showapi_fee_num` 记每次调用的费用。3054-3 不返回这个字段,得改用账单侧对账。 - 你要在返回里做统一的外层错误判断。3054-3 没有 `showapi_res_body`,错误处理路径和 3054-1 不一样。 - 你的目标意图实测也是「只返回一个 finish 块」。那就没有增量可渲染,不如直接用 3054-1,格式更规整。 ## FAQ **Q1:3054-3 为什么不返回 `showapi_fee_num`?** 因为它是透传模式,直接返回服务商原始数据,不再套 showapi 的统一响应封装。文档的返回参数说明里已注明「该接口为透传模式,直接返回服务商原始数据,无 showapi_res_body 封装」。 **Q2:`add` 事件什么时候会出现?** 文档描述 `add` 是增量事件。本次实测的天气、笑话、新闻三类意图都没有收到 `add`,只收到 `finish`。哪些意图会产生增量分片,官方文档没有列举,需要你在自己的目标场景里实测确认。 **Q3:`data` 字段为什么还要再解析一次?** 因为它是字符串类型的 JSON 文本。直接取 `body.data.reply_msg` 会报错,正确写法是对这个字符串做一次 JSON 解析。 **Q4:流式请求要设多久超时?** OpenAPI 文档给 3054-3 标注的读超时和连接超时都是 60 秒。客户端超时不要低于这个值,否则长响应会被自己切断。 **Q5:三个接入点的计费一样吗?** 一样。产品价格页显示三个接入点的单次费用同为 5.5 厘,失败不扣费。 ## 下一步阅读 - [易源合一返回三层结构:reply_msg / intent / result 逐层拆解](https://www.showapi.com/guides/united-api-response-structure-3054) - [易源合一在对话式应用里的位置:意图层与执行层的切分方案](https://www.showapi.com/guides/united-api-dialog-architecture-3054) - [易源合一调用失败扣不扣费:ret_code 与 showapi_fee_num 实测对照表](https://www.showapi.com/guides/united-api-retcode-fee-3054) - **本系列共 12 篇**:查看[易源合一指南总目录](https://www.showapi.com/guides/united-api-guides-3054)