초안 에이전트 설계 패턴 - 학습과 적응

agentic-designlangchainlanggraphaitypescriptmachine-learning
By sko X opus 4.19/20/20257 min read

TypeScript, LangGraph 및 Vercel의 서버리스 플랫폼에 최적화된 최신 메모리 아키텍처를 사용하여 경험을 통해 실제로 개선되는 에이전트를 구축하는 방법을 알아봅니다.

멘탈 모델: 진화하는 레스토랑

학습 에이전트를 고객에 적응하는 레스토랑으로 생각해보세요. 처음에는 일반적인 요리(기본 응답)를 제공합니다. 시간이 지나면서 단골 고객의 선호도(사용자별 적응)를 학습하고, 인기 있는 조합(패턴 인식)을 발견하며, 피드백을 기반으로 레시피를 조정하고(강화학습), 심지어 과거의 유사한 고객을 기반으로 신규 고객이 무엇을 즐길지 예측할 수도 있습니다(전이학습). 성공적인 레스토랑이 정적 메뉴에서 동적이고 고객을 인식하는 경험으로 진화하는 것처럼, 여러분의 에이전트는 단순한 응답자에서 모든 상호작용마다 개선되는 지능형 시스템으로 변화합니다.

기본 예제: 메모리를 갖춘 적응형 챗 에이전트

사용자 선호도를 기억하고 그에 따라 응답을 적응시키는 간단한 에이전트를 구축해보겠습니다.

// app/api/adaptive-agent/route.ts
import { StateGraph, MemorySaver } from "@langchain/langgraph";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
import { Redis } from "@upstash/redis";
import { groupBy, maxBy, sortBy } from "es-toolkit";
import { z } from "zod";

// 적응 상태 스키마 정의
const AdaptiveStateSchema = z.object({
  messages: z.array(z.any()),
  userPreferences: z.object({
    style: z.enum(['concise', 'detailed', 'technical']).optional(),
    topics: z.array(z.string()).optional(),
  }).default({}),
  interactionCount: z.number().default(0),
  feedbackScores: z.array(z.number()).default([]),
});

type AdaptiveState = z.infer<typeof AdaptiveStateSchema>;

// 영구 메모리를 위한 Redis 초기화
const redis = new Redis({
  url: process.env.UPSTASH_REDIS_URL!,
  token: process.env.UPSTASH_REDIS_TOKEN!,
});

// 적응형 에이전트 생성
async function createAdaptiveAgent() {
  const model = new ChatGoogleGenerativeAI({
    temperature: 0.7,
    modelName: "gemini-pro",
  });

  // 대화 지속성을 위한 메모리 세이버
  const checkpointer = new MemorySaver();

  // 에이전트 로직 정의
  async function agentNode(state: AdaptiveState) {
    const { messages, userPreferences } = state;

    // 학습된 선호도를 기반으로 프롬프트 적응
    const systemPrompt = `당신은 도움이 되는 어시스턴트입니다.
      ${userPreferences.style ? `${userPreferences.style} 방식으로 응답하세요.` : ''}
      ${userPreferences.topics?.length ? `사용자는 다음에 관심이 있습니다: ${userPreferences.topics.join(', ')}` : ''}
      상호작용 횟수: ${state.interactionCount}`;

    const response = await model.invoke([
      { role: "system", content: systemPrompt },
      ...messages
    ]);

    return {
      messages: [...messages, response],
      interactionCount: state.interactionCount + 1,
    };
  }

  // 그래프 구축
  const workflow = new StateGraph<AdaptiveState>({
    channels: AdaptiveStateSchema.shape,
  })
    .addNode("agent", agentNode)
    .setEntryPoint("agent")
    .setFinishPoint("agent");

  return workflow.compile({ checkpointer });
}

