Agent 沙盒机制学习笔记

一、本章概览

本模块演示如何让 AI Agent 在隔离环境中安全执行代码,包含三种实现方案:

文件 方案 需要云服务 状态
local_sandbox.py Docker 本地沙盒核心实现 不需要 ✅ 已验证
docker_sandbox_test.py Agent + Docker 沙盒集成测试 不需要 ✅ 已验证
sandbox_test.py Agent + Daytona 云沙盒 需要 API Key ☁️ 可选
渲染错误: Mermaid 渲染失败: Lexical error on line 2. Unrecognized text. ...TB subgraph 本地方案(推荐) User[用户 ----------------------^

二、为什么需要沙盒

Agent 需要执行代码来完成计算、文件操作等任务。但直接在宿主机执行代码有严重风险:

风险 后果
代码删除文件 宿主机数据丢失
安装恶意包 系统中毒
死循环占满内存 电脑卡死
网络请求 数据泄露

沙盒解决了这些问题:代码在隔离环境中执行,即使出错也不影响宿主机。


三、Docker 本地沙盒(核心实现)

3.1 工作原理

每次执行代码时,DockerSandbox 会:

  1. 创建一个临时 Docker 容器
  2. 将代码作为 python -c 参数传入
  3. 捕获 stdout/stderr 输出
  4. 自动销毁容器
宿主机 Docker 容器 DockerSandbox Agent 宿主机 Docker 容器 DockerSandbox Agent 宿主机完全不受影响 execute("print('hello')") 创建临时容器(内存限制 256MB) container.start() python -c "print('hello')" stdout: "hello" container.remove()(自动销毁) "hello"

3.2 DockerSandbox 类

import docker
import uuid

class DockerSandbox:
    def __init__(
        self,
        image: str = "python:3.12-slim",   # Docker 镜像
        mem_limit: str = "256m",             # 内存限制
        cpu_quota: int = 50000,              # CPU 配额(50% 单核)
        network_disabled: bool = False,      # 是否禁用网络
        timeout: int = 30,                   # 超时时间(秒)
    ):
        self.client = docker.from_env()      # 连接本地 Docker

    def execute(self, code: str) -> str:
        """在沙盒中执行代码"""
        container = self.client.containers.create(
            image=self.image,
            command=["python", "-c", code],
            mem_limit=self.mem_limit,
            network_disabled=self.network_disabled,
            detach=True,
        )
        container.start()
        result = container.wait(timeout=self.timeout)
        stdout = container.logs(stdout=True, stderr=False).decode("utf-8")
        stderr = container.logs(stdout=False, stderr=True).decode("utf-8")
        container.remove(force=True)  # 自动清理
        return stdout.strip() or stderr.strip() or "(无输出)"

3.3 安全隔离机制

隔离维度 实现方式 效果
文件系统 Docker 容器隔离 容器内的文件操作不影响宿主机
网络 network_disabled=True 沙盒无法联网,防止数据泄露
内存 mem_limit="256m" 超出限制直接 OOM Kill(退出码 137)
CPU cpu_quota=50000 限制为单核的 50%
生命周期 container.remove(force=True) 执行完毕自动销毁,不留残余

3.4 LangChain Tool 封装

将 DockerSandbox 封装为 LangChain Tool,供 Agent 调用:

from langchain_core.tools import tool

def create_sandbox_tool(sandbox: DockerSandbox = None):
    if sandbox is None:
        sandbox = DockerSandbox()

    @tool
    def run_in_sandbox(code: str) -> str:
        """在隔离的 Docker 沙盒中执行 Python 代码。
        入参是纯 Python 代码字符串,不要包裹在 ```python 标记中。"""
        return sandbox.execute(code)

    return run_in_sandbox

四、Agent 集成测试

4.1 完整代码流程

from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver

# 1. 创建 Docker 沙盒(禁用网络,完全隔离)
sandbox = DockerSandbox(
    image="python:3.12-slim",
    mem_limit="256m",
    network_disabled=True,
    timeout=30,
)
sandbox_tool = create_sandbox_tool(sandbox)

# 2. 创建 Agent(带内存记忆)
memory = MemorySaver()
agent = create_react_agent(
    model=llm.llm,
    tools=[sandbox_tool],
    checkpointer=memory,
)

# 3. 发送任务,Agent 自主决定在沙盒中执行代码
result = await agent.ainvoke(
    {"messages": [("user", "在沙盒中计算 1 到 100 的累加和")]},
    config={"configurable": {"thread_id": "sandbox-001"}},
)

4.2 Agent 决策流程

渲染错误: Mermaid 渲染失败: Parse error on line 9: ...01)); print(total)") Tool->>Docker: -----------------------^ Expecting '()', 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'SOLID_ARROW_TOP', 'SOLID_ARROW_BOTTOM', 'STICK_ARROW_TOP', 'STICK_ARROW_BOTTOM', 'SOLID_ARROW_TOP_DOTTED', 'SOLID_ARROW_BOTTOM_DOTTED', 'STICK_ARROW_TOP_DOTTED', 'STICK_ARROW_BOTTOM_DOTTED', 'SOLID_ARROW_TOP_REVERSE', 'SOLID_ARROW_BOTTOM_REVERSE', 'STICK_ARROW_TOP_REVERSE', 'STICK_ARROW_BOTTOM_REVERSE', 'SOLID_ARROW_TOP_REVERSE_DOTTED', 'SOLID_ARROW_BOTTOM_REVERSE_DOTTED', 'STICK_ARROW_TOP_REVERSE_DOTTED', 'STICK_ARROW_BOTTOM_REVERSE_DOTTED', 'BIDIRECTIONAL_SOLID_ARROW', 'DOTTED_ARROW', 'BIDIRECTIONAL_DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', 'SOLID_POINT', 'DOTTED_POINT', got 'NEWLINE'

