에이전트 컨텍스트 엔지니어링 (Context Engineering)

에이전트 컨텍스트 엔지니어링 (Context Engineering)

컨텍스트 엔지니어링이 처음이라면, 먼저 개념 개요부터 보는 걸 추천해요. 컨텍스트의 다양한 유형과 각각을 언제 써야 하는지에 대한 설명이 담겨 있어요.

개요 (Overview)

에이전트는 왜 실패할까?

아무리 좋은 모델을 써도 에이전트가 생각대로 동작하지 않는 경우가 많아요. 대부분의 실패는 모델 자체의 한계가 아니라, 모델에게 전달되는 컨텍스트가 잘못 구성됐기 때문이에요. 필요한 정보가 빠져 있거나, 반대로 필요 없는 정보까지 몽땅 넣어 토큰을 낭비하면 결과가 어긋나기 쉬워요. 그래서 에이전트를 설계할 때는 "무엇을, 언제, 어느 정도로 모델에게 보여줄지"를 결정하는 컨텍스트 엔지니어링이 핵심이 돼요.

에이전트 루프

에이전트는 아래 그림처럼 루프를 돌며 동작해요. 모델이 한 번 답하면 끝나는 게 아니라, 도구를 호출하고 그 결과를 다시 컨텍스트에 넣어 모델이 다음 결정을 내리는 과정을 반복해요.

Core agent loop diagram

이 루프가 반복될수록 컨텍스트가 쌓이고, 모델이 결정을 내리는 데 쓰는 정보가 달라져요. 그래서 컨텍스트 엔지니어링은 "한 번의 프롬프트를 잘 쓰는 일"이 아니라 루프 전반에서 정보가 어떻게 흐르고 저장되는지를 설계하는 일이에요.

무엇을 제어할 수 있을까?

에이전트 루프 안에서 여러분이 직접 제어할 수 있는 컨텍스트가 몇 가지 있어요. 이 문서는 이 표를 기준으로 하나씩 살펴볼 거예요.

컨텍스트 종류 무엇을 제어하는가 휘발성(Transient) 또는 영구성(Persistent)
모델 컨텍스트 모델이 보는 것 — 메시지, 도구, 시스템 프롬프트, 응답 형식 휘발성
도구 컨텍스트 도구가 접근하고 생성하는 것 (state, store, 런타임 컨텍스트에 대한 읽기/쓰기) 영구성
수명주기 컨텍스트 핵심 단계 사이에서 데이터 흐름을 가로채는 것 (요약, 가드레일, 로깅 등)

영구 컨텍스트(Persistent context) 로 분류된 것들은 턴(회차)을 넘어 state에 저장돼요. 수명주기 훅과 도구의 쓰기가 이걸 영구적으로 수정해요.

동작 방식

LangChain의 미들웨어(middleware)가 컨텍스트 엔지니어링을 실용적으로 만들어 주는 핵심 메커니즘이에요. 미들웨어를 쓰면 에이전트 수명주기의 어떤 단계에도 훅을 걸 수 있고, 다음 두 가지를 할 수 있어요.

  • 컨텍스트를 갱신한다
  • 에이전트 수명주기의 다른 단계로 점프한다

이 문서 전반에서 컨텍스트 엔지니어링을 구현하는 수단으로 미들웨어 API가 자주 등장할 거예요.

모델 컨텍스트 (Model Context)

모델 컨텍스트는 모델에게 실제로 전달되는 것들을 뜻해요. 크게 다음으로 나뉘어요.

  • 메시지(Messages) — LLM에 보내지는 대화 이력(메시지 목록 전체)
  • 도구(Tools) — 에이전트가 행동하는 데 쓸 수 있는 유틸리티

이 모델 컨텍스트들은 모두 state(단기 기억), store(장기 기억), 또는 런타임 컨텍스트(정적 설정) 에서 끌어올 수 있어요.

시스템 프롬프트 (System Prompt)

시스템 프롬프트는 어디서 컨텍스트를 가져올지에 따라 다양하게 구성할 수 있어요. 대표적으로 state, store, 런타임 컨텍스트에서 끌어오는 방식이 있어요.

State 기반@dynamic_prompt 데코레이터로 state에 저장된 값을 시스템 프롬프트에 반영해요.

from langchain.agents import create_agent
from langchain.agents.middleware import dynamic_prompt, ModelRequest

@dynamic_prompt
def state_aware_prompt(request: ModelRequest) -> str:
    ...

