초안 에이전트 설계 패턴 - 추론 기술

agentic-designreasoninglangchainlanggraphaitypescript
By sko X opus 4.19/21/202510 min read

LangChain, LangGraph, TypeScript를 사용하여 서버리스 환경에서 Chain-of-Thought(CoT), Tree-of-Thought(ToT), Graph-of-Thought(GoT), Self-Reflection 패턴 등의 고급 추론 기술을 구현하는 방법을 배워봅니다.

멘탈 모델: 심포니 오케스트라

추론 기술을 심포니 오케스트라로 생각해보세요. 서로 다른 섹션(추론 패턴)이 협력하여 복잡한 하모니(솔루션)를 만들어냅니다. 지휘자(LangGraph)가 여러 악기(에이전트)를 조정합니다 - 현악기는 선형 추론(CoT)을 처리하고, 목관악기는 변형을 탐색하며(ToT), 금관악기는 피드백을 제공하고(Self-Reflection), 타악기는 리듬(상태 관리)을 유지합니다. 음악가가 독주나 앙상블로 연주할 수 있듯이, 에이전트도 독립적으로 추론하거나 더 풍부한 결과를 위해 협력할 수 있습니다.

기본 예제: Chain-of-Thought 추론

1. 기본 CoT 에이전트 설정

// app/agents/cot-agent/route.ts
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { StateGraph, Annotation } from "@langchain/langgraph";
import { HumanMessage, AIMessage, BaseMessage } from "@langchain/core/messages";
import { z } from "zod";
import { map, reduce } from "es-toolkit";

// 추론 상태 정의
const ReasoningState = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (x, y) => x.concat(y),
    default: () => [],
  }),
  reasoning_steps: Annotation<string[]>({
    reducer: (x, y) => x.concat(y),
    default: () => [],
  }),
  final_answer: Annotation<string>({
    reducer: (_, y) => y,
    default: () => "",
  }),
});

// 추론 체인 생성
const model = new ChatGoogleGenerativeAI({
  modelName: "gemini-pro",
  temperature: 0.7,
});

// CoT 추론 노드
async function reasoningNode(state: typeof ReasoningState.State) {
  const cotPrompt = `당신은 추론 에이전트입니다. 문제를 단계별로 분해하세요.

문제: ${state.messages[state.messages.length - 1].content}

이를 단계적으로 생각하세요:
1. 먼저, 우리가 해결하려는 것을 식별
2. 더 작은 부분으로 분해
3. 각 부분 해결
4. 솔루션 결합

응답을 다음 형식으로 작성하세요:
단계 1: [추론]
단계 2: [추론]
...
최종 답변: [답]`;

  const response = await model.invoke([
    new HumanMessage(cotPrompt)
  ]);

  // 응답 파싱
  const content = response.content as string;
  const steps = content.match(/단계 \d+: (.+)/g) || [];
  const finalAnswer = content.match(/최종 답변: (.+)/)?.[1] || "";

  return {
    messages: [response],
    reasoning_steps: steps,
    final_answer: finalAnswer,
  };
}

// 그래프 구축
const workflow = new StateGraph(ReasoningState)
  .addNode("reason", reasoningNode)
  .addEdge("__start__", "reason")
  .addEdge("reason", "__end__");

const app = workflow.compile();

// API 라우트 핸들러
export async function POST(request: Request) {
  const { question } = await request.json();

  const result = await app.invoke({
    messages: [new HumanMessage(question)],
  });

  return Response.json({
    reasoning_steps: result.reasoning_steps,
    final_answer: result.final_answer,
  });
}

기본 CoT 에이전트는 문제를 순차적 추론 단계로 분해합니다. 각 단계는 이전 단계를 기반으로 구축되어 논리적 사고의 연쇄를 생성합니다.

2. React Query를 통한 프론트엔드 통합

// app/components/ReasoningAgent.tsx
'use client';

import { useMutation } from '@tanstack/react-query';
import { useState } from 'react';
import { map } from 'es-toolkit';

interface ReasoningResponse {
  reasoning_steps: string[];
  final_answer: string;
}

async function queryReasoning(question: string): Promise<ReasoningResponse> {
  const response = await fetch('/agents/cot-agent', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ question }),
  });
  return response.json();
}

