<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>AI Agent on Renxin's Blog</title><link>https://zh.renxinblog.cn/categories/ai-agent/</link><description>Recent content in AI Agent on Renxin's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Fri, 28 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://zh.renxinblog.cn/categories/ai-agent/index.xml" rel="self" type="application/rss+xml"/><item><title>第十六话｜SSE 是什么？为什么它会用在 AI Agent 开发中</title><link>https://zh.renxinblog.cn/post/sse-in-ai-agent-development/</link><pubDate>Fri, 28 Aug 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/sse-in-ai-agent-development/</guid><description>&lt;p&gt;前面把 Agent 的循环、状态、记忆和工具接起来之后，用户仍可能面对一个黑盒。&lt;/p&gt;
&lt;p&gt;他发出“帮我检查这个仓库的测试为什么失败”，页面却在几十秒内没有任何变化。Agent 也许正在生成，也许正在调用工具，也许已经失败；用户无从分辨。&lt;/p&gt;
&lt;p&gt;问题出在传输方式上：普通 HTTP 是一问一答——客户端发一个请求，服务端算完最后一次性返回完整响应。Agent 干活要几十秒，响应就空白几十秒。要让 Agent 不再像黑盒，前端不只需要最终答案，还要持续接收文本增量、工具状态和 Run 的终态。&lt;/p&gt;
&lt;p&gt;SSE（Server-Sent Events）就是把这类&lt;strong&gt;服务端到浏览器的连续事件&lt;/strong&gt;放进 HTTP 的一种标准方式。结论先说：&lt;strong&gt;SSE 不负责让 Agent 变聪明；它负责把 Agent 已经发生的事，按事件边界可靠地呈现给 UI。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="一旧方案为什么看不见"&gt;一、旧方案为什么看不见
&lt;/h2&gt;&lt;p&gt;“给我一个答案”用普通 HTTP 正合适：提交命令，取回结果，一次交互结束。但“让我看着你干活”不行——服务端在没有完整结果前不会返回响应，浏览器也就拿不到任何进度。&lt;/p&gt;
&lt;p&gt;要让用户看见进度，服务端必须&lt;strong&gt;在计算完成前就开始输出&lt;/strong&gt;，并且持续输出。这正是 SSE 站在 HTTP 上的理由：一个响应保持打开，服务端不断写入“有边界的小段文本”。&lt;/p&gt;
&lt;p&gt;这里有个经常被混在一起的概念要拆开说清楚。&lt;strong&gt;HTTP 持久连接&lt;/strong&gt;（persistent connection / Keep-Alive）指的是底层连接可以复用、承载多次请求响应，它降低建连成本，但服务端不会在没有新请求时主动推送业务消息。&lt;a class="link" href="https://www.rfc-editor.org/rfc/rfc9112.html#section-9.3" target="_blank" rel="noopener"
 &gt;RFC 9112 §9.3&lt;/a&gt; &lt;strong&gt;长轮询&lt;/strong&gt;是应用层写法：服务端先不立即返回，等到有数据或超时才结束响应，浏览器收到后再发起下一次请求。它们都不等于“一个持续打开、服务端主动下推的响应”。&lt;/p&gt;
