본문 바로가기
인공지능 활용하기

DeepAgents로 배우는 실전 LangGraph 에이전트 설계

by 빈이름 2026. 3. 31.

AI 에이전트, 막상 만들려니 막막하다...

LangChain 공식 문서 따라 챗봇 하나 만들어봤고, LangGraph 튜토리얼로 간단한 에이전트도 돌려봤습니다. 그런데 막상 "이걸 진짜 서비스에 붙여보자"는 생각이 드는 순간, 머릿속이 하얘집니다.

"대화 내용은 어디에 저장하지? 에이전트가 무한 루프에 빠지면 어떡하지? 파일을 실수로 날려버리면? API 비용은 왜 이렇게 나오는 거야?"

 

튜토리얼에서는 아무도 이런 얘기를 안 해줍니다. 그냥 agent.invoke()만 보여주고 끝이죠.

 

그래서 오늘은 LangGraph 팀이 직접 공개한 실전 에이전트 프레임워크, DeepAgents의 소스코드를 분석해 그 안에 담긴 설계 패턴을 뽑아보려고 합니다. DeepAgents는 복잡한 작업을 자율적으로 수행하는 에이전트의 레퍼런스 구현체입니다.

 

즉, "AI 에이전트를 처음부터 다 짜지 않아도 되게" 만든 틀인데, 이 틀 자체가 실제 product 단계의 AI 서비스가 어떤 식으로 구현되고, 어떤 프롬프트를 사용하고 있는지를 파악할 수 있는 좋은 예시가 되어 줍니다.

 

이 글에서 다뤄 볼 패턴은 총 6가지 입니다.

1. 전체 구조
2. 시스템 프롬프트 설계
3. 기억 기능
4. 토큰 비용 절약
5. 안전장치 설계
6. 서브 에이전트

읽어보고 내가 만들고 싶은 서비스에 어떤 패턴들을 적용하면 좋을지 도움이 되었으면 좋겠습니다.


1. DeepAgents의 전체구조

에이전트는 단순 챗봇을 넘어 직접 작업을 수행할 수 있는 인공지능을 의미합니다. AI가 직접 작업을 수행하기 위해선 AI가 사용할 수 있는 툴을 제공해 줘야 합니다. 처음 langchain 개발을 하는 입장에서 이 툴을 에이전트가 올바르게 이해하게 하려면 어떻게 설명해야 하는지, 실제 도구의 기능은 어디에 구현해야 하는지 막막할 수 있습니다.

 

DeepAgents는 이 두가지 역할을 미들웨어(Middleware)와 백엔드(Backend)로 나눠서 처리합니다.

  • Backend : 각 도구의 실제 기능 구현체. 파일을 읽고 쓰고, 셸 명령을 실행하고, 웹 검색을 수행하는 실제 기능들이 구현되어 있습니다.
  • Middleware : 에이전트가 그 도구를 언제, 왜, 어떻게 써야 하는지 이해할 수 있도록 감싸주는 레이어입니다.

비유하자면 Backend는 실제 요리를 하는 주방 셰프이고, Middleware는 에이전트에게 메뉴를 설명해주는 홀 직원과 같습니다. 손님(에이전트)은 메뉴판만 보고 주문하고, 실제 요리는 주방에서 처리하는 것이죠.

 

또한 에이전트이기 때문에 그래프 노드를 직접 구성하지 않고 create_agent() 모듈을 사용해 그래프 구성을 에이전트가 스스로 하도록 위임하고 있습니다. 이렇게 자율적으로 맡기는 방식은 복잡하고 비선형적인 작업에 유연하게 대응가능하고, 기능을 추가할 때 그래프를 추가적으로 구성할 필요가 없다는 이점을 갖습니다.

 

DeepAgents에는 10가지의 툴을 기본적으로 제공하고 있는데요, 간단하게 하나 정도만 실제 backend와 middleware 구현체를 살펴보고 나머지는 코드에서 직접 살펴보시는거로 하겠습니다.

 

FileSystem의 파일 생성 부분의 backend 코드입니다. (실제 코드)

