让 Agent 写代码时,最容易出现的叙事是“模型不够聪明”:需求理解错了、上下文丢了、工具调用不稳定,所以任务失败。
但我把自己本机的会话日志做了一次归因,看到的画面完全不同。2026-09-10 的那次扫描覆盖 Claude 和 Codex 两条链路。Codex 侧有 2,281 个会话、约 4.33 万次 exec、3,624 次失败;Claude 侧有 587 个会话、4.97 万个工具结果、1,431 次失败。按当时的错误模式和修复路径归类,大约六成失败与环境前置有关,和业务代码无关。
这不是一个可以外推到所有机器的统计实验。它来自一台真实工作站、一组真实项目和一次自动归因,样本也会被同一类重复错误放大。但它足够说明一个问题:Agent 的第一道业务逻辑,不在代码里,而在它有没有获得一个可解释、可重复的执行环境。
一、非登录 shell:二进制存在,但 PATH 里没有
最常见的失败长得很荒谬:bun、gh、psql 明明已经安装,命令却返回 command not found。
原因是 Agent 启动的通常是 non-login shell,不读取 ~/.zprofile。而本机的 ~/.zprofile 第一行是 eval "$(brew shellenv)",PATH 还没补齐,命令查找就已经失败。
另一个更隐蔽的问题是版本遮蔽:~/.local/bin/node 可能先于 nvm 的 Node 出现在 PATH 里。于是一个 node 能运行,另一个 node 才是项目需要的版本;pnpm 报“至少需要 Node.js 18”,看起来像 pnpm 坏了,实际是拿到了错误的运行时。
当时有效的修复不是在每个任务里临时 source nvm,而是把环境初始化放到 non-login shell 也会读取的位置,并让 agent-env 根据 .nvmrc 明确选版本、调整 PATH。
这件事给 Agent 的教训很直接:不要用“我这边能跑”验证环境。 要在 Agent 真正使用的 shell 类型、PATH、工作目录和用户权限下验证。
二、代理不是越早设置越好
网络失败出现时,人的第一反应常常是“把代理加上”。但如果当前环境已经有一个可用代理,强行覆盖成某个固定端口,会把本来正常的网络打断。
Git 还有一个额外陷阱:~/.gitconfig 里的 http.proxy / https.proxy 优先级高于环境变量。只要全局配置钉着一个没有监听的端口,沙箱注入的代理再正确,git 仍然会去连那个死端口。测试里,git ls-remote 直接失败;临时让 GIT_CONFIG_GLOBAL=/dev/null 后恢复正常。
最终规则应该反过来:
- 先直接执行;
- 只有看到明确的连接失败或证书错误,才设置代理;
- Git 的代理优先由环境变量控制,不在全局配置里永久钉端口;
- 报证书错误时,先判断是 HTTP 代理、TLS 根证书还是 macOS 平台校验,不要把所有网络错误都归到代理。
把 7890 写进所有配置看起来像是在“修网络”,实际是在制造一个新的单点故障。
三、文件锁:git 写命令需要和清锁发生在同一条链路里
在装有文件系统代理的机器上,.git/worktrees/<name>/index 被外部写入后,可能预先生成 0 字节的 *.lock 占位文件。git 下一次使用 O_EXCL 创建锁时就失败:
fatal: Unable to create '...lock': File exists
这不是代码冲突,也不是远端权限问题。处理方式也不是手动删掉所有锁文件,因为非 0 字节的 index.lock 可能是一次中断的 git add 留下的完整暂存索引,删掉会丢现场。
比较稳的做法是使用一个 gitw 包装器:先清理没有活跃 git 进程时的 0 字节锁,再立刻执行真正的 git 写命令。清锁和写操作必须在同一条 shell 命令里连续完成,中间插入其他步骤,占位锁可能又生成。
这类问题会严重误导 Agent:它看到 commit 失败,可能开始检查用户身份、分支、远端认证,甚至尝试改写历史。正确的第一步应该是识别错误类型,确认这是锁创建失败,而不是仓库或业务问题。
四、包管理器不是普通子进程
pnpm 在沙箱里失败得很典型:项目注册 symlink 会遇到 EEXIST,报 CODEBUDDY_BROKER_DENY。这个 symlink 出现在 pnpm 的上下文初始化阶段,所以几乎任何子命令都可能失败。
解决办法不是全局 unset NODE_OPTIONS。那会让后续所有命令都失去文件写保护,包括 rm。正确做法是只对本次包管理器调用使用 nobroker:去掉这一条子进程链上的 shim 和 broker hook,shell 自身仍保留保护。
另一个看似无关的问题是 TTY。非交互 shell 下,pnpm 可能拒绝重装 node_modules,报 ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY。这时加 CI=true 比伪造一个终端更稳定。
原则可以概括成两句话:
- 只在必要的最小进程树上降级保护,不要修改整个 shell。
- 包管理器的错误可能来自权限、TTY、代理、Node 版本或文件系统 hook,不要只盯着 lockfile。
五、macOS 沙箱里,Go 工具可能需要一条 mach-lookup 权限
还有一类错误几乎不可能从应用层猜出来:gh 在沙箱中恒报 TLS 证书无法验证,x509: OSStatus -26276。
curl 和 git 正常,只有 Go 工具失败。根因不是证书过期,也不是代理没设,而是 Go 在 macOS 上会走 Security.framework 的平台校验器;验证 443 证书需要访问 com.apple.trustd.agent,而沙箱没有放行这个 mach lookup。
因此下面这些常见修复都无效:
- 设置
SSL_CERT_FILE; - 使用 fallback roots;
- 给命令再加一层代理。
正确处理是给沙箱配置精确的 mach lookup 权限,而不是把 com.apple.* 整体放开。安全边界应该逐个服务增加,每次都说明为什么需要、影响是什么。
这也说明 Agent 的“网络正常”不能只用 curl 验证。不同语言、不同 TLS 栈、不同沙箱策略可能得到完全不同的结果。
六、还有一类失败发生在初始化脚本本身
pyenv rehash 在受限文件系统里会失败;补全脚本如果被第三方初始化器重复触发,可能向用户目录写 ~/.zcompdump,在沙箱里触发大量审批并留下孤立文件。
这些问题看起来都属于“shell 环境”,但它们有一个共同特征:命令还没进入业务,就已经在执行副作用。
Agent 很难从最终错误反推出这是初始化脚本写的文件,因为报错可能只是某个命令启动失败。最好的修复也不是让 Agent 多点几次“允许”,而是把初始化改成幂等、可重入、写入固定缓存目录,并尽量避免交互式门控。
七、我给 Agent 的开工前检查清单
现在我会把环境当成项目的一部分,先过下面这些条件:
- Shell 类型明确。 知道当前是 login、non-login 还是交互式 shell,关键初始化文件是否会被读取。
- 运行时版本可解释。
node、python、bun、pnpm来自哪里,和.nvmrc是否一致。 - 网络先直连。 代理只在明确失败后启用,Git 配置不永久钉死端口。
- 写操作有包装器。 Git 锁、文件系统占位锁、权限降级都有明确的入口。
- 包管理器环境可重复。 非交互 TTY、CI 标志、broker hook 和项目注册路径都在验证范围内。
- 错误按层归因。 先分 shell、PATH、网络、权限、锁、运行时、业务,不要看到一个红字就改代码。
- 无法确认时 fail closed。 尤其涉及沙箱降级、生产环境和凭据时,不知道就停止,不靠猜继续。
最后
Agent 写得快,不代表它已经在一个可靠的环境里工作。真正让任务反复失败的,可能不是模型不会重构,而是它连正确的 node 都没拿到,连 git 的锁都创建不了,或者网络请求根本走错了 TLS 栈。
Prompt 决定 Agent 想做什么,环境决定它能不能把这件事变成事实。
把失败日志当成系统日志看,而不是当成聊天记录看,很多“模型问题”会立刻变成可修复的工程问题。
COMMENTS
读者评论