a05d2f474e
- td_knowledge_graph 테이블 (user_id, subject, relation, object 트리플) - GraphService: MultiDiGraph 인메모리 캐시 + MySQL 영속화 - add_relation / query_entity LangChain 도구 - call_model에 그래프 요약 자동 주입 (시스템 프롬프트) - GRAPH_ENABLED=true 환경변수로 활성화 - requirements.txt에 networkx>=3.0 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
712 lines
33 KiB
Markdown
712 lines
33 KiB
Markdown
# 율봇 개발 로드맵
|
|
|
|
## 현재 구현 상태
|
|
|
|
| 영역 | 현황 |
|
|
|------|------|
|
|
| LLM | Qwen3-8B-4bit (MLX, Apple Silicon) |
|
|
| Agent | LangGraph ReAct + Tool Calling + Thinking 모드 |
|
|
| Scheduler | asyncio task 기반 알림 스케줄러 — D-7/D-1/D-0 Telegram push (`SchedulerService`) |
|
|
| 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`, `set_reminder`, `list_reminders` (7개) |
|
|
| Feedback | Gradio 👍/👎 → `td_feedback` DB 저장 + LangSmith `create_feedback()` 연동 |
|
|
| UI | CLI + Gradio Web UI + 음성 입력(STT)/출력(TTS) |
|
|
| Memory | LangGraph MemorySaver (세션 내) + MySQL 대화 저장 + 장기 사용자 프로필 |
|
|
| Tracing | LangSmith 트레이싱 |
|
|
| Streaming | 비동기 토큰 스트리밍 + 타입별 이벤트 분리 (`__meta` / `__thinking` / `__status`) |
|
|
| 사고 과정 UI | 스트리밍 중 현재 줄 실시간 표시 → 완료 후 접기/펼치기 (`<details>`) |
|
|
| History Compact | 대화 20턴 초과 시 오래된 절반을 LLM으로 자동 요약 (`CompactService`) |
|
|
| 나이 계산 | 시스템 프롬프트에 오늘 날짜 주입 + 한국 나이/만 나이 자동 계산 |
|
|
|
|
---
|
|
|
|
## 버그 수정 현황
|
|
|
|
### ✅ 버그 1 — RAG 중복 수집 (수정 완료)
|
|
`IngestionService._delete_by_source()`를 구현해 같은 파일 경로로 저장된 기존 청크를 `ingest()` 시작 시 삭제한다.
|
|
|
|
### ✅ 버그 2 — LangGraph MemorySaver와 MySQL 이력 미연동 (수정 완료)
|
|
`AgentService.__init__`에서 MySQL에 저장된 최근 10턴을 `_pending_history`로 불러온 뒤, 첫 `stream_response()` 호출 시 LangGraph 초기 메시지로 주입한다.
|
|
|
|
### ✅ 버그 3 — 단일 사용자 전제 (수정 완료)
|
|
DB 스키마(`td_conversations.user_id`, `td_user_profile.user_id`)는 `_migrate_schema`로 자동 마이그레이션. `AgentService`에 `user_id` 파라미터 추가, 모든 Repository 호출에 전파. Gradio에 사용자 선택 드롭다운(아록/근혜/도율/하율) 추가 및 사용자별 에이전트 캐시 구현.
|
|
|
|
### ✅ 버그 4 — 나이 계산 오류 (수정 완료)
|
|
LLM이 훈련 데이터 기준 연도로 나이를 계산하는 문제. `AgentService.call_model()`에서 매 호출 시 시스템 프롬프트 앞에 `오늘 날짜: {date.today().isoformat()}`를 주입. 프로필에서 생년월일/생년 값을 파싱해 한국 나이(현재연도-출생연도+1)와 만 나이(생일 기준 정확 계산)를 자동 계산해 시스템 프롬프트에 포함.
|
|
|
|
### ✅ 버그 6 — TTS가 진행 메시지까지 읽는 문제 (수정 완료)
|
|
|
|
`stream_response()`가 `[LangGraph → agent: ...]`, `문서 검색 중...` 등 진행 메시지와 실제 답변을 동일한 plain string으로 yield해 TTS가 전부 읽던 문제.
|
|
|
|
- `stream_response()` yield 타입 분리: 답변 → `plain str`, 진행/thinking/출처 → `{"__meta": str}` dict
|
|
- thinking 토큰은 별도 `{"__thinking": str}` key 사용
|
|
- `call_model` 시작 직후 `writer({"__start": True})` emit → `{"__status": label}` 변환으로 LLM 추론 전 즉각 피드백
|
|
- `api.py`: `json.dumps(token)` 이 dict/str 모두 처리하므로 변경 없음
|
|
- WebUI `respond()`: `tts_text` 누적 변수 분리, `__meta`·`__thinking` 토큰 제외 후 TTS 전달
|
|
- Telegram `bot.py`: `__meta`·`__thinking` 토큰 skip
|
|
|
|
### ✅ 버그 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)
|
|
|
|
- `app.py` — Gradio ChatInterface + `stream_response()` 연결
|
|
- PDF/TXT 파일 업로드 → 인제스트 버튼
|
|
- 사고 과정(thinking) 표시 토글
|
|
- 대화 초기화 버튼
|
|
|
|
---
|
|
|
|
## ✅ Phase 5 — 장기 사용자 메모리
|
|
|
|
- MySQL `td_user_profile` 테이블 + Tool 2개 등록
|
|
- `remember_user_info(key, value)` — 영구 저장 (아이 생년, 재정 목표 등)
|
|
- `recall_user_info(key)` — 이전 저장 정보 조회
|
|
- `UserProfileRepository` (`services/db/user_profile_repository.py`)
|
|
|
|
---
|
|
|
|
## ✅ Phase 6 — 실시간 웹 검색 Tool
|
|
|
|
- `web_search(query)` — DuckDuckGo (무료, API 키 불필요)
|
|
- 최신 금리, 육아 정책, 뉴스 등 실시간 정보 검색 가능
|
|
|
|
---
|
|
|
|
## ✅ Phase 7 — LangSmith 트레이싱
|
|
|
|
- `.env`에서 `LANGCHAIN_TRACING_V2=true` + `LANGCHAIN_API_KEY` 설정으로 활성화
|
|
- Tool Call 실패 원인, RAG 청크 내용, 에이전트 루프 흐름 시각화 가능
|
|
|
|
---
|
|
|
|
## ✅ Phase 9 — 문서 관리
|
|
|
|
- `IngestionService._delete_by_source()` — 파일 경로 기반 중복 청크 삭제
|
|
- `RetrieverService.list_documents()` — Qdrant scroll로 고유 source 목록 반환
|
|
- `RetrieverService.delete_document(source)` — source 기준 청크 전체 삭제
|
|
- Gradio "문서 관리" 탭 — 목록 테이블 + 경로 입력 삭제 버튼 + 앱 로드 시 자동 새로고침
|
|
|
|
---
|
|
|
|
## ✅ Phase 10 — 멀티유저 지원
|
|
|
|
Bug 3 수정 및 Phase 9 작업과 함께 완전 구현됨.
|
|
|
|
- DB 마이그레이션: `mysql_service._migrate_schema()`가 `td_conversations`, `td_user_profile` 양쪽에 `user_id` 컬럼 자동 추가
|
|
- `ConversationRepository`: `create_conversation(user_id)` / `get_latest_conversation_id(user_id)` — user_id 기반 격리
|
|
- `AgentService`: `user_id` 파라미터 추가, 모든 프로필·대화 조회에 전파
|
|
- `make_memory_tools(profile_repo, user_id)`: remember/recall 도구가 올바른 사용자 데이터만 접근
|
|
- Gradio: 사용자 선택 드롭다운(아록/근혜/도율/하율, 기본값 아록) + `_agent_cache` 사전으로 사용자별 에이전트 분리
|
|
|
|
---
|
|
|
|
## ✅ Phase 11 — 대화 이력 복원
|
|
|
|
버그 2와 함께 해결됨. `AgentService` 초기화 시 MySQL에서 최근 10턴을 `_pending_history`에 로드 → 첫 메시지와 함께 LangGraph에 주입.
|
|
|
|
```python
|
|
turns = conversation_repository.load_turns_after(self._conv_id, None, limit=10)
|
|
# → HumanMessage / AIMessage 변환 후 _pending_history에 저장
|
|
```
|
|
|
|
---
|
|
|
|
## ✅ Phase 12 — 답변 피드백 & 품질 개선
|
|
|
|
**배경**: 에이전트가 잘못된 답변을 해도 피드백 루프가 없어 개선이 어려움.
|
|
|
|
**구현 내용**:
|
|
- 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 품질 향상 ★★★ (완료)
|
|
|
|
**배경**: 고정 크기 청킹 + 벡터 유사도 검색만으로는 관련 없는 청크가 섞일 수 있음.
|
|
|
|
**✅ Semantic Chunker — 완료**
|
|
|
|
커스텀 `_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 저장 문서는 재등록해야 새 청킹 방식이 적용됨.
|
|
|
|
**난이도**: 중간 | **임팩트**: 중간 (답변 정확도 향상)
|
|
|
|
---
|
|
|
|
## ✅ Phase 14 — 음성 인터페이스
|
|
|
|
**배경**: 육아 중에는 손이 자유롭지 않아 타이핑이 어려움.
|
|
|
|
**구현 내용**:
|
|
- `openai-whisper` (small 모델) — 마이크 녹음 → 한국어 텍스트 변환, 지연 로딩
|
|
- macOS `say -v Yuna` — 에이전트 응답을 음성으로 읽어줌 (aiff 파일 경유)
|
|
- Gradio "대화" 탭 확장 — 마이크 녹음 + "음성→텍스트 변환" 버튼 + "음성으로 답변 읽기" 체크박스 + TTS 오디오 플레이어
|
|
- LLM/Agent 레이어 변경 없음 — 순수 I/O 어댑터로 구현
|
|
|
|
**config.py 추가**: `whisper_model_size = "small"`, `tts_voice = "Yuna"`
|
|
|
|
**난이도**: 중간 | **임팩트**: 높음 (핵심 사용 시나리오)
|
|
|
|
---
|
|
|
|
## ✅ Phase 13-B — Reranker ★★☆
|
|
|
|
**배경**: 벡터 유사도 검색은 의미적으로 비슷한 청크를 가져오지만, 질문과 실제로 관련 있는 청크를 정확히 가려내지 못하는 경우가 있다. Reranker는 검색 후 순위를 재조정해 LLM에 전달되는 컨텍스트 품질을 높인다.
|
|
|
|
**구현 내용**:
|
|
- `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`로 활성화, 기본 비활성 (첫 실행 시 모델 다운로드)
|
|
|
|
| 설정 | 기본값 | 설명 |
|
|
|------|--------|------|
|
|
| `RERANKER_ENABLED` | `false` | `true`로 설정 시 활성화 |
|
|
| `RERANKER_MODEL_ID` | `cross-encoder/mmarco-mMiniLMv2-L12-H384-v1` | 한국어 포함 다국어 모델 (117MB) |
|
|
| `RERANKER_FETCH_K` | `10` | rerank 전 벡터 검색 후보 수 |
|
|
|
|
**난이도**: 중간 | **임팩트**: 높음 (관련성 낮은 청크 필터링 → 답변 정확도 향상)
|
|
|
|
---
|
|
|
|
## ✅ Phase 18 — Hybrid Search (BM25 + Vector) ★★☆
|
|
|
|
**배경**: 한국어 질문에서 고유명사·전문용어가 포함된 경우 의미 검색(Dense)만으로는 recall이 떨어진다. BM25 키워드 검색과 결합(Hybrid)하면 보완이 가능하다.
|
|
|
|
**구현 내용**:
|
|
- `FastEmbedSparse(model_name="Qdrant/bm25")` — 언어 무관 BM25 sparse 임베딩 (`fastembed` 패키지)
|
|
- `IngestionService`: `HYBRID_SEARCH_ENABLED=true` 시 dense + sparse 동시 저장 (`RetrievalMode.HYBRID`)
|
|
- `RetrieverService`: hybrid 스토어로 검색 → Qdrant 내장 RRF로 결과 통합; sparse vector 미설정 컬렉션은 dense로 자동 폴백
|
|
- `_ensure_collection_schema()`: hybrid 전환 시 스키마 불일치 컬렉션 자동 재생성 (기존 문서 재수집 필요)
|
|
- `.env` `HYBRID_SEARCH_ENABLED=true`로 활성화, 활성화 후 기존 문서 재수집 필요
|
|
|
|
| 설정 | 기본값 | 설명 |
|
|
|------|--------|------|
|
|
| `HYBRID_SEARCH_ENABLED` | `false` | `true`로 설정 시 활성화 |
|
|
| `SPARSE_MODEL_ID` | `Qdrant/bm25` | fastembed sparse 모델 (첫 실행 시 자동 다운로드) |
|
|
|
|
**난이도**: 중간 | **임팩트**: 높음 (키워드 포함 질문 recall 대폭 향상)
|
|
|
|
---
|
|
|
|
## ✅ Phase 19 — Query Rewriting ★☆☆
|
|
|
|
**배경**: 사용자 구어체 질문("아이가 밥을 안 먹어요")은 벡터 검색에 최적화되어 있지 않다. LLM이 검색 전에 질문을 재작성하면 관련 문서 검색 확률이 높아진다.
|
|
|
|
**구현 내용**:
|
|
- LangGraph 그래프에 `query_rewrite` 노드 추가 — `agent → query_rewrite → tools` 순서
|
|
- `search_documents` 호출 시에만 작동하는 조건부 라우팅 (`route_after_agent`): 다른 도구 호출이나 tool 없음 케이스는 그대로 통과
|
|
- 구어체 → 키워드 중심 쿼리로 변환 + 대명사·지시어를 구체적 명칭으로 해소 (이전 대화 2턴 컨텍스트 활용)
|
|
- `tools_condition` 제거 → 커스텀 `route_after_agent` 함수로 대체
|
|
- 변환 결과를 custom stream 이벤트로 emit → `RAG_VERBOSE=true` 시 `쿼리 최적화: "원본" → "최적화"` 출력
|
|
- `.env` `QUERY_REWRITE_ENABLED=true`로 활성화
|
|
|
|
**난이도**: 하 | **임팩트**: 중간 (구어체 질문 검색 품질 향상)
|
|
|
|
---
|
|
|
|
## ✅ Phase 21 — Telegram Bot ★★☆
|
|
|
|
**배경**: Gradio Web UI는 브라우저에서만 사용 가능. 텔레그램으로 이동 중에도 율봇과 대화하고 싶음.
|
|
|
|
**구현 방식**: youlbot REST API(Phase 22) 호출 — `youlbot-telegram/` 별도 프로젝트로 분리.
|
|
|
|
```
|
|
youlbot-telegram/
|
|
├── bot.py ← Application (python-telegram-bot >= 20.0, async)
|
|
│ ├── /start, /reset CommandHandler
|
|
│ └── MessageHandler → api_client.chat() → edit_message_text() (타이핑 효과)
|
|
├── api_client.py ← httpx 기반 REST API 클라이언트 (chat/reset)
|
|
├── .env ← TELEGRAM_BOT_TOKEN, YOULBOT_API_URL, 유저 ID 매핑
|
|
└── requirements.txt
|
|
```
|
|
|
|
**구현 내용**:
|
|
- `python-telegram-bot>=20.0` (asyncio 기반)
|
|
- `youlbot-telegram/bot.py` — 새 진입점 (`python bot.py`로 실행)
|
|
- `/start` — 환영 메시지 + 매핑된 youlbot 사용자 이름 표시
|
|
- `/reset` — `api_client.reset(user_id)` 호출로 대화 이력 초기화
|
|
- 일반 메시지 → `api_client.chat()` SSE 스트리밍 → 0.6초 간격 실시간 편집
|
|
- Telegram numeric ID → youlbot user_id `.env` 매핑 (`USER_아록_TELEGRAM_ID` 등)
|
|
- 미등록 사용자에게 Telegram ID 안내 메시지 표시
|
|
|
|
**실행 방법**:
|
|
```bash
|
|
cd youlbot-telegram
|
|
python bot.py
|
|
```
|
|
|
|
**난이도**: 중간 | **임팩트**: 높음 (모바일·이동 중 접근)
|
|
|
|
---
|
|
|
|
## ✅ Phase 22 — REST API (FastAPI) ★★☆
|
|
|
|
**배경**: 다른 Python 스크립트나 원격 서버에서 율봇을 호출하려면 HTTP API가 필요하다.
|
|
Telegram Bot을 별도 프로젝트로 분리해 이 API를 호출하는 구조로 사용 가능.
|
|
|
|
**구현 내용**:
|
|
- `api.py` — FastAPI 앱, `uvicorn api:app --host 0.0.0.0 --port 8000`으로 실행
|
|
- SSE(`text/event-stream`) 스트리밍: 각 라인 `data: <JSON 토큰>\n\n`, 종료 `data: [DONE]\n\n`
|
|
- Bearer Token 인증 (`.env` `API_TOKEN` 설정; 빈 값이면 개발 모드 무인증)
|
|
- `user_id` 파라미터로 멀티유저 지원 (기존 DB·메모리 구조 그대로 재사용)
|
|
|
|
| 엔드포인트 | 설명 |
|
|
|-----------|------|
|
|
| `GET /health` | 헬스체크 |
|
|
| `POST /chat` | SSE 스트리밍 대화 (`message`, `user_id`, `show_thinking`) |
|
|
| `POST /reset` | 대화 이력 초기화 (`user_id`) |
|
|
| `POST /ingest` | PDF/TXT 파일 업로드 → 벡터DB 수집 |
|
|
| `GET /documents` | 등록 문서 목록 |
|
|
| `DELETE /documents/{source}` | 문서 삭제 |
|
|
|
|
**클라이언트 예시 (별도 Telegram 봇 프로젝트)**:
|
|
```python
|
|
import httpx, json
|
|
|
|
API_URL = "http://192.168.10.x:8000"
|
|
HEADERS = {"Authorization": "Bearer YOUR_TOKEN"}
|
|
|
|
async def ask_youlbot(message: str, user_id: str) -> str:
|
|
full = ""
|
|
async with httpx.AsyncClient(timeout=120) as client:
|
|
async with client.stream("POST", f"{API_URL}/chat",
|
|
json={"message": message, "user_id": user_id},
|
|
headers=HEADERS) as r:
|
|
async for line in r.aiter_lines():
|
|
if line.startswith("data: ") and line != "data: [DONE]":
|
|
full += json.loads(line[6:])
|
|
return full
|
|
```
|
|
|
|
**난이도**: 중간 | **임팩트**: 높음 (확장성·외부 연동)
|
|
|
|
---
|
|
|
|
## ✅ Phase 23 — WebUI 분리 (youlbot-webui 별도 프로젝트) ★★☆
|
|
|
|
**배경**: 현재 `app.py`(Gradio)는 `container.py`를 직접 import해 서비스를 사용한다.
|
|
REST API(Phase 22)를 완성했으므로, WebUI를 독립 프로젝트로 분리해 API만 호출하도록 변경한다.
|
|
분리 후 youlbot은 순수 백엔드(API 서버)로만 동작하며, Telegram Bot과 WebUI가 모두 같은 API를 공유한다.
|
|
|
|
**구현 내용**:
|
|
|
|
**① youlbot/api.py 보완**
|
|
- `POST /feedback` 엔드포인트 추가 (FeedbackRepository 노출 + LangSmith 연동)
|
|
- `/chat` SSE 마지막 이벤트에 `run_id` 포함 → 피드백 연결 가능
|
|
```
|
|
data: {"__done": true, "run_id": "uuid"}
|
|
```
|
|
|
|
**② 신규 프로젝트 youlbot-webui/**
|
|
```
|
|
youlbot-webui/
|
|
├── app.py ← Gradio UI (REST API 호출 방식으로 재작성)
|
|
├── api_client.py ← httpx 기반 API 클라이언트 (chat/reset/ingest/documents/feedback)
|
|
├── .env ← YOULBOT_API_URL, YOULBOT_API_TOKEN
|
|
├── .env.example
|
|
└── requirements.txt ← gradio, httpx, python-dotenv, openai-whisper
|
|
```
|
|
|
|
| 기존 app.py (container 직접 사용) | 변경 후 (API 클라이언트) |
|
|
|---|---|
|
|
| `container.ingestion_service()` | `api_client.ingest(path)` |
|
|
| `agent.stream_response()` | `api_client.chat(msg, user_id)` |
|
|
| `retriever.list_documents()` | `api_client.list_documents()` |
|
|
| `feedback_repo.save_feedback()` | `api_client.save_feedback(...)` |
|
|
| STT (Whisper) | 변경 없음 — WebUI 로컬 실행 유지 |
|
|
| TTS (macOS say) | 변경 없음 — WebUI 로컬 실행 유지 |
|
|
|
|
**실행 방법**:
|
|
```bash
|
|
# 백엔드
|
|
cd youlbot && uvicorn api:app --host 0.0.0.0 --port 8000
|
|
|
|
# WebUI (별도 터미널, 별도 프로젝트)
|
|
cd youlbot-webui && python app.py
|
|
```
|
|
|
|
기존 `youlbot/app.py`는 레거시 직접 실행 옵션으로 보존.
|
|
|
|
**난이도**: 중간 | **임팩트**: 높음 (백엔드/프론트엔드 완전 분리, 다중 클라이언트 지원)
|
|
|
|
---
|
|
|
|
## ✅ Phase 24 — 사고 과정 UI 분리 & 실시간 피드백 ★★☆
|
|
|
|
**배경**: 사고 과정(thinking)·진행 로그가 답변과 섞여 출력되고, 10초 동안 아무 피드백 없이 대기하는 UX 문제.
|
|
|
|
**구현 내용**:
|
|
|
|
**① 스트리밍 토큰 타입 분리 (`youlbot/services/agent/agent_service.py`)**
|
|
- 답변: `yield str` (기존 그대로)
|
|
- 진행 메시지(`[LangGraph → ...]`, `문서 검색 중...` 등): `yield {"__meta": str}`
|
|
- 사고 과정 내용: `yield {"__thinking": str}`
|
|
- LLM 추론 시작 즉시: `writer({"__start": True})` → `yield {"__status": label}` 으로 변환
|
|
|
|
**② 사고 과정 전용 박스 (`youlbot-webui/app.py`)**
|
|
|
|
| 단계 | 표시 방식 | 비고 |
|
|
|------|-----------|------|
|
|
| 전송 즉시 | `🤔 질문을 분석하고 있습니다...` | 단순 div, LLM 추론 전 즉각 표시 |
|
|
| 스트리밍 중 | `🤔 분석 중...` + 현재 줄 | plain `<div>`, 새 줄 도착 시 이전 줄 교체 |
|
|
| 진행 로그 | `🤔 분석 중...` + 로그 메시지 | `__meta` 토큰 전체를 표시 |
|
|
| 완료 | `💭 분석 완료 ▶` | `<details>` 로 전환, 클릭 시 전체 내용 펼침 |
|
|
|
|
- 스트리밍 중 `<details>` 미사용 → 내용 업데이트 시 닫힘 현상 없음
|
|
- TTS는 순수 답변 토큰만 읽음 (`__meta`·`__thinking` 제외)
|
|
- 챗봇에는 답변만 표시 (진행 메시지 숨김)
|
|
- `show_thinking` 체크박스 기본값 ON으로 변경
|
|
|
|
**③ 멀티클라이언트 대응**
|
|
- `youlbot-telegram/bot.py`: `__meta`·`__thinking` 토큰 skip → 순수 답변만 스트리밍
|
|
- `asyncio.get_event_loop().run_until_complete()` → `asyncio.run()` 전체 교체 (AnyIO 워커 스레드 호환)
|
|
|
|
**난이도**: 중간 | **임팩트**: 높음 (UX 대폭 개선)
|
|
|
|
---
|
|
|
|
## ✅ Phase 25 — RAG 출처 전용 접기/펼치기 박스
|
|
|
|
**배경**: RAG 검색 출처가 사고 과정(thinking)과 같은 `__meta` 토큰으로 섞여 "💭 분석 완료" 박스 안에 표시되던 문제.
|
|
|
|
**구현 내용**:
|
|
- `agent_service.py`: 출처를 `{"__meta": "..."}` 개별 토큰 대신 `{"__sources": [{filename, page}, ...]}` 단일 토큰으로 yield
|
|
- `youlbot-webui/app.py`:
|
|
- `_sources_html()` 헬퍼 추가 — `<details>` 기반 접기/펼치기
|
|
- chatbot 바로 아래 `source_box = gr.HTML()` 컴포넌트 추가
|
|
- `respond()`에서 `__sources` 토큰 처리 → 답변 완료 후 "📄 출처 (N개)" 박스 표시
|
|
- `youlbot-telegram/bot.py`: `__sources` 토큰 skip 처리 추가
|
|
|
|
**난이도**: 하 | **임팩트**: 중간 (UX 개선 — 출처와 사고 과정 분리)
|
|
|
|
---
|
|
|
|
## ✅ Phase 20 — RAG 품질 자동 평가 (RAGAS) ★☆☆
|
|
|
|
**배경**: 청킹 전략·검색 파라미터·Reranker 변경 시 답변 품질이 실제로 나아졌는지 수치로 확인할 방법이 없다.
|
|
|
|
**구현 내용**:
|
|
|
|
```
|
|
eval/
|
|
├── dataset.jsonl ← 평가용 Q&A 쌍 (질문·정답 — 필요 시 수정)
|
|
├── run_ragas.py ← 평가 실행 스크립트
|
|
├── requirements.txt ← ragas==0.2.9, datasets, langchain-google-vertexai
|
|
└── results/ ← report_YYYYMMDD_HHMMSS.{csv,json} 저장
|
|
```
|
|
|
|
**평가 지표**:
|
|
|
|
| 지표 | 설명 |
|
|
|------|------|
|
|
| `faithfulness` | 답변이 검색 컨텍스트에 충실한가 (환각 탐지) |
|
|
| `answer_relevancy` | 답변이 질문에 얼마나 관련 있는가 |
|
|
| `context_recall` | 컨텍스트가 정답에 필요한 정보를 포함하는가 |
|
|
| `context_precision` | 검색된 컨텍스트 중 실제 유용한 비율 |
|
|
|
|
**평가 LLM 우선순위**: OpenAI GPT-4o-mini > Anthropic Claude Haiku > 로컬 Qwen3
|
|
|
|
**실행 방법**:
|
|
```bash
|
|
# API 서버 실행 후
|
|
python eval/run_ragas.py
|
|
python eval/run_ragas.py --dataset eval/dataset.jsonl --api http://localhost:8000
|
|
```
|
|
|
|
**호환성 처리**: ragas 0.2가 langchain-community 0.4+에서 `ChatVertexAI` 임포트 실패하는 문제를 런타임 shim으로 우회.
|
|
|
|
**난이도**: 중간 | **임팩트**: 중간 (장기 품질 관리 기반)
|
|
|
|
---
|
|
|
|
## Phase 15 — 모델 선택 (Claude API / OpenAI 옵션) ★☆☆
|
|
|
|
**배경**: 로컬 MLX 모델은 Apple Silicon 전용. 원격 접속 시나리오나 더 높은 품질이 필요할 때 Claude API/OpenAI를 선택할 수 있으면 유연성 확보.
|
|
|
|
**구현 방식**: `config.py`에 `model_provider` 추가, `container.py`에서 provider별 chat_model 분기.
|
|
|
|
```python
|
|
model_provider: str = "mlx" # "mlx" | "claude" | "openai"
|
|
```
|
|
|
|
**난이도**: 중간 | **임팩트**: 중간
|
|
|
|
---
|
|
|
|
## Phase 16 — Docker 컨테이너화 ★☆☆
|
|
|
|
**배경**: 현재 로컬 전용. 가족이나 지인도 쓸 수 있도록 서버 배포 가능한 형태로 패키징.
|
|
|
|
**구현 범위**:
|
|
```
|
|
docker-compose.yml
|
|
├── youlbot (Gradio app)
|
|
├── qdrant
|
|
└── mysql
|
|
```
|
|
|
|
> 주의: MLX는 Apple Silicon 전용이라 서버 배포 시 Phase 15(모델 선택)이 선행되어야 함.
|
|
|
|
**난이도**: 높음 | **임팩트**: 중간
|
|
|
|
---
|
|
|
|
## ✅ Phase 17 — 멀티모달 이미지 이해 ★☆☆
|
|
|
|
**배경**: 이유식 사진 → 재료 분석, 금융 서류 사진 → 내용 해석 등.
|
|
|
|
**구현 방식**: Dual-model C방식 — analyze_image 도구
|
|
|
|
| 모델 | 역할 |
|
|
|------|------|
|
|
| Qwen3-8B-4bit | 대화·추론 (항상 로드) |
|
|
| Qwen2.5-VL-7B-Instruct-4bit | 이미지 분석 (lazy load) |
|
|
|
|
- `services/model/mlx_vision_model.py` — MlxVisionModel (mlx-vlm 래퍼, lazy load)
|
|
- `services/agent/tools.py` — `make_vision_tool(vision_model, image_path)` 추가
|
|
- `agent_service.py` — `stream_response(image_path=None)`, config 경유 vision tool 동적 주입
|
|
- `api.py` — `image_base64` 필드 추가, temp 파일 저장 후 응답 완료 시 삭제
|
|
- `youlbot-webui` — `image_input` 컴포넌트 추가, ChatService.chat(image_path=) 연결
|
|
- `.env` — `VISION_ENABLED=true`, `VISION_MODEL_ID` 설정
|
|
|
|
**실행 방법**: API 서버 재시작 후 WebUI 이미지 첨부 버튼으로 사진 전송
|
|
|
|
**난이도**: 높음 | **임팩트**: 높음
|
|
|
|
---
|
|
|
|
## 추천 진행 순서
|
|
|
|
```
|
|
단기 (1~2주) 중기 (1개월) 장기
|
|
──────────────────────── ────────────────────── ──────────────────
|
|
Phase 20 RAGAS 평가 → Phase 15 (모델선택) → Phase 16 (Docker)
|
|
→ Phase 17 (멀티모달)
|
|
```
|
|
|
|
### 우선순위 매트릭스
|
|
|
|
| Phase | 상태 | 난이도 | 임팩트 | 추천 순위 |
|
|
|-------|------|--------|--------|-----------|
|
|
| 버그 1 RAG 중복 | ✅ 완료 | — | — | — |
|
|
| 버그 2 이력 미연동 | ✅ 완료 | — | — | — |
|
|
| 버그 3 단일 사용자 | ✅ 완료 | — | — | — |
|
|
| 버그 4 나이 계산 오류 | ✅ 완료 | — | — | — |
|
|
| 버그 5 thinking 체크박스 무효 | ✅ 완료 | — | — | — |
|
|
| 버그 6 TTS 메타 토큰 혼재 | ✅ 완료 | — | — | — |
|
|
| Phase 4 Web UI | ✅ 완료 | — | — | — |
|
|
| Phase 5 장기 사용자 메모리 | ✅ 완료 | — | — | — |
|
|
| Phase 6 웹 검색 | ✅ 완료 | — | — | — |
|
|
| Phase 7 LangSmith 트레이싱 | ✅ 완료 | — | — | — |
|
|
| Phase 9 문서 관리 | ✅ 완료 | — | — | — |
|
|
| Phase 10 멀티유저 | ✅ 완료 | — | — | — |
|
|
| Phase 11 이력 복원 | ✅ 완료 | — | — | — |
|
|
| Phase 12 피드백 | ✅ 완료 | — | — | — |
|
|
| Phase 13 Semantic Chunker | ✅ 완료 | — | — | — |
|
|
| Phase 14 음성 인터페이스 | ✅ 완료 | — | — | — |
|
|
| Phase 13-B Reranker | ✅ 완료 | — | — | — |
|
|
| Phase 18 Hybrid Search | ✅ 완료 | — | — | — |
|
|
| Phase 19 Query Rewriting | ✅ 완료 | — | — | — |
|
|
| Phase 21 Telegram Bot | ✅ 완료 | — | — | — |
|
|
| Phase 22 REST API | ✅ 완료 | — | — | — |
|
|
| Phase 23 WebUI 분리 | ✅ 완료 | — | — | — |
|
|
| Phase 24 사고 과정 UI 분리 | ✅ 완료 | — | — | — |
|
|
| Phase 25 RAG 출처 전용 박스 | ✅ 완료 | — | — | — |
|
|
| Phase 20 RAGAS 평가 | ✅ 완료 | — | — | — |
|
|
| Phase 15 모델 선택 | 🔲 미완 | 중간 | 중간 | 4순위 |
|
|
| Phase 16 Docker | 🔲 미완 | 높음 | 중간 | 5순위 |
|
|
| Phase 17 멀티모달 | ✅ 완료 | — | — | — |
|
|
|
|
---
|
|
|
|
## 💡 IDEA — 신규 개선 아이디어
|
|
|
|
### 단기 — 빠르게 임팩트 큰 것
|
|
|
|
#### ✅ IDEA-1. 대화 기반 자동 RAG 업데이트
|
|
|
|
**배경**: 현재 문서 업로드만 RAG에 들어간다. 중요 대화 내용 자체를 자동으로 벡터DB에 추가하면 사용할수록 지식이 쌓이는 시스템이 된다.
|
|
|
|
**구현 내용**:
|
|
- `IngestionService.store_text(text, metadata)` — 단일 텍스트 직접 저장 (semantic chunking 없이)
|
|
- `AgentService._maybe_index_conversation()` — 응답 완료 후 LLM이 유용한 정보 판단 → 요약 → Qdrant 저장 (asyncio background task)
|
|
- `source="conversation"`, `user_id`, `timestamp` 메타데이터로 문서 RAG와 구분
|
|
- `.env` `CONV_RAG_ENABLED=true`로 활성화 (기본 비활성)
|
|
|
|
**난이도**: 하 | **임팩트**: 높음 (지식 자동 축적)
|
|
|
|
---
|
|
|
|
#### ✅ IDEA-2. 스마트 알림 & 일정 연동
|
|
|
|
**배경**: 예방접종, 약 먹을 시간, 병원 예약 등 날짜 기반 알림이 없다.
|
|
|
|
**구현 내용**:
|
|
- `td_reminders` 테이블 (user_id, remind_date, message, sent_d0/d1/d7)
|
|
- `set_reminder(remind_date, message)` + `list_reminders()` 도구 — LangGraph 에이전트 자동 호출
|
|
- `SchedulerService` — asyncio task 기반 60초 간격 체크 → D-7/D-1/D-0 Telegram push
|
|
- FastAPI `lifespan`으로 앱 시작/종료 시 스케줄러 자동 관리
|
|
- `GET /reminders/{user_id}` API 엔드포인트 추가
|
|
- `.env` 설정: `TELEGRAM_BOT_TOKEN`, `TELEGRAM_USER_MAP={"아록":"123456"}`
|
|
|
|
**난이도**: 중간 | **임팩트**: 매우 높음 (육아 핵심 시나리오)
|
|
|
|
---
|
|
|
|
#### IDEA-3. 대화 요약 일간 리포트
|
|
|
|
**배경**: 하루에 어떤 질문을 했고 어떤 결정을 내렸는지 돌아볼 방법이 없다.
|
|
|
|
**구현 방향**:
|
|
- 매일 자정 cron → 당일 대화 요약 생성 (`CompactService` 재활용)
|
|
- Telegram으로 사용자별 요약 발송
|
|
- `/chat` API + APScheduler로 구현 가능 (새 인프라 불필요)
|
|
|
|
**난이도**: 하 | **임팩트**: 중간
|
|
|
|
---
|
|
|
|
#### IDEA-4. 텔레그램 그룹 채팅 지원
|
|
|
|
**배경**: 현재 1:1 채팅만 지원한다. 가족 그룹에서 `@율봇` 멘션으로 함께 사용하고 싶다.
|
|
|
|
**구현 방향**:
|
|
- 그룹 메시지에서 `@율봇` 멘션 감지 → 발신자 Telegram ID로 `user_id` 매핑
|
|
- 그룹 공용 컨텍스트(`user_id="family"`) 옵션
|
|
- `python-telegram-bot` 기존 코드에 그룹 핸들러 추가
|
|
|
|
**난이도**: 하 | **임팩트**: 높음 (가족 공동 사용)
|
|
|
|
---
|
|
|
|
### 중기 — RAG/에이전트 품질 향상
|
|
|
|
#### ✅ IDEA-5. Agentic RAG — 자기 교정 검색 (CRAG)
|
|
|
|
**배경**: 현재 `query_rewrite → search_documents` 1회로 끝난다. 검색 결과가 부족하면 재시도나 웹 검색 fallback이 없다.
|
|
|
|
**구현 내용**:
|
|
- `AgentState(TypedDict)` — `messages` + `crag_fallback_used` 커스텀 상태
|
|
- `crag_check` LangGraph 노드 — `search_documents` 결과가 비었으면 동일 쿼리로 `web_search` AIMessage 자동 주입
|
|
- `route_after_crag` — fallback AIMessage 있으면 tools 재실행, 없으면 agent로 복귀
|
|
- 그래프: `tools → crag_check → route_after_crag → {tools, agent}`
|
|
- 무한 루프 방지: `crag_fallback_used` 플래그로 1회만 fallback
|
|
- `.env` `CRAG_ENABLED=true`로 활성화 (기본 비활성)
|
|
|
|
**난이도**: 중간 | **임팩트**: 높음 (검색 실패 케이스 대폭 감소)
|
|
|
|
---
|
|
|
|
#### IDEA-6. 영수증/가계부 OCR
|
|
|
|
**배경**: `analyze_image` 도구가 이미 있다. 영수증 사진에서 지출을 자동 기록하면 가계 관리가 가능해진다.
|
|
|
|
**구현 방향**:
|
|
- `analyze_image` → 금액·항목·날짜 추출 → MySQL `td_expenses` 저장
|
|
- `get_monthly_expenses(month)` 도구 추가 → "이번 달 식비 얼마야?" 대응
|
|
- 카테고리 자동 분류 (식비/의료비/교육비 등)
|
|
|
|
**난이도**: 중간 | **임팩트**: 높음 (가계 관리 시나리오)
|
|
|
|
---
|
|
|
|
#### ✅ IDEA-7. RAG 파라미터 자동 튜닝 (Auto-Eval Loop)
|
|
|
|
**배경**: RAGAS 평가 인프라는 있는데, 파라미터 변경 효과를 수동으로 비교해야 한다.
|
|
|
|
**구현 내용**:
|
|
- `eval/auto_tune.py` — API 서버 없이 `RetrieverService` 직접 사용, 파라미터 조합별 `context_precision` + `context_recall` 비교
|
|
- 기본 조합 4개: `baseline(3/10)`, `top_k_5(5/15)`, `top_k_2(2/6)`, `fetch_k_20(3/20)`
|
|
- 평균 점수 기준 최적 조합 추천 + `.env` 설정값 안내
|
|
- `eval/results/tune_YYYYMMDD.json` 저장
|
|
- 실행: `python eval/auto_tune.py [--dataset eval/dataset.jsonl]`
|
|
|
|
**난이도**: 중간 | **임팩트**: 중간 (장기 품질 자동 관리)
|
|
|
|
---
|
|
|
|
### 장기 — 구조적 확장
|
|
|
|
#### ✅ IDEA-8. GraphRAG / 지식 그래프
|
|
|
|
**배경**: `td_user_profile`이 flat key-value라 엔티티 간 관계 추론이 불가능하다.
|
|
|
|
**구현 내용**:
|
|
- `td_knowledge_graph` 테이블 — (user_id, subject, relation, object) 트리플 영구 저장
|
|
- `GraphService` — NetworkX `MultiDiGraph` 인메모리 캐시 + MySQL 영속화
|
|
- `add_relation(subject, relation, obj)` 도구 — 관계 저장
|
|
예: `도율 -[알레르기]→ 복숭아`, `아록 -[자녀]→ 도율`
|
|
- `query_entity(entity)` 도구 — 출발/도착 방향 모든 관계 조회
|
|
- `call_model`에 저장된 그래프 요약을 시스템 프롬프트에 자동 주입
|
|
- `.env` `GRAPH_ENABLED=true`로 활성화 (기본 비활성)
|
|
|
|
**사용 예시**:
|
|
```
|
|
사용자: "도율이 복숭아 알레르기가 있어"
|
|
→ add_relation("도율", "알레르기", "복숭아")
|
|
|
|
사용자: "도율이 먹으면 안 되는 음식은?"
|
|
→ query_entity("도율") → "도율 -[알레르기]→ 복숭아"
|
|
```
|
|
|
|
**난이도**: 높음 | **임팩트**: 높음 (메모리 추론 능력 대폭 향상)
|
|
|
|
---
|
|
|
|
#### IDEA-9. PWA / 모바일 Web UI
|
|
|
|
**배경**: Gradio는 모바일 UX가 좋지 않다. 네이티브 앱처럼 설치하고 카메라 접근도 원활해야 한다.
|
|
|
|
**구현 방향**:
|
|
- `youlbot-webui`를 Next.js + shadcn/ui PWA로 재작성
|
|
- 홈 화면 설치, 오프라인 캐시, 네이티브 카메라 접근
|
|
- 기존 REST API 그대로 재사용 (백엔드 변경 없음)
|
|
- STT는 Web Speech API로 대체 (브라우저 내장)
|
|
|
|
**난이도**: 높음 | **임팩트**: 높음 (모바일 UX 대폭 개선)
|
|
|
|
---
|
|
|
|
### IDEA 우선순위 매트릭스
|
|
|
|
| IDEA | 설명 | 난이도 | 임팩트 | 추천 순위 |
|
|
|------|------|--------|--------|-----------|
|
|
| IDEA-2 스마트 알림 | ✅ asyncio 스케줄러 + Telegram push | 중간 | 매우 높음 | — |
|
|
| IDEA-4 텔레그램 그룹 채팅 | 기존 Bot 코드 확장 | 하 | 높음 | 1순위 |
|
|
| IDEA-3 일간 리포트 | CompactService 재활용 + SchedulerService | 하 | 중간 | 2순위 |
|
|
| IDEA-1 대화 기반 RAG | ✅ asyncio background + Qdrant 저장 | 하 | 높음 | — |
|
|
| IDEA-5 CRAG | ✅ crag_check LangGraph 노드 | 중간 | 높음 | — |
|
|
| IDEA-7 Auto-Eval | ✅ eval/auto_tune.py | 중간 | 중간 | — |
|
|
| IDEA-6 영수증 OCR | analyze_image 재활용 | 중간 | 높음 | 1순위 |
|
|
| IDEA-8 GraphRAG | ✅ NetworkX + MySQL + 2개 도구 | 높음 | 높음 | — |
|
|
| IDEA-9 PWA WebUI | 프론트엔드 재작성 | 높음 | 높음 | 8순위 |
|