4.3 测试验证结果

任务 Agent 调用的工具 沙盒返回
打印 Python 版本和目录 run_in_sandbox(import sys; print(...)) Python 3.12.11, 目录 /
创建并读取文件 run_in_sandbox(open("sandbox_test.txt"...)) hello from local docker sandbox
计算 1~100 累加和 run_in_sandbox(sum(range(1,101))) 5050

4.4 工具调用日志追踪

通过追踪 AIMessage.tool_callsToolMessage,验证 Agent 确实通过沙盒执行:

from langchain_core.messages import AIMessage, ToolMessage

for msg in result["messages"]:
    if isinstance(msg, AIMessage) and msg.tool_calls:
        for tc in msg.tool_calls:
            print(f"调用: {tc['name']}({tc['args']})")
    elif isinstance(msg, ToolMessage):
        print(f"返回: {msg.content}")

日志输出示例:

[沙盒工具调用日志]
  调用[1]: run_in_sandbox({'code': 'total = sum(range(1, 101))\nprint(total)'})
    沙盒返回: 5050
[Agent 回复] 1 到 100 的累加和为 5050

五、本地 Docker 沙盒 vs 云沙盒(Daytona)

维度 Docker 本地沙盒 Daytona 云沙盒
费用 免费 付费
API Key 不需要 需要 DAYTONA_API_KEY
网络要求 完全离线可用 必须联网
代码运行位置 本地 Docker 容器 远程云服务器
启动速度 秒级 取决于网络
资源限制 自定义(内存/CPU/网络) 由平台控制
依赖 docker Python SDK daytona-sdk + deepagents

推荐:日常开发使用本地 Docker 沙盒,生产环境按需选择。


六、Docker 沙盒独立测试

local_sandbox.py 可以独立运行,不需要 Agent:

python -m app.sandbox_agent_test.local_sandbox

测试项目及结果:

