Skip to main content

개요

에이전트(또는 모든 LLM 애플리케이션)를 구축할 때 가장 어려운 부분은 충분히 신뢰할 수 있도록 만드는 것입니다. 프로토타입에서는 잘 작동할 수 있지만, 실제 사용 사례에서는 종종 실패합니다.

에이전트가 실패하는 이유는 무엇인가요?

에이전트가 실패할 때, 일반적으로 에이전트 내부의 LLM 호출이 잘못된 작업을 수행했거나 예상한 대로 작동하지 않았기 때문입니다. LLM이 실패하는 이유는 두 가지 중 하나입니다:
  1. 기본 LLM의 능력이 충분하지 않은 경우
  2. “올바른” 컨텍스트가 LLM에 전달되지 않은 경우
대부분의 경우 - 실제로는 두 번째 이유가 에이전트를 신뢰할 수 없게 만드는 원인입니다. 컨텍스트 엔지니어링은 LLM이 작업을 완수할 수 있도록 올바른 정보와 도구를 올바른 형식으로 제공하는 것입니다. 이것이 AI 엔지니어의 가장 중요한 업무입니다. 이러한 “올바른” 컨텍스트의 부족은 더 신뢰할 수 있는 에이전트를 만드는 데 있어 가장 큰 장애물이며, LangChain의 에이전트 추상화는 컨텍스트 엔지니어링을 용이하게 하도록 독특하게 설계되었습니다.
컨텍스트 엔지니어링이 처음이신가요? 개념 개요를 시작으로 다양한 유형의 컨텍스트와 사용 시기를 이해하세요.

에이전트 루프

일반적인 에이전트 루프는 두 가지 주요 단계로 구성됩니다:
  1. 모델 호출 - 프롬프트와 사용 가능한 도구를 사용하여 LLM을 호출하고, 응답 또는 도구 실행 요청을 반환합니다
  2. 도구 실행 - LLM이 요청한 도구를 실행하고, 도구 결과를 반환합니다
핵심 에이전트 루프 다이어그램
이 루프는 LLM이 종료하기로 결정할 때까지 계속됩니다.

제어할 수 있는 것

신뢰할 수 있는 에이전트를 구축하려면, 에이전트 루프의 각 단계에서 발생하는 일과 단계 사이에 발생하는 일을 제어해야 합니다.
컨텍스트 유형제어할 수 있는 것일시적 또는 영구적
모델 컨텍스트모델 호출에 들어가는 것(지시사항, 메시지 기록, 도구, 응답 형식)일시적
도구 컨텍스트도구가 액세스하고 생성하는 것(상태, 저장소, 런타임 컨텍스트에 대한 읽기/쓰기)영구적
생명주기 컨텍스트모델과 도구 호출 사이에 발생하는 일(요약, 가드레일, 로깅 등)영구적

일시적 컨텍스트

LLM이 단일 호출에서 보는 것입니다. 상태에 저장된 내용을 변경하지 않고 메시지, 도구 또는 프롬프트를 수정할 수 있습니다.

영구적 컨텍스트

턴 간에 상태에 저장되는 것입니다. 생명주기 훅과 도구 쓰기는 이를 영구적으로 수정합니다.

데이터 소스

이 프로세스 전반에 걸쳐, 에이전트는 다양한 데이터 소스에 액세스(읽기/쓰기)합니다:
데이터 소스다른 이름범위예시
런타임 컨텍스트정적 구성대화 범위사용자 ID, API 키, 데이터베이스 연결, 권한, 환경 설정
상태단기 메모리대화 범위현재 메시지, 업로드된 파일, 인증 상태, 도구 결과
저장소장기 메모리대화 간사용자 선호도, 추출된 인사이트, 기억, 과거 데이터

작동 방식

LangChain 미들웨어는 LangChain을 사용하는 개발자에게 컨텍스트 엔지니어링을 실용적으로 만드는 내부 메커니즘입니다. 미들웨어를 사용하면 에이전트 생명주기의 모든 단계에 연결하여 다음을 수행할 수 있습니다:
  • 컨텍스트 업데이트
  • 에이전트 생명주기의 다른 단계로 이동
