LLM이 잘못된 형태(Shape)로 유효한 데이터를 보내는 문제
요약
LLM이 도구 호출 시 데이터 형식을 잘못 전달하는 문제를 해결하기 위해, 엄격한 검증 대신 강제 변환(Coercion)을 사용하는 설계 전략을 제안합니다. 개별 필드가 아닌 베이스 클래스 수준에서 파싱 로직을 구현하여 시스템의 안정성과 효율성을 높이는 방법을 다룹니다.
핵심 포인트
- LLM은 객체가 아닌 텍text를 전달하므로 경계면에서의 파싱 설계가 중요함
- 잘못된 형식을 거부하기보다 기대하는 타입으로 강제 변환(Coercion)하여 토큰 낭비 방지
- 형태 오류는 특정 도구의 문제가 아닌 경계면의 문제이므로 베이스 클래스에서 처리 권장
- JSON 규격에 완벽히 맞지 않는 'Near-JSON' 데이터에 대한 유연한 대응 필요
모델은 당신의 도구에 타입이 지정된 객체(typed object)를 절대 직접 전달하지 않습니다. 모델은 그것을 설명한다고 주장하는 텍스트를 전달할 뿐이며, 그 텍스트와 당신이 검증한 인자(arguments) 사이의 모든 과정은 당신이 제어하는 파싱(parse) 과정입니다. 그 파싱이 얼마나 관대해야 하는지가 경계면(boundary)에서의 핵심 설계 질문이며, 그 답은 양방향에서 서로 다릅니다.
- 배열이 배열을 포함하는 문자열로 도착하는 경우
- JSON이 아님에도 불구하고 완벽하게 파싱되는 값들
- 검증 단계에서 전체 데이터가 필요할 때 여전히 스트리밍(streaming) 중인 인자들
- 깔끔한 거절을 혼란스러운 에러로 바꿔버리는 조용한 폴백(fallback)
우리의 harness가 전체 과정의 실례로 사용되며, 여기에 나오는 모든 메커니즘은 오후 한나절이면 복사해 넣을 수 있을 정도로 작습니다.
가장 자주 마주하게 될 형태 오류 (malformation)
문자열 리스트를 요청받은 LLM은 문자열 리스트를 포함하는 하나의 문자열을 전달할 것입니다. 당신의 스키마(schema)가 ["a","b"]라고 선언했을 때, 모델은 '["a","b"]'라는 텍스트를 보냅니다. 데이터는 정확하지만 감싸고 있는 방식(wrapping)만 틀린 것인데, 입력 검증(input validation)—즉, 도구 코드가 실행되기 전에 인자가 선언된 형태와 일치하는지 확인하는 체크 과정—은 그 차이를 신경 쓰지 않습니다. 검증은 호출을 거부합니다. 이 거절은 모델로 다시 전달되고, 대화의 한 턴(turn) 전체가 따옴표 두 개를 제거하도록 모델을 가르치는 데 소비됩니다.
대신 강제 변환(Coerce)하십시오. 값을 거부하는 대신 당신이 기대한 타입으로 조용히 변환하는 강제 변환(Coercion)은 여기서 비용이 전혀 들지 않습니다. 왜냐하면 '["a","b"]'가 합리적으로 의미할 수 있는 것은 단 하나뿐이기 때문입니다. 아무것도 로그에 남지 않고, 재시도도 필요 없으며, 도구는 그냥 실행됩니다. 당신이 아낀 이 턴은 당신이 아낄 수 있는 가장 저렴한 턴이며, 오류를 일으켰을 모든 호출에서 이를 아낄 수 있습니다.
강제 변환을 필드가 아닌 베이스 클래스(base class)에 배치하세요
필드별로 이를 수정하는 방식은 당신이 기억해낸 필드에 대해서만 작동합니다. 저희 하네스(harness)의 모든 도구 입력 모델(tool input model)은 일반적인 검증 모델(validation model)이 아닌, 공유 베이스 클래스(base class)인 ToolInputBase를 상속받습니다. 그리고 이 베이스 클래스는 검증이 실행되기 전에 JSON 문자열을 그것이 나타내는 리스트(list)로 변환합니다. 저희 문서에는 일반 모델을 상속받는 것을 흔히 발생하는 함정(pitfall)으로 나열하고 있는데, 이는 누군가 이를 배우기 전에 한 번쯤은 걸려 넘어질 수밖에 없다는 말을 정중하게 표현한 것입니다.
이것이 베이스 클래스에 위치해야 하는 이유는, 이러한 형태 오류(malformation)가 특정 도구의 속성이 아니라 경계(boundary)의 속성이기 때문입니다. 특정 도구의 어떤 특성도 모델이 배열(array)을 따옴표로 감쌀 가능성을 높이거나 낮추지 않습니다. 따라서 내년에 작성될 도구는 작성자가 문제의 존재를 전혀 모르더라도 이 수정 사항을 상속받게 되며, 이것이 바로 실제 목표입니다. 대안은 미래의 모든 저자가 읽고, 기억하고, 수동으로 적용해야 하는 기여 가이드(contributing guide)의 한 줄을 작성하는 것뿐입니다.
파싱(parse)할 수 있다고 해서 모두 JSON인 것은 아니다
따옴표로 감싸진 배열은 더 넓은 문제의 한 사례일 뿐입니다. 즉, JSON에 가까운 형태(near-JSON)가 끊임없이 도착하지만 json.loads는 이를 모두 거부한다는 점입니다. 파이썬(Python) 자체의 딕셔너리(dictionary) repr은 작은따옴표를 사용하며, JSON이 큰따옴표와 소문자 true, false, null을 요구하는 곳에 True, False, None을 작성합니다. 이는 모호하지 않고 매우 쉽게 파싱할 수 있지만, JSON 파서(parser)는 매번 이를 거부할 것입니다.
먼저 더 허용적인 파서(parser)를 시도해 보세요. 저희 터미널이 도구 출력(tool output)을 렌더링하는 곳에서는, 파이썬 리터럴(literals)을 위한 표준 라이브러리의 안전한 평가기(evaluator)인 ast.literal_eval을 호출하고, 이것이 실패할 경우에만 JSON으로 넘어가는 규칙을 따릅니다. 이 사례는 인자 경로(argument path)가 아닌 반환 경로(return path)에 위치하므로 방향은 다르지만, 문제의 형태는 동일하며 해결책 또한 동일합니다. 저희 문서에는 그대로 가져다 써도 될 만큼 명확하게 규칙이 명시되어 있습니다. 해당 데이터에 대해 json.loads()를 직접 호출하지 마십시오.
단일 파서(parser)를 사용하는 것은 텍스트를 생성한 주체가 누구인지에 대한 가정을 내포합니다. 이러한 가정은 모델, 하위 프로세스(subprocess), 또는 로깅 레이어(logging layer)가 JSON에 가까운 형태를 생성하기 전까지는 유효합니다. 하지만 그 시점이 되면 엄격한 파서는 실제 오류가 아닌데도 마치 실제 오류인 것처럼 읽히는 실패를 반환하게 됩니다.
인자(arguments)가 아직 모두 도착하지 않았을 수 있습니다
스트리밍(Streaming)은 위에서 언급한 모든 가정의 근간을 깨뜨립니다. 즉, 파싱을 시도할 때 인자 객체(argument object) 전체를 가지고 있다는 가정이 깨지는 것입니다. 모델이 도구 호출(tool call)을 스트리밍할 때, 인자들은 점진적으로 늘어나는 문자열 형태로 도착하며, 작업을 시작하고 싶은 시점에도 여전히 큰 인자가 도착 중일 수 있습니다. 우리의 스트리밍 핸들러(streaming handlers)는 키(key)별로 그 차이를 조절합니다. 짧은 스칼라 키(scalar keys)는 전체가 완성되면 on_key_completed를 통해 전달되고, 하나의 커다란 스트리밍 가능한 키는 on_string_progress를 통해 조각조각 전달되므로, 콘텐츠가 끝나기 전에도 파일 쓰기를 시작할 수 있습니다.
이러한 분리는 '불완전함(incomplete)'이 무엇을 의미하는지 정의하도록 강제합니다. 스트림은 조기에 중단될 수 있으며, 모델은 종료 사유(finish reason)를 통해 그 이유를 보고합니다. 여기서 length라는 값은 출력이 완료된 것이 아니라 값의 중간에서 잘렸음을 의미합니다. 잘린(truncated) JSON은 따옴표로 둘러싸인 배열이 잘못된 형식을 갖는 것과는 다른 방식으로 잘못된 것이며, 복구할 수 있는 올바른 파싱 결과가 있는 것이 아니라 단지 부분적인 쓰기(partial write)를 어떻게 처리할지에 대한 결정만이 존재할 뿐입니다. 우리의 핸들러는 완료 또는 오류 경로에서 절대 예외를 발생시키지 않고 항상 결과를 반환해야 하며, 이는 한 단계 위에서 적용되는 강제 변환(coercion)과 동일한 본능입니다. 핸들러는 어떤 파일을 쓰고 있었는지와 얼마나 기록되었는지를 알지만, 몇 프레임 떨어진 곳에서 예외를 잡는 호출자(caller)는 그 어느 것도 알지 못합니다.
버그가 되는 강제 변환 (coercion)
위의 모든 메커니즘은 의미가 명확한 잘못된 형식(malformation)을 흡수하지만, 문제는 바로 그 명확성이 사라지는 지점에서 시작됩니다. 우리의 스트리밍 쓰기 핸들러(streaming write handler)는 한때 폴백(fallback)을 사용하여 모드(mode) 인자를 검증했는데, 알려진 값과 일치하면 해당 모드를 할당하고 일치하지 않으면 조용히 write로 기본값을 설정했습니다. 나중에 유효한 모드 집합에서 overwrite가 제거되었을 때, 여전히 mode=overwrite를 요청하는 모델은 해당 모드가 사라졌다는 통보를 받지 못했습니다. 대신 이는 조용히 쓰기(write)로 변환되었고, 이후 문서가 이미 존재한다는 메시지와 함께 더 하위 단계에서 실패했습니다. 모델은 문서를 교체해 달라고 요청했지만, 결과적으로는 문서가 이미 존재한다는 답변을 받은 셈입니다. 이는 기존의 거절(rejection) 방식보다 더 나쁜 결과이며, 심지어 우리의 변경 사항(change notes)에도 예상된 결과로 기록되어 있습니다.
표현(representation)을 강제 변환(coerce)하되, 선택(choice)을 강제 변환하지 마십시오. 인용(quoting), 인코딩(encoding), 리터럴 구문(literal syntax)은 표현이며, 각각에 대해 단 하나의 정답이 존재합니다. 어떤 열거형 멤버(enum member), 어떤 경로(path), 어떤 식별자(identifier)가 '선택'인지 결정하는 것은 당신의 하네스(harness)가 존중할 수 없는 선택이라면 그것은 추측해야 할 값이 아니라 거절(rejection)되어야 합니다. 거절할 때는 작동했을 옵션들을 명시하십시오. 당신의 하네스는 그것들을 알고 있지만, 모델은 그것을 알아내기 위해 한 턴(turn)을 소비해야 하기 때문입니다.
강제 변환을 아예 거부하는 경우
모델 경계(model boundary)에서 강제 변환을 정당화하는 것과 동일한 논리가 몇 단계 안쪽에서는 강제 변환을 금지합니다. 우리의 워크플로우 엔진(workflow engine)은 워크플로우 상태(workflow state)에 따라 조건부 전이(conditional transitions)를 평가하며, 어떠한 암시적 타입 강제 변환(implicit type coercion)도 수행하지 않습니다. 저장된 데이터와 예상되는 값 사이의 타입 불일치(type mismatch)가 발생하면 인접한 무언가와 비교하는 대신 예외를 발생시키며, 비교 연산자(comparison operators)는 밑바닥에 어떠한 영리한 처리도 없이 순수한 파이썬(Python) 의미론(semantics)을 적용합니다.
두 사례를 구분하는 기준은 값이 어디에서 왔느냐 하는 것입니다. 언어 모델(Language Model)이 생성한 결과물에 대해서는 관대하게 대처하십시오. 모델의 형태 오류(malformations)는 텍스트 생성 과정에서 발생하는 부산물일 뿐, 당신이 원하는 어떠한 정보도 담고 있지 않기 때문입니다. 반면, 당신의 시스템이 생성한 결과물에 대해서는 엄격해야 합니다. 시스템 내부의 불일치는 버그이며, 이를 강제로 형변환(coercing)하는 것은 버그를 발견하기 가장 저렴한 시점에 그 버그를 숨겨버리는 행위이기 때문입니다. 경계 지점에서 한 번의 단계를 아끼기 위해 사용하는 그 관대함이, 나중에 당신의 상태 머신(state machine) 어딘가에서 정수(integer)와 문자열(string)을 조용히 비교하게 만들어 결국 오후 시간 전체를 허비하게 만들 것입니다.
파서(Parser)의 위치
세 가지 질문이 이 지도 위의 모든 테스트 프레임워크(harness)를 배치합니다. 모델이 배열(array)을 따옴표로 감쌌을 때, 호출이 실패합니까? 값이 JSON 대신 파이썬 딕셔너리 표현식(Python dict repr)으로 들어왔을 때, 파서가 이를 견뎌냅니까? 인자를 준수할 수 없을 때, 호출자에게 명시적인 거절(named rejection)이 전달됩니까, 아니면 조용한 기본값(silent default)이 전달됩니까? 각각의 질문은 경계 지점에서 몇 줄의 코드일 뿐이며, 더 이상 비용을 지불하지 않아도 되는 단계를 의미합니다.
Favur는 우리의 멀티 에이전트(multi-agent) 소프트웨어 팀이자, 이러한 메커니즘이 실행되는 테스트 프레임워크입니다. 이는 폐쇄 소스이며 초대 전용이지만, 생성된 리포지토리(repositories)는 공개되어 있습니다. https://favur.dev/go/devto/tool-coercion에서 실제 실행 과정을 다시 재생해 보거나, https://evals.favur.dev/go/devto/tool-coercion에서 여러 모델에 걸쳐 점수가 매겨진 동일한 프레임워크를 확인할 수 있습니다. 제가 이 프로젝트를 작업하고 있으므로, 관점을 그에 맞춰 고려하시기 바랍니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기