검색 키워드 품질 평가 에이전트 — Hybrid Reflection Loop 설계
왜 만들었나
Danble 검색 시스템에는 발골사전(search_keywords 테이블) 이라는 게 있다. 사용자가 "폴로 랄프로렌 반팔티"를 검색하면, 이 사전에서 "폴로 랄프로렌"을 브랜드로, "반팔티"를 카테고리로 인식해 필터를 거는 구조다.
문제는 이 사전에 없거나 잘못 등록된 키워드가 검색되면 결과가 조용히 망가진다는 것이다.
- 검색어가 발골사전에 없으면 → 결과 0건. 사용자는 빈 화면을 본다.
- 일부만 인식되면 → 폴백 텍스트 매칭으로 엉뚱한 상품이 섞인다.
- 이미 등록됐지만 타입이 잘못됐으면 → AND 조건 과잉 필터링으로 결과가 없어진다.
- FK가 깨진 링크 레코드가 남아있으면 → 등록은 됐는데 브랜드 필터가 오동작한다.
result_count > 0이라 겉으로는 정상처럼 보이지만, 검색 품질이 실제로 떨어지고 있는 상황들이다. 그리고 이런 문제를 사람이 하나씩 모니터링하는 건 수천 개의 키워드 앞에서 현실적이지 않다.
그래서 만든 게 skqa-agent — Search Keyword Quality Agent. 매일 데이터를 수집하고, 10가지 유형의 문제를 탐지하고, 수정 SQL과 rollback SQL을 만들어주는 에이전트다.
핵심 설계 원칙 3가지
1. 비파괴적 (Non-Destructive)
에이전트는 절대로 DB에 직접 쓰지 않는다. SQL 파일만 만들고, 사람이 검토 후 수동 실행한다.
에이전트 출력 → reports/2026-05-11/fix_search_keywords.sql
사람이 검토 → "이거 맞네" → psql < fix_search_keywords.sql
AI가 아무리 잘해도 프로덕션 DB에 직접 쓰는 건 내 원칙이 아니다. 에이전트는 "검토할 재료"를 만드는 역할이고, 실행 결정은 사람이 한다.
2. 1-Fetch 아키텍처
DB는 하루 1번만 쿼리한다. 이후 모든 분석은 로컬 CSV 파일로만 한다.
오전 실행 → PostgreSQL + ClickHouse에서 CSV 수집
이후 분석 → 전부 DuckDB로 로컬에서
분석 중에 수십 번 쿼리가 발생해도 CSV를 읽는 거라 실 DB는 안 건드린다. 운영 DB에 부하를 주지 않으면서도 자유롭게 탐색할 수 있는 구조다.
3. 역할 분리
"탐지하는 사람"과 "SQL 쓰는 사람"과 "검증하는 사람"이 다르다.
Analyst (탐지만) → "폴로가 미등록이에요"
Generator (생성만) → "그러면 INSERT INTO ... 이렇게 써야 해요"
Reviewer (검증만) → "잠깐, 폴로는 카테고리도 있어서 BRAND 단독은 안 될 것 같아요"
같은 역할이 탐지하고 SQL을 쓰면 자기 실수를 못 잡는다. 분리하면 잡힌다.
Hybrid Reflection Loop
단계별로 이렇게 흐른다.
Phase 0 (병렬)
├── Code Reader — 백엔드 레포 코드에서 INSERT 패턴, 타입 분류 로직 추출
├── Schema Inspector — PG information_schema에서 컬럼·FK·UNIQUE 제약 수집
└── Trend Analyzer — ClickHouse 30일 검색 트렌드 집계
Phase 1 — DB에서 CSV 15개 수집 (PostgreSQL 9개 + ClickHouse 4개 + 파생 2개)
Phase 2 — Analyst (AI)
→ DuckDB MCP로 CSV 자유 조회
→ 10가지 케이스 탐지 → issues.json 출력
Phase 3 — Generator (AI)
→ issues.json 보고 fix SQL + rollback SQL 작성
→ TestRunner (코드, 기계적 검증)
FAIL → Generator 재시도
PASS ↓
Phase 4 — Reviewer (AI)
→ 5차원 루브릭으로 이슈마다 채점
→ PASS / RETRY_GENERATOR / RETRY_ANALYST / ESCALATE
"Hybrid"라고 부르는 이유는 **기계적 검증(TestRunner)**과 **AI 검증(Reviewer)**을 모두 거치기 때문이다.
TestRunner는 빠르고 확실하다. FK 깨짐, UNIQUE 위반, rollback 역방향 누락 같은 "수학적으로 틀린 것"을 잡는다. Reviewer는 느리지만 맥락을 이해한다. 키워드 타입 오분류, 의미적 오류, 사이드이펙트 같은 "도메인 지식이 필요한 것"을 잡는다.
두 단계를 모두 통과해야 최종 PASS가 된다. 그리고 Reviewer가 같은 피드백을 2회 연속 주면 강제 ESCALATE — 무한 루프 방지다.
탐지하는 10가지 케이스
| 케이스 | 문제 상황 |
|---|---|
| A | 검색해도 결과 0건. 키워드가 미등록 + 폴백도 실패 |
| B | brand_id / variant_id FK 참조가 깨진 데이터 오류 |
| C | "나이키 반팔"처럼 브랜드+카테고리 복합어가 누락 |
| D | 상품명 검색이 활성화돼야 하는 키워드에 name_search_tokens 없음 |
| E | 소재 키워드(면, 울 등)가 variant에 연결되지 않아 필터 안 걸림 |
| F | 결과는 나오지만 발골 최적화 안 된 미등록 키워드 |
| G | 미등록 키워드의 노출 상품이 특정 브랜드에 70%+ 집중 → 브랜드 의도 |
| H | 이미 등록됐지만 zero_result 비율 30%+ → 잘못된 타입으로 AND 과잉 필터링 |
| I | COLOR 키워드 + variant_ids={} → LINE 148 조기종료 → 결과 0건 |
| J | CATEGORY 등록 + 링크 테이블 없음 → phantom match → LINE 148 → 0건 |
Case I, J는 "LINE 148 트랩"이라고 부른다. 등록은 됐는데 연결 테이블 데이터가 빠져서 if normalized_query and not has_extraction() and not ctx.unmatched 조건이 True가 되어 검색이 0건으로 조기종료되는 케이스다.
누적 학습 시스템
매일 실행하면 "폴로"처럼 애매한 키워드가 매번 이슈로 올라온다. 담당자가 한 번 검토한 키워드는 기억해서 다음부터 재등장하지 않도록 reviewed_keywords.json으로 관리한다.
{
"excluded": [
{ "keyword": "폴로", "reason": "브랜드/카테고리 혼재. remaining 유지 결정." }
],
"confirmed": [
{ "keyword": "ck", "action": "BRAND, brand_id=468 (Calvin Klein)" }
],
"whitelist": [
{ "keyword": "나이키", "reason": "CTR 23%, zero_result 0%, 안정적 등록 상태" }
]
}
- excluded: 이슈로 올리지 않는다
- confirmed: 방향이 결정됐으니 바로 처리한다
- whitelist: 잘 등록된 키워드. CTR 급락이나 zero_result 급등 시에만 자동으로 재검증 대상에 포함
이 파일이 쌓일수록 에이전트가 내놓는 이슈의 품질이 올라간다.
Reviewer 5차원 루브릭
Reviewer는 이슈마다 5개 차원으로 채점한다.
| 차원 | 평가 내용 |
|---|---|
| D1 타입분류정확성 | keyword_type 분류의 비즈니스 정합성 (BLOCKING) |
| D2 이슈해결력 | 탐지한 이슈가 SQL로 실제 해결되는지 (BLOCKING) |
| D3 사이드이펙트안전성 | 다른 키워드/관계에 의도치 않은 영향 여부 (BLOCKING) |
| D4 데이터정합성 | FK, UNIQUE, 배열 형식, NULL 처리 (BLOCKING) |
| D5 영향도산출완전성 | 수정 전후 검색 결과 변화 예측, rollback 완전성 |
D1~D4는 BLOCKING 차원이다. 결함이 발견되면 해당 이슈는 REJECTED. D5만 결함이면 PASS 가능하되 미산출 항목을 명시해야 한다.
최종 결과물
reports/2026-05-11/
├── daily_summary.html ← 당일 전체 시도 요약
└── attempt_01/
├── report.md ← Reviewer 검증 리포트
├── report.html ← 다크테마 HTML 리포트
├── fix_search_keywords.sql
├── rollback_search_keywords.sql
├── issues.json
└── meta.json
같은 날 여러 번 실행하면 attempt_01/, attempt_02/ ... 로 누적된다. 담당자는 daily_summary.html에서 모든 시도의 verdict·점수·이슈 수를 한 눈에 비교하고, SQL을 검토한 뒤 수동으로 실행한다.
버전 히스토리
| 버전 | 주요 내용 |
|---|---|
| v1 | 최초 구현. Hybrid Loop, 4개 프롬프트, 1차 파이프라인 완주 (15건 탐지) |
| v2 | ClickHouse 테이블 전면 교체 (app_events_v2), PG 컬럼명 실측 교정 |
| v3 | 폴백 관련성 검증 추가. 누적 제외 목록 시스템 |
| v4 | CTR 데이터 수집, Case G·H 추가, whitelist 3-tier, 다크테마 HTML 리포트, Reviewer 5차원 루브릭 채점 |
| v4.1 | LINE 148 트랩 탐지 (Case I/J). Reviewer BLOCKING 방식 전면 개정. Code Reader 확장 |