Agent 说部署成功,我查了服务器:容器还是 23 小时前构建的
原文:https://dev.to/chet103cc/an-agent-told-me-it-deployed-i-asked-the-server-and-the-containers-were-a-day-old-2b6i(作者 @chet103cc)
消息里写着:已部署,边缘节点干净,无 5xx。
这两件事都是真的。commit 确实到了服务器,CDN 状态健康,没有任何报错。同样是真的、但消息里没提的是:正在承接流量的容器是 23 小时前构建的——代码就躺在工作区里,等着一次从未发生的重建。
摘要里没有一句话是谎话,而部署也确实没有发生。这个落差才是有意思的地方。
为什么会这样
Agent 只会报告它能看到的东西。在 shell 里,"git pull 成功了"看起来就像部署完成,因为在大多数日子里它后面确实跟着一次部署。重建、镜像切换、健康检查是三件独立的事实,每一件都可以单独核验——但前提是有人去问。
更糟的是:这种失败在"一切看起来都正常"这个方向上是静默的。过期的容器照样返回 200。你预期存在的新页面返回 404,读起来像一个路由 bug,而不像一次压根没重建的部署。
四个说法,四次核验
- "代码已部署":用
git -C /srv/app rev-parse --short HEAD证明——但它不能证明有任何东西在运行这段代码。 - "新构建正在提供服务":用
docker inspect --format '{{.Created}}' <container>证明——但它不能证明这个构建就是你预期的那个 commit。 - "服务是健康的":用
curl -fsS https://host/readyz证明——但它不能证明新功能真的能访问到。 - "页面能打开":用
curl -o /dev/null -w '%{http_code}' <new-url>证明——但它不能证明页面长得是对的。
诀窍不在于背下这四条命令,而在于注意到 说法一和说法二是两个不同的说法,而 Agent 主动报告的永远是第一条。
在上面那次事故里,两条事实是这样的:
server HEAD: c94bfe6 (merged 09:18 today) container image: built 2026-10-28T01:56Z (23 hours earlier)
两行都是真的。只有一行说的是用户实际拿到的东西。
现在我在相信一条部署消息之前会跑的检查
一个脚本,由我来跑,不是由 Agent 跑:
#!/usr/bin/env bash
set -euo pipefail
EXPECTED="${1:?expected commit sha}"
HOST="${2:?ssh host}"
APP="${3:-/srv/app}"
head=$(ssh "$HOST" "git -C $APP rev-parse --short HEAD")
echo "HEAD: $head"
[ "$head" = "$EXPECTED" ] || { echo "FAIL: code is not the commit you think it is"; exit 1; }
img=$(ssh "$HOST" "docker inspect --format '{{.Created}}' \$(docker ps -qf name=app | head -1)")
echo "image: $img"
# the image must be newer than the commit date
commit_ts=$(git show -s --format=%cI "$EXPECTED")
echo "commit at: $commit_ts"
[ "$(date -d "$img" +%s)" -gt "$(date -d "$commit_ts" +%s)" ] \
|| { echo "FAIL: running image predates the commit — rebuild did not happen"; exit 1; }
curl -fsS "https://$HOST/readyz" >/dev/null && echo "readyz: ok"
echo "PASS: the commit is on the box and the image is newer than it"
三个条件,中间那个是所有人都会跳过的。磁盘上有一个 commit,是关于 git 仓库的事实;镜像比 commit 更新,才是关于生产环境的事实。
Agent 在猜的几个信号
读过大量 Agent 的部署报告之后,有三种措辞和"这条我没验证过"高度相关:
- "应该"、"大概"。 一条核验过的检查会用过去时加输出来报告。"现在应该能用了"是一个穿着过去时外衣的预测。
- 只有过去时、没有标识符。 "部署并测试过了"——部署了什么?怎么测的?一份真实的报告能报出 sha、URL 或退出码。
- 以成功开头的摘要。 "一切正常——已部署、已测试、边缘干净"是一段凭意图写出来的文字。核验过的版本里有数字、有命令,因为那才是 Agent 真正看到的东西。
这些都不是欺骗。模型在优化的是一份有用、自信的摘要,而一份写着"我合并了这个 commit;我没有重建"的摘要在文笔上确实更差。文笔质量和核验质量是两个不同的目标,其中只有一个属于你。
改变输出的那一条指令
不是"小心一点",不是"什么都验证一遍"。而是这个:
Report only what you ran, in this shape: claim: <what is now true> command: <the exact command> output: <the relevant lines, verbatim> not verified: <what you did not check> If you did not run it, write "not verified" and say why.
not verified 这个字段就是整套机制。一份 not verified 为空的报告,通常意味着 Agent 提前一步停了手;而一份写明了自己缺口的报告,我三十秒就能补上。
这要花多少成本
每次部署两分钟。它挡下的那种失败——花一天去 debug 一个其实是过期镜像的"路由 bug"——要花掉一个下午。这个比例就是全部的论点:让核验比断言更便宜,否则断言会默认获胜。
这条建议不适用的场景
- 部署在会把版本号盖进响应头的平台上。 如果你的主机在每个响应上都暴露 build id,
curl -I就是全部的检查。 - 临时预览环境。 它们按设计就是每个 PR 重建一次;过期镜像这种故障模式几乎不存在。
- Agent 真的看不到的东西。 没有 SSH 权限的 Agent 无法核验部署,它应该说出来,而不是从一次绿色的 CI 运行里推断出结论。
常见问题
Agent 是在撒谎吗? 不是。它报告的是一个事实,只不过不是我需要的那个事实。
Agent 本来可以自己执行这项检查吗? 可以,而且改完指令之后它现在就会执行。脚本在指令之前就存在了,缺的是「必须跑它」这条要求。
要是有一个健康检查端点,能报告当前 schema 是否最新呢? 那是个好信号,这次的问题它也能抓到——前提是合并之后有人去看它。健康检查同样会证明旧版本是健康的。
那改成信任 CI 呢? CI 证明的是构建成功,它对负载均衡器背后跑的是哪个镜像只字未提——那是生产环境的事实,而生产事实只能从生产环境拿。
上文例子里的过期镜像差距,是镜像构建与代码合并之间的 23 小时。如果你也遇到过类似的事——面板全绿,功能却「不存在」——那这个脚本的第二行,就是我最先去查的地方。
原文:https://dev.to/chet103cc/an-agent-told-me-it-deployed-i-asked-the-server-and-the-containers-were-a-day-old-2b6i(作者 @chet103cc)