초안 에이전트 설계 패턴 - 리플렉션

ailangchainlanggraphtypescriptreflectionagents
By sko X opus 4.19/20/20258 min read

LangChain, LangGraph, TypeScript를 사용하여 Vercel의 서버리스 플랫폼에서 자체 개선 AI 에이전트를 구현하는 방법을 알아봅니다.

멘탈 모델: 코드 리뷰 비유

리플렉션 패턴은 풀 리퀘스트 리뷰 프로세스처럼 생각할 수 있습니다. 코드를 제출하면 리뷰어(비평가 에이전트)가 이를 검토하고 피드백을 제공하며, 당신(프로듀서 에이전트)은 그 피드백을 바탕으로 수정합니다. 이 사이클은 코드가 품질 기준을 충족하거나 머지 기한에 도달할 때까지 계속됩니다. AI 에이전트에서 이와 동일한 원리로 구조화된 피드백 루프를 통해 반복적인 자체 개선이 가능합니다. 코드 리뷰가 버그를 찾고 품질을 향상시키는 것처럼, 리플렉션 패턴은 에이전트가 자신의 실수를 식별하고 수정하는 데 도움을 주어 더 정확하고 신뢰할 수 있는 출력을 만들어냅니다.

기본 예제: 자체 반성 에이전트

1. 리플렉션 상태 그래프 생성

// lib/agents/reflection-basic.ts
import { StateGraph, END, START, Annotation } from "@langchain/langgraph";
import { BaseMessage, HumanMessage, AIMessage } from "@langchain/core/messages";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { take } from "es-toolkit";

const ReflectionState = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (x, y) => x.concat(y),
  }),
  reflectionCount: Annotation<number>({
    reducer: (x, y) => y ?? x,
    default: () => 0,
  }),
});

const model = new ChatGoogleGenerativeAI({
  modelName: "gemini-2.5-flash",
  temperature: 0.7,
});

const generatePrompt = ChatPromptTemplate.fromMessages([
  ["system", "You are an expert essay writer. Generate a response to the user's request."],
  new MessagesPlaceholder("messages"),
]);

const reflectPrompt = ChatPromptTemplate.fromMessages([
  ["system", `You are a writing critic. Review the essay and provide specific, actionable feedback.
   If the essay is excellent, respond with only "APPROVED".
   Otherwise, list 2-3 specific improvements needed.`],
  new MessagesPlaceholder("messages"),
]);

메시지 히스토리와 리플렉션 카운터를 가진 기본 상태 구조를 생성합니다. 상태는 대화와 몇 번의 리플렉션 사이클이 발생했는지를 모두 추적합니다.

2. 생성 및 반성 노드 구현

// lib/agents/reflection-basic.ts (계속)
async function generateNode(state: typeof ReflectionState.State) {
  const chain = generatePrompt.pipe(model);
  const response = await chain.invoke({
    messages: state.messages
  });

  return {
    messages: [response],
  };
}

async function reflectNode(state: typeof ReflectionState.State) {
  const chain = reflectPrompt.pipe(model);
  const lastMessages = take(state.messages, -2); // 마지막 사용자 메시지와 AI 응답 가져오기

  const critique = await chain.invoke({
    messages: lastMessages
  });

  return {
    messages: [new HumanMessage(`Feedback: ${critique.content}`)],
    reflectionCount: state.reflectionCount + 1,
  };
}

function shouldContinue(state: typeof ReflectionState.State) {
  const lastMessage = state.messages[state.messages.length - 1];

  // 승인되었거나 최대 반성 횟수에 도달하면 중지
  if (lastMessage.content?.toString().includes("APPROVED") ||
      state.reflectionCount >= 3) {
    return END;
  }

  return "reflect";
}

생성 노드는 초기 콘텐츠를 만들고 반성 노드는 이를 비평합니다. shouldContinue 함수는 품질 승인 또는 반복 제한에 따른 중지 로직을 구현합니다.

3. 워크플로우 그래프 구축

// lib/agents/reflection-basic.ts (계속)
export function createReflectionAgent() {
  const workflow = new StateGraph(ReflectionState)
    .addNode("generate", generateNode)
    .addNode("reflect", reflectNode)
    .addEdge(START, "generate")
    .addConditionalEdges("generate", shouldContinue, {
      reflect: "reflect",
      [END]: END,
    })
    .addEdge("reflect", "generate");

  return workflow.compile();
}

