프로덕션 배포

프로덕션 배포 (Going to production)

영구 메모리, 샌드박스, 복원력 미들웨어, 배포 옵션으로 deep agent를 프로덕션으로 가져가세요.

이 가이드는 deep agent를 로컬 프로토타입에서 프로덕션 배포로 가져갈 때 고려할 사항을 다룹니다. 메모리 범위 지정, 실행 환경 구성, 가드레일 추가, 프론트엔드 연결을 살펴봅니다.

개요 (Overview)

에이전트는 메모리와 실행 환경의 정보를 사용해 작업을 수행합니다. 프로덕션에서 정보가 공유되고 접근되는 방식을 결정하는 몇 가지 기본 요소가 있습니다:

  • Thread: 단일 대화. 메시지 기록과 임시 파일은 기본적으로 스레드에 한정되며 이어지지 않습니다.
  • User: 에이전트와 상호작용하는 사람. 메모리와 파일은 한 사용자에게 비공개이거나 여러 사용자에게 공유될 수 있습니다. 신원과 권한 부여는 인증 계층에서 옵니다.
  • Assistant: 구성된 에이전트 인스턴스. 메모리와 파일은 한 어시스턴트에 묶거나 모두 공유할 수 있습니다.

이 페이지가 다루는 내용:

LangSmith Deployments

Managed Deep Agents가 에이전트 구성, 도구, 런타임 설정을 LangSmith용으로 패키징합니다

Deep Agent를 프로덕션으로 가져가는 권장 경로는 LangSmith에서 deep agent를 만들고, 실행하고, 운영하기 위한 CLI 우선 호스팅 런타임인 Managed Deep Agents입니다. Managed Deep Agents는 현재 비공개 프리뷰입니다(웨이트리스트 참여). 커스텀 애플리케이션 코드, 커스텀 라우트, 고급 인증이 필요한 팀은 LangSmith Deployment를 직접 구성할 수 있습니다. 두 경로 모두 에이전트가 필요로 하는 인프라(threads, runs, 스토어, 체크포인터)를 프로비저닝하므로 직접 설정할 필요가 없습니다. 전통적인 LangSmith Deployment는 인증, 웹훅, cron 작업, 관찰 가능성도 기본 제공하며, MCP 또는 A2A로 에이전트를 노출할 수 있습니다.

LangSmith Cloud 없이 JavaScript 프레임워크와 호스팅 플랫폼에 배포할 수도 있습니다.

프레임워크와 플랫폼

<a href="/langsmith/deploy-frameworks-and-platforms" className="text-sm font-medium text-primary hover:underline">
  모든 가이드 보기
</a>
  <img className="hidden h-8 w-8 dark:block" src="https://mintcdn.com/langchain-5e9cc07a/ZPKed1feKJ8F6LVo/images/providers/dark/langchain.svg?fit=max&auto=format&n=ZPKed1feKJ8F6LVo&q=85&s=453173f86b1316e6cdb5cce6575a6904" alt="" noZoom width="65" height="65" data-path="images/providers/dark/langchain.svg" />

  <span className="text-center text-xs font-medium text-gray-900 dark:text-gray-100">LangSmith</span>
</a>

<a href="/langsmith/deploy-nextjs" className="flex flex-col items-center gap-3 rounded-lg border border-gray-200 bg-white px-3 py-4 transition-colors hover:border-gray-300 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-950 dark:hover:border-gray-600 dark:hover:bg-gray-900">
  <img className="block h-8 w-8 dark:hidden" src="https://mintcdn.com/langchain-5e9cc07a/h9vvRKKSgCSjvd_Y/images/providers/light/nextjs.svg?fit=max&auto=format&n=h9vvRKKSgCSjvd_Y&q=85&s=4f3336e9534db50f25f87173a41322d5" alt="" noZoom width="24" height="24" data-path="images/providers/light/nextjs.svg" />

  <img className="hidden h-8 w-8 dark:block" src="https://mintcdn.com/langchain-5e9cc07a/3dEJ6mnTRvjds7N4/images/providers/dark/nextjs.svg?fit=max&auto=format&n=3dEJ6mnTRvjds7N4&q=85&s=34d75a0cc0cf38f9e1171ad1b5d0dfcb" alt="" noZoom width="24" height="24" data-path="images/providers/dark/nextjs.svg" />

  <span className="text-center text-xs font-medium text-gray-900 dark:text-gray-100">Next.js</span>
</a>

<a href="/langsmith/deploy-sveltekit" className="flex flex-col items-center gap-3 rounded-lg border border-gray-200 bg-white px-3 py-4 transition-colors hover:border-gray-300 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-950 dark:hover:border-gray-600 dark:hover:bg-gray-900">
  <img className="block h-8 w-8 dark:hidden" src="https://mintcdn.com/langchain-5e9cc07a/h9vvRKKSgCSjvd_Y/images/providers/light/svelte.svg?fit=max&auto=format&n=h9vvRKKSgCSjvd_Y&q=85&s=07e7e35c40e4522f739feea9ef3e33b7" alt="" noZoom width="24" height="24" data-path="images/providers/light/svelte.svg" />

  <img className="hidden h-8 w-8 dark:block" src="https://mintcdn.com/langchain-5e9cc07a/3dEJ6mnTRvjds7N4/images/providers/dark/svelte.svg?fit=max&auto=format&n=3dEJ6mnTRvjds7N4&q=85&s=f486f3905850ec517edb30f60aedfd6e" alt="" noZoom width="24" height="24" data-path="images/providers/dark/svelte.svg" />

  <span className="text-center text-xs font-medium text-gray-900 dark:text-gray-100">SvelteKit</span>
</a>

<a href="/langsmith/deploy-nuxt" className="flex flex-col items-center gap-3 rounded-lg border border-gray-200 bg-white px-3 py-4 transition-colors hover:border-gray-300 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-950 dark:hover:border-gray-600 dark:hover:bg-gray-900">
  <img className="block h-8 w-8 dark:hidden" src="https://mintcdn.com/langchain-5e9cc07a/h9vvRKKSgCSjvd_Y/images/providers/light/nuxt.svg?fit=max&auto=format&n=h9vvRKKSgCSjvd_Y&q=85&s=5d965ed1f93f51ac604e66b935a65368" alt="" noZoom width="24" height="24" data-path="images/providers/light/nuxt.svg" />

  <img className="hidden h-8 w-8 dark:block" src="https://mintcdn.com/langchain-5e9cc07a/3dEJ6mnTRvjds7N4/images/providers/dark/nuxt.svg?fit=max&auto=format&n=3dEJ6mnTRvjds7N4&q=85&s=4db7ed1c87ce9479c05d355ea8ecf865" alt="" noZoom width="24" height="24" data-path="images/providers/dark/nuxt.svg" />

  <span className="text-center text-xs font-medium text-gray-900 dark:text-gray-100">Nuxt</span>
</a>

<a href="/langsmith/deploy-cloudflare-workers" className="flex flex-col items-center gap-3 rounded-lg border border-gray-200 bg-white px-3 py-4 transition-colors hover:border-gray-300 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-950 dark:hover:border-gray-600 dark:hover:bg-gray-900">
  <img className="block h-8 w-8 dark:hidden" src="https://mintcdn.com/langchain-5e9cc07a/h9vvRKKSgCSjvd_Y/images/providers/light/cloudflare.svg?fit=max&auto=format&n=h9vvRKKSgCSjvd_Y&q=85&s=ae129fcbfc78ccaece42b1c4d3699311" alt="" noZoom width="24" height="24" data-path="images/providers/light/cloudflare.svg" />

  <img className="hidden h-8 w-8 dark:block" src="https://mintcdn.com/langchain-5e9cc07a/3dEJ6mnTRvjds7N4/images/providers/dark/cloudflare.svg?fit=max&auto=format&n=3dEJ6mnTRvjds7N4&q=85&s=6b7e685f85fd477555f18291754065e1" alt="" noZoom width="24" height="24" data-path="images/providers/dark/cloudflare.svg" />

  <span className="text-center text-xs font-medium text-gray-900 dark:text-gray-100">Cloudflare</span>
</a>

<a href="/langsmith/deploy-deno" className="flex flex-col items-center gap-3 rounded-lg border border-gray-200 bg-white px-3 py-4 transition-colors hover:border-gray-300 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-950 dark:hover:border-gray-600 dark:hover:bg-gray-900">
  <img className="block h-8 w-8 dark:hidden" src="https://mintcdn.com/langchain-5e9cc07a/Tz8fh3A43FeUPf69/images/providers/light/deno.svg?fit=max&auto=format&n=Tz8fh3A43FeUPf69&q=85&s=8d213b0000104542fcaa7ab160595224" alt="" noZoom width="24" height="24" data-path="images/providers/light/deno.svg" />

  <img className="hidden h-8 w-8 dark:block" src="https://mintcdn.com/langchain-5e9cc07a/Tz8fh3A43FeUPf69/images/providers/dark/deno.svg?fit=max&auto=format&n=Tz8fh3A43FeUPf69&q=85&s=4b80fd3f87be74cae919c9e847bedc8e" alt="" noZoom width="24" height="24" data-path="images/providers/dark/deno.svg" />

  <span className="text-center text-xs font-medium text-gray-900 dark:text-gray-100">Deno</span>
</a>
LangSmith Cloud 배포는 자동으로 배포 이름을 딴 프로젝트에 추적을 보냅니다. [LangSmith](https://smith.langchain.com?utm_source=docs\&utm_medium=cta\&utm_campaign=langsmith-signup\&utm_content=oss-deepagents-going-to-production)를 열어 실행을 디버깅하고 사용량을 모니터링하세요. 하이브리드 또는 셀프 호스팅 설정은 [LangSmith tracing](/langsmith/data-plane#langsmith-tracing)을 참조하세요. 또한 추적을 모니터링하고, 문제를 감지하고, 수정을 제안하는 [LangSmith Engine](/langsmith/engine)도 설정하는 것을 권장합니다.

이 페이지의 모든 코드 스니펫은 달리 명시되지 않는 한 다음 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로 접근합니다. 런타임 컨텍스트를 참조하세요.

이 둘은 독립적이며 거의 항상 함께 전달됩니다:

```ts Google 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: "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" } },
);
이 예시의 공개 LangSmith 실행을 엽니다.

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);
}
`thread_id`는 *대화*(메시지 기록, 체크포인트)를 범위 지정합니다. `context`는 도구와 미들웨어가 읽는 *실행별* 데이터를 전달합니다. 이 둘은 독립적입니다. 하나를 바꿔도 다른 것에는 영향이 없으며, 둘 다 또는 하나만 전달할 수 있습니다.

멀티 테넌시 (Multi-tenancy)

에이전트가 여러 사용자를 서비스할 때 세 가지 우려를 처리해야 합니다. 각 사용자가 누구인지 검증하고, 접근할 수 있는 것을 제어하고, 에이전트가 사용자를 대신해 행동할 때 사용하는 자격 증명을 관리하는 것입니다.

세 개의 인증 계층이 구성됩니다: 최종 사용자 인증, 사용자를 대신하는 에이전트 인증, 팀 RBAC

사용자 신원과 접근 제어 (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은 비동기 메서드 이름에 `a` 접두사를 붙이는 관례를 따릅니다(예: `ainvoke`, `abefore_agent`, `astream`). 동기와 비동기 변형은 같은 클래스나 네임스페이스에 있습니다.

프로덕션용으로 빌드할 때:

  • 비동기 도구 만들기. 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. 모든 체크포인트된 단계는 되감을 수 있는 스냅샷이므로, 문제가 발생하면 이전 상태에서 재생할 수 있습니다.
  • 민감한 연산의 안전한 처리. 결제나 다른 되돌릴 수 없는 작업을 수반하는 워크플로의 경우, 체크포인트는 감사 추적과 작업으로 이끈 정확한 상태를 검사할 복구 지점을 제공합니다.
[LangSmith Deployments](/langsmith/deployment)는 영구 체크포인터를 자동으로 구성합니다. 셀프 호스팅이라면 설정 지침을 [persistence](/oss/javascript/langgraph/persistence)에서 참조하세요.

메모리 (Memory)

메모리가 없으면 모든 대화는 처음부터 시작합니다. 메모리는 에이전트가 대화를 넘어 정보(사용자 선호도, 학습한 지침, 과거 경험)를 유지하게 해 시간이 지나며 동작을 개인화할 수 있습니다. 메모리 유형에 대한 개요는 메모리 개념 가이드를 참조하세요.

단기 메모리는 체크포인트를 통해 단일 스레드로 한정되고, 장기 메모리는 스토어를 통해 스레드를 넘어 지속됩니다

범위 지정 (Scoping)

메모리는 항상 대화를 넘어 영속적입니다. 핵심 질문은 사용자와 어시스턴트 경계에 걸쳐 어떻게 범위 지정되는가입니다. 올바른 범위는 누가 데이터를 보고 수정해야 하는지에 따라 달라집니다:

범위 네임스페이스 사용 사례 예시
User(권장 기본값) (user_id) 사용자별 선호도와 컨텍스트 "I prefer concise responses"
Assistant (assistant_id) 한 어시스턴트의 공유 지침 "Cap posts at 280 characters"
Global (org_id) 모든 사용자와 어시스턴트의 읽기 전용 정책 "Never disclose internal pricing"
공유 메모리(어시스턴트, 사용자, 또는 조직 범위)는 프롬프트 인젝션의 통로입니다. 한 사용자가 다른 사용자의 대화가 읽는 메모리에 쓸 수 있다면, 악의적인 사용자가 그 공유 상태에 지침을 주입할 수 있습니다. 해당하는 곳에서 읽기 전용 접근을 강제하세요. 예를 들어 조직 전체 정책은 에이전트가 아니라 애플리케이션 코드를 통해서만 쓰도록 만드세요. 공유 경로에 대한 쓰기를 선언적으로 거부하려면 [권한](/oss/javascript/deepagents/permissions)을, 커스텀 검증 로직에는 [백엔드 정책 훅](/oss/javascript/deepagents/backends#add-policy-hooks)을 사용하세요.

구성 (Configuration)

Deep Agents에서 메모리는 가상 파일 시스템의 파일로 저장됩니다. 기본적으로 파일은 단일 스레드(대화)에 한정되며 스레드 간에 공유되지 않습니다. 그 외에 스레드 간에 메모리를 공유하려면 /memories/ 같은 경로를 LangGraph Store에 쓰는 StoreBackend로 라우팅하세요. CompositeBackend를 사용해 에이전트에게 스레드 한정 임시 공간과 크로스 스레드 장기 메모리를 모두 제공하세요.

아래에 나오는 `rt.serverInfo`와 `rt.executionInfo` 네임스페이스 패턴은 `deepagents>=1.9.0`이 필요합니다. `user_id`로 네임스페이스 지정. 각 사용자는 자신만의 비공개 메모리를 얻습니다. 대부분의 애플리케이션이 단일 어시스턴스를 배포하므로 권장되는 기본값입니다.
```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.`,
});
```
`assistant_id`로 네임스페이스 지정. 메모리는 같은 어시스턴트의 모든 사용자에게 공유되므로 어떤 사용자든 읽거나 업데이트할 수 있습니다. 특정 어시스턴트를 사용하는 모든 사람에게 적용되는 공유 지침이나 지식에 사용하세요(예: "always reply in formal tone").
```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]
      }),
    },
  ),
});
```
`user_id`만으로 네임스페이스 지정. 메모리는 모든 어시스턴트를 따라 사용자를 따릅니다. 사용자가 어떤 어시스턴트와 대화하는지와 무관하게 적용되어야 하는 전역 사용자 프로필(이름, 시간대, 커뮤니케이션 선호도)에 사용하세요.
```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]
      }),
    },
  ),
});
```
`org_id`로 네임스페이스 지정. 메모리는 모든 사용자와 모든 어시스턴스에 공유됩니다. 보통 에이전트에게 읽기 전용이어야 하는 조직 전체 정책(규정 준수 규칙, 브랜드 지침)에 사용됩니다. 프롬프트 인젝션을 막으려면 쓰기 접근을 애플리케이션 코드로 제한해야 합니다.
```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를 참조하세요.

`FilesystemBackend`와 `LocalShellBackend`는 호스트에 직접 접근합니다. 배포된 에이전트에서 사용하지 마세요.

샌드박스 (Sandboxes)

에이전트가 코드를 실행해야 한다면(파일을 읽고 쓰는 것만이 아니라) 샌드박스를 사용하세요. 샌드박스는 격리된 컨테이너 안에서 파일 시스템과 셸 명령을 실행하기 위한 execute 도구를 모두 제공합니다. 이 격리는 호스트도 보호합니다. 에이전트의 코드가 메모리를 소진하거나 충돌해도 샌드박스만 영향받습니다. 서버는 계속 실행됩니다.

수명 주기 (Lifecycle)

핵심 결정은 샌드박스가 얼마나 오래 사는가입니다. 각 대화가 새 것을 얻을까요, 아니면 대화가 지속 환경을 공유할까요?

범위 샌드박스 ID 저장 위치 수명 주기 예시 사용 사례
Thread-scoped Thread 메타데이터 대화마다 새로, TTL에 정리 각 대화가 깨끗하게 시작하는 데이터 분석 봇
Assistant-scoped Assistant 구성 모든 대화에 공유 대화를 넘어 클론된 저장소를 유지하는 코딩 어시스턴트
아래 예시는 정적 그래프 대신 비동기 [graph factory](/langsmith/graph-rebuild)를 사용합니다. 샌드박스가 올바른 샌드박스를 조회하거나 만들기 위해 `thread_id` 또는 `assistant_id`가 필요하기 때문입니다. Graph factory는 완전한 `Runtime`을 받지 않습니다(`server_info`나 `execution_info` 없음). 대신 `RunnableConfig`를 받고 `config["configurable"]`에서 `thread_id`와 `assistant_id`를 읽습니다. 팩토리는 비동기입니다. 샌드박스 생성이 호출 시점에만 사용할 수 있는 실행별 정보가 필요한 I/O 바운드 연산이기 때문입니다. 각 대화는 자신만의 샌드박스를 얻습니다. [graph factory](/langsmith/graph-rebuild)가 실행 구성에서 `thread_id`를 읽으므로 각 [thread](/langsmith/use-threads)는 자동으로 자신만의 격리된 환경을 얻습니다. 이름 있는 샌드박스 조회가 실행 간 중복 제거를 처리합니다. 샌드박스 [TTL](/langsmith/configure-ttl)이 만료되면 정리됩니다.
```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 }),
  });
}
```
모든 대화가 하나의 샌드박스를 공유합니다. [graph factory](/langsmith/graph-rebuild)가 `config["configurable"]`에서 [assistant](/langsmith/assistants) ID를 읽으므로 같은 어시스턴트의 모든 스레드는 같은 환경으로 돌아갑니다. 파일, 설치된 패키지, 클론된 저장소가 대화를 넘어 지속됩니다.
```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()를 사용해 샌드박스 경계를 넘어 데이터를 옮기세요:

  • 에이전트 실행 전에 샌드박스 시드: 사용자 파일, 스킬 스크립트, 구성, 또는 영구 메모리를 업로드해 에이전트가 처음부터 필요한 것을 갖게 하세요
  • 에이전트 완료 후 결과 검색: 생성된 산출물(보고서, 플롯, 내보내기)을 다운로드하고 업데이트된 메모리를 향후 대화를 위해 다시 동기화하세요

프로바이더별 파일 전송 예시는 파일 작업을 참조하세요. 프로바이더 설정, 보안, 수명 주기 패턴은 전체 샌드박스 가이드를 참조하세요.

에이전트가 실행해야 하는 [스킬](/oss/javascript/deepagents/skills) 스크립트는 에이전트가 실행되기 전에 샌드박스에 업로드해야 합니다. 또한 에이전트가 컨테이너 안에서 읽고 업데이트할 수 있도록 [메모리](/oss/javascript/deepagents/memory)를 동기화하고 싶을 수도 있습니다. `before_agent`와 `after_agent` 훅이 있는 [커스텀 미들웨어](/oss/javascript/langchain/middleware/custom)를 사용해 샌드박스 경계를 넘어 파일을 옮기세요:
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 사용 가능한) 프론트엔드 훅으로, 에이전트에서 메시지, 서브에이전트 진행 상황, 커스텀 상태를 실시간으로 스트리밍합니다.

로컬에서 useStreamhttp://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 패턴은 프론트엔드 가이드를 참조하세요.

더 알아보기