LangGraph의 툴 호출은 bind_tools가 보여주는 것보다 훨씬 공급자 종속적임
Tool calling in LangGraph is more provider-specific than bind_tools made it look
핵심 요약
LangGraph에서 LLM을 교체할 때 발생하는 툴 호출 규격 불일치와 그에 따른 대응 방안에 대한 고찰입니다.
- 툴 호출 규격 — 모델마다 병렬 호출 방식과 ID 생성 체계가 달라 호환성 문제가 발생함
- 필수 호출 무시 — 일부 소형 모델은 tool_choice='required'를 제대로 이행하지 않는 경우가 있음
- 데이터 파싱 오류 — 중첩된 객체 스키마가 딕셔너리가 아닌 JSON 문자열로 반환되는 현상이 있음
- 래퍼 노드 도입 — 상태 관리를 위해 툴 호출을 정규화하는 별도의 래퍼 노드를 사용 중임
LangGraph 데이터 분석 에이전트를 실서비스에 돌리는 중이다. 노드 6개짜리 그래프고, 스키마 조회, SQL 실행, 결과 포맷팅 툴을 쓴다. 원래는 gpt-5.4에 bind_tools랑 병렬 툴 호출 써서 돌렸는데, 잘 돌아가긴 하는데 비용이 너무 많이 나오더라.
그래서 메인 노드 뒤에 붙는 LLM을 바꿔보기로 했다. GMI Cloud에서 GPT-OSS 120B랑 DeepSeek V4-Pro를 돌려봤고, 비교군으로 OpenAI 호환 호스트 몇 군데도 더 써봤다.
겉보기엔 다 똑같다. ChatOpenAI(base_url=...)에 bind_tools(tools) 박아 넣으면 에이전트가 끝까지 잘 돌아가는 것처럼 보임. 근데 툴 호출 단계에서 실제로 뭐가 넘어오는지 뜯어보면 슬슬 문제가 터지기 시작한다.
병렬 호출 문제다. gpt-5.4는 AIMessage 하나에 tool_calls 여러 개를 묶어서 던져주는데, DeepSeek V4-Pro도 대부분 그렇게 하긴 하지만 가끔 호출 두 개가 메시지 두 개로 쪼개져서 올 때가 있다. ToolNode는 둘 다 잘 처리하는데, 내 리듀서(reducer)가 이걸 못 받아먹더라.
툴 호출 ID도 골치 아프다. 어떤 엔드포인트는 OpenAI 스타일로 call_id를 잘 뽑아주는데, 어떤 건 짧은 해시값만 던지고, 부하 걸리면 아예 비어서 올 때도 있다. MessagesState는 이 ID를 기준으로 ToolMessage 응답을 매칭하는데, ID가 비어 있으면 다음 턴에서 바로 400 에러 뜬다.
tool_choice="required" 옵션도 문제다. gpt-5.4는 말 잘 듣는데, 작은 모델들은 5~8% 확률로 이걸 씹고 그냥 툴 호출 없는 일반 AIMessage를 뱉어버린다. 시스템 프롬프트를 더 빡세게 수정해서 재시도하거나, 아예 하드코딩으로 폴백 경로를 만들어야 한다.
인자(args)를 문자열로 던지는 경우도 있다. 어떤 호스트에서는 30번에 한 번꼴로 중첩 객체 스키마가 딕셔너리가 아니라 JSON 인코딩된 문자열로 넘어온다. tool_calls 필드는 있는데, args 파싱이 안 된 상태로 오는 거다.
각 엔드포인트마다 OpenAI 호환 툴 호출을 지들 맘대로 구현해놔서, 딱히 뭐가 틀렸다고 하기도 애매하다. bind_tools의 핵심은 LLM을 바꿔 끼워도 그래프는 그대로 돌아가야 한다는 건데, 단발성 구조화 출력은 그럭저럭 먹히지만, 툴 호출이 상태를 바꾸고 다음 프롬프트에 영향을 주는 LangGraph 루프 안에서는 최소 네 군데 이상에서 박살이 난다.
지금은 모델 노드 앞에 얇은 래퍼(wrapper)를 하나 씌워서 상태에 들어가기 전에 tool_calls를 정규화하고 있다. call_id 없으면 채워 넣고, args가 문자열로 오면 파싱하고, 툴 호출이 아예 없으면 더 엄격한 프롬프트로 모델을 한 번 더 재시도하게 만들었다.
진짜 해결 안 되는 건 꼬리(tail) 쪽이다. p50은 어떤 공급자를 써도 다 비슷비슷한데, p95는 차이가 너무 크다. 우리 SLO(서비스 수준 목표)가 p95 기준이라 이게 문제다. 다단계 LangGraph 에이전트를 여러 공급자에서 돌리는 형들은 에이전트마다 호스트를 고정해두고 쓰냐, 아니면 모델 노드마다 래퍼 레이어 안 씌워도 되는 깔끔한 방법 찾은 사람 있냐?