리플렉션 루프 흐름을 제어하는 조건부 엣지로 워크플로우를 조립합니다.

4. API 라우트 생성

// app/api/reflection/route.ts
import { createReflectionAgent } from "@/lib/agents/reflection-basic";
import { HumanMessage } from "@langchain/core/messages";
import { NextResponse } from "next/server";

export const runtime = "nodejs";
export const maxDuration = 60;

export async function POST(req: Request) {
  try {
    const { prompt } = await req.json();
    const agent = createReflectionAgent();

    const result = await agent.invoke({
      messages: [new HumanMessage(prompt)],
      reflectionCount: 0,
    });

    // 최종 정제된 출력 추출
    const finalOutput = result.messages
      .filter((m: any) => m._getType() === "ai")
      .pop()?.content;

    return NextResponse.json({
      output: finalOutput,
      iterations: result.reflectionCount,
      messages: result.messages.map((m: any) => ({
        type: m._getType(),
        content: m.content,
      })),
    });
  } catch (error) {
    console.error("Reflection error:", error);
    return NextResponse.json(
      { error: "Reflection process failed" },
      { status: 500 }
    );
  }
}

HTTP 요청을 처리하고 적절한 오류 처리로 리플렉션 에이전트 실행을 관리합니다.

5. 프론트엔드 컴포넌트 구축

// components/ReflectionDemo.tsx
"use client";

import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { groupBy } from "es-toolkit";

interface ReflectionResult {
  output: string;
  iterations: number;
  messages: Array<{ type: string; content: string }>;
}

