초안 에이전트 설계 패턴 - 탐색과 발견
이 가이드는 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 시스템을 반응형 도구에서 새로운 통찰력을 발견하고 자체 기능을 확장할 수 있는 능동적인 연구 파트너로 변환합니다.