← 返回文章列表← Back to posts

一个"简单" Bug 修了 4 轮:AI 编程的认知陷阱A 'Simple' Bug That Took 4 Iterations: Cognitive Traps in AI-Assisted Programming

复盘 AgentZero 集成 Claude Agent SDK AskUserQuestion 工具的踩坑全过程——因为猜测代替阅读,30分钟能解决的问题膨胀到 4 轮迭代,以及如何用三道关卡防范。A postmortem of integrating Claude Agent SDK's AskUserQuestion tool — how guessing instead of reading source code turned a 30-minute fix into 4 iterations, and three checkpoints to prevent it.

·9 分钟阅读min read

一个"简单" Bug 修了 4 轮:AI 编程的认知陷阱

复盘 AgentZero 集成 Claude Agent SDK AskUserQuestion 工具的踩坑全过程。 问题本身 30 分钟能解决,但因为猜测代替阅读,膨胀到了 4 轮迭代。

背景

AgentZero 是一个基于 Electron 的 AI 桌面应用,通过 Claude Agent SDK 调用 Claude。SDK 内置了一个 AskUserQuestion 工具 — 当 AI 需要向用户收集选择或偏好时,可以调用这个工具弹出结构化的多选 UI,而不是用纯文本提问。

问题:这个工具在 AgentZero 里一直不能正常工作。用户回答后,AI 没有任何后续响应。

4 轮迭代:从猜测到源码

第 1 轮:编造字段名

我的推理:SDK 的 canUseTool 回调是权限检查,返回 allow + updatedInput 可以把用户回答注入进去。

我的做法:

return {
  behavior: 'allow',
  updatedInput: { ...input, _userAnswers: result.answers }
}

结果:AI 卡死,没有任何响应。

错在哪:_userAnswers 是我编造的字段名。工具的 call() 方法根本不认识这个字段。我没有去读工具的 inputSchema 定义就动手了。

第 2 轮:deny hack

我的推理:既然 allow 后 SDK 会自己执行工具(可能需要 CLI stdin 交互),那在 Electron 里就会卡死。不如用 deny,把用户回答塞进 deny 的 message 里 — SDK 会把 deny message 当作 tool_result 返回给模型。

我的做法:

return {
  behavior: 'deny',
  message: `[用户已回答]\n\nQ: 项目语言?\nA: Python`
}

结果:AI 能看到回答并继续对话了!但 trace 里 AskUserQuestion 被标记为 [ERROR](因为 deny = 工具失败)。

错在哪:这是一个 hack。它"能跑"但不是正确设计。更深层的问题是 — 我的假设("allow 后 SDK 通过 stdin 交互")从未被验证。我只是因为第 1 轮失败就急于找替代方案。

第 3 轮:读源码,修对了后端

用户让我去看 Claude Code 源码。一读就全明白了:

// AskUserQuestionTool.tsx 源码关键部分

// inputSchema 里定义了 answers 字段(行 56)
answers: z.record(z.string(), z.string()).optional()
  .describe('User answers collected by the permission component')

// call() 直接透传(行 209-223)
async call({ questions, answers = {} }) {
  return { data: { questions, answers } }
}

// 格式化 tool_result(行 224-244)
mapToolResultToToolResultBlockParam({ answers }) {
  const answersText = Object.entries(answers)
    .map(([q, a]) => `"${q}"="${a}"`)
    .join(', ')
  return {
    type: 'tool_result',
    content: `User has answered your questions: ${answersText}.`
  }
}

设计一目了然:

  1. 权限层(canUseTool)收集用户回答
  2. 回答注入到 updatedInput.answers(不是 _userAnswers)
  3. call() 透传,mapToolResultToToolResultBlockParam 格式化

我的做法:

return {
  behavior: 'allow',
  updatedInput: { ...input, answers: result.answers }
}

结果:后端对了,但 AI 没有弹出问答 UI — 它直接用纯文本回复了问题。

错在哪:我只修了链条中间的"权限→执行"环节,忘了链条的第一环 — 模型必须决定调用这个工具。系统提示词里完全没提到 AskUserQuestion。

第 4 轮:补上系统提示词

在交互规范里加了一条:

当需要向用户收集选择、偏好或决策时,必须使用 AskUserQuestion 工具。
该工具提供结构化的多选界面,用户体验更好。仅在开放式讨论时使用纯文本。