def write(self, file_path: str, content: str) -> WriteResult:
    resolved_path = self._resolve_path(file_path)   # 경로 정규화 + 탈출 방지

    if resolved_path.exists():                       # 이미 존재하면 에러 반환
        return WriteResult(error="Cannot write to ... because it already exists.")

    resolved_path.parent.mkdir(parents=True, exist_ok=True)  # 부모 디렉토리 생성

    flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
    if hasattr(os, "O_NOFOLLOW"):
        flags |= os.O_NOFOLLOW            # 심볼릭 링크 공격 방지
    fd = os.open(resolved_path, flags, 0o644)
    with os.fdopen(fd, "w", encoding="utf-8") as f:
        f.write(content)

    return WriteResult(path=file_path, files_update=None)

구체적인 내용을 보실 필요는 없고, 실제로 시스템에서 파일을 생성하는 코드만 작성되어 있다는 것만 확인하면 됩니다.

 

다음으로 연결된 Middleware 부분을 살펴볼게요. (실제 코드)

 

1. 툴 설명 : 에이전트가 이 툴을 언제 쓸지 판단하는 기준이 됩니다. (L:227)

WRITE_FILE_TOOL_DESCRIPTION = """Writes to a new file in the filesystem.

Usage:
- The write_file tool will create the a new file.
- Prefer to edit existing files (with the edit_file tool) over creating new ones when possible.
"""

 

2. 입력 스키마 : 에이전트가 이 툴을 사용하기 위해 어떤 파라미터를 넘길지 정의하는 부분입니다. (L: 141)

class WriteFileSchema(BaseModel):
    """Input schema for the `write_file` tool."""

    file_path: str = Field(description="Absolute path where the file should be created. Must be absolute, not relative.")
    content: str = Field(description="The text content to write to the file. This parameter is required.")

 

3. 실제 툴 함수 : 실제 backend 호출과 그 결과를 포장하는 함수가 구현되어 있는 부분입니다. (L: 789)

    def _create_write_file_tool(self) -> BaseTool:
        """Create the write_file tool."""
        tool_description = self._custom_tool_descriptions.get("write_file") or WRITE_FILE_TOOL_DESCRIPTION

        def sync_write_file(
            file_path: Annotated[str, "Absolute path where the file should be created. Must be absolute, not relative."],
            content: Annotated[str, "The text content to write to the file. This parameter is required."],
            runtime: ToolRuntime[None, FilesystemState],
        ) -> Command | str:
            """Synchronous wrapper for write_file tool."""
            resolved_backend = self._get_backend(runtime)
        ...

 

이런 식으로 Backend와 Middleware의 분리를 통해 코드의 유지보수성을 높였으며, 에이전트에게 툴의 기능을 설명하는 프롬프트가 어떤 식으로 작성되는지 참고할 수 있습니다.


2. 시스템 프롬프트 설계

시스템 프롬프트란, 에이전트가 작업을 수행하면서 항상 유념해야 하는 철칙 같은 것을 정리한 프롬프트입니다. 에이전트의 모든 작업마다 이 프롬프트를 참고해서 작업을 수행하게 되죠.

그런만큼 시스템 프롬프트에는 에이전트가 일을 처리하는 방법, 절대 하면 안 되는 일 등 여러가지를 구체적으로 작성해 놓는 것이 좋은데요, 실제 deepagents에서 사용한 시스템 프롬프트를 보고 한번 감을 잡아 봅시다.

 

DeepAgents의 Agents.md라는 파일을 보면 시스템 프롬프트 전체를 확인해 볼 수 있습니다. 분량이 꽤 긴데요, 대략적으로 어떤 내용들이 담겼는지만 요약해 보겠습니다. 자세한 내용은 여기서 직접 확인하시면 됩니다.

1. Project architecture and context

에이전트가 작업할 환경을 설명합니다. 폴더 구조, 실행 가능한 커맨드, 의존 패키지 목록 등 에이전트가 "내가 지금 어디서 뭘 하는지"를 파악하는데 필요한 정보가 담깁니다.

2. Core development principles

