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
- 配置 Anthropic 提供商(claude-sonnet-4-6),启动 PicoClaw。
- 发送一条触发 skill 执行的消息(skill 提示词会使 LLM 在多次迭代中调用多个工具)。
- 观察到前 1–3 次迭代正常执行(如
read_file、exec 等工具调用成功)。
- 第 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_id 的 tool_result,API 就会拒绝该请求。
2. sanitizeHistoryForProvider 未对 tool result 去重
在 pkg/agent/context.go:674-682 中,第二遍扫描仅验证每个 tool_call_id 是否有至少一个匹配的 tool result,但不会检测或移除相同 ToolCallID 的重复 tool result。如果 session 历史中存在重复条目(例如来自上一次失败的对话轮次、session 数据损坏或提供商切换),这些重复会直接通过而不被过滤。
✅ Expected Behavior
- Anthropic 提供商应将连续的 tool result 消息合并为单个
user 消息(包含多个 tool_result 内容块),而非依赖 API 的自动合并行为。
sanitizeHistoryForProvider 应对 tool result 去重:如果多个 tool 角色消息共享相同的 ToolCallID,仅保留第一个。
- 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},
})
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
📸 Steps to Reproduce
read_file、exec等工具调用成功)。invalid_request_error。错误日志:
❌ 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_id的tool_result,API 就会拒绝该请求。2.
sanitizeHistoryForProvider未对 tool result 去重在
pkg/agent/context.go:674-682中,第二遍扫描仅验证每个tool_call_id是否有至少一个匹配的 tool result,但不会检测或移除相同ToolCallID的重复 tool result。如果 session 历史中存在重复条目(例如来自上一次失败的对话轮次、session 数据损坏或提供商切换),这些重复会直接通过而不被过滤。✅ Expected Behavior
user消息(包含多个tool_result内容块),而非依赖 API 的自动合并行为。sanitizeHistoryForProvider应对 tool result 去重:如果多个tool角色消息共享相同的ToolCallID,仅保留第一个。💬 Additional Context
受影响的代码位置
pkg/providers/anthropic_messages/provider.gotool消息 → 独立user消息(应合并)pkg/agent/context.gopkg/agent/loop.go可能的触发场景
processMessage在迭代中途失败(例如上下文窗口超限、超时等),session 中会保留部分消息(assistant + tool results),但没有最终的 assistant 文本回复。下一轮加载该历史时可能触发边界情况。tool_useID 的提供商服务,切换到 Anthropic 后可能暴露重复问题。建议修复方案
修复 1 — 在
sanitizeHistoryForProvider中添加去重:修复 2 — 在 Anthropic 提供商中合并 tool result: