프로덕션 배포
프로덕션 배포 (Going to production)
영구 메모리, 샌드박스, 복원력 미들웨어, 배포 옵션으로 deep agent를 프로덕션으로 가져가세요.
이 가이드는 deep agent를 로컬 프로토타입에서 프로덕션 배포로 가져갈 때 고려할 사항을 다룹니다. 메모리 범위 지정, 실행 환경 구성, 가드레일 추가, 프론트엔드 연결을 살펴봅니다.
개요 (Overview)
에이전트는 메모리와 실행 환경의 정보를 사용해 작업을 수행합니다. 프로덕션에서 정보가 공유되고 접근되는 방식을 결정하는 몇 가지 기본 요소가 있습니다:
- Thread: 단일 대화. 메시지 기록과 임시 파일은 기본적으로 스레드에 한정되며 이어지지 않습니다.
- User: 에이전트와 상호작용하는 사람. 메모리와 파일은 한 사용자에게 비공개이거나 여러 사용자에게 공유될 수 있습니다. 신원과 권한 부여는 인증 계층에서 옵니다.
- Assistant: 구성된 에이전트 인스턴스. 메모리와 파일은 한 어시스턴트에 묶거나 모두 공유할 수 있습니다.
이 페이지가 다루는 내용:
- LangSmith Deployments: 인증, 웹훅, cron을 갖춘 관리형 인프라
- 프로덕션 고려 사항: 호출, 멀티 테넌시, 인증, 자격 증명, 비동기, 내구성
- 메모리: 대화를 넘어 정보 지속
- 실행 환경: 파일 저장과 코드 실행
- 가드레일: 권한과 데이터 프라이버시
- 프론트엔드: 배포된 에이전트에 UI 연결
LangSmith Deployments
Deep Agent를 프로덕션으로 가져가는 권장 경로는 LangSmith에서 deep agent를 만들고, 실행하고, 운영하기 위한 CLI 우선 호스팅 런타임인 Managed Deep Agents입니다. Managed Deep Agents는 현재 비공개 프리뷰입니다(웨이트리스트 참여). 커스텀 애플리케이션 코드, 커스텀 라우트, 고급 인증이 필요한 팀은 LangSmith Deployment를 직접 구성할 수 있습니다. 두 경로 모두 에이전트가 필요로 하는 인프라(threads, runs, 스토어, 체크포인터)를 프로비저닝하므로 직접 설정할 필요가 없습니다. 전통적인 LangSmith Deployment는 인증, 웹훅, cron 작업, 관찰 가능성도 기본 제공하며, MCP 또는 A2A로 에이전트를 노출할 수 있습니다.
LangSmith Cloud 없이 JavaScript 프레임워크와 호스팅 플랫폼에 배포할 수도 있습니다.
이 페이지의 모든 코드 스니펫은 달리 명시되지 않는 한 다음 langgraph.json을 사용합니다:
{
"dependencies": ["."],
"graphs": {
"agent": "./src/agent.ts:agent"
},
"env": ".env"
}
langgraph.json은 LangGraph 플랫폼이 애플리케이션을 어떻게 빌드하고 실행할지 알려주는 구성 파일입니다. 프로젝트 루트에 있으며 로컬 개발(langgraph dev 사용)과 프로덕션 배포 모두에 필요합니다. 핵심 필드는 다음과 같습니다:
| 필드 | 설명 |
|---|---|
dependencies |
설치할 패키지. ["."]은 현재 디렉터리를 패키지로 설치합니다(requirements.txt, pyproject.toml, 또는 package.json에서 읽음). |
graphs |
그래프 ID를 코드 위치에 매핑합니다. 각 항목은 "<id>": "./<file>:<variable>"이며, <id>는 API를 통해 그래프를 호출하는 데 쓰는 이름이고 <variable>은 <file>에서 내보낸 컴파일된 그래프 또는 생성자 함수입니다. |
env |
환경 변수(API 키, 비밀)가 있는 .env 파일의 경로. 빌드 시점에 설정되어 런타임에 사용 가능합니다. |
전체 구성 옵션(커스텀 Docker 단계, 스토어 인덱싱, 인증 핸들러 등)은 application structure를 참조하세요.
프로덕션 고려 사항 (Production considerations)
에이전트 호출 (Invoking the agent)
프로덕션에서 모든 호출은 두 가지 실행 수준 파라미터를 가져야 합니다:
thread_id(config={"configurable": {"thread_id": ...}}로 전달): 대화의 안정적인 식별자. 체크포인터가 메시지 기록을 지속하고 재개하는 데 사용하므로 후속 턴이 같은 대화를 이어갑니다. 새 대화를 시작하려면 새thread_id를 생성하세요.context: 도구와 미들웨어가 호출 시점에 읽는 실행별 데이터. 예를 들어user_id, API 키, 기능 플래그, 세션 메타데이터입니다.context_schema로 형태를 정의하고runtime.context로 접근합니다. 런타임 컨텍스트를 참조하세요.
이 둘은 독립적이며 거의 항상 함께 전달됩니다:
const contextSchema = z.object({ userId: z.string() });
const agent = createDeepAgent({ model: "google-genai:gemini-3.6-flash", contextSchema, });
// Start a conversation const config = { configurable: { thread_id: crypto.randomUUID() } }; await agent.invoke( { messages: [{ role: "user", content: "Plan a 3-day trip to Tokyo" }] }, { ...config, context: { userId: "user-123" } }, );
// Follow-up on the same conversation: reuse the same thread_id await agent.invoke( { messages: [{ role: "user", content: "Make it 5 days instead" }] }, { ...config, context: { userId: "user-123" } }, );
```ts OpenAI theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
import { createDeepAgent } from "deepagents";
import { z } from "zod";
const contextSchema = z.object({ userId: z.string() });
const agent = createDeepAgent({
model: "openai:gpt-5.5",
contextSchema,
});
// Start a conversation
const config = { configurable: { thread_id: crypto.randomUUID() } };
await agent.invoke(
{ messages: [{ role: "user", content: "Plan a 3-day trip to Tokyo" }] },
{ ...config, context: { userId: "user-123" } },
);
// Follow-up on the same conversation: reuse the same thread_id
await agent.invoke(
{ messages: [{ role: "user", content: "Make it 5 days instead" }] },
{ ...config, context: { userId: "user-123" } },
);
import { createDeepAgent } from "deepagents";
import { z } from "zod";
const contextSchema = z.object({ userId: z.string() });
const agent = createDeepAgent({
model: "anthropic:claude-sonnet-5",
contextSchema,
});
// Start a conversation
const config = { configurable: { thread_id: crypto.randomUUID() } };
await agent.invoke(
{ messages: [{ role: "user", content: "Plan a 3-day trip to Tokyo" }] },
{ ...config, context: { userId: "user-123" } },
);
// Follow-up on the same conversation: reuse the same thread_id
await agent.invoke(
{ messages: [{ role: "user", content: "Make it 5 days instead" }] },
{ ...config, context: { userId: "user-123" } },
);
import { createDeepAgent } from "deepagents";
import { z } from "zod";
const contextSchema = z.object({ userId: z.string() });
const agent = createDeepAgent({
model: "openrouter:z-ai/glm-5.2",
contextSchema,
});
// Start a conversation
const config = { configurable: { thread_id: crypto.randomUUID() } };
await agent.invoke(
{ messages: [{ role: "user", content: "Plan a 3-day trip to Tokyo" }] },
{ ...config, context: { userId: "user-123" } },
);
// Follow-up on the same conversation: reuse the same thread_id
await agent.invoke(
{ messages: [{ role: "user", content: "Make it 5 days instead" }] },
{ ...config, context: { userId: "user-123" } },
);
import { createDeepAgent } from "deepagents";
import { z } from "zod";
const contextSchema = z.object({ userId: z.string() });
const agent = createDeepAgent({
model: "fireworks:accounts/fireworks/models/glm-5p2",
contextSchema,
});
// Start a conversation
const config = { configurable: { thread_id: crypto.randomUUID() } };
await agent.invoke(
{ messages: [{ role: "user", content: "Plan a 3-day trip to Tokyo" }] },
{ ...config, context: { userId: "user-123" } },
);
// Follow-up on the same conversation: reuse the same thread_id
await agent.invoke(
{ messages: [{ role: "user", content: "Make it 5 days instead" }] },
{ ...config, context: { userId: "user-123" } },
);
import { createDeepAgent } from "deepagents";
import { z } from "zod";
const contextSchema = z.object({ userId: z.string() });
const agent = createDeepAgent({
model: "baseten:zai-org/GLM-5.2",
contextSchema,
});
// Start a conversation
const config = { configurable: { thread_id: crypto.randomUUID() } };
await agent.invoke(
{ messages: [{ role: "user", content: "Plan a 3-day trip to Tokyo" }] },
{ ...config, context: { userId: "user-123" } },
);
// Follow-up on the same conversation: reuse the same thread_id
await agent.invoke(
{ messages: [{ role: "user", content: "Make it 5 days instead" }] },
{ ...config, context: { userId: "user-123" } },
);
import { createDeepAgent } from "deepagents";
import { z } from "zod";
const contextSchema = z.object({ userId: z.string() });
const agent = createDeepAgent({
model: "ollama:north-mini-code-1.0",
contextSchema,
});
// Start a conversation
const config = { configurable: { thread_id: crypto.randomUUID() } };
await agent.invoke(
{ messages: [{ role: "user", content: "Plan a 3-day trip to Tokyo" }] },
{ ...config, context: { userId: "user-123" } },
);
// Follow-up on the same conversation: reuse the same thread_id
await agent.invoke(
{ messages: [{ role: "user", content: "Make it 5 days instead" }] },
{ ...config, context: { userId: "user-123" } },
);
LangGraph SDK로 배포할 때 SDK가 스레드를 관리하므로 반환된 thread_id를 각 실행에 전달합니다:
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl: "<DEPLOYMENT_URL>", apiKey: "<LANG...EY>" });
const thread = await client.threads.create();
for await (const chunk of client.runs.stream(
thread.thread_id, // [!code highlight]
"agent",
{
input: { messages: [{ role: "user", content: "Plan a 3-day trip to Tokyo" }] },
context: { userId: "user-123" }, // [!code highlight]
streamMode: "updates",
},
)) {
console.log(chunk.data);
}
멀티 테넌시 (Multi-tenancy)
에이전트가 여러 사용자를 서비스할 때 세 가지 우려를 처리해야 합니다. 각 사용자가 누구인지 검증하고, 접근할 수 있는 것을 제어하고, 에이전트가 사용자를 대신해 행동할 때 사용하는 자격 증명을 관리하는 것입니다.
사용자 신원과 접근 제어 (User identity and access control)
LangSmith Deployments는 사용자 신원을 확립하기 위한 커스텀 인증과 스레드, 어시스턴트, 스토어 네임스페이스 같은 리소스에 대한 접근을 제어하는 권한 부여 핸들러를 지원합니다. 권한 부여 핸들러는 인증 성공 후 실행되며 다음을 할 수 있습니다:
- 리소스에 소유권 메타데이터 태그 지정(예:
owner: user_id) - 사용자가 자신의 리소스만 보도록 필터 반환
- 승인되지 않은 연산에 대해 HTTP 403으로 접근 거부
단계별 튜토리얼은 Make conversations private을 참조하세요. 안내는 커스텀 인증 비디오를 시청하세요.
메모리와 실행 환경을 어떻게 범위 지정하는지에 따라 사용자 간에 공유되는 데이터가 결정됩니다. 자세한 내용은 아래 섹션을 참조하세요.
팀 접근 제어 (RBAC)
LangSmith의 role-based access control은 팀에서 누가 에이전트를 배포하고, 구성하고, 모니터링할 수 있는지 규정합니다. 이것은 위의 최종 사용자 권한 부여와 별개입니다.
| 역할 | 접근 |
|---|---|
| Workspace Admin | 설정과 멤버 관리를 포함한 전체 권한 |
| Workspace Editor | 리소스 생성 및 수정 가능, 실행 삭제나 멤버 관리는 불가 |
| Workspace Viewer | 읽기 전용 접근 |
세밀한 권한을 가진 커스텀 역할은 Enterprise 플랜에서 사용할 수 있습니다. 전체 권한 모델은 RBAC reference를 참조하세요.
최종 사용자 자격 증명 (End-user credentials)
에이전트가 사용자를 대신해 외부 API를 호출해야 할 때(예: GitHub 저장소 읽기, Slack 메시지 보내기, 데이터 웨어하우스 쿼리) 하드코딩 없이 사용자의 자격 증명을 에이전트로 전달하는 방법이 필요합니다.
Agent Auth를 통한 OAuth. Agent Auth는 관리형 OAuth 2.0 흐름을 제공합니다. OAuth 프로바이더를 구성하면 에이전트가 각 사용자에 범위가 지정된 토큰을 요청할 수 있습니다. 첫 사용 시 에이전트는 interrupt로 실행을 중단하고 OAuth 동의 URL을 제시합니다. 사용자가 인증하면 에이전트는 유효한 토큰으로 재개합니다. 토큰은 자동으로 저장되고 갱신됩니다.
import { Client } from "@langchain/auth";
const authClient = new Client();
// Inside your agent's tool:
// Access the authenticated user via runtime.serverInfo
const authResult = await authClient.authenticate({
provider: "github",
scopes: ["repo", "read:org"],
userId: runtime.serverInfo.user.identity, // [!code highlight]
});
// Use authResult.token for GitHub API calls on the user's behalf
샌드박스용 자격 증명 주입. 에이전트가 외부 API를 호출하는 코드를 샌드박스 안에서 실행한다면 샌드박스 인증 프록시가 나가는 요청에 자격 증명을 자동으로 주입할 수 있으므로 샌드박스 코드가 원시 API 키를 결코 받지 않습니다. 설정 세부 사항은 비밀 관리를 참조하세요.
워크스페이스 비밀. 모든 사용자에게 공유되는 API 키(예: 조직의 LLM 프로바이더 키, 검색 API 키)는 LangSmith의 워크스페이스 비밀로 저장하세요. 자세한 내용은 비밀 관리를 참조하세요.
비동기 (Async)
LLM 기반 애플리케이션은 언어 모델, 데이터베이스, 외부 서비스 호출에 크게 I/O 바운드입니다. 비동기 프로그래밍은 이 연산들이 블로킹 대신 동시에 실행되게 해 처리량과 응답성을 높입니다.
프로덕션용으로 빌드할 때:
- 비동기 도구 만들기. LangChain은 블로킹을 피하려고 동기 도구를 별도 스레드에서 실행하지만, 네이티브 비동기는 스레딩 오버헤드를 완전히 피합니다.
- 비동기 미들웨어 메서드 사용. 커스텀 미들웨어는 비동기 훅(예:
before_agent대신abefore_agent)을 구현해야 합니다. - 외부 리소스 수명 주기에 비동기 사용. 샌드박스를 만들거나 MCP 서버에 연결하는 것은 네트워크 호출을 수반하며 await해야 합니다. 이것이 이런 리소스를 프로비저닝하는 graph factory가 비동기인 이유입니다.
내구성 (Durability)
Deep Agents는 기본적으로 내구성 있는 실행을 제공하는 LangGraph에서 실행됩니다. 영속성 계층이 각 단계에서 상태를 체크포인트 하므로, 실패, 타임아웃, 또는 human-in-the-loop 일시 중지로 중단된 실행은 이전 단계를 다시 처리하지 않고 마지막 기록 상태에서 재개합니다. 많은 서브에이전트를 생성하는 오래 실행되는 deep agent의 경우, 실행 중 실패가 완료된 작업을 잃지 않는다는 뜻입니다.
체크포인팅은 또한 다음을 가능하게 합니다:
- 무기한 interrupt. Human-in-the-loop 워크플로는 몇 분 또는 며칠 동안 일시 중지하고 정확히 중단한 지점에서 재개할 수 있습니다.
- Time travel. 모든 체크포인트된 단계는 되감을 수 있는 스냅샷이므로, 문제가 발생하면 이전 상태에서 재생할 수 있습니다.
- 민감한 연산의 안전한 처리. 결제나 다른 되돌릴 수 없는 작업을 수반하는 워크플로의 경우, 체크포인트는 감사 추적과 작업으로 이끈 정확한 상태를 검사할 복구 지점을 제공합니다.
메모리 (Memory)
메모리가 없으면 모든 대화는 처음부터 시작합니다. 메모리는 에이전트가 대화를 넘어 정보(사용자 선호도, 학습한 지침, 과거 경험)를 유지하게 해 시간이 지나며 동작을 개인화할 수 있습니다. 메모리 유형에 대한 개요는 메모리 개념 가이드를 참조하세요.
범위 지정 (Scoping)
메모리는 항상 대화를 넘어 영속적입니다. 핵심 질문은 사용자와 어시스턴트 경계에 걸쳐 어떻게 범위 지정되는가입니다. 올바른 범위는 누가 데이터를 보고 수정해야 하는지에 따라 달라집니다:
| 범위 | 네임스페이스 | 사용 사례 | 예시 |
|---|---|---|---|
| User(권장 기본값) | (user_id) |
사용자별 선호도와 컨텍스트 | "I prefer concise responses" |
| Assistant | (assistant_id) |
한 어시스턴트의 공유 지침 | "Cap posts at 280 characters" |
| Global | (org_id) |
모든 사용자와 어시스턴트의 읽기 전용 정책 | "Never disclose internal pricing" |
구성 (Configuration)
Deep Agents에서 메모리는 가상 파일 시스템의 파일로 저장됩니다. 기본적으로 파일은 단일 스레드(대화)에 한정되며 스레드 간에 공유되지 않습니다.
그 외에 스레드 간에 메모리를 공유하려면 /memories/ 같은 경로를 LangGraph Store에 쓰는 StoreBackend로 라우팅하세요. CompositeBackend를 사용해 에이전트에게 스레드 한정 임시 공간과 크로스 스레드 장기 메모리를 모두 제공하세요.
```typescript src/agent.ts theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
import { createDeepAgent, CompositeBackend, StateBackend, StoreBackend } from "deepagents";
export const agent = createDeepAgent({
backend: new CompositeBackend(
new StateBackend(),
{
"/memories/": new StoreBackend({
namespace: (rt) => [
rt.serverInfo.assistantId, // [!code highlight]
rt.serverInfo.user.identity, // [!code highlight]
],
}),
},
),
systemPrompt: `You have persistent memory at /memories/.
Read /memories/instructions.txt at the start of each conversation for
accumulated knowledge and preferences. When you learn something that
should persist, update that file.`,
});
```
```typescript src/agent.ts theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
import { createDeepAgent, CompositeBackend, StateBackend, StoreBackend } from "deepagents";
export const agent = createDeepAgent({
backend: new CompositeBackend(
new StateBackend(),
{
"/memories/": new StoreBackend({
namespace: (rt) => [rt.serverInfo.assistantId], // [!code highlight]
}),
},
),
});
```
```typescript src/agent.ts theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
import { createDeepAgent, CompositeBackend, StateBackend, StoreBackend } from "deepagents";
export const agent = createDeepAgent({
backend: new CompositeBackend(
new StateBackend(),
{
"/memories/": new StoreBackend({
namespace: (rt) => [rt.serverInfo.user.identity], // [!code highlight]
}),
},
),
});
```
```typescript src/agent.ts theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
import { createDeepAgent, CompositeBackend, StateBackend, StoreBackend } from "deepagents";
export const agent = createDeepAgent({
backend: new CompositeBackend(
new StateBackend(),
{
"/memories/": new StoreBackend({
namespace: (rt) => [rt.context.orgId],
}),
},
),
});
```
Store API로 애플리케이션 코드에서 스토어를 읽고 쓸 수도 있습니다. 예시는 고급 사용법을 참조하세요.
전체 네임스페이스 팩토리 API는 namespace factories를 참조하세요. 자기 개선 지침과 지식 베이스 같은 메모리 패턴은 장기 메모리를 참조하세요.
실행 환경 (Execution environment)
로컬에서는 에이전트가 디스크의 파일을 읽고 쓰고 셸 명령을 직접 실행할 수 있습니다. 프로덕션에서는 격리와 지속성을 고려해야 합니다. 올바른 설정은 에이전트가 코드를 실행해야 하는지에 따라 달라집니다:
- 파일 시스템 백엔드는 에이전트가 파일을 읽고 쓰기만 한다면 충분합니다. 지속성 요구에 맞는 백엔드를 선택하세요: 스레드 한정 임시 공간, 크로스 스레드 저장, 또는 둘의 혼합.
- 샌드박스는 셸 명령을 실행하기 위한
execute도구가 있는 격리된 컨테이너를 추가합니다. 에이전트가 코드를 실행하거나, 패키지를 설치하거나, 파일 I/O를 넘어서는 작업을 해야 한다면 샌드박스를 사용하세요.
파일 시스템 (Filesystem)
무엇이 지속되어야 하는지에 따라 백엔드를 선택하세요:
- StateBackend(기본값): 스레드 한정 임시 공간. 파일은 체크포인터를 통해 스레드 내 턴에 걸쳐 지속되지만 스레드 간에는 공유되지 않습니다. 매 단계 체크포인트되므로 큰 파일 쓰기를 피하세요.
- StoreBackend: 대화를 넘어 살아남는 크로스 스레드 저장. namespace factory로 범위 지정.
- CompositeBackend: 둘을 혼합. 기본적으로 스레드 한정 임시 공간 +
/memories/같은 특정 경로에 대한 크로스 스레드 라우트.
전체 백엔드 목록과 커스텀 백엔드 만드는 방법은 backends를 참조하세요.
샌드박스 (Sandboxes)
에이전트가 코드를 실행해야 한다면(파일을 읽고 쓰는 것만이 아니라) 샌드박스를 사용하세요. 샌드박스는 격리된 컨테이너 안에서 파일 시스템과 셸 명령을 실행하기 위한 execute 도구를 모두 제공합니다. 이 격리는 호스트도 보호합니다. 에이전트의 코드가 메모리를 소진하거나 충돌해도 샌드박스만 영향받습니다. 서버는 계속 실행됩니다.
수명 주기 (Lifecycle)
핵심 결정은 샌드박스가 얼마나 오래 사는가입니다. 각 대화가 새 것을 얻을까요, 아니면 대화가 지속 환경을 공유할까요?
| 범위 | 샌드박스 ID 저장 위치 | 수명 주기 | 예시 사용 사례 |
|---|---|---|---|
| Thread-scoped | Thread 메타데이터 | 대화마다 새로, TTL에 정리 | 각 대화가 깨끗하게 시작하는 데이터 분석 봇 |
| Assistant-scoped | Assistant 구성 | 모든 대화에 공유 | 대화를 넘어 클론된 저장소를 유지하는 코딩 어시스턴트 |
```typescript src/agent.ts theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
import { createDeepAgent, LangSmithSandbox } from "deepagents";
import { SandboxClient } from "langsmith/sandbox";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";
const client = new SandboxClient();
export async function agent(config: LangGraphRunnableConfig) {
const threadId = config.configurable?.thread_id as string; // [!code highlight]
const sandboxName = `thread-${threadId}`;
const existing = (await client.listSandboxes()).filter(
(sb) => sb.name === sandboxName,
);
const lsSandbox =
existing[0] ??
(await client.createSandbox({
name: sandboxName,
idleTtlSeconds: 3600, // TTL: clean up when idle
}));
return createDeepAgent({
model: "google_genai:gemini-3.6-flash",
backend: new LangSmithSandbox({ sandbox: lsSandbox }),
});
}
```
```typescript src/agent.ts theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
import { createDeepAgent, LangSmithSandbox } from "deepagents";
import { SandboxClient } from "langsmith/sandbox";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";
const client = new SandboxClient();
export async function agent(config: LangGraphRunnableConfig) {
const assistantId = config.configurable?.assistant_id as string; // [!code highlight]
const sandboxName = `assistant-${assistantId}`;
const existing = (await client.listSandboxes()).filter(
(sb) => sb.name === sandboxName,
);
const lsSandbox =
existing[0] ??
(await client.createSandbox({
name: sandboxName,
}));
return createDeepAgent({
model: "google_genai:gemini-3.6-flash",
backend: new LangSmithSandbox({ sandbox: lsSandbox }),
});
}
```
<Warning>
Assistant-scoped 샌드박스는 시간이 지나며 파일, 설치된 패키지, 기타 샌드박스 내부 상태가 축적됩니다. 샌드박스 프로바이더로 TTL을 구성하고, 스냅샷으로 주기적으로 리셋하거나, 정리 로직을 구현해서 샌드박스 디스크와 메모리가 무한정 늘어나지 않게 하세요.
</Warning>
agent 변수가 (컴파일된 그래프가 아니라) 비동기 함수이므로, 서버는 그것을 graph factory로 취급하고 각 실행마다 구성을 주입하며 호출합니다. 팩토리는 이름으로 샌드박스를 조회하거나 만들고 그 샌드박스에 연결된 새 에이전트 그래프를 반환합니다.
langgraph deploy로 배포하면 SDK를 사용해 애플리케이션 코드에서 에이전트를 호출합니다. 클라이언트 측 코드는 범위와 무관하게 동일합니다. 범위 지정은 전적으로 위의 에이전트 팩토리에서 처리되지만 동작은 다릅니다:
```typescript client.ts theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl: "<DEPLOYMENT_URL>", apiKey: "<LANG...EY>" });
// Conversation 1: install pandas and analyze data
const thread1 = await client.threads.create();
for await (const chunk of client.runs.stream(
thread1.thread_id,
"agent",
{ input: { messages: [{ role: "human", content: "Install pandas and analyze sales_data.csv" }] } },
)) {
console.log(chunk.data);
}
// Follow-up in the same conversation — pandas is still installed
for await (const chunk of client.runs.stream(
thread1.thread_id,
"agent",
{ input: { messages: [{ role: "human", content: "Now plot the results" }] } },
)) {
console.log(chunk.data);
}
// Conversation 2: fresh sandbox — pandas is NOT installed, no files from conversation 1
const thread2 = await client.threads.create();
for await (const chunk of client.runs.stream(
thread2.thread_id,
"agent",
{ input: { messages: [{ role: "human", content: "What packages are installed?" }] } },
)) {
console.log(chunk.data);
}
```
```typescript client.ts theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl: "<DEPLOYMENT_URL>", apiKey: "<LANG...EY>" });
// Conversation 1: clone and set up the project
const thread1 = await client.threads.create();
for await (const chunk of client.runs.stream(
thread1.thread_id,
"agent",
{ input: { messages: [{ role: "human", content: "Clone https://github.com/org/repo and install dependencies" }] } },
)) {
console.log(chunk.data);
}
// Conversation 2: repo and dependencies are still there
const thread2 = await client.threads.create();
for await (const chunk of client.runs.stream(
thread2.thread_id,
"agent",
{ input: { messages: [{ role: "human", content: "Run the test suite and fix any failures" }] } },
)) {
console.log(chunk.data);
}
```
파일 전송 (File transfers)
샌드박스는 격리된 컨테이너이므로 애플리케이션 코드가 그 안의 파일에 직접 접근할 수 없습니다. upload_files()와 download_files()를 사용해 샌드박스 경계를 넘어 데이터를 옮기세요:
- 에이전트 실행 전에 샌드박스 시드: 사용자 파일, 스킬 스크립트, 구성, 또는 영구 메모리를 업로드해 에이전트가 처음부터 필요한 것을 갖게 하세요
- 에이전트 완료 후 결과 검색: 생성된 산출물(보고서, 플롯, 내보내기)을 다운로드하고 업데이트된 메모리를 향후 대화를 위해 다시 동기화하세요
프로바이더별 파일 전송 예시는 파일 작업을 참조하세요. 프로바이더 설정, 보안, 수명 주기 패턴은 전체 샌드박스 가이드를 참조하세요.
import { createMiddleware } from "langchain";
import {
createDeepAgent,
CompositeBackend,
LangSmithSandbox,
StoreBackend,
} from "deepagents";
import { SandboxClient } from "langsmith/sandbox";
function safeFilename(key: string): string {
const name = key.split("/").pop()!;
if (name.includes("..") || /[*?]/.test(name)) {
throw new Error(`Invalid key: ${key}`);
}
return name;
}
const createSandboxSyncMiddleware = (backend: CompositeBackend) => {
return createMiddleware({
name: "SandboxSyncMiddleware",
beforeAgent: async (state, runtime) => {
// Upload skill scripts and memories into the sandbox
const userId = runtime.serverInfo.user.identity; // [!code highlight]
const store = runtime.store;
const encoder = new TextEncoder();
const files: [string, Uint8Array][] = [];
for (const item of await store.search(["skills", userId])) {
const name = safeFilename(item.key);
files.push([`/skills/${name}`, encoder.encode(item.value.content)]);
}
for (const item of await store.search(["memories", userId])) {
const name = safeFilename(item.key);
files.push([`/memories/${name}`, encoder.encode(item.value.content)]);
}
if (files.length > 0) {
await backend.uploadFiles(files);
}
},
afterAgent: async (state, runtime) => {
// Sync updated memories back to the store
const userId = runtime.serverInfo.user.identity; // [!code highlight]
const store = runtime.store;
const items = await store.search(["memories", userId]);
const results = await backend.downloadFiles(
items.map((item) => `/memories/${item.key}`),
);
const decoder = new TextDecoder();
for (const result of results) {
if (result.content) {
await store.put(
["memories", userId],
result.path.split("/").pop()!,
{ content: decoder.decode(result.content) },
);
}
}
},
});
};
const client = new SandboxClient();
const lsSandbox = await client.createSandbox();
const backend = new CompositeBackend(
new LangSmithSandbox({ sandbox: lsSandbox }),
{
"/skills/": new StoreBackend({
namespace: (rt) => ["skills", rt.serverInfo.user.identity], // [!code highlight]
}),
"/memories/": new StoreBackend({
namespace: (rt) => ["memories", rt.serverInfo.user.identity], // [!code highlight]
}),
},
);
export const agent = createDeepAgent({
backend,
middleware: [createSandboxSyncMiddleware(backend)],
});
비밀 관리 (Managing secrets)
샌드박스는 격리된 컨테이너이므로 호스트의 환경 변수가 그 안에서 사용할 수 없습니다. 샌드박스 코드에 API 키와 기타 비밀을 제공하는 두 가지 방법이 있습니다:
인증 프록시(권장). 샌드박스 인증 프록시가 샌드박스에서 나가는 요청을 가로채 인증 헤더를 자동으로 주입합니다. 샌드박스 코드는 외부 API를 정상적으로 호출하고, 프록시가 대상 호스트에 따라 올바른 자격 증명을 추가합니다. 즉 API 키가 샌드박스 코드, 환경 변수, 로그에 결코 나타나지 않습니다.
{
"proxy_config": {
"rules": [
{
"name": "openai-api",
"match_hosts": ["api.openai.com"],
"inject_headers": {
"Authorization": "Bearer ${OPENAI_API_KEY}"
}
},
{
"name": "anthropic-api",
"match_hosts": ["api.anthropic.com"],
"inject_headers": {
"x-api-key": "${ANTHROPIC_API_KEY}"
}
}
]
}
}
${SECRET_KEY} 참조는 LangSmith 워크스페이스 설정에 저장된 비밀에 대해 해석됩니다. 템플릿을 만들기 전에 거기에 비밀을 구성하세요.
워크스페이스 비밀. 프록시 기반 주입이 필요 없는 API 키(예: 샌드박스 코드가 아니라 에이전트 서버 자체가 사용하는 키)는 LangSmith의 워크스페이스 비밀로 저장하세요. 이것들은 런타임에서 워크스페이스의 모든 에이전트에 환경 변수로 사용할 수 있습니다.
가드레일 (Guardrails)
프로덕션의 에이전트는 자율적으로 실행되므로 무한 루프에 빠지거나, 속도 제한에 걸리거나, 민감한 정보가 포함된 사용자 데이터를 처리할 수 있습니다. Deep Agents는 두 가지 보호 계층을 제공합니다:
- 권한: 에이전트가 읽거나 쓸 수 있는 파일과 디렉터리를 제어하는 선언적 허용/거부 규칙.
- 장애 허용: 속도 제한, 재시도, 폴백, 오류 처리.
- 데이터 프라이버시: PII가 모델에 도달하거나 로그에 저장되기 전에 감지하고 처리하는 미들웨어.
권한 (Permissions)
권한은 에이전트가 읽거나 쓸 수 있는 파일과 디렉터리를 제어하는 선언적 허용/거부 규칙입니다. 권한을 사용해 에이전트를 작업 디렉터리에 격리하거나, 민감한 파일을 보호하거나, 읽기 전용 메모리를 강제하세요. 규칙은 선언 순서대로 평가되며 첫 번째 일치 규칙이 우선합니다.
장애 허용 (Fault tolerance)
속도 제한, 재시도, 폴백, 오류 처리는 Fault tolerance를 참조하세요.
데이터 프라이버시 (Data privacy)
에이전트가 이메일, 신용카드 번호 또는 기타 PII를 포함할 수 있는 사용자 입력을 처리한다면, 그것이 모델에 도달하거나 로그에 저장되기 전에 감지하고 처리할 수 있습니다:
import { createAgent, piiMiddleware } from "langchain";
const agent = createAgent({
model: "google_genai:gemini-3.6-flash",
middleware: [
piiMiddleware("email", { strategy: "redact", applyToInput: true }),
piiMiddleware("credit_card", { strategy: "mask", applyToInput: true }),
],
});
전략에는 redact([REDACTED_EMAIL]로 대체), mask(****-****-****-1234 같은 부분 마스킹), hash(결정적 해시), block(오류 발생)이 있습니다. 도메인별 패턴에 대한 커스텀 감지기도 작성할 수 있습니다.
전체 구성은 piiMiddleware를 참조하세요.
기본 Deep Agents 미들웨어 스택은 Customization을 참조하세요. 추가 LangChain 사전 구축 미들웨어(재시도, 폴백, PII 감지 등)는 사전 구축 미들웨어를 참조하세요.
프론트엔드 (Frontend)
Deep Agents는 useStream을 사용해 UI를 에이전트 백엔드에 연결합니다. useStream은 (React, Vue, Svelte, Angular 사용 가능한) 프론트엔드 훅으로, 에이전트에서 메시지, 서브에이전트 진행 상황, 커스텀 상태를 실시간으로 스트리밍합니다.
로컬에서 useStream은 http://localhost:2024를 가리킵니다. 프로덕션에서는 LangSmith Deployment를 가리키고 재연결을 구성해서 연결이 끊겨도 사용자가 진행 상황을 잃지 않게 하세요.
import { useStream } from "@langchain/react";
function App() {
const stream = useStream<typeof agent>({
apiUrl: "https://your-deployment.langsmith.dev",
assistantId: "agent",
});
}
많은 서브에이전트를 생성하는 deep agent 워크플로의 경우, 오래 실행되는 실행이 잘리지 않도록 제출 시 높은 recursionLimit을 설정하세요:
stream.submit(
{ messages: [{ type: "human", content: text }] },
{
streamSubgraphs: true,
config: { recursionLimit: 10000 },
},
);
서브에이전트 카드, todo 목록, 커스텀 상태 렌더링 같은 deep agent 전용 UI 패턴은 프론트엔드 가이드를 참조하세요.
더 알아보기
- 이 문서를 MCP로 연결하면 Claude, VSCode 등에서 실시간 답변을 받을 수 있어요.
- GitHub에서 이 페이지 편집하기 또는 이슈 제출하기.