에이전트 (Agents)

에이전트 (Agents)

출처: LangChain 공식 문서 - Agents (OSS / Python)

에이전트는 주어진 작업이 끝날 때까지 모델이 도구(tool)를 반복적으로 호출하는 루프예요. 그 루프를 둘러싼 모든 것, 즉 프롬프트, 도구, 그리고 모델의 행동을 조정하는 각종 미들웨어까지 합쳐서 **하네스(harness)**라고 불러요.

에이전트 = 모델(Model) + 하네스(Harness)

하네스의 역할은 딱 하나예요. 주어진 작업을 처리하기 위해 모델에게 필요한 컨텍스트를 적절한 시점에 전달해 주는 거죠.

create_agent는 이 하네스를 아주 유연하게 설정할 수 있게 해 주는 팩토리 함수예요. 가장 단순한 형태로는 이렇게 만들 수 있어요.

그 위에 model=, tools=, system_prompt= 파라미터로 기본적인 것들을 바로 설정할 수 있고요, 더 고급 기능이 필요하면 미들웨어로 하네스를 확장해요.

Deep Agentscreate_agent를 기반으로 해서, 플래닝(planning), 파일 시스템 도구, 서브에이전트, 메모리처럼 자주 쓰이는 기능들을 미리 조립해 둔 버전이에요. 하네스를 직접 구성하고 싶을 때는 create_agent를 쓰면 돼요.

핵심 구성 요소 (Core components)

모델 (Model)

에이전트에 쓸 모델은 모델 식별자 문자열("provider:model")이나 이미 초기화된 모델 인스턴스를 넘겨서 골라요. 파라미터, 프로바이더 설정, 동적 모델 선택에 대한 내용은 Models 문서를 참고해요.

도구 (Tools)

에이전트에 도구를 제공하려면 Python 콜러블(callable), LangChain 도구, 또는 도구 딕셔너리 중 아무거나 넘기면 돼요. 도구 정의, 컨텍스트 접근, 동적 도구 선택은 Tools 문서를 보면 자세히 나와 있어요.

시스템 프롬프트 (System prompt)

에이전트가 작업에 어떻게 접근할지를 결정하는 부분이에요. system_prompt 파라미터는 문자열이나 SystemMessage를 받아요. 런타임에 동적으로 프롬프트를 바꿔야 한다면 미들웨어를 쓰는 게 방법이에요.

구조화된 출력 (Structured output)

response_format=를 사용하면 에이전트가 검증된 스키마(정해진 형태)를 반환하도록 만들 수 있어요. 전략과 예시는 Structured output 문서에 정리되어 있어요.

에이전트 상태 (Agent state)

모든 에이전트는 실행 컨텍스트를 AgentState로 관리해요. 이건 현재 대화 기록과, 도구·미들웨어가 필요로 하는 커스텀 필드를 담는 타입이 지정된 딕셔너리예요. 기본 제공 필드는 이렇게 생겼어요.

필드 타입 설명
messages list[BaseMessage] 현재 스레드의 전체 대화 기록. 추가 전용(append-only)이라 새 메시지가 추가될 뿐, 교체되지는 않아요.

AgentState는 동시에 모든 노드 스타일 미들웨어 훅(before_model, after_model 등)의 타입 시그니처이기도 해요. 훅은 현재 상태를 받아서, 다시 상태로 병합할 업데이트 딕셔너리를 반환할 수 있어요. 커스텀 필드(예: user_id나 카운터)를 추가하려면 AgentState를 서브클래싱하고, 그 서브클래스를 create_agentstate_schema=로 넘기면 돼요.

자세한 내용과 예시, 미들웨어 수준의 상태 스키마는 Short-term memoryCustom middleware를 참고해요.

호출하기 (Invocation)

이 루프의 각 단계를 추적하고, 도구 호출을 디버깅하고, 에이전트 출력을 평가하려면 LangSmith를 활용해요. tracing quickstart를 따라 설정하면 돼요. 또한 LangSmith Engine을 설정해 두면 트레이스를 모니터링하고 문제를 감지해서 수정안까지 제안해 주니 함께 쓰길 권해요.

