Đây là lỗi thứ tự thông báo, không phải lỗi công cụ. Claude yêu cầu mọi block tool_use trong lượt assistant phải được trả lời bằng block tool_result trong lượt user ngay sau đó — cùng id, không có gì ở giữa. Vòng lặp của bạn đã bỏ sót một block, thường là vì công cụ đã ném lỗi.
Lỗi
{
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "messages.1: tool_use ids were found without tool_result blocks immediately after: toolu_01A... Each tool_use block must have a corresponding tool_result block in the next message."
}
}Tổng quan nguyên nhân và cách khắc phục
| Nguyên nhân | Cách khắc phục |
|---|---|
| Công cụ đã ném lỗi nên không có gì được thêm vào | Vẫn gửi một tool_result, với is_error: true và nội dung lỗi. |
| tool_result xuất hiện trong một thông báo muộn hơn | Nó phải nằm trong chính thông báo kế tiếp — không có lượt assistant hoặc user nào ở giữa. |
| tool_use_id không khớp | Lặp lại chính xác id từ block tool_use; không tạo lại id. |
| Lịch sử bị cắt giữa một vòng khứ hồi | Chỉ cắt tại các vòng khứ hồi công cụ hoàn chỉnh, không cắt giữa hai nửa của một vòng. |
Bất biến được nêu một lần
Mỗi block tool_use trong một lượt assistant cần đúng một block tool_result trong thông báo user ngay sau đó, mang cùng tool_use_id. Nhiều block tool_use trong một lượt cần nhiều block tool_result trong cùng thông báo kế tiếp đó. Không được có gì nằm giữa hai lượt.
Luôn trả lời, kể cả khi công cụ thất bại
Mô hình xử lý công cụ thất bại hoàn toàn tốt; điều nó không thể xử lý là công cụ bị thiếu. Trả lỗi dưới dạng tool_result giữ cho cuộc trò chuyện hợp lệ và thường giúp khôi phục hợp lý thay vì nhận lỗi 400.
results = []
for block in (b for b in resp.content if b.type == "tool_use"):
try:
out = run_tool(block.name, block.input)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(out),
})
except Exception as e:
# A failed tool still owes the model an answer.
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": f"Tool failed: {e}",
"is_error": True,
})
messages.append({"role": "assistant", "content": resp.content})
messages.append({"role": "user", "content": results})Xác thực hai lượt cuối trước khi gửi
Khoảng một chục dòng assertion có thể phát hiện lỗi này tại nơi gọi thay vì nhận lỗi 400 từ mạng — duyệt các tool_use id của lượt assistant và xác nhận lượt user kế tiếp trả lời tất cả chúng.
def check_pairs(messages):
for i, m in enumerate(messages):
if m["role"] != "assistant" or not isinstance(m.get("content"), list):
continue
ids = {b.get("id") for b in m["content"]
if isinstance(b, dict) and b.get("type") == "tool_use"}
if not ids:
continue
nxt = messages[i + 1] if i + 1 < len(messages) else None
answered = {b.get("tool_use_id") for b in (nxt or {}).get("content", [])
if isinstance(b, dict) and b.get("type") == "tool_result"}
missing = ids - answered
assert not missing, f"message {i}: unanswered tool_use {missing}"Cắt lịch sử tại ranh giới khứ hồi
Việc cắt cửa sổ ngữ cảnh theo số lượng thông báo cuối cùng sẽ sớm cắt giữa tool_use và tool_result. Hãy coi cặp này là một đơn vị không thể chia tách khi quyết định nội dung cần loại bỏ.
Nếu bạn gọi qua Kunavo
Đây là payload của bạn và Kunavo không che giấu lỗi này: 400 nằm trong danh sách không thử lại, nên một vòng khứ hồi công cụ sai định dạng chỉ thất bại một lần thay vì tốn thêm độ trễ cho một vòng thượng nguồn thứ hai để nhận cùng lỗi; yêu cầu bị từ chối được ghi nhận với chi phí bằng 0. Trên /v1/messages, bạn đang nói trực tiếp với giao thức Messages, nên các block tools, tool_use và tool_result được chuyển tiếp thay vì dịch. Tại đó, lỗi 400 trả về dạng invalid_request_error, như trên API Anthropic, cùng nội dung thông báo của thượng nguồn, sau đó là request id riêng của thượng nguồn và không có trường request_id; trước ngày 24 tháng 9 năm 2026, type là api_error, nên nếu nhật ký của bạn có dữ liệu cũ hơn, hãy phân nhánh theo trạng thái HTTP và nội dung thông báo.
Câu hỏi thường gặp
Tôi có thể bỏ lượt tool_use thay vì trả lời không?
Có, nếu bạn bỏ toàn bộ lượt assistant. Điều không hợp lệ là giữ tool_use nhưng bỏ tool_result.
Endpoint tương thích OpenAI có cùng quy tắc không?
Vẫn cần ghép cặp tương tự, nhưng tên khác — tool_calls trên thông báo assistant, sau đó là một thông báo role: "tool" cho mỗi lệnh gọi, mang tool_call_id.
Yêu cầu bị từ chối có mất phí không?
Trên Kunavo thì không. Các yêu cầu thất bại được ghi nhận với chi phí bằng 0 và không bao giờ dẫn đến một lệnh gọi thượng nguồn bị tính phí.
Hướng dẫn liên quan
- model_not_found / 404 — cách đặt tên model giữa Claude, Gemini và các gateway
- Claude API 429 rate_limit_error — nguyên nhân và cách khắc phục hiệu quả
Thông tin chi tiết hơn về ý nghĩa lỗi có trong tài liệu tham chiếu lỗi; bạn chỉ mất một phút để lấy khóa qua đăng ký và hướng dẫn xác thực.