<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>https://blog-tyb.pages.dev/</id>
  <title type="text">Melusine Blog</title>
  <subtitle type="text">项目实战 · 源码学习</subtitle>
  <updated>2026-09-24T00:00:00.000Z</updated>
  <author><name>Melusine</name></author>
  <link rel="alternate" href="https://blog-tyb.pages.dev/"/>
  <link rel="self" href="https://blog-tyb.pages.dev/atom.xml"/>
  <generator uri="https://github.com/CuteLeaf/Firefly">Firefly v6.16.8</generator>
    <entry>
      <id>https://blog-tyb.pages.dev/posts/bill-splitter-analysis/</id>
      <title type="text">一个 HTML 文件的分账工具：bill-splitter 纯函数设计赏析</title>
      <published>2026-09-24T00:00:00.000Z</published>
      <updated>2026-09-24T00:00:00.000Z</updated>
      <author><name>Melusine</name></author>
      <link rel="alternate" href="https://blog-tyb.pages.dev/posts/bill-splitter-analysis/"/>
      <summary type="text">分析 bill-splitter 的 __split 纯函数：四条不变量约束、最大余数法零头分配、以及为什么防御性编程在小工具里同样重要。</summary>
      <content type="html"><![CDATA[<section><h1>bill-splitter 源码赏析：一个 HTML 文件的工程哲学<a href="#bill-splitter-源码赏析一个-html-文件的工程哲学"><span>#</span></a></h1><p><a href="https://github.com/Melusine-ichnose/bill-splitter" target="_blank">bill-splitter</a> 是一个合租账单分摊工具，整个应用只有一个 HTML 文件。但别被体积骗了——它的核心算法设计得非常严谨。</p><section><h2>核心：<code>__split</code> 纯函数<a href="#核心__split-纯函数"><span>#</span></a></h2><p>整个应用的计算逻辑收敛在一个函数里：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>__split</span><span><span>(</span><span>totalCents</span><span>, </span><span>weights</span><span>, </span></span><span>...</span><span><span>rest</span><span>) → </span><span>number</span><span>[]</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>作者给这个函数立了四条<strong>不变量</strong>（无条件成立）：</p>

<table><thead><tr><th>编号</th><th>约束</th><th>含义</th></tr></thead><tbody><tr><td>I1</td><td>长度</td><td>返回数组长度 === 权重数组长度</td></tr><tr><td>I2</td><td>整数</td><td>每个元素都是整数（金额用分表示）</td></tr><tr><td>I3</td><td>守恒</td><td>所有元素之和 === totalCents</td></tr><tr><td>I4</td><td>纯</td><td>同样输入永远得到同样输出</td></tr></tbody></table><p>这四条不变量就是一份<strong>可执行的规格文档</strong>。不需要读实现，光看这四条就知道函数的行为边界。</p></section><section><h2>最大余数法 + 零头集中<a href="#最大余数法--零头集中"><span>#</span></a></h2><p>分摊算法用的是<strong>最大余数法</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 1. 权重预处理：NaN 和负数 → 0</span></div></div><div><div><div>2</div></div><div><span>// 2. 退化处理：权重总和为 0 → 全部置 1</span></div></div><div><div><div>3</div></div><div><span>// 3. 按比例取整：floor(总额 × 权重/总权重)</span></div></div><div><div><div>4</div></div><div><span>// 4. 零头分配：剩余零头全部给余数最大的人</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>举个例子，<code>__split(101, [1, 1, 1])</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// floor(101/3) = 33, 33, 33 → 剩余 2 分</span></div></div><div><div><div>2</div></div><div><span>// 余数分别是 0.67, 0.67, 0.67（相等）</span></div></div><div><div><div>3</div></div><div><span>// 按下标从小到大，第一个承担全部零头</span></div></div><div><div><div>4</div></div><div><span>// 结果：[35, 33, 33]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>为什么零头要集中给一个人？</strong> 作者的理由很实用——房东一眼能看出每月零头算在谁头上。这是工程思维：算法上的「公平」（轮流承担）在这里不如「可解释性」重要。</p></section><section><h2>防御性边界处理<a href="#防御性边界处理"><span>#</span></a></h2><p>函数对异常输入的处理也很教科书：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>__split</span><span>(</span><span>100</span><span>, [</span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>])     </span><span>// 权重全0 → 均分 [34, 33, 33]</span></div></div><div><div><div>2</div></div><div><span>__split</span><span>(</span><span>50</span><span>, [</span><span>NaN</span><span>, </span><span>-</span><span>1</span><span>, </span><span>30</span><span>])  </span><span>// NaN/负数 → 0 → [0, 0, 50]</span></div></div><div><div><div>3</div></div><div><span>__split</span><span>(</span><span>-</span><span>100</span><span>, [</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>])    </span><span>// 负金额 → 守恒=-100</span></div></div><div><div><div>4</div></div><div><span>__split</span><span>(</span><span>0</span><span>, [</span><span>5</span><span>, </span><span>5</span><span>, </span><span>5</span><span>])       </span><span>// 零金额 → [0, 0, 0]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>每个边界 case 都有明确的行为定义，不抛异常、不返回 NaN。</p></section><section><h2>架构：数据驱动 + 全量重渲染<a href="#架构数据驱动--全量重渲染"><span>#</span></a></h2><p>页面架构极其简单：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>配置数据（PERSONS / BILLS / DAYS）</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>↓ input 事件</span></div></div><div><div><div>3</div></div><div><span>render() 全量重渲染</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>↓ 每笔账单调 __split()</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>没有框架、没有虚拟 DOM、没有状态管理。因为数据量小（3 人 × 4 笔账单），全量重渲染的性能完全够用。<strong>在正确的场景用正确的复杂度</strong>——这也是工程能力。</p></section><section><h2>学到的三件事<a href="#学到的三件事"><span>#</span></a></h2><ol>
<li><strong>不变量比实现重要。</strong> 先定义行为约束，再写代码，测试和 review 都有据可依。</li>
<li><strong>金额必须用整数（分）计算。</strong> 浮点数在金额场景是万恶之源。</li>
<li><strong>零头集中不是 bug 是 feature。</strong> 工程决策要考虑业务可解释性，不只是数学正确性。</li>
</ol><p>项目地址：<a href="https://github.com/Melusine-ichnose/bill-splitter" target="_blank">github.com/Melusine-ichnose/bill-splitter</a></p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://blog-tyb.pages.dev/posts/deepstudy-source-analysis/</id>
      <title type="text">DeepStudy 源码解析：三层卷积 CNN 的标准训练范式</title>
      <published>2026-09-24T00:00:00.000Z</published>
      <updated>2026-09-24T00:00:00.000Z</updated>
      <author><name>Melusine</name></author>
      <link rel="alternate" href="https://blog-tyb.pages.dev/posts/deepstudy-source-analysis/"/>
      <summary type="text">拆解 DeepStudy 的 MNIST CNN 实现：三层卷积的尺寸推导、log_softmax+NLLLoss 组合、训练五步循环与完整的保存加载闭环。</summary>
      <content type="html"><![CDATA[<section><h1>DeepStudy 源码解析：三层卷积 CNN 的标准训练范式<a href="#deepstudy-源码解析三层卷积-cnn-的标准训练范式"><span>#</span></a></h1><p><a href="https://github.com/Melusine-ichnose/DeepStudy" target="_blank">DeepStudy</a> 是一个 PyTorch 手写数字识别项目：用三层卷积网络在 MNIST 上做 0–9 分类，官方 README 写明 5 轮训练后测试集准确率约 99%。整个仓库只有两个 Python 文件——<code>SY8.py</code> 负责模型定义、训练、评估、保存，<code>test.py</code> 负责加载、推理、可视化，加起来不到两百行。</p><p>但读下来你会发现，这两百行把深度学习训练的完整闭环都走了一遍：<strong>定义 → 训练 → 评估 → 保存 → 加载 → 推理 → 可视化</strong>。麻雀虽小，五脏俱全。这篇笔记记录我认为值得展开的几个点。</p><section><h2>项目结构：一个文件训练，一个文件推理<a href="#项目结构一个文件训练一个文件推理"><span>#</span></a></h2><p>仓库结构非常干净（来自 <a href="https://github.com/Melusine-ichnose/DeepStudy/blob/main/README.md" target="_blank">README.md</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>DeepStudy/</span></div></div><div><div><div>2</div></div><div><span>├── SY8.py     # 定义 CNN 模型，完成训练、评估、模型保存与样例可视化</span></div></div><div><div><div>3</div></div><div><span>├── test.py    # 加载已训练模型，对测试集图像进行预测并以 3×3 网格展示</span></div></div><div><div><div>4</div></div><div><span>└── README.md</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>训练和推理拆成两个文件，但模型定义只有一份——<code>test.py</code> 通过 <code>from DeepStudy.SY8 import CNN</code> 直接导入，而不是复制一份过来。这个选择后面再展开，先看模型本身。</p></section><section><h2>网络定义：为 28×28 量身定制的尺寸推导<a href="#网络定义为-2828-量身定制的尺寸推导"><span>#</span></a></h2><p>模型定义在 <a href="https://github.com/Melusine-ichnose/DeepStudy/blob/main/DeepStudy/SY8.py" target="_blank">SY8.py</a>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span><span> </span><span>CNN</span><span>(</span><span>torch</span><span>.</span><span>nn</span><span>.</span><span>Module</span><span>)</span></span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>):</span></div></div><div><div><div>3</div></div><div><span>        </span><span>super</span><span>().</span><span>__init__</span><span>()</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span><span>.conv1 </span><span>=</span><span> torch.nn.</span><span>Conv2d</span><span>(</span></span><span>1</span><span>, </span><span>32</span><span>, </span><span>kernel_size</span><span>=</span><span>3</span><span>, </span><span>stride</span><span>=</span><span>1</span><span>, </span><span>padding</span><span>=</span><span>1</span><span>)</span></div></div><div><div><div>5</div></div><div><span>        </span><span>self</span><span><span>.conv2 </span><span>=</span><span> torch.nn.</span><span>Conv2d</span><span>(</span></span><span>32</span><span>, </span><span>64</span><span>, </span><span>kernel_size</span><span>=</span><span>3</span><span>, </span><span>stride</span><span>=</span><span>1</span><span>, </span><span>padding</span><span>=</span><span>1</span><span>)</span></div></div><div><div><div>6</div></div><div><span>        </span><span>self</span><span><span>.conv3 </span><span>=</span><span> torch.nn.</span><span>Conv2d</span><span>(</span></span><span>64</span><span>, </span><span>128</span><span>, </span><span>kernel_size</span><span>=</span><span>3</span><span>, </span><span>stride</span><span>=</span><span>1</span><span>, </span><span>padding</span><span>=</span><span>1</span><span>)</span></div></div><div><div><div>7</div></div><div><span>        </span><span>self</span><span><span>.fc1 </span><span>=</span><span> torch.nn.</span><span>Linear</span><span>(</span></span><span>128</span><span><span> </span><span>*</span><span> </span></span><span>3</span><span><span> </span><span>*</span><span> </span></span><span>3</span><span>, </span><span>128</span><span>)</span></div></div><div><div><div>8</div></div><div><span>        </span><span>self</span><span><span>.fc2 </span><span>=</span><span> torch.nn.</span><span>Linear</span><span>(</span></span><span>128</span><span>, </span><span>10</span><span>)</span></div></div><div><div><div>9</div></div><div><span>        </span><span>self</span><span><span>.pool </span><span>=</span><span> torch.nn.</span><span>MaxPool2d</span><span>(</span></span><span>2</span><span>, </span><span>2</span><span>)</span></div></div><div><div><div>10</div></div><div><span>        </span><span>self</span><span><span>.dropout </span><span>=</span><span> torch.nn.</span><span>Dropout</span><span>(</span></span><span>0.5</span><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>def</span><span> </span><span>forward</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>x</span></span><span>):</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> torch.nn.functional.</span><span>relu</span><span>(</span><span>self</span><span><span>.</span><span>conv1</span><span>(x))</span></span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>pool</span><span>(x)</span></span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> torch.nn.functional.</span><span>relu</span><span>(</span><span>self</span><span><span>.</span><span>conv2</span><span>(x))</span></span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>pool</span><span>(x)</span></span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> torch.nn.functional.</span><span>relu</span><span>(</span><span>self</span><span><span>.</span><span>conv3</span><span>(x))</span></span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>pool</span><span>(x)</span></span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> x.</span><span>view</span><span>(</span><span>-</span><span>1</span><span>, </span><span>128</span><span><span> </span><span>*</span><span> </span></span><span>3</span><span><span> </span><span>*</span><span> </span></span><span>3</span><span>)</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> torch.nn.functional.</span><span>relu</span><span>(</span><span>self</span><span><span>.</span><span>fc1</span><span>(x))</span></span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>dropout</span><span>(x)</span></span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> torch.nn.functional.</span><span>log_softmax</span><span>(</span><span>self</span><span><span>.</span><span>fc2</span><span>(x), </span></span><span>dim</span><span>=</span><span>1</span><span>)</span></div></div><div><div><div>23</div></div><div><span>        </span><span>return</span><span> x</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三个卷积层完全同构：3×3 卷积、stride=1、padding=1，也就是说<strong>卷积只换通道数、不动空间尺寸</strong>，尺寸全靠后面的 MaxPool2d(2, 2) 减半。于是空间维度的演化是一条确定的直线：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>28×28 → pool → 14×14 → pool → 7×7 → pool → 3×3（向下取整）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>理解了这条线，<code>x.view(-1, 128 * 3 * 3)</code> 里的 <code>3 * 3</code> 就不是魔法数字，而是 28 连续三次减半（下取整）的结果。代价是这个网络是<strong>为 MNIST 硬编码的</strong>——输入一变，flatten 处直接 shape mismatch。对一个专攻 MNIST 的教学项目来说，这种取舍完全合理，但如果你要把它迁移到别的数据集，这三处地方（三个 conv 通道数、fc1 的输入维度、view 的参数）得连着改。</p><p>另一个细节：Dropout(0.5) 只放在 fc1 之后，三个卷积层一个都没加。这是教科书做法——卷积层的参数在感受野内共享，本身自带正则化，而过参数化最严重的全连接层才是过拟合的重灾区，dropout 该花在刀刃上。</p></section><section><h2>log_softmax + NLLLoss：一对老搭档<a href="#log_softmax--nllloss一对老搭档"><span>#</span></a></h2><p><code>forward</code> 的最后一层是 <code>log_softmax(self.fc2(x), dim=1)</code>，而 main 函数里配的损失函数是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#负对数似然损失函数</span></div></div><div><div><div>2</div></div><div><span><span>criterion </span><span>=</span><span> torch.nn.</span><span>NLLLoss</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这两个必须成对出现：log_softmax 输出的是<strong>对数概率</strong>，NLLLoss 做的事情只有一个——取出真实类别对应的那个 log 概率，取负。合起来正好是交叉熵。</p><p>初学者常见的疑惑是：为什么不直接 Softmax + CrossEntropyLoss？因为 PyTorch 的 <code>CrossEntropyLoss</code> 内部本来就是 <code>log_softmax + NLLLoss</code> 的合体，如果先手动 Softmax 再喂 CrossEntropyLoss，等于 softmax 算了两遍，而且第一遍没有 log-sum-exp 的数值保护，大 logits 下容易溢出。这里的写法是”分开放”的流派：网络自己保证输出语义是 log 概率，损失函数只管挑类别。两种流派都对，唯独”Softmax + CrossEntropyLoss”这个直觉组合是错的。</p></section><section><h2>训练循环：教科书五步，一个都不能少<a href="#训练循环教科书五步一个都不能少"><span>#</span></a></h2><p>主训练循环同样在 <a href="https://github.com/Melusine-ichnose/DeepStudy/blob/main/DeepStudy/SY8.py" target="_blank">SY8.py</a>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>for</span><span> epoch </span><span>in</span><span> </span><span>range</span><span>(</span><span>5</span><span>):</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>running_loss </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>3</div></div><div><span>    </span><span>for</span><span> i, (x, y) </span><span>in</span><span> </span><span>enumerate</span><span>(train_data):</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>optimizer.</span><span>zero_grad</span><span>()</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>output </span><span>=</span><span> net.</span><span>forward</span><span>(x)</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>loss </span><span>=</span><span> </span><span>criterion</span><span>(output, y)</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>loss.</span><span>backward</span><span>()</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>optimizer.</span><span>step</span><span>()</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>running_loss </span><span>+=</span><span> loss.</span><span>item</span><span>()</span></div></div><div><div><div>10</div></div><div><span>        </span><span>if</span><span><span> i </span><span>%</span><span> </span></span><span>200</span><span><span> </span><span>==</span><span> </span></span><span>199</span><span>:</span></div></div><div><div><div>11</div></div><div><span>            </span><span>print</span><span>(</span><span>f</span><span>"[</span><span>{</span><span><span>epoch </span><span>+</span><span> </span></span><span>1}</span><span>, </span><span>{</span><span><span>i </span><span>+</span><span> </span></span><span>1}</span><span>] 损失: </span><span>{</span><span><span>running_loss </span><span>/</span><span> </span></span><span>200</span><span>:.3f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>running_loss </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>accuracy </span><span>=</span><span> </span><span>evaluate</span><span>(test_data, net)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"第 </span><span>{</span><span><span>epoch </span><span>+</span><span> </span></span><span>1}</span><span> 轮准确率: </span><span>{</span><span><span>accuracy </span><span>*</span><span> </span></span><span>100</span><span>:.2f</span><span>}</span><span>%"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>zero_grad → forward → loss → backward → step</code> 这五步是 PyTorch 训练的骨架，其中最容易被初学者漏掉的是第一步。PyTorch 的梯度默认<strong>累积</strong>：<code>loss.backward()</code> 是往参数的 <code>.grad</code> 上做加法，不清零的话，上一个 batch 的梯度会叠加进这个 batch，等于在用一个脏梯度更新参数。这个坑几乎人人踩过，这里写对了。</p><p>日志节奏也处理得克制：不是每个 batch 都打印，而是攒够 200 个 batch 打一次平均损失，然后把 <code>running_loss</code> 清零重新累计。每 200 个 batch 一次、每个 epoch 结束评一次准确率——信息密度刚好够观察训练是否收敛，又不至于刷屏。</p><p>优化器与学习率没什么花哨的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>optimizer </span><span>=</span><span> torch.optim.</span><span>Adam</span><span>(net.</span><span>parameters</span><span>(), </span></span><span>lr</span><span>=</span><span>0.001</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h2>evaluate：朴素但正确的评估<a href="#evaluate朴素但正确的评估"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>evaluate</span><span>(</span><span>test_data</span><span>,</span><span><span> </span><span>net</span></span><span>):</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>n_correct </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>n_total </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>4</div></div><div><span>    </span><span>with</span><span><span> torch.</span><span>no_grad</span><span>():</span></span></div></div><div><div><div>5</div></div><div><span>        </span><span>for</span><span> (x, y) </span><span>in</span><span> test_data:</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>outputs </span><span>=</span><span> net.</span><span>forward</span><span>(x)</span></div></div><div><div><div>7</div></div><div><span>            </span><span>for</span><span> i, output </span><span>in</span><span> </span><span>enumerate</span><span>(outputs):</span></div></div><div><div><div>8</div></div><div><span>                </span><span>if</span><span><span> torch.</span><span>argmax</span><span>(output) </span><span>==</span><span> y[i]:</span></span></div></div><div><div><div>9</div></div><div><span><span>                    </span></span><span>n_correct </span><span>+=</span><span> </span><span>1</span></div></div><div><div><div>10</div></div><div><span><span>                </span></span><span>n_total </span><span>+=</span><span> </span><span>1</span></div></div><div><div><div>11</div></div><div><span>    </span><span>return</span><span><span> n_correct </span><span>/</span><span> n_total</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个值得说的点：</p><ol>
<li><strong><code>torch.no_grad()</code> 包住了整个循环。</strong> 评估不需要梯度，关掉自动求导的图构建能省显存、提速。评估函数忘写 no_grad 是另一个经典失误，尤其配合 <code>retain_graph</code> 相关的显存爆掉问题。</li>
<li><strong>逐样本的 Python 循环很朴素。</strong> 每个输出挨个 <code>argmax</code> 再比较，其实一行向量化就能干完：<code>(outputs.argmax(dim=1) == y).sum().item()</code>。MNIST 上无所谓，但这是从”能跑”到”会写”的分界线，值得留意。</li>
</ol><p>比实现本身更有意思的是 main 里的这个调用位置：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(</span><span>"初始准确率:"</span><span><span>, </span><span>evaluate</span><span>(test_data, net))</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>训练还没开始，先评估一次随机初始化的网络。在 MNIST 十分类上，随机网络准确率应该在 10% 附近——正好是瞎猜的水平。这个 baseline 一举两得：既验证了评估函数没有 bug（如果初始准确率明显不是 10%，多半是评估或数据加载出了问题），又给后面每轮的 99% 一个”从哪爬上来的”参照。很小的习惯，但反映出的工程意识不小时。</p></section><section><h2>test.py：推理侧的三个细节<a href="#testpy推理侧的三个细节"><span>#</span></a></h2><p>推理脚本 <a href="https://github.com/Melusine-ichnose/DeepStudy/blob/main/DeepStudy/test.py" target="_blank">test.py</a> 不长，但每一步都踩在正确的点上：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>load_model</span><span>(</span><span>model_path</span><span>):</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>model </span><span>=</span><span> </span><span>CNN</span><span>()</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>model.</span><span>load_state_dict</span><span>(torch.</span><span>load</span><span>(model_path))</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>model.</span><span>eval</span><span>()</span></div></div><div><div><div>5</div></div><div><span>    </span><span>return</span><span> model</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>#预测单张图像</span></div></div><div><div><div>8</div></div><div><span>def</span><span> </span><span>predict_single_image</span><span>(</span><span>model</span><span>,</span><span><span> </span><span>image_tensor</span></span><span>):</span></div></div><div><div><div>9</div></div><div><span>    </span><span>with</span><span><span> torch.</span><span>no_grad</span><span>():</span></span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>output </span><span>=</span><span> </span><span>model</span><span>(image_tensor.</span><span>unsqueeze</span><span>(</span><span>0</span><span>))</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>prediction </span><span>=</span><span> torch.</span><span>argmax</span><span>(output).</span><span>item</span><span>()</span></div></div><div><div><div>12</div></div><div><span>    </span><span>return</span><span> prediction</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>细节一：state_dict 保存与加载。</strong> 训练脚本结束时用的是 <code>torch.save(net.state_dict(), 'mnist_cnn.pth')</code>——只保存参数张量，不 pickle 整个模型对象。这带来了推理侧的一个必然结果：必须先实例化一个同结构的 <code>CNN()</code>，再把权重灌进去。这是 PyTorch 官方推荐的保存方式，比 pickle 整个对象更可移植（不依赖源码路径和类定义的 pickle 兼容性）。副作用是模型定义必须单点维护，于是 <code>test.py</code> 用 <code>from DeepStudy.SY8 import CNN</code> 导入——定义只有一份，保存和加载永远不会漂移。</p><p><strong>细节二：<code>model.eval()</code> 不可省。</strong> 训练时 Dropout(0.5) 会随机置零一半的激活，推理时如果忘了 <code>eval()</code>，dropout 还在随机丢，预测结果就是随机化的——99% 的准确率会瞬间掉到不可用的水平。这个 bug 不报错、能跑完，只是结果莫名变差，属于最阴险的一类。这里写对了。</p><p><strong>细节三：<code>unsqueeze(0)</code> 补 batch 维。</strong> 从数据集里取出的单张图是 <code>(1, 28, 28)</code> 三维张量，而 <code>Conv2d</code> 只接受 <code>(batch, channel, h, w)</code> 四维输入，所以推理前要 <code>unsqueeze(0)</code> 补上 batch 维变成 <code>(1, 1, 28, 28)</code>。单样本推理和批量的维度差异，是写推理代码时最常见的报错来源。</p><p>顺带一提，README 专门写了这个项目必须这样运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 在 DeepStudy 的父目录中执行</span></div></div><div><div><div>2</div></div><div><span>python</span><span> </span><span>-m</span><span> </span><span>DeepStudy.test</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>原因正是上面的包导入：<code>from DeepStudy.SY8 import CNN</code> 是包导入语法，直接在仓库目录里 <code>python test.py</code> 会 import 失败，必须从父目录以模块方式跑。README 把这个坑提前写清楚了，省了后来者一次报错。</p></section><section><h2>值得学习的地方<a href="#值得学习的地方"><span>#</span></a></h2><p>读完整仓源码，我认为有四点值得带走：</p><ol>
<li><strong>闭环完整。</strong> 不到两百行，训练、评估、保存、加载、推理、可视化全都有，而且职责边界清楚：<code>SY8.py</code> 只管”从数据到权重”，<code>test.py</code> 只管”从权重到预测”。入门项目最怕只写训练不写推理，权重存下来就没人管了。</li>
<li><strong>Baseline 意识。</strong> 训练前先测一次随机准确率，10% 的数字同时验证了评估函数和数据管线。以后写任何训练脚本，我都建议保留这一行。</li>
<li><strong>组合拳用得规范。</strong> log_softmax + NLLLoss、dropout 只放全连接层、state_dict 保存、eval() 切推理模式——这些不是炫技点，而是保证结果可复现、推理不翻车的地基。</li>
<li><strong>把坑写在文档里。</strong> <code>python -m</code> 的运行方式、初始准确率预期、每轮准确率预期，README 都提前交代了。代码短，文档不糊弄。</li>
</ol><p>作为 MNIST 级别的入门项目，DeepStudy 没有任何多余的东西，也没有缺关键的东西——这大概就是对”标准训练范式”四个字最好的注解。</p><p>项目地址：<a href="https://github.com/Melusine-ichnose/DeepStudy" target="_blank">github.com/Melusine-ichnose/DeepStudy</a></p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://blog-tyb.pages.dev/posts/how-to-read-source-code/</id>
      <title type="text">如何阅读优秀开源项目的源码</title>
      <published>2026-09-24T00:00:00.000Z</published>
      <updated>2026-09-24T00:00:00.000Z</updated>
      <author><name>Melusine</name></author>
      <link rel="alternate" href="https://blog-tyb.pages.dev/posts/how-to-read-source-code/"/>
      <summary type="text">分享一套可复用的源码阅读方法论：从宏观架构到核心链路，再到设计模式的落地应用。</summary>
      <content type="html"><![CDATA[<section><h1>如何阅读优秀开源项目的源码<a href="#如何阅读优秀开源项目的源码"><span>#</span></a></h1><p>阅读源码是程序员进阶的必经之路。但很多人打开一个开源项目，面对成千上万的文件往往无从下手。这篇文章总结我在阅读开源项目时的一套方法论。</p><section><h2>第一步：先用起来，再读源码<a href="#第一步先用起来再读源码"><span>#</span></a></h2><p><strong>永远不要在没有使用过某个框架的情况下直接读它的源码。</strong></p><p>正确姿势：</p><ol>
<li>先阅读官方文档的 Quick Start，把 demo 跑起来</li>
<li>在自己的项目里实际使用它，踩几个坑</li>
<li>带着「它是怎么做到的」这个问题去读源码</li>
</ol><p>只有真正用过，你才会对框架的能力边界有体感，读源码时才知道哪些模块是重点。</p></section><section><h2>第二步：从宏观架构入手<a href="#第二步从宏观架构入手"><span>#</span></a></h2><p>打开一个项目，不要急着钻进某个类里。先做三件事：</p><ol>
<li><strong>看目录结构</strong>：<code>src/main</code> 下有哪些顶级包？通常包名就是模块划分</li>
<li><strong>看 README 和官方架构文档</strong>：作者通常会介绍核心概念和设计哲学</li>
<li><strong>看依赖关系</strong>：<code>pom.xml</code> / <code>package.json</code> 能看出它依赖了哪些底层库</li>
</ol><p>以 SpringBoot 为例，顶级结构里 <code>spring-boot-autoconfigure</code> 就是自动装配的核心，<code>spring-boot-starter-*</code> 是各种场景入口。先建立这张「地图」，后面才不会迷路。</p></section><section><h2>第三步：跟踪核心链路<a href="#第三步跟踪核心链路"><span>#</span></a></h2><p>每个框架都有几条「生命线」级的调用链路。抓住它们，就抓住了框架的骨架。</p><ul>
<li><strong>Web 框架</strong>：一次 HTTP 请求从进来到响应的完整链路</li>
<li><strong>ORM 框架</strong>：一条 SQL 从方法调用到数据库执行的链路</li>
<li><strong>RPC 框架</strong>：一次远程调用的序列化、网络传输、反序列化链路</li>
</ul><p>具体做法：找到一个入口方法（比如 Controller 的接口），用 IDE 的「调用层次结构」（Call Hierarchy）功能一路向下追，画出时序图。</p></section><section><h2>第四步：关注设计模式与取舍<a href="#第四步关注设计模式与取舍"><span>#</span></a></h2><p>源码阅读的精华在于理解<strong>作者为什么这么设计</strong>：</p><ul>
<li>这里为什么用策略模式而不是 if-else？</li>
<li>这里为什么用事件驱动而不是直接调用？</li>
<li>这里的缓存为什么要分两级？</li>
</ul><p>推荐边读边记笔记，把这些设计决策摘录下来，思考能不能用到自己的项目里。</p></section><section><h2>第五步：输出倒逼输入<a href="#第五步输出倒逼输入"><span>#</span></a></h2><p>读完不写等于白读。输出的形式可以很多：</p><ul>
<li>写一篇源码解析博客（就像本站的文章）</li>
<li>给项目提一个小的 PR（修 typo 也好）</li>
<li>模仿它的设计，自己写一个 mini 版本</li>
</ul><p>以 LangGraph 为例，如果你想真正理解它的状态图编排，最好的方式是动手写一个简化版的 Agent 循环执行框架。写的过程中遇到的问题，会逼着你回头把源码彻底吃透。</p></section><section><h2>推荐几个值得精读的项目<a href="#推荐几个值得精读的项目"><span>#</span></a></h2>

<table><thead><tr><th>项目</th><th>语言</th><th>为什么值得读</th></tr></thead><tbody><tr><td>SpringBoot</td><td>Java</td><td>自动装配、条件注解的教科书级实现</td></tr><tr><td>Netty</td><td>Java</td><td>高性能网络编程、Reactor 模式的最佳实践</td></tr><tr><td>Redis</td><td>C</td><td>单线程模型、事件驱动、数据结构设计的典范</td></tr><tr><td>LangChain / LangGraph</td><td>Python</td><td>AI Agent 编排的前沿设计，状态机思想的落地</td></tr><tr><td>Vue 3</td><td>TypeScript</td><td>响应式系统、编译器优化的现代前端实践</td></tr></tbody></table><hr /><p>读源码没有捷径，但有方法。希望这篇文章能帮你少走弯路。</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://blog-tyb.pages.dev/posts/mall-source-analysis/</id>
      <title type="text">多商户商城系统源码解析：一笔订单背后的拆单、渲染与兜底</title>
      <published>2026-09-24T00:00:00.000Z</published>
      <updated>2026-09-24T00:00:00.000Z</updated>
      <author><name>Melusine</name></author>
      <link rel="alternate" href="https://blog-tyb.pages.dev/posts/mall-source-analysis/"/>
      <summary type="text">深入一个 B2B2C 多商户商城的 Java 源码，拆解交易-订单两级拆单、购物车渲染流水线、定时兜底与商家端数据隔离四个核心设计。</summary>
      <content type="html"><![CDATA[<section><h1>多商户商城系统源码解析：一笔订单背后的拆单、渲染与兜底<a href="#多商户商城系统源码解析一笔订单背后的拆单渲染与兜底"><span>#</span></a></h1><p><a href="https://github.com/Melusine-ichnose/Multi-merchant-Mall-System" target="_blank">Multi-merchant-Mall-System</a> 是一个 B2B2C 多商户商城系统的后端仓库：Java + Spring Boot，九个后端模块、一千五百多个源文件，消息队列用 RocketMQ，搜索用 Elasticsearch，定时任务用 XXL-Job。翻代码的时候能明显看出这套代码与开源项目 lilishop 同源——包名是 <code>cn.lili</code>，类注释里的署名是 Chopper、paulG 这些 lilishop 的作者。这篇文章不管归属，就把它当成一份完整的 B2B2C 教材，记录我读源码时认为最值得学的几个设计。</p><section><h2>一、模块划分：一个角色一个 API 服务<a href="#一模块划分一个角色一个-api-服务"><span>#</span></a></h2><p>先看顶层结构。仓库不是单体大杂烩，而是按角色切了模块：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>admin          独立的管理端启动器</span></div></div><div><div><div>2</div></div><div><span>buyer-api      买家端接口</span></div></div><div><div><div>3</div></div><div><span>seller-api     商家端接口</span></div></div><div><div><div>4</div></div><div><span>manager-api    平台运营端接口</span></div></div><div><div><div>5</div></div><div><span>common-api     公共接口</span></div></div><div><div><div>6</div></div><div><span>consumer       消息消费者 + 定时任务</span></div></div><div><div><div>7</div></div><div><span>framework      实体、服务层等公共业务代码</span></div></div><div><div><div>8</div></div><div><span>im-api         即时通讯</span></div></div><div><div><div>9</div></div><div><span>xxl-job        分布式调度</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>业务代码几乎全下沉在 <code>framework</code> 里，三个 API 模块只是薄薄的 Controller 层。买家、商家、平台三个角色各走各的服务，物理上就隔离开了——商家端被打穿不会波及买家端。这个拆法在商城这类多角色系统里非常实用。</p></section><section><h2>二、多商户的根基：Trade → Order 两级拆单<a href="#二多商户的根基trade--order-两级拆单"><span>#</span></a></h2><p>多商户商城和普通商城最本质的区别是：<strong>一次下单可能跨多个店铺，必须按店铺拆单</strong>。这套代码的建模方式是”交易（Trade）- 订单（Order）“两级结构。</p><p><code>TradeDTO</code>（贯穿下单流程的视图对象）里有两条线索。一个是平台券和店铺券分开建模（来自 <a href="https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/framework/src/main/java/cn/lili/modules/order/cart/entity/dto/TradeDTO.java" target="_blank">TradeDTO.java</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/**</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>* 使用平台优惠券，一笔订单只能使用一个平台优惠券</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>*/</span></div></div><div><div><div>4</div></div><div><span>private</span><span> </span><span>MemberCouponDTO</span><span> platformCoupon</span><span>;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>/**</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>* key 为商家id</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>* value 为商家优惠券</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>* 店铺优惠券</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>*/</span></div></div><div><div><div>11</div></div><div><span>private</span><span> </span><span>Map</span><span>&lt;</span><span>String</span><span><span>,</span><span> </span></span><span>MemberCouponDTO</span><span><span>&gt;</span><span> storeCoupons</span></span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>平台券一张订单只能用一张，店铺券按商家 ID 分组、每个店铺各用各的——优惠券体系从数据结构上就按”平台/商户”分了层。</p><p>拆单发生在购物车阶段：购物车里每个 <code>CartVO</code> 就是一个店铺的商品集合，下单时一个 <code>CartVO</code> 生成一个 <code>Order</code>。所以 <code>Order</code> 实体里直接冗余了店铺字段（来自 <a href="https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/framework/src/main/java/cn/lili/modules/order/order/entity/dos/Order.java" target="_blank">Order.java</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>ApiModelProperty</span><span>(</span><span>"交易编号 关联Trade"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>private</span><span> </span><span>String</span><span> tradeSn</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>@</span><span>ApiModelProperty</span><span>(</span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>"店铺ID"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>private</span><span> </span><span>String</span><span> storeId</span><span>;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>@</span><span>ApiModelProperty</span><span>(</span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>"店铺名称"</span><span>)</span></div></div><div><div><div>8</div></div><div><span>private</span><span> </span><span>String</span><span> storeName</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>入库的实现在 <a href="https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/framework/src/main/java/cn/lili/modules/order/order/serviceimpl/OrderServiceImpl.java" target="_blank">OrderServiceImpl.java</a> 的 <code>intoDB</code>，整个方法包在一个事务里：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>2</div></div><div><span>@</span><span>Transactional</span><span>(</span><span>rollbackFor</span><span><span> </span><span>=</span><span> </span></span><span>Exception</span><span>.</span><span>class</span><span>)</span></div></div><div><div><div>3</div></div><div><span>public</span><span> </span><span>void</span><span> </span><span>intoDB</span><span>(</span><span>TradeDTO</span><span> tradeDTO) {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>//检查TradeDTO信息</span></div></div><div><div><div>5</div></div><div><span>    </span><span>checkTradeDTO</span><span><span>(tradeDTO)</span><span>;</span></span></div></div><div><div><div>6</div></div><div><span>    </span><span>//存放购物车，即业务中的订单</span></div></div><div><div><div>7</div></div><div><span>    </span><span>List</span><span>&lt;</span><span>Order</span><span><span>&gt;</span><span> orders </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>ArrayList</span><span><span>&lt;&gt;</span><span>(</span></span><span>tradeDTO</span><span>.</span><span>getCartList</span><span>().</span><span>size</span><span><span>()</span><span>)</span><span>;</span></span></div></div><div><div><div>8</div></div><div><span>    </span><span>//存放自订单/订单日志</span></div></div><div><div><div>9</div></div><div><span>    </span><span>List</span><span>&lt;</span><span>OrderItem</span><span><span>&gt;</span><span> orderItems </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>ArrayList</span><span><span>&lt;&gt;</span><span>()</span><span>;</span></span></div></div><div><div><div>10</div></div><div><span>    </span><span>List</span><span>&lt;</span><span>OrderLog</span><span><span>&gt;</span><span> orderLogs </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>ArrayList</span><span><span>&lt;&gt;</span><span>()</span><span>;</span></span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>//循环购物车</span></div></div><div><div><div>13</div></div><div><span>    </span><span>tradeDTO</span><span>.</span><span>getCartList</span><span>().</span><span>forEach</span><span>(item </span><span>-&gt;</span><span> {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>Order</span><span><span> </span><span>order</span><span> </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>Order</span><span>(item, tradeDTO);</span></div></div><div><div><div>15</div></div><div><span>        </span><span>orders</span><span>.</span><span>add</span><span>(order);</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>...</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>18</div></div><div><span>    </span><span>//批量保存订单</span></div></div><div><div><div>19</div></div><div><span>    </span><span>this</span><span>.</span><span>saveBatch</span><span>(orders);</span></div></div><div><div><div>20</div></div><div><span>    </span><span>//批量保存 子订单</span></div></div><div><div><div>21</div></div><div><span>    </span><span>orderItemService</span><span>.</span><span>saveBatch</span><span>(orderItems);</span></div></div><div><div><div>22</div></div><div><span>    </span><span>//批量记录订单操作日志</span></div></div><div><div><div>23</div></div><div><span>    </span><span>orderLogService</span><span>.</span><span>saveBatch</span><span>(orderLogs);</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>而支付是按交易维度进行的，<a href="https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/framework/src/main/java/cn/lili/modules/order/order/serviceimpl/TradeServiceImpl.java" target="_blank">TradeServiceImpl.java</a> 的 <code>payTrade</code> 把一笔交易下的所有订单逐个推进支付流程：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>2</div></div><div><span>@</span><span>Transactional</span><span>(</span><span>rollbackFor</span><span><span> </span><span>=</span><span> </span></span><span>Exception</span><span>.</span><span>class</span><span>)</span></div></div><div><div><div>3</div></div><div><span>public</span><span> </span><span>void</span><span> </span><span>payTrade</span><span>(</span><span>String</span><span><span> tradeSn</span><span>,</span><span> </span></span><span>String</span><span><span> paymentName</span><span>,</span><span> </span></span><span>String</span><span> receivableNo) {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>LambdaQueryWrapper</span><span>&lt;</span><span>Order</span><span><span>&gt;</span><span> orderQueryWrapper </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>LambdaQueryWrapper</span><span><span>&lt;&gt;</span><span>()</span><span>;</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>orderQueryWrapper</span><span>.</span><span>eq</span><span>(Order</span><span>::</span><span>getTradeSn, tradeSn);</span></div></div><div><div><div>6</div></div><div><span>    </span><span>List</span><span>&lt;</span><span>Order</span><span><span>&gt;</span><span> orders </span></span><span><span>=</span><span> </span></span><span>orderService</span><span>.</span><span>list</span><span>(orderQueryWrapper);</span></div></div><div><div><div>7</div></div><div><span>    </span><span>for</span><span> (</span><span>Order</span><span> order </span><span>:</span><span> orders) {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>orderService</span><span>.</span><span>payOrder</span><span>(</span><span>order</span><span>.</span><span>getSn</span><span>(), paymentName, receivableNo);</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>...</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>用户付一次钱，N 个店铺的订单各自流转各自的发货、售后。这就是 B2B2C 的”一次支付、拆单履约”。</p></section><section><h2>三、价格计算：可编排的渲染流水线<a href="#三价格计算可编排的渲染流水线"><span>#</span></a></h2><p>价格计算是商城里最乱的部分——商品促销、满减、优惠券、运费、佣金层层叠加，还要区分购物车展示、结算页、各种营销下单等不同场景。这套代码的解法我特别喜欢：<strong>把每一步计算做成一个 Bean，用数组声明流水线的顺序</strong>。</p><p>先定义步骤接口（<a href="https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/framework/src/main/java/cn/lili/modules/order/cart/render/CartRenderStep.java" target="_blank">CartRenderStep.java</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public</span><span> </span><span>interface</span><span><span> </span><span>CartRenderStep</span><span> </span></span><span>{</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>    </span><span>RenderStepEnums</span><span> </span><span>step</span><span>();</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>void</span><span> </span><span>render</span><span>(</span><span>TradeDTO</span><span><span> </span><span>tradeDTO</span><span>);</span></span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后每种场景声明自己的步骤组合（<a href="https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/framework/src/main/java/cn/lili/modules/order/cart/render/RenderStepStatement.java" target="_blank">RenderStepStatement.java</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/**</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>* 交易创建前渲染</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>* 渲染购物车 生成SN 》分销人员佣金渲染 》平台佣金渲染</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>*/</span></div></div><div><div><div>5</div></div><div><span>public</span><span> </span><span>static</span><span> </span><span>RenderStepEnums</span><span>[] tradeRender </span><span><span>=</span><span> {</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>RenderStepEnums</span><span>.</span><span>CHECKED_FILTER</span><span>,</span></div></div><div><div><div>7</div></div><div><span>        </span><span>RenderStepEnums</span><span>.</span><span>CHECK_DATA</span><span>,</span></div></div><div><div><div>8</div></div><div><span>        </span><span>RenderStepEnums</span><span>.</span><span>SKU_PROMOTION</span><span>,</span></div></div><div><div><div>9</div></div><div><span>        </span><span>RenderStepEnums</span><span>.</span><span>FULL_DISCOUNT</span><span>,</span></div></div><div><div><div>10</div></div><div><span>        </span><span>RenderStepEnums</span><span>.</span><span>COUPON</span><span>,</span></div></div><div><div><div>11</div></div><div><span>        </span><span>RenderStepEnums</span><span>.</span><span>SKU_FREIGHT</span><span>,</span></div></div><div><div><div>12</div></div><div><span>        </span><span>RenderStepEnums</span><span>.</span><span>CART_PRICE</span><span>,</span></div></div><div><div><div>13</div></div><div><span>        </span><span>RenderStepEnums</span><span>.</span><span>CART_SN</span><span>,</span></div></div><div><div><div>14</div></div><div><span>        </span><span>RenderStepEnums</span><span>.</span><span>DISTRIBUTION</span><span>,</span></div></div><div><div><div>15</div></div><div><span>        </span><span>RenderStepEnums</span><span>.</span><span>PLATFORM_COMMISSION</span></div></div><div><div><div>16</div></div><div><span><span>}</span><span>;</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>执行器是 <a href="https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/framework/src/main/java/cn/lili/modules/order/cart/render/TradeBuilder.java" target="_blank">TradeBuilder.java</a>，利用 Spring 把所有 <code>CartRenderStep</code> 实现类注入成列表，再按声明的顺序依次调用：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>private</span><span> </span><span>void</span><span> </span><span>renderCartBySteps</span><span>(</span><span>TradeDTO</span><span><span> tradeDTO</span><span>,</span><span> </span></span><span>RenderStepEnums</span><span>[] defaultRender) {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>for</span><span> (</span><span>RenderStepEnums</span><span> step </span><span>:</span><span> defaultRender) {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>for</span><span> (</span><span>CartRenderStep</span><span> render </span><span>:</span><span> cartRenderSteps) {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>try</span><span> {</span></div></div><div><div><div>5</div></div><div><span>                </span><span>if</span><span> (</span><span>render</span><span>.</span><span>step</span><span>().</span><span>equals</span><span><span>(step)</span><span>) {</span></span></div></div><div><div><div>6</div></div><div><span>                    </span><span>render</span><span>.</span><span>render</span><span>(tradeDTO);</span></div></div><div><div><div>7</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>} </span><span>catch</span><span> (</span><span>ServiceException</span><span><span> </span><span>e</span><span>) {</span></span></div></div><div><div><div>9</div></div><div><span>                </span><span>throw</span><span><span> e</span><span>;</span></span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>} </span><span>catch</span><span> (</span><span>Exception</span><span><span> </span><span>e</span><span>) {</span></span></div></div><div><div><div>11</div></div><div><span>                </span><span>log</span><span>.</span><span>error</span><span>(</span><span>"购物车{}渲染异常："</span><span>, </span><span>render</span><span>.</span><span>getClass</span><span>(), e);</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>不同场景复用不同流水线：购物车展示只要校验、促销、满减、算价四步；结算页要加上优惠券和运费；积分/砍价这类单品购买跳过满减；普通下单则要再追加流水号、分销佣金、平台佣金。新增一种营销玩法，写一个 <code>CartRenderStep</code> 实现类、往数组里插一个枚举就行，价格计算这个”重灾区”被治理得井井有条。</p><p>那段异常处理也值得品：<code>ServiceException</code> 是业务校验失败（比如商品已下架），必须中断下单直接抛出；而其他未知异常只记日志、继续渲染——展示购物车时一个非关键步骤挂掉不应该让整个购物车打不开。区分”必须失败的异常”和”可以带病运行的异常”，这是业务系统里很见功力的细节。</p></section><section><h2>四、未支付订单的自动关闭：定时任务兜底<a href="#四未支付订单的自动关闭定时任务兜底"><span>#</span></a></h2><p>和上一篇座位预约系统一样，这套代码对”应该发生但用户没做”的事情也用了定时任务兜底。订单超时未支付要自动释放库存、关闭订单，实现在 <a href="https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/consumer/src/main/java/cn/lili/timetask/handler/impl/order/CancelOrderTaskExecute.java" target="_blank">CancelOrderTaskExecute.java</a>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>Override</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>void</span><span> </span><span>execute</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Setting</span><span> setting </span><span><span>=</span><span> </span></span><span>settingService</span><span>.</span><span>get</span><span>(</span><span>SettingEnum</span><span>.</span><span>ORDER_SETTING</span><span>.</span><span>name</span><span>());</span></div></div><div><div><div>4</div></div><div><span>    </span><span>OrderSetting</span><span> orderSetting </span><span><span>=</span><span> </span></span><span>JSONUtil</span><span>.</span><span>toBean</span><span>(</span><span>setting</span><span>.</span><span>getSettingValue</span><span>(), </span><span>OrderSetting</span><span>.</span><span>class</span><span>);</span></div></div><div><div><div>5</div></div><div><span>    </span><span>if</span><span><span> (orderSetting </span><span>!=</span><span> </span></span><span>null</span><span><span> </span><span>&amp;&amp;</span><span> </span></span><span>orderSetting</span><span>.</span><span>getAutoCancel</span><span><span>()</span><span> </span><span>!=</span><span> </span></span><span>null</span><span>) {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>//订单自动取消时间 = 当前时间 - 自动取消时间分钟数</span></div></div><div><div><div>7</div></div><div><span>        </span><span>DateTime</span><span> cancelTime </span><span><span>=</span><span> </span></span><span>DateUtil</span><span>.</span><span>offsetMinute</span><span>(</span><span>DateUtil</span><span>.</span><span>date</span><span><span>(), </span><span>-</span></span><span>orderSetting</span><span>.</span><span>getAutoCancel</span><span>());</span></div></div><div><div><div>8</div></div><div><span>        </span><span>LambdaQueryWrapper</span><span>&lt;</span><span>Order</span><span><span>&gt;</span><span> queryWrapper </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>LambdaQueryWrapper</span><span><span>&lt;&gt;</span><span>()</span><span>;</span></span></div></div><div><div><div>9</div></div><div><span>        </span><span>queryWrapper</span><span>.</span><span>eq</span><span>(Order</span><span>::</span><span>getOrderStatus, </span><span>OrderStatusEnum</span><span>.</span><span>UNPAID</span><span>.</span><span>name</span><span>());</span></div></div><div><div><div>10</div></div><div><span>        </span><span>//订单创建时间 &lt;= 订单自动取消时间</span></div></div><div><div><div>11</div></div><div><span>        </span><span>queryWrapper</span><span>.</span><span>le</span><span>(Order</span><span>::</span><span>getCreateTime, cancelTime);</span></div></div><div><div><div>12</div></div><div><span>        </span><span>List</span><span>&lt;</span><span>Order</span><span><span>&gt;</span><span> list </span></span><span><span>=</span><span> </span></span><span>orderService</span><span>.</span><span>list</span><span>(queryWrapper);</span></div></div><div><div><div>13</div></div><div><span>        </span><span>List</span><span>&lt;</span><span>String</span><span><span>&gt;</span><span> cancelSnList </span></span><span><span>=</span><span> </span></span><span>list</span><span>.</span><span>stream</span><span>().</span><span>map</span><span>(Order</span><span>::</span><span>getSn).</span><span>collect</span><span>(</span><span>Collectors</span><span>.</span><span>toList</span><span>());</span></div></div><div><div><div>14</div></div><div><span>        </span><span>for</span><span> (</span><span>String</span><span> sn </span><span>:</span><span> cancelSnList) {</span></div></div><div><div><div>15</div></div><div><span>            </span><span>orderService</span><span>.</span><span>systemCancel</span><span>(sn, </span><span>"超时未支付自动取消"</span><span>, </span><span>false</span><span>);</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三个细节：超时时长从系统设置表里读而不是写死在代码里，运营可以在后台调；查询条件是”状态 = UNPAID 且创建时间早于临界点”，扫描完逐单走统一的 <code>systemCancel</code> 流程（而不是直接改状态，保证取消副作用一致）；任务类实现 <code>EveryMinuteExecute</code> 接口，由 consumer 模块统一驱动。订单状态本身则收敛在 <code>OrderStatusEnum</code>：<code>UNPAID → PAID → UNDELIVERED → PARTS_DELIVERED → DELIVERED → COMPLETED</code>，外加待自提、待核验、已关闭，所有操作都翻译成状态流转。</p></section><section><h2>五、下单事件：事务提交后再发消息<a href="#五下单事件事务提交后再发消息"><span>#</span></a></h2><p>下单成功后的积分扣减、砍价收尾、消息通知这些周边动作，没有塞在下单事务里，而是走 RocketMQ 异步。这里有个容易踩坑的时序问题：<strong>如果事务还没提交就把消息发出去，消费者可能读到不存在的数据</strong>。</p><p><a href="https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/framework/src/main/java/cn/lili/modules/order/order/serviceimpl/TradeServiceImpl.java" target="_blank">TradeServiceImpl.java</a> 的处理分两步。先把 <code>TradeDTO</code> 整体写进缓存，MQ 消息体只携带一个 key：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//写入缓存，给消费者调用</span></div></div><div><div><div>2</div></div><div><span>cache</span><span>.</span><span>put</span><span>(key, </span><span>JSONUtil</span><span>.</span><span>toJsonStr</span><span>(tradeDTO));</span></div></div><div><div><div>3</div></div><div><span>applicationEventPublisher</span><span>.</span><span>publishEvent</span><span>(</span><span>new</span><span> </span><span>TransactionCommitSendMQEvent</span><span>(</span><span>"订单创建消息"</span><span>, </span><span>rocketmqCustomProperties</span><span>.</span><span>getOrderTopic</span><span>(),</span></div></div><div><div><div>4</div></div><div><span>        </span><span>OrderTagsEnum</span><span>.</span><span>ORDER_CREATE</span><span>.</span><span>name</span><span>(), key));</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>TransactionCommitSendMQEvent</code> 借助 Spring 事件机制，把真正的 MQ 发送推迟到事务提交之后——避免”消息先到、数据未落库”。消费者端 <a href="https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/consumer/src/main/java/cn/lili/listener/OrderMessageListener.java" target="_blank">OrderMessageListener.java</a> 再从缓存取回完整数据：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>case</span><span> ORDER_CREATE</span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>String</span><span> key </span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>String</span><span>(</span><span>messageExt</span><span>.</span><span>getBody</span><span><span>()</span><span>)</span><span>;</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>TradeDTO</span><span> tradeDTO </span><span><span>=</span><span> </span></span><span>JSONUtil</span><span>.</span><span>toBean</span><span>(</span><span>cache</span><span>.</span><span>getString</span><span>(key), </span><span>TradeDTO</span><span>.</span><span>class</span><span>);</span></div></div><div><div><div>4</div></div><div><span>    </span><span>boolean</span><span> result </span><span><span>=</span><span> </span></span><span>true</span><span>;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>for</span><span> (</span><span>TradeEvent</span><span> event </span><span>:</span><span> tradeEvent) {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>try</span><span> {</span></div></div><div><div><div>7</div></div><div><span>            </span><span>event</span><span>.</span><span>orderCreate</span><span>(tradeDTO);</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>} </span><span>catch</span><span> (</span><span>Exception</span><span><span> </span><span>e</span><span>) {</span></span></div></div><div><div><div>9</div></div><div><span>            </span><span>log</span><span>.</span><span>error</span><span>(</span><span>"交易{}入库,在{}业务中，状态修改事件执行异常"</span><span>, ...);</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>result </span><span>=</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>    </span><span>//如所有步骤顺利完成</span></div></div><div><div><div>14</div></div><div><span>    </span><span>if</span><span> (</span><span>Boolean</span><span>.</span><span>TRUE</span><span>.</span><span>equals</span><span><span>(result)</span><span>) {</span></span></div></div><div><div><div>15</div></div><div><span>        </span><span>//清除记录信息的trade cache key</span></div></div><div><div><div>16</div></div><div><span>        </span><span>cache</span><span>.</span><span>remove</span><span>(key);</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>    </span><span>break</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>注入的是 <code>List&lt;TradeEvent&gt;</code>——每个下游业务实现同一个事件接口，消费者遍历广播，谁都不依赖谁。任何一个业务失败就保留缓存 key 不删除，配合消息重试还能再跑一轮。订单状态变更（<code>STATUS_CHANGE</code>）同理广播给所有 <code>OrderStatusChangeEvent</code> 实现。</p></section><section><h2>六、商家端的数据隔离：每个查询都带着 storeId<a href="#六商家端的数据隔离每个查询都带着-storeid"><span>#</span></a></h2><p>多商户系统最敏感的问题是越权：A 商家能不能改 B 商家的订单？这套代码的答案是把”判断归属”做成了惯用法。商家端控制器 <a href="https://github.com/Melusine-ichnose/Multi-merchant-Mall-System/blob/main/seller-api/src/main/java/cn/lili/controller/order/OrderStoreController.java" target="_blank">OrderStoreController.java</a> 里，凡是按订单号操作的地方都先过一道 <code>OperationalJudgment.judgment</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>GetMapping</span><span>(</span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>"/{orderSn}"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>ResultMessage</span><span><span>&lt;</span><span>OrderDetailVO</span><span>&gt;</span><span> </span></span><span>detail</span><span><span>(</span><span>@</span></span><span>NotNull</span><span><span> </span><span>@</span></span><span>PathVariable</span><span> </span><span>String</span><span> orderSn) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>OperationalJudgment</span><span>.</span><span>judgment</span><span>(</span><span>orderService</span><span>.</span><span>getBySn</span><span>(orderSn));</span></div></div><div><div><div>4</div></div><div><span>    </span><span>return</span><span> </span><span>ResultUtil</span><span>.</span><span>data</span><span>(</span><span>orderService</span><span>.</span><span>queryDetail</span><span>(orderSn));</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>服务层内部则统一从 <code>UserContext</code> 取当前登录商家的店铺 ID 作为查询条件，比如核验自提订单：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>String</span><span> storeId </span><span><span>=</span><span> </span></span><span>Objects</span><span>.</span><span>requireNonNull</span><span>(</span><span>UserContext</span><span>.</span><span>getCurrentUser</span><span>()).</span><span>getStoreId</span><span>();</span></div></div><div><div><div>2</div></div><div><span>Order</span><span> order </span><span><span>=</span><span> </span></span><span>this</span><span>.</span><span>getOne</span><span>(</span><span>new</span><span> </span><span>LambdaQueryWrapper</span><span>&lt;</span><span>Order</span><span>&gt;()</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>.</span><span>in</span><span>(Order</span><span>::</span><span>getOrderStatus, </span><span>OrderStatusEnum</span><span>.</span><span>TAKE</span><span>.</span><span>name</span><span>(), </span><span>OrderStatusEnum</span><span>.</span><span>STAY_PICKED_UP</span><span>.</span><span>name</span><span>())</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>.</span><span>eq</span><span>(Order</span><span>::</span><span>getStoreId, storeId)</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>.</span><span>eq</span><span>(Order</span><span>::</span><span>getVerificationCode, verificationCode));</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>没有做复杂的行级权限框架，就是最朴素的原则：<strong>商家端所有查询都显式带 storeId 条件</strong>。防重复提交则统一挂在方法注解上（<code>@PreventDuplicateSubmissions</code>），发货、改价、取消这些写操作全都加了一遍。</p></section><section><h2>值得学习的地方<a href="#值得学习的地方"><span>#</span></a></h2><ol>
<li><strong>按角色拆 API 服务，业务下沉 framework。</strong> 多角色系统的天然切分线就是角色，物理隔离比一堆 if-else 判断身份可靠得多。</li>
<li><strong>Trade → Order 两级模型支撑拆单。</strong> 平台券/店铺券分层、订单冗余店铺信息、支付按交易、履约按订单，“多商户”不是一个字段，而是一整套建模。</li>
<li><strong>渲染流水线治理价格计算。</strong> 步骤即 Bean、顺序即数组，扩展营销玩法不改老代码；同时区分业务异常（中断）与系统异常（降级继续）。</li>
<li><strong>事务提交后再发消息 + 缓存中转大报文。</strong> 消息只传 key，时序正确性和消息体大小两个问题一起解决。</li>
<li><strong>数据隔离靠纪律而不是框架。</strong> 商家端每个查询显式带 storeId，归属校验做成 <code>OperationalJudgment</code> 这样的惯用法，简单但有效。</li>
</ol><p>作为一个学习样本，它把 B2B2C 商城会遇到的典型问题——拆单、价格叠加、异步履约、越权防护——都给出了一种可参考的工程解法，值得通读一遍。</p><p>项目地址：<a href="https://github.com/Melusine-ichnose/Multi-merchant-Mall-System" target="_blank">github.com/Melusine-ichnose/Multi-merchant-Mall-System</a></p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://blog-tyb.pages.dev/posts/seat-reservation-analysis/</id>
      <title type="text">源码阅读笔记：Seat Reservation System 的定时任务与状态机设计</title>
      <published>2026-09-24T00:00:00.000Z</published>
      <updated>2026-09-24T00:00:00.000Z</updated>
      <author><name>Melusine</name></author>
      <link rel="alternate" href="https://blog-tyb.pages.dev/posts/seat-reservation-analysis/"/>
      <summary type="text">分析自习室座位预约系统的核心设计：座位状态机流转、TimerTask 定时异常处理、信用分奖惩机制。</summary>
      <content type="html"><![CDATA[<section><h1>Seat Reservation System 源码阅读笔记<a href="#seat-reservation-system-源码阅读笔记"><span>#</span></a></h1><p><a href="https://github.com/Melusine-ichnose/Seat-Reservation-System" target="_blank">Seat Reservation System</a> 是一个前后端分离的自习室座位预约系统（Spring Boot 2.7 + Vue 2）。这篇文章记录我阅读它的源码时，认为最值得学习的三个设计。</p><section><h2>一、座位状态机：用数字流转约束业务规则<a href="#一座位状态机用数字流转约束业务规则"><span>#</span></a></h2><p>自习室座位的核心是一个状态机。<code>reservation</code> 表的 <code>status</code> 字段定义了完整流转：</p>

<table><thead><tr><th>状态值</th><th>含义</th><th>触发动作</th></tr></thead><tbody><tr><td>0</td><td>待签到</td><td>用户预约成功</td></tr><tr><td>1</td><td>使用中</td><td>到场扫码签到</td></tr><tr><td>2</td><td>未及时签到</td><td>超时自动判违规</td></tr><tr><td>3</td><td>暂离</td><td>用户主动暂离</td></tr><tr><td>4</td><td>暂离超时</td><td>定时任务检测</td></tr><tr><td>-1</td><td>完成</td><td>使用结束，释放座位</td></tr></tbody></table><p>这个设计的价值在于：<strong>所有业务操作最终都收敛为状态变更</strong>。</p><ul>
<li>用户签到 → <code>0 → 1</code></li>
<li>用户暂离 → <code>1 → 3</code></li>
<li>用户回来 → <code>3 → 1</code></li>
<li>暂离超时 → <code>3 → 4</code></li>
<li>使用结束 → <code>1 → -1</code></li>
</ul><p>每个状态变更都绑定一个副作用（释放座位 / 扣信用分 / 记违规），代码路径非常清晰。</p></section><section><h2>二、TimerTask：异常场景的自动兜底<a href="#二timertask异常场景的自动兜底"><span>#</span></a></h2><p>系统最复杂的地方不是正常流程，而是<strong>异常场景</strong>。预约了不来怎么办？暂离了一直不回来怎么办？</p><p>项目的答案是 <code>TimerTask</code> 定时任务：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 伪代码示意：预约超时未签到自动判违规</span></div></div><div><div><div>2</div></div><div><span>TimerTask</span><span> checkSignTimeout </span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>TimerTask</span><span>() {</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>@</span><span>Override</span></div></div><div><div><div>4</div></div><div><span>    </span><span>public</span><span> </span><span>void</span><span> </span><span>run</span><span><span>()</span><span> </span><span>{</span></span></div></div><div><div><div>5</div></div><div><span>        </span><span>// 找到所有 status=0 且超过签到时间的预约</span></div></div><div><div><div>6</div></div><div><span>        </span><span>List</span><span>&lt;</span><span>Reservation</span><span><span>&gt; </span><span>timeoutList</span><span> </span></span><span><span>=</span><span> reservationMapper</span></span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>.</span><span>findTimeoutReservations</span><span>(</span><span>now</span><span>());</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>        </span><span>for</span><span> (</span><span>Reservation</span><span><span> </span><span>r</span><span> </span></span><span>:</span><span> timeoutList) {</span></div></div><div><div><div>10</div></div><div><span>            </span><span>r</span><span>.</span><span>setStatus</span><span>(</span><span>2</span><span>); </span><span>// 未及时签到</span></div></div><div><div><div>11</div></div><div><span>            </span><span>seatService</span><span>.</span><span>release</span><span>(</span><span>r</span><span>.</span><span>getSeatId</span><span>());</span></div></div><div><div><div>12</div></div><div><span>            </span><span>creditService</span><span>.</span><span>deduct</span><span>(</span><span>r</span><span>.</span><span>getUserId</span><span>(), </span><span>10</span><span>); </span><span>// 扣信用分</span></div></div><div><div><div>13</div></div><div><span>            </span><span>violationService</span><span>.</span><span>record</span><span>(</span><span>r</span><span>.</span><span>getId</span><span>(), </span><span>"超时未签到"</span><span>);</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span><span>}</span><span>;</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>定时任务每 N 分钟扫一次，把「应该发生但用户没做」的事情自动补掉。这是典型的<strong>最终一致性</strong>思路——不依赖用户操作，系统自己兜底。</p></section><section><h2>三、信用分：约束行为的奖惩机制<a href="#三信用分约束行为的奖惩机制"><span>#</span></a></h2><p>信用分是这个系统里很巧妙的约束设计：</p><ul>
<li>初始 100 分</li>
<li>超时未签到 → 扣分</li>
<li>暂离超时 → 扣分</li>
<li>教师可手动加/扣分</li>
<li>信用分过低 → 限制预约权限</li>
</ul><p>它不是简单的惩罚，而是<strong>用分数门槛间接控制资源分配</strong>。分数低的人自动失去预约资格，不需要管理员手动拉黑。</p></section><section><h2>前端：双 UI 库的组合<a href="#前端双-ui-库的组合"><span>#</span></a></h2><p>这个项目的前端架构也值得注意：</p><ul>
<li><strong>PC 端管理界面</strong> → Element UI 2.15（给管理员/教师用）</li>
<li><strong>移动端 H5</strong> → Vant 2.12（给学生用）</li>
</ul><p>同一套后端 API，根据设备类型加载不同的 UI 库。Axios 统一封装，通过 <code>devServer.proxy</code> 把 <code>/api</code> 代理到后端 9003 端口。</p></section><section><h2>总结<a href="#总结"><span>#</span></a></h2><p>这个项目的复杂度不高，但<strong>异常处理的设计意识</strong>很到位。定时任务兜底、状态机约束流转、信用分约束行为，这三板斧在很多业务系统里都适用。</p><p>项目地址：<a href="https://github.com/Melusine-ichnose/Seat-Reservation-System" target="_blank">github.com/Melusine-ichnose/Seat-Reservation-System</a></p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://blog-tyb.pages.dev/posts/taskweaver-source-analysis/</id>
      <title type="text">TaskWeaver 源码解析：LangGraph 四节点循环 Agent 架构</title>
      <published>2026-09-24T00:00:00.000Z</published>
      <updated>2026-09-24T00:00:00.000Z</updated>
      <author><name>Melusine</name></author>
      <link rel="alternate" href="https://blog-tyb.pages.dev/posts/taskweaver-source-analysis/"/>
      <summary type="text">深入 TaskWeaver 源码，拆解 Planner-Executor-Reviewer-Responder 四节点循环图的实现，以及它如何用双计数器防止 Agent 死循环。</summary>
      <content type="html"><![CDATA[<section><h1>TaskWeaver 源码解析：LangGraph 四节点循环 Agent 架构<a href="#taskweaver-源码解析langgraph-四节点循环-agent-架构"><span>#</span></a></h1><p><a href="https://github.com/Melusine-ichnose/TaskWeaver" target="_blank">TaskWeaver</a> 是一个基于 LangChain + LangGraph + MCP 协议 + FastAPI 的生产级 AI Agent 服务。这篇文章带你读它的核心源码，看看一个可落地的 Agent 循环是怎么搭起来的。</p><section><h2>整体架构：一张循环图<a href="#整体架构一张循环图"><span>#</span></a></h2><p>项目的灵魂在 <code>app/agent/graph.py</code>，整个 Agent 就是一张 LangGraph 状态图：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>START → planner → executor → reviewer ┐</span></div></div><div><div><div>2</div></div><div><span><span>                      </span></span><span>▲               │</span></div></div><div><div><div>3</div></div><div><span><span>                      </span></span><span>└── not done ───┤</span></div></div><div><div><div>4</div></div><div><span><span>                                      </span></span><span>│</span></div></div><div><div><div>5</div></div><div><span><span>                      </span></span><span>done → responder → END</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>对应源码（来自 <a href="https://github.com/Melusine-ichnose/TaskWeaver/blob/main/app/agent/graph.py" target="_blank">graph.py</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>graph </span><span>=</span><span> </span><span>StateGraph</span><span>(AgentState)</span></span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>graph.</span><span>add_node</span><span>(</span></span><span>"planner"</span><span>, planner_node)</span></div></div><div><div><div>4</div></div><div><span><span>graph.</span><span>add_node</span><span>(</span></span><span>"executor"</span><span>, executor_node)</span></div></div><div><div><div>5</div></div><div><span><span>graph.</span><span>add_node</span><span>(</span></span><span>"reviewer"</span><span>, reviewer_node)</span></div></div><div><div><div>6</div></div><div><span><span>graph.</span><span>add_node</span><span>(</span></span><span>"responder"</span><span>, responder_node)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span><span>graph.</span><span>add_edge</span><span>(</span><span>START</span><span>, </span></span><span>"planner"</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>graph.</span><span>add_edge</span><span>(</span></span><span>"planner"</span><span>, </span><span>"executor"</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>graph.</span><span>add_edge</span><span>(</span></span><span>"executor"</span><span>, </span><span>"reviewer"</span><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span># 条件边：reviewer 决定继续执行还是收尾</span></div></div><div><div><div>13</div></div><div><span><span>graph.</span><span>add_conditional_edges</span><span>(</span></span></div></div><div><div><div>14</div></div><div><span>    </span><span>"reviewer"</span><span>,</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>_route_after_review,</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>{</span><span>"executor"</span><span>: </span><span>"executor"</span><span>, </span><span>"responder"</span><span>: </span><span>"responder"</span><span>},</span></div></div><div><div><div>17</div></div><div><span>)</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span><span>graph.</span><span>add_edge</span><span>(</span></span><span>"responder"</span><span><span>, </span><span>END</span><span>)</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>四个节点的职责划分非常清晰：</p>

<table><thead><tr><th>节点</th><th>职责</th><th>类比</th></tr></thead><tbody><tr><td>Planner</td><td>把用户问题拆解成步骤清单</td><td>项目经理排计划</td></tr><tr><td>Executor</td><td>执行当前步骤，调工具或直接产出</td><td>干活的工程师</td></tr><tr><td>Reviewer</td><td>评审当前步骤是否完成</td><td>质检/Code Review</td></tr><tr><td>Responder</td><td>汇总所有步骤产出，生成最终回答</td><td>写交付报告</td></tr></tbody></table></section><section><h2>状态设计：AgentState<a href="#状态设计agentstate"><span>#</span></a></h2><p>LangGraph 的核心思想是<strong>节点之间不直接传参，而是读写共享状态</strong>。<code>app/agent/state.py</code> 定义了这个共享状态：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span><span> </span><span>AgentState</span><span>(</span><span>TypedDict</span><span>)</span></span><span>:</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>messages: Annotated[list[BaseMessage], add_messages]</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>user_query: </span><span>str</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>plan: list[</span><span>str</span><span>]</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>current_step_index: </span><span>int</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>step_results: list[</span><span>str</span><span>]</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>is_complete: </span><span>bool</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>final_response: </span><span>str</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>review_iterations: </span><span>int</span><span>  </span><span># 防死循环计数器</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个值得注意的设计：</p><ol>
<li><strong><code>messages</code> 用了 <code>add_messages</code> reducer</strong>：LangGraph 默认对状态字段是覆盖更新，但 <code>messages</code> 声明了 reducer 后，每个节点返回的新消息会<strong>自动追加合并</strong>而不是覆盖，对话历史就这样自然累积起来了。</li>
<li><strong><code>review_iterations</code> 显式计数</strong>：这是防止 Agent 死循环的关键，后面详细说。</li>
</ol></section><section><h2>Reviewer：最值得学的节点<a href="#reviewer最值得学的节点"><span>#</span></a></h2><p><code>app/agent/nodes/reviewer.py</code> 是整个项目里工程思考最密集的部分。它要回答一个问题：<strong>当前步骤算完成了吗？</strong></p><section><h3>三层防护，防死循环烧 Token<a href="#三层防护防死循环烧-token"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>MAX_REVIEW_ITERATIONS</span><span> </span><span>=</span><span> </span></span><span>12</span><span>  </span><span># 单次图执行最大评审次数</span></div></div><div><div><div>2</div></div><div><span><span>MAX_STEP_RETRIES</span><span> </span><span>=</span><span> </span></span><span>2</span><span>        </span><span># 单步最大重试次数</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>第一层：总评审次数硬上限。</strong> 达到 12 次直接强制收尾，而且这个判断<strong>先于 LLM 调用</strong>，不消耗任何 token：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>if</span><span><span> iterations </span><span>&gt;=</span><span> </span><span>MAX_REVIEW_ITERATIONS</span><span>:</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>return</span><span> {</span><span>"review_iterations"</span><span>: iterations, </span><span>"is_complete"</span><span>: </span><span>True</span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>第二层：单步重试上限。</strong> 同一步骤连续 2 次评审不过，强制推进到下一步——宁可带着不完美的结果往前走，也不在一个步骤上空转：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>step_retries </span><span>=</span><span> state.</span><span>get</span><span>(</span></span><span>"step_retry_count"</span><span>, </span><span>0</span><span><span>) </span><span>+</span><span> </span></span><span>1</span></div></div><div><div><div>2</div></div><div><span>if</span><span><span> step_retries </span><span>&gt;=</span><span> </span><span>MAX_STEP_RETRIES</span><span>:</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span># 强制推进</span></div></div><div><div><div>4</div></div><div><span>    </span><span>return</span><span> {</span><span>"current_step_index"</span><span><span>: next_index, </span><span>...</span><span>}</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>第三层：LLM 输出兜底的「安全侧」判定。</strong> Reviewer 让 LLM 输出 DONE/CONTINUE，但 LLM 的输出你永远不能完全信任。这里的取舍是：<strong>无法判定时默认完成</strong>——</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>if</span><span> has_done:</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>is_step_done </span><span>=</span><span> </span><span>True</span></div></div><div><div><div>3</div></div><div><span>elif</span><span> has_continue:</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>is_step_done </span><span>=</span><span> </span><span>False</span></div></div><div><div><div>5</div></div><div><span>else</span><span>:</span></div></div><div><div><div>6</div></div><div><span>    </span><span># 无法识别时默认完成（安全侧，防死循环烧 token）</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>is_step_done </span><span>=</span><span> </span><span>True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>为什么默认完成而不是默认继续？因为「继续」意味着再跑一轮 Executor → 再调一次 LLM → 再烧一轮 token，而且可能永远不满足。默认完成是<strong>成本安全侧</strong>的选择。这是做 Agent 系统和写普通业务代码思路差别最大的地方：你要时刻假设 LLM 会给出无法解析的输出。</p></section><section><h3>单步任务快速通道：省掉一次 LLM 调用<a href="#单步任务快速通道省掉一次-llm-调用"><span>#</span></a></h3><p>对于单步任务（<code>len(plan) == 1</code>），reviewer 先跑一个<strong>不调 LLM</strong> 的本地判定：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>_autocomplete_single_step</span><span>(</span><span>messages</span><span>:</span><span> </span><span>list</span><span>) -&gt; </span><span>bool</span><span><span> </span><span>|</span><span> </span></span><span>None</span><span>:</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>last </span><span>=</span><span> messages[</span><span>-</span><span>1</span><span>]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>if</span><span> </span><span>isinstance</span><span>(last, ToolMessage):</span></div></div><div><div><div>4</div></div><div><span>        </span><span>if</span><span><span> content.</span><span>startswith</span><span>(</span></span><span>"错误"</span><span>):</span></div></div><div><div><div>5</div></div><div><span>            </span><span>return</span><span> </span><span>None</span><span>   </span><span># 工具失败，交给 LLM 评审</span></div></div><div><div><div>6</div></div><div><span>        </span><span>return</span><span> </span><span>True</span><span>        </span><span># 工具成功 → 完成</span></div></div><div><div><div>7</div></div><div><span>    </span><span>if</span><span> </span><span>isinstance</span><span>(last, AIMessage):</span></div></div><div><div><div>8</div></div><div><span>        </span><span>if</span><span> last.content </span><span>and</span><span> </span><span>not</span><span> last.tool_calls:</span></div></div><div><div><div>9</div></div><div><span>            </span><span>return</span><span> </span><span>True</span><span>    </span><span># Executor 已直接产出答案 → 完成</span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> </span><span>None</span><span>            </span><span># 不确定 → 走 LLM 评审</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>逻辑很朴素：Executor 刚跑完，如果消息序列末尾是一个成功的工具结果或一段正常的 AI 文本，这一步就完成了，没必要再问一次 LLM「你觉得完成了吗」。就这么一个判断，把简单请求的 LLM 调用次数砍掉了一大半。</p></section></section><section><h2>Executor：工具调用的经典模式<a href="#executor工具调用的经典模式"><span>#</span></a></h2><p><code>executor_node</code> 展示了 LangChain 体系下标准的工具调用循环：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>response: AIMessage </span><span>=</span><span> </span></span><span>await</span><span><span> llm.</span><span>ainvoke</span><span>(messages)</span></span></div></div><div><div><div>2</div></div><div><span><span>tool_calls </span><span>=</span><span> </span></span><span>getattr</span><span>(response, </span><span>"tool_calls"</span><span>, </span><span>None</span><span>) </span><span>or</span><span> []</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>for</span><span> tool_call </span><span>in</span><span> tool_calls:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>tool </span><span>=</span><span> </span><span>next</span><span>((t </span><span>for</span><span> t </span><span>in</span><span> tools </span><span>if</span><span><span> t.name </span><span>==</span><span> tool_call[</span></span><span>"name"</span><span>]), </span><span>None</span><span>)</span></div></div><div><div><div>6</div></div><div><span>    </span><span>if</span><span> tool </span><span>is</span><span> </span><span>None</span><span>:</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>content </span><span>=</span><span> </span><span>f</span><span>"错误：未找到工具 '</span><span>{</span><span>tool_call[</span><span>'name'</span><span>]</span><span>}</span><span>'"</span></div></div><div><div><div>8</div></div><div><span>    </span><span>else</span><span>:</span></div></div><div><div><div>9</div></div><div><span>        </span><span>try</span><span>:</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>result </span><span>=</span><span> </span><span>await</span><span><span> tool.</span><span>ainvoke</span><span>(tool_call[</span></span><span>"args"</span><span>])</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>content </span><span>=</span><span> </span><span>str</span><span>(result)</span></div></div><div><div><div>12</div></div><div><span>        </span><span>except</span><span> </span><span>Exception</span><span> </span><span>as</span><span> e:</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>content </span><span>=</span><span> </span><span>f</span><span>"工具执行错误: </span><span>{</span><span>e</span><span>}</span><span>"</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>new_messages.</span><span>append</span><span>(</span><span>ToolMessage</span><span>(</span><span>content</span><span><span>=</span><span>content, </span><span>...</span><span>))</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>要点：<strong>工具的异常从不向上抛，而是包成错误文本写进 ToolMessage</strong>。这样 LLM 下一轮能「看到」工具失败了，有机会自己换参数重试或换个工具——把错误也变成 LLM 可利用的上下文。</p></section><section><h2>学到的三件事<a href="#学到的三件事"><span>#</span></a></h2><ol>
<li><strong>Agent 的可靠性来自工程防护，不是 Prompt。</strong> 双计数器 + 兜底判定这些「无聊」的代码，才是生产环境和玩具 Demo 的分水岭。</li>
<li><strong>把 LLM 放在路径的关键决策点上，其他地方尽量不调。</strong> 单步快速通道省的是真金白银的 token 和延迟。</li>
<li><strong>状态图（StateGraph）天然适合表达 Agent 循环。</strong> 比起手写 while 循环，图结构让「评审不过就回到 Executor」这种回边表达得直白且可视化（配合 LangSmith 能直接看到每次循环的轨迹）。</li>
</ol><p>项目地址：<a href="https://github.com/Melusine-ichnose/TaskWeaver" target="_blank">github.com/Melusine-ichnose/TaskWeaver</a></p></section></section>]]></content>
    </entry>
</feed>
