代码语言

知识点思维导图

21 个知识节点

Claude Code(04) - 工作循环与检查点回溯:读—改—跑—验证 + 后悔药

读完后,你应能完成以下任务:

  • 绘制“Claude Code(04) - 工作循环与检查点回溯:读—改—跑—验证 + 后悔药 / 它干活的基本节奏:读—改—跑—验证”的关键对象与数据流,解释“读:它 grep 搜 cart、读 cart.js 和 cart.test.js; -> 改:它定位到 bug,编辑 cart.js(屏幕显示 diff); -> 跑:它执行 npm test; -> 验证:测试全绿 → 交付;”,并用源码位置、日志或 Trace 标注证据。
  • 为“Claude Code(04) - 工作循环与检查点回溯:读—改—跑—验证 + 后悔药 / 每一步你都有控制权”设计正常与异常输入,验证“🔑 心法:你是 reviewer,它是 executor。”,输出首个偏差位置与回归测试结果。
  • 实现“Claude Code(04) - 工作循环与检查点回溯:读—改—跑—验证 + 后悔药 / 后悔药:检查点与回溯”的最小代码或配置,检验“回溯主要恢复的是文件内容。”,输出命令、结果与 Diff,并说明不适用边界。

本章目标:理解 Claude Code 的核心工作循环;学会用检查点把文件回退到任意一步。


一、它干活的基本节奏:读—改—跑—验证

你下完指令,它不是「啪」一下吐出代码就完。一个完整的循环长这样:

   ┌─────────────────────────────────────────────┐
   │  1. 读:搜索 + 读相关文件,搞清现状           │
   │  2. 改:编辑文件(每次改动都展示给你看)       │
   │  3. 跑:执行命令(跑测试 / 构建 / lint)       │
   │  4. 验证:看结果。没过?回到第 2 步自己修      │
   └─────────────────────────────────────────────┘
                      ↓ 验证通过
                    交付给你审查

举个例子,你说「修复 cart.test.js 失败的用例」,你会依次看到:

  1. :它 grep 搜 cart、读 cart.jscart.test.js
  2. :它定位到 bug,编辑 cart.js(屏幕显示 diff);
  3. :它执行 npm test
  4. 验证:测试全绿 → 交付;若还有红 → 它自己回去再改。

看懂这个循环,你就知道任何时刻它在哪一步、该不该打断。


二、每一步你都有控制权

  • 它要改文件 / 跑命令时,默认会先问你(权限机制第 06 章细讲)。你可以:
    • 同意这一次;
    • 同意并以后类似操作不再问;
    • 拒绝,并补充说明让它换个做法。
  • 它跑偏了:按 Esc 立刻打断,然后纠正方向。
  • 想看它改了啥:如果项目用 git,随时 git diff 看全部改动。

🔑 心法:你是 reviewer,它是 executor。 让它干活,但每一步的「准/不准」由你把关。


三、后悔药:检查点与回溯

试错难免。Claude Code 提供了「检查点(checkpoint)」——它会在对话推进中记录文件状态,让你能把文件回退到之前任意一步,就像游戏存档读档。

3.1 怎么用(交互式)

连按两次 Esc Esc,会弹出对话历史里的各个节点,选一个就能把文件回退到那一刻的状态。

适用场景:

  • 它连续改了好几轮,越改越歪 → 回退到「还没歪」的那一步重来;
  • 你想换个思路试试 → 先回到岔路口,再走另一条路;
  • 改崩了不想手动一个个撤销 → 一键回到干净状态。

3.2 要分清:回退的是「文件」,不是「记忆」

回溯主要恢复的是文件内容。对话的上下文还在,所以你可以「回到那一步的文件 + 带着这一路的教训」重新出发。

💡 这和 git 不冲突。git 是你主动提交的版本;检查点是会话内的细粒度「即时存档」,更轻、更密。两者配合:大节点用 git 提交,会话内试错用检查点回溯。


四、常见错误

错误 1:放着它自动跑一长串却从不看 diff 它能力强,但不是永远对。养成看改动的习惯,尤其是它「连跑好几步」之后。

