에이전트의 컨텍스트 엔지니어링

에이전트의 컨텍스트 엔지니어링 (Context engineering in agents)

개요 (Overview)

에이전트(또는 어떤 LLM 애플리케이션이든)를 구축할 때 어려운 부분은 충분히 신뢰할 수 있게 만드는 것이에요. 프로토타입에서는 작동할 수 있지만 실사용 사례에서는 종종 실패해요.

왜 에이전트가 실패하나? (Why do agents fail?)

에이전트가 실패할 때는 대개 에이전트 안의 LLM 호출이 잘못된 동작을 했거나 / 우리가 기대한 대로 하지 않았기 때문이에요. LLM은 두 가지 이유 중 하나로 실패해요:

  1. 기반 LLM이 충분히 유능하지 않음
  2. "올바른" 컨텍스트가 LLM에 전달되지 않음

대부분의 경우, 실제로 에이전트를 신뢰할 수 없게 만드는 원인은 두 번째예요.

컨텍스트 엔지니어링은 올바른 정보와 툴을 올바른 형식으로 제공해 LLM이 작업을 수행할 수 있게 하는 것이에요. 이것은 AI 엔지니어의 가장 중요한 작업이에요. 이 "올바른" 컨텍스트의 부재는 더 신뢰할 수 있는 에이전트를 막는 첫 번째 장애물이며, LangChain의 에이전트 추상화는 컨텍스트 엔지니어링을 촉진하도록 독특하게 설계되었어요.

컨텍스트 엔지니어링이 처음인가요? [개념 개요](/oss/javascript/concepts/context)부터 시작해 컨텍스트의 여러 유형과 사용 시점을 이해하세요.

에이전트 루프 (The agent loop)

일반적인 에이전트 루프는 두 가지 주요 단계로 구성돼요:

  1. 모델 호출 (Model call) - 프롬프트와 사용 가능한 툴로 LLM을 호출하고, 응답 또는 툴 실행 요청을 반환
  2. 툴 실행 (Tool execution) - LLM이 요청한 툴을 실행하고 툴 결과를 반환

이 루프는 LLM이 종료를 결정할 때까지 계속돼요.

무엇을 제어할 수 있나 (What you can control)

신뢰할 수 있는 에이전트를 구축하려면 에이전트 루프의 각 단계에서 그리고 단계 사이에서 일어나는 일을 제어해야 해요.

컨텍스트 유형 제어하는 것 일시 또는 영구
모델 컨텍스트 모델 호출에 들어가는 것 (지침, 메시지 기록, 툴, 응답 형식) 일시 (Transient)
툴 컨텍스트 툴이 접근하고 생성할 수 있는 것 (상태·스토어·런타임 컨텍스트 읽기/쓰기) 영구 (Persistent)
수명주기 컨텍스트 모델·툴 호출 사이에 일어나는 것 (요약, 가드레일, 로깅 등) 영구 (Persistent)

일시적 컨텍스트(Transient context): 단일 호출에서 LLM이 보는 것. 상태에 저장된 것을 바꾸지 않고 메시지, 툴, 프롬프트를 수정할 수 있어요.

영구적 컨텍스트(Persistent context): 턴에 걸쳐 상태에 저장되는 것. 수명주기 훅과 툴 쓰기가 이것을 영구적으로 수정해요.

데이터 소스 (Data sources)

이 과정 전반에 걸쳐 에이전트는 서로 다른 데이터 소스에 접근(읽기/쓰기)해요:

데이터 소스 다른 이름 범위
런타임 컨텍스트 (Runtime Context) 정적 구성 대화 범위 사용자 ID, API 키, 데이터베이스 연결, 권한, 환경 설정
상태 (State) 단기 메모리 대화 범위 현재 메시지, 업로드된 파일, 인증 상태, 툴 결과
스토어 (Store) 장기 메모리 대화 간 교차 사용자 선호도, 추출된 인사이트, 메모리, 과거 데이터

어떻게 작동하나 (How it works)

LangChain 미들웨어가 그 뒤에서 컨텍스트 엔지니어링을 개발자에게 실용적으로 만들어 주는 메커니즘이에요. 미들웨어로 에이전트 수명주기의 어느 단계에든 연결할 수 있고:

  • 컨텍스트 업데이트
  • 에이전트 수명주기의 다른 단계로 이동