测试 代码 预期结果 实际结果
基础输出 print('Hello') Hello from Docker sandbox!
系统信息 sys.version + os.getcwd() Python 3.12.x, 目录 /
文件操作 写入→读取→打印 文件内容正确
内存限制 分配 512MB 列表 退出码 137(OOM Kill)

退出码 137 表示容器因超出内存限制被系统强制终止(OOM Killed),证明资源限制生效。


七、环境准备

7.1 安装依赖

pip install docker

7.2 安装 Docker Desktop

  • Windows/Mac:下载 Docker Desktop
  • Linuxsudo apt install docker.io

7.3 拉取 Python 镜像

# 直接拉取(需科学上网)
docker pull python:3.12-slim

# 国内网络使用华为云镜像
docker pull swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/python:3.12-slim
docker tag swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/python:3.12-slim python:3.12-slim

八、关键 API 速查

# ---- Docker 沙盒 ----
from app.sandbox_agent_test.local_sandbox import DockerSandbox, create_sandbox_tool

sandbox = DockerSandbox(
    image="python:3.12-slim",
    mem_limit="256m",
    network_disabled=True,
    timeout=30,
)
result = sandbox.execute("print('hello')")
sandbox.cleanup()

# ---- 作为 LangChain Tool ----
tool = create_sandbox_tool(sandbox)
# tool.name = "run_in_sandbox"
# tool.description = "在隔离的 Docker 沙盒中执行 Python 代码"

# ---- Agent 集成 ----
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver

agent = create_react_agent(
    model=llm.llm,
    tools=[create_sandbox_tool(sandbox)],
    checkpointer=MemorySaver(),
)
result = await agent.ainvoke(
    {"messages": [("user", "在沙盒中执行代码")]},
    config={"configurable": {"thread_id": "001"}},
)

附录:完整源码

A.1 local_sandbox.py — Docker 沙盒核心实现

"""
Docker 本地沙盒核心实现

基于 Docker 构建完全隔离的代码执行环境,无需任何云服务。

核心能力:
    - 在 Docker 容器中执行 Python 代码,与宿主机完全隔离
    - 自动限制资源(CPU、内存),防止恶意代码耗尽系统资源
    - 执行完毕自动清理容器
    - 封装为 LangChain Tool,供 Agent 调用

架构图:
    Agent → run_in_sandbox(code) → Docker 容器 → 返回执行结果

依赖:
    pip install docker
    本地需安装 Docker Desktop(Windows/Mac)或 Docker Engine(Linux)
"""

from __future__ import annotations

import docker
import uuid
from typing import Optional
from langchain_core.tools import tool


# ============================================================
# Docker 沙盒类
# ============================================================