错误 2:改崩了手动一行行撤 没必要。Esc Esc 回溯到上一个好状态,比手动撤销快且不易漏。

错误 3:把检查点当成 git 的替代品 检查点是会话内的临时存档,关键里程碑还是要 git commit 落地。会话结束后,git 才是你长期可靠的版本记录。

错误 4:它在「跑」的步骤卡住/报错,却没给它反馈 它跑命令失败时,往往会自己尝试修。如果它卡住,把报错信息补给它,或 Esc 后换个思路。


五、最佳实践

  1. 干活前先 git commit 一个干净基线:万一要彻底推倒,git 兜底。
  2. 大改之前,心里标个检查点:知道「这步之前是好的」,出问题随时 Esc Esc 回去。
  3. 盯住「验证」这一步:它跑的是不是对的测试/命令?验证标准对了,结果才可信。
  4. 它自我纠错时给点耐心:测试没过它会自己再改,别急着插手;但若它在原地打转两次以上,就该介入换思路。

六、动手实践:Demo 04 · 读—改—跑—验证 + 检查点回溯实战

这个 Demo 是一个带测试、且故意留了一个 bug 的小项目(购物车计价)。你将完整体验一遍 Claude Code 的工作循环,并练习用 Esc Esc 回溯。

6.1 怎么用

  1. 先建一个干净基线(强烈建议):
    cd 05-工作循环与检查点回溯-demo
    git init && git add -A && git commit -m "baseline"
    
  2. 跑测试,亲眼看到失败:
    node cart.test.js
    
    你会看到「折扣计算」相关用例不通过。
  3. 启动 Claude Code,下达带验证方式的指令:
    运行 `node cart.test.js`,有用例失败。
    帮我定位 @cart.js 里的 bug 并修复,修完重新跑测试,全绿才算完成。
    
  4. 观察它的工作循环:读 → 改(看 diff)→ 跑测试 → 没过就自己再改 → 全绿交付。

6.2 练习检查点回溯

  • 等它改完后,故意说一句「换个写法重来」之类的,让它再改一版;
  • 然后按 Esc Esc,把文件回退到之前某一步,体会「后悔药」。
  • 对比:git diff 看的是相对 baseline 的总改动;检查点回退的是会话内的某一步。

6.3 文件说明

  • cart.js:购物车计价逻辑,applyDiscount 里有个故意的 bug。
  • cart.test.js:极简测试(用 Node 自带 assert,无需安装依赖)。

七、总结

  • 它干活的基本节奏:读—改—跑—验证:读:它 grep 搜 cart、读 cart.js 和 cart.test.js; -> 改:它定位到 bug,编辑 cart.js(屏幕显示 diff); -> 跑:它执行 npm test; -> 验证:测试全绿 → 交付;
  • 每一步你都有控制权:🔑 心法:你是 reviewer,它是 executor。
  • 后悔药:检查点与回溯:回溯主要恢复的是文件内容。
  • 最佳实践:干活前先 git commit 一个干净基线:万一要彻底推倒,git 兜底。 -> 大改之前,心里标个检查点:知道「这步之前是好的」,出问题随时 Esc Esc 回去。 -> 盯住「验证」这一步:它跑的是不是对的测试/命令? -> 它自我纠错时给点耐心:测试没过它会自己再改,别急着插手;
  • 怎么用:先建一个干净基线(强烈建议): -> 跑测试,亲眼看到失败: -> 启动 Claude Code,下达带验证方式的指令: -> 观察它的工作循环:读 → 改(看 diff)→ 跑测试 → 没过就自己再改 → 全绿交付。
  • 练习检查点回溯:对比:git diff 看的是相对 baseline 的总改动;

