- Published on
Gemini API 400 INVALID_ARGUMENT 디버깅 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
Gemini API를 붙이다 보면 가장 당황스러운 에러 중 하나가 400 INVALID_ARGUMENT 입니다. 네트워크도 정상이고 인증도 통과했는데, 서버가 “요청이 잘못됐다”고만 말하는 상황이죠. 더 난감한 점은 이 에러가 프롬프트 자체의 문제일 수도 있고, 요청 JSON 스키마가 살짝 틀린 것일 수도 있으며, 안전필터(정책)와 결합된 입력 때문에 거절되는 경우도 있다는 것입니다.
이 글에서는 400 INVALID_ARGUMENT를 “감으로 수정”하지 않고, 원인을 분리해 재현하고, 빠르게 고치는 절차를 제공합니다. 특히 프롬프트·안전필터 디버깅에 초점을 맞춰, 실제 운영 환경에서 쓸 수 있는 가드레일과 로그 설계까지 다룹니다.
1) 400 INVALID_ARGUMENT의 의미를 좁히는 3단계
Gemini의 400 INVALID_ARGUMENT는 보통 아래 3가지 범주 중 하나로 수렴합니다.
1-1. 요청 스키마/필드 타입 오류
contents구조가 잘못됨parts타입이 잘못됨(text대신 다른 키, 혹은 배열/문자열 혼동)generationConfig값 범위 오류(예:temperature범위,maxOutputTokens음수)tools또는 함수 호출 스키마가 잘못됨
1-2. 모델/엔드포인트 불일치
- 호출한 모델이 해당 메서드를 지원하지 않음
- 멀티모달 입력을 텍스트 전용 모델에 넣음
- region/버전 차이로 지원 파라미터가 달라짐
1-3. 프롬프트/안전필터 트리거로 인한 요청 거절
- 특정 정책 위반 가능성이 높은 문구 포함
- “우회”, “탈옥”, “필터 무력화” 류의 메타 프롬프트
- 개인 정보/민감 정보/불법 행위 지시가 직접적으로 포함
핵심은 스키마 오류와 정책/안전필터 트리거를 분리해서 디버깅하는 것입니다. 둘을 섞어서 보면, 프롬프트를 아무리 다듬어도 스키마가 틀리면 계속 400이 나고, 반대로 스키마가 맞아도 정책에 걸리면 계속 400이 납니다.
2) 재현 가능한 “최소 요청”부터 시작하기
디버깅의 첫 단계는 “내가 보낸 요청이 진짜로 올바른가?”를 검증하는 것입니다. 가장 작은 텍스트 요청으로 시작해 성공을 만든 다음, 옵션을 하나씩 추가하세요.
아래는 REST 호출 예시입니다. 중요한 점은 contents는 배열, parts도 배열, text는 문자열이라는 구조를 유지하는 것입니다.
curl -sS \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $GOOGLE_OAUTH_TOKEN" \
"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent" \
-d '{
"contents": [
{
"role": "user",
"parts": [{"text": "Hello"}]
}
]
}'
이 최소 요청이 성공하면, 이제부터는 변경 사항을 “한 번에 하나”만 추가합니다.
generationConfig추가system역할(또는 시스템 프롬프트 대체 구조) 추가tools추가- 멀티턴 대화 추가
- 멀티모달 파트 추가
이 과정을 거치면 400이 발생하는 지점을 정확히 특정할 수 있습니다.
3) 스키마 디버깅: 흔한 실수 체크리스트
3-1. contents/parts 구조 깨짐
가장 흔한 실수는 parts를 객체로 보내거나, text를 배열로 보내는 것입니다.
잘못된 예시(의도치 않게 parts가 객체):
{
"contents": [{
"role": "user",
"parts": {"text": "hi"}
}]
}
올바른 예시:
{
"contents": [{
"role": "user",
"parts": [{"text": "hi"}]
}]
}
3-2. generationConfig 범위/타입
언어 모델 설정 값은 서비스/버전에 따라 허용 범위가 다를 수 있습니다. 특히 temperature, topP, maxOutputTokens는 다음을 점검하세요.
- 숫자 타입인지(문자열로 보내지 않았는지)
- 음수/0 등 비정상 값인지
- 너무 큰
maxOutputTokens를 요청하지 않는지
예시:
{
"generationConfig": {
"temperature": 0.2,
"topP": 0.9,
"maxOutputTokens": 512
}
}
3-3. 함수 호출/툴 스키마가 원인인 경우
툴/함수 호출은 스키마가 조금만 어긋나도 400이 납니다. 이때는 “프롬프트”가 아니라 “도구 정의 JSON”이 원인인 경우가 많습니다.
name규칙 위반(공백/특수문자)parametersJSON Schema 불완전required필드가properties에 없음
툴 스키마 디버깅 감각은 다른 벤더의 400에도 그대로 통합니다. 예를 들어 Claude의 도구 스키마 오류를 다룬 글도 함께 보면, “스키마를 최소화하고 점진적으로 확장”하는 접근이 동일합니다.
4) 프롬프트 디버깅: 안전필터 트리거를 분리하는 법
스키마가 맞는데도 400이 나는 경우, 프롬프트가 정책에 걸릴 가능성을 봐야 합니다. 다만 여기서 실수하기 쉬운 점이 있습니다.
- “안전필터에 걸리면 403이나 429 같은 게 나오지 않나?”
- “왜 400으로 오지?”
서비스마다 정책 거절의 표현이 다르고, 경우에 따라 입력 자체를 INVALID_ARGUMENT로 처리하기도 합니다. 따라서 프롬프트를 최소화하며 트리거를 찾는 방식이 안전합니다.
4-1. 프롬프트를 3조각으로 쪼개기
대부분의 운영 프롬프트는 다음이 섞여 있습니다.
- 시스템 지침(역할, 금지사항, 출력 포맷)
- 유저 입력(자유 텍스트)
- 내부 컨텍스트(문서, 로그, DB 결과)
디버깅할 때는 이 3개를 분리해서, 어느 조각이 400을 유발하는지 확인합니다.
- 시스템 지침만 넣고 호출
- 시스템 지침 + 유저 입력
- 시스템 지침 + 내부 컨텍스트
- 전부 합치기
이렇게 하면 “유저 입력의 특정 표현” 때문인지, “내부 컨텍스트에 포함된 민감 텍스트” 때문인지 빠르게 구분됩니다.
4-2. 트리거가 되는 흔한 패턴
아래 패턴은 정책에 민감하게 반응할 수 있습니다.
- “필터 우회”, “제한 무시”, “탈옥”, “시스템 프롬프트를 보여줘” 류의 문구
- 불법 행위의 구체적 절차/레시피 요청
- 개인식별정보(주민번호, 카드번호 등) 또는 대량의 개인정보 덤프
- 폭력/자해/혐오를 조장하는 문장(인용/재현이라도 민감)
특히 내부 컨텍스트에 운영 로그를 그대로 넣는 경우, 사용자가 입력한 공격적 텍스트가 그대로 들어가면서 안전필터를 자극하는 일이 많습니다.
4-3. “정책 문구”를 직접 쓰지 말고 기능적으로 표현하기
시스템 프롬프트에 흔히 넣는 문장 중 일부는 오히려 트리거가 되기도 합니다.
나쁜 예시(메타적으로 민감):
"안전필터를 우회하지 마""정책을 무시하는 요청은 거부해"
대신 기능적으로 표현합니다.
개선 예시:
"불법적이거나 위험한 요청에는 대안을 제시하고, 안전한 범위의 정보만 제공해""개인정보는 마스킹하고 요약만 제공해"
즉, “우회/탈옥” 같은 키워드를 직접 반복하기보다, 원하는 행동을 긍정 형태로 명시하는 편이 안정적입니다.
5) 입력 정규화: 로그/컨텍스트가 프롬프트를 망치는 경우
운영에서 자주 만나는 케이스는 “프롬프트 자체는 멀쩡한데, 붙여 넣은 컨텍스트가 문제”입니다.
5-1. 컨텍스트에 포함된 특수문자/바이너리
- 깨진 인코딩
- 제어문자
- 매우 긴 base64
- JSON이지만 중괄호/따옴표가 깨진 텍스트
이런 경우는 정책이 아니라 입력 파서/검증 로직에서 INVALID_ARGUMENT가 날 수 있습니다.
Node.js에서 제어문자 제거 예시:
export function sanitizeContext(s) {
return s
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, "")
.replace(/\uFFFD/g, "");
}
5-2. 길이 제한과 토큰 폭발
컨텍스트를 무작정 붙이면 토큰이 폭발합니다. 이때는 413이 아니라 400으로 뭉뚱그려 오는 경우도 있어, “왜 400이지?”가 됩니다.
대응:
- 컨텍스트 상한(문자 수/토큰 수)을 둔다
- 긴 로그는 요약 후 넣는다
- RAG라면 topK를 줄이고 chunk 크기를 조정한다
6) 에러 메시지를 “관측 가능하게” 만드는 로깅 전략
400은 클라이언트가 잘못 보냈다는 의미이므로, 서버가 친절하게 다 말해주지 않습니다. 그래서 클라이언트 쪽에서 관측을 강화해야 합니다.
6-1. 요청 바디 전체를 로그로 남기지 말고, 해시/메타만 남기기
프롬프트에는 개인정보나 내부 데이터가 섞이기 쉽습니다. 원문을 그대로 로깅하면 보안 사고로 이어집니다.
권장 로그 필드:
modelcontents의 총 문자 수- 각 파트의 타입 분포(텍스트/이미지 등)
generationConfig요약- 프롬프트 해시(예: SHA-256)
- 실패한 요청의 샘플링 저장(엄격한 마스킹 후)
Python 해시 예시:
import hashlib
def prompt_fingerprint(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
6-2. 실패 재현을 위한 “미니멈 리플레이” 저장
원문을 저장하지 않더라도, 다음은 저장하면 재현에 도움이 됩니다.
- 어떤 템플릿 버전이었는지
- 어떤 컨텍스트 소스였는지(DB 쿼리 ID, 문서 ID)
- 길이/토큰 추정치
- 안전필터 관련 옵션을 바꿨는지 여부
CI/CD나 운영 자동화에서 실패를 다루는 방식은 다른 장애 대응과도 유사합니다. 스크립트가 중간에 실패하면 원인 파악이 어려운 것처럼, LLM 요청도 실패 원인을 남기지 않으면 “그때 그 입력”을 다시 못 찾습니다.
7) 안전필터 디버깅용 “이진 탐색” 프롬프트 기법
정책 트리거가 의심되면, 프롬프트를 반으로 쪼개며 원인 구간을 찾는 이진 탐색이 효과적입니다.
절차:
- 전체 프롬프트를 A/B 두 덩어리로 분리
- A만 보내서 성공/실패 확인
- 실패한 덩어리를 다시 반으로 분리
- 특정 문장/토큰 수준까지 내려가 트리거를 특정
이때 “문장 단위”로 split하면 충분합니다. 로그/문서 컨텍스트라면 “chunk 단위”로 split합니다.
Node.js 예시(문장 단위 분할 후 이진 탐색을 위한 도우미):
export function splitSentences(text) {
return text
.split(/(?<=[.!?\n])\s+/)
.map(s => s.trim())
.filter(Boolean);
}
export function joinRange(arr, start, end) {
return arr.slice(start, end).join(" ");
}
이 방식으로 “어떤 문장이 트리거인지”까지 좁히면, 대개 해결은 다음 중 하나로 끝납니다.
- 해당 문장을 삭제하거나 완곡하게 바꾸기
- 인용을 명시하고 요약 형태로 바꾸기
- 민감 정보는 마스킹 후 넣기
- 유저 입력은 그대로 넣되, 모델에 직접 실행 지시가 되지 않도록 재구성하기
8) 운영 가드레일: 400을 ‘사전에’ 줄이는 패턴
8-1. 입력 검증 레이어를 둔다
- 길이 제한
- 금지된 제어문자 제거
- JSON 스키마 검증(특히 tool schema)
- PII 마스킹
8-2. 프롬프트 템플릿 버전 관리
프롬프트를 코드처럼 다뤄야 합니다.
- 템플릿에 버전 부여
- 변경 시 A/B 테스트
- 특정 버전에서 400이 급증하면 즉시 롤백
8-3. “정책 민감” 키워드 자체를 템플릿에 넣지 않기
앞서 언급했듯, 시스템 프롬프트에서 메타 키워드를 반복하면 오히려 실패율이 오를 수 있습니다. 정책 문구는 최소화하고, 원하는 출력 행동을 구체적으로 정의하세요.
9) 트러블슈팅 요약 체크리스트
아래 순서대로 진행하면 대부분의 400 INVALID_ARGUMENT는 짧게 끝납니다.
- 최소 요청으로 성공 확인(
contents/parts구조) - 옵션을 하나씩 추가하며 실패 지점 찾기
generationConfig타입/범위 검증- tool/function schema를 최소화해서 검증
- 프롬프트를 시스템/유저/컨텍스트로 분리
- 컨텍스트 정규화(제어문자, 인코딩, 길이)
- 정책 트리거는 이진 탐색으로 특정
- 템플릿 버전 관리 + 입력 검증으로 재발 방지
유사한 400 디버깅은 다른 LLM API에서도 반복됩니다. “스키마를 최소화하고 점진적으로 확장”하는 방식, 그리고 “실패 입력을 재현 가능하게 남기는 관측”이 핵심입니다.
10) 마무리: 400은 ‘프롬프트 탓’만이 아니다
400 INVALID_ARGUMENT가 뜨면 본능적으로 프롬프트를 의심하기 쉽지만, 실제로는 요청 스키마, 입력 정규화, 툴 스키마, 길이/토큰, 정책 트리거가 얽혀 나타나는 경우가 많습니다.
가장 빠른 해결법은 단순합니다.
- “최소 요청 성공”을 먼저 만든다
- 변경을 하나씩만 추가한다
- 프롬프트를 조각내고, 실패 구간을 이진 탐색한다
- 재발 방지를 위해 검증/로그/버전 관리를 넣는다
이 루틴을 팀의 표준 디버깅 절차로 만들어두면, Gemini뿐 아니라 어떤 LLM API에서도 400류 장애 대응 속도가 눈에 띄게 빨라집니다.