<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>可观测与协议 on 扎塔-Zata</title><link>https://www.zata.cc/tags/%E5%8F%AF%E8%A7%82%E6%B5%8B%E4%B8%8E%E5%8D%8F%E8%AE%AE/</link><description>Recent content in 可观测与协议 on 扎塔-Zata</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><copyright>Example Person</copyright><lastBuildDate>Thu, 24 Sep 2026 17:09:06 +0800</lastBuildDate><atom:link href="https://www.zata.cc/tags/%E5%8F%AF%E8%A7%82%E6%B5%8B%E4%B8%8E%E5%8D%8F%E8%AE%AE/index.xml" rel="self" type="application/rss+xml"/><item><title>Agent 决策审计落地：写入点、复核器与门禁降级判据</title><link>https://www.zata.cc/p/agent-decision-audit-implementation/</link><pubDate>Thu, 24 Sep 2026 14:15:00 +0800</pubDate><guid>https://www.zata.cc/p/agent-decision-audit-implementation/</guid><description>&lt;img src="https://www.zata.cc/p/agent-decision-audit-implementation/images/index/index.svg" alt="Featured image of post Agent 决策审计落地：写入点、复核器与门禁降级判据" />&lt;p>一份能用的决策审计，第一版可以只有六行 JSONL。还是&lt;a class="link" href="https://www.zata.cc/p/agent-decision-audit-and-tracing/" >上一篇&lt;/a>里那个改数据库连接配置的例子——Agent 选中了单元测试和真实数据库冒烟，这次把事件流补全：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;seq&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;decision_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;dec-42&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;event_type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;plan_created&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;actor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;planner&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;source_revision&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;a1b2c3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;detail&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="nt">&amp;#34;checks&amp;#34;&lt;/span>&lt;span class="p">:[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;check_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;unit-tests&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;disposition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;selected&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;check_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;real-db-smoke&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;disposition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;selected&amp;#34;&lt;/span>&lt;span class="p">}]}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;seq&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;decision_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;dec-42&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;event_type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;check_started&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;actor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;executor&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;detail&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="nt">&amp;#34;check_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;unit-tests&amp;#34;&lt;/span>&lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;seq&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;decision_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;dec-42&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;event_type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;check_finished&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;actor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;executor&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;detail&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="nt">&amp;#34;check_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;unit-tests&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;result&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;passed&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;exit_code&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;evidence_ref&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;runs/17/logs/pytest-1&amp;#34;&lt;/span>&lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;seq&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;decision_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;dec-42&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;event_type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;check_started&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;actor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;executor&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;detail&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="nt">&amp;#34;check_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;real-db-smoke&amp;#34;&lt;/span>&lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;seq&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;decision_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;dec-42&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;event_type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;check_finished&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;actor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;executor&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;detail&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="nt">&amp;#34;check_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;real-db-smoke&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;result&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;unavailable&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;reason&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;无预发凭据&amp;#34;&lt;/span>&lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;seq&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;decision_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;dec-42&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;event_type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;verdict_issued&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;actor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;verifier&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;detail&amp;#34;&lt;/span>&lt;span class="p">:{&lt;/span>&lt;span class="nt">&amp;#34;verdict&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s2">&amp;#34;escalate&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nt">&amp;#34;missing_boundaries&amp;#34;&lt;/span>&lt;span class="p">:[&lt;/span>&lt;span class="s2">&amp;#34;连接池真实行为&amp;#34;&lt;/span>&lt;span class="p">]}}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>字段比设计篇的记录少一些——&lt;code>run_id&lt;/code>、&lt;code>attempt_id&lt;/code>、&lt;code>occurred_at&lt;/code>、&lt;code>trace_id&lt;/code> 都还在，这里略去不展。第六行就是这套东西的价值：冒烟拿不到凭据时，裁决不是&amp;quot;通过&amp;quot;，是升级。事后任何人拿着 &lt;code>dec-42&lt;/code> 都能回答：当时计划跑什么、实际跑到哪、缺了什么、为什么没放行。&lt;/p>
&lt;p>设计篇把&amp;quot;该记什么&amp;quot;讲清了：身份模型、计划/执行/裁决分离、&lt;code>history_complete&lt;/code>。这篇接着回答落地时真正卡住的三件事——**写入点挂在哪、复核器怎么不变成第二个 Agent、什么条件下才允许它影响放行。**我的判断放在前面：存储本身一晚上就能写完，卡人的从来是这三件。&lt;/p>
&lt;h2 id="一先说不建什么">一、先说不建什么
&lt;/h2>&lt;p>动手前先划掉三个&amp;quot;看起来该做&amp;quot;的东西。审计的第一死因不是记得不准，是被绕过——系统一重，团队总有办法绕过它。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>做重了的信号&lt;/th>
&lt;th>v1 的做法&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>起一个独立的审计服务&lt;/td>
&lt;td>一张追加式表，执行进程内直接写&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>把原文（prompt、源码、终端输出）存进审计&lt;/td>
&lt;td>只存引用与摘要，原文走受控存储&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>上线即接管放行、替换现有门禁&lt;/td>
&lt;td>先与固定门禁并行，只记录分歧&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>第三条是全文最重要的边界。设计篇说决策审计让&amp;quot;自选验证深度&amp;quot;的自由度可复核，但可复核不等于可放行——从&amp;quot;并行记录&amp;quot;走到&amp;quot;参与放行&amp;quot;，中间隔着第五节那套判据，判据没满足之前，记录再完整也不构成授权。&lt;/p>
&lt;h2 id="二最小数据契约一张只允许-insert-的表">二、最小数据契约：一张只允许 INSERT 的表
&lt;/h2>&lt;p>DDL 以 SQLite 3 为准，换 PostgreSQL 只需要改触发器写法：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">decision_events&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">seq&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INTEGER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AUTOINCREMENT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">event_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">UNIQUE&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- 幂等键，由业务身份拼出
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">decision_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">run_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">attempt_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">event_type&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">CHECK&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">event_type&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;plan_created&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s1">&amp;#39;check_started&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s1">&amp;#39;check_finished&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;verdict_issued&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s1">&amp;#39;decision_invalidated&amp;#39;&lt;/span>&lt;span class="p">)),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">actor&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">source_revision&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">policy_version&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">trace_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">occurred_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">detail&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- JSON，自带 detail_schema_version
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INDEX&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">idx_events_decision&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">decision_events&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">decision_id&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TRIGGER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">no_update&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BEFORE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">UPDATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">decision_events&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">BEGIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RAISE&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">ABORT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;append-only&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">END&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TRIGGER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">no_delete&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BEFORE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DELETE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">decision_events&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">BEGIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RAISE&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">ABORT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;append-only&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">END&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>追加式不是审美偏好，它换来两个便宜：没有 UPDATE，历史就不可改；完整性验证退化为检查&amp;quot;存在且有序&amp;quot;（第六节展开）。触发器挡的是自己人手滑，真正的防线在读取端。&lt;/p>
&lt;p>五个事件类型各自的写入责任：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>event_type&lt;/th>
&lt;th>谁写&lt;/th>
&lt;th>必填 detail&lt;/th>
&lt;th>记录的事实&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>plan_created&lt;/td>
&lt;td>planner&lt;/td>
&lt;td>checks（每项含 disposition 与 reason）&lt;/td>
&lt;td>决策的存在起点&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>check_started&lt;/td>
&lt;td>executor&lt;/td>
&lt;td>check_id&lt;/td>
&lt;td>某项检查开始执行&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>check_finished&lt;/td>
&lt;td>executor&lt;/td>
&lt;td>check_id、result、evidence_ref&lt;/td>
&lt;td>执行器观察到的结果&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>verdict_issued&lt;/td>
&lt;td>verifier / 放行器&lt;/td>
&lt;td>verdict、依据摘要&lt;/td>
&lt;td>对计划与证据的裁决&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>decision_invalidated&lt;/td>
&lt;td>平台&lt;/td>
&lt;td>失效原因、新 revision&lt;/td>
&lt;td>旧裁决作废&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>两个容易写错的字段。&lt;code>occurred_at&lt;/code> 用写入方本地时钟就行，排序靠 &lt;code>seq&lt;/code>，时钟只负责展示——分布式环境里别指望时钟可排序。&lt;code>detail&lt;/code> 里必须带 &lt;code>detail_schema_version&lt;/code>：追加式意味着旧记录永远不会迁移，演进只能靠新版本号，读取端按版本解释。&lt;/p>
&lt;p>事件不是随便堆的，读取端会按一个状态机校验它们：&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/agent-decision-audit-implementation/images/index/event-state-machine.svg"
loading="lazy"
alt="五个事件类型构成的状态机"
>&lt;/p>
&lt;p>&lt;em>▲ 图：自绘&lt;/em>&lt;/p>
&lt;h2 id="三写入点审计不改变控制流只挂在三个钩子上">三、写入点：审计不改变控制流，只挂在三个钩子上
&lt;/h2>&lt;p>决策审计在 Runtime 里没有自己的环节。它不参与循环，不加延迟，只挂在三个既有节点的出口上：&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/agent-decision-audit-implementation/images/index/write-points.svg"
loading="lazy"
alt="三个写入点在 Runtime 中的位置"
>&lt;/p>
&lt;p>&lt;em>▲ 图：自绘&lt;/em>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>写入点&lt;/th>
&lt;th>挂在哪&lt;/th>
&lt;th>actor&lt;/th>
&lt;th>写什么&lt;/th>
&lt;th>不写什么&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>计划成立时&lt;/td>
&lt;td>planner 的结构化输出通过 schema 校验之后&lt;/td>
&lt;td>planner&lt;/td>
&lt;td>plan_created&lt;/td>
&lt;td>模型原始 token 流&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>检查起止时&lt;/td>
&lt;td>执行器进程内，每项检查的前后&lt;/td>
&lt;td>executor&lt;/td>
&lt;td>check_started / check_finished&lt;/td>
&lt;td>Agent 对结果的转述&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>裁决签发时&lt;/td>
&lt;td>verifier 出结论、放行器动动作&lt;/td>
&lt;td>verifier&lt;/td>
&lt;td>verdict_issued&lt;/td>
&lt;td>覆盖 planner 的记录&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>三个坑都在挂的位置上。&lt;/p>
&lt;p>**计划写入要在 schema 校验之后。**校验之前写，Agent 输出里每段格式垃圾都进审计表。审计表被噪声淹没后没人再信它——这比没有审计更糟。&lt;/p>
&lt;p>**check_finished 只能由执行器写。**这是设计篇&amp;quot;Agent 承诺和执行器事实分开&amp;quot;的实现面：Agent 在上下文里说&amp;quot;测试通过了&amp;quot;不算数，执行器进程里那行代码写下的才算。执行器与 Agent 同进程时，至少保证写入点在执行函数内部，而不是在 Agent 转述的文本后面。&lt;/p>
&lt;p>**审计写不进去时，决策还能不能继续？**按阶段分两说。v1 的并行阶段，写入失败只把该 attempt 计为&amp;quot;审计缺失&amp;quot;，让缺失率本身成为统计指标；到了将来接管的阶段，写不进审计的 attempt 一律不允许自动放行。哪种都可以，唯独不能静默吞掉失败。&lt;/p>
&lt;p>写入器是全文最简单的代码，唯一值得注意的是幂等键——由业务身份拼出来，重试同一个动作不会写进两条：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">append_event&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">conn&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">decision_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">run_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">event_type&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">actor&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">source_revision&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">policy_version&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">detail&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">attempt_id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">None&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">trace_id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">None&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">key_suffix&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">event_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">decision_id&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">event_type&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">key_suffix&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="c1"># 确定性幂等键&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">conn&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;INSERT OR IGNORE INTO decision_events &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;(event_id, decision_id, run_id, attempt_id, event_type, actor,&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34; source_revision, policy_version, trace_id, occurred_at, detail)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34; VALUES (?,?,?,?,?,?,?,?,?,?,?)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">event_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">decision_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">run_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">attempt_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">event_type&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">actor&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">source_revision&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">policy_version&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">trace_id&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">datetime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">timezone&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">utc&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">isoformat&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">detail&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ensure_ascii&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">)),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">conn&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">commit&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>幂等键的取法和&lt;a class="link" href="https://www.zata.cc/p/agent-production-engineering-handbook/" >全景手册&lt;/a>对工具调用的要求同源：按业务身份（&lt;code>decision_id + event_type + key_suffix&lt;/code>）去重，不按请求 ID。检查类事件里 &lt;code>key_suffix&lt;/code> 就是 &lt;code>check_id&lt;/code>，同一项检查反复重试，&lt;code>check_started&lt;/code> 只落一条。&lt;/p>
&lt;h2 id="四复核器规则在前模型在后">四、复核器：规则在前，模型在后
&lt;/h2>&lt;p>复核器最大的落地风险，是写成&amp;quot;再跑一个 Agent 看一遍&amp;quot;——那就成了第二个 planner，共享同样的盲点，还多付一遍模型钱。把复核拆成两层，规则层不花钱，不过就升级：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">verify_decision&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">events&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">pending_revision&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">plan&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">first_of&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">events&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;plan_created&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 缺它，整份记录无效&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">checks&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;detail&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s2">&amp;#34;check_id&amp;#34;&lt;/span>&lt;span class="p">]:&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;detail&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">e&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">events&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;event_type&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;check_finished&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">missing&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">c&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;check_id&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">c&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">plan&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;detail&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s2">&amp;#34;checks&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1"># 规则一&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">c&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;disposition&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;selected&amp;#34;&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">c&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;check_id&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">checks&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">not_passed&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">cid&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">cid&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">d&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">checks&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">items&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># 规则二&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">d&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;result&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;passed&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">stale&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">plan&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;source_revision&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="n">pending_revision&lt;/span> &lt;span class="c1"># 规则三&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">unexplained&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">c&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;check_id&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">c&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">plan&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;detail&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s2">&amp;#34;checks&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1"># 规则四&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">c&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;disposition&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;skipped&amp;#34;&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">c&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;reason&amp;#34;&lt;/span>&lt;span class="p">)]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">missing&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="n">not_passed&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="n">stale&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="n">unexplained&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">escalate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">missing&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">not_passed&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">stale&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">unexplained&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">model_coverage_review&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">plan&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">events&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 规则全过，才问模型覆盖面&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>四条规则各自挡一种&amp;quot;结果是绿的、判断仍然错&amp;quot;：&lt;strong>规则一&lt;/strong>挡&amp;quot;计划写了但没跑完&amp;quot;；&lt;strong>规则二&lt;/strong>挡 &lt;code>unavailable&lt;/code>、&lt;code>failed&lt;/code> 被当通过——开场例子里冒烟拿不到凭据，裁决只能 escalate，就是这条在起作用；&lt;strong>规则三&lt;/strong>挡&amp;quot;检查的是 A、要合并的是 B&amp;quot;；&lt;strong>规则四&lt;/strong>挡跳过不留理由。规则四其实应该在 &lt;code>plan_created&lt;/code> 写入时就被 schema 拦下，复核器再查一遍，是因为写入端校验会随时间漂移，读取端要有独立的防线。&lt;/p>
&lt;p>规则层全过，才进第二层。模型复核只回答规则回答不了的问题：&lt;strong>计划对风险事实的覆盖面&lt;/strong>。&amp;ldquo;改了连接池配置，计划里却没有真实数据库冒烟&amp;quot;这类漏边界，规则查不出来，模型或人可以。第二层的产出同样是一条 &lt;code>verdict_issued&lt;/code>，&lt;code>actor=verifier&lt;/code>，引用同一个 &lt;code>decision_id&lt;/code>，不覆盖任何旧记录。verdict 只需要两个值：支持放行、升级人工——&amp;ldquo;拦截&amp;quot;不是它的职责，规则层已经拦了。&lt;/p>
&lt;p>防共享盲点的最低成本做法：verifier 的提示词版本、甚至模型路由，都和 planner 不同，且两者的 &lt;code>policy_version&lt;/code> 都写进了记录。事后能看出&amp;quot;同一个模型自己复核自己&amp;quot;这种结构缺陷。&lt;/p>
&lt;h2 id="五降级判据比记录格式更重要">五、降级判据比记录格式更重要
&lt;/h2>&lt;p>并行阶段的目标不是&amp;quot;验证审计好用&amp;rdquo;，是&lt;strong>把固定门禁和决策审计放在同一批任务上，量化它们在哪儿不一致&lt;/strong>。每个 attempt 两边各有结论，落在四个象限：&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/agent-decision-audit-implementation/images/index/divergence-quadrant.svg"
loading="lazy"
alt="分歧四象限与降级判据"
>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>固定门禁拦下&lt;/th>
&lt;th>固定门禁放行&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>决策审计放行&lt;/strong>&lt;/td>
&lt;td>漏报候选——最危险的象限，逐条人工复检&lt;/td>
&lt;td>一致放行，正常交付，抽样复核&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>决策审计拦截&lt;/strong>&lt;/td>
&lt;td>一致拦截，抽查确认拦截理由&lt;/td>
&lt;td>误报候选——计入审计的误拦成本&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>分歧统计一条 SQL 就够：&lt;code>gate_outcomes&lt;/code> 记固定门禁结果，&lt;code>audit_outcomes&lt;/code> 从 &lt;code>verdict_issued&lt;/code> 聚合出 &lt;code>passed&lt;/code>（verdict 为&amp;quot;支持放行&amp;rdquo;）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">g&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">gate_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">attempts&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">SUM&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">g&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">blocked&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">passed&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">audit_missed&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">SUM&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">g&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">blocked&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">passed&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">audit_false_block&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">gate_outcomes&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">g&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">audit_outcomes&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">USING&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">attempt_id&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GROUP&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">g&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">gate_name&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>（SQLite 3 的布尔求和写法；PostgreSQL 换成 &lt;code>COUNT(*) FILTER (WHERE …)&lt;/code>。）&lt;/p>
&lt;p>什么时候允许决策审计接管某个固定门禁？三个条件必须&lt;strong>同时&lt;/strong>满足，并且要写进试点协议、带上样本量下限：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>漏报候选归零&lt;/strong>。人工逐条复检这个象限，没有一条真实漏网。样本量下限按该门禁的历史拦截率反推——拦得越少的门禁，需要越长的并行期。&lt;/li>
&lt;li>&lt;strong>误报候选率不高于固定门禁自己的误拦率&lt;/strong>。否则不是升级，只是把可见的脚本误报换成不易发现的判断误报。&lt;/li>
&lt;li>&lt;strong>分歧可归因&lt;/strong>。每条分歧都能落到具体原因：策略版本差、检查覆盖差、环境不可用。归因不出的分歧占比高，说明记录本身还不可复核——先修记录，别谈接管。&lt;/li>
&lt;/ol>
&lt;p>三个条件连续 N 轮（N 写进协议）同时满足，才把&lt;strong>这一项&lt;/strong>门禁从阻断降为提示，逐项降、不打包。迁移链完整性这种失误代价极高的规则，可能永远不该降。&lt;/p>
&lt;p>并行期里 &lt;code>decision_invalidated&lt;/code> 同样要写：待合并分支更新导致 &lt;code>source_revision&lt;/code> 变化、&lt;code>policy_version&lt;/code> 升级，都追加一条失效事件。放行器只认&amp;quot;verdict 绑定的 revision 等于当前待放行的 revision&amp;quot;。&lt;/p>
&lt;p>判据比记录格式重要，这句话值得单独一段：&lt;strong>没有判据的并行记录，只是一份更贵的日志。&lt;/strong>&lt;/p>
&lt;h2 id="六审计存储自身会坏读取端怎么认账">六、审计存储自身会坏：读取端怎么认账
&lt;/h2>&lt;p>设计篇提过 &lt;code>history_complete&lt;/code>，落到读取端是一个便宜的校验函数：拿一个 &lt;code>decision_id&lt;/code> 的事件序列，检查它以 &lt;code>plan_created&lt;/code> 开头、以 &lt;code>verdict_issued&lt;/code> 结尾、每个 &lt;code>check_started&lt;/code> 都有配对的 &lt;code>check_finished&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">history_complete&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">events&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="c1"># events 已按 seq 排序、按 decision_id 过滤&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">types&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;event_type&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">e&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">events&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">types&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="n">types&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;plan_created&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">False&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="s2">&amp;#34;verdict_issued&amp;#34;&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">types&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="s2">&amp;#34;decision_invalidated&amp;#34;&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">types&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">False&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">started&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;detail&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s2">&amp;#34;check_id&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">e&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">events&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;event_type&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;check_started&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">finished&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;detail&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s2">&amp;#34;check_id&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">e&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">events&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;event_type&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;check_finished&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">started&lt;/span> &lt;span class="o">&amp;lt;=&lt;/span> &lt;span class="n">finished&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>注意校验按 &lt;code>decision_id&lt;/code> 分组做，不按全局 &lt;code>seq&lt;/code>——并发之下全局 &lt;code>seq&lt;/code> 本来就有空洞，完整性是每个决策自己的性质。&lt;/p>
&lt;p>校验不过，这个 decision 的证据就当不存在：放行器走&amp;quot;无审计&amp;quot;路径（人工或固定门禁），并把&amp;quot;无审计放行&amp;quot;本身记下来。沉默缺失被误读成通过，是审计存储最坏的失败方式，宁可显式降级。&lt;/p>
&lt;p>单机 SQLite 的丢失风险要诚实写进 v1 的边界：它没解决多副本，靠的是这张表很小（一次决策几 KB）、全量备份便宜、备份脚本可以拿校验函数自检。真正占空间的是 evidence 原文，那是另一套保留策略——审计表只存 &lt;code>evidence_ref&lt;/code> 和摘要，本来就不该跟着膨胀。&lt;/p>
&lt;h2 id="七我的判断">七、我的判断
&lt;/h2>&lt;ul>
&lt;li>v1 的合理成本上限是&lt;strong>一两周&lt;/strong>。做的过程中一旦发现要起独立服务、要改现有 CI 的控制流，说明做重了，回第一节的表对照。&lt;/li>
&lt;li>决策审计改变的不是风险本身，是漏报的&lt;strong>可发现性&lt;/strong>：没有它，&amp;ldquo;Agent 跳过了该跑的检查&amp;quot;只能在事故后追查；有了并行期，漏报候选在统计表里按周可见。&lt;/li>
&lt;li>反过来，如果 Agent 只提建议、放行始终由人或固定规则拍板，trace 加审批日志就够了。这套东西的成本，只在放行权真的开始转移时回本。&lt;/li>
&lt;/ul>
&lt;h2 id="速查落地顺序与每步的完成判据">速查：落地顺序与每步的完成判据
&lt;/h2>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>步骤&lt;/th>
&lt;th>交付物&lt;/th>
&lt;th>完成判据&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1. 建表&lt;/td>
&lt;td>DDL + 追加式触发器&lt;/td>
&lt;td>UPDATE / DELETE 被 ABORT&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2. 写入点&lt;/td>
&lt;td>三个钩子 + 幂等写入器&lt;/td>
&lt;td>同一事件重试只落一条&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3. 读取校验&lt;/td>
&lt;td>按 decision_id 的完整性函数&lt;/td>
&lt;td>缺头、缺尾、缺配对的序列被判无效&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4. 规则复核器&lt;/td>
&lt;td>四条确定性规则&lt;/td>
&lt;td>任一不过即 escalate，不进模型层&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5. 并行运行&lt;/td>
&lt;td>分歧四象限统计&lt;/td>
&lt;td>每个门禁的漏报/误报候选都有数&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6. 判据与降级&lt;/td>
&lt;td>写进试点协议的三个条件&lt;/td>
&lt;td>逐项降级，永不打包&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>一句话收尾：&lt;strong>先让它记录分歧，再让它参与放行——降级判据写清楚之前，决策审计只是一份更贵的日志。&lt;/strong>&lt;/p>
&lt;h2 id="延伸阅读">延伸阅读
&lt;/h2>&lt;ul>
&lt;li>&lt;a class="link" href="https://www.zata.cc/p/agent-decision-audit-and-tracing/" >Agent 决策审计：它与 Tracing 的关系&lt;/a>——设计篇：身份模型、记录字段与决策、Trace 的边界&lt;/li>
&lt;li>&lt;a class="link" href="https://www.zata.cc/p/agent-production-engineering-handbook/" >Agent 生产工程全景手册：从 Runtime 到业务闭环&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://www.zata.cc/p/agent-tracing-%E5%9F%BA%E7%A1%80trace-span-%E4%B8%8E-opentelemetry-%E5%9F%8B%E7%82%B9/" >Agent Tracing 基础：Trace、Span 与 OpenTelemetry 埋点&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Session、Thread、Run：一条消息为什么是一个 Run</title><link>https://www.zata.cc/p/session-thread-run/</link><pubDate>Wed, 23 Sep 2026 23:11:14 +0800</pubDate><guid>https://www.zata.cc/p/session-thread-run/</guid><description>&lt;img src="https://www.zata.cc/p/session-thread-run/images/index/index.svg" alt="Featured image of post Session、Thread、Run：一条消息为什么是一个 Run" />&lt;p>把 Run 表定义摊开看，有个细节很别扭：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">AgentRunModel&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Base&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Mapped&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">str&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1"># Run ID&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">thread_id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Mapped&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">str&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1"># 外键 -&amp;gt; conversation_thread，NOT NULL&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">session_id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Mapped&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">str&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1"># 外键 -&amp;gt; chat_session，允许为空&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agent_id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Mapped&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">str&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">status&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Mapped&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">str&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>一张表上挂了两个&amp;quot;会话类&amp;quot;外键，一个必填、一个可空。第一眼看上去像是设计没收敛——要么统一用 session，要么统一用 thread，为什么两个都要？&lt;/p>
&lt;p>真正解释这件事的，是那个&lt;strong>允许为空&lt;/strong>的字段。它存在的理由，不是一个空值占位，而是&amp;quot;一次执行可以不隶属于任何对话&amp;quot;。等子 Agent 出现时，这个空位会立刻被填满意义。&lt;/p>
&lt;h2 id="一三个名字三种事实">一、三个名字，三种事实
&lt;/h2>&lt;p>这三层不是同义词，也不是新旧替代关系，它们各自是一类问题的权威答案：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>层&lt;/th>
&lt;th>表&lt;/th>
&lt;th>谁看得见&lt;/th>
&lt;th>回答的问题&lt;/th>
&lt;th>生命周期&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Session&lt;/td>
&lt;td>&lt;code>chat_session&lt;/code>&lt;/td>
&lt;td>用户&lt;/td>
&lt;td>这是哪一段对话&lt;/td>
&lt;td>长期，可继续&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Thread&lt;/td>
&lt;td>&lt;code>conversation_thread&lt;/code>&lt;/td>
&lt;td>只有后端&lt;/td>
&lt;td>模型的连续记忆装在哪&lt;/td>
&lt;td>与对话同寿&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Run&lt;/td>
&lt;td>&lt;code>agent_run&lt;/code>&lt;/td>
&lt;td>用户（状态/取消）、管理员（审计）&lt;/td>
&lt;td>这是哪一次执行、用了什么、跑到哪&lt;/td>
&lt;td>单次，有终态&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Session 用户可见的一段对话：标题、owner、历史消息
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ Thread 模型的连续记忆（checkpointer 线程），前端不维护
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ Run 一次执行：快照 / 状态机 / 事件流 / 取消 / 计费
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;img src="https://www.zata.cc/p/session-thread-run/images/three-layers.svg"
loading="lazy"
alt="会话、记忆、执行三层身份"
>&lt;/p>
&lt;p>几处容易混的地方，一次说清：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Session 是产品概念&lt;/strong>。它管标题、归属、消息顺序；删掉一个 Session 意味着用户不再看到那段对话。&lt;/li>
&lt;li>&lt;strong>Thread 是实现细节&lt;/strong>。Session 到 Thread 的映射在后端完成，客户端不需要传也不需要存 &lt;code>thread_id&lt;/code>。它存在的唯一理由是：模型需要一个连续的上下文容器。&lt;/li>
&lt;li>&lt;strong>Run 是事实源&lt;/strong>。它不属于&amp;quot;对话&amp;quot;这个层级，而属于&amp;quot;执行&amp;quot;这个层级：一次输入落进去，出来的是带序号的事件流和一个终态。&lt;/li>
&lt;/ul>
&lt;p>所以那条别扭的表定义翻译过来是：&lt;strong>每次执行必须属于一个记忆线程，但不一定属于一段用户对话。&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>这里要跟另一套常见用法区分开：在审计与可观测性的语境里，&lt;code>run_id&lt;/code> 有时指&amp;quot;跨暂停、重试仍稳定的业务任务身份&amp;quot;，再用 &lt;code>attempt_id&lt;/code>、&lt;code>trace_id&lt;/code> 区分每一次尝试（可参考本系列的《Agent 决策审计：它与 Tracing 的关系》）。本文说的 Run 是&lt;strong>执行层&lt;/strong>的单位：单次、有终态、不可重开——想重跑，就是一次新的 Run。两套用法服务的问题不同，混用会在&amp;quot;重试算不算同一次&amp;quot;上直接打架。&lt;/p>
&lt;/blockquote>
&lt;h2 id="二一条消息就是一个-run">二、一条消息，就是一个 Run
&lt;/h2>&lt;p>用户视角里，一次交互是&amp;quot;在对话里发了一句话&amp;quot;。到 API 这一层，它变成一次 Run 创建：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-http" data-lang="http">&lt;span class="line">&lt;span class="cl">&lt;span class="err">POST /sessions/{session_id}/messages
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">→ 202 { &amp;#34;message_id&amp;#34;: &amp;#34;...&amp;#34;, &amp;#34;run_id&amp;#34;: &amp;#34;run_xxx&amp;#34;, &amp;#34;events_url&amp;#34;: &amp;#34;/api/agent-runs/run_xxx/events&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err"> &amp;#34;agent_id&amp;#34;: &amp;#34;...&amp;#34;, &amp;#34;agent_name&amp;#34;: &amp;#34;...&amp;#34; }
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>返回体里同时出现 &lt;code>session_id&lt;/code>（在 URL 上）和 &lt;code>run_id&lt;/code>（在响应里），这不是冗余，而是两个层级各自交回自己的凭据：前者继续指向那段对话，后者指向刚刚开始的这次执行——订阅事件、取消、查状态，用的全是它。&lt;/p>
&lt;p>历史消息也保留了这层关系：assistant 消息上挂一个唯一的 &lt;code>run_id&lt;/code> 回指生成它的那次执行。这样当用户在同一个对话里换了 Agent，回看历史时依然能说出&amp;quot;这条回答当时是哪个 Agent 给的&amp;quot;，而不是拿当前绑定去反推过去。&lt;/p>
&lt;p>而 Session 上的 &lt;code>agent_id&lt;/code> 只是&lt;strong>初始默认值&lt;/strong>：同一段对话的后续每条消息都可以逐次切换 Agent，切换只影响下一次发送。这一点是后面所有讨论的前提——如果 Session 就等于 Run，这条规则根本无法表达。&lt;/p>
&lt;h2 id="三为什么不能一个-session-一个-run-id">三、为什么不能&amp;quot;一个 Session 一个 Run ID&amp;quot;
&lt;/h2>&lt;p>合并成一层听起来更简洁，但会立刻遇到四个对不上的地方：&lt;/p>
&lt;p>&lt;strong>1. 快照必须逐次冻结。&lt;/strong> 每个 Run 在创建事务里固化两份不可变快照：Agent 执行快照和资料上下文快照。它们记录的是&amp;quot;这一次用的版本&amp;quot;，而不是&amp;quot;这段对话现在用什么&amp;quot;。会话级 ID 没有位置安放&amp;quot;逐次&amp;quot;这个语义。&lt;/p>
&lt;p>&lt;strong>2. 终态是执行的属性，不是对话的。&lt;/strong> Run 的状态机有唯一终态且不可重开：&lt;code>succeeded&lt;/code>、&lt;code>failed&lt;/code>、&lt;code>cancelled&lt;/code>、&lt;code>interrupted&lt;/code>。一次执行失败，不应该把整段还能继续聊的对话标记为失败。&lt;/p>
&lt;p>&lt;strong>3. 幂等键挂在 Run 上。&lt;/strong> 创建 Run 必须带 &lt;code>Idempotency-Key&lt;/code>，唯一约束是 &lt;code>(owner_id, idempotency_key_hash)&lt;/code>。同一用户、同一个 key、同一份请求会重放原来那次 Run；同样的 key 配不同请求则稳定返回 &lt;code>idempotency_conflict&lt;/code>。重放的单位是&amp;quot;那一次执行&amp;quot;，不是&amp;quot;那段对话&amp;quot;。&lt;/p>
&lt;p>&lt;strong>4. 取消、租约、事件游标都是 per-Run 的。&lt;/strong> &lt;code>GET /api/agent-runs/{run_id}/events?after_seq=N&lt;/code> 的游标是 Run 内的序号；执行租约（lease）也写在 Run 上。刷新续流用 &lt;code>Last-Event-ID&lt;/code> 接上原来那条流——如果一段对话只有一条流，换 Agent、重试、取消全都会糊在同一根管道里。&lt;/p>
&lt;p>换个角度看得更清楚。挂在 Run 上的这些字段，每一条都在说&amp;quot;这是单次执行的属性&amp;quot;：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>事实&lt;/th>
&lt;th>字段&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>用哪个 Agent、什么版本&lt;/td>
&lt;td>&lt;code>agent_id&lt;/code>、&lt;code>agent_execution_snapshot&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>看的哪些资料&lt;/td>
&lt;td>&lt;code>context_pack_snapshot&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>跑到哪一步&lt;/td>
&lt;td>&lt;code>status&lt;/code>、&lt;code>last_event_seq&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>谁在跑&lt;/td>
&lt;td>&lt;code>executor_instance_id&lt;/code>、&lt;code>lease_expires_at&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>能不能重放&lt;/td>
&lt;td>&lt;code>idempotency_key_hash&lt;/code>、&lt;code>request_checksum&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>结果与失败&lt;/td>
&lt;td>&lt;code>final_message_id&lt;/code>、&lt;code>error&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>时间线&lt;/td>
&lt;td>&lt;code>created_at&lt;/code> / &lt;code>started_at&lt;/code> / &lt;code>finished_at&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="四run-的状态机把事实和活性分开">四、Run 的状态机：把&amp;quot;事实&amp;quot;和&amp;quot;活性&amp;quot;分开
&lt;/h2>&lt;p>既然执行是单位，状态就必须说准。非终态有三个，终态有四个：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">queued ─→ running ─→ succeeded
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ └→ failed
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ├─────→ cancelled
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─────────┴─────→ interrupted
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> cancelling 是 running 到 cancelled 之间的过渡态
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>三条容易踩的语义：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>租约不是历史事实。&lt;/strong> &lt;code>lease_expires_at&lt;/code> 只表示&amp;quot;执行者还活着&amp;quot;，它过期不代表这次执行失败了。失联的 Run 由 reconciliation 追加 &lt;code>run.interrupted&lt;/code> 收口——而且&lt;strong>不会自动重跑模型&lt;/strong>。想重跑，是人的决定，是一次新的 Run。&lt;/li>
&lt;li>&lt;strong>取消只追加一次终态。&lt;/strong> 用户取消先写 &lt;code>run.cancelling&lt;/code>，运行中的执行器收到信号后回收当前图或模型流，恰好追加一次 &lt;code>run.cancelled&lt;/code>（带 &lt;code>external_stop_confirmed: true&lt;/code>）。确认取消之后，不会再出现 &lt;code>run.completed&lt;/code> 或 &lt;code>run.failed&lt;/code>。&lt;/li>
&lt;li>&lt;strong>终态不可重开。&lt;/strong> 一个 Run 进入终态，它的故事就结束了。任何&amp;quot;再来一次&amp;quot;都是新 Run、新快照、新的幂等键。&lt;/li>
&lt;/ul>
&lt;p>这套规则的价值不在单 Agent 场景——那里一条流从头读到尾就够了。它的价值在于：&lt;strong>当你需要并行、需要重试、需要给一部分工作单独踩刹车时，你必须有一个不可重开的最小单位。&lt;/strong>&lt;/p>
&lt;h2 id="五委派那天这个空字段被填满了">五、委派那天，这个空字段被填满了
&lt;/h2>&lt;p>现在回到开头那个可空的 &lt;code>session_id&lt;/code>。&lt;/p>
&lt;p>子 Agent 委派落地后，父 Run 在执行过程中会请求平台创建 child Run。child 是&lt;strong>另一次真实执行&lt;/strong>：目标 Agent 不同、快照不同、独立线程、独立事件流、独立状态与独立取消入口。它满足&amp;quot;执行&amp;quot;的全部定义，但不隶属于用户的那段对话——用户没有在对话里发过那句话。&lt;/p>
&lt;p>所以 child 的 &lt;code>session_id&lt;/code> 就是 &lt;code>NULL&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">root Run session_id = sess_xxx 用户可见，出现在会话历史
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├─ child A session_id = NULL 内部执行，只有树里能看到
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─ child B session_id = NULL
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>父子关系不表达在 Session 上，而是靠 Run 表自己的两个自引用字段：&lt;code>root_run_id&lt;/code> 指向所属的根执行，&lt;code>parent_run_id&lt;/code> 指向直接父节点。运行树因此可以从持久化记录里重建：刷新页面后树还在，服务重启后未完成节点统一收敛为 &lt;code>interrupted&lt;/code>。&lt;/p>
&lt;p>对前端而言，这个分层直接决定了工作量：原来&amp;quot;发一次消息 = 拿到一个 run_id = 连一条 &lt;code>events_url&lt;/code>&amp;ldquo;的假设，要变成&amp;quot;先读树，再按每个节点的 &lt;code>events_url&lt;/code> 各自订阅&amp;rdquo;。一条流变多条流，正是因为一次对话里现在真的有了多次执行。&lt;/p>
&lt;p>反过来验证一下：如果当初把 Run 合并进 Session，委派就得在 Session 上开洞——一段对话要同时容纳多个 Agent、多份快照、多个终态、多个取消目标。这个洞会一直开到把 Session 拆回去为止。&lt;/p>
&lt;h2 id="几点收获">几点收获
&lt;/h2>&lt;ul>
&lt;li>&lt;strong>ID 的层级应该跟生命周期对齐，而不是跟界面层级对齐。&lt;/strong> 用户看到的是对话，系统需要的是执行；把两者合成一个 ID，短期少一个字段，长期处处要打补丁。&lt;/li>
&lt;li>&lt;strong>允许为空的外键往往在讲一个未来的故事。&lt;/strong> &lt;code>session_id&lt;/code> 可空不是随手留的，它提前承认了&amp;quot;执行可以不属于任何对话&amp;quot;这类存在。&lt;/li>
&lt;li>&lt;strong>状态机要区分事实与活性。&lt;/strong> 心跳、租约、连接状态说明的是&amp;quot;谁还在跑&amp;quot;；状态、事件流说明的是&amp;quot;发生了什么&amp;quot;。把两者混在一起，就会出现&amp;quot;进程没了所以这次执行失败了&amp;quot;这种错误结论。&lt;/li>
&lt;li>&lt;strong>幂等、取消、游标、计费都需要一个不可重开的最小单位。&lt;/strong> 它们四个的需求指向同一个答案：Run。先把这层定清楚，后面加并行、加委派、加预算，都是在这个单位上做加法。&lt;/li>
&lt;/ul>
&lt;p>回到最开始那张表：两个外键不是没收敛的设计，而是两层身份各自在场的证据。一个必填，因为执行总需要记忆；一个可空，因为执行不一定需要观众。&lt;/p></description></item><item><title>Agent 决策审计：它与 Tracing 的关系</title><link>https://www.zata.cc/p/agent-decision-audit-and-tracing/</link><pubDate>Tue, 22 Sep 2026 11:30:00 +0800</pubDate><guid>https://www.zata.cc/p/agent-decision-audit-and-tracing/</guid><description>&lt;img src="https://www.zata.cc/p/agent-decision-audit-and-tracing/images/index/index.svg" alt="Featured image of post Agent 决策审计：它与 Tracing 的关系" />&lt;p>假设一个代码 Agent 改了数据库连接配置。它跑过单元测试，然后决定跳过真实数据库验证，给出“可以合并”的结论。几天后线上出现连接池故障。此时仅看到一条 &lt;code>tool.pytest succeeded&lt;/code> 的 trace，并不能回答关键问题：&lt;strong>它当时看到了哪些改动，为什么认为真实数据库验证可以跳过？&lt;/strong>&lt;/p>
&lt;p>这正是决策审计（decision audit）要解决的问题。它与 Agent tracing 可以出现在同一条运行时间线上，但两者回答的问题不同：&lt;strong>tracing 还原执行过程；决策审计保存判断依据与责任边界。&lt;/strong> 当 Agent 的判断会影响放行、权限或资源使用时，后者就不能只是几行自由文本日志。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/agent-decision-audit-and-tracing/images/decision-and-trace.svg"
loading="lazy"
alt="一次任务中的执行轨迹与决策记录"
>&lt;/p>
&lt;h2 id="一先把四种记录分清">一、先把四种记录分清
&lt;/h2>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>记录&lt;/th>
&lt;th>核心问题&lt;/th>
&lt;th>典型内容&lt;/th>
&lt;th>常见用途&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Trace&lt;/td>
&lt;td>经过了哪些步骤，耗时和错误在哪里？&lt;/td>
&lt;td>Agent、模型、工具、命令的 span 与父子关系&lt;/td>
&lt;td>排障、性能分析&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Log&lt;/td>
&lt;td>某一刻发生了什么？&lt;/td>
&lt;td>命令退出码、异常、状态变化的文本或结构化事件&lt;/td>
&lt;td>定位具体故障&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Decision record&lt;/td>
&lt;td>基于哪些输入作了什么判断？&lt;/td>
&lt;td>风险、候选项、选择/跳过及理由、决策版本&lt;/td>
&lt;td>复核判断&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Evidence&lt;/td>
&lt;td>判断是否得到事实支持？&lt;/td>
&lt;td>命令结果、测试报告、截图、审查报告及其摘要&lt;/td>
&lt;td>验收与复验&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>一份 trace 完全可以包含 &lt;code>validation.plan&lt;/code>、&lt;code>validation.execute&lt;/code> 之类的 span，也可以链接决策记录。但&lt;strong>把 span 存下来，不等于完成审计&lt;/strong>：span 常为排障服务，可能被采样、截断或按较短期限清理；审计记录则要明确必填字段、留存策略、版本关联与完整性状态。反过来，只有决策表而没有 trace，也很难诊断工具超时、重试和上下文断裂。&lt;/p>
&lt;p>这里的“审计”不是声称记录模型的全部内心推理。我们能记录和复核的是&lt;strong>可观察的输入、显式给出的理由、执行器实际取得的结果，以及最终授权动作&lt;/strong>。自由文本解释可以帮助人理解，但不能替代结果和证据。&lt;/p>
&lt;h2 id="二决策审计是-tracing-的一部分吗">二、决策审计是 tracing 的一部分吗？
&lt;/h2>&lt;p>从&lt;strong>用户界面&lt;/strong>看，可以是一部分：点开一次 Agent run，沿着时间线看到“分析改动 → 制订验证计划 → 执行 → verifier 复核 → 放行”。从&lt;strong>数据责任&lt;/strong>看，最好是独立记录，再通过标识关联。它们可以共用一次任务的 &lt;code>run_id&lt;/code>，而 &lt;code>trace_id&lt;/code> 指向这一轮执行的诊断轨迹。&lt;/p>
&lt;p>需要特别区分 &lt;code>run_id&lt;/code> 与 &lt;code>trace_id&lt;/code>。一次长期任务可能暂停、恢复或重试，仍属于同一个业务 run，却产生多个 trace；一次 trace 也可能只覆盖其中一段执行。因此审计主键应围绕业务对象与代码版本设计，不能把“当前 trace 恰好存在”当作放行条件。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">PRD / Issue / change
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ run_id：业务任务身份
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├─ decision_id：一次验证计划或放行判断
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├─ attempt_id：一次执行尝试
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ trace_id：该次尝试的诊断轨迹
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>一种实用做法是：&lt;strong>审计记录保留最小且稳定的事实，trace 保留丰富的执行细节&lt;/strong>。审计记录可以引用 trace；trace 丢失或过期后，仍能知道当时基于哪个代码版本、采用了哪些检查、结果如何。若审计存储本身也可能故障，必须显式标记 &lt;code>history_complete=false&lt;/code>，不能默默显示“通过”。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/agent-decision-audit-and-tracing/images/identity-model.svg"
loading="lazy"
alt="四种标识的关联关系"
>&lt;/p>
&lt;h2 id="三一条可复核的决策记录长什么样">三、一条可复核的决策记录长什么样
&lt;/h2>&lt;p>以“Agent 自主选择验证项”为例，至少要分开记录&lt;strong>计划、执行与裁决&lt;/strong>。这是本文最重要的边界：Agent 写下“我将运行测试”，不代表测试真的运行过。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;schema_version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;decision_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;dec-42&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;run_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;run-17&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;attempt_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;attempt-2&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;source_revision&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;git-tree-sha-or-worktree-fingerprint&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;policy_version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;validation-policy-v3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;actor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;validation-planner&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;risk_facts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;changed: database pool configuration&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;checks&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;check_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;unit-tests&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;disposition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;selected&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;reason&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;配置解析路径已变更&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;check_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;real-db-smoke&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;disposition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;selected&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;reason&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;连接池行为依赖真实数据库&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;created_at&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2026-09-22T03:30:00Z&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这条记录只是&lt;strong>计划&lt;/strong>。执行器随后追加每项检查的 &lt;code>started&lt;/code>、&lt;code>passed&lt;/code>、&lt;code>failed&lt;/code>、&lt;code>timed_out&lt;/code> 或 &lt;code>unavailable&lt;/code> 结果，并附命令摘要、退出码、环境说明、产物引用和摘要值。verifier 再引用同一个 &lt;code>decision_id&lt;/code>，说明它是否认可计划覆盖面和实际证据。最终放行记录引用 verifier 结论及代码版本，避免“检查的是 A，合并的是 B”。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>字段&lt;/th>
&lt;th>为什么需要&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>source_revision&lt;/code>&lt;/td>
&lt;td>防止旧证据给新代码放行；未提交改动也需要工作区指纹&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>policy_version&lt;/code>&lt;/td>
&lt;td>以后规则变化时，还能解释当时采用哪套标准&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>disposition&lt;/code> 与 &lt;code>reason&lt;/code>&lt;/td>
&lt;td>把跳过项显式化，便于统计误判与复核&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>result&lt;/code> 与 &lt;code>evidence_ref&lt;/code>&lt;/td>
&lt;td>区分 Agent 承诺和执行器观察到的事实&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>actor&lt;/code>&lt;/td>
&lt;td>区分 planner、执行器、verifier 和人工签核&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>history_complete&lt;/code>&lt;/td>
&lt;td>告诉读者记录缺失，避免把沉默误读为成功&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>原始 prompt、源码、密钥和完整终端输出不应默认写进长期审计表。可以将原始产物放入受控存储，审计表只保留路径、摘要、脱敏摘要和访问范围。**哈希能证明后来查看的是同一份产物，但不能证明产物当初真实或结论正确。**真实性仍需靠可信执行器、环境记录和必要时的复跑。&lt;/p>
&lt;h2 id="四它怎样帮助替代固定门禁">四、它怎样帮助替代固定门禁
&lt;/h2>&lt;p>固定门禁适合确定、便宜、误报低且失误代价高的规则，比如迁移链完整性或禁止提交密钥。Agent 适合根据改动上下文决定验证深度，例如是否需要浏览器真实入口、真实数据库或特定回归场景。决策审计让后者的自由度能够被复核。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/agent-decision-audit-and-tracing/images/validation-loop.svg"
loading="lazy"
alt="验证计划、执行与复核的闭环"
>&lt;/p>
&lt;p>可落地的执行顺序是：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>识别风险&lt;/strong>：从 diff、任务要求、依赖边界和历史故障提取具体事实，而不是只给“高/中/低”标签。&lt;/li>
&lt;li>&lt;strong>列出候选检查&lt;/strong>：同时记录选择与跳过；跳过需说明前提，例如“仅改文案，没有可执行行为变化”。&lt;/li>
&lt;li>&lt;strong>执行并留证&lt;/strong>：由执行器记录实际命令、环境、退出状态和产物；失败、超时、无凭据分别处理。&lt;/li>
&lt;li>&lt;strong>独立复核&lt;/strong>：verifier 检查计划是否漏掉关键边界，以及证据是否对应当前版本。它的意见也应作为一条新记录，而不是覆盖原计划。&lt;/li>
&lt;li>&lt;strong>形成最终裁决&lt;/strong>：只有与当前代码版本匹配、记录完整且满足必要硬约束的结果，才能支持自动放行；其余情况升级给人。&lt;/li>
&lt;/ol>
&lt;p>这不是要求每次都运行最昂贵的验证。它要求&lt;strong>验证深度的取舍有可见依据&lt;/strong>。例如前端仅调整按钮文案，可以跳过端到端流程，但要说明未改交互或请求结果；修改 Dialog、Portal 或跨页流程时，单独的组件截图就不足以证明真实入口可用。&lt;/p>
&lt;h3 id="一个常见失败结果是绿的判断仍然错">一个常见失败：结果是绿的，判断仍然错
&lt;/h3>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>症状&lt;/th>
&lt;th>原因&lt;/th>
&lt;th>修复&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>单元测试通过，线上连接池仍故障&lt;/td>
&lt;td>计划漏了真实数据库边界&lt;/td>
&lt;td>verifier 审查“覆盖了哪些风险”，而不只看退出码&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>证据文件存在，实际测试没跑完&lt;/td>
&lt;td>Agent 把计划或旧文件当结果&lt;/td>
&lt;td>执行器写入退出码、时间与产物摘要；绑定代码版本&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>verifier 判绿，但新提交改变了代码&lt;/td>
&lt;td>裁决没有绑定修订版本&lt;/td>
&lt;td>代码版本变化时使旧裁决失效并重新评估&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="五从最小实现开始不必先建平台">五、从最小实现开始，不必先建平台
&lt;/h2>&lt;p>已有 runner、日志文件和生命周期账本的项目，可以先增加一张追加式 &lt;code>decision_events&lt;/code> 表或等价的 JSONL 存储：&lt;code>decision_id&lt;/code>、&lt;code>run_id&lt;/code>、&lt;code>attempt_id&lt;/code>、&lt;code>event_type&lt;/code>、&lt;code>actor&lt;/code>、&lt;code>source_revision&lt;/code>、&lt;code>occurred_at&lt;/code>、&lt;code>detail&lt;/code>。事件类型先控制在 &lt;code>plan_created&lt;/code>、&lt;code>check_started&lt;/code>、&lt;code>check_finished&lt;/code>、&lt;code>verdict_issued&lt;/code>、&lt;code>decision_invalidated&lt;/code>。原始输出继续落文件，通过 &lt;code>evidence_ref&lt;/code> 关联。&lt;/p>
&lt;p>第一阶段让 Agent 决策与现有门禁&lt;strong>并行运行&lt;/strong>，只记录分歧，不自动改变放行。挑选常误拦、执行成本高的门禁，统计它拦住过哪些真实问题、Agent 漏掉了哪些检查、verifier 能否识别。只有在这些反例上表现稳定，才逐项把固定门禁降为提示。否则只是把可见的脚本误报，换成不易发现的判断漏报。&lt;/p>
&lt;p>这个方案也有边界。独立 verifier 可能与 planner 共享同一种盲点；本机 SQLite 可能丢失；外部服务验证可能受凭据和环境限制。审计不能神奇地消除这些风险，它的价值是把&lt;strong>依据、缺口与责任&lt;/strong>留在可复核的位置，让后续改进有真实样本。&lt;/p>
&lt;h2 id="速查遇到一个问题该看哪里">速查：遇到一个问题该看哪里
&lt;/h2>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>你要问的问题&lt;/th>
&lt;th>先看&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>为什么这次 run 很慢？&lt;/td>
&lt;td>Trace 的 span 树与耗时&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>命令究竟报了什么错？&lt;/td>
&lt;td>关联 &lt;code>trace_id&lt;/code> 的日志及原始产物&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>为什么跳过真实入口验证？&lt;/td>
&lt;td>决策记录中的候选项、理由和政策版本&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>跳过是否合理？&lt;/td>
&lt;td>风险事实、任务要求、verifier 结论&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>测试通过是否能支持当前合并？&lt;/td>
&lt;td>执行结果、证据引用、代码版本与最终裁决&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>我的判断是：&lt;strong>决策审计可以嵌进 Agent tracing 的浏览体验，但应有独立的数据契约和留存责任。&lt;/strong> 当 Agent 只是辅助写代码，trace 通常足以排障；当 Agent 开始决定“哪些门禁可以不跑、这次是否放行”，决策记录就成为运行时的必要组成部分。&lt;/p>
&lt;p>继续阅读：&lt;a class="link" href="https://www.zata.cc/p/agent-decision-audit-implementation/" >Agent 决策审计落地：写入点、复核器与门禁降级判据&lt;/a>——本文的落地实现篇；&lt;a class="link" href="https://www.zata.cc/p/agent-tracing-%E5%9F%BA%E7%A1%80trace-span-%E4%B8%8E-opentelemetry-%E5%9F%8B%E7%82%B9/" >Agent Tracing 基础：Trace、Span 与 OpenTelemetry 埋点&lt;/a>；&lt;a class="link" href="https://www.zata.cc/p/agent-runtime-explained/" >Agent Runtime 详解&lt;/a>。&lt;/p></description></item><item><title>Agent Tracing 基础：Trace、Span 与 OpenTelemetry 埋点</title><link>https://www.zata.cc/p/agent-tracing-%E5%9F%BA%E7%A1%80trace-span-%E4%B8%8E-opentelemetry-%E5%9F%8B%E7%82%B9/</link><pubDate>Tue, 22 Sep 2026 09:51:00 +0800</pubDate><guid>https://www.zata.cc/p/agent-tracing-%E5%9F%BA%E7%A1%80trace-span-%E4%B8%8E-opentelemetry-%E5%9F%8B%E7%82%B9/</guid><description>&lt;img src="https://www.zata.cc/p/agent-tracing-%E5%9F%BA%E7%A1%80trace-span-%E4%B8%8E-opentelemetry-%E5%9F%8B%E7%82%B9/images/index/index.svg" alt="Featured image of post Agent Tracing 基础：Trace、Span 与 OpenTelemetry 埋点" />&lt;p>用户说：这个 Agent 回答一个问题要 40 秒。&lt;/p>
&lt;p>我去 grep 日志，符合条件的只有一行：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">2026-09-20 14:02:11 INFO run finished run_id=run-7f3a elapsed=41.3s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>41.3 秒，和用户说的对得上。然后呢？没有然后了——&lt;strong>日志告诉你&amp;quot;慢&amp;quot;，但告诉不了&amp;quot;慢在哪一步&amp;quot;&lt;/strong>。&lt;/p>
&lt;p>于是我加 print：在每轮 LLM 调用前后打一行，在每次工具调用前后打一行。半小时后日志变成这样：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">14:01:30 LLM #1 start
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">14:01:37 LLM #1 done
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">14:01:37 tool web_search start
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">14:02:01 tool web_search done
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">14:02:01 LLM #2 start
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">14:02:16 LLM #2 done
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>能看了，但问题也很明显：这些行&lt;strong>彼此之间没有关系&lt;/strong>。换个用户并发进来，两组行就交织成一片；想知道&amp;quot;哪一步占了这 41 秒&amp;quot;，还得靠人肉减时间戳。更别说线上没有 print，只有一条 &lt;code>run finished&lt;/code>。&lt;/p>
&lt;p>这篇文章要解决的，就是把这个&amp;quot;人肉减时间戳&amp;quot;的过程，换成一套有结构、能自动关联的数据。这正是 &lt;strong>Tracing&lt;/strong> 干的事。&lt;/p>
&lt;h2 id="一trace-与-span一次-agent-run-就是一棵树">一、Trace 与 Span：一次 Agent Run 就是一棵树
&lt;/h2>&lt;p>先建立两个最基础的概念，用上面那个 run 当例子。&lt;/p>
&lt;p>&lt;strong>Trace（链路）&lt;/strong>：一次完整请求的全过程，有一个全局唯一的 &lt;code>trace_id&lt;/code>。上面这次调用对应一条 trace。&lt;/p>
&lt;p>&lt;strong>Span（跨度）&lt;/strong>：trace 里的一段工作，有名字、有开始时间、有结束时间，可以有属性和事件。上面每一行 print，本质上都是一个 span。&lt;/p>
&lt;p>Span 之间靠 &lt;strong>父子关系&lt;/strong> 组织成树。上面那次 run 画成 span 树是这样的：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">agent.run run.id=run-7f3a session.id=s-12 [t=0.0 → 41.3s]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├─ gen_ai.chat 第 1 轮 gen_ai.request.model=某模型 [t=0.2 → 6.8s]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├─ tool.web_search tool.name=web_search [t=7.0 → 24.1s] ← 元凶
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├─ gen_ai.chat 第 2 轮 gen_ai.usage.input_tokens=4821 [t=24.3 → 39.6s]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─ agent.finalize [t=39.7 → 41.2s]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>树的形状一出来，答案就自己浮上来了：&lt;strong>24.1 秒花在 &lt;code>web_search&lt;/code> 上，占掉一半以上&lt;/strong>。不需要减时间戳，不需要猜。&lt;/p>
&lt;p>这里有三点值得单独说清楚，因为后面所有的坑都出在这三件事上。&lt;/p>
&lt;p>&lt;strong>第一，父子关系是&amp;quot;包含&amp;quot;关系，不是&amp;quot;先后&amp;quot;关系。&lt;/strong> 子 span 的时间区间落在父 span 区间内。父 span 的耗时不是子 span 的加总——&lt;code>agent.run&lt;/code> 是 41.3 秒，四个子项加起来也差不多，但如果两个子 span 时间重叠，说明它们在并行，加总就会超过父 span。&lt;strong>时间重叠 = 并发&lt;/strong>，这是 span 树比日志强的最直观的一点。&lt;/p>
&lt;p>&lt;strong>第二，根 span 只有一个。&lt;/strong> 一次 run 对应一个 &lt;code>agent.run&lt;/code> span（root span），其余都是它的后代。&lt;code>trace_id&lt;/code> 相同、&lt;code>parent_span_id&lt;/code> 串起来，就构成了这次 run 的完整路径。排查时从 root 往下看，就是一条推理链。&lt;/p>
&lt;p>&lt;strong>第三，span 可以跨进程。&lt;/strong> 一次 Agent Run 里，&lt;code>tool.web_search&lt;/code> 很可能不是本地函数，而是打到一个远程服务甚至别人的 MCP Server 上。只要上下文传得过去（第五节讲），那边产生的 span 会挂到你这棵树上，变成你 trace 的一部分。&lt;/p>
&lt;h2 id="二属性与事件span-上该记什么">二、属性与事件：span 上该记什么
&lt;/h2>&lt;p>有了 tree 骨架，还得有内容。span 上能挂两类东西：&lt;strong>属性（Attributes）&lt;/strong> 和 &lt;strong>事件（Events）&lt;/strong>。这两个词经常被混用，但分工很清楚。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>属性 Attributes&lt;/th>
&lt;th>事件 Events&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>形态&lt;/td>
&lt;td>键值对，附着在 span 上&lt;/td>
&lt;td>带时间戳的独立记录&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>回答的问题&lt;/td>
&lt;td>这个 span &lt;strong>是什么&lt;/strong>&lt;/td>
&lt;td>这个 span &lt;strong>过程中发生了什么&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>典型时机&lt;/td>
&lt;td>span 存续期间的任何时刻&lt;/td>
&lt;td>某个确定的时间点&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>主要用途&lt;/td>
&lt;td>筛选、分组、聚合&lt;/td>
&lt;td>还原时间线、记录离散动作&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Agent 里的例子&lt;/td>
&lt;td>&lt;code>gen_ai.request.model&lt;/code>、&lt;code>gen_ai.usage.output_tokens&lt;/code>、&lt;code>tool.name&lt;/code>&lt;/td>
&lt;td>重试一次、首个 token 到达、工具返回、抛异常&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>一个够用的判断口诀：&lt;strong>能被拿来当筛选条件的，放属性；只在某一刻发生、要看&amp;quot;什么时候&amp;quot;的，放事件。&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&amp;ldquo;所有用了 A 模型的 run 平均耗时多少&amp;rdquo; → 模型名必须是属性（要聚合)。&lt;/li>
&lt;li>&amp;ldquo;这次 run 重试了 3 次，每次间隔多久&amp;rdquo; → 重试是事件（要看时刻）。&lt;/li>
&lt;/ul>
&lt;p>在 Agent 语境里，属性这块已经有一份现成的答案：&lt;strong>OpenTelemetry 的 GenAI 语义约定&lt;/strong>（&lt;code>gen_ai.*&lt;/code>）。模型名、token 用量、工具名、会话 id 该叫什么，规范里都钉死了，照着填就能被各种观测后端正确解析。这份约定的字段清单和几个已知的坑，我在 &lt;a class="link" href="https://www.zata.cc/p/agent-%E5%9F%8B%E7%82%B9%E6%8E%A5-arms%E4%B8%8A%E6%8A%A5%E8%BF%94%E5%9B%9E-success%E6%8E%A7%E5%88%B6%E5%8F%B0%E5%8D%B4%E6%98%AF%E7%A9%BA%E7%9A%84/" >Agent 埋点接 ARMS&lt;/a> 里已经逐条整理过，这里只补一条最容易踩的边界：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>属性会被当成聚合维度，所以基数要有意识。&lt;/strong> &lt;code>run.id&lt;/code>、&lt;code>session.id&lt;/code> 这种每次都不一样的值，挂在 span 上当属性没问题（trace 是按 &lt;code>trace_id&lt;/code> 检索的，不存在&amp;quot;按属性建索引&amp;quot;的压力）；但如果这些属性被顺手套进了 Metrics 的维度里，一张指标卡片就会被炸成几万条时间线。分界线不是&amp;quot;能不能当属性&amp;quot;，而是&amp;quot;这个字段会不会流进指标聚合&amp;quot;。同样地，&lt;code>gen_ai.input.messages&lt;/code> 这类可能含用户隐私的字段，规范明确标成了 Opt-In，不该默认记录。&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>事件这块有一件新变化，值得单独提醒。&lt;/strong> OTel 在 &lt;strong>2026 年 3 月&lt;/strong>宣布弃用 &lt;strong>Span Event API&lt;/strong>（也就是 &lt;code>Span.AddEvent&lt;/code> / &lt;code>Span.RecordException&lt;/code>），原因是&amp;quot;span 事件&amp;quot;和&amp;quot;日志事件&amp;quot;两套并行的机制造成了重复和困惑。新的事件应该走 &lt;strong>Logs API&lt;/strong>，通过上下文与当前 span 关联。需要注意边界：&lt;/p>
&lt;ul>
&lt;li>这是&lt;strong>弃用 API，不是删除能力&lt;/strong>。&lt;code>add_event()&lt;/code> 现在还能用，存量数据和在 trace 视图里看事件也照常工作。&lt;/li>
&lt;li>建议是：新写的埋点别再加对 &lt;code>add_event()&lt;/code> 的新依赖；自己封装的异常上报，优先走日志库 + OTel 日志桥接，它会自动带上 &lt;code>trace_id&lt;/code> / &lt;code>span_id&lt;/code>。&lt;/li>
&lt;/ul>
&lt;p>如果你现在就在写埋点，实操上可以这么记：&lt;strong>属性照旧填在 span 上；&amp;ldquo;某一刻发生了什么&amp;quot;优先记一条结构化日志&lt;/strong>——既符合新方向，也天然可搜索。&lt;/p>
&lt;h2 id="三tracesmetricslogs三根柱子各自回答一个问题">三、Traces、Metrics、Logs：三根柱子各自回答一个问题
&lt;/h2>&lt;p>到这可能会有一个误解：既然 tracing 这么好，是不是日志和指标就不需要了？&lt;/p>
&lt;p>不是。它们是三种不同粒度的问题，互相不能替代：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>信号&lt;/th>
&lt;th>回答&lt;/th>
&lt;th>Agent 里的典型用法&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Metrics&lt;/strong>&lt;/td>
&lt;td>&amp;ldquo;&lt;strong>多少 / 趋势&lt;/strong>&amp;rdquo;&lt;/td>
&lt;td>成功率、P95 延迟、token 消耗与成本、工具错误率&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Traces&lt;/strong>&lt;/td>
&lt;td>&amp;ldquo;&lt;strong>哪里 / 路径&lt;/strong>&amp;rdquo;&lt;/td>
&lt;td>单次 run 的完整执行路径、哪一步慢、哪一步失败&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Logs&lt;/strong>&lt;/td>
&lt;td>&amp;ldquo;&lt;strong>具体是什么&lt;/strong>&amp;rdquo;&lt;/td>
&lt;td>prompt/response 全文、工具入参出参、异常堆栈&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>换个说法：&lt;strong>指标告诉你&amp;quot;出问题了&amp;rdquo;，trace 告诉你&amp;quot;出在哪个环节&amp;quot;，日志告诉你&amp;quot;那个环节具体发生了什么&amp;quot;。&lt;/strong>&lt;/p>
&lt;p>三者真正的威力在&lt;strong>关联&lt;/strong>。Agent 场景里最常见的一条排查路径长这样：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">① 指标：今天 token 消耗比昨天涨了 40% ← Metrics 发现异常
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">② 点开这张图的某个时间点 ← 靠 exemplar 带出 trace_id
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">③ 落到那一次 run 的 span 树 ← 看到工具被循环调用了 12 次
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">④ 跳到对应 span 的日志 ← 看到检索关键词空字符串，导致反复搜
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这条链要成立，靠的是一个共同的锚点：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>&lt;code>trace_id&lt;/code> 是把三根柱子缝在一起的线。&lt;/strong> 指标里带 exemplar 引用 trace，日志记录里带 &lt;code>trace_id&lt;/code>/&lt;code>span_id&lt;/code>，trace 上挂结构化的 usage 属性——三条路径都指向同一根线，你才能从&amp;quot;指标异常&amp;quot;一路点到&amp;quot;具体那次请求的日志&amp;quot;。&lt;/p>
&lt;/blockquote>
&lt;p>反过来说，如果日志里没有 &lt;code>trace_id&lt;/code>，这套关联就断了，你又要回到&amp;quot;人肉 grep + 减时间戳&amp;quot;。&lt;/p>
&lt;h2 id="四opentelemetry-埋点从-api-到-span-树">四、OpenTelemetry 埋点：从 API 到 span 树
&lt;/h2>&lt;p>OTel 的埋点 API 其实只有三层，理解了这个层次就不会写乱：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">TracerProvider 一个进程一个，负责&amp;#34;把 span 送去哪里&amp;#34;（exporter / processor）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ Tracer 按模块命名，比如 &amp;#34;agent.runtime&amp;#34;、&amp;#34;agent.tools&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ Span 一次具体操作，就是树上的一个节点
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最小可运行的接线（&lt;code>opentelemetry-sdk 1.44.0&lt;/code>，2026-09 的版本）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">opentelemetry&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">trace&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">opentelemetry.sdk.trace&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">TracerProvider&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">opentelemetry.sdk.trace.export&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">BatchSpanProcessor&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">opentelemetry.exporter.otlp.proto.http.trace_exporter&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">OTLPSpanExporter&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">provider&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">TracerProvider&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">provider&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add_span_processor&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">BatchSpanProcessor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">OTLPSpanExporter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">endpoint&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://localhost:4318/v1/traces&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">trace&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_tracer_provider&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">provider&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">tracer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">trace&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get_tracer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;agent.runtime&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>埋点本身，推荐用上下文管理器——它保证&lt;strong>异常也会结束 span&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">with&lt;/span> &lt;span class="n">tracer&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">start_as_current_span&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;agent.run&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">span&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">span&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_attribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;run.id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">run_id&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">span&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_attribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;gen_ai.conversation.id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">session_id&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">with&lt;/span> &lt;span class="n">tracer&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">start_as_current_span&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;gen_ai.chat&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">chat&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">chat&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_attribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;gen_ai.request.model&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">model&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">chat&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_attribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;gen_ai.usage.input_tokens&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">usage&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">input_tokens&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>注意 &lt;code>start_as_current_span&lt;/code> 里的 &lt;strong>current&lt;/strong> 两个字——它做的事情是&amp;quot;把新 span 设为当前 span 并放进上下文&amp;quot;，下一个 span 创建时就会自动认它当爹。&lt;strong>父子关系不是手写的，是从上下文里自动读出来的&lt;/strong>。这也解释了为什么上下文一丢，span 树就会散成一片（下一节）。&lt;/p>
&lt;p>如果用现成的框架，通常不需要手写这些。LangChain / LangGraph 走 callback，OTel 有 instrumentation 包能自动建 span；纯手写的 Agent Runtime 才需要自己决定&amp;quot;哪些动作值得单独一个 span&amp;quot;。我的经验是：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>值得建 span&lt;/strong>：一次 LLM 调用、一次工具调用、一次检索、一次子 Agent 委派、一次 run 的起止。&lt;/li>
&lt;li>&lt;strong>不值得建 span&lt;/strong>：纯粹的参数拼装、字符串裁剪。这类动作建 span 只会让树变胖，真要记录就记事件/日志。&lt;/li>
&lt;/ul>
&lt;h2 id="五上下文传递让父子关系不断链">五、上下文传递：让父子关系不断链
&lt;/h2>&lt;p>这是入门到能用之间最容易被绊倒的一节。OTel 的上下文（Context）在 Python 里建立在 &lt;code>contextvars&lt;/code> 之上，含义是&amp;quot;当前执行流里，此刻的当前 span 是谁&amp;quot;。它有两个天然的断裂点。&lt;/p>
&lt;h3 id="51-进程内异步任务与线程池">5.1 进程内：异步任务与线程池
&lt;/h3>&lt;p>&lt;code>asyncio&lt;/code> 里 &lt;code>await&lt;/code> 前后的上下文是连续的，一般不会丢。但下面这些会丢：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 断链：新线程不会继承父线程的上下文&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">executor&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">submit&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">do_work&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># do_work 里 get_current_span() 拿到的是 INVALID_SPAN（不记录的占位）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 补救：显式把上下文复制过去&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">contextvars&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ctx&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">contextvars&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">copy_context&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">executor&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">submit&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ctx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">run&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">do_work&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>同理，&lt;code>process&lt;/code> 池、Celery 之类的任务队列，都必须&lt;strong>显式把上下文当参数传过去&lt;/strong>，再在另一端 &lt;code>attach&lt;/code>。这类 bug 的表现很有辨识度：&lt;strong>span 数量是对的，但全是平铺的——每个都是 root，没有父子关系&lt;/strong>。&lt;/p>
&lt;h3 id="52-跨进程w3c-trace-context">5.2 跨进程：W3C Trace Context
&lt;/h3>&lt;p>跨服务传递靠的是 W3C 标准的 &lt;code>traceparent&lt;/code> 头：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └── trace_id (32 hex) ─────────┘ └─ parent ──┘ └flags
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └ version
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>注入和提取都是现成的：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">opentelemetry&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">propagate&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 发送方：把当前上下文写进 HTTP header&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">headers&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">propagate&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">inject&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">headers&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># -&amp;gt; {&amp;#39;traceparent&amp;#39;: &amp;#39;00-...&amp;#39;}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 接收方：从 header 还原上下文，再建 span 就会挂上去&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ctx&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">propagate&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">extract&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">received_headers&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">with&lt;/span> &lt;span class="n">tracer&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">start_as_current_span&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;remote.tool&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">ctx&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>只要用了带自动埋点的 HTTP/gRPC 客户端，这一步通常是零成本的。&lt;strong>但 Agent 场景有几个地方容易漏&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>自己实现的流式接口&lt;/strong>（SSE 推 run 事件给前端）：手写的 &lt;code>fetch&lt;/code> 或自定义协议不会自动带头，得手动 &lt;code>inject&lt;/code>。&lt;/li>
&lt;li>&lt;strong>调用远程 MCP Server&lt;/strong>：这是最常漏的一处。MCP 的请求里如果没带 &lt;code>traceparent&lt;/code>，那边的 span 就是一棵独立的树，你的 trace 到工具调用这一步就断了。&lt;/li>
&lt;li>&lt;strong>后台任务&lt;/strong>：异步评估、异步摘要、定时任务，这些不在请求链路里，需要&lt;strong>显式&lt;/strong>把发起时的上下文带过去，否则它们永远是孤儿 span。&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>一个自查方法：跑一次完整 run，数一下 trace 里 root span 有几个。&lt;strong>健康的 trace 只有一个 root&lt;/strong>；出现多个 root，说明某处上下文断了，去上面三个地方找。&lt;/p>
&lt;/blockquote>
&lt;h2 id="六三个最常见的坑">六、三个最常见的坑
&lt;/h2>&lt;p>按&amp;quot;症状 → 原因 → 修复&amp;quot;记，方便回头查。&lt;/p>
&lt;p>&lt;strong>症状一：树是平的，所有 span 都是 root。&lt;/strong>
原因是上下文在异步/线程边界断了。
修复：线程池用 &lt;code>contextvars.copy_context()&lt;/code> 包一层；跨进程检查 &lt;code>traceparent&lt;/code> 是否真的发出去了（抓一次包最快）。&lt;/p>
&lt;p>&lt;strong>症状二：某个 span 永远不结束，trace 一直在长。&lt;/strong>
原因是走了 &lt;code>start_span()&lt;/code> 手动模式却忘了 &lt;code>end()&lt;/code>，或者异常路径上没走到 &lt;code>end()&lt;/code>。
修复：默认用 &lt;code>start_as_current_span()&lt;/code> 上下文管理器；确需手动模式就套 &lt;code>try/finally&lt;/code>。&lt;/p>
&lt;p>&lt;strong>症状三：属性没上去，或者上报报类型错误。&lt;/strong>
原因是 OTel 的属性只接受&lt;strong>基本类型&lt;/strong>（字符串、数字、布尔和它们的数组），结构化对象塞不进去。
修复：结构化内容先 &lt;code>json.dumps()&lt;/code> 成字符串；同时想清楚这个字段是不是该进指标维度。&lt;/p>
&lt;h2 id="七速查表">七、速查表
&lt;/h2>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>概念&lt;/th>
&lt;th>一句话&lt;/th>
&lt;th>Agent 里对应什么&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Trace&lt;/td>
&lt;td>一次完整请求的全过程，有唯一 &lt;code>trace_id&lt;/code>&lt;/td>
&lt;td>一次 Agent Run&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Span&lt;/td>
&lt;td>一段有起止时间的工作，树上的一个节点&lt;/td>
&lt;td>一次 LLM 调用 / 工具调用&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>父子关系&lt;/td>
&lt;td>子 span 时间落在父 span 内，靠上下文自动建立&lt;/td>
&lt;td>&lt;code>agent.run&lt;/code> → &lt;code>tool.web_search&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>属性&lt;/td>
&lt;td>键值对，描述 span&amp;quot;是什么&amp;quot;&lt;/td>
&lt;td>&lt;code>gen_ai.request.model&lt;/code>、&lt;code>tool.name&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>事件&lt;/td>
&lt;td>带时间戳，记录&amp;quot;某一刻发生了什么&amp;quot;&lt;/td>
&lt;td>重试、首个 token 到达、异常&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Metrics&lt;/td>
&lt;td>回答&amp;quot;多少 / 趋势&amp;quot;&lt;/td>
&lt;td>成本、成功率、P95 延迟&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Logs&lt;/td>
&lt;td>回答&amp;quot;具体是什么&amp;quot;&lt;/td>
&lt;td>prompt/response、工具入参出参&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>上下文传递&lt;/td>
&lt;td>&lt;code>contextvars&lt;/code>（进程内）+ &lt;code>traceparent&lt;/code>（跨进程）&lt;/td>
&lt;td>MCP 调用、SSE 流式、后台任务&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="几点收获">几点收获
&lt;/h2>&lt;ul>
&lt;li>&lt;strong>先问&amp;quot;我要回答什么问题&amp;quot;，再决定用什么信号。&lt;/strong> &amp;ldquo;慢在哪一步&amp;quot;是 trace 的问题，&amp;ldquo;涨了多少&amp;quot;是指标的问题，硬用日志去回答前两个，就会退化成打印追踪。&lt;/li>
&lt;li>&lt;strong>属性填得对不对，看它能不能被拿来筛。&lt;/strong> 能筛的放属性，只能看时刻的放事件，这个口诀能解掉八成的纠结。&lt;/li>
&lt;li>&lt;strong>上下文是隐式的全局状态，隐式的东西最容易断。&lt;/strong> 凡是跨了线程、跨了进程、跨了任务队列的边界，都要问一句&amp;quot;当前 span 传过去了吗&amp;rdquo;。&lt;/li>
&lt;li>&lt;strong>&lt;code>trace_id&lt;/code> 是整个可观测性的粘合剂。&lt;/strong> 日志里印上它、指标 exemplar 里带上它，三根柱子才真正连成一张网。&lt;/li>
&lt;li>&lt;strong>规范在变，别把 &lt;code>add_event()&lt;/code> 写进新代码。&lt;/strong> Span Event API 已经在 2026 年 3 月弃用，新的事件走 Logs API，能力不减，方向更统一。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;blockquote>
&lt;p>基础篇到此，概念就这些。下一步是把它接到具体的观测后端上——&lt;code>gen_ai.*&lt;/code> 字段怎么填、上报成功但看不到数据怎么办，这些实战里的坑见下一篇 &lt;a class="link" href="https://www.zata.cc/p/agent-%E5%9F%8B%E7%82%B9%E6%8E%A5-arms%E4%B8%8A%E6%8A%A5%E8%BF%94%E5%9B%9E-success%E6%8E%A7%E5%88%B6%E5%8F%B0%E5%8D%B4%E6%98%AF%E7%A9%BA%E7%9A%84/" >Agent 埋点接 ARMS&lt;/a>。&lt;/p>
&lt;/blockquote></description></item><item><title>Agent 埋点接 ARMS：上报返回 success，控制台却是空的</title><link>https://www.zata.cc/p/agent-%E5%9F%8B%E7%82%B9%E6%8E%A5-arms%E4%B8%8A%E6%8A%A5%E8%BF%94%E5%9B%9E-success%E6%8E%A7%E5%88%B6%E5%8F%B0%E5%8D%B4%E6%98%AF%E7%A9%BA%E7%9A%84/</link><pubDate>Fri, 18 Sep 2026 18:05:00 +0800</pubDate><guid>https://www.zata.cc/p/agent-%E5%9F%8B%E7%82%B9%E6%8E%A5-arms%E4%B8%8A%E6%8A%A5%E8%BF%94%E5%9B%9E-success%E6%8E%A7%E5%88%B6%E5%8F%B0%E5%8D%B4%E6%98%AF%E7%A9%BA%E7%9A%84/</guid><description>&lt;img src="https://www.zata.cc/p/agent-%E5%9F%8B%E7%82%B9%E6%8E%A5-arms%E4%B8%8A%E6%8A%A5%E8%BF%94%E5%9B%9E-success%E6%8E%A7%E5%88%B6%E5%8F%B0%E5%8D%B4%E6%98%AF%E7%A9%BA%E7%9A%84/images/index/index.svg" alt="Featured image of post Agent 埋点接 ARMS：上报返回 success，控制台却是空的" />&lt;p>上报接口返回 &lt;code>200&lt;/code>，响应体是两个引号包着的 &lt;code>&amp;quot;success&amp;quot;&lt;/code>。控制台刷新了五遍，调用链分析里 &lt;code>Span 数量: 0&lt;/code>。&lt;/p>
&lt;p>第一反应当然是上报没成功。于是回头去查 payload、查鉴权、查代理——查了小半个小时，最后发现&lt;strong>数据一直都在，是我看的地方不对&lt;/strong>。&lt;/p>
&lt;p>这篇文章把这个过程完整记下来。前面是排查（含三个可以复用的判别实验），后面是这次顺带做的一个具体问题：&lt;strong>对话里带附件时，span 应该怎么记&lt;/strong>。&lt;/p>
&lt;blockquote>
&lt;p>前置概念（Trace 与 Span、父子关系、属性与事件的边界、上下文传递）见同系列 &lt;a class="link" href="https://www.zata.cc/p/agent-tracing-%E5%9F%BA%E7%A1%80trace-span-%E4%B8%8E-opentelemetry-%E5%9F%8B%E7%82%B9/" >Agent Tracing 基础：Trace、Span 与 OpenTelemetry 埋点&lt;/a>。本篇不重复讲基础，直接从&amp;quot;接进 ARMS 之后为什么看不到&amp;quot;讲起。&lt;/p>
&lt;/blockquote>
&lt;h2 id="一gen_ai-到底是什么">一、&lt;code>gen_ai.*&lt;/code> 到底是什么
&lt;/h2>&lt;p>动手之前先明确一件事：埋点里那一堆 &lt;code>gen_ai.xxx&lt;/code> 不是我起的名字，是 &lt;strong>OpenTelemetry 的 GenAI 语义约定&lt;/strong>（Generative AI semantic conventions）——社区为&amp;quot;生成式 AI / LLM 应用&amp;quot;单独定义的一套属性规范。&lt;/p>
&lt;p>它解决的问题很朴素。没有规范时，同一个&amp;quot;模型名&amp;quot;会被写成 &lt;code>model&lt;/code>、&lt;code>model_name&lt;/code>、&lt;code>llm.model&lt;/code>；&amp;ldquo;输入 token&amp;quot;叫 &lt;code>prompt_tokens&lt;/code>、&lt;code>input_tokens&lt;/code>、&lt;code>usage.prompt&lt;/code>。结果就是&lt;strong>换个观测后端，埋点得重写一遍&lt;/strong>。&lt;/p>
&lt;p>约定把名字、类型、取值都钉死了，所以一份埋点能同时被 Langfuse、Phoenix、ARMS 正确解析。它大致覆盖五类：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>类别&lt;/th>
&lt;th>属性&lt;/th>
&lt;th>说明&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>操作身份&lt;/td>
&lt;td>&lt;code>gen_ai.operation.name&lt;/code>&lt;/td>
&lt;td>&lt;code>chat&lt;/code> / &lt;code>embeddings&lt;/code> / &lt;code>invoke_agent&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>厂商与模型&lt;/td>
&lt;td>&lt;code>gen_ai.provider.name&lt;/code>、&lt;code>gen_ai.request.model&lt;/code>、&lt;code>gen_ai.response.model&lt;/code>&lt;/td>
&lt;td>provider 是判别字段&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>对话内容&lt;/td>
&lt;td>&lt;code>gen_ai.input.messages&lt;/code>、&lt;code>gen_ai.output.messages&lt;/code>、&lt;code>gen_ai.system_instructions&lt;/code>&lt;/td>
&lt;td>结构化消息，见下文&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>用量与结果&lt;/td>
&lt;td>&lt;code>gen_ai.usage.input_tokens&lt;/code>、&lt;code>output_tokens&lt;/code>、&lt;code>gen_ai.response.finish_reasons&lt;/code>&lt;/td>
&lt;td>成本核算靠这组&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>工具与会话&lt;/td>
&lt;td>&lt;code>gen_ai.tool.name&lt;/code>、&lt;code>gen_ai.tool.call.id&lt;/code>、&lt;code>gen_ai.conversation.id&lt;/code>&lt;/td>
&lt;td>关联工具调用与多轮会话&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>其中 &lt;code>input.messages&lt;/code> / &lt;code>output.messages&lt;/code> 是&lt;strong>结构化&lt;/strong>的，不是一段纯文本。每条消息是 &lt;code>{role, parts[]}&lt;/code>，part 有这些类型：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>part 类型&lt;/th>
&lt;th>必填字段&lt;/th>
&lt;th>用在哪&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>text&lt;/code>&lt;/td>
&lt;td>&lt;code>content&lt;/code>&lt;/td>
&lt;td>普通文本&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>uri&lt;/code>&lt;/td>
&lt;td>&lt;code>uri&lt;/code>、&lt;code>modality&lt;/code>&lt;/td>
&lt;td>文件已在对象存储里，span 只记引用&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>file&lt;/code>&lt;/td>
&lt;td>&lt;code>file_id&lt;/code>、&lt;code>modality&lt;/code>&lt;/td>
&lt;td>用厂商预上传能力（如 OpenAI Files API）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>blob&lt;/code>&lt;/td>
&lt;td>&lt;code>content&lt;/code>、&lt;code>modality&lt;/code>&lt;/td>
&lt;td>只有必须内联时才用，base64 进 span 有成本也有合规风险&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>tool_call&lt;/code> / &lt;code>tool_call_response&lt;/code>&lt;/td>
&lt;td>&lt;code>name&lt;/code> / &lt;code>response&lt;/code>&lt;/td>
&lt;td>工具调用与结果&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>三个必须知道的坑：&lt;strong>这些属性绝大部分还是 &lt;code>Development&lt;/code> 状态，会改名&lt;/strong>——&lt;code>gen_ai.system&lt;/code> 就被重命名成了 &lt;code>gen_ai.provider.name&lt;/code>，&lt;code>gen_ai.prompt.*&lt;/code> 也被 &lt;code>input.messages&lt;/code> 取代；&lt;strong>结构化属性落不到 span 上&lt;/strong>，OTel 的 Python SDK 只接受基本类型，所以得 &lt;code>json.dumps&lt;/code> 成字符串再塞；&lt;strong>&lt;code>input/output.messages&lt;/code> 是 Opt-In 属性&lt;/strong>，规范明确说不该默认记录，因为可能含用户隐私。&lt;/p>
&lt;/blockquote>
&lt;h2 id="二span-树怎么设计">二、span 树怎么设计
&lt;/h2>&lt;p>目标形态是一次 Agent Run 对应一棵 span 树，业务库的 Run 记录里存 &lt;code>trace_id&lt;/code> 作为关联键：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">agent.run [run.id=run-xxx, user.id=demo-user-1, session.id=demo-session-1]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├─ agent.attach_document file.uri / file.mime_type / file.size_bytes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├─ gen_ai chat gen_ai.input.messages / usage.* / latency_ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├─ tool get_freight_quote gen_ai.tool.name / gen_ai.tool.call.id / tool.result
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─ gen_ai chat 基于工具结果的第二轮作答
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>写属性的部分收敛成几个 helper，别散在业务代码里——改名时只改一处：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">set_llm_request&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">span&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">provider&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">model&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">messages&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">conversation_id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">None&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">span&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_attribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;gen_ai.operation.name&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;chat&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">span&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_attribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;gen_ai.provider.name&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">provider&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">span&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_attribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;gen_ai.request.model&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">model&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">conversation_id&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">span&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_attribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;gen_ai.conversation.id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">conversation_id&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># system 消息按约定不进 input messages，单独走 system_instructions&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">span&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_attribute&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;gen_ai.input.messages&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">to_input_messages&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">messages&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">ensure_ascii&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>to_input_messages&lt;/code> 里做两件事：把 system 消息摘出去、把文本统一截断。&lt;strong>截断不是可选项&lt;/strong>——&lt;code>input.messages&lt;/code> 里塞进一个 8000 字的 prompt，span 会大得没法用。&lt;/p>
&lt;h2 id="三一个-200-success-的假象">三、一个 200 success 的假象
&lt;/h2>&lt;p>配置填完，跑起来，日志里看到这一行：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">POST http://tracing-analysis-dc-sg.aliyuncs.com/adapt_&amp;lt;xx&amp;gt;_&amp;lt;xx&amp;gt;/api/otlp/traces HTTP/1.1&amp;#34; 200 9
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>200&lt;/code>、响应体 9 字节（就是 &lt;code>&amp;quot;success&amp;quot;&lt;/code> 加引号）。这种时候人很容易直接跳到&amp;quot;好，通了&amp;rdquo;，然后去控制台发现什么都没有，再回头怀疑人生。&lt;/p>
&lt;p>我做了三个实验来定位问题到底在链路哪一段。这三个实验本身挺通用，值得记：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>实验&lt;/th>
&lt;th>操作&lt;/th>
&lt;th>结果&lt;/th>
&lt;th>排除了什么&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>绕过本地代理&lt;/td>
&lt;td>&lt;code>--noproxy '*'&lt;/code> 直连重发&lt;/td>
&lt;td>同样 &lt;code>200 &amp;quot;success&amp;quot;&lt;/code>&lt;/td>
&lt;td>不是代理伪造的响应&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>发垃圾 payload&lt;/td>
&lt;td>&lt;code>head -c 300 /dev/urandom&lt;/code> 当 body 发过去&lt;/td>
&lt;td>&lt;code>400 Bad Request&lt;/code>&lt;/td>
&lt;td>网关&lt;strong>真的在解析&lt;/strong> protobuf，不是无脑返回成功&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>伪造 license key&lt;/td>
&lt;td>把 &lt;code>/adapt_.../&lt;/code> 换成随机串&lt;/td>
&lt;td>&lt;code>403 Forbidden&lt;/code>&lt;/td>
&lt;td>key 是&lt;strong>有效&lt;/strong>的，且服务端会校验 workspace&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>三个实验做完，结论就很硬了：&lt;strong>数据确实被服务端收下了&lt;/strong>，而且收进了一个合法的 workspace。既然服务端没问题，那&amp;quot;看不到&amp;quot;就只剩一种可能——&lt;strong>去看的那个地方，不是数据所在的地方&lt;/strong>。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/agent-%E5%9F%8B%E7%82%B9%E6%8E%A5-arms%E4%B8%8A%E6%8A%A5%E8%BF%94%E5%9B%9E-success%E6%8E%A7%E5%88%B6%E5%8F%B0%E5%8D%B4%E6%98%AF%E7%A9%BA%E7%9A%84/images/index/diagnosis.svg"
loading="lazy"
alt="诊断切段图：把上报链路切成 Agent 进程、本地代理、ARMS 网关、控制台四段，实验①绕过代理排除代理伪造，实验②发垃圾字节得到 400 排除网关不解析，实验③伪造 license key 得到 403 证明 key 有效，四段全部通过后剩下唯一的失败段是控制台的地域、页签与页面作用域"
>&lt;/p>
&lt;p>事实也确实如此。&lt;/p>
&lt;h2 id="四三个坑">四、三个坑
&lt;/h2>&lt;h3 id="坑一地域">坑一：地域
&lt;/h3>&lt;p>接入点是 &lt;code>tracing-analysis-dc-sg.aliyuncs.com&lt;/code>，&lt;code>dc-sg&lt;/code> 就是&lt;strong>新加坡&lt;/strong>。而控制台默认停在别的地域，调用链分析自然是空的。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>症状&lt;/strong>&lt;/td>
&lt;td>上报 &lt;code>200 success&lt;/code>，控制台调用链分析 &lt;code>Span 数量: 0&lt;/code>，左侧所有维度显示&amp;quot;没有匹配的值&amp;quot;&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>原因&lt;/strong>&lt;/td>
&lt;td>控制台地域与接入点地域不一致。数据在 &lt;code>ap-southeast-1&lt;/code>，眼睛在别处&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>修复&lt;/strong>&lt;/td>
&lt;td>控制台切到与接入点同地域；&lt;code>应用列表&lt;/code> 也要在同地域看&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>这类问题最坑的地方在于：&lt;strong>它不报错&lt;/strong>。上报端一切正常，服务端一切正常，只有&amp;quot;人对不上&amp;quot;。&lt;/p>
&lt;h3 id="坑二入口">坑二：入口
&lt;/h3>&lt;p>地域切对之后，还有第二层错位。控制台里同一个应用有不止一个入口：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>应用列表&lt;/strong> 分 &lt;code>OpenTelemetry&lt;/code> 和 &lt;code>ARMS&lt;/code> 两个页签。通过 OTel SDK 上报的应用只出现在前者；后者是 ARMS 探针接入的应用列表。看错页签 = 看到空列表。&lt;/li>
&lt;li>&lt;strong>应用维度的调用链分析&lt;/strong> 是带过滤的。我一度搜索框里直接粘 trace_id，仍然是 0 —— 因为页面作用域被钉死在某个应用上，搜索条件没换掉&amp;quot;应用&amp;quot;这个更大的前提。页面里搜出来的搜索框内容长这样：&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">serviceName:&amp;#34;agentrun-agent-quick-oUpbu&amp;#34; and resources.acs.arms.service_id : &amp;#34;azmulz8rp8...&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第三段 &lt;code>resources.acs.arms.service_id&lt;/code> 是 ARMS 探针注入的资源属性。&lt;strong>OTel 自己上报的 span 根本没有这个属性&lt;/strong>，所以这个条件永远匹配不上。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>症状&lt;/strong>&lt;/td>
&lt;td>搜 trace_id 也搜不到；应用列表某个页签永远是空的&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>原因&lt;/strong>&lt;/td>
&lt;td>页面作用域（应用 / 页签）与数据的归属不匹配&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>修复&lt;/strong>&lt;/td>
&lt;td>换到不绑定单个应用的入口，或把搜索条件改成 &lt;code>serviceName:&amp;quot;&amp;lt;你的服务名&amp;gt;&amp;quot;&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="坑三配置写法">坑三：配置写法
&lt;/h3>&lt;p>第三个坑不致命，但错误信息很难自查，因为&lt;strong>不报错&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 错的：ARMS 的 header 不是 Authorization: Bearer&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">OTEL_EXPORTER_OTLP_HEADERS&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">Authorization&lt;/span>&lt;span class="o">=&lt;/span>Bearer &amp;lt;token&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 对的：HTTP 接入点鉴权编码在 /adapt_&amp;lt;xx&amp;gt;_&amp;lt;xx&amp;gt;/ 路径里，headers 留空&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span>&lt;span class="o">=&lt;/span>http://tracing-analysis-dc-sg.aliyuncs.com/adapt_&amp;lt;xx&amp;gt;_&amp;lt;xx&amp;gt;/api/otlp/traces
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">OTEL_EXPORTER_OTLP_HEADERS&lt;/span>&lt;span class="o">=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 只有 gRPC 才需要 header，格式是 Authentication=&amp;lt;token&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span>&lt;span class="o">=&lt;/span>http://tracing-analysis-dc-sg.aliyuncs.com:8090
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">OTEL_EXPORTER_OTLP_HEADERS&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">Authentication&lt;/span>&lt;span class="o">=&lt;/span>&amp;lt;token&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>还有一条同类的：HTTP 接入点&lt;strong>自带&lt;/strong> &lt;code>/api/otlp/traces&lt;/code>，不要再拼 &lt;code>/v1/traces&lt;/code>——那是本地 Jaeger 的写法，拼上去就是 404。&lt;/p>
&lt;h2 id="五对话里带附件怎么记">五、对话里带附件怎么记
&lt;/h2>&lt;p>这是这次真正想解决的问题：Agent 的对话里带了文件，trace 上该怎么表达。&lt;/p>
&lt;p>关键决定是&lt;strong>文件不进 span 的字节流&lt;/strong>。做法是三处落点：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>位置&lt;/th>
&lt;th>记什么&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>agent.attach_document&lt;/code> span&lt;/td>
&lt;td>&lt;code>file.uri&lt;/code> / &lt;code>file.mime_type&lt;/code> / &lt;code>file.size_bytes&lt;/code> / &lt;code>file.doc_id&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai chat&lt;/code> 的 &lt;code>gen_ai.input.messages&lt;/code>&lt;/td>
&lt;td>该条 user 消息的 &lt;code>parts&lt;/code> 里一个 &lt;code>uri&lt;/code> part + 一个 &lt;code>text&lt;/code> part&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>业务库&lt;/td>
&lt;td>文件归属业务层（doc_id ↔ Run），trace 里只留引用&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;img src="https://www.zata.cc/p/agent-%E5%9F%8B%E7%82%B9%E6%8E%A5-arms%E4%B8%8A%E6%8A%A5%E8%BF%94%E5%9B%9E-success%E6%8E%A7%E5%88%B6%E5%8F%B0%E5%8D%B4%E6%98%AF%E7%A9%BA%E7%9A%84/images/index/attachment.svg"
loading="lazy"
alt="三层落点图：左边对象存储是唯一保存文件字节的地方；中间 trace 用 agent.attach_document 记 file.uri 与规模，用 gen_ai.input.messages 的 text、uri、text 三个 part 记模型看到的内容；右边业务库用 trace_id 关联两层"
>&lt;/p>
&lt;p>实测落进 ARMS 的属性长这样：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[{&lt;/span>&lt;span class="nt">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;parts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;按附件里的信息报价&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;uri&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;uri&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;oss://bucket/uploads/doc-adcc4d1f/booking-note.txt&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;modality&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;document&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;mime_type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;text/plain&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;订舱委托书 / BOOKING NOTE\n委托编号：BN-2026-0918-075…&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">]}]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>三个设计取舍值得说一下：&lt;/p>
&lt;p>&lt;strong>第一，&lt;code>uri&lt;/code> part 和 &lt;code>text&lt;/code> part 成对出现。&lt;/strong> &lt;code>uri&lt;/code> 是溯源（文件从哪来），&lt;code>text&lt;/code> 是模型真正读到的内容（服务端预抽取的结果）。只记 &lt;code>uri&lt;/code>，排查时无法回答&amp;quot;模型到底看没看到附件&amp;quot;；只记 &lt;code>text&lt;/code>，又丢了文件来源。两个都记才查得动。&lt;/p>
&lt;p>&lt;strong>第二，&lt;code>modality&lt;/code> 是必填。&lt;/strong> &lt;code>uri&lt;/code> 和 &lt;code>file&lt;/code> part 都要求这个字段，取值 &lt;code>image&lt;/code> / &lt;code>video&lt;/code> / &lt;code>audio&lt;/code> / &lt;code>document&lt;/code>。容易漏，漏了后端可能直接忽略这个 part。&lt;/p>
&lt;p>&lt;strong>第三，模型调用侧和 trace 侧要分开构造。&lt;/strong> 应用侧消息保留结构化内容（&lt;code>{type: &amp;quot;text&amp;quot;}&lt;/code> / &lt;code>{type: &amp;quot;document&amp;quot;}&lt;/code>），发给 OpenAI 兼容端点时拍平成纯文本；转成 &lt;code>gen_ai.input.messages&lt;/code> 时才展开成 uri + text。这样两边各取所需，不会为了迁就某一方而变形：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 模型侧：附件以抽取文本注入&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">to_openai_content&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">content&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">isinstance&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">content&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">content&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">chunks&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">item&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">content&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">item&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">chunks&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">item&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">item&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;document&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">chunks&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;[[附件 &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">item&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;uri&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">]]&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">item&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;extracted_text&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">chunks&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>验证方式是让模型复述附件里的独有信息——它在回答里用上了委托书的 &lt;code>ETD 2026-09-25&lt;/code>、截关时间和&amp;quot;14 天免箱期&amp;quot;。这说明附件确实进了模型上下文，不只是进了 span。&lt;/p>
&lt;p>顺手补的一个洞：&lt;code>BatchSpanProcessor&lt;/code> 默认靠 &lt;code>atexit&lt;/code> 冲刷，进程被强杀（Ctrl-C、沙箱超时）时最后一批 span 会丢。Run 结束处显式 &lt;code>force_flush()&lt;/code> 更稳。&lt;/p>
&lt;h2 id="六数据怎么拿出来给-ai-看">六、数据怎么拿出来给 AI 看
&lt;/h2>&lt;p>控制台是给人看的，AI 想自己查得走另外三条路：&lt;/p>
&lt;p>&lt;strong>1. OpenAPI&lt;/strong>（免费，最直接）。&lt;code>xtrace&lt;/code> 产品两个接口就能拼出一个导出脚本：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>接口&lt;/th>
&lt;th>作用&lt;/th>
&lt;th>关键参数&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>SearchTraces&lt;/code>&lt;/td>
&lt;td>按条件列出调用链，返回 TraceID 列表&lt;/td>
&lt;td>&lt;code>RegionId&lt;/code>、&lt;code>StartTime&lt;/code>/&lt;code>EndTime&lt;/code>（毫秒）、&lt;code>ServiceName&lt;/code>、&lt;code>OperationName&lt;/code>、&lt;code>MinDuration&lt;/code>、&lt;code>Tag&lt;/code>、&lt;code>PageNumber&lt;/code>/&lt;code>PageSize&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>GetTrace&lt;/code>&lt;/td>
&lt;td>按 TraceID 取完整 span 明细&lt;/td>
&lt;td>&lt;code>TraceID&lt;/code>、&lt;code>RegionId&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>翻页拿 ID，再逐条取详情，落地成 JSON 就完了。&lt;/p>
&lt;p>&lt;strong>2. 可观测 MCP Server&lt;/strong>（&lt;code>aliyun/alibabacloud-observability-mcp-server&lt;/code>）。Go 单二进制，里面正好有对应的工具：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mcpServers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;alibaba_cloud_observability&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;command&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;./bin/alibabacloud-observability-mcp-server&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;args&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;start&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;--stdio&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;env&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ALIBABA_CLOUD_ACCESS_KEY_ID&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;lt;AK&amp;gt;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ALIBABA_CLOUD_ACCESS_KEY_SECRET&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;lt;SK&amp;gt;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ALIBABA_CLOUD_REGION&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;cn-singapore&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>接上之后可以直接问&amp;quot;昨天耗时最长的几条 Run 是什么，&lt;code>gen_ai.input.messages&lt;/code> 里带没带附件&amp;quot;，由 &lt;code>umodel_search_traces&lt;/code> / &lt;code>umodel_get_traces&lt;/code> 去取。两个注意点：用只读 RAM 用户（&lt;code>AliyunARMSReadOnlyAccess&lt;/code>），别用主账号 AK；&lt;code>sls_text_to_sql&lt;/code>、&lt;code>sls_sop&lt;/code> 这类 AI 工具走 STAROps 单独计费，不需要就在 &lt;code>config.yaml&lt;/code> 的 &lt;code>enabled_tools&lt;/code> 里关掉。&lt;/p>
&lt;p>&lt;strong>3. Collector 双写&lt;/strong>。反正上报端在自己手里，OTel Collector 同一份数据同时投 ARMS 和本地存储（ClickHouse / 文件 / 对象存储）。本地那份没有 AK、没有分页、随便 SQL——&lt;strong>AI 分析用它比用控制台 API 舒服得多&lt;/strong>。ARMS 负责看瀑布图和告警，本地那份负责喂给 Agent。&lt;/p>
&lt;h2 id="总结">总结
&lt;/h2>&lt;p>几个收获，都是这次真金白银换来的：&lt;/p>
&lt;p>&lt;strong>1. 200 不等于数据可用。&lt;/strong> &lt;code>&amp;quot;success&amp;quot;&lt;/code> 只说明网关收了，说明不了数据落到了你正在看的那个地方。判断&amp;quot;到底通没通&amp;quot;要有独立证据——我这次用的是&amp;quot;发垃圾字节看它报不报错&amp;quot;和&amp;quot;伪造 key 看它拦不拦&amp;quot;，两个实验各一次请求，就能把链路切段定位。&lt;/p>
&lt;p>&lt;strong>2. 可观测性接入的坑，多半在&amp;quot;人对不上&amp;quot;而不是&amp;quot;数据不通&amp;quot;。&lt;/strong> 地域、页签、页面作用域、filter 条件，这四样任何一个不对，现象都是同一个：空列表。而且全都不报错。排查时优先怀疑这个，比怀疑 SDK 划算。&lt;/p>
&lt;p>&lt;strong>3. 约定比实现重要。&lt;/strong> 用 &lt;code>gen_ai.*&lt;/code> 的收益在这次很直观：同一份埋点，改个 endpoint 就从本地控制台切到了 ARMS，一行代码没动。代价是要接受它还在 &lt;code>Development&lt;/code>、会改名——所以属性写入必须收敛到 helper 里。&lt;/p>
&lt;p>&lt;strong>4. 文件类数据，&amp;ldquo;引用 + 摘要&amp;quot;比&amp;quot;内容&amp;quot;有用。&lt;/strong> 把 pdf 塞进 span 不会让排查更容易，只会让它更贵更难搜。&lt;code>uri&lt;/code> part 记来源、&lt;code>text&lt;/code> part 记模型看到的内容、业务库存文件本身，三层各司其职。&lt;/p>
&lt;p>&lt;strong>5. 顺手能修的洞就别留着。&lt;/strong> &lt;code>force_flush()&lt;/code> 这种一行的事，不补就是&amp;quot;偶发丢 trace&amp;rdquo;，而偶发丢 trace 是最难查的那类问题——它会让上面所有的排查经验都建立在错误的观察上。&lt;/p></description></item><item><title>AG-UI：当 Agent 学会了和前端说话</title><link>https://www.zata.cc/p/ag-ui%E5%BD%93agent%E5%AD%A6%E4%BC%9A%E4%BA%86%E5%92%8C%E5%89%8D%E7%AB%AF%E8%AF%B4%E8%AF%9D/</link><pubDate>Thu, 10 Sep 2026 14:30:00 +0800</pubDate><guid>https://www.zata.cc/p/ag-ui%E5%BD%93agent%E5%AD%A6%E4%BC%9A%E4%BA%86%E5%92%8C%E5%89%8D%E7%AB%AF%E8%AF%B4%E8%AF%9D/</guid><description>&lt;img src="https://www.zata.cc/p/ag-ui%E5%BD%93agent%E5%AD%A6%E4%BC%9A%E4%BA%86%E5%92%8C%E5%89%8D%E7%AB%AF%E8%AF%B4%E8%AF%9D/images/index/index.svg" alt="Featured image of post AG-UI：当 Agent 学会了和前端说话" />&lt;p>最近在给自己的 Agent Runtime 设计输入输出契约时，我在 README 里画了一张三方对照表：我定义的事件类型一列、Runtime 原生事件一列，第三列空着——那是留给 &lt;a class="link" href="https://github.com/ag-ui-protocol/ag-ui" target="_blank" rel="noopener"
>AG-UI&lt;/a> 的。填完这张表之后我发现，AG-UI 值得不只是一列，它值得被完整讲一遍。为了写这篇文章，我把官方仓库、Python SDK 源码和 PyPI/npm 的发布记录都翻了一遍，下面带着版本号讲。&lt;/p>
&lt;h2 id="先看仓库现状2026-09-10">先看仓库现状（2026-09-10）
&lt;/h2>&lt;p>先把事实钉住，本文所有描述都对应这个版本：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>项&lt;/th>
&lt;th>值&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>仓库&lt;/td>
&lt;td>&lt;a class="link" href="https://github.com/ag-ui-protocol/ag-ui" target="_blank" rel="noopener"
>&lt;code>github.com/ag-ui-protocol/ag-ui&lt;/code>&lt;/a>，创建于 2025-05-07&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>文档&lt;/td>
&lt;td>&lt;a class="link" href="https://docs.ag-ui.com/" target="_blank" rel="noopener"
>docs.ag-ui.com&lt;/a>（draft spec + SDK 参考）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>热度&lt;/td>
&lt;td>&lt;strong>15.8k stars / 1.4k forks&lt;/strong>，MIT License&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>本文对应 commit&lt;/td>
&lt;td>&lt;code>main@5f32a64ee999&lt;/code>（2026-09-09）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Python SDK&lt;/td>
&lt;td>&lt;a class="link" href="https://pypi.org/project/ag-ui-protocol/" target="_blank" rel="noopener"
>&lt;code>ag-ui-protocol&lt;/code>&lt;/a> &lt;strong>0.1.22&lt;/strong>（PyPI，2026-08-31 发布，首版 0.1.4 于 2025-04-30）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TypeScript SDK&lt;/td>
&lt;td>&lt;a class="link" href="https://www.npmjs.com/package/@ag-ui/core" target="_blank" rel="noopener"
>&lt;code>@ag-ui/core&lt;/code>&lt;/a> 及 client/encoder/proto 系列 &lt;strong>0.0.59&lt;/strong>（npm latest，另有 canary/alpha 通道）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>发布节奏&lt;/td>
&lt;td>几乎&lt;strong>每日&lt;/strong>切一次 release（2026-08-20 到 09-09 切了 5 个），协议仍处 draft 阶段&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Python 包只依赖 &lt;code>pydantic&amp;gt;=2.11.2&lt;/code>，一个依赖，克制得不像一个协议实现。&lt;/p>
&lt;h2 id="三层协议的最后一块">三层协议的最后一块
&lt;/h2>&lt;p>把 2024-2026 这两年 agent 协议的版图摊开，会发现一个很规整的分层：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>MCP&lt;/strong>（Anthropic）：agent ↔ 工具。让模型能标准地&amp;quot;伸手拿东西&amp;quot;。&lt;/li>
&lt;li>&lt;strong>A2A&lt;/strong>（Google）：agent ↔ agent。让多个 agent 标准地互相委派任务。&lt;/li>
&lt;li>&lt;strong>AG-UI&lt;/strong>（CopilotKit）：agent ↔ 前端。让 agent 的运行过程标准地&amp;quot;演给用户看&amp;quot;。&lt;/li>
&lt;/ul>
&lt;p>前两层这几年讨论得足够多了，第三层却长期处于&amp;quot;每家自己造&amp;quot;的状态：LangGraph 自己定义 stream mode，OpenAI Assistants 自己定义 run steps，各家 ChatUI 各自解析各家的事件格式。你写一个 agent 后端，想换个前端，事件层的适配就得重写一遍；你写一个聊天前端，想接不同的 agent，每种 agent 的 SSE 格式都得单独处理。&lt;/p>
&lt;p>AG-UI 的野心就是把这层标准化掉。它不是委员会里设计出来的协议——它是从 CopilotKit 这个产品里长出来的：CopilotKit 做了几年&amp;quot;把 agent 嵌进 React 应用&amp;quot;这件事，攒够了事件类型的实战样本，回头把内部格式提炼成了开放协议，初始合作伙伴是 LangChain（LangGraph）和 CrewAI。这一点从仓库结构里看得见：&lt;code>integrations/&lt;/code> 目录下有 21 个条目——18 个框架适配层、2 个 server starter 模板加一个 community 目录；&lt;code>apps/&lt;/code> 里挂着 dojo（官方演示站）和一个 CLI 示例应用。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/ag-ui%E5%BD%93agent%E5%AD%A6%E4%BC%9A%E4%BA%86%E5%92%8C%E5%89%8D%E7%AB%AF%E8%AF%B4%E8%AF%9D/images/three-protocol-layers.svg"
loading="lazy"
alt="三层 Agent 协议:各管一个边界"
>&lt;/p>
&lt;h2 id="仓库解剖一个协议-monorepo-长什么样">仓库解剖：一个协议 monorepo 长什么样
&lt;/h2>&lt;p>官方仓库是一个 pnpm + nx 的 monorepo，顶层结构本身就是协议生态的切片：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">ag-ui/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── sdks/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── python/ # ag-ui-protocol 0.1.22 (core + encoder)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── typescript/ # @ag-ui/core|client|encoder|proto 0.0.59 + cli
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── dotnet/ # 官方 .NET SDK
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── community/ # dart / rust / ruby / c++ / kotlin / go / java ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── integrations/ # 20 个框架适配层(见后文生态一节)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── middlewares/ # a2a / a2ui / mcp / mcp-apps / event-throttle
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── apps/ # dojo(官方演示) + client-cli-example
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── docs/ # 文档源
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>两个细节值得注意：&lt;strong>middlewares 是独立一层&lt;/strong>——A2A 桥接、MCP 桥接、MCP Apps、A2UI、事件节流各占一个包，说明协议把&amp;quot;和别的协议对接&amp;quot;也当成了标准件而不是用户自理；&lt;strong>protocol 有 TypeScript 和 Python 两套官方实现&lt;/strong>，且明确以 TypeScript 为参考实现（源码注释里直接写着 TS 的行为是其他 SDK 对齐的基准）。&lt;/p>
&lt;h2 id="事件流是唯一事实源">事件流是唯一事实源
&lt;/h2>&lt;p>AG-UI 的核心设计只有一句话：&lt;strong>agent 的一次运行 = 一条有序事件流，前端状态完全由事件序列归约得到&lt;/strong>。&lt;/p>
&lt;p>没有独立的 REST 查询接口，没有&amp;quot;拉取当前状态&amp;quot;的端点。前端想知道 agent 在干什么？订阅事件流，逐条归约。想在断线后恢复？重新拿一遍事件（或快照），重放。这个思想和事件溯源（Event Sourcing）一脉相承，也和我自己的 Canonical Run 契约不谋而合——事件即事实，投影即状态。&lt;/p>
&lt;p>协议在 0.1.22 的定义里共有 &lt;strong>36 个事件枚举成员&lt;/strong>——31 个在用，加上 5 个已废弃的 &lt;code>THINKING_*&lt;/code>（1.0.0 移除）。全部继承自一个四字段信封：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;TEXT_MESSAGE_CONTENT&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;timestamp&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1730000000&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;rawEvent&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;metadata&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>按功能分组看一眼全集，就能感受到协议想覆盖的交互光谱：&lt;/p>
&lt;p>&lt;strong>生命周期（5 种）&lt;/strong>——&lt;code>RUN_STARTED&lt;/code>、&lt;code>RUN_FINISHED&lt;/code>、&lt;code>RUN_ERROR&lt;/code> 管起止，&lt;code>STEP_STARTED&lt;/code>/&lt;code>STEP_FINISHED&lt;/code> 标记过程里的小步。注意 run 的两个标识 &lt;code>threadId&lt;/code> 和 &lt;code>runId&lt;/code> 都是客户端生成的，服务端在 &lt;code>RUN_STARTED&lt;/code> 里回显——这和 OpenAI 式&amp;quot;服务端生成 ID 返回给你&amp;quot;正好相反。&lt;/p>
&lt;p>&lt;strong>文本消息（4 种）&lt;/strong>——经典三段式 &lt;code>TEXT_MESSAGE_START&lt;/code> → &lt;code>TEXT_MESSAGE_CONTENT&lt;/code>（带 &lt;code>delta&lt;/code>）→ &lt;code>TEXT_MESSAGE_END&lt;/code>，外加一个 &lt;code>TEXT_MESSAGE_CHUNK&lt;/code> 快捷形式：省掉配对开销，客户端自己展开。消息靠 &lt;code>messageId&lt;/code> 关联。&lt;/p>
&lt;p>&lt;strong>工具调用（5 种）&lt;/strong>——&lt;code>TOOL_CALL_START&lt;/code>（带 &lt;code>toolCallId&lt;/code> 和 &lt;code>toolCallName&lt;/code>）→ &lt;code>TOOL_CALL_ARGS&lt;/code>（参数 JSON 增量下发，支持流式渲染参数）→ &lt;code>TOOL_CALL_END&lt;/code>，加上 &lt;code>TOOL_CALL_RESULT&lt;/code> 和 &lt;code>TOOL_CALL_CHUNK&lt;/code>。&lt;/p>
&lt;p>&lt;strong>共享状态（3 种）&lt;/strong>——这是 AG-UI 最有辨识度的部分。&lt;code>STATE_SNAPSHOT&lt;/code> 全量下发一份 typed state，之后 &lt;code>STATE_DELTA&lt;/code> 用 &lt;strong>RFC 6902 JSON Patch&lt;/strong> 增量同步。这意味着前端不只是&amp;quot;看&amp;quot;agent 生成文本，而是和 agent 共享一份可编辑的应用状态——agent 改了表单、改了画布、改了文档大纲，前端实时跟着变。CopilotKit 的生成式 UI 就建立在这上面。&lt;/p>
&lt;p>&lt;strong>活动消息（2 种）&lt;/strong>——&lt;code>ACTIVITY_SNAPSHOT&lt;/code>/&lt;code>ACTIVITY_DELTA&lt;/code>，聊天消息之间的结构化进度材料，为 generative UI 准备的通道。&lt;/p>
&lt;p>&lt;strong>推理（7 种）&lt;/strong>——&lt;code>REASONING_*&lt;/code> 一族把思维链流式透出（&lt;code>REASONING_MESSAGE_CONTENT&lt;/code> 的 &lt;code>delta&lt;/code>），甚至有 &lt;code>REASONING_ENCRYPTED_VALUE&lt;/code> 用于加密透传私有推理。另有 5 个旧的 &lt;code>THINKING_*&lt;/code> 事件已废弃、1.0.0 移除。&lt;/p>
&lt;p>&lt;strong>子代理（3 种）&lt;/strong>——&lt;code>SUBAGENT_STARTED&lt;/code>/&lt;code>FINISHED&lt;/code>/&lt;code>ERROR&lt;/code>，用 &lt;code>subagentRunId&lt;/code> + &lt;code>parentSubagentRunId&lt;/code> 构成树。有个字段很说明问题：&lt;code>SUBAGENT_STARTED&lt;/code> 带可选的 &lt;code>parentToolCallId&lt;/code>，直接支持&amp;quot;agent-as-tool&amp;quot;模式——子代理是父代理调用的一个工具（deep agents 的 &lt;code>task&lt;/code> 就是这个模式），事件树和工具调用树是同一棵。&lt;/p>
&lt;p>&lt;strong>特殊（2 种）&lt;/strong>——&lt;code>RAW&lt;/code>（底层框架事件透传）和 &lt;code>CUSTOM&lt;/code>（自定义事件通道）。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/ag-ui%E5%BD%93agent%E5%AD%A6%E4%BC%9A%E4%BA%86%E5%92%8C%E5%89%8D%E7%AB%AF%E8%AF%B4%E8%AF%9D/images/event-stream-model.svg"
loading="lazy"
alt="事件流是唯一事实源:BaseEvent 信封与 36 个枚举的九个功能组"
>&lt;/p>
&lt;h2 id="能力声明协议里藏着一份agent-名片">能力声明：协议里藏着一份&amp;quot;agent 名片&amp;quot;
&lt;/h2>&lt;p>读 Python SDK 源码时的一个意外发现：除了事件和输入类型，&lt;code>ag_ui/core/&lt;/code> 下还有一个 &lt;code>capabilities.py&lt;/code>（415 行），定义了一整套 &lt;strong>&lt;code>AgentCapabilities&lt;/code> 能力声明体系&lt;/strong>，十个分类：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>分类&lt;/th>
&lt;th>回答的问题&lt;/th>
&lt;th>典型字段&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>identity&lt;/code>&lt;/td>
&lt;td>你是谁&lt;/td>
&lt;td>name / type(框架标识) / version / provider / documentation_url&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>transport&lt;/code>&lt;/td>
&lt;td>怎么连你&lt;/td>
&lt;td>streaming / websocket / http_binary / push_notifications / &lt;strong>resumable&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>tools&lt;/code>&lt;/td>
&lt;td>你能调什么&lt;/td>
&lt;td>supported / 自带工具清单(完整 JSON Schema) / parallel_calls / &lt;strong>client_provided&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>output&lt;/code>&lt;/td>
&lt;td>你产出什么&lt;/td>
&lt;td>structured_output / supported_mime_types&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>state&lt;/code>&lt;/td>
&lt;td>你的状态怎么管&lt;/td>
&lt;td>snapshots / deltas / &lt;strong>memory&lt;/strong>(跨会话记忆) / persistent_state&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>multi_agent&lt;/code>&lt;/td>
&lt;td>你和谁协作&lt;/td>
&lt;td>delegation / handoffs / sub_agents 清单&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>reasoning&lt;/code>&lt;/td>
&lt;td>你的思考可见吗&lt;/td>
&lt;td>supported / streaming / &lt;strong>encrypted&lt;/strong>(零数据保留模式)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>multimodal&lt;/code>&lt;/td>
&lt;td>什么模态进、什么出&lt;/td>
&lt;td>input: image/audio/video/pdf/file; output: image/audio&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>execution&lt;/code>&lt;/td>
&lt;td>你的执行边界&lt;/td>
&lt;td>code_execution / sandboxed / max_iterations / max_execution_time&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>human_in_the_loop&lt;/code>&lt;/td>
&lt;td>人在哪介入&lt;/td>
&lt;td>approvals / interventions / feedback / &lt;strong>interrupts&lt;/strong> / approve_with_edits&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>关键语义写在 docstring 里：&lt;strong>&amp;ldquo;所有字段可选，缺省意味着未声明（unknown），不等于不支持&amp;rdquo;&lt;/strong>——典型的开放协议渐进声明风格；外加 &lt;code>custom&lt;/code> 逃生舱给集成特有能力。这份能力体系和我之前给自己协议设计 discovery 契约（&lt;code>RuntimeDescriptor&lt;/code>）时想解决的问题一模一样：让调用方在运行前就知道对端有什么。AG-UI 的答案粒度更细——它同时服务于&amp;quot;agent 市场、发现 UI、调试&amp;quot;三类消费场景，甚至 &lt;code>sub_agents&lt;/code> 清单的注释都写着&amp;quot;帮助客户端构建 agent 选择界面&amp;quot;。&lt;/p>
&lt;h2 id="输入全量历史的无状态哲学">输入：全量历史的无状态哲学
&lt;/h2>&lt;p>事件流往回走，输入往前送。AG-UI 的请求体 &lt;code>RunAgentInput&lt;/code> 长这样：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">RunAgentInput&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">BaseModel&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">thread_id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="c1"># 会话标识&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">run_id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="c1"># 本次运行,客户端生成&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">parent_run_id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kc">None&lt;/span> &lt;span class="c1"># agent-started-agent 场景的父 run&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">state&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Any&lt;/span> &lt;span class="c1"># 起始共享状态&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">messages&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">Message&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1"># 全量对话历史,按序&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tools&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">Tool&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1"># 前端工具定义&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">Context&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1"># 注入上下文&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">forwarded_props&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Any&lt;/span> &lt;span class="c1"># 透传段,中间层不得改动&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">resume&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">ResumeEntry&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kc">None&lt;/span> &lt;span class="c1"># 中断应答&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最有态度的是 &lt;code>messages&lt;/code>：&lt;strong>全量历史回传，按序，无旁路通道&lt;/strong>。服务器不存对话记忆，每一轮前端把完整历史发过来。这是刻意的——无状态服务器好扩展、好重放、好调试，代价是每轮请求变大、长对话依赖客户端裁剪。&lt;/p>
&lt;p>消息定义了七种角色：&lt;code>developer&lt;/code>、&lt;code>system&lt;/code>、&lt;code>assistant&lt;/code>（可带 OpenAI 风格的 &lt;code>tool_calls&lt;/code>）、&lt;code>user&lt;/code>（支持多模态：text/image/audio/video/document，URL 或 base64 inline）、&lt;code>tool&lt;/code>（工具结果）、&lt;code>activity&lt;/code>（渲染材料，发送前必须剥离，不作为恢复历史）、&lt;code>reasoning&lt;/code>。&lt;/p>
&lt;p>人在回路是一个完整闭环：&lt;code>RUN_FINISHED&lt;/code> 的 &lt;code>outcome&lt;/code> 可以是 &lt;code>{type: &amp;quot;interrupt&amp;quot;, interrupts: [...]}&lt;/code>——agent 停下来等输入，且源码里有校验：&lt;strong>interrupt outcome 至少携带一个 interrupt&lt;/strong>，空的直接构造失败。每个 interrupt 带 &lt;code>id&lt;/code>、&lt;code>reason&lt;/code>、可选的 &lt;code>response_schema&lt;/code> 和 &lt;code>expires_at&lt;/code>；客户端下一轮在 &lt;code>resume&lt;/code> 数组里逐条应答（&lt;code>resolved&lt;/code> 或 &lt;code>cancelled&lt;/code>，可带 &lt;code>payload&lt;/code>）——审批、确认、澄清问题，都是这个机制的产品化。能力声明里的 &lt;code>approve_with_edits&lt;/code> 说明连&amp;quot;批准前修改参数&amp;quot;都在协议考虑内。&lt;/p>
&lt;h2 id="从-sdk-源码里读出的工程细节">从 SDK 源码里读出的工程细节
&lt;/h2>&lt;p>我读了 Python SDK（&lt;code>ag-ui-protocol&lt;/code> 0.1.22）的几个核心模块，这几处注释值得单独说：&lt;/p>
&lt;p>&lt;strong>序列化的血泪史&lt;/strong>。基类 &lt;code>ConfiguredBaseModel&lt;/code> 自定义了 serializer：可选字段为 None 时，整个键从线上 JSON 省略，而不是写 &lt;code>null&lt;/code>。注释里明说了缘由——Python SDK 曾是唯一把 null 写上线的 producer，为此协议吃了三个兼容补丁（&lt;code>TOOL_CALL_START.parentMessageId&lt;/code>、&lt;code>TOOL_CALL_CHUNK.parentMessageId&lt;/code>、&lt;code>RUN_FINISHED.outcome&lt;/code>）。类型注释里那句&amp;quot;在基类上统一省略，才能保证每一条序列化路径都生效&amp;quot;是一位工程师被 null 坑过之后的防御性姿势。字段 camelCase 上线（&lt;code>alias_generator=to_camel&lt;/code>），&lt;code>extra=&amp;quot;allow&amp;quot;&lt;/code> 放行未知字段——协议演进不锁死旧 producer。&lt;/p>
&lt;p>&lt;strong>token 计数的跨语言红线&lt;/strong>。&lt;code>token_usage.py&lt;/code> 里有一个细节：所有计数上限被钳在 &lt;strong>2⁵³−1&lt;/strong>——不是 int64 的上限，而是因为&amp;quot;TypeScript 的 protobuf 解码器停在 &lt;code>Number.MAX_SAFE_INTEGER&lt;/code>，这才是各语言绑定之间的真实天花板&amp;quot;。而且注释明确写了这个结构会喂给匿名遥测，&lt;strong>不允许携带任何内容字段&lt;/strong>（无 prompt、无补全、无 thread/run/user ID），只有 provider/model 标签和纯数字。跨语言兼容和隐私边界都焊死在类型定义里。&lt;/p>
&lt;p>&lt;strong>metadata 的克制&lt;/strong>。每个事件都带可选 &lt;code>metadata&lt;/code>，开放键值空间，但保留 &lt;code>&amp;quot;ag-ui&amp;quot;&lt;/code> 键给协议自己用——而且是纯约定，不做运行时强制。注释的原话：验证它的 shape 会&amp;quot;contradict that&amp;quot;（与开放性矛盾）。&lt;/p>
&lt;p>&lt;strong>双编码传输&lt;/strong>。&lt;code>encoder.py&lt;/code> 不只是 SSE——协议定义了二进制编码，媒体类型 &lt;code>application/vnd.ag-ui.event+proto&lt;/code>（protobuf over HTTP），TypeScript 侧有独立的 &lt;code>@ag-ui/proto&lt;/code> 包维护 &lt;code>.proto&lt;/code> 定义。文本 SSE 用于开发调试，二进制用于生产，同一套事件类型两种编码。&lt;/p>
&lt;h2 id="运行时管线一次-run-的完整解剖">运行时管线：一次 run 的完整解剖
&lt;/h2>&lt;p>前面讲的是协议的&amp;quot;静态&amp;quot;部分（类型和字段）。协议真正的工程含量在客户端 SDK 的&lt;strong>运行时管线&lt;/strong>里——一次 &lt;code>runAgent()&lt;/code> 调用背后是一条精心编排的 RxJS 管道。以 TypeScript &lt;code>@ag-ui/client&lt;/code> 0.0.59 的 &lt;code>AbstractAgent.runAgent()&lt;/code>（&lt;code>src/agent/agent.ts&lt;/code>）为例，事件从网络到你的回调要穿过这条流水线：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">run()（子类实现,发 HTTP/SSE 请求）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ↓ pipe 依次串接:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">① middleware 链 ← reduceRight 组装成洋葱模型
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">② transformChunks ← CHUNK 快捷事件展开成三段式
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">③ verifyEvents ← 事件文法状态机校验
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">④ takeUntil(detach$) ← 单次运行的中止信号
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">⑤ apply ← 把事件归约进 messages/state
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">⑥ processApplyEvents ← 分发到 subscriber 回调
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">⑦ catchError / finalize ← 错误与收尾
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每一层都可以单独讲。这正是&amp;quot;看哪些文件&amp;quot;的答案：读懂这七个环节，你就读懂了 AG-UI 客户端的全部。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/ag-ui%E5%BD%93agent%E5%AD%A6%E4%BC%9A%E4%BA%86%E5%92%8C%E5%89%8D%E7%AB%AF%E8%AF%B4%E8%AF%9D/images/runtime-pipeline.svg"
loading="lazy"
alt="一次 run 的运行管线:事件从网络到回调穿过七个环节"
>&lt;/p>
&lt;h3 id="-middleware洋葱模型的拦截器">① Middleware：洋葱模型的拦截器
&lt;/h3>&lt;p>中间件机制定义在 &lt;code>src/middleware/middleware.ts&lt;/code>。核心接口只有一个方法：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">abstract&lt;/span> &lt;span class="kr">class&lt;/span> &lt;span class="nx">Middleware&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">abstract&lt;/span> &lt;span class="nx">run&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">input&lt;/span>: &lt;span class="kt">RunAgentInput&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">next&lt;/span>: &lt;span class="kt">AbstractAgent&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">Observable&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">BaseEvent&lt;/span>&lt;span class="p">&amp;gt;;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 调下一个 agent,附带 chunk 展开
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">protected&lt;/span> &lt;span class="nx">runNext&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">input&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">run&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">input&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">pipe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">transformChunks&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 调下一个,且把每一步之后的 messages/state 一起带出来
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">protected&lt;/span> &lt;span class="nx">runNextWithState&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">input&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">Observable&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">EventWithState&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>组装方式是 &lt;code>agent.ts&lt;/code> 里的 &lt;code>reduceRight&lt;/code>——数组里&lt;strong>后注册的中间件先执行&lt;/strong>，&lt;code>next&lt;/code> 指向链上更内层的 agent（最内层是真正 agent 的 &lt;code>run()&lt;/code>）。这就是经典的洋葱模型，和 Koa/Express 中间件一个思想。&lt;/p>
&lt;p>有意思的是 &lt;code>runNextWithState&lt;/code> 的实现：它用一个 &lt;code>ReplaySubject&lt;/code> 把事件喂给 &lt;code>defaultApplyEvents&lt;/code>（事件归约器），让中间件在&lt;strong>每个事件后&lt;/strong>都能拿到&amp;quot;应用了这个事件之后&amp;quot;的 messages 和 state——中间件因此能基于语义状态做决策，而不只是看原始事件。实现里那句 &lt;code>await new Promise(resolve =&amp;gt; setTimeout(resolve, 0))&lt;/code> 是给归约器留一个微任务窗口同步状态，朴素但有效。&lt;/p>
&lt;p>官方仓库 &lt;code>middlewares/&lt;/code> 目录下有 6 个可参考的成品：&lt;code>event-throttle&lt;/code>（按帧率节流 + 合并 delta）、&lt;code>mcp&lt;/code>（把 MCP 工具桥接进事件流）、&lt;code>a2a&lt;/code>（A2A 协议互转）、&lt;code>a2ui&lt;/code>、&lt;code>mcp-apps&lt;/code>，外加一个 &lt;code>middleware-starter&lt;/code> 脚手架。&lt;/p>
&lt;h3 id="-三个值得抄的中间件设计">② 三个值得抄的中间件设计
&lt;/h3>&lt;p>&lt;strong>EventThrottleMiddleware&lt;/strong>（&lt;code>middlewares/event-throttle-middleware/src/index.ts&lt;/code>）——把高频 delta 按时间窗（默认 16ms ≈ 60fps）和最小字符数节流并合并，防止前端被打爆。它的三个设计决策都写满了&amp;quot;为什么&amp;quot;：&lt;/p>
&lt;p>其一，&lt;strong>只对白名单事件做缓冲&lt;/strong>。&lt;code>BUFFERABLE_EVENT_TYPES&lt;/code> 是显式白名单（各种 &lt;code>*_CHUNK&lt;/code>、&lt;code>*_CONTENT&lt;/code>、&lt;code>STATE_*&lt;/code>、&lt;code>ACTIVITY_*&lt;/code>），&lt;strong>不在表内的事件一律立即透传&lt;/strong>。注释原话：&amp;ldquo;这是白名单而非黑名单，这样协议将来新增的事件类型默认走立即透传——对生命周期/边界事件来说，这是更安全的失败模式。&amp;ldquo;新事件宁可多一次渲染，也不能被错误地延迟。&lt;/p>
&lt;p>其二，&lt;strong>合并只在同一 subagent lane 内进行&lt;/strong>。chunk 的合并键是 &lt;code>JSON.stringify([kind, owner, entityId])&lt;/code>——用 JSON 编码而不是分隔符拼接，因为 owner/id 是任意字符串，&amp;ldquo;任何分隔符都可能出现在某个分量里，把两个不同的 (owner, id) 别名到同一个键上&amp;rdquo;。这个注释值得所有写缓存键的人读一遍。&lt;/p>
&lt;p>其三，&lt;strong>metadata 反向合并&lt;/strong>。合并两个 chunk 时，&lt;code>role&lt;/code>/&lt;code>name&lt;/code> 取&lt;strong>第一个&lt;/strong>（它们只出现在首 chunk），但 &lt;code>metadata&lt;/code> 取&lt;strong>最后一个&lt;/strong>并按 key 逐项合并——因为 metadata 是设计成&amp;quot;最后到达&amp;quot;的，携带 usage 和 finish reason。&amp;ldquo;在这里丢掉它会吞掉一个纯 usage 的尾 chunk。&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>FilterToolCallsMiddleware&lt;/strong>（&lt;code>src/middleware/filter-tool-calls.ts&lt;/code>）——按工具名拦截工具调用事件。看起来平平无奇，但注释记了一个真实并发 bug：中间件实例是跨 run 复用的，如果被拦截的 toolCallId 集合放在&lt;strong>实例&lt;/strong>上，&amp;ldquo;一个卡住的 run 的订阅还开着时，下一个 run 启动会抹掉那些还在过滤这个卡住的 run 的 id，于是它被禁的工具的 &lt;code>TOOL_CALL_ARGS&lt;/code>/&lt;code>END&lt;/code>/&lt;code>RESULT&lt;/code> 就开始漏出来了&amp;rdquo;。解法是用 &lt;code>defer&lt;/code> 给&lt;strong>每个订阅&lt;/strong>一份独立集合，并在 &lt;code>RUN_STARTED&lt;/code> 时重置。并发场景下&amp;quot;状态放实例还是放订阅&amp;quot;这个坑，这是教科书级的案例。&lt;/p>
&lt;p>&lt;strong>四个 BackwardCompatibility 中间件&lt;/strong>（&lt;code>src/middleware/backward-compatibility-0-0-*.ts&lt;/code>）——这是我见过最优雅的协议演进手法。agent 构造时读对端声明的 &lt;code>maxVersion&lt;/code>，按版本号&lt;strong>自动插到中间件链最前面&lt;/strong>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">compareVersions&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">maxVersion&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;0.0.39&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">middlewares&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">unshift&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">new&lt;/span> &lt;span class="nx">BackwardCompatibility_0_0_39&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">compareVersions&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">maxVersion&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;0.0.57&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 0.0.57 之前的 agent 不认识 subagent:剥掉 subagentRunId、丢弃 SUBAGENT_* 事件
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">middlewares&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">unshift&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">new&lt;/span> &lt;span class="nx">BackwardCompatibility_0_0_57&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>0.0.45&lt;/code> 中间件把废弃的 &lt;code>THINKING_*&lt;/code> 事件翻译成新的 &lt;code>REASONING_*&lt;/code>，&lt;code>0.0.47&lt;/code> 把老的 &lt;code>BinaryInputContent&lt;/code> 映射成新的分模态输入类型，&lt;code>0.0.57&lt;/code> 给不认识 subagent 的老客户端剥掉归属字段。&lt;strong>每个协议破坏性变更都固化为一个可测试、可移除的适配中间件&lt;/strong>，而不是散落在业务代码里的 if-else。协议处于日更的 draft 阶段还能保持向后兼容，靠的就是这套机制。&lt;/p>
&lt;h3 id="-verifyevents1052-行的事件文法状态机">③ verifyEvents：1052 行的事件文法状态机
&lt;/h3>&lt;p>&lt;code>src/verify/verify.ts&lt;/code> 是整个客户端最硬核的文件——它把&amp;quot;合法的 AG-UI 事件流&amp;quot;编码成了一个状态机，逐事件校验。维护四组&amp;quot;哪些实体还开着&amp;quot;的集合（文本消息、工具调用、推理、活动），外加每个实体类型的&lt;strong>归属映射&lt;/strong>（哪个 subagent 开的）。&lt;/p>
&lt;p>这个文件的注释本身就是一部协议演进史，每条规则背后都是真实 bug：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>为什么 owners 按实体类别分桶&lt;/strong>：一个 message 和一个 tool call 可以都叫 &amp;ldquo;x&amp;rdquo; 而不冲突。早期单个桶时，&amp;ldquo;tool call 的写入会覆盖 message 的 owner，导致后续一个加密值拿了错误的 owner 做校验而被接受&amp;rdquo;。&lt;/li>
&lt;li>&lt;strong>为什么 owners 关闭后不清除&lt;/strong>：&lt;code>REASONING_ENCRYPTED_VALUE(subtype=&amp;quot;tool-call&amp;quot;)&lt;/code> 合法地要在 &lt;code>TOOL_CALL_END&lt;/code> &lt;strong>之后&lt;/strong>到达，&amp;ldquo;清掉 owner 会让这个不匹配变得无法匹配从而接受一个错误的&amp;rdquo;。&lt;/li>
&lt;li>&lt;strong>为什么 step 要按 (owner, name) 嵌套 Map 而不是拼字符串&lt;/strong>：曾经用分隔符拼接 owner 和 name，结果&amp;quot;没有 owner 的父级会和 subagent id 是空字符串的实体撞键&amp;rdquo;——而空字符串是合法的 opaque id。注释直言：&amp;ldquo;有个设计伙伴从真实的 deepagents run 里报回了这个 bug：合法的嵌套 step 被拒，而非法的跨 owner 关闭却被接受。&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>为什么 &lt;code>STEP_STARTED&lt;/code> 判重按 owner 分&lt;/strong>：父 agent 和子 agent 常常跑同一个图，两边&lt;strong>同时&lt;/strong>有一个叫 &amp;ldquo;tools&amp;rdquo; 的 step 是合法的（父的包着委派，子的是自己的内部工作）——按名字单独判重会把合法嵌套当成错误拒绝。&lt;/li>
&lt;/ul>
&lt;p>这些不是设计文档里能写出来的东西，全是生产流量打磨出来的。想理解 AG-UI 的边界条件，&lt;code>verify.ts&lt;/code> 比 spec 更有信息量。&lt;/p>
&lt;h3 id="-transformchunks快捷事件的双向门">④ transformChunks：快捷事件的双向门
&lt;/h3>&lt;p>&lt;code>src/chunks/transform.ts&lt;/code>（564 行）负责 &lt;code>*_CHUNK&lt;/code> 快捷事件和 &lt;code>*_START/CONTENT/END&lt;/code> 三段式之间的转换。核心概念是 &lt;strong>lane（通道）&lt;/strong>：每个 subagent 一条，&amp;ldquo;一条 lane 上最多有一路正在组装的流，因为 chunk 的简写只靠&amp;rsquo;和之前一样&amp;rsquo;来标识延续&amp;rdquo;。这里也有个精妙的元数据规则——chunk 的 metadata 会扩散到由它合成的每个事件上，&lt;strong>但绝不施加于关闭前一条消息的合成 &lt;code>*_END&lt;/code>&lt;/strong>，&amp;ldquo;这就是防止某个 chunk 的 metadata 泄漏到它正在关闭的那条消息上的原因&amp;rdquo;。&lt;/p>
&lt;h3 id="-apply-与-subscribers归约器和观察者">⑤ apply 与 subscribers：归约器和观察者
&lt;/h3>&lt;p>&lt;code>src/apply/default.ts&lt;/code>（约 1500 行）是事件归约器——一个巨型 &lt;code>switch(event.type)&lt;/code>，把每一种事件映射成 &lt;code>messages&lt;/code>/&lt;code>state&lt;/code> 的变更（&lt;code>AgentStateMutation&lt;/code>）。这是&amp;quot;事件即事实、状态即投影&amp;quot;落地的地方。它对 &lt;code>*_CHUNK&lt;/code> 事件直接抛错（&lt;code>TEXT_MESSAGE_CHUNK must be transformed before being applied&lt;/code>）——这是管线的纪律：chunk 必须先过 &lt;code>transformChunks&lt;/code> 这道门，归约器只认展开后的三段式。&lt;/p>
&lt;p>&lt;code>src/agent/subscriber.ts&lt;/code>（408 行）则是应用层的观察者接口，钩子分三类：生命周期（&lt;code>onRunInitialized&lt;/code>/&lt;code>onRunFailed&lt;/code>/&lt;code>onRunFinalized&lt;/code>）、按事件类型（&lt;code>onTextMessageContentEvent&lt;/code> 连 &lt;code>textMessageBuffer&lt;/code> 都给你拼好了、&lt;code>onToolCallArgsEvent&lt;/code> 带 &lt;code>toolCallBuffer&lt;/code> 和 &lt;code>toolCallName&lt;/code>）、变更通知（&lt;code>onMessagesChanged&lt;/code>/&lt;code>onStateChanged&lt;/code>）。&lt;/p>
&lt;p>这里有个订阅者协议的关键设计：&lt;strong>订阅者不直接改状态，而是返回一个 &lt;code>AgentStateMutation&lt;/code>&lt;/strong>，由 &lt;code>runSubscribersWithMutation&lt;/code> 统一应用。多个订阅者的 mutation 可以 &lt;code>stopPropagation&lt;/code> 拦截后续订阅者——订阅者之间也是中间件式的。实现里还有一段性能注释：dev 环境会对输入做 &lt;code>structuredClone&lt;/code> + &lt;code>deepFreeze&lt;/code> 来抓&amp;quot;原地修改&amp;quot;的 bug，但这个守卫是&amp;quot;每个事件最大的一笔分配&amp;rdquo;，流式大参数时会把 V8 堆打爆，所以它在生产环境关闭、dev 环境下 payload 过大时也跳过——&amp;ldquo;常见的不改状态的事件cost 零次克隆&amp;rdquo;。&lt;/p>
&lt;h2 id="怎么读-ag-ui-的源码一份导航">怎么读 AG-UI 的源码：一份导航
&lt;/h2>&lt;p>如果你要接 AG-UI 或者只是想学它的设计，按这个顺序读最省时间：&lt;/p>
&lt;p>&lt;strong>第一梯队——协议本身（先读，两小时）&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>文件&lt;/th>
&lt;th>读什么&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>sdks/python/ag_ui/core/events.py&lt;/code>&lt;/td>
&lt;td>全部事件类型（555 行，每个类型的字段和约束都在）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sdks/python/ag_ui/core/types.py&lt;/code>&lt;/td>
&lt;td>&lt;code>RunAgentInput&lt;/code>、七种消息、多模态、中断类型（416 行）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sdks/python/ag_ui/core/capabilities.py&lt;/code>&lt;/td>
&lt;td>能力声明体系（415 行，看注释里的&amp;quot;为什么这样设计&amp;quot;）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sdks/python/ag_ui/encoder/encoder.py&lt;/code>&lt;/td>
&lt;td>SSE 与二进制编码（很短，看序列化约定）&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>第二梯队——客户端运行时（TypeScript &lt;code>packages/client/src/&lt;/code>，深读）&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>文件&lt;/th>
&lt;th>读什么&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>agent/agent.ts&lt;/code>&lt;/td>
&lt;td>运行管线七环节 + middleware 链组装 + 生命周期（看 &lt;code>runAgent&lt;/code>）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>middleware/middleware.ts&lt;/code>&lt;/td>
&lt;td>洋葱模型基类 + &lt;code>runNextWithState&lt;/code> 的状态追踪&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>middleware/backward-compatibility-0-0-*.ts&lt;/code>&lt;/td>
&lt;td>协议演进如何固化成中间件（四份文件一起看）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>verify/verify.ts&lt;/code>&lt;/td>
&lt;td>事件文法状态机（注释里的 bug 案例最值钱）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>apply/default.ts&lt;/code>&lt;/td>
&lt;td>事件归约器（看状态怎么从事件投影出来）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>chunks/transform.ts&lt;/code> + &lt;code>agent/subscriber.ts&lt;/code>&lt;/td>
&lt;td>chunk 双向门 + 订阅者协议&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>第三梯队——生态接线&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>目录&lt;/th>
&lt;th>读什么&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>integrations/langgraph/{python,typescript}/&lt;/code>&lt;/td>
&lt;td>一个真实框架适配层的全貌（含中断处理、SSE 断线恢复测试）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>middlewares/event-throttle-middleware/&lt;/code>&lt;/td>
&lt;td>生产级中间件的最完整示例（含性能和不变量注释）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>apps/dojo/&lt;/code>&lt;/td>
&lt;td>官方演示站，交互式体验所有事件类型&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>一个实用建议：读 TS 客户端时&lt;strong>先读注释再读代码&lt;/strong>——这个仓库的注释密度和坦诚度罕见，&amp;ldquo;我们当初这么写、后来发现 X 有问题、现在的写法是 Y、原因是 Z&amp;quot;的叙事随处可见。很多边界条件 spec 里没有，但注释里有。&lt;/p>
&lt;h2 id="生态谁在用它">生态：谁在用它
&lt;/h2>&lt;p>AG-UI 的知名产品就是创造它的 &lt;a class="link" href="https://www.copilotkit.ai/" target="_blank" rel="noopener"
>CopilotKit&lt;/a>（React 前端 agent 框架，&lt;code>useCopilotAction&lt;/code> 的 human-in-the-loop 模式就是协议里前端工具机制的产品化），加上官方 demo 站 &lt;strong>Dojo&lt;/strong>（&lt;code>apps/dojo&lt;/code>，仓库内）。&lt;/p>
&lt;p>但真正的覆盖面在 &lt;code>integrations/&lt;/code> 目录里，18 个框架适配层的原文清单：LangGraph、LangChain、CrewAI、Microsoft Agent Framework、Google ADK、AWS Strands、Agno、Mastra、Pydantic AI、LlamaIndex、AG2、Vercel AI SDK、watsonx、Langroid、&lt;strong>Claude Agent SDK&lt;/strong>、&lt;strong>Claude Managed Agents&lt;/strong>、A2A、Agent Spec，另有两个 server starter 模板。社区 SDK 覆盖 Dart、Rust、Ruby、C++、Kotlin、Go、Java。客户端不止浏览器——终端、React Native、Slack、Teams 都能当 AG-UI client。&lt;/p>
&lt;p>不过要校准一下预期：AG-UI 的&amp;quot;知名&amp;quot;集中在&lt;strong>框架生态&lt;/strong>，不是终端产品。和 MCP 一样，它是管道协议，终端用户永远看不到它——人们用的是接了它的 CopilotKit 应用和 LangGraph agent。&lt;/p>
&lt;h2 id="和有状态-runtime-的张力">和有状态 Runtime 的张力
&lt;/h2>&lt;p>最后说一个我实际设计契约时撞上的问题。AG-UI 的无状态模型很优雅，但我的 Runtime 是&lt;strong>有状态&lt;/strong>的：对话记忆按 &lt;code>session_id&lt;/code> 存在服务端（SQLite checkpointer），每次 Run 只需要送最新的问题。&lt;/p>
&lt;p>两套范式接在一起时的正确姿势是：AG-UI 前端发全量 &lt;code>messages&lt;/code>，adapter 只取&lt;strong>最后一条 user 消息&lt;/strong>作为 Runtime 的输入，历史由服务端记忆提供；事件流方向则反过来，把 Runtime 的事件投影成 AG-UI 事件喂给前端。适配层的本质是两种会话范式之间的翻译。&lt;/p>
&lt;p>还有几块 AG-UI 有、多数自研契约没有的东西，值得列为后续演进的清单：interrupt/resume 的人在回路（自研契约通常只有&amp;quot;取消&amp;rdquo;，没有&amp;quot;挂起等输入&amp;quot;）、共享状态的 StateSnapshot/StateDelta、子代理事件树、前端工具的反向执行回路，以及那份克制的能力声明体系。&lt;/p>
&lt;p>&lt;img src="https://www.zata.cc/p/ag-ui%E5%BD%93agent%E5%AD%A6%E4%BC%9A%E4%BA%86%E5%92%8C%E5%89%8D%E7%AB%AF%E8%AF%B4%E8%AF%9D/images/stateful-vs-stateless.svg"
loading="lazy"
alt="两种会话范式对比:AG-UI 的无状态服务器 vs Canonical Runtime 的服务端记忆"
>&lt;/p>
&lt;p>三层协议凑齐之后的图景其实很清晰：MCP 让 agent 拿到工具，A2A 让 agent 找到同伴，AG-UI 让用户看见过程。你的 agent 用哪套实现无所谓——只要它在这三个边界上说标准语言，就同时获得了被所有前端渲染、被所有 agent 编排、被所有工具增强的资格。&lt;/p>
&lt;hr>
&lt;p>&lt;em>本文基于 &lt;code>ag-ui-protocol/ag-ui&lt;/code> main@5f32a64ee999（2026-09-09）、Python SDK ag-ui-protocol 0.1.22、TypeScript SDK @ag-ui/client 0.0.59 写成，源码研读覆盖 &lt;code>sdks/python/ag_ui/core/&lt;/code>（events / types / capabilities / token_usage / encoder）与 &lt;code>sdks/typescript/packages/client/src/&lt;/code>（agent / middleware / verify / apply / chunks / interrupts）、&lt;code>middlewares/event-throttle-middleware/&lt;/code>。协议处于 draft 阶段、日更演进，引用前请核对最新版本。&lt;/em>&lt;/p></description></item><item><title>Agent Run 流式协议：事件溯源、SSE 投影与断线恢复</title><link>https://www.zata.cc/p/agent-run-%E6%B5%81%E5%BC%8F%E5%8D%8F%E8%AE%AE%E4%BA%8B%E4%BB%B6%E6%BA%AF%E6%BA%90sse-%E6%8A%95%E5%BD%B1%E4%B8%8E%E6%96%AD%E7%BA%BF%E6%81%A2%E5%A4%8D/</link><pubDate>Mon, 07 Sep 2026 10:30:00 +0800</pubDate><guid>https://www.zata.cc/p/agent-run-%E6%B5%81%E5%BC%8F%E5%8D%8F%E8%AE%AE%E4%BA%8B%E4%BB%B6%E6%BA%AF%E6%BA%90sse-%E6%8A%95%E5%BD%B1%E4%B8%8E%E6%96%AD%E7%BA%BF%E6%81%A2%E5%A4%8D/</guid><description>&lt;img src="https://www.zata.cc/p/agent-run-%E6%B5%81%E5%BC%8F%E5%8D%8F%E8%AE%AE%E4%BA%8B%E4%BB%B6%E6%BA%AF%E6%BA%90sse-%E6%8A%95%E5%BD%B1%E4%B8%8E%E6%96%AD%E7%BA%BF%E6%81%A2%E5%A4%8D/images/index/index.svg" alt="Featured image of post Agent Run 流式协议：事件溯源、SSE 投影与断线恢复" />&lt;p>很多 Agent 系统的流式输出，本质上是把模型服务的 chunk 直接转发给浏览器。这样做上线很快，但一旦遇到刷新页面、断线重连、审计和取消，就会立刻发现：&lt;strong>前端消费的不是一个可恢复的事件流，而是一条一次性管道。&lt;/strong>&lt;/p>
&lt;p>我现在的做法是把协议拆成三层：Runtime 先产出领域事件，Adapter 统一映射成 canonical 事件并落库，最后由 SSE 端点从事件库里做投影。这篇文章记录这套协议的实际形态，以及它为什么这样设计。&lt;/p>
&lt;h2 id="一三层结构">一、三层结构
&lt;/h2>&lt;p>整条链路可以这样理解：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Runtime / Runner
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ AgentEvent 执行器内部事件
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ Adapter 映射
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ AgentRunEvent canonical 事实，落库并分配 seq
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ SSE 端点 committed event 的投影
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ 前端
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="runner-端口层">Runner 端口层
&lt;/h3>&lt;p>&lt;code>AgentEvent&lt;/code> 是 Runtime 对外的最小事件模型，目前有 7 种类型：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>类型&lt;/th>
&lt;th>专用字段&lt;/th>
&lt;th>含义&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>state_change&lt;/code>&lt;/td>
&lt;td>&lt;code>state&lt;/code>&lt;/td>
&lt;td>Runtime 内部状态迁移&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>delta&lt;/code>&lt;/td>
&lt;td>&lt;code>content&lt;/code>&lt;/td>
&lt;td>模型文本增量&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>tool_call&lt;/code>&lt;/td>
&lt;td>&lt;code>tool_name&lt;/code> / &lt;code>tool_call_id&lt;/code> / &lt;code>tool_args&lt;/code>&lt;/td>
&lt;td>工具调用开始&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>tool_result&lt;/code>&lt;/td>
&lt;td>&lt;code>tool_name&lt;/code> / &lt;code>tool_call_id&lt;/code> / &lt;code>tool_result&lt;/code>&lt;/td>
&lt;td>工具结果摘要&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>artifact&lt;/code>&lt;/td>
&lt;td>&lt;code>artifact&lt;/code>&lt;/td>
&lt;td>结构化产物元数据&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>done&lt;/code>&lt;/td>
&lt;td>&lt;code>answer&lt;/code>&lt;/td>
&lt;td>最终答案全文&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>error&lt;/code>&lt;/td>
&lt;td>&lt;code>error_code&lt;/code> / &lt;code>error_message&lt;/code>&lt;/td>
&lt;td>失败&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>这一层有意做薄。不管 Runtime 是 LangGraph、DeepAgents，还是新接的沙箱执行器，只要转换成这 7 类事件，后端后面的所有逻辑都不用重复实现。&lt;/p>
&lt;h3 id="canonical-层">Canonical 层
&lt;/h3>&lt;p>Adapter 把 Runtime 事件映射成 canonical 事件。canonical 层不是照抄 Runtime 词汇，而是补齐运行生命周期：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Canonical 事件&lt;/th>
&lt;th>来源&lt;/th>
&lt;th>payload 要点&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>run.started&lt;/code>&lt;/td>
&lt;td>Adapter 生成&lt;/td>
&lt;td>&lt;code>started_at&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>message.started&lt;/code>&lt;/td>
&lt;td>Adapter 生成&lt;/td>
&lt;td>&lt;code>message_id&lt;/code>、&lt;code>role&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>message.delta&lt;/code>&lt;/td>
&lt;td>&lt;code>delta&lt;/code>&lt;/td>
&lt;td>&lt;code>message_id&lt;/code>、&lt;code>content_index&lt;/code>、&lt;code>delta&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>tool.call.started&lt;/code>&lt;/td>
&lt;td>&lt;code>tool_call&lt;/code>&lt;/td>
&lt;td>&lt;code>tool_call_id&lt;/code>、&lt;code>tool_name&lt;/code>、&lt;code>arguments&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>tool.call.completed&lt;/code>&lt;/td>
&lt;td>&lt;code>tool_result&lt;/code>&lt;/td>
&lt;td>&lt;code>result&lt;/code>、&lt;code>result_checksum&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>artifact.created&lt;/code>&lt;/td>
&lt;td>&lt;code>artifact&lt;/code>&lt;/td>
&lt;td>产物 metadata&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>message.completed&lt;/code>&lt;/td>
&lt;td>&lt;code>done&lt;/code>&lt;/td>
&lt;td>&lt;code>content&lt;/code>、&lt;code>content_checksum&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>run.completed&lt;/code>&lt;/td>
&lt;td>&lt;code>done&lt;/code> 后置&lt;/td>
&lt;td>&lt;code>message_id&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>run.failed&lt;/code>&lt;/td>
&lt;td>&lt;code>error&lt;/code>&lt;/td>
&lt;td>&lt;code>error.code&lt;/code>、&lt;code>error.message&lt;/code>、&lt;code>error.retryable&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>run.cancelled&lt;/code>&lt;/td>
&lt;td>取消路径&lt;/td>
&lt;td>&lt;code>external_stop_confirmed&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>落库后的 envelope 长这样：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;run_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;run_xxx&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;seq&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;event_type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;message.delta&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;occurred_at&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2026-09-07T10:30:00.123456&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;recorded_at&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2026-09-07T10:30:00.126000&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;agent_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;freight_agent&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;agent_snapshot_checksum&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;payload&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;message_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;message_xxx&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;delta&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;payload_checksum&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;schema_version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;trace_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;span_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这里最重要的是 &lt;code>seq&lt;/code>。Runtime 事件本身不携带权威序号；Adapter 只提交 candidate，repository 落库时按 &lt;code>last_event_seq + 1&lt;/code> 分配。&lt;strong>事件一旦提交，顺序就是系统事实。&lt;/strong>&lt;/p>
&lt;h2 id="二sse-wire-format">二、SSE wire format
&lt;/h2>&lt;p>对外入口是：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-http" data-lang="http">&lt;span class="line">&lt;span class="cl">&lt;span class="err">GET /api/agent-runs/{run_id}/events
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>SSE 使用命名帧：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">id: 4
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">event: message.delta
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">data: {&amp;#34;run_id&amp;#34;:&amp;#34;run_xxx&amp;#34;,&amp;#34;seq&amp;#34;:4,&amp;#34;event_type&amp;#34;:&amp;#34;message.delta&amp;#34;,...}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>几个关键语义：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>重放优先。&lt;/strong> 客户端可以用 &lt;code>?after_seq=&lt;/code> 查询参数，也可以用标准 SSE 的 &lt;code>Last-Event-ID&lt;/code> 请求头；服务端取较大值，然后从事件库里回放。&lt;/li>
&lt;li>&lt;strong>游标安全。&lt;/strong> 游标为负，或大于当前 &lt;code>last_event_seq&lt;/code>，返回 422，避免客户端拿着错误游标等一个永远不会来的事件。&lt;/li>
&lt;li>&lt;strong>实时推送。&lt;/strong> 端点每 250ms 从 committed repository 拉取新事件；发现序号 gap 就断流，不静默跳过。&lt;/li>
&lt;li>&lt;strong>keep-alive。&lt;/strong> 空闲 15 秒发送 SSE 注释帧 &lt;code>: keep-alive&lt;/code>。&lt;/li>
&lt;li>&lt;strong>自然终止。&lt;/strong> Run 进入终态且事件全部送完，连接主动关闭。&lt;/li>
&lt;/ol>
&lt;p>一次成功 Run 的典型序列：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">id: 1 event: run.started
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">id: 2 event: message.started
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">id: 3 event: message.delta
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">id: 4 event: tool.call.started
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">id: 5 event: tool.call.completed
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">id: 6 event: message.delta
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">id: 7 event: artifact.created
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">id: 8 event: message.completed
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">id: 9 event: run.completed
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>失败或取消时，终态分别是 &lt;code>run.failed&lt;/code> 或 &lt;code>run.cancelled&lt;/code>。Adapter 用一个终态闸保证三者最多出现一个。&lt;/p>
&lt;h2 id="三为什么先落库再投影">三、为什么先落库，再投影
&lt;/h2>&lt;p>这是整套协议的核心取舍。&lt;/p>
&lt;h3 id="断线恢复变成查询问题">断线恢复变成查询问题
&lt;/h3>&lt;p>客户端刷新页面后，不需要 Runtime 重跑一遍，也不需要服务端缓存整个连接状态。只要带上 &lt;code>Last-Event-ID: 5&lt;/code>，服务端就查询 &lt;code>seq &amp;gt; 5&lt;/code> 的事件继续发。断线恢复从连接层问题退化成数据库查询问题。&lt;/p>
&lt;h3 id="runtime-崩溃不丢执行事实">Runtime 崩溃不丢执行事实
&lt;/h3>&lt;p>事件写入 committed store 之后，Runtime 进程是否还活着，不影响前端重放历史。审计、排障和 UI 恢复消费的是同一份数据。&lt;/p>
&lt;h3 id="多-runtime-共享同一套前端协议">多 Runtime 共享同一套前端协议
&lt;/h3>&lt;p>不同执行器的事件风格可能差异很大。Adapter 负责把差异吸收在入口处，前端只认识 canonical 词汇。新增 Runtime 时，可以避免出现“沙箱一套流、业务 Agent 一套流”的分裂。&lt;/p>
&lt;h3 id="工具调用能稳定配对">工具调用能稳定配对
&lt;/h3>&lt;p>&lt;code>tool_call_id&lt;/code> 优先使用底层模型下发的 id。如果 Runtime 丢了开始事件，Adapter 会补发一个 &lt;code>tool.call.started&lt;/code>，让 UI 仍然成对渲染。这个“配对自愈”很小心地只影响展示完整性，不把 Run 打成失败。&lt;/p>
&lt;h2 id="四当前边界和改进方向">四、当前边界和改进方向
&lt;/h2>&lt;p>这套协议也有几个明显的边界。&lt;/p>
&lt;p>&lt;strong>SSE 出口是客户端胖投影。&lt;/strong> 目前 &lt;code>data&lt;/code> 帧直接序列化完整 envelope，&lt;code>agent_snapshot_checksum&lt;/code>、&lt;code>provenance&lt;/code>、&lt;code>payload_checksum&lt;/code> 等审计字段也被发给前端。短期省事，长期更适合收敛成只包含 &lt;code>seq&lt;/code>、&lt;code>event_type&lt;/code>、&lt;code>occurred_at&lt;/code>、&lt;code>payload&lt;/code> 的客户端投影。&lt;/p>
&lt;p>&lt;strong>实时性靠 250ms 轮询。&lt;/strong> 对文本流足够，但并发观看的连接多了，数据库查询会线性放大。合理演进是加一个进程内或跨实例 pub/sub，只负责“唤醒”轮询，不负责权威事件投递；事件仍然以数据库为准。&lt;/p>
&lt;p>&lt;strong>内容模型只有文本增量。&lt;/strong> &lt;code>message.delta&lt;/code> 目前只有 &lt;code>content_index&lt;/code> 和文本，没有结构化 content block。未来要支持图片、图表卡片或分块富文本，应增量引入 block 类型，而不是扩出新的顶层事件。&lt;/p>
&lt;p>&lt;strong>&lt;code>state_change&lt;/code> 是死词汇。&lt;/strong> Runtime 端口定义了它，但 canonical Adapter 不会映射。后续应该删除或明确语义，避免接入方误以为它会被消费。&lt;/p>
&lt;p>&lt;strong>协议演进规则需要写死。&lt;/strong> 客户端对未知事件类型必须忽略，而不是报错。&lt;code>schema_version&lt;/code> 目前存在，但配套演进策略还要补文档。&lt;/p>
&lt;h2 id="总结">总结
&lt;/h2>&lt;p>这套协议不是行业标准，传输层用的也不是什么新东西，但它把几件事做对了：&lt;strong>事件先成为带序号的事实，SSE 只是投影；Runtime 词汇保持最小，canonical 词汇保持稳定；断线恢复用标准 SSE 游标；工具调用、产物、失败、取消全部事件化。&lt;/strong>&lt;/p>
&lt;p>如果要从这套设计里抽象一条通用经验，那就是：Agent 的远程流式接口不应该让客户端订阅“模型的输出过程”，而应该让客户端订阅“系统已经发生并且可恢复的执行事实”。管道可以丢，事实不应该丢。&lt;/p></description></item><item><title>全量解码与增量解码：原理、区别以及应用</title><link>https://www.zata.cc/p/%E5%85%A8%E9%87%8F%E8%A7%A3%E7%A0%81%E4%B8%8E%E5%A2%9E%E9%87%8F%E8%A7%A3%E7%A0%81%E5%8E%9F%E7%90%86%E5%8C%BA%E5%88%AB%E4%BB%A5%E5%8F%8A%E5%BA%94%E7%94%A8/</link><pubDate>Fri, 14 Mar 2025 17:49:58 +0800</pubDate><guid>https://www.zata.cc/p/%E5%85%A8%E9%87%8F%E8%A7%A3%E7%A0%81%E4%B8%8E%E5%A2%9E%E9%87%8F%E8%A7%A3%E7%A0%81%E5%8E%9F%E7%90%86%E5%8C%BA%E5%88%AB%E4%BB%A5%E5%8F%8A%E5%BA%94%E7%94%A8/</guid><description>&lt;img src="https://www.zata.cc/p/%E5%85%A8%E9%87%8F%E8%A7%A3%E7%A0%81%E4%B8%8E%E5%A2%9E%E9%87%8F%E8%A7%A3%E7%A0%81%E5%8E%9F%E7%90%86%E5%8C%BA%E5%88%AB%E4%BB%A5%E5%8F%8A%E5%BA%94%E7%94%A8/images/index/index.png" alt="Featured image of post 全量解码与增量解码：原理、区别以及应用" />&lt;h2 id="引言">&lt;strong>引言&lt;/strong>
&lt;/h2>&lt;p>在自然语言处理（NLP）领域，尤其是大语言模型（LLM）中，解码（decoding）是模型生成输出的核心步骤。你可能好奇：为什么模型接收到完整输入后，生成回复时却不是一次性吐出整个句子？答案在于解码方式的不同。本教程将详细讲解两种主要解码方式——&lt;strong>全量解码&lt;/strong>和&lt;strong>增量解码&lt;/strong>，包括它们的定义、原理、优缺点、区别，以及在大语言模型中的应用。&lt;/p>
&lt;hr>
&lt;h2 id="第一部分什么是解码">&lt;strong>第一部分：什么是解码？&lt;/strong>
&lt;/h2>&lt;h3 id="解码的定义">&lt;strong>解码的定义&lt;/strong>
&lt;/h3>&lt;p>解码是大语言模型根据输入生成输出的过程。简单来说：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>输入&lt;/strong>：用户给模型一个问题或句子（比如“请解释一下AI”）。&lt;/li>
&lt;li>&lt;strong>解码&lt;/strong>：模型根据内部表示（概率分布或隐藏状态），逐步或一次性生成回答（比如“AI是一种技术”）。&lt;/li>
&lt;/ul>
&lt;h3 id="输入与解码的区别">&lt;strong>输入与解码的区别&lt;/strong>
&lt;/h3>&lt;ol>
&lt;li>
&lt;p>&lt;strong>输入阶段&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>用户通常是一次性把完整输入（比如“我不太懂，模型输入不是一次性就输入进去了吗”）交给模型。&lt;/li>
&lt;li>模型会通过编码器（如果是Encoder-Decoder架构，如T5）或直接通过自回归方式（像GPT）处理整个输入，生成某种内部表示（比如隐藏状态或上下文向量）。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>解码阶段&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>解码是模型根据输入生成输出的过程，也就是从内部表示逐步生成文本的过程。&lt;/li>
&lt;li>即使输入是一次性给的，模型生成输出时并不一定一次性吐出整个答案，而是需要一步步决定每个词（或标记），这就是解码方式的重点。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>换句话说，&lt;strong>输入是一次性给模型的，但输出（解码）可以是逐步生成的&lt;/strong>。全量解码和增量解码的区别在于：模型是如何基于输入一步步生成输出的。&lt;/p>
&lt;h3 id="为什么解码不是一次性完成">&lt;strong>为什么解码不是一次性完成？&lt;/strong>
&lt;/h3>&lt;p>大语言模型（尤其是基于Transformer的模型）通常是自回归的，意味着它们生成输出的方式是序列化的：每生成一个词，模型会把这个词加入上下文，再预测下一个词。这种设计有以下原因：&lt;/p>
&lt;p>&lt;strong>1. 语言的序列性质：&lt;/strong>
自然语言是有序的，比如“我喜欢你”和“你喜欢我”意思完全不同。模型需要逐词生成，才能保证语法和语义的连贯性。
如果一次性生成整个句子，模型需要同时决定所有词，这在计算上非常复杂，且难以保证一致性。&lt;/p>
&lt;p>&lt;strong>2. 模型架构限制：&lt;/strong>
GPT这类模型是自回归的，当前词的预测依赖于之前生成的词。它们没有能力一次性输出整个序列，而是必须逐步构建。
即使是Encoder-Decoder模型（如T5），解码器在生成时也是逐步进行的。&lt;/p>
&lt;p>&lt;strong>3. 概率分布：&lt;/strong>
模型每一步会输出一个概率分布（比如下一个词可能是“我”0.7、“你”0.2、“他”0.1），然后根据解码策略（贪心、采样等）选择一个词。
这个过程天然是增量的，因为每一步的选择会影响下一步的概率。&lt;/p>
&lt;hr>
&lt;h2 id="第二部分全量解码-full-decoding">&lt;strong>第二部分：全量解码 (Full Decoding)&lt;/strong>
&lt;/h2>&lt;h3 id="定义">&lt;strong>定义&lt;/strong>
&lt;/h3>&lt;p>全量解码是指模型一次性生成或评估整个输出序列，试图找到全局最优解。它强调对所有可能输出的全面考虑。&lt;/p>
&lt;h3 id="工作原理">&lt;strong>工作原理&lt;/strong>
&lt;/h3>&lt;ol>
&lt;li>接收完整输入（比如“你好”）。&lt;/li>
&lt;li>计算所有可能的输出序列及其概率：
&lt;ul>
&lt;li>“Hello” (0.9)&lt;/li>
&lt;li>“Hi there” (0.7)&lt;/li>
&lt;li>“Greetings” (0.5)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>选择得分最高的序列（比如“Hello”）作为输出。&lt;/li>
&lt;/ol>
&lt;h3 id="典型算法">&lt;strong>典型算法&lt;/strong>
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>束搜索 (Beam Search)&lt;/strong>：维护一组候选序列（束），每步保留得分最高的前k个，最终选最佳序列。&lt;/li>
&lt;li>&lt;strong>维特比算法 (Viterbi Algorithm)&lt;/strong>：在隐马尔可夫模型中寻找全局最优路径。&lt;/li>
&lt;li>（注：真正的全量解码需要遍历所有可能性，但在实践中因计算量太大，通常用近似方法。）&lt;/li>
&lt;/ul>
&lt;h3 id="优点">&lt;strong>优点&lt;/strong>
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>全局最优&lt;/strong>：考虑整个序列的上下文，结果更连贯。&lt;/li>
&lt;li>&lt;strong>高质量&lt;/strong>：适合需要精确输出的任务。&lt;/li>
&lt;/ul>
&lt;h3 id="缺点">&lt;strong>缺点&lt;/strong>
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>计算复杂度高&lt;/strong>：可能的序列组合随长度指数增长。&lt;/li>
&lt;li>&lt;strong>实时性差&lt;/strong>：需要等待完整计算，无法边生成边输出。&lt;/li>
&lt;/ul>
&lt;h3 id="应用场景">&lt;strong>应用场景&lt;/strong>
&lt;/h3>&lt;ul>
&lt;li>机器翻译（追求高质量翻译）。&lt;/li>
&lt;li>文本摘要（需要全局一致性）。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="第三部分增量解码-incremental-decoding">&lt;strong>第三部分：增量解码 (Incremental Decoding)&lt;/strong>
&lt;/h2>&lt;h3 id="定义-1">&lt;strong>定义&lt;/strong>
&lt;/h3>&lt;p>增量解码是指模型逐步生成输出序列，每一步只基于当前信息预测下一个词，而不依赖未来全局信息。它是一种“边解码边生成”的方式。&lt;/p>
&lt;h3 id="工作原理-1">&lt;strong>工作原理&lt;/strong>
&lt;/h3>&lt;ol>
&lt;li>接收输入（比如“你好”）。&lt;/li>
&lt;li>逐步生成输出：
&lt;ul>
&lt;li>第一步：输出“Hello”。&lt;/li>
&lt;li>第二步：根据“Hello”决定是否继续（如停止或加词）。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>重复直到生成完整序列或达到终止条件。&lt;/li>
&lt;/ol>
&lt;h3 id="典型算法-1">&lt;strong>典型算法&lt;/strong>
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>贪心解码 (Greedy Decoding)&lt;/strong>：每步选择概率最高的词。&lt;/li>
&lt;li>&lt;strong>采样解码 (Sampling)&lt;/strong>：根据概率分布随机选择词（如Top-k或Top-p采样）。&lt;/li>
&lt;/ul>
&lt;h3 id="优点-1">&lt;strong>优点&lt;/strong>
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>高效&lt;/strong>：每步计算量小，适合快速响应。&lt;/li>
&lt;li>&lt;strong>实时性强&lt;/strong>：边输入边输出，用户体验好。&lt;/li>
&lt;li>&lt;strong>资源占用低&lt;/strong>：无需存储所有可能性。&lt;/li>
&lt;/ul>
&lt;h3 id="缺点-1">&lt;strong>缺点&lt;/strong>
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>局部最优风险&lt;/strong>：每步只看当前，可能偏离全局最优。&lt;/li>
&lt;li>&lt;strong>上下文不足&lt;/strong>：无法利用序列后部的完整信息。&lt;/li>
&lt;/ul>
&lt;h3 id="应用场景-1">&lt;strong>应用场景&lt;/strong>
&lt;/h3>&lt;ul>
&lt;li>实时对话（聊天机器人）。&lt;/li>
&lt;li>流式语音转文字（边说边转录）。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="第四部分全量解码与增量解码的对比">&lt;strong>第四部分：全量解码与增量解码的对比&lt;/strong>
&lt;/h2>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;strong>特性&lt;/strong>&lt;/th>
&lt;th>&lt;strong>全量解码&lt;/strong>&lt;/th>
&lt;th>&lt;strong>增量解码&lt;/strong>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>生成方式&lt;/strong>&lt;/td>
&lt;td>一次性生成整个序列&lt;/td>
&lt;td>逐步生成序列&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>优化目标&lt;/strong>&lt;/td>
&lt;td>全局最优&lt;/td>
&lt;td>局部最优&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>计算复杂度&lt;/strong>&lt;/td>
&lt;td>高（指数级）&lt;/td>
&lt;td>低（线性或接近线性）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>实时性&lt;/strong>&lt;/td>
&lt;td>差（需完整输入和计算）&lt;/td>
&lt;td>强（边输入边输出）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>上下文依赖&lt;/strong>&lt;/td>
&lt;td>依赖整个序列&lt;/td>
&lt;td>只依赖当前和之前&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>典型算法&lt;/strong>&lt;/td>
&lt;td>束搜索、维特比算法&lt;/td>
&lt;td>贪心解码、采样解码&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="举例说明">&lt;strong>举例说明&lt;/strong>
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>输入&lt;/strong>：“I love you”。&lt;/li>
&lt;li>&lt;strong>全量解码&lt;/strong>：
&lt;ul>
&lt;li>计算所有可能翻译：
&lt;ul>
&lt;li>“我爱你” (0.9)&lt;/li>
&lt;li>“我喜欢你” (0.6)&lt;/li>
&lt;li>“我爱你们” (0.3)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>输出：“我爱你”。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>增量解码&lt;/strong>：
&lt;ul>
&lt;li>逐步生成：
&lt;ul>
&lt;li>“我” → “爱” → “你”。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>输出：“我爱你”。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h3 id="举例说明2">&lt;strong>举例说明2&lt;/strong>
&lt;/h3>&lt;p>假设你输入：“请解释一下AI”。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>输入阶段&lt;/strong>：模型一次性接收“请解释一下AI”，并理解其含义（通过编码器或自回归上下文）。&lt;/li>
&lt;li>&lt;strong>解码阶段&lt;/strong>：
&lt;ul>
&lt;li>&lt;strong>增量解码&lt;/strong>（实际常用）：
&lt;ul>
&lt;li>模型先输出“AI”，然后根据“AI”预测“是”，再根据“AI是”预测“一种”，逐步生成“AI是一种技术”。&lt;/li>
&lt;li>你可能会看到我逐词回复，像这样：AI→是→一种→技术。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>全量解码&lt;/strong>（理论上）：
&lt;ul>
&lt;li>模型计算所有可能的回答：
&lt;ul>
&lt;li>“AI是一种技术” (概率 0.9)&lt;/li>
&lt;li>“AI是人工智能” (概率 0.85)&lt;/li>
&lt;li>“AI很复杂” (概率 0.6)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>然后一次性输出“AI是一种技术”。&lt;/li>
&lt;li>但实际上，这需要巨大计算量，通常只用近似方法（如束搜索）。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="第五部分大语言模型中的解码方式">&lt;strong>第五部分：大语言模型中的解码方式&lt;/strong>
&lt;/h2>&lt;h3 id="现状">&lt;strong>现状&lt;/strong>
&lt;/h3>&lt;p>现代大语言模型（如GPT、ChatGPT、Grok）主要基于&lt;strong>自回归架构&lt;/strong>，天然倾向于&lt;strong>增量解码&lt;/strong>，但会根据任务搭配不同策略。&lt;/p>
&lt;h3 id="常用解码策略">&lt;strong>常用解码策略&lt;/strong>
&lt;/h3>&lt;ol>
&lt;li>&lt;strong>贪心解码&lt;/strong>：
&lt;ul>
&lt;li>每步选概率最高的词。&lt;/li>
&lt;li>简单高效，但输出单一。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>束搜索&lt;/strong>：
&lt;ul>
&lt;li>近似全量解码，保留多个候选序列。&lt;/li>
&lt;li>用于高质量生成（如翻译）。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>采样解码&lt;/strong>：
&lt;ul>
&lt;li>&lt;strong>Top-k采样&lt;/strong>：从前k个高概率词中随机选。&lt;/li>
&lt;li>&lt;strong>Top-p采样&lt;/strong>：从累计概率达p的词中选。&lt;/li>
&lt;li>增强多样性，常见于对话模型。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>自回归生成&lt;/strong>：
&lt;ul>
&lt;li>逐步生成，每步依赖前文。&lt;/li>
&lt;li>是增量解码的基础框架。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;h3 id="为什么偏向增量解码">&lt;strong>为什么偏向增量解码？&lt;/strong>
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>实时性&lt;/strong>：对话中需快速响应。&lt;/li>
&lt;li>&lt;strong>效率&lt;/strong>：避免指数级计算。&lt;/li>
&lt;li>&lt;strong>架构&lt;/strong>：自回归模型逐词生成。&lt;/li>
&lt;/ul>
&lt;h3 id="全量解码的应用">&lt;strong>全量解码的应用&lt;/strong>
&lt;/h3>&lt;ul>
&lt;li>在翻译、摘要等任务中，束搜索常用于追求高质量输出。&lt;/li>
&lt;li>但真正的全量解码（遍历所有序列）因计算成本过高，几乎不用。&lt;/li>
&lt;/ul>
&lt;h3 id="实际例子">&lt;strong>实际例子&lt;/strong>
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>ChatGPT&lt;/strong>：用Top-p采样+自回归，生成自然多样的回复。&lt;/li>
&lt;li>&lt;strong>Grok&lt;/strong>：类似增量解码，逐词输出，追求实时性。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="第六部分常见疑问解答">&lt;strong>第六部分：常见疑问解答&lt;/strong>
&lt;/h2>&lt;h3 id="q1输入不是一次性给的吗为什么解码不是一次性完成">&lt;strong>Q1：输入不是一次性给的吗？为什么解码不是一次性完成？&lt;/strong>
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>答&lt;/strong>：
&lt;ul>
&lt;li>输入确实是一次性给模型的，模型会理解完整上下文。&lt;/li>
&lt;li>但解码是生成输出的过程，自回归模型需要逐词预测，因为：
&lt;ol>
&lt;li>语言的序列性要求逐步构建。&lt;/li>
&lt;li>每步预测依赖前文输出。&lt;/li>
&lt;li>一次性生成所有词的组合计算量太大。&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h3 id="q2大语言模型用哪种解码">&lt;strong>Q2：大语言模型用哪种解码？&lt;/strong>
&lt;/h3>&lt;ul>
&lt;li>&lt;strong>答&lt;/strong>：
&lt;ul>
&lt;li>主要用&lt;strong>增量解码&lt;/strong>（自回归+采样），适合实时对话。&lt;/li>
&lt;li>&lt;strong>全量解码&lt;/strong>（如束搜索）用于特定任务，但多为近似形式。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="第七部分总结">&lt;strong>第七部分：总结&lt;/strong>
&lt;/h2>&lt;ul>
&lt;li>&lt;strong>全量解码&lt;/strong>：追求全局最优，计算成本高，适合高质量任务。&lt;/li>
&lt;li>&lt;strong>增量解码&lt;/strong>：高效实时，适合对话和流式应用。&lt;/li>
&lt;li>&lt;strong>大语言模型&lt;/strong>：以增量解码为主（如采样），灵活搭配策略，平衡质量与效率。&lt;/li>
&lt;/ul>
&lt;p>通过理解这两种解码方式，你可以更好地把握大语言模型的工作机制，并根据需求选择合适的策略。&lt;/p>
&lt;hr></description></item></channel></rss>