Files
exAichatbot_agent/exAiChatBot-chatbot2.0-agent/PROCESS_FLOW.md
T
Macbook 4b86b2a660 Agent 2.0 exdev 서버 배포 스택
- server-dev start/stop/deploy 및 Gitea push 자동 배포
- local-dev 로컬 개발 환경

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 22:57:30 +09:00

608 lines
24 KiB
Markdown

# exAiChatBot 프로세스 흐름
> **현재 기준:** Agent 모드 운영, Qdrant 단일 벡터 스토어, `chatbotApp` 웹 UI, `chatbotAdmin` 학습·통계 운영
> **주요 엔드포인트:** RAG API `:28012`, Vector Admin `:28013`, Qdrant `:6333`
> **최종 갱신:** 2026-07-14
이 문서는 `exAiChatBot`이 실제 운영 흐름에서 어떤 역할을 하는지 정리합니다.
상세 시스템 배치는 [`../SYSTEM_ARCHITECTURE.md`](../SYSTEM_ARCHITECTURE.md), Agent 구조는 [`../docs/AGENT_ARCHITECTURE.md`](../docs/AGENT_ARCHITECTURE.md)를 기준 문서로 봅니다.
---
## 1. 한눈에 보는 현재 흐름
```mermaid
flowchart LR
User[사용자]
Kakao[카카오톡 / 오픈빌더]
Web[kakaoChatbotSkill\n:8083]
App[chatbotApp\n/chatbot/ai/]
RAG[exAiChatBot API\n:28012]
WAS[chatbotApi\n:8086]
QD[(Qdrant\n:6333)]
LLM[LLM / Embedding / Reranker\nGateway]
ADM[exAiChatBot Admin\n:28013]
CA[chatbotAdmin\n:8087]
Mongo[(MongoDB)]
Oracle[(Oracle)]
Data[data.ex.co.kr]
User --> Kakao
Kakao -->|fallback skill| Web
Web -->|redirect-only button| App
App -->|POST /web/ask| Web
Web -->|POST /agent/chat| RAG
RAG -->|tool definitions / execute| WAS
WAS -->|WAS domain tool| Oracle
WAS -->|WEB domain tool| Web
Web --> Data
RAG -->|rag_search| QD
RAG -->|chat / embed / rerank| LLM
RAG -->|history / pending| Mongo
CA -->|학습 / FAQ CRUD| ADM
ADM --> QD
```
### 핵심 원칙
| 구분 | 현재 동작 |
|------|-----------|
| 카카오톡 | 답변 본문 처리보다 `chatbotApp`을 여는 버튼 제공 중심 |
| 웹 채팅 | `/web/ask`가 Agent 응답을 직접 받아 `WebAskResponse`로 매핑 |
| Agent | `/agent/chat`에서 LLM tool loop 실행 |
| FAQ 검색 | `rag_search` tool이 Qdrant 벡터 검색 + rerank 수행 |
| 도메인 조회 | RAG Agent가 WAS tool API를 호출, WAS/WEB fetchOwner에 따라 실행 |
| 환각 방지 | tool 또는 Qdrant 근거가 없으면 자유 답변 폐기 후 guidance 안내 |
| 학습 관리 | `chatbotAdmin` → `exAiChatBot Admin(:28013)` → Qdrant upsert/delete |
---
## 2. 실행 서비스와 책임
| 서비스 | 포트 | 주요 파일 | 책임 |
|--------|------|-----------|------|
| RAG API | `28012` | `scripts/run_service_qa.py` | `/agent/chat`, `/health` |
| AgentService | 내부 | `scripts/agent/agent_service.py` | tool loop, pending, 강제 RAG, guidance, 최종 응답 |
| ToolExecutor | 내부 | `scripts/agent/tool_executor.py` | `rag_search`, `ask_user`, WAS domain tool 실행 |
| Vector Admin | `28013` | `scripts/admin_service.py` | FAQ 검색/목록/추가/수정/삭제, keyword-search |
| QdrantStore | `6333` | `scripts/vector_store.py` | Qdrant collection 연결, upsert, query, scroll, delete |
| Batch ingest | 일회성 | `scripts/ingest_qa.py` | `qa_raw.jsonl` → embedding → Qdrant upsert |
> `build_index_qa.py`는 FAISS 시절 단계입니다. `VECTOR_STORE=qdrant`이면 즉시 종료하며, 현재 Qdrant 운영에서는 별도 인덱스 파일을 만들지 않고 upsert가 곧 반영입니다.
---
## 3. 런타임 초기화
```mermaid
flowchart TD
Start[api 컨테이너 시작]
Config[환경변수 로드\nConfig.from_env]
Clients[LLM / Embedding / Reranker client]
Qdrant[QdrantStore 연결\ncollection qa_vectors]
Mongo[MongoDB 연결\n대화 이력 / pending]
Handlers[SearchHandler / PromptBuilder\nLLMHandler / QueryRewriter 등]
Tools[ChatbotToolClient\nWAS tool API]
Agent[AgentService 생성]
Ready[FastAPI 대기\n/agent/chat, /health]
Start --> Config --> Clients --> Qdrant --> Mongo --> Handlers --> Tools --> Agent --> Ready
```
### 주요 환경변수
| 환경변수 | 기본값 | 설명 |
|----------|--------|------|
| `VECTOR_STORE` | `qdrant` | 운영 벡터 스토어 |
| `QDRANT_COLLECTION` | `qa_vectors` | FAQ collection |
| `FAISS_TOP_K` | `30` | 이름은 레거시지만 Qdrant 검색 top-k에 사용 |
| `FAISS_THRESHOLD` | `0.55` | 1차 벡터 검색 score threshold |
| `FAISS_THRESHOLD_REWRITE` | `0.50` | no-match 재검색 완화 threshold |
| `RERANK_CANDIDATES` | `20` | reranker 입력 후보 수 |
| `TOP_N_FOR_LLM` | `5` | 최종 답변 프롬프트 참고 FAQ 수 |
| `LOW_CONFIDENCE_THRESHOLD` | `0.65` | 낮은 신뢰도·제안 기준 |
| `HIGH_CONFIDENCE_THRESHOLD` | `0.75` | high confidence 기준 |
| `AGENT_MAX_ROUNDS` | `6` | Agent tool loop 최대 반복 |
| `CHATBOT_API_BASE_URL` | `http://127.0.0.1:8086/api` | WAS tool API |
| `INTERNAL_TOOL_API_KEY` | 빈 값 | RAG→WAS 내부 인증 키 |
| `CHAT_HISTORY_LIMIT` | `10` | 최근 대화 이력 조회 수 |
| `CHAT_HISTORY_HOURS` | `24` | 대화 이력 조회 시간 범위 |
---
## 4. Agent 응답 흐름 (`POST /agent/chat`)
운영 주 경로입니다. `kakaoChatbotSkill`의 `RagAgentClient`와 웹 채팅의 `WebChatAgentService`가 호출합니다.
### 요청
```json
{
"query": "사용자 질문",
"botId": "conversation-id",
"pendingIntentType": "FARE_SEARCH",
"pendingParams": {
"toIc": "신갈"
}
}
```
`pendingIntentType`, `pendingParams`는 선택입니다. 저장된 pending이 있으면 `AgentPendingStore`에서 보완합니다.
### 처리 순서
```mermaid
flowchart TD
Q[사용자 질문]
Pending[저장 pending 조회]
Special{인사/종료?}
History[대화 이력 로드]
Tools[tool 목록 구성\nrag_search + ask_user + WAS tools]
Loop[LLM tool loop]
Tool{tool 호출}
RAG[rag_search\nQdrant + rerank]
Domain[WAS domain tool\n/tools/execute]
Ask[ask_user / clarify]
Ground[근거 기반 최종 답변 생성\nPromptBuilder 공통]
Safe{근거 있음?}
Guidance[guidance 안내\n콜센터/정확 확인]
Save[Mongo 이력 저장]
Response[AgentResponse 반환]
Q --> Pending --> Special
Special -->|yes| Response
Special -->|no| History --> Tools --> Loop --> Tool
Tool -->|rag_search| RAG --> Loop
Tool -->|domain tool| Domain --> Loop
Tool -->|ask_user| Ask --> Response
Loop --> Safe
Safe -->|domainData 또는 references| Ground --> Save --> Response
Safe -->|없음| Guidance --> Save --> Response
```
### Agent 안전망
| 안전망 | 목적 |
|--------|------|
| 매 턴 tool 사용 강제 프롬프트 | 대화 이력만 보고 사실 답변을 재생성하지 않도록 제한 |
| 강제 `rag_search` | LLM이 정보성 질문에서 tool을 스킵하면 Qdrant를 강제 조회 |
| 검색어 맥락화 | 이력이 있는 후속 질문은 `QueryRewriter`로 완결형 검색어를 만든 뒤 검색 |
| 도메인 의도 가드 | 통행요금·미납처럼 도메인 의도가 강하면 FAQ 검색보다 되물음 우선 |
| pending 중 안전망 스킵 | 슬롯필링 중 강제 RAG가 pending을 지우는 회귀 방지 |
| 단일 system 메시지 | LLM tool-call 요청에서 pending 안내를 첫 system prompt에 병합해 게이트웨이 400 오류 방지 |
| 근거 없는 final 폐기 | tool/Qdrant 근거가 없으면 LLM 자유 답변을 버리고 guidance 반환 |
| PromptBuilder 사용 | Agent 최종 답변에서 FAQ·도메인 근거를 일관된 형식으로 사용 |
---
## 5. `rag_search` 상세 흐름
`rag_search`는 Agent의 로컬 tool입니다. FAQ/일반 안내 질문에 사용합니다.
```mermaid
flowchart TD
Query[검색어]
Embed[EmbeddingClient.embed\nis_query=true]
Search1[Qdrant query_points\nthreshold 0.55]
Retry{검색 결과 있음?}
Search2[Qdrant 재검색\nthreshold 0.50]
Candidates[meta 언랩\nq/a/url/category]
Rerank[Reranker\n질문+답변 passage]
HasRefs{references 있음?}
KeywordGuard{keyword retry 허용?}
Keyword[Qdrant 문자열 검색\n최대 3건]
KeywordRerank[Reranker 검증\nLOW_CONFIDENCE_THRESHOLD 이상]
Refs[references 최대 5개]
Empty[references 0건\nguidance 후보]
Query --> Embed --> Search1 --> Retry
Retry -->|yes| Candidates
Retry -->|no| Search2 --> Candidates
Candidates --> Rerank --> HasRefs
HasRefs -->|yes| Refs
HasRefs -->|no| KeywordGuard
KeywordGuard -->|yes| Keyword --> KeywordRerank
KeywordGuard -->|no| Empty
KeywordRerank -->|통과| Refs
KeywordRerank -->|미달| Empty
```
### 현재 검색 특성
| 항목 | 동작 |
|------|------|
| 저장 임베딩 | FAQ 질문 `q`만 document 모드로 임베딩 |
| sparse 저장 | `qa_vectors_v2` 또는 `HYBRID_SEARCH_ENABLED=true`면 `q+a+category+source` 기반 모델 없는 sparse vector도 저장 |
| 검색 임베딩 | 사용자 검색어를 query 모드로 임베딩 |
| 1차 검색 | Qdrant cosine score threshold `0.55` |
| hybrid 검색 | 활성화 시 dense 후보 + sparse 후보를 병합한 뒤 rerank |
| 재검색 | 0건이면 threshold `0.50`으로 한 번 완화 |
| rerank 문서 | `질문: {q}\n답변: {a}` 형태로 Q+A 전체 전달 |
| keyword retry | 벡터/완화 재검색 후 references가 0건일 때만 문자열 검색 최대 3건 |
| keyword 검증 | keyword 결과도 reranker 최고 점수가 `LOW_CONFIDENCE_THRESHOLD` 이상일 때만 채택 |
`희망드림`처럼 FAQ 질문에 실제 포함된 짧은 고유명사가 벡터 score threshold 아래로 누락되는 경우를 보완하기 위해, 최종 guidance 직전 보조 검색을 둡니다.
단 `할인`, `감면`, `요금`, `?`처럼 너무 넓거나 비정보성인 단독 발화는 keyword retry를 실행하지 않습니다. keyword 결과가 있어도 reranker 점수가 낮으면 references를 비워 두고 기존 guidance로 전환합니다.
### 5.1 Agent 재검색: 강제 RAG와 검색어 맥락화
Agent 모드에서는 LLM이 정상적으로 `rag_search`를 호출하면 위 흐름 그대로 검색합니다.
다만 LLM이 대화 이력만 보고 tool 호출 없이 final text를 만들거나, 도메인 근거도 Qdrant 근거도 없는 경우에는 `AgentService`가 안전망으로 `rag_search`를 강제 실행합니다.
```mermaid
flowchart TD
Final[LLM tool loop 종료]
Domain{사용 가능한 domainData 있음?}
RagDone{rag_search 이미 실행?}
Pending{pending 진행 중?}
Guard{명백한 도메인 의도?}
History{botId + 대화 이력 있음?}
Rewrite[QueryRewriter.rewrite_query]
NoRewrite[원문 검색어 사용]
Search[rag_search 강제 실행]
Guide[근거 없으면 guidance]
Final --> Domain
Domain -->|yes| SearchDone[도메인 근거로 답변 생성]
Domain -->|no| RagDone
RagDone -->|yes| SearchDone
RagDone -->|no| Pending
Pending -->|yes| Guide
Pending -->|no| Guard
Guard -->|yes| Clarify[FAQ 대신 되물음]
Guard -->|no| History
History -->|yes| Rewrite --> Search
History -->|no| NoRewrite --> Search
Search --> SearchDone
```
#### 강제 RAG가 실행되는 조건
| 조건 | 의미 |
|------|------|
| 도메인 tool 결과 없음 | `domainData`가 없거나 사용할 수 없는 상태 |
| `rag_search` 미실행 | LLM이 이번 턴에서 Qdrant를 조회하지 않음 |
| pending 진행 중 아님 | 슬롯필링 중 강제 검색이 pending을 지우는 회귀 방지 |
| 도메인 의도 가드 통과 | 차량번호/통행요금 계산처럼 명백한 도메인 조회는 FAQ 대신 되물음 |
#### 검색어 맥락화 순서
1. `botId`가 있고 `CHAT_HISTORY_ENABLED=true`이면 MongoDB에서 최근 대화 이력을 조회합니다.
2. 조회 범위는 `CHAT_HISTORY_HOURS` 시간 이내, 최대 `CHAT_HISTORY_LIMIT`개입니다.
3. 이력은 오래된 대화 → 최신 대화 순서로 정렬됩니다.
4. 답변 본문에서 `혹시 이런 것을 찾으셨나요?` 제안 블록은 제거합니다.
5. `QueryRewriter.rewrite_query()`가 현재 질문이 후속 질문인지 판단합니다.
6. 후속 질문이면 완결형 검색어를 반환하고, 새 주제면 `__NO_REWRITE__`를 반환합니다.
7. 재작성 결과가 없거나 원문과 같으면 원문 검색어로 `rag_search`를 실행합니다.
예시는 다음과 같습니다.
| 이전 대화 | 현재 질문 | 검색어 |
|-----------|-----------|--------|
| "하이패스 단말기가 뭔가요?" | "그럼 어디서 사나요?" | "하이패스 단말기는 어디서 구매할 수 있나요?" |
| "하이패스 단말기에 대해 알려줘" | "얼마야?" | "하이패스 단말기 가격은 얼마인가요?" |
| "통행요금 조회해줘" | "안녕" | 재작성 안 함 |
| "하이패스 설명" | "동김천 휴게소 메뉴 알려줘" | 재작성 안 함 |
### 5.2 QueryRewriter 판단 기준
`QueryRewriter`는 무조건 이전 대화에 붙이지 않습니다.
현재 질문이 아래 조건 중 하나에 해당할 때만 후속 질문 후보로 보고, 최종 판단은 LLM 재작성 프롬프트에서 한 번 더 합니다.
| 판단 기준 | 예 |
|-----------|----|
| 지시어 포함 | `그럼`, `그거`, `거기`, `아까`, `이어서`, `해당` |
| 짧은 후속 질문 | 20자 이하의 `얼마야?`, `어디서 사?`, `어떻게 해?`, `가능해?` |
| 시간/방법/이유 질문 | `언제`, `기간`, `왜`, `방법`, `절차`, `신청` |
재작성 프롬프트의 핵심 규칙은 다음과 같습니다.
| 규칙 | 설명 |
|------|------|
| 후속 질문만 재작성 | 새 주제면 `__NO_REWRITE__`만 출력 |
| 지시어 구체화 | `그거`, `거기` 등을 이전 대화의 명사로 치환 |
| 의도 유지 | 사용자가 물은 범위를 넓히거나 새 정보를 추가하지 않음 |
| 한 문장 출력 | 재작성된 질문 한 문장 또는 `__NO_REWRITE__`만 허용 |
| thinking 제거 | `<think>...</think>`가 있으면 후처리에서 제거 |
## 6. 도메인 tool 흐름
RAG Agent가 tool을 선택하면 WAS `chatbotApi`가 실제 정책·검증·조회 실행을 담당합니다.
```mermaid
sequenceDiagram
participant Agent as exAiChatBot Agent
participant WAS as chatbotApi
participant Skill as kakaoChatbotSkill
participant DB as Oracle
participant Data as data.ex.co.kr
Agent->>WAS: GET /api/v1/tools/definitions
WAS-->>Agent: intent-definitions.yml 기반 tools
Agent->>WAS: POST /api/v1/tools/execute
alt fetchOwner=WAS
WAS->>DB: Oracle/domain handler 조회
WAS-->>Agent: domainData, uiType, intentType
else fetchOwner=WEB
WAS->>Skill: POST /internal/tools/data-portal
Skill->>Data: 외부 API 조회
Skill-->>WAS: domainData
WAS-->>Agent: domainData, uiType, intentType
else needsClarification
WAS-->>Agent: clarificationQuestion, missingParams
end
```
### 대표 tool
| 구분 | Tool |
|------|------|
| WAS/Oracle | `FARE_SEARCH`, `FARE_UNPAID`, `FARE_REFUND`, `IC_TEL`, `REST_AREA_PRICE`, `TROAD_*` |
| WEB/data.ex.co.kr | `GASSTATION`, `BRAND_SHOP`, `REST_AREA_FOOD_LIST` |
| RAG 로컬 | `rag_search`, `ask_user` |
WAS가 `needsClarification=true`를 반환하면 Agent는 pending을 저장하고 `clarify` 응답을 반환합니다.
---
## 7. Pending · 멀티턴 흐름
```mermaid
sequenceDiagram
participant User as 사용자
participant Web as kakaoChatbotSkill
participant RAG as AgentService
participant WAS as chatbotApi
participant Mongo as MongoDB
User->>Web: 신갈까지 통행요금
Web->>RAG: POST /agent/chat
RAG->>WAS: FARE_SEARCH {toIc: 신갈}
WAS-->>RAG: needsClarification, missing fromIc
RAG->>Mongo: pending 저장
RAG-->>Web: clarify + pendingIntentType
Web-->>User: 출발 IC를 알려주세요
User->>Web: 판교
Web->>RAG: POST /agent/chat
RAG->>Mongo: pending 조회
RAG->>WAS: FARE_SEARCH {fromIc: 판교, toIc: 신갈}
WAS-->>RAG: domainData
RAG->>Mongo: pending clear
RAG-->>Web: domain answer
```
| 저장소 | 역할 |
|--------|------|
| `AgentPendingStore` | `agent_pending` Mongo collection, 실패 시 in-memory fallback |
| 대화 이력 | `rag_conversations` collection, 재작성·통계·문맥 참고 |
| Skill | pending을 직접 소유하지 않고 응답 필드를 UI에 반영 |
| WAS | stateless. 매 요청의 params를 기준으로 검증/조회 |
---
## 8. 최종 답변 생성
Agent loop의 `final_content`는 그대로 사용자에게 내보내지 않습니다.
근거가 있으면 `PromptBuilder`로 다시 최종 답변을 생성하고, 근거가 없으면 guidance로 전환합니다.
```mermaid
flowchart TD
ToolResult[tool 결과]
Domain{사용 가능한 domainData?}
RAG{Qdrant references?}
Prompt[PromptBuilder.build_answer_prompt_messages]
LLM[LLMHandler.generate_answer_from_messages]
Suggest[낮은 신뢰도면 제안 질문 추가]
Guide[PromptBuilder.build_guidance_prompt]
Save[대화 이력 저장]
Out[응답 반환]
ToolResult --> Domain
Domain -->|yes| Prompt
Domain -->|no| RAG
RAG -->|yes| Prompt
RAG -->|no| Guide
Prompt --> LLM --> Suggest --> Save --> Out
Guide --> Save --> Out
```
### 응답 주요 필드
| 필드 | 의미 |
|------|------|
| `answer`, `llmAnswer` | 사용자에게 보여줄 텍스트 |
| `routeType` | `agent`, `domain`, `web_domain`, `clarify`, `greeting` 등 |
| `intentType` | 도메인 tool 상세 (`FARE_SEARCH`, `IC_TEL` 등) |
| `domainData` | 도메인 조회 결과 |
| `references` | Qdrant FAQ 근거 |
| `faqUrls` | HTTP/HTTPS 원문 URL 최대 3개 |
| `toolTrace` | 호출 tool, arguments, 성공/실패 |
| `needsClarification` | 되물음 여부 |
| `pendingIntentType`, `missingParams` | 멀티턴 슬롯필링 정보 |
---
## 9. Web 채팅 흐름 (`/web/ask`)
현재 웹 채팅은 카카오 `SkillResponse`를 우회합니다.
`WebChatService`가 Agent 응답을 받은 뒤 `WebChatResponseMapper`가 웹 전용 DTO로 변환합니다.
```mermaid
flowchart TD
App[chatbotApp]
Controller[WebChatController]
Service[WebChatService]
AgentSvc[WebChatAgentService]
RAG[exAiChatBot /agent/chat]
Mapper[WebChatResponseMapper]
DTO[WebAskResponse]
App -->|POST /web/ask| Controller --> Service --> AgentSvc
AgentSvc -->|agent.mode=agent| RAG
RAG --> Mapper
Mapper --> DTO --> App
```
### Web 전용 변환
| 원본 필드 | 웹 UI |
|-----------|-------|
| `answer` | MarkdownBody 렌더링 |
| `references`, `faqUrls` | 원문 버튼 |
| `domainData`, `uiType` | 요금/주유/브랜드/음식/전화 카드 |
| `icCandidates`, `quickReplies` | 빠른 선택 버튼 |
| 제안 질문 블록 | 본문 안에서 클릭 가능한 세로 버튼 |
---
## 10. FAQ 데이터 적재 흐름
### 10.1 Batch ingest
```mermaid
flowchart TD
Raw[data/qa_raw.jsonl]
Load[q/a + source_created_at 검사]
Dedup[동일 질문 최신 source_created_at만 유지]
Embed[EmbeddingClient.embed\nis_query=false]
Upsert[Qdrant upsert_vectors\nskip_if_older=true]
QD[(qa_vectors)]
Raw --> Load --> Dedup --> Embed --> Upsert --> QD
```
| 단계 | 내용 |
|------|------|
| 입력 | `q`/`a` 또는 `question`/`answer` 지원 |
| 필수 | 질문, 답변, `source_created_at` |
| 중복 | 동일 질문은 최신 `source_created_at`만 유지 |
| 임베딩 | 질문 `q`만 document 모드로 임베딩 |
| 저장 | payload에 `q`, `a`, `category`, `url`, `source`, `source_id`, 날짜 저장 |
| 갱신 | 기존 데이터가 더 최신이면 upsert skip |
### 10.2 Vector Admin API
`scripts/admin_service.py`는 `chatbotAdmin`과 운영자가 사용하는 Qdrant 큐레이션 API입니다.
| Method | URL | 용도 |
|--------|-----|------|
| `POST` | `/api/search` | 벡터 의미 검색 |
| `POST` | `/api/keyword-search` | 문자열 포함 검색 (`q`, `a`, `both`) |
| `GET` | `/api/points` | FAQ 목록/페이지 조회 |
| `GET` | `/api/points/{id}` | 단건 조회 |
| `POST` | `/api/points` | FAQ 추가 |
| `PUT` | `/api/points/{id}` | FAQ 수정, 질문 변경 시 새 ID 생성 후 기존 ID 삭제 |
| `DELETE` | `/api/points/{id}` | FAQ 삭제 |
| `POST` | `/api/points/delete-batch` | 일괄 삭제 |
| `GET` | `/api/categories` | 카테고리 목록/건수 |
> `/api/keyword-search`는 Admin 검색용 API입니다. 챗봇 `rag_search`는 같은 Qdrant 문자열 검색 메서드를 직접 호출하되, 벡터 검색 실패 후 guidance 직전에 최대 3건만 보조 검색합니다.
---
## 11. chatbotAdmin 학습 관리 흐름
```mermaid
flowchart TD
Excel[엑셀 업로드]
Rows[TrainingQaRow 파싱]
Similar[유사 FAQ 검색\nFaqSimilarityService]
Decision{유사 후보 있음?}
Add[Qdrant 즉시 적재]
Conflict[training_conflicts 저장\n검토 대기]
RowLog[training_job_rows 저장\n행별 결과]
Review[운영자 검토]
Action{검토 액션}
Keep[기존 유지 / 신규 폐기]
New[신규 추가]
Update[기존 업데이트\n질문 변경 시 delete + create]
Excel --> Rows --> Similar --> Decision
Decision -->|없음| Add --> RowLog
Decision -->|있음| Conflict --> RowLog --> Review --> Action
Action --> Keep
Action --> New
Action --> Update
```
| 저장소 | 역할 |
|--------|------|
| `training_jobs` | 업로드 작업 단위 상태·집계 |
| `training_job_rows` | 모든 행의 결과 (`ADDED`, `CONFLICT`, `SKIPPED_*`, `FAILED`) |
| `training_conflicts` | 유사 FAQ 충돌 검토 대상만 저장 |
| Qdrant | 최종 FAQ 원천 |
### 검토 액션
| 액션 | 결과 |
|------|------|
| `keep_existing` / `skip_new` | 기존 FAQ 유지, 신규 데이터 폐기 |
| `add_new` / `create_new` | 신규 FAQ 추가 |
| `update_existing` | 기존 FAQ 수정. 질문이 바뀌면 기존 FAQ 삭제 후 신규 생성 |
| `replace_existing` | 기존 FAQ 삭제 후 신규 FAQ 생성 |
삭제나 교체 전에는 `FaqService`가 기존 FAQ를 버전 스냅샷으로 남깁니다.
다만 현재 검토 화면에는 즉시 되돌리기 버튼은 없으므로, 삭제성 액션에는 확인 UX를 추가하는 것이 안전합니다.
---
## 12. 로깅·통계
Agent 응답은 MongoDB 대화 이력에 저장됩니다.
| 필드 | 용도 |
|------|------|
| `bot_id` | 대화 단위 |
| `user_query` | 사용자 발화 |
| `ai_response` | 최종 답변 |
| `matched_questions` | 참고 FAQ 질문 |
| `scores` | rerank 점수 |
| `metadata.routeType` | 처리 경로 |
| `metadata.intentType` | 도메인 tool 상세 |
| `metadata.answer_confidence` | high/medium/low |
| `metadata.num_references` | 참고 문서 수 |
`chatbotAdmin`의 통계 분석은 이 실제 대화 이력을 기준으로 실패질문, 전체 로그, 많이 묻는 질문, 근거 구분(Qdrant/도메인 툴)을 보여줍니다.
---
## 13. 주요 파일 역할
| 파일 | 역할 |
|------|------|
| `scripts/run_service_qa.py` | FastAPI 엔트리포인트, `/agent/chat`, `/health` |
| `scripts/agent/agent_service.py` | Agent tool loop, pending, 강제 RAG, 도메인 가드, guidance |
| `scripts/agent/tool_executor.py` | `rag_search`, `ask_user`, WAS domain tool 실행 |
| `scripts/agent/chatbot_tool_client.py` | WAS `/api/v1/tools/*` HTTP client |
| `scripts/agent/pending_store.py` | Agent pending 저장소 |
| `scripts/handlers/search_handler.py` | Qdrant 검색과 rerank |
| `scripts/handlers/prompt_builder.py` | 최종 답변/guidance 프롬프트 |
| `scripts/handlers/query_rewriter.py` | 대화 이력 기반 검색어 재작성 |
| `scripts/handlers/llm_handler.py` | 최종 LLM 답변 생성 |
| `scripts/handlers/response_handler.py` | 대화 이력 저장·응답 보조 유틸 |
| `scripts/vector_store.py` | QdrantStore 구현 |
| `scripts/admin_service.py` | Vector Admin API |
| `scripts/ingest_qa.py` | batch FAQ 적재 |
| `scripts/api_clients.py` | LLM/Embedding/Reranker client |
---
## 14. 운영 확인 포인트
| 확인 항목 | 기준 |
|-----------|------|
| RAG API | `GET :28012/health`가 `ok` 또는 원인 확인 가능한 `degraded` 반환 |
| Qdrant 적재 | `/health.vector_count > 0`, Admin 목록에서 FAQ 조회 |
| Agent tool | `/agent/chat` 응답에 `toolTrace` 확인 |
| FAQ 근거 | FAQ 답변에 `references`, `faqUrls` 포함 |
| 도메인 근거 | 도메인 질문에 `intentType`, `domainData`, `uiType` 포함 |
| 멀티턴 | clarify 후 다음 발화에서 pending이 이어지는지 확인 |
| Web UI | `/web/ask` 응답이 `WebAskResponse` 형태로 카드/버튼을 포함 |
| 학습 관리 | 업로드 후 `training_job_rows`, `training_conflicts` 상태 확인 |