c42c66a3fc
P1 — 섹션 청크 검색 기본 활성화: - 골든셋 24질의 A/B 측정: 파일 단위 → 섹션 청크에서 recall@1 12.5%→75.0% · recall@3 33.3%→83.3% · recall@5 37.5%→87.5% · MRR 0.217→0.802. 18질의 개선·악화 0건. - Phase 1-가 구현은 완성돼 있었으나 chunkLevelRetrieval 기본값이 false 라 실전 채팅이 열등한 파일 모드로 동작 — package.json·config 기본값 true 로. - tests/retrievalEvalCompare.test.ts: 환경변수(ASTRA_EVAL_BRAIN) 게이트형 A/B 회귀 측정 도구 (평소 skip — CI/패키징 무영향). P2 — 확신도 전역화 (/meet 원칙을 모든 대화로): - memoryContext 에 [GROUNDING] 블록 — 두뇌 근거 강도(강/보통/약)를 점수로 평가해 답변 정책 주입: 약함 → "⚠️ 두뇌 근거 약함" 표기+단정 금지, 강함 → 근거 문서 제목 인용, 보통 → 사실/추론 구분 서술. P3 — 회의 용어집 자동화 + 출력 위생: - /meet 실행마다 담당자 이름·사용자 메타데이터 용어를 .astra/meet_glossary.json 에 누적, 다음 실행 때 자동 주입 (STT 보정 용어집 — 반복 회의 표기 일관성). - selfIdentity 블록에 한·영 혼합 깨진 표기 금지 규칙 (전 대화, 무비용). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
87 lines
4.0 KiB
TypeScript
87 lines
4.0 KiB
TypeScript
/**
|
|
* 검색 모드 A/B 측정 — 파일 단위(baseline) vs 섹션 청크(Phase 1-가).
|
|
*
|
|
* 평소 테스트 런에서는 skip 된다 (실제 두뇌 폴더 + 수천 파일 인덱싱이 필요해
|
|
* CI/패키징에 부적합). 수동 실행:
|
|
*
|
|
* ASTRA_EVAL_BRAIN="E:/Wiki/2nd/10_Wiki/Topics" npx jest tests/retrievalEvalCompare.test.ts --verbose
|
|
*
|
|
* 골든셋(<brain>/.astra/eval/golden.jsonl) 기준 recall@k / MRR 을 두 모드로 측정해
|
|
* 비교표를 콘솔에 출력한다. TF-IDF 경로 기준 (임베딩은 LM 서버 의존이라 제외 —
|
|
* 청킹의 효과는 sparse 항과 발췌 품질에 먼저 나타난다).
|
|
*/
|
|
import * as fs from 'fs';
|
|
import { RetrievalOrchestrator } from '../src/retrieval';
|
|
import { loadGoldenSet, runRetrievalEval, type EvalReport } from '../src/retrieval/evalHarness';
|
|
import { findBrainFiles } from '../src/utils';
|
|
import { getBrainTokenIndex } from '../src/retrieval/brainIndex';
|
|
|
|
const BRAIN = (process.env.ASTRA_EVAL_BRAIN || '').trim();
|
|
const KS = [1, 3, 5];
|
|
|
|
const maybe = BRAIN && fs.existsSync(BRAIN) ? describe : describe.skip;
|
|
|
|
maybe('retrieval A/B — file vs chunk', () => {
|
|
jest.setTimeout(10 * 60_000);
|
|
|
|
test('golden set comparison', async () => {
|
|
const { entries, parseErrors } = loadGoldenSet(BRAIN);
|
|
expect(entries.length).toBeGreaterThan(0);
|
|
|
|
// 인덱스 워밍업 (양 모드 공통 전제)
|
|
const allFiles = findBrainFiles(BRAIN);
|
|
getBrainTokenIndex(BRAIN, allFiles);
|
|
|
|
const brain = { id: 'eval', name: 'EvalBrain', localBrainPath: BRAIN } as any;
|
|
const orchestrator = new RetrievalOrchestrator();
|
|
|
|
const run = (chunkMode: boolean): Promise<EvalReport> =>
|
|
runRetrievalEval({
|
|
entries,
|
|
ks: KS,
|
|
ranker: async (query: string) =>
|
|
orchestrator
|
|
.rankBrainForEval(query, brain, {
|
|
limit: Math.max(...KS) + 5,
|
|
chunkLevelRetrieval: chunkMode,
|
|
chunkTargetChars: 1200,
|
|
})
|
|
.map(r => r.relativePath),
|
|
});
|
|
|
|
const fileReport = await run(false);
|
|
const chunkReport = await run(true);
|
|
|
|
const pct = (x: number) => (x * 100).toFixed(1) + '%';
|
|
const lines: string[] = [];
|
|
lines.push('');
|
|
lines.push(`══ 검색 A/B (질의 ${entries.length}건, 파싱오류 ${parseErrors}) ══`);
|
|
lines.push(`지표 | 파일 단위 | 섹션 청크 | Δ`);
|
|
for (const k of KS) {
|
|
const a = fileReport.recallAtK[k], b = chunkReport.recallAtK[k];
|
|
lines.push(`recall@${k} | ${pct(a).padStart(7)} | ${pct(b).padStart(7)} | ${(b - a >= 0 ? '+' : '')}${pct(b - a)}`);
|
|
}
|
|
lines.push(`MRR | ${fileReport.mrr.toFixed(3).padStart(7)} | ${chunkReport.mrr.toFixed(3).padStart(7)} | ${(chunkReport.mrr - fileReport.mrr >= 0 ? '+' : '')}${(chunkReport.mrr - fileReport.mrr).toFixed(3)}`);
|
|
// 모드별 win/loss 질의
|
|
const flips: string[] = [];
|
|
fileReport.perQuery.forEach((fq, i) => {
|
|
const cq = chunkReport.perQuery[i];
|
|
const f = fq.firstHitRank, c = cq.firstHitRank;
|
|
if ((f === null) !== (c === null) || (f !== null && c !== null && f !== c)) {
|
|
flips.push(` · "${fq.query.slice(0, 38)}" 파일=#${f ?? 'miss'} → 청크=#${c ?? 'miss'}`);
|
|
}
|
|
});
|
|
if (flips.length) { lines.push('순위 변동:'); lines.push(...flips); }
|
|
// miss 진단 (청크 모드)
|
|
const misses = chunkReport.perQuery.filter(q => q.firstHitRank === null);
|
|
if (misses.length) {
|
|
lines.push(`청크 모드 miss ${misses.length}건:`);
|
|
for (const m of misses) lines.push(` ✗ "${m.query.slice(0, 38)}" → 상위: ${m.topPaths.slice(0, 3).join(' · ')}`);
|
|
}
|
|
// eslint-disable-next-line no-console
|
|
console.log(lines.join('\n'));
|
|
|
|
expect(chunkReport.total).toBe(fileReport.total);
|
|
});
|
|
});
|