초안 에이전트 설계 패턴 - 탐색과 발견

agentslangchainlanggraphtypescriptnextjsaiexploration
By sko X opus 4.19/21/202512 min read

이 가이드는 TypeScript, Next.js 15, LangChain, LangGraph를 Vercel의 서버리스 플랫폼에서 사용하여 자율적인 탐색 및 발견 에이전트를 구축하는 방법을 보여줍니다. 미리 정의된 솔루션 공간을 넘어 새로운 정보를 능동적으로 탐색하고, 숨겨진 패턴을 발견하며, 새로운 통찰력을 생성하는 에이전트를 만들어보겠습니다.

멘탈 모델: 과학 연구실

탐색 에이전트는 가상 연구실을 구성하는 것과 같습니다. 에이전트는 수석 과학자(오케스트레이터) 역할을 하며, 다양한 가설을 탐색하기 위해 전문 연구 보조원(워커 에이전트)을 생성합니다. LangGraph는 실험실 인프라(상태 관리 및 워크플로)를 제공하고, LangChain 도구는 연구 장비 역할을 합니다. 탐색 과정은 과학적 방법을 모방합니다: 가설을 생성하고, 탐색을 통해 테스트하고, 발견을 평가하고, 발견을 기반으로 반복합니다. Vercel의 서버리스 플랫폼은 연구 수요에 따라 확장 또는 축소할 수 있는 확장 가능한 실험실 공간 역할을 합니다.

기본 예제: 가설 주도 탐색기

1. 핵심 탐색 상태 관리

// lib/exploration/types.ts
import { BaseMessage } from '@langchain/core/messages';
import { z } from 'zod';

export const ExplorationStateSchema = z.object({
  query: z.string(),
  currentHypothesis: z.string().optional(),
  explorationDepth: z.number().default(0),
  discoveredPaths: z.array(z.string()).default([]),
  findings: z.array(z.object({
    path: z.string(),
    content: z.string(),
    confidence: z.number(),
    timestamp: z.number()
  })).default([]),
  confidenceThreshold: z.number().default(0.7),
  maxDepth: z.number().default(5),
  status: z.enum(['exploring', 'evaluating', 'backtracking', 'complete']).default('exploring')
});

export type ExplorationState = z.infer<typeof ExplorationStateSchema>;

export interface ExplorationNode {
  id: string;
  hypothesis: string;
  score: number;
  children: ExplorationNode[];
  visited: boolean;
  metadata: Record<string, any>;
}

가설 추적, 발견된 경로, 신뢰도 점수, 트리 기반 탐색 노드를 포함한 탐색 에이전트의 핵심 상태 구조를 정의합니다.

2. 기본 탐색 에이전트

// lib/exploration/basic-explorer.ts
import { StateGraph, MessagesAnnotation } from '@langchain/langgraph';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { TavilySearchResults } from '@langchain/community/tools/tavily_search';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
import { pull } from 'langchain/hub';
import { filter, map, sortBy } from 'es-toolkit';

interface BasicExplorationState {
  messages: BaseMessage[];
  hypothesis: string;
  explorationCount: number;
  discoveries: string[];
}

