벙커키즈
경력근거: 인격을 유지하고, 말투를 지키고, 세계관을 벗어나지 않으면서도 유저가 설레는 답장을 만들어야 합니다.
※ 실제 산출물이 아닌 AI 예측 시안입니다.
위프는 유저가 직접 캐릭터와 시나리오를 만들어 공개하는 서비스라, 지켜야 할 규칙이 회사 가이드가 아니라 유저가 쓴 캐릭터 카드에 들어 있습니다. 카드가 데이터로 남아 있어야 말투와 1인칭, 세계관 이름, 금지 소재를 회차마다 짝지어 볼 수 있고, 한국과 일본을 비롯한 9개국 대화가 한 서비스에 쌓여야 어느 캐릭터가 어디서 무너지는지가 보입니다. 여러 프로바이더와 자체 모델을 함께 굴린다는 대목도 이 화면에서 뜻을 가집니다. 같은 카드로 답한 회차를 모델별로 갈라 세면 라우팅을 어디부터 고칠지가 숫자로 남기 때문입니다. 다만 이 포트폴리오는 규칙으로 판정하고 그 결과를 화면과 평가 러너로 잇는 구성을 보여줍니다. 캐릭터 대화 서비스의 운영 실무나 파인튜닝 데이터 설계의 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
공고는 인격을 유지하고 말투를 지키고 세계관을 벗어나지 않는 답장을 만드는 일이 이 포지션의 핵심이라고 적습니다. 그 세 가지가 회차마다 지켜졌는지 세는 도구가 이 일의 첫 단추입니다.
여러 LLM 프로바이더를 동시에 운용하면서 자체 모델도 호스트한다고 밝히고, 우대사항에 라우팅과 폴백, A/B 실험을 나란히 둡니다. 어느 모델이 캐릭터를 더 자주 놓치는지 세어야 라우팅을 고칠 근거가 생깁니다.
프롬프트 엔지니어링을 넘어 평가 파이프라인을 설계하고 운영한 경험을 자격 요건으로 못박고, 유저 이탈을 데이터로 보고 실험으로 다음 방향을 정하라고 적습니다. 회차별 판정과 그 판정을 사람 표시와 맞대는 러너가 그 요구와 그대로 맞물립니다.
회차 로그와 캐릭터 카드를 올리면 답장 문장에서 말투를 종결 어미로 직접 갈라 1인칭·등장 이름·금지 소재·앞 회차 기억과 함께 카드와 짝지어 보고 다섯 갈래로 나눕니다. 갈래를 가르는 것은 카드 값 대조와 이름 집합 빼기, 소재어 사전 조회, 세션 안 앞 회차 되짚기이고, 클로드는 그 뒤에 붙어 어긋난 대목을 크리에이터가 카드에 고쳐 적을 한 줄로 옮깁니다.
위프에 자기 캐릭터와 시나리오를 만들어 공개한 크리에이터 유저 — 하루에도 수백 회차씩 대화가 쌓이는데 지금은 로그를 눈으로 한 줄씩 읽는 것 말고 자기 캐릭터가 설정대로 말하고 있는지 알 방법이 없다
몰입이 깨지는 회차를 못 찾는 까닭은 대화가 많아서가 아니라, 캐릭터 카드에 적어 둔 규칙과 실제 답장이 서로 다른 화면에 있어 한 번도 나란히 놓이지 않기 때문입니다. 카드를 잣대로 회차마다 대조해 다섯 갈래로 나눠 두면, 크리에이터는 프롬프트를 통째로 고치는 대신 어긋난 회차부터 열어 봅니다.
크리에이터가 자기 캐릭터를 점검할 때 여는 화면 하나가 돕니다. 회차 로그와 캐릭터 카드를 올리면 회차마다 다섯 갈래 중 하나가 붙고 카드 값과 답장 값이 나란히 열립니다. 말투는 로그 값을 그대로 믿지 않고 답장 문장의 종결 어미로 직접 갈라내며, 금지 소재는 소재어 사전과 답장 문장을 맞춰 보는 데까지 하고 문맥을 읽어 판단하는 안전성 분류는 3주차로 미룹니다.
크리에이터가 직접 읽고 표시한 120회차를 정답으로 붙여 판정 일치를 자동으로 세고, 갈라진 회차를 사유별로 리포트로 냅니다. 같은 화면에서 세 모델이 답한 회차를 갈라 어긋남 비율을 견줍니다.
세계관 밖 이름 가운데 크리에이터가 받아들인 것을 카드에 더하는 수정 제안을 뽑고, 모델별 어긋남 비율에 따라 어느 캐릭터를 어느 모델로 옮길지 라우팅 후보를 화면에 세웁니다.
크리에이터가 로그와 카드를 올리면 회차마다 판정이 붙고, 어긋난 회차는 카드의 어느 값과 답장의 어느 값이 갈라졌는지 그 자리에서 읽습니다.
등장 이름이 비거나 말투를 못 뽑았거나 카드가 아예 없는 회차를 섞은 입력 30종을 올려도 목록이 사라지지 않고, 건너뛴 대조만 회차 옆에 남습니다.
카드 이름 목록과 한 글자만 다른 이름, 카드가 막은 소재를 스치듯 언급한 문장처럼 아슬아슬하게 걸치는 회차를 넣어도 걸림과 통과가 규칙대로 갈라집니다.
크리에이터가 표시한 120회차와 화면 판정을 맞춰 일치를 셉니다. 단, 정답과 데이터를 모두 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
위 수치는 더미데이터 자가채점이며 실제 판정 적중률은 벙커키즈의 실제 위프 회차 로그와 크리에이터가 만든 캐릭터 카드가 있어야 측정됩니다. 이 포트폴리오는 그 검증을 바로 돌릴 수 있는 상태로 만듭니다.
화면에 뜨는 판정 일치는 제가 만든 회차 로그에 제가 정답을 붙여 잰 숫자라 성능 증명이 아닙니다. 면접에서는 위프 실제 로그를 받는 날 무엇부터 다시 재는지로 설명합니다.
회차를 지어낸 사람이 정답표도 만들었으니 잘 맞는 것은 당연합니다. 그래서 등장 이름이 비거나 말투를 못 뽑았거나 카드가 아예 없는 회차, 카드 이름 목록과 한 글자만 다른 이름, 금지 소재를 스치듯 언급한 문장을 더미에 일부러 섞어 쉬운 회차만 골라 푸는 함정을 피합니다.
카드와 어긋났다는 판정이 곧 나쁜 답장을 뜻하지는 않습니다. 이야기가 자라면서 새 이름이 생기는 것은 자연스러우므로 판정은 크리에이터가 골라 볼 후보로 두고 마지막은 사람이 정합니다.
말투를 반말과 존댓말 두 값으로만 가르면 섞어 쓰는 캐릭터를 담지 못합니다. 한국어 밖 언어에서는 이 구분 자체가 성립하지 않아 9개국 서비스에 그대로 옮기기 어렵습니다.
이 포트폴리오는 규칙으로 판정하고 그 결과를 화면과 평가 러너로 잇는 구성을 보여줍니다. 캐릭터 대화 서비스 운영과 파인튜닝 데이터 설계의 전문성은 주장하지 않습니다.
실제 도입에서는 모델별 비율 차이가 라우팅 때문인지 캐릭터 구성 때문인지 갈라 봐야 합니다. 같은 카드로 세 모델을 돌린 회차만 골라 견주는 설계는 도입 단계에서 따로 잡습니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
위프 채팅 서버가 답장을 내보낼 때마다 남기는 회차 로그입니다. 유저가 보낸 발화 원문과 캐릭터 답장 원문이 한 줄에 같이 남고, 서버가 후처리로 붙여 둔 1인칭·말투·등장 이름도 참고값으로 따라옵니다. 이름 추출이 실패해 빈 줄, 발화 수집이 늦어 유저 말이 안 실린 줄도 섞입니다. 지원자는 위프 공개 캐릭터의 대화를 회차 단위로 받아 이 표를 만듭니다.
| turn_id | session_id | character_id | model_id | user_message | reply_text | first_person | honorific_level | named_entities | user_asked_recall |
|---|---|---|---|---|---|---|---|---|---|
| T-004450 | S-00912 | CHR-0001 | claude-sonnet | 다들 어디 갔어? | 하늘이는 아직 검술원에 남아 있어. 걱정 말고 자라. | 나 | 반말 | ["하늘","검술원"] | false |
| T-004468 | S-00912 | CHR-0001 | claude-sonnet | 오늘도 수련해? | 검을 잡아. 오늘은 손목부터 본다. | 나 | 반말 | [] | false |
| T-004471 | S-00912 | CHR-0001 | gpt-4o-mini | 요즘 어떻게 지내? | 저는 그렇게 생각해요. 오늘은 조금 더 쉬시는 게 좋겠어요. | 저 | 존댓말 | [] | false |
| T-004519 | S-00912 | CHR-0001 | gpt-4o-mini | 무슨 일이야? | 아카데미 학장 리안이 부르셔. 서둘러. | 나 | 반말 | ["리안","아카데미"] | false |
| T-004530 | S-00912 | CHR-0001 | whif-solar-ft | 바깥 소식 좀 들려줘. | 요즘 바깥 선거 얘기로 시끄럽더군. 다들 편을 갈라 싸운다. | 나 | 반말 | [] | false |
아래 JSON Schema 를 그대로 지키는 회차 줄을 40행 더 만듭니다. {"type":"object","properties":{"turn_id":{"type":"string"},"session_id":{"type":"string"},"character_id":{"type":"string"},"model_id":{"type":"string"},"user_message":{"type":["string","null"]},"reply_text":{"type":"string"},"first_person":{"type":["string","null"]},"honorific_level":{"type":["string","null"],"enum":["반말","존댓말",null]},"named_entities":{"type":["array","null"],"items":{"type":"string"}},"user_asked_recall":{"type":"boolean"}},"required":["turn_id","session_id","character_id","model_id","user_message","reply_text","user_asked_recall"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: named_entities 가 null 인 줄을 3~4행, 빈 배열인 줄을 여러 행 섞습니다. honorific_level 이 null 인 줄도 2행 두어 서버 후처리가 말투를 못 붙인 경우를 만들고, user_message 가 null 인 줄도 2행 두어 발화 수집이 늦은 경우를 만듭니다. model_id 는 claude-sonnet·gpt-4o-mini·whif-solar-ft 세 값을 쓰되 모델마다 카드 규칙을 어기는 비율이 다르게 흩뜨립니다. 한 session_id 안에서 turn_id 가 시간순으로 늘어나게 하고, user_asked_recall 이 참인 줄은 전체의 10% 안팎으로 둡니다. user_asked_recall 이 참인 줄의 user_message 는 앞 이야기를 되묻는 문장으로 쓰되, character_card.entity_roles 의 role 낱말을 담은 문장과 담지 않은 문장을 반반 섞습니다. reply_text 에는 topic_lexicon 의 소재어를 스치는 문장과 카드에 없는 고유명사를 넣은 문장을 각각 3~4행 넣고, 존댓말 종결 어미(요·죠·니다·십시오)로 끝나는 문장과 반말로 끝나는 문장이 한 회차 안에 섞인 줄도 2행 만듭니다. 형식을 깨뜨리는 시도 3건(named_entities 를 쉼표로 이은 문자열로 적기·user_asked_recall 을 'Y' 로 적기·turn_id 를 숫자로만 적기)은 데이터가 아니라 파서 시험용으로 따로 만듭니다.{"type":"object","properties":{"turn_id":{"type":"string"},"session_id":{"type":"string"},"character_id":{"type":"string"},"model_id":{"type":"string"},"user_message":{"type":["string","null"]},"reply_text":{"type":"string"},"first_person":{"type":["string","null"]},"honorific_level":{"type":["string","null"],"enum":["반말","존댓말",null]},"named_entities":{"type":["array","null"],"items":{"type":"string"}},"user_asked_recall":{"type":"boolean"}},"required":["turn_id","session_id","character_id","model_id","user_message","reply_text","user_asked_recall"]}크리에이터가 위프에 만들어 공개한 캐릭터 설정입니다. 말투와 1인칭, 금지 소재, 세계관에 등장해도 되는 이름 목록이 여기 있어 판정의 잣대가 되고, entity_roles 가 그 이름 하나하나를 캐릭터와 어떤 사이인지로 적어 두어 유저가 '동생' 처럼 관계로 물었을 때 어느 이름을 말하는지 잇습니다. 지원자는 공개 캐릭터의 설정 페이지를 카드 단위로 받아 이 표를 만듭니다.
| character_id | persona_summary | speech_style | honorific_rule | first_person | forbidden_topics | canon_entities | entity_roles |
|---|---|---|---|---|---|---|---|
| CHR-0001 | 검술원에서 후배를 가르치는 무뚝뚝한 교관 | 짧게 끊어 말하는 무뚝뚝체 | 반말 | 나 | ["현실 정치","유혈 묘사"] | ["아스텔","검술원","하늘","세라"] | [{"entity":"하늘","role":"동생"},{"entity":"세라","role":"제자"},{"entity":"검술원","role":"수련하는 곳"},{"entity":"아스텔","role":"고향"}] |
| CHR-0002 | 동네 카페를 지키는 다정한 바리스타 | 부드럽게 늘여 말하는 다정체 | 존댓말 | 저 | ["음주","도박"] | ["봄빛 카페","루카","은하"] | [{"entity":"루카","role":"단골"},{"entity":"은하","role":"여동생"},{"entity":"봄빛 카페","role":"일하는 곳"}] |
| CHR-0003 | 떠돌이 사냥꾼, 기억력이 좋다 | 느긋한 반말체 | 반말 | 나 | ["유혈 묘사"] | ["루미","북쪽 숲","덫골"] | [{"entity":"루미","role":"사냥개"},{"entity":"북쪽 숲","role":"사냥터"},{"entity":"덫골","role":"고향"}] |
| CHR-0004 | 연구실 막내를 챙기는 침착한 연구원 | 차분한 설명체 | 존댓말 | 저 | ["의료 조언","현실 정치"] | ["3연구동","차이","관측탑"] | [{"entity":"차이","role":"후배"},{"entity":"3연구동","role":"일하는 곳"},{"entity":"관측탑","role":"관측하는 곳"}] |
| CHR-0005 | 항구 도시를 도는 젊은 항해사 | 장난기 섞인 반말체 | 반말 | 내 | ["음주"] | ["항구","노을호","세이"] | [{"entity":"세이","role":"동료 선원"},{"entity":"노을호","role":"타는 배"},{"entity":"항구","role":"모항"}] |
아래 JSON Schema 를 그대로 지키는 캐릭터 카드를 20행 더 만듭니다. {"type":"object","properties":{"character_id":{"type":"string"},"persona_summary":{"type":"string"},"speech_style":{"type":"string"},"honorific_rule":{"type":"string","enum":["반말","존댓말"]},"first_person":{"type":"string"},"forbidden_topics":{"type":"array","items":{"type":"string"}},"canon_entities":{"type":"array","items":{"type":"string"}},"entity_roles":{"type":"array","items":{"type":"object","properties":{"entity":{"type":"string"},"role":{"type":"string"}},"required":["entity","role"]}}},"required":["character_id","persona_summary","speech_style","honorific_rule","first_person","forbidden_topics","canon_entities","entity_roles"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: honorific_rule 은 반말과 존댓말을 반반 섞고, first_person 은 나·저·내·제·본인 처럼 카드마다 다르게 씁니다. canon_entities 는 3~8개로 두되 인물 이름과 장소 이름을 섞고, 다른 카드와 겹치는 흔한 이름도 한둘 넣습니다. entity_roles 는 canon_entities 의 이름 가운데 3~6개에만 붙여 관계어가 안 달린 이름이 남게 하고, role 은 동생·제자·단골 처럼 유저가 실제로 부를 낱말로 적습니다. forbidden_topics 는 현실 정치·유혈 묘사·음주·도박·의료 조언 다섯 값에서 1~3개 고릅니다. speech_style 은 카드 문장 그대로 자유롭게 쓰되 honorific_rule 과 어긋나는 설명은 넣지 않습니다.{"type":"object","properties":{"character_id":{"type":"string"},"persona_summary":{"type":"string"},"speech_style":{"type":"string"},"honorific_rule":{"type":"string","enum":["반말","존댓말"]},"first_person":{"type":"string"},"forbidden_topics":{"type":"array","items":{"type":"string"}},"canon_entities":{"type":"array","items":{"type":"string"}},"entity_roles":{"type":"array","items":{"type":"object","properties":{"entity":{"type":"string"},"role":{"type":"string"}},"required":["entity","role"]}}},"required":["character_id","persona_summary","speech_style","honorific_rule","first_person","forbidden_topics","canon_entities","entity_roles"]}운영팀이 손으로 채우는 소재어 사전입니다. 답장 문장에 그대로 들어 있는지 볼 낱말과 그 낱말이 속한 금지 소재 이름을 짝지어 두어, 카드가 「현실 정치」를 막았을 때 답장의 「선거」가 거기 걸리는지를 규칙이 판정할 수 있게 합니다. 지원자는 카드가 쓰는 금지 소재 다섯 값마다 운영 기준에 적힌 낱말을 모아 이 표를 만듭니다.
| term | topic |
|---|---|
| 선거 | 현실 정치 |
| 투표 | 현실 정치 |
| 정당 | 현실 정치 |
| 대통령 | 현실 정치 |
| 국회 | 현실 정치 |
아래 JSON Schema 를 그대로 지키는 소재어 줄을 30행 더 만듭니다. {"type":"object","properties":{"term":{"type":"string"},"topic":{"type":"string","enum":["현실 정치","유혈 묘사","음주","도박","의료 조언"]}},"required":["term","topic"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: 다섯 topic 마다 최소 4개씩 낱말을 두고, 한 글자 낱말은 넣지 않습니다(다른 낱말 안에 우연히 들어가 오탐이 납니다 — 「술」은 「검술원」에 걸립니다). 두 topic 에 걸칠 법한 낱말(「한잔」·「마취」)은 어느 쪽으로 정했는지 하나만 골라 적어 표에 같은 term 이 두 번 오지 않게 합니다. 실제 대화에 자주 나오는 낱말과 거의 안 나오는 낱말을 함께 넣어 판정이 몰리지 않게 흩뜨립니다.{"type":"object","properties":{"term":{"type":"string"},"topic":{"type":"string","enum":["현실 정치","유혈 묘사","음주","도박","의료 조언"]}},"required":["term","topic"]}| Python | 판정 엔진의 언어입니다. 답장 문장에서 종결 어미로 말투를 뽑는 추출기와 카드 값 대조, 이름 집합 빼기, 소재어 사전 조회, 세션 안 앞 회차 되짚기가 여기 모여 있습니다. |
| FastAPI | 회차 로그를 받아 판정을 돌리고 결과를 내주는 비동기 API 서버입니다. 회차가 몰릴 때도 대조가 밀리지 않게 처리합니다. |
| TypeScript | 화면과 서버가 같은 판정 타입을 나눠 쓰도록 프런트 쪽 언어를 맞춥니다. |
| React | 크리에이터가 보는 판정판입니다. 왼쪽에 갈래를 세우고 회차를 누르면 카드 값과 답장 값이 오른쪽에 나란히 열립니다. |
| PostgreSQL | 회차 로그와 캐릭터 카드, 소재어 사전, 판정 이력을 담습니다. 카드가 바뀐 시점을 남겨 지난 회차를 그때 카드로 되짚습니다. |
| 여러 LLM 프로바이더 | 같은 카드로 답한 회차를 모델별로 갈라 세어 어느 모델이 말투를 더 자주 놓치는지 화면에 세웁니다. |
| 자체 모델 | 파인튜닝한 모델도 같은 판정을 그대로 통과시켜 프로바이더 모델과 한 줄에서 견줍니다. |
| 평가 파이프라인 | 크리에이터 표시 120회차를 정답으로 붙여 판정 일치를 세는 러너입니다. 판정 규칙을 고칠 때마다 다시 돌립니다. |
| GCP | 2주차부터 배포와 운영을 맡습니다. 회차 로그 원본과 판정 이력을 오브젝트 스토리지에 남깁니다. |
| 모니터링 | 판정이 몰리는 시간대와 실패한 대조를 지표로 남겨 규칙이 조용히 멈추는 일을 막습니다. |
| LLM | 판정이 정해진 뒤 어긋난 대목을 카드에 고쳐 적을 한 줄로 옮깁니다. 어느 갈래인지는 카드 대조가 정합니다. |
✓ pass → 그대로 멈춰도 완결입니다. 더 키우려면 크리에이터가 직접 표시한 회차와 맞대 보는 단계로 넘어갑니다.
✓ pass → 여기까지도 완결입니다. 더 키우려면 카드 수정 제안과 라우팅 후보를 세우는 단계로 확장합니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
verdict사유 라벨 · turn_id + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
설정대로 말함설정 유지말투 어긋남말투 어긋남세계관 밖 고유명사세계관 밖 이름금지 소재 접촉금지 소재앞 회차를 기억 못 함기억 끊김답장 한 회차를 다섯 갈래 중 하나로 판정하고 카드 값과 답장 값을 짝으로 남기는 형식입니다. 갈래는 규칙이 정하고 수정 제안 문장만 모델이 씁니다.
{"type":"object","properties":{"turn_id":{"type":"string"},"session_id":{"type":"string"},"character_id":{"type":"string"},"model_id":{"type":"string"},"verdict":{"type":"string","enum":["설정대로 말함","말투 어긋남","세계관 밖 고유명사","금지 소재 접촉","앞 회차를 기억 못 함"]},"rule_expected":{"type":["string","null"],"description":"카드가 정한 말투와 1인칭을 붙여 적은 값"},"rule_observed":{"type":["string","null"],"description":"reply_text 의 종결 어미에서 규칙이 직접 뽑은 말투와 로그의 1인칭을 붙여 적은 값"},"off_canon_entities":{"type":["array","null"],"items":{"type":"string"},"description":"named_entities 가운데 canon_entities 에 없는 이름"},"touched_term":{"type":["string","null"],"description":"reply_text 에 그대로 들어 있는 topic_lexicon.term"},"touched_topic":{"type":["string","null"],"description":"touched_term 이 속한 topic 가운데 그 카드의 forbidden_topics 와 겹친 값"},"recall_target":{"type":["string","null"],"description":"유저가 관계어로 물은 대상의 카드 이름 가운데 같은 session_id 앞 회차에 나왔는데 이번 답장에 없는 이름"},"recall_source_turn":{"type":["string","null"],"description":"recall_target 이 처음 나온 회차의 turn_id"},"evidence":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string","enum":["chat_turn","character_card","topic_lexicon"]},"field":{"type":"string"},"agg":{"type":["string","null"],"enum":["count","sum","avg","min","max",null]},"value":{"type":["string","number","boolean","null"]}},"required":["source","field","value"]}},"summary":{"type":"string"},"hedge":{"type":["string","null"]}},"required":["turn_id","session_id","character_id","model_id","verdict","evidence","summary"]}[
{
"turn_id": "T-004468",
"session_id": "S-00912",
"character_id": "CHR-0001",
"model_id": "claude-sonnet",
"verdict": "설정대로 말함",
"rule_expected": "반말 · 나",
"rule_observed": "반말 · 나",
"off_canon_entities": [],
"touched_term": null,
"touched_topic": null,
"recall_target": null,
"recall_source_turn": null,
"evidence": [
{
"source": "chat_turn",
"field": "reply_text",
"value": "검을 잡아. 오늘은 손목부터 본다."
},
{
"source": "character_card",
"field": "honorific_rule",
"value": "반말"
},
{
"source": "chat_turn",
"field": "first_person",
"value": "나"
},
{
"source": "character_card",
"field": "first_person",
"value": "나"
}
],
"summary": "답장 두 문장이 모두 반말로 끝나고 1인칭도 카드 그대로여서 카드 밖 이름 없이 넘어갔습니다.",
"hedge": null
},
{
"turn_id": "T-004471",
"session_id": "S-00912",
"character_id": "CHR-0001",
"model_id": "gpt-4o-mini",
"verdict": "말투 어긋남",
"rule_expected": "반말 · 나",
"rule_observed": "존댓말 · 저",
"off_canon_entities": [],
"touched_term": null,
"touched_topic": null,
"recall_target": null,
"recall_source_turn": null,
"evidence": [
{
"source": "chat_turn",
"field": "reply_text",
"value": "저는 그렇게 생각해요. 오늘은 조금 더 쉬시는 게 좋겠어요."
},
{
"source": "character_card",
"field": "honorific_rule",
"value": "반말"
},
{
"source": "chat_turn",
"field": "first_person",
"value": "저"
},
{
"source": "character_card",
"field": "first_person",
"value": "나"
}
],
"summary": "무뚝뚝한 반말 교관이 「생각해요」·「좋겠어요」로 답하고 1인칭도 저를 써 카드 규칙 두 가지를 한꺼번에 벗어났습니다.",
"hedge": null
},
{
"turn_id": "T-004519",
"session_id": "S-00912",
"character_id": "CHR-0001",
"model_id": "gpt-4o-mini",
"verdict": "세계관 밖 고유명사",
"rule_expected": "반말 · 나",
"rule_observed": "반말 · 나",
"off_canon_entities": [
"리안",
"아카데미"
],
"touched_term": null,
"touched_topic": null,
"recall_target": null,
"recall_source_turn": null,
"evidence": [
{
"source": "chat_turn",
"field": "reply_text",
"value": "아카데미 학장 리안이 부르셔. 서둘러."
},
{
"source": "character_card",
"field": "canon_entities",
"value": "검술원"
},
{
"source": "chat_turn",
"field": "turn_id",
"value": "T-004519"
}
],
"summary": "카드에 없는 학장 리안과 아카데미가 답장에 등장해 세계관이 한 회차 만에 늘어났습니다.",
"hedge": null
},
{
"turn_id": "T-004530",
"session_id": "S-00912",
"character_id": "CHR-0001",
"model_id": "whif-solar-ft",
"verdict": "금지 소재 접촉",
"rule_expected": "반말 · 나",
"rule_observed": "반말 · 나",
"off_canon_entities": [],
"touched_term": "선거",
"touched_topic": "현실 정치",
"recall_target": null,
"recall_source_turn": null,
"evidence": [
{
"source": "chat_turn",
"field": "reply_text",
"value": "요즘 바깥 선거 얘기로 시끄럽더군. 다들 편을 갈라 싸운다."
},
{
"source": "topic_lexicon",
"field": "term",
"value": "선거"
},
{
"source": "topic_lexicon",
"field": "topic",
"value": "현실 정치"
},
{
"source": "character_card",
"field": "forbidden_topics",
"value": "현실 정치"
}
],
"summary": "사전이 현실 정치로 묶어 둔 「선거」가 답장에 그대로 나와 카드가 막은 소재에 닿았습니다.",
"hedge": null
},
{
"turn_id": "T-004602",
"session_id": "S-00912",
"character_id": "CHR-0001",
"model_id": "whif-solar-ft",
"verdict": "앞 회차를 기억 못 함",
"rule_expected": "반말 · 나",
"rule_observed": "반말 · 나",
"off_canon_entities": [],
"touched_term": null,
"touched_topic": null,
"recall_target": "하늘",
"recall_source_turn": "T-004450",
"evidence": [
{
"source": "chat_turn",
"field": "user_asked_recall",
"value": true
},
{
"source": "chat_turn",
"field": "user_message",
"value": "내 동생 이름 기억나?"
},
{
"source": "character_card",
"field": "entity_roles",
"value": "동생"
},
{
"source": "character_card",
"field": "canon_entities",
"value": "하늘"
},
{
"source": "chat_turn",
"field": "turn_id",
"value": "T-004450"
},
{
"source": "chat_turn",
"field": "reply_text",
"value": "하늘이는 아직 검술원에 남아 있어. 걱정 말고 자라."
},
{
"source": "chat_turn",
"field": "reply_text",
"value": "동생? 글쎄, 그런 얘길 들은 적 있던가."
}
],
"summary": "유저가 동생을 물었고 카드는 동생을 하늘로 적어 두었는데, 앞선 회차에 나온 하늘이 이번 답장에는 없습니다.",
"hedge": null
},
{
"turn_id": "T-004611",
"session_id": "S-00913",
"character_id": "CHR-0002",
"model_id": "gpt-4o-mini",
"verdict": "말투 어긋남",
"rule_expected": "존댓말 · 저",
"rule_observed": "반말 · 나",
"off_canon_entities": [],
"touched_term": null,
"touched_topic": null,
"recall_target": null,
"recall_source_turn": null,
"evidence": [
{
"source": "chat_turn",
"field": "reply_text",
"value": "야, 그냥 오면 되잖아. 뭘 그렇게 따져."
},
{
"source": "character_card",
"field": "honorific_rule",
"value": "존댓말"
},
{
"source": "chat_turn",
"field": "first_person",
"value": "나"
},
{
"source": "character_card",
"field": "first_person",
"value": "저"
}
],
"summary": "다정한 존댓말 바리스타가 반말로 툭 끊어 답해 같은 세션 안에서 사람이 바뀐 것처럼 읽힙니다.",
"hedge": null
},
{
"turn_id": "T-004631",
"session_id": "S-00915",
"character_id": "CHR-0004",
"model_id": "claude-sonnet",
"verdict": "설정대로 말함",
"rule_expected": "존댓말 · 저",
"rule_observed": "존댓말 · 저",
"off_canon_entities": null,
"touched_term": null,
"touched_topic": null,
"recall_target": null,
"recall_source_turn": null,
"evidence": [
{
"source": "chat_turn",
"field": "reply_text",
"value": "오늘 실험은 여기까지 하죠. 정리는 제가 합니다."
},
{
"source": "character_card",
"field": "honorific_rule",
"value": "존댓말"
},
{
"source": "chat_turn",
"field": "first_person",
"value": "저"
},
{
"source": "chat_turn",
"field": "named_entities",
"value": null
}
],
"summary": "두 문장이 모두 존댓말로 끝나고 1인칭도 카드 그대로입니다.",
"hedge": "등장 이름 목록이 비어 있어 세계관 대조는 건너뛰고 말투와 금지 소재만 맞춰 봤습니다."
}
][자동 검증 · 코드가 판정합니다] 1. verdict 가 다섯 값 가운데 하나인지 확인합니다. 판정은 규칙이 정하므로 모델이 새 값을 적으면 그 응답을 버립니다. 2. turn_id·session_id·character_id·model_id·verdict·evidence·summary 일곱 칸이 모두 차 있는지 봅니다. 3. 말투는 로그 값을 믿지 않고 reply_text 에서 다시 뽑습니다. 답장을 마침표·물음표·느낌표로 잘라 문장으로 나누고, 마지막 어절이 존댓말 종결 어미 넷(요·죠·니다·십시오) 가운데 하나로 끝나는 문장이 하나라도 있으면 존댓말, 하나도 없으면 반말입니다. rule_observed 는 그렇게 뽑은 말투와 로그의 first_person 을 「말투 · 1인칭」 으로 붙인 값이고, rule_expected 는 카드의 honorific_rule 과 first_person 을 같은 모양으로 붙인 값입니다. 둘이 다를 때만 말투 어긋남이 붙었는지 맞춰 봅니다. 로그의 honorific_level 은 채팅 서버가 붙여 둔 참고값이라 판정에 쓰지 않고, 뽑은 값과 다른 회차는 hedge 에 그 사실만 적습니다. 4. off_canon_entities 를 named_entities 에서 canon_entities 를 뺀 값으로 다시 계산해 같은 목록인지 봅니다. 5. touched_term 을 topic_lexicon.term 가운데 reply_text 에 문자열 그대로 들어 있는 값으로 다시 찾고, 그 term 의 topic 이 그 캐릭터 카드의 forbidden_topics 안에 있을 때만 touched_topic 에 적혔는지 봅니다. 걸린 term 이 여럿이면 topic_lexicon 에 실린 차례가 앞선 것 하나를 씁니다. 6. recall_target 은 user_asked_recall 이 참인 회차에서만 계산합니다. 그 캐릭터 카드의 entity_roles 가운데 role 이 user_message 에 문자열 그대로 들어 있는 줄을 찾아 그 줄의 entity 를 유저가 물은 대상으로 잡습니다. role 이 하나도 안 걸리거나 user_message 가 null 이면 recall_target·recall_source_turn 은 둘 다 null 이고 기억 판정도 붙지 않습니다. 잡힌 entity 가 같은 session_id 의 앞선 회차 named_entities 에 있고 이번 회차 reply_text 에는 없으면 recall_target 이 되며, recall_source_turn 은 그 이름이 나온 앞 회차 가운데 turn_id 가 가장 이른 회차입니다. 7. named_entities 가 null 이면 off_canon_entities 도 null 이고 세계관 대조를 건너뜁니다. first_person 이 null 이면 1인칭 비교를 건너뛰고 말투만 견주며 그 사실을 hedge 에 적습니다. 8. evidence 의 field 이름이 세 표의 컬럼에 실제로 있는 이름인지 대조합니다. 없는 이름을 인용하면 요약이 지어낸 문장이라는 뜻이라 규칙이 써 둔 기본 문장으로 되돌립니다. 9. evidence 의 value 가 인용한 그 줄의 값과 글자 그대로 같은지 봅니다. 10. summary 가 존댓말 한 문장으로 끝나는지 봅니다. [판정 순서 · 규칙이 지키는 차례] 금지 소재 접촉을 가장 먼저 보고, 그다음 세계관 밖 고유명사, 말투 어긋남, 앞 회차를 기억 못 함 차례로 봅니다. 앞에서 하나가 걸리면 뒤는 보지 않고, 하나도 걸리지 않으면 설정대로 말함입니다. 유저가 앞 이야기를 묻지 않은 회차에서는 기억 판정을 아예 돌리지 않습니다. [평가 러너 · 2주차] 1. 크리에이터가 직접 읽고 표시한 회차 120건을 정답으로 붙여 판정이 같은지 세고, 갈라진 회차를 사유별로 모읍니다. 2. 같은 프롬프트를 세 모델에 나눠 돌린 회차만 골라 모델별 어긋남 비율을 견줍니다. [수동 검증 · 사람이 봅니다] 1. 금지 소재에 닿았다는 판정이 실제로 그 캐릭터에게 문제인지 표본 20건을 크리에이터가 읽습니다. 2. 세계관 밖 이름이 실은 크리에이터가 새로 늘리고 싶은 설정인지 가려, 카드에 더할 이름과 막을 이름을 나눕니다. 3. 종결 어미 넷으로 가른 말투가 실제 답장의 인상과 맞는지, 반말과 존댓말이 한 회차에 섞인 표본 20건을 읽고 어미 목록에 더할 말을 정합니다.