Skip to content

[BUG] Anthropic API 返回 400:重复 tool_result 及连续 tool result 未合并 #1792

@liuliqiang

Description

@liuliqiang

Quick Summary

使用 Anthropic 提供商(claude-sonnet-4-6)时,LLM 调用返回 400 错误:each tool_use must have a single result. Found multiple tool_result blocks with id: toolu_xxx。原因是 Anthropic Messages 适配器将每个 tool result 作为独立的 user 消息发送,且 sanitizeHistoryForProvider 未对相同 ToolCallID 的 tool result 进行去重。

Environment & Tools

  • PicoClaw Version: main 分支(commit 38e1fe4
  • Go Version: go 1.22+
  • AI Model & Provider: claude-sonnet-4-6,通过 Anthropic Messages API
  • Operating System: Linux
  • Channels: Feishu(飞书)

📸 Steps to Reproduce

  1. 配置 Anthropic 提供商(claude-sonnet-4-6),启动 PicoClaw。
  2. 发送一条触发 skill 执行的消息(skill 提示词会使 LLM 在多次迭代中调用多个工具)。
  3. 观察到前 1–3 次迭代正常执行(如 read_fileexec 等工具调用成功)。
  4. 第 4 次迭代时,LLM 调用失败,返回 400 invalid_request_error

错误日志:

ERR pkg/agent/loop.go:1207 > LLM call failed error="bad request (400):
{\"type\":\"error\",\"error\":{\"type\":\"invalid_request_error\",
\"message\":\"messages.6.content.2: each tool_use must have a single result.
Found multiple `tool_result` blocks with id: toolu_01DKqMow1fXVBsctDmZDtxaa\"},
\"request_id\":\"req_011CZCfhDyjTUh1TUmfCydsu\"}"
agent_id=main component=agent iteration=4 model=claude-sonnet-4-6

❌ Actual Behavior

Anthropic API 返回 HTTP 400 拒绝请求,对话中断,用户收到错误信息。根因包含两个层面:

1. Anthropic 提供商为每个 tool result 创建独立的 user 消息

pkg/providers/anthropic_messages/provider.go:248-260 中,每个 tool 角色的消息被转换为一个独立的 user 角色消息,包含单个 tool_result 内容块。当一个 assistant 消息后面跟随多个 tool result 时,会产生多个连续的 user 消息。Anthropic API 会自动合并连续的同角色消息为一条消息,合并后的内容块中如果存在相同 tool_use_idtool_result,API 就会拒绝该请求。

2. sanitizeHistoryForProvider 未对 tool result 去重

pkg/agent/context.go:674-682 中,第二遍扫描仅验证每个 tool_call_id 是否有至少一个匹配的 tool result,但不会检测或移除相同 ToolCallID 的重复 tool result。如果 session 历史中存在重复条目(例如来自上一次失败的对话轮次、session 数据损坏或提供商切换),这些重复会直接通过而不被过滤。

✅ Expected Behavior

  1. Anthropic 提供商应将连续的 tool result 消息合并为单个 user 消息(包含多个 tool_result 内容块),而非依赖 API 的自动合并行为。
  2. sanitizeHistoryForProvider 应对 tool result 去重:如果多个 tool 角色消息共享相同的 ToolCallID,仅保留第一个。
  3. LLM 迭代循环应正常完成,不会因 API 返回 400 错误而中断。

💬 Additional Context

受影响的代码位置

文件 行号 说明
pkg/providers/anthropic_messages/provider.go 248–260 每个 tool 消息 → 独立 user 消息(应合并)
pkg/agent/context.go 674–682 完整性检查缺少去重逻辑
pkg/agent/loop.go 1440–1448 Tool result 创建逻辑(本身正确,每个 tool call 对应一个 result)

可能的触发场景

  • 上一轮对话失败:如果上一次 processMessage 在迭代中途失败(例如上下文窗口超限、超时等),session 中会保留部分消息(assistant + tool results),但没有最终的 assistant 文本回复。下一轮加载该历史时可能触发边界情况。
  • Session 数据损坏:如果 JSONL 后端写入了 tool result 后进程崩溃,部分恢复可能导致重复条目。
  • 提供商切换:如果 session 之前由一个生成非唯一 tool_use ID 的提供商服务,切换到 Anthropic 后可能暴露重复问题。

建议修复方案

修复 1 — 在 sanitizeHistoryForProvider 中添加去重:

// 在 sanitizeHistoryForProvider 的第二遍扫描中添加
seen := make(map[string]bool)
for i := 0; i < len(sanitized); i++ {
    msg := sanitized[i]
    if msg.Role == "tool" && msg.ToolCallID != "" {
        if seen[msg.ToolCallID] {
            continue // 跳过重复的 tool result
        }
        seen[msg.ToolCallID] = true
    }
    final = append(final, msg)
}

修复 2 — 在 Anthropic 提供商中合并 tool result:

// 在 buildRequestBody 中,将连续的 tool 角色消息
// 合并为单个 user 消息,包含多个 tool_result 内容块
case "tool":
    toolResultBlock := map[string]any{
        "type":        "tool_result",
        "tool_use_id": msg.ToolCallID,
        "content":     msg.Content,
    }
    // 如果上一条 API 消息是包含 tool_result 的 user 消息,追加到其中
    if len(apiMessages) > 0 {
        prev := apiMessages[len(apiMessages)-1].(map[string]any)
        if prev["role"] == "user" {
            if content, ok := prev["content"].([]map[string]any); ok {
                prev["content"] = append(content, toolResultBlock)
                continue
            }
        }
    }
    // 否则创建新的 user 消息
    apiMessages = append(apiMessages, map[string]any{
        "role":    "user",
        "content": []map[string]any{toolResultBlock},
    })

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions