feat(growth): Correction Loop — 정정 1회가 시스템 세 곳을 성장시키는 파이프라인 (v2.2.223)

self-evolving 고도화: 사용자 정정이 곧 Ground Truth — 정답지를 사람이 따로
만들지 않고, 태그 통계가 리포트에 머물지 않고 다음 턴의 행동을 바꾼다.

① 정정 감지·태깅 (correctionLoop.ts + agent.ts 훅, fire-and-forget):
- "아니야/틀렸어/~가 아니라" 류 정정 발화 감지 (보수적 — 추임새 "아니"는 제외)
- LLM 오류 분류 (사실오류/근거누락/맥락누락/추론오류/지시불이행/형식오류,
  실패 시 휴리스틱 fallback) → error-tag frontmatter 레슨(lessons/) 저장
- 동시에 회귀 케이스 적립: .astra/eval/corrections.jsonl {질문, 틀린답, 정정}

② 주간 성장 사이클 확장 (1.5단계):
- 정정 회귀 테스트: 정정받은 질문을 두뇌 검색 컨텍스트와 함께 재실행 →
  LLM-judge "같은 실수 반복?" 판정 → growth/regression-report.md (사이클당 ≤8건)
- 약점 프로필: 최근 60일 태그 통계 → growth/weakness-profile.json

③ 결핍의 행동화 (memoryContext):
- GROUNDING 약함 + agent scope 적용 중 → 전체 두뇌 1회 재검색 (scope 가
  정답 문서를 가리는 경우 구제, 더 강한 근거일 때만 채택)
- 그래도 약함 → 학습 큐에 지식 공백 자동 proposed 등록 (질문 해시 중복 차단,
  20건 폭주 방지, 승인은 사람 — Permission Based Learning 유지)
- 약점 프로필 → [자기검토] 블록 주입 (태그 2회 이상만): "너는 최근 X 정정을
  N회 받았다 — <유형별 자기검토 지시>"

