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:
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user