
LLM은 JSON을 어려워하는 것 같다. 그래서 생성하기 쉬운 중간 언어를 직접 만들었다
요약
LLM이 JSON 생성 시 겪는 구조적 한계와 추론 성능 저하 문제를 해결하기 위해, AI 에이전트 간의 절차 전달에 최적화된 중간 언어인 'StepArgs DSL'을 설계한 과정을 다룹니다.
핵심 포인트
- JSON의 엄격한 문법은 LLM의 생성 오류와 추론 성능 저하를 유발할 수 있음
- AI 에이전트 간의 오케스트레이션을 위한 새로운 중간 표현 방식의 필요성
- 인간의 가독성과 투명성을 위해 자연어(일본어)를 지원하는 DSL 설계
- 처리를 스텝 단위로 나누고 인자를 연결하는 구조적 접근
왜 JSON을 대신할 중간 언어를 만들려고 생각했는가
계기는 작은 위화감이었습니다.
지난번 기사에서도 썼지만, 제가 만든 Editor4AI라는 이 도구의 MCP 호출이 아주 가끔 실패하곤 합니다.
원인을 찾기 위해 AI에게 물어보니, 의외의 대답이 돌아왔습니다.
JSON과 같은 형식은 LLM에게 있어 생성이 특기인 분야가 아닌 것 같다고 말이죠.
괄호나 따옴표를 엄격하게 닫는 구조는 인간에게는 자연스럽지만, 확률적으로 문장을 생성하는 LLM에게는 가끔 무너질 때가 있습니다.
이것은 저의 착각이 아니었던 것 같습니다.
나중에 AI에게 조사해 달라고 요청한 결과에 따르면, 이 현상에는 몇 가지 이유가 있는 듯합니다.
LLM은 애초에 유효한 JSON을 생성하는 것 자체를 잘하지 못하며, 지시를 해도 코드 페ンス (Code Fence)나 불필요한 설명문이 섞여 망가지는 경우가 있습니다.
더욱 흥미로웠던 점은, JSON 형식을 강제하면 생성의 정확성뿐만 아니라 AI의 추론 성능 자체가 떨어지는 경우가 있다는 이야기였습니다.
딱딱한 구조에 갇히게 되면, AI는 생각할 여지를 빼앗긴다는 뜻입니다.
물론 지금은 각 사의 API가 구조화된 출력 (Structured Output) 기능을 통해 이러한 문제에 대한 대책을 진행하고 있습니다.
그럼에도 불구하고 "LLM이 생성하기 쉬운 중간 표현이 필요하다"라는 과제 의식 그 자체는 지금도 유효하다고 느낍니다.
도구 호출의 실패는 그곳에 한 가지 원인이 있었던 것입니다.
마침 그 무렵, 저는 AI 에이전트의 오케스트레이션 (Orchestration) 도구를 LangChain으로 만들려고 하고 있었습니다.
그 과정에서 어떤 개념을 알게 됩니다.
AI가 다음 AI에게 실행해야 할 절차를 만들어 전달한다──에이전트끼리 태스크를 분해하여 주고받는다는 생각입니다.
여기서 두 가지 문제가 겹쳤습니다.
AI에게 절차를 전달하려면 어떠한 구조화된 형식이 필요합니다.
하지만 그 정석인 JSON은 정작 LLM이 생성을 어려워하고 있습니다.
절차를 주고받는 데 JSON을 사용할수록, 구조가 무너져 실패할 리스크가 커집니다.
그렇다면 JSON을 대신할 형식을 만들면 됩니다.
LLM이 생성하기 쉬우면서도, 그러면서도 절차를 제대로 표현할 수 있는 구문.
그것을 AI와 함께 설계하기 시작했습니다.
그것이 StepArgs DSL입니다.
고집한 포인트 그 첫 번째: 일본어로 쓸 수 있고, 인간이 읽을 수 있을 것
설계에서 가장 먼저 고집한 것은 일본어로 쓸 수 있다는 점이었습니다.
이유는 솔직히 말해서 제가 영어를 잘 못하기 때문입니다.
JSON이나 YAML로 구조를 쓸 때 키 이름은 자연스럽게 영어가 됩니다.
하지만 저는 일본어로 적혀 있는 편이 한눈에 이해됩니다.
내가 사용하는 도구이니만큼, 내가 가장 읽기 쉬운 형태로 만들고 싶다. 그렇게 생각했습니다.
쓰기 시작하면서 또 하나의 장점을 깨달았습니다.
이 중간 언어는 AI 에이전트끼리 절차를 주고받기 위해 사용하는 것입니다.
즉 평소에는 인간의 눈에 닿지 않는 곳에서 AI와 AI가 주고받습니다. 내버려 두면 블랙박스가 됩니다.
그런데 일본어로 인간도 읽을 수 있는 형태로 만들어 두면, AI가 지금 어떤 절차를 짜고 있고 다음에 무엇을 하려고 하는지를 그대로 눈으로 쫓을 수 있습니다.
"나에게 편리하기 위해" 일본어 대응을 했을 뿐인데, 결과적으로 AI의 동작을 인간이 확인할 수 있는 투명성을 얻게 되었습니다.
훌륭한 설계 사상에서 시작한 것은 아닙니다.
내가 읽기 쉬운 것을 만들었더니 우연히 형편이 좋았습니다.
하지만 도구를 오래 사용하려면 만드는 본인이 가장 스트레스 없이 읽을 수 있는 것이 생각보다 중요하다고 믿고 있습니다.
고집한 포인트 그 두 번째: 스텝별로 필요한 데이터를 종류별로 나누어 전달할 것
또 다른 축은 처리를 "스텝 (Step)"이라는 단위로 나누고, 각 스텝에 필요한 데이터를 인자 (Argument)로서 연결하는 구조입니다.
예를 들어 번역 지시는 다음과 같이 씁니다.
--- 번역 ---
번역[입력 언어:일본어]
번역[출력 언어:영어]
...
--- 번역 ---로 하나의 처리 단위를 구분하고, 그 안에 "입력 언어", "출력 언어", "내용"이라는 인자를 종류별로 나누어 나열합니다.
어떤 스텝에서 각각 무엇을 전달하고 있는지가 한눈에 들어옵니다.
JSON의 중첩된 괄호를 쫓아가는 것보다 훨씬 시인성이 높습니다.
이 "스텝 단위"라는 구분에는 또 하나의 이점이 있습니다.
처리를 스텝별로 나누어 생각할 수 있기 때문에 추가, 삭제, 분기가 쉽습니다.
AI 에이전트가 태스크를 분해하여 전달할 때, 이 단위가 그대로 절차 하나하나에 대응합니다.
고집한 포인트 그 세 번째: 짧은 값도, 장문도 JSON도, 같은 구문으로 다룰 수 있을 것
전달하고 싶은 데이터가 항상 짧은 문자열인 것은 아닙니다.
긴 문장, JSON, 코드 전체를 전달하고 싶을 때도 있습니다.
그래서 두 가지 작성 방식을 준비했습니다. 짧은 값은 그대로 [인수명:값]
으로 쓰고, 긴 데이터는 <<<와 >>>로 감싸는 히어 도큐먼트 (Heredoc) 형식으로 작성합니다.
문서 처리[대상:<<<
여러 줄에 걸친 긴 문장이나,
JSON이나 코드 덩어리도,
...
이렇게 해두면 문자열, JSON, 코드, 장문에 이르기까지 동일한 하나의 구문 안에서 다룰 수 있습니다.
데이터의 종류가 바뀔 때마다 형식을 전환할 필요가 없습니다.
이것이 MCP와 같은 다양한 도구와 조합해서 사용할 수 있는 범용성으로 이어지고 있습니다.
이론은 모른다. 하지만 정규 표현식으로 필요한 것은 만들 수 있었다
마지막으로, 파서 (Parser) 구현에 대해 솔직하게 적어둡니다.
DSL을 만든다고 하면 보통 어휘 분석기 (Lexer)나 구문 트리 (AST)와 같이 오토마타 (Automata) 이론에 기반한 "올바른" 파서를 구현하는 것을 생각할 것입니다.
하지만 저는 부끄럽게도 오토마타를 제대로 이해하지 못하고 있습니다.
그래서 그런 정공법의 구문 분석은 만들지 않았습니다.
대신 사용한 것은 정규 표현식 (Regular Expression)입니다.
입력을 한 줄씩 읽으며 "이 줄은 스텝의 구분인가", "단일 행 인수인가", "여러 줄의 시작인가"를 각 줄마다 정규 표현식으로 매칭시켜 판정해 나갑니다. 이론적으로는 분명 아름답지 않은 방법입니다.
하지만 StepArgs DSL의 구문은 심플하게 설계되어 있었기에, 이것만으로도 필요 충분하게 동작했습니다.
게다가 만드는 과정에서 단순히 변환하는 것뿐만 아니라, 구문 오류를 검출하는 기능도 추가했습니다.
히어 도큐먼트 (Heredoc)가 닫히지 않았거나, 스텝 이름이 일치하지 않거나, 인수가 중복되는 등의 실수를 찾아내어 몇 번째 줄이 잘못되었는지 반환합니다.
용도에 따라 엄격함을 전환할 수 있도록도 만들었습니다.
항상 이론을 완벽하게 알지 못하더라도, 목적에 필요한 것은 만들 수 있습니다.
물론 오토마타를 이해하고 있었다면 훨씬 견고하고 우아한 구현이 되었겠지요.
그 부분은 앞으로의 숙제입니다.
하지만 "모르니까 만들지 않는다"가 아니라 "아는 도구로, 동작하는 곳까지 만든다".
저의 개발은 대체로 이런 식입니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기