一次模型调用留下了什么:Agent 研究证据的采集、留存与回读
研究 Agent 失败后,我们常常有两种令人困惑的记录:网关说模型调用结束了,追踪平台却找不到那轮完整输入;或者研究报告保存了,却无法确认它使用的是哪一份资料。最容易想到的办法是“把更多日志全都留下”。然而更多记录并不自然意味着更可靠:用户不愿保留的材料可能被采集,队列可能阻塞模型请求,过期的来源也可能在回读时重新暴露。
本文从一个独立能力出发:研究调用证据留存与回读(E04)。它负责已经发生的模型 exchange/attempt 的有限观察副本,不负责模型实际发送、交易执行、正文发布、语义评价或恢复执行。
这是一篇机制说明,案例及标识均为合成。涉及的权限、容量和清理描述取自截至 2026 年 10 月 31 日的既有工程合同;局部工程验证不代表生产环境完整采集率、金融研究质量或账单可复核。
一、先区分三种“证据”
| 记录 | 谁拥有原事实 | 能回答什么 | 不能证明什么 |
|---|---|---|---|
| 执行/工具回执 | 原 Gateway、Host、Run 和业务 Owner | 请求是否派发,工具是否完成,正文是否提交 | 模型是否正确理解、是否满足用户目的 |
| E04 研究调用副本 | 研究证据 Store | 某个 exchange/attempt 的逻辑输入和可观察输出留下了多少 | 供应商完整 HTTP wire、准确费用、可重跑的恢复点 |
| Trace/诊断观察 | 各自的遥测与诊断 Owner | 哪些阶段被观测、耗时和缺口在哪里 | 原存储中的全部事件都已完整出现 |
三者应该用相同的执行身份建立关联,但不能归并为一个“成功”字段。一次模型请求确实发送了,不要求 E04 必然保存成功;E04 排队成功,也不等于数据库最终已经落盘。
二、一次调用如何留下可审计的副本
图 1:采集是旁路;每个分支的拒绝只影响观察,不改写原执行。
按起点分组阅读关系与条件;完整图保留在下方。
- → 建立 exchange 与 attempt 身份
- → Run 获准采集且策略允许? (判断)
- → 原 Gateway 继续发送与结算
- 否 → 记录有限缺口原因
- 是 → 旁路观察逻辑请求与输出
- 不否决 → 原 Gateway 继续发送与结算 [虚线关系]
- → 可编码且队列有容量? (判断)
- 否 → 记录有限缺口原因
- 是 → 排队写入持久证据 Store
- → 写入与来源复核成功? (判断)
- 是 → 保留独立证据引用
- 否或未知 → 记录有限缺口原因
完整关系图 · 可放大查看细节
这里刻意没有让观察器站在 Gateway 的必经路径上。原模型调用负责自己的授权、预算、派发、重试和结算,E04 只能拒绝本次采集或后续读取。发生观察异常时,不能把“无法记录”升级成“禁止原研究继续”。
实际采集分为两个片段:
- logical-request:当次真正提交到 SDK 的供应商中立输入视图,包括模型与路由、消息、工具结构和请求选项;它不是预先写好的 Prompt 模板,也不是底层 HTTP 原始字节。
- observable-output:能观察到的文本、工具调用及输入片段、结束状态和有限用量;不完整时保留截断或缺失事实,而不是补造内容。
绑定至少要区分 Run、exchange、attempt、part。一个逻辑 exchange 可能有多次物理 attempt;同一 attempt 的请求与输出也可能因不同失败点分别缺失。不能拿“有一条输出”反推“它的输入一定完整”。
三、隐私与准入发生在读取正文之前
被动采集最重要的顺序是:先判断能不能采,再决定是否遍历可能含敏感信息的正文。模型请求里可能有持仓、个人偏好、受权文件和用户原话,因此不能等序列化结束以后才检查用户是否关闭内容采集。
在原设计中,采集需要 Run/Scope 身份、服务端提供的准入以及内容策略同时成立。临时模式、内容退出、采样未命中、保留期到期等都可能让一次观察被跳过。采集数据时同时绑定来源 ID 与依赖版本;异步写入前再次核对授权、撤权和依赖资格,避免“采到时有权限”被偷换为“保存时一定仍可使用”。
一个有意不采集的 Run,不应在存储中创建可回放的正文作为“调试兜底”。拒绝采集与不可采集都应当被描述为相应的观察缺口,而不是默认为空请求、零 Token 或成功的“无内容输出”。
四、队列是有限的,且不能向模型请求施加背压
保存观察需要复制有限内容、计算摘要并进入异步队列。工程默认策略以 256 KiB 单片、128 片/Run、16 MiB/Run、32 个待写片和 4 MiB 待写字节、2 秒写入观察超时 为初始值;这些是配置默认值,不是每次运行都采满的事实,也不是生产完整性保证。
容量达上限时,采集返回明确的未采集原因;编码失败、片段超限或存储超时也一样。重要的是:
- 排队确认 queued 不是持久成功 stored。
- 超时后底层写入不一定已经停止,容量预留需要在底层工作实际结算后释放,不能反复制造无界写队列。
- 可选证据失败不改变原请求的派发、预算、Run 终态和已保存正文。
同一调用可能只保存请求、不保存输出,也可能反过来。这不是数据模型异常,而是必须允许的观察现实。
五、回读不是打开一份已经过期的模型上下文
图 2:回读走独立授权,先列索引,再读单个片段。
按起点分组阅读关系与条件;完整图保留在下方。
- → 查看者和 Scope 合法? (判断)
- 否 → permission_denied
- 是 → 读取采集元数据与分页索引
- → 保留期内且片段存在? (判断)
- 否 → not_found 或 retention_expired
- 是 → 按证据引用选定原片段
- → 摘要和来源/依赖仍有效? (判断)
- 否 → integrity_mismatch 或 source_unavailable
- 是 → 返回限定内容与身份
完整关系图 · 可放大查看细节
列表页可展示引用、片段类别、字节数、捕获时间、派发和结算状态,但不直接泄露原始来源绑定。分页窗口是 per-page-live:每一页有自己的观察时间窗,不代表整个列表的一致性快照;若中途过期或清理,后续页可以不同。
选中片段才按当前查看权限、原内容摘要、来源撤权和依赖版本做进一步检查。即使原请求在执行时有权看到文件,也不意味着另一个用户、新 Run 或后来被撤权的查看者可以重读它。
过期和清除应保留清晰的不可用语义。删除后历史正文不能因为缓存、旧引用或列表索引重新暴露;但 E04 的清除不代表原业务记录、交易账本或用户发表的正文也被一并删除。
六、用三个反例检验这套设计
以下均为说明机制的合成输入,不是实际运行追踪。
| 输入/观察 | 正确处理 | 错误做法 |
|---|---|---|
| Run 处于临时模式,模型请求仍成功 | 跳过正文采集,记作未采集;原执行独立结算 | 为方便排错悄悄保留原 Prompt |
| Attempt A 发送后流中断,只留下 logical-request | 保存可确认的请求片段,输出标记未知或缺失 | 根据请求自动伪造失败输出或用量为 0 |
| 一份先前可读的资料后来撤权,历史片段被访问 | 在回读处重新检查来源和依赖,必要时拒绝 | 只要历史哈希没变就原样泄露 |
一个简化的结构化回执可以是:
{
"runId": "demo-run",
"exchangeId": "research-4",
"attemptId": "physical-2",
"part": "observable-output",
"capture": "not_captured",
"reason": "capacity_exceeded",
"dispatch": "unknown"
}这里的 JSON 是教学投影,不是公开生产接口;“未知”不能被表述成“未发送”。
七、E04 与可观测、成本、Recovery 各自负责什么
E01/Trace 提供时间线和诊断视角;即使 Trace 查询 complete,也不能替 E04 证明每一轮输入都被保存。成本观察 可以消费原物理 attempt 与供应商返回的局部用量,但订阅余额、标价、实际扣费和交付物因果分摊仍是不同问题;留下调用副本不等于算出一份报告的完整成本。Recovery 需要可执行状态、原授权与检查点;E04 的只读副本不能直接变成“再执行一次”的许可。
因此,这个模块真正的 Depth 在于:外部只需提交合法 Run 身份与观察,内部完成准入、有限编码、异步存储、版本验证、回读和清除;它不把自己的失败扩散为全局故障。
结语:可回查,首先意味着知道缺了什么
好的证据系统不应承诺“每次请求都能百分之百复原”。它应当让读者区分:请求确实发生了、观察尝试了、正文保存了、历史仍有权读,以及哪些片段已缺失或过期。只有这些坐标分别成立,回查才有边界,研究才能可靠地被解释。
延伸阅读:从工具调用到模型输入 · Trace 显示 Complete · 报告成本如何归因 · 可观测全景。