이 가이드 전반에 걸쳐, 컨텍스트 엔지니어링 목적을 위한 수단으로 미들웨어 API를 자주 사용하는 것을 볼 수 있습니다.

모델 컨텍스트

각 모델 호출에 들어가는 것을 제어합니다 - 지시사항, 사용 가능한 도구, 사용할 모델, 출력 형식. 이러한 결정은 신뢰성과 비용에 직접적인 영향을 미칩니다. 이러한 모든 유형의 모델 컨텍스트는 상태(단기 메모리), 저장소(장기 메모리) 또는 런타임 컨텍스트(정적 구성)에서 가져올 수 있습니다.

시스템 프롬프트

시스템 프롬프트는 LLM의 동작과 기능을 설정합니다. 다양한 사용자, 컨텍스트 또는 대화 단계에는 다양한 지시사항이 필요합니다. 성공적인 에이전트는 메모리, 선호도 및 구성을 활용하여 대화의 현재 상태에 적합한 지시사항을 제공합니다.
  • 상태
  • 저장소
  • 런타임 컨텍스트
상태에서 메시지 수 또는 대화 컨텍스트에 액세스:
import { createAgent } from "langchain";

const agent = createAgent({
  model: "openai:gpt-4o",
  tools: [...],
  middleware: [
    dynamicSystemPromptMiddleware((state) => {
      // 상태에서 읽기: 대화 길이 확인
      const messageCount = state.messages.length;

      let base = "You are a helpful assistant.";

      if (messageCount > 10) {
        base += "\nThis is a long conversation - be extra concise.";
      }

      return base;
    }),
  ],
});

메시지

메시지는 LLM에 전송되는 프롬프트를 구성합니다. LLM이 올바른 정보를 가지고 잘 응답할 수 있도록 메시지의 내용을 관리하는 것이 중요합니다.
  • 상태
  • 저장소
  • 런타임 컨텍스트
현재 쿼리와 관련이 있을 때 상태에서 업로드된 파일 컨텍스트를 주입:
import { createMiddleware } from "langchain";

const injectFileContext = createMiddleware({
  name: "InjectFileContext",
  wrapModelCall: (request, handler) => {
    // request.state는 request.state.messages의 단축키입니다
    const uploadedFiles = request.state.uploadedFiles || [];  

    if (uploadedFiles.length > 0) {
      // 사용 가능한 파일에 대한 컨텍스트 구축
      const fileDescriptions = uploadedFiles.map(file =>
        `- ${file.name} (${file.type}): ${file.summary}`
      );

      const fileContext = `Files you have access to in this conversation:
${fileDescriptions.join("\n")}

Reference these files when answering questions.`;

      // 최근 메시지 이전에 파일 컨텍스트 주입
      const messages = [  
        ...request.messages  // 대화의 나머지 부분
        { role: "user", content: fileContext }
      ];
      request = request.override({ messages });  
    }

    return handler(request);
  },
});

const agent = createAgent({
  model: "openai:gpt-4o",
  tools: [...],
  middleware: [injectFileContext],
});
일시적 vs 영구적 메시지 업데이트:위의 예시는 wrap_model_call을 사용하여 일시적 업데이트를 수행합니다 - 상태에 저장된 내용을 변경하지 않고 단일 호출을 위해 모델에 전송되는 메시지를 수정합니다.상태를 수정하는 영구적 업데이트(예: 생명주기 컨텍스트의 요약 예시)의 경우, before_model 또는 after_model과 같은 생명주기 훅을 사용하여 대화 기록을 영구적으로 업데이트하세요. 자세한 내용은 미들웨어 문서를 참조하세요.

도구

도구를 사용하면 모델이 데이터베이스, API 및 외부 시스템과 상호 작용할 수 있습니다. 도구를 정의하고 선택하는 방법은 모델이 작업을 효과적으로 완료할 수 있는지 여부에 직접적인 영향을 미칩니다.

도구 정의

