RAG Knowledge Base v1.0 구현 플랜
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: 컨텍스트 소실 문제 근본 해결 — 보고서/CEO 지시/계획 파일을 자동 RAG 저장하고, 표준 검색 프로토콜 및 Compact 백업 체계를 구축한다.
Architecture: 신규 knowledge_base 테이블(NeonDB jarvis-rag)을 생성하여 범용 문서를 저장한다. ingest-document.ts가 단건 저장 핵심, ingest-directory.ts가 배치 저장을 담당한다. vice-reply.sh에 RAG 훅을 추가해 보고서가 생성될 때마다 자동 저장된다.
Tech Stack: Node.js 22, TypeScript ESM, pg (PostgreSQL), OpenAI text-embedding-3-small, NeonDB pgvector
전제 조건
RAG_DB_URL환경변수에 jarvis-rag NeonDB URL이 설정되어 있어야 함.env파일 경로:projects/rag-knowledge-base/.env- CLAUDE.md에 명시된 값:
RAG_DB_URL=postgresql://neondb_owner:npg_JcHZf5IQs2vq@ep-empty-smoke-a13aust8.ap-southeast-1.aws.neon.tech/neondb?sslmode=require - 작업 디렉토리:
/Users/nbs22/(Claude)/(claude).projects/business-builder/projects/rag-knowledge-base
Task 1: .env + config.ts — RAG_DB_URL 지원
목적: 현재 config.ts는 DB_URL을 사용하지만 신규 jarvis-rag DB는 RAG_DB_URL에 있다. RAG_DB_URL 우선 사용으로 변경한다.
Files:
- Modify:
src/config.ts - Modify:
.env(RAG_DB_URL 추가) - Modify:
.env.example
Step 1: .env에 RAG_DB_URL 추가
# .env 파일에 아래 줄 추가 (CLAUDE.md의 값 사용)
echo 'RAG_DB_URL=postgresql://neondb_owner:npg_JcHZf5IQs2vq@ep-empty-smoke-a13aust8.ap-southeast-1.aws.neon.tech/neondb?sslmode=require' >> .env
Step 2: config.ts 수정
기존 6번째 줄:
const DB_URL = process.env.DB_URL ||
'postgresql://neondb_owner:npg_OWVKrmC21gNk@ep-divine-darkness-a1gvyg6j-pooler.ap-southeast-1.aws.neon.tech/neondb?sslmode=require';
변경 후:
// RAG_DB_URL 우선, DB_URL fallback (하드코딩 fallback 제거)
const DB_URL = process.env.RAG_DB_URL || process.env.DB_URL;
if (!DB_URL) {
throw new Error('RAG_DB_URL 또는 DB_URL 환경변수가 필요합니다.');
}
Step 3: .env.example 업데이트
OPENAI_API_KEY=sk-your-openai-api-key-here
RAG_DB_URL=postgresql://neondb_owner:YOUR_PASSWORD@YOUR_HOST.neon.tech/neondb?sslmode=require
DB_URL=postgresql://neondb_owner:YOUR_PASSWORD@YOUR_HOST.neon.tech/neondb?sslmode=require
Step 4: 빌드 및 동작 확인
cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/projects/rag-knowledge-base
npm run build
Expected: 빌드 오류 없음
node dist/search.js "의사결정" 3
Expected: 기존 task_comments 검색 결과 3건 출력
Step 5: Commit
git add src/config.ts .env.example
git commit -m "feat: config.ts RAG_DB_URL 우선 사용으로 변경"
Task 2: DB 스키마 — knowledge_base 테이블 생성
목적: 보고서/계획/Obsidian 문서를 저장할 범용 테이블을 jarvis-rag DB에 생성한다.
Files:
- Create:
src/setup-knowledge-base.ts
Step 1: setup-knowledge-base.ts 생성
// src/setup-knowledge-base.ts
import { getPool, EMBEDDING_DIMENSIONS } from './config.js';
/**
* knowledge_base 테이블 생성 (최초 1회 실행).
* task_comments 외에 보고서/계획/Obsidian 문서를 저장하는 범용 테이블.
*/
async function setupKnowledgeBase(): Promise<void> {
const pool = getPool();
try {
console.log('=== knowledge_base 테이블 설정 시작 ===\n');
// 1. pgvector 확인 (이미 활성화되어 있어야 함)
await pool.query('CREATE EXTENSION IF NOT EXISTS vector;');
console.log('1. pgvector 확장 확인 완료');
// 2. knowledge_base 테이블 생성
await pool.query(`
CREATE TABLE IF NOT EXISTS knowledge_base (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source_type TEXT NOT NULL
CHECK (source_type IN ('report','instruction','plan','obsidian','compact_backup','decision','other')),
source_path TEXT,
source_date TIMESTAMPTZ,
task_id TEXT,
title TEXT NOT NULL,
content TEXT NOT NULL,
content_hash TEXT,
metadata JSONB,
embedding vector(${EMBEDDING_DIMENSIONS}),
created_at TIMESTAMPTZ DEFAULT NOW()
);
`);
console.log('2. knowledge_base 테이블 생성 완료');
// 3. content_hash 유니크 인덱스 (중복 저장 방지)
await pool.query(`
CREATE UNIQUE INDEX IF NOT EXISTS idx_kb_content_hash
ON knowledge_base (content_hash)
WHERE content_hash IS NOT NULL;
`);
console.log('3. content_hash 유니크 인덱스 생성 완료');
// 4. HNSW 벡터 인덱스
const indexExists = await pool.query(`
SELECT indexname FROM pg_indexes
WHERE tablename = 'knowledge_base' AND indexname = 'idx_kb_embedding';
`);
if (indexExists.rows.length > 0) {
console.log('4. 벡터 인덱스 이미 존재, 스킵');
} else {
await pool.query(`
CREATE INDEX idx_kb_embedding
ON knowledge_base
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
`);
console.log('4. HNSW 벡터 인덱스 생성 완료');
}
// 5. source_type 인덱스 (필터링용)
await pool.query(`
CREATE INDEX IF NOT EXISTS idx_kb_source_type
ON knowledge_base (source_type);
`);
console.log('5. source_type 인덱스 생성 완료');
console.log('\n=== knowledge_base 테이블 설정 완료 ===');
} catch (error) {
console.error('설정 중 오류:', error);
throw error;
} finally {
await pool.end();
}
}
setupKnowledgeBase().catch((err) => {
console.error('Fatal error:', err);
process.exit(1);
});
Step 2: package.json scripts 추가
"setup-kb": "node dist/setup-knowledge-base.js" 추가
Step 3: 빌드 및 실행
npm run build
node dist/setup-knowledge-base.js
Expected:
=== knowledge_base 테이블 설정 시작 ===
1. pgvector 확장 확인 완료
2. knowledge_base 테이블 생성 완료
3. content_hash 유니크 인덱스 생성 완료
4. HNSW 벡터 인덱스 생성 완료
5. source_type 인덱스 생성 완료
=== knowledge_base 테이블 설정 완료 ===
Step 4: Commit
git add src/setup-knowledge-base.ts package.json
git commit -m "feat: knowledge_base 테이블 스키마 추가"
Task 3: ingest-document.ts — 단건 문서 저장 핵심 모듈
목적: 텍스트 한 건을 knowledge_base에 INSERT + embedding 자동 생성. content_hash로 중복 저장 방지. 함수 export로 다른 모듈에서 import 가능.
Files:
- Create:
src/ingest-document.ts
Step 1: ingest-document.ts 생성
// src/ingest-document.ts
import crypto from 'crypto';
import { getPool } from './config.js';
import { embedText } from './embeddings.js';
export interface IngestParams {
source_type: 'report' | 'instruction' | 'plan' | 'obsidian' | 'compact_backup' | 'decision' | 'other';
title: string;
content: string;
source_path?: string;
source_date?: Date | string;
task_id?: string;
metadata?: Record<string, unknown>;
}
export interface IngestResult {
id: string;
embedded: boolean;
skipped: boolean; // content_hash 중복으로 스킵된 경우
}
/**
* 문서 1건을 knowledge_base에 저장하고 embedding을 생성한다.
* content_hash가 동일한 문서는 중복 저장하지 않는다.
* 다른 모듈에서 import하여 사용 가능.
*/
export async function ingestDocument(
params: IngestParams,
externalPool?: import('pg').Pool
): Promise<IngestResult> {
const pool = externalPool || getPool();
const shouldClosePool = !externalPool;
try {
// content_hash 계산 (SHA256, 중복 방지)
const contentHash = crypto
.createHash('sha256')
.update(params.content.trim())
.digest('hex');
// 중복 확인
const existing = await pool.query<{ id: string }>(
'SELECT id FROM knowledge_base WHERE content_hash = $1',
[contentHash]
);
if (existing.rows.length > 0) {
console.log(`[ingest] 중복 스킵: ${params.title.substring(0, 50)}`);
return { id: existing.rows[0].id, embedded: false, skipped: true };
}
// embedding용 텍스트 조합: title + content (앞 3000자)
const embeddingText = `[${params.source_type}] ${params.title}\n${params.content.substring(0, 3000)}`;
// embedding 생성
let embedding: number[] | null = null;
let embedded = false;
try {
embedding = await embedText(embeddingText);
embedded = true;
} catch (e) {
console.error('[ingest] embedding 생성 실패 (저장은 계속):', e);
}
const vectorStr = embedding ? `[${embedding.join(',')}]` : null;
// INSERT
const result = await pool.query<{ id: string }>(
`INSERT INTO knowledge_base
(source_type, source_path, source_date, task_id, title, content, content_hash, metadata, embedding)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::vector)
RETURNING id`,
[
params.source_type,
params.source_path || null,
params.source_date || null,
params.task_id || null,
params.title,
params.content,
contentHash,
params.metadata ? JSON.stringify(params.metadata) : null,
vectorStr,
]
);
const id = result.rows[0].id;
console.log(`[ingest] 저장 완료: ${id} | ${params.source_type} | ${params.title.substring(0, 50)}`);
return { id, embedded, skipped: false };
} finally {
if (shouldClosePool) {
await pool.end();
}
}
}
/**
* CLI 실행:
* node dist/ingest-document.js --type report --title "제목" --content "내용" [옵션]
*/
async function main(): Promise<void> {
const args = process.argv.slice(2);
if (args.length === 0 || args.includes('--help')) {
console.log(`사용법: node dist/ingest-document.js [옵션]
필수:
--type <type> report|instruction|plan|obsidian|compact_backup|decision|other
--title <text> 문서 제목
--content <text> 문서 내용 (또는 --content-stdin으로 stdin 입력)
선택:
--content-stdin stdin에서 content 읽기 (파이프 사용 시)
--source-path <path> 원본 파일 경로
--source-date <date> 문서 날짜 (ISO 8601)
--task-id <uuid> 관련 태스크 ID
예시:
node dist/ingest-document.js --type report --title "일일 보고" --content "오늘 완료한 작업..."
cat report.md | node dist/ingest-document.js --type plan --title "RAG v1 계획" --content-stdin
`);
process.exit(0);
}
function getArg(name: string): string | undefined {
const idx = args.indexOf(`--${name}`);
if (idx === -1 || idx + 1 >= args.length) return undefined;
return args[idx + 1];
}
const type = getArg('type') as IngestParams['source_type'];
const title = getArg('title');
let content = getArg('content');
const useStdin = args.includes('--content-stdin');
// stdin 읽기
if (useStdin) {
const chunks: Buffer[] = [];
for await (const chunk of process.stdin) {
chunks.push(chunk as Buffer);
}
content = Buffer.concat(chunks).toString('utf-8').trim();
}
if (!type || !title || !content) {
console.error('필수 인자 누락: --type, --title, --content (또는 --content-stdin)');
process.exit(1);
}
const result = await ingestDocument({
source_type: type,
title,
content,
source_path: getArg('source-path'),
source_date: getArg('source-date'),
task_id: getArg('task-id'),
});
if (result.skipped) {
console.log(`결과: 중복 스킵 (id=${result.id})`);
process.exit(0);
}
console.log(`결과: id=${result.id}, embedded=${result.embedded}`);
process.exit(result.embedded ? 0 : 1);
}
const isMainModule = process.argv[1]?.endsWith('ingest-document.js');
if (isMainModule) {
main().catch((err) => {
console.error('Fatal error:', err);
process.exit(1);
});
}
Step 2: package.json scripts 추가
"ingest": "node dist/ingest-document.js"
Step 3: 빌드 확인
npm run build
Expected: 오류 없음
Step 4: 동작 테스트
node dist/ingest-document.js \
--type report \
--title "RAG v1.0 구현 테스트" \
--content "v1.0 구현을 위한 테스트 문서입니다. knowledge_base 테이블에 저장됩니다."
Expected:
[ingest] 저장 완료: <uuid> | report | RAG v1.0 구현 테스트
결과: id=<uuid>, embedded=true
같은 명령 재실행 시:
[ingest] 중복 스킵: RAG v1.0 구현 테스트
결과: 중복 스킵 (id=<uuid>)
Step 5: Commit
git add src/ingest-document.ts package.json
git commit -m "feat: ingest-document.ts — knowledge_base 단건 저장 + embedding"
Task 4: ingest-directory.ts — 디렉토리 .md 파일 배치 저장
목적: 지정 디렉토리 내 모든 .md 파일을 재귀적으로 읽어 knowledge_base에 저장한다. content_hash로 이미 저장된 파일은 자동 스킵.
Files:
- Create:
src/ingest-directory.ts
Step 1: ingest-directory.ts 생성
// src/ingest-directory.ts
import fs from 'fs';
import path from 'path';
import { getPool } from './config.js';
import { ingestDocument, IngestParams } from './ingest-document.js';
type SourceType = IngestParams['source_type'];
interface IngestDirResult {
saved: number;
skipped: number;
errors: number;
}
/**
* 디렉토리 내 .md 파일을 재귀 탐색하여 knowledge_base에 일괄 저장.
* 중복(content_hash)은 자동 스킵.
*
* @param dirPath - 탐색할 디렉토리 절대/상대 경로
* @param sourceType - 저장할 source_type
*/
export async function ingestDirectory(
dirPath: string,
sourceType: SourceType = 'plan'
): Promise<IngestDirResult> {
const pool = getPool();
const result: IngestDirResult = { saved: 0, skipped: 0, errors: 0 };
try {
const absDir = path.resolve(dirPath);
if (!fs.existsSync(absDir)) {
console.error(`[ingest-dir] 디렉토리 없음: ${absDir}`);
return result;
}
const mdFiles = collectMdFiles(absDir);
console.log(`[ingest-dir] ${mdFiles.length}개 .md 파일 발견 (${absDir})`);
for (const filePath of mdFiles) {
try {
const content = fs.readFileSync(filePath, 'utf-8').trim();
if (!content) {
result.skipped++;
continue;
}
// 파일명을 제목으로 사용 (확장자 제거)
const title = path.basename(filePath, '.md');
// 파일 수정일
const stat = fs.statSync(filePath);
const sourceDate = stat.mtime;
const ingestResult = await ingestDocument(
{
source_type: sourceType,
title,
content,
source_path: filePath,
source_date: sourceDate,
},
pool
);
if (ingestResult.skipped) {
result.skipped++;
} else {
result.saved++;
}
// API 레이트 제한 방지 (500ms 간격)
await new Promise(r => setTimeout(r, 500));
} catch (err) {
console.error(`[ingest-dir] 오류: ${filePath}`, err);
result.errors++;
}
}
console.log(`[ingest-dir] 완료: 저장=${result.saved}, 스킵=${result.skipped}, 오류=${result.errors}`);
return result;
} finally {
await pool.end();
}
}
/**
* 디렉토리를 재귀 탐색하여 .md 파일 경로 목록 반환
*/
function collectMdFiles(dirPath: string): string[] {
const files: string[] = [];
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
files.push(...collectMdFiles(fullPath));
} else if (entry.isFile() && entry.name.endsWith('.md')) {
files.push(fullPath);
}
}
return files;
}
/**
* CLI 실행:
* node dist/ingest-directory.js <디렉토리 경로> [source_type]
*/
async function main(): Promise<void> {
const dirPath = process.argv[2];
const sourceType = (process.argv[3] || 'plan') as SourceType;
if (!dirPath) {
console.log('사용법: node dist/ingest-directory.js <디렉토리 경로> [source_type]');
console.log('예시:');
console.log(' node dist/ingest-directory.js /path/to/docs/plans plan');
console.log(' node dist/ingest-directory.js /path/to/ObsidianVault/projects obsidian');
process.exit(1);
}
const result = await ingestDirectory(dirPath, sourceType);
process.exit(result.errors > 0 ? 1 : 0);
}
const isMainModule = process.argv[1]?.endsWith('ingest-directory.js');
if (isMainModule) {
main().catch((err) => {
console.error('Fatal error:', err);
process.exit(1);
});
}
Step 2: package.json scripts 추가
"ingest-dir": "node dist/ingest-directory.js"
Step 3: 빌드 및 테스트
npm run build
# docs/plans/2026/02/26/ 디렉토리 테스트 (소규모)
node dist/ingest-directory.js \
"/Users/nbs22/(Claude)/(claude).projects/business-builder/docs/plans/2026/02/26" \
plan
Expected: 해당 디렉토리의 .md 파일들이 저장됨 (중복은 스킵)
Step 4: Commit
git add src/ingest-directory.ts package.json
git commit -m "feat: ingest-directory.ts — 디렉토리 .md 배치 저장"
Task 5: search-kb.ts — knowledge_base + task_comments 통합 검색
목적: knowledge_base와 task_comments를 통합 검색하는 CLI. 기존 search.ts는 task_comments만 검색하므로 신규 통합 검색이 필요하다.
Files:
- Create:
src/search-kb.ts
Step 1: search-kb.ts 생성
// src/search-kb.ts
import { getPool } from './config.js';
import { embedText } from './embeddings.js';
interface KBResult {
id: string;
source: 'knowledge_base' | 'task_comments';
source_type: string;
source_path: string | null;
title: string;
content_preview: string;
similarity: number;
created_at: string;
}
/**
* knowledge_base + task_comments를 통합 유사도 검색.
*
* @param query - 자연어 검색 쿼리
* @param topK - 반환 결과 수 (기본 5)
* @param source - 검색 대상 ('all' | 'kb' | 'comments', 기본 'all')
*/
export async function searchKB(
query: string,
topK: number = 5,
source: 'all' | 'kb' | 'comments' = 'all'
): Promise<KBResult[]> {
const pool = getPool();
try {
const queryEmbedding = await embedText(query);
const vectorStr = `[${queryEmbedding.join(',')}]`;
let sql: string;
if (source === 'kb') {
sql = `
SELECT
id,
'knowledge_base'::text AS source,
source_type,
source_path,
title,
LEFT(content, 300) AS content_preview,
1 - (embedding <=> $1::vector) AS similarity,
created_at::text
FROM knowledge_base
WHERE embedding IS NOT NULL
ORDER BY embedding <=> $1::vector
LIMIT $2
`;
} else if (source === 'comments') {
sql = `
SELECT
c.id,
'task_comments'::text AS source,
c.type AS source_type,
NULL AS source_path,
COALESCE(t.title, '(태스크 없음)') AS title,
LEFT(c.content, 300) AS content_preview,
1 - (c.embedding <=> $1::vector) AS similarity,
c.created_at::text
FROM task_comments c
LEFT JOIN tasks t ON c.task_id = t.id
WHERE c.embedding IS NOT NULL
ORDER BY c.embedding <=> $1::vector
LIMIT $2
`;
} else {
// 통합: UNION ALL 후 정렬
sql = `
SELECT * FROM (
SELECT
id,
'knowledge_base'::text AS source,
source_type,
source_path,
title,
LEFT(content, 300) AS content_preview,
1 - (embedding <=> $1::vector) AS similarity,
created_at::text
FROM knowledge_base
WHERE embedding IS NOT NULL
UNION ALL
SELECT
c.id,
'task_comments'::text AS source,
c.type AS source_type,
NULL AS source_path,
COALESCE(t.title, '(태스크 없음)') AS title,
LEFT(c.content, 300) AS content_preview,
1 - (c.embedding <=> $1::vector) AS similarity,
c.created_at::text
FROM task_comments c
LEFT JOIN tasks t ON c.task_id = t.id
WHERE c.embedding IS NOT NULL
) combined
ORDER BY similarity DESC
LIMIT $2
`;
}
const result = await pool.query<KBResult>(sql, [vectorStr, topK]);
return result.rows;
} finally {
await pool.end();
}
}
function formatKBResults(query: string, results: KBResult[]): void {
console.log(`\n${'='.repeat(70)}`);
console.log(`검색 쿼리: "${query}"`);
console.log(`결과: ${results.length}건`);
console.log(`${'='.repeat(70)}\n`);
if (results.length === 0) {
console.log('검색 결과 없음');
return;
}
results.forEach((r, i) => {
const pct = (r.similarity * 100).toFixed(1);
const sourceLabel = r.source === 'knowledge_base' ? `📄 KB` : `💬 코멘트`;
console.log(`--- [${i + 1}] 유사도: ${pct}% | ${sourceLabel} | ${r.source_type} ---`);
console.log(`제목: ${r.title}`);
if (r.source_path) {
console.log(`경로: ${r.source_path}`);
}
console.log(`내용: ${r.content_preview}${r.content_preview.length >= 300 ? '...' : ''}`);
console.log(`날짜: ${r.created_at}`);
console.log();
});
}
async function main(): Promise<void> {
const query = process.argv[2];
const topK = parseInt(process.argv[3] || '5', 10);
const source = (process.argv[4] || 'all') as 'all' | 'kb' | 'comments';
if (!query) {
console.log('사용법: node dist/search-kb.js "쿼리" [결과수] [all|kb|comments]');
console.log('예시:');
console.log(' node dist/search-kb.js "AI 뉴스레터 의사결정" 5');
console.log(' node dist/search-kb.js "NeonDB 비용" 3 kb');
console.log(' node dist/search-kb.js "컨텍스트 소실" 5 comments');
process.exit(1);
}
try {
const results = await searchKB(query, topK, source);
formatKBResults(query, results);
} catch (err) {
console.error('검색 오류:', err);
process.exit(1);
}
}
main().catch((err) => {
console.error('Fatal error:', err);
process.exit(1);
});
Step 2: package.json scripts 추가
"search-kb": "node dist/search-kb.js"
Step 3: 빌드 및 테스트
npm run build
node dist/search-kb.js "RAG 지식베이스" 5
Expected: knowledge_base + task_comments 통합 결과 5건 출력 (Task 3에서 저장한 테스트 문서 포함)
Step 4: Commit
git add src/search-kb.ts package.json
git commit -m "feat: search-kb.ts — knowledge_base + task_comments 통합 검색"
Task 6: scripts/rag-save-report.sh — vice-reply.sh 연동 wrapper
목적: vice-reply.sh에서 호출 가능한 얇은 bash wrapper. 보고 내용을 knowledge_base에 저장한다.
Files:
- Create:
scripts/rag-save-report.sh
주의: 이 스크립트는 business-builder 루트의 scripts/ 디렉토리에 생성한다.
(RAG 프로젝트의 scripts/가 아님)
Step 1: scripts/rag-save-report.sh 생성
경로: /Users/nbs22/(Claude)/(claude).projects/business-builder/scripts/rag-save-report.sh
#!/bin/bash
# RAG 보고서 저장 wrapper — vice-reply.sh에서 호출
# 사용법: ./scripts/rag-save-report.sh "제목" "내용" [source_type] [tag]
#
# 실패해도 조용히 종료 (vice-reply.sh의 주요 흐름을 방해하지 않음)
TITLE="${1:-}"
CONTENT="${2:-}"
SOURCE_TYPE="${3:-report}"
TAG="${4:-report}"
if [ -z "$TITLE" ] || [ -z "$CONTENT" ]; then
exit 0 # 내용 없으면 조용히 종료
fi
RAG_DIR="/Users/nbs22/(Claude)/(claude).projects/business-builder/projects/rag-knowledge-base"
if [ ! -d "$RAG_DIR" ]; then
exit 0
fi
export PATH="/Users/nbs22/.nvm/versions/node/v22.12.0/bin:$PATH"
# .env 로드 (RAG_DB_URL, OPENAI_API_KEY)
if [ -f "$RAG_DIR/.env" ]; then
set -a
source "$RAG_DIR/.env"
set +a
fi
# ingest-document CLI 호출 (에러 무시)
cd "$RAG_DIR" && node dist/ingest-document.js \
--type "$SOURCE_TYPE" \
--title "$TITLE" \
--content "$CONTENT" \
--source-path "report:$TAG:$(date +%Y-%m-%d)" \
2>/dev/null || true
exit 0
Step 2: 실행 권한 설정
chmod +x /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/scripts/rag-save-report.sh
Step 3: 단독 테스트
cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
./scripts/rag-save-report.sh "테스트 보고서" "rag-save-report.sh 동작 테스트 완료" report test
Expected: 에러 없이 종료 (knowledge_base에 저장됨)
검증:
cd projects/rag-knowledge-base
node dist/search-kb.js "rag-save-report 테스트" 3 kb
Step 4: Commit
git add scripts/rag-save-report.sh
git commit -m "feat: rag-save-report.sh — vice-reply.sh 연동 wrapper"
Task 7: vice-reply.sh — RAG 훅 step 5 추가
목적: vice-reply.sh 실행 시 보고 내용이 자동으로 knowledge_base에 저장된다. 기존 4단계(텔레그램/DB/wake/md) 이후 step 5로 추가.
Files:
- Modify:
scripts/vice-reply.sh
Step 1: vice-reply.sh 끝부분 확인
현재 마지막 줄:
echo "[4/4] 로컬 md 백업 완료: memory/jarvis-reports/${FILENAME}"
Step 2: step 5 추가 (마지막 echo 이후에 삽입)
echo "[4/4] 로컬 md 백업 완료: memory/jarvis-reports/${FILENAME}"
# 5) RAG 지식베이스 저장 (실패해도 계속)
REPORT_TITLE="[${TAG}] $(echo "$TEXT" | head -1 | cut -c1-80)"
"${PROJECT_ROOT}/scripts/rag-save-report.sh" "$REPORT_TITLE" "$TEXT" "report" "$TAG" || true
echo "[5/5] RAG 지식베이스 저장 완료"
Step 3: 동작 테스트
cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
./scripts/vice-reply.sh "RAG v1.0 훅 연동 테스트. vice-reply.sh에서 자동 RAG 저장 확인." "test"
Expected: [5/5] RAG 지식베이스 저장 완료 출력
cd projects/rag-knowledge-base
node dist/search-kb.js "vice-reply RAG 훅 연동" 3 kb
Expected: 방금 저장된 보고서 검색됨
Step 4: Commit
git add scripts/vice-reply.sh
git commit -m "feat: vice-reply.sh RAG 훅 추가 (step 5)"
Task 8: scripts/rag-ingest-docs.sh — docs/plans/ 배치 저장
목적: business-builder/docs/plans/ 하위 모든 .md 파일을 knowledge_base에 배치 저장한다.
Files:
- Create:
scripts/rag-ingest-docs.sh
경로: /Users/nbs22/(Claude)/(claude).projects/business-builder/scripts/rag-ingest-docs.sh
Step 1: rag-ingest-docs.sh 생성
#!/bin/bash
# docs/plans/ 하위 .md 파일을 knowledge_base에 배치 저장
# 사용법: ./scripts/rag-ingest-docs.sh [디렉토리] [source_type]
#
# 예시:
# ./scripts/rag-ingest-docs.sh # 전체 docs/plans/
# ./scripts/rag-ingest-docs.sh docs/plans/2026/02/26 # 특정 날짜만
set -euo pipefail
PROJECT_ROOT="/Users/nbs22/(Claude)/(claude).projects/business-builder"
RAG_DIR="${PROJECT_ROOT}/projects/rag-knowledge-base"
TARGET_DIR="${1:-${PROJECT_ROOT}/docs/plans}"
SOURCE_TYPE="${2:-plan}"
export PATH="/Users/nbs22/.nvm/versions/node/v22.12.0/bin:$PATH"
# .env 로드
if [ -f "$RAG_DIR/.env" ]; then
set -a
source "$RAG_DIR/.env"
set +a
fi
echo "[rag-ingest-docs] 시작: $TARGET_DIR (type=$SOURCE_TYPE)"
cd "$RAG_DIR"
node dist/ingest-directory.js "$TARGET_DIR" "$SOURCE_TYPE"
echo "[rag-ingest-docs] 완료"
Step 2: 실행 권한 설정
chmod +x /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/scripts/rag-ingest-docs.sh
Step 3: 실행 테스트
cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
# 오늘 날짜 플랜만 (빠른 테스트)
./scripts/rag-ingest-docs.sh docs/plans/2026/02/26 plan
Expected: 저장 건수 + 중복 스킵 건수 출력
Step 4: Commit
git add scripts/rag-ingest-docs.sh
git commit -m "feat: rag-ingest-docs.sh — docs/plans/ 배치 저장"
Task 9: scripts/rag-ingest-obsidian.sh — ObsidianVault 배치 저장
목적: ObsidianVault/projects/ 하위 문서를 knowledge_base에 배치 저장한다.
Files:
- Create:
scripts/rag-ingest-obsidian.sh
경로: /Users/nbs22/(Claude)/(claude).projects/business-builder/scripts/rag-ingest-obsidian.sh
Step 1: rag-ingest-obsidian.sh 생성
#!/bin/bash
# ObsidianVault/projects/ 하위 .md 파일을 knowledge_base에 배치 저장
# 사용법: ./scripts/rag-ingest-obsidian.sh [하위 경로]
#
# 예시:
# ./scripts/rag-ingest-obsidian.sh # 전체 projects/
# ./scripts/rag-ingest-obsidian.sh plans # plans/ 만
set -euo pipefail
PROJECT_ROOT="/Users/nbs22/(Claude)/(claude).projects/business-builder"
RAG_DIR="${PROJECT_ROOT}/projects/rag-knowledge-base"
OBSIDIAN_ROOT="/Users/nbs22/ObsidianVault/projects"
SUBPATH="${1:-}"
TARGET_DIR="${OBSIDIAN_ROOT}/${SUBPATH}"
export PATH="/Users/nbs22/.nvm/versions/node/v22.12.0/bin:$PATH"
# .env 로드
if [ -f "$RAG_DIR/.env" ]; then
set -a
source "$RAG_DIR/.env"
set +a
fi
if [ ! -d "$TARGET_DIR" ]; then
echo "[rag-ingest-obsidian] 경로 없음: $TARGET_DIR"
exit 1
fi
echo "[rag-ingest-obsidian] 시작: $TARGET_DIR"
cd "$RAG_DIR"
node dist/ingest-directory.js "$TARGET_DIR" "obsidian"
echo "[rag-ingest-obsidian] 완료"
Step 2: 실행 권한 설정
chmod +x /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/scripts/rag-ingest-obsidian.sh
Step 3: 실행 테스트
cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
# plans/ 하위만 (소규모 테스트)
./scripts/rag-ingest-obsidian.sh plans
Expected: Obsidian plans 문서들이 knowledge_base에 저장됨
Step 4: Commit
git add scripts/rag-ingest-obsidian.sh
git commit -m "feat: rag-ingest-obsidian.sh — ObsidianVault 배치 저장"
Task 10: scripts/rag-compact-backup.sh — Compact 전 세션 백업
목적: 컨텍스트 압축 전 현재 세션의 주요 진행사항/의사결정을 knowledge_base에 저장한다. 파이프 or 인자로 내용을 받는다.
Files:
- Create:
scripts/rag-compact-backup.sh
경로: /Users/nbs22/(Claude)/(claude).projects/business-builder/scripts/rag-compact-backup.sh
Step 1: rag-compact-backup.sh 생성
#!/bin/bash
# Compact 직전 세션 백업 — 현재 세션 핵심 내용을 knowledge_base에 저장
#
# 사용법:
# ./scripts/rag-compact-backup.sh "세션 요약" [태스크ID]
# echo "세션 내용" | ./scripts/rag-compact-backup.sh --stdin [태스크ID]
#
# 예시 (자비스 compact 직전 실행):
# ./scripts/rag-compact-backup.sh "오늘 완료: RAG v1.0 구현. 남은 작업: ObsidianVault 통합"
set -euo pipefail
PROJECT_ROOT="/Users/nbs22/(Claude)/(claude).projects/business-builder"
RAG_DIR="${PROJECT_ROOT}/projects/rag-knowledge-base"
export PATH="/Users/nbs22/.nvm/versions/node/v22.12.0/bin:$PATH"
# .env 로드
if [ -f "$RAG_DIR/.env" ]; then
set -a
source "$RAG_DIR/.env"
set +a
fi
TIMESTAMP=$(date +"%Y-%m-%d %H:%M")
TITLE="[compact_backup] ${TIMESTAMP}"
# stdin 모드
if [ "${1:-}" = "--stdin" ]; then
CONTENT=$(cat)
TASK_ID="${2:-}"
else
CONTENT="${1:-}"
TASK_ID="${2:-}"
fi
if [ -z "$CONTENT" ]; then
echo "사용법: ./scripts/rag-compact-backup.sh \"세션 요약\" [태스크ID]"
echo " echo \"내용\" | ./scripts/rag-compact-backup.sh --stdin"
exit 1
fi
TASK_ARG=""
if [ -n "$TASK_ID" ]; then
TASK_ARG="--task-id $TASK_ID"
fi
echo "[compact-backup] 세션 백업 시작..."
cd "$RAG_DIR"
node dist/ingest-document.js \
--type compact_backup \
--title "$TITLE" \
--content "$CONTENT" \
--source-path "compact:${TIMESTAMP}" \
$TASK_ARG
echo "[compact-backup] 완료: ${TITLE}"
Step 2: 실행 권한 설정
chmod +x /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder/scripts/rag-compact-backup.sh
Step 3: 동작 테스트
cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
./scripts/rag-compact-backup.sh \
"RAG v1.0 구현 완료. Task 1~10 모두 완성. 주요 의사결정: content_hash 중복 방지, UNION ALL 통합 검색, vice-reply.sh 훅 연동."
Expected: [compact-backup] 완료: [compact_backup] 2026-02-26 HH:MM
Step 4: Commit
git add scripts/rag-compact-backup.sh
git commit -m "feat: rag-compact-backup.sh — compact 전 세션 백업"
Task 11: rag-usage.md 스킬 업데이트
목적: 자비스 표준 RAG 운영 프로토콜을 업데이트한다. 새 테이블/명령어/저장 방법을 반영한다.
Files:
- Modify:
.claude/skills/jarvis-only/rag-usage.md
경로: /Users/nbs22/(Claude)/(claude).projects/business-builder/.claude/skills/jarvis-only/rag-usage.md
Step 1: rag-usage.md 전체 교체
# RAG 지식베이스 운영 프로토콜
`projects/rag-knowledge-base/`의 검색 시스템으로 과거 의사결정/계획/보고를 참조한다.
## 검색 명령
```bash
cd /Users/nbs22/(Claude)/(claude).projects/business-builder/projects/rag-knowledge-base
# 통합 검색 (task_comments + knowledge_base)
node dist/search-kb.js "검색 쿼리" [topK] [all|kb|comments]
# 예시
node dist/search-kb.js "AI 뉴스레터 의사결정" 5
node dist/search-kb.js "NeonDB 비용" 3 kb
node dist/search-kb.js "유사 과업 선례" 5 comments
# 구버전 (task_comments 전용)
node dist/search.js "쿼리" [topK]
필수 참조 상황
- 새 태스크 기획 착수 시 (과거 유사 프로젝트 의사결정)
- 기술 스택/서비스 선택 시 (이전 동일 선택 여부, 사유 확인)
- 비용 산출 시 (과거 비용 분석 결과 재활용)
holding/drop판단 시 (유사 태스크 보류/폐기 사유)
불필요 상황 (쓰지 않는다)
- 단순 DB 조회로 충분한 경우
- 코드 구현 중 기술적 질문
- CEO와 실시간 대화 중 즉답
- 이미 해당 태스크의 comments를 SQL 조회한 경우
제한
- 1회 검색당 상위 5건 (topK=5)
- 한 태스크 기획에서 최대 3회 검색
- 검색 결과는 판단의 참고 자료로만 활용
RAG 저장 방법
자동 저장 (훅)
vice-reply.sh실행 시 → 자동으로 knowledge_base에 저장됨 (step 5)
배치 저장 (초기/정기)
cd /Users/nbs22/(Claude)/(claude).projects/business-builder
# docs/plans/ 전체
./scripts/rag-ingest-docs.sh
# 특정 날짜만
./scripts/rag-ingest-docs.sh docs/plans/2026/02/26
# ObsidianVault 전체
./scripts/rag-ingest-obsidian.sh
# ObsidianVault plans만
./scripts/rag-ingest-obsidian.sh plans
Compact 전 백업
cd /Users/nbs22/(Claude)/(claude).projects/business-builder
./scripts/rag-compact-backup.sh "현재 세션 요약: 완료된 작업, 남은 작업, 주요 의사결정"
단건 저장
cd projects/rag-knowledge-base
node dist/ingest-document.js \
--type report \
--title "문서 제목" \
--content "내용"
저장 데이터 구조
| source_type | 내용 |
|---|---|
report | vice-reply.sh 보고서 |
instruction | CEO 지시사항 |
plan | docs/plans/ 기획서 |
obsidian | ObsidianVault 문서 |
compact_backup | Compact 전 세션 백업 |
decision | 독립적 의사결정 기록 |
**Step 2: Commit**
```bash
git add .claude/skills/jarvis-only/rag-usage.md
git commit -m "docs: rag-usage.md 스킬 v1.0 업데이트 — knowledge_base + 자동 저장 프로토콜"
Task 12: 초기 데이터 일괄 저장 (실행)
목적: 기존 docs/plans/ 전체 + ObsidianVault/projects/ 전체를 knowledge_base에 저장한다. 일회성 실행.
주의: OpenAI API 비용 발생. docs/plans/ ≈ 15개 파일 × $0.0002 ≈ $0.003. ObsidianVault ≈ 수십~수백 파일. 비용 최소화를 위해 plans/만 먼저 저장 권장.
Step 1: docs/plans/ 전체 저장
cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
./scripts/rag-ingest-docs.sh
Step 2: 검색 테스트
cd projects/rag-knowledge-base
node dist/search-kb.js "KoreaAI Hub 계획" 5 kb
node dist/search-kb.js "결제 시스템 의사결정" 5
Step 3: ObsidianVault plans 저장 (선택)
cd /Users/nbs22/\(Claude\)/\(claude\).projects/business-builder
./scripts/rag-ingest-obsidian.sh plans
./scripts/rag-ingest-obsidian.sh strategy
Step 4: Commit
# 코드 변경 없음. git push만.
git push origin main
완료 체크리스트
- Task 1: config.ts RAG_DB_URL 지원, .env 업데이트
- Task 2: knowledge_base 테이블 생성 완료
- Task 3: ingest-document.ts 빌드 + 단건 저장 동작 확인
- Task 4: ingest-directory.ts 빌드 + docs/plans/2026/02/26 테스트
- Task 5: search-kb.ts 빌드 + 통합 검색 동작 확인
- Task 6: rag-save-report.sh 단독 실행 테스트
- Task 7: vice-reply.sh 훅 추가 + 보고 후 검색으로 저장 확인
- Task 8: rag-ingest-docs.sh 실행 권한 + 동작 확인
- Task 9: rag-ingest-obsidian.sh 실행 권한 + 동작 확인
- Task 10: rag-compact-backup.sh 동작 확인
- Task 11: rag-usage.md 스킬 업데이트
- Task 12: 초기 데이터 일괄 저장 + 검색 품질 확인
주요 설계 결정
| 결정 | 이유 |
|---|---|
knowledge_base 별도 테이블 | task_comments와 스키마 분리 → 범용 문서 저장 가능 |
content_hash 중복 방지 | 배치 저장 반복 실행 시 중복 embedding 방지 |
| UNION ALL 통합 검색 | 단일 쿼리로 양쪽 테이블 검색 (인덱스 각각 활용) |
| vice-reply.sh step 5 | 보고 생성 = 저장 원칙. 별도 훅 없이 기존 흐름에 편승 |
| embedding 실패 시 저장 계속 | API 오류가 데이터 손실로 이어지지 않도록 |
| 500ms 배치 간격 | OpenAI rate limit 준수 (tier 1: 3000 RPM) |