Daytona와 Deep Agents를 사용하여 스레드 범위의 AI 데이터 과학 샌드박스 구축하기
요약
Daytona와 Deep Agents를 활용하여 에이전트가 안전하게 코드를 실행할 수 있는 격리된 데이터 과학 샌드박스 구축 방법을 소개합니다. 대화 메모리와 실행 환경의 상태를 분리하여 지속성을 유지하는 아키텍처를 다룹니다.
핵심 포인트
- Daytona를 통한 원격 개발 샌드박스 환경 구축
- Deep Agents와 LangGraph를 이용한 에이전트 워크플로우 제어
- 대화 상태(LangGraph)와 실행 환경(Daytona)의 분리 및 관리
- 동일한 thread_id를 활용한 파일 및 패키지 설치 지속성 확보
AI 코딩 에이전트(AI coding agents)는 파일을 작성하고, Python 패키지를 설치하며, 스크립트를 실행하고, 차트를 생성할 수 있습니다. 하지만 그 코드는 어디에서 실행되어야 할까요?
에이전트가 생성한 임의의 코드를 로컬 노트북이나 운영 서버에서 직접 실행하는 것은 위험할 수 있습니다. **샌드박스 (sandbox)**는 에이전트가 호스트 머신에 직접 접근하지 않고도 파일 시스템과 셸 명령(shell commands)을 사용할 수 있는 격리된 환경을 제공합니다.
이 글에서는 다음을 사용하는 간단한 2단계(two-turn) 데이터 과학 파이프라인을 소개합니다:
- 원격 개발 샌드박스를 위한 Daytona
- 파일 시스템 및 셸 접근 권한을 가진 AI 에이전트를 위한 Deep Agents
- 대화 메모리를 위한 LangGraph
- LLM 제공자로서의 NVIDIA NIM
목표는 단순히 모델을 학습시키는 것이 아닙니다. 이 예제는 다음을 입증합니다:
- 첫 번째 단계에서 설치된 Python 패키지가 두 번째 단계에서도 계속 사용 가능합니다.
- 첫 번째 단계에서 생성된 파일이 두 번째 단계에서도 여전히 존재합니다.
- 동일한
thread_id를 재사용할 때 에이전트가 대화 문맥(conversation context)을 유지합니다.
예제가 구축하는 것
프로그램은 두 번의 에이전트 호출을 수행합니다:
| 호출 | 에이전트 작업 | 지속성 증명 |
|---|---|---|
| 호출 1 | Python 패키지 설치, 데이터 생성, 모델 학습 및 차트 저장 | data.csv, model.pkl, chart.png 생성 |
| 호출 2 | 저장된 모델 로드, 클래스 예측 및 차트 확인 | 호출 1 중에 생성된 파일 읽기 |
두 호출 모두 동일한 샌드박스 이름을 사용합니다:
THREAD_ID = "ds-pipeline-demo-1"
SANDBOX_NAME = f"thread-{THREAD_ID}"
이는 다음과 같은 샌드박스 이름을 생성합니다:
thread-ds-pipeline-demo-1
호출 2가 시작되면 프로그램은 해당 이름의 Daytona 샌드박스를 검색합니다. 만약 존재한다면, 프로그램은 새로운 환경을 만드는 대신 이를 재사용합니다.
두 가지 유형의 지속성 이해하기
**대화 상태 (conversation state)**와 **샌드박스 상태 (sandbox state)**를 분리하는 것이 중요합니다.
| 상태 유형 (State type) | 저장 주체 (Stored by) | 포함 내용 (What it contains) | 이 예제에서의 사용 키 (Key used in this example) |
|---|---|---|---|
| 대화 메모리 (Conversation memory) | LangGraph 체크포인터 (checkpointer) | 이전 메시지 및 에이전트 상태 (agent state) | thread_id |
| 실행 환경 (Execution environment) | Daytona 샌드박스 (sandbox) | 파일, 설치된 패키지 및 워크스페이스 상태 (workspace state) | thread-<thread_id> |
동일한 thread_id가 두 가지 서로 다른 용도로 사용됩니다.
config = {"configurable": {"thread_id": THREAD_ID}}
이는 LangGraph에 대화를 위한 안정적인 식별자 (identifier)를 제공합니다.
샌드박스 이름은 동일한 값으로부터 파생됩니다:
SANDBOX_NAME = f"thread-{THREAD_ID}"
이는 Daytona에 원격 워크스페이스 (remote workspace)를 위한 예측 가능한 이름을 제공합니다.
thread_id가 있다고 해서 Daytona가 자동으로 샌드박스를 재사용하는 것은 아닙니다. 애플리케이션에서 매핑 전략 (mapping strategy)을 구현해야 합니다. 이 예제에서는 매핑 방식이 안정적인 샌드박스 이름입니다.
아키텍처 (Architecture)
전체적인 흐름은 다음과 같습니다:
사용자 프롬프트 (User prompt)
|
v
...
에이전트 (agent)는 애플리케이션 내에서 실행되는 반면, 파일 시스템 (filesystem) 및 셸 (shell) 작업은 Daytona 환경에서 실행됩니다.
사전 요구 사항 (Prerequisites)
필요한 패키지를 설치합니다:
pip install daytona langchain-daytona deepagents \
langchain-nvidia-ai-endpoints python-dotenv
.env 파일을 생성합니다:
DAYTONA_API_KEY=your_daytona_api_key
NVIDIA_API_KEY=your_nvidia_api_key
.env 파일과 API 키는 버전 관리 시스템 (version control)에 커밋해서는 안 됩니다.
1단계: 공유 설정 생성 (Create shared configuration)
환경 변수를 로드하고 Daytona 클라이언트를 설정하는 것으로 시작합니다.
from dotenv import load_dotenv
from daytona import Daytona, CreateSandboxFromSnapshotParams
from langchain_daytona import DaytonaSandbox
...
InMemorySaver()는 무엇을 하나요?
InMemorySaver()는 LangGraph 상태를 메모리에 저장합니다. 두 에이전트 호출이 모두 동일한 Python 스크립트 내에서 발생하므로, 두 번째 호출은 첫 번째 호출에서 생성된 대화 상태 (conversation state)에 접근할 수 있습니다.
중요한 제한 사항이 있습니다:
InMemorySaver()는 영구적이지 (durable) 않습니다. 애플리케이션이 중단되었다가 다시 시작되면 대화 메모리 (conversation memory)는 손실됩니다.
Daytona 샌드박스 (sandbox)가 만료되지 않았다면 이름으로 찾을 수 있기 때문에 여전히 재사용이 가능할 수 있습니다. 하지만 프로덕션 애플리케이션 (production application)은 데이터베이스나 다른 영구 저장 서비스 (durable storage service)를 기반으로 하는 영구적인 LangGraph 체크포인터 (checkpointer)를 사용해야 합니다.
2단계: 샌드박스 찾기 또는 생성하기
이 함수는 샌드박스 재사용 패턴의 핵심입니다:
def get_or_create_sandbox(name: str):
"""이 이름의 샌드박스를 재사용하거나, 없으면 생성합니다."""
...
다양한 상황에서 발생하는 일은 다음과 같습니다:
| 상황 | 결과 |
|---|---|
| 대상 이름의 샌드박스가 존재하지 않음 | 새로운 샌드박스가 생성됨 |
| ... |
파일과 설치된 패키지들은 LangGraph 체크포인터 (checkpointer)가 아니라 Daytona 샌드박스 내부에 존재합니다.
이것이 바로 호출 2 (Call 2)가 모델을 로드할 수 있는 이유입니다:
Call 1 writes /home/daytona/model.pkl
|
v
...
3단계: 에이전트에게 샌드박스 접근 권한 부여하기
다음으로, Daytona 샌드박스를 Deep Agents 백엔드 (backend)로 감쌉니다.
def build_agent(sandbox):
"""하나의 Daytona 샌드박스에 연결된 AI 에이전트를 생성합니다."""
...
샌드박스 백엔드는 에이전트에게 다음과 같은 작업들을 위한 도구 (tools)를 제공합니다:
- 파일 쓰기 (Writing files)
- 파일 읽기 (Reading files)
- 파일 편집 (Editing files)
- 디렉토리 목록 나열 (Listing directories)
- 파일 검색 (Searching files)
- 셸 명령 실행 (Running shell commands)
이러한 도구들을 통해 에이전트는 패키지를 설치하고, Python 스크립트를 생성하고, 이를 실행하며, 생성된 결과물 (artifacts)을 검사할 수 있습니다.
/home/daytona가 중요한 이유
시스템 프롬프트 (system prompt)는 에이전트가 오직 다음 경로 내부에서만 작업하도록 지시합니다:
/home/daytona
이 관례 (convention)는 생성된 파일들을 하나의 쓰기 가능하고 예측 가능한 위치에 유지합니다.
예를 들어:
/home/daytona/train.py
/home/daytona/data.csv
/home/daytona/model.pkl
...
절대 경로 (absolute paths)를 사용하면 파일이 예상치 못한 위치나 보호된 루트 레벨 (root-level) 디렉토리에 작성될 가능성도 줄어듭니다.
4단계: 학습 턴 (training turn) 실행하기
첫 번째 호출에서, 에이전트는 종속성(dependencies)을 설치하고, 학습 스크립트(training script)를 생성하며, 이를 실행하고, 출력물을 검증하라는 지침을 받습니다.
call1_prompt = (
"샌드박스 내에서 다음 작업을 단계별로 수행하세요:\n"
"1. 설치: pandas matplotlib scikit-learn (pip 사용).\n"
...
샌드박스 내부의 워크플로우는 다음과 유사합니다:
cd /home/daytona
pip install pandas matplotlib scikit-learn
python train.py
...
첫 번째 호출 이후, Daytona 워크스페이스에는 다음이 포함되어 있어야 합니다:
/home/daytona/
├── train.py
├── data.csv
...
5단계: 두 번째 호출에서 샌드박스 재사용하기
두 번째 호출을 수행하기 전에, 동일한 조회(lookup) 함수가 다시 실행됩니다:
sandbox = get_or_create_sandbox(SANDBOX_NAME)
agent = build_agent(sandbox)
SANDBOX_NAME이 변경되지 않았으므로, 프로그램은 기존 환경을 찾아 재사용합니다.
다음 프롬프트는 이전 턴(turn)의 아티팩트(artifacts)를 사용합니다:
call2_prompt = (
"샌드박스 내 이전 턴의 아티팩트를 사용하여:\n"
"1. `model.pkl`을 로드하고 이 새로운 행에 대해 예측을 실행하세요:\n"
...
두 번째 호출이 성공하면 다음 사항이 입증됩니다:
scikit-learn이 여전히 설치되어 있음
model.pkl이 여전히 존재함
chart.png가 여전히 존재함
이것이 스레드 범위(thread-scoped) 샌드박스의 실질적인 가치입니다.
도구 활동 스트리밍 (Streaming tool activity)
run_turn() 함수는 작업이 진행되는 동안 에이전트의 동작을 스트리밍합니다:
for chunk in agent.stream(
{"messages": [{"role": "user", "content": prompt}]},
config=config,
...
이를 통해 워크플로우를 관찰할 수 있습니다:
[agent] -> execute({"command": "cd /home/daytona && pip install ..."})
[tools] <- execute: Successfully installed ...
[agent] -> write_file({"path": "/home/daytona/train.py", ...})
...
스트리밍은 코드 실행 에이전트(code-running agents)에게 유용한데, 그 이유는 다음을 보여주기 때문입니다:
- 어떤 명령어가 실행되었는지
- 어떤 파일이 작성되었는지
- 명령어가 실패했는지 여부
- 에이전트가 최종 파일들을 검증했는지 여부
6단계: 샌드박스 정리하기
이 예제는 스크립트가 종료된 후 샌드박스 (sandbox)를 중지하기 위해 finally 블록을 사용합니다:
finally:
try:
sb = DAYTONA.get(SANDBOX_NAME)
...
샌드박스는 원격 컴퓨팅 리소스 (remote compute resources)를 소비하기 때문에 이 과정이 중요합니다.
또한 예제에서는 다음을 설정합니다:
ttl_minutes=60
TTL (Time To Live)은 백업 정리 메커니즘입니다. 애플리케이션이 충돌하거나 세션이 반환되지 않는 경우, 설정된 기간 동안 유휴 상태(idle)로 유지되면 샌드박스를 제거할 수 있습니다.
이 데모에서는 명시적인 stop()이 Call 2 이후에 실행되므로, TTL은 주로 폴백 (fallback) 역할을 합니다.
보안 고려 사항 (Security considerations)
샌드박스는 에이전트가 생성한 코드와 로컬 머신 또는 기본 애플리케이션 서버 간의 격리 (isolation)를 제공합니다.
하지만 샌드박스가 모든 보안 문제를 자동으로 해결해 주는 것은 아닙니다.
| 위험 요소 | 여전히 중요한 이유 |
|---|---|
| 프롬프트 인젝션 (Prompt injection) | 신뢰할 수 없는 입력이 에이전트를 설득하여 샌드박스 내부에서 원치 않는 명령을 실행하게 할 수 있음 |
| ... |
실질적인 안전 규칙은 다음과 같습니다:
- 가능한 경우 API 키를 샌드박스 외부에 보관하세요.
- 필요한 경우가 아니라면 민감한 운영 데이터 (production data)를 샌드박스에 넣지 마세요.
- 짧은 샌드박스 TTL 값을 사용하세요.
- 다중 사용자 시스템에서는 고유하고 추측하기 어려운 스레드 ID (thread ID)를 사용하세요.
- 사용자가 제공한 파일 경로와 명령어를 검증하세요.
- 사용 가능한 경우 런타임 (runtime), 스토리지 (storage), 패키지 (package) 및 네트워크 제한을 적용하세요.
- 작업이 완료되면 환경을 정리하세요.
전체 코드
"""
스레드 범위의 Daytona 샌드박스: 멀티 턴 (multi-turn) 데이터 과학 파이프라인.
동일한 `thread_id`를 사용하여 두 번의 호출 간에 샌드박스 + 대화 재사용을 시연합니다:
Call 1: pandas/matplotlib/scikit-learn 설치, CSV 생성, 소규모 모델 학습, `model.pkl` + `chart.png` 저장.
Call 2: 저장된 모델을 로드하여 새로운 행에 대해 예측 수행; 차트 재표시. 설치된 패키지와 파일 모두가 턴(turn) 사이에도 유지됨을 증명함.
재사용 방식:
- 샌드박스(sandbox)는
thread-<thread_id>로 명명됩니다. 두 번째 호출(Call 2)에서 기존 샌드박스 목록을 나열하고 이름으로 해당 샌드박스를 찾아 재사용합니다 (파일 + 설치된 패키지가 여전히 유지됨). 찾을 수 없는 경우 새로 생성합니다. - 동일한
thread_id를 키(key)로 사용하는 LangGraph 체크포인터(checkpointer)가 두 번의 호출(invocation) 간에 대화 메모리(conversation memory)를 유지합니다. ttl_minutes는 유휴 시간(idle time) 이후 샌드박스를 자동으로 삭제하여, 다시 돌아오지 않을 경우 계속 비용이 발생하는 것을 방지합니다.
사전 요구 사항 (이미 pyproject.toml에 포함됨):
pip install daytona langchain-daytona deepagents langchain-nvidia-ai-endpoints
환경 변수 (이미 .env에 포함됨):
DAYTONA_API_KEY - Daytona()용
NVIDIA_API_KEY - LLM용
실행:
python daytona_thread_scoped.py
"""
from dotenv import load_dotenv
from daytona import Daytona, CreateSandboxFromSnapshotParams
from langchain_daytona import DaytonaSandbox
from langgraph.checkpoint.memory import InMemorySaver
from deepagents import create_deep_agent
.env에서 DAYTONA_API_KEY / NVIDIA_API_KEY 로드
load_dotenv()
DAYTONA = Daytona()
스크립트 전체 동안 유지되는 단일 체크포인터(checkpointer)를 사용하여
두 호출 모두 thread_id를 키로 하는 대화 메모리를 공유하도록 함.
CHECKPOINTER = InMemorySaver()
대화/샌드박스 키. 두 호출 모두 동일한 값을 사용하면 => 재사용.
THREAD_ID = "ds-pipeline-demo-1"
SANDBOX_NAME = f"thread-{THREAD_ID}"
SANDBOX_TTL_MINUTES = 60 # 1시간 유휴 시 자동 삭제
def get_or_create_sandbox(name: str):
"""이름으로 기존 Daytona 샌드박스를 찾거나, 새로운 것을 생성합니다.
(실행 중인) Sandbox를 반환합니다. 턴(turn) 사이에 동일한 이름을 재사용하는 것이
파일과 설치된 패키지가 호출 간에 지속되도록 만드는 핵심입니다.
"""
for sb in DAYTONA.list():
if getattr(sb, "name", None) == name:
print(f"기존 샌드박스 재사용 중: {name} (id={sb.id}, state={sb.state})")
# 실행 중인지 확인 (중지/일시 중지되었을 수 있음).
if str(sb.state).lower() not in ("running", "started"):
sb.start()
return sb
새로운 샌드박스 생성: {name} (ttl={SANDBOX_TTL_MINUTES}분)...
sb = DAYTONA.create(
CreateSandboxFromSnapshotParams(
name=name,
language="python",
ttl_minutes=SANDBOX_TTL_MINUTES,
)
)
print(f"샌드박스 생성됨: {name} (id={sb.id})")
return sb
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기