Skip to main content
本指南涵盖了将 Deep Agent 从本地原型部署到生产环境的考虑因素。它将逐步介绍内存范围界定、执行环境配置、添加防护措施以及连接前端。

概述

代理使用来自内存和执行环境的信息来完成任务。 在生产环境中,有几个基本要素决定了信息的共享和访问方式:
  • 线程:单个对话。消息历史记录和临时文件默认限定在线程范围内,不会跨线程传递。
  • 用户:与您的代理交互的人。内存和文件可以是用户私有的,也可以跨用户共享。身份验证和授权来自您的认证层
  • 助理:配置的代理实例。内存和文件可以绑定到一个助理,也可以在所有助理之间共享。
本页涵盖:

LangSmith 部署

将 Deep Agent 投入生产的最快方法是使用 deepagents deploy,它会打包您的代理配置,并通过一个命令将其部署为 LangSmith 部署。或者,您可以直接配置 LangSmith 部署。无论哪种方式,都会为您的代理配置所需的基础设施:助理线程运行、存储和检查点器,因此您无需自行设置。它还开箱即用地提供认证Webhookcron 作业可观测性,并且可以通过 MCPA2A 公开您的代理。 对于基于 CLI 的方法,请参阅使用 CLI 部署。对于手动设置,请参阅 LangSmith 部署快速入门 除非另有说明,本页上的所有代码片段都使用以下 langgraph.json
langgraph.json
{
  "dependencies": ["."],
  "graphs": {
    "agent": "./src/agent.ts:agent"
  },
  "env": ".env"
}
langgraph.json 是一个配置文件,用于告诉 LangGraph 平台如何构建和运行您的应用程序。它位于项目的根目录,对于本地开发(使用 langgraph dev)和生产部署都是必需的。关键字段如下:
字段描述
dependencies要安装的包。["."] 将当前目录安装为包(从 requirements.txtpyproject.tomlpackage.json 读取)。
graphs将图 ID 映射到其代码位置。每个条目是 "<id>": "./<file>:<variable>",其中 <id> 是您通过 API 调用图时使用的名称,<variable> 是从 <file> 导出的已编译图或构造函数。
env包含环境变量(API 密钥、机密)的 .env 文件路径。这些在构建时设置,并在运行时可用。
有关完整的配置选项(自定义 Docker 步骤、存储索引、认证处理程序等),请参阅应用程序结构

生产考虑因素

多租户

当您的代理服务于多个用户时,您需要处理三个问题:验证每个用户的身份、控制他们可以访问的内容,以及管理代理代表他们操作时使用的凭证。

用户身份和访问控制