class DockerSandbox:
    """
    基于 Docker 的本地代码沙盒。

    原理:
        每次执行代码时,创建一个临时 Docker 容器,
        将代码写入容器内执行,捕获输出后销毁容器。

    安全隔离:
        - 文件系统隔离:容器内的文件操作不影响宿主机
        - 网络隔离:可选禁用网络访问
        - 资源限制:限制 CPU 和内存使用
        - 自动清理:执行完毕自动删除容器
    """

    def __init__(
        self,
        image: str = "python:3.12-slim",
        mem_limit: str = "256m",
        cpu_quota: int = 50000,
        network_disabled: bool = False,
        timeout: int = 30,
    ):
        self.image = image
        self.mem_limit = mem_limit
        self.cpu_quota = cpu_quota
        self.network_disabled = network_disabled
        self.timeout = timeout

        # 连接本地 Docker
        self.client = docker.from_env()
        print(f"[DockerSandbox] 镜像: {image}, 内存限制: {mem_limit}, "
              f"网络: {'禁用' if network_disabled else '启用'}")

    def execute(self, code: str) -> str:
        """在沙盒中执行 Python 代码"""
        container_name = f"sandbox-{uuid.uuid4().hex[:8]}"
        container = None

        try:
            # 创建容器(不启动)
            container = self.client.containers.create(
                image=self.image,
                name=container_name,
                command=["python", "-c", code],
                mem_limit=self.mem_limit,
                cpu_quota=self.cpu_quota,
                network_disabled=self.network_disabled,
                detach=True,
            )

            # 启动并等待完成
            container.start()
            result = container.wait(timeout=self.timeout)

            # 获取输出
            stdout = container.logs(stdout=True, stderr=False).decode("utf-8", errors="replace")
            stderr = container.logs(stdout=False, stderr=True).decode("utf-8", errors="replace")

            # 组装返回结果
            exit_code = result.get("StatusCode", -1)
            output_parts = []

            if stdout.strip():
                output_parts.append(stdout.strip())
            if stderr.strip():
                output_parts.append(f"[stderr] {stderr.strip()}")
            if exit_code != 0:
                output_parts.append(f"[退出码] {exit_code}")

            return "\n".join(output_parts) if output_parts else "(无输出)"

        except docker.errors.ContainerError as e:
            return f"沙盒执行错误: {e}"
        except Exception as e:
            return f"沙盒异常: {e}"
        finally:
            # 自动清理容器
            if container is not None:
                try:
                    container.remove(force=True)
                except Exception:
                    pass

    def cleanup(self):
        """释放 Docker 客户端资源"""
        self.client.close()


# ============================================================
# LangChain Tool 封装
# ============================================================

def create_sandbox_tool(sandbox: Optional[DockerSandbox] = None) -> tool:
    """创建沙盒执行工具,供 Agent 调用"""
    if sandbox is None:
        sandbox = DockerSandbox()

    @tool
    def run_in_sandbox(code: str) -> str:
        """在隔离的 Docker 沙盒中执行 Python 代码。
        沙盒环境与宿主机完全隔离,代码执行不会影响本地系统。
        入参是纯 Python 代码字符串,不要包裹在 ```python 标记中。"""
        return sandbox.execute(code)

    return run_in_sandbox


# ============================================================
# 独立运行测试
# ============================================================

if __name__ == "__main__":
    print("=" * 60)
    print("Docker 本地沙盒 - 独立测试")
    print("=" * 60)

    sb = DockerSandbox(network_disabled=True)

    # 测试1:基础输出
    print("\n--- 测试1: 基础输出 ---")
    result = sb.execute("print('Hello from Docker sandbox!')")
    print(f"结果: {result}")

    # 测试2:系统信息
    print("\n--- 测试2: 获取系统信息 ---")
    result = sb.execute("""
import sys, os
print(f'Python 版本: {sys.version}')
print(f'工作目录: {os.getcwd()}')
print(f'文件列表: {os.listdir(\\".\\")}')
""")
    print(f"结果: {result}")

    # 测试3:文件操作(沙盒内)
    print("\n--- 测试3: 沙盒内文件操作 ---")
    result = sb.execute("""
with open('sandbox_probe.txt', 'w') as f:
    f.write('hello from docker sandbox')
with open('sandbox_probe.txt', 'r') as f:
    content = f.read()
print(f'文件内容: {content}')
""")
    print(f"结果: {result}")

    # 测试4:资源限制验证
    print("\n--- 测试4: 内存限制验证(尝试分配 512MB)---")
    result = sb.execute("""
try:
    big_list = [0] * (512 * 1024 * 1024 // 8)
    print('分配成功(不应该发生)')
except MemoryError:
    print('内存不足,被沙盒限制成功拦截!')
""")
    print(f"结果: {result}")

    sb.cleanup()
    print("\n" + "=" * 60)
    print("测试完成")
    print("=" * 60)

A.2 docker_sandbox_test.py — Agent + Docker 沙盒集成测试

"""
Docker 本地沙盒 Agent 集成测试

让 Agent 在本地 Docker 沙盒中执行代码,无需任何云服务。

架构:
    用户指令 → LangGraph Agent → Docker 沙盒工具 → Docker 容器 → 返回结果
"""

