2afd1ac589
신뢰성 코어 (P1~P2): - Requirement Graph: 업무 유형(회의록/시장조사/업무조사/일정) 필수 요소 주입 + 커버리지 hook - Confidence Engine(0~100 결정론적) / Escalation Engine(검토 요청) / Epistemic Guard(모름·추정·확실 3분류) - Provenance: citationTrace 에 출처 수정일·오래됨 경고 - Critic Loop: 문제 신호 turn 만 LLM 검수 1회 + 보완 카드 성장 루프 (P3): - Gap Detector(Requirement-Knowledge) / Need Engine(30/25/20/15/10 공식) / Knowledge Inventory - Learning Queue(proposed 전용 병합 — 승인은 사람만) / Decision Journal / Reflection 기록 - 반복 누락 요소(3회+)는 다음 turn 체크리스트에 자동 강조 (T5 루프) 지식 운영 (P4) + 기억 (P5) + 학습 실행 (P6): - Knowledge Validation + Belief Revision(중복 reject·충돌 시 update/add 권고) - Knowledge Decay(분야별 반감기 감사) / Knowledge Debt(blocked x impact) - Organizational Memory(.astra/organization.md 상시 주입) - Research Agent(approved 큐 -> 조사 브리프+추정 라벨 초안+Validation 게이트 -> proposals/) - Skill Score(전/후반 추세) + Success Pattern DB(전요소충족+확신도90+ 자동 적재) 병렬 트랙: - 캘린더 충돌 게이트: conflictCheck + 구조화 이벤트 캐시 + create_calendar_event 차단(force 는 사용자 승인 후) - Task Eval Harness: 회의록 골든셋 자동 채점 명령 + 성장 리포트/학습 큐/노후 점검 명령 신규 모듈 17종(src/intelligence/), VS Code 명령 5종, 설정 11종, 테스트 +89건(전체 508 통과). 설계 문서: docs/SELF_EVOLVING_OS_MASTER_PLAN.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
175 lines
7.1 KiB
TypeScript
175 lines
7.1 KiB
TypeScript
/**
|
|
* Critic Agent + Debate Loop (v1) — 제출된 업무 산출물의 LLM 검수.
|
|
*
|
|
* Self-Evolving OS 마스터 플랜 Phase 1 / Track 2-3. 신뢰 조건 T3 의 LLM 계층:
|
|
* Requirement Coverage(결정론적, 정규식) 가 "요소가 *언급* 됐는가" 만 보면,
|
|
* Critic 은 "내용이 *충실* 한가 + 결정/미결 구분이 맞는가 + 근거 없는 단정이
|
|
* 없는가" 를 본다.
|
|
*
|
|
* Debate Loop 원형은 작성→비판→재작성→재검토지만, 로컬 Gemma 의 latency 비용
|
|
* 때문에 v1 은 *조건부 1-pass 검수* — 결정론적 검사(커버리지 누락 또는 확신도
|
|
* <70)가 문제를 신호할 때만 Critic LLM 1회 호출, 결과를 답변 아래 보완 카드로
|
|
* 표시. 전면 다회전 debate 는 config knob(maxRounds) 만 준비해 두고 후속 증분.
|
|
*
|
|
* 모든 LLM 의존은 주입(critique caller) — 모듈 자체는 순수, 테스트 가능.
|
|
*/
|
|
|
|
import type { TaskRequirement } from './requirementGraph';
|
|
|
|
export interface CriticIssue {
|
|
severity: 'major' | 'minor';
|
|
description: string;
|
|
}
|
|
|
|
export interface CritiqueResult {
|
|
/** true = 검수 통과 (보완 불필요). */
|
|
pass: boolean;
|
|
issues: CriticIssue[];
|
|
/** 누락 요소를 보완하는 추가 섹션 제안 (Critic 이 생성 가능했을 때만). */
|
|
supplement: string;
|
|
/** 디버그용 원문 (파싱 실패 분석). */
|
|
raw?: string;
|
|
}
|
|
|
|
/** 주입형 LLM caller — agent.ts 의 callNonStreaming 또는 평가 하니스의 단순 호출. */
|
|
export type CritiqueLlmCall = (system: string, user: string, maxTokens: number) => Promise<string>;
|
|
|
|
export interface CriticOptions {
|
|
/** 검수 대상 초안 최대 길이 (chars) — 초과분 잘라서 전달. 기본 12000. */
|
|
maxDraftChars: number;
|
|
/** Critic 응답 max tokens. 기본 700. */
|
|
maxTokens: number;
|
|
}
|
|
|
|
export const DEFAULT_CRITIC_OPTIONS: CriticOptions = {
|
|
maxDraftChars: 12000,
|
|
maxTokens: 700,
|
|
};
|
|
|
|
export function buildCritiquePrompt(
|
|
userPrompt: string,
|
|
draft: string,
|
|
requirement: TaskRequirement | null,
|
|
missingLabels: string[],
|
|
opts: CriticOptions = DEFAULT_CRITIC_OPTIONS,
|
|
): { system: string; user: string } {
|
|
const reqSection = requirement
|
|
? [
|
|
`업무 유형: ${requirement.label}`,
|
|
'필수 요소:',
|
|
...requirement.elements.map((e) => `- ${e.label}: ${e.hint}`),
|
|
].join('\n')
|
|
: '업무 유형: (미분류)';
|
|
const missingSection = missingLabels.length > 0
|
|
? `결정론적 검사가 누락 가능성을 표시한 요소: ${missingLabels.join(', ')}`
|
|
: '결정론적 검사 통과 (참고용 재확인)';
|
|
|
|
const system = [
|
|
'너는 업무 산출물 검수자(Critic)다. 동료가 작성한 초안을 비판적으로 검토한다.',
|
|
'검수 기준:',
|
|
'1. 필수 요소가 *내용으로* 충실한가 (단어만 등장 ≠ 충족).',
|
|
'2. 결정사항과 미결(논의만 된 것)이 구분돼 있는가.',
|
|
'3. 근거 없는 단정·지어낸 수치/이름/날짜가 없는가. 원문에 없는 내용 발견 시 major.',
|
|
'4. 정보가 없는 항목은 "(확인 필요)" 로 솔직히 표시했는가.',
|
|
'',
|
|
'반드시 아래 JSON *만* 출력 (다른 텍스트 금지):',
|
|
'{"pass": true|false, "issues": [{"severity": "major"|"minor", "description": "..."}], "supplement": "누락 보완 텍스트 (보완 불가능하면 빈 문자열)"}',
|
|
'supplement 는 초안에 *실제로 추가할 수 있는* 마크다운 섹션만. 원문에 없는 내용을 지어내 보완하는 것은 금지 — 그 경우 "(확인 필요)" 항목으로 작성.',
|
|
].join('\n');
|
|
|
|
const draftCapped = draft.length > opts.maxDraftChars ? draft.slice(0, opts.maxDraftChars) + '\n…(잘림)' : draft;
|
|
const user = [
|
|
`[원래 요청]\n${userPrompt}`,
|
|
`[검수 기준 컨텍스트]\n${reqSection}\n${missingSection}`,
|
|
`[검수 대상 초안]\n${draftCapped}`,
|
|
].join('\n\n');
|
|
|
|
return { system, user };
|
|
}
|
|
|
|
/** Critic LLM 응답에서 JSON 추출 — 코드펜스/잡설 섞여도 첫 균형 {} 블록을 파싱. */
|
|
export function parseCritique(raw: string): CritiqueResult | null {
|
|
if (!raw || !raw.trim()) return null;
|
|
const start = raw.indexOf('{');
|
|
if (start === -1) return null;
|
|
// 균형 괄호 스캔 — 중첩 객체(issues 배열 내부) 안전.
|
|
let depth = 0;
|
|
let end = -1;
|
|
let inString = false;
|
|
let escaped = false;
|
|
for (let i = start; i < raw.length; i++) {
|
|
const ch = raw[i];
|
|
if (escaped) { escaped = false; continue; }
|
|
if (ch === '\\') { escaped = true; continue; }
|
|
if (ch === '"') { inString = !inString; continue; }
|
|
if (inString) continue;
|
|
if (ch === '{') depth++;
|
|
else if (ch === '}') {
|
|
depth--;
|
|
if (depth === 0) { end = i; break; }
|
|
}
|
|
}
|
|
if (end === -1) return null;
|
|
try {
|
|
const obj = JSON.parse(raw.slice(start, end + 1));
|
|
const issues: CriticIssue[] = Array.isArray(obj.issues)
|
|
? obj.issues
|
|
.filter((i: any) => i && typeof i.description === 'string')
|
|
.map((i: any) => ({
|
|
severity: i.severity === 'major' ? 'major' : 'minor',
|
|
description: String(i.description).slice(0, 500),
|
|
}))
|
|
: [];
|
|
return {
|
|
pass: obj.pass === true && issues.length === 0,
|
|
issues,
|
|
supplement: typeof obj.supplement === 'string' ? obj.supplement.slice(0, 4000) : '',
|
|
raw: raw.slice(0, 200),
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Critic 검수 1회 실행. LLM 실패/파싱 실패 시 null — 호출자(hook)는 silent skip
|
|
* (검수 실패가 main turn 을 막지 않도록).
|
|
*/
|
|
export async function runCriticReview(params: {
|
|
userPrompt: string;
|
|
draft: string;
|
|
requirement: TaskRequirement | null;
|
|
missingLabels: string[];
|
|
callLlm: CritiqueLlmCall;
|
|
options?: Partial<CriticOptions>;
|
|
}): Promise<CritiqueResult | null> {
|
|
const opts: CriticOptions = { ...DEFAULT_CRITIC_OPTIONS, ...(params.options || {}) };
|
|
const { system, user } = buildCritiquePrompt(params.userPrompt, params.draft, params.requirement, params.missingLabels, opts);
|
|
let raw: string;
|
|
try {
|
|
raw = await params.callLlm(system, user, opts.maxTokens);
|
|
} catch {
|
|
return null;
|
|
}
|
|
return parseCritique(raw);
|
|
}
|
|
|
|
/** 검수 결과 footer — pass 면 빈 문자열 (노이즈 방지). */
|
|
export function formatCriticFooter(critique: CritiqueResult): string {
|
|
if (critique.pass) return '';
|
|
const lines: string[] = [];
|
|
lines.push('\n\n> 🔁 **검수 (Critic)** — 초안에서 발견된 문제:');
|
|
for (const issue of critique.issues.slice(0, 6)) {
|
|
const tag = issue.severity === 'major' ? '🔴' : '🟡';
|
|
lines.push(`> - ${tag} ${issue.description}`);
|
|
}
|
|
if (critique.supplement && critique.supplement.trim()) {
|
|
lines.push('>');
|
|
lines.push('> **보완 제안:**');
|
|
for (const l of critique.supplement.trim().split('\n')) {
|
|
lines.push(`> ${l}`);
|
|
}
|
|
}
|
|
return lines.join('\n');
|
|
}
|