Files
zyt/artifacts/desktop-ai-stream-backend-review.md
T
2026-10-17 16:08:24 +08:00

3.7 KiB
Raw Blame History

桌面端 AI 助手:服务端流式链路审查

日期:2026-10-17

结论

  • 正常链路已经逐帧转发:DifyChatService::sendStreamRequest() 在 CURLOPT_WRITEFUNCTION 内解析 SSE 并立即调用 onDelta;DiagnosisController::runAssistantSse() 对每个 delta echo 后执行 ob_flush() / flush(),没有等模型全文完成才发送。
  • 控制器清理 PHP 输出缓冲、关闭输出压缩、设置 X-Accel-Buffering: no 和 Cache-Control: no-cache, no-transform,并先发送 2048 字节注释和 start 事件。
  • 首字之前仍有真实耗时:prepareAssistant() 在 SSE headers 前完成权限、DataScope、病例聚合和模型预检。病例正文超过 120,000 字节时,fitContextForPrompt() 同步调用 PatientAiReportLogic::compactSourceForPrompt();后者依次通过 blocking 请求读取全部分片并可能归并摘要。保留这一流程以保持全量临床来源覆盖,不能用截断病例来换速度。
  • 兼容重试也可能增加首字等待:通用 /v1 地址存在协议回退;streamPreparedAssistant() 在尚未交付任何 delta 且流失败为 UPSTREAM_REJECTED / INCOMPLETE_RESPONSE / EMPTY_RESPONSE 时,会额外做一次 blocking 兼容尝试。已经交付文本后不会重复生成。
  • 因此前端需要在用户提交时立即反馈并开始计秒,不能等 SSE start 才开始计时。AssistantSseTiming 已提供不含患者正文的 prepare、first_delta、upstream_done 等日志,可定位实际延迟阶段。

修复

发现 SSE 分帧正则 (?:\r\n|\r|\n){2} 允许回溯,把一个 CRLF 拆成两个换行;仅收到一行 data 时就会提前解析。合法的多行 CRLF data 事件会被拆坏并丢失文本,可能进一步触发无 delta 的兼容重试。

将换行匹配改为原子组 (?>\r\n|\r|\n){2},使 CRLF 始终作为一个换行。事件到达空行即转发,不等待 message_end 或网络 EOF。接口、权限、事件格式、错误码和凭据保护保持兼容。

修改文件:

  • server/app/common/service/DifyChatService.php
  • server/tests/DifyChatStreamContractTest.php
  • server/tests/DifyChatStreamDeliveryTest.php(新增)

新增行为覆盖:

  • data 行尚未结束事件时不解析;完整首帧在终止事件及 EOF 前立即交付。
  • 多行 CRLF data 在 1 字节、2 字节及整段传输切块下保持为同一事件。
  • 离线模拟实际 cURL header/write 回调:每帧在下一次网络读取之前、curl_exec() 返回之前到达消费者;Dify / OpenAI 均覆盖。
  • HTTP 错误正文不进入 delta;消费者拒绝后停止继续读取和交付。

新增回归在修复前实际失败:FAIL: a data line alone is not a complete SSE event;应用修复后通过。

验证

以下命令全部退出码 0:

php server/tests/DifyChatStreamContractTest.php
php server/tests/DifyChatStreamDeliveryTest.php
php server/tests/DifyChatAncientBooksReuseContractTest.php
php server/tests/DiagnosisAiAssistantStreamContractTest.php
php server/tests/DiagnosisAiAssistantTimingTest.php
php server/tests/DiagnosisAiAssistantContractTest.php
php server/tests/DiagnosisAiPrescriptionGeneratePermissionTest.php
php server/tests/DiagnosisAiAnalysisSecurityTest.php
php server/tests/DiagnosisAiAnalysisContractTest.php
php server/tests/PrescriptionAiUpstreamContractTest.php
php -l server/app/common/service/DifyChatService.php
php -l server/tests/DifyChatStreamContractTest.php
php -l server/tests/DifyChatStreamDeliveryTest.php
git diff --check -- server/app/common/service/DifyChatService.php server/tests/DifyChatStreamContractTest.php server/tests/DifyChatStreamDeliveryTest.php

验证均离线进行,没有调用生产模型、读取凭据或测试生产代理配置;因此不提供生产首字时延数值。