4b86b2a660
- server-dev start/stop/deploy 및 Gitea push 자동 배포 - local-dev 로컬 개발 환경 Co-authored-by: Cursor <cursoragent@cursor.com>
712 lines
26 KiB
Markdown
712 lines
26 KiB
Markdown
# Docker 운영 가이드 (Slim 버전, 폐쇄망 기준)
|
|
|
|
사용 파일: `docker-compose-slim.yml`
|
|
이미지: `rag-batch-slim` (임베딩), `rag-api-slim` (API·어드민), `qdrant/qdrant` (벡터 DB)
|
|
|
|
> **벡터 스토어는 Qdrant 단일 구성입니다(FAISS 미사용).** 이 문서는 Qdrant 기준입니다.
|
|
> 이 문서는 **폐쇄망 서버에서 이미지를 새로 빌드하지 않고 실행**하는 것을 기본 전제로 합니다.
|
|
> `docker-compose build`, `docker compose --build`, `docker pull`은 인터넷 또는 내부 mirror/registry가 준비된 경우에만 사용하세요.
|
|
|
|
---
|
|
|
|
## 전체 구조 한눈에 보기
|
|
|
|
```
|
|
[로컬 PC / 서버]
|
|
│
|
|
├── excel_to_jsonl.py ← 수동 실행 (Docker 외부)
|
|
│ xlsx → data/qa_raw.jsonl
|
|
│
|
|
└── docker-compose-slim.yml
|
|
│
|
|
├─ [qdrant 서비스] 벡터 DB (상시 실행, ./qdrant_data 영속 저장)
|
|
│
|
|
├─ [embed 서비스] ingest_qa.py ← qdrant 기동 후 시작
|
|
│ qa_raw.jsonl → Embedding(게이트웨이) → 벡터 생성 → Qdrant upsert
|
|
│ 완료 후 자동 종료 (restart: no)
|
|
│
|
|
├─ [index 서비스] build_index_qa.py ← embed 완료 후 자동 시작
|
|
│ Qdrant 모드에서는 할 일이 없어 자동 스킵 (FAISS 전용 단계)
|
|
│ 완료 후 자동 종료 (restart: no)
|
|
│
|
|
├─ [api 서비스] run_service_qa.py ← qdrant 후 자동 시작
|
|
│ POST /ask 엔드포인트 (포트 28012)
|
|
│
|
|
└─ [admin 서비스] admin_service.py ← 벡터DB 큐레이션 웹/API (포트 28013)
|
|
api와 같은 이미지, 같은 Qdrant 공유
|
|
```
|
|
|
|
### 서비스 의존 관계
|
|
|
|
```
|
|
qdrant ──▶ embed ──(완료)──▶ index(스킵) ──▶ api
|
|
└────────────────────────────────────▶ admin
|
|
(상시) (1회성) (1회성/스킵) (상시) (상시)
|
|
```
|
|
|
|
> `embed`, `index`는 작업 완료 후 컨테이너가 자동 종료됩니다.
|
|
> `qdrant`, `api`, `admin`은 상시 실행되며 오류 시 자동 재시작합니다.
|
|
> **벡터 스토어는 Qdrant 단일 구성**입니다. 벡터 데이터는 `./qdrant_data`에 영속 저장됩니다.
|
|
|
|
---
|
|
|
|
## 사전 준비
|
|
|
|
### 1. 디렉토리 구조 확인
|
|
|
|
```
|
|
exAiChatBot/
|
|
├── data/ ← 데이터 파일 위치 (없으면 mkdir data)
|
|
│ └── 원본.xlsx ← 여기에 엑셀 파일 배치
|
|
├── scripts/ ← Python 소스 코드
|
|
├── docker/ ← Dockerfile들
|
|
├── docker-compose-slim.yml
|
|
├── env.template ← .env 템플릿
|
|
└── images/ ← 폐쇄망 반입용 Docker image tar 파일 위치(권장)
|
|
```
|
|
|
|
### 2. .env 파일 생성 (최초 1회)
|
|
|
|
```bash
|
|
cp env.template .env
|
|
```
|
|
|
|
`.env`에서 반드시 수정할 항목:
|
|
|
|
```bash
|
|
# 모델 게이트웨이 (HTTPS + Bearer 인증 + 자체서명 TLS)
|
|
GATEWAY_IP=172.16.163.96 # extra_hosts 매핑용 (DNS 미등록 시)
|
|
MODEL_API_KEY=<실제 발급 키> # ★ 모든 모델 공통 인증 키 (필수 교체)
|
|
MODEL_VERIFY_SSL=false # 자체서명 인증서 → 검증 비활성화
|
|
|
|
# LLM (답변 생성·intent 분석·질문 재작성)
|
|
LLM_BASE_URL=https://llm-ai.ex.co.kr
|
|
LLM_MODEL_NAME=Qwen/Qwen3.6-27B-FP8
|
|
|
|
# Embedding (텍스트 → 벡터)
|
|
TEI_EMBED_URL=https://embedding-ai.ex.co.kr
|
|
EMBED_MODEL_NAME=Qwen/Qwen3-Embedding-8B
|
|
|
|
# Reranker (검색 결과 정렬)
|
|
TEI_RERANK_URL=https://reranker-ai.ex.co.kr
|
|
TEI_RERANK_MODEL=Qwen/Qwen3-Reranker-8B
|
|
|
|
# 벡터 DB (Qdrant, 로컬 컨테이너)
|
|
VECTOR_STORE=qdrant
|
|
QDRANT_HOST=127.0.0.1 # 모든 서비스 network_mode: host → 서비스명 대신 127.0.0.1
|
|
QDRANT_PORT=6333
|
|
QDRANT_COLLECTION=qa_vectors
|
|
VECTOR_BATCH_SIZE=100 # Qdrant 저장 배치 (4096차원 32MB 한도 초과 방지)
|
|
|
|
# 큐레이션 어드민
|
|
ADMIN_PORT=28013
|
|
|
|
# MongoDB 호스트 (대화 이력 저장) — host 네트워크면 127.0.0.1
|
|
MONGO_HOST=127.0.0.1
|
|
MONGO_PORT=27017
|
|
MONGO_DATABASE=chat_history
|
|
MONGO_COLLECTION=rag_conversations
|
|
```
|
|
|
|
> DNS 미등록이면 compose `extra_hosts`(이미 포함) 또는 서버 `/etc/hosts`에
|
|
> `172.16.163.96 llm-ai.ex.co.kr embedding-ai.ex.co.kr reranker-ai.ex.co.kr` 매핑이 필요합니다.
|
|
|
|
### 3. 폐쇄망 Docker 이미지 반입 (최초 1회)
|
|
|
|
인터넷이 되는 환경에서 이미지를 빌드/저장한 뒤 폐쇄망 서버로 `tar` 파일을 반입합니다.
|
|
|
|
#### 인터넷 가능 환경에서 이미지 생성
|
|
|
|
```bash
|
|
cd exAiChatBot
|
|
|
|
# 내부/외부 네트워크가 가능한 곳에서만 실행
|
|
mkdir -p images
|
|
docker compose -f docker-compose-slim.yml build
|
|
docker pull qdrant/qdrant:v1.18.2
|
|
|
|
docker save rag-api-slim:latest -o images/rag-api-slim_latest.tar
|
|
docker save rag-batch-slim:latest -o images/rag-batch-slim_latest.tar
|
|
docker save qdrant/qdrant:v1.18.2 -o images/qdrant_v1.18.2.tar
|
|
```
|
|
|
|
#### 폐쇄망 서버에서 이미지 로드
|
|
|
|
```bash
|
|
cd exAiChatBot
|
|
|
|
docker load -i images/rag-api-slim_latest.tar
|
|
docker load -i images/rag-batch-slim_latest.tar
|
|
docker load -i images/qdrant_v1.18.2.tar # ★ compose의 qdrant.image 태그와 일치해야 함
|
|
|
|
docker images | grep -E "rag-|qdrant"
|
|
```
|
|
|
|
> 폐쇄망 서버에서는 이미지가 이미 존재해야 합니다. 이미지가 없는 상태에서 `docker compose up`을 실행하면 `build:` 설정 때문에 빌드를 시도할 수 있고, `apt-get`/`pip install` 단계에서 실패할 수 있습니다.
|
|
|
|
---
|
|
|
|
## 시나리오 1: 최초 실행 (처음 세팅)
|
|
|
|
```bash
|
|
# 1. 엑셀 → JSONL 변환 (Docker 외부, 로컬에서 실행)
|
|
# 기본: 컬럼명 자동 감지 ("질문"/"답변" 또는 "question"/"answer")
|
|
python scripts/excel_to_jsonl.py data/파일명.xlsx
|
|
|
|
# 특정 열 직접 지정 시 (예: 3열=질문, 4열=답변, 0부터 카운트)
|
|
python scripts/excel_to_jsonl.py data/파일명.xlsx 2 3
|
|
|
|
# 결과: data/qa_raw.jsonl 생성 확인
|
|
# 형식: {"q": "질문 원문", "a": "답변 원문"}
|
|
|
|
# 2. Docker 이미지 로드 여부 확인 (폐쇄망에서는 build 금지)
|
|
docker images | grep rag-api-slim
|
|
docker images | grep rag-batch-slim
|
|
|
|
# 3. 전체 파이프라인 실행 (이미지 재빌드 없음)
|
|
# embed → index → api 순서 자동 처리
|
|
docker compose -f docker-compose-slim.yml up -d --no-build
|
|
|
|
# 4. 실행 상태 확인
|
|
docker compose -f docker-compose-slim.yml ps
|
|
|
|
# 5. 임베딩/인덱싱 진행 로그 확인
|
|
docker compose -f docker-compose-slim.yml logs -f embed
|
|
docker compose -f docker-compose-slim.yml logs -f index
|
|
|
|
# 6. API 서비스 정상 기동 확인
|
|
docker compose -f docker-compose-slim.yml logs -f api
|
|
curl http://localhost:28012/health
|
|
```
|
|
|
|
**정상 기동 시 로그 예시:**
|
|
```
|
|
[Service] ✅ 대화 이력 기능 활성화
|
|
[Service] 벡터 스토어 로드 완료: 1234개
|
|
[Service] 초기화 완료!
|
|
```
|
|
|
|
---
|
|
|
|
## 시나리오 2: 데이터 변경 후 재적재 (Qdrant)
|
|
|
|
Qdrant는 **결정적 ID(`source_id`/질문 기반 UUID)** 를 쓰므로, 같은 항목은 다시 넣으면 **덮어쓰기(upsert)** 됩니다. 따라서 일반적인 추가/수정은 인덱스 삭제 없이 embed만 다시 돌리면 됩니다.
|
|
|
|
### A) 추가/수정만 (가장 흔함) — 삭제 불필요
|
|
```bash
|
|
# 1. qa_raw.jsonl 갱신 (새 줄 추가 또는 기존 줄 수정)
|
|
# (엑셀에서 만들 경우) python scripts/excel_to_jsonl.py data/새파일명.xlsx
|
|
|
|
# 2. embed만 재실행 → Qdrant에 upsert (api/admin은 그대로 둬도 즉시 반영)
|
|
docker compose -f docker-compose-slim.yml up embed
|
|
|
|
# 3. 반영 확인
|
|
curl http://localhost:6333/collections/qa_vectors # points_count
|
|
curl http://localhost:28013/api/stats
|
|
```
|
|
> ⚠️ embed는 **upsert만** 합니다. jsonl에서 줄을 **지워도 Qdrant에서 자동 삭제되지 않습니다.** 개별 삭제는 어드민 API(`DELETE /api/points/{id}`)를 사용하세요.
|
|
|
|
### B) 완전 초기화 후 처음부터 (스테일 데이터까지 제거)
|
|
```bash
|
|
# 1. 전체 중지
|
|
docker compose -f docker-compose-slim.yml down
|
|
|
|
# 2. Qdrant 저장소 비우기 (벡터 전체 삭제) — 바인드 마운트라 디렉터리 삭제
|
|
rm -rf qdrant_data
|
|
|
|
# 3. (선택) 원본 갱신
|
|
python scripts/excel_to_jsonl.py data/새파일명.xlsx # → data/qa_raw.jsonl
|
|
|
|
# 4. 전체 재기동 (qdrant → embed 재적재 → api/admin)
|
|
docker compose -f docker-compose-slim.yml up -d --no-build
|
|
|
|
# 5. 진행 확인
|
|
docker compose -f docker-compose-slim.yml logs -f embed
|
|
```
|
|
|
|
---
|
|
|
|
## 시나리오 3: API 서비스만 재시작
|
|
|
|
xlsx와 벡터DB는 그대로이고, API 서버만 다시 띄우는 경우입니다.
|
|
|
|
```bash
|
|
# 방법 1: 단순 재시작
|
|
# - scripts/*.py만 바뀐 경우
|
|
# - 컨테이너 생성 환경변수/compose 설정 변경이 없는 경우
|
|
docker compose -f docker-compose-slim.yml restart api
|
|
|
|
# 방법 2: 중지 후 재시작
|
|
docker compose -f docker-compose-slim.yml stop api
|
|
docker compose -f docker-compose-slim.yml start api
|
|
|
|
# 방법 3: .env 또는 docker-compose-slim.yml environment/command 변경 반영
|
|
# - 기존 컨테이너를 제거하고 현재 compose/.env 기준으로 새로 생성
|
|
# - 이미지 재빌드는 하지 않음
|
|
docker compose -f docker-compose-slim.yml up -d --no-build --no-deps --force-recreate api
|
|
```
|
|
|
|
> `scripts/` 폴더는 볼륨 마운트(`./scripts:/app/scripts`)로 컨테이너와 공유됩니다.
|
|
> Python 코드 수정 후 `restart api`만 해도 변경이 반영됩니다.
|
|
>
|
|
> `.env`, `environment`, `command`, `ports`, `volumes` 등 컨테이너 생성 설정을 바꾼 경우에는
|
|
> `restart`가 아니라 `up -d --no-build --no-deps --force-recreate api`를 사용하세요.
|
|
|
|
---
|
|
|
|
## 시나리오 4: Dockerfile 또는 패키지 변경
|
|
|
|
폐쇄망 서버에서는 Dockerfile 변경 후 직접 `--build` 하지 않는 것을 권장합니다.
|
|
Dockerfile에는 `apt-get update`, `pip install`이 포함되어 있어 인터넷 또는 내부 mirror가 없으면 실패합니다.
|
|
|
|
### 폐쇄망 권장 절차
|
|
|
|
```bash
|
|
# 1. 인터넷/내부 mirror 가능 환경에서 이미지 재빌드
|
|
docker compose -f docker-compose-slim.yml build api
|
|
|
|
# 2. 이미지 저장
|
|
docker save rag-api-slim:latest -o images/rag-api-slim_latest.tar
|
|
|
|
# 3. 폐쇄망 서버로 tar 반입 후 로드
|
|
docker load -i images/rag-api-slim_latest.tar
|
|
|
|
# 4. 폐쇄망 서버에서 api 컨테이너 재생성
|
|
docker compose -f docker-compose-slim.yml up -d --no-build --no-deps --force-recreate api
|
|
```
|
|
|
|
### 폐쇄망에서 worker 수만 바꾸는 경우
|
|
|
|
Dockerfile의 `CMD --workers`를 바꾸기 위해 이미지를 재빌드하지 말고, `docker-compose-slim.yml`의 `api.command`로 덮어쓰세요.
|
|
|
|
```yaml
|
|
api:
|
|
image: "${RAG_API_IMAGE:-rag-api-slim:latest}"
|
|
command:
|
|
- python
|
|
- -m
|
|
- uvicorn
|
|
- scripts.run_service_qa:app
|
|
- --host
|
|
- 0.0.0.0
|
|
- --port
|
|
- "28012"
|
|
- --workers
|
|
- "8"
|
|
```
|
|
|
|
반영:
|
|
|
|
```bash
|
|
docker compose -f docker-compose-slim.yml up -d --no-build --no-deps --force-recreate api
|
|
```
|
|
|
|
### 온라인 또는 내부 mirror가 준비된 환경에서만 사용
|
|
|
|
```bash
|
|
docker compose -f docker-compose-slim.yml up -d --no-deps --build --force-recreate api
|
|
```
|
|
|
|
---
|
|
|
|
## 시나리오 5: Qdrant 벡터DB (현재 기본 구성)
|
|
|
|
현재 운영 기본 벡터 스토어는 **Qdrant**입니다. `docker-compose-slim.yml`의 `qdrant` 서비스는 profile이 제거되어 **별도 플래그 없이 항상 기동**되며, `api`/`embed`가 `depends_on`으로 qdrant 기동을 기다립니다.
|
|
|
|
```bash
|
|
# .env 설정 (이미 반영됨)
|
|
VECTOR_STORE=qdrant
|
|
QDRANT_HOST=127.0.0.1 # api/embed가 network_mode: host 이므로 서비스명(qdrant) 대신 127.0.0.1
|
|
QDRANT_PORT=6333
|
|
QDRANT_COLLECTION=qa_vectors
|
|
|
|
# 일반 기동 (--profile 불필요, qdrant 자동 포함)
|
|
docker compose -f docker-compose-slim.yml up -d --no-build
|
|
|
|
# Qdrant Web UI 확인
|
|
# http://localhost:6333/dashboard
|
|
```
|
|
|
|
> **이미지 태그 일치**: 폐쇄망에서는 `qdrant/qdrant` 이미지를 사전에 `docker load` 해야 하며,
|
|
> compose의 `qdrant.image` 태그(`v1.18.2`)와 **반입한 이미지 태그가 반드시 일치**해야 합니다.
|
|
> ```bash
|
|
> # (인터넷 환경) 이미지 저장
|
|
> docker pull qdrant/qdrant:v1.18.2
|
|
> docker save qdrant/qdrant:v1.18.2 -o images/qdrant_v1.18.2.tar
|
|
> # (폐쇄망 서버) 로드
|
|
> docker load -i images/qdrant_v1.18.2.tar
|
|
> ```
|
|
|
|
> **영속/백업**: 벡터는 `./qdrant_data`(→ `/qdrant/storage`)에 저장됩니다. 백업 시 이 디렉터리를 보관하세요.
|
|
|
|
> Qdrant 모드에서는 `build_index_qa.py`(index 서비스)가 자동으로 스킵됩니다.
|
|
> `ingest_qa.py`에서 임베딩과 동시에 실시간 인덱싱(컬렉션 upsert)이 완료되기 때문입니다.
|
|
|
|
---
|
|
|
|
## 시나리오 6: 큐레이션 어드민 (벡터DB 검색/조회/삭제/추가 웹)
|
|
|
|
`admin` 서비스는 실시간 `api`와 **같은 이미지/모듈**을 재사용하되 **별도 프로세스**로 분리되어, 벡터DB를 웹에서 관리합니다. (Qdrant 전용)
|
|
|
|
```bash
|
|
# 전체 기동 시 admin 도 함께 뜸 (compose 기본 포함)
|
|
docker compose -f docker-compose-slim.yml up -d --no-build
|
|
|
|
# 어드민만 재시작
|
|
docker compose -f docker-compose-slim.yml up -d --no-build --no-deps --force-recreate admin
|
|
docker compose -f docker-compose-slim.yml logs -f admin
|
|
```
|
|
|
|
접속:
|
|
|
|
```
|
|
http://<WAS서버>:28013/ ← 웹 UI (검색/목록/추가/삭제)
|
|
```
|
|
|
|
제공 기능 / API:
|
|
|
|
| 기능 | 엔드포인트 |
|
|
|------|-----------|
|
|
| 통계(벡터 수) | `GET /api/stats` |
|
|
| 의미 검색(+필터) | `POST /api/search` (body: `{query, top_k, category?, source?}`) |
|
|
| 키워드(문자열 포함) 검색 | `POST /api/keyword-search` (body: `{keyword, field?, category?, source?, limit?}`) |
|
|
| 카테고리 목록 | `GET /api/categories` |
|
|
| 목록(페이지네이션+필터) | `GET /api/points?limit=&offset=&category=&source=` |
|
|
| 단건 조회 | `GET /api/points/{id}` |
|
|
| 단건 수정 | `PUT /api/points/{id}` (body: `{q?,a?,category?,url?}`) |
|
|
| 단건 삭제 | `DELETE /api/points/{id}` |
|
|
| 일괄 삭제 | `POST /api/points/delete-batch` (body: `{ids:[...]}`) |
|
|
| 추가(텍스트→임베딩→upsert) | `POST /api/points` (body: `{q,a,category?,url?}`) |
|
|
|
|
> 상세 요청/응답 스키마는 `VECTORDB_ADMIN_API.md`(웹 개발자 핸드오프 문서) 참고.
|
|
|
|
특징:
|
|
|
|
> - **결정적 ID**: 모든 point는 `source_id`(또는 질문) 기반 UUID로 저장됩니다. 같은 FAQ를 다시 넣으면 **덮어쓰기**되어 중복이 생기지 않고, 전체 reingest 후에도 같은 항목은 같은 ID를 유지합니다.
|
|
> - **추가**는 질문을 임베딩 서버(TEI)로 벡터화한 뒤 Qdrant에 upsert합니다. 따라서 임베딩 서버(`TEI_EMBED_URL`)에 접근 가능해야 합니다.
|
|
> - 어드민과 `api`는 같은 Qdrant 컬렉션을 공유하므로, 어드민에서 추가/삭제하면 `/ask` 검색에 **즉시 반영**됩니다(재시작 불필요).
|
|
> - 포트는 `.env`의 `ADMIN_PORT`(기본 28013)로 조정합니다.
|
|
|
|
> ⚠️ 어드민은 내부망 운영 도구입니다. 외부 노출 시 별도 인증/접근제어를 두세요(현재 인증 미포함).
|
|
|
|
---
|
|
|
|
## 자주 쓰는 관리 명령어
|
|
|
|
### 상태 확인
|
|
|
|
```bash
|
|
# 전체 서비스 실행 상태 확인
|
|
docker compose -f docker-compose-slim.yml ps
|
|
|
|
# API 헬스체크 (모든 외부 API 상태 포함)
|
|
curl http://localhost:28012/health
|
|
# 응답: {"status": "ok", "vector_count": 1234, "api_services": {"llm": true, "tei_embed": true, "tei_rerank": true}}
|
|
```
|
|
|
|
### 로그 확인
|
|
|
|
```bash
|
|
# api 서비스 실시간 로그 (Ctrl+C로 종료)
|
|
docker compose -f docker-compose-slim.yml logs -f api
|
|
|
|
# embed 서비스 로그 (임베딩 완료 여부 확인)
|
|
docker compose -f docker-compose-slim.yml logs embed
|
|
|
|
# index 서비스 로그 (인덱싱 완료 여부 확인)
|
|
docker compose -f docker-compose-slim.yml logs index
|
|
|
|
# 최근 100줄만 확인
|
|
docker compose -f docker-compose-slim.yml logs --tail=100 api
|
|
```
|
|
|
|
### 중지/제거
|
|
|
|
```bash
|
|
# 모든 서비스 중지 (컨테이너 제거, 데이터는 유지)
|
|
docker compose -f docker-compose-slim.yml down
|
|
|
|
# 모든 서비스 일시 중지 (컨테이너 유지)
|
|
docker compose -f docker-compose-slim.yml stop
|
|
|
|
# 특정 서비스만 중지
|
|
docker compose -f docker-compose-slim.yml stop api
|
|
```
|
|
|
|
### API 테스트
|
|
|
|
```bash
|
|
# 기본 질문 테스트
|
|
curl -X POST http://localhost:28012/ask \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"query": "하이패스 단말기는 어디서 구매하나요?", "botId": "test-user"}'
|
|
|
|
# domainData 포함 테스트 (chatbotApi 경유 시)
|
|
curl -X POST http://localhost:28012/ask \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"query": "판교에서 신갈까지 요금이 얼마야?",
|
|
"botId": "test-user",
|
|
"intentType": "FARE_SEARCH",
|
|
"domainData": {
|
|
"llmSummary": "판교→신갈 구간 통행요금: 1종 2,200원"
|
|
}
|
|
}'
|
|
|
|
# 응답 예시:
|
|
# {
|
|
# "answer": "LLM 생성 답변",
|
|
# "matched_questions": ["관련 질문1", ...],
|
|
# "scores": [0.95, 0.88, ...],
|
|
# "num_references": 5,
|
|
# "botId": "test-user",
|
|
# "rerank_info": { ... }
|
|
# }
|
|
|
|
# 헬스체크
|
|
curl http://localhost:28012/health
|
|
```
|
|
|
|
---
|
|
|
|
## 데이터/저장소 역할 정리 (Qdrant)
|
|
|
|
```
|
|
data/
|
|
├── 원본.xlsx ← 원본 엑셀 (수동 배치, Git 제외 권장)
|
|
│
|
|
├── qa_raw.jsonl ← [수동] 벡터DB 입력 원본 (q/a + source/source_id 권장)
|
|
│ embed가 이 파일을 읽어 Qdrant에 적재
|
|
│
|
|
├── qa_failed.jsonl ← [자동 누적] 검색 threshold 미달(실패) 질문 로그
|
|
│ 형식: {"q": "실패한 질문", "ts": "타임스탬프"}
|
|
│
|
|
└── qa_successed.jsonl ← [자동 누적] 검색 성공 질문 로그
|
|
|
|
qdrant_data/ ← [자동] Qdrant 벡터 저장소(영속). 실제 벡터/메타가 여기에 보관됨
|
|
```
|
|
|
|
> FAISS 시절의 `qa_vecs.jsonl` / `qa.index` / `qa_meta.pkl`은 **Qdrant 모드에서는 생성/사용하지 않습니다.**
|
|
> 벡터 실데이터는 모두 `qdrant_data/`(Qdrant 스토리지)에 있습니다.
|
|
|
|
### 데이터 갱신 방법 (요약)
|
|
| 작업 | 방법 |
|
|
|------|------|
|
|
| 추가/수정 | `qa_raw.jsonl` 갱신 후 `docker compose up embed` (결정적 ID라 덮어쓰기) 또는 어드민 API |
|
|
| 개별 삭제 | 어드민 `DELETE /api/points/{id}` (jsonl 줄 삭제로는 안 됨) |
|
|
| 전체 초기화 | `qdrant_data/` 삭제 후 재기동 |
|
|
|
|
> `qa_failed.jsonl`, `qa_successed.jsonl`은 운영 로그이므로 삭제하지 않아도 됩니다.
|
|
|
|
---
|
|
|
|
## 재적재(embed) 동작 이해 (Qdrant)
|
|
|
|
- Qdrant 모드에서 `embed`는 실행 때마다 `qa_raw.jsonl` 전체를 임베딩해 **upsert**합니다. 결정적 ID라 **중복은 생기지 않습니다**(같은 항목은 덮어쓰기).
|
|
- `index` 서비스는 Qdrant 모드에서 **할 일이 없어 자동 스킵**됩니다(FAISS 전용 단계).
|
|
- 즉 FAISS 시절의 "스킵 파일(`qa_vecs.jsonl`) 삭제로 강제 재인덱싱" 절차는 **불필요**합니다. 그냥 `up embed`를 다시 돌리면 됩니다.
|
|
|
|
---
|
|
|
|
## .env 주요 설정값 상세
|
|
|
|
### 내부 API 서버 설정
|
|
|
|
```bash
|
|
# 모델 게이트웨이 공통 (HTTPS + Bearer + 자체서명 TLS)
|
|
GATEWAY_IP=172.16.163.96
|
|
MODEL_API_KEY=<실제 발급 키>
|
|
MODEL_VERIFY_SSL=false
|
|
|
|
# LLM API (답변 생성 + intent 분석 + 질문 재작성)
|
|
LLM_BASE_URL=https://llm-ai.ex.co.kr
|
|
LLM_MODEL_NAME=Qwen/Qwen3.6-27B-FP8
|
|
|
|
# Embedding (텍스트 → 벡터, Qwen3-Embedding-8B, OpenAI /v1/embeddings)
|
|
TEI_EMBED_URL=https://embedding-ai.ex.co.kr
|
|
EMBED_MODEL_NAME=Qwen/Qwen3-Embedding-8B
|
|
|
|
# Reranker (검색 결과 정밀 정렬, Qwen3-Reranker-8B, /score)
|
|
TEI_RERANK_URL=https://reranker-ai.ex.co.kr
|
|
TEI_RERANK_MODEL=Qwen/Qwen3-Reranker-8B
|
|
|
|
API_TIMEOUT=60 # 모델 응답 대기 시간 (초)
|
|
```
|
|
|
|
### 벡터 DB(Qdrant) 설정
|
|
|
|
```bash
|
|
VECTOR_STORE=qdrant
|
|
QDRANT_HOST=127.0.0.1
|
|
QDRANT_PORT=6333
|
|
QDRANT_COLLECTION=qa_vectors
|
|
VECTOR_BATCH_SIZE=100 # Qdrant 1회 upsert 벡터 수 (4096차원 32MB 한도 초과 방지)
|
|
EMBED_BATCH_SIZE=100 # 임베딩 API 1회 호출 텍스트 수
|
|
```
|
|
|
|
### 검색 성능 튜닝
|
|
|
|
> 변수명에 `FAISS_` 접두어가 남아있지만 **이름만 레거시**이고 Qdrant 검색에 그대로 적용됩니다(`top_k`/`score_threshold`). 점수는 코사인 유사도.
|
|
|
|
```bash
|
|
FAISS_TOP_K=30 # 1차 벡터 검색(Qdrant) 후보 수
|
|
FAISS_THRESHOLD=0.55 # 1차 검색 최소 유사도(코사인) (낮추면 더 많이 매칭)
|
|
FAISS_THRESHOLD_REWRITE=0.50 # Query Rewriting 재검색 시 임계값 (원본보다 0.05 낮게 권장)
|
|
RERANK_CANDIDATES=20 # 재랭킹에 넘길 후보 수 (FAISS_TOP_K 이하)
|
|
RERANK_BATCH_SIZE=16 # 재랭킹 배치 크기
|
|
TOP_N_FOR_LLM=5 # LLM 프롬프트에 넣을 참고자료 수
|
|
LLM_MAX_TOKENS=2048 # LLM 최대 생성 토큰
|
|
```
|
|
|
|
### Query Rewriting 및 대화 이력
|
|
|
|
```bash
|
|
QUERY_REWRITE_ENABLED=true # 검색 실패 시 LLM이 질문을 재작성하여 재검색
|
|
# ("그럼 어디서 사나요?" → "하이패스 단말기 구매처는?")
|
|
|
|
CHAT_HISTORY_LIMIT=10 # 대화 이력 최대 참고 개수
|
|
CHAT_HISTORY_HOURS=24 # 대화 이력 조회 범위 (시간)
|
|
CHAT_HISTORY_ALWAYS_INCLUDE=true # 모든 답변에 이전 대화 맥락 반영
|
|
# false: Query Rewriting 실패 시에만 사용
|
|
```
|
|
|
|
> `.env`의 `CHAT_HISTORY_*` 값을 바꾼 경우에는 컨테이너 환경변수 반영을 위해 API 컨테이너를 재생성하세요.
|
|
> ```bash
|
|
> docker compose -f docker-compose-slim.yml up -d --no-build --no-deps --force-recreate api
|
|
> docker compose -f docker-compose-slim.yml exec api printenv | grep CHAT_HISTORY
|
|
> ```
|
|
|
|
### MongoDB 연결 (환경별 설정)
|
|
|
|
```bash
|
|
# Linux 서버에서 MongoDB가 같은 서버에 있는 경우
|
|
# docker-compose-slim.yml은 network_mode: host를 사용하므로 127.0.0.1 권장
|
|
MONGO_HOST=127.0.0.1
|
|
|
|
# Mac / Windows Docker Desktop
|
|
MONGO_HOST=host.docker.internal
|
|
|
|
# 원격 MongoDB 서버
|
|
MONGO_HOST=192.168.1.100
|
|
|
|
MONGO_PORT=27017
|
|
MONGO_USER=exlink
|
|
MONGO_PASSWORD=패스워드
|
|
MONGO_DATABASE=chat_history
|
|
MONGO_COLLECTION=rag_conversations
|
|
MONGO_TTL_DAYS=30 # 30일 후 대화 이력 자동 삭제
|
|
```
|
|
|
|
> MongoDB 연결 실패 시: Query Rewriting, 대화 이력 기능이 자동 비활성화됩니다.
|
|
> API 서비스 자체는 정상 동작합니다.
|
|
|
|
---
|
|
|
|
## 트러블슈팅
|
|
|
|
### embed 결과를 확인하고 싶을 때
|
|
|
|
```bash
|
|
docker compose -f docker-compose-slim.yml logs embed
|
|
# Qdrant 모드는 실행 때마다 전체 임베딩→upsert (결정적 ID라 중복 없음)
|
|
|
|
# 적재 결과 확인
|
|
curl http://localhost:6333/collections/qa_vectors # points_count
|
|
```
|
|
|
|
### API 서비스가 뜨지 않음
|
|
|
|
```bash
|
|
# embed 완료 여부 확인 (index는 Qdrant 모드라 스킵이 정상)
|
|
docker compose -f docker-compose-slim.yml logs embed | tail -5
|
|
|
|
# qdrant 상태 확인
|
|
curl http://localhost:6333/readyz
|
|
|
|
# api 수동 재시작
|
|
docker compose -f docker-compose-slim.yml up -d --no-build api
|
|
```
|
|
|
|
### API 응답에서 "임베딩 실패" 오류
|
|
|
|
```bash
|
|
# 임베딩 게이트웨이 접근 확인 (DNS 미등록이면 --resolve 사용)
|
|
curl -k --resolve embedding-ai.ex.co.kr:443:$GATEWAY_IP \
|
|
https://embedding-ai.ex.co.kr/v1/embeddings \
|
|
-H "Authorization: Bearer $MODEL_API_KEY" -H 'Content-Type: application/json' \
|
|
-d '{"model":"Qwen/Qwen3-Embedding-8B","input":["테스트"]}'
|
|
|
|
# .env의 임베딩 설정 확인
|
|
cat .env | grep -E "TEI_EMBED|EMBED_MODEL|MODEL_API_KEY"
|
|
```
|
|
|
|
### MongoDB 연결 실패 로그
|
|
|
|
```bash
|
|
docker compose -f docker-compose-slim.yml logs api | grep "대화 이력"
|
|
# "[Service] ❌ 대화 이력 기능 비활성화" → MongoDB 연결 실패 (API는 정상 동작)
|
|
|
|
# 컨테이너에서 MongoDB 접근 테스트
|
|
docker exec $(docker compose -f docker-compose-slim.yml ps -q api) \
|
|
python3 -c "import os; from pymongo import MongoClient; host=os.getenv('MONGO_HOST','127.0.0.1'); port=os.getenv('MONGO_PORT','27017'); MongoClient(f'mongodb://{host}:{port}/', serverSelectionTimeoutMS=3000).admin.command('ping'); print('OK')"
|
|
```
|
|
|
|
### 재랭킹 점수가 항상 None
|
|
|
|
```bash
|
|
# 리랭커 게이트웨이 접근 확인 (DNS 미등록이면 --resolve 사용)
|
|
curl -k --resolve reranker-ai.ex.co.kr:443:$GATEWAY_IP \
|
|
https://reranker-ai.ex.co.kr/score \
|
|
-H "Authorization: Bearer $MODEL_API_KEY" -H 'Content-Type: application/json' \
|
|
-d '{"model":"Qwen/Qwen3-Reranker-8B","queries":"<Query>: 테스트","documents":["a","b"]}'
|
|
|
|
# 재랭킹 실패 시 Qdrant(벡터) 상위 결과로 자동 대체됩니다 (서비스는 정상 동작)
|
|
docker compose -f docker-compose-slim.yml logs api | grep -E "Reranker|재랭킹"
|
|
# 응답 파싱 실패 로그가 보이면 /score 실제 응답 형식 확인 필요
|
|
```
|
|
|
|
### 컨테이너 내부에서 직접 디버깅
|
|
|
|
```bash
|
|
# api 컨테이너에 bash 접속
|
|
docker exec -it $(docker compose -f docker-compose-slim.yml ps -q api) bash
|
|
|
|
# 컨테이너 내 Python 스크립트 직접 실행
|
|
docker exec $(docker compose -f docker-compose-slim.yml ps -q api) \
|
|
python3 /app/scripts/api_clients.py
|
|
```
|
|
|
|
---
|
|
|
|
## 모델 게이트웨이 연결 (현재 운영 구성)
|
|
|
|
LLM·임베딩·리랭커는 **단일 게이트웨이(HTTPS, 호스트명 라우팅, Bearer 인증)** 로 호출합니다.
|
|
|
|
```bash
|
|
GATEWAY_IP=172.16.163.96
|
|
MODEL_API_KEY=<실제 발급 키>
|
|
MODEL_VERIFY_SSL=false
|
|
|
|
LLM_BASE_URL=https://llm-ai.ex.co.kr
|
|
LLM_MODEL_NAME=Qwen/Qwen3.6-27B-FP8
|
|
|
|
TEI_EMBED_URL=https://embedding-ai.ex.co.kr
|
|
EMBED_MODEL_NAME=Qwen/Qwen3-Embedding-8B
|
|
|
|
TEI_RERANK_URL=https://reranker-ai.ex.co.kr
|
|
TEI_RERANK_MODEL=Qwen/Qwen3-Reranker-8B
|
|
```
|
|
|
|
### 호스트명 해석 (DNS 미등록 시)
|
|
compose의 `extra_hosts`(embed/api/admin에 포함)가 호스트명 → `GATEWAY_IP`를 매핑합니다. 안 먹으면 서버 `/etc/hosts`에 직접 추가:
|
|
```bash
|
|
echo "172.16.163.96 llm-ai.ex.co.kr embedding-ai.ex.co.kr reranker-ai.ex.co.kr" | sudo tee -a /etc/hosts
|
|
```
|
|
정식 DNS가 등록되면 `extra_hosts`/`/etc/hosts` 매핑은 제거해도 됩니다.
|
|
|
|
### curl 단독 테스트
|
|
```bash
|
|
curl -k --resolve llm-ai.ex.co.kr:443:$GATEWAY_IP \
|
|
https://llm-ai.ex.co.kr/v1/models -H "Authorization: Bearer $MODEL_API_KEY"
|
|
```
|