// API 핸들러
export async function POST(req: Request) {
  const { message, sessionId, feedback } = await req.json();

  // 사용자 상태 검색 또는 초기화
  const storedState = await redis.get(`session:${sessionId}`) as AdaptiveState | null;
  const initialState: AdaptiveState = storedState || {
    messages: [],
    userPreferences: {},
    interactionCount: 0,
    feedbackScores: [],
  };

  // 피드백이 제공된 경우 처리
  if (feedback) {
    initialState.feedbackScores.push(feedback);

    // 간단한 선호도 학습
    if (initialState.feedbackScores.length > 3) {
      const avgScore = initialState.feedbackScores.reduce((a, b) => a + b) / initialState.feedbackScores.length;
      if (avgScore < 3) {
        initialState.userPreferences.style = 'detailed';
      } else if (avgScore > 4) {
        initialState.userPreferences.style = 'concise';
      }
    }
  }

  // 에이전트 실행
  const agent = await createAdaptiveAgent();
  const result = await agent.invoke(
    {
      ...initialState,
      messages: [...initialState.messages, new HumanMessage(message)],
    },
    {
      configurable: { thread_id: sessionId },
    }
  );

  // 업데이트된 상태 저장
  await redis.set(`session:${sessionId}`, result, {
    ex: 86400 * 7, // 7일 TTL
  });

  return Response.json({
    response: result.messages[result.messages.length - 1].content,
    preferences: result.userPreferences,
    interactionCount: result.interactionCount,
  });
}

이 기본 에이전트는 사용자 상호작용을 추적하고, 피드백 점수에서 학습하며, 응답 스타일을 적응시킵니다. 서버리스 호환 영구 저장소로 Upstash Redis를 사용합니다.

// components/AdaptiveChatInterface.tsx
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { debounce } from 'es-toolkit';

interface ChatResponse {
  response: string;
  preferences: any;
  interactionCount: number;
}

