# 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://: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":": 테스트","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" ```