포스트 아포칼립스 TRPG를 위한 로컬 RAG(Retrieval-Augmented Generation) 시스템입니다.
- 🎲 AI 던전마스터 (Ollama + Gemini API)
- 📚 벡터 검색을 통한 게임 지식 베이스
- 🗄️ PostgreSQL + pgvector를 활용한 벡터 저장소
- 🎮 실시간 게임 진행 및 상태 관리
- 📄 TRPG 문서 자동 처리 및 청킹
- Backend: Spring Boot 3.5.4, Spring AI
- Database: PostgreSQL + pgvector
- AI: Ollama (llama3.2, nomic-embed-text), Gemini API
- Build Tool: Gradle
- Java 17 이상
- PostgreSQL with pgvector extension
- Ollama (로컬 AI 모델)
-
환경변수 설정
cp .env.example .env # .env 파일을 열어서 API 키 등을 설정하세요 -
PostgreSQL 설정
CREATE DATABASE dungeontalk_rag; CREATE EXTENSION IF NOT EXISTS vector;
-
Ollama 모델 설치
ollama pull llama3.2 ollama pull nomic-embed-text
./gradlew bootRun서버가 http://localhost:8080에서 실행됩니다.
POST /api/rag/search- RAG 기반 질문 답변POST /api/rag/document- 문서 추가GET /api/rag/status- 시스템 상태 확인
POST /api/game/action- 게임 액션 처리POST /api/game/start- 게임 시작GET /api/game/status- 게임 상태 확인
POST /api/documents/load-trpg-docs- TRPG 문서 로딩POST /api/documents/add-test-doc- 테스트 문서 추가
GET /api/db-test/connection- 데이터베이스 연결 테스트
주요 설정은 src/main/resources/application.properties에서 관리됩니다:
- 데이터베이스 연결
- Ollama 설정
- RAG 파라미터
- 벡터 저장소 설정
보안이 중요한 설정(API 키 등)은 환경변수를 사용하세요.
graph TB
subgraph "Client Layer"
A[TRPG Player] --> B[REST API]
end
subgraph "Application Layer"
B --> C[RAGController]
B --> D[GameController]
B --> E[DocumentController]
C --> F[LocalRAGService]
D --> G[GameService]
E --> H[DocumentProcessor]
F --> I[VectorStore]
G --> J[GeminiService]
G --> F
H --> I
end
subgraph "AI Models Layer"
K[Ollama Server<br/>localhost:11434]
L[Gemini API<br/>Google Cloud]
K --> M[nomic-embed-text<br/>768-dim vectors]
K --> N[llama3.2<br/>Fallback Generation]
L --> O[gemini-1.5-flash<br/>Primary Generation]
end
subgraph "Database Layer"
P[(PostgreSQL<br/>Port: 5433)]
Q[pgvector Extension]
P --> Q
R[vector_store table]
S[Document Chunks]
T[Embeddings 768-dim]
U[Metadata JSON]
Q --> R
R --> S
R --> T
R --> U
end
I --> M
I --> P
J --> O
G --> N
style A fill:#e1f5fe
style K fill:#fff3e0
style L fill:#e8f5e8
style P fill:#fce4ec
style M fill:#fff3e0
style O fill:#e8f5e8
style N fill:#fff3e0
본 시스템은 하이브리드 RAG 아키텍처를 구현하여 로컬 모델과 클라우드 API의 장점을 모두 활용합니다.
-- PostgreSQL + pgvector 데이터베이스 구조
-- 벡터 확장 설치
CREATE EXTENSION IF NOT EXISTS vector;
-- 벡터 저장 테이블 (Spring AI가 자동 생성)
CREATE TABLE vector_store (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content TEXT NOT NULL, -- 문서 청크 내용
metadata JSON NOT NULL, -- 문서 메타데이터
embedding vector(768) NOT NULL, -- 768차원 임베딩 벡터
created_at TIMESTAMP DEFAULT NOW()
);
-- HNSW 인덱스 (고성능 벡터 검색)
CREATE INDEX ON vector_store
USING hnsw (embedding vector_cosine_ops);
-- 메타데이터 인덱스 (필터링 최적화)
CREATE INDEX ON vector_store
USING GIN (metadata);
-- 예시 데이터 구조
INSERT INTO vector_store (content, metadata, embedding) VALUES (
'닥터 리오는 뉴 에덴 캠프의 의사입니다...',
'{
"filename": "NPC_닥터_리오.txt",
"type": "npc",
"category": "character",
"tags": ["medical", "important"],
"world_type": "apocalypse",
"chunk_index": 0,
"total_chunks": 3
}',
'[0.1, -0.2, 0.8, ...]' -- 768차원 벡터
);sequenceDiagram
participant U as User
participant API as RAGController
participant RAG as LocalRAGService
participant VS as VectorStore
participant DB as PostgreSQL
participant OL as Ollama
participant GM as GeminiService
participant GAPI as Gemini API
U->>API: POST /api/rag/search<br/>{"query": "닥터에게 치료받고 싶어"}
API->>RAG: searchAndGenerate(query, worldType, sessionId)
Note over RAG: 1. 키워드 추출 및 최적화
RAG->>RAG: extractKeywords("닥터에게 치료받고 싶어")<br/>→ "닥터 리오 의료"
Note over RAG,DB: 2. 벡터 검색
RAG->>VS: similaritySearch(query, topK=5)
VS->>OL: POST /api/embeddings<br/>{"model": "nomic-embed-text", "prompt": "닥터 리오 의료"}
OL-->>VS: {"embedding": [0.1, -0.2, 0.8, ...]}
VS->>DB: SELECT content, metadata<br/>FROM vector_store<br/>ORDER BY embedding <=> $1<br/>LIMIT 5
DB-->>VS: 관련 문서 청크들
VS-->>RAG: List<Document> (5개)
Note over RAG: 3. 메타데이터 필터링
RAG->>RAG: 월드타입, 카테고리 필터링
Note over RAG: 4. 컨텍스트 구성
RAG->>RAG: buildContext(documents)<br/>→ "닥터 리오는 뉴 에덴 캠프의 의사..."
Note over RAG,GAPI: 5. AI 응답 생성
RAG->>GM: generateGameResponse(action, context, gameState)
GM->>GAPI: POST /v1beta/models/gemini-1.5-flash:generateContent<br/>{"contents": [{"parts": [{"text": "프롬프트"}]}]}
GAPI-->>GM: {"candidates": [{"content": {"parts": [{"text": "🏥 닥터 리오가..."}]}}]}
alt Gemini 성공
GM-->>RAG: "🏥 닥터 리오가 당신을 맞이합니다..."
else Gemini 실패
GM->>GM: generateFallbackResponse()
GM-->>RAG: "🏥 의료진을 찾아 치료를 요청합니다..."
end
RAG-->>API: {"response": "응답", "documents": [...]}
API-->>U: JSON Response
# docker-compose.yml 또는 로컬 설치
services:
ollama:
image: ollama/ollama
ports:
- "11434:11434"
volumes:
- ollama_data:/root/.ollama
# application.properties
spring.ai.ollama.base-url=http://localhost:11434
spring.ai.ollama.embedding.model=nomic-embed-text
spring.ai.ollama.chat.model=llama3.2// DocumentProcessor.java
public void processDocument(Path filePath) {
String content = Files.readString(filePath);
// 1. 문서 청킹
List<String> chunks = chunkDocument(content, 1000, 200);
// 2. 각 청크를 Document 객체로 변환
List<Document> documents = chunks.stream()
.map(chunk -> new Document(chunk, metadata))
.collect(toList());
// 3. VectorStore에 저장 (자동으로 임베딩 생성)
vectorStore.add(documents);
/*
* 내부적으로 발생하는 과정:
* - Ollama API 호출: POST /api/embeddings
* - nomic-embed-text 모델로 768차원 벡터 생성
* - PostgreSQL에 벡터와 함께 저장
*/
}// LocalRAGService.java
public String searchAndGenerate(String query, String worldType, String sessionId) {
// 1. 벡터 검색
SearchRequest request = SearchRequest.builder()
.query(query) // "닥터에게 치료받고 싶어"
.topK(5) // 상위 5개
.similarityThreshold(0.7) // 유사도 임계값
.build();
List<Document> docs = vectorStore.similaritySearch(request);
/*
* 내부 SQL 실행:
* SELECT content, metadata,
* (embedding <=> $embedding) as distance
* FROM vector_store
* WHERE (embedding <=> $embedding) < 0.3 -- 1-0.7
* ORDER BY distance
* LIMIT 5;
*/
// 2. 컨텍스트 구성
String context = buildContext(docs);
// 3. AI 응답 생성
return generateResponse(query, context);
}@Configuration
public class VectorStoreConfig {
@Bean
public VectorStore vectorStore(
@Autowired DataSource dataSource,
@Autowired EmbeddingModel embeddingModel) {
return new PgVectorStore.Builder(dataSource, embeddingModel)
.withSchemaName("public")
.withTableName("vector_store")
.withDimensions(768)
.withDistanceType(COSINE)
.withIndexType(HNSW)
.withSchemaValidation(true)
.build();
}
}# HikariCP 연결 풀 최적화
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000
# PostgreSQL 특화 설정
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true// GeminiService.java
public String generateGameResponse(String action, String context, Map<String, Object> gameState) {
try {
// 1차: Gemini API 시도 (15초 타임아웃)
return callGeminiAPI(buildPrompt(action, context, gameState));
} catch (Exception e) {
System.err.println("Gemini API 실패, Fallback 사용: " + e.getMessage());
// 2차: 로컬 Fallback (GameService)
return gameService.generateFallbackResponse(action, context, gameState);
}
}
// GameService.java - 규칙 기반 응답 생성
private String generateFallbackResponse(String action, String context, Map gameState) {
// 액션 패턴 매칭으로 적절한 응답 템플릿 선택
if (action.contains("살펴")) return generateLookAroundResponse();
if (action.contains("이동")) return generateMovementResponse();
if (action.contains("공격")) return generateCombatResponse();
// ... 패턴별 응답 생성
}- 모델:
nomic-embed-text(Ollama) - 차원: 768차원 벡터
- 특징: 한국어/영어 멀티링구얼 지원
- 용도: TRPG 문서를 벡터로 변환하여 의미적 유사도 검색
- 장점: 완전 로컬 실행, 개인정보 보호
1차 생성: Gemini 1.5 Flash (Google)
- 모델:
gemini-1.5-flash:generateContent - 토큰 제한: 300토큰 (빠른 응답)
- 온도: 0.7 (창의성과 일관성 균형)
- 용도: 상황별 맞춤형 TRPG 응답 생성
- 장점: 뛰어난 한국어 이해력, 창의적 스토리텔링
2차 생성: 로컬 Fallback
- 모델:
llama3.2(Ollama) - 용도: Gemini API 실패 시 백업 응답
- 특징: 규칙 기반 + 템플릿 응답 생성
- 장점: 항상 사용 가능, 안정적 동작
DocumentProcessor.java → DocumentLoader.java- 청킹 전략:
- 기본 청크 크기: 1000자
- 오버랩: 200자
- 문장 경계 인식 분할
- 메타데이터 추출:
- 문서 타입 (NPC, 아이템, 시나리오, 규칙 등)
- 카테고리 분류 (전투, 의료, 희귀도 등)
- 세계관 태그 (아포칼립스)
VectorStore (Spring AI) → PostgreSQL + pgvector- 인덱싱: HNSW (Hierarchical Navigable Small World)
- 거리 측정: 코사인 유사도
- 저장소: PostgreSQL 테이블에 벡터와 메타데이터 함께 저장
LocalRAGService.searchRelevantDocuments()- 검색 방식:
- Top-K 검색 (기본 5개)
- 유사도 임계값: 0.7
- 메타데이터 필터링 (world_type, category)
- 키워드 최적화:
- 액션별 키워드 매핑
- 동의어 처리 (닥터/의사, 워커/좀비)
LocalRAGService.buildContext()- 컨텍스트 길이: 최대 400자 per document
- 랭킹: 유사도 순으로 상위 3개 문서 선택
- 포맷팅: 구조화된 프롬프트 템플릿 적용
GeminiService.generateGameResponse()
↓ (실패시)
GameService.generateFallbackResponse()Gemini API 프롬프트 구조:
당신은 포스트 아포칼립스 TRPG 던전마스터입니다.
세계관: 2040년 아케론 바이러스 5년 후, 워커(좀비) 세상
현재 상황: [캐릭터 정보 + HP + 위치]
배경 정보: [RAG 검색 결과]
플레이어 행동: "[사용자 입력]"
지침: 긴장감 있는 150자 이내 한국어 응답, 이모지 사용
# PostgreSQL + pgvector 설정
spring.ai.vectorstore.pgvector.index-type=HNSW
spring.ai.vectorstore.pgvector.distance-type=COSINE_DISTANCE
spring.ai.vectorstore.pgvector.dimensions=768// 파일명 기반 자동 분류
if (fileName.startsWith("NPC_")) → type: "npc", category: "character"
if (fileName.startsWith("아이템_무기")) → type: "item", category: "weapon"
// 내용 기반 태그 추출
if (content.contains("전투|피해|공격")) → tags.add("combat")// 액션별 최적화된 키워드 매핑
if (action.contains("닥터|의사|치료")) → searchQuery = "닥터 리오 의료"
if (action.contains("워커|좀비|감염")) → searchQuery = "워커 바이러스"- 비동기 처리: Spring WebFlux 지원
- 연결 풀링: HikariCP 최적화 설정
- 캐싱: 검색 결과 메모리 캐시
- 타임아웃: Gemini API 15초 제한
- 검색 속도: ~100ms (로컬 벡터 검색)
- 응답 생성: ~2-5초 (Gemini API)
- Fallback 응답: ~50ms (로컬 생성)
- 동시 사용자: 최대 10명 (커넥션 풀 제한)
- 문서 처리: ~20개 파일/초
# 실시간 성능 확인 API
GET /api/rag/status
{
"status": "healthy",
"documentCount": 156, # 저장된 문서 청크 수
"averageResponseTime": "2.3s", # 평균 응답 시간
"cacheHitRate": "78%", # 캐시 적중률
"timestamp": 1640995200000
}
# 데이터베이스 성능
GET /api/db-test/connection
{
"vectorExtension": "INSTALLED", # pgvector 상태
"connectionPoolSize": 8, # 활성 연결 수
"queryLatency": "45ms", # 벡터 검색 지연시간
"indexStatus": "OPTIMAL" # HNSW 인덱스 상태
}-- 인덱스 상태 확인
SELECT schemaname, tablename, indexname, indexdef
FROM pg_indexes
WHERE tablename = 'vector_store';
-- 검색 성능 분석
EXPLAIN ANALYZE
SELECT content, metadata, (embedding <=> $1) as distance
FROM vector_store
WHERE (embedding <=> $1) < 0.3
ORDER BY distance
LIMIT 5;
-- 결과 예시:
-- Index Scan using vector_store_embedding_idx on vector_store (cost=0.00..25.67 rows=5 width=1024) (actual time=0.123..0.234 rows=5 loops=1)# docker-compose.yml
version: '3.8'
services:
postgres:
image: pgvector/pgvector:pg16
environment:
POSTGRES_DB: dungeontalk_rag
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${DB_PASSWORD}
ports:
- "5433:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
ollama:
image: ollama/ollama
ports:
- "11434:11434"
volumes:
- ollama_data:/root/.ollama
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
app:
build: .
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/dungeontalk_rag
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
GEMINI_API_KEY: ${GEMINI_API_KEY}
SPRING_AI_OLLAMA_BASE_URL: http://ollama:11434
depends_on:
- postgres
- ollama
volumes:
postgres_data:
ollama_data:# application-prod.properties (운영환경)
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false
logging.level.com.new_ai=INFO
# 보안 강화
server.port=8080
server.address=127.0.0.1
management.endpoints.web.exposure.include=health,metrics
# 성능 최적화
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=10
rag.search.top-k=3
rag.similarity.threshold=0.8// 메트릭 수집을 위한 설정
@Component
public class RAGMetrics {
private final MeterRegistry meterRegistry;
private final Counter searchCounter;
private final Timer responseTimer;
public RAGMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.searchCounter = Counter.builder("rag.search.total")
.description("Total RAG searches")
.register(meterRegistry);
this.responseTimer = Timer.builder("rag.response.time")
.description("RAG response time")
.register(meterRegistry);
}
}graph LR
A[현재: nomic-embed-text] --> B[업그레이드: bge-large-ko]
C[현재: Gemini 1.5 Flash] --> D[업그레이드: GPT-4o / Claude-3.5]
E[현재: Llama3.2] --> F[업그레이드: Llama3.3 / Qwen2.5]
B --> G[다국어 지원 강화]
D --> H[응답 품질 향상]
F --> I[로컬 성능 개선]
- 멀티모달: 이미지, 음성 처리 추가
- 실시간 학습: 사용자 피드백 기반 모델 파인튜닝
- 분산 처리: 여러 Ollama 인스턴스 로드밸런싱
- 캐싱 시스템: Redis를 활용한 응답 캐시
// 다른 벡터 DB 지원을 위한 인터페이스
public interface VectorStoreProvider {
void addDocuments(List<Document> documents);
List<Document> similaritySearch(String query, int topK);
}
@Component("chroma")
public class ChromaVectorStore implements VectorStoreProvider { ... }
@Component("weaviate")
public class WeaviateVectorStore implements VectorStoreProvider { ... }src/main/java/com/new_ai/
├── controller/ # REST API 컨트롤러
│ ├── RAGController.java
│ ├── GameController.java
│ ├── DocumentController.java
│ └── DatabaseTestController.java
├── service/ # 비즈니스 로직
│ ├── LocalRAGService.java
│ ├── GameService.java
│ ├── DocumentProcessor.java
│ ├── DocumentLoader.java
│ └── GeminiService.java
└── config/ # 설정
└── WebConfig.java
- API 키는 환경변수로 관리
- 데이터베이스 비밀번호는 실제 운영에서 변경 필요
- CORS 설정은 운영 환경에 맞게 조정
- Fork the repository
- Create your feature branch
- Commit your changes
- Push to the branch
- Create a Pull Request
이 프로젝트는 POC(Proof of Concept)로 개발되었습니다.