에이전트는 메시지로 호출할 수 있어요. 내부적으로는 에이전트의 State에 업데이트를 전달하는 방식이에요. 모든 에이전트는 자신의 상태에 메시지 시퀀스를 포함하고 있는데, 에이전트를 호출할 때는 thread_id와 함께 새 메시지를 넘겨서 대화 기록을 유지·재개할 수 있게 해요.

from langchain.agents import create_agent
from langchain_core.utils.uuid import uuid7
from langgraph.checkpoint.memory import InMemorySaver

agent = create_agent(
    model="google_genai:gemini-3.6-flash",
    tools=[],
    checkpointer=InMemorySaver(),
)

config = {"configurable": {"thread_id": str(uuid7())}}

result = agent.invoke(
    {"messages": [{"role": "user", "content": "What's the weather in San Francisco?"}]},
    config=config,
)

# 같은 대화에서 이어지는 턴: 같은 thread_id를 재사용해 기록을 유지
result = agent.invoke(
    {"messages": [{"role": "user", "content": "What about tomorrow?"}]},
    config=config,
)

thread_id로 대화 기록을 유지하려면 에이전트에 체크포인터(checkpointer)를 설정해 둬야 해요. LangSmith에 배포하면 체크포인터가 자동으로 마련되고요, 로컬에서는 예시처럼 create_agent(..., checkpointer=InMemorySaver())로 직접 넘겨주면 돼요.

도구와 미들웨어에 실행별(run) 설정(예: 사용자 ID, API 키, 기능 플래그)을 함께 넘겨야 한다면, config와 나란히 context로 전달해요. 그 데이터의 형태는 context_schema로 정의하고, runtime.context를 통해 접근하면 돼요.

from dataclasses import dataclass

from langchain.agents import create_agent
from langchain_core.utils.uuid import uuid7
from langgraph.checkpoint.memory import InMemorySaver


@dataclass
class Context:
    user_id: str


agent = create_agent(
    model="google_genai:gemini-3.6-flash",
    tools=[],
    context_schema=Context,
    checkpointer=InMemorySaver(),
)

result = agent.invoke(
    {"messages": [{"role": "user", "content": "What's the weather in San Francisco?"}]},
    config={"configurable": {"thread_id": str(uuid7())}},
    context=Context(user_id="user-123"),
)

thread_id대화(메시지 기록, 체크포인트)의 범위를 정하고, context는 호출 시점에 도구·미들웨어가 읽는 실행별 데이터를 담아요. 둘 다 같이 넘기는 게 일반적이에요. 더 자세한 내용은 tool contextRuntime 문서를 확인해요.

스트리밍 (Streaming)

invoke는 실행이 끝나면 최종 응답을 반환해요. 그런데 에이전트가 여러 번 도구를 호출하는 경우, 사용자 입장에선 완료가 되기 전에도 진행 상황을 알 수 있으면 좋겠죠. 이럴 때 스트리밍을 쓰면 중간 메시지와 도구 활동을 그때그때 표시해 줄 수 있어요.

from langchain.messages import AIMessage, HumanMessage


stream = agent.stream_events(
    {"messages": [{"role": "user", "content": "Search for AI news and summarize the findings"}]},
    version="v3",
)
for snapshot in stream.values:
    # 각 스냅샷은 그 시점의 전체 상태를 담고 있어요
    latest_message = snapshot["messages"][-1]
    if latest_message.content:
        if isinstance(latest_message, HumanMessage):
            print(f"User: {latest_message.content}")
        elif isinstance(latest_message, AIMessage):
            print(f"Agent: {latest_message.content}")
    elif latest_message.tool_calls:
        print(f"Calling tools: {[tc['name'] for tc in latest_message.tool_calls]}")

스트리밍 모드, 이벤트 타입, UI 패턴에 대해선 Streaming 문서를 참고해요.

하네스 구성하기 (Configure the harness)