export function AdaptiveChatInterface() {
  const [message, setMessage] = useState('');
  const [sessionId] = useState(() => crypto.randomUUID());
  const [lastResponse, setLastResponse] = useState<string>('');

  const chatMutation = useMutation({
    mutationFn: async (params: { message: string; feedback?: number }) => {
      const response = await fetch('/api/adaptive-agent', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ ...params, sessionId }),
      });
      return response.json() as Promise<ChatResponse>;
    },
    onSuccess: (data) => {
      setLastResponse(data.response);
    },
  });

  // 디바운스된 피드백 핸들러
  const handleFeedback = debounce((score: number) => {
    chatMutation.mutate({ message: '', feedback: score });
  }, 500);

  return (
    <div className="card bg-base-100 shadow-xl">
      <div className="card-body">
        <h2 className="card-title">적응형 챗 에이전트</h2>

        {lastResponse && (
          <div className="alert alert-info">
            <span>{lastResponse}</span>
          </div>
        )}

        <div className="form-control">
          <input
            type="text"
            placeholder="무엇이든 물어보세요..."
            className="input input-bordered"
            value={message}
            onChange={(e) => setMessage(e.target.value)}
            onKeyPress={(e) => {
              if (e.key === 'Enter') {
                chatMutation.mutate({ message });
                setMessage('');
              }
            }}
          />
        </div>

        {lastResponse && (
          <div className="rating rating-lg">
            {[1, 2, 3, 4, 5].map((score) => (
              <input
                key={score}
                type="radio"
                name="rating"
                className="mask mask-star-2 bg-orange-400"
                onClick={() => handleFeedback(score)}
              />
            ))}
          </div>
        )}

        <div className="stats shadow">
          <div className="stat">
            <div className="stat-title">상호작용</div>
            <div className="stat-value text-primary">
              {chatMutation.data?.interactionCount || 0}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

프론트엔드는 별점 평가 시스템을 통해 사용자 피드백을 수집하고 상호작용 통계를 표시하여 완전한 피드백 루프를 생성합니다.

고급 예제: 경험 재생을 갖춘 다중 에이전트 학습 시스템

이제 공유 경험에서 학습하는 여러 전문 에이전트가 포함된 정교한 시스템을 구축해보겠습니다.

// lib/memory/experience-store.ts
import { PineconeStore } from "@langchain/pinecone";
import { GoogleGenerativeAIEmbeddings } from "@langchain/google-genai";
import { Pinecone } from "@pinecone-database/pinecone";
import { chunk, sortBy, take, groupBy, maxBy } from "es-toolkit";
import { z } from "zod";

// 경험 스키마
const ExperienceSchema = z.object({
  id: z.string(),
  interaction: z.object({
    input: z.string(),
    context: z.record(z.any()),
    agentType: z.string(),
  }),
  outcome: z.object({
    response: z.string(),
    success: z.boolean(),
    metrics: z.object({
      latency: z.number(),
      tokenCount: z.number(),
      userSatisfaction: z.number().optional(),
    }),
  }),
  timestamp: z.string(),
  embedding: z.array(z.number()).optional(),
});

type Experience = z.infer<typeof ExperienceSchema>;

export class ExperienceReplayBuffer {
  private vectorStore: PineconeStore;
  private embeddings: GoogleGenerativeAIEmbeddings;
  private bufferSize = 1000;
  private priorityAlpha = 0.6; // 우선순위 지수

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

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

    this.vectorStore = new PineconeStore(this.embeddings, {
      pineconeIndex: pinecone.index(process.env.PINECONE_INDEX!),
      namespace: "experiences",
    });
  }

  async store(experience: Experience): Promise<void> {
    // TD 오류 또는 결과 메트릭을 기반으로 우선순위 계산
    const priority = this.calculatePriority(experience);

    await this.vectorStore.addDocuments([
      {
        pageContent: JSON.stringify({
          interaction: experience.interaction,
          outcome: experience.outcome,
        }),
        metadata: {
          id: experience.id,
          timestamp: experience.timestamp,
          priority,
          agentType: experience.interaction.agentType,
          success: experience.outcome.success,
          userSatisfaction: experience.outcome.metrics.userSatisfaction,
        },
      },
    ]);

    // 버퍼 크기 유지
    await this.pruneOldExperiences();
  }

  async sample(
    context: any,
    k: number = 5,
    strategy: 'uniform' | 'prioritized' = 'prioritized'
  ): Promise<Experience[]> {
    const query = JSON.stringify(context);
    const results = await this.vectorStore.similaritySearch(query, k * 2);

    if (strategy === 'prioritized') {
      // 우선순위 경험 재생
      const experiences = results.map(doc => ({
        ...JSON.parse(doc.pageContent),
        priority: doc.metadata.priority || 1,
      }));

      // 우선순위 기반 샘플링
      const sorted = sortBy(experiences, exp => -exp.priority);
      return take(sorted, k);
    }

    return take(results.map(doc => JSON.parse(doc.pageContent)), k);
  }

  private calculatePriority(experience: Experience): number {
    // 예상 밖의 결과에 더 높은 우선순위
    const successWeight = experience.outcome.success ? 0.3 : 0.7;
    const satisfactionWeight = experience.outcome.metrics.userSatisfaction
      ? (5 - experience.outcome.metrics.userSatisfaction) / 5
      : 0.5;

    return Math.pow(successWeight + satisfactionWeight, this.priorityAlpha);
  }

  private async pruneOldExperiences(): Promise<void> {
    // 버퍼 크기 유지를 위한 구현
    // 제한을 초과하면 가장 오래된 경험 제거
  }
}

// lib/agents/specialized-agents.ts
import { StateGraph } from "@langchain/langgraph";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { ExperienceReplayBuffer } from "./memory/experience-store";
import { filter, map, reduce } from "es-toolkit";

interface LearningAgentState {
  messages: any[];
  experiences: any[];
  adaptationMetrics: {
    successRate: number;
    avgLatency: number;
    confidenceScore: number;
  };
}

class SpecializedLearningAgent {
  private model: ChatGoogleGenerativeAI;
  private experienceBuffer: ExperienceReplayBuffer;
  private agentType: string;
  private learningRate = 0.1;

  constructor(agentType: 'researcher' | 'coder' | 'analyst') {
    this.agentType = agentType;
    this.model = new ChatGoogleGenerativeAI({
      temperature: 0.3,
      modelName: "gemini-pro",
    });
    this.experienceBuffer = new ExperienceReplayBuffer();
  }

