47b3b9f93a
- v2.2.299 아키텍처 수렴: coreChat 통일(엔진 휴리스틱 3벌 제거), 기업 모드 검색 오케스트레이터 승격, lib/execUtil(실행 래퍼 6곳·Python 탐지 3벌 단일화), lib/kstSchedule(워처 4개 nowInKst 통합), estimateTokens 통합, 설정 접근 규칙 명문화 - v2.2.300 채팅 화면 정리: LiveReasoningFilter(스트리밍 중 <think>/Harmony 추론 토큰 단위 차단), 확신도·검토요청 footer 기본 숨김(계산·Reflection 은 유지) - v2.2.301 문맥·의도 이해: [답변 전 이해 원칙] 상시 주입, 워크플로우 의도 브리핑, Report QA 루프(규칙 레지스트리+실측치+회귀 게이트, 블로그_v3 개념 이식) - v2.2.302 /benchmark 비즈니스 렌즈(가격·수익·운영)+빌드 프롬프트 모드+QA 연계 - v2.2.303 handoff 모드(측정치 무손실 인수인계 문서)+/claude(Claude Code 터미널 위임) - v2.2.304 이식성: 지식 경로 두뇌-상대 규약(pickWikiDir 상대 해석), 이사 체크리스트 - v2.2.305 Claude 구독 엔진: claude: 프로바이더(CLI 위임, 모델 드롭다운 자동 노출, coreChat 지원 — 워크플로우·QA도 구독 모델 가능) - v2.2.306 Tone Guard: AI 상투어 금지 레지스트리(상담사 화법 실사례 8종+대조 예시) - v2.2.307 /benchmark 레이아웃 골격(sectionRoles 결정론 분석, 롤링 배너 즉답), 파트별 실패 격리, 합성 타임아웃 120→300초 검증: tsc 무오류 + jest 888 통과 + esbuild 정상 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
147 lines
5.9 KiB
TypeScript
147 lines
5.9 KiB
TypeScript
/**
|
|
* ============================================================
|
|
* 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
|
|
};
|
|
|
|
// [코어 수렴] 토큰 추정은 lib/contextManager 의 단일 구현을 쓴다 (CJK 전반 커버·
|
|
// 보수적 계수). 예전 이 파일의 자체 추정(한글만 1.5/0.25)은 제거 — 기존 소비자
|
|
// 호환을 위해 같은 이름으로 재수출.
|
|
import { estimateTokens } from '../lib/contextManager';
|
|
export { estimateTokens };
|
|
|
|
|
|
/**
|
|
* 검색 결과 청크들을 토큰 예산 내에서 선택합니다.
|
|
*
|
|
* 선택 전략:
|
|
* 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. [Structural Fix] 파일당 청크 수 제한 완화 (Deduplication -> Multi-context)
|
|
const fileChunkCounts = new Map<string, number>();
|
|
const filtered = sorted.filter((chunk) => {
|
|
const key = chunk.metadata.filePath || chunk.id;
|
|
const count = fileChunkCounts.get(key) || 0;
|
|
|
|
// 파일당 최대 3개까지의 주요 맥락 허용 (정보 유실 방지)
|
|
if (count >= 3) return false;
|
|
|
|
fileChunkCounts.set(key, count + 1);
|
|
return true;
|
|
});
|
|
|
|
// 3. Select within budget
|
|
const selected: RetrievalChunk[] = [];
|
|
const dropped: RetrievalChunk[] = [];
|
|
let tokensUsed = 0;
|
|
|
|
for (const chunk of filtered) {
|
|
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 };
|
|
}
|
|
|
|
/**
|
|
* 청크의 '주제(Subject)' 태그를 도출한다 — 서로 다른 프로젝트/주제의 정보가 한
|
|
* 컨텍스트에 섞일 때 모델이 경계를 인지하도록(무성 교차오염 방지). category 가 있으면
|
|
* 그걸, 없으면 title/filePath 의 최상위 폴더 세그먼트를 주제로 본다. 파일명만 있으면 ''.
|
|
*/
|
|
function deriveSubject(chunk: RetrievalChunk): string {
|
|
const cat = (chunk.metadata.category || '').trim();
|
|
if (cat) return cat;
|
|
const ref = (chunk.title || chunk.metadata.filePath || '').replace(/\\/g, '/');
|
|
const seg = ref.split('/').filter(Boolean);
|
|
return seg.length >= 2 ? seg[0] : '';
|
|
}
|
|
|
|
/**
|
|
* 선택된 청크들을 하나의 컨텍스트 문자열로 조립합니다.
|
|
* 소스별로 그룹화하여 가독성을 높입니다.
|
|
*/
|
|
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 (사용자 규칙/결정)',
|
|
'medium-term-memory': '🗂️ Medium-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) => {
|
|
const metadata = c.metadata;
|
|
const subject = deriveSubject(c);
|
|
const subjectTag = subject ? `[${subject}] ` : '';
|
|
const conflictTag = metadata.conflictDetected ? ` [⚠️ CONFLICT: ${metadata.conflictSeverity}]` : '';
|
|
const coverageTag = metadata.queryCoverage !== undefined ? ` (Coverage: ${metadata.queryCoverage.toFixed(2)})` : '';
|
|
return `- ${subjectTag}${c.title}${conflictTag}${coverageTag}: ${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');
|
|
}
|