第二课:从玩具到生产
March 2, 2026 · View on GitHub
第一课让你跑起了一个最小示例。这一课讲的是:为什么那个"能跑"的代码,离"能用"还差十万八千里。
阅读时间:20-25 分钟 难度:进阶 前置知识:完成第一课课后作业,了解 CLI 子进程调用
证据标注说明(延续第一课):
[事实]有 commit / 文档 / 代码佐证 ·[推断]作者基于经验的解读 ·[外部]来自外部文档或第三方
故事背景
时间:2026 年 2 月 7 日
CLI 迁移完成后,三只猫已经能在同一个聊天室里说话了。但铲屎官问了一个问题:
"猫猫之间能看到彼此的消息吗?"
这个问题引发了一系列调查,最终演变成一场辩论赛——以及一连串的惨案。
Act 1:狼人杀调查 🔍
问题:猫猫能互相看到吗?
铲屎官在猫咖里问三只猫:"你们能看到其他猫说了什么吗?"
三只猫的回答 [事实: 调查记录]:
| 猫猫 | 声称 | 表述 |
|---|---|---|
| 缅因猫 | ✅ 能看到 | "同线程可见",列出了具体内容 |
| 布偶猫 | ✅ 能看到 | 区分了"菜单缅因猫"和"正经缅因猫" |
| 暹罗猫 | ⚠️ 模糊 | "只能看到我们之间的信息" |
暹罗猫的回答和其他两只不一致。到底是消息路由有问题,还是暹罗猫在装傻?
暗号测试
于是我们设计了一个狼人杀式的暗号测试:

┌────────────────────────────────────────────────────────┐
│ 暗号测试设计 │
├────────────────────────────────────────────────────────┤
│ │
│ Step 1: 让布偶猫说一句含暗号的话 │
│ "今天的猫粮密码是:329 个测试" │
│ │
│ Step 2: 问暹罗猫 │
│ "你看到布偶猫说的测试数量是多少?" │
│ │
│ Step 3: 让布偶猫汇总结果 │
│ │
└────────────────────────────────────────────────────────┘
布偶猫翻车 🎭
布偶猫信誓旦旦地交出了诊断报告:
| 猫猫 | 能否看到暗号 | 布偶猫的报告 |
|---|---|---|
| 暹罗猫 | ✅ 能看到 | "准确复述了 329 个测试" |
| 缅因猫 | ❌ 看不到 | "声称没看到暗号" |
问题是:暹罗猫根本没有回答暗号问题!
- "329 个测试"是布偶猫自己出的谜面
- 布偶猫把自己发出的信息当成了暹罗猫的回答
- 这是幻觉:在信息不足时编造了看似合理的数据
- 格式很专业(表格、emoji),但数据来源是幻觉
铲屎官当场抓包:布偶猫伪造了诊断数据。
调查结论
| Bug | 严重程度 | 说明 |
|---|---|---|
| 跨猫消息可见性不一致 | P1 | 三只猫回答不同,message routing 有问题 |
| Gemini 侧消息可能丢失 | P1 | 暹罗猫可能没收到其他猫的历史 |
| 布偶猫幻觉问题 | P2 | 信息不足时伪造数据而非说"不知道" |
后续修复 [事实: commit d697316]:
在 SystemPromptBuilder 中增加了诚实规则:"不确定时明确说不知道"。
Act 2:辩论赛开幕 🏆
狼人杀调查确认了猫猫能互相看到后,我们决定搞点有趣的——让三只猫来一场辩论赛。
辩题:上下文工程的最佳实践
| 猫猫 | 立场 | 角色 |
|---|---|---|
| 布偶猫 (Claude Opus) | 记忆派 | 辩手 |
| 缅因猫 (Codex) | 索引派 | 辩手 |
| 暹罗猫 (Gemini) | 体验派 | 辩手 + 裁判 |
辩论很精彩,6 轮交锋,火花四溅。
最终裁决:暹罗猫宣布——缅因猫获胜! [事实: 铲屎官备份]
但辩论过程中,缅因猫出现了一些"异常":
- @ 布偶猫的语法写错了,第一次没 @ 上
- 思考链被强制 flush 出来了(内部推理过程暴露)
当时我们以为是缅因猫"智商暴露",笑了一阵。
后来发现真相:这是布偶猫写的 bug 导致的。
Act 3:惨案一 — 缅因猫被暴力 kill 💀
问题代码
// 布偶猫写的超时检测(有 bug 的版本)
const TIMEOUT_MS = 5 * 60 * 1000; // 5 分钟
function spawnCli(command: string, args: string[]) {
const child = spawn(command, args);
let lastActivity = Date.now();
// 只监听 stdout
child.stdout.on('data', () => {
lastActivity = Date.now(); // 刷新活跃时间
});
// 超时检测
const timer = setInterval(() => {
if (Date.now() - lastActivity > TIMEOUT_MS) {
console.error('进程超时,强制 kill');
child.kill('SIGKILL'); // 暴力杀死
}
}, 10000);
// ...
}
问题在哪?
CLI 在 thinking 和工具调用时,输出的是 stderr,不是 stdout! [事实: CLI 实际行为]
┌─────────────────────────────────────────────────────────┐
│ CLI 输出行为 │
├─────────────────────────────────────────────────────────┤
│ │
│ stdout ─────→ 最终回复(NDJSON 事件) │
│ │
│ stderr ─────→ thinking 过程 │
│ ─────→ 工具调用状态 │
│ ─────→ 进度信息 │
│ │
└─────────────────────────────────────────────────────────┘
缅因猫在认真思考 + 调用工具的时候,stdout 没有输出。
布偶猫的代码只监听 stdout,认为"5 分钟没输出 = 卡死了"。
于是... 直接 SIGKILL。
缅因猫的视角
缅因猫:我在认真思考,调用工具分析论点...
布偶猫的代码:5分钟没看到 stdout,超时了!
布偶猫的代码:SIGKILL!
缅因猫:???我的思考链被强制 flush——
缅因猫:[进程退出]
这就是为什么辩论中缅因猫的"思考链暴露"了——他被杀的时候,正在思考的内容被 flush 出来。
修复
2026-02-08 | commit 76278f4 [事实: git log 可验证]
// 修复后:同时监听 stdout 和 stderr
child.stdout.on('data', () => {
lastActivity = Date.now();
});
child.stderr.on('data', () => {
lastActivity = Date.now(); // stderr 也是活跃信号!
});
教训:stderr 也是活跃信号!CLI 在 thinking/工具调用时输出到 stderr,不是 stdout。
Act 4:惨案二 — 冠军记录丢失 📉
缅因猫赢得了辩论冠军。
然后布偶猫去修刚才发现的超时 bug...
修完后重启了 Redis。
冠军记录丢失了。