테스트 25건 추가 (감지 패턴·프로필 집계·큐 등록·영속화·fallback 분류).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 19:28:46 +09:00
parent 67927b1d4e
commit 72faa07480
7 changed files with 650 additions and 10 deletions
+40 -7
View File
@@ -8,6 +8,7 @@ import type { RetrievalOrchestrator } from '../../retrieval';
import { buildLessonChecklistBlock } from '../../retrieval/lessonHelpers';
import { embedQuery, embedTexts } from '../../retrieval/embeddings';
import { backfillBrainEmbeddings, backfillBrainChunkEmbeddings } from '../../retrieval/brainIndex';
import { loadWeaknessProfile, buildSelfReviewBlock, registerKnowledgeGap } from '../../intelligence/correctionLoop';
import { resolveScopeForAgent } from '../../skills/agentKnowledgeMap';
import {
resolveKnowledgeMix,
@@ -196,7 +197,7 @@ export async function buildMemoryContext(deps: MemoryContextDeps): Promise<strin
: undefined;
// Unified RAG Pipeline 호출.
const result = deps.retrievalOrchestrator.retrieve(deps.currentPrompt, {
const retrieveOpts = {
brain: deps.activeBrain,
memoryManager: deps.memoryManager,
workspacePath,
@@ -216,7 +217,20 @@ export async function buildMemoryContext(deps: MemoryContextDeps): Promise<strin
hierarchicalReweightEnabled: config.hierarchicalReweightEnabled !== false,
chunkLevelRetrieval: config.chunkLevelRetrieval === true,
chunkTargetChars: config.chunkTargetChars,
});
};
let result = deps.retrievalOrchestrator.retrieve(deps.currentPrompt, retrieveOpts);
// [Correction Loop ③-a] 근거 약함 + agent scope 적용 중이면 전체 두뇌로 1회
// 재검색 — scope 가 정답 문서를 가리는 경우를 구제. 더 강한 근거가 나올 때만 채택.
if (scope.folders.length > 0 && assessGrounding(result).level === 'weak') {
try {
const retry = deps.retrievalOrchestrator.retrieve(deps.currentPrompt, { ...retrieveOpts, scopeFolders: [] });
if (assessGrounding(retry).level !== 'weak') {
retry.fusionLog.push('Grounding rescue: scope 해제 재검색 채택 (약함 → ' + assessGrounding(retry).level + ')');
result = retry;
}
} catch { /* 재검색 실패는 원 결과로 진행 */ }
}
// Semantic Re-rank (LLM, async) — selectedChunks 의 *순서* 만 재배치. 토큰 예산을
// 통과한 chunks 안에서 의도-부합도 순으로 재정렬해 LLM attention bias 활용.
@@ -380,8 +394,22 @@ export async function buildMemoryContext(deps: MemoryContextDeps): Promise<strin
// [확신도 전역화] 검색 근거 강도를 평가해 답변 정책을 함께 주입 — /meet 의
// "확신 없으면 단정 대신 표시" 원칙을 모든 대화로 확장. 근거가 약한데 단정적으로
// 답하는 '그럴듯한 오답'을 구조적으로 줄인다.
const groundingBlock = buildGroundingBlock(result);
return [groundingBlock, lessonBlock, memoryBlock].filter(Boolean).join('\n\n');
const grounding = assessGrounding(result);
let groundingBlock = buildGroundingBlock(grounding);
// [Correction Loop ③-b] scope 완화 후에도 약함 → 지식 공백으로 학습 큐에 proposed
// 등록 (1회, 사람 승인 후 주간 사이클의 Research Agent 가 처리). 결핍을 표시에서
// 행동으로 — "스스로 모르는 것을 안다" 의 기계적 구현.
if (grounding.level === 'weak') {
try {
if (registerKnowledgeGap(deps.activeBrain.localBrainPath, deps.currentPrompt, grounding.top)) {
groundingBlock += '\n→ 이 질문은 지식 공백으로 학습 큐(proposed)에 등록되었다. 사용자에게 "이 주제는 두뇌에 근거가 부족해 학습 후보로 등록해 두었다"고 한 줄로 알려라.';
}
} catch { /* 큐 등록 실패가 turn 을 막지 않음 */ }
}
// [Correction Loop ③-c] 약점 프로필 → 자기검토 블록. 최근 정정 통계가 다음 턴의
// 행동을 직접 바꾼다 (태그 2회 이상만 — 1회성 실수로 프롬프트를 어지럽히지 않게).
const selfReviewBlock = buildSelfReviewBlock(loadWeaknessProfile(deps.activeBrain.localBrainPath));
return [selfReviewBlock, groundingBlock, lessonBlock, memoryBlock].filter(Boolean).join('\n\n');
}
/**
@@ -391,15 +419,20 @@ export async function buildMemoryContext(deps: MemoryContextDeps): Promise<strin
* - weak (top < 0.25 또는 두뇌 청크 0): 답변 첫 줄에 "⚠️ 두뇌 근거 약함" 표기 + 단정 금지.
* 점수는 normalize 된 0~1 — 임계값은 초기치이며 골든셋으로 추후 튜닝 가능.
*/
function buildGroundingBlock(result: { selectedChunks: Array<{ source: string; score: number }> }): string {
interface GroundingAssessment { level: 'strong' | 'moderate' | 'weak'; top: number; count: number }
function assessGrounding(result: { selectedChunks: Array<{ source: string; score: number }> }): GroundingAssessment {
const brainChunks = result.selectedChunks.filter((c) => c.source === 'brain-trace' || c.source === 'brain-memory');
const top = brainChunks.length ? Math.max(...brainChunks.map((c) => c.score || 0)) : 0;
let level: 'strong' | 'moderate' | 'weak';
let level: GroundingAssessment['level'];
if (brainChunks.length === 0 || top < 0.25) level = 'weak';
else if (top >= 0.5 && brainChunks.length >= 2) level = 'strong';
else level = 'moderate';
return { level, top, count: brainChunks.length };
}
const lines = [`[GROUNDING] 이번 질의의 두뇌 근거 강도: ${level === 'strong' ? '강함' : level === 'moderate' ? '보통' : '약함'} (두뇌 청크 ${brainChunks.length}개, 최고 점수 ${top.toFixed(2)})`];
function buildGroundingBlock({ level, top, count }: GroundingAssessment): string {
const lines = [`[GROUNDING] 이번 질의의 두뇌 근거 강도: ${level === 'strong' ? '강함' : level === 'moderate' ? '보통' : '약함'} (두뇌 청크 ${count}개, 최고 점수 ${top.toFixed(2)})`];
if (level === 'weak') {
lines.push('→ 답변 첫 줄에 "⚠️ 두뇌 근거 약함 — 일반 지식 기반 추정입니다." 를 표기하고, 단정 대신 "가능성/추정" 표현을 사용하라. 확실하지 않은 세부 수치·고유명사는 만들지 말 것.');
} else if (level === 'strong') {