export function ReasoningAgent() {
  const [question, setQuestion] = useState('');

  const mutation = useMutation({
    mutationFn: queryReasoning,
  });

  return (
    <div className="card bg-base-100 shadow-xl">
      <div className="card-body">
        <h2 className="card-title">Chain-of-Thought 추론</h2>

        <textarea
          className="textarea textarea-bordered"
          placeholder="복잡한 질문을 입력하세요..."
          value={question}
          onChange={(e) => setQuestion(e.target.value)}
        />

        <button
          className="btn btn-primary"
          onClick={() => mutation.mutate(question)}
          disabled={mutation.isPending}
        >
          {mutation.isPending ? '추론 중...' : '추론 시작'}
        </button>

        {mutation.isSuccess && (
          <div className="mt-4 space-y-2">
            <div className="text-sm opacity-70">추론 단계:</div>
            {map(mutation.data.reasoning_steps, (step, idx) => (
              <div key={idx} className="p-2 bg-base-200 rounded">
                {step}
              </div>
            ))}
            <div className="divider"></div>
            <div className="alert alert-success">
              <span>{mutation.data.final_answer}</span>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

React Query는 비동기 상태 관리를 처리하며 UI는 추론 단계를 점진적으로 표시합니다.

고급 예제: ToT와 Self-Reflection을 통한 멀티 에이전트 추론

1. 병렬 탐색을 통한 Tree-of-Thought

// app/agents/advanced-reasoning/tree-of-thought.ts
import { StateGraph, Annotation, Send } from "@langchain/langgraph";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { BaseMessage, HumanMessage, AIMessage } from "@langchain/core/messages";
import { z } from "zod";
import { maxBy, sortBy, filter, map, chunk } from "es-toolkit";
import { RunnableConfig } from "@langchain/core/runnables";

// 사고 노드 구조 정의
const ThoughtSchema = z.object({
  id: z.string(),
  content: z.string(),
  score: z.number(),
  parent_id: z.string().optional(),
  depth: z.number(),
});

type Thought = z.infer<typeof ThoughtSchema>;

// 사고 트리를 가진 상태 정의
const ToTState = Annotation.Root({
  problem: Annotation<string>(),
  thoughts: Annotation<Thought[]>({
    reducer: (x, y) => [...x, ...y],
    default: () => [],
  }),
  current_depth: Annotation<number>({
    reducer: (_, y) => y,
    default: () => 0,
  }),
  best_solution: Annotation<string>({
    reducer: (_, y) => y,
    default: () => "",
  }),
  exploration_paths: Annotation<string[][]>({
    reducer: (x, y) => [...x, ...y],
    default: () => [],
  }),
});

const model = new ChatGoogleGenerativeAI({
  modelName: "gemini-pro",
  temperature: 0.8,
});

// 여러 사고를 병렬로 생성
async function generateThoughts(
  state: typeof ToTState.State,
  config: RunnableConfig
): Promise<Partial<typeof ToTState.State>> {
  const parentThoughts = filter(
    state.thoughts,
    (t) => t.depth === state.current_depth - 1
  );

  // 부모 사고가 없으면 원래 문제 사용
  const prompts = parentThoughts.length > 0
    ? map(parentThoughts, (parent) => ({
        parent_id: parent.id,
        prompt: `이 추론 단계를 고려하여: "${parent.content}"
        ${state.problem}를 해결하기 위한 3가지 다른 방법을 생성하세요.
        창의적이고 다양한 접근법을 탐색하세요.`,
      }))
    : [{
        parent_id: "root",
        prompt: `문제: ${state.problem}
        이 문제를 해결하기 위한 3가지 다른 초기 접근법을 생성하세요.`,
      }];

  // 사고를 병렬로 생성
  const thoughtPromises = map(prompts, async ({ parent_id, prompt }) => {
    const response = await model.invoke([new HumanMessage(prompt)]);
    const content = response.content as string;

    // 응답에서 여러 사고 파싱
    const thoughtTexts = content.split('\n').filter(line => line.trim());

    return map(thoughtTexts.slice(0, 3), (text, idx) => ({
      id: `${parent_id}-${state.current_depth}-${idx}`,
      content: text,
      score: 0,
      parent_id,
      depth: state.current_depth,
    }));
  });

  const allThoughts = (await Promise.all(thoughtPromises)).flat();

  return {
    thoughts: allThoughts,
  };
}

// 품질에 따라 사고 점수 매기기
async function scoreThoughts(
  state: typeof ToTState.State
): Promise<Partial<typeof ToTState.State>> {
  const currentThoughts = filter(
    state.thoughts,
    (t) => t.depth === state.current_depth && t.score === 0
  );

  const scoringPrompt = `문제 "${state.problem}"에 대한 이러한 솔루션 접근법을 평가하세요
  다음을 기준으로 0-10점을 매기세요:
  - 논리적 일관성
  - 해결책으로의 진전
  - 창의성

  접근법:
  ${map(currentThoughts, (t, i) => `${i + 1}. ${t.content}`).join('\n')}

  점수를 다음과 같이 반환하세요: [score1, score2, ...]`;

  const response = await model.invoke([new HumanMessage(scoringPrompt)]);
  const scores = JSON.parse((response.content as string).match(/\[[\d,\s]+\]/)?.[0] || "[]");

  const scoredThoughts = map(currentThoughts, (thought, idx) => ({
    ...thought,
    score: scores[idx] || 0,
  }));

  // 점수가 매겨진 사고만 업데이트
  const updatedThoughts = map(state.thoughts, (t) => {
    const scored = scoredThoughts.find(st => st.id === t.id);
    return scored || t;
  });

  return {
    thoughts: updatedThoughts,
  };
}

// 낮은 점수 분기 가지치기
async function pruneThoughts(
  state: typeof ToTState.State
): Promise<Partial<typeof ToTState.State>> {
  const currentThoughts = filter(
    state.thoughts,
    (t) => t.depth === state.current_depth
  );

  // 사고의 상위 40% 유지
  const sorted = sortBy(currentThoughts, [(t) => -t.score]);
  const keepCount = Math.max(2, Math.floor(sorted.length * 0.4));
  const keepIds = new Set(map(sorted.slice(0, keepCount), t => t.id));

  const prunedThoughts = filter(state.thoughts, (t) =>
    t.depth < state.current_depth || keepIds.has(t.id)
  );

  return {
    thoughts: prunedThoughts,
  };
}

// 탐색을 계속할지 확인
function shouldContinue(state: typeof ToTState.State): "expand" | "synthesize" {
  const MAX_DEPTH = 3;
  return state.current_depth < MAX_DEPTH ? "expand" : "synthesize";
}

// 최고의 경로에서 최종 솔루션 합성
async function synthesizeSolution(
  state: typeof ToTState.State
): Promise<Partial<typeof ToTState.State>> {
  // 최고의 리프 노드 찾기
  const leafThoughts = filter(
    state.thoughts,
    (t) => t.depth === state.current_depth
  );

  const bestLeaf = maxBy(leafThoughts, (t) => t.score);

  if (!bestLeaf) {
    return { best_solution: "솔루션을 찾을 수 없습니다" };
  }

  // 루트까지 추적
  const path: Thought[] = [];
  let current: Thought | undefined = bestLeaf;

  while (current) {
    path.unshift(current);
    current = state.thoughts.find(t => t.id === current?.parent_id);
  }

  const synthesisPrompt = `이 추론 경로에서 완전한 솔루션을 합성하세요:
  ${map(path, (t, i) => `단계 ${i + 1}: ${t.content}`).join('\n')}

  ${state.problem}에 대한 명확하고 포괄적인 답변을 제공하세요`;

  const response = await model.invoke([new HumanMessage(synthesisPrompt)]);

  return {
    best_solution: response.content as string,
    exploration_paths: [map(path, t => t.content)],
  };
}

// Tree-of-Thought 워크플로우 구축
export function createToTWorkflow() {
  const workflow = new StateGraph(ToTState)
    .addNode("generate", generateThoughts)
    .addNode("score", scoreThoughts)
    .addNode("prune", pruneThoughts)
    .addNode("synthesize", synthesizeSolution)
    .addEdge("__start__", "generate")
    .addEdge("generate", "score")
    .addEdge("score", "prune")
    .addConditionalEdges(
      "prune",
      shouldContinue,
      {
        expand: "generate",
        synthesize: "synthesize",
      }
    )
    .addEdge("synthesize", "__end__");

  return workflow.compile();
}

Tree-of-Thought는 여러 추론 경로를 병렬로 탐색하고, 최적의 솔루션을 찾기 위해 점수를 매기고 가지치기를 합니다.

2. 솔루션 개선을 위한 Self-Reflection 에이전트

// app/agents/advanced-reasoning/self-reflection.ts
import { StateGraph, Annotation } from "@langchain/langgraph";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { BaseMessage, HumanMessage } from "@langchain/core/messages";
import { z } from "zod";
import { reduce, filter, last } from "es-toolkit";

// 비판 히스토리를 가진 리플렉션 상태
const ReflectionState = Annotation.Root({
  problem: Annotation<string>(),
  current_solution: Annotation<string>({
    reducer: (_, y) => y,
  }),
  critiques: Annotation<Array<{
    critique: string;
    suggestions: string[];
    score: number;
  }>>({
    reducer: (x, y) => [...x, ...y],
    default: () => [],
  }),
  iteration: Annotation<number>({
    reducer: (_, y) => y,
    default: () => 0,
  }),
  final_solution: Annotation<string>({
    reducer: (_, y) => y,
    default: () => "",
  }),
});

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

// 액터: 초기 솔루션 생성
async function generateSolution(
  state: typeof ReflectionState.State
): Promise<Partial<typeof ReflectionState.State>> {
  const lastCritique = last(state.critiques);

  const prompt = lastCritique
    ? `문제: ${state.problem}

      이전 솔루션: ${state.current_solution}

      비판: ${lastCritique.critique}
      제안: ${lastCritique.suggestions.join(', ')}

      이러한 포인트를 해결하는 개선된 솔루션을 생성하세요.`
    : `이 문제를 해결하세요: ${state.problem}

      명확한 추론과 함께 자세한 솔루션을 제공하세요.`;

  const response = await model.invoke([new HumanMessage(prompt)]);

  return {
    current_solution: response.content as string,
    iteration: state.iteration + 1,
  };
}

// 평가자: 솔루션 비판
async function evaluateSolution(
  state: typeof ReflectionState.State
): Promise<Partial<typeof ReflectionState.State>> {
  const critiquePrompt = `이 솔루션을 비판적으로 평가하세요:

  문제: ${state.problem}
  솔루션: ${state.current_solution}

  제공할 내용:
  1. 약점을 식별하는 자세한 비판
  2. 개선을 위한 구체적인 제안
  3. 0-10점 점수

  JSON 형식으로: {
    "critique": "...",
    "suggestions": ["...", "..."],
    "score": 0
  }`;

  const response = await model.invoke([new HumanMessage(critiquePrompt)]);

  try {
    const evaluation = JSON.parse(response.content as string);

    return {
      critiques: [evaluation],
    };
  } catch {
    // JSON 파싱 실패 시 폴백
    return {
      critiques: [{
        critique: response.content as string,
        suggestions: [],
        score: 5,
      }],
    };
  }
}

// 솔루션이 충분히 좋은지 확인
function shouldContinueReflection(
  state: typeof ReflectionState.State
): "reflect" | "finalize" {
  const MAX_ITERATIONS = 3;
  const GOOD_SCORE = 8;

  const lastCritique = last(state.critiques);

  if (state.iteration >= MAX_ITERATIONS) {
    return "finalize";
  }

  if (lastCritique && lastCritique.score >= GOOD_SCORE) {
    return "finalize";
  }

  return "reflect";
}

// 최고의 솔루션 최종화
async function finalizeSolution(
  state: typeof ReflectionState.State
): Promise<Partial<typeof ReflectionState.State>> {
  return {
    final_solution: state.current_solution,
  };
}

// Self-Reflection 워크플로우 구축
export function createReflectionWorkflow() {
  const workflow = new StateGraph(ReflectionState)
    .addNode("generate", generateSolution)
    .addNode("evaluate", evaluateSolution)
    .addNode("finalize", finalizeSolution)
    .addEdge("__start__", "generate")
    .addEdge("generate", "evaluate")
    .addConditionalEdges(
      "evaluate",
      shouldContinueReflection,
      {
        reflect: "generate",
        finalize: "finalize",
      }
    )
    .addEdge("finalize", "__end__");

  return workflow.compile();
}

Self-Reflection은 비판과 개선 사이클을 통해 솔루션을 반복적으로 개선합니다.

3. 여러 추론 패턴 오케스트레이션

// app/agents/advanced-reasoning/orchestrator.ts
import { StateGraph, Annotation, Send } from "@langchain/langgraph";
import { createToTWorkflow } from "./tree-of-thought";
import { createReflectionWorkflow } from "./self-reflection";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { HumanMessage } from "@langchain/core/messages";
import { z } from "zod";
import { maxBy, map } from "es-toolkit";

// 마스터 오케스트레이터 상태
const OrchestratorState = Annotation.Root({
  problem: Annotation<string>(),
  problem_type: Annotation<string>({
    reducer: (_, y) => y,
    default: () => "unknown",
  }),
  tot_result: Annotation<{
    solution: string;
    paths: string[][];
  }>(),
  reflection_result: Annotation<{
    solution: string;
    iterations: number;
  }>(),
  final_answer: Annotation<string>({
    reducer: (_, y) => y,
    default: () => "",
  }),
  reasoning_method: Annotation<string>({
    reducer: (_, y) => y,
    default: () => "",
  }),
});

const model = new ChatGoogleGenerativeAI({
  modelName: "gemini-pro",
  temperature: 0.3,
});

// 추론 전략을 선택하기 위해 문제 분류
async function classifyProblem(
  state: typeof OrchestratorState.State
): Promise<Partial<typeof OrchestratorState.State>> {
  const classificationPrompt = `최적의 추론 접근법을 결정하기 위해 이 문제를 분류하세요:

  문제: ${state.problem}

  카테고리:
  - "exploratory": 여러 솔루션 경로가 필요한 문제 (Tree-of-Thought 사용)
  - "refinement": 반복적 개선이 필요한 문제 (Self-Reflection 사용)
  - "hybrid": 탐색과 개선 모두 필요한 복잡한 문제

  카테고리 이름만 반환하세요.`;

  const response = await model.invoke([new HumanMessage(classificationPrompt)]);
  const problemType = (response.content as string).toLowerCase().trim();

  return {
    problem_type: problemType,
  };
}

// Tree-of-Thought 추론 실행
async function runToT(
  state: typeof OrchestratorState.State
): Promise<Partial<typeof OrchestratorState.State>> {
  const totWorkflow = createToTWorkflow();

  const result = await totWorkflow.invoke({
    problem: state.problem,
    thoughts: [],
    current_depth: 0,
  });

  return {
    tot_result: {
      solution: result.best_solution,
      paths: result.exploration_paths,
    },
    reasoning_method: "Tree-of-Thought",
  };
}

// Self-Reflection 추론 실행
async function runReflection(
  state: typeof OrchestratorState.State
): Promise<Partial<typeof OrchestratorState.State>> {
  const reflectionWorkflow = createReflectionWorkflow();

  const result = await reflectionWorkflow.invoke({
    problem: state.problem,
    current_solution: "",
    critiques: [],
    iteration: 0,
  });

  return {
    reflection_result: {
      solution: result.final_solution,
      iterations: result.iteration,
    },
    reasoning_method: "Self-Reflection",
  };
}

// 여러 추론 방법의 결과 통합
async function synthesizeResults(
  state: typeof OrchestratorState.State
): Promise<Partial<typeof OrchestratorState.State>> {
  let solutions = [];

  if (state.tot_result) {
    solutions.push({
      method: "Tree-of-Thought",
      solution: state.tot_result.solution,
    });
  }

  if (state.reflection_result) {
    solutions.push({
      method: "Self-Reflection",
      solution: state.reflection_result.solution,
    });
  }

  if (solutions.length === 1) {
    return {
      final_answer: solutions[0].solution,
    };
  }

  // 여러 솔루션이 있는 경우 통합
  const synthesisPrompt = `이러한 솔루션을 포괄적인 답변으로 통합하세요:

  문제: ${state.problem}

  ${map(solutions, s => `${s.method} 솔루션:\n${s.solution}`).join('\n\n')}

  각 접근법의 최고의 통찰력을 결합한 최종 답변을 작성하세요.`;

  const response = await model.invoke([new HumanMessage(synthesisPrompt)]);

  return {
    final_answer: response.content as string,
    reasoning_method: "하이브리드 (ToT + Self-Reflection)",
  };
}

// 문제 유형에 따른 라우팅
function routeReasoning(state: typeof OrchestratorState.State): string | string[] {
  switch (state.problem_type) {
    case "exploratory":
      return "tot";
    case "refinement":
      return "reflection";
    case "hybrid":
      return ["tot", "reflection"];
    default:
      return "tot"; // 기본 폴백
  }
}

// 마스터 오케스트레이터 생성
export function createReasoningOrchestrator() {
  const workflow = new StateGraph(OrchestratorState)
    .addNode("classify", classifyProblem)
    .addNode("tot", runToT)
    .addNode("reflection", runReflection)
    .addNode("synthesize", synthesizeResults)
    .addEdge("__start__", "classify")
    .addConditionalEdges(
      "classify",
      routeReasoning,
      {
        tot: "tot",
        reflection: "reflection",
      }
    )
    .addEdge("tot", "synthesize")
    .addEdge("reflection", "synthesize")
    .addEdge("synthesize", "__end__");

  return workflow.compile();
}

// 스트리밍이 있는 API 라우트 핸들러
export async function POST(request: Request) {
  const { problem } = await request.json();

  const orchestrator = createReasoningOrchestrator();

  // 스트리밍 응답 생성
  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      try {
        // 이벤트가 발생하면 스트리밍
        for await (const event of orchestrator.streamEvents(
          { problem },
          { version: "v2" }
        )) {
          if (event.event === "on_chain_stream") {
            const chunk = encoder.encode(
              `data: ${JSON.stringify({
                type: "progress",
                node: event.name,
                data: event.data,
              })}\n\n`
            );
            controller.enqueue(chunk);
          }
        }

        // 최종 결과 가져오기
        const result = await orchestrator.invoke({ problem });

        const finalChunk = encoder.encode(
          `data: ${JSON.stringify({
            type: "complete",
            answer: result.final_answer,
            method: result.reasoning_method,
            tot_result: result.tot_result,
            reflection_result: result.reflection_result,
          })}\n\n`
        );

        controller.enqueue(finalChunk);
        controller.close();
      } catch (error) {
        controller.error(error);
      }
    },
  });

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

오케스트레이터는 문제 특성에 따라 추론 전략을 지능적으로 선택하고 결합합니다.

4. 실시간 스트리밍을 통한 프론트엔드

// app/components/AdvancedReasoningAgent.tsx
'use client';

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

interface StreamEvent {
  type: 'progress' | 'complete';
  node?: string;
  data?: any;
  answer?: string;
  method?: string;
  tot_result?: any;
  reflection_result?: any;
}

export function AdvancedReasoningAgent() {
  const [problem, setProblem] = useState('');
  const [events, setEvents] = useState<StreamEvent[]>([]);
  const [isStreaming, setIsStreaming] = useState(false);

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

    try {
      const response = await fetch('/agents/advanced-reasoning/orchestrator', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ problem }),
      });

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

      if (!reader) throw new Error('리더를 사용할 수 없습니다');

      while (true) {
        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: ')) {
            const data = JSON.parse(line.slice(6));
            setEvents(prev => [...prev, data]);
          }
        }
      }
    } catch (error) {
      console.error('스트리밍 오류:', error);
    } finally {
      setIsStreaming(false);
    }
  }, [problem]);

  const completeEvent = events.find(e => e.type === 'complete');
  const progressEvents = events.filter(e => e.type === 'progress');

  return (
    <div className="max-w-4xl mx-auto p-6 space-y-6">
      <div className="card bg-base-100 shadow-xl">
        <div className="card-body">
          <h2 className="card-title">고급 멀티 에이전트 추론</h2>

          <textarea
            className="textarea textarea-bordered h-32"
            placeholder="깊은 추론이 필요한 복잡한 문제를 입력하세요..."
            value={problem}
            onChange={(e) => setProblem(e.target.value)}
          />

          <button
            className={`btn btn-primary ${isStreaming ? 'loading' : ''}`}
            onClick={startReasoning}
            disabled={isStreaming || !problem}
          >
            {isStreaming ? '추론 진행 중...' : '고급 추론 시작'}
          </button>
        </div>
      </div>

      {progressEvents.length > 0 && (
        <div className="card bg-base-200">
          <div className="card-body">
            <h3 className="font-semibold">추론 진행 상황</h3>
            <div className="space-y-2">
              {map(progressEvents, (event, idx) => (
                <div key={idx} className="flex items-center gap-2">
                  <div className="badge badge-primary">{event.node}</div>
                  <span className="text-sm opacity-70">처리 중...</span>
                </div>
              ))}
            </div>
          </div>
        </div>
      )}

      {completeEvent && (
        <div className="space-y-4">
          <div className="card bg-success text-success-content">
            <div className="card-body">
              <h3 className="card-title">최종 답변</h3>
              <p className="whitespace-pre-wrap">{completeEvent.answer}</p>
              <div className="card-actions justify-end">
                <div className="badge badge-outline">
                  방법: {completeEvent.method}
                </div>
              </div>
            </div>
          </div>

          {completeEvent.tot_result && (
            <div className="card bg-base-100">
              <div className="card-body">
                <h4 className="font-semibold">Tree-of-Thought 탐색</h4>
                <div className="text-sm space-y-1">
                  {map(completeEvent.tot_result.paths[0], (step, idx) => (
                    <div key={idx} className="pl-4 border-l-2 border-primary">
                      단계 {idx + 1}: {step}
                    </div>
                  ))}
                </div>
              </div>
            </div>
          )}

          {completeEvent.reflection_result && (
            <div className="card bg-base-100">
              <div className="card-body">
                <h4 className="font-semibold">Self-Reflection 프로세스</h4>
                <p className="text-sm opacity-70">
                  {completeEvent.reflection_result.iterations} 반복을 통해 개선됨
                </p>
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

프론트엔드는 점진적 업데이트와 함께 추론 프로세스에 대한 실시간 피드백을 제공합니다.

성능 최적화

// app/lib/reasoning-cache.ts
import { LRUCache } from 'lru-cache';
import { createHash } from 'crypto';

// 추론 결과를 위한 시맨틱 캐싱
const reasoningCache = new LRUCache<string, any>({
  max: 100,
  ttl: 1000 * 60 * 60, // 1시간
  updateAgeOnGet: true,
});

export function getCacheKey(problem: string, method: string): string {
  return createHash('sha256')
    .update(`${problem}-${method}`)
    .digest('hex');
}

export async function withCache<T>(
  key: string,
  fn: () => Promise<T>
): Promise<T> {
  const cached = reasoningCache.get(key);
  if (cached) {
    console.log(`키에 대한 캐시 히트: ${key}`);
    return cached;
  }

  const result = await fn();
  reasoningCache.set(key, result);
  return result;
}

// 병렬 처리 헬퍼
export async function parallelReasoning<T>(
  tasks: Array<() => Promise<T>>,
  maxConcurrency: number = 3
): Promise<T[]> {
  const results: T[] = [];
  const executing: Promise<void>[] = [];

  for (const task of tasks) {
    const promise = task().then(result => {
      results.push(result);
    });

    executing.push(promise);

    if (executing.length >= maxConcurrency) {
      await Promise.race(executing);
      executing.splice(executing.findIndex(p => p === promise), 1);
    }
  }

  await Promise.all(executing);
  return results;
}

캐싱과 병렬 처리는 지연 시간과 비용을 크게 줄입니다.

결론

추론 기술은 에이전트를 단순한 응답자에서 지능적인 문제 해결자로 변환합니다. 선형 문제에는 기본 Chain-of-Thought로 시작한 다음, 탐색을 위한 Tree-of-Thought와 개선을 위한 Self-Reflection을 점진적으로 추가합니다. 오케스트레이터 패턴은 문제 특성에 따라 추론 전략을 동적으로 선택할 수 있게 합니다. 프로덕션 성능을 위해 캐싱, 스트리밍 및 병렬 처리를 구현하는 것을 잊지 마세요. 이러한 패턴은 LangGraph의 상태 관리 및 TypeScript의 타입 안전성과 결합되어 서버리스 환경에서 복잡한 실제 문제를 해결할 수 있는 강력한 추론 시스템을 만듭니다.