&lt;p&gt;四种常用方式的层次差别，一张表就能看明白：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;方式&lt;/th&gt;
					&lt;th&gt;它解决什么&lt;/th&gt;
					&lt;th&gt;通信方向&lt;/th&gt;
					&lt;th&gt;一次交互是否持续&lt;/th&gt;
					&lt;th&gt;Agent 中合适的职责&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;普通 HTTP&lt;/td&gt;
					&lt;td&gt;提交命令、取得一次结果&lt;/td&gt;
					&lt;td&gt;双向，但一问一答&lt;/td&gt;
					&lt;td&gt;否&lt;/td&gt;
					&lt;td&gt;创建 Run、提交用户输入、取消任务、确认敏感工具调用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;HTTP 持久连接&lt;/td&gt;
					&lt;td&gt;复用底层连接，减少建连成本&lt;/td&gt;
					&lt;td&gt;仍是多次 HTTP 请求/响应&lt;/td&gt;
					&lt;td&gt;否&lt;/td&gt;
					&lt;td&gt;通常由客户端、网关和连接池处理，不是事件通道方案&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;SSE&lt;/td&gt;
					&lt;td&gt;服务端持续发送有边界的文本事件&lt;/td&gt;
					&lt;td&gt;服务端 → 浏览器&lt;/td&gt;
					&lt;td&gt;是&lt;/td&gt;
					&lt;td&gt;文本增量、工具进度、Run 状态、最终结果&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;WebSocket&lt;/td&gt;
					&lt;td&gt;持续的双向消息&lt;/td&gt;
					&lt;td&gt;双向&lt;/td&gt;
					&lt;td&gt;是&lt;/td&gt;
					&lt;td&gt;协同编辑、高频客户端指令、双方都需随时发消息的交互&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;figure class="gallery-image"&gt;
 &lt;a class="image-link" href="https://zh.renxinblog.cn/images/sse-in-ai-agent-development-http-protocol-relationships.svg" data-pswp-width="746" data-pswp-height="622" target="_blank"&gt;
 &lt;img src="https://zh.renxinblog.cn/images/sse-in-ai-agent-development-http-protocol-relationships.svg" width="746" height="622" alt="四种通信方式处在不同层次：HTTP 持久连接复用连接，SSE 持续响应，WebSocket 在 Upgrade 后使用双向帧"&gt;
 &lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;WebSocket 与 HTTP 的关系也要说准确：它用 HTTP Upgrade 完成开场握手，握手成功后双方交换的是 WebSocket 帧，而不是继续交换 HTTP 响应体。&lt;a class="link" href="https://datatracker.ietf.org/doc/html/rfc6455#section-4" target="_blank" rel="noopener"
 &gt;RFC 6455 §4&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键洞察：持久连接是连接复用策略；SSE 是 HTTP 上的事件流格式；WebSocket 是经 HTTP 握手切换出的双向协议。它们不在同一抽象层。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="二亲眼看sse-在线上传了什么"&gt;二、亲眼看：SSE 在线上传了什么
&lt;/h2&gt;&lt;p&gt;浏览器用 &lt;code&gt;EventSource&lt;/code&gt; 发起请求，服务端以 &lt;code&gt;Content-Type: text/event-stream&lt;/code&gt; 返回响应，并持续写入按行组织的 UTF-8 文本。每个事件以空行结束。&lt;a class="link" href="https://html.spec.whatwg.org/multipage/server-sent-events.html" target="_blank" rel="noopener"
 &gt;WHATWG HTML Standard §9.2&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;下面是一个完整事件，不是一段“随便输出的文本”：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;id: 42
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;event: text.delta
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;runId&amp;#34;:&amp;#34;run_123&amp;#34;,&amp;#34;delta&amp;#34;:&amp;#34;正在读取测试报告…&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;四个常用字段的职责如下。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;字段&lt;/th&gt;
					&lt;th&gt;协议层含义&lt;/th&gt;
					&lt;th&gt;在 Agent 流中的用法&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;data&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;事件数据；可以多行，浏览器会按规范合并&lt;/td&gt;
					&lt;td&gt;放 JSON payload，例如文本增量或工具结果摘要&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;event&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;事件类型；省略时是默认 &lt;code&gt;message&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;区分 &lt;code&gt;text.delta&lt;/code&gt;、&lt;code&gt;tool.started&lt;/code&gt;、&lt;code&gt;run.completed&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;更新浏览器保存的最后事件 ID&lt;/td&gt;
					&lt;td&gt;用作断线续传的游标；服务端仍需校验 Run 归属&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;retry&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;建议浏览器下一次重连前等待的毫秒数&lt;/td&gt;
					&lt;td&gt;仅调整重连等待，不解决事件去重或业务恢复&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注意最后一行的空行不是装饰。它是事件边界：如果连接在空行前结束，尚未完成的事件不会被分发。&lt;a class="link" href="https://html.spec.whatwg.org/multipage/server-sent-events.html" target="_blank" rel="noopener"
 &gt;WHATWG HTML Standard §9.2.5–9.2.6&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;浏览器端不需要自行解析每一行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`/runs/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;/events`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;text.delta&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;appendAssistantText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;run.completed&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;EventSource&lt;/code&gt; 对断开连接具备重连处理，并在已有事件 ID 时把 &lt;code&gt;Last-Event-ID&lt;/code&gt; 带回服务端。它提供的是恢复的协议钩子，&lt;strong&gt;不是“消息恰好一次送达”的保证&lt;/strong&gt;；应用仍要定义续传、去重和终态行为。&lt;a class="link" href="https://html.spec.whatwg.org/multipage/server-sent-events.html" target="_blank" rel="noopener"
 &gt;WHATWG HTML Standard §9.2.3–9.2.4&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="三事件的形状由业务决定agent-run-事件契约"&gt;三、事件的形状由业务决定：Agent Run 事件契约
