本页目录
Advanced / 高级能力Note 82

第五章:Going to Production

生产 Deep Agent 的核心不是“把本地 agent 变量直接部署出去”,而是让每个 run 带着稳定的 threadid 和本次请求的 context。当后端资源依赖 threadid,例如 threadscoped sandbox 时,导出的 Agent 应该是 async graph factory:平台每

生产 Deep Agent 的核心不是“把本地 agent 变量直接部署出去”,而是让每个 run 带着稳定的 thread_id 和本次请求的 context。当后端资源依赖 thread_id,例如 thread-scoped sandbox 时,导出的 Agent 应该是 async graph factory:平台每次运行调用 factory,factory 查找或创建正确 sandbox 后返回本次运行的图。

最小代码

代码在 deepagent_src/advanced_teach/05_going_to_production.py

本章用 LocalShellBackend 在临时目录模拟生产 sandbox。真实生产把 registry 换成 SandboxClient + LangSmithSandbox;核心映射关系不变:

async def build_agent(config: RunnableConfig, registry: ThreadBackendRegistry):
    thread_id = config["configurable"]["thread_id"]
    backend = await registry.get_or_create(thread_id)
    return create_deep_agent(
        model=get_gpt_model(disable_tool_streaming=True),
        backend=backend,
        subagents=[],
    )

例子会实际调用一次 LLM,让 alpha 线程的 Agent 用 write_file/thread-note.txt。随后直接检查 backend:

  1. 再次为 alpha 调 factory,拿到同一个 backend。
  2. beta 调 factory,拿到不同 backend。
  3. alphabeta 写同一路径,但内容互不影响。

运行命令

uv run python -m deepagent_src.advanced_teach.05_going_to_production

这会触发一次真实 LLM 调用,并只在临时目录中写文件。没有 CHATGPT_API_KEYCHATGPT_API_URL 时,真实调用会失败。

预期现象

alpha backend id: local-...
beta backend id: local-...
graph factory thread-scoped production pattern ok

两个 backend ID 不同,但同一 thread_id="alpha" 的两次 factory 调用复用同一个 backend。

thread_idcontext

两者容易混,职责完全不同:

数据 作用域 用途
config["configurable"]["thread_id"] 对话 / thread checkpoint、消息历史、thread-scoped sandbox
context 单次 run user_id、功能开关、权限信息、短期请求元数据

同一个 thread_id 能连续对话并返回同一 sandbox;同一 thread 的不同 run 可以携带不同 context。不要把用户身份或 API key 塞进 thread_id

生产骨架

部署根目录需要 langgraph.json,典型最小结构:

{
  "dependencies": ["."],
  "graphs": {
    "agent": "./agent.py:agent"
  },
  "env": ".env"
}

真实部署的 graph factory 大致是:

from deepagents.backends.langsmith import LangSmithSandbox
from langsmith.sandbox import SandboxClient

client = SandboxClient()

async def agent(config):
    thread_id = config["configurable"]["thread_id"]
    sandbox = client.create_sandbox(
        name=f"thread-{thread_id}",
        idle_ttl_seconds=3600,
    )
    return create_deep_agent(
        model="openai:gpt-5.5",
        backend=LangSmithSandbox(sandbox=sandbox),
    )

示意代码省略了“先按名称查已有 sandbox”的分支;实际代码必须先 lookup 再 create,避免同一线程并发请求各自创建环境。sandbox 生命周期通常用 idle TTL 清理。

常见误区

不要把 LocalShellBackend 部署到服务端。它访问宿主机文件系统,只适合本地教学。生产执行代码要用隔离 sandbox,并通过 sandbox auth proxy 注入外部服务凭证,sandbox 内不要保存用户 API key。

也不要误以为 factory 每次运行返回新的 graph 就必然浪费很大。graph factory 的必要性来自运行时资源绑定;真正昂贵的通常是 sandbox 创建。通过按 thread_id 命名、查找复用和 TTL 管理,避免重复创建即可。

验证

  1. alpha 的两次 factory 调用复用同一个 backend。
  2. alphabeta 的 backend 不同。
  3. 真实 LLM 在 alpha backend 中调用 write_file
  4. 两个 thread 在相同虚拟路径下读到各自内容。

官方依据:/oss/python/deepagents/going-to-production 说明 thread_id 用于对话与 checkpoint,context 用于 per-run 数据;需要 thread-scoped sandbox 时,使用 async graph factory 从 config["configurable"]["thread_id"] 解析 sandbox,并用 TTL 清理闲置环境。