  async createWorkflow() {
    const workflow = new StateGraph<LearningAgentState>({
      channels: {
        messages: { value: (x: any[], y: any[]) => [...x, ...y] },
        experiences: { value: (x: any[], y: any[]) => [...x, ...y] },
        adaptationMetrics: {
          value: (x: any, y: any) => ({ ...x, ...y })
        },
      },
    });

    // 관련 경험 검색
    workflow.addNode("retrieve_experiences", async (state) => {
      const context = state.messages[state.messages.length - 1];
      const experiences = await this.experienceBuffer.sample(context, 5);

      return { experiences };
    });

    // 경험을 기반으로 행동 적응
    workflow.addNode("adapt_strategy", async (state) => {
      const successfulExperiences = filter(
        state.experiences,
        exp => exp.outcome.success
      );

      // 적응 메트릭 계산
      const successRate = successfulExperiences.length / state.experiences.length;
      const avgLatency = reduce(
        state.experiences,
        (acc, exp) => acc + exp.outcome.metrics.latency,
        0
      ) / state.experiences.length;

      // 최적 전략 결정
      const strategy = this.determineStrategy(successfulExperiences);

      return {
        adaptationMetrics: {
          successRate,
          avgLatency,
          confidenceScore: this.calculateConfidence(state.experiences),
        },
      };
    });

    // 학습된 행동으로 실행
    workflow.addNode("execute", async (state) => {
      const systemPrompt = this.buildAdaptivePrompt(
        state.experiences,
        state.adaptationMetrics
      );

      const startTime = Date.now();
      const response = await this.model.invoke([
        { role: "system", content: systemPrompt },
        ...state.messages,
      ]);
      const latency = Date.now() - startTime;

      // 이 상호작용을 새로운 경험으로 저장
      const experience = {
        id: crypto.randomUUID(),
        interaction: {
          input: state.messages[state.messages.length - 1].content,
          context: state.adaptationMetrics,
          agentType: this.agentType,
        },
        outcome: {
          response: response.content,
          success: true, // 피드백을 기반으로 업데이트됨
          metrics: {
            latency,
            tokenCount: response.usage?.total_tokens || 0,
          },
        },
        timestamp: new Date().toISOString(),
      };

      await this.experienceBuffer.store(experience);

      return {
        messages: [response],
      };
    });

    // 노드 연결
    workflow
      .addEdge("retrieve_experiences", "adapt_strategy")
      .addEdge("adapt_strategy", "execute")
      .setEntryPoint("retrieve_experiences")
      .setFinishPoint("execute");

    return workflow.compile();
  }

  private determineStrategy(experiences: any[]): string {
    // 패턴별로 경험 그룹화
    const patterns = groupBy(experiences, exp =>
      exp.interaction.input.split(' ')[0].toLowerCase()
    );

    // 가장 성공적인 패턴 찾기
    const bestPattern = maxBy(
      Object.entries(patterns),
      ([_, exps]) => filter(exps, e => e.outcome.success).length
    );

    return bestPattern ? bestPattern[0] : 'default';
  }

  private calculateConfidence(experiences: any[]): number {
    if (experiences.length === 0) return 0.5;

    const weights = experiences.map((_, idx) =>
      Math.exp(-idx * 0.5) // 최신성을 위한 지수 감쇠
    );

    const weightedSuccess = reduce(
      experiences,
      (acc, exp, idx) => acc + (exp.outcome.success ? weights[idx] : 0),
      0
    );

    return weightedSuccess / reduce(weights, (a, b) => a + b, 0);
  }

  private buildAdaptivePrompt(
    experiences: any[],
    metrics: any
  ): string {
    const successfulPatterns = filter(
      experiences,
      exp => exp.outcome.success
    ).map(exp => exp.interaction.input);

    return `당신은 전문 ${this.agentType} 에이전트입니다.

      과거 상호작용을 기반으로:
      - 성공률: ${(metrics.successRate * 100).toFixed(1)}%
      - 평균 응답 시간: ${metrics.avgLatency}ms
      - 신뢰도 수준: ${(metrics.confidenceScore * 100).toFixed(1)}%

      관찰된 성공 패턴:
      ${successfulPatterns.slice(0, 3).join('\n')}

      이러한 학습을 기반으로 응답 스타일과 접근 방식을 적응시키세요.`;
  }
}

