KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
06. 工具轨迹为什么不能随便压缩? — keel 龙骨
普通对话删掉一条旧消息,最多让语义变差;工具轨迹删错一条,下一次请求可能直接违反供应商消息协议。
普通对话删掉一条旧消息,最多让语义变差;工具轨迹删错一条,下一次请求可能直接违反供应商消息协议。
支付事故里,模型曾发出 search_incident,工具返回 INC-42,随后模型依据这个结果判断近期存在超时。如果 compaction 只保留工具结果、不保留对应 call ID,结果会变成孤儿;只保留调用、不保留结果,模型又像一直在等待一个没有返回的工具。
因此工具轨迹首先是结构化协议,其次才是可摘要文本。
1. 一条完整工具回合
内部消息可以表示为:
assistant a1:
tool_call id=c1 name=search_incident args={query: payment}
tool t1:
tool_call_id=c1
result={ok: true, evidence_id: INC-42}
assistant a2:
INC-42 显示近期有超时。
关键不变量:
- call ID 唯一;
- result 引用已经出现的 call ID;
- 一个 call 不产生多个互相冲突的 result;
- 需要完整结果的 provider 不能留下未完成 call;
- 未知、失败、取消不能在摘要中变成成功。
不同供应商对 role、顺序、并行调用和消息字段的要求不同。课程使用内部协议,真正发送前必须经过 provider adapter 校验。
2. 先运行合法轨迹压缩
python courses/foundation/context-engineering/course/project/examples/04_compact_tool_trace.py
输入包含系统规则、用户请求、一次 call/result、模型结论和最新用户要求。当前输出:
strategy: tool-trace-summary
sources: ['u1', 'a1', 't1']
messages: [
('system', 's'),
('system', 'compaction-summary'),
('assistant', 'a2'),
('user', 'u2')
]
旧的完整工具回合被转成带来源的历史摘要;最近模型结论和用户请求保留。sources 证明摘要由 u1/a1/t1 派生,但原始工具事实仍应保存在事件或证据存储中,以便按 ID 回查。
3. Validator 实际检查什么
项目的 validate_tool_trace() 维护两个集合:出现过的 calls 和已完成 results。
if message.role == "tool":
if not message.tool_call_id or message.tool_call_id not in calls:
raise ValueError("orphan tool result")
if message.tool_call_id in completed:
raise ValueError("duplicate tool result")
completed.add(message.tool_call_id)
missing = set(calls) - completed
if missing:
raise ValueError("tool calls without results")
这个 validator 是 provider-neutral 的最小不变量,不等价于某家 API 的完整 schema 校验。生产适配器还要验证消息顺序、允许的 content 类型、并行调用约束、流式参数拼接和响应字段。
4. 为什么“保留最近 N 条”不安全
假设最近两条是:
tool result c1
user: 继续
机械切片会从 tool result 开始,丢掉前面的 assistant call。项目中的 _recent_tool_safe() 会向前扩展后缀,直到把对应调用一起带上。空间不足时,再以完整工具回合为单位移除,而不是逐条 pop。
对于并行调用,还要把同一 assistant 消息里的多个 call 与各自结果作为一组处理。流式调用则必须等参数和终态收束后,才能形成可压缩的稳定记录。
5. 工具结果怎样缩短而不伪造事实
不要让摘要器自由改写副作用结果。可以按风险分层:
| 结果类型 | 推荐保留 |
|---|---|
| 只读查询 | 状态、关键字段、evidence ID、来源版本 |
| 写操作成功 | 业务对象 ID、幂等键、参数摘要、确认状态 |
| 超时/未知 | unknown 状态、查询句柄、下一次对账动作 |
| 权限拒绝 | policy code、主体/资源引用,不暴露敏感规则全文 |
| 大日志 | 统计、异常片段、时间范围、原始 chunk IDs |
最危险的错误是把 outcome_unknown 压成“执行完成”。摘要更流畅,但恢复和重试策略会被破坏,甚至重复产生外部副作用。
6. 压缩后的证据边界
模型可以依据摘要继续推理,但用户要求精确证据时,系统应该通过 evidence ID 回查原始工具输出。摘要是导航,不是新的权威来源。
原始结果的保存还要满足数据最小化、租户隔离、加密和保留期限。不能因为“未来可能回查”就永久保存全部敏感工具响应。
7. 失败注入
- 只传 assistant call,不传 tool result,确认 validator 报 missing;
- 只传 tool result,确认报 orphan;
- 同一 call ID 放两个 result,确认报 duplicate;
- 让最近后缀从 tool result 开始,确认 compaction 自动向前包含 call;
- 构造一个
outcome_unknown,验证摘要和恢复状态不会把它改成 success。
运行确定性测试:
python -m unittest courses.foundation.context-engineering.course.project.tests.test_compaction -v
这些测试不需要真实模型,因为消息结构是否合法不是生成质量问题。
8. 本章验收
你应能画出一次工具回合的消息关系;解释为什么时间截断会产生 orphan;说明什么时候可以把旧工具回合转成证据摘要;并证明压缩前后 provider adapter 仍能接受消息序列。