Published on

LangChain Tool Calling 400 invalid_request 오류 9가지

Authors

서버에서 400 invalid_request가 떨어질 때 가장 난감한 지점은, LangChain 코드가 “그럴듯하게” 보이는데도 실제로는 OpenAI(또는 호환) API가 요구하는 스키마와 미묘하게 어긋나는 경우가 많다는 점입니다. 특히 Tool Calling은 요청 페이로드에 tools, tool_choice, messages(또는 input)가 함께 얽히면서, 작은 불일치가 즉시 400으로 이어집니다.

이 글은 LangChain에서 Tool Calling을 사용할 때 자주 발생하는 400 invalid_request를 9가지 유형으로 나눠, 원인과 해결책을 빠르게 찾을 수 있도록 구성했습니다. Structured Outputs와 스키마 관련 400을 더 깊게 다룬 글은 OpenAI Structured Outputs 400 해결 - JSON Schema도 함께 참고하면 좋습니다.

먼저: 400을 “재현 가능하게” 만드는 디버깅 습관

1) LangChain에서 실제로 나가는 요청을 로그로 확인

LangChain은 추상화가 두껍기 때문에, “내가 생각한 요청”과 “실제로 나간 요청”이 다를 수 있습니다. 아래처럼 디버깅을 켜고, 에러 응답의 error.message를 반드시 확보하세요.

import os
from langchain_openai import ChatOpenAI

os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "YOUR_LANGSMITH_KEY"  # 선택

llm = ChatOpenAI(
    model="gpt-4.1-mini",
    temperature=0,
)

# 호출 시점의 예외 메시지에 실제 API 에러가 포함되는지 확인

2) 실패한 요청을 “최소 입력”으로 줄이기

  • 시스템 프롬프트, 긴 컨텍스트, 여러 툴을 한 번에 넣지 말고
  • 툴 1개, 메시지 1개로 축소해서 400이 계속 나는지 확인합니다.

이 과정을 거치면 아래 9가지 중 어디에 해당하는지 훨씬 빨리 좁혀집니다.

오류 1) tools 스키마가 OpenAI 규격과 다름

증상

  • invalid_request와 함께 tools[0].function 혹은 tools[0].type 관련 메시지
  • 혹은 “Unknown field” 류의 메시지

원인

Tool Calling은 대체로 다음 형태를 요구합니다.

  • type: "function"
  • function: { name, description, parameters }
  • parameters: JSON Schema 오브젝트

LangChain에서 툴을 직접 dict로 만들거나, 커스텀 변환을 하다가 필드명이 틀어지면 400이 납니다.

해결

LangChain의 @tool 또는 StructuredTool을 사용해 표준 형태로 생성되게 하세요.

from langchain_core.tools import tool

@tool
def add(a: int, b: int) -> int:
    """Add two integers."""
    return a + b

tools = [add]

직접 dict를 만들어야 한다면 typefunction.parameters가 JSON Schema인지(특히 type: object, properties, required)를 점검하세요.

오류 2) parameters가 JSON Schema가 아니라 “예시 JSON”인 경우

증상

  • invalid_request + schema 혹은 parameters 관련 에러

원인

parameters에 아래처럼 “입력 예시”를 넣는 실수를 자주 합니다.

# 잘못된 예: parameters에 예시 JSON을 넣음
bad_tool = {
  "type": "function",
  "function": {
    "name": "search",
    "description": "search docs",
    "parameters": {"query": "hello"}
  }
}

해결

parameters는 반드시 JSON Schema여야 합니다.

ok_tool = {
  "type": "function",
  "function": {
    "name": "search",
    "description": "search docs",
    "parameters": {
      "type": "object",
      "properties": {
        "query": {"type": "string", "description": "Search query"}
      },
      "required": ["query"],
      "additionalProperties": False
    }
  }
}

스키마 엄격 모드에서의 400은 특히 흔합니다. 더 자세한 스키마 설계 팁은 OpenAI Structured Outputs 400 해결 - JSON Schema를 권장합니다.

오류 3) tool_choice 값이 모델/엔드포인트와 호환되지 않음

증상

  • tool_choice 관련 invalid_request
  • “tool_choice must be ...” 같은 메시지

원인