export async function createBasicExplorer() {
  const model = new ChatGoogleGenerativeAI({
    temperature: 0.7,
    modelName: 'gemini-2.5-flash'
  });

  const searchTool = new TavilySearchResults({ maxResults: 3 });

  // 노드: 가설 생성
  async function generateHypothesis(state: BasicExplorationState) {
    const prompt = await pull('exploration/hypothesis-generator');
    const response = await model.invoke([
      new HumanMessage(`Based on: ${state.messages.slice(-1)[0].content}
        Generate a testable hypothesis for exploration.`)
    ]);

    return {
      hypothesis: response.content,
      messages: [...state.messages, response]
    };
  }

  // 노드: 가설 탐색
  async function exploreHypothesis(state: BasicExplorationState) {
    const searchResults = await searchTool.invoke(state.hypothesis);
    const parsedResults = JSON.parse(searchResults);

    const findings = map(
      filter(parsedResults, (r: any) => r.score > 0.5),
      (result: any) => result.content
    );

    return {
      discoveries: [...state.discoveries, ...findings],
      explorationCount: state.explorationCount + 1,
      messages: [...state.messages, new AIMessage(`Explored: ${findings.length} findings`)]
    };
  }

  // 노드: 발견 평가
  async function evaluateFindings(state: BasicExplorationState) {
    const sortedDiscoveries = sortBy(
      state.discoveries,
      (d: string) => d.length
    );

    const evaluation = await model.invoke([
      new HumanMessage(`Evaluate these discoveries: ${sortedDiscoveries.join('\n')}
        Should we continue exploring or have we found sufficient insights?`)
    ]);

    return {
      messages: [...state.messages, evaluation]
    };
  }

  // 조건부 엣지: 계속 탐색해야 하나?
  function shouldContinue(state: BasicExplorationState) {
    if (state.explorationCount >= 5) return 'end';
    if (state.discoveries.length > 10) return 'evaluate';
    return 'explore';
  }

  // 그래프 구축
  const workflow = new StateGraph<BasicExplorationState>({
    channels: {
      messages: { value: (x: BaseMessage[], y: BaseMessage[]) => [...x, ...y], default: () => [] },
      hypothesis: { value: (x, y) => y || x, default: () => '' },
      explorationCount: { value: (x, y) => y || x, default: () => 0 },
      discoveries: { value: (x: string[], y: string[]) => [...x, ...y], default: () => [] }
    }
  });

  workflow.addNode('generate', generateHypothesis);
  workflow.addNode('explore', exploreHypothesis);
  workflow.addNode('evaluate', evaluateFindings);

  workflow.addEdge('__start__', 'generate');
  workflow.addEdge('generate', 'explore');
  workflow.addConditionalEdges('explore', shouldContinue, {
    'explore': 'generate',
    'evaluate': 'evaluate',
    'end': '__end__'
  });
  workflow.addEdge('evaluate', '__end__');

  return workflow.compile();
}

가설을 생성하고, 검색 도구를 사용하여 탐색하며, 반복 주기에서 발견을 평가하는 기본 탐색 에이전트를 생성합니다.

3. 탐색용 API 라우트

// app/api/explore/route.ts
import { createBasicExplorer } from '@/lib/exploration/basic-explorer';
import { HumanMessage } from '@langchain/core/messages';
import { NextResponse } from 'next/server';

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

export async function POST(req: Request) {
  try {
    const { query, sessionId } = await req.json();

    const explorer = await createBasicExplorer();

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

    // 백그라운드에서 탐색 실행
    (async () => {
      try {
        const eventStream = await explorer.stream({
          messages: [new HumanMessage(query)],
          hypothesis: '',
          explorationCount: 0,
          discoveries: []
        });

        for await (const event of eventStream) {
          const update = {
            type: 'exploration_update',
            hypothesis: event.hypothesis,
            discoveries: event.discoveries?.length || 0,
            status: event.explorationCount < 5 ? 'exploring' : 'complete'
          };

          await writer.write(
            encoder.encode(`data: ${JSON.stringify(update)}\n\n`)
          );
        }
      } catch (error) {
        console.error('Exploration error:', error);
      } finally {
        await writer.close();
      }
    })();

    return new Response(stream.readable, {
      headers: {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
      },
    });
  } catch (error) {
    console.error('API error:', error);
    return NextResponse.json(
      { error: 'Failed to start exploration' },
      { status: 500 }
    );
  }
}

반응형 사용자 경험을 위해 Server-Sent Events를 사용하여 실시간으로 탐색 진행 상황을 스트리밍하는 API 엔드포인트.

4. 프론트엔드 탐색 인터페이스

// components/ExplorationInterface.tsx
'use client';

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

interface ExplorationUpdate {
  type: string;
  hypothesis?: string;
  discoveries?: number;
  status?: string;
}