각 도구에는 명확한 이름, 설명, 인수 이름 및 인수 설명이 필요합니다. 이것들은 단순한 메타데이터가 아닙니다 - 도구를 언제 어떻게 사용할지에 대한 모델의 추론을 안내합니다.
import { tool } from "@langchain/core/tools";
import { z } from "zod";

const searchOrders = tool(
  async ({ userId, status, limit = 10 }) => {
    // 구현 여기에
  },
  {
    name: "search_orders",
    description: `상태별로 사용자 주문을 검색합니다.

    사용자가 주문 이력에 대해 묻거나 주문 상태를 확인하려는 경우에 사용합니다.
    항상 제공된 상태로 필터링합니다.`,
    schema: z.object({
      userId: z.string().describe("사용자의 고유 식별자"),
      status: z.enum(["pending", "shipped", "delivered"]).describe("필터링할 주문 상태"),
      limit: z.number().default(10).describe("반환할 최대 결과 수"),
    }),
  }
);

도구 선택

모든 도구가 모든 상황에 적합한 것은 아닙니다. 너무 많은 도구는 모델을 압도하고(컨텍스트 과부하) 오류를 증가시킬 수 있으며, 너무 적은 도구는 기능을 제한합니다. 동적 도구 선택은 인증 상태, 사용자 권한, 기능 플래그 또는 대화 단계에 따라 사용 가능한 도구 세트를 조정합니다.
  • 상태
  • 저장소
  • 런타임 컨텍스트
특정 대화 마일스톤 이후에만 고급 도구 활성화:
import { createMiddleware } from "langchain";

const stateBasedTools = createMiddleware({
  name: "StateBasedTools",
  wrapModelCall: (request, handler) => {
    // 상태에서 읽기: 인증 및 대화 길이 확인
    const state = request.state;  
    const isAuthenticated = state.authenticated || false;  
    const messageCount = state.messages.length;

    let filteredTools = request.tools;

    // 인증 후에만 민감한 도구 활성화
    if (!isAuthenticated) {
      filteredTools = request.tools.filter(t => t.name.startsWith("public_"));  
    } else if (messageCount < 5) {
      filteredTools = request.tools.filter(t => t.name !== "advanced_search");  
    }

    return handler({ ...request, tools: filteredTools });  
  },
});
더 많은 예시는 동적으로 도구 선택하기를 참조하세요.

모델

다양한 모델은 다양한 강점, 비용 및 컨텍스트 윈도우를 가지고 있습니다. 당면한 작업에 적합한 모델을 선택하세요. 에이전트 실행 중에 변경될 수 있습니다.
  • 상태
  • 저장소
  • 런타임 컨텍스트
상태의 대화 길이에 따라 다양한 모델 사용:
import { createMiddleware, initChatModel } from "langchain";

// 미들웨어 외부에서 모델을 한 번 초기화
const largeModel = initChatModel("anthropic:claude-sonnet-4-5");
const standardModel = initChatModel("openai:gpt-4o");
const efficientModel = initChatModel("openai:gpt-4o-mini");

const stateBasedModel = createMiddleware({
  name: "StateBasedModel",
  wrapModelCall: (request, handler) => {
    // request.messages는 request.state.messages의 단축키입니다
    const messageCount = request.messages.length;  
    let model;

    if (messageCount > 20) {
      model = largeModel;
    } else if (messageCount > 10) {
      model = standardModel;
    } else {
      model = efficientModel;
    }

    return handler({ ...request, model });  
  },
});
더 많은 예시는 동적 모델을 참조하세요.

응답 형식

구조화된 출력은 비구조화된 텍스트를 검증된 구조화된 데이터로 변환합니다. 특정 필드를 추출하거나 다운스트림 시스템에 데이터를 반환할 때, 자유 형식 텍스트는 충분하지 않습니다. 작동 방식: 스키마를 응답 형식으로 제공하면, 모델의 최종 응답은 해당 스키마를 준수하도록 보장됩니다. 에이전트는 모델이 도구 호출을 완료할 때까지 모델/도구 호출 루프를 실행한 다음, 최종 응답이 제공된 형식으로 강제 변환됩니다.

형식 정의

