Jev를 사용해 AI 게임 마스터를 만들면서, 의미 판별을 세밀하게 설계할수록 오히려 불안정해진 경험담
요약
AI 게임 마스터를 개발하는 과정에서 의미 판별의 세밀도를 높일수록 오히려 시스템이 불안정해지는 경험을 공유합니다. 이 글은 일반적인 채팅 응답 대신, 'YES', 'NO'와 같은 구조화된 판단(Decision Model)을 위해 Jev라는 도구를 사용한 아키텍처를 설명합니다.
핵심 포인트
- AI에게 의미 판별만 맡기고, 정책 결정은 코드가 담당하는 분리 구조가 효과적입니다.
- Jev는 채팅 모델이 아닌 Decision Model로 활용되어 구조화된 판단과 확률을 얻기에 적합했습니다.
- 의미 판별 축(coreMechanism, causalRelation 등)을 세분화할수록 복잡도가 증가하고 불안정해지는 문제가 발생했습니다.
현재 Sideways라는 작은 수수께끼(水平思考) 게임을 만들고 있습니다.
이른바 '우미가메의 수프'와 비슷한 게임으로, 플레이어에게는 신비로운 상황만 제시합니다. 그리고 플레이어는 다음과 같은 질문들을 반복하면서 진실에 다가갑니다.
손님을 깨운 것이 빛인가?
그 메커니즘이 소리를 내는가?
영문 버전이라 실제 입력은 예를 들어 다음과 같습니다.
is it light that wakes the guest?
게임 마스터라면,
Yes.
라고 대답하면 됩니다. 겉보기에는 매우 간단한 AI 기능처럼 보입니다.
하지만 실제로 만들어보니, 이것이 상당히 어려운 문제였습니다. 이번 글에서는 Jev를 사용하여 AI 게임 마스터를 만드는 과정에서, 의미 판별을 세밀하게 설계할수록 오히려 실제 게임 경험이 나빠져 버린 이야기를 쓰고자 합니다.
이 게임에서는 일반적인 채팅 AI처럼 긴 문장을 생성해 주기를 바라는 것이 아닙니다. 필요한 것은 다음과 같은 '판단'입니다.
YES
NO
UNKNOWN
...
그래서 Jev를 선택했습니다. Jev는 공식 문서에서도 채팅 모델이 아니라 decision model로 설명되어 있으며, application state와 typed question을 전달하고 구조화된 판단과 확률을 받는 설계입니다.
Choice, Score, Noul 같은 타입을 이용할 수 있습니다. 이는 이번에 만들고 싶었던 아키텍처와 매우 잘 맞았습니다.
처음부터 AI에게 애플리케이션 전체를 제어하게 할 생각은 아니었습니다. 구상한 구조는 다음과 같습니다.
플레이어의 문장
↓
AI에 의한 확률적 의미 판별
...
생각하는 바는 다음과 같습니다.
AI = 의미를 판단한다
Code = 정책을 결정한다
User = 중요한 의도를 선택한다
AI가 직접,
이 사용자에게 이 화면을 표시해야지
라고 판단하는 것이 아닙니다. AI에게는 의미만 판단하게 하고, 그 결과로부터 무엇을 할지는 TypeScript 측에서 결정합니다.
이 구조 자체는 지금도 상당히 마음에 듭니다.
문제는 AI가 얼마나 세밀한 의미 정보를 반환받느냐였습니다.
초기 Question Judge에서는 플레이어의 질문에 대해 대략 다음과 같은 정보를 판별했습니다.
{wellFormed: number,
relevant: number,
...}
그리고 TypeScript 측에서,
if (wellFormed < 0.8) return
만드는 것은 조금 너무합니다.
그래서,
`Partly`
라고 응답할 수 있는 구조로 만들었습니다.
플레이어가 '이것이 답이다'라고 Theory(가설) 형태로 보낼 경우에는, Question과는 별도의 Solution Judge를 사용합니다.
Version 2에서는 다음 5가지 축을 판정하도록 했습니다.
{coreMechanism,
causalRelation,
...
예를 들어,
There is a light.
와
The light wakes the guest.
은 같지 않습니다.
전자의 경우라면,
중요한 물체에는 알아차렸다
만 할지도 모릅니다.
후자의 경우라면,
원인까지 이해했다
라고 말할 수 있습니다.
게다가,
The alarm uses a light,
but vibration wakes the guest.
이라면, light라는 올바른 단어가 포함되어 있어도,
손님을 깨우는 것은 진동
이라는 잘못된 원인을 주장하고 있습니다.
그래서,
There is a light.
→ PARTIAL
The light wakes the guest.
...
와 구분할 수 있도록 하고 싶었던 것입니다.
설계상으로는 상당히 깔끔해졌습니다.
Version 2에서는,
880 unit/API tests
82 desktop/mobile E2E tests
Leak scan
...
등을 모두 통과시켰습니다.
게다가 6가지 문제 모두에 대해,
Calibration corpus
Generalization corpus
도 작성했습니다.
Hidden Solution은 server-only입니다.
브라우저에는 비밀 정보를 반환하지 않습니다.
일반적인 질문이라면 AI 호출은 1회입니다.
Theory도 1회입니다.
보안이나 배선 면에서는 상당히 좋은 상태가 되었습니다.
그래서 실제 Jev를 사용해서 Preview 환경에서 시험해 보았습니다.
예를 들어 다음 질문이 있습니다.
is it light?
결과:
Not enough information
다음.
is it light that wakes the guest?
결과:
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
입니다.
하지만 인간이라면 의미는 거의 확실하게 이해할 수 있습니다.
게다가,
Instead of an alarm, lights from a device wakes the guest.
Question으로 보내면,
Not enough information
Theory로 보내면,
Not quite yet.
이었습니다.
영어로는 완벽하지 않습니다.
하지만,
장치의 빛이 손님을 깨운다
라는 중요한 인과관계는 상당히 명확합니다.
여기서,
Version 2의 방향성 자체에 문제가 있는 것이 아닐까?
라고 생각하기 시작했습니다.
Version 2에서는,
문장을 이해할 수 있는지
관련성이 있는지
맞는지
...
등을 각각 독립적인 signal로 취급하고 있었습니다.
하지만 인간은 아마 그런 식으로 이해하지 않습니다.
예를 들어,
does the wake up mechines make sound?
을 읽은 인간은,
mechines는 mechanism의 오타일 것이다
↓
wake-up mechanism에 관한 것일 것이다
...
정도만 거의 동시에 처리합니다.
그런데 AI에게는,
wellFormed는 몇 점?
relevance는 몇 점?
contradicted는 몇 점?
하고 분할해서 답하게 했습니다.
TypeScript로서는 깔끔합니다.
하지만 자연어 이해(NLU)의 관점에서는 반드시 자연스러운 분할이 아니었을 가능성이 있습니다.
Version 2에서는 예를 들어,
if (wellFormed < 0.8) {
return "REPHRASE";
}
와 같은 처리가 있었습니다.
AI 측에서,
의미는 거의 이해할 수 있다
고 생각했더라도,
wellFormed = 0.77
이라면,
Please rephrase
입니다.
그 이후에 아무리 정확한 semantic evidence가 있어도 관계없습니다.
원래는,
0.77
이라는 연속적인 확률을 사용했어야 하는데,
0.7999
→ NG
0.8000
...
와 같은 급격한 경계를 앱 측에서 만들고 있었습니다.
의미의 축(axis)을 늘릴수록, 이 '절벽(cliff)'도 늘어납니다.
Jev에서는 `Choice`, `Score`, `Noul`과 같은 타입화된 decision을 다룰 수 있습니다.
공식 문서에서도 각 질문(question)을 specific하고 well-scoped하게 하는 것이 권장됩니다.
하지만 Version 2에서 실제로 시도했던 것들을 정리하자면,
망가진 영문을 이해하기
↓
대명사를 해결하기
...
이었습니다.
각 출력 항목은 작아 보입니다. 하지만, **모델에게 요구하는 의미 이해 전체는 작지 않았습니다.**
이는 이번에 상당히 중요한 발견이었습니다.
예를 들어,
is it light?
라는 문장만 보면,
itって何? (it은 무엇?)
가 되는 것은 맞습니다. 하지만 거북이 수프의 게임 마스터라면,
지금 플레이어는
「왜 손님이 일어났는지」
을 조사하고 있다
라는 문맥을 당연히 가지고 있습니다.
그러면,
is it light?
은 상당히 자연스럽게,
손님을 일으킨 원인은 빛인가?
이라고 해석할 수 있습니다.
그래서 현재 Version 3에서는,
mysteryFocus
이라는 server-only 정보를 추가할 예정입니다.
예를 들어,
소리나 물리적 접촉을 사용하지 않고,
무엇이 잠들어 있는 손님을 일으켰는가?
와 같은 정보입니다.
이것은 정답이 아닙니다. **플레이어가 무엇을 추론하려고 하는지**를 명시한 것일 뿐입니다.
Solution Judge에서는,
coreMechanism
causalRelation
등을 개별적으로 평가했습니다. 하지만,
The light wakes the guest.
라는 문장을 생각해 봅시다. 짧습니다. 하지만,
무엇이 원인인지
↓
light
...
까지 말하고 있습니다. 인간이라면 충분히 정답일 것입니다.
그런데,
coreMechanism = 높음
causalRelation = 약간 낮음
과 같은 결과가 나오면, TypeScript 측에서는 SOLVED가 되지 않습니다.
즉, **정답의 의미는 이해하고 있는데도, 채점 항목의 충족도에 의해 오답이 되는** 현상이 발생하고 있었습니다.
그래서 현재 시도하고 있는 Version 3에서는 큰 방향을 바꾸고 있습니다. Question Judge에서는 다수의 독립적인 0~1 signal 방식을 폐지합니다. 대신, **하나의 범주형 확률 분포(categorical probability distribution)**만 반환받도록 합니다.
예를 들어,
type QuestionSemanticClass =
| "SUPPORTED"
| "CONTRADICTED"
...
입니다. 이후 TypeScript에서,
SUPPORTED
→ YES
CONTRADICTED
...
로 변환합니다.
AI가 최종 UI를 결정하는 것은 아닙니다. 그 부분은 지금까지와 같습니다. 바뀌는 것은,
5개, 6개의 의미 점수
이 아니라,
하나의 의미 분류
만을 AI에게 요청하는 것입니다.
Theory 판정(判定)도 마찬가지로 간소화합니다. 예를 들어,
type SolutionSemanticClass =
| "CORE_CAUSAL_EXPLANATION"
| "CORE_WITH_CONTEXT_ERROR"
...
입니다. TypeScript에서는,
CORE_CAUSAL_EXPLANATION
→ SOLVED
CORE_WITH_CONTEXT_ERROR
...
と変換します。
つまり、
There is a light.
と、
The light wakes the guest.
と、
Vibration wakes the guest.
の違いは残します。
ただし、その違いを判断するために5종류의 독립적인 확률을 요구하는 것은 그만둡니다.
Version 3에서도 실제 Jev로 불안정했다면,それ 이상 복잡하게 하지 않을 계획입니다.
역방향으로 갑니다.
최종 후보는,
YES
OTHER
입니다.
즉 Question Judge에게는,
이 플레이어의 주장은 Hidden Truth에 의해 지지되고 있는가?
만 묻습니다.
지지되고 있다면,
YES
그 외는 전부,
OTHER
입니다.
그러면,
NO
UNKNOWN
IRRELEVANT
...
의 차이는 사라집니다.
정보량은 줄어듭니다.
하지만, 게임으로서는 이쪽이 더 자연스러워질 가능성이 있습니다.
이것은 상당히 흥미로운 트레이드오프입니다.
이번 경험을 통해,
의미 정보를 늘리는 것
↓
판단 자료가 늘어난다
...
만이 전부는 아니라는 것을 알게 되었습니다.
오히려,
의미 signal을 늘리는 것
↓
불확실한 경계가 늘어난다
...
하기도 합니다.
AI 통합에서는,
AI로부터 최대한 많은 정보를 추출하는 것보다,
제품이 정말로 필요로 하는 최소한의 판단만을 요청하는 것이 더 중요할지도 모릅니다.
또 다른 큰 배움이 있었습니다.
Version 2에서는 880건의 unit/API test와 82건의 E2E가 통과했습니다.
테스트에는 큰 가치가 있습니다.
실제로,
schema가 망가지지 않았는지
API가 망가지지 않았는지
hidden data가 새어 나오지 않았는지
...
는 확인할 수 있습니다.
하지만,
올바른 semantic signal
↓
올바른 product result
을 확인하는 테스트와,
미지의 인간 문장
↓
올바른 semantic signal
을 확인하는 테스트는 별개입니다.
전자가 모두 통과해도, 후자는 실패합니다.
앞으로는 이 두 가지를,
Layer 1
Software correctness
Layer 2
...
로 완전히 나누어 생각하려고 합니다.
이번 결과를 봐도,
일반적인 Chat LLM에 전부 맡기는 게 좋다
라고는 생각하지 않습니다.
오히려 반대입니다.
Jev는 application state에 대해 typed decision을 반환하고, probability distribution을 application 코드에서 이용할 수 있도록 설계된 것입니다.
저는 지금도,
AI
→ bounded semantic decision
TypeScript
...
라는 분리는 좋다고 생각합니다.
이번 배움은,
Typed Decision이 너무 단순하다는 것이 아닙니다.
오히려,
그 단순함을 존중했어야 했다
고 생각합니다.
애플리케이션이 하나의 분류만을 필요로 한다면,
하나의 잘 설계된 Choice
가,
작은 semantic ontology
+
대량의 threshold
보다 좋을 가능성이 있습니다.
이 글을 쓰고 있는 시점에서는,
Version 1
비교적 단순한 확률 판정
↓
...
라는 상태입니다.
Version 3가 미지의 표현에 충분히 안정적이라면, 여기서 Judge를 Freeze하겠습니다.
안되면,
YES / OTHER
까지 낮추겠습니다.
그 이상 semantic complexity를 늘릴 생각은 없습니다.
이번에 가장 컸던 배움은 상당히 간단합니다.
제품이 필요로 하는 것보다 복잡한 판단을 AI에게 요구하지 않는 것입니다.
복잡한 semantic model은 TypeScript 상에서는 예쁘게 보입니다.
타입도 예뻐지고.
테스트도 많이 쓸 수 있습니다.
이론적으로도 고도하게 보입니다.
그럼에도 불구하고,
실제로 사용자가 플레이했을 때 부자연스럽다
면 의미가 없습니다.
AI 아키텍처에서는,
Understand more.
보다,
Decide less.
하는 것이 더 좋을 때도 있습니다.
이번 Sideways 개발에서도 이를 상당히 강하게 느끼고 있습니다.
Version 3의 Jev 검증이 끝나면, 이 기사에도 결과를 추가할 예정입니다.
Sideways는 Next.js / TypeScript / Jev를 사용하여 개발 중인 실험적인 수평적 사고 게임입니다.
핵심 설계 사상은,
Probabilistic Semantic Judgment
→ Typed Output
→ Deterministic TypeScript Policy
...
입니다.
현재 시도하고 있는 것은,
이 'Semantic Judgment'를 어디까지 작게 만들 수 있는지에 대한 실험이기도 합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기