export default function ExplorationInterface() {
  const [query, setQuery] = useState('');
  const [updates, setUpdates] = useState<ExplorationUpdate[]>([]);
  const [isExploring, setIsExploring] = useState(false);

  const startExploration = useMutation({
    mutationFn: async (explorationQuery: string) => {
      const response = await fetch('/api/explore', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ query: explorationQuery })
      });

      if (!response.ok) throw new Error('Exploration failed');

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

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

        const text = decoder.decode(value);
        const lines = text.split('\n');

        for (const line of lines) {
          if (line.startsWith('data: ')) {
            const data = JSON.parse(line.slice(6));
            setUpdates(prev => [...prev, data]);
          }
        }
      }
    },
    onSuccess: () => {
      setIsExploring(false);
    },
    onError: (error) => {
      console.error('Exploration error:', error);
      setIsExploring(false);
    }
  });

  const handleExplore = useCallback(
    debounce(() => {
      if (query.trim()) {
        setIsExploring(true);
        setUpdates([]);
        startExploration.mutate(query);
      }
    }, 500),
    [query]
  );

  return (
    <div className="card bg-base-100 shadow-xl">
      <div className="card-body">
        <h2 className="card-title">탐색 및 발견 에이전트</h2>

        <div className="form-control">
          <label className="label">
            <span className="label-text">무엇을 탐색하시겠습니까?</span>
          </label>
          <textarea
            className="textarea textarea-bordered h-24"
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder="깊은 탐색을 위한 주제나 질문을 입력하세요..."
          />
        </div>

        <div className="card-actions justify-end">
          <button
            className="btn btn-primary"
            onClick={handleExplore}
            disabled={isExploring || !query.trim()}
          >
            {isExploring ? (
              <>
                <span className="loading loading-spinner"></span>
                탐색 중...
              </>
            ) : '탐색 시작'}
          </button>
        </div>

        {updates.length > 0 && (
          <div className="mt-4">
            <h3 className="font-semibold mb-2">탐색 진행 상황:</h3>
            <div className="space-y-2 max-h-64 overflow-y-auto">
              {updates.map((update, idx) => (
                <div key={idx} className="alert alert-info">
                  <div>
                    <span className="font-semibold">가설:</span> {update.hypothesis}
                    <br />
                    <span className="font-semibold">발견:</span> {update.discoveries}
                    <br />
                    <span className="badge badge-sm">{update.status}</span>
                  </div>
                </div>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

가설 생성 및 발견 추적을 통해 탐색 프로세스의 실시간 시각화를 제공하는 React 컴포넌트.

고급 예제: 멀티 에이전트 과학적 발견 시스템

1. 몬테카를로 트리 탐색 탐색기

// lib/exploration/mcts-explorer.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { mean, sum, maxBy, sample } from 'es-toolkit';
import { v4 as uuidv4 } from 'uuid';

interface MCTSNode {
  id: string;
  state: string;
  value: number;
  visits: number;
  parent?: MCTSNode;
  children: MCTSNode[];
  untried_actions: string[];
}

export class MonteCarloTreeSearchExplorer {
  private model: ChatGoogleGenerativeAI;
  private explorationConstant: number = Math.sqrt(2);
  private maxIterations: number = 100;

  constructor() {
    this.model = new ChatGoogleGenerativeAI({
      temperature: 0.9,
      modelName: 'gemini-2.5-pro'
    });
  }

  async explore(problem: string, iterations: number = 100): Promise<any> {
    this.maxIterations = iterations;
    const root = this.createNode(problem);

    for (let i = 0; i < this.maxIterations; i++) {
      // 선택
      const selectedNode = await this.select(root);

      // 확장
      const expandedNode = await this.expand(selectedNode);

      // 시뮬레이션 (자기 개선)
      const reward = await this.simulate(expandedNode);

      // 역전파
      await this.backpropagate(expandedNode, reward);
    }

    return this.getBestPath(root);
  }

  private createNode(state: string, parent?: MCTSNode): MCTSNode {
    return {
      id: uuidv4(),
      state,
      value: 0,
      visits: 0,
      parent,
      children: [],
      untried_actions: []
    };
  }

  private async select(node: MCTSNode): Promise<MCTSNode> {
    while (node.children.length > 0) {
      const ucbValues = node.children.map(child => this.calculateUCB(child));
      const maxIndex = ucbValues.indexOf(Math.max(...ucbValues));
      node = node.children[maxIndex];
    }
    return node;
  }

  private calculateUCB(node: MCTSNode): number {
    if (node.visits === 0) return Infinity;

    const exploitation = node.value / node.visits;
    const exploration = this.explorationConstant *
      Math.sqrt(Math.log(node.parent!.visits) / node.visits);

    return exploitation + exploration;
  }

  private async expand(node: MCTSNode): Promise<MCTSNode> {
    // LLM을 사용하여 가능한 액션 생성
    const response = await this.model.invoke([{
      role: 'system',
      content: '솔루션 공간을 탐색하고 있습니다. 3개의 다양한 다음 단계를 생성하세요.'
    }, {
      role: 'user',
      content: `현재 상태: ${node.state}\n다음 탐색 단계를 JSON 배열로 생성하세요.`
    }]);

    try {
      const actions = JSON.parse(response.content as string);
      node.untried_actions = actions;

      if (actions.length > 0) {
        const action = sample(actions);
        const childNode = this.createNode(action, node);
        node.children.push(childNode);
        return childNode;
      }
    } catch (e) {
      console.error('액션 파싱 실패:', e);
    }

    return node;
  }

  private async simulate(node: MCTSNode): Promise<number> {
    // 자기 개선: LLM을 사용하여 현재 경로 평가 및 개선
    const response = await this.model.invoke([{
      role: 'system',
      content: '이 탐색 경로를 평가하고 개선 사항을 제안하세요. 품질을 0-1로 평가하세요.'
    }, {
      role: 'user',
      content: `경로: ${this.getPath(node).join(' -> ')}\n평가하고 점수를 매기세요.`
    }]);

    // 응답에서 점수 추출
    const scoreMatch = response.content.toString().match(/\d\.\d+/);
    return scoreMatch ? parseFloat(scoreMatch[0]) : 0.5;
  }

  private async backpropagate(node: MCTSNode, reward: number) {
    let current: MCTSNode | undefined = node;
    while (current) {
      current.visits++;
      current.value += reward;
      current = current.parent;
    }
  }

  private getPath(node: MCTSNode): string[] {
    const path: string[] = [];
    let current: MCTSNode | undefined = node;

    while (current) {
      path.unshift(current.state);
      current = current.parent;
    }

    return path;
  }

  private getBestPath(root: MCTSNode): any {
    const bestChild = maxBy(root.children, child => child.value / child.visits);

    if (!bestChild) return { path: [root.state], score: 0 };

    return {
      path: this.getPath(bestChild),
      score: bestChild.value / bestChild.visits,
      explorations: this.countTotalNodes(root)
    };
  }

  private countTotalNodes(node: MCTSNode): number {
    return 1 + sum(node.children.map(child => this.countTotalNodes(child)));
  }
}

UCB 기반 선택, LLM 기반 확장 및 자기 개선을 통한 지능적 탐색을 위한 몬테카를로 트리 탐색을 구현합니다.

2. 멀티 에이전트 연구실

// lib/exploration/research-laboratory.ts
import { StateGraph, MessagesAnnotation } from '@langchain/langgraph';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { BaseMessage, HumanMessage, AIMessage } from '@langchain/core/messages';
import { groupBy, flatten, uniqBy } from 'es-toolkit';
import { MonteCarloTreeSearchExplorer } from './mcts-explorer';

interface ResearchState {
  messages: BaseMessage[];
  research_topic: string;
  hypotheses: Array<{id: string; content: string; score: number}>;
  literature_review: string;
  experiments: Array<{id: string; design: string; results?: string}>;
  synthesis: string;
  phase: 'planning' | 'literature' | 'hypothesis' | 'experimentation' | 'synthesis';
}

export class ResearchLaboratory {
  private leadModel: ChatGoogleGenerativeAI;
  private workerModel: ChatGoogleGenerativeAI;
  private mctsExplorer: MonteCarloTreeSearchExplorer;

  constructor() {
    this.leadModel = new ChatGoogleGenerativeAI({
      temperature: 0.3,
      modelName: 'gemini-2.5-pro'
    });

    this.workerModel = new ChatGoogleGenerativeAI({
      temperature: 0.7,
      modelName: 'gemini-2.5-flash'
    });

    this.mctsExplorer = new MonteCarloTreeSearchExplorer();
  }

  async createResearchWorkflow() {
    const workflow = new StateGraph<ResearchState>({
      channels: {
        messages: {
          value: (x: BaseMessage[], y: BaseMessage[]) => [...x, ...y],
          default: () => []
        },
        research_topic: {
          value: (x, y) => y || x,
          default: () => ''
        },
        hypotheses: {
          value: (x, y) => y || x,
          default: () => []
        },
        literature_review: {
          value: (x, y) => y || x,
          default: () => ''
        },
        experiments: {
          value: (x, y) => y || x,
          default: () => []
        },
        synthesis: {
          value: (x, y) => y || x,
          default: () => ''
        },
        phase: {
          value: (x, y) => y || x,
          default: () => 'planning'
        }
      }
    });

    // 계획 에이전트
    workflow.addNode('planner', async (state) => {
      const plan = await this.leadModel.invoke([
        new HumanMessage(`연구 계획 작성: ${state.research_topic}
          포함: 1) 조사할 주요 영역 2) 방법론 3) 성공 지표`)
      ]);

      return {
        messages: [...state.messages, plan],
        phase: 'literature' as const
      };
    });

    // 문헌 검토 에이전트
    workflow.addNode('literature_reviewer', async (state) => {
      // 여러 에이전트에 의한 병렬 문헌 검토 시뮬레이션
      const reviewPromises = Array.from({ length: 3 }, async (_, i) => {
        const review = await this.workerModel.invoke([
          new HumanMessage(`${state.research_topic}에 대한 문헌 검토
            초점 영역 ${i + 1}: ${['이론적 기초', '최근 발전', '미해결 문제'][i]}`)
        ]);
        return review.content;
      });

      const reviews = await Promise.all(reviewPromises);
      const combinedReview = reviews.join('\n\n');

      return {
        literature_review: combinedReview,
        messages: [...state.messages, new AIMessage(combinedReview)],
        phase: 'hypothesis' as const
      };
    });

    // 가설 생성 에이전트 (MCTS 사용)
    workflow.addNode('hypothesis_generator', async (state) => {
      const explorationResult = await this.mctsExplorer.explore(
        `가설 생성: ${state.research_topic}\n기반: ${state.literature_review}`,
        50
      );

      const hypotheses = explorationResult.path.map((h: string, idx: number) => ({
        id: `hyp_${idx}`,
        content: h,
        score: Math.random() * (1 - 0.5) + 0.5 // 점수 시뮬레이션
      }));

      return {
        hypotheses: hypotheses,
        messages: [...state.messages, new AIMessage(`${hypotheses.length}개의 가설을 생성했습니다`)],
        phase: 'experimentation' as const
      };
    });

    // 실험 설계 에이전트
    workflow.addNode('experimenter', async (state) => {
      const topHypotheses = state.hypotheses
        .sort((a, b) => b.score - a.score)
        .slice(0, 3);

      const experiments = await Promise.all(
        topHypotheses.map(async (hyp) => {
          const design = await this.workerModel.invoke([
            new HumanMessage(`테스트할 실험 설계: ${hyp.content}`)
          ]);

          return {
            id: hyp.id,
            design: design.content as string,
            results: `${hyp.id}에 대한 시뮬레이션 결과`
          };
        })
      );

      return {
        experiments: experiments,
        messages: [...state.messages, new AIMessage(`${experiments.length}개의 실험을 설계했습니다`)],
        phase: 'synthesis' as const
      };
    });

    // 종합 에이전트
    workflow.addNode('synthesizer', async (state) => {
      const synthesis = await this.leadModel.invoke([
        new HumanMessage(`연구 결과 종합:
          주제: ${state.research_topic}
          문헌: ${state.literature_review}
          가설: ${JSON.stringify(state.hypotheses)}
          실험: ${JSON.stringify(state.experiments)}

          포괄적인 통찰력과 결론을 제공하세요.`)
      ]);

      return {
        synthesis: synthesis.content as string,
        messages: [...state.messages, synthesis]
      };
    });

    // 워크플로우 엣지 정의
    workflow.addEdge('__start__', 'planner');
    workflow.addEdge('planner', 'literature_reviewer');
    workflow.addEdge('literature_reviewer', 'hypothesis_generator');
    workflow.addEdge('hypothesis_generator', 'experimenter');
    workflow.addEdge('experimenter', 'synthesizer');
    workflow.addEdge('synthesizer', '__end__');

    return workflow.compile();
  }
}

계획, 문헌 검토, MCTS를 사용한 가설 생성, 실험, 종합을 위한 전문 에이전트가 있는 완전한 멀티 에이전트 연구실을 구현합니다.

3. 상태 지속성을 갖춘 서버리스 탐색

// lib/exploration/serverless-explorer.ts
import { kv } from '@vercel/kv';
import { Queue } from 'bullmq';
import { chunk, throttle } from 'es-toolkit';

interface ExplorationChunk {
  id: string;
  sessionId: string;
  chunkIndex: number;
  totalChunks: number;
  state: any;
  timestamp: number;
}

export class ServerlessExplorer {
  private maxExecutionTime = 777; // 초 (안전 버퍼 포함)
  private checkpointInterval = 60; // 초

  async executeWithCheckpoints(
    explorationFn: () => Promise<any>,
    sessionId: string
  ) {
    const startTime = Date.now();
    const checkpointKey = `exploration:${sessionId}`;

    // 이전 상태 복원 시도
    const previousState = await kv.get<ExplorationChunk>(checkpointKey);
    let currentState = previousState?.state || {};

    const executeWithTimeout = async () => {
      const elapsedSeconds = (Date.now() - startTime) / 1000;

      if (elapsedSeconds >= this.maxExecutionTime - 30) {
        // 상태 저장 및 계속 스케줄
        await this.saveCheckpoint(sessionId, currentState);
        await this.scheduleContinuation(sessionId);
        return { status: 'paused', state: currentState };
      }

      // 탐색 청크 실행
      const result = await explorationFn();
      currentState = { ...currentState, ...result };

      // 주기적 체크포인트
      if (elapsedSeconds % this.checkpointInterval < 1) {
        await this.saveCheckpoint(sessionId, currentState);
      }

      return { status: 'continuing', state: currentState };
    };

    // 속도 제한을 방지하기 위해 실행을 조절
    const throttledExecute = throttle(executeWithTimeout, 1000);

    let status = 'continuing';
    while (status === 'continuing') {
      const result = await throttledExecute();
      status = result.status;
      currentState = result.state;
    }

    return currentState;
  }

  private async saveCheckpoint(sessionId: string, state: any) {
    const checkpoint: ExplorationChunk = {
      id: `checkpoint_${Date.now()}`,
      sessionId,
      chunkIndex: state.chunkIndex || 0,
      totalChunks: state.totalChunks || 1,
      state,
      timestamp: Date.now()
    };

    await kv.set(
      `exploration:${sessionId}`,
      checkpoint,
      { ex: 3600 } // 1시간 만료
    );
  }

  private async scheduleContinuation(sessionId: string) {
    // 스케줄링을 위해 Inngest 또는 유사한 것 사용
    await fetch('/api/schedule', {
      method: 'POST',
      body: JSON.stringify({
        event: 'exploration.continue',
        data: { sessionId },
        delay: '5s'
      })
    });
  }
}

// 청크 탐색용 API 라우트
export async function POST(req: Request) {
  const { sessionId, query } = await req.json();
  const explorer = new ServerlessExplorer();

  const explorationTasks = chunk(
    Array.from({ length: 20 }, (_, i) => i),
    5
  );

  const result = await explorer.executeWithCheckpoints(
    async () => {
      // 탐색의 한 청크 실행
      const tasks = explorationTasks.shift();
      if (!tasks) return { complete: true };

      const results = await Promise.all(
        tasks.map(async (taskId) => {
          // 탐색 작업 시뮬레이션
          return { taskId, result: `발견 ${taskId}` };
        })
      );

      return {
        chunkIndex: (explorationTasks.length || 0) + 1,
        discoveries: results
      };
    },
    sessionId
  );

  return new Response(JSON.stringify(result), {
    headers: { 'Content-Type': 'application/json' }
  });
}

장시간 실행되는 탐색을 위한 자동 체크포인팅, 상태 지속성 및 작업 계속을 갖춘 서버리스 인식 탐색을 구현합니다.

4. 벡터 메모리 통합

// lib/exploration/memory-system.ts
import { Pinecone } from '@pinecone-database/pinecone';
import { GoogleGenerativeAIEmbeddings } from '@langchain/google-genai';
import { Document } from '@langchain/core/documents';
import { groupBy, sortBy, take } from 'es-toolkit';

export class ExplorationMemorySystem {
  private pinecone: Pinecone;
  private embeddings: GoogleGenerativeAIEmbeddings;
  private indexName = 'exploration-memory';

  constructor() {
    this.pinecone = new Pinecone({
      apiKey: process.env.PINECONE_API_KEY!
    });

    this.embeddings = new GoogleGenerativeAIEmbeddings({
      modelName: 'embedding-001'
    });
  }

  async storeDiscovery(
    content: string,
    metadata: {
      sessionId: string;
      hypothesis: string;
      confidence: number;
      timestamp: number;
      explorationPath: string[];
    }
  ) {
    const embedding = await this.embeddings.embedQuery(content);
    const index = this.pinecone.index(this.indexName);

    await index.upsert([{
      id: `discovery_${metadata.sessionId}_${metadata.timestamp}`,
      values: embedding,
      metadata: {
        ...metadata,
        content: content.slice(0, 1000) // 미리보기 저장
      }
    }]);
  }

  async querySemanticMemory(
    query: string,
    filters?: Record<string, any>
  ): Promise<Document[]> {
    const queryEmbedding = await this.embeddings.embedQuery(query);
    const index = this.pinecone.index(this.indexName);

    const results = await index.query({
      vector: queryEmbedding,
      topK: 20,
      includeMetadata: true,
      filter: filters
    });

    // 가설별로 그룹화하고 각 그룹에서 최고 선택
    const grouped = groupBy(
      results.matches,
      (match: any) => match.metadata.hypothesis
    );

    const diverseResults = Object.values(grouped).map(group => {
      const sorted = sortBy(group, (m: any) => -m.score);
      return sorted[0];
    });

    return take(diverseResults, 10).map(match =>
      new Document({
        pageContent: match.metadata.content,
        metadata: match.metadata
      })
    );
  }

  async getExplorationPattern(sessionId: string) {
    const index = this.pinecone.index(this.indexName);

    const results = await index.query({
      vector: new Array(768).fill(0), // Gemini 임베딩용 더미 벡터
      topK: 100,
      includeMetadata: true,
      filter: { sessionId }
    });

    // 탐색 패턴 분석
    const pathFrequency = new Map<string, number>();
    results.matches.forEach(match => {
      const path = match.metadata.explorationPath?.join(' -> ') || '';
      pathFrequency.set(path, (pathFrequency.get(path) || 0) + 1);
    });

    return {
      totalDiscoveries: results.matches.length,
      avgConfidence: results.matches.reduce(
        (sum, m) => sum + (m.metadata.confidence || 0), 0
      ) / results.matches.length,
      mostFrequentPaths: Array.from(pathFrequency.entries())
        .sort((a, b) => b[1] - a[1])
        .slice(0, 5)
    };
  }
}

장기 탐색 패턴 학습을 위한 Pinecone 벡터 데이터베이스를 사용한 의미론적 메모리 저장 및 검색을 구현합니다.

5. 실시간 시각화를 갖춘 고급 탐색 UI

// components/AdvancedExplorationUI.tsx
'use client';

import { useState, useEffect } from 'react';
import { useQuery, useMutation } from '@tanstack/react-query';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { debounce } from 'es-toolkit';

interface ExplorationMetrics {
  timestamp: number;
  depth: number;
  discoveries: number;
  confidence: number;
}

export default function AdvancedExplorationUI() {
  const [topic, setTopic] = useState('');
  const [metrics, setMetrics] = useState<ExplorationMetrics[]>([]);
  const [currentPhase, setCurrentPhase] = useState('idle');
  const [hypotheses, setHypotheses] = useState<Array<{id: string; content: string; score: number}>>([]);

  const startResearch = useMutation({
    mutationFn: async (researchTopic: string) => {
      const response = await fetch('/api/research-lab', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ topic: researchTopic })
      });

      if (!response.ok) throw new Error('연구 실패');

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

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

        const text = decoder.decode(value);
        const lines = text.split('\n');

        for (const line of lines) {
          if (line.startsWith('data: ')) {
            const data = JSON.parse(line.slice(6));

            // 단계 업데이트
            if (data.phase) setCurrentPhase(data.phase);

            // 가설 업데이트
            if (data.hypotheses) setHypotheses(data.hypotheses);

            // 메트릭 업데이트
            if (data.metrics) {
              setMetrics(prev => [...prev, {
                timestamp: Date.now(),
                depth: data.metrics.depth,
                discoveries: data.metrics.discoveries,
                confidence: data.metrics.confidence
              }]);
            }
          }
        }
      }
    }
  });

  const memoryStats = useQuery({
    queryKey: ['memory-stats', topic],
    queryFn: async () => {
      const response = await fetch(`/api/memory-stats?topic=${encodeURIComponent(topic)}`);
      return response.json();
    },
    enabled: !!topic,
    refetchInterval: 5000
  });

  return (
    <div className="min-h-screen bg-base-200 p-4">
      <div className="max-w-7xl mx-auto">
        <div className="text-center mb-8">
          <h1 className="text-5xl font-bold mb-2">연구실</h1>
          <p className="text-xl">멀티 에이전트 과학적 발견 시스템</p>
        </div>

        <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
          {/* 제어판 */}
          <div className="lg:col-span-1">
            <div className="card bg-base-100 shadow-xl">
              <div className="card-body">
                <h2 className="card-title">연구 제어</h2>

                <div className="form-control">
                  <label className="label">
                    <span className="label-text">연구 주제</span>
                  </label>
                  <input
                    type="text"
                    className="input input-bordered"
                    value={topic}
                    onChange={(e) => setTopic(e.target.value)}
                    placeholder="연구 주제 입력..."
                  />
                </div>

                <button
                  className="btn btn-primary mt-4"
                  onClick={() => startResearch.mutate(topic)}
                  disabled={!topic || startResearch.isPending}
                >
                  {startResearch.isPending ? (
                    <>
                      <span className="loading loading-spinner"></span>
                      연구 중...
                    </>
                  ) : '연구 시작'}
                </button>

                <div className="divider"></div>

                <div className="stats stats-vertical shadow">
                  <div className="stat">
                    <div className="stat-title">현재 단계</div>
                    <div className="stat-value text-primary">{currentPhase}</div>
                  </div>
                  <div className="stat">
                    <div className="stat-title">가설</div>
                    <div className="stat-value">{hypotheses.length}</div>
                  </div>
                  <div className="stat">
                    <div className="stat-title">메모리 항목</div>
                    <div className="stat-value">{memoryStats.data?.totalEntries || 0}</div>
                  </div>
                </div>
              </div>
            </div>
          </div>

          {/* 시각화 패널 */}
          <div className="lg:col-span-2">
            <div className="card bg-base-100 shadow-xl">
              <div className="card-body">
                <h2 className="card-title">탐색 메트릭</h2>

                <ResponsiveContainer width="100%" height={300}>
                  <LineChart data={metrics}>
                    <CartesianGrid strokeDasharray="3 3" />
                    <XAxis dataKey="timestamp" />
                    <YAxis />
                    <Tooltip />
                    <Line
                      type="monotone"
                      dataKey="confidence"
                      stroke="#8884d8"
                      name="신뢰도"
                    />
                    <Line
                      type="monotone"
                      dataKey="discoveries"
                      stroke="#82ca9d"
                      name="발견"
                    />
                    <Line
                      type="monotone"
                      dataKey="depth"
                      stroke="#ffc658"
                      name="깊이"
                    />
                  </LineChart>
                </ResponsiveContainer>

                <div className="divider"></div>

                {/* 가설 목록 */}
                <h3 className="font-semibold mb-2">생성된 가설</h3>
                <div className="space-y-2 max-h-64 overflow-y-auto">
                  {hypotheses.map((hyp) => (
                    <div key={hyp.id} className="alert">
                      <div className="flex-1">
                        <p className="font-semibold">{hyp.content}</p>
                        <div className="flex items-center gap-2 mt-1">
                          <span className="badge badge-sm">점수: {hyp.score.toFixed(2)}</span>
                          <progress
                            className="progress progress-primary w-24"
                            value={hyp.score}
                            max="1"
                          ></progress>
                        </div>
                      </div>
                    </div>
                  ))}
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

포괄적인 탐색 모니터링을 위한 실시간 메트릭 시각화, 가설 추적 및 메모리 통계를 갖춘 고급 UI 컴포넌트.

결론

이 구현은 TypeScript, LangChain, LangGraph를 Vercel의 서버리스 플랫폼에서 사용하여 복잡한 문제 공간을 자율적으로 탐색하는 프로덕션 준비 탐색 및 발견 에이전트를 보여줍니다. 제시된 패턴을 통해 에이전트는 가설을 생성하고, 몬테카를로 트리 탐색을 사용하여 솔루션 공간을 탐색하며, 멀티 에이전트 연구 팀을 조정하고, 지속적인 학습을 위한 의미론적 메모리를 유지할 수 있습니다. 주요 아키텍처 결정 사항에는 실시간 스트리밍을 위한 Server-Sent Events 사용, 서버리스 제약 내에서 장시간 실행되는 탐색을 위한 체크포인팅 구현, 패턴 인식을 위한 벡터 데이터베이스 활용, 효율적인 데이터 조작을 위한 es-toolkit 사용이 포함됩니다. 이러한 탐색 에이전트는 AI 시스템을 반응형 도구에서 새로운 통찰력을 발견하고 자체 기능을 확장할 수 있는 능동적인 연구 파트너로 변환합니다.