AI의 답을 구조로 받는다는 것
AI의 답을 자유로운 문장이 아니라 정해진 구조로 받는 방식과, 실패까지 설계하는 관점을 이해한다.
AI에게 JSON으로 답해달라고 했는데도 결과가 바로 파싱되지 않는 일이 있다.
앞에는 "물론이죠!"가 붙고, JSON은 마크다운 코드블록 안에 들어가 있고, 마지막에는 "더 필요하신 것이 있으면 말씀해 주세요" 같은 문장이 따라온다. 사람에게는 친절한 답이지만, JSON.parse()에게는 실패한 출력이다.
JSON으로 달라고 했는데도 깨지는 이유
문제는 AI가 지시를 못 알아들은 것이 아니다.
"JSON으로 줘"라는 말을 AI는 이해했다. 그래서 JSON을 썼다. 다만 그 앞뒤로 자연스러운 문장을 함께 덧붙였다. AI 입장에서 그것이 더 완성된 답처럼 느껴졌기 때문이다.
실패 패턴은 대체로 세 가지다. JSON 앞에 서문이 붙는 경우, JSON 뒤에 부연 설명이 따라오는 경우, JSON 자체가 문법적으로 깨지는 경우 — 키에 따옴표가 없거나, 마지막 항목 뒤에 쉼표가 남거나, 단일 따옴표가 섞이는 식이다. 어느 경우든 파서는 오류를 낸다.
"더 명확하게 지시하면 해결된다"고 생각하기 쉽지만, 그것만으로는 충분하지 않을 때가 있다.
AI는 자연어로 말하도록 훈련됐다
LLM은 기본적으로 사람이 읽을 수 있는 답을 만들도록 학습되어 있다. 질문을 받으면 단순히 값을 반환하기보다, 맥락을 설명하고, 친절한 문장을 덧붙이고, 보기 좋게 정리하려는 방향으로 답을 이어갈 가능성이 높다.
학습 데이터의 대부분이 자연어 텍스트였기 때문이다. "물론이죠!", "아래에", "이 분석이 도움이 됐으면"—이런 표현들은 수많은 질문-응답 쌍에서 반복해서 등장했다. JSON은 그 데이터 안에서 훨씬 적은 비중을 차지한다.
구조화된 출력은 이 자연스러운 말하기 방향을 좁히는 일이다. "JSON으로 줘"라는 지시 하나만으로는 그 방향을 충분히 좁히지 못하는 경우가 있다.
프롬프트로 가능성을 좁히는 방법
할 것을 지시하는 것보다 하지 말아야 할 것을 명시하는 편이 더 효과적인 경우가 많다.
출력 규칙:
- { 로 시작해서 } 로 끝나는 순수 JSON만 출력한다
- "물론이죠", "아래에", "추가로" 같은 서문/후기를 쓰지 않는다
- 마크다운 코드블록(```)을 사용하지 않는다
- 주석을 넣지 않는다
출력 영역을 분리하는 방식도 있다. 모델이 설명을 쓰고 싶다면 설명 영역과 최종 출력 영역을 나누고, 시스템이 실제로 읽을 값은 마지막 출력 블록에만 두도록 정한다.
응답은 반드시 아래 형식만 따른다.
[OUTPUT]
{
"title": "",
"summary": "",
"tags": []
}
[OUTPUT] 밖에는 아무 문장도 쓰지 않는다.
형식을 말로 설명하는 것보다 올바른 예시 하나가 더 강하다. Few-shot처럼 완성된 입출력 예시를 두세 개 보여주면, 모델이 서문 없이 바로 JSON으로 시작하는 패턴을 자연스럽게 따라간다.
이 방법들은 실패 확률을 낮추지만 0으로 만들지는 않는다.
API가 형식을 보장할 때
프롬프트만으로 형식을 맞추는 데는 한계가 있다. 그래서 실제 서비스에서는 API 레벨에서 구조를 강제하는 방식을 쓰기도 한다.
OpenAI의 Structured Outputs는 JSON Schema를 통해 응답의 형태를 지정하는 방식이다. strict: true를 사용하면 모델이 제공한 스키마와 맞는 출력만 생성하도록 강제할 수 있다.
Anthropic의 Tool Use는 조금 다른 방식으로 작동한다. 개발자가 도구의 입력 스키마를 정의하고, tool_choice로 특정 도구를 반드시 사용하도록 강제하면 모델은 자연어 대신 그 스키마에 맞는 구조화된 응답을 반환한다. 모델이 판단해서 도구를 호출하는 것이 아니라, 개발자가 항상 그 형식으로 답하게 만드는 구조에 가깝다.
중요한 건 특정 API 이름이 아니다. 프롬프트 밖에서도 형식을 보장하는 장치가 필요할 수 있다는 점이다.
실패를 설계한다는 것
형식이 보장되더라도 내용은 여전히 실패할 수 있다.
필드가 빠질 수 있고, 타입이 기대와 다를 수 있고, 입력 정보가 부족해서 모델이 값을 채울 수 없는 경우도 있다. 이런 상황에서 AI가 그럴듯한 값을 채워 넣으면, 파싱은 성공하지만 의미는 틀린 데이터가 된다.
실패를 구조 안에 넣어두면 다르다.
{
"answer": null,
"error": "missing_required_information",
"missing_fields": ["target_audience", "source_text"],
"confidence": "low"
}
AI가 모르는 것을 그럴듯한 문장으로 덮는 대신, 실패를 값으로 반환하게 만드는 것이다. 파싱 실패 시 재요청할 수도 있고, 실패 유형에 따라 다른 처리를 할 수도 있다. 오류가 예외가 아니라 설계의 일부가 된다.
출력을 신뢰한다는 것
구조화된 출력은 AI의 답을 예쁘게 정리하기 위한 장식이 아니다.
AI의 답이 다음 코드로 넘어가야 한다면, 그 답은 문장이 아니라 값이어야 한다. 값에는 위치가 있고, 타입이 있고, 실패했을 때의 형태가 있어야 한다. 그때부터 AI는 대화 상대를 넘어 시스템의 한 부분이 된다.
출력을 신뢰한다는 것은 AI를 무조건 믿는다는 뜻이 아니다. 믿을 수 있는 형태로 답하게 만든다는 뜻이다.