코드를 작성할 때 지켜야 할 규칙을 정의합니다. 예를 들어 "기존 코드를 임의로 수정하지 않는다.", "네이밍 컨벤션(파일 이름 규칙)은 snake_case를 따른다"와 같은 지침이 들어갑니다.

이 부분이 없으면 에이전트가 멀쩡한 코드를 마음대로 바꿔버리거나, 팀의 코딩 스타일과 전혀 다른 스타일의 코드를 작성해 버릴 수 있고 이는 추후 프로젝트 관리에 큰 악영향을 미칠 수 있습니다.

3. Package-specific guidance

에이전트가 다룰 수 있는 각 도구나 라이브러리의 사용법을 명시하고 있습니다. 이를 통해 에이전트가 자신이 다룰 수 있는 도구와 라이브러리의 사용법을 익히고 활용하게 됩니다.

4. Additional resrouces

에이전트가 더 많은 정보를 필요로 할 때 참고할 수 있는 외부 링크 목록이 정리되어 있습니다.

시스템 프롬프트 캐싱

이렇게 많은 내용의 시스템 프롬프트가 항상 API 호출에 사용되면 비용이 많이 청구될까봐 걱정될 수도 있습니다. 하지만 각 챗봇 api 마다 캐싱 기능을 제공해주고 있기 때문에 생각보다 많은 비용이 들지는 않습니다. 반복되는 프롬프트는 미리 저장해둬서 그 비용을 아끼는 것이죠. (이에 대한 자세한 내용은 4장에서 다룰 예정입니다.)

 

그렇기 때문에 시스템 프롬프트를 최대한 자세히 작성해서 에이전트가 올바르게 작동하도록 하는 것이 좋습니다. 오히려 시스템 프롬프트를 부족하게 작성해서 에이전트가 괜히 작업을 하다가 헤매면 오히려 더 많은 비용이 소모될 수도 있겠죠?


3. 기억 기능

LLM은 기본적으로 기억 기능을 제공하지 않습니다. 따라서 이런 서비스를 제공하고자 할 때 기억이 필요하다면 직접 구현해줘야 하는데요, DeepAgents에서는 어떤 식으로 구현했는지 확인해 보겠습니다.

세션 간 기억: SQLite 체크포인터

기본 langgraph 튜토리얼에서 볼 수 있듯이, DeepAgents도 sqlite를 사용해 이전의 채팅 내역을 저장하고, thread_id를 사용해 사용자를 구분합니다. 물론 코드는 훨씬 방대하지만, 기본적인 골자는 같다는 겁니다. 구체적인 코드는 여기서 확인 가능합니다.

대화 중 기억 : MemoryMiddleware

에이전트는 기본적으로 Agents.md의 파일 내용을 메모리에 저장하고 대화를 이어갑니다. 그러나 DeepAgents는 여기서 그치지 않고, 에이전트가 사용자와 대화하는 과정에서 만약 중요한 정보가 나왔다라고 판단되면 스스로 메모리에 해당 내용을 저장합니다.

 

그 역할은 MemoryMiddleware가 수행합니다. 이와 관련한 프롬프트를 여기(97번줄)에서 확인 가능한데요, 이를 보면 에이전트가 언제 메모리를 업데이트하고 언제 메모리를 업데이트 하지 않는지 확인 가능합니다. 여기서 간단히 요약해 보자면,

[기억해야 할 것들]

- 사용자가 명시적으로 기억해달라고 요청한 것 ("내 이메일 기억해줘")
- 사용자가 자신의 역할이나 선호 방식을 설명한 것 ("나는 항상 X 방식을 선호해")
- 사용자가 내 작업에 피드백을 준 것 → 무엇이 잘못됐고 어떻게 개선할지를 패턴으로 저장 도구 사용에 필요한 정보 (슬랙 채널 ID, 이메일 주소 등)

[기억하지 말아야 할 것들]

- 일시적인 정보 ("오늘 농구하러 가서 잠깐 오프라인이야")
- 일회성 요청 ("레시피 찾아줘", "25 곱하기 4는?")
- API 키, 액세스 토큰, 비밀번호 등 민감한 정보 → 절대 저장 금지

