mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 09:17:31 +00:00
fix(agent): 发送前自检 tool_call 配对 —— 止损并定位上游 400
## 为什么还要修(前面两个修复都不够) 今天已经修了两处:263b651并发批次 stepToolBatch 漏记 ToolCalls0ed3c89L1 预算截断切开 tool 配对 并用生产 context.json 验证历史已干净: 53 个事件,0 个"结果多于调用"的帧 但 A2A **新会话**(a2a_1793778753015670668)仍然报同一个 400: An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. (insufficient tool messages following...) 同时日志显示 `ratio=0.28`(远低于 1.25 阈值)、`skip oversized frame` 计数 0 ⇒ 既不是裁剪,也不是预算截断。 ## 定位:问题在 f.Msgs,不在 L1 历史 `f.Msgs` 是**多条路径**拼出来的: prepare 重建 buildMessages 中断恢复 rebaseFramePrefix(重建 prefix + 接回 tail) 批末附加 flushBatchAttachments 占位补全 dropContinuationPlaceholders / continuation 任何一条漏掉配对,最终都表现为同一个 400,而协议**不告诉你是哪条 call**。 所以把自检放在唯一一次组装 provider 请求的位置(stepLLM,task.go:750), 而不是散在各条路径上。 ## 为什么剔 call 而不是补占位 补一条 "tool result: (missing)" 会让模型看到它**从未调用过**的结果, 可能据此继续推理 —— 那是编造事实。剔掉 call 只是少了一次尝试, 模型可以重来,语义安全。 被剔的 call 记录工具名与数量,日志里能看出是哪一轮出的问题。 ## 判据(5 条,3 次变异全部实测变红) DropsCallWithoutResult 主方向:没有结果的 call 必须剔 变异:直接 return msgs ⇒ "★ 未配对的 call c2 仍在消息里 —— 上游必然 400" DropsOrphanToolResult 反方向:找不到 call 的孤立 tool 必须删 变异:删掉第二遍循环 ⇒ 红 DropsEmptyAssistantShell call 全剔且正文为空 ⇒ 整条删 变异:去掉该分支 ⇒ 红 KeepsContentWhenAllCallsDropped call 全剔但**有正文**时保留正文(那是模型说的话,不能丢) LeavesValidSequenceUntouched 合法序列必须逐条不变 —— 修正逻辑不能引入新问题 只测一个方向会放过另一半失配,所以两个方向都钉。 ## 验证 go build ./... PASS go vet ./... PASS gofmt -l 干净 go test ./... -count=1 62 包全过,0 FAIL 干净检出(worktree) build + TestDropUnpaired 全绿 ## 下一步 这条是**止损 + 定位**,不是根因修复。部署后如果日志里出现 "dropped N unpaired tool message(s)",那 N 与工具名就指向真正漏配对的 那条路径,据此再修上游。
This commit is contained in:
@ -746,6 +746,21 @@ func (a *Agent) stepLLM(f *TaskFrame) stepOutcome {
|
||||
})
|
||||
}
|
||||
|
||||
// ★ 发送前自检:assistant(tool_calls) 必须紧跟它全部的 tool 结果。
|
||||
//
|
||||
// 为什么必须在**这里**(唯一一次组装请求的位置,task.go:750):
|
||||
// 上游对这个约束是硬拒绝(HTTP 400),一句错误信息带不出任何定位信息,
|
||||
// 而 f.Msgs 是**多条路径**拼出来的(prepare 重建、rebase 中断恢复、
|
||||
// 批末 flush、占位补全),任何一条漏掉配对都表现为同一个 400。
|
||||
//
|
||||
// 实测(2026-11-04):历史 context.json 已经干净(0 个 result 多于 call
|
||||
// 的帧),但 A2A 新会话仍报 400 ⇒ 问题不在 L1 历史渲染,在 f.Msgs。
|
||||
//
|
||||
// 这里做的是**止损 + 定位**:把没有配对结果的 tool_calls 剔掉(连同
|
||||
// 该 assistant 消息里其余仍有效的 call 一起重排),并记录被剔的是谁。
|
||||
// 不静默——日志里能看出是哪一轮、哪个工具。
|
||||
f.Msgs = dropUnpairedToolCalls(f.Msgs)
|
||||
|
||||
req := &agentAPI.CompletionRequest{
|
||||
Messages: f.Msgs,
|
||||
MaxTokens: 4096,
|
||||
|
||||
Reference in New Issue
Block a user