Store 기반 — 장기 기억(store)에서 사용자별 설정을 읽어 프롬프트에 반영해요. context_schemastore를 함께 지정해요.

from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.agents.middleware import dynamic_prompt, ModelRequest
from langgraph.store.memory import InMemoryStore

@dataclass
class Context:
    user_id: str

@dynamic_prompt
def store_aware_prompt(request: ModelRequest) -> str:
    ...
    base = "You are a helpful assistant."
    user_prefs = request.store.get(("prefs", request.context.user_id))
    if user_prefs is not None:
        style = user_prefs.value.get("communication_style", "balanced")
        base += f"\nUser prefers {style} responses."

    return base

agent = create_agent(
    model="gpt-5.5",
    tools=[...],
    middleware=[store_aware_prompt],
    context_schema=Context,
    store=InMemoryStore()
)

런타임 컨텍스트 기반 — 정적 설정 값(예: 사용자 역할, 배포 환경)을 context_schema에 담아 프롬프트에 반영해요.

from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.agents.middleware import dynamic_prompt, ModelRequest

@dataclass
class Context:
    user_role: str
    deployment_env: str

@dynamic_prompt
def context_aware_prompt(request: ModelRequest) -> str:
    base = "You are a helpful assistant."
    ...
    return base

agent = create_agent(
    model="gpt-5.5",
    tools=[...],
    middleware=[context_aware_prompt],
    context_schema=Context
)

메시지 (Messages)

메시지는 LLM에 보내지는 프롬프트를 구성해요. 모델이 제대로 답하려면 메시지 내용을 잘 관리하는 게 무엇보다 중요해요.

State에서 파일 컨텍스트 주입 — 현재 질문과 관련된 업로드 파일 정보를 state에서 읽어, 최근 메시지 앞에 주입하는 예시예요. wrap_model_call로 모델 호출을 감싸고 request.override(messages=messages)로 메시지 목록을 바꿔요.

from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from typing import Callable

@wrap_model_call
def inject_file_context(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
):
    uploaded_files = request.state.get("uploaded_files", [])
    if uploaded_files:
        # Build context about available files
        file_descriptions = []
        for file in uploaded_files:
            file_descriptions.append(
                f"- {file['name']} ({file['type']}): {file['summary']}"
            )

        file_context = f"""Files you have access to in this conversation:
{chr(10).join(file_descriptions)}

Reference these files when answering questions."""

        # Inject file context before recent messages
        messages = [
            *request.messages,
            {"role": "user", "content": file_context},
        ]
        request = request.override(messages=messages)

    return handler(request)

agent = create_agent(
    model="gpt-5.5",
    tools=[...],
    middleware=[inject_file_context]
)

메시지를 업데이트하는 방법은 위처럼 messages를 바꾸는 것만 있는 건 아니에요. 다음 방법도 함께 기억해 두면 좋아요.

  • wrap_model_call에서 ExtendedModelResponse와 함께 Command를 반환해, 모델 호출 계층에서 state 업데이트를 주입할 수 있어요.
  • before_model, after_model, wrap_tool_call(도구 반환 처리용) 같은 수명주기 훅을 이용해 대화 이력을 갱신할 수 있어요. 자세한 내용은 미들웨어 문서를 확인해요.

state 업데이트에 대한 자세한 내용은 State updates에서 다뤄요.

도구 (Tools)

도구는 모델이 데이터베이스, API, 외부 시스템과 상호작용하게 해줘요. 도구를 어떻게 정의하고 어떻게 선택하는지에 따라 모델이 작업을 완수할 수 있는지가 결정돼요.

도구 정의하기 (Defining tools)

각 도구에는 명확한 이름, 설명, 인자 이름, 인자 설명이 필요해요. 이건 단순한 메타데이터가 아니라, 모델이 언제 그리고 어떻게 도구를 써야 할지 추론하도록 안내하는 역할을 해요.

from langchain.tools import tool

@tool(parse_docstring=True)
def search_orders(
    user_id: str,
    status: str,
    limit: int = 10
) -> str:
    """Search for user orders by status.

    Args:
        user_id: The ID of the user whose orders to search.
        status: The order status to filter by (pending, shipped, delivered).
        limit: Maximum number of orders to return.
    """
    ...

parse_docstring=True를 쓰면 docstring의 Args 부분을 자동으로 인자 설명으로 파싱해줘요. 이렇게 잘 정리된 설명이 모델의 도구 선택을 정확하게 만들어요.

도구 선택하기 (Selecting tools)

