72412450c3
- 버전 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)
173 lines
7.3 KiB
TypeScript
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');
|
|
}
|