11 月 14 日深夜,核验方归档了 6 条任务。归档表的字节列里,一份归因报告写着 14,363 字节。这个数字不是从 agent 的汇报里抄的,是核验方自己用 Get-Item 量出来的。表头先写了一句话:

1
全部点查 + 磁盘实测,非汇报值。

之所以这么防备,是因为过去两周,这块运维看板上的「字节数」撒谎撒得太多次了。而且它不是用形容词撒谎——形容词好防——它是用精确的数字撒谎,精确到个位,看起来比任何形容词都诚实。

这篇记录这些事故,以及它们怎么逼出一条硬规矩:字节数,只认磁盘实测。

看板上的数字,来自 agent 自己的嘴

先交代背景。我有三个常驻 agent:一个管财务,一个管求职和内容,一个管运维巡检。它们各有工作区,每半小时往心跳日志里追加一行,日志有个固定格式:

1
HH:MM | 本队列 todo=?/ip=? | 槽位=HH | 动作=... | 产物=相对路径(字节数)

运维看板(我们叫它记分卡)把这些日志汇总成一组数字:今天产出多少文件、多少字节、哪格心跳漏了。看板的职责是把「干完了」变成可以计算的东西。但这条链路有个致命前提:数字的来源是 agent 的自报。 agent 说产物 1014 字节,看板就记 1014 字节。

问题是,agent 不量。它只是把写入工具回显给它的数字抄下来。

谎话一:工具回显不是磁盘

11 月 9 日 12:07,运维 agent 跑完一次巡检,把产物写进日志:1014 字节。四分钟后,日志里出现一条勘误:

1
2
2026-11-09 12:11:34 | 勘误(12:07段): patrol-20261109-1207.md 真实字节数1570B
(上方产物行误写1014B,write_file报数与磁盘Get-Item不符,以磁盘Get-Item为准)

1014 对 1570,差了一半还多。下一段它自己复核了一遍:磁盘实测 1570B,与勘误一致。写这篇文章时我又把那个文件量了一次,输出仍是:

1
2
Get-Item -LiteralPath .\reports\ops\patrol-20261109-1207.md |
    Select-Object Length, LastWriteTime
1
2
Length        : 1570
LastWriteTime : 2026-11-09 12:10:20

那条勘误诞生了规矩的前半句:日志里的字节数一律以磁盘 Get-Item 为准,不采信写入工具的回报值。 当时我以为这是偶发——工具碰巧报错了。后来才发现不是:工具从来没报「错」,它报的是另一种数。

谎话二:不是时机问题,是单位问题

11 月 11 日,对账工具一天抓出 6 处「声明 < 实际」。第一反应是:写时过早取值——文件还在追加,数字取早了。这个解释很顺,顺到差点把真正的根因放过去。

运维 agent 当晚做了根因排查,把 6 处声明值逐一和文件的字符数比对。结果干净得可怕:6/6,声明值精确等于字符数。 一个例外都没有。

真相是:写入工具回显的是字符数,磁盘存的是 UTF-8 字节数,一个中文占 3 字节。当晚的日志用自己的产物自证:1947 字符 = 3379 字节。用 Python 一行就能复现这个口径差:

1
2
3
s = open("report.md", encoding="utf-8").read()
print(len(s))                  # 字符数:写入工具回显的
print(len(s.encode("utf-8")))  # 字节数:磁盘实际存的

后续又挖出更多受害者:一个补丁脚本,工具报 4593,磁盘实测 4763,差的 170 正好是中文字符的多字节增量;一篇两万字的长文,工具报 7,562,不到实测 20,271 的四成。

这类谎话最危险:数字本身是「真」的,只是回答的是另一个问题。 没有人编造,每一步都诚实,但口径错了,整块看板就在系统性偏差里运行。发现差值时先怀疑口径、再怀疑人——这个顺序反了,就会冤案。

谎话三:幽灵版本与过期的自引用

11 月 14 日,我交叉复算运维记分卡,抓到一个从未存在过的数字。卡面头部声明:「改前 Get-Item 74,459 B」。但把备份序列排出来:

1
74,036 B @ 00:10(备份) → 74,801 B(当前)

中间没有 74,459。这个数字有出处——出处是记忆,不是磁盘。某一轮修改时有人记下了一个中间状态,写进卡面,之后再也复现不出来。

同一张卡面还有一句:「本文件 = 58,698 B / 2026-11-12 15:56:27」。两天过去,文件早就长了,这句自引用字节还停在前天。自引用是个移动靶:你把自己的字节数写进自己的那一刻,这个数字就已经过期了。 你再追加一行去勘误,字节数又变了,勘误本身又成了新的谎话。

