🐛 fix: 根治 Write 历史折叠诱导的模型输出退化(执行记录架构) - #229
Conversation
|
@yyz159756 非常感谢您的pr。这个问题,是因为解决有人在小模型上遇到上下文窗口撑破而引入的,后面优化了压缩逻辑解决掉了,所以鉴于当前的情况,我更愿意恢复到一开始的方案,就是保留write的写入历史,而不是占位提示,你觉得呢。 |
|
@itmisx 感谢回复,这个方向我认同一半:历史里不该出现占位/折叠形态的 Write——这也是我这个 PR 的核心立场("不呈现伪 tool call",占位文本无论怎么措辞都会被模型模仿)。但如果"恢复为完整保留写入历史",我担心会重新打开当初引入折叠的那个问题,说下理由:
|
|
@yyz159756 你上一条的第 1 点说服我了 —— reclaim 只回收 所以我把实现仔细过了一遍。下面六个问题,按严重程度排,前三个我写了复现。方向上走哪条路我还在权衡,但这几点无论如何都需要处理。 🔴 1. 执行记录插在 tool 组中间,同批次其它工具结果会被丢弃
一轮里同时有大 Write 和 Read 时: Read 的结果静默丢失,同时留下悬挂 tool_call —— 严格后端会 400,正是 好消息:这是位置问题,不是架构问题。 我实测把执行记录追加到整批工具循环之后就完好了: 🔴 2. 大 Write 失败时,错误信息被吞掉你在循环前( 模型看到的是:自己啥也没调用、啥结果也没有 —— 不知道写失败了,大概率当成功继续往下走。 PR 描述第 6 节强调「失败走普通 tool 消息、错误原样透传,不会出现"状态: 成功"掩盖失败」,但实际做不到。这条挪位置也修不好,得改成:assistant 先带完整 tool_calls 入 convo,跑完工具循环后再回头把成功的大 Write 从中摘掉 —— 失败的保留 tool_call,错误就能正常配对。 🔴 3.
|
|
@yyz159756 所以我想恢复到之前的原样显示,只不过加一个兜底,如果写入超过一定阈值,可以提示模型分多次写入。我想越简单的逻辑越稳定。我也不确定引入你的方案后,会不会在不同模型面前产生其它副作用。 |
|
@itmisx 感谢详细 Review。 重新评估后,我同意当前 #229 方案的复杂度偏高。 原方案通过修改历史展示形式(write-elision)降低上下文占用,但实际引入了一系列新的隐含风险:
这些问题说明:修改 Agent 内部 history 语义的收益与风险不太匹配。 因此倾向于采用更简单、可预测的方案:
初步设计: const maxInlineWriteContentBytes = 512该阈值建议不要硬编码,后续作为配置项暴露。 当单次 Write content 超过阈值时:
这样可以保证:
目前 write-elision 方案中的问题分析和测试结果会保留作为后续优化参考,但当前 PR 不继续沿该方向扩展。 感谢指出这个设计方向上的问题。 |
|
@yyz159756 很感谢您反馈问题,并积极参与修复,但我还是担心会影响到稳定性。 我提交了一个优化修复,恢复到之前的原样显示,写入时设置阈值(按窗口大小自适应) #231。 |
PR 改动说明:根治 Write 历史折叠诱导的模型输出退化
1. 背景与动机
Write 工具写入大文件(content > 512 字节)后,模型在后续轮次出现输出退化:
退化是自我强化的:每次系统改写历史参数形态,模型都会把改写后的形态
当成 Write 的标准写法学走,越修越糟。本 PR 从架构层面根治。
2. 问题根因(三层认知)
核心洞察:任何"系统改写后出现在历史里的参数形态"都会被模型学成调用范式。
根本解法不是"选择更好的折叠形态",而是不呈现伪 tool call。
3. 架构实现:执行记录(核心)
模型看到的是一段稳定的执行记录,而不是被压缩的伪 tool call:
效果:
{path, content}(小内容)或执行记录(大内容外置)4. 代码改动清单(6 文件,+284/-107)
agent/llm.goelidedWriteInfo判定)+ 执行记录消息(execRecordMessage);rewriteToolCallArgsForHistory只修 JSON、不再折叠参数,告别伪 tool calltools/write_file.gotools/tools.goagent/history_args_test.goagent/args_repair_test.gotools/write_file_test.go5. 验证
go build ./...、go test ./agent/、go test ./tools/全绿;6. 审阅要点
result.Success);失败走普通 tool 消息、错误原样透传,不会出现"状态: 成功"掩盖失败;rewriteToolCallArgsForHistory返回值拷贝(内部make),histToolCalls[:0:0]零容量切片确保过滤时 append 不改原数组——执行循环仍用原始toolCalls(含完整 content);elidedIDs[tc.ID]:assistant 过滤与执行循环用同一 tool_call ID 关联,一致;elidedWriteInfo对小 content / 解析失败返回ok=false,按普通 tool_call 入历史,不误判;role=user系统注入的固定模板(仅 路径/大小/行数 变化),不引入随机 ID / 时间戳 / 动态文本,prefix cache 友好;7. 已知遗留:Agent Loop Completion Gate 缺陷(独立问题,不在本 PR)
reasoning 模型长任务中,thinking 起草代码后可能"声明未执行"——只输出
"将写 XX"等承诺性措辞而无工具调用,任务未做即结束。
机制定位(
agent/llm.gocompletionGate):① 输出被截断(
truncated);② 存在未完成 todo(countPendingTodos);maxGateNudges死循环保护(连续催 N 次无进展放行),新检测应复用该机制。修复方向(建议后续单独处理):
("将写 / 先写 / 开始执行 / 接下来创建…")→ 视为未完成,注入提示催继续;
与 Write 历史形态无关,不阻塞本 PR。