&lt;/h2&gt;&lt;p&gt;只把模型 token 一段段传到 UI，用户会看到“它在说话”，但仍不知道它是否在行动。&lt;/p&gt;
&lt;p&gt;对一个会调用工具的 Agent，更有用的做法是把 Run 生命周期投影成一组应用层事件：&lt;/p&gt;
&lt;figure class="gallery-image"&gt;
 &lt;a class="image-link" href="https://zh.renxinblog.cn/images/sse-in-ai-agent-development-agent-run-event-flow.svg" data-pswp-width="864" data-pswp-height="288" target="_blank"&gt;
 &lt;img src="https://zh.renxinblog.cn/images/sse-in-ai-agent-development-agent-run-event-flow.svg" width="864" height="288" alt="一次 Agent Run 中，普通 HTTP 用来创建任务，SSE 用来把模型与工具产生的下行事件送到浏览器"&gt;
 &lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;例如，“检查测试为什么失败”这个 Run 在客户端看来可以是这样一串事件：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;event: text.delta
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;runId&amp;#34;:&amp;#34;run_123&amp;#34;,&amp;#34;delta&amp;#34;:&amp;#34;我先运行测试。&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;event: tool.started
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;runId&amp;#34;:&amp;#34;run_123&amp;#34;,&amp;#34;tool&amp;#34;:&amp;#34;run_tests&amp;#34;,&amp;#34;callId&amp;#34;:&amp;#34;call_7&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;event: tool.completed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;runId&amp;#34;:&amp;#34;run_123&amp;#34;,&amp;#34;callId&amp;#34;:&amp;#34;call_7&amp;#34;,&amp;#34;summary&amp;#34;:&amp;#34;3 个测试失败&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;event: run.completed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;runId&amp;#34;:&amp;#34;run_123&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这些名称不是 SSE 规定的字段值，而是本文建议的&lt;strong&gt;业务事件契约&lt;/strong&gt;。SSE 只规定如何分帧、如何指定事件类型、如何传递最后事件 ID；&lt;code&gt;tool.started&lt;/code&gt; 是否存在、数据结构是什么、是否向用户展示工具结果，都由你的 Agent 产品和安全策略决定。&lt;/p&gt;
&lt;p&gt;这和 Java 微服务中的“领域事件”和“Kafka 的传输记录”很像：Kafka 不替你定义订单状态机；SSE 也不替你定义 Agent Run 状态机。先定义业务状态和权限边界，再选择传输通道。&lt;/p&gt;
&lt;h2 id="四动手跑通一条本地-agent-run-事件流"&gt;四、动手：跑通一条本地 Agent Run 事件流
&lt;/h2&gt;&lt;p&gt;这一节的 demo 不调用真实模型，避免让 API Key、模型费用或网络问题遮住协议本身。服务端固定模拟一个 Run：它先发送文本增量，再报告工具开始、工具完成，最后发送 Run 完成事件。&lt;/p&gt;
&lt;p&gt;配套代码有两份等价实现：Python 位于 GYA 的 &lt;code&gt;c16-sse-agent-stream/&lt;/code&gt;，Java 位于 GYA-Java 的同名 Gradle 子模块。正文以 Python 为主，Java 版用于验证同一协议不依赖语言或 Spring 框架。&lt;/p&gt;
&lt;h3 id="python标准库即可启动"&gt;Python：标准库即可启动
&lt;/h3&gt;&lt;p&gt;前置环境：Python 3.9+ 与 &lt;code&gt;curl&lt;/code&gt;，无第三方依赖、无 API Key。&lt;/p&gt;
&lt;p&gt;终端一启动服务：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git clone git@github.com:renxin2024/GYA.git
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; GYA/c16-sse-agent-stream
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 main.py --once
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;终端二订阅：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -N http://127.0.0.1:8765/events
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;我在本机实际收到的输出如下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;id: 1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;event: text.delta
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;runId&amp;#34;:&amp;#34;run_demo&amp;#34;,&amp;#34;delta&amp;#34;:&amp;#34;我先运行测试。&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;id: 2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;event: tool.started
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;runId&amp;#34;:&amp;#34;run_demo&amp;#34;,&amp;#34;tool&amp;#34;:&amp;#34;run_tests&amp;#34;,&amp;#34;callId&amp;#34;:&amp;#34;call_1&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;id: 3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;event: tool.completed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;runId&amp;#34;:&amp;#34;run_demo&amp;#34;,&amp;#34;callId&amp;#34;:&amp;#34;call_1&amp;#34;,&amp;#34;summary&amp;#34;:&amp;#34;3 个测试失败&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;id: 4
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;event: run.completed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data: {&amp;#34;runId&amp;#34;:&amp;#34;run_demo&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里最重要的不是 &lt;code&gt;sleep(0.1)&lt;/code&gt;，而是每次写完一帧后立刻 flush，并且用最后那个空行完成事件。把空行删掉，再用浏览器 &lt;code&gt;EventSource&lt;/code&gt; 接收时，读者会发现它不会把这一帧当作完成的事件。&lt;/p&gt;
&lt;h3 id="java同一事件流不换协议"&gt;Java：同一事件流，不换协议
&lt;/h3&gt;&lt;p&gt;前置环境：JDK 21 与 &lt;code&gt;curl&lt;/code&gt;。GYA-Java 使用仓库自带的 Gradle Wrapper；首次运行会按仓库配置下载 Gradle 和 Jackson。&lt;/p&gt;
&lt;p&gt;终端一启动：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git clone git@github.com:renxin2024/GYA-Java.git
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; GYA-Java
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./gradlew :c16-sse-agent-stream:run --args&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;--once&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;终端二订阅：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -N http://127.0.0.1:8766/events
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Java 版构建通过，并按相同顺序输出 &lt;code&gt;text.delta&lt;/code&gt;、&lt;code&gt;tool.started&lt;/code&gt;、&lt;code&gt;tool.completed&lt;/code&gt;、&lt;code&gt;run.completed&lt;/code&gt;。核心的分帧代码和 Python 同构，只是把字符串拼接换成了 Jackson 序列化 payload：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;static&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;throws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;IOException&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// SSE 是 UTF-8 的按行协议。最后的空行是事件边界；删除它会让&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// EventSource 持续等待，而不是把前面的字段作为一条完整事件分发出去。&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;id: &amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;\n&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;event: &amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;\n&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;data: &amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeValueAsString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;\n\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StandardCharsets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;UTF_8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;Event&lt;/code&gt; 是一个只保存 &lt;code&gt;id&lt;/code&gt;、&lt;code&gt;type&lt;/code&gt;、&lt;code&gt;data&lt;/code&gt; 的 record；&lt;code&gt;EVENTS&lt;/code&gt; 里和 Python 一样放着四个固定事件。既然协议的“形状”完全由分帧代码决定，换语言、换框架都不会改变这条事件流的语义——这正是把协议层与业务层分开的价值。&lt;/p&gt;
&lt;p&gt;如果 Java 版本因 toolchain 无法启动，先运行 &lt;code&gt;java -version&lt;/code&gt; 确认实际环境是 JDK 21。这个 demo 故意保持 Java 21，不为了迁就本机默认 JDK 而降低系列约束。另外，不要把 Python 与 Java 输出的 JSON 键顺序当作正确性标准：JSON 字段顺序不属于 SSE 契约。&lt;/p&gt;
&lt;h2 id="五能连通--体验到好三个坑"&gt;五、能连通 ≠ 体验到好：三个坑
&lt;/h2&gt;&lt;p&gt;连得上、有输出，不代表体验是对的。下面是三个最常见的“能连通、但体验很差”的问题。&lt;/p&gt;
&lt;h3 id="1-服务端在发用户却最后一次性看到"&gt;1. 服务端在发，用户却最后一次性看到
&lt;/h3&gt;&lt;p&gt;SSE 需要端到端按事件及时传输。规范明确提醒：不恰当的块缓冲可能延迟事件分发。&lt;a class="link" href="https://html.spec.whatwg.org/multipage/server-sent-events.html" target="_blank" rel="noopener"
 &gt;WHATWG HTML Standard §9.2.5&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;因此排查顺序不该从“模型为什么这么慢”开始，而应沿着浏览器 → CDN/网关 → 反向代理 → 应用服务器逐跳确认：响应类型是否正确、是否有缓冲、应用是否真的在事件产生时写出。&lt;code&gt;curl&lt;/code&gt; 先关掉客户端缓冲（&lt;code&gt;curl -N&lt;/code&gt;）；如果 curl 能一帧帧出来、浏览器不行，问题大概率在中间层。具体关闭何种缓冲取决于网关产品和部署方式，不能从一段通用配置照抄。&lt;/p&gt;
&lt;h3 id="2-断线后重复显示或者漏掉关键状态"&gt;2. 断线后重复显示，或者漏掉关键状态
&lt;/h3&gt;&lt;p&gt;浏览器会携带 &lt;code&gt;Last-Event-ID&lt;/code&gt; 重连，但服务端必须决定这个 ID 的语义：它是某个 Run 内单调递增的序号，还是全局事件 ID？事件保留多久？补发窗口过期时如何回退？&lt;/p&gt;
&lt;p&gt;一个可维护的最小约定是：&lt;code&gt;runId + eventId&lt;/code&gt; 唯一；客户端按该组合去重；服务端只允许已授权的主体按 &lt;code&gt;runId&lt;/code&gt; 查询或续传。这里的授权检查不能因为客户端带了一个事件 ID 就被跳过。&lt;/p&gt;
&lt;h3 id="3-run-已结束连接和资源还在等待"&gt;3. Run 已结束，连接和资源还在等待
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;run.completed&lt;/code&gt;、&lt;code&gt;run.failed&lt;/code&gt;、&lt;code&gt;run.cancelled&lt;/code&gt; 这类终态应有清晰语义：服务端发送终态事件后结束流，客户端收到后关闭 &lt;code&gt;EventSource&lt;/code&gt; 并更新界面。心跳注释可以帮助发现中间链路的空闲超时，但心跳不应伪装成业务进度。&lt;a class="link" href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events" target="_blank" rel="noopener"
 &gt;MDN SSE 指南&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键洞察：SSE 解决“事件怎样流到浏览器”；事件是否可恢复、可授权、可解释，仍是 Agent Run 模型的责任。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="六回到开场黑盒变成了可见的事件流"&gt;六、回到开场：黑盒变成了可见的事件流
&lt;/h2&gt;&lt;p&gt;现在回头看开头的那个 Run。“帮我检查这个仓库的测试为什么失败”在 demo 里变成了一条可见的时间线：先是“我先运行测试。”的文本增量，然后是工具开始、工具完成（&lt;code&gt;3 个测试失败&lt;/code&gt;），最后 Run 完成。用户不再对着空白页面猜 Agent 是不是卡死了——他能看到 Agent 正在做什么、做到哪了、结果如何。&lt;/p&gt;
&lt;p&gt;这就是这一话真正改变的东西：&lt;strong&gt;“它在动”从结论变成了过程。&lt;/strong&gt; SSE 不负责让 Agent 变聪明，它负责把 Agent 正在做和已经做的事，按事件边界可靠地送到浏览器；之后要不要展示工具结果、要不要拒绝敏感调用，仍然是产品与安全决策。&lt;/p&gt;
&lt;h2 id="七选型什么时候选-sse什么时候不选"&gt;七、选型：什么时候选 SSE，什么时候不选
&lt;/h2&gt;&lt;p&gt;如果 UI 的主要需求是“服务端持续告诉用户发生了什么”，SSE 是很自然的第一候选：浏览器有原生 &lt;code&gt;EventSource&lt;/code&gt;，协议有事件类型和重连语义，接入普通 HTTP 基础设施的成本通常较低。&lt;/p&gt;
&lt;p&gt;如果客户端也需要高频、低延迟地主动发消息，例如多人协同编辑、实时控制面板或双向交互游戏，再评估 WebSocket。若只是提交用户问题、取消 Run 或同意执行高风险工具，普通 HTTP 往往更直观，也更容易把请求、权限校验和审计记录放在清晰的命令边界上。&lt;/p&gt;
&lt;p&gt;不要用“WebSocket 更实时”替代选型分析。对 Agent 来说，最关键的问题是：消息主要朝哪个方向流？是否需要浏览器原生重连与事件 ID？你的 Run 是否有可恢复的状态模型？&lt;/p&gt;
&lt;h2 id="八下一步"&gt;八、下一步
&lt;/h2&gt;&lt;p&gt;这一话放在 GYA 的生产级补充位置：它不增加 Agent 的推理能力，却决定用户能否看见、理解和恢复一次正在运行的 Agent 任务。&lt;/p&gt;
&lt;p&gt;从开场那个 Run 继续往后走，下一步应把同样的事件契约接到真实的 Run 状态存储、鉴权与可观测性中——而不是继续往 SSE 帧里塞业务判断。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;参考资料
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;a class="link" href="https://html.spec.whatwg.org/multipage/server-sent-events.html" target="_blank" rel="noopener"
 &gt;WHATWG HTML Standard — Server-sent events&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.rfc-editor.org/rfc/rfc9112.html#section-9.3" target="_blank" rel="noopener"
 &gt;RFC 9112 — HTTP/1.1 Persistence&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://datatracker.ietf.org/doc/html/rfc6455" target="_blank" rel="noopener"
 &gt;RFC 6455 — The WebSocket Protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events" target="_blank" rel="noopener"
 &gt;MDN — Using server-sent events&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-async.html#webmvc-ann-async-sse" target="_blank" rel="noopener"
 &gt;Spring Framework — MVC SSE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.spring.io/spring-framework/reference/web/webflux/controller/ann-methods/return-types.html" target="_blank" rel="noopener"
 &gt;Spring Framework — WebFlux return values&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description></item></channel></rss>