feat: integrate unified RAG pipeline and bump version to 2.60.0
This commit is contained in:
@@ -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');
|
||||
}
|
||||
Reference in New Issue
Block a user