이 가이드 전반에서 컨텍스트 엔지니어링 목표를 위한 수단으로 미들웨어 API가 자주 사용되는 것을 볼 수 있어요.

모델 컨텍스트 (Model context)

각 모델 호출에 들어가는 것 - 지침, 사용 가능한 툴, 사용할 모델, 출력 형식 - 을 제어하세요. 이 결정들은 신뢰성과 비용에 직접 영향을 줍니다.

  • 시스템 프롬프트 (System Prompt): 개발자에서 LLM으로의 기본 지침.
  • 메시지 (Messages): LLM에 전송되는 전체 메시지 목록 (대화 기록).
  • 툴 (Tools): 에이전트가 조치를 취하기 위해 접근할 수 있는 유틸리티.
  • 모델 (Model): 호출할 실제 모델 (구성 포함).
  • 응답 형식 (Response Format): 모델 최종 응답에 대한 스키마 사양.

이 모든 모델 컨텍스트 유형은 상태(단기 메모리), 스토어(장기 메모리), 또는 런타임 컨텍스트(정적 구성)에서 가져올 수 있어요.

시스템 프롬프트 (System Prompt)

시스템 프롬프트는 LLM의 행동과 기능을 설정해요. 다른 사용자, 컨텍스트, 또는 대화 단계는 다른 지침이 필요해요. 성공적인 에이전트는 메모리, 선호도, 구성을 활용해 대화의 현재 상태에 맞는 올바른 지침을 제공해요.

State에서 메시지 수 또는 대화 컨텍스트에 접근:

import { createAgent } from "langchain";
const agent = createAgent({
  model: "gpt-5.5",
  tools: [...],
  middleware: [
    dynamicSystemPromptMiddleware((state) => {
      // Read from State: check conversation length
      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;
    }),
  ],
});

Store에서 장기 메모리의 사용자 선호도에 접근:

import * as z from "zod";
import { createAgent, dynamicSystemPromptMiddleware } from "langchain";
const contextSchema = z.object({
  userId: z.string(),
});
type Context = z.infer<typeof contextSchema>;

const agent = createAgent({
  model: "gpt-5.5",
  tools: [...],
  contextSchema,
  middleware: [
    dynamicSystemPromptMiddleware<Context>(async (state, runtime) => {
      const userId = runtime.context.userId;
      // Read from Store: get user preferences
      const store = runtime.store;
      const userPrefs = await store.get(["preferences"], userId);
      if (userPrefs) {
        const style = userPrefs.value?.communicationStyle || "balanced";
        base += `\nUser prefers ${style} responses.`;
      }
      return base;
    }),
  ],
});

Runtime Context에서 사용자 ID 또는 구성에 접근:

const contextSchema = z.object({
  userId: z.string(),
  userRole: z.string(),
  deploymentEnv: z.string(),
});

const agent = createAgent({
  model: "gpt-5.5",
  tools: [...],
  contextSchema,
  middleware: [
    dynamicSystemPromptMiddleware<Context>((state, runtime) => {
      // Read from Runtime Context: user role and environment
      const userRole = runtime.context.userRole;
      const env = runtime.context.deploymentEnv;
      let base = "You are a helpful assistant.";
      if (userRole === "admin") {
        base += "\nYou have admin access. You can perform all operations.";
      } else if (userRole === "viewer") {
        base += "\nYou have read-only access. Guide users to read operations only.";
      }
      if (env === "production") {
        base += "\nBe extra careful with any data modifications.";
      }
      return base;
    }),
  ],
});

메시지 (Messages)

메시지는 LLM에 전송되는 프롬프트를 구성해요. LLM이 잘 응답할 올바른 정보를 갖도록 메시지 내용을 관리하는 것이 중요해요.

State에서 현재 쿼리와 관련된 업로드 파일 컨텍스트 주입:

