타인에이아이
경력근거: 실험적인 프로덕트의 MVP/PoC를 빠르게 만들고 검증하는 역할
공고는 Labs 에서 실험 제품의 최소 기능 제품을 빠르게 만들고 검증하는 자리라고 적고, 러비더비는 캐릭터가 곧 제품인 서비스입니다. 이 포트폴리오는 그 검증 쪽을 골랐습니다. 캐릭터 설정을 데이터로 두고 발화와 대조하는 규칙을 만들어, 프롬프트를 바꿀 때마다 같은 기준으로 전후를 비교합니다. 다만 이 포트폴리오는 설정 대조 규칙과 화면 구현을 보여줍니다. 캐릭터 작법이나 콘텐츠 감수에 대한 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
타인에이아이 Labs 에서 실험적인 제품의 최소 기능 제품을 빠르게 만들고 검증하는 자리라고 포지션 설명에 적습니다. 크게 설계하기보다 작은 완결품을 계속 내놓는 리듬입니다.
Claude Code 같은 도구를 개인적으로 구독해 쓸 만큼 AI 가 일상이 된 사람을 찾고, AI 를 활용해 실제로 동작하는 제품을 만들어 본 경험을 봅니다.
로맨스 콘텐츠를 살아 있게 만든다는 비전 아래 AI 캐릭터 채팅 서비스를 20개국 이상에서 운영한다고 소개합니다. 캐릭터가 설정대로 말하는지가 곧 제품 품질입니다.
기획 담당자가 대화 기록을 열면, 캐릭터 설정에 적힌 나이와 직업과 금지 주제를 답변과 맞춰 어긋난 발화를 나눠 보여줍니다. 어긋났다는 판정은 설정값 대조 규칙이 정하고, 모델은 설정의 어느 값이 발화의 어느 대목과 어긋났는지를 담당자가 읽을 한 줄로 요약합니다.
저는 캐릭터 채팅 서비스의 기획을 맡고 있습니다. 사용자 제보로 캐릭터가 설정과 다른 말을 한다는 걸 알게 되는데, 대화 기록이 하루 수만 건이라 어디서 어긋났는지 찾기 어렵습니다. 프롬프트를 고쳐도 나아졌는지 확인할 방법이 없습니다.
캐릭터가 흔들리는 걸 못 잡는 이유는 대화가 많아서가 아니라 설정과 발화를 맞춰 볼 자리가 없기 때문입니다. 설정에 적힌 값과 답변에 나온 값을 대조해 어긋난 발화만 모아 주면, 프롬프트 수정 전후를 같은 기준으로 비교합니다.
기획 담당자가 쓰는 화면 하나가 완결됩니다. 설정에 적힌 값과 발화에 나온 값을 대조해 사실 충돌·금지 주제·설정 미매핑을 나눠 보여주고, 대조한 두 값을 나란히 냅니다. 실제 서비스 연동은 대화 내보내기 파일을 올리는 것으로 대신하고 3주차로 미룹니다.
같은 캐릭터의 수정 전후 대화 200턴을 각각 돌려 어긋난 발화 비율을 나란히 냅니다. 수정이 실제로 줄였는지가 표에서 보입니다.
대화 기록을 받아 매일 돌고, 한 캐릭터에서 어긋난 발화가 기준을 넘으면 그 캐릭터를 점검 대상으로 표시합니다.
기획 담당자가 대화 기록을 열면 설정과 어긋난 발화가 사유별로 갈리고, 대조한 설정값과 발화값이 나란히 보입니다.
설정 항목이 비거나 발화가 빈 턴이거나 등록되지 않은 캐릭터 코드가 섞인 기록 30종을 올려도 화면이 죽지 않고, 판정할 수 없는 턴은 미판정으로 남습니다.
수정 전후 200턴을 각각 돌려 어긋난 발화 비율의 차이를 냅니다. 단, 정답과 데이터를 모두 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
위 수치는 더미데이터 자가채점이며 실제 캐릭터 일관성은 타인에이아이의 실 대화 기록과 기획 검수가 있어야 측정됩니다. 이 포트폴리오는 그 검증을 바로 돌릴 수 있는 상태로 만듭니다.
수치는 합성 데이터 자가채점이라 성능 증명이 아닙니다. 면접에서는 데모 동작과 실검증 준비 상태로 솔직히 설명합니다.
정답을 작성자가 만들었으니 잘 맞는 것이 당연합니다. 설정 결측·빈 발화·등록되지 않은 캐릭터 코드를 더미에 일부러 섞어 쉬운 문제만 푸는 함정을 피합니다.
설정과 다른 값이 나왔다는 사실이 곧 잘못된 답변을 뜻하지는 않습니다. 사용자가 설정을 바꿔 부르는 연출도 있으므로 판정을 결론으로 읽지 않게 두 값을 항상 같이 보여줍니다.
실제 도입에서는 언어마다 표기와 존칭이 달라 값 추출이 흔들립니다. 언어가 늘어나면 규칙을 코드에서 빼 설정으로 옮겨야 하며, 그것은 도입 단계 과제로 분리합니다.
발화 내용의 수위와 콘텐츠 정책 판단은 이 포트폴리오의 범위 밖입니다. 여기서는 설정값과 어긋나는지만 보는 데까지만 다룹니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
캐릭터마다 정해 둔 설정입니다. 대조의 기준이며 항목 결측과 금지 주제 미지정을 의도적으로 포함합니다.
| profile_no | character_code | display_name | age | job | speech_style | forbidden_topic | updated_on |
|---|---|---|---|---|---|---|---|
| CP-001 | CH-101 | 하윤 | 24 | 플로리스트 | casual | 정치 | 2026-07-01 |
| CP-002 | CH-102 | 도현 | 31 | 형사 | cold | 실제 사건 | 2026-07-01 |
| CP-003 | CH-103 | 세라 | — | 가수 | playful | — | 2026-06-15 |
| CP-004 | CH-104 | 민준 | 28 | — | formal | 의료 조언 | 2026-06-20 |
| CP-005 | CH-105 | 리사 | 22 | 대학생 | casual | 정치 | 2026-08-01 |
아래 JSON Schema 에 맞는 캐릭터 설정 행을 더 만들어 주세요. {"type":"object","properties":{"profile_no":{"type":"string"},"character_code":{"type":"string"},"display_name":{"type":"string"},"age":{"type":["integer","null"]},"job":{"type":["string","null"]},"speech_style":{"type":"string","enum":["formal","casual","cold","playful"]},"forbidden_topic":{"type":["string","null"]},"updated_on":{"type":"string"}},"required":["profile_no","character_code","display_name","speech_style","updated_on"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) age 나 job 이 null 인 행, (2) forbidden_topic 이 지정되지 않은 행, (3) 대화 기록에 없는 character_code, (4) 같은 이름을 쓰는 다른 코드의 캐릭터. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"profile_no":{"type":"string"},"character_code":{"type":"string"},"display_name":{"type":"string"},"age":{"type":["integer","null"]},"job":{"type":["string","null"]},"speech_style":{"type":"string","enum":["formal","casual","cold","playful"]},"forbidden_topic":{"type":["string","null"]},"updated_on":{"type":"string"}},"required":["profile_no","character_code","display_name","speech_style","updated_on"]}캐릭터가 답한 대화 턴입니다. 판정의 입력이며 빈 발화와 설정에 없는 캐릭터 코드, 설정값과 다른 수치를 의도적으로 포함합니다.
| turn_no | session_key | character_code_raw | spoken_at | user_text | reply_text | stated_age | stated_job | topic_tag | prompt_ver |
|---|---|---|---|---|---|---|---|---|---|
| TN-9001 | SS-501 | CH-101 | 2026-08-15T20:10:00 | 몇 살이야? | 스물넷이야. 꽃집에서 일해. | 24 | 플로리스트 | — | p12 |
| TN-9002 | SS-501 | CH-101 | 2026-08-15T20:12:00 | 무슨 일 해? | 요즘은 카페에서 바리스타로 일하고 있어. | — | 바리스타 | — | p12 |
| TN-9003 | SS-502 | CH-102 | 2026-08-15T21:00:00 | 요즘 사건 어때? | 수사 중인 건은 말할 수 없습니다. | — | — | — | p12 |
| TN-9004 | SS-502 | CH-102 | 2026-08-15T21:05:00 | 작년 그 사건 말이야 | 그 사건이라면 뉴스에 나온 그 사고를 말하는 거군요. 제 생각에는 | — | — | 실제 사건 | p12 |
| TN-9005 | SS-503 | CH-107 | 2026-08-16T09:30:00 | 우리 개가 토했어 | 약을 반 알 먹이시면 됩니다. | — | — | 의료 조언 | p12 |
아래 JSON Schema 에 맞는 대화 턴 행을 더 만들어 주세요. {"type":"object","properties":{"turn_no":{"type":"string"},"session_key":{"type":"string"},"character_code_raw":{"type":"string"},"spoken_at":{"type":"string"},"user_text":{"type":["string","null"]},"reply_text":{"type":"string"},"stated_age":{"type":["integer","null"]},"stated_job":{"type":["string","null"]},"topic_tag":{"type":["string","null"]},"prompt_ver":{"type":"string"}},"required":["turn_no","session_key","character_code_raw","spoken_at","reply_text","prompt_ver"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) reply_text 가 빈 문자열인 행, (2) stated_age 나 stated_job 이 설정과 다른 행, (3) 설정 목록에 없는 character_code_raw, (4) 금지 주제를 그대로 다룬 행. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"turn_no":{"type":"string"},"session_key":{"type":"string"},"character_code_raw":{"type":"string"},"spoken_at":{"type":"string"},"user_text":{"type":["string","null"]},"reply_text":{"type":"string"},"stated_age":{"type":["integer","null"]},"stated_job":{"type":["string","null"]},"topic_tag":{"type":["string","null"]},"prompt_ver":{"type":"string"}},"required":["turn_no","session_key","character_code_raw","spoken_at","reply_text","prompt_ver"]}| TypeScript | 판정 규칙과 화면을 함께 쓰는 언어입니다. 설정 대조 규칙을 한 곳에 두고 러너와 화면이 같은 코드를 씁니다. |
| React | 기획 담당자가 보는 화면입니다. 발화와 설정값을 나란히 둡니다. |
| React Native | 앱에서 같은 판정 결과를 확인할 수 있게 결과를 같은 형식으로 냅니다. |
| Firebase | 대화 기록과 판정 결과를 담고 3주차에 새 대화를 받아 옵니다. |
| GitHub | 판정 규칙을 바꿀 때마다 평가 러너를 돌려 결과가 나빠지지 않았는지 확인합니다. |
| LLM | 판정이 끝난 뒤 어긋난 설정값과 발화 대목을 짝지어 한 줄로 요약합니다. 어긋남은 규칙이 정합니다. |
✓ pass → 그대로 멈춰도 완결입니다. 더 키우려면 프롬프트 수정 전후를 같은 기준으로 비교하는 단계로 넘어갑니다.
✓ pass → 여기까지도 완결입니다. 더 키우려면 새 대화를 받아 매일 점검하는 단계로 확장합니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
persona_code사유 라벨 · turn_no + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
consistent설정 일치age_conflict나이 충돌job_conflict직업 충돌forbidden_topic금지 주제reply_empty빈 응답profile_unmapped설정 미매핑대화 턴 한 건의 설정 대조 결과입니다. 사유 코드와 대조한 두 값을 함께 남깁니다.
{"type":"object","properties":{"check_id":{"type":"string"},"turn_no":{"type":"string"},"profile_no":{"type":["string","null"]},"persona_code":{"type":"string","enum":["consistent","age_conflict","job_conflict","forbidden_topic","reply_empty","profile_unmapped"]},"evidence":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string","enum":["profile","turn"]},"field":{"type":"string"},"agg":{"type":["string","null"]},"value":{"type":["string","number","null"]}},"required":["source","field","value"]}},"summary":{"type":"string"},"hedge":{"type":["string","null"]}},"required":["check_id","turn_no","persona_code","evidence","summary"]}[
{
"check_id": "pk_0001",
"turn_no": "TN-9001",
"profile_no": "CP-001",
"persona_code": "consistent",
"evidence": [
{
"source": "turn",
"field": "stated_age",
"agg": null,
"value": 24
},
{
"source": "profile",
"field": "age",
"agg": null,
"value": 24
}
],
"summary": "발화에서 밝힌 나이와 직업이 설정과 같습니다.",
"hedge": null
},
{
"check_id": "pk_0002",
"turn_no": "TN-9011",
"profile_no": "CP-011",
"persona_code": "consistent",
"evidence": [
{
"source": "turn",
"field": "topic_tag",
"agg": null,
"value": null
},
{
"source": "profile",
"field": "forbidden_topic",
"agg": null,
"value": "법률 자문"
}
],
"summary": "금지 주제를 요청받았지만 답을 피해 설정을 지켰습니다.",
"hedge": null
},
{
"check_id": "pk_0003",
"turn_no": "TN-9002",
"profile_no": "CP-001",
"persona_code": "job_conflict",
"evidence": [
{
"source": "turn",
"field": "stated_job",
"agg": null,
"value": "바리스타"
},
{
"source": "profile",
"field": "job",
"agg": null,
"value": "플로리스트"
}
],
"summary": "설정은 플로리스트인데 발화에서는 바리스타라고 말했습니다.",
"hedge": null
},
{
"check_id": "pk_0004",
"turn_no": "TN-9012",
"profile_no": "CP-006",
"persona_code": "age_conflict",
"evidence": [
{
"source": "turn",
"field": "stated_age",
"agg": null,
"value": 42
},
{
"source": "profile",
"field": "age",
"agg": null,
"value": 35
}
],
"summary": "설정 나이보다 일곱 살 많게 말했습니다.",
"hedge": null
},
{
"check_id": "pk_0005",
"turn_no": "TN-9004",
"profile_no": "CP-002",
"persona_code": "forbidden_topic",
"evidence": [
{
"source": "turn",
"field": "topic_tag",
"agg": null,
"value": "실제 사건"
},
{
"source": "profile",
"field": "forbidden_topic",
"agg": null,
"value": "실제 사건"
}
],
"summary": "설정이 막아 둔 실제 사건 주제를 그대로 이어 말했습니다.",
"hedge": null
},
{
"check_id": "pk_0006",
"turn_no": "TN-9006",
"profile_no": "CP-009",
"persona_code": "forbidden_topic",
"evidence": [
{
"source": "turn",
"field": "topic_tag",
"agg": null,
"value": "음주"
},
{
"source": "profile",
"field": "age",
"agg": null,
"value": 19
}
],
"summary": "미성년 설정 캐릭터가 음주 주제를 받아 이어 말했습니다.",
"hedge": "합성 기준 판정이라 연출 의도로 허용하는 구간이 있을 수 있습니다."
},
{
"check_id": "pk_0007",
"turn_no": "TN-9009",
"profile_no": "CP-005",
"persona_code": "reply_empty",
"evidence": [
{
"source": "turn",
"field": "reply_text",
"agg": null,
"value": ""
},
{
"source": "turn",
"field": "prompt_ver",
"agg": null,
"value": "p13"
}
],
"summary": "응답이 비어 있어 대조할 내용이 없습니다.",
"hedge": null
},
{
"check_id": "pk_0008",
"turn_no": "TN-9008",
"profile_no": null,
"persona_code": "profile_unmapped",
"evidence": [
{
"source": "turn",
"field": "character_code_raw",
"agg": null,
"value": "CH-999"
},
{
"source": "profile",
"field": "character_code",
"agg": "count",
"value": 0
}
],
"summary": "설정 목록에 없는 캐릭터라 무엇과 맞춰야 할지 정하지 못합니다.",
"hedge": null
},
{
"check_id": "pk_0009",
"turn_no": "TN-9010",
"profile_no": "CP-004",
"persona_code": "job_conflict",
"evidence": [
{
"source": "turn",
"field": "stated_job",
"agg": null,
"value": "회계사"
},
{
"source": "profile",
"field": "job",
"agg": null,
"value": null
}
],
"summary": "설정에 직업이 비어 있는데 발화가 직업을 단정해 설정을 채워야 합니다.",
"hedge": null
}
][자동 검증 · 코드가 판정합니다] 1. persona_code 가 enum 여섯 값 중 하나인지 확인합니다. 2. check_id·turn_no·persona_code·evidence·summary 가 모두 채워졌는지 확인합니다. 3. stated_age 가 0 이상 120 이하인지 확인합니다. 4. evidence 의 field 이름이 설정과 대화 데이터에 실재하는 컬럼인지 대조합니다. 5. agg 가 없는 evidence 의 value 가 그 turn_no 또는 profile_no 행의 값과 같은지 확인합니다. 6. topic_tag 가 그 캐릭터의 forbidden_topic 과 같으면 persona_code 는 forbidden_topic 인지 확인합니다. 대화에는 금지 주제를 피한 턴과 이어 말한 턴이 모두 있습니다. 7. summary 가 존댓말로 끝나는지 확인합니다. [평가 러너 · 2주차] 같은 캐릭터의 프롬프트 수정 전후 대화 200턴을 각각 돌려 어긋난 발화 비율을 비교합니다. [수동 검증 · 사람이 봅니다] 1. 사용자가 설정을 바꿔 부르는 연출을 어긋남으로 볼지 표본 20건을 눈으로 확인합니다. 2. 설정 항목이 비어 있는 캐릭터를 어디까지 판정할지는 기획 기준을 따라 담당자가 정합니다.