SSE 流式响应多行文本编码方案

目录

一、概述

AI 聊天流式接口(SSE)推送的 thinking/reply 等文本事件常包含换行符(\n)。SSE 协议以 \n 作为帧分隔符,data: 字段中若出现裸换行,多行内容会被拆帧;非标准解析器(手写 fetch 按行解析、部分按行转发的代理)会把不以 data: 开头的行当作未知字段丢弃,导致返回信息丢失

二、候选方案

方案一:JSON 编码 data 字段

发送侧将文本/对象统一 JSON 序列化(如 JsonUtils.toJson),\n 被转义为字面量 \\n,帧层面永不出现裸换行;客户端统一 JSON.parse 还原。

text 复制代码
data: {"delta":"第一行\n第二行"}

后端(WebFlux):

java 复制代码
} else if (agentEvent instanceof TextBlockDeltaEvent delta) {
    // 文本增量统一 JSON 编码,\n 被转义为字面量,帧层面不出现裸换行
    return ServerSentEvent.builder()
            .event("reply")
            .data(JsonUtils.toJson(Map.of(
                    "replyId", delta.getReplyId(),
                    "delta", delta.getDelta())))
            .build();
}

前端(EventSource):

javascript 复制代码
source.addEventListener('reply', (e) => {
    const payload = JSON.parse(e.data);   // 统一 JSON.parse 还原
    appendToUI(payload.delta);             // "第一行\n第二行" 完整还原
});
  • ✅ 业界事实标准(OpenAI / Anthropic / Vercel AI SDK 均采用)
  • ✅ 无歧义、无碰撞风险,任意文本安全传输
  • ✅ 可顺带携带 replyIdindexfinish 等元数据,便于扩展
  • ❌ 每条事件多一层序列化/反序列化开销(对流式文本量级可忽略)

方案二:SSE 原生多 data:

利用规范允许的事件内多条 data: 行,标准客户端(EventSource)自动以 \n 拼接还原:

text 复制代码
event: reply
data: 第一行
data: 第二行

后端(Spring SseEmitter 传多行字符串时自动拆为多个 data: 行):

java 复制代码
// multiLineText = "第一行\n第二行"
sseEmitter.send(SseEmitter.event()
        .name("reply")
        .data(multiLineText));
// 实际发送帧:
// event:reply
// data:第一行
// data:第二行
//

前端(必须使用标准 EventSource,浏览器自动以 \n 拼接还原):

javascript 复制代码
source.addEventListener('reply', (e) => {
    appendToUI(e.data);   // 浏览器已还原为 "第一行\n第二行"
});
  • ✅ 协议原生,标准客户端零成本还原(Spring SseEmitter 发多行字符串时内部即此行为)
  • ❌ 仅对标准解析器有效,手写 fetch 解析或按行转发的代理仍会丢数据
  • ❌ 末尾换行语义易错(结尾 \n 会多出空行事件)
  • ❌ 空行(\n\n)语义难以可靠表达

方案三:自定义占位符转义

\n 替换为自定义占位符(如当前 AiChatController 使用的 #LR#),客户端反向还原。

后端:

java 复制代码
} else if (agentEvent instanceof TextBlockDeltaEvent delta) {
    // 换行符替换为占位符后再发送
    String escaped = delta.getDelta().replace("\n", "#LR#");
    return ServerSentEvent.builder()
            .event("reply")
            .data(escaped)
            .build();
}

前端:

javascript 复制代码
source.addEventListener('reply', (e) => {
    // 客户端需维护与服务端一致的转义表,反向还原
    appendToUI(e.data.replaceAll('#LR#', '\n'));
});
  • ✅ 实现简单,无需 JSON 依赖
  • ❌ 非标准协议,前后端须共同维护转义表
  • ❌ 占位符存在与真实内容碰撞的风险(AI 输出不可控)
  • ❌ 流式 token 粒度下,占位符可能被拆分到相邻增量中,还原逻辑更脆弱

方案四:按行拆分为多个事件

多行文本按 \n 拆行,每行发送一条独立 SSE 事件(当前 sseMultiLineSeparate 的做法)。

后端:

java 复制代码
private void sseMultiLineSeparate(SseEmitter sseEmitter, String eventName, String multiLines) throws IOException {
    if (!StringUtils.hasText(multiLines)) {
        return;
    }
    // 按换行拆分,每行一条独立事件;空行被 hasText 过滤,\n\n 语义丢失
    for (String line : multiLines.split("\n")) {
        sseEmitter.send(SseEmitter.event().name(eventName).data(line));
    }
}

