mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 09:17:31 +00:00
feat(agent): 超长工具结果落盘 + toolresult_read 按范围回读(补齐工具环)
问题(WSL2 真机诊断报告问题 2):单条工具结果 36783 token,直接冲破
整个对话预算 → overflow 判「单条超限,裁剪救不了」→ 整轮终止。
模型看到的只是"对话没反应"。日志:max_ctx=32768 target=26214
fixed=498 mem=8572 ctx=17144,而单条 36783 是它的 2.1 倍。
此前两条路都不通,本提交走第三条:
· **截断**(方案 A):模型拿到残缺信息且**不知道**被截过,会基于残缺
数据下结论 —— 与本仓反复吃亏的「静默降级」同族。
· **只统计不裁剪**(方案 B,toolresult_budget.go):设计上正确(不欺骗
模型),但停在「报告」一步 —— oversizeTools 计数**没有任何消费者**
(OversizeToolReports/OversizeToolNames 全仓零调用者),所以那条超大
结果照样整条进上下文、照样撞窗口。
本提交把方案 B 缺的那一半补上:
结果超限(沿用 toolresult_budget.go 的 ContextTokens/8 阈值)
→ 正文完整写入 <context.json 同级>/toolresults/<evt_id>/tool_N.txt
→ 上下文里换成「预览 + event_id 句柄 + 总字节/行数 + 读法」
→ 模型调 toolresult_read(event_id, offset, limit) 取任意区间(≤64KB/次)
→ 事件离开 L1 时全文目录一并删除
设计要点:
· **存活期跟随事件,不引入 TTL**。与 Blocks 同一套处置(见 mediaref.go
「块存储在事件自身,不维护引用账本」)。刻意不做独立过期/清理定时器
——那会让"一件事记两处",两处一旦不一致就出现"事件还在但全文没了"。
· **正文不内联进 context.json**:它每 5s 全量重写,内联几 MB 正文会让
每次 flush 都写几 MB。
· **磁盘 IO 放 context.go**(记忆面持久化,架构守卫允许的唯一落点),
toolresult_store.go 只留纯逻辑 —— 满足 TestKernelHasNoMediaFileReading。
· **toolresult_read 按需暴露**:只在真有落盘结果时注册 schema
(本仓已有"175 个工具占满上下文"的教训)。
· 落盘失败**保持原内容**:宁可撞窗口也不让模型什么都看不到。
判据(toolresult_store_test.go,7 条,3 条经变异验证):
· TestOffloadReplacesFullTextWithPreview —— 上下文拿预览非全文,且三要素
齐全(说明是预览/给句柄/给读法)。变异:返回全文 ⇒ 红。
· TestReadBackReassemblesByteIdentical —— 分段拼回**逐字节相同**,含中文
多字节字符。这是"信息不丢"的唯一硬证据。变异:少读 1 字节 ⇒ 红。
· TestAppendClaimsPendingToolResults —— 事件进 L1 必须认领落盘记录,
否则重启后句柄全失效。变异:不认领 ⇒ 红。
· TestReadAfterPurgeIsExplicit —— 淘汰后明确告知,不静默返回空串。
· TestPersistedToEventDirNotInline / TestReadWithBadHandleIsActionable /
TestSmallResultNotOffloaded
This commit is contained in:
@ -117,6 +117,14 @@ type TaskFrame struct {
|
||||
// 在若干帧后突然失去超页处理能力(而那时恰恰最需要它)。
|
||||
OverflowRecover int
|
||||
|
||||
// EventID 是本帧工具结果落盘时使用的**事件标识**。
|
||||
//
|
||||
// 为什么要挂在帧上:同一帧内可能有多条超长结果,它们都属于同一个
|
||||
// 上下文事件,应当落在同一个事件目录里(tool_0.txt / tool_1.txt)。
|
||||
// 惰性生成(首次落盘时分配),因为绝大多数帧没有任何超长结果——
|
||||
// 全量生成会给每条事件多加一个字段,与 ContextEvent.ID 的取舍一致。
|
||||
EventID string
|
||||
|
||||
// 当前工具批
|
||||
PendingTools []agentAPI.ToolCall
|
||||
ToolIdx int
|
||||
@ -1173,14 +1181,27 @@ func (a *Agent) stepToolAfter(f *TaskFrame) stepOutcome {
|
||||
}
|
||||
f.Msgs = append(f.Msgs, toolMsg)
|
||||
|
||||
// 工具结果大小统计(方案 B:**只统计不裁剪**)。
|
||||
// 工具结果大小处置:超限则**全文落盘 + 上下文只放预览**。
|
||||
//
|
||||
// 为什么在这里而不是 stepToolExec:那里拿到的 outcome.Text 尚未经
|
||||
// after_toolcall 阶段改写,而模型最终看到的是**这里**的内容。
|
||||
// ⚠️ 刻意不截断 —— 截断会让模型基于残缺数据下结论,且它不知道被截过
|
||||
// (与本仓「静默降级」同族)。处置权交回调度器/上层。
|
||||
//
|
||||
// ★ 刻意**不截断**(截断会让模型拿到残缺信息且不知道被截过,与本仓
|
||||
// 「静默降级」同族)。也刻意**不原样放行**(那正是 36783 token 单条
|
||||
// 结果冲破预算、整轮终止的成因)。
|
||||
//
|
||||
// 走的是第三条路:正文完整保存到事件目录,上下文里换成
|
||||
// 「预览 + 句柄 + 总量」+ toolresult_read 工具按需取区间。
|
||||
// 取哪一段由模型决定,信息不丢(判据要求分段拼回逐字节相同)。
|
||||
if a.checkToolResultSize(tc.Name, toolMsg.Content) {
|
||||
f.noteOversizeTool(tc.Name)
|
||||
if replaced, ok := a.offloadToolResult(f, tc.Name, toolMsg.Content); ok {
|
||||
toolMsg.Content = replaced
|
||||
result = replaced
|
||||
if len(f.ToolResults) > 0 {
|
||||
f.ToolResults[len(f.ToolResults)-1].Output = replaced
|
||||
}
|
||||
}
|
||||
}
|
||||
if mediaMsg != nil {
|
||||
// 延迟到批末(见 batchAttachments 的注释):当场追加会把后续 tool 消息
|
||||
|
||||
Reference in New Issue
Block a user