tool_choice는 API/모델에 따라 허용 형태가 다릅니다. LangChain에서 다음을 섞어 쓰다 400이 납니다.

  • "auto"
  • "required"
  • 특정 함수 강제 선택 형태

또한 tools를 넘기지 않았는데 tool_choice만 지정하면 실패합니다.

해결

  • tools가 있을 때만 tool_choice를 설정
  • 강제 호출이 필요하면 “특정 함수 지정”을 올바른 형태로 전달
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4.1-mini", temperature=0)

# LangChain 버전에 따라 bind_tools 인자가 다를 수 있음
llm_with_tools = llm.bind_tools(
    tools=[add],
    tool_choice="auto",  # 우선 auto로 검증
)

강제 호출이 필요하다면 먼저 auto로 400이 사라지는지 확인한 뒤, 그 다음에 강제 호출을 적용하는 순서가 안전합니다.

오류 4) Tool 이름이 규칙을 위반하거나 중복됨

증상

  • invalid_request + tool name 관련
  • 혹은 “duplicate tool name”

원인

  • 툴 이름에 공백, 특수문자 등이 들어감
  • 같은 이름의 툴을 여러 개 등록
  • LangChain에서 자동 생성된 이름이 충돌

해결

  • 명시적으로 유니크한 name을 부여
  • 툴 리스트를 합치기 전에 이름 충돌 검사
from langchain_core.tools import StructuredTool

def add_impl(a: int, b: int) -> int:
    return a + b

add_tool = StructuredTool.from_function(
    func=add_impl,
    name="math_add",
    description="Add two integers",
)

오류 5) additionalProperties 또는 required 설정 불일치로 스키마가 모순됨

증상

  • invalid_request + JSON Schema validation 관련

원인

아래 같은 “스키마 모순”이 있으면 400이 날 수 있습니다.

  • required에 정의되지 않은 필드가 포함
  • additionalProperties: False인데 모델이 생성해야 할 필드가 properties에 없음
  • properties는 있는데 최상위 typeobject가 아님

해결

스키마를 단순화해 단계적으로 강화하세요.

parameters = {
  "type": "object",
  "properties": {
    "a": {"type": "integer"},
    "b": {"type": "integer"}
  },
  "required": ["a", "b"],
  "additionalProperties": False
}

그리고 툴 입력을 Pydantic 기반으로 만들면(버전에 따라 pydantic v1/v2 호환 주의) 이런 실수를 크게 줄일 수 있습니다.

오류 6) 메시지 포맷 불일치: messages에 잘못된 role/content 구조

증상

  • invalid_request + messages[0]... 같은 인덱스 기반 에러

원인

LangChain의 메시지 타입을 섞어 쓰거나, 커스텀으로 messages를 만들면서 아래 실수를 합니다.

  • role이 허용되지 않는 값
  • content가 문자열이 아닌데 포맷을 맞추지 않음
  • tool 결과를 tool role로 넣어야 하는데 assistant로 넣음

해결

LangChain 메시지 클래스를 사용하고, 툴 실행 결과는 LangChain이 붙이도록(에이전트/툴 실행 루프) 맡기는 편이 안전합니다.

from langchain_core.messages import SystemMessage, HumanMessage

messages = [
    SystemMessage(content="You are a helpful assistant."),
    HumanMessage(content="1 더하기 2는? 툴을 써도 돼."),
]

resp = llm_with_tools.invoke(messages)

직접 tool 결과 메시지를 구성해야 한다면, LangChain 버전별 tool message 포맷을 확인하고 그대로 따르세요.

오류 7) 모델이 Tool Calling을 지원하지 않거나, 엔드포인트가 다름

증상

  • 같은 코드가 모델만 바꾸면 400
  • This model does not support tools 류의 메시지

원인

  • Tool Calling 미지원 모델 사용
  • OpenAI 호환 서버(게이트웨이, 프록시, 사설 LLM)에서 tools 필드를 미지원
  • Chat Completions 스타일과 Responses API 스타일을 혼용

해결

  • Tool Calling 지원 모델로 변경
  • 사용하는 LangChain 통합이 어떤 API 스타일을 쓰는지 확인
  • 사설 게이트웨이라면 tools 지원 여부를 문서로 확인