前端:

javascript 复制代码
// 需自行约定拼接规则(如约定空行事件 = data 为 null)
source.addEventListener('reply', (e) => {
    appendToUI(e.data ?? '\n');
});
  • ✅ 每帧均为单行,无转义问题
  • ❌ 空行被过滤,\n\n 段落语义丢失
  • ❌ 事件数量膨胀,前端需自行拼接且无法区分行边界来源

三、方案对比总览

维度 方案一 JSON 编码 方案二 多 data: 方案三 占位符转义 方案四 按行拆事件
标准化程度 ✅ 业界事实标准 ✅ SSE 规范内 ❌ 自定义协议 ❌ 自定义协议
标准客户端(EventSource)可靠性 ✅ 高 ✅ 高 ⚠️ 需自行还原 ⚠️ 需自行拼接
非标准链路(手写 fetch 解析 / 代理转发)可靠性 ✅ 不丢数据 ❌ 会丢数据 ⚠️ 依赖转义约定 ✅ 不丢数据
空行 / 末尾换行语义 ✅ 完整保留 ❌ 易错 ✅ 完整保留 ❌ 空行丢失
内容碰撞 / 拆断风险 ✅ 无 ✅ 无 ❌ 占位符可碰撞、可被 token 拆断 ✅ 无
元数据扩展能力 ✅ 强(JSON 字段) ❌ 弱 ❌ 弱 ❌ 弱
前后端改造成本 低(序列化/反序列化一行代码) 最低 中(维护转义表) 中(拼接 + 行边界处理)
运行时开销 序列化开销,流式量级可忽略 字符串替换,低 事件数膨胀
业界使用 OpenAI / Anthropic / Vercel AI SDK 少用于业务文本 少数旧框架 / IM 推送 罕见

四、最终决策

采用方案一:SSE data 字段统一 JSON 编码,客户端 JSON.parse 解码。

理由:

  1. 与业界主流流式 AI 协议(OpenAI、Anthropic、Vercel AI SDK)对齐,前端可复用成熟解析模式;
  2. 唯一在"手写解析、代理转发"等非标准链路下也不丢数据的方案;
  3. 结构化载荷天然支持后续扩展(多事件元数据、错误码等)。

落地约定:

  • 后端:所有 SSE 事件 data 一律 JSON 字符串(文本增量经 JsonUtils.toJson 编码);
  • 前端:统一 JSON.parse 后取字段,不再做占位符替换或按行拼接;

五、备选方案(未采纳)

方案二仅适合纯浏览器原生 EventSource 且链路可控的场景;方案三、四为非标准过渡做法,维护成本与正确性风险均高于 JSON 编码,故弃用。

相关推荐
CIO_Alliance3 小时前
AI基础系列(1)| 向量、矩阵、张量在AI中分别扮演什么角色?
大数据·人工智能·线性代数·ai·矩阵·企业cio联盟·企业级ai化转型
yuhulkjv3354 小时前
Gemini鸿蒙版导出word格式的终极解法:AI 导出鸭如何重构AI内容落地链路
人工智能·ai·word·harmonyos·ai导出鸭
慧都小妮子4 小时前
C# 实现AI合同审查:从读取、风险标注到批量签发
ai·自然语言处理·c#·.net·办公自动化·ai合同审查·文档ai代理
腾视科技-AIoT6 小时前
私有云时代来临:AI NAS如何重塑你的数字生活?
人工智能·ai·生活·nas·ai算力模组·ainas·腾视科技
fthux6 小时前
装闭 RenoPit 源码解析(12):从AI分析结果到React避坑报告
人工智能·ai·开源·github·open source·renopit
安_6 小时前
langgrah使用pgsql做会话存储会自己创建表吗
数据库·ai
DS随心转APP6 小时前
生成word文档的ChatGPT格式乱码终结者:AI导出鸭横向测评与工程化架构解析 摘要
人工智能·ai·chatgpt·word·deepseek·ai导出鸭
今天你AiPy了吗7 小时前
AI桌面助手的隐私底线 数据可落本地不出域,还能一键切换云端,全能智能体
人工智能·python·ai·ai编程·智能体
spencer_tseng7 小时前
Deepseek has increased its price 2026.08.17
ai·deepseek