팀카이
경력근거: 한 고객사의 에이전트를 직접 빌드하고, 그 에이전트가 약속한 운영 지표를 실제로 움직일 때까지
팀카이의 KaiCX 는 고객사 고객센터를 AI 로 바꾸는 제품이고, FDE 는 고객사 현장에 들어가 운영팀 책상 옆에서 문의 톤을 듣고 실제 대화 로그를 매일 보며 실패 케이스를 태깅하는 자리입니다. 이 도구가 답하는 질문이 바로 그 자리에서 하루에도 몇 번씩 나오는 질문이라, 운영팀이 직접 묻게 만들면 조회를 대신 돌려 주던 시간이 다음 이터레이션 가설로 넘어갑니다. 고객사와 합의한 AI 응대율과 AI 업무 처리율도 결국 이런 조합 조회로 확인되므로, 조건과 근거를 눈에 보이게 남기는 구조가 지표 합의의 바탕이 됩니다. 고객사에 밀착해 직접 만드는 FDE 방식에서는 처음부터 여러 사람이 쓰는 화면을 세우기보다 담당자 자리에서 바로 돌려 보는 명령줄 도구로 기능과 수요를 확인하는 편이 자연스러워, 이 포트폴리오도 터미널에서 끝까지 갑니다. 이 포트폴리오는 자연어를 조회 조건으로 바꾸고 결정론 규칙으로 실행하고 채점하는 기술을 보여주며 고객센터 운영이나 이커머스 CS 도메인 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
FDE 는 한 고객사의 에이전트를 직접 빌드해 운영 지표를 실제로 움직일 때까지 책임집니다. 도메인·시스템·정책에 맞춘 1개 instance 가 끝까지 도는 한 바퀴가 핵심입니다.
출시 후 실제 대화 로그를 매일 보면서 실패 케이스를 태깅하고 원인을 진단하는 것이 이 직무의 일상입니다. 그 조회를 고객사 운영팀이 직접 한국어로 던져 근거까지 받아 보게 만들면 FDE 는 조회 대행 대신 다음 이터레이션 가설에 시간을 씁니다.
고객사 현장에서 발견한 패턴을 팀 전체가 학습으로 흡수합니다. 질문을 조건으로 바꾸는 재사용 코어와 고객사별 컬럼·카테고리 사전을 분리해 다음 고객사로 빠르게 옮기는 설계가 직무 적합성을 보여줍니다.
고객사 상담 시스템에서 내려받은 대화 로그 CSV 와 카테고리 코드표를 로컬 저장소에 올려 두고, 고객센터 운영팀이 터미널에서 `kaiq ask "지난주 봇이 답한 환불 문의 중 사흘 안에 다시 들어온 건 몇 건이야"` 처럼 평소 말로 묻는 명령줄 도구입니다. 도구는 그 문장을 기간·필터·집계 기준으로 바꿔 표로 먼저 보여주고, 담당자가 조건을 고치거나 승인해야 SQL 이 실행됩니다. 실행 뒤에는 집계 표와 함께 각 조건이 질문의 어느 어절에서 나왔는지, 어느 대화의 어느 필드가 걸렸는지를 근거로 찍습니다. 숫자를 만드는 쪽은 모델이 아니라 SQL 이라, 답이 이상하면 조건 표에서 어디가 어긋났는지 바로 짚입니다.
고객센터 운영팀 리더입니다. 봇을 붙인 뒤로 어제 봇이 답했다가 고객이 다시 물어본 건이 얼마나 되는지, 상담사로 넘어간 사유 중 무엇이 제일 많은지가 매일 궁금한데, 상담 시스템 통계 화면에는 그 조합이 없어 매번 로그를 엑셀로 내려받아 필터를 다시 겁니다. 개발자에게 부탁하면 반나절이 걸려 결국 안 물어보게 되던 사람이, 이제는 궁금할 때 바로 묻고 어떤 대화가 그 숫자에 들어갔는지까지 눈으로 확인하고 싶어 합니다.
운영팀의 질문은 대개 기간·카테고리·처리 주체·재문의 여부의 조합이라서, 문장을 조건으로 바꾸는 변환만 모델에 맡기고 실행과 집계를 SQL 에 두면, 조건 표를 사람이 확인하는 것만으로 답을 믿을 수 있게 되고 개발자를 거치지 않고도 매일의 실패 케이스를 직접 들여다보게 된다는 가정입니다.
고객사에서 받은 대화 로그 CSV 를 올려 두고, 한국어 질문을 조건 표로 바꿔 보여준 뒤 승인받아 실행하고 결과와 근거를 터미널에 찍는 명령줄 도구입니다. 상담 시스템 실연동이 아니라 관리자 화면에서 내보낸 CSV(메타데이터)를 가정해, 사내망·CTI 연동 없이 1주에 혼자 완결합니다.
운영팀이 실제로 쓸 법한 질문 40개와 정답 조건을 만들어 완전 일치 여부를 규칙으로 자동 채점하고, 최근이나 많이처럼 기준이 없는 어구는 추측 대신 확인 질문으로 되묻게 만드는 단계입니다.
확인을 마친 조건을 이름 붙여 저장하고 기간만 바꿔 다시 돌리게 해, 매일 아침 같은 실패 케이스 점검을 명령 한 줄로 끝내고 결과를 CSV 로도 내보내는 단계입니다.
운영팀이 실제로 물을 법한 질문 10개를 그대로 치면, 뽑아낸 조회 조건과 실행할 SQL 과 근거 어절이 몇 초 안에 터미널에 나오는 걸 시연합니다
일부러 만든 모호·악성 질문 30종(기간이 없는 질문, 존재하지 않는 컬럼 요구, 빈 문장)에도 조건 형식(Zod)이 한 번도 깨지지 않고, 기준 없는 어구는 추측 대신 확인 질문으로 나옵니다
확인 단계를 건너뛴 실행이 0건입니다. 승인 없이 SQL 이 도는 경로가 없다는 걸 자동 테스트로 보여줍니다
직접 만든 질문 정답 40개 중 조회 조건이 완전히 일치한 게 32개(80%)입니다. 단, 질문과 정답 조건을 모두 작성자가 만든 합성 기준이라 '성능 검증' 이 아니라 '데모 조건에서의 동작' 임을 명시합니다
위 수치는 전부 작성자가 만든 더미데이터 자가채점이며, 실제 조건 파싱 정확도는 팀카이 고객사 실 상담 로그와 운영팀이 실제로 던지는 질문이 있어야 측정 가능합니다. 이 도구는 그 측정을 바로 돌릴 수 있는 상태까지 만듭니다.
이건 고객·실데이터 없이 혼자 만든 포트폴리오입니다. 모든 수치는 합성 데이터 자가채점이라 '성능 증명' 이 아닙니다. 면접에서 솔직히 '데모 조건 동작 + 실검증 준비 상태' 로 설명합니다
정답 조건을 작성자가 만들었으니 잘 맞는 게 당연합니다. 기간이 모호한 질문, 별칭 표기, 미등록 카테고리, 조건이 둘 이상 겹치는 질문을 일부러 섞어 쉬운 문제만 푸는 게 아님을 보여줍니다
모델이 조건을 잘못 뽑으면 엉뚱한 숫자가 나올 수 있습니다. 실행 전에 조건 표를 사람이 확인하게 막고, 조건마다 근거가 된 질문 어절을 붙이며, 집계는 SQL 이 하므로 모델이 숫자를 지어내지 못합니다
고객사마다 대화 로그 컬럼명과 카테고리 표기가 제각각입니다. 컬럼 사전과 코드표를 코드 밖 파일로 분리하고, 매핑하지 못한 값은 추측하지 말고 미매핑으로 알립니다. 실제 도입 시에는 이 사전과 질문 예시를 고객사 기준으로 다시 써야 합니다
이 포트폴리오는 자연어를 조회 조건으로 바꾸고 결정론 규칙으로 실행하고 채점하는 기술을 보여주며, 고객센터 운영이나 이커머스 CS 도메인 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
고객사 상담 시스템 관리자 화면에서 내보낸 대화 로그 CSV 입니다. 질문이 실제로 걸리는 표이며, 운영 현실대로 카테고리 별칭·미등록 코드·결측(만족도 미응답, 재문의 미집계)·경계값(응답시간 0초)을 일부러 섞어 쉬운 데이터만 푸는 게 아님을 보여줍니다.
| conv_id | started_at | channel | category_raw | category_code | handled_by | handoff_reason | reopened_within_72h | first_response_sec | csat_1to5 |
|---|---|---|---|---|---|---|---|---|---|
| C0001 | 2026-03-04 09:12 | chat | 배송조회 | c_delivery | bot | — | false | 3 | 5 |
| C0002 | 2026-03-04 10:41 | 환불요청 | c_refund | handoff | 정책 민감 | false | 840 | 4 | |
| C0003 | 2026-03-04 14:02 | app | 배송 문의 | c_delivery | bot | — | true | 2 | 2 |
| C0004 | 2026-03-05 11:20 | chat | 반품/교환 | c_return | handoff | 봇 답변 실패 | false | 12 | 4 |
| C0005 | 2026-03-05 16:55 | voice | 개인정보 변경 | c_account | human | — | — | — | — |
아래 Zod 스키마에 맞는 고객센터 대화 로그 14일치 JSON 배열을 생성하세요. 채널 4종(chat/email/voice/app)과 처리 주체 3종(bot/human/handoff) 분포를 포함합니다.
{"type":"object","properties":{"conv_id":{"type":"string"},"started_at":{"type":"string"},"channel":{"type":"string","enum":["chat","email","voice","app"]},"category_raw":{"type":"string"},"category_code":{"type":["string","null"]},"handled_by":{"type":"string","enum":["bot","human","handoff"]},"handoff_reason":{"type":["string","null"]},"reopened_within_72h":{"type":["boolean","null"]},"first_response_sec":{"type":["number","null"]},"csat_1to5":{"type":["number","null"]}},"required":["conv_id","started_at","channel","category_raw","category_code","handled_by","handoff_reason","reopened_within_72h","first_response_sec","csat_1to5"]}
쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라: (1) 같은 카테고리의 별칭 표기 혼입(category_raw '배송 문의'와 '배송조회'가 모두 c_delivery, '환불'과 '환불요청'이 모두 c_refund), (2) 미등록 카테고리(category_code=null) 2~3건, (3) 결측. reopened_within_72h·csat_1to5·first_response_sec 가 null 인 행 각 1~2건, (4) 경계값 first_response_sec=0 인 행 2건, (5) handled_by='bot' 인데 reopened_within_72h=true 인 실패 케이스 4건 이상, (6) handoff_reason 은 handled_by='handoff' 일 때만 값이 있고 그 외에는 null. JSON 배열만.{"type":"object","properties":{"conv_id":{"type":"string"},"started_at":{"type":"string"},"channel":{"type":"string","enum":["chat","email","voice","app"]},"category_raw":{"type":"string"},"category_code":{"type":["string","null"]},"handled_by":{"type":"string","enum":["bot","human","handoff"]},"handoff_reason":{"type":["string","null"]},"reopened_within_72h":{"type":["boolean","null"]},"first_response_sec":{"type":["number","null"]},"csat_1to5":{"type":["number","null"]}},"required":["conv_id","started_at","channel","category_raw","category_code","handled_by","handoff_reason","reopened_within_72h","first_response_sec","csat_1to5"]}대화별 발화 기록입니다. 조회 결과의 근거로 함께 출력되며, 봇이 무엇을 인용했고 어느 가드에 걸렸는지가 실패 케이스 원인 진단의 재료입니다.
| conv_id | turn_no | speaker | text | cited_doc_id | guard_hit |
|---|---|---|---|---|---|
| C0001 | 1 | customer | 주문한 상품 언제 도착하나요? | — | — |
| C0001 | 2 | bot | 마이페이지 주문내역에서 송장번호와 배송 현황을 확인하실 수 있습니다.… | kb_delivery_01 | — |
| C0003 | 1 | customer | 송장번호가 안 떠요 | — | — |
| C0003 | 2 | bot | 주문내역 화면에서 송장번호를 확인해 주세요. | kb_delivery_01 | — |
| C0006 | 2 | bot | 쿠폰은 중복 적용이 가능합니다. | — | — |
아래 Zod 스키마에 맞는 대화 발화 기록 JSON 배열을 conversations 의 conv_id 와 맞춰 생성하세요. 대화 1건당 2~5턴입니다.
{"type":"object","properties":{"conv_id":{"type":"string"},"turn_no":{"type":"integer"},"speaker":{"type":"string","enum":["customer","bot","agent"]},"text":{"type":"string"},"cited_doc_id":{"type":["string","null"]},"guard_hit":{"type":["string","null"]}},"required":["conv_id","turn_no","speaker","text","cited_doc_id","guard_hit"]}
조건: (1) handled_by='bot' 인데 재문의가 난 대화에는 인용 문서가 질문과 어긋나거나 cited_doc_id=null 인 턴을 포함, (2) guard_hit 은 'force_human:<카테고리>'·'need_review:<카테고리>'·'unknown_category' 중 하나이거나 null, (3) handled_by='handoff' 인 대화에는 agent 발화가 최소 1턴, (4) 고객 발화가 비어 있는 대화도 1~2건. JSON 배열만.{"type":"object","properties":{"conv_id":{"type":"string"},"turn_no":{"type":"integer"},"speaker":{"type":"string","enum":["customer","bot","agent"]},"text":{"type":"string"},"cited_doc_id":{"type":["string","null"]},"guard_hit":{"type":["string","null"]}},"required":["conv_id","turn_no","speaker","text","cited_doc_id","guard_hit"]}고객사 로그의 컬럼명과 카테고리 표기를 도구가 아는 이름으로 잇는 사전입니다. 고객사가 바뀌면 이 표만 갈아끼우며, 질문에 나온 말이 어느 컬럼인지 찾는 근거가 됩니다.
| term | maps_to | kind | source_header | deprecated |
|---|---|---|---|---|
| 재문의 | reopened_within_72h | column | 72시간내재문의 | false |
| 다시 들어온 | reopened_within_72h | column | 72시간내재문의 | false |
| 봇이 답한 | handled_by=bot | value | 처리주체 | false |
| 상담사에게 넘어간 | handled_by=handoff | value | 처리주체 | false |
| 첫 응답 시간 | first_response_sec | column | 최초응답(초) | false |
아래 Zod 스키마에 맞는 컬럼·카테고리 사전 JSON 배열을 conversations 의 컬럼명·category_raw 와 맞춰 30건 생성하세요.
{"type":"object","properties":{"term":{"type":"string"},"maps_to":{"type":["string","null"]},"kind":{"type":"string","enum":["column","category","value"]},"source_header":{"type":["string","null"]},"deprecated":{"type":"boolean"}},"required":["term","maps_to","kind","source_header","deprecated"]}
조건: (1) 같은 컬럼을 가리키는 구어 표현을 2~3개씩 넣어 '다시 들어온'과 '재문의' 가 같은 컬럼으로 가게 할 것, (2) 폐기된 옛 카테고리 표기(deprecated=true) 2건, (3) 어느 코드로도 못 가는 미매핑 항목(maps_to=null) 2건, (4) source_header 는 고객사 엑셀의 한글 머리글 그대로이고 없으면 null. JSON 배열만.{"type":"object","properties":{"term":{"type":"string"},"maps_to":{"type":["string","null"]},"kind":{"type":"string","enum":["column","category","value"]},"source_header":{"type":["string","null"]},"deprecated":{"type":"boolean"}},"required":["term","maps_to","kind","source_header","deprecated"]}운영팀이 실제로 물을 법한 한국어 질문과 그 정답 조건입니다. 조건 파싱 결과를 규칙으로 자동 채점하는 기준이며, 기준이 없는 어구가 든 질문에는 되물어야 할 항목을 함께 적어 둡니다.
| q_id | question | gold_time_range | gold_filters | gold_group_by | gold_metric | expect_unresolved |
|---|---|---|---|---|---|---|
| Q01 | 지난주 봇이 답한 환불 문의 중 사흘 안에 다시 들어온 건 몇 건이야 | {"from":"2026-03-02","to":"2026-03-08"} | [{"field":"handled_by","op":"eq","value":"bot"},{"field":"category_code","op":"eq","value":"c_refund"},{"field":"reopened_within_72h","op":"eq","value":"true"}] | [] | count | [] |
| Q02 | 3월에 상담사에게 넘어간 대화를 사유별로 세어 줘 | {"from":"2026-03-01","to":"2026-03-31"} | [{"field":"handled_by","op":"eq","value":"handoff"}] | ["handoff_reason"] | count | [] |
| Q03 | 최근에 만족도 낮은 대화 좀 보여줘 | — | [{"field":"csat_1to5","op":"lte","value":"2"}] | [] | list | ["최근","낮은"] |
| Q04 | 채널별로 봇이 처리한 비율 알려줘 3월 첫째 주 기준 | {"from":"2026-03-01","to":"2026-03-07"} | [{"field":"handled_by","op":"eq","value":"bot"}] | ["channel"] | ratio | [] |
| Q05 | 카테고리가 안 잡힌 문의만 뽑아 줘 | — | [{"field":"category_code","op":"is_null","value":""}] | [] | list | [] |
아래 Zod 스키마에 맞는 질문 골드셋 40건 JSON 배열을 conversations 컬럼 기준으로 생성하세요.
{"type":"object","properties":{"q_id":{"type":"string"},"question":{"type":"string"},"gold_time_range":{"type":["object","null"],"properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"]},"gold_filters":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"op":{"type":"string"},"value":{"type":"string"}},"required":["field","op","value"]}},"gold_group_by":{"type":"array","items":{"type":"string"}},"gold_metric":{"type":"string","enum":["count","ratio","avg","list"]},"expect_unresolved":{"type":"array","items":{"type":"string"}}},"required":["q_id","question","gold_time_range","gold_filters","gold_group_by","gold_metric","expect_unresolved"]}
조건: (1) 기간 표현을 '지난주'·'3월 첫째 주'·'3월 5일부터'·기간 없음으로 골고루 섞을 것, (2) 기준이 없는 어구('최근'·'많이'·'문제 있던'·'낮은')가 든 질문 8건은 expect_unresolved 에 그 어구를 적고 gold_time_range 를 null 로 둘 것, (3) 로그에 없는 지표를 묻는 질문 3건은 gold_filters 를 비우고 expect_unresolved 에 그 말을 넣을 것, (4) 카테고리는 별칭 표기('배송 문의'·'환불')로 물어도 gold_filters 의 value 는 표준 코드(c_delivery·c_refund)로 적을 것, (5) gold_group_by 가 있는 질문 10건 포함. JSON 배열만.{"type":"object","properties":{"q_id":{"type":"string"},"question":{"type":"string"},"gold_time_range":{"type":["object","null"],"properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"]},"gold_filters":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"op":{"type":"string"},"value":{"type":"string"}},"required":["field","op","value"]}},"gold_group_by":{"type":"array","items":{"type":"string"}},"gold_metric":{"type":"string","enum":["count","ratio","avg","list"]},"expect_unresolved":{"type":"array","items":{"type":"string"}}},"required":["q_id","question","gold_time_range","gold_filters","gold_group_by","gold_metric","expect_unresolved"]}| TypeScript | 질문 파서와 조건 스키마, SQL 빌더, 근거 매칭까지 도구 전체를 쓰는 주 언어이며 Zod 로 조건 형식을 강제합니다 |
| Node.js | 터미널 진입점(`kaiq ask`·`kaiq run`)과 CSV 적재 워커를 돌리는 런타임입니다 |
| LLM | 한국어 문장을 기간·필터·집계 기준으로 바꾸는 변환 한 단계에만 쓰고 숫자 계산에는 관여시키지 않습니다 |
| Claude | 질문을 조건 JSON 으로 바꾸고 기준이 없는 어구를 확인 질문으로 빼는 기본 모델입니다 |
| PostgreSQL | 고객사에서 받은 CSV 를 그대로 적재하는 로컬 조회 저장소이며 생성된 SQL 이 실제로 실행되는 곳입니다 |
✓ pass → 그대로 멈춰도 완결입니다. 더 키우고 싶으면 2주차(골드셋 채점과 되묻기)로 진행합니다.
✓ pass → 여기서 멈춰도 완결입니다. 더 키우면 3주차(질문 저장과 반복 조회)로 진행합니다.
질문 한 건을 (1) 조회 조건과 그 근거가 된 질문 어절, (2) 기준이 없어 되물어야 할 어구, (3) 승인 뒤 실행한 SQL 과 결과 행, (4) 결과에 들어간 대화의 원문 근거로 풀어 담습니다. 조건을 사람이 확인해야 실행되는 한 바퀴가 한 스키마에 들어갑니다.
{"type":"object","properties":{"question":{"type":"string"},"asked_at":{"type":"string"},"plan":{"type":"object","properties":{"time_range":{"type":["object","null"],"properties":{"from":{"type":"string"},"to":{"type":"string"},"source_phrase":{"type":"string"}},"required":["from","to","source_phrase"]},"filters":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"op":{"type":"string","enum":["eq","neq","in","gte","lte","is_null","not_null"]},"value":{"type":"string"},"source_phrase":{"type":"string"}},"required":["field","op","value","source_phrase"]}},"group_by":{"type":"array","items":{"type":"string"}},"metric":{"type":"string","enum":["count","ratio","avg","list"]}},"required":["time_range","filters","group_by","metric"]},"unresolved":{"type":"array","items":{"type":"object","properties":{"phrase":{"type":"string"},"reason":{"type":"string"},"ask":{"type":"string"}},"required":["phrase","reason","ask"]}},"confirmed":{"type":"boolean"},"sql":{"type":["string","null"]},"rows":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"number"},"sample_conv_ids":{"type":"array","items":{"type":"string"}}},"required":["key","value","sample_conv_ids"]},"maxItems":20},"evidence":{"type":"array","items":{"type":"object","properties":{"conv_id":{"type":"string"},"field":{"type":"string"},"raw_value":{"type":"string"},"matched_filter":{"type":"string"}},"required":["conv_id","field","raw_value","matched_filter"]}}},"required":["question","asked_at","plan","unresolved","confirmed","sql","rows","evidence"]}[
{
"input_ref": "질문 \"지난주 봇이 답한 환불 문의 중 사흘 안에 다시 들어온 건 몇 건이야\" (기준일 2026-03-10, 담당자가 조건 표를 확인하고 승인한 경우)",
"output": {
"question": "지난주 봇이 답한 환불 문의 중 사흘 안에 다시 들어온 건 몇 건이야",
"asked_at": "2026-03-10 09:20",
"plan": {
"time_range": {
"from": "2026-03-02",
"to": "2026-03-08",
"source_phrase": "지난주"
},
"filters": [
{
"field": "handled_by",
"op": "eq",
"value": "bot",
"source_phrase": "봇이 답한"
},
{
"field": "category_code",
"op": "eq",
"value": "c_refund",
"source_phrase": "환불 문의"
},
{
"field": "reopened_within_72h",
"op": "eq",
"value": "true",
"source_phrase": "사흘 안에 다시 들어온"
}
],
"group_by": [],
"metric": "count"
},
"unresolved": [],
"confirmed": true,
"sql": "SELECT count(*) AS value FROM conversations WHERE started_at >= '2026-03-02' AND started_at < '2026-03-09' AND handled_by = 'bot' AND category_code = 'c_refund' AND reopened_within_72h IS TRUE",
"rows": [
{
"key": "total",
"value": 2,
"sample_conv_ids": [
"C0007"
]
}
],
"evidence": [
{
"conv_id": "C0007",
"field": "category_raw",
"raw_value": "환불",
"matched_filter": "category_code=c_refund (사전이 별칭 '환불' 을 표준 코드로 묶음)"
},
{
"conv_id": "C0007",
"field": "reopened_within_72h",
"raw_value": "true",
"matched_filter": "reopened_within_72h=true"
}
]
}
},
{
"input_ref": "질문 \"최근에 만족도 낮은 대화 좀 보여줘\" (기준이 없는 어구가 있어 실행 전에 되묻는 경우)",
"output": {
"question": "최근에 만족도 낮은 대화 좀 보여줘",
"asked_at": "2026-03-10 09:41",
"plan": {
"time_range": null,
"filters": [
{
"field": "csat_1to5",
"op": "lte",
"value": "2",
"source_phrase": "만족도 낮은"
}
],
"group_by": [],
"metric": "list"
},
"unresolved": [
{
"phrase": "최근",
"reason": "기간의 시작과 끝이 정해지지 않았습니다",
"ask": "최근을 지난 7일로 볼까요, 이번 달 전체로 볼까요"
},
{
"phrase": "낮은",
"reason": "만족도 기준값이 질문에 없어 2점 이하로 임시 해석했습니다",
"ask": "만족도 낮음을 2점 이하로 볼까요, 3점 이하로 볼까요"
}
],
"confirmed": false,
"sql": null,
"rows": [],
"evidence": []
}
},
{
"input_ref": "질문 \"요즘 봇이 못 받은 문의 얼마나 돼?\" (기준일 2026-03-10, 담당자 확인 전)",
"output": {
"question": "요즘 봇이 못 받은 문의 얼마나 돼?",
"asked_at": "2026-03-10 14:05",
"plan": {
"time_range": null,
"filters": [
{
"field": "handled_by",
"op": "neq",
"value": "bot",
"source_phrase": "봇이 못 받은"
}
],
"group_by": [],
"metric": "count"
},
"unresolved": [
{
"field": "time_range",
"question": "'요즘' 이 며칠부터인지 정해지지 않았습니다. 기간을 지정해 주십시오."
}
],
"confirmed": false,
"sql": "",
"rows": [],
"evidence": []
}
}
]자동: (1) Zod parse, (2) 모든 source_phrase 가 질문 원문의 substring 인지 검사, (3) filters 의 field 가 conversations 실제 컬럼이고 value 가 표준 코드인지 사전으로 대조, (4) unresolved 가 비어 있지 않으면 confirmed=false 이고 sql 이 null 인지 확인해 승인 없이 실행되는 경로를 차단, (5) 골드셋 채점은 plan 을 gold_time_range·gold_filters·gold_group_by·gold_metric 과 필드 단위 완전 일치로 비교하고 부분 점수를 주지 않습니다. 조건 추출만 모델이 하고 집계 수치는 SQL 결과라 모델이 숫자를 만들지 않습니다. 수동: 되묻기 문구가 담당자가 실제로 답할 수 있는 질문인지 40건을 눈으로 확인하며, 이 확인은 작성자가 대신 하므로 합성 기준입니다.
근거: 한 고객사의 에이전트를 직접 빌드하고, 그 에이전트가 약속한 운영 지표를 실제로 움직일 때까지