from __future__ import annotations

import sys
import os
import asyncio

_project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
if _project_root not in sys.path:
    sys.path.insert(0, _project_root)

from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver
from langchain_core.runnables import RunnableConfig
from langchain_core.messages import AIMessage, ToolMessage

from app.common import llm
from app.sandbox_agent_test.local_sandbox import DockerSandbox, create_sandbox_tool


# ============================================================
# 工具调用日志(追踪 Agent 是否真的通过沙盒执行代码)
# ============================================================

_printed_indices: set[int] = set()


def print_tool_call_logs(messages):
    """从 Agent 的 messages 中提取工具调用记录"""
    print("  [沙盒工具调用日志]")
    call_count = 0

    for idx, msg in enumerate(messages):
        if idx in _printed_indices:
            continue
        _printed_indices.add(idx)

        if isinstance(msg, AIMessage) and msg.tool_calls:
            for tc in msg.tool_calls:
                call_count += 1
                args_str = str(tc["args"])
                if len(args_str) > 150:
                    args_str = args_str[:150] + "..."
                print(f"  调用[{call_count}]: {tc['name']}({args_str})")

        elif isinstance(msg, ToolMessage):
            content = str(msg.content)
            if len(content) > 300:
                content = content[:300] + "..."
            print(f"    沙盒返回: {content}")

    if call_count == 0:
        print("  (本次未调用工具,Agent 直接回复)")
    print()


# ============================================================
# 测试函数
# ============================================================

async def run_sandbox_test():
    """运行 Docker 沙盒 Agent 测试"""

    print(f"[INFO] 当前使用模型: {llm.LLM_MODEL_NAME}")
    print(f"[INFO] 当前 Base URL: {llm.LLM_BASE_URL}")

    # 创建 Docker 沙盒和工具
    sandbox = DockerSandbox(
        image="python:3.12-slim",
        mem_limit="256m",
        network_disabled=True,  # 禁用网络,完全隔离
        timeout=30,
    )
    sandbox_tool = create_sandbox_tool(sandbox)

    # 创建 Agent(带内存记忆)
    memory = MemorySaver()
    agent = create_react_agent(
        model=llm.llm,
        tools=[sandbox_tool],
        checkpointer=memory,
    )

    config = RunnableConfig(configurable={"thread_id": "docker-sandbox-001"})

    # 预定义测试任务
    tasks = [
        "请在沙盒中执行:打印当前 Python 版本和工作目录",
        "在沙盒中创建一个文件 sandbox_test.txt,写入 'hello from local docker sandbox',然后读取并打印内容",
        "在沙盒中计算 1 到 100 的累加和,并打印结果",
    ]

    print("\n" + "=" * 60)
    print("Docker 本地沙盒 Agent 测试")
    print("=" * 60)

    for i, task in enumerate(tasks, 1):
        print(f"\n--- 任务 {i}/{len(tasks)}: {task} ---")

        result = await agent.ainvoke(
            input={"messages": [("user", task)]},
            config=config,
        )

        # 打印工具调用日志
        print_tool_call_logs(result["messages"])

        # 打印 Agent 最终回复
        ai_message = result["messages"][-1].content
        print(f"[Agent 回复] {ai_message}")

    # 清理沙盒
    sandbox.cleanup()

    print("\n" + "=" * 60)
    print("测试完成")
    print("=" * 60)


if __name__ == "__main__":
    print("=" * 60)
    print("Docker 本地沙盒 Agent 集成测试")
    print("=" * 60)
    asyncio.run(run_sandbox_test())

A.3 sandbox_test.py — Daytona 云沙盒 Agent 测试(可选)