相关资源

  • 查看示例代码:deepagent_src/advanced_teach/05_going_to_production.py
    from __future__ import annotations
    
    import asyncio
    import os
    from dataclasses import dataclass, field
    from pathlib import Path
    from tempfile import TemporaryDirectory
    from typing import Any
    
    from deepagents import create_deep_agent
    from deepagents.backends import LocalShellBackend
    from langchain.messages import HumanMessage
    from langchain_core.runnables import RunnableConfig
    
    from deepagent_src.llms import get_gpt_model
    
    
    @dataclass
    class ThreadBackendRegistry:
        root_dir: Path
        backends: dict[str, LocalShellBackend] = field(default_factory=dict)
    
        async def get_or_create(self, thread_id: str) -> LocalShellBackend:
            if not thread_id:
                raise ValueError("configurable.thread_id is required")
            if thread_id not in self.backends:
                workspace = self.root_dir / f"thread-{thread_id}"
                workspace.mkdir(parents=True, exist_ok=True)
                self.backends[thread_id] = LocalShellBackend(
                    root_dir=workspace,
                    virtual_mode=True,
                    env={"PATH": "/usr/bin:/bin"},
                    timeout=10,
                )
            return self.backends[thread_id]
    
    
    def get_thread_id(config: RunnableConfig) -> str:
        configurable = config.get("configurable", {})
        thread_id = configurable.get("thread_id")
        if not isinstance(thread_id, str) or not thread_id:
            raise ValueError("configurable.thread_id is required")
        return thread_id
    
    
    async def build_agent(
        config: RunnableConfig,
        registry: ThreadBackendRegistry,
    ) -> Any:
        thread_id = get_thread_id(config)
        backend = await registry.get_or_create(thread_id)
        return create_deep_agent(
            model=get_gpt_model(disable_tool_streaming=True),
            backend=backend,
            subagents=[],
            system_prompt=(
                "You are a graph-factory teaching assistant. "
                f"You are serving thread {thread_id}. "
                "You must call write_file once with path /thread-note.txt and "
                f"content thread={thread_id}. Then reply with stored {thread_id}."
            ),
        )
    
    
    def read_text(backend: LocalShellBackend, path: str) -> str:
        result = backend.read(path)
        if result.error:
            raise AssertionError(result.error)
        return result.file_data["content"]
    
    
    def tool_call_names(messages: list[Any]) -> list[str]:
        names: list[str] = []
        for message in messages:
            for call in getattr(message, "tool_calls", None) or []:
                names.append(call.get("name", ""))
        return names
    
    
    async def main() -> None:
        os.environ["LANGSMITH_TRACING"] = "false"
        os.environ["LANGCHAIN_TRACING_V2"] = "false"
    
        with TemporaryDirectory(prefix="deepagents-production-factory-") as tmp:
            registry = ThreadBackendRegistry(Path(tmp))
            alpha_config: RunnableConfig = {"configurable": {"thread_id": "alpha"}}
            beta_config: RunnableConfig = {"configurable": {"thread_id": "beta"}}
    
            alpha_agent = await build_agent(alpha_config, registry)
            alpha_backend = registry.backends["alpha"]
            _ = await build_agent(alpha_config, registry)
            beta_agent = await build_agent(beta_config, registry)
            beta_backend = registry.backends["beta"]
    
            assert registry.backends["alpha"] is alpha_backend
            assert alpha_backend is not beta_backend
    
            state = await alpha_agent.ainvoke(
                {"messages": [HumanMessage(content="Store this thread note now.")]}
            )
            alpha_messages = state["messages"]
            assert "write_file" in tool_call_names(alpha_messages)
            assert read_text(alpha_backend, "/thread-note.txt") == "thread=alpha"
    
            beta_backend.write("/thread-note.txt", "thread=beta")
            assert read_text(alpha_backend, "/thread-note.txt") == "thread=alpha"
            assert read_text(beta_backend, "/thread-note.txt") == "thread=beta"
    
            assert beta_agent is not alpha_agent
            print(f"alpha backend id: {alpha_backend.id}")
            print(f"beta backend id: {beta_backend.id}")
            print("graph factory thread-scoped production pattern ok")
    
    
    if __name__ == "__main__":
        asyncio.run(main())