7.1 可运行实验:Checkpoint、回溯与上下文压缩

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>AC-10 在线实验</title>
  <style>
    :root{color-scheme:dark;font-family:Inter,system-ui,sans-serif}*{box-sizing:border-box}body{margin:0;background:#0f1211;color:#e7ece9;font-size:13px}.shell{padding:16px}.top{display:flex;justify-content:space-between;gap:16px;margin-bottom:14px}h1{margin:3px 0;font-size:18px}.id,.value{color:#68e0b5;font-family:ui-monospace,monospace}.summary{margin:4px 0;color:#a5afa9}.run{border:0;border-radius:6px;background:#68e0b5;color:#07110d;padding:8px 14px;font-weight:700}.grid{display:grid;grid-template-columns:minmax(220px,.8fr) minmax(0,1.8fr);gap:12px}.panel{border:1px solid #29322e;background:#141817;padding:12px}.control{display:grid;gap:5px;margin-bottom:11px}.head{display:flex;justify-content:space-between;gap:8px}select,input{width:100%;accent-color:#68e0b5;background:#0d100f;color:#e7ece9}.toggle{display:flex;justify-content:space-between;border-top:1px solid #29322e;padding-top:9px}.toggle input{width:18px}.metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px}.metric{border:1px solid #29322e;padding:8px}.metric b{display:block;color:#68e0b5;font-size:16px}.stages{display:flex;gap:6px;overflow:auto;margin:10px 0}.stage{border:1px solid #8a6230;padding:7px;min-width:90px}.stage.ok{border-color:#367a61}.stage.fail{border-color:#8b4545}table{width:100%;border-collapse:collapse}td{border-top:1px solid #29322e;padding:7px}.diagnosis{margin-top:9px;border-left:3px solid #68e0b5;background:#101412;padding:9px;line-height:1.5}.danger{border-color:#ef7f7f}@media(max-width:680px){.top,.grid{display:grid;grid-template-columns:1fr}.metrics{grid-template-columns:repeat(2,1fr)}}
  </style>
</head>
<body>
  <main class="shell">
    <header class="top"><div><div class="id">AC-10 · DETERMINISTIC LAB</div><h1 id="title"></h1><p class="summary" id="summary"></p></div><button class="run" id="run">运行实验</button></header>
    <section class="grid"><div class="panel"><div id="controls"></div><label class="toggle"><span>注入典型故障</span><input id="failure" type="checkbox"></label></div><div class="panel"><div class="metrics" id="metrics"></div><div class="stages" id="stages"></div><table><tbody id="rows"></tbody></table><div class="diagnosis" id="diagnosis"></div></div></section>
  </main>
  <script>
    const scenario = { title: 'Checkpoint、回溯与上下文压缩', summary: '区分代码快照、执行状态和对话摘要,避免把三种恢复机制混为一谈。', controls: [
        { key: 'steps', label: '累计修改步骤', type: 'range', min: 2, max: 12, value: 7, suffix: ' 步' },
        { key: 'checkpoint', label: '检查点间隔', type: 'range', min: 1, max: 6, value: 3, suffix: ' 步' },
        { key: 'operation', label: '恢复操作', type: 'select', value: 'restore', options: [['undo', '仅撤销代码'], ['restore', '恢复检查点'], ['compact', '仅压缩对话']] }
      ] };
    const controls = document.querySelector('#controls');
    const failure = document.querySelector('#failure');
    document.querySelector('#title').textContent = scenario.title;
    document.querySelector('#summary').textContent = scenario.summary;
    function renderControl(control) {
      const label = document.createElement('label'); label.className = 'control';
      const head = document.createElement('span'); head.className = 'head'; head.innerHTML = '<span>' + control.label + '</span><span class="value" data-value="' + control.key + '"></span>'; label.appendChild(head);
      const input = document.createElement(control.type === 'select' ? 'select' : 'input'); input.dataset.key = control.key;
      if (control.type === 'select') control.options.forEach(option => { const item = document.createElement('option'); item.value = option[0]; item.textContent = option[1]; item.selected = option[0] === control.value; input.appendChild(item); });
      else { input.type = 'range'; input.min = control.min; input.max = control.max; input.step = control.step || 1; input.value = control.value; }
      input.addEventListener('input', updateValues); label.appendChild(input); return label;
    }
    function updateValues() { scenario.controls.forEach(control => { const input = controls.querySelector('[data-key="' + control.key + '"]'); document.querySelector('[data-value="' + control.key + '"]').textContent = control.type === 'select' ? input.options[input.selectedIndex].text : input.value + (control.suffix || ''); }); }
    function readValues() { const values = {}; scenario.controls.forEach(control => { const input = controls.querySelector('[data-key="' + control.key + '"]'); values[control.key] = control.type === 'range' ? Number(input.value) : input.value; }); values.failure = failure.checked; return values; }
    function stage(name, state, detail) { return { name, state, detail }; }
    const aiStage = stage;
    function clamp(value, minimum, maximum) { return Math.min(maximum, Math.max(minimum, value)); }
    function simulate(values) { const fail = values.failure;
          /** 按间隔产生的可用检查点数量。 */
          const checkpoints = Math.floor(values.steps / values.checkpoint);
          /** 最近检查点之后需要重放的步骤数。 */
          const replay = values.steps % values.checkpoint;
          /** 选择恢复操作后真正恢复的状态范围。 */
          const restored = values.operation === 'restore' ? '代码 + 工具状态' : values.operation === 'undo' ? '仅代码 Diff' : '仅对话摘要';
          return { metrics: [[checkpoints, '检查点'], [replay, '需重放步骤'], [restored, '恢复范围'], [fail ? 'STALE' : 'VALID', '快照状态']], stages: [stage('修改', 'ok', values.steps), stage('保存快照', checkpoints ? 'ok' : 'warn', checkpoints), stage('选择操作', 'ok', values.operation), stage('恢复', fail ? 'fail' : 'ok', restored), stage('校验', values.operation === 'compact' || fail ? 'warn' : 'ok', 'workspace')], rows: [['Undo', '只撤销代码,不自动恢复外部工具副作用'], ['Restore', '恢复检查点记录的工作区与 Harness 状态'], ['Compact', '减少对话 Token,不改变磁盘或执行状态']], diagnosis: fail ? '快照对应的仓库版本已变化,恢复前必须校验基线。' : values.operation === 'compact' ? '上下文已压缩,但代码和工具状态没有回退。' : '恢复边界明确,可根据检查点继续执行。', danger: fail };
         }
    function render() { const result = simulate(readValues()); document.querySelector('#metrics').innerHTML = result.metrics.map(item => '<div class="metric"><b>' + item[0] + '</b><span>' + item[1] + '</span></div>').join(''); document.querySelector('#stages').innerHTML = result.stages.map(item => '<div class="stage ' + item.state + '"><b>' + item.name + '</b><div>' + item.detail + '</div></div>').join(''); document.querySelector('#rows').innerHTML = result.rows.map(item => '<tr><td>' + item[0] + '</td><td>' + item[1] + '</td></tr>').join(''); const diagnosis = document.querySelector('#diagnosis'); diagnosis.textContent = result.diagnosis; diagnosis.className = 'diagnosis' + (result.danger ? ' danger' : ''); }
    scenario.controls.forEach(control => controls.appendChild(renderControl(control))); updateValues(); document.querySelector('#run').addEventListener('click', render); render();
  </script>
</body>
</html>

学完自测

选择所有正确答案;提交后逐项核对判断依据。

1在“工作循环与检查点回溯:读—改—跑—验证 + 后悔药”中,需要同时满足“它干活的基本节奏:读—改—跑—验证”与“每一步你都有控制权”。给定正文约束“你下完指令,它不是「啪」一下吐出代码就完。”,哪些判断保持了原有处理机制?多选
2“工作循环与检查点回溯:读—改—跑—验证 + 后悔药”出现偏差:“在“工作循环与检查点回溯:读—改—跑—验证 + 后悔药 / 后悔药:检查点与回溯”中,即使不满足“Claude Code 提供了「检查点(checkpoint)」——它会在对话推进中记录文件状态,让你能把文件回退到之前任意一步,就像游戏存档读档”,结果与副作用仍会保持不变。”已成为实际行为。围绕“后悔药:检查点与回溯”与“怎么用(交互式)”,哪些判断能定位被改变的职责或边界?多选
3评审“工作循环与检查点回溯:读—改—跑—验证 + 后悔药”方案时,验收条件包含“对话的上下文还在,所以你可以「回到那一步的文件 + 带着这一路的教训」重新出发。”。关于“要分清:回退的是「文件」,不是「记忆」”与“最佳实践”的哪些决策符合正文机制?多选