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 这类编码 Agent,TaoToken 也提供了对应的接入方式。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_URL=https://taotoken.net/api TAOTOKEN_API_KEY=sk-你的实际Key TAOTOKEN_MODEL_ID=claude-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_ok=True) container = _client.containers.run( image=image, name=f"agent-{user_id}", detach=True, 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_limit="2g", cpu_period=100000, cpu_quota=50000, network_disabled=False, ) _containers[user_id] = container return container def close_container(user_id: str): container = _containers.pop(user_id, None) if container: container.stop(timeout=5) container.remove(force=True)这段代码里几个参数值得说明。mem_limit="2g" 限制每个容器最多用 2G 内存,防止某个用户的任务把宿主机吃满。cpu_quota=50000 配合 cpu_period=100000,表示每个容器最多用 0.5 个 CPU 核,多用户并发时不会互相抢。volumes 把用户工作区挂进去,容器内路径固定为 /workspace。network_disabled=False 是因为浏览器任务需要联网,如果你只跑代码执行,可以设为 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_code=400, detail="user_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", errors="replace"), } @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 为 0,output 里打印 /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_url=os.environ["TAOTOKEN_BASE_URL"], api_key=os.environ["TAOTOKEN_API_KEY"], ) resp = client.chat.completions.create( model=os.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 方案可以继续用。如果你要做长期编码 Agent,Coding 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 服务了。