feat: integrate unified RAG pipeline and bump version to 2.60.0

This commit is contained in:
g1nation
2026-05-04 11:00:01 +09:00
parent 0515dd625d
commit 445d530b63
16 changed files with 2178 additions and 112 deletions
+130
View File
@@ -0,0 +1,130 @@
/**
* ============================================================
* Context Budget Manager (컨텍스트 예산 관리)
*
* 시스템 프롬프트의 토큰 예산을 관리하여
* 로컬 모델의 context window를 효율적으로 활용합니다.
* ============================================================
*/
import { RetrievalChunk, ContextBudgetConfig } from './types';
const DEFAULT_BUDGET: ContextBudgetConfig = {
totalBudget: 8000, // ~32K context 중 retrieval에 할당
retrievalRatio: 0.4, // 40%
minChunks: 2,
maxChunks: 12
};
/**
* 토큰 수를 대략 추정합니다 (문자 수 / 4).
* 한국어는 글자당 토큰이 더 많으므로 보정합니다.
*/
export function estimateTokens(text: string): number {
// 한국어 비율 추정
const koreanChars = (text.match(/[가-힣]/g) || []).length;
const totalChars = text.length;
const koreanRatio = totalChars > 0 ? koreanChars / totalChars : 0;
// 한국어는 글자당 ~1.5 토큰, 영어는 ~0.25 토큰
const koreanTokens = koreanChars * 1.5;
const otherTokens = (totalChars - koreanChars) * 0.25;
return Math.ceil(koreanTokens + otherTokens);
}
/**
* 검색 결과 청크들을 토큰 예산 내에서 선택합니다.
*
* 선택 전략:
* 1. 스코어 내림차순 정렬
* 2. 중복 제거 (같은 filePath를 가진 청크)
* 3. 토큰 예산 내에서 순서대로 선택
* 4. 최소 청크 수 보장
*/
export function selectWithinBudget(
chunks: RetrievalChunk[],
config: Partial<ContextBudgetConfig> = {}
): { selected: RetrievalChunk[]; dropped: RetrievalChunk[]; tokensUsed: number } {
const cfg = { ...DEFAULT_BUDGET, ...config };
const budget = Math.floor(cfg.totalBudget * cfg.retrievalRatio);
// 1. Sort by score descending
const sorted = [...chunks].sort((a, b) => b.score - a.score);
// 2. Deduplicate by filePath
const seen = new Set<string>();
const deduped = sorted.filter((chunk) => {
const key = chunk.metadata.filePath || chunk.id;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
// 3. Select within budget
const selected: RetrievalChunk[] = [];
const dropped: RetrievalChunk[] = [];
let tokensUsed = 0;
for (const chunk of deduped) {
const chunkTokens = chunk.tokenEstimate || estimateTokens(chunk.content);
if (selected.length >= cfg.maxChunks) {
dropped.push(chunk);
continue;
}
if (tokensUsed + chunkTokens > budget && selected.length >= cfg.minChunks) {
dropped.push(chunk);
continue;
}
selected.push(chunk);
tokensUsed += chunkTokens;
}
return { selected, dropped, tokensUsed };
}
/**
* 선택된 청크들을 하나의 컨텍스트 문자열로 조립합니다.
* 소스별로 그룹화하여 가독성을 높입니다.
*/
export function assembleContext(chunks: RetrievalChunk[]): string {
if (chunks.length === 0) return '';
const sourceLabels: Record<string, string> = {
'brain-trace': '📚 Second Brain Knowledge',
'brain-memory': '📚 Brain Knowledge',
'long-term-memory': '🧠 Long-Term Memory (사용자 규칙/결정)',
'project-memory': '📂 Project Memory (프로젝트 컨텍스트)',
'procedural-memory': '📋 Procedural Memory (반복 절차)',
'episodic-memory': '📖 Episodic Memory (과거 대화 흐름)',
'project-scan': '🔍 Project Scan',
'recent-knowledge': '📄 Recent Project Knowledge'
};
// Group by source
const groups = new Map<string, RetrievalChunk[]>();
for (const chunk of chunks) {
const key = chunk.source;
if (!groups.has(key)) groups.set(key, []);
groups.get(key)!.push(chunk);
}
const sections: string[] = [];
for (const [source, groupChunks] of groups) {
const label = sourceLabels[source] || source;
const items = groupChunks
.map((c) => `- ${c.title}: ${c.content}`)
.join('\n');
sections.push(`### ${label}\n${items}`);
}
return [
'[MEMORY CONTEXT]',
'Review this layered memory before preparing the answer. Use it only when relevant, and prefer the current user request when there is conflict.',
'',
sections.join('\n\n')
].join('\n');
}