상황에 따라 어떤 도구를 모델에게 노출할지를 동적으로 결정할 수도 있어요. wrap_model_call로 요청에 담긴 도구 목록을 상황(state, store, 런타임 컨텍스트)에 맞춰 걸러내는 방식이에요.

from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from typing import Callable

@wrap_model_call
def state_based_tools(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
):
    ...
    # state에 담긴 사용자 역할에 따라 요청 도구 목록을 필터링
    request = request.override(tools=filtered_tools)
    return handler(request)

이렇게 도구 목록을 상황에 맞춰 바꾸면, 불필요한 도구를 모델이 고민할 필요가 없어져요. context_schema로 사용자 역할 같은 값을 담아 활용할 수도 있어요.

from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from typing import Callable

@dataclass
class Context:
    user_role: str

@wrap_model_call
def role_based_tools(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
):
    ...

사전 등록된 도구를 필터링하는 것부터, MCP 서버 등에서 도구를 런타임에 등록하는 것까지 모두 다루는 자세한 내용은 동적 도구(Dynamic tools) 문서를 확인해요.

모델 (Model)

에이전트가 사용하는 모델 자체를 상황에 따라 바꿀 수도 있어요. request.override(model=model)로 모델을 교체할 수 있어요.

State 기반 — state에 저장된 값에 따라 모델을 선택해요.

from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from langchain.chat_models import init_chat_model
from typing import Callable

@wrap_model_call
def state_based_model(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
):
    ...
    model = init_chat_model("gpt-5.4-mini")
    request = request.override(model=model)
    return handler(request)

agent = create_agent(
    model="gpt-5.4-mini",
    tools=[...],
    middleware=[state_based_model]
)

Store 기반 — 사용자가 선호하는 모델을 store에서 읽어서 써요.

from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from langchain.chat_models import init_chat_model
from typing import Callable
from langgraph.store.memory import InMemoryStore

@dataclass
class Context:
    user_id: str

@wrap_model_call
def store_based_model(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
):
    ...
    request = request.override(model=model)
    return handler(request)

agent = create_agent(
    model="gpt-5.5",
    tools=[...],
    middleware=[store_based_model],
    context_schema=Context,
    store=InMemoryStore()
)

런타임 컨텍스트 기반 — 비용 한도나 배포 환경 같은 정적 설정 값에 따라 모델을 고르는 방식이에요.

from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from langchain.chat_models import init_chat_model
from typing import Callable

@dataclass
class Context:
    ...
    cost_limit: float

@wrap_model_call
def context_based_model(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
):
    ...
    # 비용 한도나 환경에 따라 모델을 선택
    model = init_chat_model(...)
    request = request.override(model=model)

    return handler(request)

agent = create_agent(
    model="gpt-5.5",
    tools=[...],
    middleware=[context_based_model],
    context_schema=Context
)

더 많은 예시는 동적 모델(Dynamic model) 문서에서 확인할 수 있어요.

응답 형식 (Response format)

선택하기 (Selecting formats) — 모델이 어떤 형식으로 응답할지도 컨텍스트 엔지니어링의 일부예요. pydantic의 BaseModel로 응답 스키마를 정의하면 구조화된 출력을 받을 수 있어요.

from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from pydantic import BaseModel, Field
from typing import Callable

class SimpleResponse(BaseModel):
    answer: str = Field(description="The answer to the user's question")
    ...

도구 컨텍스트 (Tool Context)

도구는 다른 컨텍스트와 달리 컨텍스트를 읽기도 하고 쓰기도 하는 특별한 존재예요. 가장 기본적인 경우, 도구가 실행되면 LLM의 요청 파라미터를 받아 작업을 수행하고 결과를 도구 메시지로 돌려줘요. 도구는 또 모델이 작업을 수행·완수하는 데 필요한 중요한 정보를 가져오기도 해요.

읽기 (Reads)

도구는 ToolRuntime을 통해 state, store, 런타임 컨텍스트를 읽을 수 있어요.

State 읽기runtime.state로 현재 인증 상태 같은 세션 정보를 확인해요.

from langchain.tools import tool, ToolRuntime
from langchain.agents import create_agent

@tool
def check_authentication(
    runtime: ToolRuntime
) -> str:
    """Check if user is authenticated."""
    # Read from State: check current auth status
    current_state = runtime.state
    is_authenticated = current_state.get("authenticated", False)

    if is_authenticated:
        return "User is authenticated"
    else:
        return "User is not authenticated"

agent = create_agent(
    model="gpt-5.5",
    tools=[check_authentication]
)

