LLM의 도구 호출(Tool Calls)이 조용히 실패하는 이유와 ~10µs 해결책
요약
LLM 스트리밍 응답 중 도구 호출(Tool calls)이나 구조화된 출력이 중간에 끊겨 발생하는 JSON 파싱 오류의 원인을 분석합니다. 이를 해결하기 위해 코드 수정 없이 네트워크 수준에서 스트림을 복구하는 프록시 Suture를 소개합니다.
핵심 포인트
- 스트리밍 중 max_tokens 도달이나 소켓 끊김으로 인한 JSON 불완전성 문제 발생
- 단순 재시도는 비용이 높고 비결정론적이라 근본적인 해결책이 아님
- Suture는 바이트 수준 엔진으로 끊긴 JSON을 감지하고 종료 문자를 삽입하여 복구
- API 키 수정 없이 마이크로초 단위의 낮은 지연시간으로 스트림 복구 가능
LLM으로부터 도구 호출(Tool calls)이나 구조화된 출력(Structured output)을 스트리밍(Stream)한다면, 운영 환경에서 다음과 같은 오류 중 하나를 거의 확실히 목격했을 것입니다:
json.decoder.JSONDecodeError: Unterminated string starting at: line 1 column 12 (char 11)
serde_json::Error: EOF while parsing a string at line 1 column 4096
이 오류는 보통 부하가 걸릴 때, 혹은 가장 길고 중요한 응답을 처리할 때 나타납니다. 모델은 자신의 할 일을 다 했음에도 불구하고 단순히 중간에 끊겨버린 것이기에 매우 당혹스럽습니다. 이 포스트에서는 왜 이런 일이 발생하는지, 왜 뻔한 해결책들이 실제로 작동하지 않는지, 그리고 코드나 API 키를 수정하지 않고도 마이크로초 단위로 네트워크 상에서 이를 해결해 주는 작은 프록시(Suture)에 대해 다룹니다.
실제로 무엇이 고장 나는가
채팅 완성(Chat completion)을 스트리밍할 때, 제공자(Provider)는 하나의 JSON 문서를 보내는 것이 아닙니다. 대신 조각(Fragment)을 담고 있는 각각의 완전하고 유효한 작은 JSON 객체인 서버 전송 이벤트(Server-Sent Events, SSE)의 긴 시퀀스를 보냅니다:
data: {"choices":[{"delta":{"tool_calls":[{"function":{"arguments":"{\"ci"}}]}}}]}
data: {"choices":[{"delta":{"tool_calls":[{"function":{"arguments":"ty\":\"Par"}}]}}]}
data: {"choices":[{"delta":{"tool_calls":[{"function":{"arguments":"is\"}"}}]}}]}
...
여러분의 SDK는 이 모든 이벤트에 걸쳐 있는 arguments 필드를 하나의 문자열 — {"city":"Paris"} — 으로 **재조합(Reassembles)**한 후에 이를 파싱(Parse)합니다. 여기서 문제는, 실제 JSON 데이터(도구 인자(Tool arguments) 또는 구조화된 출력의 content)가 이러한 조각들 _내부_에 존재하며, 스트림 전체가 도착해야만 비로소 완전해진다는 점입니다.
따라서 스트림이 조기에 종료되면 — 모델이 max_tokens에 도달하거나, 컨텍스트 윈도우(Context window)를 초과하거나, 혹은 소켓(Socket)이 그냥 끊겨버리면 — 여러분은 다음과 같은 상태를 마주하게 됩니다:
{"city":"Par
SSE 봉투(Envelope)는 문제가 없었습니다. 하지만 재조합된 JSON은 문제가 있습니다. 따라서 파서(Parser)에서 오류가 발생합니다.
뻔한 해결책들이 작동하지 않는 이유
- 요청 재시도 (Retry the request). 긴 생성 과정 전체에 대해 비용을 다시 지불해야 하며, 똑같은 방식으로 다시 잘릴 수 있습니다. 비용이 많이 들고 비결정론적 (non-deterministic)입니다.
try/except후 무시하기. 모델이 실제 토큰을 사용하여 생성한 응답을 버리게 됩니다. 종종 `
Suture는 귀하의 요청을 있는 그대로 전달하며(귀하의 키는 단순히 통과할 뿐이며, Suture는 아무것도 저장하지 않습니다), 스트리밍 응답을 관찰하고, 바이트 수준 엔진(byte-level engine)을 통해 재조립된 도구 인자(tool-args) / 구조화된 콘텐츠(structured content)를 추적하며, 스트림이 끝날 때 종료 토큰(terminator) 직전의 최종적이고 잘 형성된 델타 이벤트(delta event)로서 이를 닫는 데 필요한 정확한 문자를 방출합니다. 귀하의 클라이언트는 유효한 JSON을 재조립하게 되며, 무엇인가 잘못되었다는 사실을 전혀 알지 못합니다.
중요한 설계 선택 사항:
- 추가 전용(append-only) 및 통과(passthrough) 방식입니다. 완전한 이벤트는 손상되지 않은 채 그대로 스트리밍되며, Suture는 마지막에 닫기 델타(closing delta)만 추가합니다. 추가되는 지연 시간(latency)은 청크당 CPU 기준 약 ~10µs입니다(
criterion으로 측정) — 이는 모델을 기다리는 데 소비하는 시간보다 3자릿수(three orders of magnitude)나 적은 수치입니다. - 바이트를 단순히 다루는 것이 아니라 콘텐츠를 인식합니다(content-aware). 재조립된(reassembled) 필드를 복구하며, JSON을 포함하는 필드에 대해서만 작동합니다(도구 인자는 항상 포함되며,
content는 실제로 JSON인 경우에만 포함됩니다). 따라서 산문(prose)을 망가뜨리지 않습니다. - 압축 및 4개의 제공업체를 처리합니다. gzip/brotli/deflate는 실시간으로 디코딩, 복구 및 재인코딩됩니다. OpenAI, Anthropic, Google Vertex (Gemini + Claude-on-Vertex), 그리고 AWS Bedrock(
ConverseStream, 바이너리 CRC 체크 프레임 프로토콜)이 모두 지원됩니다.
키(keys)에 관한 참고 사항, 별도의 주의 사항이 되어야 하기에
Suture는 귀하의 자격 증명(credential)을 전달할 뿐 아무것도 보유하지 않습니다. AWS Bedrock의 경우 더욱 강력합니다. SigV4 서명 방식은 비밀 액세스 키(secret access key)가 네트워크를 통해 전혀 전달되지 않고 요청당 서명만 전달됨을 의미합니다. 따라서 프록시가 침해되더라도 재사용 가능한 AWS 자격 증명을 훔칠 수 없습니다. (저희는 AWS로 향하는 업스트림 Host도 검증합니다. Host 헤더를 악용하려던 SSRF 공격을 검토 과정에서 발견하여 수정했습니다.)
솔직한 한계
이것은 마법이 아니며 모든 상황에 적용되는 것도 아닙니다. 제공업체들은 잘못된 형식의 (malformed) JSON을 줄여주는 네이티브 구조화된 출력 보장(structured-output guarantees, 엄격한 스키마, 제약된 디코딩)을 출시하고 있으며, 이는 좋은 현상입니다. 하지만 그들이 해결하지 못하는 것은 바로 **절단 (truncation)**입니다. 토큰 제한에서 스트림이 끊기거나 소켓이 종료되면, 다양한 모델, Bedrock, 그리고 오래된 API들에 걸쳐 여전히 유효하지만 불완전한 JSON이 남게 됩니다. Suture가 존재하는 이유가 바로 이 잔여물 때문입니다. 또한 Suture는 아예 도착하지 않은 데이터를 되살리지는 않으며, 도착한 데이터를 파싱 가능한 상태로 만들어 줍니다.
사용해 보기
Suture는 Rust로 작성되었으며, MIT/Apache-2.0 이중 라이선스를 따르고, 약 100개의 테스트를 포함하고 있습니다. GitHub 주소는 다음과 같습니다:
https://github.com/tensorhq/suture-stream-repair. 만약 프로세스 내에서 직접 복구하여 응답 바이트조차 네트워크에 노출시키지 않기를 원한다면, 복구 엔진은 독립형 라이브러리(standalone library)로 제공됩니다.
만약 여러분의 구조화된 출력(structured-output) 파이프라인이 절단된 스트림으로 인해 오류를 일으킨 적이 있다면, base_url을 한 줄만 변경하여 이것이 도움이 되는지 바로 확인할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기