Implement Phase 12 feedback, Phase 13 Semantic Chunker, Phase 13-B Reranker, Bug 5 thinking fix

- Phase 12: FeedbackRepository + td_feedback 테이블, Gradio 👍/👎 이벤트, run_id 추적, LangSmith create_feedback() 연동
- Phase 13: 커스텀 _SemanticSplitter 제거 → langchain_experimental.SemanticChunker 교체, buffer_size/threshold_type 환경변수 적용
- Phase 13-B: RerankService (Cross-Encoder), RetrieverService.search()에 reranker 통합, tools.py as_retriever() → search() 전환
- Bug 5: mlx_chat_model enable_thinking 런타임 오버라이드, agent_service stream_mode=["messages","custom"] 이중 스트림, thinking 토큰 custom 이벤트로 emit
- ROADMAP: LLM 모델명 8B 반영, RAG에 Reranker 추가, 추천 진행 순서 갱신

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sal
2026-05-29 17:41:36 +09:00
parent e1d7e9cc21
commit 145b0cc96f
13 changed files with 469 additions and 143 deletions
+103 -52
View File
@@ -4,10 +4,11 @@
| 영역 | 현황 |
|------|------|
| LLM | Qwen3-14B-4bit (MLX, Apple Silicon) |
| LLM | Qwen3-8B-4bit (MLX, Apple Silicon) |
| Agent | LangGraph ReAct + Tool Calling + Thinking 모드 |
| RAG | Qdrant + BAAI/bge-m3 임베딩 + Semantic Chunking (`_SemanticSplitter`) |
| RAG | Qdrant + BAAI/bge-m3 임베딩 + Semantic Chunking (`SemanticChunker`) + Reranker (BAAI/bge-reranker-v2-m3) |
| Tools | `search_documents`, `web_search`, `get_current_date`, `remember_user_info`, `recall_user_info` (5개) |
| Feedback | Gradio 👍/👎 → `td_feedback` DB 저장 + LangSmith `create_feedback()` 연동 |
| UI | CLI + Gradio Web UI + 음성 입력(STT)/출력(TTS) |
| Memory | LangGraph MemorySaver (세션 내) + MySQL 대화 저장 + 장기 사용자 프로필 |
| Tracing | LangSmith 트레이싱 |
@@ -31,6 +32,13 @@ DB 스키마(`td_conversations.user_id`, `td_user_profile.user_id`)는 `_migrate
### ✅ 버그 4 — 나이 계산 오류 (수정 완료)
LLM이 훈련 데이터 기준 연도로 나이를 계산하는 문제. `AgentService.call_model()`에서 매 호출 시 시스템 프롬프트 앞에 `오늘 날짜: {date.today().isoformat()}`를 주입. 프로필에서 생년월일/생년 값을 파싱해 한국 나이(현재연도-출생연도+1)와 만 나이(생일 기준 정확 계산)를 자동 계산해 시스템 프롬프트에 포함.
### ✅ 버그 5 — 사고 과정(thinking) 체크박스 무효 (수정 완료)
ON/OFF와 무관하게 사고 과정이 표시되지 않던 버그.
- `call_model` 내부에서 `get_stream_writer()`로 thinking 토큰을 custom 이벤트로 emit → 답변 앞에 먼저 스트리밍
- 체크박스 값을 LangGraph configurable → `llm_with_tools.bind(enable_thinking=...)` 로 모델 레벨까지 전달 (`.env` `ENABLE_THINKING` 설정과 독립)
- `stream_response` 루프를 `stream_mode=["messages", "custom"]` 이중 스트림으로 전환
- `self._think_verbose` 인스턴스 변수 참조 버그 수정 (`_think_verbose` 로컬 변수 사용)
---
## ✅ Phase 4 — Web UI (Gradio)
@@ -97,43 +105,42 @@ turns = conversation_repository.load_turns_after(self._conv_id, None, limit=10)
---
## Phase 12 — 답변 피드백 & 품질 개선 ★★☆
## Phase 12 — 답변 피드백 & 품질 개선
**배경**: 에이전트가 잘못된 답변을 해도 피드백 루프가 없어 개선이 어려움.
**구현 범위**:
- Gradio 채팅 메시지마다 👍 / 👎 버튼
- `td_feedback` 테이블에 메시지·평점 저장
- LangSmith의 `run_id`와 연결해 피드백을 트레이스에 기록 (`langsmith.Client().create_feedback()`)
```sql
CREATE TABLE td_feedback (
id INT AUTO_INCREMENT PRIMARY KEY,
message TEXT,
response TEXT,
rating TINYINT, -- 1: 좋음, -1: 나쁨
langsmith_run_id VARCHAR(100),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
```
**구현 내용**:
- Gradio Chatbot 메시지마다 👍 / 👎 버튼 (`chatbot.like()` 이벤트)
- `td_feedback` 테이블에 `user_id`, 질문, 답변, 평점 저장 (`FeedbackRepository`)
- `AgentService`에서 응답마다 `run_id`(UUID)를 LangChain config에 주입 → `last_run_id` property로 노출
- `run_ids_state`(gr.State)로 대화 턴별 `run_id` 추적
- LangSmith `Client().create_feedback()` 연동 (트레이싱 활성화 시 자동 기록)
**난이도**: 중간 | **임팩트**: 중간 (장기 품질 향상)
---
## Phase 13 — RAG 품질 향상 ★★ (부분 완료)
## Phase 13 — RAG 품질 향상 ★★ (완료)
**배경**: 고정 크기 청킹 + 벡터 유사도 검색만으로는 관련 없는 청크가 섞일 수 있음.
**✅ Semantic Chunker — 완료**
- `_SemanticSplitter` 클래스 직접 구현 (`services/rag/ingestion_service.py`)
- `langchain-experimental` 사용 없이 numpy + 기존 BAAI/bge-m3 임베딩으로 구현
- 인접 문장 간 코사인 유사도 계산 → 유사도 하위 5% 지점에서 청크 분리
- `config.py`에서 `rag_chunk_size` / `rag_chunk_overlap` 제거 → `semantic_breakpoint_threshold_type` 추가
**🔲 미완 — Reranker**
1. **Reranker 추가**`cross-encoder/ms-marco-MiniLM-L-6-v2`로 검색 결과 재순위
2. **top_k 조정** — 검색 후 rerank → 상위 3개만 LLM에 전달
커스텀 `_SemanticSplitter`를 제거하고 `langchain_experimental.SemanticChunker`로 교체 (`services/rag/ingestion_service.py`).
기존에 무시되던 `semantic_breakpoint_threshold_type` 설정이 이제 실제로 적용된다.
| 기능 | 지원 여부 |
|------|----------|
| breakpoint_threshold_type | ✅ percentile / standard_deviation / interquartile / gradient |
| buffer_size | ✅ `SEMANTIC_BUFFER_SIZE` 환경변수로 설정 |
| min_chunk_size | ✅ (SemanticChunker 기본 지원) |
| HuggingFaceEmbeddings 재사용 | ✅ 기존 임베딩 모델 그대로 사용 |
> **langchain-experimental 패키지 상태**:
> `langchain-experimental` v0.4.2는 공식 유지보수 종료가 선언됐지만([#87](https://github.com/langchain-ai/langchain-experimental/issues/87)),
> `SemanticChunker` 자체는 현재 정상 동작하며 후속 패키지(`langchain-text-splitters`)로 이전 완료 시 migration 예정.
**✅ 미완 1 — Semantic Chunker 기능 완성 (완료)**
> 기존 Qdrant 저장 문서는 재등록해야 새 청킹 방식이 적용됨.
@@ -157,26 +164,67 @@ CREATE TABLE td_feedback (
---
## Phase 15 — 예방접종·건강검진 알림 스케줄러 ★★☆
## Phase 13-B — Reranker ★★☆
**배경**: 아이 생년을 기억하고 있으므로, 예방접종 일정(BCG, DTaP 등)을 자동 계산해 알림을 줄 수. 율봇의 차별화 포인트.
**배경**: 벡터 유사도 검색은 의미적으로 비슷한 청크를 가져오지만, 질문과 실제로 관련 있는 청크를 정확히 가려내지 못하는 경우가. Reranker는 검색 후 순위를 재조정해 LLM에 전달되는 컨텍스트 품질을 높인다.
**구현 방식**:
- `td_user_profile`에서 아이 생년 조회 → 예방접종 스케줄 계산 Tool
- Gradio "건강 일정" 탭: 달력형 일정 표시
- APScheduler로 당일 알림 (또는 Gradio 시작 시 오늘 일정 배너)
**구현 내용**:
- `services/rag/rerank_service.py``RerankService` 클래스 (Cross-Encoder 래퍼)
- `RetrieverService.search()`: reranker 활성화 시 `rerank_fetch_k`(기본 10)개 후보 검색 → rerank → 상위 `rag_top_k`(기본 3)개 반환
- `tools.py` `make_retriever_tool`: `as_retriever()``search()` 직접 호출로 변경 (reranker 자동 적용)
- `.env` `RERANKER_ENABLED=true`로 활성화, 기본 비활성 (첫 실행 시 모델 다운로드)
```python
@tool
def get_vaccination_schedule(birth_year: int, birth_month: int) -> str:
"""아이 생년월을 기반으로 예방접종 일정을 계산합니다."""
```
| 설정 | 기본값 | 설명 |
|------|--------|------|
| `RERANKER_ENABLED` | `false` | `true`로 설정 시 활성화 |
| `RERANKER_MODEL_ID` | `cross-encoder/mmarco-mMiniLMv2-L12-H384-v1` | 한국어 포함 다국어 모델 (117MB) |
| `RERANKER_FETCH_K` | `10` | rerank 전 벡터 검색 후보 수 |
**난이도**: 중간 | **임팩트**: 높음 (육아 특화 차별화)
**난이도**: 중간 | **임팩트**: 높음 (관련성 낮은 청크 필터링 → 답변 정확도 향상)
---
## Phase 16모델 선택 (Claude API / OpenAI 옵션) ★
## Phase 18Hybrid Search (BM25 + Vector) ★
**배경**: 한국어 질문에서 고유명사·전문용어가 포함된 경우 의미 검색(Dense)만으로는 recall이 떨어진다. BM25 키워드 검색과 결합(Hybrid)하면 보완이 가능하다.
**구현 방식**:
- Qdrant의 Sparse Vector 지원 활용 (`FastEmbedSparseEmbeddings` 또는 BM42)
- 인덱싱 시 dense + sparse 두 벡터 동시 저장
- 검색 시 `RRF(Reciprocal Rank Fusion)`로 결과 통합
- `IngestionService`, `RetrieverService` 양쪽 수정 필요
**난이도**: 중간 | **임팩트**: 높음 (키워드 포함 질문 recall 대폭 향상)
---
## Phase 19 — Query Rewriting ★☆☆
**배경**: 사용자 구어체 질문("아이가 밥을 안 먹어요")은 벡터 검색에 최적화되어 있지 않다. LLM이 검색 전에 질문을 재작성하면 관련 문서 검색 확률이 높아진다.
**구현 방식**:
- LangGraph에 `query_rewrite` 노드 추가 (agent → query_rewrite → tools 순서)
- 또는 `search_documents` 도구 내부에서 rewrite 후 검색
- 프롬프트: "다음 질문을 문서 검색에 최적화된 키워드 중심 문장으로 변환하세요"
**난이도**: 하 | **임팩트**: 중간 (구어체 질문 검색 품질 향상)
---
## Phase 20 — RAG 품질 자동 평가 (RAGAS) ★☆☆
**배경**: 청킹 전략·검색 파라미터·Reranker 변경 시 답변 품질이 실제로 나아졌는지 수치로 확인할 방법이 없다.
**구현 방식**:
- `ragas` 라이브러리로 Faithfulness·Answer Relevancy·Context Recall 자동 측정
- 테스트 질문-정답 셋을 `eval/` 디렉터리에 관리
- 설정 변경 후 `python eval/run_ragas.py`로 비교 가능
**난이도**: 중간 | **임팩트**: 중간 (장기 품질 관리 기반)
---
## Phase 15 — 모델 선택 (Claude API / OpenAI 옵션) ★☆☆
**배경**: 로컬 MLX 모델은 Apple Silicon 전용. 원격 접속 시나리오나 더 높은 품질이 필요할 때 Claude API/OpenAI를 선택할 수 있으면 유연성 확보.
@@ -190,7 +238,7 @@ model_provider: str = "mlx" # "mlx" | "claude" | "openai"
---
## Phase 17 — Docker 컨테이너화 ★☆☆
## Phase 16 — Docker 컨테이너화 ★☆☆
**배경**: 현재 로컬 전용. 가족이나 지인도 쓸 수 있도록 서버 배포 가능한 형태로 패키징.
@@ -202,13 +250,13 @@ docker-compose.yml
└── mysql
```
> 주의: MLX는 Apple Silicon 전용이라 서버 배포 시 Phase 16(모델 선택)이 선행되어야 함.
> 주의: MLX는 Apple Silicon 전용이라 서버 배포 시 Phase 15(모델 선택)이 선행되어야 함.
**난이도**: 높음 | **임팩트**: 중간
---
## Phase 18 — 멀티모달 이미지 이해 ★☆☆
## Phase 17 — 멀티모달 이미지 이해 ★☆☆
**배경**: 이유식 사진 → 재료 분석, 금융 서류 사진 → 내용 해석 등.
@@ -221,10 +269,10 @@ docker-compose.yml
## 추천 진행 순서
```
단기 (1~2주) 중기 (1개월) 장기
──────────────── ────────────────── ──────────────
Phase 15 (알림) → Phase 13 Reranker → Phase 17 (Docker)
Phase 12 (피드백) Phase 16 (모델선택) Phase 18 (멀티모달)
단기 (1~2주) 중기 (1개월) 장기
──────────────────────── ────────────────────── ──────────────────
Phase 18 Hybrid Search → Phase 15 (모델선택) → Phase 16 (Docker)
Phase 19 Query Rewriting Phase 20 (RAGAS 평가) Phase 17 (멀티모달)
```
### 우선순위 매트릭스
@@ -235,6 +283,7 @@ Phase 12 (피드백) Phase 16 (모델선택) Phase 18 (멀티모달)
| 버그 2 이력 미연동 | ✅ 완료 | — | — | — |
| 버그 3 단일 사용자 | ✅ 완료 | — | — | — |
| 버그 4 나이 계산 오류 | ✅ 완료 | — | — | — |
| 버그 5 thinking 체크박스 무효 | ✅ 완료 | — | — | — |
| Phase 4 Web UI | ✅ 완료 | — | — | — |
| Phase 5 장기 사용자 메모리 | ✅ 완료 | — | — | — |
| Phase 6 웹 검색 | ✅ 완료 | — | — | — |
@@ -242,11 +291,13 @@ Phase 12 (피드백) Phase 16 (모델선택) Phase 18 (멀티모달)
| Phase 9 문서 관리 | ✅ 완료 | — | — | — |
| Phase 10 멀티유저 | ✅ 완료 | — | — | — |
| Phase 11 이력 복원 | ✅ 완료 | — | — | — |
| Phase 12 피드백 | ✅ 완료 | — | — | — |
| Phase 13 Semantic Chunker | ✅ 완료 | — | — | — |
| Phase 14 음성 인터페이스 | ✅ 완료 | — | — | — |
| Phase 15 예방접종 알림 | 🔲 미완 | 중간 | 높음 | ⭐ 1순위 |
| Phase 12 피드백 | 🔲 미완 | 중간 | 중간 | 2순위 |
| Phase 13 Reranker | 🔲 진행 중 | 중간 | 중간 | 3순위 |
| Phase 16 모델 선택 | 🔲 미완 | 중간 | 중간 | 4순위 |
| Phase 17 Docker | 🔲 미완 | 높음 | 중간 | 5순위 |
| Phase 18 멀티모달 | 🔲 미완 | 높음 | 높음 | 6순위 |
| Phase 13-B Reranker | ✅ 완료 | | | |
| Phase 18 Hybrid Search | 🔲 신규 | 중간 | 높음 | ⭐ 1순위 |
| Phase 19 Query Rewriting | 🔲 신규 | | 중간 | 3순위 |
| Phase 15 모델 선택 | 🔲 미완 | 중간 | 중간 | 4순위 |
| Phase 20 RAGAS 평가 | 🔲 신규 | 중간 | 중간 | 5순위 |
| Phase 16 Docker | 🔲 미완 | 높음 | 중간 | 6순위 |
| Phase 17 멀티모달 | 🔲 미완 | 높음 | 높음 | 7순위 |