用 Docker 本地托管 Claude Agent SDK 研究型 Agent:临时单次运行与持久会话的两种落地模式
本文介绍 claude-cookbooks 仓库中 hosting/docker 的 Tier 1 本地 Docker 托管方案:它复用研究 Agent 的共享镜像,通过 docker run 实现「一个提示、一个进程、跑完即退」的临时(Ephemeral)模式,或通过 docker compose 拉起 FastAPI + SSE 服务,把会话目录挂载到宿主机,让多轮对话在容器重建后依然可续。读完你将掌握基于 Claude Agent SDK 打包 Agent 镜像、用环境变量注入 API Key 与提示词、以及利用 CLAUDE_CONFIG_DIR + resume= 机制做会话持久化的完整实操方法。
背景:这个 Docker 方案在整个托管体系中的位置
这份 README 是 hosting 目录 中「三层托管」的第一层。整体设计围绕 00_The_one_liner_research_agent.ipynb 里构建的研究 Agent 展开:WebSearch 搜集信息、按要求输出带来源引用的研究结论。三层托管(本地 Docker / Modal / Kubernetes)共享同一份 Agent 代码、同一个容器镜像、同一个 HTTP 接口契约,只是容器外围的运维机制不同,Docker 正是其中最轻、最适合本机验证与批处理的一层。
镜像的构建上下文是 claude_agent_sdk/(hosting 的父目录)而非 hosting/,因为 Agent 会 import 与 hosting/ 平级的 research_agent/ 与 utils/ 模块。所有部署命令都假定你先进入该目录:
cd claude_agent_sdk/
准备工作:构建共享镜像与准备密钥
在尝试两种运行模式之前,先用 hosting/Dockerfile 构建一次镜像:
cd claude_agent_sdk/
docker build -f hosting/Dockerfile -t research-agent .
该镜像值得拆开看几个关键点,它们是后面两种模式成立的前提:
- 运行时缺一不可的依赖:基于
python:3.11-slim;由于 Agent SDK 底层会把任务交给 Claude Code CLI 执行,因此镜像内先安装 Node 20 再npm install -g @anthropic-ai/claude-code@2.1.140(版本被硬锁定在 requirements.txt 注释所说的 pin 集合内,升级需连同claude-agent-sdk==0.1.50一起刻意为之)。 - Python 依赖固定版本:
claude-agent-sdk、fastapi、uvicorn[standard]、sse-starlette、python-dotenv全部硬 pin,避免未来版本悄然破坏 notebook 的端到端流程。 - 工作目录被钉死:
WORKDIR /app。原因正如 Dockerfile 注释所述:SDK 生成的会话记录存放在$CLAUDE_CONFIG_DIR/projects/<编码后的cwd>/下,只有 cwd 稳定,resume=才能在容器重启后找回旧会话。 - 会话存储重定向:
ENV CLAUDE_CONFIG_DIR=/data。镜像内不声明VOLUME,而是由每一层托管显式地往/data挂载持久化存储(compose 的 bind mount、Modal Volume、k8s PVC),避免docker run时泄漏匿名卷。
镜像的构建还依赖同目录下的 Dockerfile.dockerignore,它排除了 .env、notebook、会话目录以及 hosting/docker|modal|kubernetes 各层专属文件,让镜像只包含 Agent、utils 与共享服务代码。该忽略文件采用了带前缀的名字,需要 BuildKit(Docker 23.0+ 与 Docker Desktop 的默认 builder);老引擎需 DOCKER_BUILDKIT=1。
镜像入口统一为 entrypoint.sh,它按第一个参数分发到两种模式,正好对应 README 的两条路径:
# 默认:临时模式——用 $PROMPT 跑一次 Agent 后退出
exec python -m hosting.run_once
# 传入 serve:启动 FastAPI 服务
exec uvicorn hosting.server:app --host 0.0.0.0 --port 8000
临时模式:一个 Prompt、一个进程、跑完即退
README 把临时模式定义为 "One prompt, one process, then exit",适合没有对话需要恢复的任务型(job-shaped)工作,例如批处理、一次性分析。
cd claude_agent_sdk/
docker build -f hosting/Dockerfile -t research-agent .
docker run --rm \
-e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
-e PROMPT="What is the Claude Agent SDK?" \
research-agent
命令要点逐条说明:
--rm:容器在进程退出后自动清理,契合"跑完即退"的无状态语义;-e ANTHROPIC_API_KEY:接口契约要求的最小环境变量集合之一;-e PROMPT:本次运行要交给 Agent 的提示词,与下面的entrypoint.sh分发逻辑联动。
执行链路为:entrypoint 未收到 serve → python -m hosting.run_once → hosting/run_once.py 读取 $PROMPT,缺失时向 stderr 打印错误并返回退出码 2,随后调用 research_agent.agent.send_query(prompt, model=..., display_result=False) 并把最终结果打印到 stdout、返回 0。
与常驻服务相比,run_once.py 有两处值得注意的取舍:
- 默认模型:
DEFAULT_MODEL = "claude-sonnet-4-6",与 server.py 一致,"让测试跑得更便宜";可用-e MODEL=claude-opus-4-6覆盖,以完全对齐 research_agent/agent.py 中 notebook 00 的默认配置(其默认是claude-opus-4-6)。 - 保留完整工具集:run_once.py 沿用了 agent.py 中
allowed_tools=["WebSearch", "Read"]的默认工具。这是安全的——调用方掌握$PROMPT,容器里也没有其他会话的状态,恶意网页结果没有可让Read偷读的敏感目标;而常驻服务恰恰因此主动移除了 Read(见下文服务模式的说明)。
混合模式:docker compose 拉起 FastAPI 服务并持久化会话
临时模式跑完即退,无法多轮对话。README 的第二条路径是混合模式(Hybrid):启动 FastAPI 服务,并把宿主机的 ./sessions 挂载到容器 /data,让对话在容器重启后依然存活。
cd claude_agent_sdk/hosting/docker/
docker compose up --build
这条命令背后是 docker-compose.yml,其设计比表面看起来更讲究,逐项拆解:
services:
research-agent:
build:
# 构建上下文是 claude_agent_sdk/,这样 research_agent/ 与 utils/ 都在范围内
context: ../..
dockerfile: hosting/Dockerfile
image: research-agent
command: ["serve"] # 覆盖 entrypoint 参数,进入服务模式
env_file:
- ../.env # 从 hosting/.env 读取 ANTHROPIC_API_KEY 等
ports:
# 仅回环地址。服务默认无鉴权,默认不暴露到局域网——
# `curl localhost:8000` 仍然可用。生产部署必须在前方加带鉴权的反向代理。
- "127.0.0.1:8000:8000"
volumes:
# 让会话记录跨容器重启持久化
- ./sessions:/data
配置里隐藏着三个关键工程决策:
- 端口只绑回环:
127.0.0.1:8000:8000意味着服务只在本机可达。原因是 hosting/server.py 明文警告:这个服务默认没有任何鉴权,它信任一切能摸到 8000 端口的人。因此 README 明确建议生产环境必须在前面加一个能做「调用者鉴权 + 只转发属于该调用者的 session_id」的网关/反向代理,绝不要把 8000 直接暴露到公网。 .env注入密钥:compose 从../.env(即hosting/.env)读取环境变量。仓库在 hosting/.env.example 中给出了模板——把ANTHROPIC_API_KEY填成你的真实密钥并另存为.env(.env已被 gitignore,绝不能把真实密钥提交进仓库);可选MODEL覆盖默认模型。- 会话持久化挂载:
./sessions:/data把宿主机上hosting/docker/sessions/目录映射到容器内CLAUDE_CONFIG_DIR指向的/data。会话记录(包括 SDK 内部会话 ID 映射)都落在这里,容器删了、重建了,记录还在。
服务模式下的 Agent 配置与 notebook 00 略有不同,体现为 server.py 中 _build_options() 的几点收紧:
- 系统提示复用:直接 import
research_agent.agent的RESEARCH_SYSTEM_PROMPT,保证「部署的就是 notebook 00 那个 Agent」; - 工具裁剪为仅
WebSearch:常驻服务没有上传入口,Read唯一能读到的是容器内部——其他会话在/data的对话记录、/proc/self/environ里的 API Key。被注入的恶意网页结果可能诱导 Agent 用Read把这些敏感内容外带,所以托管形态下研究 Agent = 只有 WebSearch; - 缓冲与请求体上限:
MAX_BUFFER_SIZE = 10 * 1024 * 1024(与 notebook 一致),MAX_BODY_BYTES = 256 * 1024在请求体到达 JSON 解析器之前拦截超大请求(仅对带Content-Length的请求生效,生产应在网关处再加真上限); - 会话 ID 白名单校验:
session_id必须匹配^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$。这并非装饰性约束——该 ID 最终会进入持久化映射并参与文件系统查找,非法字符会导致路径穿越,因此非法 ID 直接返回 400。
从另一个 shell 发起多轮对话
服务起来后,README 建议从另一个终端验证。发送第一轮:
curl -N -X POST http://localhost:8000/sessions/demo-1/messages \
-H 'Content-Type: application/json' \
-d '{"prompt":"What are the latest AI agent trends?"}'
紧跟一轮追问——Agent 记得第一轮的内容:
# Follow-up — the agent remembers the first turn:
curl -N -X POST http://localhost:8000/sessions/demo-1/messages \
-H 'Content-Type: application/json' \
-d '{"prompt":"Tell me more about the second one."}'
健康检查(刻意不设鉴权,供编排器做存活探测):
curl http://localhost:8000/health
接口行为符合 hosting/README.md 定义的接口契约:
GET /health→200 {"status": "ok"};POST /sessions/{session_id}/messages请求体为{"prompt": "..."},响应是text/event-stream:event: message携带序列化后的 SDK 消息(SystemMessage | AssistantMessage | ResultMessage,type字段标注了消息类名),event: done表示本轮结束,event: error携带{"message": "..."};- 会话已存在则续接,不存在则新建;流中只含本轮新产生的消息,不含历史;
- 必填环境变量
ANTHROPIC_API_KEY;可选MODEL(默认claude-sonnet-4-6)、CLAUDE_CONFIG_DIR(默认/data)、AGENT_AUTH_TOKEN。
重启后上下文为何还在:resume 机制的底层原理
README 用一句话描述了验收标准:"Stop the container, docker compose up again, send another follow-up — the agent still has context because ./sessions persisted /data。" 这背后是 server.py 点明的一个 SDK 设计事实:SDK 会自行生成会话 ID,调用方无法指定。
所以服务端维护了一张小的持久化映射(落在 CLAUDE_CONFIG_DIR/hosting_session_map.json,即挂载卷内的 /data/hosting_session_map.json),把调用方 URL 里的 session_id(如 demo-1)映射到 SDK 内部生成的 ID:
- 首轮:外部 ID 尚无映射,
_build_options(resume=None)开启新会话;当消息流中出现带session_id的ResultMessage时,_remember()学习这个内部 ID 并原子写入映射(先写.tmp再replace); - 后续轮:从映射取出 SDK 内部 ID,传给
ClaudeAgentOptions(resume=sdk_session_id),配合钉死的cwd="/app"与CLAUDE_CONFIG_DIR=/data,SDK 便能定位到同一次会话的历史记录。
映射文件与会话记录放在同一目录,因此它与会话一起跨重启持久化。README 与源码也坦诚指出了两个边界:其一,同一外部 ID 若并发发起两个首次请求,会各自开启全新 SDK 会话并发生「后写覆盖」,对 cookbook 这种"每会话单调用方"的形态没问题,生产服务应把「读-建-写」整段加锁(Kubernetes 那一层就是用 Redis SET NX 规避的);其二,服务端不做生命周期管理,空闲容器由编排器负责回收。
安全提示:无鉴权服务的正确打开方式
混合模式虽方便,但必须牢记 hosting/README.md 的红色警告:服务默认无鉴权。本地验证时 compose 已把端口绑到 127.0.0.1 回环地址,这本身是一道防线;若要跨机器暴露,二选一:
- 前面架设能鉴权调用者且只转发属于该调用者的 session_id 的网关/反向代理(三层托管的 Kubernetes 网关即按此契约路由);
- 在没有网关的场景下(例如 Modal 分发出公网隧道时),设置
AGENT_AUTH_TOKEN,服务端随即要求/sessions/*携带Authorization: Bearer <token>,且用secrets.compare_digest做常数时间比较避免时序侧信道;/health保持开放。源码注释强调这只是网关的最小替身而非替代品,因为它并不把 session_id 隔离到调用者。
小结:两种模式如何选
| 维度 | 临时模式(Ephemeral) | 混合模式(Hybrid) |
|---|---|---|
| 启动命令 | docker run --rm -e PROMPT=... research-agent |
docker compose up --build |
| 进程形态 | 一个进程,跑完即退 | FastAPI + SSE 常驻服务 |
| 会话能力 | 无(天然无状态) | 多轮续接,重启后上下文仍在 |
| 典型场景 | 批处理、一次性分析 | 需要与人多轮交互的服务形态 |
| 数据持久化 | 不适用 | ./sessions:/data bind mount |
| 安全面 | 调用方自己掌控 $PROMPT |
默认无鉴权,端口绑回环 / 需网关或 AGENT_AUTH_TOKEN |
选型逻辑一句话即可概括:没有需要恢复的对话就用 docker run 的临时模式,把容器当"一次性的函数"调用;需要对话在容器重启间存活、或要对外提供接口,就切到 docker compose 的混合模式,并把 /data 的持久化挂在显眼的位置。想继续往规模化演进,可以接着阅读同一托管体系中基于 Modal 的 Tier 2 与基于 Kubernetes 的 Tier 3,两者复用本层的同一镜像与同一接口契约,只是把会话持久化从本机 bind mount 换成了 Volume 与 PVC。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00