feat(retrieval): 임베딩 하이브리드 검색 활성화 — 자동 감지 + 측정 기반 수정 (v2.2.222)

골든셋(24질의) 측정으로 기존 하이브리드 구현의 결함 3건을 잡고 기본 활성화.
측정 결과: recall@3 83.3%→87.5%, MRR 0.802→0.806, recall@1 회귀 없음 (α=0.5).

수정 (측정으로 검증):
- 임베딩 입력을 토큰 재조합(tokens.join)→원문 슬라이스로 교체 + nomic/e5
  task prefix (search_query:/search_document:). 토큰 죽 입력은 하이브리드를
  전 지표 하락시켰음 (recall@1 75%→54%). @r2 리비전 키로 구벡터 자동 무효화.
- 블렌드 스케일 버그: 벡터 있는 후보만 정규화돼 벡터 없는 후보의 raw 점수가
  상위 독식 → 전 후보 정규화 + cosine 후보군 내 min-max 정규화.
- 헤딩-only 청크도 헤딩 텍스트로 임베딩 (벡터 공백 제거).

추가:
- embeddingBootstrap: 활성화 시 엔진 모델 목록에서 임베딩 모델 자동 감지 →
  embeddingModel 자동 설정 + "전체 색인" 버튼 알림. 다국어 모델(e5/bge-m3) 우선.
  사용자가 의도적으로 비우면 재설정 안 함 (globalState 가드).
- 벡터 저장 시 소수 4자리 양자화 — 캐시 360MB→~150MB (코사인 순위 영향 없음).
- tests/retrievalEvalEmbedding.test.ts: env-gated 하이브리드 측정 하니스 (alpha sweep).
- scripts/compact_brain_index.mjs: 기존 full-precision 캐시 1회 압축 도구.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 19:02:56 +09:00
parent c7c84702af
commit 67927b1d4e
9 changed files with 324 additions and 28 deletions
+80
View File
@@ -0,0 +1,80 @@
/**
* 임베딩 모델 자동 감지 부트스트랩 — 하이브리드(sparse+dense) 검색 온보딩.
*
* 문제: g1nation.embeddingModel 기본값이 '' (비활성) — LM Studio 에 임베딩 모델이
* 로드돼 있어도 사용자가 설정 키와 모델명 문자열을 알아야만 켜진다. 비개발자
* 사용자에게는 사실상 영구 비활성. 측정 결과 임베딩 블렌드가 어휘 갭
* ("심판"↔"LLM-as-a-Judge" 류 동의어/패러프레이즈) miss 를 해소하므로 기본 ON 가치가 있다.
*
* 해결: 활성화 시 embeddingModel 이 비어 있으면 엔진(ollamaUrl)의 모델 목록을 조회해
* 이름에 'embed' 가 들어간 모델을 찾아 자동 설정 + 1회 알림 ("전체 색인" 버튼 포함).
*
* 보호 장치:
* - 이미 embeddingModel 설정됨 → no-op (기존 사용자 무영향).
* - 자동 설정 이력이 있는데 현재 비어 있음 = 사용자가 의도적으로 껐다 → 재설정 안 함.
* - 엔진 미응답/임베딩 모델 없음 → 조용히 skip (다음 활성화 때 재시도).
*/
import * as vscode from 'vscode';
import { logError, logInfo } from '../utils';
const STATE_KEY = 'astra.embeddingAutoConfigured';
const PROBE_TIMEOUT_MS = 4000;
/** 엔진의 모델 목록에서 임베딩 모델 id 를 찾는다 (LM Studio /v1/models · Ollama /api/tags). */
async function detectEmbeddingModel(baseUrl: string): Promise<string | null> {
const root = baseUrl.replace(/\/+$/, '');
const tryFetch = async (url: string): Promise<any | null> => {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
try {
const r = await fetch(url, { signal: controller.signal });
return r.ok ? await r.json() : null;
} catch { return null; } finally { clearTimeout(timer); }
};
// OpenAI-호환 (LM Studio) 우선, 실패 시 Ollama.
const v1 = await tryFetch(`${root}/v1/models`);
let ids: string[] = Array.isArray(v1?.data) ? v1.data.map((m: any) => String(m?.id || '')) : [];
if (ids.length === 0) {
const tags = await tryFetch(`${root}/api/tags`);
ids = Array.isArray(tags?.models) ? tags.models.map((m: any) => String(m?.name || '')) : [];
}
// 임베딩 모델 후보 — 'embed' 외에 대표 임베딩 계열 이름도 포착
// (multilingual-e5-small, bge-m3 등은 이름에 'embed' 가 없다).
const candidates = ids.filter(id => /embed|(^|[^a-z0-9])(bge|gte|e5)([^a-z0-9]|$)|minilm|arctic-embed/i.test(id));
if (candidates.length === 0) return null;
// 한국어 두뇌 특성상 다국어 모델 우선 (골든셋 측정: nomic 은 한국어 동의어 매칭 한계).
const rank = (id: string) => (/multilingual|bge-m3|e5/i.test(id) ? 0 : 1);
candidates.sort((a, b) => rank(a) - rank(b));
return candidates[0];
}
/**
* embeddingModel 미설정 시 엔진에서 임베딩 모델을 자동 감지해 설정한다.
* 활성화 초기에 1회 호출 (fire-and-forget — 검색은 TF-IDF 로 이미 동작 중).
*/
export async function ensureEmbeddingConfigured(context: vscode.ExtensionContext): Promise<void> {
try {
const cfg = vscode.workspace.getConfiguration('g1nation');
const current = (cfg.get<string>('embeddingModel', '') || '').trim();
if (current) return; // 이미 설정됨
if (context.globalState.get<boolean>(STATE_KEY)) return; // 사용자가 의도적으로 비움
const baseUrl = (cfg.get<string>('ollamaUrl', '') || 'http://127.0.0.1:1234').trim();
const model = await detectEmbeddingModel(baseUrl);
if (!model) { logInfo('Embedding bootstrap: 엔진에서 임베딩 모델 미발견 — skip.', { baseUrl }); return; }
await cfg.update('embeddingModel', model, vscode.ConfigurationTarget.Global);
await context.globalState.update(STATE_KEY, true);
logInfo('Embedding bootstrap: 하이브리드 검색 자동 활성화.', { model, baseUrl });
void vscode.window.showInformationMessage(
`Astra: 임베딩 모델 "${model}" 을 발견해 하이브리드 검색을 켰습니다. ` +
'전체 색인을 한 번 실행하면 즉시 전체 두뇌에 적용됩니다 (이후는 자동 증분).',
'지금 전체 색인',
).then(pick => {
if (pick === '지금 전체 색인') void vscode.commands.executeCommand('g1nation.embeddings.backfill');
});
} catch (e: any) {
logError('Embedding bootstrap 실패 (TF-IDF 검색은 정상 동작).', { error: e?.message ?? String(e) });
}
}