import { createMiddleware } from "langchain";
const injectFileContext = createMiddleware({
  name: "InjectFileContext",
  wrapModelCall: (request, handler) => {
    // request.state is a shortcut for request.state.messages
    const uploadedFiles = request.state.uploadedFiles || [];  // [!code highlight]
    if (uploadedFiles.length > 0) {
      // Build context about available files
      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.`;
      // Inject file context before recent messages
      const messages = [  // [!code highlight]
        ...request.messages,  // Rest of conversation
        { role: "user", content: fileContext }
      ];
      request = request.override({ messages });  // [!code highlight]
    }
    return handler(request);
  },
});

const agent = createAgent({
  model: "gpt-5.5",
  tools: [...],
  middleware: [injectFileContext],
});

Store에서 사용자의 이메일 작성 스타일을 주입해 초안 작성 안내:

const injectWritingStyle = createMiddleware({
  name: "InjectWritingStyle",
  wrapModelCall: async (request, handler) => {
    const userId = request.runtime.context.userId;  // [!code highlight]
    // Read from Store: get user's writing style examples
    const store = request.runtime.store;  // [!code highlight]
    const writingStyle = await store.get(["writing_style"], userId);  // [!code highlight]
    if (writingStyle) {
      const style = writingStyle.value;
      // Build style guide from stored examples
      const styleContext = `Your writing style:
- Tone: ${style.tone || 'professional'}
- Typical greeting: "${style.greeting || 'Hi'}"
- Typical sign-off: "${style.signOff || 'Best'}"
- Example email you've written:
${style.exampleEmail || ''}`;
      // Append at end - models pay more attention to final messages
      const messages = [
        ...request.messages,
        { role: "user", content: styleContext }
      ];
      request = request.override({ messages });
    }
    return handler(request);
  },
});

Runtime Context에서 사용자의 관할 구역에 따라 규정 준수 규칙 주입:

const contextSchema = z.object({
  userJurisdiction: z.string(),
  industry: z.string(),
  complianceFrameworks: z.array(z.string()),
});

const injectComplianceRules = createMiddleware<Context>({
  name: "InjectComplianceRules",
  wrapModelCall: async (request, handler) => {
    // Read from Runtime Context: get compliance requirements
    const { userJurisdiction, industry, complianceFrameworks } = request.runtime.context;  // [!code highlight]
    // Build compliance constraints
    const rules = [];
    if (complianceFrameworks.includes("GDPR")) {
      rules.push("- Must obtain explicit consent before processing personal data");
      rules.push("- Users have right to data deletion");
    }
    if (complianceFrameworks.includes("HIPAA")) {
      rules.push("- Cannot share patient health information without authorization");
      rules.push("- Must use secure, encrypted communication");
    }
    if (industry === "finance") {
      rules.push("- Cannot provide financial advice without proper disclaimers");
    }
    if (rules.length > 0) {
      const complianceContext = `Compliance requirements for ${userJurisdiction}:
${rules.join("\n")}`;
      const messages = [...request.messages, { role: "user", content: complianceContext }];
      request = request.override({ messages });
    }
    return handler(request);
  },
});
**일시 vs 영구 메시지 업데이트:**

위 예제는 wrap_model_call을 사용해 일시적 업데이트를 하며 - 단일 호출에서 모델로 전송되는 메시지를 수정하고 상태에 저장된 것은 바꾸지 않아요.

상태를 수정하는 영구적 업데이트는:

  • wrapModelCall에서 Command를 직접 반환해 모델 호출 계층에서 상태 업데이트를 주입.
  • beforeModel, afterModel, wrapToolCall(툴 반환용) 같은 수명주기 훅으로 대화 기록을 업데이트. 자세한 내용은 미들웨어 문서 참고.
  • 자세한 내용은 상태 업데이트 참고.

툴 (Tools)

툴은 모델이 데이터베이스, API, 외부 시스템과 상호작용하게 해줘요. 툴을 어떻게 정의·선택하는지는 모델이 작업을 효과적으로 완료할 수 있는지에 직접 영향을 줍니다.

툴 정의 (Defining tools)

각 툴은 명확한 이름, 설명, 인자 이름, 인자 설명이 필요해요. 이는 단지 메타데이터가 아니라 모델이 언제·어떻게 툴을 사용할지 추론하는 것을 안내해요.

import { tool } from "@langchain/core/tools";
import { z } from "zod";
const searchOrders = tool(
  async ({ userId, status, limit }) => {
    // Implementation here
  },
  {
    name: "search_orders",
    description: `Search for user orders by status.
Use this when the user asks about order history or wants to check
order status. Always filter by the provided status.`,
    schema: z.object({
      userId: z.string().describe("Unique identifier for the user"),
      status: z.enum(["pending", "shipped", "delivered"]).describe("Order status to filter by"),
      limit: z.number().default(10).describe("Maximum number of results to return"),
    }),
  }
);

툴 선택 (Selecting tools)

모든 툴이 모든 상황에 적절한 것은 아니에요. 툴이 너무 많으면 모델을 압도하고(컨텍스트 과부하) 오류를 늘리며, 너무 적으면 기능이 제한돼요. 동적 툴 선택은 인증 상태, 사용자 권한, 기능 플래그, 또는 대화 단계에 따라 사용 가능한 툴셋을 조정해요.

특정 대화 이정표 후에만 고급 툴 활성화:

const stateBasedTools = createMiddleware({
  name: "StateBasedTools",
  wrapModelCall: (request, handler) => {
    // Read from State: check authentication and conversation length
    const state = request.state;  // [!code highlight]
    const isAuthenticated = state.authenticated || false;  // [!code highlight]
    const messageCount = state.messages.length;
    let filteredTools = request.tools;
    // Only enable sensitive tools after authentication
    if (!isAuthenticated) {
      filteredTools = request.tools.filter(t => t.name.startsWith("public_"));  // [!code highlight]
    } else if (messageCount < 5) {
      filteredTools = request.tools.filter(t => t.name !== "advanced_search");  // [!code highlight]
    }
    return handler({ ...request, tools: filteredTools });  // [!code highlight]
  },
});

Store에서 사용자 선호도 또는 기능 플래그에 따라 툴 필터링:

const storeBasedTools = createMiddleware({
  name: "StoreBasedTools",
  wrapModelCall: async (request, handler) => {
    const userId = request.runtime.context.userId;
    // Read from Store: get user's enabled features
    const store = request.runtime.store;  // [!code highlight]
    const featureFlags = await store.get(["features"], userId);  // [!code highlight]
    let filteredTools = request.tools;
    if (featureFlags) {
      const enabledFeatures = featureFlags.value?.enabledTools || [];
      filteredTools = request.tools.filter(t => enabledFeatures.includes(t.name));  // [!code highlight]
    }
    return handler({ ...request, tools: filteredTools });
  },
});

Runtime Context에서 사용자 권한에 따라 툴 필터링:

const contextBasedTools = createMiddleware({
  name: "ContextBasedTools",
  wrapModelCall: (request, handler) => {
    // Read from Runtime Context: get user role
    const userRole = request.runtime.context.userRole;  // [!code highlight]
    let filteredTools = request.tools;
    if (userRole === "admin") {
      // Admins get all tools
    } else if (userRole === "editor") {
      filteredTools = request.tools.filter(t => t.name !== "delete_data");  // [!code highlight]
    } else {
      filteredTools = request.tools.filter(t => t.name.startsWith("read_"));  // [!code highlight]
    }
    return handler({ ...request, tools: filteredTools });
  },
});

사전 등록 툴 필터링과 런타임 툴 등록(예: MCP 서버에서) 모두에 대해 동적 툴을 참고하세요.

모델 (Model)

서로 다른 모델은 서로 다른 강점, 비용, 컨텍스트 윈도우를 가져요. 작업에 맞는 모델을 선택하세요. 이는 에이전트 실행 중에 변경될 수 있어요.

State의 대화 길이에 따라 다른 모델 사용:

import { createMiddleware, initChatModel } from "langchain";
// Initialize models once outside the middleware
const largeModel = initChatModel("claude-sonnet-4-6");
const standardModel = initChatModel("gpt-5.5");
const efficientModel = initChatModel("gpt-5.4-mini");
const stateBasedModel = createMiddleware({
  name: "StateBasedModel",
  wrapModelCall: (request, handler) => {
    // request.messages is a shortcut for request.state.messages
    const messageCount = request.messages.length;  // [!code highlight]
    let model;
    if (messageCount > 20) {
      model = largeModel;
    } else if (messageCount > 10) {
      model = standardModel;
    } else {
      model = efficientModel;
    }
    return handler({ ...request, model });  // [!code highlight]
  },
});

Store에서 사용자의 선호 모델 사용:

// Initialize available models once
const MODEL_MAP = {
  "gpt-5.5": initChatModel("gpt-5.5"),
  "gpt-5.4-mini": initChatModel("gpt-5.4-mini"),
  "claude-sonnet": initChatModel("claude-sonnet-4-6"),
};
const storeBasedModel = createMiddleware({
  name: "StoreBasedModel",
  wrapModelCall: async (request, handler) => {
    const userId = request.runtime.context.userId;
    // Read from Store: get user's preferred model
    const store = request.runtime.store;  // [!code highlight]
    const userPrefs = await store.get(["preferences"], userId);  // [!code highlight]
    let model = request.model;
    if (userPrefs) {
      const preferredModel = userPrefs.value?.preferredModel;
      if (preferredModel && MODEL_MAP[preferredModel]) {
        model = MODEL_MAP[preferredModel];  // [!code highlight]
      }
    }
    return handler({ ...request, model });
  },
});

Runtime Context의 비용 한도나 환경에 따라 모델 선택:

const contextSchema = z.object({
  costTier: z.string(),
  environment: z.string(),
});
const premiumModel = initChatModel("claude-sonnet-4-6");
const budgetModel = initChatModel("gpt-5.4-mini");
const contextBasedModel = createMiddleware({
  name: "ContextBasedModel",
  wrapModelCall: (request, handler) => {
    // Read from Runtime Context: cost tier and environment
    const costTier = request.runtime.context.costTier;  // [!code highlight]
    const environment = request.runtime.context.environment;  // [!code highlight]
    let model = request.model;
    if (environment === "production" && costTier === "premium") {
      model = premiumModel;
    } else if (costTier === "budget") {
      model = budgetModel;
    }
    return handler({ ...request, model });
  },
});

더 많은 예제는 동적 모델을 참고하세요.

응답 형식 (Response format)

구조화된 출력은 비정형 텍스트를 검증된 구조화 데이터로 변환해요. 특정 필드를 추출하거나 다운스트림 시스템에 데이터를 반환할 때 자유 형식 텍스트로는 충분하지 않아요.

작동 방식: 응답 형식으로 스키마를 제공하면 모델의 최종 응답이 해당 스키마를 따르는 것이 보장돼요. 에이전트는 모델이 툴 호출을 끝낼 때까지 모델/툴 호출 루프를 실행하고, 최종 응답을 제공된 형식으로 강제 변환해요.

형식 정의 (Defining formats)

스키마 정의는 모델을 안내해요. 필드 이름, 타입, 설명이 출력이 따라야 할 정확한 형식을 지정해요.

const customerSupportTicket = z.object({
  category: z.enum(["billing", "technical", "account", "product"]).describe(
    "Issue category"
  ),
  priority: z.enum(["low", "medium", "high", "critical"]).describe(
    "Urgency level"
  ),
  summary: z.string().describe(
    "One-sentence summary of the customer's issue"
  ),
  customerSentiment: z.enum(["frustrated", "neutral", "satisfied"]).describe(
    "Customer's emotional tone"
  ),
}).describe("Structured ticket information extracted from customer message");

형식 선택 (Selecting formats)

동적 응답 형식 선택은 사용자 선호도, 대화 단계, 또는 역할에 따라 스키마를 조정해요 - 초기에는 단순 형식을, 복잡성이 증가하면 상세 형식을 반환해요.

대화 상태에 따라 구조화된 출력 구성:

import { z } from "zod";
import { createMiddleware } from "langchain";
const simpleResponse = z.object({
  answer: z.string().describe("A brief answer"),
});
const detailedResponse = z.object({
  answer: z.string().describe("A detailed answer"),
  reasoning: z.string().describe("Explanation of reasoning"),
  confidence: z.number().describe("Confidence score 0-1"),
});
const stateBasedOutput = createMiddleware({
  name: "StateBasedOutput",
  wrapModelCall: (request, handler) => {
    const messageCount = request.messages.length;
    let responseFormat;
    if (messageCount < 3) {
      // Early conversation - use simple format
      responseFormat = simpleResponse; // [!code highlight]
    } else {
      // Established conversation - use detailed format
      responseFormat = detailedResponse; // [!code highlight]
    }
    return handler({ ...request, responseFormat });
  },
});

Store의 사용자 선호도에 따라 출력 형식 구성:

const verboseResponse = z.object({
  answer: z.string().describe("Detailed answer"),
  sources: z.array(z.string()).describe("Sources used"),
});
const conciseResponse = z.object({
  answer: z.string().describe("Brief answer"),
});
const storeBasedOutput = createMiddleware({
  name: "StoreBasedOutput",
  wrapModelCall: async (request, handler) => {
    const userId = request.runtime.context.userId;
    const userPrefs = await request.runtime.store.get(["preferences"], userId);
    // Read from Store: get user's preferred response style
    const style = userPrefs?.value?.responseStyle || "concise";
    const responseFormat =
      style === "verbose" ? verboseResponse : conciseResponse;  // [!code highlight]
    return handler({
      ...request,
      responseFormat,
    });
  },
});

Runtime Context(사용자 역할·환경)에 따라 출력 형식 구성:

const adminResponse = z.object({
  answer: z.string().describe("Answer"),
  debugInfo: z.record(z.any()).describe("Debug information"),
  systemStatus: z.string().describe("System status"),
});
const userResponse = z.object({
  answer: z.string().describe("Answer"),
});
const contextBasedOutput = createMiddleware({
  name: "ContextBasedOutput",
  wrapModelCall: (request, handler) => {
    // Read from Runtime Context: user role and environment
    const userRole = request.runtime.context.userRole;
    const environment = request.runtime.context.environment;
    let responseFormat;
    if (userRole === "admin" && environment === "production") {
      responseFormat = adminResponse;  // [!code highlight]
    } else {
      responseFormat = userResponse;  // [!code highlight]
    }
    return handler({ ...request, responseFormat });
  },
});

툴 컨텍스트 (Tool context)

툴은 컨텍스트를 읽고 쓴다는 점에서 특별해요. 가장 기본적인 경우, 툴이 실행되면 LLM의 요청 파라미터를 받고 툴 메시지를 반환해요. 툴은 작업을 수행하고 결과를 만들어요. 툴은 또한 모델이 작업을 수행·완료할 수 있는 중요한 정보를 가져올 수 있어요.

읽기 (Reads)

대부분의 실제 툴은 LLM 파라미터 이상이 필요해요. 데이터베이스 쿼리용 사용자 ID, 외부 서비스용 API 키, 결정을 위한 현재 세션 상태가 필요해요. 툴은 이 정보에 접근하기 위해 상태, 스토어, 런타임 컨텍스트에서 읽어요.

State에서 현재 세션 정보 확인:

import { createAgent, tool, type ToolRuntime } from "langchain";
const checkAuthentication = tool(
  async (_, runtime: ToolRuntime) => {
    // Read from State: check current auth status
    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: "Check if user is authenticated",
    schema: z.object({}),
  }
);

Store에서 영구화된 사용자 선호도에 접근:

const getPreference = tool(
  async ({ preferenceKey }, runtime: ToolRuntime) => {
    const userId = runtime.context.userId;
    // Read from Store: get existing preferences
    const store = runtime.store;
    const existingPrefs = await store.get(["preferences"], userId);
    if (existingPrefs) {
      const value = existingPrefs.value?.[preferenceKey];
      return value ? `${preferenceKey}: ${value}` : `No preference set for ${preferenceKey}`;
    }
    return "No preferences found";
  },
  {
    name: "get_preference",
    description: "Get user preference from Store",
    schema: z.object({
      preferenceKey: z.string(),
    }),
  }
);

Runtime Context에서 API 키·사용자 ID 같은 구성 읽기:

import { tool } from "@langchain/core/tools";
const contextSchema = z.object({
  userId: z.string(),
  apiKey: z.string(),
  dbConnection: z.string(),
});
const fetchUserData = tool(
  async ({ query }, runtime: ToolRuntime<any, typeof contextSchema>) => {
    // Read from Runtime Context: get API key and DB connection
    const { userId, apiKey, dbConnection } = runtime.context;
    // Use configuration to fetch data
    const results = await performDatabaseQuery(dbConnection, query, apiKey);
    return `Found ${results.length} results for user ${userId}`;
  },
  {
    name: "fetch_user_data",
    description: "Fetch data using Runtime Context configuration",
    schema: z.object({
      query: z.string(),
    }),
  }
);

쓰기 (Writes)

툴 결과는 에이전트가 주어진 작업을 완료하는 데 도움이 돼요. 툴은 결과를 모델에 직접 반환할 수도 있고, 미래 단계에서 중요한 컨텍스트를 사용할 수 있도록 에이전트 메모리를 업데이트할 수도 있어요.

Command를 사용해 State에 세션 특정 정보 기록:

import { Command } from "@langchain/langgraph";
const authenticateUser = tool(
  async ({ password }) => {
    // Perform authentication
    if (password === "correct") {
      // Write to State: mark as authenticated using Command
      return new Command({
        update: { authenticated: true },
      });
    } else {
      return new Command({ update: { authenticated: false } });
    }
  },
  {
    name: "authenticate_user",
    description: "Authenticate user and update State",
    schema: z.object({
      password: z.string(),
    }),
  }
);

Store에 세션 간 데이터를 영구화:

const savePreference = tool(
  async ({ preferenceKey, preferenceValue }, runtime: ToolRuntime<any, typeof contextSchema>) => {
    const userId = runtime.context.userId;
    // Read existing preferences
    const existingPrefs = await runtime.store.get(["preferences"], userId);
    // Merge with new preference
    const prefs = existingPrefs?.value || {};
    prefs[preferenceKey] = preferenceValue;
    // Write to Store: save updated preferences
    await runtime.store.put(["preferences"], userId, prefs);
    return `Saved preference: ${preferenceKey} = ${preferenceValue}`;
  },
  {
    name: "save_preference",
    description: "Save user preference to Store",
    schema: z.object({
      preferenceKey: z.string(),
      preferenceValue: z.string(),
    }),
  }
);

툴에서 state, store, runtime context에 접근하는 종합적인 예제는 을 참고하세요.

수명주기 컨텍스트 (Life-cycle context)

핵심 에이전트 단계 사이에 일어나는 일을 제어하세요 - 요약, 가드레일, 로깅 같은 횡단 관심사를 구현하기 위해 데이터 흐름을 가로채요.

모델 컨텍스트툴 컨텍스트에서 본 것처럼, 미들웨어가 컨텍스트 엔지니어링을 실용적으로 만드는 메커니즘이에요. 미들웨어로 에이전트 수명주기의 어느 단계에든 연결하고 다음 중 하나를 할 수 있어요:

  1. 컨텍스트 업데이트 (Update context) - 변경을 영구화하기 위해 state와 store를 수정하고, 대화 기록을 업데이트하거나, 인사이트를 저장
  2. 수명주기 이동 (Jump in the lifecycle) - 컨텍스트에 따라 에이전트 주기의 다른 단계로 이동 (예: 조건이 충족되면 툴 실행 건너뛰기, 수정된 컨텍스트로 모델 호출 반복)

예제: 요약 (Example: Summarization)

가장 흔한 수명주기 패턴 중 하나는 기록이 너무 길어질 때 대화 기록을 자동으로 압축하는 것이에요. 모델 컨텍스트의 일시적 메시지 다듬기와 달리 요약은 상태를 영구적으로 업데이트 - 모든 미래 턴에 저장되는 요약으로 이전 메시지를 영구히 대체해요.

LangChain은 이를 위한 내장 미들웨어를 제공해요:

import { createAgent, summarizationMiddleware } from "langchain";
const agent = createAgent({
  model: "gpt-5.5",
  tools: [...],
  middleware: [
    summarizationMiddleware({
      model: "gpt-5.4-mini",
      trigger: { tokens: 4000 },
      keep: { messages: 20 },
    }),
  ],
});

대화가 토큰 한도를 초과하면 SummarizationMiddleware가 자동으로:

  1. 별도의 LLM 호출로 이전 메시지를 요약
  2. State에서 요약 메시지로 대체 (영구히)
  3. 최근 메시지는 컨텍스트를 위해 그대로 보존

요약된 대화 기록은 영구적으로 업데이트되어 미래 턴은 원래 메시지 대신 요약을 보게 돼요.

내장 미들웨어의 전체 목록, 사용 가능한 훅, 커스텀 미들웨어 생성 방법은 미들웨어 문서를 참고하세요.

모범 사례 (Best practices)

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

출처: 문서

본문

이 페이지는 에이전트의 컨텍스트 엔지니어링을 다뤄요. 신뢰할 수 있는 에이전트를 위해 올바른 정보·툴을 올바른 형식으로 제공하는 것의 중요성을 설명하고, 세 가지 컨텍스트 유형(모델 컨텍스트, 툴 컨텍스트, 수명주기 컨텍스트)과 데이터 소스(상태·스토어·런타임 컨텍스트)를 다뤄요. 각 컨텍스트 유형을 미들웨어로 제어하는 방법을 코드 예제로 보여주며, 요약 같은 수명주기 패턴과 모범 사례를 포함해요.

더 알아보기 (Learn more)