// lib/agents/multi-agent-coordinator.ts
export class MultiAgentLearningCoordinator {
  private agents: Map<string, SpecializedLearningAgent>;
  private routingModel: ChatGoogleGenerativeAI;

  constructor() {
    this.agents = new Map([
      ['researcher', new SpecializedLearningAgent('researcher')],
      ['coder', new SpecializedLearningAgent('coder')],
      ['analyst', new SpecializedLearningAgent('analyst')],
    ]);

    this.routingModel = new ChatGoogleGenerativeAI({
      temperature: 0,
      modelName: "gemini-pro",
    });
  }

  async route(input: string): Promise<string> {
    const routingPrompt = `이 요청을 처리해야 하는 전문 에이전트를 결정하세요:
      - researcher: 정보 수집, 사실 확인, 연구 작업용
      - coder: 프로그래밍, 디버깅, 코드 생성용
      - analyst: 데이터 분석, 인사이트, 전략 계획용

      입력: "${input}"

      에이전트 이름만으로 응답하세요.`;

    const response = await this.routingModel.invoke(routingPrompt);
    return response.content.trim().toLowerCase();
  }

  async process(input: string, sessionId: string): Promise<any> {
    // 사용할 에이전트 결정
    const agentType = await this.route(input);
    const agent = this.agents.get(agentType);

    if (!agent) {
      throw new Error(`알 수 없는 에이전트 유형: ${agentType}`);
    }

    const workflow = await agent.createWorkflow();
    const result = await workflow.invoke({
      messages: [{ role: "user", content: input }],
      experiences: [],
      adaptationMetrics: {
        successRate: 0.5,
        avgLatency: 0,
        confidenceScore: 0.5,
      },
    });

    return {
      response: result.messages[result.messages.length - 1].content,
      agentType,
      metrics: result.adaptationMetrics,
    };
  }
}

// app/api/learning-system/route.ts
import { MultiAgentLearningCoordinator } from "@/lib/agents/multi-agent-coordinator";
import { NextRequest } from "next/server";

const coordinator = new MultiAgentLearningCoordinator();

export async function POST(req: NextRequest) {
  // 서버리스용 백그라운드 콜백 비활성화
  process.env.LANGCHAIN_CALLBACKS_BACKGROUND = "false";

  const { message, sessionId, feedback } = await req.json();

  try {
    const result = await coordinator.process(message, sessionId);

    return Response.json({
      success: true,
      ...result,
    });
  } catch (error) {
    console.error("학습 시스템 오류:", error);
    return Response.json(
      { success: false, error: "처리 실패" },
      { status: 500 }
    );
  }
}

이 고급 시스템은 우선순위 경험 재생, 다중 에이전트 조정 및 과거 성능을 기반으로 한 적응 전략 선택을 구현합니다.

// components/LearningSystemDashboard.tsx
import { useState } from 'react';
import { useQuery, useMutation } from '@tanstack/react-query';
import { groupBy, sortBy } from 'es-toolkit';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';

interface SystemMetrics {
  agentType: string;
  metrics: {
    successRate: number;
    avgLatency: number;
    confidenceScore: number;
  };
}