대략 이런 식으로 기억해야 할 것들과 기억하지 말아야 할 것들을 정리해 놓은 걸 확인할 수 있습니다. 특히 각 항목마다 예시를 함께 작성해둔 것이나 메모리 관련 가이드라인이 굉장히 구체적인 것이 인상적입니다. 한 번 보고나면 많은 도움이 될 것 같습니다.


4. 토큰 비용 절약

LLM 서비스는 기본적으로 LLM API를 호출해서 사용하죠. 이런 API 비용들은 당연히 공짜가 아니며, 특히 이런 에이전트의 경우 도구를 호출하고 결과를 확인하는 과정에서 토큰이 굉장히 많이 소모되기 때문에 반드시 토큰 비용을 절약하기 위한 전략들을 준비해야 합니다.

 

DeepAgents에 적용된 토큰 비용 최적화 전략 몇 가지를 살펴 보도록 하겠습니다.

1. recursion_limit으로 무한루프 방지

recursion_limit이란 작업을 수행하는 반복 횟수를 의미합니다. 이를 설정하지 않으면 에이전트는 작업을 완료할 때까지 무한히 작업을 반복하는 루프에 빠질 수 있고, 그에 비례해서 토큰 비용이 기하급수적으로 증가하게 됩니다.

 

DeepAgents에서는 10,000번을 limit으로 설정해뒀는데 스스로 작업을 수행하는 에이전트의 특성 상, 작업 호출 횟수가 많이 필요하기 때문에 넉넉히 잡은 것으로 보입니다. 여러분들은 여러분들의 서비스에 맞춰서 적절히 설정하는게 중요하겠습니다. 코드는 여기서 확인 가능합니다.

2. PrivateStateAttr로 불필요한 정보 차단

DeepAgents의 대화에는 기본적으로 3가지 구성원이 참여합니다. 사용자, 메인 에이전트, 서브 에이전트 입니다. 서브 에이전트는 메인 에이전트가 혼자 처리하기 부담스럽거나 병렬 작업이 필요한 경우에 작동하는 별도의 에이전트입니다.

 

여기서 메인 에이전트의 경우 Agents.md의 내용이나, 사용자와의 대화에서 기록된 중요한 내용, 파일 내용 등 여러가지 많은 정보들을 메모리에 저장하고 있습니다. 그러나 이런 내용들은 사용자나 서브 에이전트에게는 불필요한 내용입니다.

 

사용자는 메인 에이전트의 메모리 정보보다는 메인 에이전트가 수행한 결과를 보는 것이 중요하고, 서브 에이전트의 경우 자신이 메인 에이전트에게 할당 받은 작업에 관련된 내용만 필요합니다. 따라서 메인 에이전트의 방대한 메모리가 굳이 서브 에이전트나 사용자에게 전달되는 것은 토큰 낭비입니다.

 

따라서 이런 메모리 정보가 다른 곳에 노출되지 않도록 DeepAgents에서는 PrivateStateAttr를 활용하여 차단하고 있습니다. 이는 langchain에 구현되어 있는 내용으로 아래와 같이 공유되지 않았으면 하는 내용에 추가하기만 하면 바로 적용 가능합니다. (자세한 코드는 여기서 확인 가능합니다.)

from langchain.agents.middleware.types import PrivateStateAttr

memory_contents: NotRequired[Annotated[dict[str, str], PrivateStateAttr]]

3. SummarizationMiddleware로 자동 요약

컨텍스트 윈도우란, LLM 모델이 한번에 처리 가능한 최대 토큰 수를 의미합니다. 대화가 길어지고 작업이 많아지면 이 컨텍스트 윈도우가 금방 꽉 차게 되고 비용도 급격히 증가하게 됩니다. DeepAgents에서는 이에 대비하기 위해 요약 기능을 사용합니다.

 

