文章建议分别记录工具原始输出、适配器处理结果和模型收到片段的 SHA-256 与字节长度,并通过代码推导变化发生的位置。示例为教学构造,强调只哈希工具结果片段,避免完整提示词的无关变化干扰定位。
一位夜班运维人员打开了同一次 tool call 的两份日志。进程日志里是合法的 JSON,包含两行数据。但在下一轮对话中,模型却声称工具结果无法读取。
这个场景是为教学重构的,并非引用某次真实故障。下文中的数字来自测试样例,并非生产环境中的实测指标。
工具进程写出第一段字节串。接着,adapter 对这段字节串进行解析、包装或脱敏。最后,prompt builder 将其中一个片段复制到下一条模型消息中。
任何一个责任方都可能修改字节,而工具名称保持不变。只存一个 hash,无法判断是谁做了修改。你需要在每个边界分别记录一个摘要,以及该边界处的长度。
把这些字段放在同一条 tool call 记录中。用代码计算 mutation_hop,不要在编辑器里手动填写。后续查看记录的人,应该能根据 hash 重新计算出这个标签。
process_sha256 是进程原始输出的 hash。
adapter_sha256 是经过解析、包装和脱敏后的 hash。
model_sha256 只对工具结果片段计算 hash。
将 process_len、adapter_len 和 model_len 保存为字节长度。
mutation_hop 是根据下表选出的标签。
对工具结果片段计算 hash,而不是对整个 prompt 计算。只要任何一条同级消息发生变化,整个 prompt 的 hash 就会改变。这种噪声会干扰排查,让人看不出工具载荷其实一直保持不变。
下面这条 JSON 记录只是用于教学的示例,并非从生产环境的 Agent 中捕获的 trace。请将简短的 hash 占位符替换为真实的 SHA-256 值。
{
"run_id": "run_7f3a",
"tool_call_id": "call_91",
"tool_name": "read_file",
"process_sha256": "replace-with-real-sha256",
"adapter_sha256": "replace-with-real-sha256",
"model_sha256": "replace-with-other-sha256",
"process_len": 36,
"adapter_len": 20,
"model_len": 20,
"mutation_hop": "adapter_shrink"
}
在打开服务提供商的状态页面之前,先看这张表。先比较三个摘要,再用长度辅助判定。将排查工作交给匹配行所指向的环节。
最后一行检验的是你的观测实现,而不是模型。hash 相同但长度不同的情况,应该很少出现。如果遇到这一行,说明计算模型 hash 时覆盖了不同的字节范围。
这些长度是对样例字面量手工计数得出的,并非来自真实 Agent trace 的字节数。运行文件时,请用 len() 核实每一个数字。
wrap 样例中的进程输出字面量长 20 个 ASCII 字节。wrap 样例中的模型片段字面量长 48 个 ASCII 字节。多出来的 28 个字节来自 type 字段和 body 包装。
shrink 样例中的进程输出字面量长 36 个 ASCII 字节。shrink 样例中的 adapter 字面量长 20 个 ASCII 字节。少掉的 16 个字节,就是原先插入的 note 字段。
下面的脚本是一份尚未执行的教学方案。本文没有在托管 Agent 中运行它。它仅使用 Python 标准库计算 hash。
#!/usr/bin/env python3
"""Classify which hop mutated a tool result. Unexecuted teaching fixture."""
import hashlib
import json
from typing import Any
def sha256_bytes(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def canonicalize(obj: Any) -> bytes:
text = json.dumps(
obj,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
)
return text.encode("utf-8")
def classify(process: bytes, adapter: bytes, model_part: bytes) -> dict:
p = sha256_bytes(process)
a = sha256_bytes(adapter)
m = sha256_bytes(model_part)
if p != a and len(adapter) < len(process):
hop = "adapter_shrink"
elif p != a and len(adapter) > len(process):
hop = "adapter_grow"
elif p == a and a != m and len(model_part) > len(adapter):
hop = "prompt_builder_wrap"
elif p == a and a != m and len(model_part) < len(adapter):
hop = "prompt_builder_drop"
elif p == a and a == m and len(process) == len(adapter) == len(model_part):
hop = "no_payload_mutation"
elif p == a and a == m:
hop = "hash_slice_mismatch"
else:
hop = "needs_manual_review"
return {
"process_sha256": p,
"adapter_sha256": a,
"model_sha256": m,
"process_len": len(process),
"adapter_len": len(adapter),
"model_len": len(model_part),
"mutation_hop": hop,
}
def fixture_wrap() -> dict:
process = b'{"ok":true,"rows":2}'
adapter = canonicalize({"ok": True, "rows": 2})
model_part = b'{"type":"tool_result","body":{"ok":true,"rows":2}}'
return classify(process, adapter, model_part)
def fixture_shrink() -> dict:
process = b'{"ok":true,"rows":2,"note":"abcdef"}'
adapter = b'{"ok":true,"rows":2}'
model_part = adapter
return classify(process, adapter, model_part)
if __name__ == "__main__":
print(json.dumps({"wrap": fixture_wrap(), "shrink": fixture_shrink()}, indent=2))
在信任这些标签之前,先在笔记本电脑上运行样例。检查打印出的 JSON 对象是否符合两项预期。wrap 样例应该返回 prompt_builder_wrap 标签。
shrink 样例应该返回 adapter_shrink 标签。如果 wrap 样例返回 adapter_grow,就停下来比较输入。你拿经过规范化的 adapter 字节,与原始进程字节进行了 hash 比较。
即使值相同,键的顺序和空格也会改变 hash。在责怪 adapter 之前,先把这种不一致当作样例中的 bug 处理。每次修改 canonicalize 后,都重新运行这两个样例。
python3 three_boundary.py
同时保存 adapter 的原始 hash 和规范化 hash。用原始 hash 检测任何字节变化,用规范化 hash 忽略对象键的顺序。
不要用规范化 hash 替代原始 hash。后续的某次故障可能取决于精确的字节内容,其中也包括模型实际收到的空格。
这里的规范形式,是按键排序的紧凑 JSON。它并不是适用于所有载荷的通用 JSON 标准。日期、大整数和重复键,都需要在本地另行制定规则。
针对失败轮次中的一个 tool_call_id,执行下面这套流程。在第四步完成之前,不要开始重试。第二次运行可能会覆盖唯一一份完整、未经改动的副本。
处理这些文件的 shell 命令草案如下。这里的参数,是为后续脚本提出的接口方案。上面的教学脚本仍然只使用内存中的样例。
python3 three_boundary.py \
--process traces/call_91.process.bin \
--adapter traces/call_91.adapter.bin \
--model traces/call_91.model_part.bin \
>> traces/mutation_hops.jsonl
记录生成后,根据标签把后续排查交给对应责任方。adapter_shrink 交给 adapter 的作者检查;prompt_builder_wrap 交给 prompt 的作者检查。
no_payload_mutation 表示可以结束传输层排查,转入行为分析。先过滤日志,让排查从实际发生变化的记录开始。
jq -c 'select(.mutation_hop != "no_payload_mutation")' traces/mutation_hops.jsonl
这个过滤条件只保留交接环节改变了字节的记录。先从这份更短的列表开始排查,不要一上来就看整个文件。
摘要相同,并不意味着工具选对了文件。它只意味着参与 hash 计算的片段彼此完全一致。一个错误的路径,也可能在每个交接环节中被完整保留。
即使工具载荷被如实传递,模型仍可能读错。这属于行为问题,而不是载荷变化问题。把这两个问题分开,否则重试会改动错误的层。
不要因为摘要记录显示正常,就删除进程 stdout。摘要是索引,字节内容才是证据。在故障复盘结束之前,两份产物都要保留。
工具的原始输出经常包含源代码或秘密信息。摘要记录通常不包含这些秘密。与分享原始文件相比,分享记录更安全。
披露:本文是 MonkeyCode 产品推广的一部分。MonkeyCode 提供免费的模型访问和免费服务器选项。这两点均由运营方提供,仅用于本稿。
本文未说明任何配额、模型名称或机器规格,也没有说明这两项可用性声明的结束日期。请把进程文件留在生成它们的工具主机上。
团队成员无需打开文件内容,就能按 mutation_hop 分组。可以使用免费的模型访问进行一次受控重放检查。发送已保存的工具结果片段,而不是重新运行工具。
询问模型:这个完全相同的片段,是否仍然看起来无法读取?如果重放失败,说明保存的片段能够复现这一问题。如果重放成功,说明实时请求与保存的片段存在差异。
这个检查是工作流程建议,不是质量评分。本文没有报告任何准确率。元数据是否可以离开内部网络,仍由策略决定。
在主机上完成本地脱敏之前,不要上传任何内容。在这里,秘密信息的 hash 也可能是敏感信息。如果私有语料库很小,即使只有摘要,也可能识别出具体内容。
优先在替换完成之后,再对载荷计算 hash。在日志中,将脱敏凭据保存在摘要记录旁边。
如果两次运行之间的 rule_id 发生变化,就标记为策略漂移。不要默认把这种漂移称为模型回归。没有脱敏凭据,环节判定表就无法看见规则变化。
如果工具不返回响应体,就跳过这套流程。只返回退出码的工具,需要另一种记录结构。三个空内容的 hash,只会浪费排查时间。
如果无法捕获发往模型的请求,也应跳过。控制台上的 token 总数无法填入 model_sha256。那样做是在猜测片段,而不是对片段计算 hash。
如果元数据必须留在内部,就不要使用共享的免费服务器。摘要加上工具名称,仍可能识别出某个私有文件。语料库较小时,这种识别风险是具体存在的。
不要把这套流程用作模型排行榜或供应商评分。分类器定位的是一次已记录调用中的某个环节。它不会给编程模型排名。
这份草案中的分类器忽略了流式传输的分块边界,也忽略了压缩和 content-encoding 的差异。它假定进程 hash 是基于原始字节计算的。
在这份草案中,canonicalize 会有意改变 hash。如果键的顺序具有意义,比较时就跳过这个函数。规范化 hash 只作为第二个字段保留。
时钟偏差不在这个方法的处理范围内。这个方法不会读取任一主机上的时间戳。一个缓慢但正确的交接环节,在这里看起来仍然正常。
本文中的任何样例都没有在托管 Agent 上运行过。请把打印出的标签视为需要验证的预期结果。如果你的运行环境添加了字节顺序标记,hash 就会不同。
流式工具在分类之前,还需要增加一条规则:等流关闭后,对拼接完成的进程输出计算 hash。流传输过程中的 hash,不会与最终 adapter 输出的 hash 一致。
这种错误的 adapter_grow 标签会把你引向错误的责任方。先等流关闭,再计算 hash,最后分类。不要在调用过程中对不完整的缓冲区分类。
从最新的 trace 中选择一个失败的 tool call id。以这个 id 将三个片段写入磁盘。任何重试之前,都要运行分类器并保存 mutation_hop。
如果团队需要共享摘要日志,可以用免费服务器保存这些记录。MonkeyCode 的免费服务器是一个可保存这些摘要记录的地方。除非策略允许复制,否则原始载荷应留在工具主机上。
如需采取进一步措施,可以考虑屏蔽此人和/或举报滥用行为。