<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Pi-Review-Agent on Svtter's Blog</title><link>https://svtter.cn/tags/pi-review-agent/</link><description>Recent content in Pi-Review-Agent on Svtter's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Sun, 28 Jun 2026 23:00:00 +0800</lastBuildDate><atom:link href="https://svtter.cn/tags/pi-review-agent/index.xml" rel="self" type="application/rss+xml"/><item><title>pi-review-agent 是怎么"记住"上一次 review 的</title><link>https://svtter.cn/p/pi-review-agent-%E6%98%AF%E6%80%8E%E4%B9%88%E8%AE%B0%E4%BD%8F%E4%B8%8A%E4%B8%80%E6%AC%A1-review-%E7%9A%84/</link><pubDate>Sun, 28 Jun 2026 23:10:00 +0800</pubDate><guid>https://svtter.cn/p/pi-review-agent-%E6%98%AF%E6%80%8E%E4%B9%88%E8%AE%B0%E4%BD%8F%E4%B8%8A%E4%B8%80%E6%AC%A1-review-%E7%9A%84/</guid><description>&lt;p&gt;AI code review 的 GitHub Action 里，&lt;a class="link" href="https://github.com/sun-praise/pi-review-agent" target="_blank" rel="noopener"
 &gt;pi-review-agent&lt;/a&gt; 有一个不太显眼但很关键的设计——&lt;strong&gt;跨 CI run 续接每个 reviewer 的会话&lt;/strong&gt;：同一个 PR 第二次推送时，reviewer 知道&amp;quot;上次我提了什么、作者改了什么、哪些 issue 还没解决&amp;quot;，而不是每次从零开始。&lt;/p&gt;
