<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>SSE on niuzj</title>
    <link>https://niuzj.org/tags/sse/</link>
    <description>Recent content in SSE on niuzj</description>
    <generator>Hugo</generator>
    <language>zh-cn</language>
    <lastBuildDate>Tue, 12 May 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://niuzj.org/tags/sse/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Agent SSE 可恢复流：后端别把长连接当任务生命周期</title>
      <link>https://niuzj.org/posts/agent-sse-resumable-stream/</link>
      <pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate>
      <guid>https://niuzj.org/posts/agent-sse-resumable-stream/</guid>
      <description>&lt;p&gt;做 Agent 聊天时，最开始很容易把 SSE 当成一条“任务管道”。&lt;/p&gt;&#xA;&lt;p&gt;用户点发送，后端开一个流，Agent 一边跑一边往这个 HTTP response 里写数据。浏览器断开，就认为用户不需要了，于是顺手把后台任务也取消。&lt;/p&gt;&#xA;&lt;p&gt;这套在 demo 里很顺。但一到长任务就会暴露问题。&lt;/p&gt;&#xA;&lt;p&gt;生成视频、跑工作流、调图像模型，可能要几十秒甚至几分钟。用户网络抖一下、页面切后台、代理断一下，SSE 连接断了。但 Agent 任务本身不应该死。&lt;/p&gt;&#xA;&lt;p&gt;真正要拆开的，是两件事：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Agent run&lt;/strong&gt;：后台任务生命周期&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;SSE connection&lt;/strong&gt;：前端订阅事件的传输连接&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;连接可以断，可以重连。run 不能因为连接断了就被杀。&lt;/p&gt;&#xA;&lt;h2 id=&#34;架构图&#34;&gt;架构图&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR&#xA;  FE[&amp;#34;Frontend&amp;lt;br/&amp;gt;sendMessage / resumeActiveRun&amp;lt;br/&amp;gt;POST /v1/agent/runs&amp;lt;br/&amp;gt;fetch-event-source 订阅 events&amp;lt;br/&amp;gt;最近 500 个 SSE id 去重&amp;#34;]&#xA;  API[&amp;#34;Agent API&amp;lt;br/&amp;gt;POST /runs 创建 run&amp;lt;br/&amp;gt;GET /runs/{run_id}/events 订阅&amp;lt;br/&amp;gt;cancel / HITL&amp;lt;br/&amp;gt;active-run 只用于页面恢复&amp;#34;]&#xA;  Producer[&amp;#34;Background Agent Producer&amp;lt;br/&amp;gt;执行 Agent run&amp;lt;br/&amp;gt;写 Redis Stream 事件&amp;lt;br/&amp;gt;刷新 runner lease&amp;lt;br/&amp;gt;最终历史写 MongoDB&amp;#34;]&#xA;  Redis[&amp;#34;Redis Runtime Layer&amp;lt;br/&amp;gt;Stream per run&amp;lt;br/&amp;gt;id + event + data&amp;lt;br/&amp;gt;active run state&amp;lt;br/&amp;gt;runner lease TTL&amp;#34;]&#xA;  SSE[&amp;#34;SSE Events Endpoint&amp;lt;br/&amp;gt;读取 Last-Event-ID header&amp;lt;br/&amp;gt;XREAD Redis Stream&amp;lt;br/&amp;gt;输出标准 SSE&amp;lt;br/&amp;gt;heartbeat comment&amp;#34;]&#xA;  Mongo[&amp;#34;MongoDB Final History&amp;lt;br/&amp;gt;完整聊天历史&amp;lt;br/&amp;gt;最终一致性持久化&amp;#34;]&#xA;&#xA;  FE --&amp;gt;|&amp;#34;create run / 409 active_run_id&amp;#34;| API&#xA;  API --&amp;gt;|&amp;#34;start background task&amp;#34;| Producer&#xA;  Producer --&amp;gt;|&amp;#34;XADD event&amp;#34;| Redis&#xA;  SSE --&amp;gt;|&amp;#34;XREAD from cursor&amp;#34;| Redis&#xA;  FE -.-&amp;gt;|&amp;#34;SSE reconnect with Last-Event-ID&amp;#34;| SSE&#xA;  SSE -.-&amp;gt;|&amp;#34;id / event / data&amp;#34;| FE&#xA;  Producer --&amp;gt;|&amp;#34;final messages&amp;#34;| Mongo&#xA;  SSE -.-&amp;gt;|&amp;#34;closes only on run.completed / run.failed / run.cancelled&amp;#34;| FE&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个图里最重要的箭头不是 SSE，而是 &lt;code&gt;Producer -&amp;gt; Redis&lt;/code&gt;。&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
