가이드 목록으로
문제 해결·2026년 8월 28일·6분 분량

Claude API 400 “tool_use ids가 tool_result 블록 없이 발견됨” — 순서 규칙

이는 도구 오류가 아니라 메시지 순서 오류입니다. Claude는 assistant 턴의 모든 tool_use 블록이 바로 다음 user 턴의 tool_result 블록으로 응답되기를 요구합니다. 동일한 id여야 하며 그 사이에 다른 내용이 없어야 합니다. 일반적으로 도구에서 예외가 발생했을 때 루프가 하나를 누락합니다.

마지막 검토일: .

이는 도구 오류가 아니라 메시지 순서 오류입니다. Claude는 assistant 턴의 모든 tool_use 블록이 바로 다음 user 턴의 tool_result 블록으로 응답되기를 요구합니다. 동일한 id여야 하며 그 사이에 다른 내용이 없어야 합니다. 일반적으로 도구에서 예외가 발생했을 때 루프가 하나를 누락합니다.

오류

response (HTTP 400)
{
  "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."
  }
}

원인과 해결 방법 한눈에 보기

원인해결 방법
도구에서 예외가 발생해 아무것도 추가되지 않음is_error: true와 오류 텍스트를 포함한 tool_result를 그래도 전송하세요.
tool_result가 나중 메시지에 들어감반드시 바로 다음 메시지여야 합니다. 그 사이에 assistant 또는 user 턴이 올 수 없습니다.
tool_use_id가 일치하지 않음tool_use 블록의 정확한 id를 그대로 반영하세요. 새로 생성하지 마세요.
왕복 중간에 기록이 잘림두 절반으로 이루어진 하나의 왕복 중간이 아니라, 전체 도구 왕복 단위로 잘라내세요.

불변 조건을 한 번에 정리하면

assistant 턴의 모든 tool_use 블록에는 바로 다음 user 메시지에 정확히 하나의 tool_result 블록이 있어야 하며, 동일한 tool_use_id를 포함해야 합니다. 한 턴에 tool_use 블록이 여러 개라면 바로 다음 메시지 하나에 그 수만큼의 tool_result 블록이 필요합니다. 두 턴 사이에는 아무것도 올 수 없습니다.

도구가 실패해도 항상 응답하세요

모델은 실패한 도구를 문제없이 처리할 수 있지만 누락된 도구는 처리하지 못합니다. 오류를 tool_result로 반환하면 대화가 유효하게 유지되고, 대개 400 대신 합리적인 복구가 이루어집니다.

tool_loop.py
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})

전송 전에 마지막 두 턴을 검증하세요

십여 줄의 단언으로 네트워크에서 400을 받은 뒤가 아니라 호출 지점에서 문제를 잡을 수 있습니다. assistant 턴의 tool_use id를 순회하면서 다음 user 턴이 모두 응답하는지 확인하세요.

validate.py
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}"

왕복 경계에서 기록을 잘라내세요

메시지 수를 기준으로 컨텍스트 창을 자르면 결국 tool_use와 tool_result 사이가 잘립니다. 버릴 내용을 결정할 때 이 쌍을 나눌 수 없는 하나의 단위로 취급하세요.

Kunavo를 통해 호출하는 경우

이 문제는 페이로드 자체에 있으며 Kunavo가 이를 덮어 주지는 않습니다. 400은 재시도하지 않는 목록에 있으므로 잘못된 도구 왕복은 한 번만 실패하고, 같은 오류를 받기 위해 두 번째 업스트림 왕복에 따른 지연을 추가로 발생시키지 않으며, 거부된 요청의 비용은 0으로 기록됩니다. /v1/messages에서는 Messages 프로토콜을 직접 사용하므로 tools, tool_use 및 tool_result 블록이 변환되지 않고 전달됩니다. 이 경우 400은 Anthropic API와 마찬가지로 invalid_request_error 유형으로 반환되며, 업스트림 메시지 텍스트 뒤에 업스트림 자체 요청 ID가 붙고 request_id 필드는 없습니다. 2026년 9월 24일까지는 유형이 api_error였으므로 로그가 그 이전까지 거슬러 올라간다면 HTTP 상태와 메시지 텍스트를 기준으로 분기하세요.

자주 묻는 질문

tool_use 턴을 그냥 삭제하고 응답하지 않아도 되나요?

예, 전체 assistant 턴을 삭제한다면 가능합니다. 유효하지 않은 것은 tool_use를 남겨 두고 tool_result를 생략하는 것입니다.

OpenAI 호환 엔드포인트에도 같은 규칙이 적용되나요?

동일한 페어링이 필요하지만 표기는 다릅니다. assistant 메시지에 tool_calls가 있고, 각 호출마다 tool_call_id를 포함한 role: "tool" 메시지가 하나씩 뒤따라야 합니다.

거부된 요청에도 비용이 발생하나요?

Kunavo에서는 비용이 들지 않습니다. 실패한 요청은 비용 0으로 기록되며 요금이 청구되는 업스트림 호출에 절대 도달하지 않습니다.

관련 가이드

오류 의미에 대한 자세한 내용은 오류 참조에서 확인할 수 있습니다. 가입 및 인증 가이드를 통해 1분이면 키를 받을 수 있습니다.