create_agent는 확장성이 아주 높아요. 커스터마이징의 기본 단위가 미들웨어인데, 각 미들웨어는 딱 하나의 관심사만 처리하고, 에이전트 루프의 적절한 시점에 훅으로 들어가며, 다른 미들웨어와 자유롭게 조합돼요. 자기 유스케이스에 필요한 것만 골라 쓰고 나머지는 과감히 제외하면 돼요. 자주 쓰는 패턴들은 일급(first-class) 미들웨어로 미리 만들어져 있고, 그 외의 것은 커스텀 미들웨어로 직접 만들 수 있어요.

에이전트가 복잡한 작업을 맡게 되면 몇 가지 핵심 영역에서 지원이 필요해요. 미들웨어 생태계가 제공하는 것들은 다음과 같아요.

  • 실행 환경 (Execution environment): 도구, 파일 시스템, 샌드박스, 코드 실행
  • 컨텍스트 관리 (Context management): 요약, 메모리, 스킬, 프롬프트 캐싱
  • 플래닝과 위임 (Planning and delegation): 병렬·격리 작업을 위한 할 일 목록과 서브에이전트
  • 내결함성 (Fault tolerance): 재시도, 폴백, 호출 제한
  • 가드레일 (Guardrails): PII 탐지와 콘텐츠 제어
  • 스티어링 (Steering): 영향력이 큰 작업 전 사람의 승인(Human-in-the-loop)

create_deep_agent는 오래 걸리는 코딩·리서치 작업을 위해 이 스택을 미리 조립해 둔 버전이에요(파일 시스템, 요약, 서브에이전트, 프롬프트 캐싱이 기본 포함). 미리 만들어진 전체 하네스는 Deep Agents 문서를 봐요.

실행 환경 (Execution environment)

에이전트는 단순히 텍스트를 생성하는 것보다 실제로 행동을 취할 수 있을 때 특히 유용해져요. 실행 환경은 에이전트에게 작업 공간을 제공해 주는데, 호출할 수 있는 도구, 턴 사이에 파일을 읽고 쓸 파일 시스템, 그리고 스크립트나 셸 명령을 실행할 코드 실행 기능이 그 대상이에요.

FilesystemMiddleware, Sandboxes, Interpreters 문서를 참고해요.

이 예시는 deepagents 패키지에서 임포트하는데, 설치 명령은 아래에 있어요.

컨텍스트 관리 (Context management)

모든 모델 호출에는 고정된 컨텍스트 창이 있어요. 에이전트가 실행되면 이 창은 쌓여가는 기록, 도구 결과, 중간 단계로 가득 차요. 요약(Summarization)은 오버플로가 나기 전에 기록을 압축하고, 메모리(Memory)는 시작 시점에 영구적인 지침을 로드해서 지식이 세션을 넘어 이어지게 하며, 스킬(Skills)은 모든 걸 한꺼번에 로드하는 대신 필요할 때 도메인 지식을 꺼내 쓰게 해 줘요.

from deepagents.backends import StateBackend
from deepagents.middleware import FilesystemMiddleware, MemoryMiddleware, SkillsMiddleware, SummarizationMiddleware

backend = StateBackend()
model = "google_genai:gemini-3.6-flash"

agent = create_agent(
    model=model,
    tools=[search],
    middleware=[
        FilesystemMiddleware(backend=backend),
        SummarizationMiddleware(model=model, backend=backend),
        MemoryMiddleware(backend=backend, sources=["./AGENTS.md"]),
        SkillsMiddleware(backend=backend, sources=["./skills/"]),
    ],
)

SummarizationMiddleware, MemoryMiddleware, Skills, Context engineering 문서를 참고해요.

이 예시는 deepagents 패키지에서 임포트하는데, 설치 명령은 아래에 있어요.

플래닝과 위임 (Planning and delegation)

복잡한 작업은 하나의 컨텍스트 창이 감당하기 어려운 경우가 많아요. 이럴 때 위임(Delegation)을 쓰면 메인 에이전트가 작업을 여러 조각으로 쪼개서, 각각 자체 격리된 컨텍스트에서 실행되는 서브에이전트에게 넘겨줄 수 있어요. 메인 에이전트는 실행에 매몰되지 않고 조율에 집중하게 되죠. 작업은 병렬로 실행될 수 있고, 메인 에이전트의 컨텍스트는 깨끗하게 유지돼요.