LangSmith 部署支持自定义认证来建立用户身份,以及授权处理程序来控制对线程、助理和存储命名空间等资源的访问。授权处理程序在认证成功后运行,并且可以:
  • 为资源标记所有权元数据(例如,owner: user_id
  • 返回过滤器,以便用户只能看到自己的资源
  • 对未经授权的操作返回 HTTP 403 拒绝访问
有关分步教程,请参阅使对话私有化。有关演练,请观看自定义认证视频 您如何界定内存范围执行环境决定了用户之间共享哪些数据。有关详细信息,请参阅以下部分。

团队访问控制 (RBAC)

LangSmith 的基于角色的访问控制管理您团队中谁可以部署、配置和监控代理。这与上述最终用户授权是分开的。
角色访问权限
工作区管理员完全权限,包括设置和成员管理
工作区编辑者创建和修改资源,但不能删除运行或管理成员
工作区查看者只读访问
企业版计划提供具有细粒度权限的自定义角色。有关完整的权限模型,请参阅 RBAC 参考

最终用户凭证

当您的代理需要代表用户调用外部 API(例如,读取他们的 GitHub 仓库、发送 Slack 消息、查询他们的数据仓库)时,您需要一种方法将用户的凭证传递给代理,而无需硬编码。 通过代理认证进行 OAuth。 代理认证提供托管的 OAuth 2.0 流程。配置 OAuth 提供程序后,代理可以请求限定于每个用户的令牌。首次使用时,代理会中断执行并呈现 OAuth 同意 URL。用户认证后,代理使用有效令牌恢复。令牌会自动存储和刷新。
import { Client } from "@langchain/auth";

const authClient = new Client();

// 在代理的工具内部:
// 通过 runtime.serverInfo 访问经过认证的用户
const authResult = await authClient.authenticate({
  provider: "github",
  scopes: ["repo", "read:org"],
  userId: runtime.serverInfo.user.identity,
});
// 使用 authResult.token 代表用户进行 GitHub API 调用
沙盒的凭证注入。 如果您的代理在沙盒内运行代码来调用外部 API,沙盒认证代理可以自动将凭证注入出站请求,因此沙盒代码永远不会收到原始 API 密钥。有关设置详细信息,请参阅管理机密 工作区机密。 对于跨所有用户共享的 API 密钥(例如,您组织的 LLM 提供程序密钥、搜索 API 密钥),请将它们存储为 LangSmith 中的工作区机密。有关详细信息,请参阅管理机密

异步

基于 LLM 的应用程序严重受 I/O 限制:调用语言模型、数据库和外部服务。异步编程让这些操作并发运行而不是阻塞,从而提高吞吐量和响应能力。
LangChain 遵循在异步方法名称前加 a 的约定(例如,ainvokeabefore_agentastream)。同步和异步变体位于同一个类或命名空间中。
构建生产环境时:
  • 创建异步工具。 LangChain 在单独的线程中运行同步工具以避免阻塞,但原生异步可以完全避免线程开销。
  • 使用异步中间件方法。 自定义中间件应实现异步钩子(例如,使用 abefore_agent 而不是 before_agent)。
  • 对外部资源生命周期使用异步。 创建沙盒或连接到 MCP 服务器涉及网络调用,应等待。这就是为什么配置这些资源的图工厂是异步的。

持久性

Deep Agent 在 LangGraph 上运行,LangGraph 开箱即用地提供持久执行持久化层在每个步骤检查点状态,因此因故障、超时或人机回环暂停而中断的运行可以从其最后记录的状态恢复,而无需重新处理之前的步骤。对于生成许多子代理的长时间运行的 Deep Agent,这意味着运行中失败不会丢失已完成的工作。 检查点还支持:
  • 无限期中断 人机回环工作流可以暂停几分钟或几天,并在离开的地方准确恢复。
  • 时间旅行 每个检查点步骤都是一个快照,您可以回退到该状态,如果出现问题,可以从早期状态重放。
  • 安全处理敏感操作。 对于涉及付款或其他不可逆操作的工作流,检查点提供审计跟踪和恢复点,以检查导致操作的确切状态。
LangSmith 部署会自动配置持久检查点器。如果您是自托管,请参阅持久化了解设置说明。

内存

没有内存,每次对话都从头开始。内存让您的代理能够跨对话保留信息(用户偏好、学习的指令、过去的经验),以便随着时间的推移个性化其行为。有关内存类型的概述,请参阅内存概念指南

范围界定

内存始终在对话之间持久化。主要问题是如何在用户和助理边界之间界定范围。正确的范围取决于谁应该查看和修改数据:
范围命名空间用例示例
用户(推荐默认)(user_id)每个用户的偏好和上下文“我更喜欢简洁的回复”
助理(assistant_id)一个助理的共享指令“将帖子限制在 280 个字符”
全局(org_id)所有用户和助理的只读策略“切勿披露内部定价”
共享内存(助理、用户或组织范围)是提示注入的载体。如果一个用户可以写入另一个用户对话读取的内存,恶意用户可能会将指令注入该共享状态。在适当的地方强制执行只读访问。例如,使组织范围的策略只能通过应用程序代码写入,而不能由代理本身写入。使用权限以声明方式拒绝写入共享路径,或使用后端策略钩子进行自定义验证逻辑。

配置

在 Deep Agent 中,内存作为文件存储在虚拟文件系统中。默认情况下,文件仅在单个对话中有效。要持久化它们,请将路径(如 /memories/)路由到一个 StoreBackend,该后端写入 LangGraph Store。使用 CompositeBackend 为代理提供临时暂存空间和持久的长期内存
下面显示的 rt.serverInfort.executionInfo 命名空间模式需要 deepagents>=1.9.0
user_id 划分命名空间。每个用户都有自己的私有内存。这是推荐的默认设置,因为大多数应用程序部署单个助理。
src/agent.ts
import { createDeepAgent, CompositeBackend, StateBackend, StoreBackend } from "deepagents";

export const agent = createDeepAgent({
  backend: new CompositeBackend(
    new StateBackend(),
    {
      "/memories/": new StoreBackend({
        namespace: (rt) => [
          rt.serverInfo.assistantId,
          rt.serverInfo.user.identity,
        ],
      }),
    },
  ),
  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.`,
});
您还可以使用 Store API 从应用程序代码读取和写入存储。有关示例,请参阅高级用法 有关完整的命名空间工厂 API,请参阅命名空间工厂。有关自改进指令和知识库等内存模式,请参阅长期内存

执行环境

在本地,代理可以直接读写磁盘上的文件并运行 shell 命令。在生产环境中,您需要考虑隔离和持久性。正确的设置取决于您的代理是否需要执行代码:
  • 文件系统后端:如果您的代理只读写文件就足够了。选择符合您持久性需求的后端:临时暂存空间、持久存储或两者的混合。
  • 沙盒:添加一个隔离的容器,其中包含用于运行 shell 命令的 execute 工具。如果您的代理需要运行代码、安装包或执行任何超出文件 I/O 的操作,请使用沙盒。

文件系统

根据需要持久化的内容选择后端:
  • StateBackend(默认):临时暂存空间,限定在单个对话范围内。在每个步骤检查点,因此避免写入大文件。
  • StoreBackend:持久存储,在对话之间持久存在。使用命名空间工厂限定范围。
  • CompositeBackend:混合两者。默认情况下提供临时暂存空间,并为特定路径(如 /memories/)提供持久路由。
有关后端的完整列表以及如何构建自定义后端,请参阅后端
FilesystemBackendLocalShellBackend 直接访问主机。不要在部署的代理中使用它们。

沙盒

如果您的代理需要运行代码(而不仅仅是读写文件),请使用沙盒。沙盒提供文件系统和用于运行 shell 命令的 execute 工具,所有这些都在隔离的容器内。这种隔离还可以保护您的主机:如果代理的代码耗尽内存或崩溃,只有沙盒会受到影响。您的服务器会继续运行。

生命周期

关键决策是沙盒的存活时间。每个对话是获得一个新的沙盒,还是对话共享一个持久环境?
范围沙盒 ID 存储位置生命周期示例用例
线程范围线程 元数据每个对话一个新沙盒,在 TTL 时清理数据分析机器人,每次对话都从干净状态开始
助理范围助理 配置跨所有对话共享编程助理,在对话之间维护克隆的仓库
下面的示例使用异步图工厂而不是静态图,因为沙盒需要 thread_idassistant_id 来查找或创建正确的沙盒。图工厂不接收完整的 Runtime(没有 server_infoexecution_info);而是接受 RunnableConfig 并从 config["configurable"] 读取 thread_idassistant_id。工厂是异步的,因为沙盒创建是一个 I/O 密集型操作,需要仅在调用时可用的每个运行信息。
每个对话都有自己的沙盒。图工厂从运行配置中读取 thread_id,因此每个线程都会自动获得自己的隔离环境。提供程序的基于标签的查找处理跨运行的重复数据删除。当沙盒 TTL 过期时清理。
src/agent.ts
import { Daytona } from "@daytonaio/sdk";
import { DaytonaSandbox } from "@langchain/daytona";
import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";

const client = new Daytona();

export async function agent(config: LangGraphRunnableConfig) {
  const threadId = config.configurable?.thread_id as string;
  let sandbox;
  try {
    sandbox = await client.findOne({ labels: { thread_id: threadId } });
  } catch {
    sandbox = await client.create({
      labels: { thread_id: threadId },
      autoDeleteInterval: 3600, // TTL: 空闲时清理
    });
  }
  return createDeepAgent({ backend: await DaytonaSandbox.fromId(sandbox.id) });
}
因为 agent 变量是一个异步函数(而不是已编译的图),服务器将其视为图工厂,并在每次运行时调用它,注入配置。工厂通过提供程序的基于标签的搜索查找或创建沙盒,并返回连接到该沙盒的新代理图。 使用 langgraph deploy 部署后,使用 SDK 从应用程序代码调用代理。客户端代码与范围无关。范围界定完全在上面的代理工厂中处理,但行为有所不同:
每个线程都有自己的沙盒。同一线程内的后续消息重用同一个沙盒,但新线程总是从干净状态开始,没有之前对话的残留文件或安装的包。
client.ts
import { Client } from "@langchain/langgraph-sdk";

const client = new Client({ apiUrl: "<DEPLOYMENT_URL>", apiKey: "<LANGSMITH_API_KEY>" });

// 对话 1:安装 pandas 并分析数据
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);
}

// 同一对话中的后续操作 — pandas 仍已安装
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);
}

// 对话 2:新沙盒 — pandas 未安装,没有对话 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);
}

文件传输

沙盒是隔离的容器,因此您的应用程序代码无法直接访问其中的文件。使用 upload_files()download_files() 在沙盒边界之间移动数据:
  • 在代理运行之前播种沙盒:上传用户文件、技能脚本、配置或持久内存,以便代理从一开始就拥有所需的内容
  • 代理完成后检索结果:下载生成的工件(报告、图表、导出)并同步更新的内存以供将来对话使用
有关特定于提供程序的文件传输示例,请参阅处理文件。有关提供程序设置、安全性和生命周期模式,请参阅完整的沙盒指南
代理需要执行的技能脚本必须在代理运行之前上传到沙盒中。您可能还想同步内存,以便代理可以在容器内读取和更新它们。使用带有 before_agentafter_agent 钩子的自定义中间件在沙盒边界之间移动文件:
src/agent.ts
import { createMiddleware } from "langchain";
import { createDeepAgent, CompositeBackend, StoreBackend } from "deepagents";
import { DaytonaSandbox } from "@langchain/daytona";

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) => {
      // 将技能脚本和内存上传到沙盒中
      const userId = runtime.serverInfo.user.identity;
      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) => {
      // 将更新的内存同步回存储
      const userId = runtime.serverInfo.user.identity;
      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 backend = new CompositeBackend(
  await DaytonaSandbox.fromId(sandbox.id),
  {
    "/skills/": new StoreBackend({
      namespace: (rt) => ["skills", rt.serverInfo.user.identity],
    }),
    "/memories/": new StoreBackend({
      namespace: (rt) => ["memories", rt.serverInfo.user.identity],
    }),
  },
);

export const agent = createDeepAgent({
  backend,
  middleware: [createSandboxSyncMiddleware(backend)],
});

管理机密

沙盒是隔离的容器,因此来自主机的环境变量在其中不可用。有两种方法可以向沙盒代码提供 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 中的工作区机密。这些在运行时作为环境变量对工作区中的所有代理可用。
避免通过环境变量或文件上传将机密传递到沙盒中。代理可以读取沙盒内任何可访问的文件或环境变量,包括凭据。认证代理使机密完全脱离沙盒。

防护措施

生产环境中的代理自主运行,这意味着它们可能会无限循环、达到速率限制或处理包含敏感信息的用户数据。Deep Agent 提供两层保护:
  • 权限:声明性的允许/拒绝规则,控制代理可以读取或写入哪些文件和目录。使用权限将代理隔离到工作目录、保护敏感文件或强制执行只读内存。
  • 中间件:包装模型和工具调用的钩子,用于速率限制、错误处理和数据隐私。

速率限制

这里的速率限制指的是限制代理自身在一次运行中的 LLM 和工具使用,而不是传入请求的 API 网关速率限制。 如果没有限制,一个混乱的代理可能会在几分钟内耗尽您的 LLM API 预算,因为它在同一个工具调用上循环或进行数百次模型调用。对每次运行的模型调用和工具执行设置上限:
import { createAgent, modelCallLimitMiddleware, toolCallLimitMiddleware } from "langchain";

const agent = createAgent({
  model: "google_genai:gemini-3.1-pro-preview",
  middleware: [
    modelCallLimitMiddleware({ runLimit: 50 }),
    toolCallLimitMiddleware({ runLimit: 200 }),
  ],
});
使用 run_limit 限制单次调用内的调用(每次轮次重置)。使用 thread_limit 限制整个对话的调用(需要检查点器)。有关完整配置,请参阅 ModelCallLimitMiddlewareToolCallLimitMiddleware

错误处理

并非所有错误都应以相同方式处理。瞬态故障(网络超时、速率限制)应自动重试。LLM 可以恢复的错误(错误的工具输出、解析失败)应反馈给模型。需要人工输入的错误应暂停代理。有关完整分解和代码示例,请参阅适当处理错误 中间件处理瞬态情况。模型调用和工具调用各自都有自己的重试中间件,具有指数退避。如果您的主要模型提供程序完全宕机,后备中间件会切换到替代方案:
import {
  createAgent,
  modelFallbackMiddleware,
  modelRetryMiddleware,
  toolRetryMiddleware,
} from "langchain";

const agent = createAgent({
  model: "google_genai:gemini-3.1-pro-preview",
  middleware: [
    // 在速率限制、超时和 5xx 错误时重试模型调用
    modelRetryMiddleware({ maxRetries: 3, backoffFactor: 2.0, initialDelayMs: 1000 }),
    // 如果主要模型完全宕机,则后备到替代方案
    modelFallbackMiddleware("gpt-4.1"),
    // 重试命中外部 API 的特定工具(不是所有工具)
    toolRetryMiddleware({
      maxRetries: 2,
      tools: ["search", "fetch_url"],
      retryOn: [TimeoutError, TypeError],
    }),
  ],
});
ToolRetryMiddleware 限定于特定工具,而不是重试所有工具。失败的文件系统 read_file 不会从重试中受益,但超时的网络搜索可能会。有关完整配置,请参阅 ModelRetryMiddlewareModelFallbackMiddleware

数据隐私

如果您的代理处理可能包含电子邮件、信用卡号或其他个人身份信息 (PII) 的用户输入,您可以在其到达模型或存储在日志中之前检测并处理它:
import { createAgent, piiMiddleware } from "langchain";

const agent = createAgent({
  model: "google_genai:gemini-3.1-pro-preview",
  middleware: [
    piiMiddleware("email", { strategy: "redact", applyToInput: true }),
    piiMiddleware("credit_card", { strategy: "mask", applyToInput: true }),
  ],
});
策略包括 redact(替换为 [REDACTED_EMAIL])、mask(部分掩码,如 ****-****-****-1234)、hash(确定性哈希)和 block(引发错误)。您还可以为特定于域的模式编写自定义检测器。 有关完整配置,请参阅 piiMiddleware 有关可用中间件的完整列表,请参阅预构建中间件

前端

Deep Agent 使用 useStream 将您的 UI 连接到代理后端。useStream 是一个前端钩子(适用于 React、Vue、Svelte 和 Angular),可实时流式传输消息、子代理进度和来自代理的自定义状态。 在本地,useStream 指向 http://localhost:2024。在生产环境中,将其指向您的 LangSmith 部署 并配置重新连接,以便用户在连接断开时不会丢失进度。
import { useStream } from "@langchain/react";

function App() {
  const stream = useStream<typeof agent>({
    apiUrl: "https://your-deployment.langsmith.dev",
    assistantId: "agent",
    reconnectOnMount: true,    // 页面刷新或导航后恢复流
    fetchStateHistory: true,   // 挂载时加载完整的线程历史记录
  });
}
reconnectOnMount 会自动恢复进行中的运行。如果用户在代理工作时刷新页面,他们将看到它继续而不是空白屏幕。fetchStateHistory 加载线程的完整对话历史记录,因此返回的用户会看到之前的消息。 对于生成许多子代理的深度代理工作流,在提交时设置较高的 recursionLimit 以避免切断长时间运行的执行:
stream.submit(
  { messages: [{ type: "human", content: text }] },
  {
    streamSubgraphs: true,
    config: { recursionLimit: 10000 },
  },
);
有关深度代理特定的 UI 模式,例如子代理卡片、待办事项列表和自定义状态渲染,请参阅前端指南