Daeseon Yoo
Back to project
·Tech retro·4 min

Vendor-Specific JSON Contract — Gemini responseSchema vs Anthropic tool_use

Vision LLM에서 JSON 강제 출력 메커니즘이 vendor마다 다름. Anthropic은 SYSTEM_PROMPT 텍스트로 충분하지만, Gemini는 명시적 responseSchema 필수 — Phase 7.2의 vendor swap 시 이 패턴을 layer화하는 결정 기록.

문제

Gemini 호출 3회 측정에서 2회 parse 실패 발생. raw_len 173/174의 짧은 응답이 free-form 텍스트로 옴:

17:24:29 → 15s, raw_len=173, 모든 fields None (parse 실패)
17:38:43 → 8s, raw_len=593, state ✓, next ✓, coords ✓ (parse 성공)
18:46:06 → 9s, raw_len=174, 모든 fields None (parse 실패)

SYSTEM_PROMPT에서 "JSON 외 텍스트 금지"라고 명시했는데도 Gemini가 자유로운 텍스트 형식으로 응답. Anthropic Sonnet은 99%+ JSON 준수하지만, Gemini는 이 규칙을 "느슨하게" 해석.

코드 메트릭스:

결정 분기

A. LLM 정확도 올림 — 더 큰 모델, 더 정밀한 prompt

B. 응답 받은 후 retry — parse 실패 시 다시 호출

C. ⭐ Gemini generationConfig에 명시적 JSON schema 박음

선택 C → 동시에 vendor 추상화 layer 결정 기록.

박힌 거

GeminiDispatcher — generationConfig 추가

"generationConfig": {
    "maxOutputTokens": DEFAULT_MAX_TOKENS,
    "temperature": 0.0,
    "responseMimeType": "application/json",
    "responseSchema": {
        "type": "object",
        "properties": {
            "screen_state": {"type": "string"},
            "next_action": {"type": "string"},
            "coordinates": {
                "type": "array",
                "items": {"type": "integer"},
                "minItems": 4,
                "maxItems": 4
            },
            "reasoning": {"type": "string"}
        },
        "required": ["screen_state", "next_action", "reasoning"]
    }
}

coordinates optional (모델이 못 잡는 케이스도 있음). Gemini는 이제 schema 따라 strict JSON만 출력 → parse_analysis 안정화.

AnthropicDispatcher — 그대로 둠

// SYSTEM_PROMPT의 텍스트만으로 충분. 
// 추가 schema 박을 필요 없음.
let response = self.client.post(ANTHROPIC_API_URL)
    .header("x-api-key", &self.api_key)
    .header("anthropic-version", ANTHROPIC_VERSION)
    .header("anthropic-beta", BETA_VISION)
    .json(&request_body)
    .send()
    .await?;

Anthropic tool_use도 없음 (이건 structured output이 필요 없는 케이스 — 이미 JSON SYSTEM_PROMPT로 안정적).

비용

교훈 — Vendor JSON Enforcement 패턴

각 LLM vendor마다 JSON 강제 메커니즘이 구조적으로 다름:

Vendor메커니즘안정성
AnthropicSYSTEM_PROMPT "JSON 외 금지" 텍스트99%+
GeminigenerationConfig.responseMimeType + responseSchema (OpenAPI subset)100% (schema 주면)
Groqrequest_options.json_object~95%
OpenAIrequest.response_format.json_schema100% (schema 주면)

패턴: vendor가 strict output 강제할 때 방식이 다름. API 설계 철학 차이:

Phase 7.2에 vendor swap 시 이 테이블을 LLMDispatcher trait 아래 abstractionLayer로 박을 것. 그때가 "generationConfig 자동 조립" layer의 탄생. 지금은 GeminiDispatcher에만 박혀있지만, 나중에:

// 이렇게 될 것 (추상화 예시):
trait LLMDispatcher {
    fn json_enforcement_config(&self) -> JsonEnforcementConfig;
    // ...
}

Commit

1428843 (2026-05-27 14:49 -0400)