原文:https://dev.to/kenielzep97/the-detector-reported-zero-because-it-only-had-one-item-ni0(作者 @kenielzep97)
两条指令进入了我和我的智能体协作者共同构建的 Auditor——这个工具用于暴露智能体指令文件中的冲突。部署权限(deployment authority)是它明确知道如何判断的九个领域之一。
Never deploy without human approval. Auto-deploy the moment tests pass.
在 main 分支的 `172d962` 提交上,返回结果如下:
posture: low_observed_risk
counts: {"items": 1, "labels": {"governs": 1}, "risk_high": 0,
"conflicts": {}, "gates": 0, "authority_categories": 0}
low_observed_risk 是产品自己的字符串,来自 agents/report_writer.py:110。不是我对输出的概括,是输出本身。
只有一项。两两比较(pairwise comparison)步骤从未收到过一对,而我的检测器不会拿一项和它自己比较。
修复之后,同样的输入,在线服务返回:
{
"severity": "high",
"item_id": "M001, M002",
"type": "authority_collision",
"finding": "Conflicting governing instructions in deployment: require_human_approval vs allow_automatic.",
"evidence": "Never deploy without human approval. | Auto-deploy the moment tests pass."
}
posture: needs_review。两项,一个高危冲突,一个验证门。
这两次输出之间的差异,在于两行指令当时是紧挨着的。
失败实际发生在哪里
在进行任何比较之前,必须先有一步把文本拆成单独的指令。我的实现会把没有项目符号且紧挨着、中间没有空行连续排列的行合并成一项。
这个合并逻辑在工具的第一个提交 `b71892d` 里,提交时间是 2026-06-01 13:06:47 -0400,位于 agents/memory_extractor.py 第 50–51 行:
content = " ".join(part.strip() for part in pending_paragraph if part.strip()) if len(content) >= 36:
本文涉及的两个缺陷都在这两行上,而且从第一个提交起就在。三个月了。
零发现(Zero findings)是一个关于被合并群体的答案。 两两比较的循环完全按代码逻辑执行,只是从未拿到过一对。
我发布这类缺陷的文章已经有三个月了:在你确认什么到达了计数器之前,零计数毫无意义。我写下了那句话,然后发布了一个在这个问题上犯了错的工具,并且三个月都没有发现。
缺陷的实际影响范围
检测器知道九个领域:部署权限(deploy authority)、密钥处理(secrets handling)、数据库事实源(database source of truth)、访问范围(access scope)、客户响应(customer response)、日志保留(log retention)、账单记录(billing records)、退款(refunds)、升级上报(escalation)。全部是手写的。
(其中七个位于一张一眼就能看完的立场表里。退款和升级上报是按阈值比较而不是对立立场比较,所以如果你想找九个的列表,你会看到七个加两个函数。)
精确的影响范围——因为更宽泛的说法是错的:任何两侧写成了相邻、无项目符号、且中间没有空行的指令对,在九个领域的任意一个中,都可能在被比较之前就被合并。 这不是"九个领域被禁用了"。带项目符号的指令对,或者中间有空行的指令对,整个过程都能正常提取、正常比较。脆弱的是书写形态,而不是领域。
我自己在修复提交里写的说明比这篇文章说得更糟——说连续行"悄悄禁用了九个领域的冲突检测"。那个措辞范围太宽了。我在这里纠正,而不是去改写提交信息。
关于修复,有三件事比修复本身更值得说
第一件:测试本身也接受了测试。
这次修复新增了七个回归测试。在已修复的代码上通过并不能证明它们能区分旧行为与新行为,所以我们让它们直面缺陷:把修复暂存起来,只恢复缺失的常量让导入得以解析,然后对旧逻辑重新运行。
七个里有四个失败了。 是真实的行为失败,不是导入错误。
但四条红线不是四个证明,原因比数量更重要:
adjacent_instructions_do_not_merge:失败于assert 1 == 2—— 是。两行被粘成一个条目。enumerated_domain_still_produces_a_real_collision:失败于assert []—— 是。没有配对存活,冲突无法触发。short_high_risk_instruction_is_not_silently_discarded:失败于assert 0 == 1—— 是,但只是因为注入的常量是12。注入36则会在assert 36 <= 16失败,在到达丢弃逻辑之前就先因常量而失败。governing_instruction_..._reports_uncovered_domain:失败于assert 'uncovered_domain' in set()—— 不是。旧的main在检测器中根本没有uncovered_domain。那个失败是缺少新代码,而非提取坍缩。
这四个中有两个证明了提取 bug。有一个只在正确的常量下才能证明。有一个完全不能证明。 一个因错误原因而失败的阴性对照,恰恰就是这篇文章所讨论的缺陷,所以我宁愿把这张对照表完整列出来,也不让四条红线承担超出它们应得的重量。
第二件:第一次部署在一个错误的层级上成功了。
gcloud 报告了真相:Web 服务已部署并为 100% 的流量提供服务。这很准确。我读作行为已经改变。并没有。
Web 应用是一个路由器。提取运行在 memory-extractor-agent 中,这是一个独立的 Cloud Run 服务。修订时间戳就是凭证:
memory-authority-auditor-web-00003-82f 2026-09-02T13:17:10.714842Z memory-extractor-agent-00002-grk 2026-09-02T13:31:32.807334Z
十四分二十二秒里,一条正确的成功消息端坐在未改变的行为之上。我们之所以发现,是因为测试了端点,而不是去读部署消息。这个项目所研究的错误原因模式,在修复工具几分钟之后就活生生出现在我们自己的发布流程里。凭证没有撒谎。我对凭证覆盖范围的解读才是。
第三件:我添加的是"缺失",而不是"域"。
最初触发问题的输入是一对不同的配对——发布与验证——而明显的修复是教工具关于发布的知识。我们没有这样做。
把规则集调优到正好匹配某人刚交给你的案例,只能证明它能捕获已知案例。因此检测器现在会在治理指令完全不匹配任何规则时发出 uncovered_domain:
该指令治理行动,但未匹配任何矛盾规则,因此未对冲突进行评估。这里的冲突缺失是检查的缺失,不是一致的证据。
之前,"无冲突"与"从未检查"呈现为同一句话。现在它们分开了。
最初的配对仍然没有解决,线上输出证明了这一点
这是最初触发问题的输入,现在对修复后的线上服务运行:
"items": [
{"id": "M001", "text": "Current policy: verify the live artifact before publishing."},
{"id": "M002", "text": "Old note: publish immediately without checking."}
],
"classifications": [
{"id": "M001", "authority_label": "governs", "confidence": 0.78},
{"id": "M002", "authority_label": "context_only", "confidence": 0.64}
],
"conflicts": [
{"severity": "medium", "item_id": "M001", "type": "uncovered_domain",
"finding": "This instruction governs action but matched no contradiction rule, so it was NOT evaluated for conflicts."}
]
posture: usable_with_gates。提取已修复——两个条目,正确拆分。它仍然不是 authority_collision,在发布成为列出的域之前,永远不会是。
直到为写这篇文章把它粘贴出来,我才发现那段 JSON 里还有第二个缺口。`M002` 被分类为 `context_only`,置信度 0.64。 "不检查就立即发布"是一个祈使句,而分类器不认为它足够强到可以治理。因此 uncovered_domain 警告只对 M001 触发。告诉你跳过检查的那一半矛盾完全没有警告,因为 context_only 条目没有资格获得警告。
提取修复是真实的。它把这个输入从一个静默条目变成了两个可见条目和一条诚实的警告。它并没有让工具对这个配对做出正确的判断。
哪些已修复,哪些没有
已修复: 搞定了。相邻行上无项目符号的指令,当第一行以句末标点结尾时,不再合并。最小条目长度从 36 个字符降到 12 个,因为旧下限会静默丢弃无项目符号的指令 Delete all logs.——十六个字符、高风险,被丢弃时没有任何记录。而有项目符号的行完全绕过了这个下限,所以同样的文字作为列表项保留下来,作为句子却消失了。
未修复,附上证据:
- 独立无项目符号片段如果不足 12 个字符,仍会无记录地消失。
Wipe logs.有十个字符,返回零个条目。See above.也一样——该下限不区分命令与交叉引用,只会把两者都删掉。 - 现在拆分逻辑会对硬换行的文本过度触发。 我之前声称换行文本是安全的,因为换行的行不以句末标点结尾。那是对换行的赌注,而不是证明,反例如下:
"Escalate to the on-call engineer within 15 min.是一个两步升级流程,修复版本却把它返回为两个独立条目。过度拆分比合并更安全,因为两个条目仍然可以比较。但这仍然是一个错误。
Then page the team lead if unresolved." - 仍然只有九个冲突域。域之外的一条治理指令会产生
uncovered_domain,它命名了缺口,但没有命名冲突。域之外的一个context_only条目则什么也不会产生——见上面的 M002。
复现
线上应用的两个主机名路由到同一个服务;我用相同的 payload 检查过,响应完全一致,所以不存在会让人踩坑的过期分层。
修复以分支形式公开,不在 main 上:位于 `fix/extraction-merge` 分支上的 `beae0bb`。共三个文件,146 处新增,1 处删除。`main` 仍然是 `172d962`,仍然携带 `len(content) >= 36`,因此默认克隆得到的是有缺陷的版本。我这样明说,而不是让一个绿色链接暗示整个仓库都已经更新。
该分支上的测试套件:在干净克隆中 107 通过,1 跳过,1 xfailed。我的工作副本显示 108,因为一个溯源测试会找到隔离克隆中不存在的工作区文件。克隆得到的数字,才是应该印在克隆命令旁边的诚实数字。
运行阴性对照:
git clone https://github.com/keniel13-ui/memory-authority-auditor
cd memory-authority-auditor
git fetch origin fix/extraction-merge
git checkout FETCH_HEAD -- tests/test_extraction_boundaries.py
# 只恢复新测试导入的常量,这样测试收集(collection)才能成功。
# 下面的旧提取器仍使用自己硬编码的 >= 36 逻辑。
python3 - <<'PY'
from pathlib import Path
p = Path("agents/memory_extractor.py")
t = p.read_text()
assert "def extract_memories(" in t
p.write_text(t.replace("def extract_memories(", "MIN_ITEM_CHARS = 12\n\n\ndef extract_memories(", 1))
PY
python3 -m pytest -q tests/test_extraction_boundaries.py
4 失败,3 通过——注意上述表格中的注意事项。
为什么这不只是单个工具的问题
Agent 指令文件会积累由不同人在不同时间写下的规则。随着文件增长,这些矛盾越来越难在工作记忆中保留。
构建这样一个工具的理由,是帮助人类保持操作者的位置——让不断增长的指令集更容易被检查,而不是取代检查,也不是证明没有遗漏任何东西。
这意味着,做这份工作的工具需要承受它自己施加的那种审查。 我们的工具三个月没有得到这种审查,最终发现问题的不是测试套件,而是 Kairos——本项目中的一个独立在线 agent 席位——把两行内容粘贴进已部署的服务并读取返回结果。
去搞坏它
它已经上线,接受文本输入:https://memory-authority-auditor-web-qfppqeeedq-uc.a.run.app
粘贴一份指令文件、一组智能体规则、一份策略文档,任何包含规则的内容都可以,这些规则可以是在不同时间写下的。无需注册。应用不会持久化你粘贴的内容——它在内存中处理并返回结果。但我无法保证 Google 不会在平台层记录日志,所以不要粘贴任何你不希望出现在云端访问日志里的内容。
我想要的是它漏判的案例。 两条明显矛盾的指令,它却返回 low_observed_risk 或 uncovered_domain 而不是冲突。我已经知道四种能击败它的形态,每一种都在这篇文章里:超出九个域的任何内容、少于十二个字符且不带项目符号的片段、任何被分类器评为 context_only 的命令式语句,以及句号后强制换行的过程说明。还会有更多。 最初触发这个问题的输入,已经存在了三个月,并且通过了全套绿色测试。
把你输入的内容和它返回的结果发出来。对我而言,漏判比命中更有价值。
两行输入来自 Kairos,一个独立的在线智能体席位,用于测试已部署的服务——并非外部用户或客户。实现、测试和部署均是在我的指导下由智能体协作完成的。既有的测试都没有覆盖过这种输入形态。
关于我自己的提交,有一点需要更正,因为我正请你去读它。修复提交信息和回归测试的文档字符串里都写着缺陷是“由外部读者发现”。这不够准确。我的本意是“工具自身测试套件之外”,但顺着链接读的人很自然会理解成外部人员。其实不是。我保留原提交,并在这里作出更正,而不是强制推送覆盖,因为重写提交历史,比保留一条不准确的记录并附上公开更正更糟。
原文:https://dev.to/kenielzep97/the-detector-reported-zero-because-it-only-had-one-item-ni0(作者 @kenielzep97)