from deepagents.backends import StateBackend
from deepagents.middleware import FilesystemMiddleware
from deepagents.middleware.subagents import SubAgentMiddleware
from langchain.agents import create_agent
from langchain.agents.middleware import TodoListMiddleware
from langchain.tools import tool


@tool
def search(query: str) -> str:
    """Search for a query and return a short summary."""
    return f"Search results for: {query}"


backend = StateBackend()

agent = create_agent(
    model="google_genai:gemini-3.6-flash",
    tools=[search],
    middleware=[
        FilesystemMiddleware(backend=backend),
        TodoListMiddleware(),
        SubAgentMiddleware(
            backend=backend,
            subagents=[
                {
                    "name": "researcher",
                    "description": "Searches and returns a structured summary.",
                    "system_prompt": "Use the search tool to research the question and summarize key points.",
                    "tools": [search],
                    "model": "anthropic:claude-sonnet-4-6",
                    "middleware": [],
                }
            ],
        ),
    ],
)

Subagents 문서를 참고해요.

이 예시는 deepagents 패키지에서 임포트하는데, 설치 명령은 아래에 있어요.

에이전트에 이름 붙이기 (Name your agent)

에이전트에 식별자를 붙일 수 있어요. 특히 멀티 에이전트 시스템에서 이 에이전트를 서브그래프(subgraph)로 임베드할 때 유용해요.

내결함성 (Fault tolerance)

프로덕션에서 에이전트를 돌리다 보면 개발 중에는 좀처럼 보지 못했던 실패를 마주치게 돼요. 레이트 리밋, 모델 타임아웃, 일시적인 API 에러 같은 것들이죠. 내결함성 미들웨어는 이런 것들을 인프라 레벨에서 처리해 줘서, 도구와 비즈니스 로직이 매 호출마다 try/catch를 감싸지 않아도 되게 해 줘요.

from langchain.agents import create_agent
from langchain.agents.middleware import ModelRetryMiddleware, ToolRetryMiddleware
from langchain.tools import tool


@tool
def search(query: str) -> str:
    """Search for a query and return a short summary."""
    return f"Search results for: {query}"


agent = create_agent(
    model="google_genai:gemini-3.6-flash",
    tools=[search],
    middleware=[
        ModelRetryMiddleware(max_retries=3),
        ToolRetryMiddleware(max_retries=2),
    ],
)

ModelRetryMiddleware, ToolRetryMiddleware, Prebuilt middleware 문서를 참고해요.

가드레일 (Guardrails)

어떤 정책은 프롬프트에 담을 수 없어요. 모델이 무슨 짓을 하든 결정적으로(deterministically) 강제돼야 하거든요. 가드레일은 데이터가 에이전트 루프를 흐를 때 그것을 가로채서, 도구 결과가 모델의 컨텍스트에 도달하기 전에 규정 준수(compliance) 규칙이나 콘텐츠 정책을 적용해요.

PIIMiddleware, Prebuilt middleware 문서를 참고해요.

스티어링 (Steering)

항상 완전한 자율이 적절한 건 아니에요. 스티어링은 파괴적인 쓰기, 비싼 API 호출, 판단이 필요한 일 같은 특정 결정 지점에 사람을 배치할 수 있게 해 줘요. 에이전트를 재구성할 필요 없이요. 에이전트는 잠시 멈추고 기다렸다가, 사람이 승인·수정·거부하면 실행을 계속해요.

HumanInTheLoopMiddleware, Human-in-the-loop 문서를 참고해요.

미들웨어 자료 (Middleware resources)

  • Middleware overview — 미들웨어 스택이 어떻게 동작하고 훅이 언제 발동하는지
  • Prebuilt middleware — 설정 예시를 포함한 전체 참조
  • Custom middleware — 비즈니스 로직, PII 스크러빙 등을 위한 나만의 훅 작성