"""
Daytona 沙盒 Agent 集成测试

使用 Daytona 沙盒隔离代码执行环境,Agent 在沙盒内运行 Python 脚本,
不会影响宿主机文件系统。

核心组件:
    - Daytona: 沙盒基础设施,提供隔离的执行环境
    - DaytonaSandbox: langchain-daytona 适配器,将沙盒封装为 LangChain 工具
    - deepagents: Agent 框架,自动赋予 Agent 沙盒执行能力

依赖安装:
    pip install daytona-sdk langchain-daytona deepagents

必需环境变量:
    DAYTONA_API_KEY     Daytona API Key(在 Daytona 控制台获取)
"""

from __future__ import annotations

import sys
import os
import traceback
from typing import Any

_project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
if _project_root not in sys.path:
    sys.path.insert(0, _project_root)

from daytona import Daytona
from deepagents import create_deep_agent
from langchain_daytona import DaytonaSandbox

from app.common import llm


# ============================================================
# 沙盒任务:在隔离环境中执行 Python 脚本
# ============================================================

SANDBOX_TASK = """
请在沙盒里执行一个小型 Python 脚本,完成以下动作:
1. 打印当前 Python 版本。
2. 打印当前工作目录。
3. 在沙盒当前目录创建 sandbox_probe.txt。
4. 写入内容:hello from daytona sandbox。
5. 读取 sandbox_probe.txt 并打印读取结果。
6. 打印当前目录文件列表。
请明确说明这些操作是在沙盒内完成的。
""".strip()


# ============================================================
# 沙盒执行函数
# ============================================================

def invoke_agent(agent: Any) -> Any:
    """调用 Agent 执行沙盒任务"""
    return agent.invoke(
        {
            "messages": [
                {
                    "role": "user",
                    "content": SANDBOX_TASK,
                }
            ]
        }
    )


# ============================================================
# 主函数
# ============================================================

def main() -> int:
    sandbox = None

    try:
        # 检查 Daytona API Key
        if not os.getenv("DAYTONA_API_KEY"):
            raise RuntimeError("缺少必需环境变量:DAYTONA_API_KEY")

        print(f"[INFO] 当前使用模型: {llm.LLM_MODEL_NAME}")
        print(f"[INFO] 当前 Base URL: {llm.LLM_BASE_URL}")

        # 第一步:创建 Daytona 沙盒实例
        print("\n[1/4] 正在创建 Daytona 沙盒...")
        sandbox = Daytona().create()
        backend = DaytonaSandbox(sandbox=sandbox)

        # 第二步:创建具备沙盒能力的 Agent(使用项目统一的 LLM)
        print("[2/4] 正在创建具备沙盒能力的 Agent...")
        agent = create_deep_agent(
            model=llm.llm,
            system_prompt=(
                "你是一个拥有 Daytona 沙盒访问权限的 Python 编程助手。"
                "所有需要执行的代码都必须在沙盒中运行,不要在宿主机执行。"
                "执行完成后,返回关键命令输出和生成文件验证结果。"
            ),
            backend=backend,
        )

        # 第三步:调用 Agent 执行沙盒任务
        print("[3/4] 正在调用 Agent 执行沙盒任务...")
        result = invoke_agent(agent)

        # 第四步:输出执行结果
        print("[4/4] Agent 执行结果:")
        print(result)
        return 0

    except Exception as exc:
        # 捕获所有异常,完整输出失败原因
        print(f"沙盒 Agent 测试失败:{exc}", file=sys.stderr)
        traceback.print_exc()
        return 1

    finally:
        # 无论成功失败,都要清理沙盒资源
        if sandbox is not None:
            print("\n正在清理 Daytona 沙盒...")
            try:
                sandbox.stop()
                print("Daytona 沙盒已停止。")
            except Exception as cleanup_exc:
                print(f"停止 Daytona 沙盒失败:{cleanup_exc}", file=sys.stderr)


if __name__ == "__main__":
    print("=" * 60)
    print("Daytona 沙盒 Agent 集成测试")
    print("=" * 60)
    raise SystemExit(main())
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