본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 26. 09:17

서로 호환되지 않는 5개의 LLM API로부터 구조화된 JSON을 추출하는 방법 — 그리고 모델이 이를 무시할 때의 대응책

요약

서로 다른 5개의 LLM API로부터 일관된 JSON 구조를 추출하기 위한 전략을 다룹니다. 각 모델의 고유한 구조화된 출력 메커니즘을 활용하면서도, 파서를 통해 데이터의 무결성을 검증하고 실패 시 안정적으로 대응하는 방법을 제시합니다.

핵심 포인트

  • LLM 제공업체마다 구조화된 출력을 구현하는 방식(tool_use, json_schema 등)이 다름
  • 구조화된 출력은 보장이 아닌 스펙트럼의 개념으로 접근해야 함
  • 단일 파서를 통해 모든 API 출력을 검증하고 실패 시 Markdown으로 강등하는 방어적 설계 필요
  • 스키마는 파싱 가능성을 보장할 뿐, 내용의 정확성(Hallucination)까지 보장하지는 않음

CommitBrief는 코드 리뷰를 카드, JSON schema v1 또는 CI 종료 코드(exit code)로 렌더링합니다. 이는 LLM이 산문(prose)이 아닌 구조화된 결과물(structured findings)을 반환해야 함을 의미합니다. 모든 제공업체(provider)가 이를 수행할 수 있습니다. 문제는 어떤 두 업체도 동일한 방식으로 수행하지 않으며, 일부는 아예 수행하지 못한다는 점입니다.

시스템 전체가 목표로 하는 스키마(schema)는 정확히 하나입니다. 네 개의 네이티브 API가 이 스키마를 준수하도록 만드는 데는 네 가지의 완전히 다른 메커니즘이 필요합니다. 추가로 세 개의 API를 더 확보하는 것은 정중하게 요청하되 그 답변을 신뢰하지 않는 문제와 같습니다. 이것이 작동하는 방식이며, 모델이 계약(contract)을 무시할 때 어떤 일이 발생하는지에 대한 설명입니다.

요약 (TL;DR)

  • 하나의 스키마, 다양한 방언. 모든 제공업체는 해당 벤더가 제공하는 구조화된 출력(structured-output) 메커니즘 — tool_use, 엄격한 json_schema, responseSchema, 또는 단순히 format: "json" — 을 통해 표현되는 동일한 Finding 형태를 목표로 합니다.
  • 구조화된 출력은 보장이 아닌 스펙트럼입니다. 이는 "API가 형태를 강제함"에서부터 "프롬프트(prompt)에서 요청함"에 이르는 스펙트럼 상에 존재합니다.
  • 진정한 계약은 파서(parser)입니다. 하나의 ParseFindings가 모든 제공업체의 출력을 동일한 방식으로 검증합니다. 실패 시 한 번 재시도하며, 그 후에는 경고와 함께 Markdown으로 강등(degrade)됩니다. 파이프라인은 잘못된 응답으로 인해 절대 충돌(crash)하지 않습니다.
  • 한계점. 스키마는 출력을 *파싱 가능(parseable)*하게 만들 뿐, *정확(correct)*하게 만들지는 않습니다. 모델이 그럴듯하지만 틀린 결과(finding)를 지어내는 것을 막을 수는 없습니다.

모두가 목표로 하는 단 하나의 스키마

결과물(finding)은 평면적인 구조체(flat struct)입니다. 5개의 필수 필드, 3개의 선택적 필드, 그리고 제한된 어휘에서 추출된 심각도(severity)로 구성됩니다:

