导语:大家好,欢迎来到我们第二周的最后一讲。在过去的几天里,我们成功地从零到一构建了一个强大的多智能体研究系统——DeepResearch。它可以在我们的本地机器上出色地完成任务。但是,如何将这个强大的 AI 应用交付给最终用户?如何确保它在任何环境下都能以相同的方式、可靠地运行?答案就是容器化。在本章中,我们将学习如何使用 Docker,将我们的 DeepResearch 应用及其所有依赖项,打包成一个独立的、可移植的、生产就绪的容器。这不仅是 MLOps(机器学习运维)的关键一步,也是衡量一个 AI 应用是否真正“可用”的重要标准。
目录
- 从“我的电脑能跑”到“到处都能跑”:为什么需要容器化?
- “依赖地狱”:Python 环境的脆弱性
- Docker 的核心价值:环境隔离、一致性、可移植性
- 部署流程概览:代码 -> Dockerfile -> 镜像 (Image) -> 容器 (Container)
- 第一步:暴露 API 端点
- 让 Agent 可被调用:为什么需要一个 API Server?
- 使用 FastAPI 封装 LangGraph 应用
- 代码实战:创建一个
main.py,提供/invoke和/stream端点
- 第二步:准备 Dockerfile
requirements.txt:固定我们的 Python 依赖- 编写
Dockerfile:一步步解读指令FROM python:3.11-slim: 选择一个轻量级的基础镜像WORKDIR /app: 设置工作目录COPY . .: 将项目文件复制到容器中RUN pip install --no-cache-dir -r requirements.txt: 安装依赖ENV: 设置环境变量(如 API Keys)EXPOSE 8000: 声明容器将监听的端口CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]: 定义容器启动命令
- 第三步:构建与运行 Docker 容器
- 构建 Docker 镜像:
docker build -t deep-research-app . - 传递环境变量/Secrets:使用
--env-file或-e标志 - 运行 Docker 容器:
docker run -p 8000:8000 ... deep-research-app
- 构建 Docker 镜像:
- 第四步:测试容器化的 DeepResearch 服务
- 使用
curl或 Pythonrequests库调用容器内的 API - 发起一个研究任务,验证一切是否正常工作
- 查看容器日志:
docker logs <container_id>
- 使用
- 生产环境部署的考量
- 镜像优化:多阶段构建(Multi-stage builds)以减小镜像体积
- 数据库持久化:将
SqliteSaver的数据库文件挂载到宿主机卷(Volume)上,防止容器删除后数据丢失 - 编排与扩展:使用 Docker Compose 或 Kubernetes (K8s) 管理多个容器,实现高可用和水平扩展
- 总结:迈向 AI 应用的工业化生产
1. 从“我的电脑能跑”到“到处都能跑”:为什么需要容器化?
“在我的电脑上是好的啊!”("It works on my machine!")——这句程序员间的经典“名言”,道出了软件开发中最常见的痛点之一。一个应用能在你的开发机上运行,不代表它能在你同事的电脑、测试服务器或最终的生产环境云服务器上顺利运行。
“依赖地狱”:Python 环境的脆弱性
Python 应用尤其容易陷入“依赖地狱”:
- 你的电脑上装的是
langchain==0.1.0,而服务器上是0.2.0,导致 API 不兼容。 - 你的操作系统是 Windows,而服务器是 Linux,导致某些库(如
psycopg2)的编译方式不同。 - 你的 Python 版本是 3.11,而同事的是 3.9,导致某些语法不兼容。
Docker 的核心价值
Docker 通过容器化技术,完美地解决了这个问题。你可以把 Docker 容器想象成一个“集装箱”,这个集装箱里包含了:
- 你的应用程序代码(
deep_research.py)。 - 你的应用所需的所有 Python 依赖(
langchain,langgraph,fastapi等)。 - 应用运行所需的环境(如特定版本的 Python 解释器)。
- 所有配置信息(如环境变量)。
这个“集装箱”是完全自给自足和与世隔绝的。无论你把它运到哪里——另一台笔记本电脑、一台裸金属服务器,还是 AWS、Azure 等云平台——它都能以完全相同的方式打开和运行,因为运行它所需的一切都已经在箱子里面了。
部署流程概览
我们的目标是将 DeepResearch 应用打包成这样一个“集装箱”。流程如下:
- 编写
Dockerfile:一份“装箱清单”,告诉 Docker 如何一步步构建这个集装箱。 - 构建镜像 (Image):执行
Dockerfile,生成一个只读的“集装箱模板”,我们称之为镜像。 - 运行容器 (Container):基于这个镜像,启动一个或多个可读写的“集装箱实例”,也就是正在运行的容器。我们的应用就在这个容器里跑着。
2. 第一步:暴露 API 端点
我们的 deep_research.py 目前是一个命令行脚本,直接运行。要让它成为一个可供外部调用的“服务”,我们需要把它包装在一个 API 服务器里。FastAPI 是完成这项任务的最佳选择之一。
代码实战:创建一个 main.py
我们将创建一个 main.py 文件,它负责导入我们编译好的 LangGraph app,并使用 FastAPI 将其暴露为 HTTP API 端点。
# main.py
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Dict, Any
import uuid
# 假设我们的 DeepResearch 应用已经在一个名为 deep_research 的模块中
# 并且编译好的 app 已经准备好
from deep_research import app
# 创建 FastAPI 应用实例
fastapi_app = FastAPI(
title="DeepResearch API",
description="API for the DeepResearch multi-agent research assistant.",
version="1.0.0",
)
# 定义请求体的数据模型
class InvokeRequest(BaseModel):
task: str
thread_id: str | None = None
@fastapi_app.post("/invoke")
async def invoke_agent(request: InvokeRequest) -> Dict[str, Any]:
"""
同步调用 DeepResearch Agent,直到获得最终结果。
"""
thread_id = request.thread_id or str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}
inputs = {"task": request.task, "messages": []}
# 使用 ainvoke 进行异步调用
final_result = None
async for output in app.astream(inputs, config):
for key, value in output.items():
if key == "aggregator":
final_result = value['messages'][-1].content
return {"result": final_report, "thread_id": thread_id}
# 你也可以添加一个 /stream 端点来支持流式响应
# ...
现在,我们的 Agent 不再是只能从命令行启动的脚本,而是一个可以通过 POST /invoke 被调用的网络服务了。
3. 第二步:准备 Dockerfile
requirements.txt:固定我们的 Python 依赖
在项目根目录下创建一个 requirements.txt 文件。这是保证环境一致性的关键。
pip freeze > requirements.txt
打开这个文件,确保里面包含了我们所有需要的核心库,并固定版本号。一个精简版的 requirements.txt 可能长这样:
# requirements.txt
fastapi==0.111.0
uvicorn==0.29.0
langchain==0.2.0
langgraph==0.0.52
langchain-openai==0.1.7
tavily-python==0.3.3
sqlalchemy==2.0.30
aiosqlite==0.20.0
psycopg2-binary==2.9.9
tiktoken==0.7.0
# ... 其他依赖
编写 Dockerfile
在项目根目录下创建 Dockerfile 文件(没有后缀名)。
# Dockerfile
# 1. 选择一个官方的、轻量级的 Python 基础镜像
FROM python:3.11-slim
# 2. 设置容器内的工作目录
WORKDIR /app
# 3. 将项目文件复制到容器中
# 第一个 '.' 代表当前宿主机目录下的所有文件
# 第二个 '.' 代表容器内的当前工作目录 (/app)
COPY . .
# 4. 安装 Python 依赖
# --no-cache-dir: 不保留缓存,减小镜像体积
# -r requirements.txt: 从文件安装
RUN pip install --no-cache-dir -r requirements.txt
# 5. 设置环境变量 (重要!)
# 直接写在 Dockerfile 中是不安全的,这里仅为演示
# 生产环境中应使用其他方式传入
# ENV OPENAI_API_KEY="your_openai_key"
# ENV TAVILY_API_KEY="your_tavily_key"
# 6. 声明容器对外暴露的端口
EXPOSE 8000
# 7. 定义容器启动时要执行的命令
# 启动 uvicorn 服务器,监听所有网络接口 (0.0.0.0)
CMD ["uvicorn", "main:fastapi_app", "--host", "0.0.0.0", "--port", "8000"]
4. 第三步:构建与运行 Docker 容器
确保你的电脑上已经安装了 Docker Desktop。
构建 Docker 镜像
在项目根目录下打开终端,运行 docker build 命令:
docker build -t deep-research-app:latest .
docker build: 构建命令。-t deep-research-app:latest: 为我们构建的镜像打上一个标签(tag),格式是name:tag。这方便我们后续引用。.: Dockerfile 所在的路径(上下文路径),这里是当前目录。
Docker 会逐行执行 Dockerfile 中的指令,下载基础镜像、复制文件、安装依赖...最终,你就有了一个名为 deep-research-app 的本地镜像。
传递环境变量/Secrets
直接把 API Key 写在 Dockerfile 里是极其不安全的,因为任何能获取到镜像的人都能看到它。正确的方式是在运行容器时传入。
创建一个名为 .env 的文件(并确保已将其加入 .gitignore):
# .env
OPENAI_API_KEY=sk-your-deepseek-or-openai-key
TAVILY_API_KEY=tvly-your-tavily-key
运行 Docker 容器
现在,使用 docker run 命令来启动我们的应用容器:
docker run -d -p 8000:8000 --env-file .env --name my-deep-research-instance deep-research-app:latest
docker run: 运行命令。-d: 后台运行(detached mode)。-p 8000:8000: 端口映射。将宿主机的 8000 端口映射到容器的 8000 端口。这样,我们就可以通过访问宿主机的 8000 端口来访问容器内的服务。--env-file .env: 从.env文件中读取所有环境变量并注入到容器中。--name my-deep-research-instance: 为正在运行的容器实例起一个方便记忆的名字。deep-research-app:latest: 我们要基于哪个镜像来启动容器。
运行后,你可以通过 docker ps 命令看到你正在运行的容器。
5. 第四步:测试容器化的 DeepResearch 服务
容器已经在后台运行,现在我们可以像测试任何远程 API 一样测试它。
curl -X 'POST' \
'http://localhost:8000/invoke' \
-H 'Content-Type: application/json' \
-d '{
"task": "What are the latest advancements in Large Language Models as of this year?"
}'
如果一切正常,稍等片刻,你应该能收到一个包含最终研究报告的 JSON 响应。
查看容器日志
如果出现问题,或者你想观察容器内部的运行情况(比如 LangGraph 节点的 print 输出),可以使用 docker logs 命令:
# -f 参数可以持续跟踪日志输出
docker logs -f my-deep-research-instance
6. 生产环境部署的考量
我们现在已经成功地将应用容器化了,但距离真正的“生产级”部署,还有几点需要考虑。
镜像优化:多阶段构建
我们的 Dockerfile 会把所有源代码、测试文件等都复制进去,导致镜像体积较大。可以使用多阶段构建来优化:第一阶段构建 Python 依赖环境,第二阶段只把运行所需的代码和依赖复制到一个干净的基础镜像中,可以大大减小最终镜像的体积。
数据库持久化:挂载卷(Volume)
我们使用的 SqliteSaver 会在容器内部的 /app 目录下创建一个 deep_research.sqlite 文件。这是一个严重的问题:当容器被删除时(比如应用更新),这个数据库文件会随之消失,所有持久化的记忆都会丢失!
解决方案是使用卷(Volume),将容器内的数据库文件路径,映射到宿主机的一个特定目录上。
# -v/--volume 参数格式: <host_path>:<container_path>
docker run -d -p 8000:8000 --env-file .env \
-v "$(pwd)/data:/app/data" \
--name my-deep-research-instance deep-research-app:latest
(这需要你修改 SqliteSaver.from_conn_string("sqlite:///data/deep_research.sqlite"),将数据库文件放在一个单独的 data 目录中。)
这样,容器内对 /app/data/deep_research.sqlite 的所有读写,实际上都作用于宿主机的 $(pwd)/data/deep_research.sqlite 文件。即使容器被销毁,数据依然安全地保留在宿主机上。
编排与扩展
在生产环境中,你可能需要运行多个应用实例来实现高可用和负载均衡。手动管理多个容器非常繁琐。这时就需要容器编排工具:
- Docker Compose: 适用于在单台宿主机上定义和运行多容器应用。你可以用一个
docker-compose.yml文件来同时定义你的 Agent 服务、一个 Redis 缓存服务、一个 Postgres 数据库服务等。 - Kubernetes (K8s): 事实上的容器编排标准,适用于跨多个服务器集群的大规模部署。它提供了自动扩缩容、服务发现、滚动更新等强大的功能。
7. 总结:迈向 AI 应用的工业化生产
恭喜你!通过本章的学习,你已经完成了从“本地开发”到“容器化部署”的关键一跃。你不仅构建了一个强大的 AI 应用,更掌握了如何将其以一种标准化的、可靠的、可扩展的方式进行打包和交付。
容器化是 AI 应用“工业化生产”的第一步。它将你的代码从脆弱的、依赖特定环境的“手工作坊”,变成了可以在任何地方、大规模复制的“标准件”。掌握了 Docker,你就拥有了将你的 AI 创想交付给全世界用户的船票。