모델/엔드포인트 이슈는 애플리케이션 레벨에서 타임아웃/재시도와 함께 나타나기도 합니다. 네트워크/서버 지연까지 포함해 운영 관점에서 점검하려면 OpenAI Responses API 408 타임아웃 재현과 해결 실전 가이드도 같이 보면 좋습니다.

오류 8) tool 입력 타입 불일치: 문자열로 와야 하는데 객체로 보냄(또는 반대)

증상

  • invalid_request + type mismatch 또는 expected string

원인

LangChain에서 툴 인자를 자동으로 직렬화하는 과정에서,

  • 이미 dict인데 JSON 문자열로 한 번 더 인코딩
  • 반대로 문자열이어야 하는 필드에 dict를 그대로 전달

특히 “툴이 내부적으로 HTTP 호출을 하는데 body를 문자열로 받도록 구현”한 경우에 많이 발생합니다.

해결

툴 함수 시그니처를 “최종적으로 받고 싶은 타입”으로 명확히 하세요. 문자열 JSON을 받지 말고, 가능한 한 구조화된 인자를 받도록 설계합니다.

from langchain_core.tools import tool
from pydantic import BaseModel

class SearchArgs(BaseModel):
    query: str
    top_k: int = 5

@tool(args_schema=SearchArgs)
def search(query: str, top_k: int = 5) -> str:
    """Search documents by query."""
    return f"query={query}, top_k={top_k}"

이렇게 하면 스키마와 런타임 타입이 일치해 400 가능성이 크게 줄어듭니다.

오류 9) LangChain 버전/의존성 불일치로 tool 스키마 생성이 깨짐

증상

  • 최근 업그레이드 이후 갑자기 400
  • 같은 코드가 로컬에서는 되는데 CI/서버에서는 400

원인

  • langchain, langchain-core, langchain-openai 버전 조합이 맞지 않음
  • pydantic v1/v2 혼재로 JSON Schema 생성이 달라짐
  • 툴 스키마 생성 시 nullable, anyOf 등이 예상과 다르게 생성되어 API가 거부

해결

  1. 의존성 버전을 “세트로” 고정하세요.
  2. 배포 환경과 로컬의 lockfile을 일치시키세요.
pip show langchain langchain-core langchain-openai pydantic
# requirements.txt 예시(프로젝트 상황에 맞게 고정)
langchain==0.3.XX
langchain-core==0.3.XX
langchain-openai==0.3.XX
pydantic==2.XX.X

또한 툴 스키마를 로그로 덤프해서, 실제로 생성된 JSON Schema가 API가 허용하는 형태인지 확인하는 것이 핵심입니다.

import json

# StructuredTool/@tool로 만든 경우에도 내부적으로 schema를 만들 수 있음
# 아래는 개념 예시이며, 실제 접근자는 버전에 따라 다를 수 있음
schema = add.args_schema.model_json_schema()  # pydantic v2 기준
print(json.dumps(schema, ensure_ascii=False, indent=2))

실전 체크리스트: 400 invalid_request를 10분 내로 끝내는 순서

  1. tools를 완전히 제거한 호출이 정상인지 확인(모델/엔드포인트 기본 동작 점검)
  2. 툴 1개만 추가하고 tool_choiceauto
  3. 툴 이름 중복/규칙 위반 확인
  4. parameters가 JSON Schema인지 확인(특히 type: object, properties, required)
  5. additionalProperties: False를 잠시 제거해 스키마 엄격성으로 인한 400인지 분리
  6. Pydantic 기반 args_schema로 옮겨 타입 불일치 제거
  7. 의존성 버전 고정 및 로컬/서버 동일화

마무리: “요청 페이로드가 정확히 무엇이냐”가 답이다

LangChain Tool Calling의 400 invalid_request는 대부분 “툴 스키마” 또는 “메시지/옵션 조합”의 미세한 불일치에서 발생합니다. 해결의 지름길은 추상 레이어를 믿기보다, 실제로 전송된 tools JSON과 에러 메시지의 필드 경로를 1:1로 대조하는 것입니다.

스키마가 조금만 복잡해져도 400이 잦다면, 툴 입력을 Pydantic으로 표준화하고, JSON Schema를 점진적으로 강화하는 방식으로 운영 안정성을 확보하세요. Structured Outputs 관점에서의 스키마 설계는 OpenAI Structured Outputs 400 해결 - JSON Schema가 직접적인 도움이 됩니다.