프라이빗 RAG 시스템 구축: 로컬‑우선 AI 저널에서 얻은 교훈
출처: Dev.to
대부분의 AI 앱은 조용히 여러분의 데이터를 클라우드로 전송합니다. DiaryGPT는 정반대로 동작합니다 — 그리고 이것이 전체 기술 스토리입니다.
일기를 쓸 때는 입으로는 절대 말하지 않을 것들을 적습니다. 그 텍스트가 남의 서버에 저장돼 모델을 학습시키거나, 보안 침해로 노출되는 일은 절대 원하지 않겠죠.
하지만 AI는 일기에 정말 유용합니다. 놓치기 쉬운 패턴을 찾아주고, 여러분에게 반영해 주며, 빈 페이지가 절대 물어볼 수 없는 질문을 던집니다. 프라이버시를 포기하지 않고 AI 인사이트를 얻고 싶다는 갈등이 현실입니다.
대부분의 앱은 개인정보 처리방침을 믿고 해결합니다. 저는 기술적인 보장을 원했습니다.
그래서 저는 DiaryGPT를 만들었습니다 — 기본적으로 데이터가 여러분의 기기를 떠나지 않는 AI 기반 개인 일기장. 작동 방식은 다음과 같습니다.
앱이 제공하는 기능
- AI 감정 분석 – 모든 일기에 대해 감정, 주제, 반영 응답, 후속 질문을 제공합니다.
- RAG 기반 채팅 – “내가 가장 불안했을 때는 언제였지?” 같은 질문에 실제 일기 내용을 근거로 답변합니다.
- 시맨틱 검색 – 키워드가 아니라 의미로 일기를 찾습니다. (“외로웠던 순간”이 “고립된”, “단절된”, “우울한” 등과 매치)
- 주간 회고 – 일주일 동안의 감정 흐름을 AI가 요약합니다.
- 맞춤형 일기 프롬프트 – 최근 글쓰기 패턴을 기반으로 생성됩니다.
- 작성 연속 기록 및 추억 – “작년 오늘은 … 썼었지” 같은 기능.
- AI 동반자 모드 – CBT/DBT 기반 반영과 위기 감지를 제공 (공인 치료사를 대체하지는 않음).
- 감정 체크인 – 1~10 점수 기록과 히스토리 차트.
- 음성 입력·음성 채팅 – 말로 일기를 쓰고, 응답을 음성으로 들을 수 있습니다.
- AES‑256‑GCM 전면 암호화 – 모든 일기, 채팅, 메모가 암호화됩니다.
DiaryGPT는 두 가지 모드가 있습니다. 설정에서 선택합니다.
1️⃣ 로컬 전용 모드 (기본)
모든 것이 여러분의 기기에서 실행됩니다. AI 모델, 검색, 분석 모두 Ollama를 통해 로컬에서 동작합니다.
당신의 일기 입력
↓
Ollama (nomic-embed-text) → 숫자로 변환 → SQLite에 저장
↓
Ollama (llama3.2 / qwen2.5) → 감정 분석 → 암호화된 상태로 저장
데이터가 기기를 떠나지 않습니다.
2️⃣ 클라우드 연동 모드
더 높은 추론 품질을 원하고 API 전송에 익숙한 사용자를 위한 옵션입니다. 직접 API 키(Groq, OpenAI, Anthropic, Gemini)를 제공하고, 키는 로컬에만 저장됩니다.
당신의 일기 입력
↓
Ollama (embeddings) → 여전히 로컬, 외부 전송 없음
↓
상위 5개 관련 발췌 → 선택한 제공자의 API → 답변 스트리밍
전체 일기가 전송되는 일은 없으며, 아주 작은 조각만 전송됩니다.
RAG란?
Retrieval‑Augmented Generation의 약자로, 매 요청마다 여러분이 쓴 모든 내용을 언어 모델에 보내지 않고도 AI가 여러분을 “알고” 있는 듯한 경험을 제공하는 기술입니다.
각 일기는 의미에 대한 GPS 좌표와 같은 숫자 리스트(임베딩)로 변환됩니다.
"I felt anxious today" → [0.21, 0.83, 0.12, 0.74, ...]
"I was really stressed" → [0.22, 0.81, 0.14, 0.71, ...] ← 매우 유사
"I love hiking" → [0.91, 0.12, 0.67, 0.23, ...] ← 매우 다름
유사한 의미 = 유사한 숫자. 이것이 시맨틱 검색이 작동하는 원리이며, 정확한 단어가 아니라 개념으로 검색합니다.
일기 → 임베딩 흐름
- 입력: “Today was rough. Felt anxious about the deadline.”
- Ollama (nomic-embed-text) 가 텍스트를 숫자 배열로 변환 →
[0.21, 0.83, 0.12, 0.74, …] - SQLite / PostgreSQL 에 저장
entry text→ AES‑256‑GCM 암호화embedding→ 원본 그대로 저장 (수학 연산에 필요)mood/themes→ LLM이 분석 후 암호화 저장
이 과정은 비동기적으로 진행됩니다. 일기는 즉시 저장되고, 분석은 백그라운드에서 수행됩니다.
질문 → 검색 흐름
- 질문: “When did I feel anxious about work?”
- Ollama 가 질문을 숫자 배열로 변환
- 코사인 유사도 검색 이 여러분의 로컬 DB(
sqlite-vec혹은pgvector) 에서 실행 (외부 호출 없음)
entry A: 0.91 match ✓
entry B: 0.87 match ✓
entry C: 0.79 match ✓
entry D: 0.31 match ✗ (제외)
- 상위 5개 일기를 메모리에서 복호화
- LLM 에게 시스템 프롬프트 + 일기 발췌 + 질문 전송
- LLM 이 단어 단위로 스트리밍 응답 (SSE)
핵심 인사이트: 임베딩은 읽을 대상을 찾고, LLM 은 그에 대해 말한다. LLM 은 전체 일기를 보지 않으며, 가장 관련성 높은 5개만 확인합니다. 코사인 유사도 연산은 전적으로 여러분의 서버에서만 이루어집니다. 클라우드 모드를 선택하지 않으면 외부 서비스로 데이터가 전송되지 않습니다.
위기 대응 모드
동반자 모드의 핵심 규칙: 위기 상황에서는 LLM 이 절대 실행되지 않는다.
사용자 입력 → 위기 감지 (키워드 매칭, 서버 측)
"suicide", "hurt myself", "want to die" 등
↓
CRISIS? SAFE?
↓ ↓
Hardcoded response LLM runs with CBT/DBT 프롬프트
988 + Crisis Text Acknowledges → reflects → one question
Line + findahelpline
LLM never called 암호화된 상태로 companion_messages 에 저장
위기 대응은 하드코딩되어 있어, 프롬프트 조작으로 회피하거나 변조할 수 없습니다. UI 에 표시되는 배너 “This is an AI companion, not a licensed therapist” 역시 하드코딩되어 AI 가 생성하지 않습니다.
동반자 시스템은 CBT 사고 재구성, DBT 스킬, 반영적 경청을 중심으로 설계된 별도 시스템 프롬프트를 사용합니다. 세션은 저장되고 재개 가능합니다.
제한점: 키워드 감지는 “I want to die” 같은 명시적 문구는 잡지만, “I just want it to stop” 혹은 “Everyone would be better off without me” 같은 완곡한 표현은 놓칠 수 있습니다. 로드맵에는 2단계 필터링(키워드 → 로컬 분류기) 이 포함되어 있어, 첫 번째 라인은 빠르고 감사 가능하며, 두 번째는 암시적 신호를 포착합니다.
암호화 흐름
모든 사용자 콘텐츠는 DB에 저장되기 전 AES‑256‑GCM 로 암호화됩니다.
// Every diary entry, chat message, companion note goes through this
encrypt(text) // before DB insert
decrypt(text) // after DB read, before sending to LLM or browser
암호화 키는 여러분이 직접 생성하고 .env 파일에 저장하는 64자리 16진수 문자열입니다. 키가 없으면 DB 를 읽을 수 없습니다. 서버는 절대 키를 전송하지 않습니다.
예외: 임베딩 벡터
임베딩 벡터 자체는 암호화되지 않은 채 저장됩니다. 코사인 유사도 연산에 원본 숫자가 필요하기 때문입니다. 임베딩을 만든 원본 텍스트는 별도로 암호화되어 저장됩니다. 보안 경계는 원본 텍스트에 존재하고, 파생된 벡터는 그 안에 있지 않습니다.
기술 스택
| 구성 요소 | 상세 |
|---|---|
| 런타임 | Node.js + Express |
| 프론트엔드 | Vanilla JS SPA (빌드 단계·프레임워크 없음) |
| 인증 | JWT + Argon2id 비밀번호 해싱 |
| 암호화 | AES‑256‑GCM (Node.js crypto 모듈) |
| 스토리지 | SQLite (기본 로컬) 또는 PostgreSQL (다중 디바이스) |
| 벡터 검색 | sqlite-vec (로컬) 또는 pgvector (Postgres) |
| 임베딩 | Ollama nomic-embed-text (기본 로컬) |
| **LLM |