自动化失败后要先判断 failure type
问题:为什么不能一律重试
自动化工作流最怕的不是失败,而是不知道为什么失败。
早期版本的 stage_with_agents.sh 只有很简单的错误处理:非零退出码就重试,重试几次还失败就放弃。这种做法有三个问题:
- 浪费时间:有些失败是环境临时问题(网络抖动、SSH 超时),重试就能恢复;有些失败是代码 bug(逻辑错误、断言失败),重试 100 次也没用。
- 掩盖真相:把不同类型的失败混在一起,事后排查时很难还原现场。
- 阻塞决策:不知道失败类型,就无法决定下一步该做什么——是重试、是修代码、还是等用户确认?
核心洞察:自动化失败不是二元事件(成功/失败),而是多维事件(哪类失败、是否可重试、下一步该做什么)。
失败类型分类系统
classify_failure() 函数把失败分成 9 种类型,每种类型对应不同的处理策略:
1. interrupted(中断)
- 触发条件:退出码 130(Ctrl+C)
- 含义:用户或系统主动中断执行
- 可重试:是
- 下一步:等用户准备好后从断点恢复
2. agent_missing(Agent 缺失)
- 触发条件:退出码 127,或错误信息包含 "command not found" / "no such file or directory"
- 含义:指定的 AI agent(kimi/claude/codex)没有安装或不在 PATH 中
- 可重试:否
- 下一步:安装 agent 或更新 agent 列表配置
3. permission(权限错误)
- 触发条件:错误信息包含 "permission denied" / "not permitted" / "operation not permitted" / "eacces"
- 含义:文件系统权限或工具权限不足
- 可重试:否
- 下一步:修复权限后重新运行
4. environment(环境问题)
- 触发条件:错误信息包含 "timeout" / "timed out" / "no space left" / "out of memory" / "killed"
- 含义:运行时环境资源不足或超时
- 可重试:是
- 下一步:释放资源或增加超时后重试
5. unclear_plan(计划不清晰)
- 触发条件:失败发生在 readiness 阶段
- 含义:PLAN.md / TASKS.md / EXECUTION.md / CONTEXT.md 文档不完整,agent 无法理解要做什么
- 可重试:是
- 下一步:补全计划文档后重新运行 readiness
6. remote_unreachable(远程机器不可达)
- 触发条件:错误信息包含 "ssh: could not resolve" / "connection refused" / "connection timed out" / "no route to host" / "remote_unreachable"
- 含义:SSH 连接失败,无法到达远程工作机器
- 可重试:是
- 下一步:检查网络/SSH 配置/远程机器状态
7. dirty_worktree(工作目录不干净)
- 触发条件:错误信息包含 "dirty worktree" / "uncommitted changes" / "would be overwritten"
- 含义:Git 工作目录有未提交的修改,自动执行会覆盖本地改动
- 可重试:取决于用户意图
- 下一步:用户决定是提交改动、丢弃改动、还是手动接管
8. test_failed(测试失败)
- 触发条件:错误信息包含 "pytest" / "test failed" / "tests failed" / "assertionerror" / "failures="
- 含义:代码逻辑有 bug,自动化测试不通过
- 可重试:是(修完 bug 后)
- 下一步:修复测试失败的原因,重新运行验证
9. confirmation_required(需要人工确认)
- 触发条件:错误信息包含 "confirm" / "approval" / "credential" / "secret" / "deploy" / "migration" / "destructive"
- 含义:下一步操作有风险(部署、数据库迁移、删除数据),需要用户显式确认
- 可重试:否(除非用户确认)
- 下一步:暂停,等用户审查并确认
10. agent_failed(Agent 执行失败)
- 触发条件:以上都不匹配
- 含义:AI agent 运行时出错,但具体原因不明确
- 可重试:是
- 下一步:检查 agent 输出,更新计划状态,换一个 agent 重试
分类如何驱动决策
失败分类不是为了让日志好看,而是为了驱动自动化决策。
retryable:是否重试?
failure_retryable() 函数根据失败类型决定是否值得重试:
failure_retryable() {
case "$1" in
unclear_plan|test_failed|agent_failed|remote_unreachable|interrupted|environment) printf 'true' ;;
*) printf 'false' ;;
esac
}
可重试的失败:interrupted、environment、unclear_plan、remote_unreachable、dirty_worktree、test_failed、agent_failed
不可重试的失败:agent_missing、permission、confirmation_required
设计逻辑:
- 可重试的失败通常是环境临时问题或可修复的问题(计划不完整、测试失败、agent 出错)
- 不可重试的失败通常是配置错误或需要人工介入(agent 没装、权限不够、需要确认)
next_action:下一步做什么?
failure_next_action() 函数根据失败类型和当前角色,生成人类可读的下一步指导:
failure_next_action() {
local failure_type="$1"
local role="$2"
local agent="$3"
case "$failure_type" in
agent_missing) printf 'Install or expose agent command %s, or override fallback list for role %s.' "$agent" "$role" ;;
environment) printf 'Fix local runtime/resource issue, then rerun from %s.' "$role" ;;
permission) printf 'Fix filesystem/tool permission, then rerun from %s.' "$role" ;;
unclear_plan) printf 'Harden PLAN.md, TASKS.md, EXECUTION.md, CONTEXT.md, TASK_GRAPH.json, then rerun readiness.' ;;
remote_unreachable) printf 'Check SSH/network/inventory for target machine, then regenerate remote handoff.' ;;
dirty_worktree) printf 'Inspect git status, preserve useful partial work, then rerun intentionally or take over.' ;;
test_failed) printf 'Inspect failing tests, fix required issues, rerun verification, then resume.' ;;
confirmation_required) printf 'Get explicit user confirmation before continuing.' ;;
interrupted) printf 'Resume from interrupted role when ready.' ;;
*) printf 'Inspect output, update plan state if needed, then retry role %s or choose another agent.' "$role" ;;
esac
}
关键设计:next_action 不是自动执行的,而是写给用户看的。自动化系统把失败类型和下一步建议写到 FAILURE.json 和 STAGE.md,用户可以根据这些信息快速决策。
实际案例:jp013 的 failure type 实践
案例 1:agent_missing
场景:在 mac 机器上运行 stage_with_agents.sh,但 mac 上没有安装 kimi CLI。
失败输出:
/path/to/stage_with_agents.sh: line 123: kimi: command not found
分类结果:agent_missing
自动化处理:不重试,直接报错的把 next_action 写到 FAILURE.json:
{
"failure_type": "agent_missing",
"retryable": "false",
"next_action": "Install or expose agent command kimi, or override fallback list for role plan."
}
人工处理:用户在 mac 上安装 kimi,或者设置 JF_PLAN_AGENT=codex 改用本地 codex。
案例 2:remote_unreachable
场景:apple64 要给 mac 派任务,但 mac 的 SSH 服务没启动。
失败输出:
ssh: connect to host mac.local port 22: Connection refused
分类结果:remote_unreachable
自动化处理:标记为可重试,写入 FAILURE.json,等待下次重试。
人工处理:用户检查 mac 的 SSH 配置,启动 SSH 服务,然后重新触发任务派发。
案例 3:test_failed
场景:jp013 的自动化测试发现 GitHub API 调用逻辑有 bug。
失败输出:
pytest tests/test_github_api.py::test_list_tools -v
FAILED tests/test_github_api.py::test_list_tools - AssertionError: expected 200, got 404
分类结果:test_failed
自动化处理:不自动重试(因为测试失败说明代码有 bug,重试没用),写入 FAILURE.json 并附上测试失败详情。
人工处理:用户检查测试代码和被测试代码,修复 bug,然后重新运行测试。
案例 4:dirty_worktree
场景:自动化流程要在 /Volumes/jf01/Documents/Video/00feishu/github-projects 目录里执行 git 操作,但这个目录有未提交的修改。
失败输出:
error: cannot pull with dirty worktree
分类结果:dirty_worktree
自动化处理:标记为"视情况可重试",写入 FAILURE.json,暂停执行。
人工处理:用户有三种选择:
- 提交改动:
git add -A && git commit -m "..." && git push - 丢弃改动:
git reset --hard origin/main - 手动接管:不在自动化流程里跑,改成手动执行
决策链:从失败到恢复
完整的失败处理决策链:
1. Agent 执行失败(非零退出码)
↓
2. classify_failure() 判断失败类型
↓
3. failure_retryable() 判断是否值得重试
↓
4. failure_next_action() 生成下一步建议
↓
5. 写入 FAILURE.json + STAGE.md
↓
6. 如果是可重试的失败 → 等待下次重试(或立即重试)
如果是不可重试的失败 → 暂停,等用户介入
↓
7. 用户根据 FAILURE.json 和 STAGE.md 决策
↓
8. 修复问题后,从失败的 role 重新开始
关键设计原则:
- 快速失败:一旦分类出不可重试的失败,立即停止,不浪费时间
- 保留现场:把所有失败相关信息写到
FAILURE.json,方便事后排查 - 人类可读的 next_action:下一步建议不是机器指令,而是人类能看懂的操作指导
- 状态可追溯:
STAGE.md记录每次失败的 failure_type、retryable、next_action,形成完整的失败处理历史
下次我会怎么做
-
所有自动化脚本都应该有失败分类:不仅仅是
stage_with_agents.sh,任何有重试逻辑的自动化脚本都应该先分类失败,再决定是否重试。 -
failure_type 应该是结构化数据:不要用字符串匹配(grep)来分类失败,而应该用结构化的错误码或错误对象。但Shell 脚本里用 grep 是务实的妥协。
-
next_action 要写到人类能看懂:不要把错误信息直接暴露给用户,而要翻译成"下一步该做什么"的指导。
-
可重试 ≠ 应该立即重试:有些失败虽然技术上可重试(比如
remote_unreachable),但立即重试大概率还是会失败。应该加上退避策略(exponential backoff)或者重试次数上限。 -
失败分类要可扩展:新增失败类型时,不能改
classify_failure()的主体逻辑,而要像现在这样用case语句,方便扩展。
总结
自动化失败处理后,第一步不是"重试",而是"分类"。
分类的价值:
- 避免无效重试(浪费时间)
- 加速问题定位(知道是哪类失败)
- 驱动正确决策(该重试还是该人工介入)
分类的方法:
- 根据退出码(130=中断,127=命令缺失)
- 根据错误信息关键词(permission denied、timeout、test failed)
- 根据失败发生的阶段(readiness 阶段失败 = 计划不清晰)
分类的结果:
failure_type:失败类型retryable:是否可重试next_action:下一步建议
这三元组构成自动化失败处理的最小决策单元。没有这个单元,自动化就是"盲人摸象"——只知道失败了,但不知道为什么失败,也不知道该怎么办。