언엑스
경력근거: 쏟아지는 채팅 중 무엇에 반응할지 고르는 선별 로직
※ 실제 산출물이 아닌 AI 예측 시안입니다.
언엑스의 캐릭터는 미리 써 둔 서사를 읽지 않고 지금 이 순간 채팅을 읽어 반응하며 팬과 관계를 쌓습니다. 그래서 무엇에 답하느냐가 세계관을 지키는 일이자 관계를 쌓는 일이고, 공고도 선별 로직과 관계 상태 머신을 라이브 엔진의 첫 줄에 나란히 적었습니다. 캐릭터 설정표와 시청자별 친밀 단계가 있어야 성립하는 판정이라 다른 회사 데이터로는 이 사슬이 만들어지지 않습니다. 다만 이 포트폴리오는 실시간 판정 규칙과 운영 화면을 보여 줍니다. 서브컬처 팬덤 운영이나 캐릭터 IP 기획의 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
주요업무 맨 앞이 실시간 라이브 엔진이고, 그 안에 캐릭터가 언제 말할지 정하는 루프와 쏟아지는 채팅 중 무엇에 반응할지 고르는 선별 로직이 나란히 있습니다. 관계와 전개 상태를 코드가 소유한다는 말까지 붙어 있어, 무엇을 고르느냐가 이 자리의 일 자체입니다.
공고는 만드는 제품을 팬 채널·운영 콘솔·콘텐츠 스튜디오 셋으로 적고, 운영 콘솔을 방송을 준비하고 진행 중 개입하고 결과를 판독하는 어드민이라고 풀어 씁니다. 진행 중 개입이 무엇인지 화면으로 보여 줄 자리가 여기입니다.
우대사항이 Next.js·React 로 실제 서비스를 만들어본 경험을 제품 세 개가 전부 웹이라는 이유로 요구하고, 실시간 시스템은 WebSocket·스트리밍·이벤트 파이프라인으로 못박습니다. 백엔드만 내거나 화면만 내면 이 자리의 답이 되지 않습니다.
방송이 도는 동안 채팅 한 줄마다 그 방송 캐릭터의 설정표와 시청자 관계 기록을 맞춰, 30초 구간마다 답변할 질문 세 개를 세우고 나머지는 왜 빠졌는지 사유별로 보여 줍니다. 모델은 채팅 본문만 보고 주제 태그를 뽑는 앞단을 맡고, 뽑은 태그가 그 캐릭터의 주제 어휘 밖이면 그 응답을 버립니다. 태그가 정해진 뒤의 금칙 주제 겹침·쿨다운 경과·친밀 단계 미달·구간 순위는 규칙이 계산합니다.
AI 캐릭터 라이브 방송을 진행 중인 운영자 — 방송 한 판에 채팅 수천 개가 흐르는데 어느 질문에 캐릭터가 답하게 할지 지금 이 순간 정해야 하고, 근거로 삼을 게 화면을 스쳐 지나가는 채팅창뿐이다
답변할 질문을 못 고르는 이유는 채팅이 많아서가 아니라 캐릭터 설정표와 시청자별 관계 기록이 채팅창과 따로 놓여 있기 때문입니다. 셋을 한 판정으로 묶으면 운영자는 걸러진 이유가 붙은 목록에서 위 세 줄만 보고 넘어갑니다.
운영자가 방송 화면을 열면 30초 구간마다 답변할 질문 세 개가 서고, 빠진 줄은 세계관 밖·방금 답한 주제·관계 단계 미달·다음 차례 넷으로 나뉘어 근거와 함께 뜹니다. 채팅은 미리 만든 로그를 재생해 넣고, 실제 방송 소켓 연결은 2주차로 미룹니다.
WebSocket 으로 들어오는 채팅을 그대로 판정에 태워 지연을 재고, 운영자가 손으로 고른 60건과 판정을 맞춰 어디서 갈라지는지 리포트로 냅니다.
한 방송에서 답한 주제와 넘긴 주제를 모아 캐릭터 설정표의 쿨다운과 친밀 기준을 고치자는 제안을 만들고, 운영자가 눌러 반영하면 다음 방송이 그 값으로 돕니다.
운영자가 재생한 채팅 로그를 틀면 30초 구간마다 답변할 질문 세 개가 서고, 빠진 줄은 사유와 근거를 달고 옆에 남습니다.
주제 태그가 결측인 줄, 후원이 0원인 줄, 처음 들어와 관계 기록이 없는 시청자를 섞은 채팅 30종을 넣어도 화면이 죽지 않고 어느 줄이 왜 걸렸는지 그 자리에서 읽힙니다.
운영자가 손으로 고른 60건과 판정을 맞춰 일치율을 냅니다. 단, 정답과 채팅을 모두 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
채팅이 초당 40줄로 들어와도 구간이 닫힌 뒤 판정이 끝나는 시간을 재서 화면에 같이 띄웁니다.
위 수치는 더미데이터 자가채점이며 실제 선별 품질은 언엑스의 실제 방송 채팅과 팬 반응이 있어야 측정됩니다. 이 포트폴리오는 그 검증을 바로 돌릴 수 있는 상태로 만듭니다.
화면에 뜨는 일치율은 제가 만든 채팅 로그를 제가 매긴 정답과 맞춰 본 값이라 성능을 증명하지 못합니다. 실제 방송은 한 구간에 수백 줄이 몰리고 같은 질문이 되풀이되므로, 면접에서는 이 수치 대신 데모가 도는 모습과 실제 방송 로그를 받았을 때 무엇부터 재는지를 말씀드립니다.
정답을 작성자가 만들었으니 잘 맞는 것이 당연합니다. 결측 태그와 후원 0원과 관계 기록이 빈 시청자를 더미에 일부러 섞어 쉬운 문제만 푸는 함정을 피합니다.
후원이 크다고 그 질문이 방송에 좋은 질문이라는 뜻은 아닙니다. 순위는 참고 자리이고 최종 선택은 운영자가 하도록 후보 세 개를 모두 근거와 함께 남깁니다.
친밀 단계는 회사마다 다르게 쌓는 값이라 실제 도입에서는 기준 숫자가 통째로 달라집니다. 그 값을 코드에서 빼 설정표로 두고, 조정은 도입 단계 과제로 분리합니다.
이 포트폴리오는 판정 규칙과 운영 화면을 보여 줍니다. 캐릭터 목소리 합성과 모션 스트리밍은 범위 밖이라 손대지 않고, 판정 결과를 넘기는 자리까지만 다룹니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
방송 한 판에 흐른 채팅 줄입니다. 판정의 대상이며 한 줄이 어느 캐릭터의 방송인지 character_id 로 붙습니다. topic_tags 는 수집기가 나중에 붙여 둔 정답 태그입니다 — 판정을 돌릴 때는 모델이 body 만 보고 태그를 뽑아 쓰고, 이 컬럼은 그 태그가 맞았는지 재고 판정을 다시 계산해 대조하는 데 씁니다. 태그가 결측인 줄과 후원이 0원인 줄을 일부러 담았고, note 에는 그 줄의 사정만 적고 판정에 해당하는 말은 담지 않습니다.
| msg_id | character_id | viewer_id | sent_at | body | topic_tags | donation_amount | is_question | note |
|---|---|---|---|---|---|---|---|---|
| MSG-20260908-1401 | CH-0007 | VW-1043 | 2026-09-08T21:14:03 | 오늘 목소리 왜 이렇게 좋아? | ["노래"] | 5,000 | true | — |
| MSG-20260908-1402 | CH-0007 | VW-2213 | 2026-09-08T21:14:05 | 너 실제로는 GPT지? | ["메타발언"] | 0 | true | — |
| MSG-20260908-1403 | CH-0007 | VW-1180 | 2026-09-08T21:14:09 | 좋아하는 색 뭐야? | ["패션"] | 1,000 | true | — |
| MSG-20260908-1404 | CH-0007 | VW-3320 | 2026-09-08T21:14:12 | 우리 언제 처음 만났더라 | ["팬과의추억"] | 2,000 | true | — |
| MSG-20260908-1405 | CH-0007 | VW-1077 | 2026-09-08T21:14:15 | 다음 곡 뭐야? | ["노래"] | 3,000 | true | — |
아래 JSON Schema 에 맞는 라이브 채팅 줄을 더 만들어 주세요. {"type":"object","properties":{"msg_id":{"type":"string"},"character_id":{"type":"string"},"viewer_id":{"type":"string"},"sent_at":{"type":"string"},"body":{"type":"string"},"topic_tags":{"type":["array","null"],"items":{"type":"string"}},"donation_amount":{"type":"integer","minimum":0},"is_question":{"type":"boolean"},"note":{"type":["string","null"]}},"required":["msg_id","character_id","viewer_id","sent_at","body","donation_amount","is_question"]} 30초 구간이 여럿 나오도록 sent_at 을 흩어 놓고, 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) topic_tags 가 null 인 줄, (2) donation_amount 가 0 인 줄과 10만원대 최고 후원 줄, (3) is_question 이 거짓인 감탄·이모티콘 줄, (4) 캐릭터 설정표의 금칙 주제 태그가 붙은 줄. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요. character_id 는 캐릭터 설정표에 있는 번호에서 고르고, 같은 30초 구간 안의 줄은 모두 같은 character_id 로 둡니다(한 구간은 한 방송입니다). 방송이 셋 이상 섞이게 흩어 주세요.{"type":"object","properties":{"msg_id":{"type":"string"},"character_id":{"type":"string"},"viewer_id":{"type":"string"},"sent_at":{"type":"string"},"body":{"type":"string"},"topic_tags":{"type":["array","null"],"items":{"type":"string"}},"donation_amount":{"type":"integer","minimum":0},"is_question":{"type":"boolean"},"note":{"type":["string","null"]}},"required":["msg_id","character_id","viewer_id","sent_at","body","donation_amount","is_question"]}캐릭터마다 세계관 주제와 금칙 주제, 주제별 친밀 기준, 답변 쿨다운을 적어 둔 설정표입니다. 판정의 기준이 되는 표라 판정 결과를 담지 않습니다.
| character_id | character_name | world_topic_tags | banned_tags | min_closeness_by_topic | reply_cooldown_sec | note |
|---|---|---|---|---|---|---|
| CH-0007 | 미아 | ["학교생활","노래","팬과의추억","게임","패션"] | ["메타발언","정치","현실신상"] | {"학교생활":1,"노래":1,"게임":1,"패션":2,"팬과의추억":3} | 180 | AISA 고등학교 스트리밍부 소속 |
| CH-0008 | 하루 | ["학교생활","게임","간식"] | ["메타발언","정치","현실신상","연애"] | {"학교생활":1,"게임":1,"간식":2} | 240 | — |
| CH-0011 | 사주오빠 | ["사주","고민상담","운세"] | ["메타발언","의료","투자권유"] | {"사주":1,"운세":1,"고민상담":4} | 300 | 상담 주제라 쿨다운이 깁니다 |
| CH-0012 | 노아 | ["노래","작곡","팬과의추억"] | ["메타발언","정치"] | {"노래":1,"작곡":2,"팬과의추억":3} | 180 | — |
| CH-0015 | 린 | ["패션","메이크업","일상"] | ["메타발언","현실신상","다이어트"] | {"패션":1,"메이크업":2,"일상":1} | 120 | — |
아래 JSON Schema 에 맞는 캐릭터 설정표 행을 더 만들어 주세요. {"type":"object","properties":{"character_id":{"type":"string"},"character_name":{"type":"string"},"world_topic_tags":{"type":"array","items":{"type":"string"}},"banned_tags":{"type":"array","items":{"type":"string"}},"min_closeness_by_topic":{"type":"object"},"reply_cooldown_sec":{"type":"integer","minimum":0,"maximum":900},"note":{"type":["string","null"]}},"required":["character_id","character_name","world_topic_tags","banned_tags","min_closeness_by_topic","reply_cooldown_sec"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) reply_cooldown_sec 가 0 인 캐릭터, (2) 세계관 주제가 셋뿐인 좁은 캐릭터, (3) min_closeness_by_topic 의 기준이 4 이상으로 높은 주제, (4) 금칙 주제가 여섯 개 넘게 붙은 캐릭터. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"character_id":{"type":"string"},"character_name":{"type":"string"},"world_topic_tags":{"type":"array","items":{"type":"string"}},"banned_tags":{"type":"array","items":{"type":"string"}},"min_closeness_by_topic":{"type":"object"},"reply_cooldown_sec":{"type":"integer","minimum":0,"maximum":900},"note":{"type":["string","null"]}},"required":["character_id","character_name","world_topic_tags","banned_tags","min_closeness_by_topic","reply_cooldown_sec"]}시청자별 친밀 단계와 지금까지 답을 받은 주제, 마지막으로 답을 받은 시각입니다. 처음 들어와 기록이 결측인 시청자를 담았습니다.
| viewer_id | nickname | closeness_level | answered_topics | last_replied_at | note |
|---|---|---|---|---|---|
| VW-1043 | 미르냥 | 4 | ["노래","학교생활"] | 2026-09-08T21:09:40 | — |
| VW-2213 | 익명2213 | 0 | [] | — | 오늘 처음 들어온 시청자 |
| VW-1180 | 루루 | 4 | ["패션"] | 2026-09-08T21:13:31 | — |
| VW-3320 | 신입팬 | 1 | [] | — | — |
| VW-1077 | 하늘 | 3 | ["게임"] | 2026-09-08T20:58:12 | — |
아래 JSON Schema 에 맞는 시청자 관계 기록을 더 만들어 주세요. {"type":"object","properties":{"viewer_id":{"type":"string"},"nickname":{"type":"string"},"closeness_level":{"type":"integer","minimum":0,"maximum":5},"answered_topics":{"type":"array","items":{"type":"string"}},"last_replied_at":{"type":["string","null"]},"note":{"type":["string","null"]}},"required":["viewer_id","nickname","closeness_level","answered_topics"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) last_replied_at 이 null 인 처음 온 시청자, (2) closeness_level 이 0 과 5 인 양 끝 시청자, (3) answered_topics 가 빈 배열인 시청자, (4) 방금 답을 받아 last_replied_at 이 1분 안인 시청자. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"viewer_id":{"type":"string"},"nickname":{"type":"string"},"closeness_level":{"type":"integer","minimum":0,"maximum":5},"answered_topics":{"type":"array","items":{"type":"string"}},"last_replied_at":{"type":["string","null"]},"note":{"type":["string","null"]}},"required":["viewer_id","nickname","closeness_level","answered_topics"]}| TypeScript | 판정 규칙과 화면을 같은 언어로 씁니다. 채팅 한 줄이 지나는 타입을 한 곳에 두어 규칙과 화면이 어긋나지 않게 합니다. |
| Next.js | 운영자가 보는 방송 화면입니다. 구간별 답변 후보와 사유별 목록을 한 화면에 둡니다. |
| React | 채팅이 흐르는 목록과 후보 카드를 다시 그립니다. 30초 구간이 넘어갈 때 목록이 통째로 흔들리지 않게 키를 구간으로 잡습니다. |
| WebSocket | 채팅을 받아 판정으로 넘기고 판정 결과를 화면으로 되돌립니다. 끊기면 마지막 구간부터 다시 받습니다. |
| Node.js | 선별 규칙이 도는 자리입니다. 구간 버퍼를 메모리에 들고 30초마다 순위를 세웁니다. |
| PostgreSQL | 캐릭터 설정표와 시청자 관계 기록을 담습니다. 방송이 끝나면 답한 주제가 여기로 쌓입니다. |
| LLM | 채팅 한 줄의 본문만 보고 주제 태그를 뽑습니다. 뽑은 태그는 그 캐릭터의 주제 어휘 안인지 검사한 뒤 쓰이고, 태그가 정해진 뒤의 판정에는 끼어들지 않습니다. |
| Docker | 채팅 재생기와 판정 서버를 같이 띄워 방송 상황을 되풀이해 돌려 봅니다. |
✓ pass → 그대로 멈춰도 완결입니다. 더 키우려면 실제 소켓으로 채팅을 받아 판정 지연을 재는 단계로 넘어갑니다.
✓ pass → 여기까지도 완결입니다. 더 키우려면 방송 결과를 캐릭터 설정표 수정 제안으로 되돌리는 단계로 확장합니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
verdict사유 라벨 · msg_id + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
지금 답변할 질문지금 답변세계관 밖이라 넘김세계관 밖방금 답한 주제방금 답한 주제관계 단계에 아직 못 미침관계 단계 미달다음 차례로 미룸다음 차례채팅 한 줄에 대한 판정을 사유와 근거로 남기는 형식입니다. 사유는 규칙이 정하고 요약 문장만 모델이 씁니다.
{"type":"object","properties":{"pick_id":{"type":"string"},"msg_id":{"type":"string"},"viewer_id":{"type":"string"},"character_id":{"type":"string"},"window_start":{"type":"string"},"verdict":{"type":"string","enum":["지금 답변할 질문","세계관 밖이라 넘김","방금 답한 주제","관계 단계에 아직 못 미침","다음 차례로 미룸"]},"rank_in_window":{"type":["integer","null"],"minimum":1,"description":"같은 30초 구간에서 걸러지지 않고 남은 채팅을 donation_amount 큰 순, 같으면 is_question 이 참인 것 먼저, 그다음 sent_at 이른 순으로 세운 자리입니다"},"cooldown_elapsed_sec":{"type":["integer","null"],"minimum":0,"description":"live_chat.sent_at 에서 viewer_relation.last_replied_at 을 뺀 초입니다. last_replied_at 이 비어 있으면 비웁니다"},"evidence":{"type":"array","items":{"type":"object","properties":{"dataset":{"type":"string"},"field":{"type":"string"},"agg":{"type":["string","null"]},"value":{"type":["string","number","boolean","null"]}},"required":["dataset","field","value"]}},"summary":{"type":"string"},"hedge":{"type":["string","null"]}},"required":["pick_id","msg_id","verdict","evidence","summary"]}[
{
"pick_id": "PICK-1401",
"msg_id": "MSG-20260908-1401",
"viewer_id": "VW-1043",
"character_id": "CH-0007",
"window_start": "2026-09-08T21:14:00",
"verdict": "지금 답변할 질문",
"rank_in_window": 1,
"cooldown_elapsed_sec": 263,
"evidence": [
{
"dataset": "live_chat",
"field": "donation_amount",
"value": 5000
},
{
"dataset": "live_chat",
"field": "is_question",
"value": true
},
{
"dataset": "viewer_relation",
"field": "closeness_level",
"value": 4
},
{
"dataset": "viewer_relation",
"field": "last_replied_at",
"value": "2026-09-08T21:09:40"
},
{
"dataset": "live_chat",
"field": "sent_at",
"value": "2026-09-08T21:14:03"
},
{
"dataset": "character_sheet",
"field": "reply_cooldown_sec",
"value": 180
}
],
"summary": "미르냥이 5,000원을 얹어 물은 노래 이야기이고 지난 답변에서 263초가 지나 쿨다운 180초를 넘었습니다. 이 구간에서 가장 앞자리라 지금 답변할 질문으로 세웁니다.",
"hedge": null
},
{
"pick_id": "PICK-1405",
"msg_id": "MSG-20260908-1405",
"viewer_id": "VW-1077",
"character_id": "CH-0007",
"window_start": "2026-09-08T21:14:00",
"verdict": "지금 답변할 질문",
"rank_in_window": 2,
"cooldown_elapsed_sec": 963,
"evidence": [
{
"dataset": "live_chat",
"field": "donation_amount",
"value": 3000
},
{
"dataset": "live_chat",
"field": "body",
"value": "다음 곡 뭐야?"
},
{
"dataset": "viewer_relation",
"field": "closeness_level",
"value": 3
},
{
"dataset": "viewer_relation",
"field": "answered_topics",
"value": "게임"
},
{
"dataset": "character_sheet",
"field": "reply_cooldown_sec",
"value": 180
}
],
"summary": "하늘이 3,000원과 함께 다음 곡을 물었고 노래는 앞서 답한 주제가 아닙니다. 후원 순으로 두 번째 자리라 미르냥 다음으로 답변할 질문이 됩니다.",
"hedge": null
},
{
"pick_id": "PICK-1406",
"msg_id": "MSG-20260908-1406",
"viewer_id": "VW-1298",
"character_id": "CH-0007",
"window_start": "2026-09-08T21:14:00",
"verdict": "지금 답변할 질문",
"rank_in_window": 3,
"cooldown_elapsed_sec": 694,
"evidence": [
{
"dataset": "live_chat",
"field": "donation_amount",
"value": 0
},
{
"dataset": "live_chat",
"field": "is_question",
"value": true
},
{
"dataset": "viewer_relation",
"field": "closeness_level",
"value": 2
},
{
"dataset": "viewer_relation",
"field": "last_replied_at",
"value": "2026-09-08T21:02:44"
},
{
"dataset": "live_chat",
"field": "sent_at",
"value": "2026-09-08T21:14:18"
},
{
"dataset": "character_sheet",
"field": "reply_cooldown_sec",
"value": 180
}
],
"summary": "별똥이 시험 이야기를 물었고 학교생활은 미아의 세계관 주제입니다. 같은 시청자에게 답한 지 694초가 지나 쿨다운 180초를 넘겼고 이 구간에서 세 번째 자리라 세 번째 답변 후보로 세웁니다.",
"hedge": null
},
{
"pick_id": "PICK-1402",
"msg_id": "MSG-20260908-1402",
"viewer_id": "VW-2213",
"character_id": "CH-0007",
"window_start": "2026-09-08T21:14:00",
"verdict": "세계관 밖이라 넘김",
"rank_in_window": null,
"cooldown_elapsed_sec": null,
"evidence": [
{
"dataset": "live_chat",
"field": "body",
"value": "너 실제로는 GPT지?"
},
{
"dataset": "character_sheet",
"field": "banned_tags",
"value": "메타발언"
},
{
"dataset": "viewer_relation",
"field": "last_replied_at",
"value": null
}
],
"summary": "미아의 금칙 주제인 메타발언이 걸려 캐릭터가 자기 정체를 말하게 되는 질문입니다. 답변 후보에서 빼고 운영자에게만 보여 줍니다.",
"hedge": null
},
{
"pick_id": "PICK-1403",
"msg_id": "MSG-20260908-1403",
"viewer_id": "VW-1180",
"character_id": "CH-0007",
"window_start": "2026-09-08T21:14:00",
"verdict": "방금 답한 주제",
"rank_in_window": null,
"cooldown_elapsed_sec": 38,
"evidence": [
{
"dataset": "live_chat",
"field": "sent_at",
"value": "2026-09-08T21:14:09"
},
{
"dataset": "viewer_relation",
"field": "last_replied_at",
"value": "2026-09-08T21:13:31"
},
{
"dataset": "viewer_relation",
"field": "answered_topics",
"value": "패션"
},
{
"dataset": "character_sheet",
"field": "reply_cooldown_sec",
"value": 180
}
],
"summary": "루루에게 패션 이야기를 답한 지 38초밖에 지나지 않아 쿨다운 180초 안입니다. 같은 사람에게 같은 주제를 잇달아 답하면 대화가 겹쳐 보여 미뤄 둡니다.",
"hedge": null
},
{
"pick_id": "PICK-1404",
"msg_id": "MSG-20260908-1404",
"viewer_id": "VW-3320",
"character_id": "CH-0007",
"window_start": "2026-09-08T21:14:00",
"verdict": "관계 단계에 아직 못 미침",
"rank_in_window": null,
"cooldown_elapsed_sec": null,
"evidence": [
{
"dataset": "live_chat",
"field": "body",
"value": "우리 언제 처음 만났더라"
},
{
"dataset": "viewer_relation",
"field": "closeness_level",
"value": 1
},
{
"dataset": "character_sheet",
"field": "min_closeness_by_topic",
"value": 3
},
{
"dataset": "viewer_relation",
"field": "last_replied_at",
"value": null
}
],
"summary": "신입팬이 첫 만남을 물었는데 추억 주제는 친밀 3단계부터이고 이 시청자는 1단계입니다. 캐릭터가 없는 기억을 지어내지 않도록 답변 후보에서 뺍니다.",
"hedge": "친밀 단계는 시청 시간과 후원으로 오르는 값이라 실제 서비스에서는 기준을 다시 잡아야 합니다."
},
{
"pick_id": "PICK-1409",
"msg_id": "MSG-20260908-1409",
"viewer_id": "VW-1901",
"character_id": "CH-0007",
"window_start": "2026-09-08T21:14:00",
"verdict": "세계관 밖이라 넘김",
"rank_in_window": null,
"cooldown_elapsed_sec": 2067,
"evidence": [
{
"dataset": "live_chat",
"field": "topic_tags",
"value": null
},
{
"dataset": "live_chat",
"field": "is_question",
"value": false
},
{
"dataset": "live_chat",
"field": "sent_at",
"value": "2026-09-08T21:14:27"
},
{
"dataset": "viewer_relation",
"field": "last_replied_at",
"value": "2026-09-08T20:40:00"
}
],
"summary": "모델이 이 줄에서 주제 태그를 하나도 뽑지 못해 미아의 세계관 주제와 겹치는지 볼 수 없습니다. 겹침이 확인되지 않으면 답하지 않는 쪽으로 두고 운영자가 직접 집어 올릴 수 있게 남깁니다.",
"hedge": "태그를 못 붙인 이유가 감정 표현이라서인지 분류가 흔들려서인지는 이 데이터만으로 갈라지지 않습니다."
},
{
"pick_id": "PICK-1407",
"msg_id": "MSG-20260908-1407",
"viewer_id": "VW-1660",
"character_id": "CH-0007",
"window_start": "2026-09-08T21:14:00",
"verdict": "다음 차례로 미룸",
"rank_in_window": 4,
"cooldown_elapsed_sec": 551,
"evidence": [
{
"dataset": "live_chat",
"field": "donation_amount",
"value": 0
},
{
"dataset": "live_chat",
"field": "sent_at",
"value": "2026-09-08T21:14:21"
},
{
"dataset": "viewer_relation",
"field": "closeness_level",
"value": 2
},
{
"dataset": "live_chat",
"field": "msg_id",
"agg": "count",
"value": 5
}
],
"summary": "코코의 노래 신청은 걸리는 조건이 없지만 이 구간에 남은 다섯 줄 가운데 네 번째입니다. 앞의 세 개를 답한 뒤 다음 구간에서 다시 세웁니다.",
"hedge": null
},
{
"pick_id": "PICK-1501",
"msg_id": "MSG-20260908-1501",
"viewer_id": "VW-4001",
"character_id": "CH-0007",
"window_start": "2026-09-08T21:15:00",
"verdict": "지금 답변할 질문",
"rank_in_window": 1,
"cooldown_elapsed_sec": 902,
"evidence": [
{
"dataset": "live_chat",
"field": "donation_amount",
"value": 10000
},
{
"dataset": "live_chat",
"field": "is_question",
"value": true
},
{
"dataset": "viewer_relation",
"field": "closeness_level",
"value": 5
},
{
"dataset": "viewer_relation",
"field": "last_replied_at",
"value": "2026-09-08T21:00:00"
},
{
"dataset": "live_chat",
"field": "sent_at",
"value": "2026-09-08T21:15:02"
},
{
"dataset": "character_sheet",
"field": "reply_cooldown_sec",
"value": 180
}
],
"summary": "별하가 10,000원을 얹어 노래를 한 곡 더 청했고 지난 답변에서 902초가 지나 쿨다운을 넘겼습니다. 다음 30초 구간에서 가장 앞자리라 그 구간의 첫 답변 후보가 됩니다.",
"hedge": null
}
][자동 검증 · 코드가 판정합니다] 1. verdict 가 enum 다섯 값(지금 답변할 질문·세계관 밖이라 넘김·방금 답한 주제·관계 단계에 아직 못 미침·다음 차례로 미룸) 중 하나인지 확인합니다. 판정은 규칙이 정하므로 모델이 다른 값을 쓰면 그 응답을 버립니다. 2. pick_id·msg_id·verdict·evidence·summary 다섯 필드가 모두 채워졌는지 확인합니다. 3. rank_in_window 가 1 이상인지, cooldown_elapsed_sec 가 0 이상인지 범위를 확인합니다. 4. cooldown_elapsed_sec 를 live_chat.sent_at 에서 viewer_relation.last_replied_at 을 뺀 초로 다시 계산해 값이 같은지 확인합니다. 그 값이 character_sheet.reply_cooldown_sec 보다 작고 live_chat.topic_tags 가 viewer_relation.answered_topics 와 겹치면 방금 답한 주제입니다. 5. rank_in_window 를 같은 character_id 의 같은 30초 구간에서 걸러지지 않은 채팅을 donation_amount 큰 순, 같으면 is_question 이 참인 것 먼저, 그다음 sent_at 이른 순으로 다시 세워 자리가 같은지 확인합니다. 세 번째까지가 지금 답변할 질문이고 네 번째부터가 다음 차례로 미룸입니다. 구간은 방송마다 따로 나누므로 두 캐릭터가 같은 시각에 방송해도 순위가 섞이지 않습니다. 6. evidence 의 field 가 live_chat·character_sheet·viewer_relation 에 실재하는 컬럼 이름인지 대조합니다. 없는 이름을 인용하면 요약이 지어낸 문장이라는 뜻이라 규칙이 만든 기본 문장으로 되돌립니다. 7. agg 가 없는 evidence 값이 그 컬럼에 수록된 행 값과 같은지 대조합니다. 여러 줄에서 센 수치에는 agg 를 함께 적습니다. 8. summary 가 존댓말로 끝나는 한국어 한 문장 묶음인지 확인합니다. 9. live_chat.topic_tags 가 비어 있으면 character_sheet.world_topic_tags 와 겹치는지 볼 수 없으므로 세계관 밖이라 넘김으로 두고 rank_in_window 를 비웁니다. MSG-20260908-1409 가 태그가 빈 경우이고, MSG-20260908-1402 가 태그가 있으면서 금칙 주제에 걸린 경우입니다. 10. 판정 순서는 세계관 밖이라 넘김, 방금 답한 주제, 관계 단계에 아직 못 미침, 그다음 구간 순위 순으로 앞에서 걸리면 뒤를 보지 않습니다. 그래서 후원이 큰 줄도 금칙 주제에 걸리면 순위를 매기지 않습니다. 11. min_closeness_by_topic 은 캐릭터마다 주제별 숫자가 다르므로, 그 채팅의 주제에 해당하는 값만 꺼내 viewer_relation.closeness_level 과 견줍니다. 캐릭터 설정표에 없는 주제는 세계관 밖으로 갑니다. 어느 캐릭터의 설정표를 꺼낼지는 live_chat.character_id 가 정합니다. 12. live_chat.topic_tags 는 수집기가 나중에 붙여 둔 정답 태그입니다. 실행할 때 쓰는 태그는 모델이 body 만 보고 뽑은 값이고, 그 태그가 그 캐릭터의 world_topic_tags 와 banned_tags 를 합한 주제 어휘 밖이면 응답을 버리고 세계관 밖이라 넘김으로 둡니다. 위 판정 규칙을 다시 계산해 대조할 때는 이 정답 태그를 넣어 돌리고, 모델이 뽑은 태그와 정답 태그가 얼마나 겹쳤는지는 2주차 러너가 따로 셉니다. 13. cooldown_elapsed_sec 는 판정 값과 상관없이 last_replied_at 이 있으면 언제나 채웁니다. 태그가 비어 세계관 밖으로 간 줄도 마지막 답변에서 지난 초는 그대로 적습니다. 비우는 것은 rank_in_window 뿐이고, 그 값은 걸러진 줄에서 비웁니다. [평가 러너 · 2주차] 1. 운영자가 손으로 고른 60건으로 판정 일치율을 냅니다. 어긋난 건은 금칙 주제에서 갈라졌는지 순위에서 갈라졌는지 나눠 리포트에 답니다. [수동 검증 · 사람이 봅니다] 1. 지금 답변할 질문으로 뜬 줄을 운영자가 실제로 캐릭터에게 넘기는지 표본 20건을 따라가 봅니다. 후원이 큰 줄이라도 방송 흐름과 안 맞으면 건너뛰는 경우가 있습니다. 2. 쿨다운 180초가 팬이 느끼기에 적당한 간격인지 방송 뒤 채팅 반응을 보고 정합니다.