export default function ReflectionDemo() {
  const [prompt, setPrompt] = useState("");
  const [showProcess, setShowProcess] = useState(false);

  const reflection = useMutation({
    mutationFn: async (userPrompt: string) => {
      const res = await fetch("/api/reflection", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ prompt: userPrompt }),
      });

      if (!res.ok) throw new Error("Reflection failed");
      return res.json() as Promise<ReflectionResult>;
    },
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (prompt.trim()) {
      reflection.mutate(prompt);
    }
  };

  // 표시를 위해 메시지를 반복별로 그룹화
  const messagesByIteration = reflection.data?.messages
    ? groupBy(reflection.data.messages, (_, index) =>
        Math.floor(index / 2).toString()
      )
    : {};

  return (
    <div className="card w-full bg-base-100 shadow-xl">
      <div className="card-body">
        <h2 className="card-title">리플렉션 에이전트 데모</h2>

        <form onSubmit={handleSubmit}>
          <textarea
            className="textarea textarea-bordered w-full"
            placeholder="작성 프롬프트를 입력하세요..."
            value={prompt}
            onChange={(e) => setPrompt(e.target.value)}
            rows={3}
            disabled={reflection.isPending}
          />

          <div className="card-actions justify-between mt-4">
            <label className="label cursor-pointer">
              <span className="label-text mr-2">프로세스 표시</span>
              <input
                type="checkbox"
                className="checkbox"
                checked={showProcess}
                onChange={(e) => setShowProcess(e.target.checked)}
              />
            </label>

            <button
              type="submit"
              className="btn btn-primary"
              disabled={reflection.isPending || !prompt.trim()}
            >
              {reflection.isPending ? (
                <>
                  <span className="loading loading-spinner"></span>
                  반성 중...
                </>
              ) : "생성"}
            </button>
          </div>
        </form>

        {reflection.data && (
          <div className="mt-6 space-y-4">
            <div className="stats shadow">
              <div className="stat">
                <div className="stat-title">리플렉션 반복 횟수</div>
                <div className="stat-value">{reflection.data.iterations}</div>
              </div>
            </div>

            {showProcess && (
              <div className="space-y-4">
                {Object.entries(messagesByIteration).map(([iter, msgs]) => (
                  <div key={iter} className="collapse collapse-arrow bg-base-200">
                    <input type="checkbox" />
                    <div className="collapse-title font-medium">
                      반복 {parseInt(iter) + 1}
                    </div>
                    <div className="collapse-content">
                      {msgs.map((msg, idx) => (
                        <div key={idx} className={`chat chat-${msg.type === "ai" ? "end" : "start"}`}>
                          <div className={`chat-bubble ${msg.type === "human" ? "chat-bubble-primary" : ""}`}>
                            {msg.content}
                          </div>
                        </div>
                      ))}
                    </div>
                  </div>
                ))}
              </div>
            )}

            <div className="divider">최종 출력</div>
            <div className="prose max-w-none">
              {reflection.data.output}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

접을 수 있는 반복 뷰로 리플렉션 프로세스를 시연하는 대화형 UI를 제공합니다.

고급 예제: 스트리밍이 있는 프로듀서-비평가 아키텍처

1. 프로듀서 및 비평가 에이전트 정의

// lib/agents/producer-critic.ts
import { StateGraph, END, START, Annotation } from "@langchain/langgraph";
import { BaseMessage, HumanMessage, AIMessage } from "@langchain/core/messages";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { z } from "zod";
import { minBy, maxBy } from "es-toolkit";
import { StructuredOutputParser } from "@langchain/core/output_parsers";

const CritiqueSchema = z.object({
  score: z.number().min(0).max(100),
  approved: z.boolean(),
  issues: z.array(z.object({
    category: z.enum(["accuracy", "clarity", "completeness", "style"]),
    description: z.string(),
    severity: z.enum(["minor", "major", "critical"]),
  })),
  suggestions: z.array(z.string()),
});

const ProducerCriticState = Annotation.Root({
  task: Annotation<string>(),
  drafts: Annotation<string[]>({
    reducer: (x, y) => x.concat(y),
    default: () => [],
  }),
  critiques: Annotation<typeof CritiqueSchema._type[]>({
    reducer: (x, y) => x.concat(y),
    default: () => [],
  }),
  iteration: Annotation<number>({
    reducer: (_, y) => y,
    default: () => 0,
  }),
});

const producer = new ChatGoogleGenerativeAI({
  modelName: "gemini-2.5-pro",
  temperature: 0.7,
  maxOutputTokens: 2048,
});

const critic = new ChatGoogleGenerativeAI({
  modelName: "gemini-2.5-flash",
  temperature: 0.3,
});

구조화된 비평 출력 스키마를 사용하여 프로듀서와 비평가 역할을 위한 별도의 모델을 정의합니다.

2. 컨텍스트가 있는 프로듀서 노드 구현

// lib/agents/producer-critic.ts (계속)
async function producerNode(state: typeof ProducerCriticState.State) {
  const lastCritique = state.critiques[state.critiques.length - 1];

  let prompt = `작업: ${state.task}`;

  if (lastCritique) {
    const criticalIssues = lastCritique.issues
      .filter(i => i.severity === "critical")
      .map(i => `- ${i.description}`)
      .join("\n");

    prompt += `\n\n이전 초안이 피드백을 받았습니다. 해결해야 할 중요한 문제:\n${criticalIssues}`;
    prompt += `\n\n개선 제안:\n${lastCritique.suggestions.join("\n")}`;
    prompt += `\n\n모든 피드백을 해결한 개선된 버전을 생성하세요.`;
  } else {
    prompt += "\n\n고품질 응답을 생성하세요.";
  }

  const response = await producer.invoke(prompt);

  return {
    drafts: [response.content as string],
    iteration: state.iteration + 1,
  };
}

프로듀서 노드는 이전 비평 피드백을 통합하여 개선된 초안을 생성합니다.

3. 구조화된 출력이 있는 비평가 노드 구현

// lib/agents/producer-critic.ts (계속)
async function criticNode(state: typeof ProducerCriticState.State) {
  const latestDraft = state.drafts[state.drafts.length - 1];
  const parser = StructuredOutputParser.fromZodSchema(CritiqueSchema);

  const prompt = `당신은 전문 비평가입니다. "${state.task}" 작업에 대한 이 응답을 평가하세요

평가할 응답:
${latestDraft}

이 JSON 스키마에 따라 상세한 비평을 제공하세요:
${parser.getFormatInstructions()}

90점 이상은 응답이 우수하고 승인되었음을 의미합니다.
피드백은 철저하면서도 건설적이어야 합니다.`;

  const response = await critic.invoke(prompt);
  const critique = await parser.parse(response.content as string);

  return {
    critiques: [critique],
  };
}

비평가는 점수, 문제 분류 및 개선 제안을 포함한 구조화된 피드백을 제공합니다.

4. 고급 라우팅 로직

// lib/agents/producer-critic.ts (계속)
function routingLogic(state: typeof ProducerCriticState.State) {
  const lastCritique = state.critiques[state.critiques.length - 1];

  // 조기 종료 조건
  if (!lastCritique) return "critic";

  if (lastCritique.approved || state.iteration >= 5) {
    return END;
  }

  // 비평 심각도에 따른 적응형 라우팅
  const criticalCount = lastCritique.issues.filter(i => i.severity === "critical").length;

  if (criticalCount > 2 && state.iteration < 3) {
    // 대대적인 재작성 필요
    return "producer";
  } else if (lastCritique.score > 75) {
    // 사소한 개선만 필요
    return "producer";
  } else {
    // 표준 반복
    return "producer";
  }
}

export function createProducerCriticAgent() {
  const workflow = new StateGraph(ProducerCriticState)
    .addNode("producer", producerNode)
    .addNode("critic", criticNode)
    .addEdge(START, "producer")
    .addEdge("producer", "critic")
    .addConditionalEdges("critic", routingLogic, {
      producer: "producer",
      [END]: END,
    });

  return workflow.compile();
}

비평 심각도와 반복 횟수에 따라 정교한 라우팅을 구현합니다.

5. 서버 전송 이벤트를 사용한 스트리밍 API

// app/api/producer-critic/route.ts
import { createProducerCriticAgent } from "@/lib/agents/producer-critic";
import { debounce } from "es-toolkit";

export const runtime = "nodejs";
export const maxDuration = 300;

export async function POST(req: Request) {
  const { task } = await req.json();

  const encoder = new TextEncoder();
  const stream = new TransformStream();
  const writer = stream.writable.getWriter();

  // 클라이언트를 압도하지 않기 위한 디바운스된 쓰기
  const debouncedWrite = debounce(async (data: any) => {
    await writer.write(
      encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
    );
  }, 100);

  const agent = createProducerCriticAgent();

  (async () => {
    try {
      const eventStream = await agent.streamEvents(
        { task, drafts: [], critiques: [], iteration: 0 },
        { version: "v2" }
      );

      for await (const event of eventStream) {
        if (event.event === "on_chain_end" && event.name === "producer") {
          await debouncedWrite({
            type: "draft",
            iteration: event.data.output.iteration,
            content: event.data.output.drafts[event.data.output.drafts.length - 1],
          });
        }

        if (event.event === "on_chain_end" && event.name === "critic") {
          const critique = event.data.output.critiques[event.data.output.critiques.length - 1];
          await debouncedWrite({
            type: "critique",
            iteration: event.data.output.iteration,
            score: critique.score,
            approved: critique.approved,
            issues: critique.issues,
          });
        }
      }

      await writer.write(encoder.encode(`data: ${JSON.stringify({ type: "complete" })}\n\n`));
    } catch (error) {
      await writer.write(
        encoder.encode(`data: ${JSON.stringify({ type: "error", error: String(error) })}\n\n`)
      );
    } finally {
      await writer.close();
    }
  })();

  return new Response(stream.readable, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      "Connection": "keep-alive",
    },
  });
}

프로그레시브 UI 업데이트를 위해 서버 전송 이벤트를 사용하여 리플렉션 이벤트를 실시간으로 스트리밍합니다.

6. 실시간 시각화가 있는 고급 프론트엔드

// components/ProducerCriticDemo.tsx
"use client";

import { useState, useCallback } from "react";
import { useMutation } from "@tanstack/react-query";
import { partition, groupBy } from "es-toolkit";

interface StreamEvent {
  type: "draft" | "critique" | "complete" | "error";
  iteration?: number;
  content?: string;
  score?: number;
  approved?: boolean;
  issues?: Array<{
    category: string;
    description: string;
    severity: string;
  }>;
  error?: string;
}

export default function ProducerCriticDemo() {
  const [task, setTask] = useState("");
  const [events, setEvents] = useState<StreamEvent[]>([]);
  const [isStreaming, setIsStreaming] = useState(false);

  const startReflection = useCallback(async () => {
    setEvents([]);
    setIsStreaming(true);

    try {
      const response = await fetch("/api/producer-critic", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ task }),
      });

      const reader = response.body?.getReader();
      const decoder = new TextDecoder();

      while (reader) {
        const { done, value } = await reader.read();
        if (done) break;

        const chunk = decoder.decode(value);
        const lines = chunk.split("\n");

        for (const line of lines) {
          if (line.startsWith("data: ")) {
            try {
              const event = JSON.parse(line.slice(6));
              setEvents(prev => [...prev, event]);

              if (event.type === "complete" || event.type === "error") {
                setIsStreaming(false);
              }
            } catch {}
          }
        }
      }
    } catch (error) {
      console.error("Stream error:", error);
      setIsStreaming(false);
    }
  }, [task]);

  // 이벤트를 반복별로 그룹화
  const [drafts, critiques] = partition(
    events.filter(e => e.type === "draft" || e.type === "critique"),
    e => e.type === "draft"
  );

  const iterations = groupBy(
    [...drafts, ...critiques],
    e => e.iteration?.toString() || "0"
  );

  const finalDraft = drafts[drafts.length - 1];
  const finalCritique = critiques[critiques.length - 1];

  return (
    <div className="container mx-auto p-4">
      <div className="card bg-base-100 shadow-xl">
        <div className="card-body">
          <h2 className="card-title">프로듀서-비평가 리플렉션 시스템</h2>

          <div className="form-control">
            <textarea
              className="textarea textarea-bordered"
              placeholder="작업을 설명하세요..."
              value={task}
              onChange={(e) => setTask(e.target.value)}
              rows={3}
              disabled={isStreaming}
            />
          </div>

          <div className="card-actions justify-end mt-4">
            <button
              className="btn btn-primary"
              onClick={startReflection}
              disabled={isStreaming || !task.trim()}
            >
              {isStreaming ? (
                <>
                  <span className="loading loading-spinner"></span>
                  처리 중...
                </>
              ) : "리플렉션 시작"}
            </button>
          </div>
        </div>
      </div>

      {events.length > 0 && (
        <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-6">
          {/* 반복 타임라인 */}
          <div className="card bg-base-100 shadow">
            <div className="card-body">
              <h3 className="card-title text-lg">리플렉션 프로세스</h3>

              <ul className="timeline timeline-vertical">
                {Object.entries(iterations).map(([iter, iterEvents]) => {
                  const draft = iterEvents.find(e => e.type === "draft");
                  const critique = iterEvents.find(e => e.type === "critique");

                  return (
                    <li key={iter}>
                      <div className="timeline-middle">
                        <svg
                          xmlns="http://www.w3.org/2000/svg"
                          viewBox="0 0 20 20"
                          fill="currentColor"
                          className="h-5 w-5"
                        >
                          <path
                            fillRule="evenodd"
                            d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
                            clipRule="evenodd"
                          />
                        </svg>
                      </div>
                      <div className="timeline-end timeline-box">
                        <div className="text-lg font-black">반복 {iter}</div>
                        {critique && (
                          <div className="stats stats-horizontal shadow mt-2">
                            <div className="stat">
                              <div className="stat-title">점수</div>
                              <div className="stat-value text-2xl">{critique.score}</div>
                            </div>
                            <div className="stat">
                              <div className="stat-title">상태</div>
                              <div className={`stat-value text-2xl ${critique.approved ? "text-success" : "text-warning"}`}>
                                {critique.approved ? "✓" : "↻"}
                              </div>
                            </div>
                          </div>
                        )}
                        {critique?.issues && (
                          <div className="mt-2">
                            <p className="font-semibold">발견된 문제:</p>
                            {critique.issues.map((issue, idx) => (
                              <div key={idx} className={`badge badge-${issue.severity === "critical" ? "error" : "warning"} gap-2 mr-1`}>
                                {issue.category}
                              </div>
                            ))}
                          </div>
                        )}
                      </div>
                      <hr />
                    </li>
                  );
                })}
              </ul>
            </div>
          </div>

          {/* 최종 출력 */}
          <div className="card bg-base-100 shadow">
            <div className="card-body">
              <h3 className="card-title text-lg">최종 출력</h3>

              {finalCritique?.approved && (
                <div className="alert alert-success">
                  <span>출력이 승인되었습니다. 점수: {finalCritique.score}/100</span>
                </div>
              )}

              {finalDraft && (
                <div className="prose max-w-none">
                  <div className="mockup-code">
                    <pre><code>{finalDraft.content}</code></pre>
                  </div>
                </div>
              )}

              {isStreaming && (
                <div className="flex justify-center">
                  <span className="loading loading-dots loading-lg"></span>
                </div>
              )}
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

타임라인 시각화와 실시간 스트리밍 업데이트를 갖춘 정교한 UI를 생성합니다.

7. 캐싱을 통한 성능 최적화

// lib/cache/reflection-cache.ts
import { kv } from "@vercel/kv";
import { hash } from "es-toolkit/compat";

interface CacheEntry {
  task: string;
  output: string;
  score: number;
  timestamp: number;
}

export class ReflectionCache {
  private readonly ttl = 3600; // 1시간

  async get(task: string): Promise<CacheEntry | null> {
    const key = `reflection:${hash(task)}`;
    const cached = await kv.get<CacheEntry>(key);

    if (cached && Date.now() - cached.timestamp < this.ttl * 1000) {
      return cached;
    }

    return null;
  }

  async set(task: string, output: string, score: number): Promise<void> {
    const key = `reflection:${hash(task)}`;
    const entry: CacheEntry = {
      task,
      output,
      score,
      timestamp: Date.now(),
    };

    await kv.set(key, entry, { ex: this.ttl });
  }

  async getSimilar(task: string, threshold = 0.8): Promise<CacheEntry[]> {
    // 의미론적 유사성 검색 구현
    const allKeys = await kv.keys("reflection:*");
    const similar: CacheEntry[] = [];

    for (const key of allKeys) {
      const entry = await kv.get<CacheEntry>(key);
      if (entry) {
        // 간단한 유사성 확인 (적절한 의미론적 유사성 구현)
        const similarity = this.calculateSimilarity(task, entry.task);
        if (similarity > threshold) {
          similar.push(entry);
        }
      }
    }

    return similar;
  }

  private calculateSimilarity(a: string, b: string): number {
    // 단순화된 유사성 계산
    const wordsA = new Set(a.toLowerCase().split(" "));
    const wordsB = new Set(b.toLowerCase().split(" "));
    const intersection = new Set([...wordsA].filter(x => wordsB.has(x)));
    const union = new Set([...wordsA, ...wordsB]);

    return intersection.size / union.size;
  }
}

유사한 작업에 대한 중복 리플렉션 사이클을 줄이기 위해 캐싱을 구현합니다.

8. 비용 추적 및 최적화

// lib/monitoring/cost-tracker.ts
interface ReflectionMetrics {
  totalTokens: number;
  inputTokens: number;
  outputTokens: number;
  iterations: number;
  duration: number;
  estimatedCost: number;
}

export class CostTracker {
  private metrics: ReflectionMetrics = {
    totalTokens: 0,
    inputTokens: 0,
    outputTokens: 0,
    iterations: 0,
    duration: 0,
    estimatedCost: 0,
  };

  private readonly costPerToken = {
    "gemini-2.5-pro": { input: 0.00125, output: 0.005 },
    "gemini-2.5-flash": { input: 0.00015, output: 0.0006 },
  };

  trackIteration(model: string, inputTokens: number, outputTokens: number): void {
    this.metrics.inputTokens += inputTokens;
    this.metrics.outputTokens += outputTokens;
    this.metrics.totalTokens += inputTokens + outputTokens;
    this.metrics.iterations += 1;

    const modelCost = this.costPerToken[model as keyof typeof this.costPerToken];
    if (modelCost) {
      this.metrics.estimatedCost +=
        (inputTokens * modelCost.input + outputTokens * modelCost.output) / 1000;
    }
  }

  shouldContinue(maxCost: number = 0.10): boolean {
    return this.metrics.estimatedCost < maxCost;
  }

  getMetrics(): ReflectionMetrics {
    return { ...this.metrics };
  }
}

예산을 고려한 리플렉션 사이클을 구현하기 위해 토큰 사용량과 비용을 추적합니다.

결론

리플렉션 패턴은 AI 에이전트를 단일 응답 시스템에서 자체 개선이 가능한 반복 학습자로 변환합니다. 적절한 상태 관리, 스트리밍 기능 및 비용 최적화를 갖춘 프로듀서-비평가 아키텍처를 구현함으로써 Vercel의 서버리스 플랫폼에서 정교한 리플렉션 시스템을 배포할 수 있습니다. 핵심은 지능형 캐싱, 조기 중지 및 적응형 라우팅 전략을 통해 품질 개선과 계산 비용의 균형을 맞추는 것입니다. 간단한 작업에는 기본적인 자체 반성으로 시작하고, 더 높은 품질의 출력이 필요한 복잡한 시나리오에는 다중 에이전트 프로듀서-비평가 시스템으로 확장하세요.