Files
connectai/src/features/selfReflector/selfReflectorVerifier.ts
T
koriweb 72412450c3 feat: v2.2.3 - Stability, Self-Reflector & Intent Alignment
- 버전 2.2.3 상향 및 PATCHNOTES.md 업데이트

- [신규] src/features/selfReflector/ - 성찰 실행/검증/프롬프트 모듈 추가

- [신규] intentAlignment.ts, intentClassifier.ts - 의도 정렬 시스템 추가

- [신규] pixelOfficeState.ts - 픽셀 오피스 상태 관리 추가

- sidebarProvider, dispatcher, chatHandlers 핵심 로직 최적화

- astra-2.2.3.vsix 패키지 생성 완료 (298 tests PASS)
2026-05-15 14:16:14 +09:00

173 lines
7.3 KiB
TypeScript

/**
* Self-Reflector Phase B — *분리된 콘텍스트*에서 LLM 한 번 더 호출해 응답을
* 외부 시각으로 검증.
*
* Phase A의 self-check는 같은 모델·같은 콘텍스트에서 자기 자신을 보는 한계가
* 있다. 모델이 자기가 만든 답변을 자신 있게 잘못 평가하는 *과신 편향*은
* LLM의 잘 알려진 약점이다. Phase B는 이걸 보완하기 위해:
*
* 1. specialist 응답이 끝나면
* 2. *새로운* system prompt로 LLM에게 (task + 응답)을 보여주고
* 3. "이 응답이 task를 충실히 처리했나? 명백한 오류가 있나?"를 묻는다
* 4. {verdict: pass|warn|fail, issues: [...]} JSON으로 받음
* 5. fail이면 issue들을 prepend해 같은 specialist 1회 retry
*
* 호출자(dispatcher)는 verdict + issues + final response를 받아 그대로 chat에
* 표시한다. 검증 LLM 자체가 실패해도 *원본 응답은 보존* — 검증 layer가
* 진행 자체를 막지 않는다.
*/
import { IAIService } from '../../core/services';
import { logError, logInfo } from '../../utils';
export interface VerifyInput {
/** specialist에게 줬던 task 문자열 (revisionNote 등 prefix 포함). */
task: string;
/** specialist의 raw 응답 (action-tag 실행 *전*). */
response: string;
/** specialist가 누구였는지 (검증 프롬프트 컨텍스트). */
agentName: string;
/** 검증에 사용할 모델. 비싸지 않아도 OK — 검증은 짧고 가볍게. */
model?: string;
/** Requirement Contract — 있으면 검증 기준으로 직접 활용. */
contractBlock?: string;
}
export type VerifyVerdict = 'pass' | 'warn' | 'fail';
export interface VerifyResult {
verdict: VerifyVerdict;
/** 발견된 이슈 목록 (verdict='warn'·'fail' 시 1~3개). */
issues: string[];
/** 한 줄 요약 — chat label용. */
summary: string;
/** 검증 LLM이 실패한 경우 true; 호출자는 원본 응답 보존하고 진행. */
verifierError?: boolean;
}
const SYSTEM_PROMPT = `당신은 *외부 감리* 입니다. 다른 에이전트가 방금 사용자 task에 대해 만든 응답을 객관적으로 점검합니다. 응답을 만든 본인이 아니므로 *과신 없이* 보세요.
점검 기준:
1. task 요구를 *직접* 처리했는가 (회피·동문서답 X)
2. 명백한 사실 오류·논리 모순이 없는가
3. 코드/파일이 포함됐다면 정의되지 않은 변수·잘못된 경로·존재하지 않는 import가 없는가
4. Requirement Contract가 있으면 criteria를 위반하지 않는가
🛑 **빈 깡통(Hollow Code) 자동 fail**:
코드 파일이 포함됐는데 *실제 로직이 비어 있으면* 무조건 "fail":
- 함수 본문이 \`pass\` / \`return None\` / \`return null\` / \`...\` 한 줄뿐
- 함수 본문이 TODO/FIXME 주석뿐
- 클래스/모듈에 import만 있고 로직 0줄
- "여기에 X를 구현하세요" 같은 placeholder만 들어 있음
이런 패턴은 *문법은 통과해도 사용자가 원한 결과물이 아닙니다*. issues에
"빈 깡통: <함수명> 본문이 stub뿐" 처럼 *구체적인 위치*를 적으세요.
평가 라벨:
- "pass" : 위 모든 기준 통과
- "warn" : 일부 약점이 있지만 사용자가 받아 볼 만함 (issues에 적기)
- "fail" : 핵심 오류·누락 또는 *빈 깡통* 발견 — 재작업 필요 (issues에 적기)
⚠️ 반드시 아래 JSON 한 번만. 다른 텍스트(설명·코드펜스) 일체 금지.
{"verdict":"pass"|"warn"|"fail","issues":["<이슈1>","<이슈2>"],"summary":"한 줄(30자 이내)"}`;
function _buildUserMessage(input: VerifyInput): string {
const lines: string[] = [];
if (input.contractBlock) {
lines.push(input.contractBlock);
lines.push('');
}
lines.push(`[검증 대상 에이전트] ${input.agentName}`);
lines.push('');
lines.push('[task]');
lines.push(input.task.slice(0, 2000));
lines.push('');
lines.push('[해당 에이전트의 응답]');
lines.push((input.response || '').slice(0, 4000));
lines.push('');
lines.push('점검 JSON만 출력:');
return lines.join('\n');
}
function _parseVerdictJson(raw: string): { verdict: VerifyVerdict; issues: string[]; summary: string } | null {
if (!raw || !raw.trim()) return null;
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
const stage1 = (fenced ? fenced[1] : raw).trim();
const tryParse = (s: string) => {
try {
const obj = JSON.parse(s) as Record<string, unknown>;
const v = typeof obj.verdict === 'string' ? obj.verdict.toLowerCase().trim() : '';
if (v !== 'pass' && v !== 'warn' && v !== 'fail') return null;
const issues = Array.isArray(obj.issues)
? obj.issues.filter((x): x is string => typeof x === 'string' && x.trim().length > 0)
.map((x) => x.trim()).slice(0, 5)
: [];
const summary = typeof obj.summary === 'string' ? obj.summary.trim() : '';
return { verdict: v as VerifyVerdict, issues, summary };
} catch {
return null;
}
};
const direct = tryParse(stage1);
if (direct) return direct;
const m = stage1.match(/\{[\s\S]*\}/);
if (m) {
const balanced = tryParse(m[0]);
if (balanced) return balanced;
}
// 최후의 fallback: 정규식으로 verdict만이라도.
const verdictMatch = stage1.match(/"verdict"\s*:\s*"(pass|warn|fail)"/i);
if (verdictMatch) {
return {
verdict: verdictMatch[1].toLowerCase() as VerifyVerdict,
issues: [],
summary: '',
};
}
return null;
}
/**
* 검증 한 회차. 호출 실패 / JSON 파싱 실패는 *통과로 간주* (verifierError=true
* 표시). 검증 layer가 작업 진행을 막지 않게 — 본래 의도가 안전망이지 통제관문이
* 아니므로.
*/
export async function verifyResponse(
ai: IAIService,
input: VerifyInput,
): Promise<VerifyResult> {
let raw = '';
try {
const result = await ai.chat({
system: SYSTEM_PROMPT,
user: _buildUserMessage(input),
model: input.model,
});
raw = result.content || '';
} catch (e: any) {
logError('selfReflectorVerifier: call failed; treating as pass.', { error: e?.message ?? String(e) });
return { verdict: 'pass', issues: [], summary: '검증 실패 — 원본 유지', verifierError: true };
}
const parsed = _parseVerdictJson(raw);
if (!parsed) {
logInfo('selfReflectorVerifier: parse failed; treating as pass.', { rawHead: raw.slice(0, 100) });
return { verdict: 'pass', issues: [], summary: '검증 응답 파싱 실패 — 원본 유지', verifierError: true };
}
return {
verdict: parsed.verdict,
issues: parsed.issues,
summary: parsed.summary || `verdict: ${parsed.verdict}`,
};
}
/**
* 검증 결과를 retry 시 사용할 prompt prefix로 직렬화. 호출자가 task 앞에
* prepend 해 specialist를 1회 더 호출한다.
*/
export function formatIssuesForRetry(issues: string[]): string {
if (!issues.length) return '';
const lines: string[] = [];
lines.push('[외부 감리 지적 — 반드시 반영]');
for (const i of issues) lines.push(`- ${i}`);
return lines.join('\n');
}