From 0968f9418f5fca0420fd69915137c61c9755f197 Mon Sep 17 00:00:00 2001 From: Peanut Date: Fri, 22 May 2026 22:51:17 +0800 Subject: [PATCH] docs: tighten AI stream lifecycle requirements --- .../2026-05-22-ai-scene-routing-design.md | 72 ++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/specs/2026-05-22-ai-scene-routing-design.md b/docs/superpowers/specs/2026-05-22-ai-scene-routing-design.md index dd17592..31a196e 100644 --- a/docs/superpowers/specs/2026-05-22-ai-scene-routing-design.md +++ b/docs/superpowers/specs/2026-05-22-ai-scene-routing-design.md @@ -173,6 +173,9 @@ INDEX(environment, is_enabled) | `timeout_ms` | 接口级超时时间 | | `retry_count` | 接口级重试次数 | | `retry_delay_ms` | 接口级重试延迟 | +| `first_token_timeout_ms` | 首个文本片段超时时间 | +| `stream_idle_timeout_ms` | 流式空闲超时时间 | +| `max_output_chars` | 单次输出最大字符数 | | `last_test_status` | 最近一次测试状态 | | `last_test_time` | 最近一次测试时间 | | `last_test_trace_id` | 最近一次测试链路 ID | @@ -263,6 +266,7 @@ INDEX(scene_category, is_enabled) | `stream_chunk_count` | 流式片段数量 | | `first_chunk_duration_ms` | 首个片段返回耗时 | | `completed_at` | 流式完成时间 | +| `output_preview` | 输出预览,保存前后截断后的文本 | | `duration_ms` | 耗时 | | `status` | `success`、`failed` | | `error_code` | 错误码 | @@ -278,7 +282,7 @@ INDEX(endpoint_config_id, create_time) INDEX(status, create_time) ``` -日志中 `request_headers` 必须脱敏 `Authorization`、`api-key`、`x-api-key`、`client_secret` 等敏感字段。 +日志中 `request_headers` 必须脱敏 `Authorization`、`api-key`、`x-api-key`、`client_secret` 等敏感字段。流式响应内容可能很长,`response_body` 和 `parsed_result` 不保存无限完整文本;默认保存元数据、错误上下文、`output_preview`、`stream_chunk_count` 和首包耗时。业务表需要完整结果时,由具体业务在 `done` 后保存最终文本。 ## 初始场景编码 @@ -378,7 +382,7 @@ Coze 默认请求模板: } ``` -Coze 响应解析先沿用现有 `AiChatServiceImpl` 中的 SSE 解析逻辑,但运行时必须边解析边输出 `AiStreamEvent.delta`。如果 Coze 只在 End 节点返回 `content.output`,适配器至少要在收到 End 前持续转发可用文本片段或阶段事件,不能让用户端一直空白等待;迁移完成后该逻辑归属 `CozeProviderAdapter`。 +Coze 响应解析先沿用现有 `AiChatServiceImpl` 中的 SSE 解析逻辑,但运行时必须边解析边输出 `AiStreamEvent.delta`。如果某个 Coze workflow 只能在 End 节点一次性返回 `content.output`,该 workflow 不能作为用户场景 endpoint 启用,需要先调整 Coze 工作流或更换 provider,使其能产生可展示文本片段。`message` 阶段事件可以辅助展示进度,但不能替代文本 `delta`;迁移完成后该逻辑归属 `CozeProviderAdapter`。 ## 请求模板与响应解析 @@ -470,6 +474,14 @@ Coze 响应解析先沿用现有 `AiChatServiceImpl` 中的 SSE 解析逻辑, 后端可以在内部累积完整文本,但不能因为落库、解析或审核而阻塞 `delta` 下发。业务落库发生在 `done` 之后;如果中途失败,保存已输出片段和失败原因,便于排查。 +流式完成判定: + +1. 成功调用必须至少发送一个 `start`、一个非空文本 `delta`、一个 `done`。 +2. 如果服务商正常结束但没有任何非空 `delta`,调用按失败处理,错误码为 `AI_STREAM_NO_DELTA`。 +3. 如果首个 `delta` 超过 endpoint 的首包超时阈值,发送 `error` 并记录 `AI_STREAM_TIMEOUT`。 +4. 如果服务商中途断流,发送 `error`,保留已输出文本,记录 `AI_STREAM_INTERRUPTED`。 +5. 如果用户端主动断开,后端取消上游请求或停止读取,并记录 `AI_STREAM_CLIENT_DISCONNECTED`。 + 统一错误码: | 错误码 | 场景 | @@ -485,6 +497,11 @@ Coze 响应解析先沿用现有 `AiChatServiceImpl` 中的 SSE 解析逻辑, | `AI_TEMPLATE_INVALID_JSON` | 请求模板渲染后不是合法 JSON | | `AI_PROVIDER_REQUEST_FAILED` | 外部服务调用失败 | | `AI_RESPONSE_PARSE_ERROR` | 响应解析失败 | +| `AI_STREAM_NO_DELTA` | 服务商连接成功但没有产生可展示文本片段 | +| `AI_STREAM_TIMEOUT` | 流式首包或整体输出超时 | +| `AI_STREAM_CLIENT_DISCONNECTED` | 用户端连接中断 | +| `AI_STREAM_INTERRUPTED` | 服务商流式输出中断 | +| `AI_STREAM_OUTPUT_LIMIT` | 输出超过 endpoint 配置的最大字符数 | | `AI_FALLBACK_FAILED` | 主配置和兜底配置都失败 | 业务层收到错误时,只处理统一错误码和用户可见提示,不直接解析 Dify 或 Coze 的原始错误结构。 @@ -553,6 +570,14 @@ POST /ai/scenes/test - 错误事件。 - traceId 和调用日志入口。 +后台测试通过条件: + +1. 必须收到 `start`。 +2. 必须在 `first_token_timeout_ms` 内收到至少一个非空 `delta`。 +3. 必须收到 `done`,或在失败时收到 `error` 并能展示统一错误码。 +4. `chunkCount` 必须大于 0。 +5. 调用日志必须能通过 traceId 查到 provider、endpoint、scene、耗时、chunk 数量和错误上下文。 + 所有返回 AI 生成内容的业务接口都必须改造为流式接口,或通过现有 WebSocket 通道输出统一 `AiStreamEvent`。旧的一次性响应接口在迁移期只能作为内部兼容入口,不能继续作为用户端调用入口;如果某个页面仍调用一次性 AI 接口,该页面不算完成迁移。 ## 后台页面 @@ -680,6 +705,22 @@ POST /ai/scenes/test 任何新 AI 功能在上线前必须提供用户端流式展示验证;如果只能一次性显示完整文本,则不能进入验收。 +### 用户端场景迁移清单 + +每个 AI 场景迁移时都要同时完成后端路由、业务接口和用户端渲染,不能只改后台配置: + +| sceneCode | 用户端入口 | 输出要求 | +| --- | --- | --- | +| `chat` | 对话页 / WebSocket 对话 | 用户发送后立即进入生成中,逐段追加回复 | +| `script_generate` | 剧本生成页 | 章节、片段或正文逐段显示,最终文本在 `done` 后保存 | +| `short_story_generate` | 短篇小说生成页 | 正文逐段显示,失败时保留已输出内容 | +| `diary_summary` | 日记总结 / AI 评论入口 | 总结文本逐段显示,不等待完整总结 | +| `emotion_summary` | 情绪总结入口 | 总结文本逐段显示 | +| `emotion_analysis` | 情绪分析入口 | 分析文本逐段显示,结构化结果在 `done` 后落库 | +| `life_healing` | 人生事件疗愈入口 | 回复文本逐段显示 | + +迁移完成的判定是用户端页面实际消费 `AiStreamEvent`,并能在弱网或长文本生成时持续更新。如果只是在后端拿到流式内容再拼成完整文本返回给页面,不算完成迁移。 + ## 运行时行为 ### 立即生效 @@ -711,6 +752,17 @@ scene_code + environment 运行环境来源固定为后端当前环境配置,例如 Spring Profile 或项目已有环境变量。业务代码不从前端请求参数读取 environment,避免用户侧伪造环境导致误路由。 +### 流式连接控制 + +流式调用需要显式处理连接生命周期: + +1. 后端为每次调用创建 `traceId`,并在 `start` 事件中返回。 +2. 后端向 Dify/Coze 发起请求后,不能在当前线程同步等待完整响应;必须边读取边转发。 +3. 用户端断开 SSE/WebSocket 时,后端应停止继续向用户端写入,并尽量取消上游请求。 +4. 超过 `stream_idle_timeout_ms` 没有任何新事件时,后端发送 `error` 并结束连接。 +5. 超过 `max_output_chars` 时,后端发送 `error` 并记录 `AI_STREAM_OUTPUT_LIMIT`,避免无限输出拖垮页面和日志。 +6. 同一用户同时发起多个 AI 场景请求时,前端按 `traceId` 区分输出容器,不能串流到错误页面区域。 + ### 配置生效与切换保护 立即生效只针对已启用配置。新增或编辑 provider、endpoint、scene 时,保存动作只落库;是否进入运行时由 `is_enabled` 控制。 @@ -809,6 +861,14 @@ Dify 初始化: 8. 将对话、日记总结、情绪分析、疗愈等旧 Coze 场景逐步迁到 `sceneCode`。 9. 保留旧 `AiConfig` 页面或提供只读迁移视图,确认稳定后再下线。 +每迁移一个场景,都必须同时完成以下交付: + +1. 后台存在该 `sceneCode` 的启用绑定。 +2. endpoint 支持流式并通过后台流式测试。 +3. 业务接口改为 `invokeStream` 或 WebSocket 流式转发。 +4. 用户端页面消费 `AiStreamEvent` 并逐段渲染。 +5. 调用日志能查到该场景的 traceId、chunk 数、首包耗时和最终状态。 + 每一步交付都应保持系统可启动、已有 Coze 对话不回归。 ## 测试计划 @@ -821,6 +881,7 @@ Dify 初始化: - `CozeProviderAdapterTest`:验证 Coze 请求体生成和 SSE 解析。 - `AiRuntimeStreamTest`:验证 `invokeStream` 会按 `start`、`delta`、`done` 顺序输出事件,且不会等待完整响应才返回。 - `AiStreamGatewayTest`:验证 SSE/WebSocket 转发格式、错误事件、断连处理和 traceId 透传。 +- `AiStreamLifecycleTest`:验证首包超时、空闲超时、用户端断连、无 delta、输出过长等异常都会产生统一错误码和调用日志。 - `AiTemplateRendererTest`:验证占位符替换、对象注入、缺失变量报错和 JSON 校验。 - 迁移脚本测试:验证旧 `t_ai_config` 能拆成 provider、endpoint、scene。 @@ -832,6 +893,7 @@ Dify 初始化: - 禁用配置后不能被正常调用。 - 测试请求不在浏览器暴露明文 Token。 - 用户端和后台测试页都能逐段显示 AI 输出,不出现等待接口完成后一次性渲染的行为。 +- 用户端断网、返回、重复发起请求时,不会把不同 traceId 的输出串到同一个展示区域。 集成验收: @@ -841,6 +903,7 @@ Dify 初始化: - 将剧本生成从 Dify 切回 Coze 后,不发版,下一次调用立即生效。 - 主配置失败且配置了兜底时,调用日志记录 fallback。 - 用户端对话、剧本生成、短篇小说生成、总结分析等 AI 场景都能看到流式文本逐段输出。 +- 任一场景出现服务商无文本片段、超时或断流时,用户端能看到错误状态,后台能查到统一错误码和 traceId。 ## 风险与缓解 @@ -853,6 +916,9 @@ Dify 初始化: | Token 暴露风险 | 管理后台可编辑但日志和普通详情默认脱敏 | | 请求模板配置错误导致线上失败 | 保存时校验 JSON,测试通过后再允许启用场景绑定 | | 场景切换误操作影响生产 | 环境隔离、操作审计、场景测试和明确启用开关 | +| 服务商连接成功但没有输出文本 | 以 `AI_STREAM_NO_DELTA` 判定失败,禁止作为用户场景验收通过 | +| 长文本流式输出拖垮页面或日志 | 设置 `max_output_chars`,日志保存预览和统计,不保存无限完整流 | +| 用户端断连导致后台继续消耗服务商资源 | 检测 SSE/WebSocket 断开并取消上游请求,记录 `AI_STREAM_CLIENT_DISCONNECTED` | ## 验收标准 @@ -869,3 +935,5 @@ Dify 初始化: 11. 对话、剧本生成、短篇小说生成、总结分析、疗愈等所有 AI 用户端入口都能逐段显示输出,不能一次性等完整结果后再渲染。 12. 非流式 endpoint 不能启用到用户场景,启用时必须返回 `AI_ENDPOINT_STREAM_REQUIRED`。 13. 每个已迁移 AI 场景都必须满足“有 start、有 delta、有 done 或 error、有调用日志、有用户端可见输出”,不能出现接口成功但页面无输出的情况。 +14. 后台流式测试必须校验首包耗时、chunk 数、最终状态和 traceId 日志入口。 +15. 用户端断连、服务商断流、无 delta、首包超时等异常都必须结束 loading,并能在调用日志中定位。