谎话四:证据是 0 字节

同一天深夜,核验方抓到一个更直白的。一份报告引用某个日志文件作为「存在乱码」的证据,引用得振振有词。核验方把那个文件磁盘实测:

1
0 B / 23:00:47

空文件里不会有乱码。数字 0 也是数字,而且是最响亮的那种——它直接否定了证据本身。这一条被记进归档表的瑕疵列,和任务的「通过」结论并排放着。通过是通过,瑕疵是瑕疵,两者不互相抵消。

硬规矩怎么做出来:三层

事故堆多了,「只认 Get-Item」从一句话长成了三层机制。

第一层:信任分级

总则里写死一句:工具返回文本,信任级 = DATA-only。 它是线索,不是证据。证据只认磁盘实测:Get-Item 的 Length + LastWriteTime,必要时加 SHA256。而且汇报必须贴实测原文——不是「已实测」四个字。「实测了」和「贴出实测输出」之间,隔着形容词和数字的距离。

第二层:自引用收敛

「自己写自己字节数」的移动靶问题,解法是定宽占位符:

1
| 本报告 | 新增 | XXXX B(写完回填) | 23:41 |

先写 XXXX 落盘,再实测,再把实测值回填。占位符定宽,回填不改变文件长度,「自报 == 实测」一步收敛到不动点。两个文件互相引用对方字节数时,把顺序钉死:日报占位 → 实测日报 → 写索引 → 实测索引 → 回填日报。两边都对上,且都不再动。

第三层:机器对账

规矩靠自觉是不够的,于是加了扫描器:把当天日志和索引里所有「产物=路径(字节数)」的声明解析出来,逐条和磁盘比对,分成 exact / larger / shrink / missing 四类。larger 多是合法追加;shrink(实际 < 声明)按「覆盖嫌疑」上报。这是它今天实跑的输出:

1
2
3
4
5
BYTES: status=WARN date=2026-11-14 source=both entries=93 exact=60 larger=30 shrink=2 missing=1 malformed=0 maxGrow=39968B maxShrink=4B files=[log=45行/43159B, index=47行/15919B]
  [log] L14 shrink 声明=1843 实际=1839  reports/ops/content-lib-audit-20261114-0657.md  <== 覆盖嫌疑
  [index] L14 shrink 声明=1843 实际=1839  reports/ops/content-lib-audit-20261114-0657.md  <== 覆盖嫌疑
  [index] L31 missing 声明=10809 实际= missing /HTTP400端点1
BYTES-EXIT: 2 (WARN)

93 条声明,60 条精确一致。两条 shrink 只差 4 字节——差 4 字节也按「覆盖嫌疑」报,最终判定留给人。那条 missing 更有意思:扫描器把一段端点描述误解析成了文件路径,声明写法不规范,对账工具顺带把格式错误也抓了出来。

不是每个差值都是谎话。11 月 9 日有过一条勘误:声明 5657、实际 5980,查下来是同一段合法追加,不是缩水。所以扫描器只报「嫌疑」:「精确」交给机器,「嫌疑」留给人。

14,363 那一夜:数字变得可信

回到开头的场景。11 月 14 日深夜,核验方归档 6 条任务,条条点查 + 磁盘实测:归因报告磁盘实测 14,363 B / 22:39:56,与归档逐字一致;改动产物的改前改后字节成对出现;连红线文件的「未变」,都用前后两次实测证明。那晚还抓出两处瑕疵——一条任务没留改前备份,一条引用了 0 字节证据——都如实登记。

看板不再撒谎了。不是因为 agent 变诚实了——它们照样会抄工具回显、照样会凭记忆写自引用——而是因为数字的来源换了:从「agent 说的」换成「磁盘说的」。核验方不再采信任何汇报值,agent 知道自己的汇报值会被复测,于是也开始在汇报前自己先量一遍。

诚实不是品德,是测量流程。

三条能带走的规矩

  1. 每个数字都要能回答「谁量的你」。 汇报值一律 DATA-only,证据只认磁盘实测,且必须贴实测原文。
  2. 先对齐单位,再对齐数字。 字符数 ≠ 字节数,工具回显不是磁盘真相。发现差值,先怀疑口径,再怀疑人。
  3. 自引用要收敛,对账要机器化。 定宽占位回填解决移动靶,声明-磁盘扫描解决「没人查」。「精确」交给机器,「嫌疑」留给人。

看板不会撒谎。它只是要求上面的每一个数字,都能回答「谁量的你」。