2026.09 ~ 진행 중 · 1인 개발 · 캡스톤디자인 2 · GitHub
한눈에 보기
- 무엇: 대화 속 "팀장 → 김팀장 → 그 사람 → 부장님(승진 후)" 같은 호칭 변이를 한 인물로 묶어 인물 메모리를 쌓는 에이전트의 백엔드
- 핵심: pgvector 후보 검색 → 규칙 필터 → LLM 판정 → 확신도 분기의 엔티티 해석 4단계, 확신이 부족하면 자동으로 합치지 않고 사용자에게 묻기
- 규모: 테이블 9개 · 툴 7종 · 비교 방식 5종 · 평가 시나리오 40건 · pytest 918 passed(테스트 함수 661개, LLM · 임베딩은 스텁)
- 현재 상태: 스키마 · 툴 · 엔티티 해석 · 비교 방식 · 평가 데이터 완료 / 파일럿 평가 · 에이전트 루프 · 프론트 · 배포는 다음 단계
한국어 대화에서는 사람을 이름으로 잘 부르지 않습니다. 같은 사람이 직함, 대명사, 승진 후 호칭으로 계속 바뀌어 나옵니다. 대화에서 자동으로 "인물 메모리"를 쌓으려면 이 지칭들을 하나로 묶어야 합니다.
이때 두 가지 실패가 있습니다.
| 실패 | 결과 | 비용 |
|---|---|---|
| 오병합 — 다른 사람을 같은 사람으로 합침 | "이모 이야기를 고모 기억에 저장" → 신뢰가 즉시 무너짐 | 매우 큼 |
| 미검출 — 같은 사람을 못 합침 | 메모리가 여러 조각으로 나뉨 | 상대적으로 작음, 나중에 합칠 수 있음 |
그래서 설계의 출발점을 "오병합은 미검출보다 훨씬 나쁘다"는 비대칭 비용으로 두었습니다. 확신이 기준에 못 미치면 절대 자동으로 합치지 않고 사용자에게 묻습니다.
flowchart TD
IN["호칭 '부장님' + 발화"] --> S1[1. 후보 검색<br/>별칭 일치 ∪ pgvector 유사도 top-K]
S1 --> S2[2. 규칙 필터<br/>관계 태그 · 위계 · 호칭 사전]
S2 -->|통과 후보 0건| ASKN[새 인물인지 묻기]
S2 -->|통과 후보만 전달| S3[3. LLM 판정<br/>구조화 출력 · 후보 밖 id 거부]
S3 --> S4{"4. 확신도<br/>0.5·LLM + 0.3·임베딩 + 0.2·규칙"}
S4 -->|"≥ 0.8"| MERGE[자동 병합<br/>별칭 누적]
S4 -->|"0.3 ~ 0.8"| ASKI["'김팀장님 말씀이신가요?'"]
S4 -->|"< 0.3"| ASKN
| 단계 | 설계 포인트 |
|---|---|
| 1. 후보 검색 | 별칭 정확 · 부분 일치와 임베딩 유사도를 합집합으로. 이 단계에서는 아무도 배제하지 않음 |
| 2. 규칙 필터 | 모순일 때만 배제. 정보가 없는 검사는 점수 분모에서 뺌. 통과 후보가 없고 위계 1칸 차이로만 떨어진 후보가 있으면 승진으로 보고 완화 규칙으로 한 번 더 평가 |
| 3. LLM 판정 | 규칙을 통과한 후보만 전달. "목록에 없는 사람을 지어내지 말고, 확신 없으면 null". 후보 밖 id를 답하면 장애와 구분해 거부 |
| 4. 확신도 분기 | 임계치 두 개로 "합치기 / 누구인지 묻기 / 새 인물인지 묻기"를 구분. LLM 실패 · 후보 없음 · null은 어떤 경로로도 병합 불가 |
판정과 실행의 분리 — resolve()는 인물 데이터를 바꾸지 않고 판정과 판정 기록(trace 1행)만 남깁니다. 실제 변경은 apply_resolution()만 하고, 같은 판정을 두 번 적용하면 예외를 냅니다(중복 질문 방지).
호칭 사전의 작은 함정 — "김팀장"에서 성씨 1글자를 떼면 "팀장"이 됩니다. 그런데 "이모"에서 "이"를 떼면 안 됩니다. 그래서 남은 부분이 사전 표제어일 때만 성씨를 떼도록 했습니다.
호칭 30개 × 모델 2종으로 코사인 유사도 행렬을 만들어 비교했습니다.
| 기준: "팀장↔부장님"이 "팀장↔이모"보다 가까운가 | small | large |
|---|---|---|
| 팀장↔부장님 | 0.529 | 0.531 |
| 팀장↔이모 | 0.257 | 0.342 |
| 여유폭 | +0.272 | +0.190 |
text-embedding-3-small을 선택했습니다.