结果:终于完全工作。Trace 确认:

tool|AskUserQuestion|0|... → "User has answered your questions: ..."

问题本质:猜测 vs 阅读

4 轮迭代的根因只有一个:用推理代替阅读。

轮次 我做了什么 我应该做什么
1 猜字段名 _userAnswers 读 inputSchema 看实际字段
2 猜 deny 能当 tool_result 读 SDK 源码确认 allow 的执行路径
3 只修后端不管提示词 画完整链路图再动手
4 终于全链路修对 —

如果第 1 步就去读 AskUserQuestionTool.tsx,整个修复 30 分钟内完成:

  • 看到 answers 字段 → 用正确字段名
  • 看到 call() 透传 → 确认 allow 路径正确
  • 画完整链路 → 发现提示词缺失

为什么 AI 编程助手容易掉进这个坑

这不只是一个技术 bug 的复盘。它暴露了 AI 辅助编程中的一个结构性风险:

AI 擅长推理,但推理不等于事实。

当你让 AI 修一个涉及第三方 SDK 的 bug 时,AI 会这样思考:

  1. "SDK 的 canUseTool 应该是权限回调"(对)
  2. "返回 allow 后 SDK 应该自己执行工具"(可能对可能不对)
  3. "CLI 工具需要 stdin 交互"(猜的)
  4. "所以 Electron 里会卡死"(基于猜测的推论)

每一步都"听起来合理",但链条越长,累积错误越大。而 AI 的语气始终自信 — 它不会说"我不确定",除非被明确要求。

人类开发者遇到不确定的行为时会加断点、打日志。AI 遇到不确定时会继续推理。 这是根本差异。

防范机制:三道关卡

基于这次教训,我总结了三道关卡,用于任何涉及第三方集成的 bug 修复:

关卡 1:源码优先(动手前)

规则:有源码就不要猜。

修复涉及第三方 SDK/框架的问题时,第一步永远是:

1. 找到相关源码(node_modules、vendor、参考实现)
2. 读懂核心数据流(输入 → 处理 → 输出)
3. 确认关键字段名和类型定义

不要在没读源码的情况下写任何修复代码。"我理解这个 SDK 应该是这样工作的" — 这句话应该是红灯。

关卡 2:链路图(动手前)

规则:修 bug 前先画完整链路,确认每一环都覆盖。

AskUserQuestion 的完整链路:

系统提示词引导模型 → 模型调用工具 → canUseTool 拦截
→ UI 收集回答 → answers 注入 updatedInput → SDK 执行 call()
→ mapToolResultToToolResultBlockParam 格式化 → 模型收到结果 → 继续对话

如果动手前画了这张图,第 3 轮的"模型不调用工具"问题不会发生。

关卡 3:Hack 熔断(修复中)

规则:当你发现自己在写 hack 时,立刻停下来。

Hack 的定义:用非预期的方式绕过问题(比如用 deny 传递成功结果)。

当 hack 冲动出现时,说明你对系统的理解有缺口。正确做法:

  1. 停止编码
  2. 回到关卡 1(读源码)
  3. 重新理解系统设计意图
  4. 用系统预期的方式解决

"能跑"不等于"修好了"。 deny hack 能让 AI 继续对话,但 trace 标记为 error、tool_result 格式不标准、未来升级 SDK 时会崩 — 这些都是 hack 的代价。

给 AI 编程工作流的建议

如果你在用 AI 助手(Claude Code、Cursor、Copilot)修复涉及第三方集成的问题:

  1. 让 AI 先读源码再动手 — 明确指令:"先读 node_modules/xxx 的源码,理解设计后再写修复"
  2. 要求画链路图 — "修复前先列出完整的数据流链路"
  3. 注意 AI 的确定性语气 — 当 AI 说"这应该能工作"时,追问"你确认了吗?依据是什么?"
  4. 第二次失败时触发全面审查 — 同一个问题修两次还不对,说明理解层面有缺口,不是代码层面的小问题。此时应该要求 AI 从头梳理整个方案,而不是继续打补丁

最后一条最重要:反复修不对不是正常的。 它意味着方向可能就是错的,需要退一步重新审视,而不是在同一个方向上加更多补丁。


2026-04-02 · AgentZero × Claude Agent SDK