feat(self): 자기 평가 질의에 기능 인벤토리 강제 주입 + 충돌 신뢰도 비교 권고 (v2.2.226)
[자기 지식 구식화 — 마지막 구멍 봉쇄] 인벤토리를 자동 생성해도(v2.2.225) 모델이 검색 없이 기억으로 답하면 무용 — 실사례: 답변 말미 "출처: 모델 지식 (검색 미사용)" 후 이미 있는 기능 (CoVe·멀티스텝 플래닝·노후점검 자동화)을 신규 제안. 프롬프트 규칙은 검색을 강제할 수 없으므로 scheduleContext 와 동일 패턴으로 해결: - selfAssessContext: "기능 개선/고도화/self-evolving/무슨 기능" 류 질의 감지 시 인벤토리 전문을 RAG 경쟁 없이 결정론적 주입 + "이미 있는 기능 신규 제안 금지, '현재 X 있음 — 빠진 증분 Y' 형태" 지시. 인벤토리 미생성 시 정직 안내. [충돌 해결사 — 권고까지만, 자동 결정은 안 함] - conflictScan 에 신뢰도 비교 추가: 양쪽 frontmatter(source_trust_level S~D, confidence_score) + 최신성으로 "신규/기존 우선 권고" 생성. 메타데이터 없거나 비등하면 권고 보류 (근거 없는 권고 금지). 삭제·덮어쓰기는 여전히 사람 결정. 테스트 17건 추가 (질의 감지·인벤토리 주입·신뢰 파싱·권고 분기). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "astra",
|
||||
"version": "2.2.225",
|
||||
"version": "2.2.226",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "astra",
|
||||
"version": "2.2.225",
|
||||
"version": "2.2.226",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lmstudio/sdk": "^1.5.0",
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "astra",
|
||||
"displayName": "Astra",
|
||||
"description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.",
|
||||
"version": "2.2.225",
|
||||
"version": "2.2.226",
|
||||
"publisher": "g1nation",
|
||||
"license": "MIT",
|
||||
"icon": "assets/icon.png",
|
||||
|
||||
@@ -20,6 +20,7 @@ import { SessionManager } from './core/session';
|
||||
import { AgentWorkflowManager } from './agents/AgentWorkflowManager';
|
||||
import { buildAstraModeArchitectureContext } from './lib/contextBuilders/astraModeArchitecture';
|
||||
import { isScheduleRequest, buildScheduleContext } from './lib/contextBuilders/scheduleContext';
|
||||
import { isSelfAssessRequest, buildSelfAssessContext } from './lib/contextBuilders/selfAssessContext';
|
||||
import { looksLikeCorrection, captureCorrection } from './intelligence/correctionLoop';
|
||||
import { shouldUseMultiAgentWorkflow } from './lib/contextBuilders/multiAgentRouting';
|
||||
import { buildThinkingPartnerResponseContract } from './lib/contextBuilders/thinkingPartnerContract';
|
||||
@@ -537,6 +538,17 @@ export class AgentExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
// [자기 평가 정본 주입] 기능 개선/자기 평가 질의는 RAG 경쟁에 맡기지 않고
|
||||
// 현행 기능 인벤토리를 결정론적으로 주입 — 모델이 검색 없이 기억으로 답해
|
||||
// 이미 있는 기능을 신규 제안하던 구식화 버그(3회 재발)의 마지막 구멍 봉쇄.
|
||||
if (prompt && loopDepth === 0 && !isCasualConversation && activeBrain?.localBrainPath && isSelfAssessRequest(prompt)) {
|
||||
try {
|
||||
contextBlock += `\n\n${buildSelfAssessContext(activeBrain.localBrainPath)}`;
|
||||
} catch (e: any) {
|
||||
logError('자기 평가 컨텍스트 주입 실패 (계속 진행).', { error: e?.message ?? String(e) });
|
||||
}
|
||||
}
|
||||
|
||||
// [Correction Loop ①] 이 발화가 직전 답변에 대한 *정정*이면 fire-and-forget
|
||||
// 캡처 — 오류 분류 → 태깅 레슨 + 회귀 케이스(.astra/eval/corrections.jsonl).
|
||||
// 정정 자체가 Ground Truth 가 되어 주간 회귀 테스트·약점 프로필의 원료가 된다.
|
||||
|
||||
@@ -61,6 +61,48 @@ export interface ConflictFinding {
|
||||
newDoc: string;
|
||||
existingDoc: string;
|
||||
summary: string;
|
||||
/** 신뢰도 비교 기반 우선 권고 (자동 삭제는 하지 않음 — 결정은 사람). */
|
||||
recommend: string;
|
||||
}
|
||||
|
||||
// ── 신뢰도 비교 — frontmatter(source_trust_level·confidence_score) + 최신성 ──
|
||||
|
||||
export interface DocTrust {
|
||||
trustLevel: string; // S/A/B/C/D 또는 ''
|
||||
confidence: number; // 0~1, 미상 -1
|
||||
mtimeMs: number;
|
||||
}
|
||||
|
||||
const TRUST_RANK: Record<string, number> = { S: 4, A: 3, B: 2, C: 1, D: 0 };
|
||||
|
||||
/** 문서 머리 frontmatter 에서 신뢰 메타데이터 추출 (없으면 빈 값). */
|
||||
export function parseDocTrust(content: string, mtimeMs: number): DocTrust {
|
||||
const head = (content || '').slice(0, 1200);
|
||||
const trust = head.match(/^source_trust_level:\s*["']?([SABCD])["']?\s*$/mi)?.[1]?.toUpperCase() || '';
|
||||
const confRaw = head.match(/^confidence_score:\s*["']?([\d.]+)["']?\s*$/mi)?.[1];
|
||||
const confidence = confRaw !== undefined && Number.isFinite(Number(confRaw)) ? Math.max(0, Math.min(1, Number(confRaw))) : -1;
|
||||
return { trustLevel: trust, confidence, mtimeMs };
|
||||
}
|
||||
|
||||
const fmtTrust = (t: DocTrust) =>
|
||||
`${t.trustLevel || '등급없음'}·${t.confidence >= 0 ? t.confidence.toFixed(1) : '점수없음'}`;
|
||||
|
||||
/**
|
||||
* 충돌 양쪽의 신뢰도를 비교해 우선 권고 문자열 생성.
|
||||
* 점수 = 신뢰등급(0~4)×2 + confidence(0~1)×2 + 최신성(+1). 메타데이터가 양쪽 다
|
||||
* 없으면 권고 보류 — 근거 없는 권고는 하지 않는다.
|
||||
*/
|
||||
export function buildTrustRecommendation(newer: DocTrust, older: DocTrust): string {
|
||||
const hasMetaN = !!newer.trustLevel || newer.confidence >= 0;
|
||||
const hasMetaO = !!older.trustLevel || older.confidence >= 0;
|
||||
if (!hasMetaN && !hasMetaO) return '권고 보류 — 양쪽 모두 신뢰 메타데이터 없음 (내용으로 직접 판단 필요)';
|
||||
const score = (t: DocTrust, isNewer: boolean) =>
|
||||
(TRUST_RANK[t.trustLevel] ?? 0) * 2 + (t.confidence >= 0 ? t.confidence : 0) * 2 + (isNewer ? 1 : 0);
|
||||
const sNew = score(newer, true), sOld = score(older, false);
|
||||
if (Math.abs(sNew - sOld) < 1) return `권고 보류 — 신뢰도 비등 (신규 ${fmtTrust(newer)} vs 기존 ${fmtTrust(older)})`;
|
||||
return sNew > sOld
|
||||
? `권고: **신규 우선** (신규 ${fmtTrust(newer)}·최신 vs 기존 ${fmtTrust(older)})`
|
||||
: `권고: **기존 우선** (기존 ${fmtTrust(older)} vs 신규 ${fmtTrust(newer)}·최신)`;
|
||||
}
|
||||
|
||||
/** 1회 스캔 실행. 반환: 요약 문자열. */
|
||||
@@ -114,7 +156,14 @@ export async function runConflictScanOnce(): Promise<string> {
|
||||
if (!m) continue;
|
||||
const parsed = JSON.parse(m[0]);
|
||||
if (parsed?.conflict === true && String(parsed?.summary || '').trim()) {
|
||||
findings.push({ newDoc: rel, existingDoc: n.relativePath, summary: String(parsed.summary).slice(0, 200) });
|
||||
let existingMtime = 0;
|
||||
try { existingMtime = fs.statSync(n.filePath).mtimeMs; } catch { /* 0 유지 */ }
|
||||
findings.push({
|
||||
newDoc: rel,
|
||||
existingDoc: n.relativePath,
|
||||
summary: String(parsed.summary).slice(0, 200),
|
||||
recommend: buildTrustRecommendation(parseDocTrust(content, t.m), parseDocTrust(existing, existingMtime)),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -128,7 +177,7 @@ export async function runConflictScanOnce(): Promise<string> {
|
||||
const reportFile = path.join(brainPath, REPORT_REL);
|
||||
const block = [
|
||||
`\n## ${new Date().toLocaleString()} — 충돌 ${findings.length}건`,
|
||||
...findings.map(f => `- ⚔️ 신규 **${f.newDoc}** ↔ 기존 **${f.existingDoc}**: ${f.summary}\n → 어느 쪽을 유지할지 결정 후 다른 쪽을 수정/삭제하세요 (시스템은 덮어쓰지 않음).`),
|
||||
...findings.map(f => `- ⚔️ 신규 **${f.newDoc}** ↔ 기존 **${f.existingDoc}**: ${f.summary}\n → ${f.recommend}\n → 어느 쪽을 유지할지 결정 후 다른 쪽을 수정/삭제하세요 (시스템은 덮어쓰지 않음).`),
|
||||
].join('\n');
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(reportFile), { recursive: true });
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 자기 평가/개선 질의 컨텍스트 — "기능 개선 아이디어 줘" 류 질문에 ASTRA 의
|
||||
* *현행* 기능 인벤토리(자동 생성 문서)를 결정론적으로 직접 주입한다.
|
||||
*
|
||||
* 문제 (3회 재발한 자기 지식 구식화의 마지막 구멍): 인벤토리 문서를 자동 생성해도
|
||||
* RAG 점수 경쟁에서 안 뽑히거나 모델이 검색 없이 기억으로 답하면 — 실제 사례:
|
||||
* 답변 말미에 "출처: 모델 지식 (검색 미사용)" — 이미 있는 기능을 신규 제안한다.
|
||||
* 프롬프트 규칙("인벤토리와 대조하라")은 검색을 강제할 수 없다.
|
||||
*
|
||||
* 수정: scheduleContext(일정 질의→캘린더 실데이터 강제 주입)와 동일 패턴 —
|
||||
* 자기 평가 질의를 감지하면 인벤토리 전문을 RAG 경쟁 없이 컨텍스트에 넣는다.
|
||||
* "검색 안 함" 실패 모드 자체를 제거.
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { INVENTORY_FILE } from '../../extension/featureInventory';
|
||||
|
||||
const IMPROVE_RE = /(개선|고도화|발전|보완|제안|평가|분석|방향|방법|아이디어|로드맵|업그레이드|날카롭|강화)/i;
|
||||
const SELF_RE = /(기능|역량|능력|아키텍처|구조|시스템|self.?evolv|자기\s*진화|자기\s*개선|아스트라|astra|너(가|의|는)?|네가)/i;
|
||||
const CAPABILITY_RE = /(무슨|어떤|할\s*수\s*있는)\s*(기능|일|것)|기능\s*(목록|리스트)|capabilit/i;
|
||||
|
||||
/**
|
||||
* 자기 평가·개선·기능 질의인지. 오탐 비용이 낮으므로(인벤토리 ~3KB 추가 주입뿐)
|
||||
* 누락(또 구식 제안)보다 과잉 감지를 택한다.
|
||||
*/
|
||||
export function isSelfAssessRequest(prompt: string): boolean {
|
||||
const p = (prompt || '').trim();
|
||||
if (!p || p.length > 600) return false;
|
||||
return CAPABILITY_RE.test(p) || (IMPROVE_RE.test(p) && SELF_RE.test(p));
|
||||
}
|
||||
|
||||
const MAX_INVENTORY_CHARS = 7000;
|
||||
|
||||
/** 인벤토리 전문 + 대조 지시 블록. 파일 없으면 정직한 안내 (지어내기 방지). */
|
||||
export function buildSelfAssessContext(brainPath: string): string {
|
||||
const header = '[ASTRA 현행 기능 인벤토리 — 소스 코드에서 자동 생성된 정본]';
|
||||
let body = '';
|
||||
try {
|
||||
const file = path.join(brainPath, INVENTORY_FILE);
|
||||
if (fs.existsSync(file)) body = fs.readFileSync(file, 'utf8').slice(0, MAX_INVENTORY_CHARS);
|
||||
} catch { /* 아래 fallback */ }
|
||||
if (!body.trim()) {
|
||||
return [
|
||||
header,
|
||||
'상태: 인벤토리 문서가 아직 생성되지 않음 (다음 활성화 때 자동 생성).',
|
||||
'→ 기억에 의존해 기능 목록을 단정하지 말고, 기능 존재 여부가 불확실하면 "확인 필요"로 표시하라.',
|
||||
].join('\n');
|
||||
}
|
||||
return [
|
||||
header,
|
||||
'자기 기능 평가·개선 제안 시 반드시 아래 인벤토리와 대조하라:',
|
||||
'- 아래에 **이미 있는 기능을 신규 제안하지 마라.**',
|
||||
'- 제안은 "현재 X가 있고, 빠진 증분은 Y" 형태로.',
|
||||
'- 아래에 없는 기능을 있다고 주장하지도 마라.',
|
||||
'',
|
||||
body,
|
||||
].join('\n');
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 자기 평가 정본 주입 + 충돌 신뢰도 비교 — 순수 로직 테스트.
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { isSelfAssessRequest, buildSelfAssessContext } from '../src/lib/contextBuilders/selfAssessContext';
|
||||
import { INVENTORY_FILE } from '../src/extension/featureInventory';
|
||||
import { parseDocTrust, buildTrustRecommendation } from '../src/features/growth/conflictScan';
|
||||
|
||||
describe('isSelfAssessRequest — 자기 평가/기능 질의 감지', () => {
|
||||
test.each([
|
||||
'너의 기능 개선 아이디어 줘',
|
||||
'self-evolving 엔진을 더 날카롭게 만들 방법은?',
|
||||
'아스트라 아키텍처 분석하고 개선 방향 제안해줘',
|
||||
'시스템 고도화 방안 알려줘',
|
||||
'너 무슨 기능 있어?',
|
||||
'아스트라가 할 수 있는 일 목록',
|
||||
])('감지: %s', (p) => expect(isSelfAssessRequest(p)).toBe(true));
|
||||
|
||||
test.each([
|
||||
'오늘 업무 목록 알려줘',
|
||||
'이 회의록 요약해줘',
|
||||
'삼성전자 주가 어때',
|
||||
'커밋하고 푸쉬해줘',
|
||||
])('비대상: %s', (p) => expect(isSelfAssessRequest(p)).toBe(false));
|
||||
});
|
||||
|
||||
describe('buildSelfAssessContext', () => {
|
||||
test('인벤토리 있으면 전문 + 대조 지시 주입', () => {
|
||||
const brain = fs.mkdtempSync(path.join(os.tmpdir(), 'astra-sa-'));
|
||||
fs.writeFileSync(path.join(brain, INVENTORY_FILE), '# 인벤토리 v9\n- 기능 A', 'utf8');
|
||||
const block = buildSelfAssessContext(brain);
|
||||
expect(block).toContain('이미 있는 기능을 신규 제안하지 마라');
|
||||
expect(block).toContain('기능 A');
|
||||
});
|
||||
test('인벤토리 없으면 정직한 안내 (지어내기 방지)', () => {
|
||||
const block = buildSelfAssessContext(fs.mkdtempSync(path.join(os.tmpdir(), 'astra-sa-')));
|
||||
expect(block).toContain('생성되지 않음');
|
||||
expect(block).toContain('확인 필요');
|
||||
});
|
||||
});
|
||||
|
||||
describe('충돌 신뢰도 비교', () => {
|
||||
const fm = (trust: string, conf: number) => `---\nsource_trust_level: "${trust}"\nconfidence_score: ${conf}\n---\n본문`;
|
||||
test('frontmatter 파싱', () => {
|
||||
const t = parseDocTrust(fm('S', 0.95), 123);
|
||||
expect(t).toMatchObject({ trustLevel: 'S', confidence: 0.95, mtimeMs: 123 });
|
||||
expect(parseDocTrust('본문만 있음', 1).trustLevel).toBe('');
|
||||
expect(parseDocTrust('본문만 있음', 1).confidence).toBe(-1);
|
||||
});
|
||||
test('신규 S vs 기존 C → 신규 우선', () => {
|
||||
const r = buildTrustRecommendation(parseDocTrust(fm('S', 1.0), 2), parseDocTrust(fm('C', 0.5), 1));
|
||||
expect(r).toContain('신규 우선');
|
||||
});
|
||||
test('신규 등급없음 vs 기존 S → 기존 우선 (최신이어도 신뢰가 이김)', () => {
|
||||
const r = buildTrustRecommendation(parseDocTrust('본문', 2), parseDocTrust(fm('S', 1.0), 1));
|
||||
expect(r).toContain('기존 우선');
|
||||
});
|
||||
test('양쪽 메타 없음 → 권고 보류 (근거 없는 권고 금지)', () => {
|
||||
const r = buildTrustRecommendation(parseDocTrust('a', 2), parseDocTrust('b', 1));
|
||||
expect(r).toContain('권고 보류');
|
||||
});
|
||||
test('비등 → 보류', () => {
|
||||
const r = buildTrustRecommendation(parseDocTrust(fm('A', 0.8), 2), parseDocTrust(fm('A', 0.9), 1));
|
||||
expect(r).toContain('권고 보류');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user