AI 판별을 복잡하게 하자 오히려 정확도가 떨어져서, Jev의 Judge를 단순화한 이야기
요약
플레이어의 질문에 대한 AI의 의미 판별(semantic judgment)이 핵심인 'Sideways'라는 수평적 사고 게임 개발 과정을 다룹니다. 초기에는 세밀한 의미 판별을 시도했으나, 오히려 답변이 부자연스러워지는 문제를 겪었습니다. 결국 AI에게 판단시키는 내용을 줄이는 역방향 설계로 개선하여 시스템의 안정성을 확보했습니다.
핵심 포인트
- AI를 통한 확률적 의미 판별과 결정론적 정책(Code) 분리가 핵심 구조입니다.
- 세밀한 의미 정보 판별은 오히려 답변을 부자연스럽게 만들었습니다.
- Theory 평가 시 core mechanism, causal relation 등 다차원적인 분석이 필요했습니다.
- TypeScript와 다양한 테스트를 통해 시스템의 안정성을 확보했습니다.
현재 Sideways라는 수평적 사고 게임을 개발하고 있습니다.
이른바 '해무새의 수프'와 비슷한 게임으로, 플레이어는 수수께끼 같은 상황에 대해 질문하며 진상을 알아냅니다.
예를 들어,
is it light?
라고 질문하면,
Yes
이라고 돌아옵니다.
혹은,
does the wake up mechanism make sound?
이라면,
No
라고 답합니다.
언뜻 보기에는 AI로서는 상당히 간단한 처리처럼 보입니다. 하지만 실제로 만들어보니, 이 'Yes / No를 자연스럽게 반환하는 것'이 생각보다 어려운 문제였습니다.
특히 흥미로웠던 점은,
의미 판별을 세밀하게 설계할수록, 실제 Jev의 답변이 부자연스러워졌다는 것입니다.
결국 역방향으로 진행하여,
AI에게 판단시키는 내용을 줄임으로써 상당히 개선했습니다.
이번 글에서는 구체적으로 무엇을 변경했고, 실제 답변이 어떻게 달라졌는지 작성하겠습니다.
Sideways에서 AI가 게임 전체를 제어하는 것은 아닙니다. 기본 구조는 다음과 같습니다.
플레이어의 자연어
↓
AI에 의한 확률적 의미 판별
...
생각 방식으로는,
AI = semantic judgment
Code = deterministic policy
User = consequential intent
입니다.
예를 들어 Question이라면 최종적으로 사용자에게 반환되는 것은 다음과 같습니다.
YES
NO
PARTLY
...
Theory라면,
SOLVED
PARTIAL
NOT_SOLVED
입니다.
AI가 멋대로 화면을 결정하거나 자유문으로 대답하는 것이 아닙니다. 이 분리 자체는 현재도 좋은 설계라고 생각합니다.
문제는,
AI에게 얼마나 세밀한 의미 정보를 판별하게 해야 하는가?
이었습니다.
Version 2의 Question Judge에서는, 대체로 다음과 같은 정보를 Jev로부터 받았습니다.
{wellFormed,
relevant,
...}
각각 0~1의 확률입니다.
그 후 TypeScript 측에서,
if (wellFormed < 0.8) {
return "REPHRASE";
}
...
와 같은 처리를 했습니다.
Theory를 평가하는 Solution Judge는 더욱 세밀하여,
{coreMechanism,
causalRelation,
...}
라는 5축이 있었습니다.
이유는 다음과 같은 문장을 구별하고 싶었기 때문입니다.
There is a light.
이것은,
빛이 있다는 것을 알아차린 정도
에 불과하므로,
PARTIAL
로 하고 싶습니다.
한편,
The light wakes the guest.
이라면,
빛이 손님을 깨운다
라는 인과관계까지 이해하고 있습니다.
이것은,
SOLVED
로 하고 싶습니다.
더 나아가,
There is a light, but vibration wakes the guest.
이라면, light라는 올바른 단어가 포함되어 있어도,
손님을 깨운 것은 진동
이라는 잘못된 원인을 주장하고 있습니다.
이것은,
NOT_SOLVED
로 하고 싶습니다.
그렇기 때문에,
core mechanism
causal relation
supporting insight
...
을 나누어 평가하는 설계로 했습니다.
TypeScript로 보면 상당히 깔끔합니다. Version 2에서는,
880 unit/API tests
82 desktop/mobile E2E tests
Leak scan
...
등이 모두 통과했습니다.
Calibration corpus도 만들었습니다. Generalization용 engineering holdout도 만들었습니다.
Hidden Solution은 server-only입니다. 일반적인 Question은 Jev 호출 1회. Theory도 1회입니다. 보안이나 소프트웨어 구조로서는 상당히 좋은 상태였습니다.
그런데 실제 Jev에서 테스트 플레이를 하니 문제가 발생했습니다. 한 문제에서는,
소리가 나지 않고, 아무것도 만지지 않았는데
잠자고 있는 손님이 일어난다
이러한 상황을 다루고 있습니다.
진실에는 '빛'이 관련되어 있습니다.
그래서,
is it light?
이라고 질문했습니다.
Version 2:
Not enough information
다음으로,
is it light that wakes the guest?
Version 2:
Not enough information
게다가 이것을 Theory로 보내자,
Not quite yet.
이었습니다.
인간 게임 마스터라면 상당히 쉽게 의미를 이해할 수 있는 문장입니다.
더 나아가,
does the wake up mechanism make sound?
은,
Please rephrase
이었습니다.
철자를 조금 틀려서,
does the wake up mechines make sound?
이라고 해도,
Please rephrase
입니다.
영어로서 완벽하지는 않습니다.
하지만,
wake up mechines
이,
wake-up mechanism
같은 의미라는 것은 인간이라면 상당히 쉽게 알 수 있습니다.
그래서,
Jev의 능력이라기보다는, 이쪽 Judge 설계가 너무 엄격한 것이 아닐까?
라고 생각하기 시작했습니다.
이것은 지금도 꽤 가능성이 있다고 생각합니다.
예를 들어 Jev 내부에서는,
wellFormed = 0.74
relevant = 0.92
contradicted = 0.89
정도까지 의미를 이해하고 있었을 가능성이 있습니다.
하지만 코드 측면에서는,
if (wellFormed < 0.8) {
return "REPHRASE";
}
입니다.
그 때문에,
wellFormed = 0.74
의 시점에서,
Please rephrase
이 되어버립니다.
그 이후의,
relevant = 0.92
contradicted = 0.89
은 사용되지 않습니다.
원래 0~1의 연속값을 사용했었는데,
0.7999 → NG
0.8000 → OK
라는 '절벽'을 앱 측에서 만들고 있었던 것입니다.
임계치를 0.6 정도로 낮추면 개선했을 가능성은 있습니다.
다만, Version 2에는 0.8의 조건이 여러 개 존재했습니다.
그 때문에 문제는 단순히,
0.8이 높았던 것
만은 아니라고 생각합니다.
Version 2는 실질적으로,
의미가 충분히 파악됨
AND
관련성이 충분히 높음
...
이라는 거대한 조건식이 되어 있었습니다.
각각의 확률이 적당히 정확하더라도,
어느 하나라도 threshold를 밑돌면
최종 결과가 바뀝니다.
즉,
하나의 불확실한 AI 판단을
여러 개의 불확실한 AI 판단 + hard threshold
로 분해한 결과, 오히려 깨지기 쉬워졌을 가능성이 있습니다.
예를 들어,
does the wake up mechines make sound?
을 인간이 읽는다면,
"mechines"는 mechanism의 오타일 것이다
↓
wake-up mechanism에 관한 것이겠지...
정도까지 하나로 이해합니다.
반면 Version 2에서는,
wellFormed는 몇 점?
relevant는 몇 점?
truth는 몇 점?
으로 분할했습니다.
TypeScript의 타입으로는 깔끔합니다.
하지만 자연어 의미 이해로서는 너무 많이 나눈 것일 가능성이 있습니다.
예를 들어,
is it light?
라는 문장만이라면,
it은 무엇인가?
은 확실히 모호합니다.
하지만 수평사고 게임에서는 플레이어가 지금,
왜 손님이 일어났는지?
을 조사하고 있습니다.
인간 게임 마스터라면,
is it light?
을,
손님을 일으킨 원인은 빛인가?
으로 자연스럽게 보완할 수 있습니다.
그래서 Version 3에서는,
mysteryFocus
이라는 정보를 추가했습니다.
예를 들어,
알람이 소리를 내지 않고 아무것도 손대지 않았을 때, 잠든 손님이 깨는 원인은 무엇인가?
입니다.
이것은 답이 아닙니다.
단순히,
지금 플레이어가 무엇을 풀려고 하는지를
server-side에서 명시하고 있습니다.
Version 2를 더 세밀하게 조정하는 것이 아니라, 구조 자체를 간단하게 했습니다.
Question Judge에서는,
type QuestionSemanticClass =
|"SUPPORTED"
|"CONTRADICTED"
...
라는 하나의 Choice distribution만 Jev에게 받습니다.
그 후 TypeScript에서,
SUPPORTED
→ YES
CONTRADICTED
...
로 변환합니다.
Version 2에 있던,
wellFormed >= 0.8
relevant >= 0.8
entailed >= 0.8
같은 판정 체인(判定chain)은 active path에서 제거했습니다.
Theory도 마찬가지입니다.
type SolutionSemanticClass =
|"CORE_CAUSAL_EXPLANATION"
|"CORE_WITH_CONTEXT_ERROR"
...
Jev는 이 중 어느 것에 가까운지에 대한 probability distribution만 반환합니다.
TypeScript에서는,
CORE_CAUSAL_EXPLANATION
→ SOLVED
CORE_WITH_CONTEXT_ERROR
...
로 처리합니다.
즉,
의미상의 차이
는 남겼습니다.
하지만,
5가지 의미 축을 개별적으로 채점하는 것
은 중단했습니다.
Version 3에서는,
minor spelling mistake
non-native English
missing article
...
는 허용하도록 했습니다.
다만, 각각을 독립적으로 채점하지는 않습니다.
최종적으로 묻는 것은,
이 플레이어의 문장이 의미적으로 어떤 카테고리인지?
입니다.
같은 문제로 다시 테스트했습니다.
Version 3:
is it light?
→ Yes
다음:
is brightness involved?
→ Yes
더 나아가,
something bright wake him?
→ Yes
입니다.
이 영어 문장은 상당히 부자연스럽습니다.
그럼에도 의미는 올바르게 이해되었습니다.
이전에 실패했던,
does the wake up mechines make sound?
도,
No
가 되었습니다.
더 나아가,
does it buzz?
→ No
is there vibration?
→ No
같은 짧은 표현도 작동했습니다.
이것은 mysteryFocus
가 있기 때문에, 짧은 질문을 게임 문맥에서 해석하기 쉬워졌을 가능성이 있습니다.
다음과 같이 두 가지 내용을 한 번에 입력했습니다.
something bright wake him?
does the wake up mechines make sound?
결과:
Partly
UI에서는,
Some of that is right, but another part isn't.
이 되었습니다.
이는 좋은 결과입니다.
PARTLY는,
50% 정도 맞을 것 같다
라는 의미가 아닙니다.
올바른 주장
+
틀린 주장
이 실제로 공존하는 경우에만 사용하고 싶었기 때문입니다.
Question:
does light wake him?
결과:
Yes
같은 문장을 Theory로 보내자,
You got it.
가 되었습니다.
더 나아가,
the room gets bright and that wakes him
도,
Question → Yes
Theory → You got it.
입니다.
Version 2에서는 비슷한 정도의 표현이라도,
Not quite yet.
이런 상태였습니다.
이 차이는 상당히 큽니다.
단순화함으로써 무엇이든 정답이 되는 것은 곤란합니다.
예를 들어,
lamp is there but vibration wakes him
에서는,
Question
→ No
Theory:
Not quite yet.
이었습니다.
lamp이라는 관련 단어가 포함되어 있어도,
객을 깨운 원인은 vibration
이라고 주장하고 있으므로 정답이 될 수 없습니다.
이것은 중요합니다.
Case 1은 '빈 프레임 속에서 무언가가 변화하여 보이는' 문제입니다.
예를 들어,
is the frame big
결과:
Not relevant
is it an animal
결과:
No
is it a TV
결과:
No
is it a machine
결과:
No
그리고,
it changes its color with sun light
결과:
Yes
같은 문장을 Theory로 하면,
You got it.
이 되었습니다.
문법적으로는 완벽하지 않습니다.
하지만 올바른 메커니즘은 이해되고 있습니다.
이 정도면 수평적 사고 게임(lateral thinking game)으로 상당히 자연스럽습니다.
완벽해진 것은 아닙니다.
예를 들어 같은 세션에서,
is it the sunrise and sunset
→ Yes
이 된 반면,
is it sunrise and sunset
→ Not enough information
이 되는 경우도 있었습니다.
이는 아직 semantic boundary에 다소 흔들림이 있다는 것을 보여줍니다.
다만, Version 2의 문제와는 성격이 다릅니다.
Version 2에서는,
의미가 명확히 알 수 있는 문장
→ Please rephrase
이 빈번하게 발생했습니다.
Version 3에서는,
상당히 경계적인 문장
→ Yes였거나 Unknown이었던 정도
까지 개선되었습니다.
PoC(Proof of Concept)로서는 후자가 상당히 좋은 실패 방식이라고 생각합니다.
여기는 솔직하게 적어야 합니다.
Version 2 → Version 3에서는 동시에 여러 변경을 했습니다.
복수 Noun을 1 Choice로 변경
0.8 / 0.2 threshold chain 삭제
mysteryFocus 추가
...
따라서,
0.8을 제거했기 때문에 개선되었다고는 아직 단정할 수 없습니다.
어쩌면,
0.8 → 0.6
으로만 변경해도 Version 2는 상당히 개선되었을 가능성이 있습니다.
반대로,
mysteryFocus
이 가장 효과적이었을 가능성도 있습니다.
혹은,
여러 독립적인 판단을 하지 않은 것
이 중요했을 가능성도 있습니다.
현재 알고 있는 것은,
이들을 종합하여 단순화한 Version 3이 실전 플레이에서는 명확하게 더 자연스러웠다는 것입니다.
개발을 계속하면서 Jev에 대한 감각도 조금 바뀌었습니다.
일반적인 생성 AI처럼,
AI에게 일을 맡기는 것
이라기보다는,
자연어를 이해할 수 있는 IF문(if statement)을 만드는
감각에 가깝습니다.
일반적인 코드는,
if (text.includes(
AI 기반의 switch/case
이렇게 생각하니 훨씬 이해하기 쉬워졌습니다.
그렇게 생각하면 Version 2의 문제도 쉽게 파악할 수 있습니다.
처음에는,
의미적으로 올바른가?
→ YES
정도였던 것이,
IF wellFormed >= 0.8
AND relevant >= 0.8
AND entailed >= 0.8
...
라는 거대한 조건문이 되어 있었습니다.
Version 3에서는,
switch (semanticClass) {
case "SUPPORTED":
return "YES";
...
정도까지 되돌렸습니다.
이쪽이 이번 용도에는 더 적합한 것 같습니다.
이번에 또 느낀 점은,
calibration(보정)에는 인간의 노동 비용이 있다는 것입니다.
실제로 이 게임에서는,
semantic category를 생각하기
실제 플레이하기
짧은 문장 테스트하기
...
와 같은 작업이 필요했습니다.
이는 상당한 개발 비용입니다.
Jev 덕분에,
LLM의 자유 문장을 JSON으로 변환하는
같은 처리는 줄어듭니다.
반면에,
decision design (결정 설계)
calibration(보정)
threshold 설계
...
의 중요성이 높아집니다.
따라서,
Jev가 일반 LLM보다 노동 비용이 높다고까지는 아직 말할 수 없습니다.
비교 실험을 하지 않았기 때문입니다.
다만,
Jev에서는 개발 공수 일부가 output parsing에서 decision design과 calibration으로 이동한다는 느낌은 상당히 있습니다.
현재 Version 3에서는,
849 unit/API tests
82 desktop/mobile E2E tests
Leak scan
...
이 통과하고 있습니다.
하지만 그래도 실제 Jev를 만져보지 않으면 이번 개선을 확인할 수 없었습니다.
오프라인 테스트로 확인할 수 있는 것은,
semantic class
↓
TypeScript mapping
...
의 정확성입니다.
반면, 정말 알고 싶은 것은,
미지의 인간 문장
↓
Jev
...
입니다.
이 두 가지는 별개입니다.
앞으로는,
Layer 1
Software correctness(소프트웨어 정확성)
Layer 2
...
로 나누어 생각할 필요가 있다고 생각합니다.
아직 개선할 수 있는 포인트는 있습니다.
예를 들어,
sunrise and sunset
의 변동도 고치려고 하면 고칠 수도 있을지 모릅니다.
하지만 여기서 또,
특정 구문을 rubric에 추가하기
category를 추가하기
threshold를 추가하기
...
를 시작하게 되면, Version 2로 돌아가게 됩니다.
따라서 현재는,
Version 3을 PoC(개념 증명)의 Freeze 후보로 하는 방향입니다.
완벽한 Judge를 만드는 것보다,
자연스러운 짧은 문장을 이해하기
오타가 있어도 의미를 이해하기
올바른 인과관계라면 SOLVED가 되기
...
을 우선합니다.
더 단순한 fallback(폴백)도 생각하고 있습니다.
Question을,
YES
OTHER
만으로 하는 방법입니다.
`OTHER`에는,
NO
UNKNOWN
IRRELEVANT
...
등을 전부 모읍니다.
정보량은 크게 줄어듭니다.
하지만 만약 그쪽이 게임으로서 안정적이라면 채택할 가치가 있습니다.
이 프로젝트에서는,
AI로부터 최대한 많은 정보를 추출하는 것 자체가 목적은 아닙니다.
필요한 것은,
프로덕트에 필요한 최소한의 판단을, 안정적으로 받아내는 것입니다.
이번에 가장 크게 배운 점은,
AI의 의미 판정을 세밀하게 설계할수록 좋다고만 할 수 없다는 것이었습니다.
Version 2는 고도했습니다.
semantic signals가 많고
probability가 많고
threshold가 많은
...
상태였습니다.
하지만 실제 플레이에서는 부자연스러웠습니다.
Version 3에서는 반대로,
1 semantic Choice
↓
1 probability distribution
...
까지 간단하게 했습니다.
그러자,
is it light?
→ Yes
무언가 밝은 것이 그를 깨우나요?
→ 예
깨어나는 기계는 소리를 내나요?
→ 아니요
빛이 그를 깨우나요?
→ 맞아요.
와 같이, 상당히 자연스러워졌습니다.
이번 경험을 통해 AI 제품에서는,
"더 이해하게 하는 것(Understand more)."
보다,
"덜 결정하게 하는 것(Decide less)."
이 더 좋을 때가 있다는 것을 느꼈습니다.
그리고 Jev와 같은 의사결정 모델을 사용할 경우에는,
'무엇을 AI에게 판단시키지 않을 것인가'를 설계하는 것도 중요한 AI 설계일 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기