<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Coding Agent on 老朱的赛博工位</title><link>https://blog.zjkun.top/tags/coding-agent/</link><description>Recent content in Coding Agent on 老朱的赛博工位</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Mon, 19 Oct 2026 12:30:00 +0800</lastBuildDate><atom:link href="https://blog.zjkun.top/tags/coding-agent/index.xml" rel="self" type="application/rss+xml"/><item><title>AI 写代码自称做完？断言 4 步拆穿假完成</title><link>https://blog.zjkun.top/posts/ai-code-assertion-4-steps/</link><pubDate>Mon, 19 Oct 2026 12:30:00 +0800</pubDate><guid>https://blog.zjkun.top/posts/ai-code-assertion-4-steps/</guid><description>晚上 11 点半 AI 丢下一句「测试全过」你就睡了，早上打开页面按钮根本没反应。假完成的根源是断言写得太松。4 步设计法：用例五件事、红灯要坏得对、逻辑用替身逐字段断言、效果真模型多跑几轮。</description><content:encoded><![CDATA[<p>晚上 11 点半，AI 丢下一句「测试全过 ✅」，你关了电脑睡觉；早上打开页面，新加的按钮根本没反应。</p>
<p>这种「假完成」我上周遇到三次——AI 一口气报 5 次「全过」，3 次是假的，全是旧数据被覆盖这类无声坑。它是真觉得自己完成了，因为它的测试压根抓不住。问题不出在测试数量，出在<strong>断言设计</strong>。</p>
<p>这是 AI 编程避坑系列第 2 篇，承接<a href="/posts/tame-coding-agent-5-steps/">《5 步驯服 Coding Agent》</a>：那篇讲流程，这篇讲第三步「验收检查怎么写」里最容易翻车的一环。</p>
<h2 id="先掰词">先掰词</h2>
<p><strong>断言 = 测试里写死的一句「什么才算对」。</strong> 像验货对着清单逐项打勾，不是快递说「到了」就算到。断言只写「成功了没」，没写「它把旧数据覆盖了没」，测试全绿也拦不住假完成。</p>
<h2 id="断言设计-4-步">断言设计 4 步</h2>
<h3 id="第-1-步预期和不应发生都要写">第 1 步：预期和「不应发生」都要写</h3>
<p>一条完整用例 = 前置条件 + 输入操作 + 预期结果 + <strong>不该出现什么</strong> + 怎么检查。</p>
<p>拿「导入 2 条新消息」举例：断言总数加 2 之外，还必须断言<strong>旧记录的原文、发布者一个字没变</strong>。关键字段逐个比，别只数条数。我练习题里的真实写法：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">import_messages</span>(existing: list[dict], batch: list[dict]) <span style="color:#f92672">-&gt;</span> list[dict]:
</span></span><span style="display:flex;"><span>    seen <span style="color:#f92672">=</span> {(m[<span style="color:#e6db74">&#34;room_id&#34;</span>], m[<span style="color:#e6db74">&#34;msg_id&#34;</span>]) <span style="color:#66d9ef">for</span> m <span style="color:#f92672">in</span> existing}
</span></span><span style="display:flex;"><span>    result <span style="color:#f92672">=</span> list(existing)          <span style="color:#75715e"># 不改动已有记录（原文/发送者/采集时间保留）</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> m <span style="color:#f92672">in</span> batch:
</span></span><span style="display:flex;"><span>        key <span style="color:#f92672">=</span> (m[<span style="color:#e6db74">&#34;room_id&#34;</span>], m[<span style="color:#e6db74">&#34;msg_id&#34;</span>])
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> key <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> seen:          <span style="color:#75715e"># 身份 = 房间 + 消息ID，不是文本</span>
</span></span><span style="display:flex;"><span>            seen<span style="color:#f92672">.</span>add(key)
</span></span><span style="display:flex;"><span>            result<span style="color:#f92672">.</span>append(dict(m))   <span style="color:#75715e"># 拷贝存入，避免外部对象后续被改</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> result
</span></span></code></pre></div><p>配套断言不能只写 <code>len(result) == 2</code>，必须逐字段比对已存在的 M01 原文未被改写——只检查「成功」，就永远发现不了「它把错误内容覆盖进去了」。</p>
<h3 id="第-2-步红灯要坏得对">第 2 步：红灯要坏得对</h3>
<p>TDD 先红再绿，但<strong>红的失败原因必须是「目标行为还没实现」</strong>。缺依赖、模块导不进来，那是考场坏了，不算验收——先修运行条件，这条测试还没开始检查业务规则。</p>
<p>测试第一次就直接绿：别故意改坏代码造假红灯。要么这个行为早已实现（保留测试即可），要么你的断言漏了关键结果。后者更常见。</p>
<h3 id="第-3-步查逻辑固定假数据--模型替身">第 3 步：查逻辑——固定假数据 + 模型替身</h3>
<p>去重、筛选、引用越界这类题有标准答案，像数学题。喂固定假数据，把模型换成替身（它自己记录被调用几次），逐字段断言。数数是程序干的活，不归模型：</p>
<ul>
<li>快照为空 → 断言 <code>call_count == 0</code>，模型一次都不许被调</li>
<li>引用越界 → 断言被拒，返回越界的 ID 清单</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">validate_answer</span>(answer: dict, input_ids: set[str]) <span style="color:#f92672">-&gt;</span> tuple[bool, list[str]]:
</span></span><span style="display:flex;"><span>    bad <span style="color:#f92672">=</span> [i <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> answer[<span style="color:#e6db74">&#34;cited_ids&#34;</span>] <span style="color:#66d9ef">if</span> i <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> input_ids]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> (len(bad) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>, bad)
</span></span></code></pre></div><p>替身的价值是<strong>稳定制造</strong>「没调用 / 乱调用」这些边界，真模型反而演不出来。</p>
<h3 id="第-4-步看效果真模型多跑几轮">第 4 步：看效果——真模型多跑几轮</h3>
<p>摘要准不准、有没有编——程序能校验「引用没越界」，校验不了「引用真的支撑了回答」。这类必须真实调用 + 多样本 + 语义标准：<strong>允许措辞变，固定住「意思必须保留」</strong>。比如「爬长城很累吗」和「爬到一半能返回吗」被概括时，两条的差异有没有被揉掉——这是语义检查，替身做不了。</p>
<p>一次通过不等于稳定，同一条至少跑几轮看一致性。</p>
<h2 id="速查表">速查表</h2>
<ol>
<li>用例五件事：前置｜输入｜预期｜<strong>不应发生</strong>｜检查方法</li>
<li>关键字段逐个比，不只数条数</li>
<li>红灯 = 目标行为没实现，不是考场坏了；第一次就绿先查断言漏没漏</li>
<li>逻辑用替身逐字段断言，效果用真模型多样本多跑几轮</li>
</ol>
<h2 id="验收时多看一眼">验收时多看一眼</h2>
<p>AI 报告「测试全部通过」时，去它的汇报里找两行：真实模型调用过没有？页面实际操作过没有？我那 3 次假完成，两次都是单元测试全绿、但「测试全过」覆盖的只是程序逻辑那一半——模型效果和用户流程根本没被执行过。</p>
<p>一句话收束：<strong>AI 写代码算不算做完，不能它嘴上说了算，断言说了算。</strong></p>
<blockquote>
<p>本篇最早于 2026 年 10 月 19 日发布于小红书，此为博客完整版。</p>
</blockquote>
]]></content:encoded></item><item><title>AI 写代码跑偏？5 步驯服 Coding Agent</title><link>https://blog.zjkun.top/posts/tame-coding-agent-5-steps/</link><pubDate>Fri, 16 Oct 2026 12:30:00 +0800</pubDate><guid>https://blog.zjkun.top/posts/tame-coding-agent-5-steps/</guid><description>一句话生成 App 看着爽，真实项目里全靠模型猜。SpecDD 定义驱动 + TDD 测试驱动的 5 步工作流：画图纸、盯脑补、红绿重构、两种验证。附我做的 5 道代码题真实片段。</description><content:encoded><![CDATA[<p>让 AI 写代码最爽的一刻是 prompt 下去、代码出来的那 30 秒；最崩的一刻是打开页面发现它「做完了」但根本不是你要的。</p>
<p>我上周把 Vibe Coding 第一课（莫欣）的讲义啃完，加了 5 道代码题实操，把 SpecDD + TDD 这套流程整理成 5 步。核心就一句话：<strong>AI 只能「应试」，不能「自证」</strong>——它写没写完、写对没写对，必须有一个先于代码存在的客观检查说了算。</p>
<h2 id="先把两个词掰开">先把两个词掰开</h2>
<p><strong>SpecDD（Spec-Driven Development，定义驱动开发）</strong>：动手之前，先和 AI 把「什么算做对了」写成一份规格文件——场景、输入输出、关键行为、异常处理、验收方式，再加一条「本次不做什么」。检验标准很朴素：<strong>照着这份 Spec 干活，还有没有要猜的地方。</strong> 有，就还没到开工的时候。</p>
<p><strong>TDD（Test-Driven Development，测试驱动开发）</strong>：代码还没写，先写一个小测试表达 Spec 里的预期行为。这时功能未实现，测试必然失败（红）→ 写最小代码让它通过（绿）→ 整理代码不改行为（重构）。AI 场景里，这个测试就是防止 Agent「自称做完」的验收单。</p>
<p>关系一句话说清：SpecDD 定义验收标准，TDD 把标准变成先于代码存在的检查。AI 只能「应试」不能「自证」。</p>
<h2 id="第-1-步先画图纸再开工">第 1 步：先画图纸再开工</h2>
<p>让 AI 写码前，先写个文件讲清五件事：<strong>做什么给谁用｜输入什么输出什么｜正常该怎么表现｜出了问题怎么办｜最后怎么验收</strong>。再补一句「本次不做什么」：不自动回复、不管理后台。</p>
<p>边界清单比愿望清单好控得多。「做到什么程度算过」没有定义时，AI 给你做词频图也算「实现了需求」——但词频图、自动回复、问答页面解决的是三个不同的问题。</p>
<p>我做题时最深的一个体会是「口径必须写进 Spec」。题目要求按时间范围过滤消息，<code>sent_at</code>（发送时间）和 <code>collected_at</code>（采集时间）两种口径结果不同，Spec 确认后必须显式传入，不许默认；没有发送时间的消息「保留未知」，不许偷换成采集时间：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">filter_by_range</span>(messages: list[dict], start, end, field: str) <span style="color:#f92672">-&gt;</span> list[dict]:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> field <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> (<span style="color:#e6db74">&#34;sent_at&#34;</span>, <span style="color:#e6db74">&#34;collected_at&#34;</span>):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">ValueError</span>(<span style="color:#e6db74">&#34;时间口径必须由 Spec 明确指定&#34;</span>)
</span></span><span style="display:flex;"><span>    out <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> m <span style="color:#f92672">in</span> messages:
</span></span><span style="display:flex;"><span>        t <span style="color:#f92672">=</span> m<span style="color:#f92672">.</span>get(field)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> t <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>                 <span style="color:#75715e"># 未知就是未知，不偷换口径</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> start <span style="color:#f92672">&lt;=</span> t <span style="color:#f92672">&lt;=</span> end:
</span></span><span style="display:flex;"><span>            out<span style="color:#f92672">.</span>append(m)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> out
</span></span></code></pre></div><h2 id="第-2-步ai-给你规划后只盯一样东西它自己脑补的">第 2 步：AI 给你规划后，只盯一样东西：它自己脑补的</h2>
<p>规划输出里那些「写得很肯定、但你从没提过」的规则——比如我遇到的「只分析带问号的评论」——是 AI 替你做的决定，不是需求。第一动作不是采纳也不是删掉，是<strong>追问：这条规则来自用户需求，还是你自己的建议？</strong></p>
<p>审查规划可以按四类拆：已确认需求 / Agent 假设 / 实现方案 / 开发任务。重点盯第二类，跑得越顺的脑补规则越危险。</p>
<h2 id="第-3-步验收清单写在开工前">第 3 步：验收清单写在开工前</h2>
<p>TDD 循环：红→绿→重构。两个坑：</p>
<ol>
<li><strong>红灯要坏得对</strong>。缺依赖、模块导不进来这类报错是「考场坏了」，不算验收——先修运行条件，这时候还不能说明业务规则得到了检查。</li>
<li><strong>测试第一次就绿，别造假红灯</strong>。要么目标行为早已实现（保留即可），要么断言漏了关键结果——是断言的问题，不是改坏代码表演一次的事。</li>
</ol>
<h2 id="第-4-步ai-应用最核心验证分两种别混">第 4 步：AI 应用最核心——验证分两种，别混</h2>
<p><strong>查逻辑</strong>（去重、筛选、引用不越界）→ 固定假数据 + 模型替身，逐字段断言，像数学题有标准答案。数数这种活是程序干的，不归模型：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">FakeModel</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">__init__</span>(self, answer): self<span style="color:#f92672">.</span>answer <span style="color:#f92672">=</span> answer; self<span style="color:#f92672">.</span>call_count <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">complete</span>(self, msgs, q):
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>call_count <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> self<span style="color:#f92672">.</span>answer
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">start_analysis</span>(snapshot: list[dict], question: str, model) <span style="color:#f92672">-&gt;</span> dict:
</span></span><span style="display:flex;"><span>    msgs <span style="color:#f92672">=</span> tuple(snapshot)           <span style="color:#75715e"># 快照语义：本批输入就此冻结</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> len(msgs) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> {<span style="color:#e6db74">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;empty_range&#34;</span>}
</span></span><span style="display:flex;"><span>    ans <span style="color:#f92672">=</span> model<span style="color:#f92672">.</span>complete(msgs, question)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> {<span style="color:#e6db74">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;ok&#34;</span>, <span style="color:#e6db74">&#34;answer&#34;</span>: ans,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;count&#34;</span>: len(msgs),      <span style="color:#75715e"># 程序统计，不是模型统计</span>
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;input_ids&#34;</span>: [m[<span style="color:#e6db74">&#34;msg_id&#34;</span>] <span style="color:#66d9ef">for</span> m <span style="color:#f92672">in</span> msgs]}
</span></span></code></pre></div><p>快照为空时断言 <code>call_count == 0</code>——模型一次都不能被调用；引用了范围外的 msg_id 断言被拒。这类用替身测，稳定、免费、可重复。</p>
<p><strong>看效果</strong>（摘要准不准、有没有编）→ 真实调用、多样本反复跑。程序只能校验「引用没越界」，校验不了「引用真能支撑回答」——后者是语义判断，标准是「措辞可以变，意思必须留住」。一次通过≠稳定。</p>
<h2 id="第-5-步别信测试全部通过这句话">第 5 步：别信「测试全部通过」这句话</h2>
<p>AI 报告全部通过时，看四项汇报里有没有：<strong>真实模型调用过吗？页面实际操作过吗？</strong> 程序逻辑测试 100% 通过，只说明确定性逻辑没问题；模型效果和用户流程仍然是待验证状态。这是两个轨道，不能互相顶替。</p>
<h2 id="速查表">速查表</h2>
<table>
	<thead>
			<tr>
					<th>模块</th>
					<th>核心</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Spec</td>
					<td>场景+输入输出+关键行为+异常+验收；「哪些还要靠猜」</td>
			</tr>
			<tr>
					<td>审规划</td>
					<td>已确认需求／Agent假设／实现方案／开发任务；盯「很肯定但没讨论过」</td>
			</tr>
			<tr>
					<td>用例</td>
					<td>前置+输入操作+预期+<strong>不应发生</strong>+检查方法</td>
			</tr>
			<tr>
					<td>TDD</td>
					<td>红灯(因目标行为)→最小绿灯→重构；不为通过删断言</td>
			</tr>
			<tr>
					<td>双轨验证</td>
					<td>程序逻辑用替身精确断言；模型效果真实调用+语义标准</td>
			</tr>
	</tbody>
</table>
<p>一句话总结：<strong>一句话生成是表演，说清什么叫「做对了」才是工程。</strong></p>
<blockquote>
<p>本篇最早于 2026 年 10 月 16 日发布于小红书，此为博客完整版。</p>
</blockquote>
]]></content:encoded></item></channel></rss>