스키마 정의는 모델을 안내합니다. 필드 이름, 유형 및 설명은 출력이 준수해야 하는 형식을 정확하게 지정합니다.
import { z } from "zod";

const customerSupportTicket = z.object({
  category: z.enum(["billing", "technical", "account", "product"]).describe(
    "문제 카테고리"
  ),
  priority: z.enum(["low", "medium", "high", "critical"]).describe(
    "긴급도 수준"
  ),
  summary: z.string().describe(
    "고객 문제의 한 문장 요약"
  ),
  customerSentiment: z.enum(["frustrated", "neutral", "satisfied"]).describe(
    "고객의 감정적 톤"
  ),
}).describe("고객 메시지에서 추출된 구조화된 티켓 정보");

형식 선택

동적 응답 형식 선택은 사용자 선호도, 대화 단계 또는 역할에 따라 스키마를 조정합니다 - 초기에는 간단한 형식을 반환하고 복잡도가 증가함에 따라 상세한 형식을 반환합니다.
  • 상태
  • 저장소
  • 런타임 컨텍스트
대화 상태에 따라 구조화된 출력 구성:
import { createMiddleware } from "langchain";
import { z } from "zod";

const simpleResponse = z.object({
  answer: z.string().describe("간략한 답변"),
});

const detailedResponse = z.object({
  answer: z.string().describe("상세한 답변"),
  reasoning: z.string().describe("추론 설명"),
  confidence: z.number().describe("신뢰도 점수 0-1"),
});

const stateBasedOutput = createMiddleware({
  name: "StateBasedOutput",
  wrapModelCall: (request, handler) => {
    // request.state는 request.state.messages의 단축키입니다
    const messageCount = request.messages.length;  

    if (messageCount < 3) {
      // 초기 대화 - 간단한 형식 사용
      responseFormat = simpleResponse; 
    } else {
      // 확립된 대화 - 상세한 형식 사용
      responseFormat = detailedResponse; 
    }

    return handler({ ...request, responseFormat });
  },
});

도구 컨텍스트

도구는 컨텍스트를 읽고 쓰는 것이 특별합니다. 가장 기본적인 경우, 도구가 실행되면 LLM의 요청 매개변수를 받고 도구 메시지를 반환합니다. 도구는 작업을 수행하고 결과를 생성합니다. 도구는 또한 모델이 작업을 수행하고 완료할 수 있도록 하는 중요한 정보를 가져올 수 있습니다.

읽기

대부분의 실제 도구는 LLM의 매개변수보다 더 많은 것이 필요합니다. 데이터베이스 쿼리를 위한 사용자 ID, 외부 서비스를 위한 API 키 또는 결정을 내리기 위한 현재 세션 상태가 필요합니다. 도구는 이 정보에 액세스하기 위해 상태, 저장소 및 런타임 컨텍스트에서 읽습니다.
  • 상태
  • 저장소
  • 런타임 컨텍스트
현재 세션 정보를 확인하기 위해 상태에서 읽기:
import * as z from "zod";
import { tool } from "@langchain/core/tools";
import { createAgent } from "langchain";

const checkAuthentication = tool(
  async (_, { runtime }) => {
    // 상태에서 읽기: 현재 인증 상태 확인
    const currentState = runtime.state;
    const isAuthenticated = currentState.authenticated || false;

    if (isAuthenticated) {
      return "User is authenticated";
    } else {
      return "User is not authenticated";
    }
  },
  {
    name: "check_authentication",
    description: "사용자가 인증되었는지 확인",
    schema: z.object({}),
  }
);

쓰기

도구 결과는 에이전트가 주어진 작업을 완료하는 데 사용될 수 있습니다. 도구는 모델에 직접 결과를 반환하고 향후 단계에서 중요한 컨텍스트를 사용할 수 있도록 에이전트의 메모리를 업데이트할 수 있습니다.
  • 상태
  • 저장소
Command를 사용하여 세션별 정보를 추적하기 위해 상태에 쓰기:
import * as z from "zod";
import { tool } from "@langchain/core/tools";
import { createAgent } from "langchain";
import { Command } from "@langchain/langgraph";

