Repository navigation
Expand file tree
/
Copy pathatom.xml
More file actions
577 lines (301 loc) · 471 KB
/
Copy pathatom.xml
File metadata and controls
577 lines (301 loc) · 471 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>U7'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'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 业务代码] --> B[平台线程池\n一请求一线程] A --> C[虚拟线程\n由 JDK 提供] B --> E[OS 线程\n内核调度] C --> F[载体线程 ForkJoinPool\nJVM 调度] F --> E K[Kotlin 业务代码] --> D[协程\n由库提供] D --> G[编译器生成的状态机\n续体对象] G --> H[Dispatchers 线程池\n库负责调度] H --> E E --> 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 [*] --> 新建 新建 --> 运行中 : 被调度器选中并挂载到载体 运行中 --> 运行中 : 纯计算 一直占着载体线程 运行中 --> 已卸载 : 遇到可卸载阻塞 例如 Socket 读 已卸载 --> 运行中 : 事件就绪 重新挂载到某个载体 运行中 --> 已钉住 : synchronized 内阻塞 或 native 回调阻塞 已钉住 --> 运行中 : 阻塞结束 载体才被释放 运行中 --> [*] : 任务结束 </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 & 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> {</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><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()) {</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 < <span class="number">10_000</span>; i++) {</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(() -> {</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">"done "</span> + orderId);</span><br><span class="line"> <span class="keyword">return</span> orderId;</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"> } <span class="comment">// close 会等待所有任务结束</span></span><br><span class="line"> }</span><br><span class="line">}</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 {</span><br><span class="line"> delay(<span class="number">50</span>) <span class="comment">// 挂起点:不占线程</span></span><br><span class="line"> println(<span class="string">"done "</span> + id)</span><br><span class="line"> id</span><br><span class="line">}</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 {</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 { id -> async(Dispatchers.IO) { loadOrder(id) } }</span><br><span class="line"> .awaitAll()</span><br><span class="line"> println(results.size)</span><br><span class="line">}</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>{{title}}</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">"<span class="template-variable">{{title}}</span>"</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">"<span class="template-variable">{{date}}</span> <span class="template-variable">{{time}}</span>"</span></span><br></pre></td></tr></table></figure><p>这不是文章,这是 hexo new page 用的<strong>页面模板</strong>。它的 title 就是占位符本身,date 也是占位符,所以渲染出来的文章标题永远是 <code>{{title}}</code>、发布时间永远是字符串 <code>{{date}} {{time}}</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">'0'</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/_posts] --> B[readDirWalker 遍历目录] B --> C{ignore 是否命中目录本身} C -->|是| D[目录层剪枝 整棵子树跳过] C -->|否| E{isHiddenFile 判定路径段首字符} E -->|是 下划线或点| F[当作隐藏文件跳过] E -->|否| G[注册为文章并渲染] G --> H[写入 db.json 与 public] H --> I[静态托管部署] I --> J[自定义域名经 CDN 缓存] J --> 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>) {</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">}</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>) {</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) && !<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">}</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">'_posts/copilot/**'</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">'_posts/scaffolds/**'</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>) {</span><br><span class="line"> <span class="keyword">return</span> path && ignore && ignore.<span class="property">length</span> && <span class="title function_">isMatch</span>(path, ignore);</span><br><span class="line">}</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>) {</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> =></span> {</span><br><span class="line"> <span class="keyword">if</span> (err && err.<span class="property">code</span> === <span class="string">'ENOENT'</span>) <span class="keyword">return</span> [];</span><br><span class="line"> <span class="keyword">throw</span> err;</span><br><span class="line"> }), <span class="keyword">async</span> path => {</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">${prefix}</span><span class="subst">${path}</span>`</span>;</span><br><span class="line"> <span class="keyword">if</span> (stats.<span class="title function_">isDirectory</span>()) {</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">${prefixdPath}</span>/`</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (!<span class="title function_">isIgnoreMatch</span>(fullpath, ignore)) { <span class="comment">// :261 传的是 fullpath</span></span><br><span class="line"> results.<span class="title function_">push</span>(prefixdPath);</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line">}</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">'**/_posts/copilot'</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">'**/_posts/scaffolds'</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> { isMatch } = <span class="built_in">require</span>(<span class="string">'micromatch'</span>);</span><br><span class="line"><span class="keyword">const</span> dir = <span class="string">'/Users/youqi/u7_blog/source/_posts/scaffolds'</span>;</span><br><span class="line"><span class="keyword">const</span> file = dir + <span class="string">'/page.md'</span>;</span><br><span class="line"><span class="keyword">const</span> deep = <span class="string">'/Users/youqi/u7_blog/source/_posts/copilot/copilot-conversations/x.md'</span>;</span><br><span class="line"></span><br><span class="line"><span class="title function_">isMatch</span>(dir, [<span class="string">'**/_posts/scaffolds'</span>]); <span class="comment">// true</span></span><br><span class="line"><span class="title function_">isMatch</span>(file, [<span class="string">'**/_posts/scaffolds'</span>]); <span class="comment">// false</span></span><br><span class="line"><span class="title function_">isMatch</span>(dir, [<span class="string">'**/_posts/scaffolds/*'</span>]); <span class="comment">// false</span></span><br><span class="line"><span class="title function_">isMatch</span>(file, [<span class="string">'**/_posts/scaffolds/*'</span>]); <span class="comment">// true</span></span><br><span class="line"><span class="title function_">isMatch</span>(deep, [<span class="string">'**/_posts/copilot/*'</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 模式写在哪个层级] --> B[匹配到目录本身] A --> C[只匹配到子文件] B --> D[readDirWalker 第一行直接返回] D --> E[整棵子树不再递归 深层文件全挡住] C --> F[目录仍被进入 递归继续向下] F --> G[必须为每一层深度各补一条模式] G --> H[漏一层就漏一篇 且不报错] E --> 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 && 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>{{title}}</code>、<code>{{date}}</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->>R: 提交两条 ignore 与清理后的 public R->>H: 触发构建并部署 H->>C: 回源取到新页面 C-->>B: 仍返回旧缓存 92KB B->>C: 刷新后再次请求 C-->>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 层 指令与示例] --> C[Context 层 上下文与记忆装配] C --> T[Tool 层 工具契约与参数校验] T --> H[Harness 层 状态机 重试 沙箱 观测] H --> R[Runtime 层 进程 配额 隔离] R --> O[产物与轨迹日志] O --> 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->>H: 提交任务与幂等键 H->>L: 装配上下文并请求下一步 L-->>H: 返回工具调用与参数 H->>H: 校验 schema 与权限白名单 H->>T: 携带幂等键执行 T-->>H: 返回超时 Note right of H: 只重试可重试错误 其余直接失败 H->>O: 记录一次尝试与错误分类 H->>T: 退避后重试 复用同一幂等键 T-->>H: 返回成功结果 H->>O: 落盘完整轨迹供回放 H-->>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> {</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> { OK, RETRYABLE, FATAL, EXHAUSTED }</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> {}</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> {}</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> {</span><br><span class="line"> <span class="type">public</span> <span class="title function_">ToolException</span><span class="params">(String msg)</span> { <span class="built_in">super</span>(msg); }</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_">RetryableToolException</span> <span class="keyword">extends</span> <span class="title class_">ToolException</span> {</span><br><span class="line"> <span class="type">public</span> <span class="title function_">RetryableToolException</span><span class="params">(String msg)</span> { <span class="built_in">super</span>(msg); }</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_">FatalToolException</span> <span class="keyword">extends</span> <span class="title class_">ToolException</span> {</span><br><span class="line"> <span class="type">public</span> <span class="title function_">FatalToolException</span><span class="params">(String msg)</span> { <span class="built_in">super</span>(msg); }</span><br><span class="line"> }</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> {</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"> }</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> {</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"> }</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> {</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"> }</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> {</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 <= MAX_ATTEMPTS; attempt++) {</span><br><span class="line"> <span class="keyword">try</span> {</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"> } <span class="keyword">catch</span> (FatalToolException e) {</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"> } <span class="keyword">catch</span> (RetryableToolException e) {</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"> }</span><br><span class="line"> }</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">"tool exhausted after "</span> + MAX_ATTEMPTS + <span class="string">" attempts: "</span> + call.name());</span><br><span class="line"> }</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> {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> Thread.sleep(<span class="number">200L</span> * (<span class="number">1L</span> << (attempt - <span class="number">1</span>)));</span><br><span class="line"> } <span class="keyword">catch</span> (InterruptedException e) {</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">"interrupted during backoff"</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</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 [*] --> 纯缓存 纯缓存 --> 外挂模块 : 需要全文与向量检索 外挂模块 --> 内核一体 : Redis 8 合并模块 内核一体 --> 混合检索 : 8.4 提供 FT.HYBRID 混合检索 --> 检索引擎分家 : 内存成本或一致性越界 检索引擎分家 --> 纯缓存 : 检索迁出 只留缓存 </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">'*=>[KNN 10 @emb $vec AS score]'</span> \</span><br><span class="line"> PARAMS 2 vec <span class="string">"<span class="variable">$VEC</span>"</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">'(@tag:{cache|redis})=>[KNN 10 @emb $vec AS score]'</span> \</span><br><span class="line"> PARAMS 2 vec <span class="string">"<span class="variable">$VEC</span>"</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">"*"</span> LOAD 2 <span class="string">"@price"</span> <span class="string">"@stock"</span> \</span><br><span class="line"> APPLY <span class="string">"@price*@stock"</span> AS amount \</span><br><span class="line"> GROUPBY 1 <span class="string">"@tag"</span> REDUCE SUM 1 <span class="string">"@amount"</span> AS total \</span><br><span class="line"> SORTBY 2 <span class="string">"@total"</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:{...})=>[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[业务写入] --> B[文档键 与 向量字段] B --> C[索引增量更新] C --> D[全文倒排] C --> E[向量 HNSW 图] end subgraph RP[读路径] F[查询请求] --> G[关键词召回] F --> H[向量 KNN 召回] G --> I[融合打分 RRF 或 LINEAR] H --> I I --> J[前置过滤 排序 分页] J --> K[结果回写热点缓存] end D -.-> G E -.-> H K -.-> B M[内存预算] -.-> D M -.-> 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">'io_method'</span>, <span class="string">'io_min_workers'</span>, <span class="string">'io_max_workers'</span>,</span><br><span class="line"> <span class="string">'io_worker_idle_timeout'</span>, <span class="string">'io_worker_launch_interval'</span>,</span><br><span class="line"> <span class="string">'effective_io_concurrency'</span>, <span class="string">'maintenance_io_concurrency'</span>,</span><br><span class="line"> <span class="string">'io_combine_limit'</span>, <span class="string">'io_max_concurrency'</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">>=</span> <span class="string">'2026-01-01'</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[查询发起 顺序扫描] --> B[本次需要读 1000 个数据页] B --> C[同步模型 一次只发一个] C --> C1[后端起一个 pread 然后睡眠] C1 --> C2[队列深度 1 设备排队能力被浪费] C2 --> E[NVMe 带宽利用率低 延迟无法叠加] B --> D[异步模型 一次提交 64 个请求] D --> D1[共享内存 IO 队列] D1 --> D2[worker 进程池 或 io_uring] D2 --> D3[完成队列 批量唤醒后端] D3 --> 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->>B: 发送 SELECT 全表扫描 B->>S: 提交首批预取请求 多个数据页 S->>W: 按队列分发 W->>D: 并发下发读请求 D-->>W: 第一批完成 W-->>S: 写入完成队列 S-->>B: 唤醒后端 交付数据页 B->>B: 过滤行 组装元组 B->>S: 补齐水位 继续提交 Note over B,W: 关键点 后端等待期间设备仍有请求在飞 B-->>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[写入请求] --> IM{选择索引模式} IM -->|standard| S1[倒排索引加 doc values] IM -->|columnar| S2[仅 doc values 单一存储] IM -->|vectordb_document| S3[向量文件优先预加载] S1 --> Q[查询层] S2 --> Q S3 --> Q Q --> A1[全文检索] Q --> A2[聚合与排序] Q --> A3[kNN 近似检索] A1 --> M[结果归并与重排] A2 --> M A3 --> 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">"localhost:9200/orders-columnar"</span> -H <span class="string">'Content-Type: application/json'</span> -d<span class="string">'</span></span><br><span class="line"><span class="string">{</span></span><br><span class="line"><span class="string"> "settings": {</span></span><br><span class="line"><span class="string"> "mode": "columnar",</span></span><br><span class="line"><span class="string"> "sort.field": ["tenant.id", "@timestamp"],</span></span><br><span class="line"><span class="string"> "sort.order": ["asc", "desc"]</span></span><br><span class="line"><span class="string"> },</span></span><br><span class="line"><span class="string"> "mappings": {</span></span><br><span class="line"><span class="string"> "properties": {</span></span><br><span class="line"><span class="string"> "@timestamp": { "type": "date" },</span></span><br><span class="line"><span class="string"> "tenant.id": { "type": "keyword" },</span></span><br><span class="line"><span class="string"> "amount": { "type": "double" },</span></span><br><span class="line"><span class="string"> "status": { "type": "keyword" },</span></span><br><span class="line"><span class="string"> "message": { "type": "text" }</span></span><br><span class="line"><span class="string"> }</span></span><br><span class="line"><span class="string"> }</span></span><br><span class="line"><span class="string">}'</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->>CO: kNN 请求 携带 topk 与过滤条件 CO->>SH: 分发到各分片 携带段级参数 SH->>DS: 用 skipper 元信息跳过不匹配文档区间 DS-->>SH: 返回候选文档区间 SH->>DS: 读取 bfloat16 向量做近似距离计算 DS-->>SH: 返回距离结果 SH->>SH: 段级 oversample 后重打分 SH-->>CO: 返回分片内 topk CO->>CO: 归并排序并截断 CO-->>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[业务数据] --> B[单一集群] B --> C[文本字段 走倒排索引] B --> D[向量字段 dense_vector] C --> E[同一请求内混合过滤加 kNN] D --> E E --> F[一次结果返回] end subgraph T2[方案二 ES 加专用向量库] G[业务数据] --> H[Elasticsearch] G --> I[向量库 Milvus 或 Qdrant] I --> J[返回向量 ID 与距离] J --> K[回查 ES 拿正文] H --> K K --> 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] --> G[网关 鉴权 限流 路由] G --> S1[MCP Server 副本 A] G --> S2[MCP Server 副本 B] G --> S3[MCP Server 副本 C] S1 --> D[外部状态存储 Redis 或 DB] S2 --> D S3 --> D S1 --> T[链路追踪 采集] S2 --> T S3 --> T G --> 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->>S: initialize 握手 S-->>C: 下发 Mcp-Session-Id 与能力 C->>S: tools/call 携带会话头 S-->>C: 结果依赖实例内存中的会话 Note over C,S: 新模型 无状态核心 C->>S: POST tools/call 带 _meta 版本与能力 S-->>C: result 带 serverInfo 与 ttlMs C->>S: 再次调用 显式携带 basket_id S-->>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">{</span></span></span><br><span class="line"><span class="language-json"> <span class="attr">"jsonrpc"</span><span class="punctuation">:</span> <span class="string">"2.0"</span><span class="punctuation">,</span></span></span><br><span class="line"><span class="language-json"> <span class="attr">"id"</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">"method"</span><span class="punctuation">:</span> <span class="string">"tools/call"</span><span class="punctuation">,</span></span></span><br><span class="line"><span class="language-json"> <span class="attr">"params"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span></span><br><span class="line"><span class="language-json"> <span class="attr">"name"</span><span class="punctuation">:</span> <span class="string">"get_weather"</span><span class="punctuation">,</span></span></span><br><span class="line"><span class="language-json"> <span class="attr">"arguments"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span></span><br><span class="line"><span class="language-json"> <span class="attr">"location"</span><span class="punctuation">:</span> <span class="string">"Seattle, WA"</span></span></span><br><span class="line"><span class="language-json"> <span class="punctuation">}</span><span class="punctuation">,</span></span></span><br><span class="line"><span class="language-json"> <span class="attr">"_meta"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span></span><br><span class="line"><span class="language-json"> <span class="attr">"io.modelcontextprotocol/protocolVersion"</span><span class="punctuation">:</span> <span class="string">"2026-07-28"</span><span class="punctuation">,</span></span></span><br><span class="line"><span class="language-json"> <span class="attr">"io.modelcontextprotocol/clientInfo"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span></span><br><span class="line"><span class="language-json"> <span class="attr">"name"</span><span class="punctuation">:</span> <span class="string">"ExampleClient"</span><span class="punctuation">,</span></span></span><br><span class="line"><span class="language-json"> <span class="attr">"version"</span><span class="punctuation">:</span> <span class="string">"1.0.0"</span></span></span><br><span class="line"><span class="language-json"> <span class="punctuation">}</span><span class="punctuation">,</span></span></span><br><span class="line"><span class="language-json"> <span class="attr">"io.modelcontextprotocol/clientCapabilities"</span><span class="punctuation">:</span> <span class="punctuation">{</span><span class="punctuation">}</span></span></span><br><span class="line"><span class="language-json"> <span class="punctuation">}</span></span></span><br><span class="line"><span class="language-json"> <span class="punctuation">}</span></span></span><br><span class="line"><span class="language-json"><span class="punctuation">}</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<@Nullable String></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<@Nullable String></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[源码 注解声明] --> IDE[IDE 实时提示] SRC --> JC[javac 编译] JC --> EP[Error Prone 插件 NullAway] EP -->|契约不满足| FAIL[构建失败] EP -->|契约满足| CLS[class 文件 类型注解] IDE --> CLS JC --> KOT[Kotlin 调用方 编译期报错] CLS --> RT[运行期 Nullness 查询] RT --> 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> {</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> {</span><br><span class="line"> <span class="built_in">this</span>.repository = repository;</span><br><span class="line"> }</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> {</span><br><span class="line"> <span class="keyword">return</span> repository.findById(id);</span><br><span class="line"> }</span><br><span class="line">}</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> {</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> {</span><br><span class="line"> <span class="built_in">this</span>.repository = repository;</span><br><span class="line"> }</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> {</span><br><span class="line"> <span class="keyword">return</span> repository.findById(id);</span><br><span class="line"> }</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> {</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">"unknown"</span> : order.title();</span><br><span class="line"> }</span><br><span class="line">}</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("null, _ → fail")</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("NullAway") // 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->>Build: 提交代码 或执行 mvn compile Build->>Build: 在 NullMarked 作用域内做数据流检查 alt 契约不满足 Build-->>Dev: 构建失败 报出空性错误 else 契约满足 Build->>Class: 写入类型注解 与 参数注解 Class->>Frame: 启动或解析时反射读取 Frame-->>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] --> P[协议层] R --> T[运行时默认值] R --> L[语言与 API] P --> P1[JEP 527 TLS 1.3 后量子混合密钥交换] P --> P2[JEP 538 加密对象 PEM 编码 第三预览] T --> T1[JEP 523 G1 全环境默认] T --> T2[JEP 534 紧凑对象头默认] T --> T3[JEP 536 JFR 数据脱敏] L --> L1[JEP 533 结构化并发 第七预览] L --> L2[JEP 531 惰性常量 第三预览] L --> L3[JEP 532 基本类型模式 第五预览] L --> 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> {</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 {</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">"example.com"</span>, <span class="number">443</span>)) {</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">"default groups = "</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>[] {</span><br><span class="line"> <span class="string">"X25519MLKEM768"</span>, <span class="string">"x25519"</span></span><br><span class="line"> });</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">"protocol = "</span> + socket.getSession().getProtocol());</span><br><span class="line"> System.out.println(<span class="string">"cipher = "</span> + socket.getSession().getCipherSuite());</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</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->>S: ClientHello 携带 X25519MLKEM768 与 x25519 两个 key share S->>C: ServerHello 选中 X25519MLKEM768 Note over C,S: 会话密钥同时依赖 ML-KEM-768 与 X25519 C->>M: 同样握手 但中间设备只认识 x25519 M->>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 & 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 [*] --> 新建 新建 --> 运行中 : 启动 运行中 --> 阻塞 : 发起阻塞式调用 阻塞 --> 已卸载 : 让出载体线程 已卸载 --> 运行中 : 数据就绪 重新挂载 运行中 --> 钉住 : 进入 native 或 FFM 回调 钉住 --> 运行中 : native 返回 运行中 --> [*] : 任务结束 </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["Skill<br/>能力单元"]:::concept --> B["Superpowers<br/>工作方法"]:::method B --> C["Agent Team<br/>协作编排"]:::method C --> D["dev-skill<br/>仓库与治理"]:::infra B --> 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["用户请求"] --> B["任务识别 / 路由"] B --> C["加载相关 Rules / Skill"] C --> D["读取 references / 执行 scripts"] D --> E["产出结果并校验"] E --> F["沉淀经验 / 更新知识"] </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["目录层<br/>name + description"] --> B["方法层<br/>SKILL.md"] B --> C["资料层<br/>references/"] B --> D["执行层<br/>scripts/ tools"] </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["Rules<br/>长期背景"] --> E["Superpowers<br/>组织与调度"] B["Skills<br/>任务能力"] --> E C["MCP / Tools<br/>查询与执行"] --> E D["Memory / Repo<br/>知识与沉淀"] --> E E --> F["单 Agent 稳定产出"] F --> G["Agent Team 多角色协作"] </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["一次成功任务"] --> B["提炼稳定做法"] B --> C["写入 Skill / Script / Doc"] C --> D["后续任务继续复用"] D --> E["再次优化与治理"] </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 -> 排查 -> 改代码 -> CR”的闭环,大致可以是这样:</p><div class="mermaid-wrap"><pre class="mermaid-src" hidden> flowchart LR A["用户提需求 / 给出 JIRA"] --> B["路由到对应 workflow"] B --> C["读取相关 skill 说明"] C --> D["调用日志 / trace / jira / repo 工具"] D --> E["产出修改建议或代码改动"] E --> F["触发 review / test / 文档沉淀"] </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["Development:把需求变成可执行开发任务"] BRAIN["brainstorming<br/>澄清目标、约束和方案选择"] SCANNER["app-scanner<br/>扫描多应用工作区,提取业务能力"] SDD["sdd-dev-workflow<br/>产出 requirements / design / plan / tasks"] PAIR["ai-pair-programmer<br/>结对实现、修复、重构和验证"] end subgraph REVIEW["Review:把改动变成可放心交付的结果"] AUTHOR["author-final-review<br/>审查单一作者最终态变更"] CODE["code-review<br/>检查质量、安全、性能和架构风险"] TEST["integration-test<br/>按风险生成集成测试用例"] FULL["full-review<br/>汇总代码审查、影响面和测试设计"] TEAM["team-cr<br/>准备多人 CR 会议讨论清单"] end subgraph VIS["Visualization:把复杂信息画清楚"] DIAGRAM["diagram-creation<br/>生成流程图、架构图、时序图、路线图"] end subgraph PLATFORM["Platform:管理工作环境和使用统计"] WORKTREE["git-worktree<br/>管理并行开发 worktree"] TRACKER["skill-usage-tracker<br/>记录技能使用统计"] end BRAIN --> SDD SCANNER --> SDD SDD --> PAIR PAIR --> AUTHOR AUTHOR --> CODE AUTHOR --> TEST CODE --> FULL TEST --> FULL FULL --> TEAM DIAGRAM -. "可插入任意阶段" .-> SDD WORKTREE -. "隔离分支环境" .-> PAIR TRACKER -. "记录每次 Skill 调用" .-> 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/<分类>/<skill 名>/</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["需求例子<br/>订单系统支持优惠券抵扣"] subgraph PHASE1["阶段 1:想清楚要做什么"] BRAIN["brainstorming<br/>澄清优惠券规则、边界和验收口径"] OUT1["产物<br/>哪些券可用、如何叠加、异常怎么处理"] end subgraph PHASE2["阶段 2:找到会被影响的代码"] SCAN["app-scanner<br/>多应用场景下扫描订单、营销、支付等应用"] SDD["sdd-dev-workflow<br/>结合真实代码证据整理方案"] OUT2["产物<br/>requirements / design / plan / tasks"] end subgraph PHASE3["阶段 3:隔离环境并开始实现"] WT["git-worktree<br/>为优惠券需求创建独立工作区"] PAIR["ai-pair-programmer<br/>修改接口、领域逻辑、测试和配置"] OUT3["产物<br/>可运行的代码改动与验证结果"] end subgraph PHASE4["阶段 4:把风险收住"] AUTHOR["author-final-review<br/>先审查本次作者最终态变更"] CODE["code-review<br/>检查兼容性、金额计算、安全和性能风险"] TEST["integration-test<br/>生成 P0 / P1 集成测试用例"] FULL["full-review<br/>汇总审查结论、影响面和测试覆盖"] end subgraph PHASE5["阶段 5:准备协作交付"] DIAGRAM["diagram-creation<br/>画调用链、状态流转或优惠券计算流程"] TEAM["team-cr<br/>整理多人 CR 会议讨论清单"] TRACK["skill-usage-tracker<br/>记录技能使用,沉淀本地统计"] end START --> BRAIN --> OUT1 --> SCAN --> SDD --> OUT2 OUT2 --> WT --> PAIR --> OUT3 OUT3 --> AUTHOR --> CODE --> TEST --> FULL FULL --> DIAGRAM --> TEAM BRAIN -. "每次调用前记录" .-> TRACK PAIR -. "每次调用前记录" .-> TRACK FULL -. "每次调用前记录" .-> 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["skills/<分类>/<skill><br/>唯一源码"] --> B["安装器"] C["config/profiles/diy.skills<br/>默认安装清单"] --> B B --> D["Cursor"] B --> E["Claude Code"] B --> F["Codex"] </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><=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>>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 -> retail-trademanager -> msg-push -> 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 金额与计算金额差异 <=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 透传字段和后端计算口径,尤其是 <=1 分取 pluAmount、>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 总价相差 <=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,并保持 <=1 分取标签金额、>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 -> 让 AI 分析 -> 生成结论</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 -> 线索抽取 -> trace / 日志 / 代码 / 知识检索 -> 追问补证据 -> 根因假设 -> 验证 -> 修复 / 收口</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["JIRA 输入"] --> B["抽取线索<br/>应用 / 时间 / 店铺 / trace / 关键词"] B --> C{"证据是否足够?"} C -- "足够" --> D["查 trace / 日志 / 代码"] C -- "不足" --> E["生成追问或反查策略"] E --> B D --> F["形成根因假设"] F --> G{"能否验证?"} G -- "能" --> H["验证假设"] G -- "不能" --> E H --> I{"需要修复?"} I -- "需要" --> J["改代码 / 补测试 / CR"] I -- "不需要" --> K["输出结论与证据"] J --> L["复查与收口"] L --> 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["元数据<br/>name + description"] --> B["SKILL.md 正文<br/>流程、约束、SOP"] B --> C["references/<br/>模板、补充文档、案例"] B --> D["scripts/<br/>校验、同步、查询、自动化执行"] C --> E["按需继续加载"] D --> F["按需执行,不必全进上下文"] </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["用户任务"] --> S["Skill<br/>决定流程与方法"] S --> M["MCP / Scripts / Tools<br/>执行真实动作"] M --> R["结果返回"] R --> 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 多问几个问题,而是把“需求澄清 -> 方案对比 -> 分段确认 -> 落文档”的流程固定下来。</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 -> 方案 -> 开发 -> 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["🟩 analyst<br/>需求分析专家<br/>brainstorming<br/>Tmux %2"] developer["🟨 developer<br/>代码实现专家<br/>sdd-dev-workflow<br/>Tmux %3"] reviewer["🟦 reviewer<br/>代码审查专家<br/>full-review<br/>Tmux %1"] lead["🫵 Team Lead<br/>就是本 AI"] lead --> analyst analyst --> developer developer --> reviewer reviewer --> 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["用户需求输入"] --> a["🟩 analyst<br/>brainstorming"] a --> d["🟨 developer<br/>sdd-dev-workflow"] d --> r["🟦 reviewer<br/>full-review"] r --> check{"发现问题?"} check -- "是" --> loop["进入修复闭环"] check -- "否" --> done["🎉 交付完成"] loop --> 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["发现问题 ❌"] --> confirm["analyst 确认问题 🤔"] confirm --> repair["developer 修复问题 🔧"] repair --> recheck["reviewer 复审 ✅"] recheck --> pass{"通过了吗?"} pass -- "否" --> confirm pass -- "是" --> done["🎉 广播完成"] </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["用户需求"] --> analyst["analyst<br/>需求分析"] analyst --> plan["📄 技术方案"] plan --> developer["developer<br/>代码实现"] developer --> code["💻 7 个代码文件"] code --> reviewer["reviewer<br/>代码审查"] reviewer --> report["📋 审查报告"] report --> problem{"发现问题?"} problem -- "YES" --> fix["闭环修复"] fix --> analyst problem -- "NO" --> finish["🎉 完成"] </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>协调流:推动“发现问题 -> 确认 -> 修复 -> 复审”</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>