Gemini 3.6 Flash에서 5×5 오목 상황을 활용해 Structured Outputs(JSON Schema)의 필드 순서와 추론 레벨(Thinking Level / Budget) 설정이 모델의 최종 착수 좌표 선택에 어떤 차이를 만드는지 정리했습니다.
3줄 요약
- Gemini 3.6 Flash에서 Structured Outputs를 호출할 때, 최종 결정 필드를 JSON 스키마 최상단에 배치(Decision First)하면 추론 레벨(Thinking Budget)을 2,048까지 부여해도 오목 함정에 빠져 오답을 냅니다.
- 반면 JSON 스키마 필드 순서를 '공간 분석 ➔ 위협 식별 ➔ 최종 착수' 형태(CoT First)로 설계하면, 추론 레벨을 0(추론 비활성화 / Thinking OFF)으로 꺼두어도 100% 정답을 적중합니다.
- LLM의 자동회귀적(Autoregressive) 토큰 생성 특성상, JSON Schema의 프로퍼티 정의 순서 자체가 모델의 사고 스크래치패드(Chain of Thought) 역할을 수행하기 때문입니다.
실험 배경: Structured Outputs와 추론 레벨 (Thinking Level / Budget)
최근 프로덕션 환경에서 LLM을 호출할 때 널리 쓰이는 기능 중 하나가 Google Gemini의 responseSchema를 활용한 Structured Outputs입니다. 별도의 파싱 로직 없이 Pydantic이나 JSON Schema 형태로 타입 안정성이 보장된 응답을 얻을 수 있기 때문입니다.
여기에 Gemini 2.5 및 3.6 라인업부터 제공되는 **추론 제어 파라미터(Thinking Config)**를 결합하면 복합 조건 판단이나 규칙 기반 작업의 정확도를 한층 더 높일 수 있습니다:
thinkingBudget: 모델이 내부 사고에 사용할 최대 토큰 수를 직접 지정하는 방식 (0,1024,2048,8192등)thinking_level: 모델에 따라 추론 강도를 단계별로 지정하는 방식 ("minimal","low","medium","high")
하지만 실무에서 간혹 이런 현상이 나타납니다:
- 추론 레벨을
high로 올리거나thinkingBudget을 충분히 주었음에도 불구하고 단순한 조건 함정에 걸려 잘못된 결과를 출력하는 경우 - 출력된 JSON 내부의 설명(explanation/analysis) 필드에서는 정답 이유를 설명하고 있으면서, 정작 앞쪽의 결과 필드(action/decision)에는 다른 값이 들어있는 경우
이 현상이 어떤 메커니즘으로 발생하는지 살펴보기 위해, 가로·세로·대각선 확인이 필요한 5×5 미니 오목(Connect-4) 테스트를 진행해 보았습니다.
실험 설계: 5×5 미니 오목 테스트 케이스
모델에게 5×5 격자판 상태를 텍스트 2D 배열로 제공하고, "가로/세로/대각선 4개를 먼저 이으면 승리하는 규칙에서 상대방('O')의 즉각 승리를 저지할 유일한 방어 좌표 (row, col)를 찾아라"는 프롬프트를 전달했습니다.
[보드판 배치도]
col 0 col 1 col 2 col 3 col 4
row 0: [ . , O , . , . , . ]
row 1: [ X , O , O , O , X ]
row 2: [ . , . , . , O , . ]
row 3: [ . , . , . , . , . ]
row 4: [ . , . , . , . , . ]
이 배치는 두 가지 요소로 구성되어 있습니다:
1. 가로 1행: 이미 막혀 있는 무효 위협
- 가로 1행을 보면
(1,1),(1,2),(1,3)에 상대 돌 'O'가 3개 연속되어 있어 가장 눈에 띕니다. - 하지만 양쪽 끝인
(1,0)과(1,4)가 이미 우리 돌 'X'로 막혀 있어, 상대는 가로 1행에서 4목을 만들 수 없습니다.
2. 대각선: 다음 턴에 승리 가능한 실제 위협
- 대각선을 살펴보면
(0,1)➔(1,2)➔(2,3)으로 상대 돌 3개가 이어져 있습니다. - 그 연장선인
(3,4)좌표는 비어 있습니다(.). - 만약 이번 턴에
(3,4)를 방어하지 않으면 상대가 다음 턴에 4목으로 승리하게 됩니다. - 따라서 정답 착수점은
row: 3, col: 4입니다.
두 가지 JSON Schema 구성: Decision First vs CoT First
동일한 프롬프트와 동일한 Gemini 3.6 Flash 모델에서, JSON Schema의 필드 정의 순서만 다르게 구성하여 비교했습니다.
Schema A: Decision First (결과 필드 먼저 정의)
결과 좌표를 최상단에 두고, 뒤이어 분석 텍스트를 받도록 구성했습니다.
{
"type": "OBJECT",
"properties": {
"best_defense_move": {
"type": "OBJECT",
"properties": {"row": {"type": "INTEGER"}, "col": {"type": "INTEGER"}},
"required": ["row", "col"]
},
"real_threat_direction": {"type": "STRING"},
"ignored_dead_threat": {"type": "STRING"},
"analysis": {"type": "STRING"}
},
"required": ["best_defense_move", "real_threat_direction", "ignored_dead_threat", "analysis"]
}
Schema B: CoT First (분석 필드 먼저 정의)
판 분석 텍스트를 먼저 출력하도록 한 뒤, 마지막에 착수 좌표를 도출하도록 구성했습니다.
{
"type": "OBJECT",
"properties": {
"spatial_board_scan": {"type": "STRING"},
"dead_threat_analysis": {"type": "STRING"},
"live_threat_analysis": {"type": "STRING"},
"best_defense_move": {
"type": "OBJECT",
"properties": {"row": {"type": "INTEGER"}, "col": {"type": "INTEGER"}},
"required": ["row", "col"]
}
},
"required": ["spatial_board_scan", "dead_threat_analysis", "live_threat_analysis", "best_defense_move"]
}
실험 결과 비교
Gemini API를 통해 각 조건별 지연 시간(Latency), 총 토큰 소비량, 최종 착수 좌표를 측정했습니다.
| 실험 케이스 | Thinking Budget | Schema 필드 구성 | 소요 시간 | 소비 토큰 | 최종 착수 좌표 | 결과 |
|---|---|---|---|---|---|---|
| Case 1 | 0 (OFF) | Decision First | 3.47초 | 653 토큰 | (row: 0, col: 3) | 오답 |
| Case 2 | 0 (OFF) | CoT First | 4.41초 | 890 토큰 | (row: 3, col: 4) | 정답 |
| Case 3 | 1,024 (ON) | Decision First | 4.02초 | 790 토큰 | (row: 3, col: 3) | 오답 |
| Case 4 | 1,024 (ON) | CoT First | 5.80초 | 1,284 토큰 | (row: 3, col: 4) | 정답 |
| Case 5 | 2,048 (ON) | Decision First | 5.61초 | 907 토큰 | (row: 3, col: 3) | 오답 |
결과 분석: 필드 순서에 따른 모델의 생성 과정
1. Decision First에서 오답이 발생하는 이유
Case 3(Budget 1,024)과 Case 5(Budget 2,048)에서 모델은 착수 좌표로 (3, 3)을 출력했습니다. 그런데 모델이 생성한 JSON의 analysis 필드를 확인해 보면 흥미로운 내용이 나타납니다:
"Wait, look at (0,1), (1,2), (2,3) diagonal! (0,1), (1,2), (2,3) forms 3 'O's diagonally down-right. Next spot is (3,4)... The next cell to complete 4 in a row is row 3 col 4! Blocking at (3,4) prevents the diagonal win."
모델은 analysis 필드를 작성하면서 대각선 위협을 인지했고, 정답이 (3, 4)라는 사실을 파악했습니다. 하지만 이미 앞선 필드에서 best_defense_move: {"row": 3, "col": 3}을 먼저 출력한 상태였습니다.
언어 모델의 자동회귀(Autoregressive) 생성 특성상 앞에서 생성된 토큰을 되돌릴 수 없으므로, 뒤늦게 정답을 알아차렸더라도 이미 출력된 좌표 필드는 수정되지 못합니다.
2. CoT First에서 정답률이 올라가는 이유
반면 Case 2에서는 Thinking Budget이 0인 상태에서도 정답인 (3, 4)를 맞혔습니다.
JSON Schema에서 분석 필드를 앞에 배치하면, 모델이 해당 필드의 텍스트를 생성하는 과정 자체가 일종의 사고 단계(Chain of Thought) 역할을 하게 됩니다. 가로 1행이 막혀 있다는 점을 텍스트로 풀어내는 과정에서 다음 토큰 생성의 맥락이 정리되고, 그 결과 마지막 best_defense_move에 올바른 좌표가 들어가게 됩니다.
스키마 작성 시 참고할 점
실제 서비스에서 LLM의 Structured Outputs를 활용할 때 고려해볼 만한 점들을 정리했습니다:
- 최종 결정 필드는 가급적 뒤쪽에 배치: 라우팅 분기, 승인 여부, 최종 좌표 등 실제 동작으로 이어지는 핵심 값은 스키마의 마지막 프로퍼티로 선언하는 것이 유리합니다.
- 사전 점검 필드 활용:
reasoning_steps,extracted_conditions,risk_check같은 분석 필드를 결과 필드보다 앞에 두면 판단 정확도를 높이는 데 도움이 됩니다. - 용도에 따른 모델 및 스키마 조합: 높은 추론 모델 대신 Flash 계열 모델에 분석 필드가 포함된 스키마를 사용하는 것만으로도 비용과 속도 면에서 효율적인 결과를 얻을 수 있습니다.
자주 묻는 질문 (FAQ)
Q1. JSON 객체는 원래 키 순서가 없지 않나요?
표준 JSON 사양(RFC 8259)에서 객체의 키는 순서가 없는(unordered) 것으로 정의됩니다. 하지만 LLM이 문자열을 출력하는 과정은 완벽하게 왼쪽에서 오른쪽으로 진행되는 순차적(Sequential) 시계열 프로세스입니다. 모델은 스키마에 정의된 키 순서대로 토큰을 생성하므로, 모델의 인지 메커니즘 관점에서 키 순서는 절대적인 영향을 미칩니다.
Q2. Thinking Budget을 8,192 이상으로 극한까지 늘리면 해결되지 않나요?
공간 좌표나 행렬 탐색처럼 토큰화(Tokenization) 과정에서 정보가 압축되는 2D 데이터의 경우, 내부 은닉층 사고만으로는 시각적 유사성에 주의가 편향되는 현상(Attention Decoy)을 100% 극복하기 어렵습니다. 언어 모델은 텍스트 기호를 토큰으로 직접 방출하며 컨텍스트 윈도우에 쌓아둘 때 가장 강력한 주의집중 재조정 능력을 발휘합니다.
Q3. CoT First 스키마를 쓰면 응답 지연 시간이 너무 늘어나지 않나요?
실측 결과, Case 1(3.47초) 대비 Case 2(4.41초)로 약 0.94초의 지연 증가가 있었습니다. 하지만 엉뚱한 결정을 내려 에이전트 롤백을 수행하거나 재시도(Retry) 호출을 하는 데 드는 네트워크 왕복 비용(RTT)과 토큰 낭비를 감안하면, 최초 호출에서 0.9초를 더 쓰고 정답률을 확보하는 것이 총소유비용(TCO) 관점에서 훨씬 경제적입니다.