<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>https://physics-agent-lab.pages.dev/</id>
  <title type="text">Physics &amp; Agent Lab</title>
  <subtitle type="text">记录 AI 开发、桌游实践与物理思考</subtitle>
  <updated>2026-10-17T00:00:00.000Z</updated>
  <author><name>Stack</name></author>
  <link rel="alternate" href="https://physics-agent-lab.pages.dev/"/>
  <link rel="self" href="https://physics-agent-lab.pages.dev/atom.xml"/>
  <generator uri="https://github.com/CuteLeaf/Firefly">Firefly v6.16.8</generator>
    <entry>
      <id>https://physics-agent-lab.pages.dev/posts/module-boundaries/</id>
      <title type="text">用 AI 写项目，先把模块边界划清楚</title>
      <published>2026-10-17T00:00:00.000Z</published>
      <updated>2026-10-17T00:00:00.000Z</updated>
      <author><name>Stack</name></author>
      <link rel="alternate" href="https://physics-agent-lab.pages.dev/posts/module-boundaries/"/>
      <summary type="text">从一个简化的棋类网页出发，谈模块职责、接口契约和多 Agent 协作：怎样让功能能跑，也让后续修改有迹可循。</summary>
      <content type="html"><![CDATA[<section><h2>从“加个悔棋”说起<a href="#从加个悔棋说起"><span>#</span></a></h2><p>假设我们正在做一个棋类网页，落子、回合切换和 AI 对手都能运行。现在想加一个悔棋按钮，却发现点击事件里既有合法性判断，又有动画、历史记录和 AI 调用。撤回一步之后，该撤销哪些状态、已经开始思考的 AI 怎么处理，都得沿着代码重新找一遍。</p><p>下面就用这个简化场景讨论模块边界。它是说明性的例子，不对应某个已上线项目的实现。</p><p>用 AI 写功能时，很容易把注意力放在眼前的效果上：按钮能点，页面能打开，任务就像完成了。等到第二次、第三次修改，代码之间的依赖才逐渐显出来。为了少走到“哪里都不敢动”的地步，值得在开工前先写清每部分的职责。</p></section>
<section><h2>按职责和变化原因划分<a href="#按职责和变化原因划分"><span>#</span></a></h2><p>给文件夹起名叫 <code>core</code>、<code>ui</code>、<code>utils</code>，还不足以说明边界。更有用的问题是：它负责什么，谁管理状态，对外提供什么能力？</p><p>调整棋子的移动动画，主要应该影响界面；修改合法落子的条件，应能找到统一的规则入口；更换 AI 的选步方法，也应能定位到决策部分。如果每次都要在几个角落各改一遍同样的判断，就需要检查职责是否散开了。</p><p>这里借鉴了 Martin Fowler 对 <a href="https://martinfowler.com/bliki/BoundedContext.html" target="_blank">Bounded Context（限界上下文）</a> 的介绍：模型内部的概念要一致，不同上下文之间的关系要明确。它讨论的尺度比一个小组件大，不能直接等同于“多拆几个文件”。对个人项目而言，可以先在同一个应用里落实这些约定，不必一上来就拆微服务。</p><p>规则调整也可能影响 AI 或界面。合理的边界应让影响路径容易判断，并不要求每个模块永远互不影响。</p></section>
<section><h2>先写一张职责表<a href="#先写一张职责表"><span>#</span></a></h2><p>对于这个棋类例子，可以先尝试下面的划分：</p>

<table><thead><tr><th>模块</th><th>负责什么</th><th>如何与其他部分协作</th></tr></thead><tbody><tr><td>规则</td><td>检查动作，计算落子后的局面及结束条件</td><td>接收局面和动作，返回处理结果</td></tr><tr><td>对局流程</td><td>保存当前局面，安排回合操作，管理历史</td><td>调用规则，把新状态交给界面</td></tr><tr><td>AI 决策</td><td>根据局面选择候选动作</td><td>提交动作，仍经过规则校验</td></tr><tr><td>界面</td><td>显示棋盘，播放动画，接收操作</td><td>发出操作意图，按状态更新显示</td></tr></tbody></table><p>这张表需要随玩法调整。它先解决一个很实际的问题：当前局面由对局流程保存，规则负责计算新局面，界面不应在动画结束时顺手改掉轮到谁。</p><p>AI 也一样。它可以采用不同的选步算法，但不能因为是“程序自己走的棋”，就绕过普通玩家使用的合法性检查。以后换算法时，调用方仍可以沿用约定好的动作入口。</p></section>
<section><h2>接口要写到失败时怎么办<a href="#接口要写到失败时怎么办"><span>#</span></a></h2><p>我在整理开发流程时，会把契约先行作为一个约束：先说清模块之间怎么交接，再让 AI 实现各自的内部逻辑。这里的契约可以很短，例如：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>applyMove(state, move)</span></div></div><div><div><div>2</div></div><div><span>合法：返回新局面 nextState</span></div></div><div><div><div>3</div></div><div><span>非法：返回拒绝原因 reason</span></div></div><div><div><div>4</div></div><div><span>两种情况都不修改传入的 state</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这只是接口示意。除了参数名字，还要约定“合法”由谁判断、回合变化在哪里计算、调用失败时旧局面是否保留。比如规定规则模块在 <code>nextState</code> 中计算下一回合，对局流程保存结果，就能避免两边各自推进一次。</p><p>如果只写“返回一个局面”，实现者很可能作出不同理解：一个模块原地修改对象，另一个却还把它当作历史快照。历史记录也需要稳定的快照或不可变数据，不能只是把同一个可变对象反复塞进数组。</p><p>遇到悔棋这样的新功能，还要补充流程约定：恢复到哪一步，如何处理正在执行的 AI 任务，旧任务返回后是否允许落子。这些问题适合在接口讨论时暴露出来。</p></section>
<section><h2>给 Agent 一张范围清楚的任务卡<a href="#给-agent-一张范围清楚的任务卡"><span>#</span></a></h2><p>多开几个 Agent，并不会自动得到低耦合的代码。大家如果各自理解一遍需求、各自发明一套数据结构，最后仍得重新拼接。</p><p>可以把“完善棋盘逻辑”改写成下面这样的任务：</p><blockquote><p>目标：补充非法落子的处理。允许修改规则模块及其测试；沿用现有局面和动作类型。暂不调整棋盘布局或 AI 策略。验收时检查越界、占用位置、非当前回合操作；被拒绝后，局面和回合都保持不变。</p></blockquote><p>任务卡让实现者和审查者对“完成”有共同理解。如果过程中发现共享类型不够用，应先列出需要调整的接口及调用方，再统一修改，避免在某个模块里悄悄追加一套兼容逻辑。</p><p>Google 的 <a href="https://google.github.io/eng-practices/review/developer/small-cls.html" target="_blank">Small CLs</a> 建议也适合放在这里：把修改组织成范围集中、能够独立理解的一次改动，并带上相关测试。小步修改不只是限制行数；新增接口时，合理的调用示例也应一起出现，让审查者知道它怎么用。</p><p>若需要大幅移动文件或重命名模块，尽量与新增功能分开处理，否则很难看出行为到底在哪里发生了变化。</p></section>
<section><h2>用行为检查边界是否有效<a href="#用行为检查边界是否有效"><span>#</span></a></h2><p>职责表写完，还需要实际检查。对于这个例子，我会先关注这些行为：</p><ul>
<li>非法动作被拒绝后，棋盘和当前回合都不变。</li>
<li>合法动作只应用一次，回合不会被重复推进。</li>
<li>AI 提交的动作也要经过规则校验。</li>
<li>纯界面改动不会影响规则计算的结果。</li>
</ul><p>这些检查不能证明架构已经完美，但能帮助发现哪些约定没有被遵守。规则可以单独测试，再用集成检查验证界面、流程与规则连在一起时的行为。只测某个函数返回正确，还不足以证明点击按钮后没有重复提交。</p><p>边界也需要调整。如果两个模块长期互相读取内部数据，或同一条业务规则总在几处同时修改，就值得重新看职责划分。反过来，一个简单动作若要经过许多只做转发的模块，继续细拆也可能只是在增加理解成本。</p><p>下一次让 AI 加功能前，我会先写下：这次改哪里，依赖哪份接口，怎样检查完成。先让这几项约定在一个小功能里跑通，再根据实际修改中遇到的问题调整结构。</p></section>
<section><h2>参考资料<a href="#参考资料"><span>#</span></a></h2><ol>
<li>Martin Fowler：<a href="https://martinfowler.com/bliki/BoundedContext.html" target="_blank">Bounded Context</a>。用于理解概念边界与上下文之间的关系。</li>
<li>Google Engineering Practices：<a href="https://google.github.io/eng-practices/review/developer/small-cls.html" target="_blank">Small CLs</a>。用于理解改动范围、审查与相关测试的组织方式。</li>
</ol></section>]]></content>
    </entry>
</feed>