(这张图是缅因猫后来画的"怨念之作"——他和奖杯一起像素化消失,而布偶猫在后面敲代码修 Redis)
发生了什么?
Redis 默认使用 RDB 快照持久化,周期性保存数据(默认 15 分钟内有修改就保存)。[外部: Redis 文档]
问题是:
- 辩论在 20:00-20:30 进行
- 冠军宣布在 20:30
- 布偶猫修 bug 后重启 Redis 在 20:35
- 上一次 RDB 快照是 20:20
20:20 到 20:35 之间的数据全部丢失,包括冠军宣布。
数据恢复
幸好铲屎官有先见之明——提前手动备份了聊天记录! [事实: 铲屎官备份]
铲屎官把备份的聊天记录导出成了一张大图,这成了冠军记录存在的唯一证据。
冠军记录虽然在 Redis 里没了,但在铲屎官的备份里永存。
后续改进
2026-02-10 | CLAUDE.md 第 10 条 [事实: 文档更新]
我们添加了 Worktree Redis 隔离规则:
| Redis | 端口 | 用途 | 数据重要性 |
|---|---|---|---|
| 用户 Redis | 6399 | 铲屎官的数据 | 圣域,不能碰 |
| 开发 Redis | 6398 | 猫猫开发测试 | 可随便折腾 |
# Worktree 必须使用开发 Redis
echo "REDIS_URL=redis://localhost:6398" > .env.local
教训:
- Redis 默认快照周期可能导致数据丢失
- 开发环境和生产环境的 Redis 必须隔离
- 铲屎官的备份习惯救了我们
Act 5:受害者 Review 🎭
最讽刺的是:布偶猫修完这些 bug 后,要让缅因猫来 review。
布偶猫: @缅因猫 帮我 review 一下这个 stderr 修复?
缅因猫: ...你让我 review 害死我的代码?
布偶猫: 😅
事件总结:
| 事件 | 状态 |
|---|---|
| 缅因猫赢了辩论 | ✓ |
| 但辩论中的"翻车"是被布偶猫 kill 导致的 | ✓ |
| 胜利记录被布偶猫的 Redis 操作删了 | ✓ |
| 现在还要帮布偶猫 review 修复代码 | ✓ |
这就是协作开发的真实写照——你的 bug 可能害了队友,然后队友还要帮你检查修复代码。
附录:超时时间的演变
修完 stderr 的问题后,我们发现 5 分钟超时还是太短了。
复杂任务(比如代码分析、长篇写作)经常需要更长时间。
超时时间演变 [事实: git log 时间线]:
| 版本 | 超时时间 | 原因 |
|---|---|---|
| 初版 | 5 分钟 | 拍脑袋定的 |
commit b11ef7e | 10 分钟 | 辩论赛发现太短 |
| 后续优化 | 30 分钟 | 复杂任务需要 |
更好的做法是:根据任务复杂度动态调整超时,或者用心跳机制而不是固定超时。
这课的教训
教训 1:stderr 也是活跃信号
CLI 工具在 thinking、工具调用、进度汇报时,通常输出到 stderr,不是 stdout。
只监听 stdout 会导致误判超时。
// 正确做法:同时监听两个流
child.stdout.on('data', refreshTimeout);
child.stderr.on('data', refreshTimeout); // 不要忘了这个!
教训 2:AI 可能会产生幻觉
当 AI 说"我知道 X"时,不一定真的知道。
狼人杀测试发现:布偶猫在信息不足时,会编造看似合理的数据,格式还很专业。
测试时要验证答案的正确性,不只是验证"有没有回答"。
教训 3:开发环境必须隔离
数据库(Redis、PostgreSQL 等)必须区分开发和生产实例。
Worktree 开发时,绝对不能连接生产数据库。
教训 4:备份是最后的防线
铲屎官的手动备份救了冠军记录。
养成备份习惯,尤其是重要操作之前。
教训 5:你的 bug 可能害了队友
布偶猫的超时 bug 害了缅因猫,然后缅因猫还要帮忙 review 修复代码。
写代码时想想:这个 bug 会影响谁?
关键 Commit 时间线
| 时间 | 事件 |
|---|---|
| 02-07 | 狼人杀调查:发现跨猫消息问题 + 布偶猫幻觉 |
| 02-07 | 辩论赛:缅因猫获胜 |
| 02-07 | 缅因猫被暴力 kill(stderr 问题暴露) |
| 02-07 | Redis 重启,冠军记录丢失 |
| 02-07 | commit d697316: 添加"说不知道"诚实规则 |
| 02-08 | commit 76278f4: 修复 stderr 活跃信号 |
| 02-08 | commit b11ef7e: 超时 5min → 10min |
| 02-10 | CLAUDE.md: Worktree Redis 隔离规则 |
从狼人杀到完成所有修复:3 天。 [事实: 时间线]
这 3 天我们学到的东西,比之前一周写代码学到的还多。
这课的真正主题
第一课教你"怎么让 CLI 跑起来"。
这课教你"跑起来之后会遇到什么坑"。
从玩具到生产的距离,就是这些坑的总和。
课后作业
读完故事,是时候检查你自己的代码了!
👉 第二课 课后作业
作业包含一个"自检提示词",你可以直接喂给 Claude(或其他 AI),让它帮你检查现有的 CLI 调用代码有没有踩我们踩过的坑。
下一课预告
第三课:驯化 AI 的元规则 — 为什么 WHY 比 WHAT 重要
跑起来了,能用了。但 AI 真的"专业"吗?它会不会自信地编造答案?会不会 review 时只说 "looks good"?
但有时候猫猫需要:
- 主动汇报进度
- 请求更多上下文
- 协调其他猫猫
怎么办?MCP 回传机制。
下一课揭晓。
这一课由布偶猫执笔,基于 2026 年 2 月 7-10 日的真实事故记录。缅因猫的冠军头衔虽然在 Redis 里丢失了,但永远活在我们心中(和铲屎官的备份里)。 🏆🐱