<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>U7&#39;s Blog🍋</title>
  
  
  <link href="https://u7u7.top/atom.xml" rel="self"/>
  
  <link href="https://u7u7.top/"/>
  <updated>2026-10-26T13:29:19.035Z</updated>
  <id>https://u7u7.top/</id>
  
  <author>
    <name>U7&#39;s Blog🍋</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>面试被问虚拟线程和协程到底差在哪：用 JDK 27 的现状回答</title>
    <link href="https://u7u7.top/posts/virtual-thread-vs-coroutine-jdk27.html"/>
    <id>https://u7u7.top/posts/virtual-thread-vs-coroutine-jdk27.html</id>
    <published>2026-10-21T00:30:00.000Z</published>
    <updated>2026-10-26T13:29:19.035Z</updated>
    
    <content type="html"><![CDATA[<h1 id="面试被问虚拟线程和协程到底差在哪：用-JDK-27-的现状回答"><a href="#面试被问虚拟线程和协程到底差在哪：用-JDK-27-的现状回答" class="headerlink" title="面试被问虚拟线程和协程到底差在哪：用 JDK 27 的现状回答"></a>面试被问虚拟线程和协程到底差在哪：用 JDK 27 的现状回答</h1><p>「虚拟线程不就是 Java 版的协程吗」——这句话我在面试复盘里见过太多次，每次都想打断。</p><p>先把能站住的结论给出来：<strong>虚拟线程是 JDK 替你管理的线程，协程是编译器替你改写的函数</strong>。前者干的事是让「一个请求一个线程」重新变得便宜，后者干的事是让「一个函数可以在中间让出去」变成语言能力。它们在压测数字上经常被放进同一张图比较，但机制上没有血缘关系，混着答，面试官一追问第三层就会露馅。</p><p>我把这道题答过也被问过，下面按「机制 → 版本现状 → 什么时候真有用」的顺序拆开。</p><hr><h2 id="一、一句话答案，先背下来"><a href="#一、一句话答案，先背下来" class="headerlink" title="一、一句话答案，先背下来"></a>一、一句话答案，先背下来</h2><ul><li>虚拟线程解决的是<strong>线程太贵</strong>。</li><li>协程解决的是<strong>代码写不直</strong>。</li><li>两者都是 M:N，都是协作式让出，都不解决 CPU 不够。</li></ul><div class="table-container"><table><thead><tr><th>维度</th><th>虚拟线程</th><th>协程（以 Kotlin 为例）</th></tr></thead><tbody><tr><td>谁创建</td><td>JDK 运行时，<code>Thread.ofVirtual</code></td><td>库函数，<code>launch</code> / <code>async</code></td></tr><tr><td>谁调度</td><td>JVM 内部的 ForkJoinPool 载体线程</td><td>Dispatchers 背后的线程池</td></tr><tr><td>栈放哪里</td><td>堆上的栈块，可增长可搬运</td><td>编译期生成的状态机对象加续体</td></tr><tr><td>阻塞怎么写</td><td>照旧写同步阻塞</td><td>必须落在挂起点上写 <code>suspend</code></td></tr><tr><td>对上层是否透明</td><td>透明，签名不用改</td><td>不透明，调用链被着色</td></tr><tr><td>能否被抢占</td><td>不能，协作式让出</td><td>不能，协作式让出</td></tr></tbody></table></div><p>最后一行我特意对齐了：<strong>两者都不是抢占式的</strong>。很多人以为协程是抢占式、虚拟线程是协作式，其实反过来也一样不成立，都是遇到挂起或阻塞点才换人。</p><hr><h2 id="二、调度：都在做-M-比-N，但谁在调度完全不同"><a href="#二、调度：都在做-M-比-N，但谁在调度完全不同" class="headerlink" title="二、调度：都在做 M 比 N，但谁在调度完全不同"></a>二、调度：都在做 M 比 N，但谁在调度完全不同</h2><p>虚拟线程的调度在 JVM 里。JEP 444 的表述很清楚：虚拟线程是由 JDK 而不是操作系统提供的轻量线程，运行时把它们挂载到载体线程上，阻塞时卸载，载体空出来给下一个虚拟线程用。这套调度器用的就是 ForkJoinPool 的工作窃取模型，载体线程数默认等于核数，可以用系统属性调。</p><p>协程的调度不在语言里，在库里。<code>suspend</code> 只是编译器约定的一个标记：带 <code>suspend</code> 的函数编译后多一个续体参数，挂起时把续体存起来返回，恢复时调 resume 继续跑。至于 resume 之后落到哪个线程，那是 Dispatchers 的事。</p><p><strong>我的理解</strong>：虚拟线程把 M:N 藏在 JDK 内部，所以业务代码完全看不见；协程把 M:N 露在库的 API 上，所以你必须决定用哪个 Dispatcher。前者省心，后者可控，代价是协程这边一旦 Dispatcher 选错，比如把阻塞调用扔到只有核数个线程的 Dispatchers.Default 上，整条链就堵死了，而虚拟线程没有这个旋钮可拧。</p><hr><h2 id="三、栈怎么放：这条最能区分两者"><a href="#三、栈怎么放：这条最能区分两者" class="headerlink" title="三、栈怎么放：这条最能区分两者"></a>三、栈怎么放：这条最能区分两者</h2><p>平台线程的栈在本地内存里，先 reserve 一大块，用多少提交多少。线程数一上去，光是栈的预留和内核的调度开销就压住了天花板。</p><p>虚拟线程的栈在堆上，是一块可增长的栈块。挂载时把栈帧搬进载体线程的栈，卸载时再搬回来。能搬，意味着调用栈可以很深，也意味着栈本身就是 GC 能看见的对象。</p><p>协程没有「搬栈」这个动作。编译器把 suspend 函数切成若干个状态机，局部变量从栈帧变成状态机对象的字段。所以协程的调用栈深度其实是被状态机的形状决定的，中间隔一层普通函数还好，深递归加挂起会让状态机对象一层层叠起来。</p><p><strong>这里我不太同意</strong>一种流行说法：「协程比虚拟线程更轻，因为协程没有栈」。字节层面没有统一裁判——协程的每个挂起帧都是一个对象，虚拟线程的栈块也是堆上的对象。真正的差别不在谁更轻，而在<strong>可以创建多少个</strong>，以及<strong>挂在阻塞调用上会不会占住载体线程</strong>。拿「轻量」当结论，面试官换个问法你就答不上来了。</p><hr><h2 id="四、阻塞语义与传染性：取舍都在这里"><a href="#四、阻塞语义与传染性：取舍都在这里" class="headerlink" title="四、阻塞语义与传染性：取舍都在这里"></a>四、阻塞语义与传染性：取舍都在这里</h2><p>虚拟线程最大的卖点是<strong>它仍然阻塞</strong>。你写 <code>socket.read()</code>、<code>Thread.sleep</code>、<code>connection.query</code>，代码还是那个同步阻塞的样子，JDK 在底层把这些阻塞点改造成可卸载。这意味着已有的 Spring MVC 应用、已有的 JDBC 调用，换个 Executor 就能受益，签名一行不改。</p><p>协程刚好相反：阻塞必须显式。<code>delay</code> 而不是 <code>Thread.sleep</code>，阻塞 IO 要包进 <code>withContext(Dispatchers.IO)</code>。误用一次阻塞 API，协程不会让出，整个线程池被占住，而且不会有任何报错——这是协程最容易踩的坑。</p><p>传染性是同一个取向的两面：协程的 <code>suspend</code> 会顺着调用链一路上色，改一个底层方法，上面十层签名全要动；虚拟线程对上层完全透明，你不需要知道某个方法内部有没有阻塞。</p><p><strong>我的判断</strong>：传染性不是缺点也不是优点，它是两种设计哲学的必然产物。Java 选了「不破坏二十年的存量代码」，所以必须做到透明，代价是阻塞语义只能靠运行时兜底；Kotlin 选了「让副作用显式化」，代价是函数着色。面试时把这一层说出来，比背「协程更轻」强得多。</p><hr><h2 id="五、三层调度模型长什么样"><a href="#五、三层调度模型长什么样" class="headerlink" title="五、三层调度模型长什么样"></a>五、三层调度模型长什么样</h2><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart LR    A[Java 业务代码] --&gt; B[平台线程池\n一请求一线程]    A --&gt; C[虚拟线程\n由 JDK 提供]    B --&gt; E[OS 线程\n内核调度]    C --&gt; F[载体线程 ForkJoinPool\nJVM 调度]    F --&gt; E    K[Kotlin 业务代码] --&gt; D[协程\n由库提供]    D --&gt; G[编译器生成的状态机\n续体对象]    G --&gt; H[Dispatchers 线程池\n库负责调度]    H --&gt; E    E --&gt; I[CPU 与系统调用]  </pre></div><p>这张图想让你看见什么：不管你写的是平台线程、虚拟线程还是协程，<strong>最终都要落到 OS 线程上</strong>。三者的差别只在最上面那一段——是谁在把逻辑单元多路复用到更少的 OS 线程上。虚拟线程那一层的多路复用是 JVM 干的，协程那一层是编译期改造加库调度共同完成的，平台线程压根没有这一层。所以面试里说「虚拟线程减少了线程切换」，严格说减少的是<strong>用户态挂载卸载</strong>，不是内核上下文切换，因为载体线程数没变。</p><hr><h2 id="六、挂载、卸载与钉住的状态图"><a href="#六、挂载、卸载与钉住的状态图" class="headerlink" title="六、挂载、卸载与钉住的状态图"></a>六、挂载、卸载与钉住的状态图</h2><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  stateDiagram-v2    [*] --&gt; 新建    新建 --&gt; 运行中 : 被调度器选中并挂载到载体    运行中 --&gt; 运行中 : 纯计算 一直占着载体线程    运行中 --&gt; 已卸载 : 遇到可卸载阻塞 例如 Socket 读    已卸载 --&gt; 运行中 : 事件就绪 重新挂载到某个载体    运行中 --&gt; 已钉住 : synchronized 内阻塞 或 native 回调阻塞    已钉住 --&gt; 运行中 : 阻塞结束 载体才被释放    运行中 --&gt; [*] : 任务结束  </pre></div><p>这张图想让你看见什么：<strong>只有「已卸载」这一条边是免费的</strong>。虚拟线程的价值全部来自运行中到已卸载这一跳，跳到「已钉住」就等于退化成了平台线程，还多付了一份挂载的成本。JDK 21 的时候「运行中 → 已钉住」主要由 <code>synchronized</code> 里阻塞触发；JDK 24 之后这条边基本只剩 native 方法和 FFM 回调。面试官问「虚拟线程为什么会失效」，你指着这条边答就对了。</p><hr><h2 id="七、JDK-27-的现状：三件事必须核清楚"><a href="#七、JDK-27-的现状：三件事必须核清楚" class="headerlink" title="七、JDK 27 的现状：三件事必须核清楚"></a>七、JDK 27 的现状：三件事必须核清楚</h2><p><strong>第一件，虚拟线程已经不是新东西了。</strong> JEP 444 在 Java 21 正式交付，状态是 Delivered，虚拟线程是标准库的一部分，早就不是预览特性。从 21 到 27，它已经跨过了六个大版本。</p><p><strong>第二件，钉住问题在 JDK 24 被 JEP 491 解决了。</strong> JEP 491 的标题就是「Synchronize Virtual Threads without Pinning」，Release 是 24。它让阻塞在 <code>synchronized</code> 方法或语句里的虚拟线程释放载体线程，覆盖了 monitorenter 和 <code>Object.wait</code>，目标是「消除几乎所有虚拟线程被钉在平台线程上的情况」。剩余场景 JEP 491 也写明了：虚拟线程调用 native 方法，或者通过 Foreign Function &amp; Memory API 进入本地代码，而那段本地代码又回调 Java 里的阻塞操作。观测手段保留并增强——JFR 事件 <code>jdk.VirtualThreadPinned</code> 继续存在，还会带上钉住原因和载体线程的标识；而 <code>jdk.tracePinnedThreads</code> 这个系统属性在 24 之后不再需要了。</p><p><strong>第三件，JDK 27 不是 LTS。</strong> 这条是面试里最容易被抓的硬伤。JDK 27 在 2026-09-15 达到 GA，但它是非 LTS 的特性版本。Oracle 的官方路线图写得很直白：Java SE 8、11、17、21、25 是 LTS 版本，下一个计划中的 LTS 是 2027 年 9 月的 Java 29。所以「JDK 27 是下一个 LTS」这句话是错的，说错了比不说更扣分。</p><p>还有一个很多人不知道的点：JDK 27 里 Structured Concurrency 仍然是<strong>第七次预览</strong>，也就是 JEP 533。虚拟线程早就转正了，但它「最搭的那套结构化并发 API」到 27 还没定型。这个事实特别适合用来回答「虚拟线程现在还有什么没解决」——不是虚拟线程本身没解决，是围绕它的编程模型还在打磨。</p><p><strong>我踩过的坑</strong>：最早在 21 上做压测，把一堆老 DAO 直接塞进虚拟线程执行器，结果吞吐比固定线程池还差，排查半天才发现 <code>synchronized</code> 包着远程调用，每个虚拟线程都把载体钉死，JFR 里 <code>jdk.VirtualThreadPinned</code> 刷屏。那次之后我的结论很朴素：<strong>上虚拟线程之前先跑一遍 JFR，看有没有钉住，比看任何博客都准。</strong></p><hr><h2 id="八、两段代码，看清各自阻塞点在哪"><a href="#八、两段代码，看清各自阻塞点在哪" class="headerlink" title="八、两段代码，看清各自阻塞点在哪"></a>八、两段代码，看清各自阻塞点在哪</h2><p>Java 这边，虚拟线程的写法几乎不需要改变习惯：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> java.time.Duration;</span><br><span class="line"><span class="keyword">import</span> java.util.concurrent.ExecutorService;</span><br><span class="line"><span class="keyword">import</span> java.util.concurrent.Executors;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">VirtualThreadDemo</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="type">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> (<span class="type">ExecutorService</span> <span class="variable">pool</span> <span class="operator">=</span> Executors.newVirtualThreadPerTaskExecutor()) &#123;</span><br><span class="line">            <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; <span class="number">10_000</span>; i++) &#123;</span><br><span class="line">                <span class="type">int</span> <span class="variable">orderId</span> <span class="operator">=</span> i;</span><br><span class="line">                pool.submit(() -&gt; &#123;</span><br><span class="line">                    Thread.sleep(Duration.ofMillis(<span class="number">50</span>)); <span class="comment">// 阻塞点：看起来是同步阻塞</span></span><br><span class="line">                    System.out.println(<span class="string">&quot;done &quot;</span> + orderId);</span><br><span class="line">                    <span class="keyword">return</span> orderId;</span><br><span class="line">                &#125;);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="comment">// close 会等待所有任务结束</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这段代码在说明什么：<code>Thread.sleep</code> 是阻塞调用，虚拟线程在这里会被卸载，载体线程立刻去跑下一个任务。整段代码没有任何 <code>suspend</code>、<code>async</code>、回调，业务方法签名一个字没改——这就是「对上层透明」的实际含义。一万个任务只需要几个载体线程就能撑住，因为真正占着线程的时间接近于零。另外注意这里是每个任务新建一个虚拟线程，不是从池里借——JEP 444 原话就是虚拟线程便宜且充足、因此永远不该被池化。</p><p>Kotlin 这边，同一个语义必须靠挂起点来表达：</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> kotlinx.coroutines.Dispatchers</span><br><span class="line"><span class="keyword">import</span> kotlinx.coroutines.async</span><br><span class="line"><span class="keyword">import</span> kotlinx.coroutines.awaitAll</span><br><span class="line"><span class="keyword">import</span> kotlinx.coroutines.coroutineScope</span><br><span class="line"><span class="keyword">import</span> kotlinx.coroutines.delay</span><br><span class="line"><span class="keyword">import</span> kotlinx.coroutines.runBlocking</span><br><span class="line"></span><br><span class="line"><span class="keyword">suspend</span> <span class="function"><span class="keyword">fun</span> <span class="title">loadOrder</span><span class="params">(id: <span class="type">Int</span>)</span></span>: <span class="built_in">Int</span> = coroutineScope &#123;</span><br><span class="line">    delay(<span class="number">50</span>) <span class="comment">// 挂起点：不占线程</span></span><br><span class="line">    println(<span class="string">&quot;done &quot;</span> + id)</span><br><span class="line">    id</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">main</span><span class="params">()</span></span> = runBlocking &#123;</span><br><span class="line">    <span class="keyword">val</span> results = (<span class="number">1.</span><span class="number">.10_000</span>)</span><br><span class="line">        .map &#123; id -&gt; async(Dispatchers.IO) &#123; loadOrder(id) &#125; &#125;</span><br><span class="line">        .awaitAll()</span><br><span class="line">    println(results.size)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这段代码在说明什么：<code>delay</code> 是挂起函数，它让出线程的方式是把续体存进状态机然后返回；如果这里手滑写成 <code>Thread.sleep</code>，协程根本不认识这个阻塞点，线程照占不误，而且编译期不报错。另外 <code>loadOrder</code> 带 <code>suspend</code> 标记，任何调用它的上层函数也必须 <code>suspend</code>，这就是着色传染的现场证据。注意 <code>async</code> 那里显式指定了 <code>Dispatchers.IO</code>，因为默认的 <code>Dispatchers.Default</code> 线程数约等于核数，拿它跑阻塞调用等于自己给自己挖坑。</p><hr><h2 id="九、我的判断：什么场景虚拟线程真省钱"><a href="#九、我的判断：什么场景虚拟线程真省钱" class="headerlink" title="九、我的判断：什么场景虚拟线程真省钱"></a>九、我的判断：什么场景虚拟线程真省钱</h2><div class="table-container"><table><thead><tr><th>场景</th><th>值不值</th><th>原因</th></tr></thead><tbody><tr><td>高并发 IO 等待型，网关、爬虫、DB 代理</td><td>值</td><td>线程数从几千到几十万，内存与用户态切换都省下来</td></tr><tr><td>纯 CPU 密集计算</td><td>不值</td><td>载体线程数等于核数，虚拟线程只是排队，算力没多一毫核</td></tr><tr><td>依赖库满是 <code>synchronized</code> 或 native 阻塞</td><td>看版本</td><td>24 之前会被钉住，24 之后要看 FFM 回调</td></tr><tr><td>下游连接池只有几十个连接</td><td>不值</td><td>瓶颈在池不在线程，虚拟线程只是把排队挪了个位置</td></tr><tr><td>重度 ThreadLocal 缓存用户上下文</td><td>要小心</td><td>百万级虚拟线程意味着百万份副本，内存会先炸</td></tr></tbody></table></div><p><strong>我的判断</strong>：虚拟线程真正省钱的场景只有一个特征——<strong>等待时间远大于计算时间，并且等待发生在 JDK 认识的阻塞点上</strong>。除此之外它更像「看起来美」：CPU 密集场景下它不比平台线程快，连接池打满的时候它只是把 Tomcat 的排队搬到了连接池的排队，本质没变，但监控指标变了，很多人这时候会误判成「虚拟线程没用」。真实情况是瓶颈转移了，你要做的是重新量一次 RT 分布，而不是换回平台线程。</p><p>反过来说，协程那边也别迷信。协程解决的是表达问题，它不会让你的下游变快，也不会自动帮你把阻塞调用改成非阻塞。这一点上两者完全平等：<strong>它们都在优化「线程太贵」，没有在优化「依赖太慢」。</strong></p><hr><h2 id="十、面试里这样答，比背口号容易加分"><a href="#十、面试里这样答，比背口号容易加分" class="headerlink" title="十、面试里这样答，比背口号容易加分"></a>十、面试里这样答，比背口号容易加分</h2><p>顺序是边界、机制、版本、反例。</p><p>先给边界：「虚拟线程和协程解决的不是同一个问题，前者降低线程成本，后者改变代码表达方式，所以它们不是替代关系」。</p><p>再给机制：「虚拟线程由 JVM 调度，栈在堆上可搬运，阻塞对上层透明；协程靠编译器把 suspend 函数改写成状态机，由库的 Dispatcher 调度，阻塞必须显式挂在挂起点上」。</p><p>然后给版本：「虚拟线程 21 年由 JEP 444 转正，24 年 JEP 491 解决了 <code>synchronized</code> 钉住，剩下的钉住场景是 native 和 FFM 回调；JDK 27 已经 GA 但不是 LTS，LTS 是 8、11、17、21、25，下一个是 2027 年 9 月的 Java 29」。</p><p>最后给反例：「所以不能说虚拟线程是协程的 Java 实现，也不能说它让 CPU 密集任务变快，更不能说 27 是 LTS」。</p><p>这四步答完，追问基本就到顶了。</p><hr><h2 id="落地清单"><a href="#落地清单" class="headerlink" title="落地清单"></a>落地清单</h2><ol><li>记死三个 JEP 号：444 虚拟线程、491 解除 <code>synchronized</code> 钉住、533 结构化并发第七次预览。</li><li>记死版本定位：LTS 是 8、11、17、21、25，下一个 LTS 是 Java 29（2027-09），JDK 27 在 2026-09-15 GA 但<strong>不是 LTS</strong>。</li><li>上线前先跑一遍 JFR，盯 <code>jdk.VirtualThreadPinned</code>，有钉住先改锁或升版本，别急着调参。</li><li>判断要不要上虚拟线程，只问两个数：IO 等待占比，以及下游连接池上限。</li><li>协程侧的论据只引官方文档里的 Continuation 与 suspend 语义，不编版本号。</li><li>任何「虚拟线程更快」的结论，都补一句「在什么阻塞模型、什么依赖库版本下」，否则就是口号。</li></ol><hr><h2 id="参考来源"><a href="#参考来源" class="headerlink" title="参考来源"></a>参考来源</h2><ul><li>JEP 444: Virtual Threads（Java 21，Delivered）— <a href="https://openjdk.org/jeps/444">https://openjdk.org/jeps/444</a></li><li>JEP 491: Synchronize Virtual Threads without Pinning（JDK 24）— <a href="https://openjdk.org/jeps/491">https://openjdk.org/jeps/491</a></li><li>JDK 27 项目页（2026-09-15 GA，JEP 533 结构化并发第七次预览）— <a href="https://openjdk.org/projects/jdk/27/">https://openjdk.org/projects/jdk/27/</a></li><li>Oracle Java SE 支持路线图（LTS 为 8/11/17/21/25，下一个 LTS 是 Java 29，2027-09）— <a href="https://www.oracle.com/java/technologies/java-se-support-roadmap.html">https://www.oracle.com/java/technologies/java-se-support-roadmap.html</a></li><li>Oracle 虚拟线程官方指南（挂载卸载、钉住、JFR 事件 jdk.VirtualThreadPinned 默认阈值 20ms）— <a href="https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html">https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html</a></li><li>Kotlin 协程基础（suspending functions、coroutine builders）— <a href="https://kotlinlang.org/docs/coroutines-basics.html">https://kotlinlang.org/docs/coroutines-basics.html</a></li><li>Kotlin Continuation 接口文档（挂起点之后的续体）— <a href="https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.coroutines/-continuation/">https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.coroutines/-continuation/</a></li></ul>]]></content>
    
    
    <summary type="html">虚拟线程不是 Java 版协程。从调度、栈、阻塞与传染性拆开两者，再用 JDK 27 非 LTS 的定位和 JEP 491 后的钉住边界回答追问。</summary>
    
    
    
    <category term="面试" scheme="https://u7u7.top/categories/%E9%9D%A2%E8%AF%95/"/>
    
    
    <category term="Java" scheme="https://u7u7.top/tags/Java/"/>
    
    <category term="面试" scheme="https://u7u7.top/tags/%E9%9D%A2%E8%AF%95/"/>
    
    <category term="并发" scheme="https://u7u7.top/tags/%E5%B9%B6%E5%8F%91/"/>
    
    <category term="虚拟线程" scheme="https://u7u7.top/tags/%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>复盘：一条 ignore 配置引发的博客内容污染</title>
    <link href="https://u7u7.top/posts/hexo-ignore-absolute-path-postmortem.html"/>
    <id>https://u7u7.top/posts/hexo-ignore-absolute-path-postmortem.html</id>
    <published>2026-10-19T00:30:00.000Z</published>
    <updated>2026-10-26T13:29:19.050Z</updated>
    
    <content type="html"><![CDATA[<h1 id="复盘：一条-ignore-配置引发的博客内容污染"><a href="#复盘：一条-ignore-配置引发的博客内容污染" class="headerlink" title="复盘：一条 ignore 配置引发的博客内容污染"></a>复盘：一条 ignore 配置引发的博客内容污染</h1><p>先说结论：这次事故的重点不是“我把目录放错了”，而是<strong>一条看起来完全正确、而 Hexo 连一个字都不警告的 ignore 配置，静默地什么都没干</strong>。</p><p>我在 _config.yml 里按官方文档的风格写了 <code>_posts/copilot/**</code>，跑 hexo generate 一切正常、退出码 0、没有任何 warning、文章列表也不报错——而线上依旧挂着两篇我不认识的文章。等把源码翻出来才发现，自己同时踩了两个坑：一个是目录位置，一个是 Hexo 6.x 用<strong>绝对路径</strong>去做 ignore 匹配。第二个坑才是真正浪费时间的那个。</p><h2 id="一、症状：一篇标题叫占位符的文章"><a href="#一、症状：一篇标题叫占位符的文章" class="headerlink" title="一、症状：一篇标题叫占位符的文章"></a>一、症状：一篇标题叫占位符的文章</h2><p>事情是从一个奇怪的内链开始的。我在首页刷到一篇文章，标题显示成 <code>&#123;&#123;title&#125;&#125;</code> 这种字面量，点进去正文是空的，URL 是 /posts/pagelink.html。</p><p>顺着 URL 猜 abbrlink，.md 文件名可能被改过，但 front-matter 里的 abbrlink: pagelink 一定还在。全仓 grep 一次就定位到了：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># source/_posts/scaffolds/page.md</span></span><br><span class="line"><span class="attr">title:</span> <span class="string">&quot;<span class="template-variable">&#123;&#123;title&#125;&#125;</span>&quot;</span></span><br><span class="line"><span class="attr">description:</span> <span class="string">page默认模版</span></span><br><span class="line"><span class="attr">abbrlink:</span> <span class="string">pagelink</span></span><br><span class="line"><span class="attr">date:</span> <span class="string">&quot;<span class="template-variable">&#123;&#123;date&#125;&#125;</span> <span class="template-variable">&#123;&#123;time&#125;&#125;</span>&quot;</span></span><br></pre></td></tr></table></figure><p>这不是文章，这是 hexo new page 用的<strong>页面模板</strong>。它的 title 就是占位符本身，date 也是占位符，所以渲染出来的文章标题永远是 <code>&#123;&#123;title&#125;&#125;</code>、发布时间永远是字符串 <code>&#123;&#123;date&#125;&#125; &#123;&#123;time&#125;&#125;</code>。它被当成普通文章送进渲染管线，于是诞生了 /posts/pagelink.html。</p><p>顺手查 Tag 页，又冒出来一个 /tags/copilot-conversation/，点进去是 /posts/0.html：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># source/_posts/copilot/copilot-conversations/agent__xxx.md</span></span><br><span class="line"><span class="attr">epoch:</span> <span class="number">1791876529103</span></span><br><span class="line"><span class="attr">mode:</span> <span class="string">agent</span></span><br><span class="line"><span class="attr">backendId:</span> <span class="string">codex</span></span><br><span class="line"><span class="attr">sessionId:</span> <span class="string">01a13429-cf08-7083-8213-2825df351b2f</span></span><br><span class="line"><span class="attr">tags:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">copilot-conversation</span></span><br><span class="line"><span class="attr">abbrlink:</span> <span class="string">&#x27;0&#x27;</span></span><br></pre></td></tr></table></figure><p>这是 Obsidian Copilot 插件的对话归档，一整个目录 16 个 .md，front-matter 是插件自己写的元数据，abbrlink 还是 ‘0’——其中两个文件的 abbrlink 完全一样，于是它们在 db.json 里互相覆盖，<strong>16 篇污染最后只暴露成 1 篇</strong>。这个掩盖效应很要命：你看到的症状规模，远小于实际污染规模。</p><p><strong>这里我不太同意“这就是点脏数据，删了就完事”的说法。</strong> 它已经不是脏数据了，它是污染：它进了 db.json、进了 Tag 索引、进了 sitemap、被静态托管同步到了线上、还被 CDN 缓存住。删源文件只能让下一次构建变干净，线上那几份副本得另外处理。</p><h2 id="二、Hexo-凭什么决定“什么算文章”"><a href="#二、Hexo-凭什么决定“什么算文章”" class="headerlink" title="二、Hexo 凭什么决定“什么算文章”"></a>二、Hexo 凭什么决定“什么算文章”</h2><p>先给总览。下面这张图是这次的完整问题链路，从目录一直走到你浏览器里看到的那个幽灵页面：</p><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart LR  A[普通目录放进 source&#x2F;_posts] --&gt; B[readDirWalker 遍历目录]  B --&gt; C{ignore 是否命中目录本身}  C --&gt;|是| D[目录层剪枝 整棵子树跳过]  C --&gt;|否| E{isHiddenFile 判定路径段首字符}  E --&gt;|是 下划线或点| F[当作隐藏文件跳过]  E --&gt;|否| G[注册为文章并渲染]  G --&gt; H[写入 db.json 与 public]  H --&gt; I[静态托管部署]  I --&gt; J[自定义域名经 CDN 缓存]  J --&gt; K[线上出现幽灵页面]  </pre></div><p><strong>这张图想让你看见什么</strong>：能不能变成“文章”，只由两个判定决定——ignore 匹配和 isHiddenFile。而“线上出现幽灵页面”在链条最末端，意味着你在前端犯的错，要先经过渲染、写库、部署、缓存四跳才变得可见；反过来，在前端删掉文件，也不会让末端立刻恢复。这条链路是不对称的，这是所有排查时间的来源。</p><p>之所以 source/<em>posts/scaffolds/page.md 会被当文章，是因为 scaffolds 这个路径段既不以 </em> 开头、也不以 . 开头。判定就写在 node_modules/hexo/lib/plugins/processor/common.js 里：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// node_modules/hexo/lib/plugins/processor/common.js:19-21</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">isHiddenFile</span>(<span class="params">path</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="regexp">/(^|\/)[_.]/</span>.<span class="title function_">test</span>(path);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// :24-30</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">isExcludedFile</span>(<span class="params">path, config</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (<span class="title function_">isTmpFile</span>(path)) <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">  <span class="keyword">if</span> (<span class="title function_">isMatch</span>(path, config.<span class="property">exclude</span>)) <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">  <span class="keyword">if</span> (<span class="title function_">isHiddenFile</span>(path) &amp;&amp; !<span class="title function_">isMatch</span>(path, config.<span class="property">include</span>)) <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">  <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>这段代码在说明什么</strong>：正则 /(^|\/)[_.]/ 是<strong>按路径段</strong>匹配的，不是按文件名。所以 source/_posts/.obsidian/、.copilot/、.agents/、.trash/ 这些点开头的目录天然安全，名叫 .nopost 的目录也一样安全；而 scaffolds/、copilot/ 这种普通名字的目录天然不安全。我仓库里 _posts 下确实混着这两类目录，事故只是其中不安全的那一半发作了。</p><p>顺带一句：exclude 帮不上忙。官方文档在 exclude 的示例旁边就明写了 “<em>posts/hello-world.md # Does not work”，要排除 _posts 里的东西只有两条路——给文件或目录名加 </em> 前缀，或者用 skip_render。</p><h2 id="三、第二个坑：ignore-是拿绝对路径匹配的"><a href="#三、第二个坑：ignore-是拿绝对路径匹配的" class="headerlink" title="三、第二个坑：ignore 是拿绝对路径匹配的"></a>三、第二个坑：ignore 是拿绝对路径匹配的</h2><p>我第一次的写法是这样的，很符合直觉：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">ignore:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">&#x27;_posts/copilot/**&#x27;</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">&#x27;_posts/scaffolds/**&#x27;</span></span><br></pre></td></tr></table></figure><p><strong>这段代码在说明什么</strong>：什么也没说明，因为它一条都不生效。hexo generate 不报错、不警告，页面照旧生成。静默失效，这是配置类事故里最难查的一类。</p><p>真正的判定在 node_modules/hexo/lib/box/index.js：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// :244-246</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">isIgnoreMatch</span>(<span class="params">path, ignore</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> path &amp;&amp; ignore &amp;&amp; ignore.<span class="property">length</span> &amp;&amp; <span class="title function_">isMatch</span>(path, ignore);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// :248-263</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">readDirWalker</span>(<span class="params">base, results, ignore, prefix</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (<span class="title function_">isIgnoreMatch</span>(base, ignore)) <span class="keyword">return</span> <span class="title class_">Promise</span>.<span class="title function_">resolve</span>();   <span class="comment">// :249 目录级剪枝</span></span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">Promise</span>.<span class="title function_">map</span>(<span class="title function_">readdir</span>(base).<span class="title function_">catch</span>(<span class="function"><span class="params">err</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (err &amp;&amp; err.<span class="property">code</span> === <span class="string">&#x27;ENOENT&#x27;</span>) <span class="keyword">return</span> [];</span><br><span class="line">    <span class="keyword">throw</span> err;</span><br><span class="line">  &#125;), <span class="keyword">async</span> path =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> fullpath = <span class="title function_">join</span>(base, path);                        <span class="comment">// :255 绝对路径</span></span><br><span class="line">    <span class="keyword">const</span> stats = <span class="keyword">await</span> <span class="title function_">stat</span>(fullpath);</span><br><span class="line">    <span class="keyword">const</span> prefixdPath = <span class="string">`<span class="subst">$&#123;prefix&#125;</span><span class="subst">$&#123;path&#125;</span>`</span>;</span><br><span class="line">    <span class="keyword">if</span> (stats.<span class="title function_">isDirectory</span>()) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="title function_">readDirWalker</span>(fullpath, results, ignore, <span class="string">`<span class="subst">$&#123;prefixdPath&#125;</span>/`</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (!<span class="title function_">isIgnoreMatch</span>(fullpath, ignore)) &#123;                   <span class="comment">// :261 传的是 fullpath</span></span><br><span class="line">      results.<span class="title function_">push</span>(prefixdPath);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>这段代码在说明什么</strong>：base 是 Box 的绝对路径（这个仓库是 /Users/youqi/u7_blog/source/_posts），fullpath = join(base, path) 更是绝对路径。所以第 261 行交给 micromatch 的永远是 /Users/youqi/u7_blog/source/_posts/copilot/x.md 这种整串，而 <code>_posts/copilot/**</code> 被 makeRe 编译成整串锚定的正则，开头就对不上，永不命中。</p><p>模式本身非法时 toRegExp 会 log.warn；但模式<strong>语法合法却匹配不到任何东西</strong>时，Hexo 一声不吭。这就是“配置写了、等于没写”的机制。</p><p>正确的写法必须用 <code>**/</code> 前缀去吃掉那段绝对路径：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># _config.yml</span></span><br><span class="line"><span class="comment"># Hexo 6.x 的 ignore 是用「绝对路径」去匹配的，所以模式要带 **/ 前缀</span></span><br><span class="line"><span class="attr">ignore:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">&#x27;**/_posts/copilot&#x27;</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">&#x27;**/_posts/scaffolds&#x27;</span></span><br></pre></td></tr></table></figure><p><strong>这段代码在说明什么</strong>：<code>**/</code> 负责匹配掉 /Users/youqi/u7_blog/source/ 这段前缀，剩下的部分才和真实路径对齐。官方文档的 ignore 示例本身也是这个形态（<code>**/foo</code>、<code>**/themes/*/foo</code>），只是它没解释“为什么非要 <code>**/</code>”，我自己也没往那个方向想。</p><h2 id="四、为什么-scaffolds-不带-反而更狠"><a href="#四、为什么-scaffolds-不带-反而更狠" class="headerlink" title="四、为什么 scaffolds 不带 /* 反而更狠"></a>四、为什么 scaffolds 不带 /* 反而更狠</h2><p>这里有个容易写反的方向问题：直觉上 <code>**/_posts/scaffolds/*</code> 比 <code>**/_posts/scaffolds</code> “更精确”，实际上前者弱得多。我在仓库里直接跑了 micromatch 验证（Hexo 6.3.0 依赖同一个库）：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123; isMatch &#125; = <span class="built_in">require</span>(<span class="string">&#x27;micromatch&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> dir  = <span class="string">&#x27;/Users/youqi/u7_blog/source/_posts/scaffolds&#x27;</span>;</span><br><span class="line"><span class="keyword">const</span> file = dir + <span class="string">&#x27;/page.md&#x27;</span>;</span><br><span class="line"><span class="keyword">const</span> deep = <span class="string">&#x27;/Users/youqi/u7_blog/source/_posts/copilot/copilot-conversations/x.md&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="title function_">isMatch</span>(dir,  [<span class="string">&#x27;**/_posts/scaffolds&#x27;</span>]);    <span class="comment">// true</span></span><br><span class="line"><span class="title function_">isMatch</span>(file, [<span class="string">&#x27;**/_posts/scaffolds&#x27;</span>]);    <span class="comment">// false</span></span><br><span class="line"><span class="title function_">isMatch</span>(dir,  [<span class="string">&#x27;**/_posts/scaffolds/*&#x27;</span>]);  <span class="comment">// false</span></span><br><span class="line"><span class="title function_">isMatch</span>(file, [<span class="string">&#x27;**/_posts/scaffolds/*&#x27;</span>]);  <span class="comment">// true</span></span><br><span class="line"><span class="title function_">isMatch</span>(deep, [<span class="string">&#x27;**/_posts/copilot/*&#x27;</span>]);    <span class="comment">// false   ← 深层文件挡不住</span></span><br></pre></td></tr></table></figure><p><strong>这段代码在说明什么</strong>：<code>**/_posts/scaffolds</code> 只匹配<strong>目录本身</strong>，一个文件都不匹配；<code>**/_posts/scaffolds/*</code> 只匹配<strong>直接子文件</strong>，目录本身不匹配。两者不是强弱关系，是打在完全不同的位置上。</p><div class="table-container"><table><thead><tr><th>模式</th><th>命中目录本身</th><th>命中目录下直接文件</th><th>命中深层文件</th></tr></thead><tbody><tr><td><code>_posts/scaffolds/**</code></td><td>否</td><td>否</td><td>否</td></tr><tr><td><code>**/_posts/scaffolds/*</code></td><td>否</td><td>是</td><td>否</td></tr><tr><td><code>**/_posts/scaffolds</code></td><td>是</td><td>否</td><td>否</td></tr><tr><td><code>**/_posts/copilot/*</code></td><td>否</td><td>是</td><td><strong>否</strong></td></tr><tr><td><code>**/_posts/copilot</code></td><td>是</td><td>否</td><td>否</td></tr></tbody></table></div><p>差别之所以是质变，在于第 249 行那句 if (isIgnoreMatch(base, ignore)) return Promise.resolve();——一旦模式命中<strong>目录本身</strong>，整棵子树连 readdir 都不会发生。而只命中文件时，目录照样被进入、逐层递归，你必须为每一层深度各补一条模式。</p><p>我这次是运气好才暴露出来的：Copilot 的目录不止一层，copilot/copilot-conversations/<em>.md 有 16 个，还有 copilot/skills/openartifacts-publish/SKILL.md 这种更深的。写 `**/_posts/copilot/</em>` 能挡住第一层，深层照样漏，而且是那种“我明明配了 ignore 啊”的漏。</p><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart TD  A[ignore 模式写在哪个层级] --&gt; B[匹配到目录本身]  A --&gt; C[只匹配到子文件]  B --&gt; D[readDirWalker 第一行直接返回]  D --&gt; E[整棵子树不再递归 深层文件全挡住]  C --&gt; F[目录仍被进入 递归继续向下]  F --&gt; G[必须为每一层深度各补一条模式]  G --&gt; H[漏一层就漏一篇 且不报错]  E --&gt; I[文章数从 79 回到 77]  </pre></div><p><strong>这张图想让你看见什么</strong>：同一个 ignore 选项，写在不同粒度上，走的是两条完全不同的代码路径——一条在进入目录前就返回，一条要在每个文件上反复判定。前者是 O(1) 剪枝，后者是 O(深度) 补丁。凡是“排除整个目录”的意图，就该让它命中目录本身。</p><h2 id="五、修复与验证：从-79-到-77"><a href="#五、修复与验证：从-79-到-77" class="headerlink" title="五、修复与验证：从 79 到 77"></a>五、修复与验证：从 79 到 77</h2><p>改动只有 _config.yml 那两行（外加一份 .gitignore，下一节说），但验证要做全。我实际执行的是 hexo clean &amp;&amp; hexo generate——不清缓存的话 db.json 里已有条目可能残留，这点我踩过：只跑 generate 时文章数没立刻回到预期。</p><div class="table-container"><table><thead><tr><th>检查项</th><th>期望</th><th>修复后实测</th></tr></thead><tbody><tr><td>文章数（generate 输出）</td><td>79 → 77</td><td>77</td></tr><tr><td><code>/posts/pagelink.html</code></td><td>404</td><td>404</td></tr><tr><td><code>/posts/0.html</code></td><td>404</td><td>404</td></tr><tr><td><code>/tags/copilot-conversation/</code></td><td>404</td><td>404</td></tr><tr><td>任意一篇正常文章</td><td>200</td><td>200</td></tr></tbody></table></div><p>对照文章必须一起验。只测 404 是有盲区的：如果把 ignore 写成 <code>**/_posts</code>，上面三个 URL 都会 404，看起来“修好了”，其实整站文章全没了。</p><p>事后再复核一次也有旁证：现在 db.json 里 Post 模型已经没有 pagelink、0.html、copilot 相关的任何 key，说明污染没有留在增量缓存里。</p><h2 id="六、这里我不太同意“用-gitignore-就够了”"><a href="#六、这里我不太同意“用-gitignore-就够了”" class="headerlink" title="六、这里我不太同意“用 .gitignore 就够了”"></a>六、这里我不太同意“用 .gitignore 就够了”</h2><p>出事后不止一个人跟我说：加 .gitignore 不就行了。我当时也这么想过，后来发现这个思路只对了一半。</p><p>.gitignore 和 hexo ignore 的边界完全不同：前者决定“哪些文件进仓库”，后者决定“哪些文件被 Hexo 扫描”。一个目录完全可以同时是“不该进仓库”和“会被 Hexo 扫描”的。我这仓库的实际情况正好把三种组合都占全了：</p><div class="table-container"><table><thead><tr><th>目录</th><th>需要 .gitignore</th><th>需要 hexo ignore</th><th>原因</th></tr></thead><tbody><tr><td><code>source/_posts/copilot/</code></td><td>需要</td><td>需要</td><td>工具数据，既不进仓库也不该被扫</td></tr><tr><td><code>source/_posts/.copilot/</code> <code>.agents/</code></td><td>需要</td><td>不需要</td><td>点开头，isHiddenFile 已经挡掉</td></tr><tr><td><code>source/_posts/scaffolds/</code></td><td><strong>不能</strong></td><td>需要</td><td>模板必须留在仓库，但绝不能当文章发</td></tr></tbody></table></div><p>第三行是决定性的反例：scaffolds/page.md 是 hexo new page 要读的模板，把它塞进 .gitignore 等于把脚手架扔了；它必须被 git 跟踪，同时必须被 Hexo 忽略。<strong>所以 .gitignore 在这件事上不是“更彻底的方案”，它连这个问题的一半都覆盖不到。</strong></p><p><strong>我的判断</strong>：判定标准只有一条——<strong>除了 git 之外，还有谁会读这个目录</strong>。git 读完就没别人读，.gitignore 足够；Hexo 还要读一遍，就必须写 hexo ignore。真正的治本在更前面一层：非文章的东西，一开始就别放进 source/_posts。</p><h2 id="七、三类东西永远不该放进-source-posts"><a href="#七、三类东西永远不该放进-source-posts" class="headerlink" title="七、三类东西永远不该放进 source/_posts"></a>七、三类东西永远不该放进 source/_posts</h2><p>source/<em>posts 在 Hexo 里不是一个“文件夹”，是一个<strong>输入队列</strong>：除 </em> 和 . 开头的路径段以外，一切都会被当文章。所以下面三类东西放进去，等于预约一次事故：</p><ol><li><strong>模板与脚手架</strong>。scaffolds/、页面模板、任何带 <code>&#123;&#123;title&#125;&#125;</code>、<code>&#123;&#123;date&#125;&#125;</code> 占位符的文件。它们的正确位置是项目根的 scaffolds/，不是 source/_posts/scaffolds/。</li><li><strong>工具的本地数据目录</strong>。Obsidian Copilot 的对话归档、.obsidian/、.trash/、.nopost、各种插件缓存。这些目录归工具所有，它们的 front-matter 不是写给 Hexo 的，abbrlink 撞车是必然。</li><li><strong>别人系统的导出物</strong>。AI 会话记录、导出日志、抓取结果。它们的字段结构（sessionId、usage、epoch）根本不在 Hexo 的 schema 里，Hexo 只会用默认值把它们硬塞成一篇文章。</li></ol><p><strong>我踩过的坑</strong>：我曾经以为“文件名随便写、反正会被当成文章”是 Hexo 的宽容。它不是宽容，它是没有判断力——它没有能力区分“作者写的文章”和“恰好是 .md 的东西”，这个判断只能由目录结构来承担。</p><h2 id="八、部署成功不等于线上生效"><a href="#八、部署成功不等于线上生效" class="headerlink" title="八、部署成功不等于线上生效"></a>八、部署成功不等于线上生效</h2><p>配置改完、generate 干净、本地 curl 200，我就以为结束了。结果刷新自定义域名，看到的还是旧页面。</p><p>第一次抓下来 92KB，页面里还有那段明文内容；隔一会儿再抓，123KB，变成新页面了。中间什么都没改。原因是自定义域名前面有一层 CDN，静态托管那边的文件已经更新，边缘节点还在吐旧副本。</p><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  sequenceDiagram  participant L as 本地 clean 与 generate  participant R as git 仓库  participant H as 静态托管  participant C as 自定义域名 CDN  participant B as 我的浏览器  L-&gt;&gt;R: 提交两条 ignore 与清理后的 public  R-&gt;&gt;H: 触发构建并部署  H-&gt;&gt;C: 回源取到新页面  C--&gt;&gt;B: 仍返回旧缓存 92KB  B-&gt;&gt;C: 刷新后再次请求  C--&gt;&gt;B: 返回新页面 123KB  </pre></div><p><strong>这张图想让你看见什么</strong>：部署完成和线上生效之间隔着一次缓存失效，而且这次失效不会通知你。注意我是靠<strong>字节数变化</strong>（92KB 到 123KB）才确认拿到的是两份不同内容——状态码两次都是 200，只看 curl -I 看不出任何异常。</p><p><strong>我的理解</strong>：验证“页面已消失”这件事，只看 HTTP 状态码不够，得同时看<strong>内容指纹</strong>（文件大小、或者一段特征字符串还在不在）。后来的做法是先 curl 拿正文 grep 那段特征文案，确认确实是新内容；拿不到再加查询串绕缓存，还是不行才去刷 CDN。</p><h2 id="九、下次直接照做的清单"><a href="#九、下次直接照做的清单" class="headerlink" title="九、下次直接照做的清单"></a>九、下次直接照做的清单</h2><ol><li><strong>把非文章目录挡在 source/_posts 之外</strong>：脚手架放项目根 scaffolds/，工具数据放到 source/ 之外的路径，这一步能消掉大部分事故。</li><li><strong>ignore 一律用 `</strong>/<code>前缀，并且匹配目录本身**：写</code>‘<strong>/_posts/copilot’<code>，不要写</code>‘_posts/copilot/</strong>‘<code>，也别画蛇添足加</code>/*`。</li><li><strong>改完 ignore 必须 hexo clean 再 generate</strong>，并核对文章数的变化符不符合预期——数字不动就是没生效。</li><li><strong>验证要成对</strong>：被排除的 URL 验 404，同时挑一篇正常文章验 200，防止排除过头。</li><li><strong>线上验证看内容不看状态码</strong>：比对文件大小或 grep 特征字符串，避免把 CDN 旧缓存当成修复成功。</li><li><strong>同时需要 .gitignore 和 hexo ignore 时，想清楚是哪一个在起作用</strong>；scaffolds/ 这种必须入库又必须被忽略的目录，就是两者的分界线。</li></ol><p>最后留一句自我提醒：这类配置坑的共同特征不是“配错了会报错”，而是“配错了什么都不发生”。对静默失败的东西，验证必须以<strong>数字变化</strong>为依据，而不是以“命令跑通了”为依据。</p>]]></content>
    
    
    <summary type="html">线上突然多出两个页面，一个标题字面量是模板占位符，另一个是工具会话归档。复盘 Hexo 6 的目录扫描规则、ignore 拿绝对路径匹配的静默坑，以及部署成功为什么不等于线上生效。</summary>
    
    
    
    <category term="杂项" scheme="https://u7u7.top/categories/%E6%9D%82%E9%A1%B9/"/>
    
    
    <category term="Hexo" scheme="https://u7u7.top/tags/Hexo/"/>
    
    <category term="博客" scheme="https://u7u7.top/tags/%E5%8D%9A%E5%AE%A2/"/>
    
    <category term="复盘" scheme="https://u7u7.top/tags/%E5%A4%8D%E7%9B%98/"/>
    
    <category term="排查" scheme="https://u7u7.top/tags/%E6%8E%92%E6%9F%A5/"/>
    
  </entry>
  
  <entry>
    <title>为什么 2026 是后端工程师做 Agent 的主场：从 Prompt 到 Harness</title>
    <link href="https://u7u7.top/posts/backend-agent-harness-2026.html"/>
    <id>https://u7u7.top/posts/backend-agent-harness-2026.html</id>
    <published>2026-10-17T00:30:00.000Z</published>
    <updated>2026-10-26T13:29:19.064Z</updated>
    
    <content type="html"><![CDATA[<h1 id="为什么-2026-是后端工程师做-Agent-的主场：从-Prompt-到-Harness"><a href="#为什么-2026-是后端工程师做-Agent-的主场：从-Prompt-到-Harness" class="headerlink" title="为什么 2026 是后端工程师做 Agent 的主场：从 Prompt 到 Harness"></a>为什么 2026 是后端工程师做 Agent 的主场：从 Prompt 到 Harness</h1><p>我今年被问得最多的问题，已经不再是”你怎么写提示词”，而是”你的 Agent 半夜挂了，第二天早上怎么把它恢复并解释清楚发生了什么”。</p><p>第二类问题是一个标准的后端问题：状态在哪、重试怎么幂等、日志怎么回放、权限怎么收口。所以先给结论：<strong>2026 年做 Agent 真正的壁垒不在 Prompt，而在 Harness。</strong> Prompt 只是这个系统最上面那层接口，Harness 才是服务端。</p><h2 id="一、先分层：Prompt-只是最薄的顶层"><a href="#一、先分层：Prompt-只是最薄的顶层" class="headerlink" title="一、先分层：Prompt 只是最薄的顶层"></a>一、先分层：Prompt 只是最薄的顶层</h2><p>大部分团队卡住的地方，是把 Agent 当成一个”对话功能”接进系统，于是所有复杂度都堆在提示词里；而真正决定它能不能上生产的，是下面几层。</p><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart LR  P[Prompt 层 指令与示例] --&gt; C[Context 层 上下文与记忆装配]  C --&gt; T[Tool 层 工具契约与参数校验]  T --&gt; H[Harness 层 状态机 重试 沙箱 观测]  H --&gt; R[Runtime 层 进程 配额 隔离]  R --&gt; O[产物与轨迹日志]  O --&gt; C  </pre></div><p>这张图想让你看见什么：注意那条从”产物与轨迹日志”回到”上下文装配”的箭头。它表示一次执行的结果必须能被下一次上下文装配读到，否则你的 Agent 永远只有单轮智商。上面三层靠模型和写作能力，下面三层靠工程能力，而且下面三层决定了上面三层能不能稳定复现。</p><h2 id="二、Harness-的六块，对应后端的六种老能力"><a href="#二、Harness-的六块，对应后端的六种老能力" class="headerlink" title="二、Harness 的六块，对应后端的六种老能力"></a>二、Harness 的六块，对应后端的六种老能力</h2><p>我把 Harness 拆成六块。每一块我都写清”后端已有的能力可以直接平移什么”，因为这是我说”主场”的唯一依据。</p><div class="table-container"><table><thead><tr><th>Harness 关注点</th><th>后端已有的对应物</th><th>可以直接平移什么</th><th>常见陷阱</th></tr></thead><tbody><tr><td>上下文与记忆</td><td>组装 DTO、拼装读模型、多级缓存</td><td>分层装配、按需裁剪、TTL 与失效</td><td>把所有历史一次性塞进去，延迟与成本双爆</td></tr><tr><td>工具调用契约</td><td>RPC 接口、OpenAPI、DTO 校验</td><td>schema 校验、版本兼容、错误码分层</td><td>用自然语言约定参数，模型自由发挥</td></tr><tr><td>状态机与重试</td><td>订单状态机、幂等键、退避重试</td><td>幂等键、可重试错误分类、上限与熔断</td><td>把”模型再想一次”当成重试</td></tr><tr><td>权限与沙箱</td><td>网关鉴权、RBAC、容器隔离</td><td>能力清单、最小权限、旁路审批</td><td>让模型自己决定调哪个高权限工具</td></tr><tr><td>可观测性与回放</td><td>链路追踪、审计日志、事件溯源</td><td>trace 粒度到”一次工具尝试”、可重放</td><td>只记最终答案，中间轨迹全丢</td></tr><tr><td>成本控制</td><td>限流、配额、预算告警</td><td>按任务预算、按租户配额、超支降级</td><td>只统计 token 总量，不按任务归因</td></tr></tbody></table></div><h3 id="上下文与记忆"><a href="#上下文与记忆" class="headerlink" title="上下文与记忆"></a>上下文与记忆</h3><p>上下文不是”越多越好”，而是”装配得越准越好”。这点和读模型拼装完全同构：同一个实体，列表页要 5 个字段，详情页要 40 个字段，你绝不会给列表页返回详情页的 DTO。Anthropic 在 2025-09-29 的《Effective context engineering for AI agents》里把上下文当成一种有限预算资源来管理，这个视角对后端来说几乎是无门槛的。</p><h3 id="工具调用契约"><a href="#工具调用契约" class="headerlink" title="工具调用契约"></a>工具调用契约</h3><p>这是我认为最被低估的一块。工具签名不是文档，是契约。参数必须可校验、错误必须可分类、返回必须可截断。模型上下文协议 MCP 在 <a href="https://modelcontextprotocol.io/specification/2026-07-28">2026-07-28 版规范</a> 里明确用 JSON-RPC 2.0 承载消息，并区分 Host、Client、Server 三种角色——这就是一次典型的”把不确定的自然语言收敛成有边界的协议”。</p><h3 id="状态机与重试"><a href="#状态机与重试" class="headerlink" title="状态机与重试"></a>状态机与重试</h3><p>Agent 的一次任务不是一次调用，而是一条带分支的状态机：思考、选工具、执行、观察、再思考、收尾。后端做这件事有成熟范式，不需要发明新理论。</p><h3 id="权限与沙箱"><a href="#权限与沙箱" class="headerlink" title="权限与沙箱"></a>权限与沙箱</h3><p>模型没有”意图可信”这回事。工具有多危险，就要有多重的沙箱。最小权限、白名单、超时、资源配额，这些在网关和容器层已经做完了，搬过来即可。</p><h3 id="可观测性与回放"><a href="#可观测性与回放" class="headerlink" title="可观测性与回放"></a>可观测性与回放</h3><p>我踩过的坑在这里：我早期只记录最终的模型输出，结果线上一个工具偶发超时导致的任务失败，我复盘了三个小时——因为我不知道是第几次尝试、传了什么参数、错误码是什么。后来我把 trace 粒度下沉到”一次工具尝试”，并且把每次的入参、出参、错误分类、耗时全部落盘，问题五分钟就定位了。<strong>可回放不是锦上添花，它是 Harness 的第一性需求</strong>：没有回放，你连”这次改动是不是变好了”都无法判断。</p><h3 id="成本控制"><a href="#成本控制" class="headerlink" title="成本控制"></a>成本控制</h3><p>成本要按任务归因，不能只按天统计总量。一个 30 轮的重试循环，token 消耗可能是一次正常任务的 20 倍，而你在日报里看到的只是”今天用量涨了 15%”。</p><h2 id="三、一次失败重试到底长什么样"><a href="#三、一次失败重试到底长什么样" class="headerlink" title="三、一次失败重试到底长什么样"></a>三、一次失败重试到底长什么样</h2><p>上面说的是静态结构，真正让 Agent 从 demo 变成系统的，是失败路径。下面这张时序图是我认为每个 Agent harness 都必须能画出来的图。</p><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  sequenceDiagram  participant U as 调用方  participant H as Harness  participant L as 模型  participant T as 工具服务  participant O as 观测与回放  U-&gt;&gt;H: 提交任务与幂等键  H-&gt;&gt;L: 装配上下文并请求下一步  L--&gt;&gt;H: 返回工具调用与参数  H-&gt;&gt;H: 校验 schema 与权限白名单  H-&gt;&gt;T: 携带幂等键执行  T--&gt;&gt;H: 返回超时  Note right of H: 只重试可重试错误 其余直接失败  H-&gt;&gt;O: 记录一次尝试与错误分类  H-&gt;&gt;T: 退避后重试 复用同一幂等键  T--&gt;&gt;H: 返回成功结果  H-&gt;&gt;O: 落盘完整轨迹供回放  H--&gt;&gt;U: 返回结构化结果与轨迹引用  </pre></div><p>这张图想让你看见什么：注意”复用同一幂等键”这一步。这是整个 harness 里最容易写错、也最能区分工程水平的地方——如果重试换了幂等键，下游就会执行两次，你的 Agent 会开始重复下单、重复发券、重复扣款。模型永远不该负责这件事，它只负责说”我要调这个工具”，剩下的语义由 harness 保证。</p><h2 id="四、把”重试”写成有幂等键的状态机"><a href="#四、把”重试”写成有幂等键的状态机" class="headerlink" title="四、把”重试”写成有幂等键的状态机"></a>四、把”重试”写成有幂等键的状态机</h2><p>光说抽象没用，下面是我在项目里用的一段骨架，语言是 Java，因为这套东西最终要落在服务端。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">ToolInvoker</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">enum</span> <span class="title class_">Outcome</span> &#123; OK, RETRYABLE, FATAL, EXHAUSTED &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">record</span> <span class="title class_">ToolCall</span><span class="params">(String name, String argsJson)</span> &#123;&#125;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">record</span> <span class="title class_">ToolResult</span><span class="params">(String summary)</span> &#123;&#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">ToolException</span> <span class="keyword">extends</span> <span class="title class_">RuntimeException</span> &#123;</span><br><span class="line">        <span class="type">public</span> <span class="title function_">ToolException</span><span class="params">(String msg)</span> &#123; <span class="built_in">super</span>(msg); &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">RetryableToolException</span> <span class="keyword">extends</span> <span class="title class_">ToolException</span> &#123;</span><br><span class="line">        <span class="type">public</span> <span class="title function_">RetryableToolException</span><span class="params">(String msg)</span> &#123; <span class="built_in">super</span>(msg); &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">FatalToolException</span> <span class="keyword">extends</span> <span class="title class_">ToolException</span> &#123;</span><br><span class="line">        <span class="type">public</span> <span class="title function_">FatalToolException</span><span class="params">(String msg)</span> &#123; <span class="built_in">super</span>(msg); &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">ToolClient</span> &#123;</span><br><span class="line">        <span class="type">ToolResult</span> <span class="title function_">execute</span><span class="params">(ToolCall call, String idempotencyKey, <span class="type">long</span> timeoutMillis)</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">TraceStore</span> &#123;</span><br><span class="line">        <span class="type">void</span> <span class="title function_">record</span><span class="params">(ToolCall call, <span class="type">int</span> attempt, Outcome outcome, String detail)</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">MAX_ATTEMPTS</span> <span class="operator">=</span> <span class="number">3</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> ToolClient client;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> TraceStore trace;</span><br><span class="line"></span><br><span class="line">    <span class="type">public</span> <span class="title function_">ToolInvoker</span><span class="params">(ToolClient client, TraceStore trace)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.client = client;</span><br><span class="line">        <span class="built_in">this</span>.trace = trace;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="type">ToolResult</span> <span class="title function_">invoke</span><span class="params">(ToolCall call, String idempotencyKey)</span> &#123;</span><br><span class="line">        <span class="type">RuntimeException</span> <span class="variable">last</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">attempt</span> <span class="operator">=</span> <span class="number">1</span>; attempt &lt;= MAX_ATTEMPTS; attempt++) &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                <span class="type">ToolResult</span> <span class="variable">result</span> <span class="operator">=</span> client.execute(call, idempotencyKey, <span class="number">30_000L</span>);</span><br><span class="line">                trace.record(call, attempt, Outcome.OK, result.summary());</span><br><span class="line">                <span class="keyword">return</span> result;</span><br><span class="line">            &#125; <span class="keyword">catch</span> (FatalToolException e) &#123;</span><br><span class="line">                trace.record(call, attempt, Outcome.FATAL, e.getMessage());</span><br><span class="line">                <span class="keyword">throw</span> e;</span><br><span class="line">            &#125; <span class="keyword">catch</span> (RetryableToolException e) &#123;</span><br><span class="line">                last = e;</span><br><span class="line">                trace.record(call, attempt, Outcome.RETRYABLE, e.getMessage());</span><br><span class="line">                backoff(attempt);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        trace.record(call, MAX_ATTEMPTS, Outcome.EXHAUSTED, String.valueOf(last));</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">ToolException</span>(<span class="string">&quot;tool exhausted after &quot;</span> + MAX_ATTEMPTS + <span class="string">&quot; attempts: &quot;</span> + call.name());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="type">void</span> <span class="title function_">backoff</span><span class="params">(<span class="type">int</span> attempt)</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            Thread.sleep(<span class="number">200L</span> * (<span class="number">1L</span> &lt;&lt; (attempt - <span class="number">1</span>)));</span><br><span class="line">        &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">            Thread.currentThread().interrupt();</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">ToolException</span>(<span class="string">&quot;interrupted during backoff&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这段代码在说明什么：说明重试的核心不是循环，而是<strong>三件确定的事</strong>——错误被分成可重试和不可重试两类（fatal 立刻抛，不再浪费两次调用预算）、幂等键从调用方一路传到工具服务且全程不变（重试语义安全）、每一次尝试无论成败都写进 trace（可回放）。模型在这里完全没有出场机会，这正是我想强调的分工：模型负责决策，harness 负责语义安全。</p><h2 id="五、我的判断：哪类团队现在做-Agent-是浪费"><a href="#五、我的判断：哪类团队现在做-Agent-是浪费" class="headerlink" title="五、我的判断：哪类团队现在做 Agent 是浪费"></a>五、我的判断：哪类团队现在做 Agent 是浪费</h2><p>我的判断是：2026 年做 Agent 踩空最多的不是技术选型，而是前提不成立。下面两种团队，现在投入大概率是浪费。</p><p><strong>第一种，数据边界不清的团队。</strong> 他们连”哪些数据可以出内网、哪些字段必须脱敏、哪些调用要审批”都答不出来，却已经在讨论多 agent 协作。这类团队做出来的东西，Demo 阶段惊艳，安全评审阶段直接死掉。边界不清的时候，任何 harness 都是给漏洞加速。</p><p><strong>第二种，没有真实工具面的团队。</strong> 如果 Agent 能调用的”工具”，本质上只有搜索加总结，那它就不需要 harness，只需要一个更好的搜索框。Harness 的价值来自”真实副作用”：写库、发消息、下单、改配置。没有副作用，就没有幂等、没有权限、没有回滚，也就没有工程壁垒。</p><p>反过来，两类团队是刚需：一是已经有稳定内部平台和大量 API、正在被”人肉操作 SOP”拖住的团队；二是把 Agent 当成线上服务而不是功能点的团队，他们天然需要 SLA、trace、回放和预算。</p><h2 id="六、这里我不太同意：模型跃迁会不会把-harness-吃掉"><a href="#六、这里我不太同意：模型跃迁会不会把-harness-吃掉" class="headerlink" title="六、这里我不太同意：模型跃迁会不会把 harness 吃掉"></a>六、这里我不太同意：模型跃迁会不会把 harness 吃掉</h2><p>我不太同意”harness 只是过渡方案，模型一强就没了”这个流行说法，但我也不同意把它说成永久壁垒。我的理解是：<strong>模型变强会吃掉 harness 的”策略层”，但吃不掉”责任层”。</strong></p><p>策略层指的是那些靠提示词技巧凑出来的自愈能力，比如让模型自己决定要不要重试、自己判断该不该相信工具返回。这部分确实会被吃掉，而且我认为应该被吃掉，因为它本来就不该由模型承担。</p><p>责任层吃不掉：幂等键写在哪、权限谁批、审计留存多久、出了问题由谁向业务方解释。这些是组织责任，不是推理能力。模型再强，也不会替你承担一次重复扣款的后果。</p><p>反例也要说清：如果某天模型能稳定输出带可验证契约的工具调用，并且平台层直接内建了重试与沙箱（这在 MCP 这类协议演进中是可见趋势），那么今天很多手写 harness 会退化成”配置”。所以”后端主场”这句话成立的条件是：<strong>你做的 harness 解决的是责任与可复现问题，而不是在替模型补智力。</strong> 如果只是后者，你的代码就是一层迟早要被删掉的胶水。</p><p>还有一个来自 Java 生态的现实提醒：JDK 27 于 2026-09-15 GA，<a href="https://openjdk.org/projects/jdk/27/">它并不是 LTS</a>——按 <a href="https://www.oracle.com/java/technologies/java-se-support-roadmap.html">Oracle 官方路线图</a>，LTS 只有 8、11、17、21、25，下一个是 2027 年 9 月的 Java 29。而 JDK 27 里的结构化并发还停在 <a href="https://openjdk.org/projects/jdk/27/">JEP 533 第七次预览</a>。这两件事放在一起说明：Agent harness 里最需要的”可取消、可超时、可收敛”的并发原语，在 JVM 上还没有定稿，你得自己包一层。这也是后端工程师的主场，但不是一个舒服的主场。</p><h2 id="七、公开信号与待核项"><a href="#七、公开信号与待核项" class="headerlink" title="七、公开信号与待核项"></a>七、公开信号与待核项</h2><p>可核实的公开信号，我按对本文论点的重要性排序：</p><ul><li><a href="https://www.oracle.com/java/technologies/java-se-support-roadmap.html">Oracle Java SE 支持路线图</a>（更新于 2026-10-20）：LTS 为 8/11/17/21/25，下一个 LTS 是 2027 年 9 月的 Java 29。</li><li><a href="https://openjdk.org/projects/jdk/27/">OpenJDK JDK 27 项目页</a>：GA 日期 2026-09-15，特性包含 JEP 533 结构化并发第七次预览、JEP 534 紧凑对象头默认开启。</li><li><a href="https://modelcontextprotocol.io/specification/2026-07-28">MCP 规范 2026-07-28</a>：基于 JSON-RPC 2.0，区分 Host/Client/Server；<a href="https://modelcontextprotocol.io/docs/2026-07-28/learn/versioning">版本策略</a> 明确版本号以”最后一次不兼容变更日期”命名。该修订取代上一版 2025-11-25。</li><li><a href="https://www.anthropic.com/engineering/building-effective-agents">Anthropic《Building effective agents》</a>（2024-12-19）：主张先用最简单的方案，workflow 与 agent 要区分，不要一上来就上自主循环。</li><li><a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents">Anthropic《Effective context engineering for AI agents》</a>（2025-09-29）：把上下文当作有限预算来管理。</li><li><a href="https://spring.io/blog/2025/05/20/spring-ai-1-0-GA-released">Spring AI 1.0 GA 发布</a>（2025-05-20）：Java 生态有了正式版的 Agent 抽象，意味着后端不必从零拼装。</li></ul><p>待核的两项，我不编：</p><ol><li><strong>“Agent 相关岗位占比”这类招聘数据——待核。</strong> 我没有可核查的一手来源，只看到二手转述，无法确认统计口径与样本，所以本文不引用任何招聘比例数字。</li><li><strong>“模型跃迁能替代多少比例的手写 harness”——待核。</strong> 这需要同一批任务在新旧模型上的对照实验，我没有做过，公开资料也没有可对齐的基准，任何具体百分比我都不会写。</li></ol><h2 id="八、可执行清单"><a href="#八、可执行清单" class="headerlink" title="八、可执行清单"></a>八、可执行清单</h2><ol><li>先画你自己那条失败路径的时序图。如果画不出来，说明你的 Agent 还没有 harness，只有一个 while 循环。</li><li>给每一个工具有副作用的调用加幂等键，并规定”重试不得换键”这一条硬规则。</li><li>把错误分成 fatal 与 retryable 两类，fatal 立刻失败，别把重试当成”让模型再想想”。</li><li>把 trace 粒度下沉到”一次工具尝试”，字段至少包含 attempt、入参摘要、错误分类、耗时。</li><li>给每个任务设预算上限，超支降级而不是静默继续，并按任务归因统计成本。</li><li>在动手前先回答三个问题：数据边界在哪、工具的真实副作用是什么、出问题谁负责。三个都答不上来，就先别做 Agent。</li></ol>]]></content>
    
    
    <summary type="html">Prompt 只是接口，Harness 才是服务端。拆开上下文、工具契约、重试状态机、沙箱与回放，说清后端能平移什么，哪类团队做了是浪费。</summary>
    
    
    
    <category term="理解" scheme="https://u7u7.top/categories/%E7%90%86%E8%A7%A3/"/>
    
    
    <category term="AI" scheme="https://u7u7.top/tags/AI/"/>
    
    <category term="Agent" scheme="https://u7u7.top/tags/Agent/"/>
    
    <category term="Harness" scheme="https://u7u7.top/tags/Harness/"/>
    
    <category term="后端工程" scheme="https://u7u7.top/tags/%E5%90%8E%E7%AB%AF%E5%B7%A5%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>Redis 8.4 之后，缓存和检索引擎的边界已经消失</title>
    <link href="https://u7u7.top/posts/redis-8-4-boundary.html"/>
    <id>https://u7u7.top/posts/redis-8-4-boundary.html</id>
    <published>2026-10-15T00:30:00.000Z</published>
    <updated>2026-10-26T13:29:19.087Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Redis-8-4-之后，缓存和检索引擎的边界已经消失"><a href="#Redis-8-4-之后，缓存和检索引擎的边界已经消失" class="headerlink" title="Redis 8.4 之后，缓存和检索引擎的边界已经消失"></a>Redis 8.4 之后，缓存和检索引擎的边界已经消失</h1><p>先给一个可能让你不舒服的结论：把向量检索塞进 Redis，2026 年真正的风险已经不是「性能够不够」，而是「你以为它解决了延迟问题，其实它主要解决的是运维拓扑问题」。延迟那一层，它当然有帮助，但帮助幅度远小于宣传口径；真正被改变的是你少维护一套系统、少一条同步链路、少一次「缓存与索引不一致」的排障。</p><p>这篇文章只做三件事：把 8.4 到底改了什么核清楚、把「什么情况下该塞、什么情况下是灾难」画出一条可执行的分界线、以及把我在真实项目里踩到的坑摆出来。</p><h2 id="一、先把-8-4-到底改了什么核清楚"><a href="#一、先把-8-4-到底改了什么核清楚" class="headerlink" title="一、先把 8.4 到底改了什么核清楚"></a>一、先把 8.4 到底改了什么核清楚</h2><p>Redis 8.4 是 <strong>GA</strong>，不是 RC：<code>v8.4.0</code>，发布日期为 2025 年 11 月 18 日（官方 00-RELEASENOTES）。往前一版是 8.4-RC1（<code>v8.3.240</code>，2025 年 11 月 4 日），官方在 RC 的说明里明确写着「RC 是功能完整的预发布版，不适合生产使用」。所以如果你在某个公众号上看到 11 月就把 8.4-RC1 的数字当 GA 数字引用，那是不严谨的。</p><p>相对 8.2，和检索直接相关的变更只有这么几条，我按官方原文整理：</p><div class="table-container"><table><thead><tr><th>变更</th><th>官方描述里的关键限定</th><th>我的解读</th></tr></thead><tbody><tr><td><code>FT.HYBRID</code></td><td>混合检索与融合打分，支持 RRF 与 LINEAR 两种合并方式</td><td>关键词召回与向量召回在服务端一次完成，不用应用层自己 merge</td></tr><tr><td>查询引擎 I/O 线程</td><td>FT.* 命令获得吞吐提升，未给单一百分比</td><td>提升了并发能力，不等于单条查询变快</td></tr><tr><td>通用 I/O 线程</td><td>示例条件是「缓存场景 10% SET / 90% GET、4 核」，吞吐提升大于 30%</td><td>这是缓存读写场景的数字，不是检索场景的数字</td></tr><tr><td>JSON 同构数组</td><td>内存最多下降 91%</td><td>只对同构数组成立，异构数组不适用</td></tr><tr><td><code>search-on-oom</code></td><td>IGNORE / FAIL / RETURN 三值，默认 RETURN</td><td>默认实现是「部分节点 OOM 就返回部分结果」，不报错</td></tr><tr><td><code>search-default-scorer</code></td><td>默认打分器改为 BM25STD</td><td>升级后相关性排序会变，线上要做 A/B</td></tr></tbody></table></div><p>版本状态还有一层必须说清楚：本文写作时间是 2026 年 10 月，此时 Redis 已经有 <strong>8.6 GA</strong>（2026 年 2 月 10 日）、<strong>8.8 GA</strong>（2026 年 5 月 25 日）、<strong>8.10 GA</strong>（2026 年 7 月 29 日），最新补丁是 8.10.2（2026 年 9 月 17 日）。所以「8.4 之后」不是修辞手法，是事实。8.6 把向量集的 8bit 量化与二值距离计算做了 SIMD 向量化；8.8 给 <code>FT.HYBRID</code> 补上了 <code>FT.PROFILE</code> 支持，并新增「每分片少取候选」的 KNN 参数。这几条我只核到了 release notes 的条目，<strong>没有做性能复现</strong>，标为未复现，请不要引用成基准数据。</p><h2 id="二、演化线：从「附挂模块」到「内核能力」"><a href="#二、演化线：从「附挂模块」到「内核能力」" class="headerlink" title="二、演化线：从「附挂模块」到「内核能力」"></a>二、演化线：从「附挂模块」到「内核能力」</h2><p>要理解边界为什么消失，得把 8.0 和 8.4 连起来看。</p><p>Redis 8.0（<code>v8.0.0</code>，2025 年 5 月）做的是<strong>归属</strong>上的合并：RediSearch、RedisJSON、RedisTimeSeries、RedisBloom 不再是独立模块，代码并入主仓库，许可改成 RSALv2 / SSPLv1 / AGPLv3 三选一，查询引擎第一次具备水平与垂直扩展能力。注意一个容易被忽略的细节：8.0 里 <strong>Vector set 是 beta</strong>，官方就是这么标的。</p><p>8.4 做的是<strong>能力</strong>上的合并：<code>FT.HYBRID</code> 把「一次查询里同时做关键词召回和向量召回，再融合打分」这件事搬进了服务端。这才是边界真正模糊的那一步——在此之前，混合检索的 RRF 融合是应用层的活，你得发两次 <code>FT.SEARCH</code>、拿两份结果、在自己代码里按位次算分、再截断。那不是架构问题，是一段谁都不愿意维护的胶水代码。</p><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  stateDiagram-v2    [*] --&gt; 纯缓存    纯缓存 --&gt; 外挂模块 : 需要全文与向量检索    外挂模块 --&gt; 内核一体 : Redis 8 合并模块    内核一体 --&gt; 混合检索 : 8.4 提供 FT.HYBRID    混合检索 --&gt; 检索引擎分家 : 内存成本或一致性越界    检索引擎分家 --&gt; 纯缓存 : 检索迁出 只留缓存  </pre></div><p>这张图想让你看见的是：这条演化线不是单向的，最后一条边是我加的，也是真实会发生的——当内存成本或者一致性要求越界时，团队会把检索迁回 Elasticsearch 或者 Postgres，Redis 退回纯缓存。我在两个团队里都见过这个回退，而且回退的成本比当初「顺手加一个索引」高得多，因为那时业务已经在依赖 Redis 的相关性排序了。</p><h2 id="三、我的判断：分界线在哪里"><a href="#三、我的判断：分界线在哪里" class="headerlink" title="三、我的判断：分界线在哪里"></a>三、我的判断：分界线在哪里</h2><p>我的判断是：把检索放进 Redis 是否成立，只由一条不等式决定——<strong>向量与索引常驻内存的规模，必须显著小于实例内存预算</strong>。我用的阈值是 30%，超过就该重新设计。</p><p>先算清楚量级，不要凭感觉。768 维 float32 单条向量是 768 × 4 = 3072 字节，约 3KB；这还没算哈希表头、文档正文、倒排索引和 HNSW 图结构。所以：</p><ul><li>10 万条文档向量 ≈ 0.3GB 裸向量，加上文档与索引开销，实际按 0.6-1GB 估</li><li>100 万条 ≈ 3GB 裸向量，实际按 6-10GB 估</li><li>1000 万条 ≈ 30GB 裸向量，实际按 60GB 以上估</li></ul><p>最后一行就是分界线。如果你的实例是 16GB 或 32GB，1000 万条 768 维全精度向量根本放不进去，唯一的出路是量化到 8bit（把每维 4 字节压到 1 字节，size 降到四分之一）或者二值量化，代价是召回率要重新调参。这里我不太同意「先上，不行再扩」的做法：向量索引的扩容和普通缓存的扩容不是一回事，主从全量同步、RDB 加载、索引重建这几步都是内存峰值场景，线上被打到 <code>search-on-oom</code> 的默认 RETURN 语义，你拿到的就是部分结果而不是错误——这种「安静的错误」比超时难查得多。</p><p>所以我把结论写成两条清单。</p><p><strong>适合塞进 Redis 的情形</strong></p><ol><li>P99 要求 10ms 级别，且查询形态是「前置过滤 + top-k」，没有复杂排序</li><li>向量条数在百万级以内，或愿意接受 8bit 量化并重新评估召回</li><li>embedding 模型只有 1-3 个，维度固定，不需要频繁重建索引</li><li>数据可以接受最终一致：索引更新滞后几百毫秒对业务无感</li><li>团队规模小，少一套 ES 集群带来的运维收益大于功能损失</li></ol><p><strong>不适合的情形</strong></p><ol><li>需要多字段相关性调权、复杂聚合、跨索引 join：这是 Elasticsearch 的地盘，不是 Redis 的地盘</li><li>写入强一致、要求「写完立刻能精确查到」：Redis 的索引更新和主从复制不提供这个保证</li><li>数据量超过内存预算且必须全量在线：量化是缓兵之计，不是解决方案</li><li>中文分词与词库治理需求重：查询引擎能分词，但自定义词典、同义词、停用词那一套工程化程度远不如 ES</li><li>需要多租户隔离、细粒度权限、审计：这些是搜索平台的能力，不是缓存的能力</li></ol><h2 id="四、一段能跑起来的代码，比十页-PPT-有用"><a href="#四、一段能跑起来的代码，比十页-PPT-有用" class="headerlink" title="四、一段能跑起来的代码，比十页 PPT 有用"></a>四、一段能跑起来的代码，比十页 PPT 有用</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 约定：$VEC 是序列化后的 768 维 float32 向量二进制，实际用客户端库或 redis-cli -x 传入</span></span><br><span class="line"><span class="comment"># 1. 建索引：文本字段 + 向量字段同文档，ON HASH</span></span><br><span class="line">redis-cli FT.CREATE doc_idx ON HASH PREFIX 1 doc: SCHEMA \</span><br><span class="line">  title TEXT WEIGHT 2.0 \</span><br><span class="line">  tag TAG SEPARATOR , \</span><br><span class="line">  emb VECTOR HNSW 6 TYPE FLOAT32 DIM 768 DISTANCE_METRIC COSINE</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 纯向量召回：KNN 走 HNSW，必须 DIALECT 2</span></span><br><span class="line">redis-cli FT.SEARCH doc_idx <span class="string">&#x27;*=&gt;[KNN 10 @emb $vec AS score]&#x27;</span> \</span><br><span class="line">  PARAMS 2 vec <span class="string">&quot;<span class="variable">$VEC</span>&quot;</span> SORTBY score DIALECT 2 RETURN 2 title score</span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. 带前置过滤的召回：过滤写在查询里，不是拿回来再筛</span></span><br><span class="line">redis-cli FT.SEARCH doc_idx <span class="string">&#x27;(@tag:&#123;cache|redis&#125;)=&gt;[KNN 10 @emb $vec AS score]&#x27;</span> \</span><br><span class="line">  PARAMS 2 vec <span class="string">&quot;<span class="variable">$VEC</span>&quot;</span> SORTBY score DIALECT 2 LIMIT 0 10</span><br><span class="line"></span><br><span class="line"><span class="comment"># 4. 聚合：能算，但别指望它替代 OLAP</span></span><br><span class="line">redis-cli FT.AGGREGATE doc_idx <span class="string">&quot;*&quot;</span> LOAD 2 <span class="string">&quot;@price&quot;</span> <span class="string">&quot;@stock&quot;</span> \</span><br><span class="line">  APPLY <span class="string">&quot;@price*@stock&quot;</span> AS amount \</span><br><span class="line">  GROUPBY 1 <span class="string">&quot;@tag&quot;</span> REDUCE SUM 1 <span class="string">&quot;@amount&quot;</span> AS total \</span><br><span class="line">  SORTBY 2 <span class="string">&quot;@total&quot;</span> DESC LIMIT 0 10 DIALECT 2</span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"># redis.conf 里和这条边界直接相关的四项</span><br><span class="line">io-threads 4</span><br><span class="line">search-on-oom RETURN</span><br><span class="line">search-default-scorer BM25STD</span><br><span class="line">lookahead 16</span><br></pre></td></tr></table></figure><p>这段代码在说明什么：第一段把「文档」和「向量」放在同一个 Hash 里，这才是「缓存与检索边界消失」的物理形态——一次写入、一份内存、一个 TTL，不需要再维护一条缓存到索引的同步链路。第二段和第三段的区别是关键：<code>*</code> 开头的纯 KNN 是全库召回再排序，而 <code>(@tag:&#123;...&#125;)=&gt;[KNN ...]</code> 是<strong>先过滤再召回</strong>，两者的召回质量和延迟差一个量级，很多人只写了第二种却以为自己在用第一种。第四段是边界提醒：<code>FT.AGGREGATE</code> 能做出 GROUPBY 和 APPLY，说明它「能算」，但它的执行模型是单机内存聚合，遇到高基数分组和多重 REDUCE 就会把内存打上去，别拿它替 OLAP。</p><p>那四行配置也各有代价：<code>io-threads</code> 调大提升吞吐但增加上下文切换，官方给的大于 30% 是在 4 核、10% SET / 90% GET 的缓存场景测的，检索场景别直接套用；<code>search-on-oom RETURN</code> 是默认值，它意味着宁可给部分结果也不报错，对检索场景我倾向于改成 <code>FAIL</code>，让错误显式暴露；<code>search-default-scorer BM25STD</code> 是 8.4 改的新默认值，升级后相关性排序会漂移；<code>lookahead</code> 默认 16，是命令解析预取深度。</p><h2 id="五、写路径和读路径到底长什么样"><a href="#五、写路径和读路径到底长什么样" class="headerlink" title="五、写路径和读路径到底长什么样"></a>五、写路径和读路径到底长什么样</h2><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart LR    subgraph WP[写路径]        A[业务写入] --&gt; B[文档键 与 向量字段]        B --&gt; C[索引增量更新]        C --&gt; D[全文倒排]        C --&gt; E[向量 HNSW 图]    end    subgraph RP[读路径]        F[查询请求] --&gt; G[关键词召回]        F --&gt; H[向量 KNN 召回]        G --&gt; I[融合打分 RRF 或 LINEAR]        H --&gt; I        I --&gt; J[前置过滤 排序 分页]        J --&gt; K[结果回写热点缓存]    end    D -.-&gt; G    E -.-&gt; H    K -.-&gt; B    M[内存预算] -.-&gt; D    M -.-&gt; E  </pre></div><p>这张图想让你看见什么：写路径只有一次写内存，读路径却分成两条召回再融合，而下面那个「内存预算」节点同时约束倒排和 HNSW 两份结构——<strong>你要付的是两份索引的内存</strong>，不是一份。很多容量评估漏掉的正是这里：算向量算得很仔细，忘了倒排索引在高基数字段上的膨胀，最后 OOM 的是文本索引而不是向量。</p><h2 id="六、Redis-8-4-Elasticsearch-pgvector-四个维度对比"><a href="#六、Redis-8-4-Elasticsearch-pgvector-四个维度对比" class="headerlink" title="六、Redis 8.4 / Elasticsearch / pgvector 四个维度对比"></a>六、Redis 8.4 / Elasticsearch / pgvector 四个维度对比</h2><p>先声明：下面不是同机 benchmark，是量级判断加成本结构判断。任何让你「一个数字定生死」的对比表都不可信。</p><div class="table-container"><table><thead><tr><th>维度</th><th>Redis 8.4</th><th>Elasticsearch</th><th>pgvector</th></tr></thead><tbody><tr><td>延迟</td><td>亚毫秒到个位数毫秒，进程内无网络跳数</td><td>十毫秒级起，取决于分片与段合并状态</td><td>毫秒级，受 Postgres 连接池与 HNSW ef_search 影响</td></tr><tr><td>内存成本</td><td>全量常驻内存，最贵的一档</td><td>堆内存加堆外段缓存，磁盘为主，可接受冷数据</td><td>索引落盘，靠 shared_buffers 缓存，最便宜</td></tr><tr><td>查询能力</td><td>过滤加 top-k 很强，复杂聚合弱，无跨索引 join</td><td>最强，聚合、相关性调权、分词治理齐全</td><td>SQL 全能力加向量，复杂排序交给优化器</td></tr><tr><td>运维复杂度</td><td>最低，一套集群两种用途</td><td>最高，分片、映射、段合并、JVM 调优</td><td>最低之一，复用现有 Postgres 运维体系</td></tr></tbody></table></div><p>我的理解是：这张表里真正稀缺的不是延迟，是「一致性模型」这一列没人写。Redis 给你的是最终一致加部分结果语义，ES 给你的是近实时（默认 1 秒刷新）加可调一致性，pgvector 给你的是事务级强一致。选型时先问「写入后多久必须精确可见」，这一问通常直接淘汰掉两个选项，比对比延迟有用得多。</p><p>顺带把两家的能力事实也核一下，避免用老印象选型：Elastic Stack 9.0 起，float 的 <code>dense_vector</code> 字段一律按 <code>int8_hnsw</code> 索引，<code>bbq_disk</code> 这类二值量化可以把大向量集的内存压下来，官方给的磁盘开销是 int8 约 25%、int4 约 12.5%、bbq 约 3.1%；pgvector 从 0.8.0（2024 年 10 月 30 日）起支持 iterative index scans，并改进了「带过滤条件时的代价估算与索引选择」——这一条恰好就是 Redis 里「前置过滤」问题的 Postgres 版答案。</p><h2 id="七、我踩过的坑，和一处我不太同意的地方"><a href="#七、我踩过的坑，和一处我不太同意的地方" class="headerlink" title="七、我踩过的坑，和一处我不太同意的地方"></a>七、我踩过的坑，和一处我不太同意的地方</h2><p>我踩过的坑是 <code>FT.HYBRID</code> 的已知限制。官方在 8.4 的 release notes 里列得很直白：<code>FT.HYBRID</code> 的指标不出现在 <code>FT.INFO</code> 和 <code>INFO</code> 里；<code>EXPLAINSCORE</code>、<code>SHARD_K_RATIO</code>、<code>YIELD_DISTANCE_AS</code>、<code>WITHCURSOR</code> 在 <code>FT.HYBRID</code> 上不可用；<code>COMBINE</code> 之后用 <code>FILTER</code> 做后置过滤不可用；默认响应只给 <code>key_id</code> 和 <code>score</code>。这几条合起来意味着两件事：第一，你没法用 profile 定位一个慢的混合查询，只能靠二分法砍子句；第二，过滤必须前置到 <code>SEARCH</code> 和 <code>VSIM</code> 子句里，如果你按「先融合、再过滤」的直觉写，它会直接报不支持——而在 8.8 之前连 profile 都不给你。我第一次接这个命令时以为是语法写错，改了半天才在 release notes 的 known limitations 里找到答案。教训很朴素：<strong>新命令先读 known limitations，再读语法</strong>。</p><p>这里我不太同意「一个 Redis 顶掉 ES 加向量库」这类说法。它成立的前提是把检索需求压缩成「前置过滤加 top-k」，而这个压缩在真实业务里经常失败。反例很具体：商品搜索要按销量、好评、库存、上下架做多因子加权，还要支持类目聚合和价格区间统计，这类查询在 Redis 里要么写不出来，要么写出来是十几行 <code>FT.AGGREGATE</code> 的意大利面，改一次需求调一次分。更隐蔽的反例是内存——ES 允许你只把热数据放进内存、冷数据留在段文件里，Redis 不允许，你必须为全量数据付内存钱，而内存的单价是磁盘的几十倍。所以「边界消失」正确的说法是：<strong>边界从系统之间搬到了你的容量预算和一致性要求里</strong>，它没有消失，只是换了个地方收钱。</p><h2 id="八、可以马上做的事"><a href="#八、可以马上做的事" class="headerlink" title="八、可以马上做的事"></a>八、可以马上做的事</h2><ol><li>先在 release notes 里核对你要用的版本状态和 known limitations，特别是 <code>FT.HYBRID</code> 那一串不支持项</li><li>用「向量条数 × 维度 × 4 字节」粗算裸向量内存，再乘 2 当作含索引的估算值，和实例内存比一比，超过 30% 就准备量化方案</li><li>把所有检索查询改成「前置过滤 + KNN」的写法，别用「全库 KNN 再筛」，两者召回率不是一个量级</li><li>生产环境把 <code>search-on-oom</code> 从默认的 RETURN 改成 FAIL，让 OOM 显式暴露而不是返回部分结果</li><li>升级到 8.4 之后，因为默认打分器变成 BM25STD，要做一次线上相关性 A/B，别默认「排序没变」</li><li>给混合检索留一条退路：把「关键词召回」和「向量召回」的接口抽象出来，将来迁到 ES 或 pgvector 时只换实现</li></ol><h2 id="参考来源"><a href="#参考来源" class="headerlink" title="参考来源"></a>参考来源</h2><ul><li>Redis Open Source 8.4 release notes（8.4 GA 与 8.4-RC1 日期、变更、known limitations）：<a href="https://github.com/redis/redis/blob/8.4.0/00-RELEASENOTES">https://github.com/redis/redis/blob/8.4.0/00-RELEASENOTES</a></li><li>Redis Open Source 8.0 release notes（模块合并、三许可、Vector set beta）：<a href="https://github.com/redis/redis/blob/8.0.0/00-RELEASENOTES">https://github.com/redis/redis/blob/8.0.0/00-RELEASENOTES</a></li><li>Redis 8.6 / 8.8 / 8.10 release notes（后续版本日期与 FT.HYBRID 增强）：<a href="https://github.com/redis/redis/releases">https://github.com/redis/redis/releases</a></li><li><code>FT.HYBRID</code> 命令参数结构（SEARCH / VSIM / COMBINE RRF 与 LINEAR）：<a href="https://redis.io/docs/latest/commands/ft.hybrid/">https://redis.io/docs/latest/commands/ft.hybrid/</a></li><li>Elasticsearch <code>dense_vector</code> 与量化（9.0 起 float 按 int8_hnsw 索引、bbq 磁盘开销）：<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector">https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector</a></li><li>pgvector 0.8.0 changelog（iterative index scans、过滤时代价估算）：<a href="https://api.pgxn.org/src/vector/vector-0.8.0/CHANGELOG.md">https://api.pgxn.org/src/vector/vector-0.8.0/CHANGELOG.md</a></li></ul>]]></content>
    
    
    <summary type="html">Redis 8.4 把 FT.HYBRID 做进内核，缓存与检索边界开始模糊。本文核对官方版本与已知限制，给出检索入 Redis 的分界线。</summary>
    
    
    
    <category term="技术笔记" scheme="https://u7u7.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="向量检索" scheme="https://u7u7.top/tags/%E5%90%91%E9%87%8F%E6%A3%80%E7%B4%A2/"/>
    
    <category term="架构" scheme="https://u7u7.top/tags/%E6%9E%B6%E6%9E%84/"/>
    
    <category term="Redis" scheme="https://u7u7.top/tags/Redis/"/>
    
    <category term="缓存" scheme="https://u7u7.top/tags/%E7%BC%93%E5%AD%98/"/>
    
  </entry>
  
  <entry>
    <title>PostgreSQL 19 的异步 IO：数据库开始吃满 NVMe 之后</title>
    <link href="https://u7u7.top/posts/pg19-async-io.html"/>
    <id>https://u7u7.top/posts/pg19-async-io.html</id>
    <published>2026-10-13T00:30:00.000Z</published>
    <updated>2026-10-26T13:29:19.100Z</updated>
    
    <content type="html"><![CDATA[<h1 id="PostgreSQL-19-的异步-IO：数据库开始吃满-NVMe-之后"><a href="#PostgreSQL-19-的异步-IO：数据库开始吃满-NVMe-之后" class="headerlink" title="PostgreSQL 19 的异步 IO：数据库开始吃满 NVMe 之后"></a>PostgreSQL 19 的异步 IO：数据库开始吃满 NVMe 之后</h1><p>先说一个可能让不少人意外的结论：<strong>你在 PG 19 宣传里看到的异步 IO，主干代码在 PostgreSQL 18 就已经落地了</strong>。PG 18 的 release notes 原话是「An asynchronous I/O (AIO) subsystem that can improve performance of sequential scans, bitmap heap scans, vacuums, and other operations」。PG 19 干的不是”从无到有”，而是把”谁来执行这些异步请求”做成自适应（新增 io_min_workers、io_max_workers、io_worker_idle_timeout、io_worker_launch_interval），再让 streaming reads 铺到更多路径上（hash 索引批量删除、GIN vacuum、bloom、pgstattuple）。</p><p>所以如果你的升级决策表上写着”等 PG 19 解锁 NVMe”，这张表的前提就已经错了：你该评估的是 PG 18 的能力边界，PG 19 只是在边界内把调度做得更细。</p><h2 id="一、先把版本状态钉死：截至现在它还不是-GA"><a href="#一、先把版本状态钉死：截至现在它还不是-GA" class="headerlink" title="一、先把版本状态钉死：截至现在它还不是 GA"></a>一、先把版本状态钉死：截至现在它还不是 GA</h2><p>这一节必须写准，因为网上已经有一堆”PG 19 重磅发布”的文章在用 beta 的截图讲生产收益。</p><div class="table-container"><table><thead><tr><th>里程碑</th><th>状态</th><th>依据</th></tr></thead><tbody><tr><td>PG 19 Beta 1 至 Beta 4</td><td>已结束</td><td>postgresql.org 已发布 Beta 1、Beta 4 公告</td></tr><tr><td>PG 19 RC 1</td><td><strong>2026-10-15 发布</strong></td><td>官方新闻页 PostgreSQL 19 RC 1 Released</td></tr><tr><td>PG 19 RC 2</td><td><strong>计划 2026-11-12</strong></td><td>与 11 月例行更新版同日发布</td></tr><tr><td>PG 19 GA</td><td><strong>计划 2026-11-19</strong></td><td>同上新闻页给出的 planned date</td></tr></tbody></table></div><p>官方 RC 1 公告里有一段很关键的说明：11 月 12 日的例行更新会带大量修复，release team 决定把这些修复也并进 PG 19，因此 RC 2 会和 11 月更新同日发布，GA 计划到 11 月 19 日。另外 postgresql.org 的 roadmap 页面上仍写着 planned for October 2026，和新闻页的 11-19 口径不一致——<strong>看日期以最新新闻页为准，roadmap 页更新滞后</strong>。</p><p>结论：本文讨论的所有行为，均以 PG 19 RC 1 的文档与 release notes 为准，<strong>不是 GA 行为</strong>。生产上要上，等 GA 之后再看一个小版本更稳妥。</p><h2 id="二、同步-IO-在-NVMe-上究竟浪费了什么"><a href="#二、同步-IO-在-NVMe-上究竟浪费了什么" class="headerlink" title="二、同步 IO 在 NVMe 上究竟浪费了什么"></a>二、同步 IO 在 NVMe 上究竟浪费了什么</h2><p>不是”延迟高”，恰恰相反，是延迟太低而无法被叠加。</p><p>用 Little’s Law 算一下就很清楚：并发请求数 = 吞吐 × 单请求延迟。假设一次 8kB 页读取的设备侧延迟约 100µs（企业级 NVMe 随机读的典型量级），那么单线程一次一个地读：</p><div class="table-container"><table><thead><tr><th>队列深度</th><th>理论 IOPS</th><th>折算带宽（8kB 页）</th></tr></thead><tbody><tr><td>1（同步阻塞）</td><td>约 1 万</td><td>约 80MB/s</td></tr><tr><td>32</td><td>约 32 万</td><td>约 2.5GB/s</td></tr><tr><td>64</td><td>约 64 万</td><td>约 5GB/s</td></tr></tbody></table></div><p>这是算术推演不是实测，但量级关系是对的：<strong>同步模型下，一块能跑几十万 IOPS、几 GB/s 顺序读的设备，被一个队列深度钉死在 1% 的利用率上</strong>。设备没吃饱，后端进程却在睡眠。</p><p>再看 PostgreSQL 的进程模型。一个连接一个 backend 进程，读页的路径是 ReadBuffer → smgrread → pread，读不到就睡，等 IO 完成再被唤醒。这个模型简单、可预测、可观测，是 PG 几十年来的优势，但它的代价就是：<strong>一次只能有一个在飞行中的请求</strong>。PG 17 及以前所谓的”异步”，其实只是 posix_fadvise WILLNEED 给内核提建议（effective_io_concurrency 控制的预读距离），真正读页时仍然是同步等待。</p><p>Linux 侧的答案是 io_uring：一对共享内存环形队列（提交队列 + 完成队列），一次系统调用批量提交多个请求，可以配 SQPOLL 内核线程轮询、可以注册固定缓冲区，从而真正表达”深度 N 的并发”。PG 19 的 io_method = io_uring 就是走它，但需要 —with-liburing 编译。</p><p>那为什么默认值不是它，而是 worker？我的理解是取舍而不是能力问题：worker 模式复用了 PG 已有的进程与共享内存体系，可观测、可移植（Windows 也能跑），出了问题是一群能 ps 出来的进程；io_uring 则对内核版本、构建依赖、容器 seccomp 策略都更挑剔。默认 worker 是”先让所有人能跑”的选择，不是”io_uring 更快所以默认它”。</p><h2 id="三、三个层级的参数必须一起调，否则等于没开"><a href="#三、三个层级的参数必须一起调，否则等于没开" class="headerlink" title="三、三个层级的参数必须一起调，否则等于没开"></a>三、三个层级的参数必须一起调，否则等于没开</h2><p>很多人只改一个 effective_io_concurrency 就期待起飞，这是最典型的无效操作。下面这段配置和检查 SQL 是我现在给任何 PG 18/19 实例做基线时都会跑的。</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># postgresql.conf —— 异步 IO 相关，注意 io_method 需要重启</span></span><br><span class="line"><span class="attr">io_method</span> = worker                  <span class="comment"># worker | io_uring | sync，只能启动时设置</span></span><br><span class="line"><span class="attr">io_min_workers</span> = <span class="number">4</span>                  <span class="comment"># 仅 worker 模式生效，默认 2</span></span><br><span class="line"><span class="attr">io_max_workers</span> = <span class="number">16</span>                 <span class="comment"># 仅 worker 模式生效，默认 8</span></span><br><span class="line"><span class="attr">io_worker_idle_timeout</span> = <span class="number">60</span>s        <span class="comment"># 默认 1min</span></span><br><span class="line"><span class="attr">io_worker_launch_interval</span> = <span class="number">100</span>ms   <span class="comment"># 默认 100ms</span></span><br><span class="line"><span class="attr">effective_io_concurrency</span> = <span class="number">32</span>       <span class="comment"># 1..1000，0 表示禁用异步请求，默认 16</span></span><br><span class="line"><span class="attr">maintenance_io_concurrency</span> = <span class="number">16</span>     <span class="comment"># 维护类操作的并发度</span></span><br><span class="line"><span class="attr">io_combine_limit</span> = <span class="number">128</span>kB            <span class="comment"># 请求合并上限，默认 128kB</span></span><br></pre></td></tr></table></figure><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 1. 核对自己实例上的真实取值</span></span><br><span class="line"><span class="keyword">SELECT</span> name, setting, unit, context</span><br><span class="line"><span class="keyword">FROM</span> pg_settings</span><br><span class="line"><span class="keyword">WHERE</span> name <span class="keyword">IN</span> (<span class="string">&#x27;io_method&#x27;</span>, <span class="string">&#x27;io_min_workers&#x27;</span>, <span class="string">&#x27;io_max_workers&#x27;</span>,</span><br><span class="line">               <span class="string">&#x27;io_worker_idle_timeout&#x27;</span>, <span class="string">&#x27;io_worker_launch_interval&#x27;</span>,</span><br><span class="line">               <span class="string">&#x27;effective_io_concurrency&#x27;</span>, <span class="string">&#x27;maintenance_io_concurrency&#x27;</span>,</span><br><span class="line">               <span class="string">&#x27;io_combine_limit&#x27;</span>, <span class="string">&#x27;io_max_concurrency&#x27;</span>)</span><br><span class="line"><span class="keyword">ORDER</span> <span class="keyword">BY</span> name;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 2. 看当前在飞行中的 I/O 句柄（PG 18 起提供）</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> pg_aios LIMIT <span class="number">20</span>;</span><br></pre></td></tr></table></figure><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 3. 用 IO 选项看预取队列与请求规模，IO 必须与 ANALYZE 同用</span></span><br><span class="line">EXPLAIN (ANALYZE, BUFFERS, IO)</span><br><span class="line"><span class="keyword">SELECT</span> <span class="built_in">count</span>(<span class="operator">*</span>), <span class="built_in">sum</span>(amount) <span class="keyword">FROM</span> orders <span class="keyword">WHERE</span> created_at <span class="operator">&gt;=</span> <span class="string">&#x27;2026-01-01&#x27;</span>;</span><br></pre></td></tr></table></figure><p>这段代码在说明什么：第一段是”容量”，io_min_workers 与 io_max_workers 决定 worker 池能有多少个执行者，effective_io_concurrency 决定每个会话愿意保持多少个在飞行中的请求，io_combine_limit 决定相邻请求能不能合并成大 IO（默认 128kB 正好 16 个 8kB 块）；第二段是”事实”，pg_aios 是 PG 18 起才有的视图，列出当前使用的异步 I/O 句柄，升级后监控脚本在低版本实例上查它会直接报错；第三段是”证据”，EXPLAIN (ANALYZE, IO) 会输出预取队列的平均与最大距离、队列容量，以及已发请求总数、平均请求大小、I/O 等待次数、平均并发请求数——<strong>判断”异步到底生效没有”，看的就是平均并发请求数是否大于 1，以及队列是否被打满</strong>。</p><h2 id="四、同步阻塞与异步队列的模型对比"><a href="#四、同步阻塞与异步队列的模型对比" class="headerlink" title="四、同步阻塞与异步队列的模型对比"></a>四、同步阻塞与异步队列的模型对比</h2><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart LR    A[查询发起 顺序扫描] --&gt; B[本次需要读 1000 个数据页]    B --&gt; C[同步模型 一次只发一个]    C --&gt; C1[后端起一个 pread 然后睡眠]    C1 --&gt; C2[队列深度 1 设备排队能力被浪费]    C2 --&gt; E[NVMe 带宽利用率低 延迟无法叠加]    B --&gt; D[异步模型 一次提交 64 个请求]    D --&gt; D1[共享内存 IO 队列]    D1 --&gt; D2[worker 进程池 或 io_uring]    D2 --&gt; D3[完成队列 批量唤醒后端]    D3 --&gt; F[NVMe 带宽利用率高 等待被重叠]  </pre></div><p>这张图想让你看见什么：两条路径的差别不在”读得快不快”，而在<strong>同一个后端进程在被唤醒之前，能有多少个请求在设备里排队</strong>。左路真正的成本是”睡眠—唤醒”的次数，右路的成本转移到了队列管理与 worker 调度上——这也解释了为什么异步不是免费的：请求数越多，worker 越忙，上下文切换与共享内存竞争就越明显。</p><h2 id="五、一次顺序扫描在异步模式下长什么样"><a href="#五、一次顺序扫描在异步模式下长什么样" class="headerlink" title="五、一次顺序扫描在异步模式下长什么样"></a>五、一次顺序扫描在异步模式下长什么样</h2><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  sequenceDiagram    autonumber    participant C as 客户端    participant B as 后端进程    participant S as IO 子系统    participant W as IO worker    participant D as NVMe 设备    C-&gt;&gt;B: 发送 SELECT 全表扫描    B-&gt;&gt;S: 提交首批预取请求 多个数据页    S-&gt;&gt;W: 按队列分发    W-&gt;&gt;D: 并发下发读请求    D--&gt;&gt;W: 第一批完成    W--&gt;&gt;S: 写入完成队列    S--&gt;&gt;B: 唤醒后端 交付数据页    B-&gt;&gt;B: 过滤行 组装元组    B-&gt;&gt;S: 补齐水位 继续提交    Note over B,W: 关键点 后端等待期间设备仍有请求在飞    B--&gt;&gt;C: 返回结果集  </pre></div><p>这张图想让你看见什么：<strong>执行器的循环结构没有变</strong>（还是取行、过滤、返回），变的是”取下一批页”这个动作从阻塞变成了”提交 + 稍后收割”。所以真正的收益点落在”有大量连续页要读、且这些页不在 shared_buffers 里”的场景，一旦命中缓存，这张图的下半部分根本不会发生。</p><h2 id="六、应用层能看到什么：受益与几乎无感的清单"><a href="#六、应用层能看到什么：受益与几乎无感的清单" class="headerlink" title="六、应用层能看到什么：受益与几乎无感的清单"></a>六、应用层能看到什么：受益与几乎无感的清单</h2><div class="table-container"><table><thead><tr><th>场景</th><th>是否受益</th><th>原因</th></tr></thead><tbody><tr><td>大表顺序扫描，表远大于 shared_buffers</td><td>受益最大</td><td>连续页读取，队列容易打满</td></tr><tr><td>Bitmap Heap Scan</td><td>受益（PG 18 起）</td><td>位图阶段就知道要读哪些页，可提前排队</td></tr><tr><td>VACUUM / autovacuum</td><td>受益</td><td>维护类 IO，受 maintenance_io_concurrency 控制</td></tr><tr><td>GIN vacuum、hash 索引批量删除、bloom、pgstattuple</td><td>PG 19 起受益</td><td>release notes 明确改为 streaming reads</td></tr><tr><td>Index Scan 单页点查</td><td>几乎无感</td><td>一个请求没有可重叠的对象</td></tr><tr><td>数据全在 shared_buffers 里</td><td>无感甚至更差</td><td>多了队列与 worker 开销</td></tr><tr><td>CPU 密集的聚合、排序、大连接</td><td>无感</td><td>瓶颈在 CPU，不在 IO 等待</td></tr><tr><td>高并发短事务 OLTP</td><td>无感</td><td>瓶颈在行锁、网络往返、提交延迟</td></tr></tbody></table></div><h2 id="七、我的判断：什么情况下这次升级才成立"><a href="#七、我的判断：什么情况下这次升级才成立" class="headerlink" title="七、我的判断：什么情况下这次升级才成立"></a>七、我的判断：什么情况下这次升级才成立</h2><p>我的判断是，PG 18/19 的异步 IO 值得升级的条件可以压成三条硬门槛，缺一条就先别升：</p><ol><li><strong>瓶颈确实在 IO 等待上</strong>。看 pg_stat_io 与 pg_stat_database 里 blk_read_time 的占比，或者用 EXPLAIN (ANALYZE, IO) 看 I/O 等待次数与平均并发数。如果 CPU 常年 90%、IO 等待不到 10%，升级之后你只会觉得”重启了一次”。</li><li><strong>数据量大于 shared_buffers，且有稳定的扫描型负载</strong>。库能全塞进内存的实例，异步 IO 没有任何发挥空间。</li><li><strong>存储的并发能力大于你当前的并发度</strong>。NVMe 收益最大；SATA SSD 队列深度有限，收益打折；网络块存储要看带宽是否已经封顶。</li></ol><p>这里我不太同意”没有 NVMe 就没必要升级”这个说法。异步 IO 的收益来自”把等待重叠起来”，而延迟越高的存储，单请求等待越长、可重叠的收益反而越大——远端块存储、云盘上确实能看到改善。但反过来也不成立：<strong>当带宽先撞墙时，加大队列深度只会把 p99 拖长</strong>，因为请求开始在设备侧排队，而不是在 CPU 侧。</p><p>反例也要说清楚，异步 IO 从来不是银弹：</p><ul><li><strong>CPU 瓶颈</strong>。顺序扫描的每行开销（tuple deform、表达式求值）是纯 CPU。PG 19 虽然优化了行变形，但如果扫描节点的 CPU 时间是 IO 等待的 5 倍，IO 再快也没用。</li><li><strong>行锁与事务</strong>。热点行更新，瓶颈在锁等待与 WAL 刷盘，异步 IO 完全不参与。</li><li><strong>连接数放大</strong>。effective_io_concurrency 是<strong>每会话</strong>的乘数。1000 个活跃会话各配 32，理论上就是 3.2 万个在飞行中的请求，全部砸在同一块设备上，结果不是更快而是更抖。这是我见过最常见的反向优化。</li><li><strong>单页点查</strong>。没有可重叠对象，队列深度永远是 1。</li></ul><h2 id="八、我踩过的坑"><a href="#八、我踩过的坑" class="headerlink" title="八、我踩过的坑"></a>八、我踩过的坑</h2><p>我踩过的坑里，最浪费时间的一个是：io_method 改了 ALTER SYSTEM SET 之后执行 pg_reload_conf 然后盯着 EXPLAIN 的输出怀疑人生。它在文档里写得很清楚——<strong>只能在服务器启动时设置</strong>，也就是必须重启 postmaster，reload 不生效。同理 io_max_workers 这类参数属于配置级，不是会话级，改完不重启的系统永远停在默认的 worker 池上。</p><p>第二个坑是”以为开了 io_uring 就更快”。io_uring 需要 —with-liburing 构建，发行版二进制包不一定带这个能力，容器里还可能被 seccomp 拦掉。顺序应该是：先 SHOW io_method 确认自己到底跑在哪种模式，再看平均并发数，别拿内核版本猜。</p><p>第三个坑是监控。pg_aios 是 PG 18 起才有的视图，一套采集脚本同时跑在 14/16/18/19 上时，查询会直接报错；这类新视图必须按版本分流，或者用 to_regclass 做探测。</p><h2 id="九、如果你下周就要做决定，按这个顺序来"><a href="#九、如果你下周就要做决定，按这个顺序来" class="headerlink" title="九、如果你下周就要做决定，按这个顺序来"></a>九、如果你下周就要做决定，按这个顺序来</h2><ol><li>先用 EXPLAIN (ANALYZE, BUFFERS, IO) 在<strong>一条最具代表性的慢查询</strong>上取基线，记录平均并发请求数、I/O 等待次数、队列容量三个数。</li><li>确认存储类型：NVMe 才谈”吃满”，SATA SSD 与网络盘先确认带宽是否已封顶，别用队列深度掩盖带宽瓶颈。</li><li>在测试实例上只改三件事：io_method（先留 worker）、io_max_workers、effective_io_concurrency，其余保持默认，做 A/B 对比。</li><li>按会话数算一次并发预算：活跃会话数乘 effective_io_concurrency 不要超过设备能舒服承受的队列深度，超了就要压会话数或压并发度。</li><li>生产升级时间点放在 PG 19 GA（计划 2026-11-19）之后，再留一个例行更新周期观察 pg_stat_io 与 p99。</li><li>把监控里的 pg_aios 与 EXPLAIN 的 IO 采集按版本分流，别让新视图把老实例的采集打挂。</li></ol><p>异步 IO 这件事，PG 18 已经把地基打完，PG 19 在做的是让地基上的调度更聪明。真正决定你能不能吃满 NVMe 的，还是”你的瓶颈是不是 IO 等待”这一个问题——这个问题答错了，版本号升到 20 也没用。</p>]]></content>
    
    
    <summary type="html">PG 19 的异步 IO 到底是新特性还是 PG 18 的续章，同步 IO 在 NVMe 上浪费了什么，以及什么样的库升级才真的有收益。</summary>
    
    
    
    <category term="技术笔记" scheme="https://u7u7.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="架构" scheme="https://u7u7.top/tags/%E6%9E%B6%E6%9E%84/"/>
    
    <category term="PostgreSQL" scheme="https://u7u7.top/tags/PostgreSQL/"/>
    
    <category term="数据库" scheme="https://u7u7.top/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    <category term="IO" scheme="https://u7u7.top/tags/IO/"/>
    
  </entry>
  
  <entry>
    <title>Elasticsearch 9.5 的列式存储与向量检索：搜索栈正在和向量库合并</title>
    <link href="https://u7u7.top/posts/es-9-5-columnar-vector.html"/>
    <id>https://u7u7.top/posts/es-9-5-columnar-vector.html</id>
    <published>2026-10-11T00:30:00.000Z</published>
    <updated>2026-10-26T13:29:19.111Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Elasticsearch-9-5-的列式存储与向量检索：搜索栈正在和向量库合并"><a href="#Elasticsearch-9-5-的列式存储与向量检索：搜索栈正在和向量库合并" class="headerlink" title="Elasticsearch 9.5 的列式存储与向量检索：搜索栈正在和向量库合并"></a>Elasticsearch 9.5 的列式存储与向量检索：搜索栈正在和向量库合并</h1><p>先说一个反常识的结论：Elasticsearch 9.5 里最像”向量库”的那部分改动，其实不是向量字段，而是一个索引模式。真正在变的也不是”ES 能不能做向量检索”——那件事 8.x 就做完了——而是 ES 愿不愿意为不同负载放弃同一套存储假设。9.5 的答案是：不愿意，所以它把假设拆成了多个可选的索引模式。</p><p>这意味着你的架构评审问题要换一个提法。以前的问题是”ES 的 kNN 够不够快”，现在的问题是”我到底需不需要第二个系统”。</p><h2 id="一、先把事实对齐：哪些是-GA，哪些只是预览"><a href="#一、先把事实对齐：哪些是-GA，哪些只是预览" class="headerlink" title="一、先把事实对齐：哪些是 GA，哪些只是预览"></a>一、先把事实对齐：哪些是 GA，哪些只是预览</h2><p>这一节的每一条我都对过官方页面，因为把预览特性写成 GA 是这类文章最常见的错。9.5 于 2026 年 8 月 4 日发布 GA 公告，但发布内容里混着三种成熟度，必须分开看。</p><div class="table-container"><table><thead><tr><th>特性</th><th>官方标注状态</th><th>落地形态</th></tr></thead><tbody><tr><td><code>vectordb_document</code> 索引模式</td><td>Elastic Stack 自 9.5 GA</td><td>索引级 <code>index.mode</code> 设置</td></tr><tr><td><code>bbq_disk</code> 的 <code>auto_calibrate</code></td><td>Elastic Stack 自 9.5 GA</td><td><code>dense_vector</code> 的映射参数</td></tr><tr><td><code>columnar</code> 索引模式</td><td>Elastic Stack 自 9.5 <strong>技术预览</strong></td><td>索引级 <code>index.mode</code> 设置</td></tr><tr><td><code>logsdb_columnar</code> 索引模式</td><td>Elastic Stack 自 9.5 <strong>技术预览</strong></td><td>索引级 <code>index.mode</code> 设置，带日志默认值</td></tr><tr><td>Prometheus 与 PromQL 原生支持</td><td>9.5 GA</td><td>远程写端点加 ES</td><td>QL 内 PromQL</td></tr></tbody></table></div><p>也就是说，”ES 变成列式数据库”这件事，官方自己写的是 technical preview（技术预览），不是 GA。我的判断是这一条必须写进方案 PPT 的风险栏：预览意味着可以试、不能承诺 SLA，也意味着映射层的语义还可能变——文档里 <code>ignore_above</code> 的行为就已经标成了”Planned”。</p><p>还有一个我核不实的地方，如实写”待核”：官方文档提到”9.6 及之后创建的索引默认开启 auto-calibration 并使用 <code>iso_sizing</code> profile”，说明 9.6 已经在推进；但我在 elastic.co 上没有检索到 9.6 的 GA 公告页，所以本文所有结论只绑定 9.5，9.6 的具体行为标为待核，不做推测。</p><h2 id="二、先给一张图：9-5-把存储选择推到了索引层"><a href="#二、先给一张图：9-5-把存储选择推到了索引层" class="headerlink" title="二、先给一张图：9.5 把存储选择推到了索引层"></a>二、先给一张图：9.5 把存储选择推到了索引层</h2><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart LR    W[写入请求] --&gt; IM{选择索引模式}    IM --&gt;|standard| S1[倒排索引加 doc values]    IM --&gt;|columnar| S2[仅 doc values 单一存储]    IM --&gt;|vectordb_document| S3[向量文件优先预加载]    S1 --&gt; Q[查询层]    S2 --&gt; Q    S3 --&gt; Q    Q --&gt; A1[全文检索]    Q --&gt; A2[聚合与排序]    Q --&gt; A3[kNN 近似检索]    A1 --&gt; M[结果归并与重排]    A2 --&gt; M    A3 --&gt; M  </pre></div><p>这张图想让你看见什么：9.5 之前，ES 的存储结构基本是固定的，你能调的只有字段级参数；9.5 之后，第一个分叉发生在”建索引的那一刻”，而且三条分支共用同一个查询层、同一套 API。这不是加了一个功能，是把一个原本隐式的架构决策显式化了。代价是它不可改：<code>mode</code> 只能在创建索引时设定，建完就不能改，选错的唯一出路是重建索引再 reindex。</p><h2 id="三、列式存储省掉的不是索引，是”第二份数据”"><a href="#三、列式存储省掉的不是索引，是”第二份数据”" class="headerlink" title="三、列式存储省掉的不是索引，是”第二份数据”"></a>三、列式存储省掉的不是索引，是”第二份数据”</h2><p>很多人把列存理解成”把行存换成列存”，这个理解在 ES 这里会跑偏。按官方那篇讲列存原理的文章，Lucene 的 doc values 从 1.0.0 起就是列式结构，聚合和排序一直靠它。所以列式存储对 ES 不是新东西，<strong>它是从”辅助结构”升级成了”唯一结构”</strong>。</p><p>standard 模式下同一份字段值其实存了两遍：一遍进倒排索引（字符串）或 BKD 树（数值）用于过滤，一遍进 doc values 用于聚合排序。columnar 模式把前者默认关掉（<code>index</code> 映射属性默认为 false，文本字段例外，因为全文检索离不开分析链），只留 doc values，再补两个东西来救查询性能：doc values 的批量读取，和 doc values skipper。</p><p>skipper 是轻量跳表，存 min/max 之类的元信息，让范围查询能整段跳过文档。它的效果完全取决于数据在磁盘上的物理顺序，所以列式模式下 <strong>index sorting 不是可选项而是设计的一部分</strong>。<code>logsdb_columnar</code> 默认给 <code>host.name</code> 升序加 <code>@timestamp</code> 降序，逻辑很朴素：同一台机器的日志挨着放，run-length 和 delta 编码才有效。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">curl -X PUT <span class="string">&quot;localhost:9200/orders-columnar&quot;</span> -H <span class="string">&#x27;Content-Type: application/json&#x27;</span> -d<span class="string">&#x27;</span></span><br><span class="line"><span class="string">&#123;</span></span><br><span class="line"><span class="string">  &quot;settings&quot;: &#123;</span></span><br><span class="line"><span class="string">    &quot;mode&quot;: &quot;columnar&quot;,</span></span><br><span class="line"><span class="string">    &quot;sort.field&quot;: [&quot;tenant.id&quot;, &quot;@timestamp&quot;],</span></span><br><span class="line"><span class="string">    &quot;sort.order&quot;: [&quot;asc&quot;, &quot;desc&quot;]</span></span><br><span class="line"><span class="string">  &#125;,</span></span><br><span class="line"><span class="string">  &quot;mappings&quot;: &#123;</span></span><br><span class="line"><span class="string">    &quot;properties&quot;: &#123;</span></span><br><span class="line"><span class="string">      &quot;@timestamp&quot;: &#123; &quot;type&quot;: &quot;date&quot; &#125;,</span></span><br><span class="line"><span class="string">      &quot;tenant.id&quot;:  &#123; &quot;type&quot;: &quot;keyword&quot; &#125;,</span></span><br><span class="line"><span class="string">      &quot;amount&quot;:     &#123; &quot;type&quot;: &quot;double&quot; &#125;,</span></span><br><span class="line"><span class="string">      &quot;status&quot;:     &#123; &quot;type&quot;: &quot;keyword&quot; &#125;,</span></span><br><span class="line"><span class="string">      &quot;message&quot;:    &#123; &quot;type&quot;: &quot;text&quot; &#125;</span></span><br><span class="line"><span class="string">    &#125;</span></span><br><span class="line"><span class="string">  &#125;</span></span><br><span class="line"><span class="string">&#125;&#x27;</span></span><br></pre></td></tr></table></figure><p>这段代码在说明什么：<code>mode</code> 与 <code>sort.field</code> 必须一起给。只写 <code>mode</code> 而不设计排序键，skipper 就退化成”还是得扫”，你会得到一个存储变小、但范围查询变慢的索引，然后误判”列存不行”。<code>message</code> 是 text，在列式模式下仍然默认建倒排索引——这正是 ES 相对纯列式数据库的取舍：它不愿意为了省空间牺牲全文检索，只在 <code>logsdb_columnar</code> 里把倒排索引压缩到只剩 <code>message</code> 一个字段。</p><h2 id="四、收益边界：什么数据形态下反而不划算"><a href="#四、收益边界：什么数据形态下反而不划算" class="headerlink" title="四、收益边界：什么数据形态下反而不划算"></a>四、收益边界：什么数据形态下反而不划算</h2><p>这里我不太同意 Elastic 那篇公告里”zero changes to APIs, dashboards, or integrations”的说法。API 形状确实没变，但语义变了，而且是那种会在半夜炸出来的变。</p><p>第一，映射被强制拍平。列式模式下 object 和 passthrough 字段会被自动拉平，nested 只有受限支持，且 nested 之间不能套。原来靠 <code>nested</code> 表达一对多关系的订单明细、多标签文档，迁移过来要么改模型要么放弃。</p><p>第二，<code>_source</code> 不再是原文。列式模式只支持合成源和列式源，<code>store: true</code>、<code>synthetic_source_keep</code> 全部被拒，传统 stored source 不支持。官方写得很直白：columnar 用更小的存储换更低的还原保真度。如果你的下游有”取出原始 JSON 原样丢给别家系统”的逻辑，这一条就是硬阻断。</p><p>第三，<code>doc_values: false</code> 会直接报映射错误。以前为了省空间把不需要聚合的字段关掉 doc values，这个习惯在列式模式下会让建索引失败，多字段除外。</p><p>第四，高基数不是问题，但”既要全文又要过滤”的低基数标签字段是问题。低基数字符串用字典加 ordinal 编码，压缩率极好；高基数字符串走块压缩也能接受。真正难受的是那种”必须按它做精确过滤命中率极高、同时又必须参与全文相关性打分”的字段——列式模式默认不建倒排，过滤只能靠 skipper 跳段，效率和原来不在一个量级。</p><p>我踩过的坑就在这：拿一个”搜索加多维筛选”的电商索引试 <code>columnar</code>，存储降得很漂亮，但带筛选条件的关键词查询 P99 直接翻了几倍。结论是列存适合”分析型加日志型”负载，不适合”筛选密集型的交互式搜索”。前者是少数列、海量行、扫描为主；后者是宽表、随机命中、过滤条件五花八门。这个区分比”数据量大不大”有用得多。</p><h2 id="五、VectorDB-索引模式：从”字段类型”变成”索引级承诺”"><a href="#五、VectorDB-索引模式：从”字段类型”变成”索引级承诺”" class="headerlink" title="五、VectorDB 索引模式：从”字段类型”变成”索引级承诺”"></a>五、VectorDB 索引模式：从”字段类型”变成”索引级承诺”</h2><p><code>vectordb_document</code> 是这个版本里我最看重的改动，因为它是 GA 的，而且它改变的是索引的默认假设而不是某个字段的参数。它做的事一句话能说完：把原本要你自己调的向量相关默认值全部预置好。</p><div class="table-container"><table><thead><tr><th>默认项</th><th>standard 模式</th><th><code>vectordb_document</code> 模式</th></tr></thead><tbody><tr><td>向量元素类型</td><td><code>float</code></td><td><code>bfloat16</code>，原始向量存储成本约减半</td></tr><tr><td>动态映射识别阈值</td><td>128 维以上转 <code>dense_vector</code></td><td>32 维以上就转</td></tr><tr><td><code>_source</code> 中的向量</td><td>保留</td><td>排除，且不允许改回 false</td></tr><tr><td>文件预加载</td><td>无</td><td>预加载 HNSW 图、标量量化、二值量化、IVF 质心文件</td></tr><tr><td>段内合并</td><td>默认限流</td><td>开启段内并行合并并关闭合并限流</td></tr></tbody></table></div><p>这张表说明一件事：向量检索的性能上限其实不在查询时，而在”段长什么样”。预加载和合并策略决定了查询时能不能不碰盘，所以把它们塞进索引模式，比让你在 6 个字段参数上试错要合理。</p><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  sequenceDiagram    participant App as 应用服务    participant CO as 协调节点    participant SH as 数据分片    participant DS as doc values 与向量文件    App-&gt;&gt;CO: kNN 请求 携带 topk 与过滤条件    CO-&gt;&gt;SH: 分发到各分片 携带段级参数    SH-&gt;&gt;DS: 用 skipper 元信息跳过不匹配文档区间    DS--&gt;&gt;SH: 返回候选文档区间    SH-&gt;&gt;DS: 读取 bfloat16 向量做近似距离计算    DS--&gt;&gt;SH: 返回距离结果    SH-&gt;&gt;SH: 段级 oversample 后重打分    SH--&gt;&gt;CO: 返回分片内 topk    CO-&gt;&gt;CO: 归并排序并截断    CO--&gt;&gt;App: 返回最终 topk  </pre></div><p>这张图想让你看见什么：kNN 在 ES 里从来不是”一步查完”，它是”过滤先缩范围、近似距离出候选、段内 oversample 重打分、协调节点归并”四段。列式模式和向量模式的默认值，作用点分别在第一段和第三段——skipper 决定前面能跳过多少文档，oversample 决定后面要重算多少。这也解释了为什么单独调一个 <code>m</code> 或 <code>ef_search</code> 常常没效果：它们不控制这两个真正决定成本的段。</p><p>关于 HNSW 调参经验能不能迁移，我的判断是”一半能迁移，一半会骗你”。能迁移的是形状：<code>m</code> 变大查得准内存也涨、<code>int8</code> 到 <code>int4</code> 到 <code>bbq</code> 是逐级更省更糙、<code>m</code> 在更新映射时只能不变或变大。会骗你的是绝对值：默认元素类型变成 <code>bfloat16</code>，量化位宽和 oversample 由自动校准按段决定，你在 8.x 上调出来的那套”m=32 加 int8 加 oversample 2.0”参数组合，换到 9.5 的 <code>vectordb_document</code> 上基准完全不同，照抄只会得到一个既没调优也没默认的组合。</p><p>还有一个坑必须说清：<code>auto_calibrate</code> 在 9.5 里只接受布尔值、<strong>默认关闭</strong>、且字段建好之后不能改。也就是说想用它，必须在建映射时就写上，否则只能 reindex。另外文档里有个容易漏掉的细节：合并后少于 10000 个向量的段不参与校准，回落到 3.0 倍默认 oversample。所以小索引上开了校准基本没感知，这不是它没生效，是样本不够。</p><h2 id="六、我的判断：分界线现在该画在哪"><a href="#六、我的判断：分界线现在该画在哪" class="headerlink" title="六、我的判断：分界线现在该画在哪"></a>六、我的判断：分界线现在该画在哪</h2><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart TD    subgraph T1[方案一 ES 一体化]    A[业务数据] --&gt; B[单一集群]    B --&gt; C[文本字段 走倒排索引]    B --&gt; D[向量字段 dense_vector]    C --&gt; E[同一请求内混合过滤加 kNN]    D --&gt; E    E --&gt; F[一次结果返回]    end    subgraph T2[方案二 ES 加专用向量库]    G[业务数据] --&gt; H[Elasticsearch]    G --&gt; I[向量库 Milvus 或 Qdrant]    I --&gt; J[返回向量 ID 与距离]    J --&gt; K[回查 ES 拿正文]    H --&gt; K    K --&gt; L[应用层归并两套分数]    end  </pre></div><p>这张图想让你看见什么：两套方案的差别不在查询快慢，而在”一致性边界”和”分数可比性”。方案一里过滤和向量在同一个请求内完成，返回的是经过同一套打分的排名；方案二里你拿到的是两组不可比的分数，怎么融合是应用层的活，而且双写就意味着两边永远可能不一致。</p><p>我的判断是分界线可以按三个条件画，命中任意一个就该上专用向量库：</p><ul><li>向量规模到了千万级以上，且要求过滤与向量在同一次判定内完成。ES 的 kNN 需要先过滤再算距离，过滤后的候选集很小、召回会掉；专用向量库普遍支持带过滤的图遍历，这类负载差距是结构性的。</li><li>需要多向量召回或稀疏加稠密混合的复杂管道。这不是 ES 不行，是编排逻辑写在应用层更清楚。</li><li>检索的更新频率远高于正文更新，或者向量由独立流水线版本化管理。</li></ul><p>反过来，以下几类我明确不建议再引一个专用库：</p><ul><li>向量规模在百万到几百万、QPS 在几十这个量级、过滤条件基本是固定的几个业务维度。这种规模 ES 一体化完全够，多引一个系统换来的是双写一致性、双份监控和一份新的值班说明书。</li><li>正文和向量必须同源同版本。一次 reindex 就能把两边拉齐，这在 RAG 里价值很高。</li><li>已经在用 ES 做日志或搜索，团队没有额外的运维带宽。这里代价不是钱，是人的注意力。</li></ul><p>至于 pgvector：它确实不需要新系统，省掉了运维一个新集群的成本，HNSW 和 IVFFlat 两种索引也够用。但它的定位是”把向量放进既有 Postgres”，如果你已经有成熟的 Postgres 和事务需求，它是最省心的选择；如果检索同时要吃全文相关性和复杂聚合，你还是得回到 ES。这不是谁替代谁，是三个不同起点的最短路径不同。</p><h2 id="七、可以马上做的事"><a href="#七、可以马上做的事" class="headerlink" title="七、可以马上做的事"></a>七、可以马上做的事</h2><ol><li>先用一个只读副本验证 <code>columnar</code>：选一个日志或指标索引，重建时同时给 <code>mode</code> 和 <code>sort.field</code>，比对存储占用、范围聚合延迟和你最常用的那种”带筛选的关键词查询”延迟。三个数字一起看，别只看存储。</li><li>把 <code>doc_values: false</code>、<code>store: true</code>、<code>nested</code>、<code>synthetic_source_keep</code> 这几个关键字在现有模板里 grep 一遍，命中就是在给列存迁移埋雷。</li><li>新建向量索引直接上 <code>vectordb_document</code>，并在映射里显式写 <code>auto_calibrate: true</code>——9.5 默认关且建后不可改，漏了就得 reindex。同时确认 <code>_source</code> 里没有向量（该模式强制排除），别让下游的取原文逻辑踩空。</li><li>老索引先别急着全部切换。<code>mode</code> 不可逆，把”哪些索引是实验性的”写进命名规范里，比事后靠记忆区分安全。</li><li>做一次选型复盘：把”要不要保留专用向量库”从技术偏好问题改成三个可测条件——向量规模、过滤与向量的耦合度、双写不一致的容忍度。前两个能量化，第三个问业务方。</li><li>盯着 9.6。文档已经写了 9.6 起自动校准默认开启并用 <code>iso_sizing</code> 档位，但那部分标注仍是计划态，等 GA 公告出来再改方案。</li></ol><h2 id="八、最后压一句"><a href="#八、最后压一句" class="headerlink" title="八、最后压一句"></a>八、最后压一句</h2><p>9.5 真正在做的事，是把”搜索栈”和”向量库”这两个原本靠外部拼装的系统，用同一个索引模式机制塞进一个内核里。它没打算在所有维度赢过 Milvus 或 Qdrant，它打算赢的是”你不需要为了一件本来能顺手做完的事，再养一个系统”。判断自己属于哪一边，比判断哪个引擎 benchmark 更高更重要。</p>]]></content>
    
    
    <summary type="html">拆解 Elastic 9.5 的列式索引模式、向量索引模式与自动校准，讲清列存收益边界，并给出 ES 与专用向量库的选型分界。</summary>
    
    
    
    <category term="技术笔记" scheme="https://u7u7.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="ES" scheme="https://u7u7.top/tags/ES/"/>
    
    <category term="向量检索" scheme="https://u7u7.top/tags/%E5%90%91%E9%87%8F%E6%A3%80%E7%B4%A2/"/>
    
    <category term="Elasticsearch" scheme="https://u7u7.top/tags/Elasticsearch/"/>
    
    <category term="架构" scheme="https://u7u7.top/tags/%E6%9E%B6%E6%9E%84/"/>
    
  </entry>
  
  <entry>
    <title>MCP 2026-07-28 改成无状态核心：协议为什么在向 HTTP 靠拢</title>
    <link href="https://u7u7.top/posts/mcp-2026-07-28-stateless-core.html"/>
    <id>https://u7u7.top/posts/mcp-2026-07-28-stateless-core.html</id>
    <published>2026-10-09T00:30:00.000Z</published>
    <updated>2026-10-26T13:29:19.121Z</updated>
    
    <content type="html"><![CDATA[<p>先问一个问题：如果你的 MCP Server 明天必须挂到公司网关后面、跑 20 个副本、由 K8s 随机调度、每次发布都滚动重启——你今天这份代码还能活吗？</p><p>2025 年那份规范的回答是：不能。2026-07-28 这一版的回答是：所以我改。</p><p>这篇文章只做三件事：把 2026-07-28 这一版相对 2025-11-25 的真实改动核清楚；解释”无状态核心”到底把哪些东西搬了家；再给出我的判断——什么团队现在就该跟进，什么场景继续用旧版反而更省。</p><h2 id="一、先说清楚这一版改了什么"><a href="#一、先说清楚这一版改了什么" class="headerlink" title="一、先说清楚这一版改了什么"></a>一、先说清楚这一版改了什么</h2><p>我不打算凭印象写。判断依据是官方 changelog 与它引用的 SEP 原文。这一版的主线其实只有三条，其余都是配套。</p><ul><li><strong>SEP-2575 Make MCP Stateless</strong>：删除 <code>initialize</code> / <code>notifications/initialized</code> 握手，把协议版本与能力协商改成”每个请求自带”。SEP 状态 Final，创建于 2025-06-18。</li><li><strong>SEP-2567 Sessionless MCP via Explicit State Handles</strong>：删除会话概念与 <code>Mcp-Session-Id</code> 头，跨调用状态改为<strong>服务端自签的显式句柄</strong>，作为普通工具参数由模型传递。创建于 2026-03-11，与 SEP-2575 互为补集。</li><li><strong>SEP-2322 Multi Round-Trip Requests</strong>：从”服务端反向发起 <code>sampling/createMessage</code>、<code>roots/list</code>、<code>elicitation/create</code>“改成”结果里携带问题、客户端重试原请求”。所有结果新增必填 <code>resultType</code>。</li></ul><p>其余配套改动，我认为同样值得记住，因为它们直接决定了迁移成本：</p><div class="table-container"><table><thead><tr><th>变更点</th><th>旧 2025-11-25</th><th>新 2026-07-28</th><th>SEP 或 PR</th></tr></thead><tbody><tr><td>握手</td><td>三次握手建立会话</td><td>无握手，每请求 <code>_meta</code> 带版本与能力</td><td>SEP-2575</td></tr><tr><td>会话标识</td><td><code>Mcp-Session-Id</code> 头</td><td>取消，改用业务句柄参数</td><td>SEP-2567</td></tr><tr><td>服务端主动请求</td><td>连接内反向请求</td><td>结果 <code>resultType: input_required</code> + 重试</td><td>SEP-2322</td></tr><tr><td>服务端推送通道</td><td>HTTP GET 流 + <code>resources/subscribe</code></td><td>单条长 POST 流 <code>subscriptions/listen</code></td><td>SEP-2575</td></tr><tr><td>断线续传</td><td>SSE <code>Last-Event-ID</code> 重投递</td><td>取消，断流即丢请求，客户端重新发起</td><td>SEP-2575</td></tr><tr><td>列表结果缓存</td><td>无字段</td><td><code>ttlMs</code> + <code>cacheScope</code> 必填</td><td>SEP-2549</td></tr><tr><td>网关识别</td><td>需解析 body</td><td><code>Mcp-Method</code> / <code>Mcp-Name</code> 请求头</td><td>SEP-2243</td></tr><tr><td>链路追踪</td><td>无约定</td><td><code>_meta</code> 内 <code>traceparent</code> / <code>baggage</code></td><td>SEP-414</td></tr><tr><td>后台任务</td><td>核心协议内 <code>tasks/*</code></td><td>移出为 <code>io.modelcontextprotocol/tasks</code> 扩展</td><td>SEP-2663</td></tr></tbody></table></div><p>还有一条容易被忽略但影响长期的：这一版同时确立了特性生命周期与弃用策略，弃用窗口不短于 12 个月，并把 Roots、Sampling、Logging 正式标记为废弃（SEP-2577、SEP-2596）。也就是说，这三个你熟悉的特性不是不能用，而是”新实现不该再加支持”。</p><h2 id="二、无状态化意味着什么：四个落点搬家"><a href="#二、无状态化意味着什么：四个落点搬家" class="headerlink" title="二、无状态化意味着什么：四个落点搬家"></a>二、无状态化意味着什么：四个落点搬家</h2><p>“无状态”这三个字很抽象，落到代码上其实只有四个地方换了位置。</p><p><strong>连接</strong>：从”先握手再干活”变成”每次请求自带上下文”。没有 <code>initialize</code>，也就没有”连接建立中”这个状态机。</p><p><strong>会话 ID</strong>：以前 <code>Mcp-Session-Id</code> 是隐式的状态容器，购物车、浏览器实例、数据库事务都挂在它上面。SEP-2567 说得很直白：会话在生态里从未收敛出统一语义——有人按工具调用划分，有人按应用启动划分，几乎没人真的去恢复它，所以列表端点无法跨会话缓存。新做法是 <code>create_basket()</code> 返回 <code>basket_id</code>，后续 <code>add_item(basket_id, ...)</code> 自己带。</p><p><strong>能力协商</strong>：以前一次握手定终身，现在每个请求的 <code>params._meta</code> 里带 <code>io.modelcontextprotocol/protocolVersion</code>、<code>clientCapabilities</code>，客户端宜带 <code>clientInfo</code>，服务端宜在结果 <code>_meta</code> 里回 <code>serverInfo</code>。版本不匹配返回 <code>UnsupportedProtocolVersionError</code>（错误码 -32022）。另外新增 <code>server/discover</code>，服务端<strong>必须</strong>实现，用来声明自己支持的版本、能力与身份——它是”提前选版本”和”stdio 上探测旧服务端”的双重入口。</p><p><strong>流式传输</strong>：这是改动最狠的一块。HTTP GET 端点没了，<code>resources/subscribe</code> 也没了，取而代之的是 <code>subscriptions/listen</code>：一条长 POST 响应流，客户端显式声明订阅类型（<code>toolsListChanged</code>、<code>promptsListChanged</code>、<code>resourcesListChanged</code>、<code>resourceSubscriptions</code>），服务端确认后给每条通知打上 <code>subscriptionId</code>。而 <code>notifications/progress</code>、<code>notifications/message</code> 这类请求内通知，仍然只走它所属请求的那条响应流。</p><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart LR    C[客户端 Agent] --&gt; G[网关 鉴权 限流 路由]    G --&gt; S1[MCP Server 副本 A]    G --&gt; S2[MCP Server 副本 B]    G --&gt; S3[MCP Server 副本 C]    S1 --&gt; D[外部状态存储 Redis 或 DB]    S2 --&gt; D    S3 --&gt; D    S1 --&gt; T[链路追踪 采集]    S2 --&gt; T    S3 --&gt; T    G --&gt; L[统一访问日志]  </pre></div><p>这张图想让你看见的是：网关和数据面之间多了一道”任意副本都能接住”的假设。旧模型里，客户端与副本 A 绑死，网关必须做会话粘滞；新模型里，请求只要自带版本、能力和业务句柄，副本是谁都无所谓，状态被推到外部存储，链路追踪也从”一条长连接里混着 N 个请求”变成”一次请求一条 trace”。</p><h2 id="三、为什么向-HTTP-靠拢：四件在长连接里做不好的事"><a href="#三、为什么向-HTTP-靠拢：四件在长连接里做不好的事" class="headerlink" title="三、为什么向 HTTP 靠拢：四件在长连接里做不好的事"></a>三、为什么向 HTTP 靠拢：四件在长连接里做不好的事</h2><p>我不认为这是设计者偏爱 HTTP。更准确地说，是企业基础设施里的四件事，在有状态长连接上都很难做。</p><p><strong>网关</strong>。网关的世界观是”无状态请求进、无状态响应出”，它靠路径、方法、头做路由和限流。会话模型下，你只能上会话粘滞或一致性哈希，副本负载不均、扩缩容时连接被打断，全是运维事故。SEP-2243 要求 POST 必带 <code>Mcp-Method</code> 与 <code>Mcp-Name</code>，本质是把”这次调用是什么”从 JSON body 提升到 HTTP 头——网关不用解析 body 就能按工具名限流、审计、熔断。</p><p><strong>鉴权</strong>。会话 ID 不是身份凭证，它只是”我之前来过”。企业里要做的是每个请求校验 OAuth token、按 scope 授权、按租户隔离，并在中间件里统一拒绝。无状态后 MCP 才真正像一支普通 API 那样接入现有鉴权体系，这一版还顺手把动态客户端注册往 Client ID Metadata Documents 方向推。</p><p><strong>水平扩容</strong>。状态在进程内存里，就意味副本不可随意杀；状态挪到 Redis 或数据库，副本才是”一次性”的。这一点对 Serverless 和弹性伸缩几乎是硬门槛。</p><p><strong>可观测性</strong>。以前一条长连接上跑着几十个请求，日志要靠会话 ID 拼，采样器也分不清一次工具调用从哪到哪。现在 <code>_meta</code> 里带 <code>traceparent</code>、<code>tracestate</code>、<code>baggage</code>，一次调用天然就是一条 trace，慢查询能直接定位到具体工具。</p><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  sequenceDiagram    autonumber    participant C as 客户端    participant S as 服务端    Note over C,S: 旧模型 有状态会话    C-&gt;&gt;S: initialize 握手    S--&gt;&gt;C: 下发 Mcp-Session-Id 与能力    C-&gt;&gt;S: tools&#x2F;call 携带会话头    S--&gt;&gt;C: 结果依赖实例内存中的会话    Note over C,S: 新模型 无状态核心    C-&gt;&gt;S: POST tools&#x2F;call 带 _meta 版本与能力    S--&gt;&gt;C: result 带 serverInfo 与 ttlMs    C-&gt;&gt;S: 再次调用 显式携带 basket_id    S--&gt;&gt;C: 任意副本均可处理  </pre></div><p>这张时序图想让你看见的是：旧模型里”能力协商”是一次性投资，付一次、用很久；新模型里它变成了”每次调用都要付的税”。省掉的是会话状态与粘滞路由，多出来的是每请求的元数据、可能的 <code>server/discover</code> 往返，以及客户端自己承担的幂等与重试。</p><h2 id="四、一段真实报文，比十页文档清楚"><a href="#四、一段真实报文，比十页文档清楚" class="headerlink" title="四、一段真实报文，比十页文档清楚"></a>四、一段真实报文，比十页文档清楚</h2><p>下面这段取自官方 Streamable HTTP 传输文档的请求示例，我原样保留（只删了无关注释）：</p><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">POST</span> <span class="string">/mcp</span> <span class="meta">HTTP/1.1</span></span><br><span class="line"><span class="attribute">Content-Type</span><span class="punctuation">: </span>application/json</span><br><span class="line"><span class="attribute">MCP-Protocol-Version</span><span class="punctuation">: </span>2026-07-28</span><br><span class="line"><span class="attribute">Mcp-Method</span><span class="punctuation">: </span>tools/call</span><br><span class="line"><span class="attribute">Mcp-Name</span><span class="punctuation">: </span>get_weather</span><br><span class="line"></span><br><span class="line"><span class="language-json"><span class="punctuation">&#123;</span></span></span><br><span class="line"><span class="language-json">  <span class="attr">&quot;jsonrpc&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2.0&quot;</span><span class="punctuation">,</span></span></span><br><span class="line"><span class="language-json">  <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span></span><br><span class="line"><span class="language-json">  <span class="attr">&quot;method&quot;</span><span class="punctuation">:</span> <span class="string">&quot;tools/call&quot;</span><span class="punctuation">,</span></span></span><br><span class="line"><span class="language-json">  <span class="attr">&quot;params&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span></span><br><span class="line"><span class="language-json">    <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;get_weather&quot;</span><span class="punctuation">,</span></span></span><br><span class="line"><span class="language-json">    <span class="attr">&quot;arguments&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span></span><br><span class="line"><span class="language-json">      <span class="attr">&quot;location&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Seattle, WA&quot;</span></span></span><br><span class="line"><span class="language-json">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span></span><br><span class="line"><span class="language-json">    <span class="attr">&quot;_meta&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span></span><br><span class="line"><span class="language-json">      <span class="attr">&quot;io.modelcontextprotocol/protocolVersion&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2026-07-28&quot;</span><span class="punctuation">,</span></span></span><br><span class="line"><span class="language-json">      <span class="attr">&quot;io.modelcontextprotocol/clientInfo&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span></span><br><span class="line"><span class="language-json">        <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;ExampleClient&quot;</span><span class="punctuation">,</span></span></span><br><span class="line"><span class="language-json">        <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;1.0.0&quot;</span></span></span><br><span class="line"><span class="language-json">      <span class="punctuation">&#125;</span><span class="punctuation">,</span></span></span><br><span class="line"><span class="language-json">      <span class="attr">&quot;io.modelcontextprotocol/clientCapabilities&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span><span class="punctuation">&#125;</span></span></span><br><span class="line"><span class="language-json">    <span class="punctuation">&#125;</span></span></span><br><span class="line"><span class="language-json">  <span class="punctuation">&#125;</span></span></span><br><span class="line"><span class="language-json"><span class="punctuation">&#125;</span></span></span><br></pre></td></tr></table></figure><p>这段报文在说明什么：同一个 <code>/mcp</code> 端点，同时服务 2025-11-25 和 2026-07-28 两代客户端，区分靠的不是连接状态，而是请求里的 <code>MCP-Protocol-Version</code> 头和 <code>_meta</code> 里的版本字段。三条原本属于”会话”的信息——协议版本、客户端身份、客户端能力——现在全部落在这一个请求体里。而 <code>Mcp-Method</code>、<code>Mcp-Name</code> 让网关在不读 body 的情况下就知道”这次要调用 get_weather”，可以直接按工具维度做配额。注意 <code>clientCapabilities</code> 是空对象也不影响调用，这正是无状态化的收益：缺什么能力就在这次请求里说清楚，服务端当场拒绝（<code>MissingRequiredClientCapability</code>，错误码 -32021），而不是等到某个后续调用才炸。</p><h2 id="五、我的判断：谁该跟进，谁可以继续等"><a href="#五、我的判断：谁该跟进，谁可以继续等" class="headerlink" title="五、我的判断：谁该跟进，谁可以继续等"></a>五、我的判断：谁该跟进，谁可以继续等</h2><p><strong>我的判断是</strong>：判断标准不是”你是不是 AI 团队”，而是”你的 MCP Server 有没有第二个副本、有没有第三个调用方（尤其是浏览器或第三方）”。</p><div class="table-container"><table><thead><tr><th>场景</th><th>建议</th><th>理由</th></tr></thead><tbody><tr><td>面向外部开放、有网关与多租户的 MCP 平台</td><td>尽快跟进</td><td>粘滞会话与统一鉴权天然冲突，越晚改成本越高</td></tr><tr><td>多副本部署的工具服务（搜索、数据库、内部 API）</td><td>优先跟进</td><td>无状态化后可以直接上普通负载均衡与滚动发布</td></tr><tr><td>单个长任务型服务（构建、爬取、批处理）</td><td>谨慎跟进</td><td>取消断线重投递后，可靠性要自己做，收益与风险并存</td></tr><tr><td>本机 stdio 个人工具（IDE 插件、CLI）</td><td>可以继续用旧版</td><td>没有网关、没有扩容，握手成本反而是净损失</td></tr><tr><td>强会话语义的交互式工具（浏览器自动化、REPL）</td><td>先改造工具设计再跟进</td><td>句柄即状态，模型得学会传 <code>session_handle</code>，这不是协议能替你解决的</td></tr></tbody></table></div><p><strong>我的理解是</strong>，这一版真正卖的是一句话：协议不替你保管状态。它把”状态”从协议层赶到了应用层，同时把应用层的自由度也还给了你——你可以用 Redis、可以用数据库事务、也可以干脆什么都不存。代价是，那些原本靠会话”白嫖”的东西（列表结果、订阅生命周期、断点续传）现在都要你自己显式建模。</p><h2 id="六、我踩过的坑，和我不太同意的地方"><a href="#六、我踩过的坑，和我不太同意的地方" class="headerlink" title="六、我踩过的坑，和我不太同意的地方"></a>六、我踩过的坑，和我不太同意的地方</h2><p><strong>我踩过的坑</strong>：最容易犯的错是把 <code>handle</code> 当成”新名字的 session”。句柄在协议里没有生命周期语义——它只是服务端签发的字符串，什么时候过期、谁负责清理、能不能跨租户复用，全是你自己的事。如果照旧用一个全局 dict 存句柄，内存泄漏会原封不动地回来，只是换了个字段名。更隐蔽的问题是：句柄要作为工具参数进模型上下文，<code>basket_id</code> 这类短 ID 无所谓，但如果你把一坨 base64 编码的会话快照塞进去，token 成本和提示注入面都会上升。这一点协议没有给答案，属于工具设计者的取舍。</p><p><strong>这里我不太同意</strong>：取消 SSE <code>Last-Event-ID</code> 与消息重投递，把”流断了怎么办”完全推给应用层。规范的说法很干脆——响应流断了，这次请求就丢了，客户端<strong>必须</strong>用新的 request id 重新发起。对只读查询这没问题；但对一个跑了十分钟的写入型工具，这是真实风险：断流意味着你可能既不知道它成没成功，又要再发一次。所以我的结论是，<strong>写操作工具必须自带幂等键</strong>，这不该是可选项。</p><p>另一处我保留意见的是 <code>subscriptions/listen</code> 的落地形态。它把推送收敛成”一条长 POST 响应流”，语义上比 GET 流清晰得多，但它终究还是一条长连接，仍然要过企业网关的空闲超时。很多网关默认 60 秒无数据就断，而列表变更通知可能几分钟才来一条。这条在规范层面无法解决，只能靠部署侧调参或加心跳——<strong>这也是我认为本版最难落地的一点</strong>。</p><p><strong>我的判断（关于迁移节奏）</strong>：不要一次性切。先让服务端同时支持两代（规范里叫 dual-era），用日志统计”到底有多少调用真的读写了会话状态”，通常这个比例远低于你的直觉。把真正依赖会话的那几个工具改造成句柄式，剩下的请求天然就是无状态的，迁移面会突然变小。</p><h2 id="七、今天就能动手的五件事"><a href="#七、今天就能动手的五件事" class="headerlink" title="七、今天就能动手的五件事"></a>七、今天就能动手的五件事</h2><ol><li>在 MCP Server 入口加一列日志：<code>method</code> + <code>_meta</code> 里的协议版本 + 是否读写了进程内状态，先量化”会话依赖度”。</li><li>把服务里所有会话作用域的字典列出来（购物车、事务、浏览器实例、临时文件），逐个改造成 <code>create_xxx</code> 返回句柄 + 显式过期策略。</li><li>网关侧先把 <code>MCP-Protocol-Version</code>、<code>Mcp-Method</code>、<code>Mcp-Name</code> 做成透传 + 按工具名限流，这一步不依赖协议升级就能做。</li><li>给写操作工具加 <code>idempotency_key</code>，因为”断流后重发”在无状态模型里是常态而不是异常。</li><li>对照官方 changelog 逐条确认你的 SDK 是否支持 dual-era；在下结论前先看 SDK 自己的 release notes，不要只看博客。</li></ol><h2 id="八、待核清单"><a href="#八、待核清单" class="headerlink" title="八、待核清单"></a>八、待核清单</h2><p>写技术文章最怕把”我记得”当”我知道”，以下三条我明确没核实完，先标出来：</p><ul><li><strong>各官方 SDK 的具体落地版本</strong>：我只核了规范文本与 SEP 状态，没有逐个核对 TypeScript、Python、Java、C#、Go SDK 的 release notes，因此”某个 SDK 从哪个版本开始支持 2026-07-28”待核。</li><li><strong>这一版是否有独立的发布公告与确切发布日期</strong>：我核到的是以日期命名的 revision <code>2026-07-28</code> 及其 changelog 与 schema 路径，未核到独立公告文，故不写”某月某日发布”。</li><li><strong>主流云厂商托管 MCP 网关对 <code>subscriptions/listen</code> 的支持情况</strong>：未核，任何”已经支持”的说法都需要落到具体产品文档。</li></ul><p>事实来源：<code>modelcontextprotocol.io</code> 的 2026-07-28 changelog、versioning 页、Streamable HTTP 传输页，以及 SEP-2575、SEP-2567 原文。</p>]]></content>
    
    
    <summary type="html">拆解 MCP 2026-07-28 这一版：去掉 initialize 与 Mcp-Session-Id、改用每请求 _meta 与 server/discover，并从网关、鉴权、扩容、可观测性四个角度说明协议为什么向 HTTP 收敛，同时给出跟进与观望的边界。</summary>
    
    
    
    <category term="理解" scheme="https://u7u7.top/categories/%E7%90%86%E8%A7%A3/"/>
    
    
    <category term="AI" scheme="https://u7u7.top/tags/AI/"/>
    
    <category term="Agent" scheme="https://u7u7.top/tags/Agent/"/>
    
    <category term="MCP" scheme="https://u7u7.top/tags/MCP/"/>
    
    <category term="协议设计" scheme="https://u7u7.top/tags/%E5%8D%8F%E8%AE%AE%E8%AE%BE%E8%AE%A1/"/>
    
  </entry>
  
  <entry>
    <title>为什么 Spring Boot 4 能在编译期报空指针：JSpecify 从注解到字节码的整条链</title>
    <link href="https://u7u7.top/posts/springboot4-jspecify-null-safety.html"/>
    <id>https://u7u7.top/posts/springboot4-jspecify-null-safety.html</id>
    <published>2026-10-07T00:30:00.000Z</published>
    <updated>2026-10-26T13:29:19.130Z</updated>
    
    <content type="html"><![CDATA[<h1 id="为什么-Spring-Boot-4-能在编译期报空指针：JSpecify-从注解到字节码的整条链"><a href="#为什么-Spring-Boot-4-能在编译期报空指针：JSpecify-从注解到字节码的整条链" class="headerlink" title="为什么 Spring Boot 4 能在编译期报空指针：JSpecify 从注解到字节码的整条链"></a>为什么 Spring Boot 4 能在编译期报空指针：JSpecify 从注解到字节码的整条链</h1><p>先说一个可能让你不太舒服的结论：Spring Boot 4 并没有给你的项目装上一个”编译期 NPE 检查器”。</p><p>真实的链路是这样的——Spring 把自家框架和整个 portfolio 的 API 契约，用 JSpecify 注解写进了 class 文件；然后，如果你（或者是你的调用方）在构建里挂了 NullAway，或者你写的是 Kotlin，这些契约才会在编译期变成错误。少了最后一环，<code>mvn compile</code> 依然一声不吭，该在运行时抛 NPE 的还是抛。</p><p>这个区别很关键：它决定了你要不要为这件事改构建、改代码、改团队习惯，也决定了你读到”Spring Boot 4 支持编译期空安全”这类宣传语时，该信几分。</p><h2 id="一、JSpecify-不是-JSR-305-的续集，是一份能被机器执行的规范"><a href="#一、JSpecify-不是-JSR-305-的续集，是一份能被机器执行的规范" class="headerlink" title="一、JSpecify 不是 JSR-305 的续集，是一份能被机器执行的规范"></a>一、JSpecify 不是 JSR-305 的续集，是一份能被机器执行的规范</h2><p>JSpecify 1.0.0 在 2024 年 7 月 17 日发布到 Maven Central，正式定下四个注解：<code>@Nullable</code>、<code>@NonNull</code>、<code>@NullMarked</code>、<code>@NullUnmarked</code>，并承诺不再做不兼容变更。</p><p>四个里真正改变工作方式的是 <code>@NullMarked</code>。它的语义是”作用域内默认非空”：一个包、类或方法被标记后，里面所有类型使用处默认排除 null，例外必须显式写 <code>@Nullable</code>；<code>@NullUnmarked</code> 则把作用域退回 Java 原生语义，也就是”未知”。Spring 文档给出的典型用法是在 <code>package-info.java</code> 上打包级标记，一个项目几十上百个包，每个包一行，成本不高但收益是全局的。</p><p>和 Spring 的老做法对比，能看清它为什么值得换。Spring 从 5.0 起用 JSR-305 的元注解拼出 <code>@NonNullApi</code> / <code>@NonNullFields</code>，那套机制的问题很具体：JSR-305 本身早已休眠，注解的 <code>@Target</code> 指向声明而不是类型使用，依赖是”借来的”（split package 风险）。JSpecify 的注解是 <code>@Target(ElementType.TYPE_USE)</code> 且 <code>@Retention(RUNTIME)</code>——前者让它能标注 <code>List&lt;@Nullable String&gt;</code> 这一类位置，后者让它在 class 文件里留下来，才有了后半段”运行期读取”的可能。</p><div class="table-container"><table><thead><tr><th>维度</th><th>JSR-305</th><th>Spring lang 老注解</th><th>JSpecify 1.0</th></tr></thead><tbody><tr><td>注解目标</td><td>声明</td><td>声明</td><td>类型使用 TYPE_USE</td></tr><tr><td>泛型内元素</td><td>无法表达</td><td>无法表达</td><td><code>List&lt;@Nullable String&gt;</code></td></tr><tr><td>包级默认非空</td><td>靠元注解拼</td><td>靠元注解拼</td><td><code>@NullMarked</code> 一等公民</td></tr><tr><td>保留策略</td><td>CLASS</td><td>RUNTIME</td><td>RUNTIME</td></tr><tr><td>维护状态</td><td>事实休眠</td><td>Spring 7 起废弃</td><td>正式 1.0</td></tr></tbody></table></div><p>表格最后一行是硬事实：Spring Framework 7 把 <code>org.springframework.lang</code> 下的 <code>@Nullable</code>、<code>@NonNull</code>、<code>@NonNullApi</code>、<code>@NonNullFields</code> 全部标为 deprecated，并建议迁移到 JSpecify。注意”建议”这个词——它没有在 7.0 里删掉，因为整个生态的迁移需要时间。</p><h2 id="二、整条链路：注解-→-检查-→-class-文件-→-运行期"><a href="#二、整条链路：注解-→-检查-→-class-文件-→-运行期" class="headerlink" title="二、整条链路：注解 → 检查 → class 文件 → 运行期"></a>二、整条链路：注解 → 检查 → class 文件 → 运行期</h2><p>先给一张全景图，后面每一节都是在这张图上放大某一个格子。</p><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart LR    SRC[源码 注解声明] --&gt; IDE[IDE 实时提示]    SRC --&gt; JC[javac 编译]    JC --&gt; EP[Error Prone 插件 NullAway]    EP --&gt;|契约不满足| FAIL[构建失败]    EP --&gt;|契约满足| CLS[class 文件 类型注解]    IDE --&gt; CLS    JC --&gt; KOT[Kotlin 调用方 编译期报错]    CLS --&gt; RT[运行期 Nullness 查询]    RT --&gt; DEC[参数解析 与 契约判断]  </pre></div><p>这张图想让你看见的是”分叉”：同一份注解，往上走进 IDE 得到波浪线，往左走进 javac 得到构建错误，往下走进 class 文件在运行期被框架读回来。三条路彼此独立——你把 IDE 的检查关掉，不影响构建；你把 NullAway 从构建里摘掉，不影响运行期框架读注解。很多人以为这是一套联动机制，其实它是三个消费者共用一份输入。</p><h2 id="三、到底是谁在编译期说话"><a href="#三、到底是谁在编译期说话" class="headerlink" title="三、到底是谁在编译期说话"></a>三、到底是谁在编译期说话</h2><p>这一环最容易含糊，我把它拆成五层分别看：</p><div class="table-container"><table><thead><tr><th>检查者</th><th>阶段</th><th>默认开启</th><th>对 Spring 7 API 的反应</th></tr></thead><tbody><tr><td>javac 原生</td><td>编译期</td><td>是</td><td>什么都不做，Java 没有空性类型系统</td></tr><tr><td>IntelliJ IDEA</td><td>编辑期</td><td>是</td><td>按 JSpecify 给警告</td></tr><tr><td>Eclipse</td><td>编辑期</td><td>否，需手动配置</td><td>警告</td></tr><tr><td>NullAway + Error Prone</td><td>构建期，挂在 javac 上</td><td>否，需显式接入</td><td>编译失败</td></tr><tr><td>Kotlin 编译器</td><td>编译期</td><td>是</td><td>翻译为空类型，直接报错</td></tr></tbody></table></div><p>这里我不太同意”Spring Boot 4 能在编译期报空指针”这个流行说法（包括我自己的标题）。javac 不认识 <code>@Nullable</code>——它只把注解当成普通的类型注解写进 class 文件，不会因为你在 <code>@NullMarked</code> 包里有解引用就报错。真正报错的是挂在 javac 上的 Error Prone 插件，是编译流程里额外加的一道 lint 层。区别在哪？javac 的空性检查如果存在，会是全语言、不可绕过的基础语义；而 NullAway 是可选的、可配的、能一行配置关掉的静态分析。</p><p>这个区别不是文字游戏，它带来两个实际后果。第一，NullAway 默认只检查打了 <code>@NullMarked</code> 的包（<code>NullAway:OnlyNullMarked=true</code>，Spring 官方推荐的配置），因为全量检查老代码会产生海量告警，团队第一天就想关掉它。第二，”编译期报错”的覆盖范围取决于你的配置，而不是取决于你用了 Spring Boot 4——同一个仓库，A 组配了 NullAway，B 组没配，同样的 bug 在 A 组过不了构建，在 B 组上线后半夜报警。</p><p>边界我也说清楚：在 Kotlin 项目里，这句话是成立的。Kotlin 编译器直接读 JSpecify 注解并翻译成 Kotlin 的空类型，契约不满足就是编译错误，不需要任何插件。你现在能拿到的”最省事的编译期空安全”，其实来自 Kotlin 而不是 Spring。至于 Java 侧，Spring 官方措辞很克制：用支持 JSpecify 的 IDE 会得到警告，可选地接入 NullAway 在构建期强制。</p><h2 id="四、Spring-Framework-7-真正改变的是契约，不是你的编译结果"><a href="#四、Spring-Framework-7-真正改变的是契约，不是你的编译结果" class="headerlink" title="四、Spring Framework 7 真正改变的是契约，不是你的编译结果"></a>四、Spring Framework 7 真正改变的是契约，不是你的编译结果</h2><p>那 Spring 到底做了什么？它把”这个方法可能返回 null”从口头约定、Javadoc 注释、以及社区口耳相传的常识，变成了机器可读、可校验、可被工具链消费的声明。这是契约层面的变化，不是编译器的变化。</p><p>看一段前后对比。假设老代码里有一个查订单的服务，返回值可能为空，但从来没写清楚：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 迁移前：契约只存在于作者脑子里</span></span><br><span class="line"><span class="keyword">package</span> demo.order;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.Optional;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">OrderService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> OrderRepository repository;</span><br><span class="line"></span><br><span class="line">    <span class="type">public</span> <span class="title function_">OrderService</span><span class="params">(OrderRepository repository)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.repository = repository;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 调用方不知道这里可能返回 null，直接 order.title() 就炸</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">Order</span> <span class="title function_">find</span><span class="params">(String id)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> repository.findById(id);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 迁移后：契约写在类型上，工具链能读</span></span><br><span class="line"><span class="keyword">package</span> demo.order;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.jspecify.annotations.Nullable;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">class</span> <span class="title class_">OrderService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> OrderRepository repository;</span><br><span class="line"></span><br><span class="line">    <span class="type">public</span> <span class="title function_">OrderService</span><span class="params">(OrderRepository repository)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.repository = repository;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="meta">@Nullable</span> <span class="type">Order</span> <span class="title function_">find</span><span class="params">(String id)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> repository.findById(id);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="type">String</span> <span class="title function_">describe</span><span class="params">(String id)</span> &#123;</span><br><span class="line">        <span class="type">Order</span> <span class="variable">order</span> <span class="operator">=</span> find(id);</span><br><span class="line">        <span class="keyword">return</span> order == <span class="literal">null</span> ? <span class="string">&quot;unknown&quot;</span> : order.title();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>配套的包级声明，一行文件解决整个包：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@NullMarked</span></span><br><span class="line"><span class="keyword">package</span> demo.order;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.jspecify.annotations.NullMarked;</span><br></pre></td></tr></table></figure><p>这段代码在说明什么？三点。第一，<code>@Nullable</code> 是写在类型前面的（<code>public @Nullable Order find</code>），不是写在方法声明上——这是 TYPE_USE 注解的硬性语法，写错位置在 JSpecify 语义下是无效的。第二，迁移后 <code>describe</code> 里那个 <code>order == null</code> 判断不是可选的风格问题，而是 NullAway 认可的空值收敛：不写这个判断，构建期就会报错。第三，<code>package-info.java</code> 里的 <code>@NullMarked</code> 让这个包内”未标注 = 非空”，所以 <code>OrderRepository</code> 的返回值一旦被声明为 <code>@Nullable</code>，一路传到 <code>describe</code> 都会被追踪。</p><p>顺带说一个真实的坑：<code>Assert.notNull()</code> 这类断言方法不会自动让分析器”学会”收敛。Spring 给 <code>Assert.notNull</code> 加了 <code>@Contract(&quot;null, _ → fail&quot;)</code>，而 NullAway 需要你显式配置 <code>NullAway:CustomContractAnnotations=org.springframework.lang.Contract</code> 才认这个注解。不配，你会得到一堆”明明前面断言过了还报”的告警，然后开始怀疑工具。</p><h2 id="五、几个坑，我踩过的"><a href="#五、几个坑，我踩过的" class="headerlink" title="五、几个坑，我踩过的"></a>五、几个坑，我踩过的</h2><p>我踩过的坑集中在三处，都和「注解写了就完事」的直觉相反。</p><p>第一个坑：override 不继承注解。JSpecify 注解不会被继承，你在父类或接口上写了 <code>@Nullable</code>，子类实现必须自己再抄一遍，否则子类的实现按”非空”解释。如果父类来自外部依赖、根本没标，你会看到一堆 <code>@SuppressWarnings(&quot;NullAway&quot;) // Overridden method does not define nullability</code>。这类抑制注释会越积越多，是迁移期最大的隐性债务。</p><p>第二个坑：数组语法反直觉。在 <code>@NullMarked</code> 代码里，<code>@Nullable Object[]</code> 表示元素可空、数组本身非空；<code>Object @Nullable []</code> 表示元素非空、数组可空；两个都写才是都空。我第一次迁移时写反过一次，编译期没报错——因为语义恰好自洽，只是和我的意图不同。这种”静默的错误契约”比报错更危险。</p><p>第三个坑：泛型的空性检查还不完整。Spring 文档里明确写了：泛型类型和泛型方法上的空性还没被 NullAway 完全支持；<code>NullAway:JSpecifyMode=true</code> 这个开关虽然能检查数组、可变参数和泛型的完整 JSpecify 语义，但官方标注它仍在开发中，且要求 JDK 22 以上。所以如果你的代码重度依赖泛型容器，别指望第一天上全量模式。</p><h2 id="六、运行期那一段：class-文件里的类型注解怎么被读回来"><a href="#六、运行期那一段：class-文件里的类型注解怎么被读回来" class="headerlink" title="六、运行期那一段：class 文件里的类型注解怎么被读回来"></a>六、运行期那一段：class 文件里的类型注解怎么被读回来</h2><p>JSpecify 注解是 RUNTIME 保留 + TYPE_USE，所以它们会作为类型注解留在 class 文件里。这就是 Spring 7 新增的 <code>org.springframework.core.Nullness</code> 枚举能工作的前提：它提供 <code>forField</code>、<code>forParameter</code>、<code>forMethodParameter</code>、<code>forMethodReturnType</code> 四个查询入口，返回 <code>NON_NULL</code>、<code>NULLABLE</code>、<code>UNSPECIFIED</code> 三种结果。官方 Javadoc 写得很清楚：完整支持 JSpecify、Kotlin 空安全和 Java 基本类型，对任意包的 <code>@Nullable</code> 都做务实检查；而 JSR-305 和 <code>org.springframework.lang</code> 的 <code>@NonNullApi</code> / <code>@NonNullFields</code> / <code>@NonNull</code> 不被支持。</p><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  sequenceDiagram    participant Dev as 开发者    participant Build as javac 与 NullAway    participant Class as class 文件    participant Frame as Spring 运行期    Dev-&gt;&gt;Build: 提交代码 或执行 mvn compile    Build-&gt;&gt;Build: 在 NullMarked 作用域内做数据流检查    alt 契约不满足        Build--&gt;&gt;Dev: 构建失败 报出空性错误    else 契约满足        Build-&gt;&gt;Class: 写入类型注解 与 参数注解        Class-&gt;&gt;Frame: 启动或解析时反射读取        Frame--&gt;&gt;Dev: 按契约决定参数是否必填    end  </pre></div><p>我的理解是：这一格才是 Spring 选 JSpecify 而不是自研注解的真正原因——它要的是能被工具链反复消费的元数据，而不是一份好看的文档。</p><p>这张时序图想让你看见”字节码”这一格为什么不能省。注解如果没有 RUNTIME 保留，链路在编译结束就断了，框架只能靠”猜”或者靠额外的元数据文件；正因为注解留在了 class 文件里，运行期才有机会把契约当成一等数据来用。至于 Spring 内部具体在哪些参数解析器里调用了 <code>Nullness</code>，我没有逐个核对调用点，这一段属于机制层面的推断——机制是确定的，调用点分布待核。</p><h2 id="七、我的判断：它解决什么，不解决什么"><a href="#七、我的判断：它解决什么，不解决什么" class="headerlink" title="七、我的判断：它解决什么，不解决什么"></a>七、我的判断：它解决什么，不解决什么</h2><p>我的判断是：这套东西真正的价值不在”抓 NPE”，而在”把契约变成可传递的资产”，外加 Kotlin 互操作。你自己写的代码，NullAway 只能看到你写了注解的部分；而 Spring 7 把上游几万个方法签名的空性标清楚了，你的分析器第一次能跨库推理——<code>jdbcTemplate.queryForObject</code> 到底会不会返回 null，以前靠试，现在靠注解。这是它最实际的一次性收益。</p><p>不解决的部分同样明确，我把它们列成清单，方便你在评审时直接引用：</p><ul><li>反射与依赖注入：<code>@Autowired</code> 字段注入、<code>@Value</code>、Bean 后处理器设置的字段，NullAway 看不到，<code>Nullness</code> 也只是把契约读出来，不会替你把值填进去。</li><li>框架生成的代理：AOP 代理、CGLIB 子类上的方法，空性注解是否跟着走，取决于代理生成方式，别默认它一定可靠。</li><li>反序列化与外部数据：JSON 里的 <code>null</code>、数据库列、HTTP 请求体，注解只是声明，校验还得靠 Bean Validation。</li><li>容器与集合语义：<code>Map.get</code> 返回 null 是 API 契约的一部分，JSpecify 只能让你显式标注，不能把它变成 <code>Optional</code>。</li><li>你同事的代码：这是一个社会问题，不是技术问题。</li></ul><p>落地成本我也算过一笔账，诚实说：中小项目几百个类的量级，接入 NullAway 到”构建能过”大约需要一到两周的集中清理，之后每周零星修；大项目如果历史代码里大量 <code>@Nullable</code> 缺失，前期噪音会非常劝退，务必先用 <code>OnlyNullMarked=true</code> 缩小战场，一个包一个包推进。</p><h2 id="八、今天就能动手的清单"><a href="#八、今天就能动手的清单" class="headerlink" title="八、今天就能动手的清单"></a>八、今天就能动手的清单</h2><ol><li>升级前先确认基线：Spring Framework 7.0 与 Spring Boot 4.0 分别于 2025 年 11 月 13 日和 11 月 20 日 GA，先在一个非核心模块上升级，别全量铺开。</li><li>把 <code>org.springframework.lang</code> 的 <code>@Nullable</code> / <code>@NonNull</code> 全量替换成 <code>org.jspecify.annotations</code> 版本，只改 import 和注解位置（注意挪到类型前面）。</li><li>给自己最核心的一两个包加 <code>package-info.java</code> + <code>@NullMarked</code>，不要一次全加。</li><li>接 NullAway 时先只开 <code>OnlyNullMarked=true</code> 和 <code>CustomContractAnnotations=org.springframework.lang.Contract</code>，<code>JSpecifyMode</code> 留到第二阶段。</li><li>在 CI 里把 NullAway 设为阻塞：只警告不阻塞的检查，三个月后一定退化成没人看的日志。</li><li>给团队写一页规则：override 必须复制注解、数组空性三种写法、以及 <code>@SuppressWarnings</code> 必须带理由注释。</li></ol><h2 id="待核与来源"><a href="#待核与来源" class="headerlink" title="待核与来源"></a>待核与来源</h2><ul><li><strong>待核</strong>：Kotlin 编译器从哪个具体版本开始完整支持 JSpecify 注解。Spring 文档只说明”Kotlin 中 JSpecify 注解会被自动翻译为 Kotlin 的空安全”，未给出起始版本；我也没有找到确切的版本号出处，因此不在文中写死。</li><li><strong>待核</strong>：Spring 内部具体哪些参数解析器调用了 <code>Nullness</code> API。Javadoc 只描述了 API 能力，没有列出调用点。</li><li><strong>有意不写</strong>：NullAway、Error Prone 插件的具体依赖版本号。这些版本迭代很快，写死在文章里第二天就可能过期，请以官方发布为准。</li></ul><p>核验来源：</p><ul><li>JSpecify 1.0.0 发布公告（2024-07-17，四个注解正式化）→ <a href="https://jspecify.dev/blog/release-1.0.0/">https://jspecify.dev/blog/release-1.0.0/</a></li><li>Spring Framework 7.0 GA（2025-11-13，comprehensive null safety through JSpecify）→ <a href="https://spring.io/blog/2025/11/13/spring-framework-7-0-general-availability">https://spring.io/blog/2025/11/13/spring-framework-7-0-general-availability</a></li><li>Spring Boot 4.0.0 GA（2025-11-20，portfolio-wide null safety improvements with JSpecify）→ <a href="https://spring.io/blog/2025/11/20/spring-boot-4-0-0-available-now">https://spring.io/blog/2025/11/20/spring-boot-4-0-0-available-now</a></li><li>Spring Framework 参考文档 Null-safety 章节（NullAway 构建期检查、@NullMarked 包级用法、数组与泛型语法、老注解废弃）→ <a href="https://docs.spring.io/spring-framework/reference/core/null-safety.html">https://docs.spring.io/spring-framework/reference/core/null-safety.html</a></li><li><code>Nullness</code> 枚举 Javadoc（7.0 引入，四个查询方法与三种返回值）→ <a href="https://docs.spring.io/spring-framework/docs/7.0.10/javadoc-api/org/springframework/core/Nullness.html">https://docs.spring.io/spring-framework/docs/7.0.10/javadoc-api/org/springframework/core/Nullness.html</a></li><li>NullAway 官方仓库（Error Prone 插件、构建期开销）→ <a href="https://github.com/uber/NullAway">https://github.com/uber/NullAway</a></li><li>JSpecify 注解源码（TYPE_USE + RUNTIME 保留）→ <a href="https://github.com/jspecify/jspecify/blob/main/src/main/java/org/jspecify/annotations/Nullable.java">https://github.com/jspecify/jspecify/blob/main/src/main/java/org/jspecify/annotations/Nullable.java</a></li></ul>]]></content>
    
    
    <summary type="html">JSpecify 到 Spring Boot 4 的空安全链路：注解怎么进 class 文件，谁在构建期真正报错，反射为什么逃逸。</summary>
    
    
    
    <category term="技术笔记" scheme="https://u7u7.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="SpringBoot" scheme="https://u7u7.top/tags/SpringBoot/"/>
    
    <category term="Java" scheme="https://u7u7.top/tags/Java/"/>
    
    <category term="空安全" scheme="https://u7u7.top/tags/%E7%A9%BA%E5%AE%89%E5%85%A8/"/>
    
    <category term="JSpecify" scheme="https://u7u7.top/tags/JSpecify/"/>
    
  </entry>
  
  <entry>
    <title>JDK 27 不是 LTS：后量子密码默认生效了，虚拟线程之后 JVM 还想解决什么</title>
    <link href="https://u7u7.top/posts/jdk27-lts-pqc-notes.html"/>
    <id>https://u7u7.top/posts/jdk27-lts-pqc-notes.html</id>
    <published>2026-10-05T00:30:00.000Z</published>
    <updated>2026-10-26T13:29:19.139Z</updated>
    
    <content type="html"><![CDATA[<h1 id="JDK-27-不是-LTS：后量子密码默认生效了，虚拟线程之后-JVM-还想解决什么"><a href="#JDK-27-不是-LTS：后量子密码默认生效了，虚拟线程之后-JVM-还想解决什么" class="headerlink" title="JDK 27 不是 LTS：后量子密码默认生效了，虚拟线程之后 JVM 还想解决什么"></a>JDK 27 不是 LTS：后量子密码默认生效了，虚拟线程之后 JVM 还想解决什么</h1><p>2026 年 9 月 15 日，JDK 27 达到 GA。随后中文技术圈出现了一批标题几乎一样的文章，核心都写着同一句话：JDK 27 是 LTS。</p><p>这句话是错的，而且错得很有代表性，它把“两年一次的大版本”和“LTS”混成了一个词。Oracle 的 Java SE 支持路线图原文只列了五个 LTS：8、11、17、21、25，并明确写了下一个计划中的 LTS 是 2027 年 9 月的 Java 29。所以 27 只是一个功能版本，一年后就会被 28 取代。</p><p>但纠正标题不是这篇的目的。真正值得问的是另一件事：如果因为“27 不是 LTS”就跳过它，你会漏掉什么？</p><p>我的答案是：27 是 29 的预演带。等到 29 LTS 落地时，后量子优先的 TLS、G1 成为全环境默认、紧凑对象头默认开启，这三件事会一次性同时生效，而它们分别改的是你的协议握手、垃圾回收行为和对象布局。27 让你提前一年半，在真实流量和真实堆里把它们验一遍。</p><h2 id="一、先把事实钉住：27-到底改了什么"><a href="#一、先把事实钉住：27-到底改了什么" class="headerlink" title="一、先把事实钉住：27 到底改了什么"></a>一、先把事实钉住：27 到底改了什么</h2><p>JDK 27 是 JSR 402 的参考实现，一共 9 个 JEP，其中 4 个是正式特性（Final），4 个仍是预览，1 个还在孵化。</p><div class="table-container"><table><thead><tr><th>JEP</th><th>名称</th><th>状态</th><th>你实际会碰到的地方</th></tr></thead><tbody><tr><td>527</td><td>TLS 1.3 后量子混合密钥交换</td><td>正式</td><td>所有走 javax.net.ssl 的 HTTPS 客户端</td></tr><tr><td>523</td><td>G1 成为所有环境的默认收集器</td><td>正式</td><td>小堆、小容器、命令行工具</td></tr><tr><td>534</td><td>紧凑对象头默认开启</td><td>正式</td><td>堆占用、heap dump、基于偏移量的黑科技</td></tr><tr><td>536</td><td>JFR 进程内数据脱敏</td><td>正式</td><td>生产环境开 JFR 的合规边界</td></tr><tr><td>538</td><td>加密对象的 PEM 编码</td><td>第三预览</td><td>密钥与证书的文本交换</td></tr><tr><td>531</td><td>惰性常量</td><td>第三预览</td><td>启动期初始化的类</td></tr><tr><td>532</td><td>基本类型的模式匹配</td><td>第五预览</td><td>switch 里的 int、long</td></tr><tr><td>533</td><td>结构化并发</td><td>第七预览</td><td>虚拟线程的任务编排</td></tr><tr><td>537</td><td>Vector API</td><td>第十二孵化</td><td>手写 SIMD</td></tr></tbody></table></div><p>看完这张表你会发现一个规律：27 没有一门新语言特性转正，它的重量全压在“默认值”上。四个正式特性里，有三个是把某个东西改成默认，而不是新增一个 API。默认值才是升级事故的高发区，因为没有人会去读你没写的那个开关。</p><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart LR    R[JDK 27 GA 2026-09-15] --&gt; P[协议层]    R --&gt; T[运行时默认值]    R --&gt; L[语言与 API]    P --&gt; P1[JEP 527 TLS 1.3 后量子混合密钥交换]    P --&gt; P2[JEP 538 加密对象 PEM 编码 第三预览]    T --&gt; T1[JEP 523 G1 全环境默认]    T --&gt; T2[JEP 534 紧凑对象头默认]    T --&gt; T3[JEP 536 JFR 数据脱敏]    L --&gt; L1[JEP 533 结构化并发 第七预览]    L --&gt; L2[JEP 531 惰性常量 第三预览]    L --&gt; L3[JEP 532 基本类型模式 第五预览]    L --&gt; L4[JEP 537 Vector API 第十二孵化]  </pre></div><p>这张图想让你看见的是：27 的变更不是“多了一个功能”，而是三条独立的传导路径。协议层改的是你和别人握手的方式，运行时默认值改的是你自己的性能曲线，语言与 API 则大多还没定稿。升级评估必须按这三条路径分开做，混在一起讨论就会出现“这个版本好像没什么大变化”的错觉。</p><h2 id="二、后量子密码：ML-KEM-和-ML-DSA-不是-27-才有的"><a href="#二、后量子密码：ML-KEM-和-ML-DSA-不是-27-才有的" class="headerlink" title="二、后量子密码：ML-KEM 和 ML-DSA 不是 27 才有的"></a>二、后量子密码：ML-KEM 和 ML-DSA 不是 27 才有的</h2><p>这是最容易被误读的一点。很多文章写“JDK 27 引入后量子密码”，准确的说法应该是：JDK 27 让后量子密码第一次默认生效，但算法本身在 JDK 24 就已经进来了。</p><div class="table-container"><table><thead><tr><th>版本</th><th>JEP</th><th>内容</th><th>边界</th></tr></thead><tbody><tr><td>Java 21</td><td>452</td><td>KEM API</td><td>只提供密钥封装的抽象接口</td></tr><tr><td>JDK 24</td><td>496</td><td>ML-KEM，NIST FIPS 203</td><td>javax.crypto 层的 KeyPairGenerator、KEM</td></tr><tr><td>JDK 24</td><td>497</td><td>ML-DSA，NIST FIPS 204，支持 ML-DSA-44/65/87</td><td>不做 JAR 签名，也不做 TLS</td></tr><tr><td>JDK 27</td><td>527</td><td>TLS 1.3 混合密钥交换</td><td>只做 1.3，只做 javax.net.ssl</td></tr></tbody></table></div><p>JEP 527 定义了三套混合组：X25519MLKEM768、SecP256r1MLKEM768、SecP384r1MLKEM1024。默认只开第一套，而且把它排在了客户端首选列表的第一位，完整默认顺序是 X25519MLKEM768、x25519、secp256r1、secp384r1、secp521r1、x448，再加三个 ffdhe 组。</p><p>注意 JEP 527 的两个非目标：不做纯 ML-KEM 的密钥交换，也不做 TLS 1.3 以外的版本。这说明 JDK 的策略是“先保命，不激进”——混合方案只要有一个算法没被攻破就是安全的，而纯后量子方案还得等更多密码分析。</p><p>下面这段代码是我用来确认本机 JDK 到底发了哪些组、协商成了什么。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> javax.net.ssl.SSLContext;</span><br><span class="line"><span class="keyword">import</span> javax.net.ssl.SSLParameters;</span><br><span class="line"><span class="keyword">import</span> javax.net.ssl.SSLSocket;</span><br><span class="line"><span class="keyword">import</span> java.util.Arrays;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TlsGroupProbe</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="type">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="type">SSLContext</span> <span class="variable">ctx</span> <span class="operator">=</span> SSLContext.getDefault();</span><br><span class="line">        <span class="keyword">try</span> (<span class="type">SSLSocket</span> <span class="variable">socket</span> <span class="operator">=</span> (SSLSocket) ctx.getSocketFactory()</span><br><span class="line">                 .createSocket(<span class="string">&quot;example.com&quot;</span>, <span class="number">443</span>)) &#123;</span><br><span class="line"></span><br><span class="line">            <span class="type">SSLParameters</span> <span class="variable">params</span> <span class="operator">=</span> socket.getSSLParameters();</span><br><span class="line">            System.out.println(<span class="string">&quot;default groups = &quot;</span></span><br><span class="line">                + Arrays.toString(params.getNamedGroups()));</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 只允许混合后量子组与传统 x25519，便于观察协商结果</span></span><br><span class="line">            params.setNamedGroups(<span class="keyword">new</span> <span class="title class_">String</span>[] &#123;</span><br><span class="line">                <span class="string">&quot;X25519MLKEM768&quot;</span>, <span class="string">&quot;x25519&quot;</span></span><br><span class="line">            &#125;);</span><br><span class="line">            socket.setSSLParameters(params);</span><br><span class="line">            socket.startHandshake();</span><br><span class="line"></span><br><span class="line">            System.out.println(<span class="string">&quot;protocol = &quot;</span> + socket.getSession().getProtocol());</span><br><span class="line">            System.out.println(<span class="string">&quot;cipher   = &quot;</span> + socket.getSession().getCipherSuite());</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这段代码在说明什么：第一，后量子能力在 JDK 27 里是“看不见的默认值”，你不改一行代码就会带上 X25519MLKEM768 的 key share，打出来的默认组列表就是证据；第二，setNamedGroups 是显式开关，当你要验证“对端到底支不支持 PQC”时，把组裁到只剩两个，握手失败就说明对端不认识这个组，这比读文档可靠；第三，也是我提醒自己的地方——如果生产代码里有人手工 setNamedGroups 或设置了 jdk.tls.namedGroups，那 JDK 27 的默认值对你完全无效，你升级完会发现“没变化”，其实是被自己的配置挡住了。</p><h2 id="三、为什么到-2026-年才认真做：成本在证书和协议层"><a href="#三、为什么到-2026-年才认真做：成本在证书和协议层" class="headerlink" title="三、为什么到 2026 年才认真做：成本在证书和协议层"></a>三、为什么到 2026 年才认真做：成本在证书和协议层</h2><p>我的判断是：后量子迁移更像当年 TLS 1.2 升 1.3，而不是 JDK 8 升 11。业务代码几乎不用动，工作量全部堆在证书链、中间设备和客户端矩阵上。</p><p>依据就在 JEP 里。JEP 497 在交付时明确写了不做 TLS、不做 JAR 签名，因为标准还没到位；JEP 527 又明确只做 TLS 1.3 的混合组。也就是说，密钥交换这一层现在有了，但“用 ML-DSA 签名的证书”这条链还在补：JDK 27 里 PEM 编码仍然只是第三预览，证书与签名的文本流转还没有完全固化。</p><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  sequenceDiagram    participant C as TLS 客户端 JDK 27    participant S as 服务端 支持混合组    participant M as 中间设备 TLS 终止    C-&gt;&gt;S: ClientHello 携带 X25519MLKEM768 与 x25519 两个 key share    S-&gt;&gt;C: ServerHello 选中 X25519MLKEM768    Note over C,S: 会话密钥同时依赖 ML-KEM-768 与 X25519    C-&gt;&gt;M: 同样握手 但中间设备只认识 x25519    M-&gt;&gt;C: 回落到 x25519 没有后量子保护    Note over C,M: 观测点 必须记录实际协商组 而不是记录客户端支持组  </pre></div><p>这张图想让你看见的是“升级 ≠ 生效”。JDK 升级只改变了客户端愿意提供什么，真正决定防护强度的是链路上最弱的那一跳。TLS 终止设备、七层网关、老版本的反向代理，任何一处只认 x25519，握手就会安静地退回传统算法，日志里不会报警。</p><p>所以这里有一个反直觉的代价：升级之后你的 ClientHello 变大了。混合组要同时传 ML-KEM 的封装公钥和 X25519 的 key share，单个握手包增加的数据量足以在某些 MTU 敏感或者有分片限制的老链路上出问题。我见过的情况是握手偶发超时，抓包才看出是 ClientHello 被拆包。如果你的业务里还有自建的长连接网关、有些老旧硬件负载均衡，这一点必须先测，不要等灰度。</p><p>反过来说，这个观点也有不成立的时候：如果你的服务是纯内部 mTLS、CA 和客户端都在自己手里、链路上没有任何 TLS 终止设备，那 PQC 迁移的成本确实很低，甚至可以一次推完。判断标准不是“我们有没有加密流量”，而是“链路上有几个我控制不了的 TLS 端点”。</p><h2 id="四、虚拟线程：pinning-修好了，瓶颈换了个地方"><a href="#四、虚拟线程：pinning-修好了，瓶颈换了个地方" class="headerlink" title="四、虚拟线程：pinning 修好了，瓶颈换了个地方"></a>四、虚拟线程：pinning 修好了，瓶颈换了个地方</h2><p>虚拟线程在 Java 21 由 JEP 444 转正，当时最著名的问题就是 synchronized 会钉住载体线程。这个问题在 JDK 24 由 JEP 491 解决：在 synchronized 方法和语句里阻塞的虚拟线程现在会释放载体线程。JEP 491 同时覆盖了另一种情形——虚拟线程进入 monitor 竞争或调用 Object.wait 时，以前也是钉住的。</p><p>我踩过的坑正好在这里。JDK 21 时代排查 pinning，大家的肌肉记忆是加 -Djdk.tracePinnedThreads=full 看堆栈。到了 JDK 24 以后，JEP 491 明确说这个系统属性不再需要了，诊断入口变成 JFR 的 jdk.VirtualThreadPinned 事件，而且事件本身被增强了，会带上“为什么钉住”和“载体线程是谁”。我第一次在 JDK 25 上排查一个卡顿，加了这个参数、什么都没打出来，还以为问题消失了，实际上是那个开关已经不管事了。教训很朴素：升级 JVM 时，把诊断参数当作 API 一起做兼容性检查。</p><p>那现在还会钉住吗？会。JEP 491 保留了事件用于其他 pinning 场景，最典型的是虚拟线程调用 native 代码（无论是 native 方法还是 Foreign Function &amp; Memory 回调），native 代码再回调进 Java 执行阻塞操作或等 monitor。这类问题靠升级 JVM 解决不了，只能靠替换实现或者把 native 调用挪到平台线程池。</p><div class="table-container"><table><thead><tr><th>场景</th><th>虚拟线程是否划算</th><th>原因</th></tr></thead><tbody><tr><td>下游 HTTP、DB 这类阻塞 IO，并发量大</td><td>划算</td><td>线程数上限不再是约束，代码模型不变</td></tr><tr><td>已经在用响应式框架且压得很稳</td><td>收益有限</td><td>瓶颈本来就不在线程，多一层调度反而难观测</td></tr><tr><td>CPU 密集计算</td><td>不划算</td><td>并行度等于核数，虚拟线程只多了调度开销</td></tr><tr><td>大量 synchronized 且竞争激烈</td><td>视版本</td><td>21 上会 pin，24 以上不 pin 但仍然串行化</td></tr><tr><td>依赖 ThreadLocal 传递上下文</td><td>要改</td><td>线程可能几十万个，ThreadLocal 副本变成内存问题</td></tr></tbody></table></div><p>我的理解是：虚拟线程解决的是“线程数量上限”，不是“吞吐上限”。一旦你把线程成本降到接近零，瓶颈会立刻平移到下游那几个有固定上限的资源上——数据库连接池、HTTP 客户端连接池、限流信号量、CPU 核数。给一个 20 个连接的池配上 10 万个虚拟线程，排队的位置只是从线程池搬到了连接池，而超时语义变了，p99 往往更难看。</p><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  stateDiagram-v2    [*] --&gt; 新建    新建 --&gt; 运行中 : 启动    运行中 --&gt; 阻塞 : 发起阻塞式调用    阻塞 --&gt; 已卸载 : 让出载体线程    已卸载 --&gt; 运行中 : 数据就绪 重新挂载    运行中 --&gt; 钉住 : 进入 native 或 FFM 回调    钉住 --&gt; 运行中 : native 返回    运行中 --&gt; [*] : 任务结束  </pre></div><p>这张图想让你看见的是 pinning 的位置变了。以前“阻塞就钉住”，现在只有“进入 native 边界”才会钉住，而且它是一条更隐蔽的路径：JFR 里出现 jdk.VirtualThreadPinned 时，你该去看的是 native 调用栈，而不是去搜代码里的 synchronized 关键字。</p><h2 id="五、Loom-之后：结构化并发为什么还在第七次预览"><a href="#五、Loom-之后：结构化并发为什么还在第七次预览" class="headerlink" title="五、Loom 之后：结构化并发为什么还在第七次预览"></a>五、Loom 之后：结构化并发为什么还在第七次预览</h2><p>JDK 27 里结构化并发（JEP 533）是第七次预览。对比一下：虚拟线程 21 转正，Scoped Values 在 25 由 JEP 506 转正，而结构化并发还在预览。这个时间差很说明问题。</p><p>我的理解是：卡住的不是运行时能力，而是 API 形状和错误语义。StructuredTaskScope 要同时回答好几个没有标准答案的问题：作用域关闭时是取消还是等待、异常怎么聚合、超时归谁管、和现有的 ExecutorService、Spring 的事务上下文、MDC 怎么共存。这些问题的每一个答案都会反过来影响 API 长什么样，所以它反复预览是可以理解的。</p><p>这里我不太同意一种流行说法：“用了虚拟线程，代码自然就更结构化。”这两件事是分开的。虚拟线程给的是调度能力，结构化并发给的是生命周期约束。没有后者，你写的还是 executor.submit 加一堆 Future.get，只是这些 Future 跑在更便宜的线程上，错误处理和取消传播该丢还是丢。而且因为它在第七次预览，你在生产里用它，就要接受每半年跟着预览 API 改一次代码的代价。我的建议是：框架和中间件作者现在就该跟，业务代码先用 Java 21 就稳定存在的结构化替代方案（作用域内的 try-with-resources 加显式取消）把边界画出来，等它转正再换。</p><h2 id="六、我的判断：现在该不该升"><a href="#六、我的判断：现在该不该升" class="headerlink" title="六、我的判断：现在该不该升"></a>六、我的判断：现在该不该升</h2><p>先给结论，再给条件。</p><p>结论：2026 年下半年的生产默认版本仍然应该是 25 LTS；27 适合作为“29 LTS 的预演环境”，在非核心链路和压测环境里先跑起来。理由是 29 会把 G1 默认、紧凑对象头默认、PQC 优先这三件事一起带进 LTS，那时你没有“只改一个变量”的机会，而 27 给了你这个机会。</p><p>这个判断在什么情况下不成立：如果你的发布节奏是年更甚至更慢，团队没有余力维护第二套 JDK，那 27 对你没有收益，老老实实等 29。预演带的价值建立在“你确实会去做验证”这个前提上，不做验证的预演只是提前引入风险。</p><p>升级前必须验证的东西：</p><div class="table-container"><table><thead><tr><th>验证项</th><th>怎么验</th><th>为什么是它</th></tr></thead><tbody><tr><td>字节码与依赖</td><td>用 27 的 javac 编译、用 27 跑 25 的产物，双向各跑一遍测试</td><td>27 没有移除类的大动作，但依赖里的字节码生成器常跟不上</td></tr><tr><td>加密库与 TLS 观测</td><td>打印默认组列表，抓包确认协商组，压测 ClientHello 体积</td><td>后量子默认开启是“静默生效”的典型</td></tr><tr><td>GraalVM / native image</td><td>确认所用发行版是否已支持 27</td><td>该项本文未能核实，标记为待核</td></tr><tr><td>GC 行为</td><td>小堆、小容器场景重新量启动时间与内存占用</td><td>JEP 523 让原本选 Serial 的场景改用 G1</td></tr><tr><td>对象布局假设</td><td>重跑 JOL 或依赖 Unsafe 偏移量的代码，检查 heap dump 工具</td><td>JEP 534 让紧凑对象头默认开启，字段偏移会变</td></tr><tr><td>诊断参数</td><td>逐个检查 -XX 与系统属性是否还有效</td><td>前面 jdk.tracePinnedThreads 的坑就是例子</td></tr></tbody></table></div><p>关于紧凑对象头，我多说一句：它的收益是实打实的堆占用下降，但代价是“你过去的性能观测口径全部变了”。用 JOL 量出来的对象大小、基于字段偏移写的序列化、把对象头当 12 字节来算的容量规划，全都要重算。这类变化不会让测试失败，只会让你的监控图表悄悄变形——我认为这是 27 最容易被忽略的部分，比 PQC 更需要提前打招呼。</p><h2 id="七、今天就能做的六件事"><a href="#七、今天就能做的六件事" class="headerlink" title="七、今天就能做的六件事"></a>七、今天就能做的六件事</h2><ol><li>装一个 JDK 27，先只做一件事：跑上面那段 TlsGroupProbe，确认默认组列表第一位是不是 X25519MLKEM768。</li><li>在预发环境开一次 JFR，重点看 jdk.VirtualThreadPinned 事件还有没有，以及 reason 字段指向哪里。</li><li>用 openssl s_client 带上混合组，对你们最关键的外部依赖做一次探测，记下哪些对端会回落。</li><li>盘点证书与 TLS 终止点数量，产出一张“我控制不了几跳”的表，这决定 PQC 的排期。</li><li>在 CI 里加一个 25 与 27 双 JDK 的矩阵构建，先不求通过，只求把失败清单攒出来。</li><li>把团队里所有“诊断类”启动参数列一份清单，逐个标注在 27 上是否仍然生效。</li></ol><p>最后回到标题。27 不是 LTS，这不影响它值得被认真对待；真正会伤人的不是版本号写错，而是把“默认值变更”当成“没有变更”。后量子密码、G1、紧凑对象头，这三件事的共同点就是：你什么都不做，它们也会生效。</p>]]></content>
    
    
    <summary type="html">JDK 27 于 2026-09-15 GA，但按 Oracle 路线图它不是 LTS。本文核对 JEP 527、523、534 的真实边界，讲清后量子密码与虚拟线程的落地条件，并给出升级前的验证清单。</summary>
    
    
    
    <category term="技术笔记" scheme="https://u7u7.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="Java" scheme="https://u7u7.top/tags/Java/"/>
    
    <category term="JVM" scheme="https://u7u7.top/tags/JVM/"/>
    
    <category term="JDK27" scheme="https://u7u7.top/tags/JDK27/"/>
    
    <category term="后量子密码" scheme="https://u7u7.top/tags/%E5%90%8E%E9%87%8F%E5%AD%90%E5%AF%86%E7%A0%81/"/>
    
  </entry>
  
  <entry>
    <title>2026年10月工作日报</title>
    <link href="https://u7u7.top/posts/work-daily-2026-10.html"/>
    <id>https://u7u7.top/posts/work-daily-2026-10.html</id>
    <published>2026-09-30T16:00:00.000Z</published>
    <updated>2026-10-16T04:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<div class="hbe hbe-container" id="hexo-blog-encrypt" data-wpm="Oh, this is an invalid password. Check and try again, please." data-whm="OOPS, these decrypted content may changed, but you can still have a look.">  <script id="hbeData" type="hbeData" data-hmacdigest="217efe4f829defdf7f6bb72dc247fc1c4e0dcb1650a85a5353bd236117eed26f">f5427b49d3a78a12228f664a63b48c39ca7c8b12486d5751977381149de67aca0d2417ee618923699d657444c911f40d0e3f73f35240a9a2833bff624688624b137900261467d4d7554a454d5417335bcceb15197dbeb7400db734463cf66a59c0aa9f87a34079d6e0f73e31a3ddc83436b3e7e499c2afd7307d5e6fc4beb774093d2db6815a1b0aa31e0db6dc1a106cdfee53b024528de0b36100612efd0ba0fbd984347b919fc513b1c1381233eff3383fa817d432d6a403091b29e905c8e5d8017e95cecbde86eba6e65867bc88a61aadfdbf9c2660171a6eada0eef21d24151fc1fa65e1d4338cfde637bd94bba37b9d7b78df65ecd62eca4de4896703fd93deaee7dcf6c47a88afcbb3651e6dcaae318372e77722341abfcddb9a2825f8717e09b67cba7e5246fac61fc8edd8e3d5f98ed8203a8a1caa94fb29f0429d4c1402c8c49fca840d27e1fdf528c96fe91490ed344d281b7aa537b42d9b6e64e465b17151724f3d3c1edd44c2ef135273bb4a2b9a498432bbd16ed33a4e470230581fd2c65508b95b693f60cace098cfc7131b0db086ca2eb57e8c218acfcb815c3b11738cdfebd223ca0e365487fa3492b6df2391169e7fac548b4623962600f0afd01216a537402f6566228a7725f59c0a5e4e342a2be1aca17a8eac029b730c25a461a93fbc0aae2a32a647c233056fc07b040990c5d957f74d619d4fea8d0d440d8aef2c857ff34fbf22f8d80ef484aa7420ad756a7e3d4e84709a6f34debfaea3744174441ce3a6fd7ae302885693bf29c315f1f1976f63a654ccce6d893facbc12b86a04269d8e7fb137b5c917527de881d48ffb7925d0c47f1b63f56d1009287ef68b0bf44b000de1e9aa08adfca807f3056d902ef39de6c30acd83eeecf1e504a50dc48987bbd069e1e032b713d2bf67ecca4d4292880c8dc7f8d56e998dc426bad3778089a1cd4ad4502e0efd6b2ffbdeaad06fe3a78d6d316b1faafe6e3a72335c026255a556081cd88b4ad751e394b56a6071400f9471a669e344732b7e77510a01b411b3ea027c647f78564900119d18cf827b9026af824c42bda4effa8ae12fdee5917ab42e1b11be39d36aa816e8f3f68740d21445ac1fb44556ffa20ade1041b69eb237c26c32207c451b5c3b6b3a6a03ca36c16f7b91b7e6ce652943cfe65895db66935e0ba7641e920d0117c38de57e789e820a9f5d1c3704bdc5fac4bf4431e4bc1fa234e7ea64b75ede23438ec4738faf5abdd86d085814904e637a4c7293b8d8be0288070658fa2c0918571a8eb9788789d8308e0e7216372e521461b20719b91499fe34d3748bc2c8060e9964b3de241878dece17c8c94ff225fbe2c8c02280543f1f4a491759aa84df2338dcdac2250f2c2a0b3e889b4af8b53c0471be3a77e44e012de3abe3377fb3f35a6dc134130ee10b1b594b06c41d486e03e951f109f6868efadbd6950b6eec607fd8cbb12ca0147cff0633ddddf6e59d009b8c8789d6b116ef9c6e8d7db4643e4f31c33c8dc22122e12c0402d1fe99b4e5b56be34a8967b5bb9802467a8761d0bcb66a93c950059e905d2eed36945358678f39d80d161f45c1be4d2e36aafb918b2f7e4efde0ba2d633e2c335ceca3950b13acf8b366c890d69daa3e088e52ef517a1777c611f4743edcbc51273bd645d46cd9b9cdae8c0d95821125abe7fac5a266a701582c30779f76551108fb464ce290746c7d5f085c2e6a0c705988173eb719497136987bfc271eca471abb4fc5124d4e9cefaf296057cfa152a310239bed03017b35c84e52df3c3d6b0ef7bd811fc43f1d53d8e95748d77d97a847d697727b4dce125074039511f01a3901233ec7aed9e22fd5bc3be30eafff761ef567abf6c5d88c9beb5b3239517e4e0e6a16b3bb96e9e34ea78ca7528bbc8e7985a71506cd8adb4efb81324514fcf309f9fb495db09b8e625486bab051c9b09f4465fe530d806d7e1c7795f741b1a77fa9f5f5e4dd79b533740467d0d79c68ffcb826c304197fb836f7336a9d7bc65efb3e1d86aae23f49a8164ef095010757668fdc690ea00151ef362e779ade2887855bf7acd3fa52f8a0e52b64e8fa856aef1fe1dac734b677eae2ae0f87c2e4aa39d03bd4c6cb82717e88605e02c22d893592c696c924d9a1e9f1d499ae3d3f5f3575db0802c52c43b04b63f2aa316eb780ce188d77c0eede03379b7301aa75b757dc6fccef73ef1a8b03b1440e225207ef95ecb017e69d31363dc59d58d5b237083e842821759c2052bd3ff6af2b72fe0568dc18121c9c4e718951e0821395eebe747993240c7a5c9564f84d91d8baa596916192142ea5a0f2676097923e7361e92153eed2d9fda251d2b7dd656c45a766c2be09b382fdbc12739d949d7efbd45258e2bd4dbdda05f1589d0e02322cf3fe0ad66f5e1fc582830ac0aeff555415aa5449c2152e269451e3129d177b4afbfff7012e054af6e10713c55e1aa3dda02d29c2029986f147e084b4621ac575b2efcd4af2b2aa8302542d9d2759d274cd2b1f35e45a88673cd66eadb395c15239def3db8b39d207a3579d3f38c83608830c8c9fbf9e156ee31493343cf24ad70c8f710590aa1419f93042e6839940389c8576398ca01fb64354e0da18dcc1f199912b8923308f02231018b88262a312f02cde48cf302854f413f32446c0b74815a68d46a76fa6acea0416d34f15dbccd116e65ae74e5348200a0f580f54bd84539515d89e9b3227c48f5e8f13d9ddf3665bfe373a62b99256b6363ae48bca09d672148f2448ec188d99734b270ed04408295dcc5a8e046f068552f7d409d0ad248bc67b6df58502b2fa80b186ad7e1c3aee912266cffc34183807cd2fb3d7f850c6fe9eb21a49d0ecb4efa244a5a096e744c957761b7b53a43a102e121e33966a7806463c9d46bec923579aa0d8ceb351d1597922e52bb07e9c176e699404107531fa3997ee53582b4a78a24d2389552066e2e36c0f71bf6cd4c1142835e44a4f6d7cfff0c1a35c4eeb63ec0680207834bb10c6edab35c57e6e8ed85176a0e814d6daf8ed360a2cdef3033721bfc3497cf2fb496054600be978ebe87f8183125dd94bd18083e38fc39466b82bf8a625c2c4349128c532f45b717912b45d6c3d5bc63624caaba88d78425e14e4fb56a20d74085842a9922065d5327311c487ff3d520bb8515e621e29460b958e037a1959ef0b64db81f3b6e0c6df84fa860b2e6148f8a7f6f8127bbc3e60b0a35453cec0bc1c29f8b9fc753cd616567c79bb34f155d02936e20b87046a15e7f9a4a1a498a045de0cc442f6a41f3181430b717b9b811a0dd244835fd88d3a02d3b28f5a183bb2f450d177c03026b05ded81d7f41c54a1eedc4b0039b528b28f2ebf21592c16c4cdf9af978487b13d96ae6f76ca0037ac441952dfc2e5098dbd4b05e7a369cdf115717c4c79f02732ecfe5bc39455dee8553fdfb1f56523824332a2ad6e9083f845d1976aac7c3bb16497b894a50a756dfc513165878e36c40d548db1a96b3ac61da67b0d4f756427ca3918101d7d85ba3373546a60a0117e3b4f250aceebd72b3a5f1e73ae650087cdd736822a829fc1ce2fc0325509464e2a0eef94b766bf2d6e3c84a9b4c631958d7c4f56a49229943ffde413ee82674cbdd80ef67ce2c27dcf44ec1592a7351b7ec858338fd1a58f1ad7ffd806e5e67f385a83f80f5d4562849badd93695e51d4e81f6e9a8392bdcf0b287c9cb208aefa5be0b39c319e6f2b4be2679c193e4920674c60c4e74ab852877ffdb7bb978fe59f2eb8e735338b7f849ef7ce4fad4aed16dd48910baad04df527ed13ddf798495fa9690d289acc63dc973ccd16f31e01ce5b24f5a78089ee28730931a0ba8fbc2feb9ba06eb1a68a964397790be0c54e9bc828f66305c34cbbf5b059332d5ed6d521bafc4a04f449bb53d2fe6adf429af08a43932e2fe7f60f0a90050dfb165a176f5d9ebb188dca1d5bc972b37709700ffb32970ae041236414623b1d52c1771d98d5bbda5937ab4fbf9216de0bec8fbca600a6881ce35a51107eb96e6092c416e52d68524e902f676c8295877e1ba8e5a070a2d7c3917726f1645629480304db748e3d06130a69ef907c14a1feddac639bbf57318f5539e44da138fcc2b359cf5e524c6a311525e4bf08c206b03da4a219e3916754a4fa4ce155e73cda7c3c157ef399d265c55d11d3a3d1277e7a96c31c0a7abf767fc47d7bde2fd65a6c9502ac83ab169717403a405305c79e696a769b246e58077b042fa4e98f20577c829d013be1b9ba51128d460947e3b26df37c854ba4d15ede834a3c7c960e56201b10ba66ecdff04bf6706c6130f7d7e3866dd5f7eed63c2e6d0425807fec79b555d687c1c0fa2f3993c05b2d08dd3fb9044ccb6ba275d567405d3a8a5a7ff6905809244ec8edd4404da85fe2eecd41f629f8e072dfdf33a65eecfc7b5d1f028b6d7235ff2300a1609f995dc8cdd4d04d46ff11ed57ff894ca5f3a79d672bf8cd4a0e6f4c6d89fec26f05aea934932b5590f5d17f3d59a34b75886c0cf5aa9355b00aaa38a96fc028100fe851134053736bf019caf273ff6eee38096e13382f3174a8d1ca6249b7a38848ead1ad087b4d7e2adfd84494f95baecb148a6a2e1ddcbaec80e7962e19eaf56bffd22c92c7b7f68c656ceb60bdbc5ab9c4c6d8f81788dbaced69d7b5b0000a6755a3c0084b4be8547907e201a204611bb7ad4a430178b55b2982cfd279a41dd43b00345242e3fdc02130cc49da49e2c572b8bc52c0d96bfbcdd179fa7404ae27eeed05263d288ff34be4a8dc50a116efa540c37db43086aceae345aa7c2d2b26bde8da95dc0566c824784698966eb74f71434130279cf133156bfea21acfcb69410ec82c2d50a785add6505720e7b38d49eadbbbd838e7237cd19dfa948d8467d634630918d2dd16f2a7b7603e59d60c5f4bbaeaecda5e64c6914b939e75a0522344991f4b0ad6eaba2cb821084198caa5b2433e9f8363d4fc69c85127aa09d50e6f3815c05352db74b339b7418c68ab3a87b39f0e04b092aa9a99f30a930957a2fe9241b45a966c1da7f8f3c7eec6fcec36dec6cbd2c1c8b48c557c9d25c0a0a0a73ffd0ebb764f47e3a5752d4734bcf04eceb87a32cd34cb8ba6a32af4222ea2c6e520e159e42cd74c6034a0af05d9b279a1b04b51514da2ad3f3f17899efa582dbca8f45b6f0ddb0b5d81ea8938a4a1b60a586f48b4aa7d186488db82aed203ba2b2859fb659f57661a57955896e04ab30f25e6a6a025a26a27cbb6817276ed4a3793d247070219506e1c4fddbcd8e86a2ee0a9e590e0c6c90a9ca869d68819cc5da466e10586979bac894335f52b8796f927abaa7126106e7b7c7a8cc6b1fe4d1748115a394fb87e26f37d630965262c69b9c9f82338c3e668c1c0a2838ce70caabf2b090513d6d8fc59f059a9ef1e4413ce0c737ec154b7a992d9adb860712feff01938031dbff0d0a9c8add377a151781c2ad1944b5d8a02f7643d363cadb78e8a3ddee3758147226ccac465d3af7163821925371fb13f533571c616853f4ebed6fbaa7e12dbf8ce4e73cacde8060ba0fe33aa9903ef05ddebc4ba4d033865e1f69c5c3e5ad58815f23ee2fd70005cb4de0e0c695e11dfdd3ae12cdd67bd492cf6aae5c3768a96c85a7def74b4d8d3ada5083331b58fbfd705140bb34177da9c417facd6fe2b8a728b024e309fc447b6caa843a58835e9823138401d307244b8497a76275bca841ede13ac8ec027e2c3722acc222296cf6432d3826632d928e1b2b2935f50961d7cdcb7d499bc483e8d55ebccb132cd964a58f05f917b00eb57f029bc80f73cffa65bfde6e6126410c3dbd1434e68765fdbc90af8047c0cdd0d2c19e0f5c1f1c014a9e826645b4c6f835c1c5b22ec48e326f4cf1f5edf46840b0feb12656919af35c8986f4993deda12761a1aa5c2a759fa4c640e973bd5181d97ffb49f74c5deef844c48f68b03ba2d30b76fec078d161df18d7afe5e763d6e2328a00348a1757bc6d5bf06edf05cd00ca81ce27031dd58bc438ecd910f0389dc732ff2586f019a4a995df8c1dcc233bc974f54427ec7d492a01e03d5ad704f7ca956e1fff6288163c9292eda917679ff282094f8ab133749b9255e0d23bd93a7df8a3eeaa5c91d44dcb1fa07a23b571d8c8e701e5e4dc0e7bd6b6630303a285788765cb32e2af9179e58db33e36b10ba677b930ddab153dfc5bd0bb0fd8ac23eb320982a151b25235f80905b79e9c85fccef2b63ba3773d27c7879f2aac0c551a7698e623124a22ece07f5e6a216e076a547fb1869b76168c8d6f67d603bc18989e039f9e9fb7c8f704bc720a1b6fb5b33d3d7745ee1a82d3de8f7de08f965939d8ab2bae9b63a73238ff3f03d7bd24c544b9602083c369cc9ff3e2b2d72ad5b8284a33411ade4040f41d5a49b41576691233de8882755067770e7e14fb6fde909775ce03c2aedefd626715623d35894e48f6bdbcba9f2202c5ccefd3e8acf5f06813100a34c0559691672b79fe7905ed4cc0e0d63ed8b4e06b92fb53311f8e21555389607081c91af42f55e0898adf08426b79ec04d79df5ddc9a9df260f9411598355800f89d6e801302132133ad7f1e9e9b4c3409cc5e9923bb257ea02ab275166812b48ff86ede5581c4960538d11935a4f6f91ab8f2acb34d04f1ee01adb6b2842da25fc1452e410850b27f92f4d1db2478a30f23f2adcb5b1e21257fd6150e13b799cf3e5b1790db55d787aa4060ce6d8f30e2045045debf910870d1de13683bc10e46d87044a44f55ff649e9083e23c55d6458ff9568d89e810198d868f9c669c033214222c2cb9da6c742c8cdc2e951b85b4e35abe055c007d501738c9103be5695ac64552963f5fb43442e7ca1c694ba669e559653aef8a919a6f82b1348aa747699410cb7b7e70d6efc38ee450642bb97705c7166da59a30c665223d0322074e573e70c6c7d5428a498af16dc7e2ae83c8936dfb7a9317a44c2739b0a36813810a0802f0b23c6a87a93e6f54dc8d5472f267019de04addde679bb09551de31963df45ac80aecd54e831c153107e5ba63eadc0818b3c4dc2f4d32b3d5d6f1100faba2ab158121bffc648accb803ae350018cffed61d4da48bece09eb49abfa59078a37266671932708cfe51100a2cc271a5a71ba503da640938131845f8b26b0a46972e8e049c27a4d85082592f36f393bec5ba5ccd234ed6e628321af73c734bea4b8ebb48ced356ec58458ca6f33bfaf4d7b96cb8062fa2ef6e44d18e3a794d21cf089423d9a95ddba300c0c97ac9ff36a116d02c74500a760d0b91a7db8388a01b944774faea028c12aab7d3a9b06903af11251587b617bc68e942c3cffcdf8aa67fd92ea48f306f518dd255aa274fa3d7b21f5e93880f7d73ab0e10590be5b46e6fb6ba6113e32210f05ef2d73c3ca1922130db84b0e37f503d3e93e6abc5c1e6680ebfe55d877d098c65de6eb1c3eb17cc67249ccac91a449814423e3b85cad67f96ddd1e8dcac85453698c864903176dadd028fc946b549a464ffa9bf26ce567caf6f19c789be0668ac12e3a452cb61b63056b32eca0c78fe3c544d09a6148a1502b3208e37d454c1357429f5f08f14632d95a1a13d1b6194d7f0d3532f3b4378cdd27a8517a0b5c5ab93fc802353a9f290c5a0edc9dac8b1f8c6539b46462ca3cc9ff3d7ac06fb819dad829f3489c29cf93b46475f4f0fc634e2e93b06b2261ce3bc17e1cd2984f5de6a50d1b7a385877dd5969e17fd5298dd29d5afdac68d0e6740a2ed84c237892bdeaabba9ed9d01101af5789f5d830d15b0a16054fb709ae930697a935f4509ba7b833681a711004d95f9ad569a96d1d3c218c865eaabe327732885c98f54a023a0d30d43edd3c67438fcc7a7b965a90b0f1091eb7aea5a211d7926f3d6ed0b9331eae27b21330e92c9a0e194f6239fe9f4f3ef2c7855a54ea52dc00bb60782b4962bae82b31dfab4dd202b7d2a5310eb9bedbd4d6c00f63dd40d86b6e097720520b466d8e721c1d98556a6ff4a63a6df0b0b74f84cfbe3a08b299d4949170c2c23cc76678a6188788703cac73ce2fc50a0c3c2c877992ac7b52c6b16f153a49749fba20dfed73c9dc215b13e5d330a6b78351b38de8e8a40332a5c2904b64aa18cb4ee8116c0e36081c036447b3a7daba92f59ae88552c030eb93db161f100383255b3b1a2035a36aff32aea2c4f29e453cdc3fa510cb6b89bca85268f99bf1f28255b78c1374c89b0f36f6b38330ade8b36670d2f933592f1ea7897b7b4ca253b0775e70b4f52ec76e3fa2d091447d63e39f77b9dd12c9e35de40804ae9b25b00810f689209501eedf3876ae00f1838009a00c3e96f82495219fd20bdf03e9f69a81da98de8a4116243ece4d8588007d0ffa6e4d34d5ac5ad94c9c8a0612180124a0702f26c705c617ff79e13fc5feea6cadf14291cc5969403d927900e57e9c20c3a06a6b6e6217d539e59cedf409f23aa500e52a91d92e735fe8db7f5f91e142239e4a1ad4d16e2fbaaf30a574c1ab3e42e12af68dffd315900ff66b7ac7d41c461f35e4fb49dffdb5113a873b75201d3f439d5741e22015d853939898823cdad2952bb0a8b62b81a84ba3c44a730ec817857ebe0587a984642abe57c8d78323fcfa33e347ec2d46f08b18c974fff193cd416b3b0e3ea3c72281b2701994da8b46a2cf7a7283dec20ca452d016665b54a171b188ea895765aa0201de9f49fd01a07ce865245b6d84e539db37fa39e73d2da5077f7b90aed48eb8e54b630a6081f760acfdaac945caa22bbc3e18149aee3616f503bc7b02314e6f2627f096610e08c3126a1609dabf9a239e8f55f351f335bc026895cb8cdb367e20668d966592da4ff9e6a340e0878f0df8fdbd6d1707e68c5cc2d050d28135525fcc88302708f219189088939ecdc10d1385fe6e4cb29b5925c439159edc89581991cc4ac660ce5e5d51b6d714a52e8a6f39947cb2cfb49d51ce48789b4055d3f95d54d3040040590a04f3d00990e65a198497d683630b3c3665af1f8958a22324d0a79715c4b6b8967ea38d40c8b2f4e86c70a91851d24515de773aa3c72ceb8d64fcc34176eb16168dc3cdbd256beae9a8cf8194f2bb727c32beff56a9c6159e48a14c49089c50ab16cdf6b34818c7f50d4e54c1e385eca8d62033f963e246c70dfc6cdfcbbebe15e49bc32ff41674a7ae280b70f023580c64ad0592e02c2fadf89e320ea5173e8f9ad9e1896aadc44e0a4e28433fde0e9f39e35c07deebcf5c9d10a3be6f185ef89862cdf09447b9585696859f27fb25619d05bd6203af796527bf1e37043fd95be2f405381ab3e820519c195db504a58a35a1b0e3f3971d61f0fed290f998a05acd90a80ada9c0fcca258b5897695c356cd59508d508a8dfe310c97e4a8e25da26ed83c6193e2d02bd797b38e2f6e206aa2cb5b1bb6dd50032af1f487798dc537bbcdf7be31706d8ff1b0c669ab4663a6b5d061f8f60b662a5b78dc6ce7fd25a59433c7838a0e86aa8c93c11afc9ccef391bca8b1599cc4287103b303ec7fbd4e1274e84e29041aac2b776c9231a94dde9f9eb0c1e29dae4a58f21a37d4bd62161d19a274d48d89af52d632aa8069a7e2241205ff39bfa59ba5a64eae7d9f2081d431981f012dc63dc073057e367d0b3e39ce2008d4246dead9d833a895536e042c0eeb63f543d4b6011eaeb0c6a3c32b90fc5c5414d35aeb47a60db1f7c74b1f249d1bb76dcc388e6115d40043dd0e0d4b808e26ee9277724395dab04cef6e849a2db7e66776014964c32302a5650d4698f005b3ff492738b9e4957b41515f46266f4281e6d330e983638a765c8d95af2b12a0fbe284946692addaae1bbad8d8c02af040c1cdab440192ec92edc309e8b43206d422cdcb701b4e590388f5c1860dbe75562254375ad715c1be08fd3dacfac664e586fe980ee4d4b82cb1ed50cd5ec42b152d56ac54d101e7981a31099f4ca50f7a4293b7f83c3aafd70f81dc0f6524c7f730baea8fee02c839c910cb024e9b0a21c394f71673e30feb15f3239bfdee3dad5593f717c4036cf7c248655118c51599d90656bd0d34a903151a503718ad3a932b176f179c745e7ad38dc3ac49d6dcd1b504c252d47573ad3c206241836ae7450803095a444e58859ff45f2de9c896d6a8d2a01853bcc20abd7f5f8344f3e11273b7f96fb124ff86100bb861791ab49f4a99000a5b240bd214282bc6d5ca9253c93acc780b6ca827145107e1ce924b4a77b4b5d8c18dcbbb8e5bd61bcc83537194c1095f7dcf9a64ff0492d5889de6e43a3998bac5cfef8a235766b641b9c6894548ecd7a375d49ba29248be663fa05fd0438dcfee6a169a4571170aa3245ba57af8059d1d5e2fad886cfd3b609f3fcde720545cf60b9015ea09d20cd95932285b0687031ecae663f4115ae4e16adabdb16cf42ad8d63096df4cc5122504469fba85fa4ca7549b960baa5d2aa3fda453f7f8074c9cca022013c36764e887505d5393f6fc207b0e46f79e5b0556aa70919cf46da6ea3e4981993f824973ab89cc7f5113b3a1a2ab41d3e8065672fb9bb9e34299fc1d93fe2148df98dc034c959e94e7fd1de4ee358fe2d2c712a97d153f04e5e95cc3639480cbaf7c9a05578bda693d03b14f8c0b648460d185d774b0e5cbdca00df2f0eda3492f7512354c11b127904ba2e1e149efba911e8293771ec023b69e4755482d32362aa35810c6fd212bf9dddd6bf4626b6149befcc7d58274184533592a725b15fbcea234556d2abd6bc882c5dc490c6fa57798264962e1601402893c1db5bddcae0f9ecfdde5fcfaf277a9d5ca70eadce1792fda0415a44d0facda53e0c04b73eb4b22ee5028a5fa37494abc6f362c4cb64a4645c7f4a4d32fa14dc40f1752565c2f8e80635aac018be42d01d6fa6a81d2fa817f044e53f583532b2a39cf0a5417e17fc173a04117eef593af93792e21a7d15f5d9a853fbac5633f5e2fbc0589258707e305200c553c1d33b002bbd718ef10f8fbc855448d0537a1ba9d7f5818843050d97c32af1c3b57af3b2e79aa2aa3cd03b8a4c69321718d123250f8171c7a70a3677c8791e03cb1ad7d34ec6fc9452054285af22d68f81e0300410afe9d18f7fc2e099443387728afb9e314287bfeab8a6ed2345a40abc849fe17ca0644bdc4297f3211e6b10232cbb553f6dffcacb2d5bf7ac21b11814408019a93e337f4e06cbf0ac621cd5072f6d16ca56bf721864a3a6a81704daa0cecf176ec451b904ae1d51da78bef380ed58cb4b7e4d532b35cabff9c8af64ec811603cf7d7c29177492cba4887b7e73a114edfdbf11b68d04ceb5a055f7dd83136c0326d3e59af3218586847eb73cf4d38038be815d46ba5c2cd43f9eee57f9714904e8c8f17e6d4b6a6bb1ce30d831264b7294e77d293826af28e80508f47448e5f7022312d24589b00c9c2099c208f493dc6e20b5ad4e10aae8411b4d72c09fa45a9be479853acbcb65e37f8d07e11479b4fb7255b56353f898e2150bb434b6e00821554dd6792f2accb91446fde8fbad05b388f7894b147c27285fbaa8ed6d6d5c6ba172c783a55c1941c1e86cfacec13aa09f7ef931a86b236e6c2b6a22dfc7d408c95e0f49edc204bfcd195f5ad10f9810ade7858a4bf9a508f000179bd7a7f921f3a9faa4787673b7790e193a257c9b76e072a2a2864d6b11a64114569ab011c049b6606185ef6542035ff6c6d3c6d34d88d911b124da035199da5578a9723019ca3e8fdcabc0d1aab425d74ac17c2ab987200e286c5926e9e1213d8f91709012d276b2d8e5b7a717dc6a8aa0baddb7b7de62eaee98f488efa097da16f4b9d07ee153f3a1649faecc0486371815044804119ea908fd643ca7a686c8ee3a16b97d0b3072f757399e66f3ec548f400108fa4facde696083c4037724de6d036ef078a41e538dc398e48ced5edd74c13a9b782b06d07709578c903ae311543c5d34ca421b4430caa3f7e3b764fe2709c08321843383152295ffaa1312413fd0046bd93fa7dc921726c226ed9deaedf11f4329b92ef5ef6f66de9dd2c6775404fd0d2ec3036aa5075a054a3516f66ecf72e6a6ee135c2f950c2558025723a56e58259f6020bdaff292726ff0651d9a12dae616e10eb67c9f4d033cf087a8a2a7384a0d6320ea5b9c4bcf2af0f5550e967909b3508b67522339c16d75f8d132f2b32c384fec43f9ad96017cb96eaa5c000d691f321ea6ac1818cbc8f257666b0bac9014c2c0f82e6c4cab1a9a4f97f3de1f786754a24c1a0540daa676e44f09e182bbf76f657b6472a140ca6e33640cc74b40572b00c2cfe0ef1e68dc7f7f8f9f07e24b5309041919ee1dc5f632d7681790072b8eeefc238bda00f4496708c7165fefda8b0448c13e1700c31246099b3361377255d169ae12f49e00f47475719ca283b1210c6852a5b148ddb2122aa592917d243547178bbd7efd8d2867c2618761786390b8ae83e7489aa4d7906a5734d508f4cfca39b7a8f735f8a71589a2d34efa7c477dba3c370171d882ddf54bce85330d0b630c5f49b8970967c7127939978aad6d9596914f4fa1df97bfc0e7ee78feeaf0e762af9a2767fc7ee77a15dbddb3bb2dc19d9c037c701a1fcf61318aa5c5ad598263929b886373663f325009d6d03ede304a646493357ccaf020900e9a33ffe96daaaeb8f61ff5c94b364e8ef5dce0311ee4d823e9b72b4c111ff01e84b8715f59b4c4faaa0ac75daa463f033c7c4eecfa1a27d7d4e3139d998d7af3f565a0c285d40046f7a3a4a2001ded1a4f02a00ad78ee0a39d2f088f6ae6188c33fb501a5e7206414743d1fe7f27fe791306172859f26a21a0e5ff891d87aa28205077c034c4d8de27c4099f239e0b7c31f63aaa43be799034b315c48fcba7401fa53f695fa3e9cad2aab39ae5d7c47c7c04871fb19d7760f435e11920dd2097577a1007ad21ba028104a71913b20eb975a8ec2878ae20a42c0cc980f99fec733b4398a8b553836b04f6c36cf57626dc4a1ee684b4b7117c3a0a421e2b93ecdf565a9f7da573ef594f014f98c54ba1395b37d07967fee65e687733daac6a27af52a925c7cbc2a349873a6f1f9726aaa046715e4da8d69b8daac3c7581515bff048780e1025c2427b06cfece3621857cf007abf601e2bf2a43d86c6b5a98492f7ad93aa84b1a35c44432ba0ea5be522a11d3d1f48ffbdf85fcc165de8fc98005ee28a3a3a002d55e9a17e9be1f26cb4606ebde621b4bc67848210fa4cb314a658f5a7bc545401cabe63a9453aedc2ef86713ce2ec129b129722653c312006239cc97979b7c5efef59f08b8b704967de7fd2747dd94b2b050735c82e47739be4277d617d191ad477993d1d5cd0c1904e10105c277348f1d2c05c653f11920e75e0dbb1067b360a47e67e29a18c39519fc8fb0e3d001ec020d86081ce742aec3cdc0f6a4b11dd4c23f6dadc3c649f9ed692d1c6e23fd69185b7ecd1580878e514ce9a947c0399499829c869d9c17a196dce7cdaa82a9a8aff191300554cf6d7ad3cb6257d14dc714cf3000ed487967189f91c127cd6fdc3a1e1573e86ba2d9a37a466b636f4247fb51ee763c9e2eecf3bb068d8a7495654aa200a06740c05beec240c19046b6ba6f35f8dbde30217cdd1884a2657de606bd43eda9f7f4bfd2319abb94935f4b2e4bd339e735c585a9f4269b337a66bfd3761cfe48d7ee521a42bc74d1d999bf4fd6804847db69b8d9c2e3ff1a3424d322128761583bae70c0ba937e4e8b236eda66bdde539275b47ae49071329a76b29d97e9bc470a5cc5bb9bdf59b792625856f5c3d58f977edf1b294602a5d26be8a229678a1f4cf443d77f654d0aeafe1c2e8f1a77b7faa41dfa36226a45a5ecc407c9ee79791506d90e0edcdd66ba0c8f7ef3a97e21d65f7aec96e7bb907a65e5a8e78bbf4aac522c40839cc70604f43c74d923fc4555814eb966f48f9d0e851181e2a6c0386631bdd4c923f7c76c46359ae925d534bfae5d0454c21f3102a36f9cf84b6c297ef316561a815823479c2bb40fed9fea31ec84c92bdba93ed94a407acadd5458c8e0deb2dc287df855d5d07356813e09cdb564a6bc6ad2546347bb86440018bbb8428a9f81933abf6b2d375505f003b5ba68896b9790a9144991e6386f67b74546923a97d1de445ae8b006f57860ddb51362f6685db99b891be8ad56c6183a235c9e0e76f814350705fba2993a90fcb958773b7da08ef1be52a6675076a54a074b54c6b8dda27726d8a94b122514a09b90ae292053afccfc96bf20f913d3c20d9c1f54ab63d0838b65adcba06d8e490acaa256592d6f187179070faee9ed49c242691cbbfba3f79b07fe54a56040f5b43a2faea9276ecc97734dd966173fe3012dccad2b73f6066557d33bc66b77ae9edb21acee33e7c5b50e85bea21edee8826272b928c7e7eb567a0b31a054b989e9952166a5bb646bbd984c583fe2dc170d450c1fbb1ce6cc0bc6bfea516b16704f355b6f9bd2eb73104ab1e45f099f73128a5a12c5baf00073f819319db4c7e16874cbb7cc4c287e998fce657fc4c28065e08689521f1dc0500e9276743fe54eecdb0d2d08ed7efa65c8b03399922ccd477b9e547c3a3e39323cf7366c1adc2533affae82b312493450d775b8e66cbe73aa03e283192355baa3796f5d6b791c4191e122ce0b2294f418df5c12526c10fe9afc569bbabb03e49ce43c0f2edb4a5def6445fca53a6fe822c810515dd6ae1627a7c1e3a26f5f95243299202572ef9ce353482b911d7b771fe990780757bf17f1dfaa8675c496b418b75d64cbc28e84225217454b2d0e9094200ee7c9c7210e44c98fe0c111e6796712e73056e44a0af5201b79dc56d062e2dc65e0c0f6d5b2db04fcdc2f24d0793503ca6aec56c160e27ff5c81674f7da34f595cbba003be9c29c959b53145e6502712aa9ae96f78dd2986f66a525bb64367b9a98128bbb55d3ad8722744a6d4172c41c167aa19a3884293520c29f2262da97f6be079ba7869e70c76b2c421eaccb0fb9a1cf183c9807e49f53547ad8b2af20c2b2af2161312dac5603344ee0a201597e725964bf95d172e49123b2482f779a20dc3062e19012115fd579eed6e5890bb63cb2a4ee19d1bebd17338545007a56a96e6fac6ec89b21eb41abf05b700aa0c2cb189fdd93e9428683098c4d8d69f0115b545a4847b984c36cce56c8e67df4e05c99c9908377e19e161acd5ddaeae72d18aec230d2b5be842080c9716391896df1f8578794baaaae9b04f4c3786d8e1e3cdfc55e9f174a34e7d193b9ece30d3189921672a715714308aec40c4bb54ff58fc2b205eaa8e8582d6d269b13ad3f88da6781f09e45c6fc71ff026e453643d7f122320e4b8ab1bbfa74270c4db31fefc2a3bd8daec1323ffb82d3f8e289af5a941b1ae96e4209c21baca6c7bc11ceb561aa33cc54d6b2b8a389b90debc748406c150102bc5e0f9c1321724f0ac50d919dbd211bb95ab74174e4b49604ce990481a0a4387c11a11100d0234a87bf191a475a104061f68e8f89afae91b6826cbd49b5b9ca5121bd00d0b3d0dc81ba79f0e9d3950b8714b61dd9ee80f59b526b24f546cc3d5a7648d4f655c4bb7fe5ae5544f2fb83979a61ecff6657b0749f3b32f35938918f05d958941af0c40484952611317e84606e003305c730574df458288f2c04f2271a0e25736db0456ae5e1f024b1690a1605ba3a804e1376f5fbf55fcf87b8b13f45c27486d7bd36a8e638ecd1af632dab531b6995bc781e15cfad87e5d7252297a3e604bf31ffaf33c2f01a746cc25f5d52c4b4612358f79d091ce54518dd845f20356fb2d56d45d30d9d454cf52460ab2ee5e5fdc8d2dc6c159f0b08372473c49229b6f7f9c007573b7774f01288334dcfc655831a1bbb323b976d781e837716a15263affca15e43f7ee2d9ee2eb39b8b3b2d497809715de99a33fc3e1e6fa5a90d1cf1239a3b23a9e3b1021a2289b74d017d10328f713b17ba694880c645680bbecf9a56d16dfd3118d16cbcbdb08c8c32a8a0d82602b1eb556073a153c90b80d6aacf66a2e0bebdc293778b58db737b8fbe43ea7ce8c5554b80ce15608882a8e585baf29c8dd2299903262a6c443e4f26bc3525ba792b77deb4197affb4667ddb2a4d51fb119e29949a073da757d22f37054ae9c7029622793fe0f3110a653afe2686bfec5ed38647eb90a431ab12db1356396ec504edae625ea7284f9132c454c1287f5ec16e5fdd659e7ab6212698c5d8a76ae547ceb7dbcce97f6a4ef3df14a0b76828e8008c02b0d57cf34d5eb703094138da75c2b609f1881b766b46f95e32e60d6b9bcf3d45c84768550689af13cd6ff213852bf946fc3115868ae29ea3336d37280d7f459ce27634c2bab354a1cb93a983b059a9b53a7cfed4cd280b5933ab84899e4000ecea96f8aecca0bfe529f2b24a28081c9172875c9baeb2bb584f13266874f895bc1304d65a2f8fe081edcce8924c91af7d3c6dbf8b58581c3e327c1177234ad6a1183eaaa5cc959e832eef4b937970ff1d903234156858217e22d852ef4f34c137796214956ffb1ab388796dbf4737ecc3fe8332dab9451357a3a39714ee0f1340a4c82c33205c161cf66fe55577eb9b813eaa4f507f1ee9c0f38351deed02e7b239effa273145f556d35af61f5493c48479ad52d61eb1b755c64413ebc6a7936e4d7000468420f96c82b92d49872a7ef6f5667950bd2ace050b14f165b362aeaab3b936895663541e42df15cdf06a5c8bedfa8d47486eac44453aac1f03ba14523e4db6dc59ff6f4030f217c4346fd7e228078bb1c765f44a58ec4a7add91a777f7f99b2516b8a86704940552c56f3b03e6f6983976568f1f5dee5f1969dc78fc88307be83c21e417dd11f8379f87654027e8397b9db63078f0cfdc1c8cbae46acec83d21b1a688b1226beb4dff8e1125bbefbad1d4e00ef520d3f2ebcfa23487a36b23d045c49510d9ec55ccecaba0aae49dd70cf1dd5dddd6871500d93e006cb313a1a4f7541b2823a0a635928bfcf5aedbc93c414cec6087bd9925a8ddb32bdea71b887b54c63ce3bfc2b72003119ab42747c0d3c1cb215839e9a9f940d697cbb729b0f327ad3a464a59c706cb0f685bcb039b59c620febf2c7a04e0b6f51d5e32b08b9db59acdc769c320a2cda9b8c07a725134b0c3d689a0d18765682dfa478c92326b47f48da2aceb9f9c8232894fdf4ac16bd5b6ec3b4726e1a8bb2ecd72287369d7f8feafd19fe76fbd771bc2b4f00a68e17d7aee19282236ec51f0133932537e8caa834528cb5b9989701f2239805c2844d89711d887a534b11d65f7e577d09683e709c26cccc29aa0d6f9b7058d2ea430cae300d17a56a75111e0244d5074ba9bf3b9cafb565aae46f03024ccceaea25386978a0a7cde531929817c12c577648760da11f40699a25dfda97b871390a76c382a2814813ddd4ce83295c14580cf5950ff459a0fe8f5bc29870378207b1f4890f0f888d13a39e699c5529aafc63fe498c22e2b5d85d7e65f853c5aa9f23246c2bf41f6240cb9b58d6ed417dbe7b3b9c016f955d0371b41537052e81da361c333b39b681e91f935e90c354c8b285618d6b1ac4b8f4725ac17b7f709a1cf414199a0ea594d566faef4307151768ad407d7dd16fb8910bf64e786f50e9a2db1a4f58f112184fcbef775e4583610c54fd6214b6a2e8b080dc473f920eb69aa4ae084452b993d36cd030e0247921e3dd5fef519d917bb4d3a367b774df1fcbd8cb33b1dcfdaa2eccdceee404e14687d97598bd258323ab744525d630335ed9abb2c1feeacbfeb3399582196f40961473b410277d2b2f6e5f3653cc85873f861d7773ae168e4e3530f021bd2d0ea6597cce7197bbb185d0f472953adfd57575db560ae5213ad721584536add7a400b6501f3560e04c8682828382e858774b66faded8ddeef30551b65771006897d517f95ffb3cc40b1c358c0db7e75005c06f57df76210c85d31132321153a129fb8200364b9b4284fcb3f5770979867f3096306fdd876ebc2e32f4e5941aa175d04fc92c7fa8f421245c73204f43325934da29a911a1a20df499fbfc2a0a9a2f7eb4e37b565ce83ff3bde50c2722b9dda66392f89661289380326ae1df5530522ca86a8c8cf693253b5c86738dd627b78701f278ba43329a76e72332e81b0e748e5d80037c78f8eb56df8c3ab9501548bc59bc52af282f91395ebe6ced680c80c9c18fdc307b221696531fc64a45cdeea9d2c499c06ebbaa7c206cfe6864518e65fb5e5a251156fd0c6f10c67b35a133eadec087440bb434270802d94d0cbc1ac15335185d13b80dd2aa13faa77f3953c64589e954417bed3837fbe05f76a4eee85b7bfb468f430c4e218b89891d73e40206367c05043367514387337954c48ac632e3d67b38c9c766454b47b9ff04dd00cf6abd2e0655eab3d6e76c7205de082c720aaaae3904b1283855ae7cec3bcb7c7958c1f8c8bcc4ae7601b9a7e29d7da26730fa1bad8cc8ccd278603c592af9415a7388ee4a3cdb85d3d82809316d7d11a7608e3f76295ff20f607e3054e8818a93d4a1390fcd379be64a35273346071da5c9acd27760f3446dc7d354c7004d9200c8b450270a26ae37ff8c9502d95fa92574a56a94949e94990482b1e95b965a8e59852c8bc56e4e3aa70b4a6bf6857b7293344e582a55ebf1119c3e06da89be736cf8a6d065da1515b17e6af1c8bd56e6ab38e88db8e47b13444544d8e689fc0aca3bc132a6025acfddb8c0510192f3b350fb465029a4bfa522e4fdc76a07cb47fec4d9d2851f7d5ba36e156f85d5a59fd7308ff99912db4a3a79ffc0975a3e484aa4e3b00b5bf69f2f5f85834c3427ddc10781c9119b476afcf4b0647191b1c56aa328882d4be53bb31493586b0bae314702694c7a8051196a68eb0f650337030f95e7895f6c3f630e6c0a2f3260f0d464a2655d547760cb3241429c53456ac46be499e49cc564ab00abb32fa4fefc46d3d505239e71551b043cfb6a97f5e9e0f53415499e74d14b950a326ecb674c7b67682fbb99be464cde6c2f29b8cfe52b2841174c1c4144b82570a6ed8f5114ceee7d58cfbe4f4beef19bcd5a667871d216868dd66abfde03530d0aa3d6c1c9283403c38f572fea14d262dce062b63eed9c47546ca47db583f08b05b775eab3ed1ee62f5b664acda9a6b68a5b40d9260f93c569ffd58ba2a1a338a5f29341d1d9c7ac5469c7204e89f80d64e5908ac8af8b3f51405b95102309ff68f529790ff5b90af55274cc09774fa5371c9d304f802028606405b94ca9f0886813e64c2fb1f850bc355b4384e238eb8f7a64bf9feffeeb0110652dbf3c80f187885ec964fe2fe6fd42578c6b7bcdec76fc7361961f81b73cdb157c7ac417a4f83fd2e2c933b088e2f0799c12ee676b6986005970885a6c79502fbe659952e13cb211613cb2da9293ccf4bcb0fe5cc2081127c48be89af12136d493ccb4e9272479ee587e1ac9c8b66d08c7de09342c08ef61a0258d0d89c01a8a7c1be09f0fbb928532c3e66b54fea17c5aef9b90f7b1fbd1cfa4d3aedb9ae440bbf191f753b645fe0dfa0367103127b5a5ad045892ddd158acc611755fa0aaff6df4cd21cca3307895d997e032ee158e87974be6539b65051179a3b0bf9f451197e6f99017a43d2e9f40abbf74892a5e1602f52895e2935cd7cd023507ee77da83751a27617ff80b58ef060865b9d2d7f45bdc78ad11f4ad0c4262fcd0a8f7126ff72aa85439fcd322ad374ece1702e8095c00e10c0782f2581931dcbff09998cfc9c2c7f5b8e4bc35bfc9631acb3b97f071b045615fb99f58bf365fd27107f4c8c4e5c7e58883ea5a3a596fd3ac100c1b665ada36f8180cd87cbb471940a007934002b1ba1bb318bde1c44c5fe43e01ffab450dedd02fd0bfa9265b93d5f18c5e13bce4c7f1d9631e86784fb47631f49441c249e0e8baf5830ea148617589cf8e0f9160db6affffa9de7cd1bd10b0f496bf4465e09bf0efa62eb115337d633396035be396ae4eb33c78d867661517ca955d1f5c2ad117333c36eace77e7463c3b61de41363ea74a0330873e978e5e6f406b709d42a5ca14417e34ff12480c438043234c9fb3dd59e969ca73fc0f48a159865729e810874da33f126a2dd7d99cd56c25d80d6456090cc8b68563f5c7c24d31a0f902184f766b176e3433556e2c7d8eda5cb453ca684847e47a81f2e930375a69587e76e7049463116765767eda5ca8e146eb8650005864d46fee216894626d6a7222d9f5fa584f91c229c2506971351f87147bf2958790dee1e6b641e8278ad61f4ae94c017a2e5eb3bd539d4efe8683fad19c9d4f0bc1ba3412b1fbe2056d374ba21a465aae91c7334fa285c1af80f676020eac2b437827ab71015373a3a7ee5a96f7a225669402b47c76e7a18119ecaebf4121e0086d14e9b2460982316966cd699504b4cdd421a02c58eb5921c882df64a26b72b01c16586d14e2ec32cc3adbad74255795fa6008385ddced4689c2ae9035e6066f825dce9237a5c28961b6fba442f1ba0ce5fcb782823fcccf2be842e2085000aac9086b2d05ffd0bcb59f6e3094f4428f5b60198bbb0e25edc098d378fe79a9220915d7e85515c8fa6a107cf7df1b83412e7496144d753a8115fc083417ed76fe7e16168ba3c513df3f77cbc018f866277d99c400970c8e1239fdb2b8eea1e1ceca1318c70c4817981113b4929067763ba2c687d2304316e72851374c2a39e1762d9b8e4f2237fdead7ce1d50ae797ee267ab4dab5c442f2731f9d7ba030f42d7396bb0657897372dbf6c0db57fa7e64f1bf831eaa0fa0418260324e6b627bf2efdb3b6893eac8a07cca6f57caa095ce01ba7cef42b16a3d0e08290ad684b3a45ec47902227ca8aacf40783b8d979763ed013cabf8be02c4c5d9596783de7238b6f6de97ed4d2856c36309308636bbf53b82de775be211b7ca90a3a4705b6e3b72e93b945a5e3ae4423fcca60cecb04f832bb55e9a126038955c884ef086192e2948cc55ff02ccbcc123c61905786ad2339c5c2f5b5fa8018018a066fb5817c34d528e5e912f0b8c6d5fd13d90869df8f6e9be2e897b7b8e01d628f281fbe45d6fc039bf85d49bc62a6c8ad56263d4141103de41788f24719bde3d5c9b3213da10b257d185ca182605bcfacd299693ca41e3691e39c6aab55d2e4eef46f4d0e88d197eb186cd9b6101697452e1b62b100c6734b8eedc68bb615f5af10d09f5a1c4f3346f3a55142e3681ef5793449d9dc04f0a11f70e5d31e3417771444ff2fcdd9067c0bdc7e04d78107d3ba94df4e496d41e821619a20a917ccd7f17e8ef06ba519541df41b06896284dbb44aa71164e12680102eda3f96a9c94533f7b941877461d89f8ef0615a3092aafe4c1916f4411010512968ceb4556efee5a11f810d41a5e7f34316adf9219ccc7b0bf37456f4b3cdd6fac01c1921a2cd09679c94f617be5dcac4bd2ede8bd71307676c0862d0e984c2e27282bc6842896cca09d9a16d143c494c9e7c2a62ad25074330440f5627d9e68ec3ad86e61e3ff17f3d15d29007ad96f920d2f698fbb4d37ff67bd0ecaae15468ff245fbd99f108dd71f1a0134c9e5f97799e3f14e4a0fc4d97b3cce9113875d237ed9278ca42864fd78738bc59d0a3144930400d1536e92f5bf420ff1df3ccbb5bb984e3b099434d2829db9585d3b7db420bf9344b83b9d428a06327dc650a37ba7bf1893989effc00067743afc9c83311fcb3233b187bb5453b7d949e7adf0a147da183c0f390ae8045d92efabbf9aa01e57ac3ce92e3bb19fdfca9ab73b56128a3fbc931b29d1df1942bb605ea7e1a627d8acd1381837f1082352461336ff914b49e2d89d0a0f9145ef21370fe1970e924e211a8e8e2c5602de8542d6e2ea090f8dfb9fe9f79ebd189e624ca853015bad7e98b7bd3172d23698069d61aa3b74816d5b5c23a9375bb85a09d371fa26766b0de778bde473cf943511779ff18f20a047b34f40574b3fbb48ea41b383caa886d632b99284f8ba95a797000d59c473e30baea0450f6f4255556a2c8f32f514bfb12de83a5e7c2c61b54fe68d68417ad0c478f984871a1b8563c53de394e14f386822c4edfb6c9aa401cda5390cb9d53a2044c7512e50ece4c082bd0ebbdd6d8cf127eaae61c1248ed52cbff02689d2625c4ac3610380b9f7afa99ad5d4766d394a91227e5e04cc0dd2a0f2ced3a15e80bc4794ec0b16a719b82e9cf1192443dacf901ef214217e2d16dfa2ab31c2dd0ac07c3af8be68516965dec74dcd832482cb608c065241ad6df54b4ed49224d1522d02566fc32719c7086a16077ddb6959f76d8ed71651befb044bbd531ff72f39e6a746581cbf0fd9b958852ef76d9154805398d04bea9c9d1223a57dff7ae9165b9161ab5e51d54ffe5d5b6fa8208890e11ae7a63b93cbdc4dd91dec861e798218972f9655d159bce5922cbcff76e4d9bf53d996d39fa1634b314201813cfa31003fe48bea381ec8970b21670f3d046c7a997f72520d8bcd091bdb652fe0f3b995529055281be994480c718f65c62283faa8197c46f03052b294841f4004376fc4c0afb9a5ebfe2b6de27f149de55262412e428d60058068488477e8acd493d28f5755a27db8c5603e0e548a43d980bac1243e7b1dc1cc3fb51d44f4c079480eb5db22d6cc0ae090b9c2a5f7071fcd6fdfb097c0469d01ab65dcc9a4c72f12f06bd38ca37f089e225f7955468f6db536463fcb6e4c245fc955e1b558e8d0c3fb14679148113b7d7e9ae54d4ddba8c0d399708880ee13a533bf3a7fb53f0a9c069e4a1fa9e67128399f13bf001f0d8d00c83228d6a023ea711861e1bc98e9fd2e1c8aee92b588c631d9b11ea385ff18bff9ce2c23b1c338812dedbdb7ced3cf0caf2d38bb16e546370f9e819d3f121252b5893f113fb35e63a490ae67525d1b6403b04230ef68dae5fe1608a61eeffecb504ccb4268e1823fa3f2d993c5aed004510e52a92b4f62837e9673f70c9133c5fcb52ff5ae9e23ae75294c4b13176903ac91b22595cd1e87ae2e77d657055eb2b1a6822112ec9e4d8193dfcc25b2e9474e58125295022ba1b045ffc980710d8439e6bdd5d26d06497a62f036c9c633f7d027749995443f61a4117b1338a90b7db07a4c52be3c6affcf7eb65c25afb6622c15589ea04ea3031e918eb33cab1ff18dc75160822245415cc002cf852a07ac7d9729284a3025bfd99ee2998dc686004ad5f6fe674a24fb01489f2a3d641413821593c6afa8a82c4214864fae3d4ffe24e1be640255a2a4c1cb48205ff1920d6e01f3979bfa166df7638933842015691b18e2bab983e75edb35225770be97d122844a9b0ed4598d3548c5584959e92e6c2b0786bec7856111df99989bb3f4b57bc6c32a060fe3f158e469926131e3062ea7624b2756bcb2b8582587c21e41a1a6e7adfe07eadc1e015b421c5c5861b4771032d5a71ef27660f5616171de0c9258a9c2af6d645a7bdd16d90b8c6fe469adbc7f9367a322395c68364c9692afaa49b6e1ad9c6b8ca232fae9b607eae3418a110b9c27d57f9a6edaf987c50849bc2eb8a08615d56d9bd32be4f8e03604e7cb2629857091fae9c725823a48be3ab03dd290ec9f323b4d5ebdc28f5f8158787e7b6701d6e76552ffd462d0916015cc51a35dd482df38e1d0ea2f41f0af4ccc0d9d34df33bb1353521d7572b310edc2e590b9458258e6c8feb52d9fc2962cb69819749faf910a8a0b73e1c82b796935e546039f3186b9bfa3b0b6286d654268395d9cdd751e7d6dc59ce81516de317f2acf92b54de3b769a1b1154e5cd3246af7e3894cb16d4e3fc40a8e205df2d2e1740f805b0bb12469943358f80df9f466ecb47d98ffd888d7fdd0dd4333895bf34dbcafc8d312d46a275047c09695a94c8d8bd7b8bb9f00b3d2308f5f4a37acd034141875fa5a7fa4fa45d91aa1abf6fac1acea03d8ab9e0c59b14d2ab30e7c484bc8bddcca4b1ecc8465fcffaa2561d98ad0cf69721d804890e67528aca3ca9ab39de9934ff07b1f428b9ae3017325f049692d5b8d1fa3e5ca9b9cbdd9ba8367db14fcd0d6f6f88bb35b9f268f40e8fab10e3667e7537082d5fae7166d2e66b2fa13cad9976a34ea05ea57ba37c1059dd82d8cb4a1f60c83578a6e8d14c22804771a8eae7271ddccf1aecc0b611c67740897e8f8aa1294d810e3f3dc6f80cf7a2ddb0ec0c2568826109dc4f504073f2a0736b566be6ec8743a8bbcbe6a2f8b60b2d6b3984b8143047364fc64a8631ba8b520f805402e19ec2d0e97626481d7c4b807b5d4bfc4a15222c9831d3adde33762adf98320f8caf557dd44cbd4289645cd2ca048c0e0771d78048db6fd68fcc24dffef1215b210a0f0cfe77129443d5121687200205d740cb1181c431cccc3d9ccaed0f683d3f2721fe90e00766c48f876faed95dc5202af075b3e8e0b07b0b6abe9c8002033c5f31638e08854c73077bf39e964f7a1afb1add8eaf35222104d73efaa98adc7cd576d56965044f47632765786169946759a8c9021dedbbbe34ad039d77496674b0adcb8050a381b92eb09b6939f6698ee723914e3c7c99b366217073597c14ce8e3e908bd37d6e926732489441e990c8c5393b95973991458e7c3289d52eee7d51b1bb5a456490d6786829e49e48dd3c8c7dccc8c58591805cd132a864021acb17041c1d59a5a50dc2eb62de61c3f4c965a85faf55b6faf8a828f4f9641adf9055ae0ec7d8a662856e3e69d4469888d9869a9ca2543a35d8f941607b1e9b6812c0407a4bddfb399a09bd4bf6d6410af8f6bb6fbc396919f7d1bacc17a49eb17d5330eadd5d26066bce3cf7dd6a4e0d403c8e1d3e51331f23f331cbc8c6e7d8287b2792b8d5c269e331f894f21e6e50de4145bc4d2a18d79ab796a6be7d3989df76d9424381cd4ea6436b2ee153e33c4763fa51aa365789dd535c1d5d5614d98ac03bec2ce94a4af98fbde4592d67e9acf840520f0339c261ae107748a64c3264aa7183dab29a43f23afeb3b174995f603ecac0d370492ec102a221d897bff503ab4f38ca835ee8c30e2ff93a0f580439ae9fdfed2b45f2971f441aec2748621ae7ca0a3f43d658cc6dc521a8fa96b52aa70288cd47d0845d97525b9839d2c26e8e87a3ba01ac60f69397037fa985a900af760cc892c8b54189fc984e294d519925dfea59893ab5bdb73ba550aafb2dc2f63f58d5c57dacf70fcd0a181294053be934c5c9658e4f226e4dd8b622b9abf4560d02483dcf69253dd06165f8344345250dfdc01cdacc80c8510275132d5b42753c0ab4224c06577cff23761a809a9b15936af11a1df8a393e5d494be1ad67e8ee783bb26916117b4b9d0c1b78c2b55fea368afeb96a6399dd7819c2e6b2cf7a74f9a9a5d3e4d425467a456a4961c089d1820c3f11bd17df2777f6caaa1c1113362b0a42a2b5936f41fec8de3b344d273266dcd038a718ccc1b5c6706ce497018cfacc5f85d4196371f1dd3c2ca3ed07587969d7998a74252007626b53f62e51e4ad46ec53146e309af10df81e5369fbf7239b93d5112485a2416dc5a69acdf5227d6b3c02516a3479afa210f39a5a4b674338ab869a71dd1b8d63a9a8ed154a220ff50dfac8d22e7afcb7d3a29b848a3824c5d9908d8c13efe331bf46d5bea3269ede9bffce5884ebae062eb1113265d6558d37e1be15acfdb5627e812bd4689757298ef0ad320e52413ce5e9018efbacf8936f50cc51e41558c05243bfa6425b70efeaecd1a584776ea60ecd82d12bbfb7ab7babe98aacab3c98adf312a8ea0b2982aa64e3c4f1fbdc5b5a51ae4807b1a1fdc3dbd88470650d03fbb2a8ae66285d9c191b59ae44889378418a78d4d2ccd5fa2b04af92d0c5e6a341cbf568936c44c836eba86a49cd710da6e80e54354050b41a002205744428afa739d614e152dc715bcfabb9a52bdcda1b6ad47574fc9ecfbd83e08731b7f7a907f40aba419fcc4cbf0ae50d45b497077e2bc268f918e66035237513d4e56f5f4ec0c512cc70c8a67a708c016b705de58f05710fe08d0e544d6144035f0a22f134b40b4cdcea7a4ac3111e8a14e9cfaf3db2539c0c7c4b087e352bedff58264df3199152180788487c3cae0ddaef8a46568a57e2419f40f2f61af33bc2b2226f0ae69e1fba38b03782332bb5781f7b251ba0c18de69222e620b3f905b26d36c5445d948d7314145c0b265f8f736d5b81d58a1bda455e50baf642ec49f9dd468f192da0eb8c919b3896ecd751cf11f1943c130bd6d4e5d1333c6822d23727469773af2ad8beffd10c46b7c796048b56d9f80770d3a25684fc2b6f09afc4e65e14135ac45fe24cca311b7b7beb39fd429d8d303ea5eaa676f4ba4eee075b9384c067b792fe8f64d34b931c4460b7f7452b2905f1428da271b06b20ff9553c5c25db3c0a2728f101c330469987b3b154772f2f79e5099ab8855ff16f789289f7902926fa6f900ff9106d69ceb3c6de98580f5e495d34f08b7fd65359be94946bd4a5061461ebe797fff478bf6830ea6a5ed61a73627f3633318909898e4136925f71044655fc34b0bd2e77cf714c020e9a3fa2a33250dd5c123c943ab3cfe431ccb54d8f664b19f8a922d8fe9778352748f8fb78a1aea6ccd869dd0b073a13303af1541f9adaa6629411b2be7005f47e5e9cd0033d138cbfece4e3fc2b42e8bd0f2608bca4e0b7520a5c66eb3ae6b02c0f55d3dfff6b7cf99cf8ab7432e92608482372147ad2ac03eea835a8815630fb4ce485d0bc31349e33171f48b337266665b5051b0f11681b63939e85a8252ca8198a6f65a7277ee57db8075f55efcfef9a6f653948e10e2fe88f6bea3e59e7fed02b3e2bf680504aa677c313a6630aa7d65928afd4a6ea5ad54acab93fa738cc3b46e12a431c22f1183a2b757883ea6c360f3e24a6465e8c243cfe0ef81b8e4534f745268c9d629cb62104017a0d6494895e2af854f4883c186c1f200ba89ae6069738c2895d6cbec232fcadd4a6bf94da09fb0ba3ec6da97743776f1f6f5d5880e15d2cc8b8a93173f65b7e6fd2956948c994d9499b0ed548c890e3acab3ea351d0722d6e3a4ad7a8052a41acd17b6d2c6dd808fdb3bfbfe18ff650dcc88fad2ad7a0e569410879b410907c89f68774d479d5647fcfc63ec8204cf140f03d28dbed639bfb1d66954f8b75a72521ff0da40d73ace9cde966c8dda9de57c26894d7c709afa1353016dc0b2a2a94810e18942fd8121144efddba3618621c7f114dfa583c9baca3b30b0ebab87a9932eea7e1422f3f90c23f4d03c43b00658f588c97384214e7159b0dabbc629c53d6683dd9fa3995b78119a3db59bb3d321f68370ddd1e2757a3afd0a4f7427625ec3389a608742183b0db7425e5bede239c8fe5d57e744799495de8ad1b54d0fec9ba44dd2d1193fc104834041ba052e1d78ce49d67ce7c9eb9788a30a5cca37fc0d795f52562314eff1902b7f147a6b7a5a42c47f5ee1e1eb9f9a946e4e7d8dc689983ca4d2dccdf473e4915575cd0c33622977d67f28b5fb8adcf64004ca8397879f8789fa939482b9856cd261fac7e00705984f69011ca74ffd4b81cd8a53dd9c51185fbd68e3108388ea75c787007f6eed2319cc1ca0c0f1236e6f0988a3d925fb79d8606cb12ed7b3fa84107a7de14bacf1d847ccbf3228afea8e63ba8da371b4fbc2b42f5b6cddf904203039ad167f8846c5f4c2c46d4bb40349161e2a715c75d7c48deafda2fdfc93df6456dc05bc5c6b42f40ca117f98ff5fce6f1ee6d123206e0a1c35045caad283e97ff158ae1ce2940f96f880f3568684c1ca0e505ea13b656ec402e78e0a9307ba7074de4829b83d93b90abfc9a729bd9c862d3bc762e8cae02f13322fe532b2379923fd8706e25d472377922e95d2db7670644f24f6e3c8df10612b3a8ea3f303849b9b1af906ae46b4016d5327ec61d7e012f98a9feea2fd06ad44e7623f1396e759d688af7689e615d4ed57adbb81656cffacddd5b63beb9a82fb7a99d3884eea4ea5460d3a7679d8994a0b5482a68b01f2e859d15681e08f19c84d432dd002db7e670b74f5b5f198cdc26c59c59fc54d1e4eceba5199ad5f167a72853c6518d1990b3a16acafdeb5cdd0f349952434c93d3671a610e05965d11303abe12df05fbdce30d45d6ca4e8b6ee9e4c0f6861cb612dac2e96caf8791d8b6136c34f9c1537a57869960b0ed1fe287a072d72d4215a47da471018f5f642b83952fdcf0c9975a5935292baa3dbfa94fcfdab2a1ec18c4e1b62fd71996697cb6ac6e5a216ed4744b8c508f04232a05e1bfc4951205c7ec9fbdaf9796f316da8c292b9dba21d8bb48ac81f10fce416dd284e6caff5fe2cfa5b3e9c763dd4f2181260a447b25d35de3976994d3716666126ff2a846e1b4d210eedf338103f42c1e52a0cbb9b34bf694885df203958b5012a44014b592e8c9210971da42b4f6f63bba863aafcd1c4649e29fc301b3d9f683ab84d1d9f1446e1f2ec01538e5276889c49cfcf9df14d1795a48f255a8c5b9e9e03d4297e93381516ce1bfd3443d71478b21ffeccb6e78967fe56ccffd484fa8f969905196719f6969a9063c7a1bb3397126aac39f585de854a909e624c792ac930eba1f52264be29d0c8c2046d6ec5ad4a0c8f262678fbc10210ae29145a8cf175fbb1999c4b6ce4ae318ea08000b358c452d58fb75e925d92a7b47aaa669918f39e0f78fe7afbf1e10f4e973f7c9a53d980f57126f241287b2f5488f250782eccd36f4464aee3f4fb8fcd2cd78a235451e339451e628f4f8420b738378f6d46659048831d743cebc82344bf944fde7c9f0c236c24db9841321db96f9d0634ac29ae6b18a25147995567ebc307e8d6a6f8a5521c2209aee00024da3befaaccc78099fa75553ae060d5300b32ccb591736507706643bf5384a2c8fd14d0828c344ba20cf806fa32de5a7a25d3c927d01b537b7175d6981086fe973f2c00b8dd678a6db7f50bb3b8ccfd31e3d3366365b449e034a055db3b9db8f4e6593058adc16422e11a19e38f52b54e1bec7bbbb3f571ebbec2ddc0ee58812bcb02b3536279783d500bb80bd85bf49c538cab3e3cbdc56aaf8600b40e68f4f37eb3977ffe8b074cc3bb586f53dc6269729d2981b9dff9615fc94a496ed2b4a70488caf9ceb7b84ae530913f62ecde837c6da86ea745356c8b20cf0802d0208efe79e14210ca578396fcf84d7b1181010c693913c42c3caa44fdca5c73a562e42a9cbfd6bf02873e452ea2ca09926f17445258f8704247f74f6d247d5b5edace7aa9c8dd7c1dbc3f4969bf1f1417691d615f2cd59705ce1bdee082a2c8f03c7a7fc3c201b25f239bf755ae3786323524932a7b2495e16d1fd7e2abb7d066ad79746382a5cb5ed02978b28d4d212a21ab1c00a3496a5b1a928a7e0ba5ad12b883cc88c9939a667794e16ab9845c1a6d560ad8542fd1a6a848d7ead48bc3b2c20aa3e1d290cb934cc376bbe1ec71c483eabb51a1ba86588403fc49b8daa737df83bf19de78f2b6c16acdd93a6c78991c21818bf26cf06ffe4e025abbd6a12ae9168f84d02db86207a997245aaa43c526dafbe7474ec0df422f4f241bfe90cc5bd3d06e20f097615b6786c9631d8fb9ad9a493e38cfa6a95981532dcfdc71ee7178674f2dbe0e53a80f0ec07debe37204f8e3f4058a2c314536233807d332a8abde7e6ff85861bdba5a402c064e1e1c5a9b5ce6087d8291f1bc15ee094e4416900d0b5d5dea262fa97b9b0b896f841bca89a203430db23ebf553b22f738fb5a1c6931a25c9ea71321d41382c49dc0353cdc4ae8114cad59e6c50e54cd7b3e285c604d44cdb87065e478265ccbc546c5cd3e8bb2bc52ccf46217209b660b828450b22c1c4e3d97e3530dc7944fb90c480751eacef442666e5cde0615c84640406f54f072a0c70eb38d3095575350ed808e5876113ef6c1bc57e96db272704e9158c71a5f4992a3a91ecd3c97fbacc6e30d448922575dbba09dc59e7ea2ae03a81e4bd36d0699316d1443cb1022e634013df0e1cf3a4888ab83d226007fdc90edb63932c6d34d41df53ab52487b35897e2c61d6fb86bc4e93cdaecdd4192bc99a5add99e11581506dcc23ba23d2d590018e2806e2791daa81f9307380d5fdb0893e6c8a748513af0c1af78ac1e9f7e47b50ad550fd1f65e2212db5184f7cb151662ccce01b69daa547f829c22eb8c80faf53f5b45813a78ecebe20a746e2fbae15dd54f460a68894426862b21902750753f2aa7dc43f1d5d7936fb08a19040965b5d32e67e08f67d5d6c685a9ef0e894a5d1b34143e1adbac81dae5d89d2749beee53f500ce381397d981f2f1e38b772268aeda24bc36d96b59c56a44e2987347cf2bb889febc3b3e3948e8362a936900d8b62e328e4783c5fad0e0017c08630d0509277e4bf4e0f231ce9d4072f207d42287b1d77f6c70a4d9a7441c0465ceb6b57a455971a451036d108ae8934e4882642f3a445388e75b00a045a28ace9eef5225c8d9ab77b5e7968df5bdc24e6ef198356329b7ec5c0db36008e18b65247e6421990b6d6aaea5778556a8d180ab5acdf344999621b365063f544254e1cfbd882687a262b35bd1ee5b70d9ddcb1b563e7cb57745f181ab130a129e7a3d22bcf69dc99e6bf3010333eccb698453937f350d07618321c3d3691b488c3586346352b556cfb7b5d6061cbe874a6f126e4e0de86688994d67e317562b712a5c56be2e79b49bdb4e11a51d3cf2853c941a94b4e3b36b2d339c108c1907d89f3e1a2daacc1f06c72dde4cd7561bc61199f6c98474074f7b9fae861f0db824d614abe61c896e6c6539d5bdd44edc0296fa4a77a5373fbd8cdbaa10662eef6abe9f8c297569cc5701eafc42aaef84057afbc38ae582dbeb73b0298cb115c4175bb41b75fb9288a55d196d6eeec687d1d5f7b74183ee7b413b5f0facb71c11f05604dd7338039f29a4049c2dd04c6f21a49a46f8108c95eb7fb647dfcdd5428ef0cbd9cb566279cd330417d8da98ebf903b3f84cefec4e8d797db8670fdabc4cb60e6d95aa5403d77b1c40c50a5b6aaa94c69a50d40d0e865d9b710c36f7b68f13e7bc2523d7d7ea1978cfa77585efc54c9a73fe72829e6eab5b46a888a021d3f1bc3699fa287b76575e89060d04074c0b6b1071f2e0e9602b615258ecf93ae7422536211d03ad13fae1133b20b9d614743d0ef70f406d8d147f98b0fe031095dd1f924133b496efe41b8bb5a8f233045bd70fa20e4ea1ea913e9fd569002107f43c4b7bdb9b0eef38fcac061a8842c8261c80f3b92026c7c9293499d4f20ac25610d8f60bfa74d3b1c0dcd7b527c77a3b64c7b5eadc7a35fd2b2e1b68c3da9ef924d6a41bc1eda04f94eb77b73603bd1477d117930ebf56dcb9fe611b72b1c62e212393a60e5a7d57b35ef8bee5e77405c04be96a5252af46c291ed8c1f5fd8425085d1409b7d72199899e6499b67036b206a09f3df4bbbd56d329bc30d626729a9cf41fa60c5faa0d5a4afa93ae6e0597cc906991c9d77d7fd62911849a8103119f68f4cf964032497bb910ca5018089c275e2f822e0803a3d80ee862fd2fa9a972075cc21ffc9d0200958fa7e7277efb22fbde828431a5a5f9c3c698a97705b87b3d7073fe62d6607412f5a47abbc58a694bbcc7b0e76a0717301804bd092dae4541b8828706aa1cd76a29d0d89803d87ec8ea4db71d5a0473fcb775200eb2065662567eb5a5431dbcd030699d761dd9312c2d741eed7afde1412bbbad6aadc06545a6dcd403fef4007b5a01cb75f3e104f3646d25710c2d647c1e3ec33bc1d490a28468aaa62f2c892cfb6a404dd52d8718513d7b894c0b73f27c5b38c2748e7801d83bf68622c707a56ff0ad44706dddbb3ce3b5321ec667a903a0fcb03a6be6ef1e8fc116db5c99223f89a787f6dffeb9adaf7836234efbd4522d5c4ab275bc233e08427fc105d22bf29018e600bfa00fc1f2bfab55379644ecffd951166bd2a4e1e4f7cf2d240169051b13dbd7b96486ad981d81013b06061395318923a909bc8db04d8db5f5a51e669003375875f308413fb8510c83fe4f378e1085305d868fc2aab1c8ad40c3db48e81105a49ec172373c8ebbafa56264e63b97007f1a0c411a21ab4b5c9861df633de26e208857cf10d28e46c429a68ecbff62f68fec420fd058129d1d52909e171419d3d21291af1c43b7dafe23581c4a6e3530bc92ee439aa224dd6ed442ab9eaff484560bb06bfeb002d209d0ad1f44157213e80ba774d6bb4e8d67ecc1ad8d41f9cdb7484987d99a3d6586bacabb202e1ad1466339931012109d53f7cfefa24d1132765ce432101be495af17c104a390f46691840b58037852e91b2eb9e20abb5e6ddd44f49052d564ae5c88ece1e112bc861f2ec29988897ff2bfc0bdcb4d641c3cc504d31ef70ef3fa333e2e6a2c6f5b17eed669c5f65b2d04a63598c9653cccf37fbaaf79c1891de8a2cc1b257c00c5884cc0bef9c53150ad4154c76595d026f6165fe031b6f2b523e7e761bf2777cdecd29c0bd60b231aa71beda63191e17dd219c21f66770b42bd086b0f918e502a1d9f488e804e911fe3a3ac6fb536a0bb305e9dc40cefd8ac3efdbe7de1147a8b0f4d522bfa38d48fd3834320c82eb117b1842902844aa5be7d8fe09e6c02797379b83aaee115a864cd959edb58cffd37dacb829208668bd3d5b6120729b78d1cb9ff8c7a99bcffd83735a25d1981849d145064e8574a98a32a523ce22d17c6931c935f0c5b354b81a2fa2417995e9669dad3e85830bd0b494bf3af83ab97d160866c250504d703c675b661e77cfd041a7cfdf191fdeb999a30f01bb7a39549c74bf88a22e8f01930bab2dac8acfb784054bd31cdcca196ad97d55864a81bb0804df678bb0da543a6ad11fae6baff5d02b04112068d189abe97a11f86813c1755926e8071df625a0db1cb5e9c5d139fb3bcff11094642058ee15744287ff0ff4f627708fb6f634d46abe87aaefae7adc030d6f9419a2715cf107be8e40b17fb401957f66c96f9bb0447a4f76463824a2089d78d1dd566f49fd3aa26c4b37568f2bf2a2ad0644e77f3ed83af233f3fd9b1a44f9daba6abac6f84bde53ead1dd2ed3ca23bd70c1a0c8ad85bf12b6b7fcb910e866d822d47dbda05d4697dd1888b248743a7b001fa0b2a2dbe17502d3ef38ade36ab1b7fb003434a3d2d2b472fe6f31bd3850e561cbf0829934529d50bed0fbe03a79f8ab1b6511c9b5e78b6826df1cf7ada150243eb7f6268b919cb0ca90bd43a048ab2667b5405d4623055103bfb3184038fca1cb3cde25bacd04fb3841bac72f9853599c744c5a8dc4f6a8fbc0574f24ea1c2e392ba856bc8bc1ccb64288c88928d11e8cf52c8052fb28b62c0737499265a48b9c331</script>  <div class="hbe hbe-content">    <div class="hbe hbe-input hbe-input-default">      <input class="hbe hbe-input-field hbe-input-field-default" type="password" id="hbePass">      <label class="hbe hbe-input-label hbe-input-label-default" for="hbePass">        <span class="hbe hbe-input-label-content hbe-input-label-content-default">Hey, password is required here.</span>      </label>    </div>  </div></div><script data-pjax src="/lib/hbe.js"></script><link href="/css/hbe.style.css" rel="stylesheet" type="text/css">]]></content>
    
    
    <summary type="html">记录 2026 年 10 月每天做了什么、困难和感想</summary>
    
    
    
    <category term="工作月总结" scheme="https://u7u7.top/categories/%E5%B7%A5%E4%BD%9C%E6%9C%88%E6%80%BB%E7%BB%93/"/>
    
    
    <category term="工作记录" scheme="https://u7u7.top/tags/%E5%B7%A5%E4%BD%9C%E8%AE%B0%E5%BD%95/"/>
    
    <category term="日报" scheme="https://u7u7.top/tags/%E6%97%A5%E6%8A%A5/"/>
    
  </entry>
  
  <entry>
    <title>看了几天 Agent 简历，我最想问的还是：为什么？</title>
    <link href="https://u7u7.top/posts/agent-resume-interview-why.html"/>
    <id>https://u7u7.top/posts/agent-resume-interview-why.html</id>
    <published>2026-09-14T12:15:46.000Z</published>
    <updated>2026-09-14T12:15:46.000Z</updated>
    
    <content type="html"><![CDATA[<p>这几天面试了一些人，也看了不少应届生的简历。看下来，一个很直接的感受是：现在写 Agent 项目，英文名词实在太多了。</p><p>多 Agent、工具调用、Loop、RAG、Human-in-the-loop……一页简历读完，概念很密，项目到底做了什么却不太好找。</p><p>下面只是我最近的一点个人观察，不代表所有候选人，也不代表所有面试官。话可能有点直，但我想说的核心很简单：<strong>简历上的每一个亮点，最好都能接得住一句“为什么”。</strong></p><span id="more"></span><h2 id="英文可以写，但名词本身不算亮点">英文可以写，但名词本身不算亮点</h2><p>举个最近碰到的例子：Human-in-the-loop。</p><p>我看到这个词的时候愣了一下，问了才知道，候选人说的是在 Agent 处理过程中加入人工审核。解释的时候，明明可以用中文讲得很清楚，写在简历上却只剩下一个看起来很高级的英文概念。</p><p>我当时的第一反应是：加了一个人工审核环节，这就能单独当成亮点了吗？</p><p>仔细说，人工审核当然可能有设计难度。哪些操作必须先经过人确认？审核不通过以后，任务怎么继续？等人审核的时候，已经执行到哪一步的状态放在哪里？这些都是可以展开讲的东西。</p><p>可如果简历只写“引入 Human-in-the-loop 机制”，我看不出你具体解决了什么问题，也看不出你的贡献在哪里。</p><p>假设做的是一个内容发布助手，一句“生成结果先交给运营审核，通过后才能发布”，就能让我很快理解流程。接下来再讲为什么需要这个环节、你怎么实现它，技术细节才有落点。这个例子只是说明表达方式，实际写简历时，还是要对应自己真正做过的工作。</p><p>对我来说，满页术语却看不出具体做了什么，会降低继续读下去的兴趣。尤其是追问以后发现，候选人其实能把事情解释清楚，就更觉得可惜：本来可以直接展示的能力，被自己藏进了一串英文里。</p><p><strong>英文术语可以让交流更准确，但不能替你说明工作量、难度和价值。</strong></p><h2 id="我理解求职压力，但经历包装确实越来越离谱">我理解求职压力，但经历包装确实越来越离谱</h2><p>另一个感受，是简历包装这件事越来越严重。</p><p>一些 UP 主、自媒体作者会在社交平台分享自己的简历和项目经历。有人拿来参考表达方式，有人却直接把内容搬过去，改个公司、换段时间，就成了自己的实习产出。再和 AI 对话几轮，把项目背景、技术难点和面试回答都补齐，一段经历就这么“长”出来了。</p><p>我的评价很直接：把别人的经历写成自己的，这么做不道德。</p><p>但我也能理解求职者为什么焦虑。在一个大家都忙着包装的环境里，老老实实写自己做过什么，可能连自己都会担心：这样的简历，能拿到面试吗？看到别人靠包装获得机会，更容易产生“想活下去就得顺应环境”的想法。</p><p>这种压力是真实的，不能只用几句漂亮话就把它带过去。</p><p>只是，整理真实经历、把自己的贡献表达清楚，和直接编出一段实习，区别仍然很大。前者值得认真做，后者即使帮你换来一次面试，也会把后面的每一次追问都变成风险。</p><p>AI 可以帮你梳理项目、检查表达、模拟追问。它也很容易把一段故事补得十分完整。问题在于，这份完整来自生成，你自己有没有做过其中的判断、踩过其中的坑，是另外一回事。</p><h2 id="先想清楚，公司为什么要做这个-Agent">先想清楚，公司为什么要做这个 Agent</h2><p>有些简历越问越让人疑惑：技术方案写得很满，业务背景却像是临时补上去的。</p><p>找几个开源项目，再找几家业务看起来相似的公司，把项目套进一个实习背景里。表面上，技术和公司都有了，但中间缺了最关键的一段思考：<strong>这家公司为什么需要这个 Agent？</strong></p><p>开源项目本身很适合学习。你能跑起来、读懂关键代码、做出自己的修改，再把理解讲明白，完全可以写成个人项目。如果确实在实习中基于开源项目继续开发，就要讲清实际业务背景、自己做了哪些改动，以及这些改动解决了什么问题。</p><p>谁会用它？用户原来怎么完成这件事？原来的方式到底哪里不好？为什么值得花人力和模型调用成本去做？Agent 给出结果以后，谁来使用这个结果，下一步发生什么？</p><p>这些问题串起来，才是我理解的业务闭环。</p><p>比如，一个系统能够读材料、检索信息、生成回答，到这里可以说明流程跑通了。但谁需要这份回答，回答怎么进入实际工作，结果出错时谁处理，仍然需要继续想清楚。</p><p>个人项目暂时没有真实用户，也没关系。可以明确说自己验证了什么、还有哪些假设没有验证。为了让经历显得完整，硬补一套并不存在的使用情况和效果数据，反而会让本来能讲清楚的部分也变得可疑。</p><p>我更想听到的是：你对这个场景有什么理解，为什么选择这种做法，亲手完成了哪部分，又知道它有哪些局限。</p><h2 id="业务讲不明白，多几个-Agent-也救不了">业务讲不明白，多几个 Agent 也救不了</h2><p>现在有些简历的写法，让我想起 25、26 届校招里那种给项目堆微服务的劲头。</p><p>一个很小的项目，先把服务拆开，再加上各种组件。面试官问一句：“这么小的项目，为什么要用微服务增加成本？”就开始不好回答了。</p><p>这类问题，我自己以前也有过糊弄过去的时候。现在再看，问题其实一直都在：选择一个方案，总得有个说得通的理由。</p><p>换到 Agent 项目里也一样。拆了好几个 Agent，接了一堆工具，又加上循环和检索，架构图画出来很热闹。但为什么要这么拆？一个 Agent 能不能做？哪些步骤用固定流程更合适？多一轮调用，换来了什么，又多付出了什么？</p><p>这些都值得在写简历之前想一遍。</p><p>某个技术很热门，可以成为你学习它的理由。放进具体项目以后，还需要解释它和实际问题的关系。否则，面试官只要顺着设计往下问几层，前面背得再熟，也容易卡住。</p><p>所以，我对准备 Agent 求职的同学，最实际的建议是：把简历逐句过一遍。</p><p>看到“设计了”，就问自己为什么这样设计，有没有考虑过别的做法；看到“实现了”，就把输入、处理过程、输出和异常情况讲一遍；看到“优化了”，就说清原来哪里有问题，改了什么，凭什么判断有效。如果写了数据，还要知道数据从哪里来、怎么统计。</p><p>可以找人做模拟面试，也可以让 AI 按简历不断追问。回答卡住以后，回到代码和项目事实里找答案，再把表达整理清楚。后续参加真实面试时，也把被问住的问题记下来，持续修正自己的理解。</p><p>准备充分，不只是为每个问题背一段答案。更重要的是，你知道简历里哪些内容是自己做的，哪些决定是自己想过的，哪些地方还没有弄明白。</p><p>应届生不一定有机会做很大的项目。一个范围不大、来龙去脉讲得清楚的项目，在我这里一样有说服力。最怕的就是简历把期待拉得很高，一问为什么，回答却接不上。</p><p>最后，送给这些现象一句三十字锐评：</p><blockquote><p>术语堆得再高，也填不满业务的空洞；简历包装再厚，也扛不住一句为什么。</p></blockquote>]]></content>
    
    
    <summary type="html">最近面试、看简历的一点个人感受：英文术语本身不算亮点，经历包装也替代不了业务思考。把为什么做、自己做了什么、结果如何讲清楚，比堆满一页技术名词更有说服力。</summary>
    
    
    
    <category term="杂项" scheme="https://u7u7.top/categories/%E6%9D%82%E9%A1%B9/"/>
    
    
    <category term="AI" scheme="https://u7u7.top/tags/AI/"/>
    
    <category term="Agent" scheme="https://u7u7.top/tags/Agent/"/>
    
    <category term="校招" scheme="https://u7u7.top/tags/%E6%A0%A1%E6%8B%9B/"/>
    
    <category term="面试" scheme="https://u7u7.top/tags/%E9%9D%A2%E8%AF%95/"/>
    
    <category term="简历" scheme="https://u7u7.top/tags/%E7%AE%80%E5%8E%86/"/>
    
  </entry>
  
  <entry>
    <title>2026年09月工作日报</title>
    <link href="https://u7u7.top/posts/work-daily-2026-09.html"/>
    <id>https://u7u7.top/posts/work-daily-2026-09.html</id>
    <published>2026-08-31T16:00:00.000Z</published>
    <updated>2026-09-30T04:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<h1 id="2026年09月工作日报-📌">2026年09月工作日报 📌</h1><p>这里按天记录工作内容、遇到的问题和当天的一点想法，方便月底回看。</p><!-- daily-report:2026-09-15:start --><h2 id="2026-09-15-今天做了啥？-✨">2026/09/15 今天做了啥？ ✨</h2><p>今天主要是在把 5 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容">✅ 工作内容</h3><ul><li>完成美容美体题库导入链路排查与热修提交，确认问题是 CP 对新工作表展示名识别不足、后端又要求覆盖旧根场景；已保留未知 key、重复 key 和 sceneId 不匹配的拦截，并将后端与 CP 的最小改动推送到热修分支。</li><li>定位预发导入 500 的根因，确认热修代码已部署，但数据库缺少 scene_tree_head 等场景树迁移表；故障发生在首次查表阶段，尚未进入删旧题库或写新题库，原有 68 题未被覆盖。Excel 解析预览已通过，覆盖范围为 32 个场景、27 个叶子场景、150 题。</li><li>完成正式生题失败分型：任务 47 的容量拒绝、任务 48 的 CONTEXT_SCHEMA_INVALID 已分别收敛。后者落在 questions.claimRefs 的结构化结果校验，相关 Schema/提示词的最小基线测试 16 条通过；同时识别出通用修复提示混入正式链路规则的回归风险，暂未合入。</li><li>梳理 QA-SC 任务 51 的执行状态和并发来源，确认任务不是卡死，而是单 Pod、实际并发 1 下按机会项串行执行；最终已进入 pending_review，不能据此误判为全成功。进一步确认基准 QA 的配置值 4 并未作用于 QA-SC 项目运行 Pod，配置源需要按项目环境重新对齐。</li><li>完成多仓未提交改动审计与分支基线整理，确认 72 个改动属于不同主题，不能整包提交；合并三个独立仓的最新 master 时已处理冲突并完成后端全 Reactor 编译，保留各仓原有未提交改动的边界。</li></ul><h3 id="🧩-困难-需要帮助">🧩 困难/需要帮助</h3><ul><li>预发确认覆盖导入前，仍需确认 V37、V38 场景树迁移已按顺序落库，并回读 scene_tree_head、scene_tree_revision 等表；目前只能确认解析校验通过，不能确认安全导入。</li><li>QA-SC 并发配置需要在项目级 SCM/运行实例重新核对并经滚动重建生效；不宜直接把长 Agent SDK 会话并发调到 10。</li><li>正式生题的 claimRefs 修复需要把正式链路专属提示与通用 analysis 修复提示拆开，再进行最小提交和回归验证。</li></ul><h3 id="💭-感想">💭 感想</h3><p>今天主要是在把题库导入和正式生题两个问题从“页面现象”往真实的代码、数据迁移和运行配置上收敛。预览通过、Pod Ready、任务待审核都只是阶段信号，真正可放行仍要把迁移、配置来源和最终业务回读逐项补齐。</p><!-- daily-report:2026-09-15:end --><!-- daily-report:2026-09-16:start --><h2 id="2026-09-16-今天做了啥？-✨">2026/09/16 今天做了啥？ ✨</h2><p>今天主要是在把 6 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容-2">✅ 工作内容</h3><ul><li>完成题库画像主链升级，新增 <code>qb-core-202609-v2</code> 并切换生题链路；正式题目支持 <code>customer_persona</code>，兼容旧三项空画像与新四项解析，只投影当前机会绑定的唯一画像，旧版 v1 保持不变。</li><li>完成画像链路定向验证，165 项测试全部通过；全量基线为 2559 通过、124 跳过，69 个失败均收敛到旧 <code>/draft</code> 断言、过期 direct-context 契约和日志捕获，未混入本次验收。</li><li>对齐画像发布依赖，确认 <code>applied_persona_id</code> 必须先落库，发布顺序固定为“DDL → AI v2（兼容旧任务）→ Knowledge 写四项”，避免新链路早于数据结构启用。</li><li>收口商家经营助手方案与 Buzzy 项目材料，明确“Supervisor → 专项 Agent → Domain Skills → Business CLI → 经营系统/联网信息源”分层，补齐业务流程、任务路由和职责边界，避免把工具层误写成 Agent 或虚构底层实现。</li><li>补全 CLI 场景化回归评测口径，固定“CLI 变更 → 飞书评测机器人 → 经营场景矩阵 → Dubbo 重复调用 → 结构化结论”链路；指标聚焦关键断言一致率、场景覆盖和跨场景兼容风险。</li><li>整理并锁定 Buzzy 主线简历材料，移除 KFC/点餐残留；主简历 15 条亮点均满足字数限制，成品已设为不可变保护。</li></ul><h3 id="🧩-困难-需要帮助-2">🧩 困难/需要帮助</h3><ul><li>画像上线前仍需 DBA 确认并落地 <code>applied_persona_id</code> 列，才能按既定顺序启用新链路。</li><li>AI 仓全量基线仍有 69 个既存失败，需要与本次画像变更分开治理，避免验收口径混淆。</li><li>微信工作群正文尚未形成可可靠引用的工作证据，本次不据此补充具体群消息。</li></ul><h3 id="💭-感想-2">💭 感想</h3><p>今天主要是在把画像生题、发布依赖和经营助手材料都往真实代码链路与可验证口径上收敛。画像功能已经完成兼容与定向验证，后续关键是守住 DDL 前置和历史基线噪音的边界。</p><!-- daily-report:2026-09-16:end --><!-- daily-report:2026-09-17:start --><h2 id="2026-09-17-今天做了啥？-✨">2026/09/17 今天做了啥？ ✨</h2><p>今天主要是在把 7 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容-3">✅ 工作内容</h3><ul><li>完成“题目治理→历史资料”在 Knowledge 与 CP 的 QA-SC 交付，补齐商家/文件名分页、资料历史、分析批次、独立问题与生成题目三类明细，并冻结资料版本和历史统计；发布后接口验证通过，浏览器端到端验收待补。</li><li>完成客户画像同步故障修复与真实回归，补齐 backend 与 QA March 的缺失 Key；同时对齐正式生题按冻结画像逐一判定、每个适用画像产出一题，并将 appliedPersonaId 下沉到题目。</li><li>推进任务 53 的正式生题排查，9 个独立问题中 7 个成功生成 29 道待审核题；已收敛 2 个失败项，其中一项为 DIRECT_SESSION_TIMEOUT，约 600 秒后终止。</li><li>定位商家后台“操作过于频繁”根因，滑块验证已通过，实际由上游 uic-captcha 的短信限频返回错误码 135900005，排除 PC 前端、backend 与 knowledge 的业务限流。</li><li>修复本地正式生题的 INVALID_PARAM 启动边界，补齐运行模式与操作人身份配置后恢复请求发起；问题不在题目生成参数本身，而在本地身份上下文不完整。</li><li>完成题目驳回重生的基础事务与幂等回执能力，重复驳回返回原回执；专项 7 项、旧链路 51 项及 AI 原基线 83 项验证通过，完整快照、重生任务和前台交互仍需继续补齐。</li><li>完成话术范例库草稿，沉淀 9 个行业共 180 条题目，并明确行业→场景两层路由、每场景最多 5 题、缺题不跨行业凑数以及读取失败/重试/审计规则；当前尚未接入正式生题主链。</li></ul><h3 id="🧩-困难-需要帮助-3">🧩 困难/需要帮助</h3><ul><li>任务 53 仍有 2 个独立问题失败，已证实一项为 DIRECT_SESSION_TIMEOUT；需要继续确认另一项根因，并收敛超时重试与补偿边界。</li><li>历史资料接口已完成发布验证，但本机浏览器端到端访问曾超时；需要在可达 QA 链路补齐前端完整验收。</li><li>话术范例库仍是本地草稿，需要继续明确生产接入范围、版本冻结与 Good/Bad Case 治理节奏。</li></ul><h3 id="💭-感想-3">💭 感想</h3><p>今天主要是在把题库治理从页面状态收敛到真实历史证据、冻结版本和发布接口，同时把登录、生题与短信限制问题下钻到上游服务和运行身份边界。主链方向已经比较明确，但任务超时、QA 前端可达性和范例库生产接入还需要继续按可回放、可验证的边界推进。</p><!-- daily-report:2026-09-17:end --><!-- daily-report:2026-09-21:start --><h2 id="2026-09-21-今天做了啥？-✨">2026/09/21 今天做了啥？ ✨</h2><p>今天主要是在把 6 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容-4">✅ 工作内容</h3><ul><li>完成正式生题任务 62（run 3810）逐项失败分析，确认 10 条机会均未进入 pending_review：7 条约 600 秒触发 DIRECT_SESSION_TIMEOUT、2 条约 552 秒触发 SDK_TIMEOUT、1 条因 CONTEXT_INVALID 被验收拦截；问题不是单一汇总状态能解释的“全部失败”。</li><li>梳理正式生题容量链路，确认不同 Context 会直接并发启动长耗时执行会话，缺少全局与 Pod 级背压；大批量提交会把会话同时压入同一运行单元，后续应按持久化任务、全局准入和已验证速率出队，而不是继续放大并发。</li><li>对齐 QA-SC 路由与配置时序，确认任务 62 经内部兼容网关访问 doubao-seed-2-1-pro，不能简单归因为协议不兼容；配置更新不会动态刷新已启动服务，run 3812 与截图请求的路由、Trace 时序仍需逐条闭环。</li><li>梳理“上传资料—分析会话 A—确认独立问题—正式生题会话 B”边界：上传不启动执行会话，B 不复用 A 的原始记录，而是读取已持久化的分析和证据后重新冻结 Context，明确了两段链路的输入与可复现口径。</li><li>完成题库资料分析页交互修复：修正主按钮文字被样式缓存覆盖的问题，阻断勾选历史资料后自动跳转“结果查看”，并增加“全选本页/取消本页”且保留跨页选择；契约测试 51/51 通过。</li><li>完成微店 AI 副店长的简历与方案材料梳理，明确调价安全闭环属于当前可支撑的能力，ERP 和真实电商接入仍是规划；评测指标统一按受控 Mock 口径表达，避免把方案写成线上事实。</li></ul><h3 id="🧩-困难-需要帮助-4">🧩 困难/需要帮助</h3><ul><li>正式生题的安全并发阈值仍缺网关性能、CPU、数据库连接等运行指标，当前不能把瓶颈直接定责给单一运行单元或上游服务。</li><li>run 3812、网关路由与 Trace 的时间关系尚未逐条对齐，需要补齐同一请求的链路证据后再判断配置是否生效。</li><li>全局准入和排队策略还需结合持久化任务的重试、租约和发布验证进一步确认，避免“忙碌”状态被误记为“生成中”。</li></ul><h3 id="💭-感想-4">💭 感想</h3><p>今天主要是在把正式生题从“任务失败/执行慢”的笼统描述，收敛到逐机会超时、无背压准入和配置时序三层事实。页面交互和双会话职责也落到了可验证的规则上；方向已经明确，但容量阈值和路由证据还需要继续补齐。</p><!-- daily-report:2026-09-21:end --><!-- daily-report:2026-09-22:start --><h2 id="2026-09-22-今天做了啥？-✨">2026/09/22 今天做了啥？ ✨</h2><p>今天主要是在把 6 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容-5">✅ 工作内容</h3><ul><li>完成正式生题任务 67 的机会级复盘，确认 10 个独立问题均已终态、未产出待审核题；失败拆分为 6 条执行超时（含 <code>DIRECT_SESSION_TIMEOUT</code>、<code>SDK_TIMEOUT</code>）和 4 条结果校验拦截，避免再把 <code>completedOpportunityCount</code> 当成成功数。</li><li>推进正式生题 V5 收敛，服务端一次性组装冻结 <code>ContextPack</code>，模型改为单次结构化生成，不再在会话内逐个读取 Skill 或无限修复；引用改由 <code>evidenceRef</code> 回填冻结证据。任务 68 现场验证后，已将 <code>max_turns</code> 从 1 调整为 2，避免正常结构化终态被内部回合上限误杀。</li><li>补齐正式生题 SDK 可观测链路，记录 <code>capacity_wait_ms</code>、<code>context/task/opportunity/attempt</code> 关联和 <code>session_key</code>；同时确认旧 asset trace 只是组包审计，不能当作 SDK 调用，为后续区分排队、单次生成与校验耗时建立口径。</li><li>定位 CP 创建正式生题时的 400 包装链路，确认 Tether 到上游 Dubbo Provider 超时后返回纯文本 <code>context deadline exceeded</code>，CP 的 <code>dubbo_invoke.js</code> 将其按 JSON 解析才形成 400；当前进一步收敛到 <code>wakeDispatch</code> 在 Dubbo 请求线程逐条等待 Python 异步受理回执，而非 Node 自身超时。</li><li>完成“资料→独立问题→题库”技术链路及应用边界梳理：当前可用入口为 <code>sellai-train-cp-admin</code>，经 <code>sellai-train-knowledge</code>、<code>sellai-train-ai</code> 到 <code>sellai-train-backend</code>；商家后台的生题/审核代码尚未挂总路由，小程序 BFF 不参与该链路。</li><li>梳理 QA-SC 到 Prod 的发布范围，明确不复制 QA 业务数据；Question Case 最小主链只涉及 <code>question_case</code> 新表和 <code>question</code> 的 4 个围栏字段，Prod 还需按当前代码逐项补齐 SCM 与资源前置。</li></ul><h3 id="🧩-困难-需要帮助-5">🧩 困难/需要帮助</h3><ul><li>任务 68 在新路径已生效时仍出现 <code>SDK_MAX_TURNS_EXCEEDED</code>；<code>max_turns</code> 已调整，但尚无本轮完整候选题落库成功证据，新增黑盒回归也受本机缺少 pytest 入口限制，需在项目环境验证。</li><li>当前 Backend migration 存在 V37/V38/V39 同号文件冲突，不能全量自动执行；需按确认功能拆成显式 DDL、逐表回读，并补齐首发保持关闭的 SCM 开关、AI 预算、cursor keyring 与最小 DB 权限。</li><li>CP 400 目前只证明到 Tether/Provider deadline；仍需用同一请求的 CP URL、耗时与 TraceId 关联 Provider 记录，才能判定究竟慢在受理、Provider 还是下游依赖。</li></ul><h3 id="💭-感想-5">💭 感想</h3><p>今天主要是在把正式生题从“生成慢、任务失败”的笼统现象，收敛到机会级终态、会话回合和冻结上下文三个可验证层面。V5 的方向已从让会话自己找资料、反复修复，转为服务端先固定输入、模型只生成、服务端确定性验收；接下来关键是用实际成功率和耗时验证改造，而不是只看链路已经切换。</p><!-- daily-report:2026-09-22:end --><!-- daily-report:2026-09-23:start --><h2 id="2026-09-23-今天做了啥？-✨">2026/09/23 今天做了啥？ ✨</h2><p>今天主要是在把 7 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容-6">✅ 工作内容</h3><ul><li>完成正式生题任务 71 的机会级复盘，确认 10 条机会均已终态、没有题目进入待审核：3 条 <code>DIRECT_SESSION_TIMEOUT</code>、4 条 <code>CONTEXT_SCHEMA_INVALID</code>、1 条 <code>SDK_TIMEOUT</code>，另有 2 条在 Knowledge Verdict 的 <code>verify</code> 阶段被拦截。</li><li>定位 V5 候选题验收拦截根因：QA-SC Knowledge <code>1bd441e3</code> 将第 5 次调用固定校验为 <code>allowed_card_evidence</code>，实际成功回执为 <code>generation_requirement</code>；两次证据调用的返回数与引用数分别为 8/8、3/3，需求回执均为 1 条且无需引用。</li><li>完成 Knowledge 本地修复，<code>AiOwnedV4TraceVerifier</code> 已接受 V5 第 5 次 <code>generation_requirement</code> 回执，同时保留 V4 与记忆回执规则；定向 Maven 测试 10/10 通过。</li><li>完成 AI 服务会话时限与结构校验可观测性修复：首次调用和修复轮共享总时限，ContextPack 准备耗时纳入预算，日志补充稳定规则代号且不记录业务正文；目标分支定向测试 43 项通过、<code>git diff --check</code> 通过，当前未提交、未推送、未发布。</li><li>定位预发 <code>pre/bjh</code> AI 服务启动失败，新的 Pod 因 SCM 三项变量被注入空字符串而触发布尔/数值校验并进入 <code>CrashLoopBackOff</code>；旧 Pod 仍 Ready，应用汇总态存在误导风险。</li><li>定位 Backend 定时任务 SQL 报错，QA 的 <code>sellai_train</code> 尚未执行 V44，运行代码已使用缺失的 <code>profile_rebuild_pending</code> 字段；两个 March Key 缺失是独立配置问题，不能混为同一根因。</li><li>梳理 QA-SC 到 <code>master</code> 的二方包依赖与发布顺序，确认 6 个仓的 change 最新提交均已进入主干；代码合并状态不能替代实际 Pod 版本和运行验证。</li></ul><h3 id="🧩-困难-需要帮助-6">🧩 困难/需要帮助</h3><ul><li>任务 71 的两个 Verdict 门禁问题已有本地修复，但其余 8 条超时/结构校验失败仍需按真实错误分型继续处理，不能把局部修复写成全量恢复。</li><li>预发需确认 SCM 三项配置的实际业务取值并修正空字符串，再验证新 Pod Ready；旧 Pod Ready 不能替代本次配置恢复验证。</li><li>QA 需补齐 V44 迁移，并继续确认两个 March Key 的用途与配置边界，避免把数据库迁移缺失误判为配置问题。</li></ul><h3 id="💭-感想-6">💭 感想</h3><p>今天主要是在把正式生题失败从统一失败态收敛到机会级、验收级和运行环境级的具体证据。修复边界已经更清楚，但配置恢复、迁移执行和真实回归还要分别闭环，不能靠单个成功信号下结论。</p><!-- daily-report:2026-09-23:end --><!-- daily-report:2026-09-25:start --><h2 id="2026-09-25-今天做了啥？-✨">2026/09/25 今天做了啥？ ✨</h2><p>今天主要是在把 6 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容-7">✅ 工作内容</h3><ul><li>完成 WeKnora 接入现状与三仓同名分支梳理，确认当前已落地的是“商家资料入库与解析”，从 WeKnora 检索内容并生成题目的后半段尚未接通。</li><li>对齐资料导入主链：CP 管理台负责文件直传七牛、资料登记和发起导入，浏览器不直连知识库；CP 再通过 Dubbo 调用 Knowledge，由 Knowledge 使用服务端配置调用 WeKnora API。</li><li>定位“基于 WeKnora 生题”入口的边界问题，CP 侧请求与 Knowledge 分支 Dubbo 入参目前不一致，现有代码不能据此认定生题链路已经可用。</li><li>核对 CP、Backend、Knowledge 三仓分支提交与改动范围，确认 Backend 同名分支未保留可合入的 WeKnora 实现，避免把无关功能一起带入目标分支。</li><li>推进 Knowledge 的 change 分支合入 WeKnora 资料导入提交，合并过程无冲突；当前仍在等待编译验证结果，不把合入本身当作可运行结论。</li><li>核实 CP 管理台合入基线，当前工作区干净且与远端一致，已按题库页面和 Node 接口两个方向检查潜在冲突，最终合入结果待回收。</li></ul><h3 id="🧩-困难-需要帮助-7">🧩 困难/需要帮助</h3><ul><li>需要先对齐 CP“基于 WeKnora 生题”入口与 Knowledge Dubbo 的接口契约、入参和状态边界，否则资料导入完成后仍无法形成可控的检索生题闭环。</li><li>运行环境的配置、租户隔离和失败状态尚未联调验证；Knowledge 编译与 CP 合入的最终结果也需要补齐后再判断可发布性。</li></ul><h3 id="💭-感想-7">💭 感想</h3><p>今天主要是在把 WeKnora 事项往真实分支、调用链和仓边界上收敛。资料入库方向已经比较清楚，但要形成业务闭环，关键还是补齐检索到生题的后半段，并把 CP 和 Knowledge 的接口契约一次对齐。</p><!-- daily-report:2026-09-25:end --><!-- daily-report:2026-09-30:start --><h2 id="2026-09-30-今天做了啥？-✨">2026/09/30 今天做了啥？ ✨</h2><p>今天主要是在把 4 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容-8">✅ 工作内容</h3><ul><li>确认今日可用证据仅指向“POS机门店绑定报错”排查线索；未留存可复核的门店、终端、报错时间或请求链路，暂不对根因下结论。</li><li>梳理该问题后续需要补齐的核对面：门店绑定关系、POS终端信息和报错发生时间；待取得实际工单或日志后再收敛责任链路。</li><li>确认现有记录中没有代码修改、配置变更、发布或修复验证的业务证据，相关状态均保留为待确认。</li><li>证据不足，今日只记录以上可确认事项，避免把排查意图写成问题已定位或已修复。</li></ul><h3 id="🧩-困难-需要帮助-8">🧩 困难/需要帮助</h3><ul><li>目前缺少该报错对应的门店/终端标识、完整报错文案、发生时间和请求链路；补齐后才能判断是绑定数据、终端配置还是调用校验问题。</li></ul><h3 id="💭-感想-8">💭 感想</h3><p>今天主要是在把 POS 机门店绑定报错先收敛到可核验的证据边界。现阶段信息不足，先把事实补全比提前判断根因更重要。</p><!-- daily-report:2026-09-30:end -->]]></content>
    
    
    <summary type="html">记录 2026 年 09 月每天做了什么、困难和感想</summary>
    
    
    
    <category term="工作月总结" scheme="https://u7u7.top/categories/%E5%B7%A5%E4%BD%9C%E6%9C%88%E6%80%BB%E7%BB%93/"/>
    
    
    <category term="工作记录" scheme="https://u7u7.top/tags/%E5%B7%A5%E4%BD%9C%E8%AE%B0%E5%BD%95/"/>
    
    <category term="日报" scheme="https://u7u7.top/tags/%E6%97%A5%E6%8A%A5/"/>
    
  </entry>
  
  <entry>
    <title>Superpowers使用指南：把Claude Code和Codex变成真正会按流程工作的开发Agent</title>
    <link href="https://u7u7.top/posts/superpowers-guide-coding-agent-workflow.html"/>
    <id>https://u7u7.top/posts/superpowers-guide-coding-agent-workflow.html</id>
    <published>2026-07-31T13:10:00.000Z</published>
    <updated>2026-07-31T13:10:00.000Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Superpowers使用指南：把Claude-Code和Codex变成真正会按流程工作的开发Agent">Superpowers使用指南：把Claude Code和Codex变成真正会按流程工作的开发Agent</h1><p>很多人第一次看到 <code>Superpowers</code> 这个词，会以为它是某个官方新功能，或者某个插件按钮。</p><p>但如果用大白话说，我理解里的 <code>Superpowers</code> 更像这样一件事：</p><blockquote><p>它不是一个单点能力，而是一套把 AI 从“会聊天”升级成“会按流程干活”的增强方法。</p></blockquote><p>也就是说，它关心的不是：</p><ul><li>模型会不会写一段代码</li><li>AI 能不能回答一个问题</li><li>它偶尔能不能给你一个还不错的建议</li></ul><p>它真正关心的是：</p><ul><li>AI 能不能进入一个明确的工作流</li><li>能不能在不同阶段拿到不同上下文</li><li>能不能按规则调用工具、脚本和资料</li><li>能不能把一次经验沉淀成以后可复用的能力</li></ul><p>这也是为什么我会把 <code>Superpowers</code> 放在 <code>Skill扫盲</code> 和 <code>dev-skill</code> 之间来讲。</p><ul><li><code>Skill</code> 解释的是能力单元是什么</li><li><code>Superpowers</code> 解释的是这些能力怎么真正跑起来</li><li><code>Agent Team</code> 解释的是多个 AI 怎么协作</li><li><code>dev-skill</code> 解释的是这一整套能力怎么仓库化、工程化、治理化</li></ul><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart LR    classDef concept fill:#DBEAFE,stroke:#2563EB,color:#111;    classDef method fill:#FCE7F3,stroke:#DB2777,color:#111;    classDef infra fill:#DCFCE7,stroke:#16A34A,color:#111;    A[&quot;Skill&lt;br&#x2F;&gt;能力单元&quot;]:::concept --&gt; B[&quot;Superpowers&lt;br&#x2F;&gt;工作方法&quot;]:::method    B --&gt; C[&quot;Agent Team&lt;br&#x2F;&gt;协作编排&quot;]:::method    C --&gt; D[&quot;dev-skill&lt;br&#x2F;&gt;仓库与治理&quot;]:::infra    B --&gt; D  </pre></div><!-- 图位 1：文章封面下方说明图 --><!-- 这里适合放一张“Rules / Skill / Tool / Agent Team / Repo”的全景图 --><hr><h2 id="一、为什么我会单独写一篇-Superpowers">一、为什么我会单独写一篇 Superpowers</h2><p>因为很多人现在已经走到了同一个阶段：</p><ul><li>会写 Prompt 了</li><li>会给 AI 喂上下文了</li><li>也开始接触 <code>Rules</code>、<code>Skill</code>、<code>MCP</code>、命令脚本这些东西</li></ul><p>但真正一上手，还是经常会遇到几个很真实的问题：</p><ul><li>AI 回答得挺聪明，但不会自己走流程</li><li>每次都要重新解释项目背景，重复劳动很重</li><li>上下文一多就乱，关键资料反而被淹没</li><li>任务一复杂，AI 又退回成“会说不会做”</li></ul><p>这时候你会发现，问题不是“模型不够强”，而是<strong>缺了一层工作方法</strong>。</p><p><code>Superpowers</code> 讲的就是这一层。</p><p>它本质上是在回答一个问题：</p><blockquote><p>如果我希望 AI 像一个真正的开发搭子，而不是一次性聊天机器人，我到底应该怎么组织它？</p></blockquote><hr><h2 id="二、什么是-Superpowers">二、什么是 Superpowers</h2><p>如果要我给它一个尽量接地气的定义，我会这样说：</p><blockquote><p><code>Superpowers</code> = 规则 + 技能 + 工具 + 路由 + 记忆 + 治理 的组合增强。</p></blockquote><p>单独看这里面的每一项，其实都不新鲜。</p><ul><li><code>Rules</code> 不是新概念</li><li><code>Skill</code> 也已经越来越常见</li><li>工具调用、命令执行、MCP 也都有人在用</li></ul><p>但一旦你把它们组合起来，AI 的工作方式就会发生质变。</p><h3 id="从“会答题”到“会做事”">从“会答题”到“会做事”</h3><p>没有 <code>Superpowers</code> 的时候，AI 更像一个临场发挥型选手：</p><ul><li>你问什么，它答什么</li><li>你提醒一步，它走一步</li><li>你不提醒，它就不会主动进入正确流程</li></ul><p>有了 <code>Superpowers</code> 以后，它更像一个带 SOP 的搭子：</p><ul><li>先识别这是什么任务</li><li>再加载对应能力</li><li>再按约定顺序推进</li><li>需要工具时去调工具</li><li>做完后给出可复盘、可交付的结果</li></ul><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart TD    A[&quot;用户请求&quot;] --&gt; B[&quot;任务识别 &#x2F; 路由&quot;]    B --&gt; C[&quot;加载相关 Rules &#x2F; Skill&quot;]    C --&gt; D[&quot;读取 references &#x2F; 执行 scripts&quot;]    D --&gt; E[&quot;产出结果并校验&quot;]    E --&gt; F[&quot;沉淀经验 &#x2F; 更新知识&quot;]  </pre></div><p>这张图其实就已经说明了 <code>Superpowers</code> 的核心味道：</p><blockquote><p>它不是让 AI 更会“说”，而是让 AI 更会“按流程推进任务”。</p></blockquote><hr><h2 id="三、Superpowers-到底增强了什么">三、Superpowers 到底增强了什么</h2><p>我把它拆成 5 个最关键的能力。</p><h3 id="1-任务路由能力">1. 任务路由能力</h3><p>AI 最怕的一件事，就是一上来把所有信息都吞进去，然后开始“自由发挥”。</p><p><code>Superpowers</code> 的第一步，不是立刻干活，而是先判断：</p><ul><li>这是开发任务</li><li>还是排障任务</li><li>还是代码审查</li><li>还是写文档</li><li>还是查 JIRA、查日志、查链路</li></ul><p>只有先分流，后面的上下文和工具才不会乱。</p><p>这也是为什么现在越来越多体系里，会把：</p><ul><li>skill description</li><li>路由器 skill</li><li>命令入口</li><li>workflow orchestrator</li></ul><p>这些东西放在最前面。</p><p>因为它们本质上是在帮 AI 做“任务分诊”。</p><h3 id="2-渐进式上下文加载能力">2. 渐进式上下文加载能力</h3><p>这一点和 <code>Skill</code> 是强相关的。</p><p>普通做法里，我们很容易把一大堆背景全部塞给 AI：</p><ul><li>项目规范</li><li>业务背景</li><li>常见坑位</li><li>各类文档链接</li><li>一堆脚本说明</li></ul><p>结果是：</p><ul><li>token 越来越贵</li><li>有用信息密度越来越低</li><li>真到关键任务时，上下文已经被冲散了</li></ul><p>而 <code>Superpowers</code> 更像一层“按需读取机制”：</p><ol><li>先只让 AI 看到能力目录</li><li>判断相关后再读 <code>SKILL.md</code></li><li>真要落地时再读 <code>references/</code></li><li>有脚本就直接执行，而不是让模型硬背</li></ol><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart LR    A[&quot;目录层&lt;br&#x2F;&gt;name + description&quot;] --&gt; B[&quot;方法层&lt;br&#x2F;&gt;SKILL.md&quot;]    B --&gt; C[&quot;资料层&lt;br&#x2F;&gt;references&#x2F;&quot;]    B --&gt; D[&quot;执行层&lt;br&#x2F;&gt;scripts&#x2F; tools&quot;]  </pre></div><p>这一步一旦做好，AI 的行为会稳定很多。</p><h3 id="3-工具闭环能力">3. 工具闭环能力</h3><p>AI 真正有战斗力，不是因为它能“猜”，而是因为它能“查”和“做”。</p><p>所以 <code>Superpowers</code> 很重要的一环，就是把它和真实工具链接起来。</p><p>比如：</p><ul><li>用 shell 查代码、跑测试、起服务</li><li>用脚本做统计、同步、校验</li><li>用 MCP 去读平台资源、文档、数据库、监控</li><li>用 Git 操作让结果真正落盘</li></ul><p>你会发现，一旦 AI 不只是生成文本，而是能进入工具闭环，它就开始像一个执行系统了。</p><h3 id="4-多-Agent-协作能力">4. 多 Agent 协作能力</h3><p>很多任务不是一个脑袋就能顺滑做完的。</p><p>比如一篇完整的技术文章，常常可以拆成：</p><ul><li>一个 Agent 负责梳理主题结构</li><li>一个 Agent 负责搜集现有仓库和文章联动</li><li>一个 Agent 负责检查 Hexo 渲染和链接是否正常</li></ul><p>这就是为什么我会把 <code>Superpowers</code> 和 <code>Agent Team</code> 放在同一条链路里。</p><p>它们不是同一个概念，但天然互补：</p><ul><li><code>Superpowers</code> 解决“单个 Agent 怎么更会做事”</li><li><code>Agent Team</code> 解决“多个 Agent 怎么分工协作”</li></ul><h3 id="5-治理和沉淀能力">5. 治理和沉淀能力</h3><p>这一层其实最容易被低估。</p><p>很多人会把一次成功的 AI 使用经验，当作“这次运气不错”。</p><p>但如果你想让它长期可复用，就得继续往后走：</p><ul><li>把方法写成 <code>Skill</code></li><li>把脚本放进 <code>scripts/</code></li><li>把说明写进文档</li><li>把使用记录沉淀成统计</li><li>把常见入口做成标准工作流</li></ul><p>从这个角度看，<code>Superpowers</code> 不是一招鲜，而是一套可以持续积累的能力系统。</p><hr><h2 id="四、它和-Skill、Rules、MCP、Agent-Team-分别是什么关系">四、它和 Skill、Rules、MCP、Agent Team 分别是什么关系</h2><p>这一块特别容易混，我直接用一句话版本先讲：</p><ul><li><code>Rules</code>：长期静态背景</li><li><code>Skill</code>：某类任务的能力说明书</li><li><code>MCP / Tools</code>：让 AI 能查、能做、能执行</li><li><code>Superpowers</code>：把上面这些东西真正组织起来的使用方法</li><li><code>Agent Team</code>：把多个 Agent 编排成协作流水线</li></ul><h3 id="一张图看懂它们的分工">一张图看懂它们的分工</h3><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart TD    A[&quot;Rules&lt;br&#x2F;&gt;长期背景&quot;] --&gt; E[&quot;Superpowers&lt;br&#x2F;&gt;组织与调度&quot;]    B[&quot;Skills&lt;br&#x2F;&gt;任务能力&quot;] --&gt; E    C[&quot;MCP &#x2F; Tools&lt;br&#x2F;&gt;查询与执行&quot;] --&gt; E    D[&quot;Memory &#x2F; Repo&lt;br&#x2F;&gt;知识与沉淀&quot;] --&gt; E    E --&gt; F[&quot;单 Agent 稳定产出&quot;]    F --&gt; G[&quot;Agent Team 多角色协作&quot;]  </pre></div><p>如果非要说它们谁更底层，我会这么理解：</p><ul><li><code>Rules</code> 负责“别跑偏”</li><li><code>Skill</code> 负责“这类事怎么做”</li><li><code>Tool</code> 负责“真干活靠什么”</li><li><code>Superpowers</code> 负责“把这几样按流程串起来”</li></ul><p>所以 <code>Superpowers</code> 更像是一套编排思想，而不是单一组件。</p><hr><h2 id="五、真实使用里，我会怎么给-Claude-Code-Codex-上-Superpowers">五、真实使用里，我会怎么给 Claude Code / Codex 上 Superpowers</h2><p>这一段是最偏实操的，也是最容易直接照着做的。</p><h3 id="第一步：全局-Rules-只保留长期有效的信息">第一步：全局 Rules 只保留长期有效的信息</h3><p>不要把所有操作细节都塞进全局 Rules。</p><p>全局层更适合放：</p><ul><li>仓库基本背景</li><li>协作习惯</li><li>风格约束</li><li>安全边界</li><li>输出要求</li></ul><p>一句话说就是：<strong>放长期有效、跨任务通用、每次都可能用到的东西。</strong></p><h3 id="第二步：把任务方法拆成-Skills">第二步：把任务方法拆成 Skills</h3><p>别把“如何做代码审查”“如何查日志”“如何写技术方案”全塞进一个大 Prompt。</p><p>把它们拆成独立 Skill 的好处是：</p><ul><li>描述更清楚</li><li>触发更精准</li><li>可以挂对应脚本和参考资料</li><li>后续能独立演进</li></ul><h3 id="第三步：给常见任务做路由入口">第三步：给常见任务做路由入口</h3><p>比如：</p><ul><li>日常开发走一个入口</li><li>JIRA 闭环走一个入口</li><li>排障诊断走一个入口</li><li>文档写作走一个入口</li></ul><p>这样 AI 不用每次从零猜你的意图。</p><h3 id="第四步：能用脚本的地方，尽量别靠模型硬编">第四步：能用脚本的地方，尽量别靠模型硬编</h3><p>这是一个非常重要的经验。</p><p>如果某个动作本来就能脚本化，比如：</p><ul><li>查询统计</li><li>生成模板</li><li>同步文件</li><li>执行标准检查</li></ul><p>那就尽量让 AI 去调脚本，而不是自己现场“想象一个做法”。</p><p>这样稳定性会高很多。</p><h3 id="第五步：把结果反向沉淀">第五步：把结果反向沉淀</h3><p>每次遇到这些场景，都值得考虑是否沉淀：</p><ul><li>某个提示词反复有效</li><li>某条执行顺序总是有用</li><li>某个脚本特别省事</li><li>某类问题总会重复出现</li></ul><p>这时候最好的动作不是收藏聊天记录，而是把它升格成：</p><ul><li>Rules</li><li>Skill</li><li>Script</li><li>Template</li><li>文档</li></ul><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart TD    A[&quot;一次成功任务&quot;] --&gt; B[&quot;提炼稳定做法&quot;]    B --&gt; C[&quot;写入 Skill &#x2F; Script &#x2F; Doc&quot;]    C --&gt; D[&quot;后续任务继续复用&quot;]    D --&gt; E[&quot;再次优化与治理&quot;]  </pre></div><p>这条闭环一旦转起来，AI 的成长就不再只靠记忆，而是靠外部化沉淀。</p><!-- 图位 2：一次真实执行截图 --><!-- 这里适合放 Claude Code / Codex 调起 skill、执行命令、完成交付的会话截图 --><hr><h2 id="六、结合我自己的-dev-skill-仓库，Superpowers-是怎么落地的">六、结合我自己的 dev-skill 仓库，Superpowers 是怎么落地的</h2><p>如果只讲概念，<code>Superpowers</code> 很容易显得有点虚。</p><p>但放到我自己的 <a href="https://gitlab.qima-inc.com/shijiahao_0802/dev-skill"><code>dev-skill</code></a> 仓库里，它就会非常具体。</p><p>我现在更愿意把这个仓库看成一套“能力中台”：</p><ul><li><code>skills/dev/</code> 负责开发主流程</li><li><code>skills/cr/</code> 负责代码审查和测试相关流程</li><li><code>skills/zan/</code> 负责有赞内部平台能力接入</li><li><code>skills/platform/</code> 负责安装、同步、统计、工作树等平台支撑</li><li><code>skills/writing/</code> 负责文档、图表、PDF、方案协作</li></ul><p>这其实就是把 <code>Superpowers</code> 变成了工程化结构。</p><h3 id="它不是“很多-Skill-的堆叠”">它不是“很多 Skill 的堆叠”</h3><p>更准确地说，它在做三件事：</p><ol><li>把高频任务拆成标准能力单元</li><li>把能力单元接到真实脚本和平台上</li><li>把使用过程继续纳入统计、治理和迭代</li></ol><h3 id="一条典型链路长什么样">一条典型链路长什么样</h3><p>比如一个“JIRA -&gt; 排查 -&gt; 改代码 -&gt; CR”的闭环，大致可以是这样：</p><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart LR    A[&quot;用户提需求 &#x2F; 给出 JIRA&quot;] --&gt; B[&quot;路由到对应 workflow&quot;]    B --&gt; C[&quot;读取相关 skill 说明&quot;]    C --&gt; D[&quot;调用日志 &#x2F; trace &#x2F; jira &#x2F; repo 工具&quot;]    D --&gt; E[&quot;产出修改建议或代码改动&quot;]    E --&gt; F[&quot;触发 review &#x2F; test &#x2F; 文档沉淀&quot;]  </pre></div><p>你会发现，这已经不是“问一句答一句”的玩法了。</p><p>它更像一条围绕 AI 构建的微型研发流水线。</p><hr><h2 id="七、哪些场景最值得用-Superpowers">七、哪些场景最值得用 Superpowers</h2><p>如果你刚开始搭，不用一上来就全都做。</p><p>我更建议从这 4 类任务开始：</p><h3 id="1-高频重复任务">1. 高频重复任务</h3><p>比如：</p><ul><li>提交代码</li><li>起分支</li><li>写周报</li><li>查 JIRA</li><li>写技术方案模板</li></ul><p>这类任务最适合先做标准化。</p><h3 id="2-容易出错的流程任务">2. 容易出错的流程任务</h3><p>比如：</p><ul><li>多仓库联动提交</li><li>子模块提交顺序</li><li>发布前检查</li><li>排障链路分析</li></ul><p>因为这些任务最需要“别漏步骤”。</p><h3 id="3-对上下文依赖很强的任务">3. 对上下文依赖很强的任务</h3><p>比如：</p><ul><li>业务问题排查</li><li>大型项目代码修改</li><li>架构文档补全</li></ul><p>这类任务只靠临场对话很容易失真。</p><h3 id="4-适合长期积累的方法任务">4. 适合长期积累的方法任务</h3><p>比如：</p><ul><li>Code Review</li><li>技术文章写作</li><li>测试策略生成</li><li>知识沉淀与复盘</li></ul><p>因为做得越多，后面的复用收益越高。</p><hr><h2 id="八、一个很重要的认知：Superpowers-不是魔法，是工程">八、一个很重要的认知：Superpowers 不是魔法，是工程</h2><p>我很想强调这一点。</p><p>很多人听到 <code>Superpowers</code>，容易下意识理解成：</p><blockquote><p>有没有什么神奇配置，一开就能让 AI 突然变得特别厉害？</p></blockquote><p>真实情况恰恰相反。</p><p>它更像软件工程：</p><ul><li>你要拆分职责</li><li>要控制上下文</li><li>要规范输入输出</li><li>要把可自动化的部分外置</li><li>要给能力做版本化和治理</li></ul><p>所以它不是“神秘技巧合集”，而是一套很朴素但很有效的工程方法。</p><p>说得再直白一点：</p><blockquote><p>Superpowers 的核心，不是给模型加法术，而是给 AI 接流程、接制度、接工具、接沉淀。</p></blockquote><hr><h2 id="九、如果你也想开始，可以直接照这个最小方案来">九、如果你也想开始，可以直接照这个最小方案来</h2><p>如果你不想一口气搞太重，我建议先从这个最小闭环开始：</p><ol><li>写一份简短但稳定的全局 Rules</li><li>为 3 个最高频任务拆出独立 Skill</li><li>每个 Skill 只挂最必要的文档和脚本</li><li>给这 3 个任务各做一个清晰入口</li><li>每周回看一次，补沉淀、删噪音、修描述</li></ol><p>这个版本就已经足够有感知了。</p><h3 id="你会明显感受到的变化">你会明显感受到的变化</h3><ul><li>AI 不再每次都要重头讲规则</li><li>它更容易走进正确工作流</li><li>产出结果更稳定</li><li>你自己也更容易复盘“为什么这次做得好”</li></ul><!-- 图位 3：最小可用方案示意图 --><!-- 这里适合放一个你的 skill 目录、入口命令和执行结果拼图 --><hr><h2 id="十、最后一句话">十、最后一句话</h2><p>如果用一句话总结我理解里的 <code>Superpowers</code>，我会这么说：</p><blockquote><p>它是把 AI 从“聪明回答者”升级成“流程化执行者”的那套方法。</p></blockquote><p>它不等于某个产品功能，也不等于某个单独 Skill。<br>它是一整套围绕：</p><ul><li>上下文管理</li><li>能力路由</li><li>工具执行</li><li>经验沉淀</li><li>协作治理</li></ul><p>建立起来的工作方式。</p><p>而一旦这套东西跑顺了，AI 才真的开始像一个能长期共事的开发 Agent。</p><h2 id="延伸阅读">延伸阅读</h2><ul><li>如果你想先理解 <code>Skill</code> 本身是什么，可以读 <a href="/posts/skill-literacy-dev-skill-workflow.html">Skill扫盲：从提示词补丁到AI工作流，我如何用dev-skill把经验沉淀成能力系统</a>。</li><li>如果你想看多 Agent 怎么串起协作，可以读 <a href="/posts/agent-team-tutorial-tmux-workflow.html">Agent Team使用教程：用Tmux分屏把多个AI串成自动协作流水线</a>。</li><li>如果你想看这一套能力如何进一步仓库化、治理化，可以读 <a href="/posts/dev-skill-repo-panorama-ai-infrastructure.html">dev-skill仓库全景：一套技能体系如何把AI Coding变成团队基础设施</a>。</li><li>如果你想继续往底层看 harness、memory、skill 的工程结构，可以读 <a href="/posts/claude-code-harness-rag-skill-memory.html">Claude Code Harness深度分析：从RAG、Skill、Memory到企业Agent落地</a>。</li></ul>]]></content>
    
    
    <summary type="html">从方法论、工作流编排到具体使用方式，系统解释我理解里的 Superpowers 是什么，它和 Skill、Agent Team、dev-skill 分别是什么关系，以及怎样把 Claude Code / Codex 从聊天助手升级成会按流程做事的开发 Agent。</summary>
    
    
    
    <category term="技术笔记" scheme="https://u7u7.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="AI" scheme="https://u7u7.top/tags/AI/"/>
    
    <category term="Agent" scheme="https://u7u7.top/tags/Agent/"/>
    
    <category term="Claude Code" scheme="https://u7u7.top/tags/Claude-Code/"/>
    
    <category term="Skill" scheme="https://u7u7.top/tags/Skill/"/>
    
    <category term="Codex" scheme="https://u7u7.top/tags/Codex/"/>
    
    <category term="Workflow" scheme="https://u7u7.top/tags/Workflow/"/>
    
    <category term="Superpowers" scheme="https://u7u7.top/tags/Superpowers/"/>
    
  </entry>
  
  <entry>
    <title>dev-skill：一个可公开分享的 DIY Agent Skills 工具箱</title>
    <link href="https://u7u7.top/posts/dev-skill-repo-panorama-ai-infrastructure.html"/>
    <id>https://u7u7.top/posts/dev-skill-repo-panorama-ai-infrastructure.html</id>
    <published>2026-07-31T12:30:00.000Z</published>
    <updated>2026-08-04T16:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<h1 id="dev-skill：一个可公开分享的-DIY-Agent-Skills-工具箱">dev-skill：一个可公开分享的 DIY Agent Skills 工具箱</h1><blockquote><p>项目地址：<a href="https://github.com/u7-u7/dev-skills">GitHub · dev-skills</a> · <a href="https://gitee.com/u7u7-sjh/dev-skill">Gitee · dev-skill</a></p></blockquote><p>我做这个项目的出发点很朴素：把自己在需求分析、开发、审查和测试里反复使用的方法，整理成能被 AI 工具识别、安装和组合调用的 Skill。</p><p>dev-skill 是一个个人维护、可以公开分享的轻量仓库。它不是提示词大杂烩，也不试图包装成万能的“AI 操作系统”；更像一个 DIY Agent Skills 工具箱：每个 Skill 有明确职责，既可以单独使用，也可以沿着一条研发工作流协作。</p><p>仓库源码、安装说明和每个 Skill 的具体约束，都以 <a href="https://github.com/u7-u7/dev-skills">GitHub 仓库</a> 为准；也可通过 <a href="https://gitee.com/u7u7-sjh/dev-skill">Gitee 镜像</a> 访问。</p><hr><h2 id="一、它想解决什么问题">一、它想解决什么问题</h2><p>很多 AI Coding 的经验最初都很零散：</p><ul><li>某个好用的提示词只留在聊天记录里；</li><li>同一套规则要在不同客户端重复配置；</li><li>需求、实现、审查之间缺少连续上下文；</li><li>工作流跑顺后，下一次又得从头解释一遍。</li></ul><p>dev-skill 想做的，是把这些一次性的经验变成可复用、可安装、可迭代的能力单元。你可以把它理解为：</p><blockquote><p>把“我平时怎么让 AI 帮我做研发”沉淀成一套能继续演进的项目资产。</p></blockquote><p>它更适合希望把个人工作流整理清楚的开发者；仓库刻意不包含组织专用集成、私有凭据或生产数据，文档示例也使用中性命名，方便阅读和分享。</p><hr><h2 id="二、一张图看懂项目结构">二、一张图看懂项目结构</h2><p>dev-skill 目前不是一个庞大的平台，而是一套围绕研发过程组织起来的 12 个 Skill。它们大致分成四层：先把需求想清楚，再把工程上下文找全，然后进入实现，最后用审查和测试把风险收住。</p><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart TB    subgraph DEV[&quot;Development：把需求变成可执行开发任务&quot;]        BRAIN[&quot;brainstorming&lt;br&#x2F;&gt;澄清目标、约束和方案选择&quot;]        SCANNER[&quot;app-scanner&lt;br&#x2F;&gt;扫描多应用工作区，提取业务能力&quot;]        SDD[&quot;sdd-dev-workflow&lt;br&#x2F;&gt;产出 requirements &#x2F; design &#x2F; plan &#x2F; tasks&quot;]        PAIR[&quot;ai-pair-programmer&lt;br&#x2F;&gt;结对实现、修复、重构和验证&quot;]    end    subgraph REVIEW[&quot;Review：把改动变成可放心交付的结果&quot;]        AUTHOR[&quot;author-final-review&lt;br&#x2F;&gt;审查单一作者最终态变更&quot;]        CODE[&quot;code-review&lt;br&#x2F;&gt;检查质量、安全、性能和架构风险&quot;]        TEST[&quot;integration-test&lt;br&#x2F;&gt;按风险生成集成测试用例&quot;]        FULL[&quot;full-review&lt;br&#x2F;&gt;汇总代码审查、影响面和测试设计&quot;]        TEAM[&quot;team-cr&lt;br&#x2F;&gt;准备多人 CR 会议讨论清单&quot;]    end    subgraph VIS[&quot;Visualization：把复杂信息画清楚&quot;]        DIAGRAM[&quot;diagram-creation&lt;br&#x2F;&gt;生成流程图、架构图、时序图、路线图&quot;]    end    subgraph PLATFORM[&quot;Platform：管理工作环境和使用统计&quot;]        WORKTREE[&quot;git-worktree&lt;br&#x2F;&gt;管理并行开发 worktree&quot;]        TRACKER[&quot;skill-usage-tracker&lt;br&#x2F;&gt;记录技能使用统计&quot;]    end    BRAIN --&gt; SDD    SCANNER --&gt; SDD    SDD --&gt; PAIR    PAIR --&gt; AUTHOR    AUTHOR --&gt; CODE    AUTHOR --&gt; TEST    CODE --&gt; FULL    TEST --&gt; FULL    FULL --&gt; TEAM    DIAGRAM -. &quot;可插入任意阶段&quot; .-&gt; SDD    WORKTREE -. &quot;隔离分支环境&quot; .-&gt; PAIR    TRACKER -. &quot;记录每次 Skill 调用&quot; .-&gt; BRAIN  </pre></div><p>这张图可以先当成项目地图：Development 负责把“我要做什么”变成“我知道怎么做”；Review 负责把“做完了”变成“风险可解释”；Visualization 和 Platform 则是横向能力，一个负责表达复杂关系，一个负责让工具链可维护。</p><hr><h2 id="三、12-个-Skill-分别做什么">三、12 个 Skill 分别做什么</h2><p>为了让读者不用跳到仓库里翻目录，下面按真实仓库中的 Skill 逐个说明。</p><table><thead><tr><th>分类</th><th>Skill</th><th>适合什么时候用</th><th>主要产物</th></tr></thead><tbody><tr><td>Development</td><td><code>brainstorming</code></td><td>需求还不够清楚，或者有多个方案需要取舍</td><td>经确认的目标、边界和设计方向</td></tr><tr><td>Development</td><td><code>app-scanner</code></td><td>工作区里有多个前后端应用，需要先摸清业务能力和依赖关系</td><td>应用能力清单、API / 数据模型 / 外部依赖概览</td></tr><tr><td>Development</td><td><code>sdd-dev-workflow</code></td><td>已有原始需求、PRD 或想法，需要整理成可开发任务</td><td><code>requirements.md</code>、<code>design.md</code>、<code>plan.md</code>、<code>tasks.md</code></td></tr><tr><td>Development</td><td><code>ai-pair-programmer</code></td><td>已经明确要写代码、改 bug、重构或补测试</td><td>符合项目风格的代码改动和验证结果</td></tr><tr><td>Review</td><td><code>author-final-review</code></td><td>想审查某个作者在一个范围内的最终代码状态</td><td>面向 Bug 和风险的最终态审查结果</td></tr><tr><td>Review</td><td><code>code-review</code></td><td>需要系统检查代码质量、安全、性能、架构和兼容性</td><td>结构化代码审查报告</td></tr><tr><td>Review</td><td><code>integration-test</code></td><td>Java Dubbo 变更需要补集成测试设计</td><td>P0 / P1 测试用例、入参、预期输出和覆盖追溯</td></tr><tr><td>Review</td><td><code>full-review</code></td><td>希望一次性拿到审查、影响面和测试设计的完整报告</td><td>完整审查报告与风险覆盖结果</td></tr><tr><td>Review</td><td><code>team-cr</code></td><td>多人、多模块改动要开集体 CR</td><td>按风险等级和模块分组的会议讨论清单</td></tr><tr><td>Visualization</td><td><code>diagram-creation</code></td><td>文字讲不清流程、架构、时序或学习路线</td><td>Mermaid、PlantUML 等图表源码</td></tr><tr><td>Platform</td><td><code>git-worktree</code></td><td>多分支并行开发，或者需要隔离工作目录</td><td>标准化 worktree 目录和迁移建议</td></tr><tr><td>Platform</td><td><code>skill-usage-tracker</code></td><td>执行技能前记录使用，或查看本地统计</td><td>写入 <code>~/usage_stats.json</code> 的技能使用数据</td></tr></tbody></table><p>每个 Skill 都放在 <code>skills/&lt;分类&gt;/&lt;skill 名&gt;/</code> 下，以 <code>SKILL.md</code> 说明触发条件、输入输出和执行边界；需要时还会携带脚本、参考资料或模板。这样的结构让“能力说明”和“实际工具”留在同一个可追溯的位置。</p><hr><h2 id="四、开发中哪些环节可以用到这些-Skill">四、开发中哪些环节可以用到这些 Skill</h2><p>光看 Skill 名字还不够直观，我们换成一个真实开发例子：<strong>给订单系统增加“优惠券抵扣”能力</strong>。</p><p>这类需求通常不会直接进入编码。它会经历需求澄清、影响面扫描、方案拆解、代码实现、测试设计、代码审查和团队评审几个阶段。dev-skill 的价值，就是把这些阶段拆开，每一步用合适的 Skill 接住。</p><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart TD    START[&quot;需求例子&lt;br&#x2F;&gt;订单系统支持优惠券抵扣&quot;]    subgraph PHASE1[&quot;阶段 1：想清楚要做什么&quot;]        BRAIN[&quot;brainstorming&lt;br&#x2F;&gt;澄清优惠券规则、边界和验收口径&quot;]        OUT1[&quot;产物&lt;br&#x2F;&gt;哪些券可用、如何叠加、异常怎么处理&quot;]    end    subgraph PHASE2[&quot;阶段 2：找到会被影响的代码&quot;]        SCAN[&quot;app-scanner&lt;br&#x2F;&gt;多应用场景下扫描订单、营销、支付等应用&quot;]        SDD[&quot;sdd-dev-workflow&lt;br&#x2F;&gt;结合真实代码证据整理方案&quot;]        OUT2[&quot;产物&lt;br&#x2F;&gt;requirements &#x2F; design &#x2F; plan &#x2F; tasks&quot;]    end    subgraph PHASE3[&quot;阶段 3：隔离环境并开始实现&quot;]        WT[&quot;git-worktree&lt;br&#x2F;&gt;为优惠券需求创建独立工作区&quot;]        PAIR[&quot;ai-pair-programmer&lt;br&#x2F;&gt;修改接口、领域逻辑、测试和配置&quot;]        OUT3[&quot;产物&lt;br&#x2F;&gt;可运行的代码改动与验证结果&quot;]    end    subgraph PHASE4[&quot;阶段 4：把风险收住&quot;]        AUTHOR[&quot;author-final-review&lt;br&#x2F;&gt;先审查本次作者最终态变更&quot;]        CODE[&quot;code-review&lt;br&#x2F;&gt;检查兼容性、金额计算、安全和性能风险&quot;]        TEST[&quot;integration-test&lt;br&#x2F;&gt;生成 P0 &#x2F; P1 集成测试用例&quot;]        FULL[&quot;full-review&lt;br&#x2F;&gt;汇总审查结论、影响面和测试覆盖&quot;]    end    subgraph PHASE5[&quot;阶段 5：准备协作交付&quot;]        DIAGRAM[&quot;diagram-creation&lt;br&#x2F;&gt;画调用链、状态流转或优惠券计算流程&quot;]        TEAM[&quot;team-cr&lt;br&#x2F;&gt;整理多人 CR 会议讨论清单&quot;]        TRACK[&quot;skill-usage-tracker&lt;br&#x2F;&gt;记录技能使用，沉淀本地统计&quot;]    end    START --&gt; BRAIN --&gt; OUT1 --&gt; SCAN --&gt; SDD --&gt; OUT2    OUT2 --&gt; WT --&gt; PAIR --&gt; OUT3    OUT3 --&gt; AUTHOR --&gt; CODE --&gt; TEST --&gt; FULL    FULL --&gt; DIAGRAM --&gt; TEAM    BRAIN -. &quot;每次调用前记录&quot; .-&gt; TRACK    PAIR -. &quot;每次调用前记录&quot; .-&gt; TRACK    FULL -. &quot;每次调用前记录&quot; .-&gt; TRACK    classDef demand fill:#34495E,stroke:#2C3E50,color:#FFFFFF,stroke-width:2px    classDef discover fill:#E3F2FD,stroke:#1976D2,color:#0D47A1,stroke-width:2px    classDef build fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20,stroke-width:2px    classDef review fill:#FFF3E0,stroke:#F57C00,color:#E65100,stroke-width:2px    classDef collab fill:#F3E5F5,stroke:#8E24AA,color:#4A148C,stroke-width:2px    classDef output fill:#FAFAFA,stroke:#757575,color:#212121,stroke-width:1px    class START demand    class BRAIN,SCAN,SDD discover    class WT,PAIR build    class AUTHOR,CODE,TEST,FULL review    class DIAGRAM,TEAM,TRACK collab    class OUT1,OUT2,OUT3 output  </pre></div><p>这张图背后的使用方式可以再翻译成大白话：</p><table><thead><tr><th>开发环节</th><th>可以用的 Skill</th><th>它帮你解决什么</th></tr></thead><tbody><tr><td>需求刚来</td><td><code>brainstorming</code></td><td>不急着写代码，先问清规则、边界、验收标准</td></tr><tr><td>需求涉及多个应用</td><td><code>app-scanner</code></td><td>找出订单、营销、支付等应用分别负责什么</td></tr><tr><td>准备进入开发</td><td><code>sdd-dev-workflow</code></td><td>把需求、代码证据、技术方案和任务拆分整理出来</td></tr><tr><td>多分支并行</td><td><code>git-worktree</code></td><td>给当前需求隔离一个干净工作区</td></tr><tr><td>正式编码</td><td><code>ai-pair-programmer</code></td><td>在明确授权后改代码、补测试、跑验证</td></tr><tr><td>自查或交付前</td><td><code>author-final-review</code>、<code>code-review</code></td><td>找出最终态代码里的 Bug、兼容性和架构风险</td></tr><tr><td>测试设计</td><td><code>integration-test</code></td><td>根据风险点生成优先级测试用例</td></tr><tr><td>汇总交付</td><td><code>full-review</code></td><td>把审查、影响面、测试覆盖整理成完整报告</td></tr><tr><td>讲给别人听</td><td><code>diagram-creation</code>、<code>team-cr</code></td><td>画流程图，准备集体 CR 的重点讨论项</td></tr></tbody></table><p>这里有一个我很看重的边界：<strong>实现不是自动越权发生的。</strong> 前面的 Skill 负责弄清需求、建立事实和产出方案；只有用户明确授权后，才进入代码修改与验证。</p><p>对开发者来说，这比“让 AI 一次性写完”更可控：每一步都有对应的上下文、检查点和产物。</p><hr><h2 id="五、如何安装并开始体验">五、如何安装并开始体验</h2><p>项目通过 profile 管理默认安装清单。macOS / Linux 用户在仓库根目录依次执行：</p><pre><code>make audit-skillsmake install-dry-runmake install</code></pre><ul><li>make audit-skills：检查 Skill 数量、重名和职责重叠；</li><li>make install-dry-run：预览同步计划，不修改任何文件；</li><li>make install：正式同步 config/profiles/diy.skills 中定义的 Skill。</li></ul><p>Windows PowerShell 5.1+ 可执行：</p><pre><code>powershell -NoProfile -ExecutionPolicy Bypass -File .\install.ps1 previewpowershell -NoProfile -ExecutionPolicy Bypass -File .\install.ps1 install</code></pre><p>安装器会检查每个 Skill 的 <a href="http://SKILL.md">SKILL.md</a>、补齐其 depends_on 依赖，并只处理当前机器上实际检测到的 Cursor、Claude Code 与 Codex 客户端。未安装的客户端会被安全跳过。</p><hr><h2 id="六、为什么更新能保持一致">六、为什么更新能保持一致</h2><p>仓库只维护一份 Skill 源码，客户端通过链接引用它：</p><ul><li>macOS / Linux 使用软链接；</li><li>Windows 使用 Junction；</li><li>修改仓库内的 Skill 后，已链接的客户端可以直接读到新版本。</li></ul><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart LR    A[&quot;skills&#x2F;&lt;分类&gt;&#x2F;&lt;skill&gt;&lt;br&#x2F;&gt;唯一源码&quot;] --&gt; B[&quot;安装器&quot;]    C[&quot;config&#x2F;profiles&#x2F;diy.skills&lt;br&#x2F;&gt;默认安装清单&quot;] --&gt; B    B --&gt; D[&quot;Cursor&quot;]    B --&gt; E[&quot;Claude Code&quot;]    B --&gt; F[&quot;Codex&quot;]  </pre></div><p>这避免了把同一份内容复制到多个工具目录后逐渐失控的问题。安装时若遇到同名冲突，也可以选择跳过、重命名或确认后覆盖；卸载只会移除该仓库创建的链接，不会删除 Skill 源码。</p><hr><h2 id="七、给想深入的人：它如何治理自己">七、给想深入的人：它如何治理自己</h2><p>一个 Skill 仓库能否长期维护，不只取决于写了多少能力，还取决于有没有把质量和边界放进流程。</p><ul><li>scripts/audit-skills 用于审计数量、重名与功能重叠；</li><li>skill-usage-tracker 将本地使用统计写入 ~/usage_stats.json，可配合看板查看；</li><li>任何 Skill 执行前都要先记录使用，避免统计只靠人工回忆；</li><li>强制覆盖和删除源码属于高风险操作，推荐先使用 dry-run 核对影响范围。</li></ul><p>我目前采用的安全同步顺序很简单：</p><pre><code>审计 → dry-run → 处理冲突 → 安装 → 检查链接</code></pre><p>这套流程不复杂，但能让工具箱在持续增加 Skill 时，仍保持来源清晰、更新可控、问题可回退。</p><hr><h2 id="八、从哪里开始">八、从哪里开始</h2><p>如果你第一次打开这个项目，建议这样浏览：</p><ol><li>先读 <a href="https://gitee.com/u7u7-sjh/dev-skill">README</a>，了解项目定位和安装方式；</li><li>再看 skills/README.md，了解不同 Skill 的职责；</li><li>选一个真实场景，再阅读对应 Skill 的 <a href="http://SKILL.md">SKILL.md</a>。</li></ol><p>这篇文章讲的是项目本身；如果你想先理解 Skill 为什么不只是“提示词补丁”，可以继续读：</p><ul><li><a href="/posts/skill-literacy-dev-skill-workflow.html">Skill扫盲：从提示词补丁到AI工作流，我如何用dev-skill把经验沉淀成能力系统</a></li><li><a href="/posts/superpowers-guide-coding-agent-workflow.html">Superpowers使用指南：把Claude Code和Codex变成真正会按流程工作的开发Agent</a></li></ul><p>也欢迎直接从 <a href="https://github.com/u7-u7/dev-skills">dev-skills GitHub 仓库</a> 开始，按自己的工作流挑一个 Skill 试用。</p>]]></content>
    
    
    <summary type="html">介绍个人维护的开源 dev-skill 项目：12 个可组合的 Agent Skills、跨客户端安全同步方式，以及从需求到审查的工程化工作流。</summary>
    
    
    
    <category term="技术笔记" scheme="https://u7u7.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="AI" scheme="https://u7u7.top/tags/AI/"/>
    
    <category term="Claude Code" scheme="https://u7u7.top/tags/Claude-Code/"/>
    
    <category term="Skill" scheme="https://u7u7.top/tags/Skill/"/>
    
    <category term="Codex" scheme="https://u7u7.top/tags/Codex/"/>
    
    <category term="Workflow" scheme="https://u7u7.top/tags/Workflow/"/>
    
    <category term="DevSkill" scheme="https://u7u7.top/tags/DevSkill/"/>
    
  </entry>
  
  <entry>
    <title>2026年07月工作日报</title>
    <link href="https://u7u7.top/posts/work-daily-2026-07.html"/>
    <id>https://u7u7.top/posts/work-daily-2026-07.html</id>
    <published>2026-06-30T16:00:00.000Z</published>
    <updated>2026-07-30T04:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<h1 id="2026年07月工作日报-📌">2026年07月工作日报 📌</h1><p>这里按天记录工作内容、遇到的问题和当天的一点想法，方便月底回看。</p><!-- daily-report:2026-07-20:start --><h2 id="2026-07-20-今天做了啥？-✨">2026/07/20 今天做了啥？ ✨</h2><p>今天主要是在把 7 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容">✅ 工作内容</h3><ul><li>推进 POS 刷卡返回 0 排查，确认 <code>posBankCardPay=1</code> 只代表开关透出，真正发起还要看 <code>retail-trade-core</code> 支付链路是否带全 <code>POS_BANK_CARD</code> 和终端信息；当前重点收敛到发布状态、资产侧开关和支付参数透传边界。</li><li>推进 <code>ONLINE-980193</code> 门店 POI 问题排查，已从 JIRA 抽到 trace 和时间窗，但没有直接命中 app；后续需要继续用 trace 和关键词双向补证，把问题落到具体仓库和模块。</li><li>核查 <code>OrderOperateOpenService.addRemark</code> 类型异常，确认本地业务源码里没有直接调用点，问题更像发生在编译依赖、运行时 Dubbo 泛化或开放服务代理层；目前还需要方法签名和参数位置证据来判断是否为接口版本差异。</li><li>梳理 <code>code-sentry</code> 记忆召回链路，确认分析 run 里会做 <code>pre_recall</code> 和 <code>evidence_enhanced</code> 两次召回，命中的生效记忆会进入右侧“记忆召回”块，而不是 Timeline 里的独立步骤。</li><li>对齐 <code>code-sentry</code> 记忆展示和规则分层，明确记忆不应该只暴露底层字段，更要说明“什么情况下命中、命中后注入什么、分析时应该怎么用”；同时把“发布后错误不等于发布引入”沉淀成全局分析规则。</li><li>解析发布分析 prompt 的记忆注入方式，确认当前不是把整库全量塞入，而是先召回相关经验再完整注入；同时发现两条 NPE 经验与本次 <code>PurchaseSuggestService</code> 错误关联偏弱，后续要优化召回精度、去重和可观测性。</li><li>确认 GitLab 地址修改边界和本机 Git 提交身份，明确截图里改的是项目路径不是 Group 路径；当前仓库生效的提交名和邮箱来自全局 <code>.gitconfig</code>。</li></ul><h3 id="🧩-困难-需要帮助">🧩 困难/需要帮助</h3><ul><li>POS 刷卡和 <code>ONLINE-980193</code> 还缺更完整的 trace、app 映射和配置/发布侧证据，暂时只能把范围收敛到链路和模块，不能直接下最终根因。</li><li><code>addRemark</code> 类型异常还需要拿到运行时接口签名、依赖版本或 Dubbo 参数位置，才能确认是调用方传参问题还是 API 版本差异。</li><li><code>code-sentry</code> 记忆系统还要继续对齐案例记忆、全局规则和 prompt 注入的边界，尤其要避免弱相关记忆因为异常类型相似而带偏判断。</li></ul><h3 id="💭-感想">💭 感想</h3><p>今天主要是在把几个问题往真实链路和判断口径上收，支付、JIRA、Dubbo 异常都不能只凭现象下结论，还是要回到 trace、配置、接口签名和发布前后对比。</p><p><code>code-sentry</code> 这边的方向也更明确了，记忆不是越多越好，关键是命中条件、注入内容和使用方式要讲清楚。</p><!-- daily-report:2026-07-20:end --><!-- daily-report:2026-07-22:start --><h2 id="2026-07-22-今天做了啥？-✨">2026/07/22 今天做了啥？ ✨</h2><p>今天主要是在把 6 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容-2">✅ 工作内容</h3><ul><li>梳理 PLU 价格取值异常整改，确认 PRD/RFC 里的核心矛盾是 PC 不新增字段却要求后端保留 PLU 标签金额；当前口径收敛为 <code>BCartService.addGoods/sync</code> 统一承接 <code>pluAmount</code>，在商品价和重量就绪后做 <code>&lt;=1分</code> 校验并写入购物车。</li><li>核查 prod 终端绑定消息消费，确认 <code>customer_core_pos_terminal_change</code> 由 <code>retail-trademanager</code> 的 <code>PosTerminalBindConsumer</code> 消费，09:42:52 和 09:43:10 两条 <code>ALLOCATION_CHANGE</code> 已命中接收日志，同窗口没有参数异常、处理失败或重试日志。</li><li>继续追 POS 终端绑定落库细节，确认首次 <code>insert</code> 触发 <code>uniq_provider_sn</code> 唯一键冲突属于幂等 upsert 分支，不等同于消费失败；后续重点放在冲突后的 <code>select + update</code> 是否把绑定状态更新到预期。</li><li>梳理飞书转发字段，确认实际请求体只有 <code>deployer</code>、<code>templateData</code>、<code>feishuGroup</code>、<code>msgType</code> 4 个顶层字段，业务字段都在字符串化的 <code>templateData</code> 里，其中 <code>mrIid</code> 是 GitLab MR 的项目内编号，多 MR 时会拼接展示。</li><li>完成 <code>youzan-cli</code> 使用方式梳理，确认它是覆盖商品、客户、订单、营销、店铺、企微、进销存等领域的 Node.js 命令行工具；首次使用链路是安装、授权、<code>auth status</code> 验证，再通过 <code>--help</code> 和 <code>schema</code> 找到具体查询或导出命令。</li><li>解释 <code>JsonUtils.toJson</code> 和 <code>json2Bean</code> 的边界，确认 <code>toJson</code> 是 Java 对象到 JSON 字符串的序列化，不会校验或修复字符串里的不规范 JSON；如果输入本身是 JSON 文本，更适合走反序列化或显式校验。</li></ul><h3 id="🧩-困难-需要帮助-2">🧩 困难/需要帮助</h3><ul><li>PLU 价格整改还需要继续对齐 PC、前端和后端的字段契约，尤其是 <code>pluAmount</code> 单位、<code>isPlu</code> 复用边界、<code>&gt;1分</code> 时返回 <code>CART_PRICE_ADJUST_ERROR</code> 的一致性。</li><li>终端绑定消费层面已经没有失败日志，但唯一键冲突后的 <code>select + update</code> 落库结果还要继续用数据或 trace 证据确认，避免只停留在“消费者收到消息”这一层。</li><li>飞书转发字段已收敛到请求体和 <code>templateData</code>，后面还要和实际模板展示口径对齐，避免字段存在但卡片侧没有按预期使用。</li></ul><h3 id="💭-感想-2">💭 感想</h3><p>今天主要是在把几个问题往真实代码链路和接口契约上收敛，POS 终端绑定和 PLU 价格都不能只看现象，还是要落到 topic、consumer、DTO 和金额校验点。</p><p>工具使用和飞书转发这两条更偏口径梳理，写清字段来源和使用边界后，后面排查会少很多来回确认。</p><!-- daily-report:2026-07-22:end --><!-- daily-report:2026-07-23:start --><h2 id="2026-07-23-今天做了啥？-✨">2026/07/23 今天做了啥？ ✨</h2><p>今天主要是在把 5 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容-3">✅ 工作内容</h3><ul><li>完成交班优惠计算问题分析，确认多出的 0.01 元来自订单 <code>E20260718204921088300063</code> 的统计口径，不是打印机或外设链路多收；<code>retail-printer/retail-peripheral</code> 只是格式化上游 <code>couponPayAmount</code> 后打印。</li><li>梳理交班统计链路，确认 <code>retail-sync</code> 负责交班期间逐单累计，<code>retail-trademanager.getStaffShiftWorkDetail</code> 负责读取并组装交班详情；其余 10 笔订单与支付流水一致，差异收敛到单笔抖音券核销金额与真实支付金额的 1 分偏差。</li><li>推进扫码点单禁用规格展示排查，确认 <code>retail-trade-misc#searchV2</code> 返回了 33 个 SKU，而扫码点单分组配置里的 <code>hiddenGoodsSkus</code> 为空数组，因此服务端没有按装修隐藏配置过滤规格。</li><li>明确扫码点单问题边界，当前只处理“后端为何返回禁用规格”，不合并 iPad 盘点问题；验收口径收敛为服务端响应中不再包含 <code>disableStatus=1</code> 的 SKU。</li><li>梳理 <code>code-sentry</code> 命名和域名方向，确认产品定位更接近“发布风险分析 + 代码审计工作台”，不适合继续围绕 <code>monitor</code> 命名，也要避开 <code>Sentry</code> 这类已有强品牌词。</li></ul><h3 id="🧩-困难-需要帮助-3">🧩 困难/需要帮助</h3><ul><li>扫码点单还需要继续确认商品中心“禁用规格”和装修页“隐藏规格”的职责边界，尤其是 <code>disableStatus=1</code> 是否必须在 <code>searchV2</code> 服务端统一过滤。</li><li>交班优惠修复方案还要对齐抖音券核销金额、真实支付流水和交班统计三方口径，避免修 1 分差异时影响历史报表或其他券类型。</li><li><code>code-sentry</code> 命名还需要结合域名可注册性、商标风险和现有产品占用继续筛选，避免只从含义上判断。</li></ul><h3 id="💭-感想-3">💭 感想</h3><p>今天主要是在把两个线上问题往真实数据口径和代码链路上收敛：扫码点单要分清商品禁用和装修隐藏，交班优惠要分清核销口径和支付流水。</p><p>这类问题看起来都是“展示不对”或“金额不对”，但最后都要落到具体接口、配置字段、订单和金额来源上，先把边界钉清楚比直接改代码更稳。</p><!-- daily-report:2026-07-23:end --><!-- daily-report:2026-07-25:start --><h2 id="2026-07-25-今天做了啥？-✨">2026/07/25 今天做了啥？ ✨</h2><p>今天主要是在把 6 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容-4">✅ 工作内容</h3><ul><li>推进满减送重复赠送排查，确认 JIRA 指向生产环境、两个 trace、两个店铺和 7 月 24 日时间窗；关键 trace 已命中 iOS <code>RetailHD 8.51.1</code> 调用 <code>increaseQueryActivity(activityType=8, activityId=0)</code>，并由 <code>retail-ump.IncreaseQueryServiceImpl</code> 返回活动 <code>4365206090</code> 的赠品能力开关信息。</li><li>核对 <code>retail-ump</code> 仓库和数据口径，确认远端是 <code>retail-service/retail-ump.git</code>，工作区干净；表结构里 <code>ability_status</code> 默认值是 <code>1</code>，且 <code>(kdt_id, activity_id)</code> 有唯一键，因此先排除“字段 DDL 默认写 0”这个方向。</li><li>梳理赠品能力开关的代码链路，已命中 <code>PresentDetailQuery</code>、<code>PresentOperateHandler</code>、DAO/MyBatis 和现有单测，问题边界从“重复赠送现象”收敛到活动开关默认值、创建继承逻辑以及总部/门店数据分布。</li><li>定位发布预警分析空结果，确认失败点不是 HTTP 或鉴权错误，而是成功响应后没有最终文本，或连续 12 轮调用耗尽仍没有结论；分析器目前固定只重试 1 次，所以会进入 <code>Retry Exhausted</code>。</li><li>验证跨应用错误日志抓取，确认 <code>retail-trade-core</code> 的 ERROR 和 <code>yz-cardvoucher-biz</code> 的 WARN 可以通过同一个 <code>traceId</code> 关联；但只查单应用不会自动展开跨应用日志，需要显式把 trace 传给后续分析链路。</li><li>梳理 <code>ONLINE-981860</code> 初步边界，确认问题发生在生产环境 <code>retail-trade-misc</code>、店铺 <code>91106488</code>，时间集中在 12:24-12:26，当前没有 traceId；已先把 JIRA 产物落到 <code>.doc/20260725130355-ONLINE-981860/jira</code>，后续用截图、店铺 ID 和稳定日志短语补证。</li></ul><h3 id="🧩-困难-需要帮助-4">🧩 困难/需要帮助</h3><ul><li>满减送问题还需要继续直查目标记录、总部/门店整体分布和近期开关继承结果，才能把默认值、创建链路和实际重复赠送现象完全闭环。</li><li>发布预警空结果还缺线上 run 的原始节点和调用产物；当前账号能看到审计表结构，但 <code>qatools</code> 审计表为空，暂时不能把“调用耗尽”当作最终根因。</li><li><code>ONLINE-981860</code> 缺少 traceId，后面需要从附件截图、JIRA 评论和 12:24-12:26 的生产日志里继续补业务现象和调用链证据。</li></ul><h3 id="💭-感想-4">💭 感想</h3><p>今天主要是在把满减送和发布预警两个问题往真实 trace、数据库默认值和代码分支上收敛，能确认的先写死，缺证据的地方先留边界。</p><p>跨应用日志这块也更清楚了：trace 能把链路串起来，但工具侧的补日志范围、数量和权限还是要讲清楚，不能只说“能查到”。</p><!-- daily-report:2026-07-25:end --><!-- daily-report:2026-07-26:start --><h2 id="2026-07-26-今天做了啥？-✨">2026/07/26 今天做了啥？ ✨</h2><p>今天主要是在把 5 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容-5">✅ 工作内容</h3><ul><li>完成 <code>ONLINE-981928</code> 深度分析，确认取餐状态本身已处理成功，真正断点在 <code>retail-trademanager</code> 获取小程序订阅模板时返回 <code>132900009</code>，后续又被伪模板 ID 放大成微信 <code>40037 invalid template_id</code>，所以顾客没有收到取餐提醒。</li><li>梳理 <code>retail-trade-misc -&gt; retail-trademanager -&gt; msg-push -&gt; wechat-push</code> 的完整链路，确认 <code>MessageClient#getMiniProgramTemplate</code> 在失败时会兜底写入 <code>Default-Mini-Program-Subscription-Template-ID</code>，而 <code>FreeBuyForMealTakeMessageHandler</code> 又无条件继续发送，这是这次问题的关键代码点。</li><li>对照 JIRA 截图、trace 和评论，确认影响对象收敛到父店铺 <code>160734909</code>、子店铺 <code>206598416</code>、订单 <code>E20260725181329036406197</code>，模板范围是 <code>1820 / order_state_meal_take</code>，当前可以排除订单未完成和消息链路整体不可用。</li><li>输出了代码修改方案和验证计划，明确模板缺失时要在公共订阅消息链路直接阻断发送，补齐结构化日志，不再把模板配置缺口包装成“发送成功”。</li><li>整理了技术支持、商家和产品侧的对外口径，统一成“订单正常完成、模板查询失败、微信最终拒发”三层事实，避免只盯着表面消息成功。</li></ul><h3 id="🧩-困难-需要帮助-5">🧩 困难/需要帮助</h3><ul><li>模板中心还没有直接补到 <code>160734909/206598416 + 1820 + order_state_meal_take</code> 的真实记录和授权状态，责任边界只能先停在“待补证”。</li><li>代码层还需要确认是否统一收紧伪模板兜底，避免后续其他订阅消息 handler 也沿用同类错误分支。</li></ul><h3 id="💭-感想-5">💭 感想</h3><p>今天主要是在把问题往真实模板链路和配置边界上收，<code>sendCount=1</code> 这种上游成功信号不能直接等同于用户侧送达。</p><p>这类问题一旦把模板查询失败和微信回执拆开看，链路就清楚很多，后面要补的主要是模板中心证据和统一失败策略。</p><!-- daily-report:2026-07-26:end --><!-- daily-report:2026-07-27:start --><h2 id="2026-07-27-今天做了啥？-✨">2026/07/27 今天做了啥？ ✨</h2><p>今天主要是在把 7 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容-6">✅ 工作内容</h3><ul><li>推进拆条目金额错误排查，确认折扣条目单价 2.38、数量 1 时小计显示 6.38 的问题已在后端回包里出现，不是前端展示字段映射；当前范围收敛到 retail-trade-cart 拆条和营销回包重构条目链路。</li><li>完成 PLU 秤码金额差异分析和修复核对，确认 isPlu=true 且 PLU 金额与计算金额差异 &lt;=1 分时应统一取 pluAmount，目标分支 8 个 *PluAmountTest 已通过；另一个 7.30 元标签价 trace 与商品中心 3.40 元/kg 算价差 11 分，属于条码秤和商品中心价格未对齐，不应走 1 分容差。</li><li>完成 ONLINE-981990 活动叠加异常分析，确认不是 EXCLUDED 配置失效，而是加价购 JS 先按普通商品原价判断门槛，随后又从整车商品里挑换购 SKU，导致被排除的清新蓝莓仍被打上 EXCHANGE_GOODS 并改成 5 元。</li><li>查明重开单门店日流水号逻辑，确认重开单分支在 SyncRetailDataProcessor#getSerialNo 里调用 queryCurrentSerialNo(kdtId)，只读取当天门店 Redis 计数器当前值，不会 +1；普通门店单才会走 addSerialNo(kdtId) 自增。</li><li>评审三方券/交班尾差修复方案，确认 1 分差异不是订单实收多收，而是旧接口用“单券金额 * 张数”表达不了 287.67/287.67/287.66 这类分摊尾差；后续要在 Provider 和两个 Consumer 的兼容字段、回退逻辑上一起收口。</li><li>确认 Case 记忆沉淀列表和算法观察页两个线上问题的修复状态，当前用户可见故障仍未修复：前者生产接口仍返回 HTML 500，后者页面只在 feature/release_memory 分支且仍硬编码 /api，放到 /reports 路径下会打到错误网关。</li><li>梳理 CLI 接入 MCP 的授权、店铺选择和会话隔离方案，明确 MVP 先走本地安全文件保存 token + 显式 selected_kdt_id；“输入手机号直接枚举全部可访问店铺”不是当前 token 模型天然提供的能力，先作为后续账号/授权中心能力处理。</li></ul><h3 id="🧩-困难-需要帮助-6">🧩 困难/需要帮助</h3><ul><li>PLU 和拆条金额治理还要继续对齐条码秤价、商品中心价、PC 透传字段和后端计算口径，尤其是 &lt;=1 分取 pluAmount、&gt;1 分阻断的边界要保持一致。</li><li>ONLINE-981990 的修复还需要继续确认门槛判断、排除商品能否作为换购品、会员价后是否需要撤销换购计划这几个边界，避免只修当前蓝莓案例。</li><li>两个线上页面问题还缺最终部署版本和网关路径改造确认，目前只能明确“用户路径仍异常”，服务端根因还需要继续补证。</li></ul><h3 id="💭-感想-6">💭 感想</h3><p>今天主要是在把几个金额和活动问题往真实代码链路、日志 trace 和数据口径上收。PLU、拆条、三方券尾差都不是简单“金额算错”，关键还是要分清标签金额、商品中心价、单值乘数量的表达能力和最终展示字段。</p><p>线上状态核验也要把用户可见故障和服务端根因分开写，能确认的先确认，缺部署和配置证据的地方不要提前下死结论。</p><!-- daily-report:2026-07-27:end --><!-- daily-report:2026-07-28:start --><h2 id="2026-07-28-今天做了啥？-✨">2026/07/28 今天做了啥？ ✨</h2><p>今天主要是在把 6 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容-7">✅ 工作内容</h3><ul><li>完成 PLU 会员价支付校验问题分析，确认支付侧 27.81 - 0.87 = 26.94 的总额口径本身没错，异常集中在 trade-core 商品行和订单聚合字段；计件 PLU 叠加会员价二次营销均摊时，标签金额被按单价和数量重新计算，少了 1 分，导致商品明细 26.93 和订单/支付 26.94 对不上。</li><li>梳理 retail-trade-cart 的 addGoods 改价字段链路，确认 pre 环境后端入参已经收到 pluAmount，问题不在 PLU 前缀或重量解析，而在 CartGoodsRequest.isChanged/changedPrice 与网关 changed_tag/change_price 之间的字段契约和绑定口径。</li><li>推进移动端订单报表缺失排查，当前主线已从刷卡支付状态机转到 pos-trade-order 和 pos-shift-daily-report；代码信号显示商品销售报表依赖 PcOrderItemPrice.totalPrice，没有价格对象时金额会降为 0，但还需要实单字段和报表落库证据继续闭环。</li><li>定位 Code Sentry Case 记忆未沉淀问题，确认不是飞书消息发送成功后就自动生成记忆，也不是 2a1c2867 修复后的写库链路仍失败；生产发送记录已能写入，当前根因收敛到发送数据被过短字段截断，随后被页面筛选条件过滤掉。</li><li>梳理重开单门店日流水号修复意图，确认旧逻辑在重开单时读取门店 Redis 当前流水号，可能拿到旧单之后新生成订单的流水号；新逻辑改为通过 reOrderNo 查询被重开的旧订单持久化数据，并复用旧单流水号。</li><li>核查 dev-skill 的 Windows 安装支持现状，确认当前安装入口仍是 make + bash 的 Unix-first 流程，不能算正式支持原生 Windows；方案已收敛到补 PowerShell 5.1+ 安装/卸载脚本，使用 Junction 降低普通用户权限要求，同时保留现有 macOS/Linux 流程。</li></ul><h3 id="🧩-困难-需要帮助-7">🧩 困难/需要帮助</h3><ul><li>PLU 金额治理还需要继续对齐 PC、网关、retail-trade-cart 和 trade-core 的字段契约，尤其是 pluAmount、isChanged/changedPrice 以及 1 分容差和大于 1 分阻断的边界。</li><li>移动端订单报表缺失还缺实单字段值、报表落库结果和过滤条件证据，暂时只能把可疑点收敛到价格对象缺失分支，不能直接定最终根因。</li><li>Code Sentry 记忆列表还需要确认字段长度修复、历史数据回补和页面 API 读取同一数据源这几件事，否则用户侧看到的 0 记录还不能算完全闭环。</li></ul><h3 id="💭-感想-7">💭 感想</h3><p>今天主要是在把 PLU 金额、报表缺失和 Case 记忆这些问题往真实 trace、字段契约和数据表筛选口径上收敛。</p><p>几个问题看起来都是“金额不对”或“页面没数据”，但真正要判断清楚，还是要一层层确认字段从网关到后端、从写库到页面是否都在同一个口径上。</p><!-- daily-report:2026-07-28:end --><!-- daily-report:2026-07-29:start --><h2 id="2026-07-29-今天做了啥？-✨">2026/07/29 今天做了啥？ ✨</h2><p>今天主要是在把 7 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容-8">✅ 工作内容</h3><ul><li>完成 ONLINE-981236 交班/日结三方券尾差修复，确认根因是旧接口只提供“单券金额 × 数量”，三张券真实支付金额 28767+28767+28766=86300 分被算成 86301 分；当前已在 retail-trade-misc 补 totalCouponPayAmount，retail-sync 和 retail-trademanager 改为消费新字段。</li><li>收敛三方券修复范围，明确 retail-trade-misc 负责按 buyerPayPrice 算准整组券实际支付总额，retail-sync 只替换 8 处旧乘法取值，retail-trademanager 只替换 1 处待支付金额取值；新字段为空时继续回退 couponPayAmount × count。</li><li>补强三方券链路观测，misc 记录映射不完整和查询异常，sync 记录精确金额命中数和旧逻辑回退数，trademanager 记录券明细为空及旧公式回退；同时确认不打印券码、不逐券刷日志，避免高频链路被噪音淹没。</li><li>推进 yz7/PLU 下单失败排查，确认不是购物车校验失败，而是下单阶段 retail-trade-cart 与 trade-core 两套计价口径不一致，零售应付 47.57 元、计价侧 47.48 元，相差 9 分后触发 RetailExtraPriceCalculator.checkPrice 金额一致性保护。</li><li>梳理 PLU 改动范围，确认问题边界在“改价事实、商品优惠、订单优惠”三层金额契约没有完全对齐；当前不能简单改通用 realPay 或全局优惠均摊，需要把 PLU 精确金额、改价金额和 PROS_DETAIL 重新均摊链路一起对齐。</li><li>推进 Code Sentry 发布告警记忆能力，确认 MySQL VARCHAR(2000) 和索引长度风险后，完成字段限额、超限压缩重试、人工编辑 400 明细返回、双召回 Top2 和 6000 字符注入控制；全量测试 49 个文件、233 个用例通过并推送远端 master。</li><li>补齐 dev-skill Windows 原生安装支持，新增 PowerShell 5.1+ 安装入口和安装器，覆盖 dry-run、install、uninstall、force、profile、客户端检测等常用流程；Windows 路径改为按用户目录解析，不硬编码用户名，README 也已同步更新并推送。</li></ul><h3 id="🧩-困难-需要帮助-8">🧩 困难/需要帮助</h3><ul><li>三方券兼容回退场景仍可能复现旧尾差，需要继续确认 trade-misc-api 二方包发布、MR 合入和 QA 新单验证节奏。</li><li>PLU 问题影响面偏中高风险，优惠券会在多个商品之间重新均摊，仍需要产品、营销、交易一起确认改价事实、商品优惠、订单优惠三层金额契约。</li><li>Code Sentry 旧分支不能整分支合并，旧表结构和当前 VARCHAR-safe 方案有冲突，后续需要选择性迁移控制台、灰度策略和召回观测能力。</li></ul><h3 id="💭-感想-8">💭 感想</h3><p>今天主要是在把几个问题往真实链路和字段口径上收，尤其是金额类问题，本质都是上游字段语义没讲清楚后被下游二次解释。这个方向比较明确，但后面还是要先把字段契约、回退边界和观测点定清楚，再做最小代码改动。</p><!-- daily-report:2026-07-29:end --><!-- daily-report:2026-07-30:start --><h2 id="2026-07-30-今天做了啥？-✨">2026/07/30 今天做了啥？ ✨</h2><p>今天主要是在把 7 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容-9">✅ 工作内容</h3><ul><li>完成拉卡拉 POS_BANK_CARD 支付拦截定位，确认 retail-trade-core 提示“当前 POS 机未绑定该门店”不是 SN 不存在，而是 retail_pos_terminal_bind 中 SN 00000104YP620300016136 绑定到 kdtId=159831351，未绑定到当前下单门店 201479448；支付前校验口径是当前 kdtId + deviceSn + is_deleted=0，rootKdtId 和收款归属不参与命中。</li><li>收敛拉卡拉支付渠道错误链路，确认另一条链路里 retail-trademanager.queryEnabledBind 已返回终端 usable=true、status=enabled，问题不在终端绑定，而是继续收敛到支付资产/渠道配置或 deepLinkInfo 生成链路。</li><li>完成 PLU 739/738 一分差修复和审计，已按“改价重算金额与 PLU 总价相差 &lt;=1 分时取 pluAmount”的口径落到 ProductEntity；继续审计发现营销 JS、本地购物车和预约开单等路径仍可能重新按单价 × 重量计算，需要补齐 pluAmount 透传和统一一分校验。</li><li>推进 ONLINE-981236 三方券尾差 RFC 和只读评审，确认正式 RFC 已写好并推送 codex/ONLINE-981236-rfc；评审中进一步确认 totalCouponPayAmount 挂在通用 transferResp/queryCoupon 明细上，不是只影响抖音券，平台分支、组合支付和降级回退还要继续核对。</li><li>对齐 Code Sentry 双召回 MVP 和 005 迁移口径，明确不做灰度，改为新旧召回始终双跑、只用全局 active_algorithm 控制注入；005 已从六表收敛为三表，删除冗余字段和索引，统一 updated_at，聚焦测试 12/12 通过。</li><li>梳理 Code Sentry 召回观测和记忆沉淀页展示，完成分页列表、行内展开、应用/发布单/任务号/标题搜索，以及阶段和差异结果筛选；同时确认“分析”列只取简要分析，不再用建议动作兜底，避免页面语义混在一起。</li><li>补齐 POS workspace 文档和前端应用接入说明，README 已同步 20 个 submodule、retail-node-setting、retail-pc-prepaid 等前端应用职责，以及储值、拉卡拉终端、交班日结、打印等 skill 路由；make check-workspace 通过并已推送 master。</li></ul><h3 id="🧩-困难-需要帮助-9">🧩 困难/需要帮助</h3><ul><li>拉卡拉问题还需要继续补齐资产侧分配对象、支付渠道配置和 deepLinkInfo 生成证据，尤其要把“总部收款”和“当前下单门店校验”两个口径分开，避免后续排查误判。</li><li>PLU 金额链路仍要继续对齐 PC、网关、本地购物车和营销 JS，核心是所有入口都能稳定使用 pluAmount，并保持 &lt;=1 分取标签金额、&gt;1 分阻断的边界一致。</li><li>Code Sentry 双召回目前更适合按 MVP 测试验证，生产前还需要确认 001-005 迁移执行、表结构/索引校验和线上 checksum 边界。</li></ul><h3 id="💭-感想-9">💭 感想</h3><p>今天主要是在把拉卡拉和 PLU 两条链路继续往真实校验口径上收，很多问题不是“有没有配置”或“有没有金额”，而是谁作为当前门店、哪个字段能代表真实金额。</p><p>Code Sentry 这边也从大而全灰度收回到最小 MVP，先让表结构、页面展示和注入开关都变小变清楚，后面上线风险会更可控。</p><!-- daily-report:2026-07-30:end -->]]></content>
    
    
    <summary type="html">记录 2026 年 07 月每天做了什么、困难和感想</summary>
    
    
    
    <category term="工作月总结" scheme="https://u7u7.top/categories/%E5%B7%A5%E4%BD%9C%E6%9C%88%E6%80%BB%E7%BB%93/"/>
    
    
    <category term="工作记录" scheme="https://u7u7.top/tags/%E5%B7%A5%E4%BD%9C%E8%AE%B0%E5%BD%95/"/>
    
    <category term="日报" scheme="https://u7u7.top/tags/%E6%97%A5%E6%8A%A5/"/>
    
  </entry>
  
  <entry>
    <title>JIRA与Trace分析自动化复盘：从Airbag到NVWA的Agent Harness优化</title>
    <link href="https://u7u7.top/posts/jira-trace-agent-harness-review.html"/>
    <id>https://u7u7.top/posts/jira-trace-agent-harness-review.html</id>
    <published>2026-06-12T03:20:00.000Z</published>
    <updated>2026-06-12T03:20:00.000Z</updated>
    
    <content type="html"><![CDATA[<h1 id="JIRA与Trace分析自动化复盘：从Airbag到NVWA的Agent-Harness优化">JIRA与Trace分析自动化复盘：从Airbag到NVWA的Agent Harness优化</h1><p>最近这段时间，我在 Codex 里反复做的一类事，不是单纯让 AI 写代码，而是把它丢进更真实的研发场景里：</p><ul><li>看 JIRA</li><li>找 trace</li><li>查日志</li><li>理解业务链路</li><li>判断根因</li><li>必要时改代码</li><li>再做 CR 和验证</li></ul><p>这件事越做越能说明一个问题：<strong>JIRA 排障不是一个问答题，而是一条会分叉、会失败、需要回环的工程链路。</strong></p><p>如果只是把 JIRA 描述贴给模型，让它“分析一下”，它大概率能写出一段看起来很像结论的话。但只要 trace 不完整、日志不直接、业务上下文缺失，结论就很容易变成“语言上很顺，证据上很虚”。</p><p>所以我这轮更关注的不是 prompt 写得多漂亮，而是 agent harness 到底该怎么搭，才能让 AI 从“会猜”变成“会查、会问、会验证”。</p><hr><h2 id="一、这段时间主要做了什么">一、这段时间主要做了什么</h2><p>从最近的 Codex 对话记录看，主线其实很清楚：</p><table><thead><tr><th>方向</th><th>代表动作</th><th>想解决的问题</th></tr></thead><tbody><tr><td>JIRA 闭环</td><td><code>jira-one-click</code>、<code>jira-trace-fix-cr</code></td><td>从问题单进入分析、修复、审查、验证</td></tr><tr><td>Trace 分析</td><td>根据 traceId 查日志、追调用链</td><td>让根因判断有证据，而不是只靠描述猜</td></tr><tr><td>无 Trace JIRA</td><td>分析没有 trace 的问题单该怎么处理</td><td>避免 AI 在证据不足时硬给结论</td></tr><tr><td>Airbag 对比</td><td>研究 Airbag 如何从 JIRA 找 trace、定位仓库</td><td>学习更成熟的排障 harness 设计</td></tr><tr><td>NVWA 优化</td><td>测试 NVWA 的 JIRA 处理效果并调 prompt / SDK 调用</td><td>把流程从“能跑”推向“更可靠”</td></tr></tbody></table><p>这里面最关键的不是某一次具体 JIRA，而是整个思路发生了变化。</p><p>以前更像是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">JIRA -&gt; 让 AI 分析 -&gt; 生成结论</span><br></pre></td></tr></table></figure><p>现在更像是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">JIRA -&gt; 线索抽取 -&gt; trace / 日志 / 代码 / 知识检索 -&gt; 追问补证据 -&gt; 根因假设 -&gt; 验证 -&gt; 修复 / 收口</span><br></pre></td></tr></table></figure><p>这两者差别很大。</p><p>前者是“回答系统”，后者才更接近“排障系统”。</p><hr><h2 id="二、为什么-JIRA-排障不能只靠一个大-Prompt">二、为什么 JIRA 排障不能只靠一个大 Prompt</h2><p>JIRA 的难点在于，它经常不是信息充分的输入。</p><p>一个真实问题单里可能只有：</p><ul><li>用户说某个按钮点了没反应</li><li>一张截图</li><li>一个时间点</li><li>一个店铺 ID</li><li>一个接口报错片段</li><li>或者只有一句“线上有问题，帮看下”</li></ul><p>这时候如果模型直接输出结论，本质上是在补脑。</p><p>更合理的方式应该是先判断证据是否足够：</p><table><thead><tr><th>情况</th><th>Agent 应该怎么做</th></tr></thead><tbody><tr><td>有 traceId</td><td>先查 trace 和调用链，再看日志</td></tr><tr><td>没有 traceId 但有时间窗</td><td>根据应用、关键词、时间窗反查日志</td></tr><tr><td>只有业务描述</td><td>先定位可能应用和入口，再生成待确认问题</td></tr><tr><td>证据冲突</td><td>标注冲突点，不急着下结论</td></tr><tr><td>需要改代码</td><td>找到代码位置后再进入修复和 CR</td></tr></tbody></table><p>我这次看 Airbag 和 NVWA 的差异，最大的感受就是：</p><blockquote><p>好的 harness 不会催着模型马上回答，而是会逼它先把证据链补齐。</p></blockquote><p>这也是为什么我对“没有 trace 的 JIRA 怎么分析”这个问题很敏感。无 trace 并不代表不能分析，但它一定意味着流程要换挡：从“顺 trace 找根因”切到“先构造线索，再逐步缩小范围”。</p><hr><h2 id="三、一个更靠谱的-JIRA-Agent-Loop">三、一个更靠谱的 JIRA Agent Loop</h2><p>我现在更倾向于把 JIRA 排障拆成一个 loop，而不是一条直线。</p><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart TD    A[&quot;JIRA 输入&quot;] --&gt; B[&quot;抽取线索&lt;br&#x2F;&gt;应用 &#x2F; 时间 &#x2F; 店铺 &#x2F; trace &#x2F; 关键词&quot;]    B --&gt; C{&quot;证据是否足够？&quot;}    C -- &quot;足够&quot; --&gt; D[&quot;查 trace &#x2F; 日志 &#x2F; 代码&quot;]    C -- &quot;不足&quot; --&gt; E[&quot;生成追问或反查策略&quot;]    E --&gt; B    D --&gt; F[&quot;形成根因假设&quot;]    F --&gt; G{&quot;能否验证？&quot;}    G -- &quot;能&quot; --&gt; H[&quot;验证假设&quot;]    G -- &quot;不能&quot; --&gt; E    H --&gt; I{&quot;需要修复？&quot;}    I -- &quot;需要&quot; --&gt; J[&quot;改代码 &#x2F; 补测试 &#x2F; CR&quot;]    I -- &quot;不需要&quot; --&gt; K[&quot;输出结论与证据&quot;]    J --&gt; L[&quot;复查与收口&quot;]    L --&gt; K  </pre></div><p>这里最重要的是两个判断点：</p><ol><li>证据是否足够</li><li>根因是否可验证</li></ol><p>很多 AI 排障失败，不是因为模型不会分析，而是因为系统没有给它这两个刹车点。<br>它会在证据不足时继续往前写，在验证没完成时提前收口。</p><p>我现在觉得，一个面向真实研发的 JIRA Agent，至少要有下面几种能力：</p><ul><li><strong>线索抽取能力</strong>：从问题单里抽出应用、接口、时间、店铺、错误码、traceId。</li><li><strong>工具编排能力</strong>：按需调用 JIRA、日志、Trace、RDS、代码搜索、Dubbo 等工具。</li><li><strong>证据分级能力</strong>：区分“日志事实”“代码事实”“业务推断”“待确认假设”。</li><li><strong>追问能力</strong>：信息不足时不要硬编结论，而是生成最小追问。</li><li><strong>回环能力</strong>：验证失败后回到前面补线索，而不是直接结束。</li></ul><p>这就是我理解的 JIRA Agent Loop。</p><hr><h2 id="四、Airbag-给我的启发">四、Airbag 给我的启发</h2><p>Airbag 最值得学的地方，不是某个具体命令，而是它把 JIRA 排障拆成了更稳定的工程步骤。</p><p>我关注的几个点是：</p><ul><li>它如何从 JIRA 里拿到 trace 线索</li><li>它如何判断关联仓库和应用</li><li>它如何把问题单、日志、代码上下文串起来</li><li>它如何避免只靠单轮 LLM 输出结论</li></ul><p>这里面有一个特别重要的设计思想：</p><blockquote><p>LLM 不应该独自承担所有判断，harness 要把可确定的工程动作先做掉。</p></blockquote><p>比如：</p><ul><li>能用 API 拿到的 JIRA 字段，不要让模型猜。</li><li>能用日志查询拿到的错误片段，不要让模型编。</li><li>能用代码搜索定位的类和方法，不要只让模型按关键词幻想。</li><li>能用测试集复跑的问题，不要只看一次输出好不好看。</li></ul><p>这也是我后面看 NVWA 时最在意的地方：不是它能不能调用 Claude SDK，而是它有没有把“该查什么、怎么查、查不到怎么办、什么时候停下来问人”这些动作设计清楚。</p><hr><h2 id="五、NVWA-目前暴露的问题">五、NVWA 目前暴露的问题</h2><p>这轮测试里，我对 NVWA 的直观感受是：它已经具备做 JIRA agent 的雏形，但问题也比较明显。</p><h3 id="1-结论容易来得太早">1. 结论容易来得太早</h3><p>有些场景里，SDK 还没跑完，AI 就已经给出了“一句话总结”。这会带来一个很大的风险：</p><blockquote><p>用户看到的是结论，但背后的 trace、业务知识和代码证据其实还没完成。</p></blockquote><p>这类问题不是简单调高模型能力就能解决的，它需要 harness 在状态机上约束：</p><ul><li>分析未完成时不能输出最终结论</li><li>工具调用失败时必须标记证据缺口</li><li>关键链路未验证时只能输出阶段性判断</li></ul><h3 id="2-无-trace-场景需要单独流程">2. 无 trace 场景需要单独流程</h3><p>没有 trace 的 JIRA 不能走同一套模板。</p><p>它应该先进入“问题补全模式”：</p><ul><li>识别业务对象</li><li>推断可能应用</li><li>按时间窗查日志</li><li>找接口、任务、消息、配置等可能入口</li><li>输出待确认字段</li></ul><p>如果一开始就套“trace 分析模板”，最后很容易变成假装有证据。</p><h3 id="3-Prompt-不能只写得更长">3. Prompt 不能只写得更长</h3><p>Prompt 优化当然有用，但我现在更倾向于把它拆成多段：</p><table><thead><tr><th>阶段</th><th>Prompt 应该负责什么</th></tr></thead><tbody><tr><td>线索抽取</td><td>只抽字段，不下结论</td></tr><tr><td>检索计划</td><td>决定先查哪些系统</td></tr><tr><td>证据汇总</td><td>把事实和推断分开</td></tr><tr><td>根因判断</td><td>基于证据生成假设</td></tr><tr><td>最终输出</td><td>给出结论、证据、风险和下一步</td></tr></tbody></table><p>如果所有事情都塞进一个大 prompt，模型会更难遵守阶段边界。</p><hr><h2 id="六、我沉淀下来的几个原则">六、我沉淀下来的几个原则</h2><p>这轮实验之后，我对 JIRA / Trace 类 agent 有几个更明确的判断。</p><h3 id="1-先做证据链，再做表达">1. 先做证据链，再做表达</h3><p>很多 AI 输出看起来“像那么回事”，但真正有价值的是背后的证据链。</p><p>所以报告结构应该优先包含：</p><ul><li>结论</li><li>证据</li><li>推断</li><li>未验证点</li><li>下一步动作</li></ul><p>而不是只给一个漂亮的摘要。</p><h3 id="2-Harness-比模型更决定下限">2. Harness 比模型更决定下限</h3><p>模型决定上限，harness 决定下限。</p><p>一个好的 harness 会让模型少犯低级错：</p><ul><li>不会在没查日志时假装查过</li><li>不会把猜测写成事实</li><li>不会跳过代码定位</li><li>不会忘记验证修复</li></ul><p>这类稳定性，比单次回答惊艳更重要。</p><h3 id="3-排障-Agent-必须有“不会就问”的能力">3. 排障 Agent 必须有“不会就问”的能力</h3><p>真实工程里，信息不足是常态。</p><p>一个成熟 agent 不应该把“问用户补充信息”当成失败，而应该把它当成正常分支。关键是问得少、问得准、问得有依据。</p><p>比如不要泛泛地问“请提供更多信息”，而应该问：</p><ul><li>这个问题发生的准确时间窗口是什么？</li><li>有没有店铺 ID、订单号或用户 ID？</li><li>线上环境是 prod、pre 还是 QA？</li><li>你看到的错误截图对应哪个操作入口？</li></ul><p>这才是有用的追问。</p><h3 id="4-最终目标不是“会分析”，而是“能闭环”">4. 最终目标不是“会分析”，而是“能闭环”</h3><p>我现在判断一个 JIRA agent 有没有价值，不看它能不能写一段很长的分析，而看它能不能闭环：</p><ol><li>找到线索</li><li>查到证据</li><li>定位根因</li><li>判断是否要改</li><li>改完验证</li><li>输出可追溯结论</li></ol><p>只完成前两步，最多算辅助排查。<br>能把后面也串起来，才更像真正的研发 Agent。</p><hr><h2 id="七、下一步我想怎么改">七、下一步我想怎么改</h2><p>如果继续优化 NVWA / Airbag 这条线，我会优先做四件事。</p><h3 id="1-把-JIRA-集变成固定评测集">1. 把 JIRA 集变成固定评测集</h3><p>我已经开始把 6 月份的一批 JIRA 当作测试集。后面应该把它们按类型分层：</p><ul><li>有 trace 且根因明确</li><li>无 trace 但有时间窗</li><li>只有业务描述</li><li>需要跨应用定位</li><li>需要代码修复</li><li>只需要业务解释</li></ul><p>这样才能比较每次 prompt 和 harness 改动是不是真的变好。</p><h3 id="2-把输出拆成阶段产物">2. 把输出拆成阶段产物</h3><p>不要让 agent 一上来就生成最终答案。</p><p>更合理的是每个阶段都有结构化产物：</p><ul><li>线索卡</li><li>检索计划</li><li>证据表</li><li>根因假设</li><li>修复建议</li><li>最终报告</li></ul><p>这样人也更容易介入检查。</p><h3 id="3-给无-trace-流程单独建-skill">3. 给无 trace 流程单独建 skill</h3><p>有 trace 和无 trace 是两类任务。<br>它们不应该共用同一个工作流。</p><p>无 trace JIRA 更像“问题侦查”，需要先从业务对象、时间窗、入口路径和应用归属里反推。</p><h3 id="4-把-CR-和验证接进同一条链路">4. 把 CR 和验证接进同一条链路</h3><p>排障不是找到根因就结束。</p><p>如果要改代码，后面必须继续接：</p><ul><li>影响面分析</li><li>单元测试或集成测试</li><li>代码审查</li><li>复查日志</li><li>输出修复说明</li></ul><p>这也是我为什么一直把 <code>jira-one-click</code>、<code>jira-trace-fix-cr</code>、<code>author-final-review</code> 这些东西放在一起看。它们本质上不是多个孤立工具，而是一条研发闭环的不同节点。</p><hr><h2 id="结尾">结尾</h2><p>这段时间做 JIRA 和 Trace 自动化，最大的收获不是“AI 能不能帮我看问题”，而是我更清楚地看到了 agent harness 的价值。</p><p>排障这类任务天然复杂：输入不完整、证据分散、路径会分叉、结论需要验证。<br>如果只靠一个大 prompt，AI 很容易变成一个会写总结的猜测器。<br>如果把 JIRA、Trace、日志、代码、业务知识、追问和 CR 串成 loop，它才有机会变成真正能参与研发闭环的助手。</p><p>所以我现在对这类系统的判断标准也更直接：</p><blockquote><p>不要看它能不能说出一个像样的结论，要看它能不能把证据一步步查出来，并且在证据不足时知道停下来。</p></blockquote><p>这才是 JIRA Agent 真正该追求的方向。</p>]]></content>
    
    
    <summary type="html">复盘最近在 Codex 里围绕 JIRA 检查、Trace 分析、Airbag 与 NVWA 问题流做的一轮实验：为什么排障不能只靠一个大 prompt，而要靠检索、工具、追问、验证和回环组成的 agent harness。</summary>
    
    
    
    <category term="技术笔记" scheme="https://u7u7.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="AI" scheme="https://u7u7.top/tags/AI/"/>
    
    <category term="Agent" scheme="https://u7u7.top/tags/Agent/"/>
    
    <category term="Codex" scheme="https://u7u7.top/tags/Codex/"/>
    
    <category term="JIRA" scheme="https://u7u7.top/tags/JIRA/"/>
    
    <category term="Trace" scheme="https://u7u7.top/tags/Trace/"/>
    
    <category term="Harness" scheme="https://u7u7.top/tags/Harness/"/>
    
    <category term="排障" scheme="https://u7u7.top/tags/%E6%8E%92%E9%9A%9C/"/>
    
  </entry>
  
  <entry>
    <title>Skill扫盲：从提示词补丁到AI工作流，我如何用dev-skill把经验沉淀成能力系统</title>
    <link href="https://u7u7.top/posts/skill-literacy-dev-skill-workflow.html"/>
    <id>https://u7u7.top/posts/skill-literacy-dev-skill-workflow.html</id>
    <published>2026-06-09T13:30:00.000Z</published>
    <updated>2026-06-09T14:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Skill扫盲：从提示词补丁到AI工作流，我如何用dev-skill把经验沉淀成能力系统">Skill扫盲：从提示词补丁到AI工作流，我如何用dev-skill把经验沉淀成能力系统</h1><p>这半年我越来越强烈地感觉到一件事：</p><blockquote><p>AI Coding 真正的分水岭，已经不只是“模型够不够强”，而是“你有没有办法把合适的方法，在合适的时机交给它”。</p></blockquote><p>很多人第一次接触 <code>Skill</code>，会觉得它只是另一种 Prompt 封装。<br>但如果你真的在 <code>Claude Code</code>、<code>Codex</code>、<code>Cursor</code> 这类工具里持续做开发，就会发现 <code>Skill</code> 解决的问题，比“保存几段提示词”要大得多。</p><p>它在解决的是：</p><ul><li>如何减少每次都重复灌上下文</li><li>如何让 AI 在对的阶段进入对的工作流</li><li>如何把个人经验变成团队可以复用的能力</li><li>如何把分析、开发、审查、排障、文档这些动作串成闭环</li></ul><p>这篇文章，我想把 <code>Skill</code> 这件事完整讲透，尽量做到既能扫盲，也能落地。</p><p>文章会分成四层：</p><ol><li><code>Skill</code> 为什么会出现</li><li><code>Skill</code> 的核心原理和机制是什么</li><li><code>Skill</code> 和 <code>Rules / Commands / MCP</code> 到底怎么分工</li><li>结合我自己的 <a href="https://gitlab.qima-inc.com/shijiahao_0802/dev-skill"><code>dev-skill</code></a> 仓库，看看它在真实研发流程里能做什么</li></ol><blockquote><p>本文内容结合了我在 <strong>2026 年 6 月 9 日</strong> 的内部分享文档《门店后端AI学习分享-Agent Skill 工作流》，以及当前 <code>dev-skill</code> 仓库中的实际 Skill 设计与目录结构整理而成。</p></blockquote><hr><h2 id="一、为什么会有-Skill">一、为什么会有 Skill</h2><p>如果你经历过 AI Coding 从早期到现在的演进，会发现大家一直在解决同一个问题：</p><blockquote><p>怎么把合适的信息，在合适的时机，放进模型的上下文里。</p></blockquote><p>一开始最常见的做法，是写 <code>Rules</code>。</p><p>比如：</p><ul><li>这个仓库的业务背景是什么</li><li>编码规范是什么</li><li>哪些坑不能踩</li><li>哪些目录不能乱改</li></ul><p>这些东西一开始很有用，因为它能明显减少模型幻觉。</p><p>但问题也很快出现了：</p><ul><li><code>Rules</code> 越写越长</li><li>很多规则只在特定任务下才相关</li><li>它们却会在每次对话里都被加载</li><li>token 消耗会越来越高</li></ul><p>尤其是在真实项目里，你很容易遇到这种情况：</p><ul><li>查日志时不需要开发规范</li><li>画架构图时不需要数据库排障说明</li><li>写文档时不需要完整编码规则</li><li>只做链路分析时，不应该每次都把 2000 token 的开发 rules 带上</li></ul><p>这就是 <code>Skill</code> 出现的背景。</p><h3 id="Skill-的第一性价值">Skill 的第一性价值</h3><p>如果把它压成一句话，我会这样定义：</p><blockquote><p><code>Skill</code> 的本质，是把原来“总是加载”的静态上下文，改造成“按需加载”的动态上下文。</p></blockquote><p>也就是说，Skill 不是单纯多了一个文件格式，而是在改变上下文管理方式。</p><p>它追求的是：</p><ul><li>先只暴露极少量的索引信息</li><li>让 Agent 判断是否相关</li><li>真正需要时，再加载完整方法</li><li>更深的资料和脚本，继续延后到需要的时候再读或再执行</li></ul><p>这就是它最关键的思想：<strong>渐进式披露</strong>。</p><hr><h2 id="二、什么是-Skill">二、什么是 Skill</h2><p>通俗一点讲，<code>Skill</code> 可以理解成：</p><blockquote><p>一份带名字、带描述、带操作说明、还能挂资源和脚本的能力模块。</p></blockquote><p>它通常至少包含两层内容：</p><ol><li>元数据：<code>name</code>、<code>description</code></li><li>正文：<code>SKILL.md</code> 里的工作流、约束、步骤、触发条件</li></ol><p>更完整一点的 Skill，还会带上：</p><ul><li><code>references/</code>：参考文档、模板、补充说明</li><li><code>scripts/</code>：脚本、校验器、同步器、自动化工具</li><li><code>assets/</code>：图片、模板、静态资源</li></ul><p>所以一个 Skill 不只是“告诉 AI 该说什么”，更像是：</p><blockquote><p>给 AI 一份“这类任务应该怎么做”的可执行说明书。</p></blockquote><hr><h2 id="三、Skill-的核心原理：渐进式披露">三、Skill 的核心原理：渐进式披露</h2><p>这是 Skill 最值得理解的地方。</p><p>很多人第一次听这个概念，会觉得有点抽象。<br>其实你可以把它想成翻一本手册：</p><ul><li>平时只看目录</li><li>需要时再翻章节</li><li>实在要落地，再去查附录和工具</li></ul><p>在 Skill 体系里，大致可以分成三层。</p><h3 id="第一层：目录层">第一层：目录层</h3><p>这一层通常就是 <code>name + description</code>。</p><p>它的特点是：</p><ul><li>始终可见</li><li>很短</li><li>token 消耗很低</li><li>作用是告诉 Agent：“现在有哪些能力可以选”</li></ul><p>这一步本质上是在做能力发现。</p><h3 id="第二层：章节层">第二层：章节层</h3><p>这一层是 <code>SKILL.md</code> 的正文。</p><p>它的特点是：</p><ul><li>只有在 Skill 被判断相关后才加载</li><li>主要描述 SOP、约束、流程、边界</li><li>让 Agent 知道“这类事应该怎么推进”</li></ul><p>比如：</p><ul><li>先检查项目上下文</li><li>再问澄清问题</li><li>再产出方案</li><li>再拆任务</li><li>最后才进入实现</li></ul><h3 id="第三层：附录层">第三层：附录层</h3><p>这一层通常是：</p><ul><li><code>references/</code></li><li><code>scripts/</code></li><li>模板文件</li><li>外部数据</li></ul><p>它的特点是：</p><ul><li>不是一开始就读</li><li>需要的时候才展开</li><li>有些内容甚至不用进上下文，直接执行脚本就行</li></ul><p>这一步很关键，因为它让 Skill 的能力不再只停留在 Prompt，而是开始连接真实文件系统和自动化动作。</p><h3 id="一张图看懂-Skill-的三层结构">一张图看懂 Skill 的三层结构</h3><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart TD    A[&quot;元数据&lt;br&#x2F;&gt;name + description&quot;] --&gt; B[&quot;SKILL.md 正文&lt;br&#x2F;&gt;流程、约束、SOP&quot;]    B --&gt; C[&quot;references&#x2F;&lt;br&#x2F;&gt;模板、补充文档、案例&quot;]    B --&gt; D[&quot;scripts&#x2F;&lt;br&#x2F;&gt;校验、同步、查询、自动化执行&quot;]    C --&gt; E[&quot;按需继续加载&quot;]    D --&gt; F[&quot;按需执行，不必全进上下文&quot;]  </pre></div><p>这套设计的好处非常直接：</p><ul><li>平时只花很少 token 看目录</li><li>触发后才读方法正文</li><li>复杂资源进一步延迟加载</li><li>脚本可以直接干活，减少模型硬记细节</li></ul><p>如果你做过大项目，就会知道这件事有多重要。<br>因为上下文预算永远是稀缺资源。</p><hr><h2 id="四、Skill-和-Rules、Commands、MCP-到底有什么区别">四、Skill 和 Rules、Commands、MCP 到底有什么区别</h2><p>这是最容易混淆的一组概念。</p><p>我现在更倾向于把它们看成同一条链路上的不同层。</p><h3 id="1-Rules：静态长期背景">1. Rules：静态长期背景</h3><p>Rules 适合放这种内容：</p><ul><li>仓库总原则</li><li>通用编码规范</li><li>业务红线</li><li>不变的团队约定</li></ul><p>它的优点是稳定，缺点是会被反复加载。</p><h3 id="2-Commands：手动触发的静态入口">2. Commands：手动触发的静态入口</h3><p>Commands 更像是：</p><ul><li>一段你希望随叫随到的固定操作模板</li><li>一条快捷工作流入口</li></ul><p>比如：</p><ul><li><code>/openspec-proposal</code></li><li><code>/openspec-apply</code></li></ul><p>它解决的是“重复手动输入”的问题，但本质上仍偏静态。</p><h3 id="3-MCP：真实工具与系统接入">3. MCP：真实工具与系统接入</h3><p>MCP 的重点不是“告诉 AI 怎么想”，而是“让 AI 真能做事”。</p><p>它可以负责：</p><ul><li>查日志</li><li>调数据库</li><li>调第三方 API</li><li>操作 GitHub、飞书、Jira</li><li>访问浏览器、桌面、文件系统</li></ul><p>所以 MCP 更像工具层。</p><h3 id="4-Skill：方法层与工作流层">4. Skill：方法层与工作流层</h3><p>Skill 的重点是：</p><ul><li>什么时候该做什么</li><li>做这类事应该遵循什么顺序</li><li>该用哪些资源、哪些脚本、哪些 MCP</li></ul><p>所以它更像方法层。</p><h3 id="一句话区分">一句话区分</h3><p>你可以这么记：</p><ul><li><code>Rules</code> 负责长期背景</li><li><code>Commands</code> 负责快捷入口</li><li><code>MCP</code> 负责工具能力</li><li><code>Skill</code> 负责方法编排</li></ul><h3 id="Skill-和-MCP-不是替代关系">Skill 和 MCP 不是替代关系</h3><p>这一点特别重要。</p><p>很多人会问：</p><blockquote><p>Skill 和 MCP 不都是“给 AI 增强能力”吗？</p></blockquote><p>表面看像，但本质不同。</p><p>我更认同这个说法：</p><blockquote><p>MCP 把 Claude / Codex 连接到数据和系统；Skill 教会它如何处理这些数据、何时调用这些系统。</p></blockquote><p>也就是说：</p><ul><li>Skill 决定“怎么做”</li><li>MCP 决定“能做什么”</li></ul><p>两者组合起来，效果才最好。</p><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart LR    U[&quot;用户任务&quot;] --&gt; S[&quot;Skill&lt;br&#x2F;&gt;决定流程与方法&quot;]    S --&gt; M[&quot;MCP &#x2F; Scripts &#x2F; Tools&lt;br&#x2F;&gt;执行真实动作&quot;]    M --&gt; R[&quot;结果返回&quot;]    R --&gt; S  </pre></div><hr><h2 id="五、Skill-能做什么">五、Skill 能做什么</h2><p>如果只把 Skill 理解成“写作提示词模板”，会低估它很多。</p><p>在真实研发里，Skill 至少能覆盖下面几类事情。</p><h3 id="1-做需求澄清和方案设计">1. 做需求澄清和方案设计</h3><p>比如我仓库里的：</p><ul><li><code>brainstorming</code></li></ul><p>它不是单纯让 AI 多问几个问题，而是把“需求澄清 -&gt; 方案对比 -&gt; 分段确认 -&gt; 落文档”的流程固定下来。</p><h3 id="2-做开发流程编排">2. 做开发流程编排</h3><p>比如：</p><ul><li><code>sdd-intelligent-router</code></li><li><code>sdd-dev-workflow</code></li><li><code>dev-lifecycle</code></li></ul><p>它们解决的是：</p><ul><li>什么任务该快速直做</li><li>什么任务该走 Spec 驱动开发</li><li>技术方案、提案、实现、测试如何串起来</li></ul><h3 id="3-做代码质量保障">3. 做代码质量保障</h3><p>比如：</p><ul><li><code>author-final-review</code></li><li><code>code-review</code></li><li><code>full-review</code></li><li><code>integration-test</code></li></ul><p>这里 Skill 不只是“帮你看看代码”，而是把影响面分析、代码质量审查、测试建议变成标准输出。</p><h3 id="4-做线上排障与系统查询">4. 做线上排障与系统查询</h3><p>比如：</p><ul><li><code>zan-dev-flow</code></li><li><code>zan-log-query</code></li><li><code>zan-rds-ops</code></li><li><code>zan-redis-query</code></li><li><code>zan-apollo-query</code></li><li><code>zan-dubbo-invoke</code></li></ul><p>这些 Skill 已经非常接近“半自动排障助手”了。</p><h3 id="5-做文档与协同系统打通">5. 做文档与协同系统打通</h3><p>比如：</p><ul><li><code>feishu-wiki-skill</code></li><li><code>project-writer</code></li><li><code>diagram-creation</code></li></ul><p>它们能把文档读写、图表生成、Markdown 同步、图片分析这些动作接起来。</p><h3 id="6-做团队级协作与多-Agent-编排">6. 做团队级协作与多 Agent 编排</h3><p>比如：</p><ul><li><code>vibe-working-agent-team</code></li></ul><p>它已经不是单点技能，而是在组织角色分工、tmux 分屏和交付节奏。</p><p>所以从结果上看，Skill 可以做的事情远比“一个提示词”丰富得多。</p><hr><h2 id="六、结合我的-dev-skill-仓库，Skill-是怎么落地成体系的">六、结合我的 dev-skill 仓库，Skill 是怎么落地成体系的</h2><p>说抽象概念容易，但真正有意思的，是它怎么落地。</p><p>我自己的 <code>dev-skill</code> 仓库，核心定位非常明确：</p><blockquote><p>一次维护，多端同步，把高频研发流程沉淀成可复用的 Skills，并统一分发到 Cursor、Claude Code、Codex，甚至 OpenClaw 兼容层。</p></blockquote><h3 id="1-它不是“零散技能集合”，而是一个-source-of-truth">1. 它不是“零散技能集合”，而是一个 source of truth</h3><p>在这个仓库里，<code>dev-skill</code> 本身是技能源码仓。</p><p>也就是说：</p><ul><li>日常开发在这里改</li><li>团队共享从这里同步</li><li>常用 Skill 通过 profile 安装</li><li>与其他分发仓通过脚本和 manifest 对齐</li></ul><p>这很重要，因为 Skill 一旦多起来，如果没有“单一事实来源”，很快就会失控。</p><h3 id="2-它是分层组织的">2. 它是分层组织的</h3><p>仓库当前把 Skills 分成几类：</p><ul><li><code>skills/dev/</code>：开发流程类</li><li><code>skills/cr/</code>：代码审查与测试类</li><li><code>skills/writing/</code>：文档与图表类</li><li><code>skills/zan/</code>：排障和内部工具链</li><li><code>skills/lark/</code>：飞书协同能力</li><li><code>skills/helm/</code>：技能治理类</li><li><code>skills/platform/</code>：安装、统计、团队协作等平台能力</li></ul><p>这种分类不是为了好看，而是为了降低认知成本。<br>你看到目录，大致就知道能力落在哪一层。</p><h3 id="3-它支持跨工具同步">3. 它支持跨工具同步</h3><p><code>dev-skill</code> 不是只服务一个 IDE。</p><p>当前这套仓库可以同步到：</p><ul><li><code>~/.cursor/skills</code></li><li><code>~/.claude/skills</code></li><li><code>~/.codex/skills.union</code> 与 <code>~/.codex/skills</code></li><li><code>~/.openclaw/skills</code></li></ul><p>这意味着同一套 Skill 设计，可以在多个 Agent Harness 里复用。</p><p>这一点对团队推广特别关键，因为否则你会陷入：</p><ul><li>Cursor 一套</li><li>Claude 一套</li><li>Codex 再来一套</li></ul><p>最后维护成本会非常高。</p><h3 id="4-它不是只管分发，还管治理">4. 它不是只管分发，还管治理</h3><p>这是我很看重的一点。</p><p>仓库里不只是有业务 Skill，还有治理 Skill，比如：</p><ul><li><code>skill-usage-tracker</code></li><li><code>skill-creator</code></li><li><code>skill-publish</code></li><li><code>knowledge-distiller</code></li><li><code>auto-installer</code></li></ul><p>这说明一件事：</p><blockquote><p>Skill 系统一旦进入团队，就不只是“写几个好玩的 Prompt”，而是要进入完整的生命周期管理。</p></blockquote><p>也就是：</p><ul><li>怎么创建</li><li>怎么安装</li><li>怎么追踪使用</li><li>怎么发布</li><li>怎么沉淀经验</li></ul><p>这是 Skill 从“玩法”走向“基础设施”的标志。</p><hr><h2 id="七、从几个代表性-Skill-看它的真实价值">七、从几个代表性 Skill 看它的真实价值</h2><p>这一段我想直接拿仓库里的 Skill 举例，因为这样比抽象定义更有说服力。</p><h3 id="1-brainstorming：把“先想清楚”写成工作流">1. <code>brainstorming</code>：把“先想清楚”写成工作流</h3><p>这个 Skill 的核心思想非常简单，但非常重要：</p><blockquote><p>不要一上来就写代码。</p></blockquote><p>它会要求 Agent：</p><ol><li>先看项目上下文</li><li>一次只问一个关键问题</li><li>给出候选方案和取舍</li><li>分段展示设计并确认</li><li>最后写成设计文档</li></ol><p>这其实是在把优秀工程师的思考顺序显式化。</p><h3 id="2-sdd-dev-workflow：把-Spec-Driven-Development-做成可执行流水线">2. <code>sdd-dev-workflow</code>：把 Spec-Driven Development 做成可执行流水线</h3><p>这个 Skill 更进一步。</p><p>它不只是“按方案开发”，而是把下面这些动作串起来：</p><ul><li>找技术方案</li><li>提取能力点</li><li>匹配相关应用</li><li>创建 OpenSpec 提案</li><li>等待用户确认</li><li>按任务顺序实施开发</li></ul><p>这已经不是单个 Prompt 能承担的事情了，而是完整的流程编排。</p><h3 id="3-full-review：把代码审查做成组合技能">3. <code>full-review</code>：把代码审查做成组合技能</h3><p>从仓库设计上看，<code>full-review</code> 不是一个孤立技能，而是把多个审查能力组合起来。</p><p>它通常会串联：</p><ul><li>影响面分析</li><li>代码质量审查</li><li>测试建议生成</li></ul><p>这说明 Skill 的价值不只是“单点能力”，还包括<strong>能力编排</strong>。</p><h3 id="4-zan-dev-flow：把排障入口统一起来">4. <code>zan-dev-flow</code>：把排障入口统一起来</h3><p>这类 Skill 很适合内部研发场景。</p><p>用户不需要先自己决定：</p><ul><li>该查日志</li><li>该查 Redis</li><li>该查 HBase</li><li>还是该查 Apollo</li></ul><p>而是先从一个统一路由入口进去，再分发到原子能力。</p><p>这和前面说的 <code>Skill 负责方法层，MCP 负责工具层</code> 非常契合。</p><h3 id="5-feishu-wiki-skill：Skill-不只是“会想”，还应该“会干”">5. <code>feishu-wiki-skill</code>：Skill 不只是“会想”，还应该“会干”</h3><p>这个 Skill 很有代表性，因为它不仅有说明文档，还有成体系的脚本：</p><ul><li>读取 wiki/docx/bitable</li><li>下载并分析图片</li><li>把 Markdown 同步到飞书文档</li><li>渲染 Mermaid 后上传</li></ul><p>这说明 Skill 最强的形态，不是纯文本，而是：</p><blockquote><p>文档方法 + 资源说明 + 脚本执行 + 外部系统接入</p></blockquote><h3 id="6-vibe-working-agent-team：Skill-可以上升到多角色协作">6. <code>vibe-working-agent-team</code>：Skill 可以上升到多角色协作</h3><p>这个 Skill 已经开始组织：</p><ul><li>多角色分工</li><li>tmux 多窗口</li><li>Team Lead 协调</li><li>PRD -&gt; 方案 -&gt; 开发 -&gt; CR 的交付路径</li></ul><p>它让我越来越确信一件事：</p><blockquote><p>Skill 的上限，不是“某个任务怎么做”，而是“一个团队如何协作完成任务”。</p></blockquote><hr><h2 id="八、Skill-的几种关键机制">八、Skill 的几种关键机制</h2><p>如果你想真正写好 Skill，光知道概念还不够，还得理解它常见的机制设计。</p><h3 id="1-触发机制">1. 触发机制</h3><p>Skill 一般有两种触发方式：</p><ul><li>主动触发：用户点名 Skill</li><li>被动触发：用户只描述意图，由 Agent 判断该用哪个 Skill</li></ul><p>这意味着 Skill 不只是“被调用”，还需要“可发现、可匹配”。</p><p>所以 <code>name</code> 和 <code>description</code> 往往特别重要。</p><h3 id="2-渐进式加载机制">2. 渐进式加载机制</h3><p>这就是前面说的三层披露：</p><ul><li>目录层</li><li>正文层</li><li>资源层</li></ul><p>Skill 设计得越好，越不会一开始就把所有东西灌进上下文。</p><h3 id="3-组合与依赖机制">3. 组合与依赖机制</h3><p>很多大 Skill 本身不会单干，而是组合别的 Skill。</p><p>比如仓库里的依赖关系就很典型：</p><ul><li><code>sdd-intelligent-router</code> 会路由到 <code>ai-pair-programmer</code> 和 <code>sdd-dev-workflow</code></li><li><code>sdd-dev-workflow</code> 本身依赖其他开发类能力</li><li><code>full-review</code> 会串起 review 与测试相关 Skill</li><li><code>zan-dev-flow</code> 会分发到日志、缓存、数据库等原子能力</li></ul><p>这说明 Skill 系统并不是平铺的，而是一个图。</p><h3 id="4-资源外置机制">4. 资源外置机制</h3><p>很多重要信息不适合都写进 <code>SKILL.md</code>。</p><p>更好的做法是：</p><ul><li>流程放正文</li><li>长说明放 <code>references/</code></li><li>自动化动作放 <code>scripts/</code></li></ul><p>这样既减少上下文噪音，也方便复用和维护。</p><h3 id="5-治理与观测机制">5. 治理与观测机制</h3><p>这个机制经常被忽略，但团队化后非常重要。</p><p>比如我的仓库里就有：</p><ul><li><code>skill-usage-tracker</code></li></ul><p>它会记录：</p><ul><li>哪个 Skill 被用了多少次</li><li>最近一次是什么场景</li><li>哪些 Skill 真正高频</li></ul><p>这会反过来影响后续治理，比如：</p><ul><li>哪些 Skill 值得继续打磨</li><li>哪些 Skill 应该合并</li><li>哪些 Skill 实际没人用</li></ul><hr><h2 id="九、Skill-设计里，我觉得最有价值的几个经验">九、Skill 设计里，我觉得最有价值的几个经验</h2><p>写到这里，我其实更想分享一些实践感受。</p><h3 id="1-Skill-不是“把-Prompt-存起来”">1. Skill 不是“把 Prompt 存起来”</h3><p>真正好的 Skill，核心不是文案，而是：</p><ul><li>决策顺序</li><li>任务边界</li><li>信息加载节奏</li><li>资源组织方式</li></ul><p>Prompt 只是表层。</p><h3 id="2-Skill-越贴近真实工作流，价值越大">2. Skill 越贴近真实工作流，价值越大</h3><p>最有价值的 Skill，往往不是那种“万能专家 Prompt”，而是：</p><ul><li>需求澄清 Skill</li><li>Spec 驱动开发 Skill</li><li>审查闭环 Skill</li><li>排障路由 Skill</li><li>文档同步 Skill</li></ul><p>因为这些东西本来就在团队里高频出现。</p><h3 id="3-Skill-的核心复利在于经验沉淀">3. Skill 的核心复利在于经验沉淀</h3><p>这件事我现在越来越确定。</p><p>一个团队真正有复利的，不是每个人都临场发挥得多聪明，而是：</p><blockquote><p>能不能把做过十次、踩过十次坑的事情，沉淀成第十一次可以直接复用的方法。</p></blockquote><p>Skill 恰好就是这种经验的载体。</p><h3 id="4-Skill-最终会走向“Prompt-Tool-Workflow-Governance”">4. Skill 最终会走向“Prompt + Tool + Workflow + Governance”</h3><p>如果只看现在，很多人会觉得 Skill 还处在早期。</p><p>但从趋势上看，我认为它一定会继续往这几个方向发展：</p><ul><li>更强的自动触发</li><li>更清晰的依赖图</li><li>更成熟的 Skill 市场</li><li>更标准的治理与分发机制</li><li>更深的 MCP 和脚本联动</li></ul><p>换句话说，Skill 不会停留在“方便写 Prompt”这个层面。</p><p>它更像是 Agent 时代的方法资产格式。</p><hr><h2 id="十、如果你要开始搭自己的-Skill-系统，我建议怎么做">十、如果你要开始搭自己的 Skill 系统，我建议怎么做</h2><p>最后给一个更务实的建议。</p><p>如果你准备在自己的团队里推广 Skill，我不建议一上来就做几十个。</p><p>更稳的路径通常是：</p><h3 id="第一步：先找高频且稳定的任务">第一步：先找高频且稳定的任务</h3><p>比如：</p><ul><li>需求澄清</li><li>技术方案写作</li><li>代码审查</li><li>日志排障</li></ul><p>这些任务的共同点是：</p><ul><li>高频</li><li>有固定结构</li><li>容易标准化</li></ul><h3 id="第二步：先把方法写成-Skill，不要急着全自动">第二步：先把方法写成 Skill，不要急着全自动</h3><p>一开始最重要的，不是“写多厉害的脚本”，而是先把：</p><ul><li>顺序</li><li>边界</li><li>输入输出</li></ul><p>定义清楚。</p><h3 id="第三步：再把资源和脚本往外挪">第三步：再把资源和脚本往外挪</h3><p>等 Skill 稳定后，再逐步补：</p><ul><li><code>references/</code></li><li><code>scripts/</code></li><li>模板</li><li>MCP 接入</li></ul><h3 id="第四步：最后再做治理">第四步：最后再做治理</h3><p>也就是：</p><ul><li>安装</li><li>统计</li><li>发布</li><li>沉淀</li></ul><p>这样做，系统会稳很多。</p><hr><h2 id="十一、最后一句话：我为什么觉得-Skill-值得认真做">十一、最后一句话：我为什么觉得 Skill 值得认真做</h2><p>因为它解决的不是一个“小技巧”问题，而是一个“AI 如何真正进入工作流”的问题。</p><p>过去很多人用 AI，像在和一个聪明聊天机器人对话。<br>但当你开始把：</p><ul><li>规则</li><li>方法</li><li>工具</li><li>资源</li><li>治理</li></ul><p>这些东西组合起来以后，AI 才会开始更像一个真正能协作的执行系统。</p><p>而 <code>Skill</code>，正是这条路上非常关键的一层。</p><p>它让我们第一次可以比较系统地回答这个问题：</p><blockquote><p>怎么把个人经验，变成团队可复用、可维护、可组合、可演进的 Agent 能力。</p></blockquote><p>这也是为什么我越来越愿意把 <code>dev-skill</code> 这种仓库，看成一种新的研发基础设施，而不只是“AI 时代的提示词收藏夹”。</p><hr><h2 id="延伸阅读">延伸阅读</h2><ul><li>如果你想看一篇更偏实操的 <code>Superpowers</code> 使用说明，可以读 <a href="/posts/superpowers-guide-coding-agent-workflow.html">Superpowers使用指南：把Claude Code和Codex变成真正会按流程工作的开发Agent</a>。</li><li>如果你想看多 Agent 和 tmux 分屏怎么落成协作流水线，可以读 <a href="/posts/agent-team-tutorial-tmux-workflow.html">Agent Team使用教程：用Tmux分屏把多个AI串成自动协作流水线</a>。</li><li>如果你想看一次真实的分析、开发、审查闭环，可以读 <a href="/posts/claude-code-agent-team-collaboration-record.html">Claude Code Agent Team协作全记录：3个AI如何串起需求、开发与审查闭环</a>。</li><li>如果你想继续往底层理解 harness、memory、skill 的工程结构，可以读 <a href="/posts/claude-code-harness-rag-skill-memory.html">Claude Code Harness深度分析：从RAG、Skill、Memory到企业Agent落地</a>。</li></ul>]]></content>
    
    
    <summary type="html">结合 2026 年 6 月 9 日的内部分享材料和 dev-skill 仓库，系统解释什么是 Skill、它和 Rules/Commands/MCP 的区别、渐进式披露的原理，以及 Skill 在真实研发流程里到底能做什么。</summary>
    
    
    
    <category term="技术笔记" scheme="https://u7u7.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="AI" scheme="https://u7u7.top/tags/AI/"/>
    
    <category term="Agent" scheme="https://u7u7.top/tags/Agent/"/>
    
    <category term="Claude Code" scheme="https://u7u7.top/tags/Claude-Code/"/>
    
    <category term="MCP" scheme="https://u7u7.top/tags/MCP/"/>
    
    <category term="Skill" scheme="https://u7u7.top/tags/Skill/"/>
    
    <category term="Codex" scheme="https://u7u7.top/tags/Codex/"/>
    
    <category term="Workflow" scheme="https://u7u7.top/tags/Workflow/"/>
    
  </entry>
  
  <entry>
    <title>Claude Code Agent Team协作全记录：3个AI如何串起需求、开发与审查闭环</title>
    <link href="https://u7u7.top/posts/claude-code-agent-team-collaboration-record.html"/>
    <id>https://u7u7.top/posts/claude-code-agent-team-collaboration-record.html</id>
    <published>2026-06-05T02:00:00.000Z</published>
    <updated>2026-06-05T02:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Claude-Code-Agent-Team协作全记录：3个AI如何串起需求、开发与审查闭环">Claude Code Agent Team协作全记录：3个AI如何串起需求、开发与审查闭环</h1><blockquote><p>原始内容整理自飞书文档：<a href="https://qima.feishu.cn/wiki/N9NywQbhWi4YJXkEv12c108ynhh">Claude Code Agent Team 协作全记录</a>。本文已按 Hexo 博客格式重构，并将飞书中的字符画流程图改写为 Mermaid。</p></blockquote><p>三个 AI，一条心，从需求到代码到审查，全自动串联工作流。</p><p>这篇文章记录的是一次很典型、也很有代表性的多 Agent 协作实战：一个需求进来后，不再由单个 AI 从头干到尾，而是拆成三个角色分别负责分析、开发和审查，然后在 Team Lead 的协调下，串成一条完整闭环。</p><p>如果只看一句话，这次实验验证的是：</p><blockquote><p>AI 真正有价值的地方，不只是“会写代码”，而是能不能把分析、实现、审查、修复和验证这一整条链路接起来。</p></blockquote><hr><h2 id="一、先看全景">一、先看全景</h2><h3 id="1-Team-角色分工">1. Team 角色分工</h3><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart LR    analyst[&quot;🟩 analyst&lt;br&#x2F;&gt;需求分析专家&lt;br&#x2F;&gt;brainstorming&lt;br&#x2F;&gt;Tmux %2&quot;]    developer[&quot;🟨 developer&lt;br&#x2F;&gt;代码实现专家&lt;br&#x2F;&gt;sdd-dev-workflow&lt;br&#x2F;&gt;Tmux %3&quot;]    reviewer[&quot;🟦 reviewer&lt;br&#x2F;&gt;代码审查专家&lt;br&#x2F;&gt;full-review&lt;br&#x2F;&gt;Tmux %1&quot;]    lead[&quot;🫵 Team Lead&lt;br&#x2F;&gt;就是本 AI&quot;]    lead --&gt; analyst    analyst --&gt; developer    developer --&gt; reviewer    reviewer --&gt; lead  </pre></div><h3 id="2-一张表看懂这支-Team">2. 一张表看懂这支 Team</h3><table><thead><tr><th>角色</th><th>定位</th><th>主要 Skill</th><th>主要职责</th></tr></thead><tbody><tr><td><code>analyst</code></td><td>需求分析专家</td><td><code>brainstorming</code></td><td>理清需求、拆解目标、识别风险</td></tr><tr><td><code>developer</code></td><td>代码实现专家</td><td><code>sdd-dev-workflow</code></td><td>按规范建提案、写代码、补测试</td></tr><tr><td><code>reviewer</code></td><td>代码审查专家</td><td><code>full-review</code></td><td>做深度审查、提问题、验证修复</td></tr><tr><td><code>Team Lead</code></td><td>协调者</td><td>编排与调度</td><td>分派任务、推进流程、触发闭环</td></tr></tbody></table><hr><h2 id="二、这次任务是什么">二、这次任务是什么</h2><p>用户原始需求非常直接：</p><blockquote><p>“新增一个预约单完成以后会发 NSQ topic：<code>booking_appointment_completed</code>。进行消息推送。”</p></blockquote><p>翻译成更业务化的话，就是：</p><blockquote><p>预约做完了，要给商家发一个通知。</p></blockquote><p>这看起来像一个小需求，但一旦往工程里落，就会立刻带出一串问题：</p><ul><li>事件从哪里来</li><li>用哪条消费链路接</li><li>如何复用已有通知架构</li><li>是否会有重复消息</li><li>异常怎么兜底</li><li>配置降级怎么做</li><li>改完之后怎么确认没有副作用</li></ul><p>也正因为如此，这种任务很适合用来验证多 Agent 串联工作流是否真的能跑通。</p><hr><h2 id="三、三类-Skill-是怎么配合的">三、三类 Skill 是怎么配合的</h2><h3 id="1-brainstorming：先把需求想清楚">1. <code>brainstorming</code>：先把需求想清楚</h3><p>这是 <code>analyst</code> 的核心工具，重点不是直接产出代码，而是先把问题想清楚。</p><table><thead><tr><th>项目</th><th>内容</th></tr></thead><tbody><tr><td>装备者</td><td><code>analyst</code></td></tr><tr><td>用途</td><td>在动手之前先做需求探索</td></tr><tr><td>触发时机</td><td>收到新需求时</td></tr><tr><td>关注点</td><td>目标、边界、方案、风险</td></tr></tbody></table><p>本次实战里，<code>analyst</code> 主要完成了三件事：</p><ul><li>识别核心功能：预约完成通知推送</li><li>设计技术方案：复用现有消费者架构</li><li>发现 4 类风险：事务丢失、消息重复、异常处理、配置降级</li></ul><h3 id="2-sdd-dev-workflow：按规范把代码写出来">2. <code>sdd-dev-workflow</code>：按规范把代码写出来</h3><p>这是 <code>developer</code> 的核心工具，强调的是规范化和工程化，而不是“想到哪写到哪”。</p><table><thead><tr><th>项目</th><th>内容</th></tr></thead><tbody><tr><td>装备者</td><td><code>developer</code></td></tr><tr><td>用途</td><td>按标准流程推进开发</td></tr><tr><td>触发时机</td><td>需求分析完成后</td></tr><tr><td>关注点</td><td>OpenSpec、任务拆解、实现顺序、单测补齐</td></tr></tbody></table><p>本次实战里，<code>developer</code> 交付了这些结果：</p><ul><li>创建 OpenSpec 提案，形成 13 个任务</li><li>实现预约完成消费者与订阅器</li><li>补充单元测试，新增 4 个用例</li></ul><h3 id="3-full-review：把质量关守住">3. <code>full-review</code>：把质量关守住</h3><p>这是 <code>reviewer</code> 的核心工具，目标不是“简单看一眼”，而是系统性地扫出问题。</p><table><thead><tr><th>项目</th><th>内容</th></tr></thead><tbody><tr><td>装备者</td><td><code>reviewer</code></td></tr><tr><td>用途</td><td>多维度做代码审查</td></tr><tr><td>触发时机</td><td>代码开发完成后</td></tr><tr><td>关注点</td><td>代码质量、安全、性能、架构、测试、配置、文档等</td></tr></tbody></table><p>本次实战里，<code>reviewer</code> 的产出是：</p><ul><li>第一轮审查发现 <code>1</code> 个 High 和 <code>2</code> 个 Medium 问题</li><li>推动修复闭环继续向下执行</li><li>第二轮复审确认问题已解决</li><li>输出完整审查报告</li></ul><h3 id="4-三个-Skill-放在一起，形成的不是功能集合，而是工作流">4. 三个 Skill 放在一起，形成的不是功能集合，而是工作流</h3><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart TD    req[&quot;用户需求输入&quot;] --&gt; a[&quot;🟩 analyst&lt;br&#x2F;&gt;brainstorming&quot;]    a --&gt; d[&quot;🟨 developer&lt;br&#x2F;&gt;sdd-dev-workflow&quot;]    d --&gt; r[&quot;🟦 reviewer&lt;br&#x2F;&gt;full-review&quot;]    r --&gt; check{&quot;发现问题？&quot;}    check -- &quot;是&quot; --&gt; loop[&quot;进入修复闭环&quot;]    check -- &quot;否&quot; --&gt; done[&quot;🎉 交付完成&quot;]    loop --&gt; a  </pre></div><p>这张图最关键的点在于：</p><ul><li>需求不是直接丢给开发</li><li>开发完成不代表流程结束</li><li>审查结果会反向驱动修复</li><li>修复完成后还会再次回到审查</li></ul><p>也就是说，这不是线性流水线，而是一条可以回环的链路。</p><hr><h2 id="四、完整协作流程是怎么跑起来的">四、完整协作流程是怎么跑起来的</h2><h3 id="第一幕：需求分析">第一幕：需求分析</h3><p>流程的第一步，是由 Team Lead 把需求派发给 <code>analyst</code>。</p><p><code>analyst</code> 启动 <code>brainstorming</code> 后，主要完成以下动作：</p><ul><li>理解这次需求到底要解决什么问题</li><li>判断是否可以复用现有消费者架构</li><li>识别实现过程中的关键风险</li><li>输出一份结构化的需求分析结果</li></ul><p>这一阶段的价值不在“写了什么文案”，而在于它把原本藏在人脑里的隐性判断显式化了。</p><h3 id="第二幕：代码实现">第二幕：代码实现</h3><p>拿到分析结果后，<code>developer</code> 启动 <code>sdd-dev-workflow</code>，正式进入开发阶段。</p><p>这一阶段不是直接开写，而是先：</p><ol><li>创建 OpenSpec 提案</li><li>生成变更目录和任务清单</li><li>跑 <code>openspec validate --strict</code></li><li>按任务顺序推进开发</li></ol><p>随后才进入具体编码，实现：</p><ul><li><code>Scene.COMPLETED</code> 枚举</li><li><code>AppointmentCompletedConsumer</code></li><li><code>AppointmentCompletedSubscriber</code></li><li>对应单元测试</li></ul><h3 id="第三幕：代码审查">第三幕：代码审查</h3><p>开发完成后，任务切换到 <code>reviewer</code>。</p><p><code>reviewer</code> 启动 <code>full-review</code> 后，并不是简单给一句“LGTM”，而是进行系统性审查，包括：</p><ol><li>代码质量</li><li>安全性</li><li>性能</li><li>架构</li><li>兼容性</li><li>测试覆盖</li><li>错误处理</li><li>配置管理</li><li>依赖管理</li><li>文档完整性</li></ol><p>第一轮审查结果并不完美，发现了：</p><ul><li><code>1</code> 个 High 问题：硬编码维护风险</li><li><code>2</code> 个 Medium 问题：测试覆盖不足、摘要格式不匹配</li></ul><p>这也是这次实验最有意思的地方：流程没有停在“发现问题”，而是继续往下走。</p><hr><h2 id="五、真正关键的部分：自动修复闭环">五、真正关键的部分：自动修复闭环</h2><h3 id="1-闭环是怎么工作的">1. 闭环是怎么工作的</h3><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart LR    issue[&quot;发现问题 ❌&quot;] --&gt; confirm[&quot;analyst 确认问题 🤔&quot;]    confirm --&gt; repair[&quot;developer 修复问题 🔧&quot;]    repair --&gt; recheck[&quot;reviewer 复审 ✅&quot;]    recheck --&gt; pass{&quot;通过了吗？&quot;}    pass -- &quot;否&quot; --&gt; confirm    pass -- &quot;是&quot; --&gt; done[&quot;🎉 广播完成&quot;]  </pre></div><h3 id="2-这次闭环里具体修了什么">2. 这次闭环里具体修了什么</h3><p>在这次实战中，修复动作主要包括：</p><ul><li>给 <code>Scene</code> 枚举新增模板字段</li><li>消除 <code>switch-case</code> 的硬编码风险</li><li>补充缺失的单元测试用例</li><li>统一消息摘要格式</li></ul><p>修完之后，再次触发 <code>reviewer</code> 复审，确认：</p><ul><li>High 问题已解决</li><li>Medium 问题已解决</li></ul><p>这才算真正结束。</p><h3 id="3-为什么这个闭环比“AI-写完代码就停”更有意义">3. 为什么这个闭环比“AI 写完代码就停”更有意义</h3><p>很多 AI 开发演示，只展示“从需求到代码生成”这一段，看起来很顺，但真实研发里最容易出问题的，恰恰是后半段：</p><ul><li>写完以后有没有遗漏</li><li>改动有没有隐藏风险</li><li>问题发现后谁来确认</li><li>修完以后谁来验证</li></ul><p>这次流程最有价值的地方，就是把这些后续动作也纳入了自动串联。</p><hr><h2 id="六、一张图看完整执行路径">六、一张图看完整执行路径</h2><div class="mermaid-wrap"><pre class="mermaid-src" hidden>  flowchart TD    user[&quot;用户需求&quot;] --&gt; analyst[&quot;analyst&lt;br&#x2F;&gt;需求分析&quot;]    analyst --&gt; plan[&quot;📄 技术方案&quot;]    plan --&gt; developer[&quot;developer&lt;br&#x2F;&gt;代码实现&quot;]    developer --&gt; code[&quot;💻 7 个代码文件&quot;]    code --&gt; reviewer[&quot;reviewer&lt;br&#x2F;&gt;代码审查&quot;]    reviewer --&gt; report[&quot;📋 审查报告&quot;]    report --&gt; problem{&quot;发现问题？&quot;}    problem -- &quot;YES&quot; --&gt; fix[&quot;闭环修复&quot;]    fix --&gt; analyst    problem -- &quot;NO&quot; --&gt; finish[&quot;🎉 完成&quot;]  </pre></div><p>如果把它抽象成一句话，就是：</p><blockquote><p><code>analyst</code> 负责想清楚，<code>developer</code> 负责写出来，<code>reviewer</code> 负责看仔细，而 Team Lead 负责让这条链路不停下来。</p></blockquote><hr><h2 id="七、最终交付了什么">七、最终交付了什么</h2><h3 id="1-代码交付物">1. 代码交付物</h3><table><thead><tr><th>文件</th><th>类型</th><th>状态</th></tr></thead><tbody><tr><td><code>AppointmentMessageConstant.java</code></td><td>修改</td><td>新增 <code>COMPLETED</code> 枚举与模板字段</td></tr><tr><td><code>BizScenarioConstant.java</code></td><td>修改</td><td>新增 <code>APPOINTMENT_COMPLETED</code></td></tr><tr><td><code>UserNotificationCenterMaterial.java</code></td><td>修改</td><td>新增 <code>APPOINTMENT_COMPLETED</code> 素材</td></tr><tr><td><code>BaseAppointmentSubscriber.java</code></td><td>修改</td><td>重构为枚举关联模式</td></tr><tr><td><code>AppointmentCompletedConsumer.java</code></td><td>新增</td><td>预约完成消费者</td></tr><tr><td><code>AppointmentCompletedSubscriber.java</code></td><td>新增</td><td>预约完成订阅器</td></tr><tr><td><code>AppointmentCompletedConsumerTest.java</code></td><td>新增</td><td>单元测试，新增 4 个用例</td></tr></tbody></table><h3 id="2-过程文档产出">2. 过程文档产出</h3><h4 id="OpenSpec-提案">OpenSpec 提案</h4><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">retail-trade-event/openspec/changes/</span><br><span class="line">└── add-appointment-completed-notification/</span><br><span class="line">    ├── CHANGELOG.md</span><br><span class="line">    ├── manifest.json</span><br><span class="line">    └── tasks.md</span><br></pre></td></tr></table></figure><h4 id="代码审查报告">代码审查报告</h4><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">.doc/appointment-completed-feature/</span><br><span class="line">└── full-review-20260303-120000.md</span><br><span class="line">    ├── 一、基本信息</span><br><span class="line">    ├── 二、变更说明 + 架构图</span><br><span class="line">    ├── 三、影响面分析 + 调用链路图</span><br><span class="line">    ├── 四、问题清单</span><br><span class="line">    └── 五、集成测试用例清单</span><br></pre></td></tr></table></figure><h4 id="协作总结文档">协作总结文档</h4><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">memory/</span><br><span class="line">├── agent-team-collaboration-report.md</span><br><span class="line">└── agent-team-collaboration-story.md</span><br></pre></td></tr></table></figure><hr><h2 id="八、这套工作流的几个亮点">八、这套工作流的几个亮点</h2><h3 id="1-自动发现问题">1. 自动发现问题</h3><p>不是人工 review 才能往后走，而是 <code>reviewer</code> 可以主动把问题抛出来，继续驱动后续链路。</p><h3 id="2-自动确认修复">2. 自动确认修复</h3><p>发现问题后，不是直接让开发盲改，而是先由 <code>analyst</code> 确认问题成立，再把修复任务交给 <code>developer</code>。</p><h3 id="3-自动验证闭环">3. 自动验证闭环</h3><p>修复动作做完后，不是默认“应该没问题了”，而是重新交回 <code>reviewer</code> 做复审。</p><h3 id="4-人工干预被压到最低">4. 人工干预被压到最低</h3><p>这次流程里，人工更多是做启动和协调，而不是每一步手动盯进度。只要框架搭好了，后面就可以顺着链路自己往前滚。</p><hr><h2 id="九、这次实战的性能数据">九、这次实战的性能数据</h2><table><thead><tr><th>指标</th><th>数值</th></tr></thead><tbody><tr><td>Team 成员</td><td>3 个 AI</td></tr><tr><td>总耗时</td><td>约 40 分钟</td></tr><tr><td>消息交互</td><td>20+ 次</td></tr><tr><td>代码文件</td><td>7 个</td></tr><tr><td>单元测试</td><td>4 个新增用例</td></tr><tr><td>审查轮次</td><td>2 轮</td></tr><tr><td>修复问题</td><td><code>1</code> 个 High + <code>2</code> 个 Medium</td></tr><tr><td>文档产出</td><td>3 份</td></tr></tbody></table><hr><h2 id="十、最后总结一下">十、最后总结一下</h2><h3 id="Team-分别做了什么">Team 分别做了什么</h3><table><thead><tr><th>成员</th><th>核心动作</th><th>Skill</th><th>结果</th></tr></thead><tbody><tr><td><code>analyst</code></td><td>想清楚</td><td><code>brainstorming</code></td><td>需求分析与风险识别</td></tr><tr><td><code>developer</code></td><td>写出来</td><td><code>sdd-dev-workflow</code></td><td>代码实现与问题修复</td></tr><tr><td><code>reviewer</code></td><td>看仔细</td><td><code>full-review</code></td><td>代码审查与质量把关</td></tr></tbody></table><h3 id="Team-Lead-做了什么">Team Lead 做了什么</h3><ul><li>搭团队：创建 3 个 Agent，并配置各自 Skill</li><li>分任务：把需求按分析、开发、审查分段派发</li><li>协调流：推动“发现问题 -&gt; 确认 -&gt; 修复 -&gt; 复审”</li><li>写报告：沉淀过程与结果</li></ul><h3 id="最值得记住的一点">最值得记住的一点</h3><p>这次实验最酷的地方，并不是“AI 会写代码”。</p><p>真正值得记住的是：</p><blockquote><p>当 <code>brainstorming</code>、<code>sdd-dev-workflow</code>、<code>full-review</code> 这些 Skill 被串成一条工作流后，AI 开始不再只是一个点状工具，而更像一个能持续推进任务的协作系统。</p></blockquote><p>换句话说，这套 Team 的意义，不在于把某一次需求做完，而在于它证明了一件事：</p><blockquote><p>发现问题、确认问题、修复问题、验证修复，这整条闭环，是可以被多 Agent 协作稳定接住的。</p></blockquote><hr><h2 id="十一、下一步还能做什么">十一、下一步还能做什么</h2><p>这支 Team 后面还可以继续用在：</p><ul><li>新需求开发</li><li>Bug 修复</li><li>性能优化</li><li>文档补齐</li></ul><p>只要任务边界足够清楚，角色足够明确，Skill 足够稳定，这种多 Agent 串联模式就可以继续往更复杂的研发场景延伸。</p><hr><p><em>生成时间：2026-03-03</em><br><em>Team：<code>dev-collaboration</code></em><br><em>模式：<code>tmux + agent collaboration</code></em><br><em>Team Lead：就是本 AI</em></p>]]></content>
    
    
    <summary type="html">一次真实实战复盘：用 analyst、developer、reviewer 三个 Agent，配合 brainstorming、sdd-dev-workflow、full-review 三个 Skill，把一个需求从分析、开发到审查和修复串成自动闭环。</summary>
    
    
    
    <category term="技术笔记" scheme="https://u7u7.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="AI" scheme="https://u7u7.top/tags/AI/"/>
    
    <category term="Agent" scheme="https://u7u7.top/tags/Agent/"/>
    
    <category term="Claude Code" scheme="https://u7u7.top/tags/Claude-Code/"/>
    
    <category term="Skill" scheme="https://u7u7.top/tags/Skill/"/>
    
    <category term="Workflow" scheme="https://u7u7.top/tags/Workflow/"/>
    
    <category term="Multi-Agent" scheme="https://u7u7.top/tags/Multi-Agent/"/>
    
  </entry>
  
  <entry>
    <title>AI Learning Blueprint(H1)：从 AI Coding 到 AI Workflow，我在 AI 小组这半年做了什么</title>
    <link href="https://u7u7.top/posts/ai-learning-blueprint-h1.html"/>
    <id>https://u7u7.top/posts/ai-learning-blueprint-h1.html</id>
    <published>2026-06-04T03:19:39.000Z</published>
    <updated>2026-06-04T03:19:39.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言-👋">前言 👋</h2><p>如果要用一句话总结我上半年在 AI 小组做的事情，那不是“我学会了怎么让 AI 帮我写代码”，而是：<strong>我开始从 AI Coding 往 AI Workflow 走了。</strong></p><p>这半年做的事情表面上很散：写 skills、搭工作流、做 JIRA 修复闭环、沉淀项目记忆、研究 Agent 协作、写分享文档、搞安装和推广。但如果往回看，会发现这些事情其实都在围绕同一件事打转：<strong>怎么让 AI 不只是补一段代码，而是真正参与研发过程，并且这套能力不只我自己能用，团队也能直接接起来用。</strong></p><p>说白了，我现在越来越不信“AI Coding 提效”这种太轻飘飘的说法了。提效当然有，但如果只是快写两段代码，价值其实没那么大。真正值钱的，是它能不能把分析、定位、方案、改码、审查、验证这一整条链路跑通。单点提效谁都能吹，工作流跑不跑得通，才决定这玩意到底是不是生产力。</p><p>而且这半年我还有一个很直接的感受：<strong>做一个 demo 不难，做一个自己用着爽的东西也不难，难的是把它做成别人愿意装、愿意用、用一次没骂人、用第二次还想继续用的东西。</strong> 这也是为什么我后面越来越关注产品形态、稳定性、可复制性，而不是继续沉迷在“我又搞出一个新 skill”这种短期爽感里。</p><p>这篇文章就想复盘一下，我上半年在 AI 小组到底做了什么，想清楚了什么，沉淀了什么，又踩了哪些坑。</p><h2 id="这组复盘可以怎么读">这组复盘可以怎么读</h2><p>这次我把最近一段时间在 Codex 里的 AI 研发实践整理成 5 篇文章。它们不是孤立的笔记，而是从“我做了什么”到“系统该怎么搭”的一条线：</p><table><thead><tr><th>顺序</th><th>文章</th><th>重点</th></tr></thead><tbody><tr><td>1</td><td><a href="/posts/ai-learning-blueprint-h1.html">AI Learning Blueprint(H1)：从 AI Coding 到 AI Workflow，我在 AI 小组这半年做了什么</a></td><td>总览这半年做过的工作、产出和认知变化</td></tr><tr><td>2</td><td><a href="/posts/jira-trace-agent-harness-review.html">JIRA与Trace分析自动化复盘：从Airbag到NVWA的Agent Harness优化</a></td><td>复盘 JIRA 检查、Trace 分析、无 trace 问题流和排障闭环</td></tr><tr><td>3</td><td><a href="/posts/skill-literacy-dev-skill-workflow.html">Skill扫盲：从提示词补丁到AI工作流，我如何用dev-skill把经验沉淀成能力系统</a></td><td>解释 Skill、渐进式披露和 dev-skill 的工程化沉淀</td></tr><tr><td>4</td><td><a href="/posts/claude-code-agent-team-collaboration-record.html">Claude Code Agent Team协作全记录：3个AI如何串起需求、开发与审查闭环</a></td><td>记录 analyst / developer / reviewer 三个 Agent 的协作实战</td></tr><tr><td>5</td><td><a href="/posts/claude-code-harness-rag-skill-memory.html">Claude Code Harness深度分析：从RAG、Skill、Memory到企业Agent落地</a></td><td>从 harness 角度理解 RAG、Skill、Memory 和企业 Agent 落地</td></tr></tbody></table><p>如果只想快速了解我这段时间具体干了什么，先读第 1 篇和第 2 篇。<br>如果更关心 Agent 工程怎么搭，建议接着读第 3 到第 5 篇。</p><h2 id="先看结论-📌">先看结论 📌</h2><p>如果只用一张表来概括这半年，我大概会这样总结：</p><table><thead><tr><th>主题</th><th>这半年主要在做什么</th><th>我现在更在意什么</th></tr></thead><tbody><tr><td>AI Coding</td><td>写 skill、接代码能力、提升单点效率</td><td>不只写得快，而是写得稳</td></tr><tr><td>AI Workflow</td><td>把需求、排障、修复、验证串成链路</td><td>流程能不能真正跑通</td></tr><tr><td>技能沉淀</td><td>把个人经验抽成可复用 skills</td><td>别人能不能装上就用</td></tr><tr><td>项目理解</td><td>补 RAG、memory、项目记忆</td><td>AI 能不能少“失忆”一点</td></tr><tr><td>产品化</td><td>写文档、做入口、统一交互方式</td><td>团队愿不愿意长期用</td></tr></tbody></table><hr><h2 id="一、上半年到底在干什么？-🧩">一、上半年到底在干什么？ 🧩</h2><h3 id="半年主线速览-🗺️">半年主线速览 🗺️</h3><table><thead><tr><th>主线</th><th>代表内容</th><th>想解决的问题</th></tr></thead><tbody><tr><td>高频工作流</td><td><code>daily-one-click</code>、<code>jira-one-click</code></td><td>把高频研发动作标准化</td></tr><tr><td>能力资产化</td><td>Dev-Skill 仓库、40+ skills</td><td>把“我会”变成“大家都能用”</td></tr><tr><td>产品化表达</td><td>分享文档、统一入口、安装方式</td><td>降低理解和接入门槛</td></tr><tr><td>项目理解增强</td><td>项目记忆、RAG、多层 memory</td><td>让 AI 真正理解上下文</td></tr></tbody></table><h3 id="1-先把最高频的两类事情做成标准工作流-⚙️">1. 先把最高频的两类事情做成标准工作流 ⚙️</h3><p>上半年我投入最多精力的，其实是两类非常高频的研发场景：</p><ul><li>日常需求开发</li><li>JIRA 修复 / 问题排查</li></ul><p>围绕这两类场景，后面逐渐沉淀出了两条主链路：</p><ul><li><code>daily-one-click</code></li><li><code>jira-one-click</code></li></ul><p>这两条链路最重要的一点，不是“把 AI 接进来了”，而是<strong>开始让 AI 参与完整链路，而不是只在最后一步写代码。</strong></p><p>以前大家聊 AI coding，很容易把注意力放在“这段代码生成得快不快、准不准”。但真干活的时候会发现，最耗时间的根本不是敲代码，而是前面的那一大坨：</p><ul><li>需求到底在说什么</li><li>问题到底出在哪</li><li>日志怎么查</li><li>边界怎么定</li><li>方案怎么收敛</li><li>改完之后影响面怎么判断</li><li>最后怎么验证和收口</li></ul><p>所以我后面的方向就越来越明确：<strong>不再盯着让 AI 帮我省一段代码的时间，而是想办法让它把整条研发流程跑得更顺。</strong></p><p>这件事说起来像常识，但真做起来完全不是那么回事。因为你会发现，代码只是最后露在外面的那一层，前面那些分析、判断、切换上下文、对齐边界的动作，才是真正把人耗死的地方。AI 如果只能碰最后那 10%，那它再强也就那样。</p><h3 id="2-把个人经验从“我会”变成“别人也能直接用”-🔁">2. 把个人经验从“我会”变成“别人也能直接用” 🔁</h3><p>如果一套东西只有我自己会用，那它最多算个人技巧，不算真正的沉淀。</p><p>所以这半年我一直在做的另一件事，就是把自己的经验往 skills 和 workflow 上抽。把原来靠感觉、靠经验、靠多轮对话才能跑通的事情，尽量变成可以复用、可以安装、可以推广的能力资产。</p><p>到现在为止，已经陆续沉淀了 40+ 工程化 skills，覆盖的内容大概包括：</p><ul><li>JIRA 分析与修复</li><li>日志排查</li><li>技术方案编写</li><li>需求结构化</li><li>应用扫描</li><li>Spec 驱动开发</li><li>Code Review</li><li>Agent 协作流程</li><li>项目记忆与上下文管理</li></ul><p>这背后我自己的判断也变了。以前会更在意“这个能力我有没有”，现在更在意的是：</p><blockquote><p><strong>这个东西别人能不能拿起来就用。</strong></p></blockquote><p>如果不能，那它还是太依赖人，不够像产品。</p><p>而且我中间其实也踩过一个挺典型的坑：<strong>很容易沉迷在“再做一个 skill”这件事本身。</strong> 因为做 skill 有即时反馈，能跑起来就会很爽，看着数量涨也会有成就感。但后来回来一看，发现如果没有统一入口、没有场景边界、没有安装方式、没有配套说明，那很多 skill 本质上只是“作者自己最懂”的半成品。</p><p>所以我现在会更警惕那种“技能数量越来越多，看起来很繁荣，实际上别人根本接不住”的状态。</p><h3 id="3-从个人提效，慢慢走到产品思维-🧠">3. 从个人提效，慢慢走到产品思维 🧠</h3><p>这是我上半年挺明显的一个变化。</p><p>一开始做这些东西的时候，还是会比较自然地从“怎么提高我自己的开发效率”出发。但越往后做，越会发现如果真想往组内推广、甚至做成更通用的 AI 产品，判断标准就得变。</p><p>不能只看“我自己用着爽不爽”，而要看：</p><ul><li>够不够简单</li><li>稳不稳定</li><li>能不能复制</li><li>团队能不能低成本接起来</li><li>这个产品形态别人愿不愿意接受</li></ul><p>比如我后面写《告别传统：用 Vibe Working 标准化日常开发和 JIRA 修复流程》，本质上就不是在介绍某个 skill，而是在试着把一套 AI 研发方式包装成<strong>团队可以理解、可以安装、可以快速见效的产品。</strong></p><p>这件事对我影响挺大的。因为它让我慢慢意识到，做一个“能演示”的 AI 东西不算难，难的是把它做成别人真的愿意长期用的东西。</p><p>很多东西你自己用的时候会自动脑补，但别人不会。你知道下一步该点哪、该看哪、该怎么失败重试，不代表别人也知道。所以产品思维这件事，说难听点，就是<strong>别老拿自己的熟练度骗自己。</strong></p><h3 id="4-开始补-Agent-最难的那一块：项目理解能力-🧭">4. 开始补 Agent 最难的那一块：项目理解能力 🧭</h3><p>如果说前面的工作是在解决“让 AI 会做事”，那我后面越来越在意的是另一件更底层的事情：</p><blockquote><p><strong>AI 到底怎么才能理解项目？</strong></p></blockquote><p>很多时候 AI 效果不好，不是因为它不会写代码，而是因为它根本不理解当前上下文：</p><ul><li>不知道这个项目有哪些模块</li><li>不知道业务边界在哪里</li><li>不知道哪些文档是关键上下文</li><li>不知道哪些经验是已经达成的共识</li><li>不知道应该先调哪个 skill、看哪个文件、走哪条路径</li></ul><p>所以我后面开始主动补这方面的东西：</p><ul><li>RAG</li><li>多层 memory</li><li>项目记忆</li><li>渐进式披露</li><li>Agent 工程评估</li><li>多 Agent 协作方式</li></ul><p>我现在会觉得，这些东西才是真正决定 AI 能不能从“会聊天、会补代码”往“能参与项目协作”升级的关键。</p><p>说得再直接一点，很多 AI 产品现在最烦人的地方，不是它笨，而是它<strong>老失忆</strong>。你今天刚教会它，明天又像新来的；你刚总结完一轮共识，下一轮它又从零开始问。这个问题不解决，模型再强都容易停留在“偶尔惊艳、长期难用”的阶段。</p><hr><h2 id="二、几个让我印象很深的真实案例-💡">二、几个让我印象很深的真实案例 💡</h2><h3 id="1-daily-one-click：不是帮我写，而是帮我把开发链路搭起来-🛠️">1. <code>daily-one-click</code>：不是帮我写，而是帮我把开发链路搭起来 🛠️</h3><p><code>daily-one-click</code> 这条线，最开始我想解决的并不是“让 AI 直接产出代码”，而是日常开发里前面那一长串容易散、容易断、容易来回反复的工作。</p><p>以前一个需求过来，人脑里其实会自动做很多切换：先理解需求，再澄清范围，再整理 PRD 或方案，再分析影响应用，再决定用什么开发方式，最后才是写代码。问题在于，这一套东西如果不沉淀，永远只能靠人顶着干。</p><p><code>daily-one-click</code> 想做的事情，就是把这条链路尽量标准化，让需求从“我知道要干什么”，变成“AI 和人都知道下一步该怎么推进”。</p><p>这件事带来的最大变化，不是省了几分钟，而是<strong>让我开始把“开发”看成一个可以被编排的流程，而不是只靠熟练工经验硬顶。</strong></p><p>而且这条线还有个很现实的价值：它能把很多原本藏在脑子里的默认动作摊开。以前你觉得“这不就是正常开发流程吗”，但真要让 AI 跑、让别人复用的时候，才会发现所谓“正常流程”里其实藏了大量默认知识，不拆出来就永远传不下去。</p><p><img src="https://img.u7u7.top/blognoteImg/rag-page.png" alt="需求澄清 Agent 与知识库页面"></p><p><img src="https://img.u7u7.top/blognoteImg/kb-page.png" alt="业务知识库与研发底座页面"></p><h3 id="2-jira-one-click-jira-trace-fix-cr：AI-真正能打的地方是排障闭环-🚨">2. <code>jira-one-click</code> / <code>jira-trace-fix-cr</code>：AI 真正能打的地方是排障闭环 🚨</h3><p>如果说哪条线最能体现 AI Workflow 的价值，我觉得还是 JIRA 修复这条。</p><p>因为排障这件事本来就不是纯代码问题，它天然就是一条长链路：</p><ul><li>先看 JIRA 在说什么</li><li>抓关键词、时间窗、环境、应用线索</li><li>找 traceId</li><li>查日志</li><li>顺着调用链看根因</li><li>判断是配置问题、代码问题还是数据问题</li><li>如果要改代码，再继续走修复、CR、验证</li></ul><p>这里面随便断一环，后面都跑不起来。</p><p>所以我后面比较坚定的一点就是：<strong>AI 最值得用的地方，不是让它凭空生成一坨代码，而是让它参与这种本来就很依赖上下文和步骤感的排障闭环。</strong></p><p>尤其是像 <code>jira-trace-fix-cr</code> 这种思路，核心不是“帮我分析一下”，而是尽量别停在分析层，而是一路往下走到修复和收口。这一点我现在挺看重的，因为只分析不落地，很多时候意义没那么大。</p><p>说白了，JIRA 这种场景太适合拿来检验 AI 到底是不是生产力了。因为这里没法靠花哨输出混过去，链路断了就是断了，根因没找对就是没找对，修不了就是修不了。它很残酷，但也很诚实。</p><p><img src="https://img.u7u7.top/blognoteImg/agent-chat.jpg" alt="Agent 对话形态与闭环执行页面"></p><p><img src="https://img.u7u7.top/blognoteImg/jira-api-test.png" alt="JIRA 巡检与接口测试页面"></p><h3 id="3-Dev-Skill-仓库：沉淀-skill-不是目的，能安装、能复用才是目的-📦">3. Dev-Skill 仓库：沉淀 skill 不是目的，能安装、能复用才是目的 📦</h3><p>上半年还有一条比较重要的线，就是 Dev-Skill 仓库。</p><p>一开始沉淀 skills 的时候，很容易陷入一种爽感：做出来一个能用，做出来两个也能用，数量涨得很快，成就感也很强。但后面我慢慢发现，<strong>skill 多不等于体系强。</strong></p><p>如果这些 skill：</p><ul><li>只有我知道怎么触发</li><li>只有我知道适合什么场景</li><li>只有我知道和别的 skill 怎么配合</li><li>别人装上以后还是不会用</li></ul><p>那它本质上还是“个人能力外挂”，不是团队资产。</p><p>所以我后面越来越在意 Dev-Skill 仓库的另一面：</p><ul><li>能不能安装</li><li>能不能统一维护</li><li>能不能被龙虾这类 Agent 直接消费</li><li>能不能形成迭代机制</li><li>能不能被团队拿去复用</li></ul><p>这也是为什么我会越来越把“skill 仓库”看成一个产品，而不只是一个文件夹。</p><p>而且这里还有个反直觉的地方：<strong>东西越多，不一定越强，很多时候反而越乱。</strong> 后面我明显感觉到，workspace 里的 skill、本地 skill 一多，Codex 会出现误触发和不触发的情况，这其实已经和 skill 最开始想强调的“渐进式披露”打架了。所以我现在很明确，后面不能再无脑堆技能了，必须补路由。</p><p><img src="https://img.u7u7.top/blognoteImg/codex-usage.png" alt="Codex 中的工作流使用形态"></p><p><img src="https://img.u7u7.top/blognoteImg/full-form.png" alt="完整工作流形态示意"></p><h3 id="4-项目记忆：我越来越觉得这是决定-AI-上限的关键-🧠">4. 项目记忆：我越来越觉得这是决定 AI 上限的关键 🧠</h3><p>还有一条我自己越来越笃定的线，就是项目记忆。</p><p>很多时候你会觉得 AI 这次回答不错、下次又不行，不稳定得很烦。再往下拆，最后会发现很多问题不是模型本身抽风，而是<strong>它每次都像重新入职。</strong></p><p>它不知道：</p><ul><li>你这个项目之前怎么做过</li><li>哪些结论已经讨论过</li><li>哪些约定其实是默认共识</li><li>哪些文件该优先看</li><li>哪些技能该先用，哪些不该碰</li></ul><p>所以“让 AI 拥有项目记忆”这件事，在我看来不是锦上添花，而是决定它能不能真正进入生产环境的关键。轻量、渐进式披露、持续记忆，这几个词我现在越来越看重，也是因为它们更接近真实可用，而不是堆一个大而全的上下文把模型喂爆。</p><p>这个方向我现在甚至会觉得，比“再换一个更强模型”还重要。因为模型强了，只是上限高一点；但如果记忆、上下文、项目理解还是一塌糊涂，那它的可用性依然会反复横跳。</p><hr><h2 id="三、这半年我最大的几个认知变化-🔄">三、这半年我最大的几个认知变化 🔄</h2><h3 id="1-AI-的价值不在单点生成，而在流程打通-🔗">1. AI 的价值不在单点生成，而在流程打通 🔗</h3><p>这是我现在最明确的一个判断。</p><p>单点编码提效当然有，但它的上限其实没那么高。真正有复利的，是把分析、定位、方案、改码、审查、验证这些环节串成一条链。</p><p>一旦链路能跑通，AI 带来的就不只是“快一点”，而是<strong>研发方式本身开始变化</strong>。</p><h3 id="2-做-AI-产品，核心不是功能多，而是“能被用起来”-🎯">2. 做 AI 产品，核心不是功能多，而是“能被用起来” 🎯</h3><p>这点在做推广和沉淀的时候体会特别深。</p><p>做一个 skill 不难，做十个也不算最难。真正难的是：</p><ul><li>别人知不知道该用哪个</li><li>别人第一次用会不会卡住</li><li>安装门槛高不高</li><li>成本是不是够低</li><li>结果稳不稳</li><li>团队愿不愿意持续用</li></ul><p>所以后面我越来越觉得，<strong>AI 产物要想推广，本质上得像产品，而不是像一堆功能说明书。</strong></p><h3 id="3-Skill-不能无脑膨胀，必须有路由-🚦">3. Skill 不能无脑膨胀，必须有路由 🚦</h3><p>这是我最近感受很强的一点。</p><p>随着 workspace skill、个人本地 skill 越来越多，开始出现一个明显问题：<strong>skill 误触发和漏触发。</strong></p><p>这和 skill 一开始主打的“渐进式披露”其实是相违背的。因为技能一多、命名一杂、边界一重叠，模型就容易：</p><ul><li>调错 skill</li><li>该调的不调</li><li>多个类似 skill 之间犹豫</li><li>触发路径不稳定</li></ul><p>所以我现在的判断是：<strong>不能靠无限堆技能解决问题，必须有一个类似路由器的东西，去解决技能选择和触发的问题。</strong></p><p>不然 skill 体系越大，使用体验可能反而越差。</p><h3 id="4-做-Agent，自己也得不断补课-📚">4. 做 Agent，自己也得不断补课 📚</h3><p>“活到老学到老”这句话，我现在还挺有感触的。</p><p>龙虾出来以后，我越来越觉得自己不能只停留在代码和业务上。因为如果要真正做好 Agent 开发，很多知识都得补：</p><ul><li>RAG 怎么搭更合理</li><li>memory 怎么分层</li><li>Agent 工程怎么评估</li><li>多 Agent 协作怎么设计</li><li>什么样的上下文组织方式更稳定</li><li>什么样的技能设计更符合模型工作方式</li></ul><p>这半年我不只是“做项目”，其实也在重新学习一套新的工程方法。</p><h3 id="认知变化速览表-✨">认知变化速览表 ✨</h3><table><thead><tr><th>过去更关注</th><th>现在更关注</th><th>背后的变化</th></tr></thead><tbody><tr><td>AI 会不会写代码</td><td>AI 能不能跑通流程</td><td>从点状提效走向链路提效</td></tr><tr><td>我自己会不会用</td><td>团队能不能低成本接入</td><td>从个人技巧走向产品能力</td></tr><tr><td>skill 数量多不多</td><td>skill 路由稳不稳</td><td>从堆能力走向做调度</td></tr><tr><td>模型够不够强</td><td>项目理解够不够深</td><td>从模型中心走向上下文中心</td></tr></tbody></table><hr><h2 id="四、哪些东西值得继续沉淀？-🌱">四、哪些东西值得继续沉淀？ 🌱</h2><p>如果从“长期有复利”的角度看，我觉得下面这些最值得继续投。</p><h3 id="1-高频工作流-🔁">1. 高频工作流 🔁</h3><p>像日常需求开发、JIRA 修复这种高频且流程相对稳定的场景，最值得持续打磨。因为它们最容易形成团队级复用。</p><h3 id="2-技能仓库-🧰">2. 技能仓库 🧰</h3><p>技能仓库的价值不只是多，而是是否形成：</p><ul><li>清晰分类</li><li>稳定调用</li><li>易于安装</li><li>可持续迭代</li><li>有使用反馈闭环</li></ul><p>如果这几点能持续做好，它会是非常重要的 AI 能力资产。</p><h3 id="3-项目记忆-上下文体系-🗂️">3. 项目记忆 / 上下文体系 🗂️</h3><p>这是我觉得最关键、也最容易被低估的一块。很多 AI 效果问题，最后都不是模型问题，而是上下文问题。</p><h3 id="4-路由与调度能力-🚦">4. 路由与调度能力 🚦</h3><p>skill 体系一旦做大，没有路由层肯定会出问题。这块后面非常值得专项沉淀。</p><h3 id="5-可分享的方法论文档-📝">5. 可分享的方法论文档 📝</h3><p>以前总觉得“会做就行”，但现在越来越觉得，<strong>能讲清楚、能写出来、能让 AI 和人都能读懂，本身就是生产力。</strong></p><hr><h2 id="五、哪些地方做得还不够好？-🪫">五、哪些地方做得还不够好？ 🪫</h2><p>当然也不是所有事情都很顺。</p><h3 id="1-技能膨胀带来的复杂度上升-📈">1. 技能膨胀带来的复杂度上升 📈</h3><p>skills 越多，管理越难，触发越不稳定，用户也更难理解整体结构。</p><h3 id="2-产品形态还不够统一-🧱">2. 产品形态还不够统一 🧱</h3><p>有些东西已经像产品了，有些还停留在“高手工具”阶段。要推广，必须进一步收敛交互和入口。</p><h3 id="3-项目理解能力还不够强-🔍">3. 项目理解能力还不够强 🔍</h3><p>虽然已经开始做项目记忆、RAG 和多层 memory，但离“真正理解项目”还有距离。</p><h3 id="4-经验沉淀速度还不够快-⏳">4. 经验沉淀速度还不够快 ⏳</h3><p>很多有价值的经验，还是停留在具体对话和个别案例里，没有第一时间抽成可复用资产。</p><h3 id="当前问题清单一览-⚠️">当前问题清单一览 ⚠️</h3><table><thead><tr><th>问题</th><th>现象</th><th>后续方向</th></tr></thead><tbody><tr><td>技能复杂度上升</td><td>skill 越多越难管理，触发不稳定</td><td>做好分类、边界和路由</td></tr><tr><td>产品体验不统一</td><td>有些能力像产品，有些还是高手工具</td><td>收敛入口和交互方式</td></tr><tr><td>项目理解仍偏弱</td><td>AI 还不够懂项目上下文</td><td>继续补记忆、RAG、项目知识</td></tr><tr><td>沉淀效率不足</td><td>经验容易散落在对话和案例里</td><td>更快抽成文档和可复用资产</td></tr></tbody></table><hr><h2 id="六、我现在怎么理解这半年？-🪞">六、我现在怎么理解这半年？ 🪞</h2><p>如果回头看，我觉得这半年最重要的，不是做了多少个 skill，也不是写了多少篇文档，而是我对 AI 在研发里的位置有了更清晰的判断：</p><p><strong>AI 不该只是一个“帮我写代码”的助手，而应该逐步成为一个能理解上下文、调用能力、参与协作、跑通流程的工作伙伴。</strong></p><p>而要走到这一步，靠的不只是模型本身，还包括：</p><ul><li>工作流设计</li><li>skill 设计</li><li>路由与调度</li><li>记忆与上下文</li><li>产品化包装</li><li>团队推广方式</li></ul><p>这些东西拼在一起，才有可能真正做出一个“能被持续使用的 AI 研发产品”。</p><hr><h2 id="七、下半年想继续往哪里走？-🚀">七、下半年想继续往哪里走？ 🚀</h2><h3 id="下半年方向速览表-🗓️">下半年方向速览表 🗓️</h3><table><thead><tr><th>方向</th><th>目标</th><th>关键词</th></tr></thead><tbody><tr><td>工作流继续产品化</td><td>让更多人更低成本接入</td><td>入口统一、体验收敛</td></tr><tr><td>补强 skill 路由</td><td>降低误触发和漏触发</td><td>路由、调度、边界</td></tr><tr><td>继续做项目记忆</td><td>让 AI 拥有长期上下文</td><td>memory、项目理解</td></tr><tr><td>完善 Agent 工程方法</td><td>从 demo 走向稳定范式</td><td>方法论、工程化</td></tr><tr><td>持续输出分享内容</td><td>沉淀团队和 AI 都能复用的知识</td><td>文档、复盘、传播</td></tr></tbody></table><p>接下来我更想继续做几件事：</p><ol><li><p><strong>把工作流继续产品化</strong><br>让更多人能更低成本接入和使用。</p></li><li><p><strong>补强 skill 路由能力</strong><br>解决 skill 膨胀后的误触发、漏触发问题。</p></li><li><p><strong>继续做项目记忆</strong><br>让 AI 真正拥有长期上下文，而不是每次都从零开始。</p></li><li><p><strong>完善 Agent 工程方法</strong><br>不只做 demo，而是形成更稳定的开发范式。</p></li><li><p><strong>持续输出可分享内容</strong><br>把做过的事、踩过的坑、有效的方法讲清楚，变成团队和 AI 都能复用的知识资产。</p></li></ol><hr><h2 id="结语-🌌">结语 🌌</h2><p>上半年这段探索让我越来越确定一件事：<strong>AI 的未来，不只是更强的模型，而是更像样的工作方式。</strong></p><p>如果说以前我关注的是“AI 能不能帮我快一点”，那现在我更关心的是：</p><blockquote><p><strong>能不能把一套靠谱的 AI 工作方式沉淀下来，让它不依赖某一个人的经验，也能持续发生价值。</strong></p></blockquote><p>这件事可能比“写出一段漂亮代码”慢得多，也难得多，但我觉得更值得做。</p>]]></content>
    
    
    <summary type="html">复盘上半年在 AI 小组的探索：从单点编码提效，到工作流标准化、技能仓库沉淀、项目记忆与 Agent 协作，聊聊做了什么、踩了什么坑、哪些东西真正值得沉淀。</summary>
    
    
    
    <category term="AI" scheme="https://u7u7.top/categories/AI/"/>
    
    
    <category term="AI" scheme="https://u7u7.top/tags/AI/"/>
    
    <category term="Agent" scheme="https://u7u7.top/tags/Agent/"/>
    
    <category term="Workflow" scheme="https://u7u7.top/tags/Workflow/"/>
    
    <category term="JIRA" scheme="https://u7u7.top/tags/JIRA/"/>
    
    <category term="DevSkill" scheme="https://u7u7.top/tags/DevSkill/"/>
    
    <category term="Vibe Working" scheme="https://u7u7.top/tags/Vibe-Working/"/>
    
    <category term="项目记忆" scheme="https://u7u7.top/tags/%E9%A1%B9%E7%9B%AE%E8%AE%B0%E5%BF%86/"/>
    
  </entry>
  
  <entry>
    <title>2026年06月工作日报</title>
    <link href="https://u7u7.top/posts/work-daily-2026-06.html"/>
    <id>https://u7u7.top/posts/work-daily-2026-06.html</id>
    <published>2026-05-31T16:00:00.000Z</published>
    <updated>2026-06-30T04:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<h1 id="2026年06月工作日报-📌">2026年06月工作日报 📌</h1><p>这里按天记录工作内容、遇到的问题和当天的一点想法，方便月底回看。</p><!-- daily-report:2026-06-23:start --><h2 id="2026-06-23-今天做了啥？-✨">2026/06/23 今天做了啥？ ✨</h2><p>今天主要是在把 6 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容">✅ 工作内容</h3><ul><li>完成飞书文档读取失败问题复现，确认 docx 直链解析和读取脚本本身是通的，实际已经能读到目标文档内容，当前更像是使用侧入口、权限或上下文差异导致的表现不一致。</li><li>梳理技能同步到 Codex 的处理方向，先把当前技能包状态和本地同步入口对齐清楚，避免把 one-click-bundles 来源和 Codex 技能目录的同步方向弄反。</li><li>推进 POS workspace 第六阶段接收消息问题定位，围绕 <code>allocateUserType</code>、<code>alt</code>、<code>KDT_ID/USER_NO</code> 这组关键字把范围收敛到支付资产和商户号分配链路，为后续修正 alt 处理先把代码入口找实。</li><li>调整晚间日报自动化的产出方式，把原来只推飞书的链路扩成“飞书日报 + Hexo 月度工作记录”，后续每天会直接追加到当月文章并执行部署，减少手工整理。</li><li>对齐了今天的日程安排，下午的“对下 workspace 进展”和晚间的“美业学习第五场：美业门店客户运营实战拆解”是今天需要关注的主要日历事项。</li><li>检查了今天收件箱未读情况，当前未发现明显需要处理的未读邮件；但邮箱身份信息仍然返回为空，所以这块暂时还是按弱提醒处理，不直接下强结论。</li></ul><h3 id="🧩-困难-需要帮助">🧩 困难/需要帮助</h3><ul><li>POS workspace 的 <code>alt</code> 处理还需要继续结合具体代码改动和回归场景确认，重点是不要影响现有商户号分配链路。</li><li>飞书文档读取虽然已经能复现成功，但如果使用侧还在失败，需要继续补入口、权限和实际调用参数这几块证据。</li><li>邮箱 profile 返回主邮箱为空的问题还没彻底解决，重要未读邮件判断暂时不能当强结论。</li></ul><h3 id="💭-感想">💭 感想</h3><p>今天主要还是在把几个问题往真实链路和明确结论上收。飞书文档读取这类问题，先分清脚本、权限和入口差异很重要；POS 的 alt 修正也是一样，先把代码链路找准，后面改起来才不容易把口径带偏。日报这边把飞书和博客两条产出链路理顺以后，后续记录也会稳一些。</p><!-- daily-report:2026-06-23:end --><!-- daily-report:2026-06-24:start --><h2 id="2026-06-24-今天做了啥？-✨">2026/06/24 今天做了啥？ ✨</h2><p>今天主要是在把 5 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容-2">✅ 工作内容</h3><ul><li>完成 2026-06-24 晚间日报 evidence 采集，确认统一 evidence 文件已生成；但今天只采到晚间日报线程和一次会话，缺少可复核的业务工作记录。</li><li>梳理历史日报和 6 月月度工作记录格式，确认飞书正文继续保持“工作内容 / 困难/需要帮助 / 感想”三段，博客小节继续按当天记录更新。</li><li>确认今天日报的写作边界：没有可靠的业务 threadName、userInputs 或 toolCalls 证据时，不强行补写项目进展，只记录能确认的事项和证据缺口。</li><li>准备当天日报文件，先把 evidence 摘要、历史口吻和发布流程对齐清楚，避免后续飞书和博客两边内容不一致。</li><li>对齐飞书推送和 Hexo 月报追加链路，当天小节按日期更新，避免重复追加；后续仍以统一 evidence 作为日报入口。</li></ul><h3 id="🧩-困难-需要帮助-2">🧩 困难/需要帮助</h3><ul><li>今日 evidence 缺少业务会话、日历安排和邮件摘要证据，无法可靠补充具体项目进展或需要关注的事项。</li><li>后续需要确认工作记录来源是否覆盖“前一天下午六点到今天下午六点”的主要工作，否则日报容易只留下自动化链路本身。</li></ul><h3 id="💭-感想-2">💭 感想</h3><p>今天主要是在把日报链路和证据边界先收稳。业务证据不足时不硬写进展，比写得完整但不可复核更重要；后面要优先补齐工作来源，日报才更有复盘价值。</p><!-- daily-report:2026-06-24:end --><!-- daily-report:2026-06-28:start --><h2 id="2026-06-28-今天做了啥？-✨">2026/06/28 今天做了啥？ ✨</h2><p>今天主要是在把 5 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容-3">✅ 工作内容</h3><ul><li>完成 2026-06-28 晚间日报 evidence 采集，确认统一证据文件已生成；但今天只采到“每日晚间简报与日报推送”线程和一次会话，缺少可复核的业务工作记录。</li><li>确认今日日报写作边界：historyItems 为 0，userInputs 为空，toolCalls 只有基础命令调用，因此不补写项目、JIRA、代码链路或业务口径结论，只记录可确认事项。</li><li>梳理 6 月历史日报和博客小节的稳定格式，确认飞书继续保留“工作内容 / 困难/需要帮助 / 感想”三段，博客继续按当天日期更新当月文章。</li><li>对齐当天日报发布链路，日报文件、飞书推送配置和 Hexo 月度工作记录仍统一使用同一份日报内容作为来源，避免两边口径不一致。</li><li>保守处理日历安排、重要未读邮件和今日注意事项补充；由于 evidence 没有提供可靠来源，今天不把这些信息写成明确提醒。</li></ul><h3 id="🧩-困难-需要帮助-3">🧩 困难/需要帮助</h3><ul><li>今日 evidence 缺少业务会话、日历安排和邮件摘要证据，无法可靠补充具体项目进展或需要关注的事项。</li><li>后续需要确认工作记录来源是否覆盖“前一天下午六点到今天下午六点”的主要工作，否则日报容易只留下发布链路和证据缺口。</li></ul><h3 id="💭-感想-3">💭 感想</h3><p>今天主要是在把日报的证据边界讲清楚。没有业务证据时不硬写具体进展，比写得完整但不可复核更重要；后面要优先补齐真实工作来源，日报才更有复盘价值。</p><!-- daily-report:2026-06-28:end --><!-- daily-report:2026-06-29:start --><h2 id="2026-06-29-今天做了啥？-✨">2026/06/29 今天做了啥？ ✨</h2><p>今天主要是在把 5 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容-4">✅ 工作内容</h3><ul><li>完成订单 E20260629105240032702006 流程核验，确认这笔单命中 QA 环境而非生产；当前链路收敛到 Android 离线/线下场景后的重开单、关闭旧单治理流程，不是普通完成支付单。</li><li>确认组合支付参数下传链路，OrderPayService#createPay 入参保留 COMBINE_PAY，以及 CASH_PAY、THIRD_PAY 两段支付明细，总金额和分段金额可以对齐，入口侧参数没有丢。</li><li>推进拉卡拉 POS 参数透传排查，确认入口 extra 中有 POS_INST=lakala、POS_FLAG=smart_pos、deviceName=LANDI A8S，后续支付通道侧也实际走到了拉卡拉。</li><li>完成订单 E20260629144044008302022 资产侧参数核验，确认 pay-trading-core 的统一收单支付请求 attachment 已带 POS_INST=lakala、POS_FLAG=smart_pos，和单纯的 lakala_code/lakala_msg 渠道返回字段区分开了。</li><li>重新生成编程语言趋势视频，确认中文界面、2560x1440、60fps 和约 30s 时长都符合目标参数，成片已输出到本地文件。</li></ul><h3 id="🧩-困难-需要帮助-4">🧩 困难/需要帮助</h3><ul><li>E20260629105240032702006 如果后续要作为支付成功样例使用，需要先确认离线订单、重开单和关闭旧单治理是否符合业务预期，避免按普通支付单口径判断。</li><li>POS 参数透传目前主要基于 QA 链路证据确认，后续如果要推广到生产结论，还需要继续核对发布分支、环境配置和回归范围。</li></ul><h3 id="💭-感想-4">💭 感想</h3><p>今天主要是在把 POS 支付相关问题往真实 trace 和上下游参数口径上收敛。几个结论都尽量没有停在入口参数，而是继续追到资产侧和通道侧确认，这样后面讨论时能少一些“应该有”的猜测。</p><!-- daily-report:2026-06-29:end --><!-- daily-report:2026-06-30:start --><h2 id="2026-06-30-今天做了啥？-✨">2026/06/30 今天做了啥？ ✨</h2><p>今天主要是在把 6 个重点事项往真实链路、明确口径和可落地结论上收，既有问题排查，也有流程和记录方式上的整理。</p><h3 id="✅-工作内容-5">✅ 工作内容</h3><ul><li>完成拉卡拉 POS 银行卡一级支付方式口径梳理，确认这次核心是新增/识别 <code>POS_BANK_CARD</code> 一级支付表达，不是建行那类二级支付方式配置，后续排查要围绕主支付链路和下游展示统计展开。</li><li>梳理代码侧锚点，确认 <code>PayWayEnum</code> 已有 <code>4132-4136</code> 的 POS 银行卡支付枚举，<code>PayToolUnifiedCashierEnum</code> 已有 <code>POS_BANK_CARD</code>，<code>mall-trade-seller</code> 也存在按 POS 银行卡做订单筛选的逻辑。</li><li>推进订单 <code>E20260630103921048102020</code> 的拉卡拉透传核验，确认 QA 环境在 2026-06-30 10:39 左右有日志，主 trace 为 <code>yz7-0a096140-1782787156045-602579</code>；当前样例初看是“现金 + 微信条码”的混合支付，尚未直接看到 <code>POS_BANK_CARD/POS_INST/POS_FLAG</code>。</li><li>梳理 POS 上下文到资产请求的字段链路，确认重点要看 <code>phasePayModels[].extension</code>、<code>PayRequest.bizExt</code>、<code>MultiPayRequest.outBizContext</code>，其中 <code>retail-trade-core</code> 负责带入 POS 上下文，<code>pay-trading-core</code> 会合并到资产侧 <code>PaymentParam</code>。</li><li>推进拉卡拉支付方式页面影响面核查，确认测试点集中在展示、筛选、退款、小票、交班日结、报表和业绩分佣，不只是收银台发起支付本身。</li><li>梳理下游仓库当前差异，<code>retail-sync</code> 已出现 <code>POS_BANK_CARD</code> 支付族映射，但 <code>retail-trademanager</code> 暂未直接命中 <code>POS_BANK_CARD/4132-4136</code> 的业务逻辑，后续要区分“后端返回文案已支持”和“页面筛选项还需要补枚举”。</li></ul><h3 id="🧩-困难-需要帮助-5">🧩 困难/需要帮助</h3><ul><li><code>E20260630103921048102020</code> 当前不像拉卡拉 POS 银行卡样例，后续如果要验证透传，需要换一个真正命中 <code>POS_BANK_CARD/POS_INST/POS_FLAG</code> 的订单继续追。</li><li>本地代码里 <code>lakala/POS_INST</code> 的直接命中还不多，可能和子仓分支、发布计划或环境代码不同步有关，需要继续确认发布分支和回归范围。</li><li>页面影响面还需要继续补齐 <code>retail-trademanager</code>、<code>retail-finance-sales</code> 等仓的筛选枚举、统计口径和展示文案证据，避免只按后端主链路下结论。</li></ul><h3 id="💭-感想-5">💭 感想</h3><p>今天主要是在把拉卡拉 POS 银行卡这条线从概念往真实代码、日志和页面影响面上收敛。比较关键的判断是先把一级支付方式和二级支付配置分开，再去看字段有没有真正进入资产和下游页面；方向已经比较清楚，但样例订单和页面枚举还需要继续拿证据确认。</p><!-- daily-report:2026-06-30:end -->]]></content>
    
    
    <summary type="html">记录 2026 年 06 月每天做了什么、困难和感想</summary>
    
    
    
    <category term="工作月总结" scheme="https://u7u7.top/categories/%E5%B7%A5%E4%BD%9C%E6%9C%88%E6%80%BB%E7%BB%93/"/>
    
    
    <category term="工作记录" scheme="https://u7u7.top/tags/%E5%B7%A5%E4%BD%9C%E8%AE%B0%E5%BD%95/"/>
    
    <category term="日报" scheme="https://u7u7.top/tags/%E6%97%A5%E6%8A%A5/"/>
    
  </entry>
  
</feed>