type Finding struct {
    Severity    Severity `json:"severity"`     // 아래의 5개 중 하나
    File        string   `json:"file"`
...

최종 봉투(envelope)는 {"findings": [ ... ]}이며 그 외의 것은 포함되지 않습니다. 해당 severity 어휘는 모델과의 통신 규약(wire contract)입니다. 이는 의도적으로 영어로만 구성되며 코드 내에 고정되어 있습니다. 따라서 사용자의 커스텀 COMMITBRIEF.md가 리뷰의 '규칙(rules)'은 변경할 수 있어도, 출력의 '형태(shape)'는 결코 변경할 수 없습니다. 카드 렌더러, --json, --fail-on=high 등 모든 후속 프로세스는 이 다섯 가지 문자열이 정확히 다섯 가지 의미를 갖는다는 점에 의존합니다.

동일한 형태를 위한 네 가지 네이티브 방언

각 네이티브 API 제공업체는 자신들만의 메커니즘을 통해 해당 스키마(schema)를 강제합니다. 목표는 같지만, 네 가지 통신 형식(wire formats)이 존재합니다.

Anthropic — 강제된 도구 호출 (forced tool call). findings 스키마는 도구(tool)로 등록되며, tool_choice를 통해 해당 도구 호출을 선택 사항이 아닌 필수 사항으로 만듭니다:

params.Tools      = []sdk.ToolUnionParam{buildReportTool()}  // "report_findings"로서의 스키마
params.ToolChoice = sdk.ToolChoiceParamOfTool(toolName)      // 반드시 호출해야 함
// 도구 설명: "리뷰를 구조화된 findings로 방출하십시오. 항상 이 도구를 호출하십시오."

OpenAI — 엄격한 json_schema. Strict가 설정되면, Chat Completions API는 서버 측에서 스키마를 준수하도록 응답을 제어합니다. 또한 이를 무시할 가능성이 있는 모델로 넘어가기보다 요청 자체를 즉시 거부합니다:

func buildResponseFormat() sdk.ChatCompletionNewParamsResponseFormatUnion {
    return sdk.ChatCompletionNewParamsResponseFormatUnion{
        OfJSONSchema: &shared.ResponseFormatJSONSchemaParam{
...

(Responses-API 전용 모델은 대신 text.format json_schema 설정을 통해 동일한 스키마를 표현합니다. 즉, 동일한 형태를 위한 또 하나의 방언입니다.)

Gemini — 응답 스키마(response schema)와 MIME 타입의 조합. SDK에 *Schema 값을 전달하고 JSON을 반환하도록 지시합니다:

cfg.ResponseMIMEType = "application/json"
cfg.ResponseSchema   = responseSchema()  // *genai.Schema로서의 Findings envelope

Ollama — format: "json", 그것이 약속하는 전부입니다. 로컬 모델에 JSON을 방출하도록 지시할 수 있지만, 이 플래그는 '형태(shape)'가 아닌 '구문(syntax)'만을 제한합니다:

Format: "json", // 유효한 JSON은 보장되지만, 올바른 키(key)는 보장되지 않음

이 차이는 매우 중요합니다. Anthropic, OpenAI, Gemini는 _구조(structure)_를 제한하지만, Ollama는 출력이 어떠한 형태든 JSON으로 파싱된다는 것만을 보장합니다. 스키마 준수(schema conformance)는 다른 곳에서 이루어져야 합니다.

전혀 강제하지 않는 세 가지 제공업체

DeepSeek, Mistral, Cohere는 OpenAI 호환 SDK( part 2에서 다룸)를 통해 CommitBrief에 도달하지만, 이들의 엄격한 스키마(strict-schema) 지원은 불균일하기 때문에 response_format을 전혀 요청하지 않습니다. 이들의 JSON 형태는 전적으로 프롬프트의 계약(contract) 블록에서 결정됩니다.

따라서 7개의 API 제공업체 전반에 걸친 구조화된 출력(structured output)은 하나의 스펙트럼과 같습니다:

메커니즘제한 사항제공업체
강제 도구(Forced tool) / 엄격한 스키마(strict schema)정확한 형태(shape)anthropic, openai, gemini
...

엄격한 스키마 제공업체만을 신뢰하는 파이프라인은 7개 중 3개에 대해서만 작동할 것입니다. 나머지 4개는 JSON이 어떻게 생성되었는지에 상관없이 작동하는 안전장치(backstop)가 필요합니다.

진짜 계약은 당신의 파서(parser)입니다

그 안전장치는 모든 제공업체의 출력이 통과하는 하나의 함수입니다. ParseFindings는 봉투(envelope)를 디코딩하고 각 결과(finding)를 검증합니다. 단순히 "JSON인가"를 넘어 "_유효한 결과인가"를 확인합니다:

for i, f := range env.Findings {
    if !f.Severity.IsValid() {
        return nil, fmt.Errorf("parse findings: finding %d: unknown severity %q", i, f.Severity)
...

findings 배열은 _깨끗한 검토(clean review)_를 의미하며, nil이 아닌 빈 슬라이스(empty slice)로 반환됩니다. 이는 에러가 아닌 성공입니다. 임의로 만들어진 심각도(severity)나 파일 정보가 없는 결과는 어떤 제공업체가 생성했느냐에 관계없이 파싱 실패(parse failure)입니다. 엄격한 스키마 제공업체는 이 단계에서 걸리는 경우가 드물지만, 프롬프트 기반 제공업체는 이 단계에 의존합니다. 어느 쪽이든 검증 방식은 동일하므로, --fail-on=high 게이트는 Claude를 실행하든 로컬 qwen을 실행하든 동일한 의미를 갖습니다.

모델이 이 모든 것을 무시할 때

엄격한 스키마 (Strict schema)는 잘못된 형식의 출력을 줄여주지만, 이를 완전히 제거하지는 못하며, 제공업체 중 세 곳은 스키마를 전혀 지원하지 않습니다. 따라서 호출은 retry-once-then-degrade(한 번 재시도 후 성능 저하 모드로 전환) 방식으로 감싸져 있습니다:

resp, err := prov.Review(ctx, req)
if err != nil {
    return "", provider.Usage{}, "", err
...

이 흐름에는 세 가지 의도적인 설계가 포함되어 있습니다. 토큰 사용량은 두 번의 시도 모두에 대해 합산되므로, 성능 저하 (degrade) 모드에서도 비용 푸터 (cost footer)에 실제 지출한 금액이 반영됩니다. 결과는 형식 마커 (format marker) (FormatJSON 또는 FormatMarkdownFallback)로 기록되어 응답과 함께 캐싱됩니다. 따라서 성능 저하된 리뷰는 계속해서 경고를 띄우는 대신 캐시에서 조용히 재실행됩니다. 그리고 성능 저하 (degrade)란 모델의 원문 텍스트를 마크다운 (Markdown)으로 렌더링하고 경고를 한 번 출력함을 의미합니다. LLM이 창의력을 발휘했다고 해서 절대 크래시(crash)를 일으키거나 사용자에게 스택 트레이스 (stack trace)를 보여주어서는 안 됩니다. 성능 저하 모드에서는 임계값을 설정할 구조화된 결과물이 없으므로, stderr에 노트를 남기고 --fail-on 게이트를 건너뜁니다.

이것이 아닌 것

구조화된 출력 (Structured output)은 응답이 파싱 가능 (parseable) 함을 보장합니다. 그것이 정확함 (correct) 을 보장하지는 않습니다. 엄격한 스키마는 모델이 줄 번호를 지어내거나, 잘못된 파일에 결과물을 연결하거나, 확실하지 않은 문제를 확신하며 보고하는 것을 막을 수 없습니다. 이것이 프롬프트에 여전히 "파일 경로 나 줄 번호를 지어내지 마시오"라는 명시적인 지침이 포함되어 있는 이유이며, 이것이 마지막 검토자가 아닌 제0차 검토자인 이유입니다. 스키마는 출력을 기계가 읽을 수 있게(machine-readable) 만드는 것이며, 여러분의 판단이 출력을 신뢰할 수 있게 만듭니다.

"얼마나 자주 맞는가"에 대한 측정된 버전을 원하신다면, 평가 하네스 (eval harness)가 알려진 정답 코퍼스 (corpus)를 기준으로 모델별 정밀도 (precision) 및 오탐률 (false-positive rate)을 점수화합니다:

COMMITBRIEF_EVAL_PROVIDER=<name> make eval-live

Repo: github.com/CommitBrief/commitbrief.

Building CommitBrief의 파트 3. 다음 편: 전송 전 비밀번호 스캐너 — 8가지 패턴, 추가된 라인 전용, 그리고 방금 포착한 비밀번호를 절대 저장하지 않는 매치 레코드.

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0