&lt;p&gt;这篇文章拆开它的实现：上一次的对话记录落在哪、下一次 run 怎么加载回来、新 diff 怎么接进去，以及最关键的——为什么非要这么折腾。&lt;/p&gt;
&lt;h2 id="为什么需要记住上一次"&gt;为什么需要&amp;quot;记住上一次&amp;quot;
&lt;/h2&gt;&lt;p&gt;两个动机，一个是功能上的，一个是钱。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;功能上&lt;/strong&gt;：增量 review 需要上下文。同一个 PR 第 N 次推送时，reviewer 应该知道&amp;quot;上次我提了什么、作者改了什么、哪些 issue 还没解决&amp;quot;。无状态的 review 每次从零开始，会反复报相同的问题，会漏掉跨 commit 才看得出的 regression。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;钱&lt;/strong&gt;：DeepSeek 做 content-addressed prefix caching——如果请求的前缀（system prompt + 历史对话）和上一次完全一样，这部分 token 按缓存价计费。每次 review 都能把上一轮的 system prompt + history 完整重放给模型，那部分前缀就命中缓存，按 DeepSeek 公开定价里 cached token 大约是普通 input 的 &lt;strong&gt;2%&lt;/strong&gt;（input $0.14/1M，cacheRead $0.0028/1M）计费。PR review 这种高频、长 prompt、内容高度重叠的场景，省下来的钱是实打实的。&lt;/p&gt;
&lt;p&gt;但这里有个坑：不是所有 OpenAI-compatible 路径都把 &lt;code&gt;cacheRead&lt;/code&gt; 透传出来。opencode 的 openai-compatible 适配层会把它丢成 0（anomalyco/opencode#34022），于是即便上游真的命中了缓存、真的便宜了，账单上也看不出来，更糟的是 agent 自己的 cost 计算会按全价走。pi-review-agent 用 pi-ai 这个库，&lt;code&gt;cacheRead&lt;/code&gt; 字段一路上到 &lt;code&gt;usage&lt;/code&gt;，cost 表也单独列了 &lt;code&gt;cacheRead&lt;/code&gt; 折扣价——这是这个项目存在的直接理由。&lt;/p&gt;
&lt;p&gt;所以&amp;quot;记住上一次&amp;quot;不只是为了 review 质量，更是为了让 prefix cache 真的能命中、命中的钱真的能省下来。&lt;/p&gt;
&lt;h2 id="整体数据流"&gt;整体数据流
&lt;/h2&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart LR
 subgraph "CI run #1 (PR #42, 第一次推送)"
 A1[gh pr diff] --&gt; B1[prepareDiff 过滤]
 B1 --&gt; C1[runReview]
 C1 --&gt;|"新建 Agent, messages=[]"| D1[DeepSeek]
 D1 --&gt; E1["appendTranscript
 42/quality.jsonl"]
 end

 subgraph "actions/cache"
 F[(pi-review-session
 repo-42-run_id_1)]
 end
 E1 -.save.-&gt; F

 subgraph "CI run #2 (PR #42, 第二次推送)"
 F -.restore by prefix.-&gt; G["sessions/
 42/quality.jsonl"]
 A2[gh pr diff 新版] --&gt; B2[prepareDiff]
 B2 --&gt; C2[runReview]
 G --&gt;|loadTranscript 当 seed| C2
 C2 --&gt;|messages=旧transcript| H2[DeepSeek 命中 prefix cache]
 C2 --&gt;|prompt 追加新 diff| H2
 H2 --&gt; E2["appendTranscript
 追加到同一文件"]
 end&lt;/pre&gt;&lt;p&gt;三个阶段：&lt;strong&gt;落盘 → 跨 run 搬运 → 加载注入&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="1-落盘一个-pr-一个目录一个-persona-一个-jsonl"&gt;1. 落盘：一个 PR 一个目录，一个 persona 一个 jsonl
&lt;/h2&gt;&lt;p&gt;存储布局很简单，&lt;code&gt;src/review.ts&lt;/code&gt; 里 &lt;code&gt;sessionFile()&lt;/code&gt; 决定路径：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;sessionsRoot&amp;gt;/&amp;lt;pr&amp;gt;/&amp;lt;persona&amp;gt;.jsonl
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;每个 &lt;code&gt;(pr, persona)&lt;/code&gt; 一个文件，JSONL，每行一个 &lt;code&gt;AgentMessage&lt;/code&gt;。user / assistant / tool_use / tool_result 全部混在一起按时间顺序排。append-only——只追加不改写：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;appendTranscript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appendFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;(pr, persona)&lt;/code&gt; 这个二维 key 的选择是有意的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;pr 维度&lt;/strong&gt;隔离不同 PR 的上下文（不能让 PR #42 的 review 记忆串到 PR #43）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;persona 维度&lt;/strong&gt;隔离同一 PR 内不同 reviewer 的上下文（quality reviewer 不该看到 security reviewer 的对话）&lt;/li&gt;
&lt;li&gt;team 模式下的 coordinator 也是一个&amp;quot;persona&amp;quot;，名字就叫 &lt;code&gt;&amp;quot;coordinator&amp;quot;&lt;/code&gt;，文件 &lt;code&gt;coordinator.jsonl&lt;/code&gt;——它也走同一套 resume 路径&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="2-跨-ci-run-搬运actionscache-的-key-设计"&gt;2. 跨 CI run 搬运：actions/cache 的 key 设计
&lt;/h2&gt;&lt;p&gt;CI run 之间是无状态的——每次跑在一个全新的 runner 上，文件系统是空的。要让上一次的 jsonl 跨 run 活下来，靠 &lt;code&gt;action.yml&lt;/code&gt; 里这两步：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Restore review session&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/cache@v5&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ ... }}/.pi-review-sessions&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;pi-review-session-${{ github.repository }}-${{ steps.pr.outputs.pr }}-${{ github.run_id }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restore-keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; pi-review-session-${{ github.repository }}-${{ steps.pr.outputs.pr }}-&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这个 key 设计是整个机制的精妙之处，看一眼可能觉得理所当然，其实每个字段都有用意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;key&lt;/code&gt; 带 &lt;code&gt;github.run_id&lt;/code&gt;&lt;/strong&gt;：run_id 每次 run 都唯一。这意味着 &lt;code&gt;key&lt;/code&gt; 永远不会精确命中自己——save 总是新建一个 cache entry。如果去掉 run_id，第二次 run 会 restore 命中第一次的 entry，然后 save 时 actions/cache 发现 key 没变就跳过写回，新追加的 transcript 就丢了。带 run_id 强制每次都写。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;restore-keys&lt;/code&gt; 只有前缀（不带 run_id）&lt;/strong&gt;：精确 key 永远 miss，但前缀匹配会把这个 PR &lt;strong&gt;最近一次成功 run&lt;/strong&gt; 的 sessions 目录还原回来。这就是&amp;quot;续接&amp;quot;的实际机制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;github.repository&lt;/code&gt; 在 key 里&lt;/strong&gt;：fork 的 PR 不会串到上游 repo 的 session（cache 是 repo scope 的，但写上更明确）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pr 在 key 里，persona/team 不在&lt;/strong&gt;：一个 cache entry 装下这个 PR 的所有 reviewer + coordinator（persona 编码在文件名里）。这样 cache hit 率最高。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;粒度是 &lt;code&gt;(repo, pr)&lt;/code&gt;，不是 &lt;code&gt;(repo, pr, persona)&lt;/code&gt;。如果按 persona 拆 cache entry，N 个 persona 就要 N 次 cache round-trip；现在一次 restore 把整个目录搬回来。&lt;/p&gt;
&lt;h2 id="3-加载--注入transcript-当种子diff-当新-user-turn"&gt;3. 加载 + 注入：transcript 当种子，diff 当新 user turn
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;runReview()&lt;/code&gt; 是核心。每次调用（每个 persona、每次 run）都走这套：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;runReview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sessionFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;loadTranscript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 不存在 → []
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resumed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newMessages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;: &lt;span class="kt"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// ← 上轮 transcript 整段当种子
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;streamFn&lt;/span&gt;: &lt;span class="kt"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;collectFromAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newMessages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`Review this diff:&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="sb"&gt;n&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="sb"&gt;n&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ← 新 diff 作为下一条 user turn
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;collected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;appendTranscript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newMessages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 只追加本轮新增的消息
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resumed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;两步，泾渭分明：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;a. transcript 作为种子塞进 Agent 的 initialState&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;new Agent({ initialState: { messages: transcript } })&lt;/code&gt; 把上一轮的完整对话历史（user + assistant + tool_use + tool_result）作为这个 agent 的初始 messages。从模型的角度看，它&amp;quot;以为&amp;quot;自己刚才已经聊过这些——因为 DeepSeek 做 content-addressed prefix caching，这段重放的前缀会命中缓存，&lt;code&gt;usage.cacheRead &amp;gt; 0&lt;/code&gt;，命中部分按 2% 计费。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;b. 新 diff 作为新的 user turn 接在后面&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;agent.prompt(&amp;quot;Review this diff:\n\n&amp;quot; + diff)&lt;/code&gt; 在 pi-agent-core 里就是把传入文本当作下一条 user message 追加到 messages 末尾然后发起流。所以续接的形状是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;seeded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="err"&gt;上轮&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="err"&gt;上轮&lt;/span&gt; &lt;span class="n"&gt;assistant&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="err"&gt;上轮&lt;/span&gt; &lt;span class="k"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Review this diff:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;本轮新 diff&amp;gt;&amp;#34;&lt;/span&gt; &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="err"&gt;新注入的&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="n"&gt;turn&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;模型看到的是一段连续对话，它的&amp;quot;记忆&amp;quot;完全来自重放的 transcript，不需要任何特殊的 memory API 或向量检索。&lt;/p&gt;
&lt;p&gt;diff 本身的来历：&lt;code&gt;action.yml&lt;/code&gt; 里先 &lt;code&gt;gh pr diff&lt;/code&gt;（失败回退到 GitHub pulls API 的 &lt;code&gt;.diff&lt;/code&gt; 端点）落到 &lt;code&gt;$RUNNER_TEMP/pi-review-diff.txt&lt;/code&gt; → &lt;code&gt;index.ts&lt;/code&gt; 的 &lt;code&gt;prepareDiff()&lt;/code&gt; 过滤（锁文件必砍、&lt;code&gt;diff-exclude&lt;/code&gt; glob、&lt;code&gt;diff-max-size-kb&lt;/code&gt; 字节预算 + 超了就截断并附 notice）→ 作为 &lt;code&gt;opts.diff&lt;/code&gt; 传进 &lt;code&gt;runReview&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="几个容易写错的地方"&gt;几个容易写错的地方
&lt;/h2&gt;&lt;p&gt;这套机制能稳，靠的是几个容易写错的地方都写对了：&lt;/p&gt;
&lt;h3 id="只持久化成功-attempt-的消息"&gt;只持久化成功 attempt 的消息
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Only persist the transcript of a successful attempt — a partial
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// transcript from a failed run would poison the next session&amp;#39;s cache
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// prefix and confuse the resume path.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;appendTranscript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newMessages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;retry 循环里，&lt;code&gt;appendTranscript&lt;/code&gt; 只在成功路径调用。一次 attempt 失败（流断了、超时了、429 了），它产生的 &lt;code&gt;newMessages&lt;/code&gt; 直接丢弃。原因在注释里：半截 transcript 写进去，下一轮 resume 时它成了 prefix 的一部分，但这段 prefix 对应的是一个&lt;strong&gt;没有正常结束&lt;/strong&gt;的对话——模型上下文是断的，而且 DeepSeek 的 prefix cache 是按内容寻址的，下次重放这段坏 prefix 还是会&amp;quot;命中&amp;quot;，但模型看到的就是一个没 assistant 回复就突然冒出新的 user turn 的怪状态。宁可丢这次重试的产物，也要保证落盘的 transcript 是干净完整的。&lt;/p&gt;
&lt;h3 id="retry-每次都新建-agent但种子不变"&gt;retry 每次都新建 Agent，但种子不变
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Fresh Agent per attempt: a half-run agent after a stream error is
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// not safe to continue. The transcript seed is replayed each time;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// DeepSeek&amp;#39;s prefix cache absorbs the replay at a discount.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;一个流出错的 agent 内部状态是脏的（可能停在 tool_use 还没等到 tool_result 的中间态），继续用它会触发各种边界 bug。所以每次 retry 都新建一个干净的 Agent，把同一份 &lt;code&gt;transcript&lt;/code&gt; 种子重放进去。种子不变意味着 prefix cache 仍然命中——retry 不是免费的，但也是打折的。&lt;/p&gt;
&lt;h3 id="coordinator-也走-resume"&gt;coordinator 也走 resume
&lt;/h3&gt;&lt;p&gt;前面提过，coordinator 是一个 persona 名为 &lt;code&gt;&amp;quot;coordinator&amp;quot;&lt;/code&gt; 的 reviewer，文件 &lt;code&gt;&amp;lt;pr&amp;gt;/coordinator.jsonl&lt;/code&gt;，跨 run 同样续接。但它的&amp;quot;diff&amp;quot;不是 git diff——&lt;code&gt;orchestrate.ts&lt;/code&gt; 里 &lt;code&gt;buildCoordinatorInput()&lt;/code&gt; 把各 persona 本轮的 review 结果拼成一段文本作为它的输入。所以 coordinator 的&amp;quot;记忆&amp;quot;是&amp;quot;我上轮综合了哪些 persona 的什么结论&amp;quot;，这让它能在新一轮判断&amp;quot;哪些 issue 是新出的、哪些是没解决的&amp;quot;。&lt;/p&gt;
&lt;h3 id="fail-closed"&gt;fail-closed
&lt;/h3&gt;&lt;p&gt;如果一个 persona reviewer 整个挂了（产不出内容），&lt;code&gt;runTeamReview&lt;/code&gt; 不是当它&amp;quot;通过&amp;quot;，而是把 verdict 强制改成 &lt;code&gt;CANNOT MERGE&lt;/code&gt;。证据缺失不等于没证据——reviewer 没说话比 reviewer 说&amp;quot;没问题&amp;quot;危险得多。&lt;/p&gt;
&lt;h2 id="为什么这套设计能省钱把数字摆出来"&gt;为什么这套设计能省钱：把数字摆出来
&lt;/h2&gt;&lt;p&gt;provider 配置（&lt;code&gt;src/provider.ts&lt;/code&gt;）里 DeepSeek 的 cost 表：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;cost&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;input&lt;/span&gt;: &lt;span class="kt"&gt;0.14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 普通 input，$ / 1M tokens
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;output&lt;/span&gt;: &lt;span class="kt"&gt;0.28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;cacheRead&lt;/span&gt;: &lt;span class="kt"&gt;0.0028&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 命中缓存的 input，input 的 2%
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;cacheWrite&lt;/span&gt;: &lt;span class="kt"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;pi-ai 的 &lt;code&gt;calculateCost()&lt;/code&gt; 自动把 &lt;code&gt;usage.cacheRead&lt;/code&gt; 乘上 &lt;code&gt;cost.cacheRead&lt;/code&gt;。所以一个 PR 第二次推送时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;system prompt（可能几千 token 的 persona 指南 + 语言指令）+ 上一轮 transcript（reviewer 的 reasoning、读过的文件、grep 结果）这部分前缀全部命中 cache，按 $0.0028/1M 计费&lt;/li&gt;
&lt;li&gt;只有本轮新增的 diff（通常几百到几千 token）按 $0.14/1M&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对一个 review 频繁、persona prompt 又长（quality/security/performance 各一套几千 token 指南）的项目，第二轮开始的 review 成本能降到第一轮的零头。&lt;/p&gt;
&lt;h2 id="一句话总结"&gt;一句话总结
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;jsonl 当 seed 重放（吃 prefix cache）+ 新 diff 当下一条 user turn 追加&lt;/strong&gt;——这就是 pi-review-agent&amp;quot;加载历史 + 注入新改动&amp;quot;的全部机制。没有向量数据库，没有 memory API，没有 embedding，就是一个 append-only 的 JSONL 文件 + actions/cache 的前缀模糊命中 + pi-agent-core 的 &lt;code&gt;initialState.messages&lt;/code&gt;。简单到几乎不像一个&amp;quot;记忆系统&amp;quot;，但它正好踩中了 prefix caching 的甜点，顺便把账单打了折。&lt;/p&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/sun-praise/pi-review-agent" target="_blank" rel="noopener"
 &gt;pi-review-agent&lt;/a&gt; — 项目仓库&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>