기본적으론 대화 내용이 일정량 이상이 되면 자동으로 요약을 하지만, DeepAgents는 LLM이 스스로 대화 내용 요약을 수행하기도 합니다.

  1. 자동 요약 : 모델의 가용 토큰 길이의 85%를 초과하면 자동으로 대화내용 요약을 수행합니다.
  2. 수동 요약 : 모델이 대화 중에 필요하다고 판단될 때 스스로 요약 툴을 호출해 직접 대화내용 요약을 수행합니다.

요약도 단순히 '대화 내용을 요약해' 해서 요약하는 것이 아니라 꽤 섬세하게 전략을 짠 것을 확인할 수 있습니다.

  • 제일 최근 10% 분량의 메시지는 요약 대상에서 제외하여 직전의 맥락을 최대한 보존한다.
  • 압축되는 메시지는 삭제하지 않고 마크다운 형식의 파일로 백업해 둔다.
  • 백업된 파일의 경로를 에이전트에게 알려줘 에이전트가 필요하다고 판단할 경우, 요약되었던 내용의 원본을 직접 찾아서 다시 읽어본다.
  • 도구 호출로 인한 출력이 2000자를 넘어가면 앞의 20자만 남기고 나머진 ...(argument truncated) 와 같은 문장으로 자동으로 절삭한다.

구체적인 코드는 여기서 확인 가능합니다.

4. API 별 캐싱 전략

가장 많이 사용되는 LLM API 3개(OpenAI, Gemini, Anthropic)는 각자 다른 방식으로 반복되는 프롬프트에 대한 캐싱 전략을 제공합니다. 당연히 DeepAgents에서도 이 캐싱 전략을 사용하는데요, 3가지 서비스가 각각 제공하는 캐싱 전략에 대해서도 간단히 소개해 드리겠습니다.

  OpenAI Anthropic Gemini
링크 OpenAI Developers
LangChain Docs
Claude API Docs
LangChain Docs
Gemini API Docs
LangChain Docs
캐싱 방법 - 별도의 설정 없이 api 서버 내부에서 자동적으로 캐싱을 수행
- gpt-4o 이상의 모델을 사용하면 자동으로 캐싱을 수행
- API 호출 시 cache_control 필드를 추가하여 캐싱 기능을 활성화 - 암시적 캐싱 : Gemini 2.5 이상의 모델을 사용하면 자동으로 캐싱을 수행
- 명시적 캐싱 : 사용자가 직접 캐싱 내용을 설정. google.genai.Client()를 활용해 수행 가능.
캐싱 규칙 프롬프트가 입력될 때 프롬프트의 앞쪽에 1024토큰 길이 이상의 프롬프트가 같은 내용으로 반복적으로 요청될 경우 내부적으로 캐싱을 수행. - TTL 설정으로 캐싱 시간을 설정. (5분 or 1시간)
- 캐시 기능을 사용할 때 추가 비용이 발생함.
- TTL 설정으로 캐싱 시간을 설정. (5분 or 1시간)
- 캐시 기능을 사용할 때 추가 비용이 발생함.

OpenAI와 달리 Anthropic과 Gemini는 캐싱을 수행할 때 추가 비용이 발생하지만, 결과적으로 이후 캐시된 내용에 대한 비용 절감 효과 덕분에 결과적으로 이득이라고 합니다. 또, 캐싱할 내용을 개발자가 직접 지정할 수 있다는 자유도가 있다는 차별점이 있습니다.


5. 안전장치 설계

"내 AI가 파일을 지워버렸어요.", "내 AI가 상사에게 이상한 메일을 보냈어요." 이런 참사 관련 글들이 심심치 않게 올라오는 것을 볼 수 있습니다. 이는 AI에게 100%의 자유도를 줬을 때 언제든 생길 수 있는 불상사입니다.

 

DeepAgents의 경우에도 파일을 쓰고 삭제하는 툴을 사용하기 때문에 이런 불상사가 언제든 일어날 수 있습니다. 따라서 의도치 않은 파일 삭제를 방지하기 위한 안전장치가 deepagents에 설계되어 있는데 한번 살펴보겠습니다.

 

1. Custom reducer로 파일 안전하게 관리하기

