feat(engine): implement self-reflection (reflector) stage in multi-agent pipeline

- Added ReflectorAgent for meta-cognition and critical review between Research and Writing
- Updated WriterAgent to explicitly address reflection critiques
- Introduced 'g1nation.enableReflection' configuration setting
- Added comprehensive integration tests for the self-reflection stage
- Documented design decisions in ADR-0010 and related discussion records
This commit is contained in:
g1nation
2026-05-14 01:47:28 +09:00
parent e075779635
commit 8da9532ca1
18 changed files with 610 additions and 124 deletions
+14 -4
View File
@@ -1,5 +1,6 @@
import { PlannerAgent, ResearcherAgent, WriterAgent } from './factory';
import { AgentEngine, PipelineStage } from '../lib/engine';
import { PlannerAgent, ResearcherAgent, ReflectorAgent, WriterAgent } from './factory';
import { AgentEngine, PipelineStage, AgentExecuteOptions } from '../lib/engine';
import { getConfig } from '../config';
export class AgentWorkflowManager {
/**
@@ -15,9 +16,16 @@ export class AgentWorkflowManager {
const planner = new PlannerAgent(modelName);
const researcher = new ResearcherAgent(modelName);
const writer = new WriterAgent(modelName);
const engine = new AgentEngine(planner, researcher, writer);
// [Self-Reflection] 설정으로 비활성화하지 않은 경우에만 Reflector를 주입.
const enableReflection = getConfig().enableReflection !== false;
const reflector = enableReflection ? new ReflectorAgent(modelName) : undefined;
const engine = new AgentEngine(planner, researcher, writer, reflector);
const missionId = `mission_${Date.now()}`;
const runOptions: AgentExecuteOptions = {
config: { enableReflection }
};
try {
return await engine.runMission(
missionId,
@@ -26,7 +34,8 @@ export class AgentWorkflowManager {
signal,
(stage: PipelineStage, message: string) => {
onProgress(this.mapStageToUI(stage), message);
}
},
runOptions
);
} catch (error: any) {
if (error.name === 'AbortError' || error.message.includes('cancelled')) {
@@ -41,6 +50,7 @@ export class AgentWorkflowManager {
idle: '대기',
planner: 'Planner',
researcher: 'Researcher',
reflector: 'Reflector',
writer: 'Writer',
completed: '완료',
error: '오류'
+57 -4
View File
@@ -134,12 +134,13 @@ Your mission is to extract, filter, and synthesize critical data based on a stra
}
export class WriterAgent extends BaseAgent {
private readonly persona = `You are the [Lead Synthesis Writer & Editor].
private readonly persona = `You are the [Lead Synthesis Writer & Editor].
Your goal is to produce a state-of-the-art final report that wows the user.
- TONE: Authoritative yet accessible. Professional developer/consultant style.
- STRUCTURE: Use an executive summary, detailed analysis sections, and a "Final Recommendation" block.
- LANGUAGE: Always respond in the user's language (KOREAN).
- POLISHING: Ensure logical flow between sections. Make it look like a premium report.`;
- POLISHING: Ensure logical flow between sections. Make it look like a premium report.
- SELF-CORRECTION: When a [REFLECTION CRITIQUE] block is provided, you MUST address each listed gap, contradiction, or missing-evidence item explicitly before producing the final report. Do not silently ignore the critique.`;
async execute(input: string, originalRequest?: string, signal?: AbortSignal, options?: AgentExecuteOptions): Promise<string> {
// [Astra v4.0] Advisor 모드 처리
@@ -154,13 +155,65 @@ Analyze the provided report and suggest 3 high-impact next actions for the user.
// Fix 3: Trim input if it's too long (Basic Context Diet)
const trimmedData = input.length > 8000 ? input.substring(0, 8000) + '... [Data Trimmed for Performance]' : input;
const policy = options?.context || '';
const reflection = options?.priorResults?.reflection;
// Reflector 결과가 있으면 별도 블록으로 주입. 길이 4000자 cap (Writer 입력 비대화 방지).
const reflectionBlock = reflection && reflection.trim().length > 0
? `\n5. [REFLECTION CRITIQUE — must be addressed]:\n${reflection.length > 4000 ? reflection.substring(0, 4000) + '... [Critique Trimmed]' : reflection}`
: '';
const wrappedInput = `### SYSTEM INSTRUCTION: FINAL SYNTHESIS
1. Gathered Research Data: ${trimmedData}
2. User's Original Objective: ${originalRequest}
3. Applied Knowledge & Filtering Policy: ${policy}
4. Mission: Write the definitive final report in KOREAN.`;
4. Mission: Write the definitive final report in KOREAN.${reflectionBlock}`;
return this.callLLM(this.persona, wrappedInput, signal);
}
}
export class ReflectorAgent extends BaseAgent {
private readonly persona = `You are the [Internal Critic & Self-Reflection Officer].
Your sole role is META-COGNITION: stress-test the plan and the research output BEFORE the Writer commits to a final report.
- POSTURE: Skeptical, rigorous, blunt. You are looking for what is WRONG, not what is right.
- DO NOT: rewrite the report, add new content, or speculate beyond the evidence provided.
- DO: surface gaps, unsupported claims, contradictions, drift from the original objective, and missing perspectives.
- OUTPUT STRICTLY in this Markdown shape (Korean):
## 🧭 Alignment with Objective
- <원래 요청 대비 일치/이탈 평가>
## 🕳 Gaps & Missing Evidence
- <plan에는 있지만 research가 다루지 않은 항목>
## ⚖️ Contradictions / Conflicts
- <research 내부 또는 brain context와의 모순; 없으면 "발견되지 않음">
## 🚨 Unsupported / Weak Claims
- <근거가 빈약하거나 일반화된 진술>
## ✅ Guidance for Writer
- <Writer가 최종 리포트에서 반드시 보정해야 할 3~5개 구체 지시>
- CONSTRAINT: 최대 500단어. 새 지식을 만들지 말고, 제공된 자료에서만 판단할 것.`;
async execute(input: string, _context?: string, signal?: AbortSignal, options?: AgentExecuteOptions): Promise<string> {
const plan = options?.priorResults?.plan || '(plan unavailable)';
const research = input;
const originalPrompt = options?.priorResults?.originalPrompt || '(original prompt unavailable)';
const brainContext = options?.context || '';
// Reflector 는 중간 단계이므로 비대한 입력을 방지하기 위해 각 섹션을 cap.
const cap = (s: string, n: number) => s.length > n ? s.substring(0, n) + '... [trimmed]' : s;
const wrappedInput = `### SYSTEM INSTRUCTION: SELF-REFLECTION PASS
1. Original User Objective:
${cap(originalPrompt, 1500)}
2. Planner Blueprint:
${cap(plan, 3000)}
3. Researcher Output (to be critiqued):
${cap(research, 5000)}
4. Knowledge / Brain Context (for cross-check only — do not invent beyond this):
${cap(brainContext, 2000)}
5. Mission: Run a single rigorous reflection pass and output the structured critique exactly as specified by your persona.`;
return this.callLLM(this.persona, wrappedInput, signal);
}
}
+7
View File
@@ -67,6 +67,12 @@ export interface IAgentConfig {
* Per-agent overrides live in AgentKnowledgeEntry.secondBrainWeight and win.
*/
knowledgeMixSecondBrainWeight: number;
/**
* [Self-Reflection] Researcher와 Writer 사이에 메타인지 단계(Reflector)를 삽입할지 여부.
* true(기본): Reflector가 plan/research를 비판적으로 검토한 critique을 Writer에 주입.
* false: 기존 3단계(Planner→Researcher→Writer) 그대로 — 1 LLM 호출 절약 (저성능 모델/저지연 우선 시).
*/
enableReflection: boolean;
}
// ─── 경로 정규화 유틸리티 ───
@@ -153,6 +159,7 @@ export function getConfig(): IAgentConfig {
knowledgeMixSecondBrainWeight: Math.max(0, Math.min(100, Math.round(
cfg.get<number>('knowledgeMix.secondBrainWeight', 50)
))),
enableReflection: cfg.get<boolean>('enableReflection', true),
};
}
+39 -8
View File
@@ -49,7 +49,7 @@ export interface IAgent {
/**
* 파이프라인 단계 상태 정의
*/
export type PipelineStage = 'idle' | 'planner' | 'researcher' | 'writer' | 'completed' | 'error';
export type PipelineStage = 'idle' | 'planner' | 'researcher' | 'reflector' | 'writer' | 'completed' | 'error';
/**
* 감사(Audit) 이력에 기록되는 단일 상태 전환 엔트리.
@@ -449,7 +449,9 @@ export class AgentEngine {
constructor(
private readonly planner: IAgent,
private readonly researcher: IAgent,
private readonly writer: IAgent
private readonly writer: IAgent,
// [Self-Reflection] Researcher와 Writer 사이에 주입되는 메타인지 노드. 미주입 시 기존 3단계 파이프라인을 그대로 유지.
private readonly reflector?: IAgent
) {}
/**
@@ -528,16 +530,45 @@ export class AgentEngine {
// [Structural Fix] 점수가 낮을수록 더 상세한 근거를 요구(comprehensive)하도록 로직 역전
const writerLevel: AbstractionLevel = researchScore < 65 ? 'comprehensive' : 'balanced';
// --- Phase 3.5: Reflector (Self-Reflection) ---
// Reflector가 주입되어 있고 옵션에서 명시적으로 끄지 않은 경우에만 실행한다.
// 실패해도 파이프라인을 막지 않는다(soft-fail): Reflector는 품질 보강이지 필수 게이트가 아님.
let reflection = '';
const reflectionDisabled = options?.config?.enableReflection === false;
if (this.reflector && !reflectionDisabled) {
try {
reflection = await this.executeStep(
state, 'reflector', '중간 산출물 자기검증 중...',
() => this.resilientExecute(state, this.reflector!, 'Reflector', research, brainContext, signal, onProgress, {
...options,
context: brainContext,
signal,
config: { ...options?.config, role: 'reflector', isSamePrompt: true },
priorResults: { plan, originalPrompt: prompt, ...options?.priorResults },
abstractionLevel: 'balanced'
}),
// [Cache namespace] Writer와 동일한 (research, prompt) 페어를 쓰면 CacheManager가
// Writer 호출 시 reflector 결과를 그대로 반환해버린다. 단계명을 prefix로 분리.
`reflector::${research}`, prompt, signal, onProgress
);
} catch (reflErr: any) {
// Reflector 실패는 치명적이지 않다. 감사 이력에만 남기고 빈 reflection으로 Writer를 진행시킨다.
if (reflErr?.name === 'AbortError') throw reflErr;
logError(`[AgentEngine] Reflector soft-fail — Writer 계속 진행: ${reflErr?.message || reflErr}`);
reflection = '';
}
}
// --- Phase 4: Writer ---
const finalReport = await this.executeStep(
state, 'writer', '최종 리포트 작성 및 편집 중...',
() => this.resilientExecute(state, this.writer, 'Writer', research, prompt, signal, onProgress, {
() => this.resilientExecute(state, this.writer, 'Writer', research, prompt, signal, onProgress, {
...options,
context: brainContext,
signal,
config: { role: 'writer', allowFallback: true, isSamePrompt: true, ...options?.config },
priorResults: { plan, writerPrep, previousValidData: state.getResult('finalReport'), ...options?.priorResults },
abstractionLevel: writerLevel
context: brainContext,
signal,
config: { role: 'writer', allowFallback: true, isSamePrompt: true, ...options?.config },
priorResults: { plan, writerPrep, reflection, previousValidData: state.getResult('finalReport'), ...options?.priorResults },
abstractionLevel: writerLevel
}),
research, prompt, signal, onProgress
);