const authenticateUser = tool(
  async ({ password }, { runtime }) => {
    // 인증 수행
    if (password === "correct") {
      // 상태에 쓰기: Command를 사용하여 인증됨으로 표시
      return new Command({
        update: { authenticated: true },
      });
    } else {
      return new Command({ update: { authenticated: false } });
    }
  },
  {
    name: "authenticate_user",
    description: "사용자를 인증하고 상태를 업데이트",
    schema: z.object({
      password: z.string(),
    }),
  }
);
도구에서 상태, 저장소 및 런타임 컨텍스트에 액세스하는 포괄적인 예시는 도구를 참조하세요.

생명주기 컨텍스트

핵심 에이전트 단계 사이에 발생하는 일을 제어합니다 - 요약, 가드레일 및 로깅과 같은 교차 관심사를 구현하기 위해 데이터 흐름을 가로챕니다. 모델 컨텍스트도구 컨텍스트에서 보았듯이, 미들웨어는 컨텍스트 엔지니어링을 실용적으로 만드는 메커니즘입니다. 미들웨어를 사용하면 에이전트 생명주기의 모든 단계에 연결하여 다음 중 하나를 수행할 수 있습니다:
  1. 컨텍스트 업데이트 - 상태와 저장소를 수정하여 변경 사항을 지속하고, 대화 기록을 업데이트하거나 인사이트를 저장합니다
  2. 생명주기에서 이동 - 컨텍스트에 따라 에이전트 사이클의 다른 단계로 이동합니다(예: 조건이 충족되면 도구 실행을 건너뛰고, 수정된 컨텍스트로 모델 호출을 반복)
에이전트 루프의 미들웨어 훅

예시: 요약

가장 일반적인 생명주기 패턴 중 하나는 대화 기록이 너무 길어지면 자동으로 압축하는 것입니다. 모델 컨텍스트에 표시된 일시적 메시지 트리밍과 달리, 요약은 상태를 영구적으로 업데이트합니다 - 오래된 메시지를 모든 향후 턴에 저장되는 요약으로 영구적으로 대체합니다. LangChain은 이를 위한 내장 미들웨어를 제공합니다:
import { createAgent, summarizationMiddleware } from "langchain";

const agent = createAgent({
  model: "openai:gpt-4o",
  tools: [...],
  middleware: [
    summarizationMiddleware({
      model: "openai:gpt-4o-mini",
      maxTokensBeforeSummary: 4000, // 4000 토큰에서 요약 트리거
      messagesToKeep: 20, // 요약 후 최근 20개 메시지 유지
    }),
  ],
});
대화가 토큰 제한을 초과하면, SummarizationMiddleware는 자동으로:
  1. 별도의 LLM 호출을 사용하여 오래된 메시지를 요약합니다
  2. 상태에서 요약 메시지로 대체합니다(영구적으로)
  3. 컨텍스트를 위해 최근 메시지를 그대로 유지합니다
요약된 대화 기록은 영구적으로 업데이트됩니다 - 향후 턴은 원본 메시지 대신 요약을 볼 수 있습니다.
내장 미들웨어의 전체 목록, 사용 가능한 훅 및 사용자 정의 미들웨어를 만드는 방법은 미들웨어 문서를 참조하세요.

모범 사례

  1. 간단하게 시작 - 정적 프롬프트와 도구로 시작하고, 필요할 때만 동적 기능 추가
  2. 점진적으로 테스트 - 한 번에 하나의 컨텍스트 엔지니어링 기능 추가
  3. 성능 모니터링 - 모델 호출, 토큰 사용량 및 지연 시간 추적
  4. 내장 미들웨어 사용 - SummarizationMiddleware, LLMToolSelectorMiddleware 등 활용
  5. 컨텍스트 전략 문서화 - 어떤 컨텍스트가 전달되는지, 그 이유를 명확하게 작성
  6. 일시적 vs 영구적 이해: 모델 컨텍스트 변경은 일시적(호출당)이지만, 생명주기 컨텍스트 변경은 상태에 지속됩니다

관련 리소스


Connect these docs programmatically to Claude, VSCode, and more via MCP for real-time answers.
I