LangGraph는 기본적으로 state를 업데이트 할 때 기존 값을 덮어씁니다. 따라서 파일 목록을 state로 관리한다면, 기존의 파일이 의도치 않게 삭제되는 불상사가 발생할 수 있습니다.

 

예를 들어 현재 파일이 아래와 같이 구성되어 있다고 가정해 보겠습니다.

a.txt
b.txt

이 상황에 새로운 파일 'c.txt'를 추가하려고 한다면, 새로운 state 업데이트는 아래와 같이 추가됩니다.

c.txt

근데 이 새로운 state에는 기존의 'a.txt'와 'b.txt'에 관한 내용이 없죠? 이렇게 새로운 state로 업데이트 되면서 기존의 'a.txt', 'b.txt'가 날아가게 되는 것입니다. 그래서 DeepAgents에서는 아래와 같은 Custom Reducer를 사용하여 새로운 파일 추가가 기존의 파일을 덮어씌우지 않도록 합니다. (코드는 여기)

def _file_data_reducer(
    left: dict[str, FileData] | None,
    right: dict[str, FileData | None]
) -> dict[str, FileData]:
    """기존 파일(left)과 신규 업데이트(right)를 병합.
    right의 값이 None이면 해당 파일을 삭제."""
    if left is None:
        return {k: v for k, v in right.items() if v is not None}

    result = {**left}
    for key, value in right.items():
        if value is None:
            result.pop(key, None)  # 명시적으로 None일 때만 삭제
        else:
            result[key] = value    # 나머지는 병합
    return result

class FilesystemState(AgentState):
    files: Annotated[NotRequired[dict[str, FileData]], _file_data_reducer]

2. 가상 파일 시스템

작업을 수행하면서 모든 내용을 실제 디스크에 저장하지 않고, LangGraph 내부의 state에 가상으로 저장하는 전략도 사용합니다. 여기에는 임시로 사용되는 파일들이 주로 저장되고, 가상 파일 시스템으로 인한 장점은 아래와 같습니다.

  • 실제 저장소의 파일을 건드릴 일이 없어서 안전함.
  • 같은 저장소를 공유하는 서로 다른 세션 간에 간섭이 줄어들어 안전함.
  • 실제 파일을 삭제하고 생성하는데 필요한 권한 설정이나 보안 설정 없이 빠르게 진행 가능함.

가상 파일 시스템과 관련한 코드는 여기서 확인 가능합니다.

3. 예외 처리 전략

실제 프로그램을 구성하면 예외 처리는 항상 필수입니다. LLM을 사용하지 않는 일반 프로그램들의 경우 에러를 반환하도록 설계하지만 DeepAgents와 같은 에이전트는 이럴 경우 작업이 중단되기 때문에 에러가 아니라 '에러 메시지' 자체를 문자열로 반환하여 에이전트가 이를 확인하고 후속 조치를 취할 수 있도록 해야 합니다.

def _map_exception_to_standard_error(exc):
    if isinstance(exc, FileNotFoundError):
        return "file_not_found"     # 에이전트: "파일이 없구나, 경로를 다시 확인해볼게"
    if isinstance(exc, PermissionError):
        return "permission_denied"  # 에이전트: "권한이 없구나, 다른 방법을 써볼게"
    if isinstance(exc, IsADirectoryError):
        return "is_directory"       # 에이전트: "파일이 아니라 폴더구나"

이런 방식은 기존에 개발자가 에러 상황마다 맞춰서 후속 조치들을 구현해줬었던 것과 다르게, 에이전트가 직접 에러 메시지를 확인하고 스스로 후속 조치를 판단해서 취한다는 차이점이 있습니다. 이로 인해 코드가 단순해지고 에이전트의 자율성도 높일 수 있습니다.

4. Human in the Loop

민감한 작업을 수행하기 전에 사람의 승인을 받도록 하는 패턴입니다. 파일이 삭제되거나 개인정보가 노출될 수 있거나 하는 민감한 정보들은 사람의 승인을 꼭 받도록 하여 이런 사고를 미연에 방지하는 것입니다.

 

DeepAgents에서 human in the loop을 적용한 작업들은 아래와 같습니다.

