1. 从零理解多用户 Agent 的容器隔离骨架Manus 这类产品最容易被误解的地方是大家总盯着模型能力看但真正决定它能不能给一群人同时用的其实是工程侧的隔离设计。我试过把一个单用户 Agent 直接改成多用户结果第一个用户跑代码把工作目录写满第二个用户的文件就找不到了更麻烦的是两个用户的任务同时调用浏览器进程互相抢资源日志混在一起根本没法排查。所以这篇不讲玄学只讲怎么用容器把每个用户的 Agent 关进自己的小房间让它有独立的文件系统、独立的进程空间、独立的浏览器会话同时还能被统一调度。所谓“基于容器隔离的多用户 Agent 应用”说白了就是每个登录用户对应一个容器实例容器里跑着这个用户的代码执行环境和浏览器自动化环境容器外有一个调度层负责路由请求、管理生命周期、回收资源。它适合谁适合想自己搭一套 Agent as a Service 原型的后端同学适合做 AI 应用但被多租户问题卡住的团队也适合想理解 Manus 这类产品背后工程复杂度的开发者。你不需要先有万级并发先把两个用户互不干扰地跑通骨架就立住了。核心检索词我先摆出来容器隔离、多用户 Agent、Docker 编排、会话边界、并发调度。这几个词会贯穿全文。整个系统我拆成四层镜像层负责预置运行环境容器管理层负责按用户 ID 创建和销毁容器路由层负责把请求分发到对应用户的容器调度层负责控制并发和超时。下面按可复制的顺序一步步来每一步都有配置和验证动作。先明确一个原则Agent 的推理循环可以放在容器外但代码执行和浏览器操作必须放进容器内。为什么因为用户让 AI 生成的代码是不可信的你不可能让它在宿主机上随便跑。浏览器同理自动化浏览会产生大量临时文件、Cookie、缓存放在宿主机上很快就会污染环境。把这两类操作关进容器既保护了宿主机也让每个用户的工作区天然隔离。这个边界划清楚后面的配置才有意义。2. TaoToken 统一 Key 通道的前置准备多用户 Agent 绕不开模型调用。每个用户的任务都要调 LLM如果你让每个用户自己填 Key管理成本高不说还容易泄露。更合理的做法是后端统一持有一个 API 通道所有用户的请求都经过这个通道出去按用户 ID 做计量和限流。TaoToken 在这里的角色就是统一 Key 和 API 通道你只需要在服务端配置一次容器内的 Agent 通过环境变量拿到 Base URL 和 Key就能调模型不用在每个容器里单独配。先拿 Key。打开 https://taotoken.net/api-keys 登录后创建一个 API Key复制出来。这个 Key 只放在后端服务的环境变量里不要写进镜像也不要传给前端。Base URL 用 https://taotoken.net/api 注意这个地址不带任何查询参数直接作为 OpenAI 兼容接口的 base_url 使用。模型 ID 根据你实际要用的模型填比如 claude-sonnet-4-20250514 这类具体以控制台模型列表为准。如果你用的是 Claude Code 这类编码 AgentTaoToken 也提供了对应的接入方式。Claude Code 的配置入口在 https://taotoken.net/claude-code 里面会告诉你 Anthropic 兼容的 Base URL 和 Key 怎么填。Coding Plan 适合长期跑编码任务的场景入口在 https://taotoken.net/coding-plan 如果你打算让 Agent 持续做代码生成和执行可以走这个通道。模型对话调试入口在 https://taotoken.net/chat 用来快速验证 Key 是否可用。接入文档在 https://taotoken.net/doc 里面有完整的参数说明。前置准备的核心是三件套Base URL、API Key、Model ID。这三样在后面的容器配置里会以环境变量形式注入。我建议你在后端服务里做一个统一的模型客户端封装所有容器内的 Agent 都通过这个客户端调模型而不是各自直连。这样做的好处是Key 只存在于后端容器里只有临时凭证计量和限流可以在客户端层做模型切换只需要改一处配置。下面给一个最小化的环境变量配置示例你可以直接复制到后端的 .env 文件里。# 后端服务环境变量不要提交到代码仓库 TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_API_KEYsk-你的实际Key TAOTOKEN_MODEL_IDclaude-sonnet-4-20250514容器启动时这些变量通过 docker run 的 -e 参数注入或者通过 docker-compose 的 environment 字段传入。注意不要把 Key 写进 Dockerfile也不要在镜像构建阶段传入否则镜像层里会留下痕迹。正确的做法是运行时注入。如果你用 Docker Compose可以这样写services: agent-worker: image: agent-sandbox:latest environment: - TAOTOKEN_BASE_URL${TAOTOKEN_BASE_URL} - TAOTOKEN_API_KEY${TAOTOKEN_API_KEY} - TAOTOKEN_MODEL_ID${TAOTOKEN_MODEL_ID} - USER_ID${USER_ID} volumes: - ./workspace/${USER_ID}:/workspace这样每个用户容器启动时都会拿到自己的 USER_ID 和统一模型通道配置。工作区挂载到宿主机的 ./workspace/${USER_ID} 目录容器销毁后文件还在方便排查和恢复。注意 volumes 的路径要用用户 ID 区分否则多个容器会写同一个目录隔离就失效了。3. 可复制的容器配置与多用户路由示例这一节是全文最核心的部分直接给可复制的配置。先看 Dockerfile它决定了每个用户容器里有什么。基础镜像用 python:3.11-slim然后装代码执行需要的库和浏览器自动化需要的依赖。关键点是playwright 的浏览器要装进镜像不要运行时再装否则每个用户第一次任务都要等几分钟。start.sh 负责启动虚拟显示让 Chromium 在无头容器里能跑起来。FROM python:3.11-slim # 基础工具 RUN apt-get update apt-get install -y \ curl wget git xvfb \ rm -rf /var/lib/apt/lists/* # Python 依赖 COPY requirements.txt /tmp/requirements.txt RUN pip install --no-cache-dir -r /tmp/requirements.txt # 安装 playwright 浏览器 RUN pip install playwright playwright install chromium # 工作目录 WORKDIR /workspace # 启动脚本 COPY start.sh /start.sh RUN chmod x /start.sh ENTRYPOINT [/start.sh] CMD [sleep, infinity]start.sh 的内容很简单启动 Xvfb 后执行传入的命令#!/bin/bash Xvfb :99 -screen 0 1024x768x16 sleep 1 export DISPLAY:99 exec $requirements.txt 里放你需要的库比如 pandas、requests、beautifulsoup4、playwright 等。构建命令docker build -t agent-sandbox:latest .构建完成后用 docker images 确认镜像存在。接下来是容器管理层的代码。核心逻辑是用字典保存 user_id 到容器实例的映射每个用户只创建一个容器重复请求返回同一个实例。下面是一个可运行的 Python 封装import os import docker from typing import Dict, Optional _client docker.from_env() _containers: Dict[str, any] {} BASE_WORK_DIR os.path.abspath(./workspace) def get_container(user_id: str, image: str agent-sandbox:latest): if user_id in _containers: return _containers[user_id] work_dir os.path.join(BASE_WORK_DIR, user_id) os.makedirs(work_dir, exist_okTrue) container _client.containers.run( imageimage, namefagent-{user_id}, detachTrue, environment{ USER_ID: user_id, TAOTOKEN_BASE_URL: os.environ[TAOTOKEN_BASE_URL], TAOTOKEN_API_KEY: os.environ[TAOTOKEN_API_KEY], TAOTOKEN_MODEL_ID: os.environ[TAOTOKEN_MODEL_ID], }, volumes{work_dir: {bind: /workspace, mode: rw}}, mem_limit2g, cpu_period100000, cpu_quota50000, network_disabledFalse, ) _containers[user_id] container return container def close_container(user_id: str): container _containers.pop(user_id, None) if container: container.stop(timeout5) container.remove(forceTrue)这段代码里几个参数值得说明。mem_limit2g 限制每个容器最多用 2G 内存防止某个用户的任务把宿主机吃满。cpu_quota50000 配合 cpu_period100000表示每个容器最多用 0.5 个 CPU 核多用户并发时不会互相抢。volumes 把用户工作区挂进去容器内路径固定为 /workspace。network_disabledFalse 是因为浏览器任务需要联网如果你只跑代码执行可以设为 True 更安全。路由层用一个简单的 FastAPI 示例根据请求里的 user_id 分发到对应容器from fastapi import FastAPI, HTTPException from pydantic import BaseModel from container_manager import get_container, close_container app FastAPI() class TaskRequest(BaseModel): user_id: str code: str app.post(/execute) def execute(req: TaskRequest): if not req.user_id: raise HTTPException(status_code400, detailuser_id required) container get_container(req.user_id) result container.exec_run( cmd[python, -c, req.code], workdir/workspace, ) return { user_id: req.user_id, exit_code: result.exit_code, output: result.output.decode(utf-8, errorsreplace), } app.delete(/container/{user_id}) def destroy(user_id: str): close_container(user_id) return {status: closed, user_id: user_id}这个路由示例虽然简单但已经具备多用户隔离的核心每个 user_id 对应独立容器执行代码在容器内工作区挂载到独立目录。并发调度方面FastAPI 默认的线程池可以处理一定并发但如果用户量大建议在容器管理层加一个信号量控制同时运行的容器数量比如最多 50 个超出的排队。信号量用 threading.Semaphore 就能实现这里不展开。4. 验证请求与隔离效果实测配置写完了必须验证。验证分三步先验证单个容器能跑代码再验证两个用户互不干扰最后验证模型调用通道通。第一步启动服务后发一个请求curl -X POST http://localhost:8000/execute \ -H Content-Type: application/json \ -d {user_id:alice,code:import os; print(os.getcwd()); open(\hello.txt\,\w\).write(\alice\)}预期返回 exit_code 为 0output 里打印 /workspace。然后检查宿主机的 ./workspace/alice/hello.txt 是否存在内容是否为 alice。这一步验证了容器内工作区挂载正确。第二步用 bob 发同样的请求但写入不同内容curl -X POST http://localhost:8000/execute \ -H Content-Type: application/json \ -d {user_id:bob,code:import os; print(os.getcwd()); open(\hello.txt\,\w\).write(\bob\)}然后分别查看 ./workspace/alice/hello.txt 和 ./workspace/bob/hello.txt内容应该分别是 alice 和 bob。如果两个文件内容一样说明挂载路径没按 user_id 区分隔离失败。这一步是最关键的隔离验证一定要做。第三步验证模型调用。在容器内执行一段调用 TaoToken 的代码import os from openai import OpenAI client OpenAI( base_urlos.environ[TAOTOKEN_BASE_URL], api_keyos.environ[TAOTOKEN_API_KEY], ) resp client.chat.completions.create( modelos.environ[TAOTOKEN_MODEL_ID], messages[{role: user, content: 只回复两个字通了}], ) print(resp.choices[0].message.content)把这段代码通过 /execute 接口发给 alice预期输出“通了”。如果报 401检查 Key 是否正确注入如果报连接错误检查容器网络是否正常。这一步通了说明统一 Key 通道在容器内可用。第四步验证资源边界。同时发 10 个请求给同一个用户观察容器内存和 CPU 是否被限制在配置范围内。用 docker stats agent-alice 可以看到实时资源占用。如果内存超过 2G 被 OOM kill说明限制生效。这一步验证了资源隔离防止单用户拖垮整个服务。实测下来这套骨架在 4 核 8G 的机器上可以稳定跑 5 到 8 个用户容器每个容器 0.5 核 2G。如果用户更多要么加机器要么把容器做成按需启动、空闲回收。回收策略可以简单点任务完成后延迟 5 分钟销毁容器下次请求再重建。这样既省资源又不影响体验。5. 常见报错与排查对照多用户 Agent 跑起来后报错集中在几个地方。下面按真实报错对照排查。第一个报错401 Unauthorized或invalid api key。这通常是 TaoToken 的 Key 没注入到容器或者注入的变量名和代码里读的不一致。排查动作进入容器执行 env | grep TAOTOKEN确认三个变量都在。如果不在检查 docker run 的 -e 参数或 compose 的 environment 字段。如果变量在但还报 401检查 Key 是否有多余空格或者是否在控制台被禁用。模型对话入口 https://taotoken.net/chat 可以快速验证 Key 本身是否有效。第二个报错local proxy failed或connection refused。这通常是容器网络问题。排查动作在容器内执行 curl -I https://taotoken.net/api 看是否能通。如果不通检查宿主机的网络和 DNS 配置。注意不要用任何网络代理工具直接走正常网络即可。如果容器内 curl 不通但宿主机通检查 docker 的 network 模式默认 bridge 模式一般没问题。第三个报错reading choices相关错误比如KeyError: choices或list index out of range。这通常是模型返回结构不符合预期可能是模型 ID 填错或者请求参数不对。排查动作打印完整的 resp 对象看返回内容。如果返回的是错误信息而不是 choices根据错误信息调整。常见原因是 model 参数填了一个不存在的模型 ID或者 messages 格式不对。第四个报错OAuth相关错误比如OAuth token expired。如果你用的是 Claude Code 或类似需要 OAuth 的客户端检查配置是否过期。Claude Code 的接入配置在 https://taotoken.net/claude-code 按文档重新生成凭证即可。注意 OAuth 凭证和 API Key 是两套东西不要混用。第五个报错容器启动失败报port already in use或name already in use。这是因为同名容器已存在。排查动作docker ps -a | grep agent- 找到残留容器后 docker rm -f 删掉。代码层面可以在创建容器前先检查同名容器是否存在存在则先删除。或者用随机后缀命名避免冲突。第六个报错代码执行超时容器无响应。这通常是用户代码里有死循环或长时间阻塞操作。排查动作给 exec_run 加 timeout 参数超时后强制停止容器。同时给容器加 cpu_quota 限制防止单个任务占满 CPU。如果任务确实需要长时间运行改成异步任务队列不要让 HTTP 请求一直等着。第七个报错工作区文件权限错误报Permission denied。这是因为容器内进程的 UID 和宿主机挂载目录的 UID 不一致。排查动作在 Dockerfile 里创建一个固定 UID 的用户或者启动容器时用 --user 参数指定 UID。简单做法是给工作区目录 chmod 777但不推荐用于生产。更好的做法是容器内用非 root 用户运行宿主机目录属主和容器内用户 UID 对齐。6. 从骨架到可扩展服务的下一步骨架跑通后下一步是让它更接近生产可用。几个方向第一把容器生命周期管理从内存字典改成 Redis 或数据库这样服务重启后还能恢复用户和容器的映射关系。第二加一个任务队列比如 Celery 或 RQ把耗时的 Agent 任务异步化HTTP 接口只负责提交和查询状态。第三加计量和限流按 user_id 统计模型调用次数和 token 消耗超出配额就拒绝。第四加日志聚合每个容器的日志打到统一的地方方便排查。模型通道方面TaoToken 的统一 Key 方案可以继续用。如果你要做长期编码 AgentCoding Plan 入口在 https://taotoken.net/coding-plan 适合持续跑代码生成和执行任务。接入文档在 https://taotoken.net/doc 里面有完整的参数和错误码说明。API Key 管理在 https://taotoken.net/api-keys 可以创建多个 Key 做轮换。模型对话调试在 https://taotoken.net/chat 用来快速验证模型可用性。最后给一个实用技巧在容器启动时注入一个健康检查脚本定期检查模型通道是否可用。如果连续失败自动重建容器。这样即使某个容器的网络出问题也能自愈。健康检查脚本可以放在 start.sh 里后台运行不影响主进程。这套骨架加上这些增强基本就能支撑一个小规模的多用户 Agent 服务了。