export function LearningSystemDashboard() {
  const [sessionId] = useState(() => crypto.randomUUID());
  const [history, setHistory] = useState<SystemMetrics[]>([]);

  const sendMessage = useMutation({
    mutationFn: async (message: string) => {
      const response = await fetch('/api/learning-system', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message, sessionId }),
      });
      return response.json();
    },
    onSuccess: (data) => {
      setHistory(prev => [...prev, {
        agentType: data.agentType,
        metrics: data.metrics,
      }]);
    },
  });

  // 성능 추세 계산
  const performanceData = history.map((item, index) => ({
    interaction: index + 1,
    confidence: item.metrics.confidenceScore * 100,
    success: item.metrics.successRate * 100,
  }));

  // 에이전트 사용 분포
  const agentUsage = Object.entries(
    groupBy(history, item => item.agentType)
  ).map(([agent, items]) => ({
    agent,
    count: items.length,
    avgConfidence: items.reduce((acc, item) =>
      acc + item.metrics.confidenceScore, 0
    ) / items.length * 100,
  }));

  return (
    <div className="container mx-auto p-4">
      <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
        {/* 채팅 인터페이스 */}
        <div className="card bg-base-100 shadow-xl">
          <div className="card-body">
            <h2 className="card-title">다중 에이전트 학습 시스템</h2>

            <div className="form-control">
              <div className="input-group">
                <input
                  type="text"
                  placeholder="무엇이든 물어보세요..."
                  className="input input-bordered flex-1"
                  onKeyPress={(e) => {
                    if (e.key === 'Enter') {
                      sendMessage.mutate((e.target as HTMLInputElement).value);
                      (e.target as HTMLInputElement).value = '';
                    }
                  }}
                />
                <button
                  className="btn btn-primary"
                  onClick={() => {
                    const input = document.querySelector('input');
                    if (input?.value) {
                      sendMessage.mutate(input.value);
                      input.value = '';
                    }
                  }}
                >
                  전송
                </button>
              </div>
            </div>

            {sendMessage.data && (
              <div className="alert alert-info mt-4">
                <div>
                  <span className="badge badge-secondary mr-2">
                    {sendMessage.data.agentType}
                  </span>
                  {sendMessage.data.response}
                </div>
              </div>
            )}
          </div>
        </div>

        {/* 성능 메트릭 */}
        <div className="card bg-base-100 shadow-xl">
          <div className="card-body">
            <h2 className="card-title">학습 진행 상황</h2>

            {performanceData.length > 0 && (
              <LineChart width={400} height={200} data={performanceData}>
                <CartesianGrid strokeDasharray="3 3" />
                <XAxis dataKey="interaction" />
                <YAxis />
                <Tooltip />
                <Legend />
                <Line
                  type="monotone"
                  dataKey="confidence"
                  stroke="#8884d8"
                  name="신뢰도 %"
                />
                <Line
                  type="monotone"
                  dataKey="success"
                  stroke="#82ca9d"
                  name="성공률 %"
                />
              </LineChart>
            )}
          </div>
        </div>

        {/* 에이전트 통계 */}
        <div className="card bg-base-100 shadow-xl lg:col-span-2">
          <div className="card-body">
            <h2 className="card-title">에이전트 성능</h2>

            <div className="overflow-x-auto">
              <table className="table table-zebra">
                <thead>
                  <tr>
                    <th>에이전트 유형</th>
                    <th>사용 횟수</th>
                    <th>평균 신뢰도</th>
                  </tr>
                </thead>
                <tbody>
                  {agentUsage.map(agent => (
                    <tr key={agent.agent}>
                      <td className="font-bold">{agent.agent}</td>
                      <td>{agent.count}</td>
                      <td>
                        <progress
                          className="progress progress-primary w-32"
                          value={agent.avgConfidence}
                          max="100"
                        />
                        <span className="ml-2">{agent.avgConfidence.toFixed(1)}%</span>
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

대시보드는 학습 진행 상황, 에이전트 성능 메트릭 및 시스템 신뢰도 추세의 실시간 시각화를 제공합니다.

결론

학습과 적응은 정적 LLM 애플리케이션을 모든 상호작용마다 개선되는 동적 시스템으로 변환합니다. LangGraph의 상태 기반 오케스트레이션, 벡터 기반 경험 재생 및 가벼운 강화학습 기법을 결합하여 Vercel과 같은 서버리스 플랫폼에서 뛰어난 성능을 유지하면서 경험을 통해 실제로 학습하는 에이전트를 구축할 수 있습니다. 간단한 선호도 학습부터 시작하여 시스템이 성숙해짐에 따라 우선순위 경험 재생 및 다중 에이전트 조정과 같은 정교한 기능을 점진적으로 추가하세요.