interrupt_map = {
    "execute": ...,          # 셸 명령 실행
    "write_file": ...,       # 파일 쓰기
    "edit_file": ...,        # 파일 편집
    "web_search": ...,       # 웹 검색
    "fetch_url": ...,        # URL 가져오기
    "task": ...,             # 서브에이전트 작업
    "launch_async_subagent": ...,
    "update_async_subagent": ...,
    "cancel_async_subagent": ...,
}

 

이와 관련한 코드는 여기서 확인 가능합니다.


6. 서브 에이전트

간단한 작업이라면 에이전트 하나로도 전부 처리가 가능하지만 아래와 같은 상황에선 서브 에이전트 사용을 고려할 법 합니다.

  • 동시에 여러 일을 처리해야 하는 경우
  • 작업이 너무 오래 걸려 뒤의 작업에 병목이 오는 경우
  • 컨텍스트가 너무 길어져 비용이 폭발하는 경우

이런 서브 에이전트의 경우에도 메인 에이전트가 스스로 판단해서 사용을 하는데요, 당연히 서브 에이전트를 사용해야 하는 상황에 관련하여 가이드라인을 시스템 프롬프트 형태로 제공하고 있습니다.

[서브 에이전트를 써야 하는 경우]
- 처리 시간이 길어 메인 에이전트의 실행 흐름을 막는 경우
- 독립적으로 병렬 처리가 가능한 경우
- 내용이 너무 방대하여 메인 에이전트의 컨텍스트 윈도우를 소진시킬 수 있는 경우

[서브 에이전트를 쓰지 말아야 하는 경우]
- 도구 호출 몇 번으로 끝나는 단순한 작업
- 메인 에이전트가 실행 과정을 지속적으로 모니터링해야 하는 작업
- 서브 에이전트에 위임하는 것보다 직접 처리하는 것이 빠르거나 저렴한 작업

이와 관련한 구체적인 프롬프트는 여기에서 확인 가능합니다. 서브에이전트에게 전달되는 프롬프트 형식이나, 서브에이전트를 호출하는 경우에 관련한 프롬프트 모두 작성되어 있습니다.


7. 마무리

마지막으로 Deep Agents에 적용된 기법들을 정리하면서 내 서비스에 어떤 것들을 적용해 볼 수 있을지 정리해 보겠습니다.

  1. Middleware와 Backend의 분리 : Middleware와 Backend의 구분으로 실제 구현체와 에이전트에게 설명하는 레이어를 분리하여 코드의 유지보수성을 높일 수 있습니다.
  2. Agents.md : 시스템 프롬프트를 별개의 파일로 관리하며, 길어지더라도 최대한 구체적으로 작성하는 것이 좋습니다.
  3. 메모리 : 언제 대화 내용을 기억하고, 언제 기억하지 않을지에 대한 기준은 여러분이 구현할 챗봇 서비스에서도 적용할 수 있습니다.
  4. 토큰 비용 절약 : recursion_limit 설정, 대화 요약, PrivateStateAttr 활용, LLM API의 캐싱 기능을 활용해 토큰 비용을 절약할 수 있습니다.
  5. 안전장치 설계 : LangGraph의 state로 인한 데이터 손실을 막기 위한 Custom Reducer구현, 에러를 string 형태로 반환하는 패턴, Human in the Loop 패턴을 적용하여 의도치 않은 동작을 방지할 수 있습니다.
  6. 서브 에이전트 : 병렬 처리가 가능하거나, 너무 오래 걸리는 작업에는 서브 에이전트의 사용을 고려해 볼 수 있습니다.

당연히 여기의 구현체가 무조건 정답은 아니며, 저도 공부하는 입장에서 코드를 분석해 보게 된만큼 절대적인 요소들은 아닙니다. 하지만 실제 product 단계에서 어떤 식으로 코드가 작성되고 있고, 서비스를 개발하고 있는지 감을 잡는 데에는 너무 좋은 공부가 될 것으로 생각되어 개인적으로 분석한 내용을 공유해 봅니다.

 

도움이 되길 바라며 읽어주셔서 감사합니다.