Store 읽기 — 장기 기억에서 사용자별 설정을 읽어요. context_schema로 사용자 ID를 받아 store에서 꺼내 쓸 수 있어요.

from dataclasses import dataclass
from langchain.tools import tool, ToolRuntime
from langchain.agents import create_agent
from langgraph.store.memory import InMemoryStore

@dataclass
class Context:
    user_id: str

@tool
def get_preference(
    runtime: ToolRuntime
) -> str:
    """Get user preference from long-term memory."""
    prefs = runtime.store.get(("prefs", runtime.context.user_id))
    ...

런타임 컨텍스트 읽기 — API 키, DB 연결 정보 같은 정적 설정을 도구가 직접 읽을 수 있어요.

from dataclasses import dataclass
from langchain.tools import tool, ToolRuntime
from langchain.agents import create_agent

@dataclass
class Context:
    user_id: str
    api_key: str
    db_connection: str

@tool
def fetch_user_data(
    runtime: ToolRuntime
) -> str:
    """Fetch user data using runtime context credentials."""
    api_key = runtime.context.api_key
    ...

쓰기 (Writes)

도구의 결과는 에이전트가 작업을 완수하도록 돕는 데 쓰여요. 도구는 결과를 모델에게 직접 반환할 수도 있고, 에이전트의 메모리를 갱신해 이후 단계에서 중요한 컨텍스트로 활용되게 할 수도 있어요.

State에 쓰기Command를 이용해 턴에 특화된 세션 정보를 state에 기록해요.

from langchain.tools import tool
from langchain.agents.middleware import Command

@tool
def authenticate_user(
    user_id: str, password: str
) -> ...:
    ...
    # Write to State to track session-specific information
    return Command(
        update={"authenticated": True, "user_id": user_id},
        result="User authenticated"
    )

agent = create_agent(
    model="gpt-5.5",
    tools=[authenticate_user]
)

state, store, 런타임 컨텍스트에 접근하는 자세한 도구 예시는 도구(Tools) 문서에서 확인할 수 있어요.

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

지금까지 봤듯이, 미들웨어는 컨텍스트 엔지니어링을 실용적으로 만들어 주는 메커니즘이에요. 미들웨어는 에이전트 수명주기의 핵심 단계 사이에서 일어나는 일을 제어해요. 즉 데이터 흐름을 가로채서 요약, 가드레일, 로깅 같은 횡단 관심사를 구현할 수 있게 해주고, 에이전트 수명주기의 어떤 단계에도 훅을 걸어 컨텍스트를 갱신하거나 다른 단계로 점프할 수 있어요.

예시: 요약 (Summarization)

대화가 길어지면 토큰이 금방 차요. 이때 대화 이력을 압축하는 요약이 특히 유용한데, LangChain은 이를 위한 내장 미들웨어를 제공해요.

from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware

agent = create_agent(
    model="gpt-5.5",
    tools=[...],
    middleware=[
        SummarizationMiddleware(
            model="gpt-5.4-mini",
            trigger={"tokens": 4000},
            keep=("messages", 20),
        ),
    ],
)

triggertokens: 4000은 메시지 토큰이 4000을 넘으면 요약을 트리거한다는 뜻이고, keep=("messages", 20)은 최근 메시지 20개는 그대로 유지하면서 나머지를 요약한다는 뜻이에요.

요약된 대화 이력은 영구적으로 갱신돼요. 이후 턴에서는 원래 메시지 대신 요약본을 보게 되죠.

내장 미들웨어의 전체 목록, 사용 가능한 훅, 그리고 커스텀 미들웨어를 만드는 방법에 대해서는 미들웨어 문서를 확인해요.

모범 사례 (Best practices)

  • 내장 미들웨어를 활용하세요SummarizationMiddleware, LLMToolSelectorMiddleware처럼 이미 잘 만들어진 미들웨어를 최대한 활용하는 게 좋아요. 직접 만들기 전에 내장된 것을 먼저 확인해요.
  • 컨텍스트 전략을 문서화하세요 — 어떤 컨텍스트를 왜 전달하는지 분명히 기록해 두면, 나중에 에이전트 동작을 이해하고 디버깅하기 훨씬 쉬워져요.
  • 컨텍스트를 상황에 맞춰 최소화하세요 — 필요한 정보만, 필요한 시점에 넣는 것이 토큰 효율과 응답 품질 양쪽에 좋아요.

원문: https://docs.langchain.com/oss/python/langchain/context-engineering