算法代码为什么不能直接放进生产系统?
码海寻道 · AI 算法工程化与系统落地实战第 1 篇
很多算法项目在本地运行得很好:准备一个 Notebook,加载模型,传入一张图片,几秒钟就能得到结果。于是有人会自然地认为,把这段代码放进服务器、加一个接口,就完成了上线。
实际上,本地能跑只是“算法验证完成”,并不等于“系统可以交付”。生产系统需要面对并发请求、错误输入、模型加载、依赖版本、超时、日志、权限、资源限制、数据保存和服务重启。算法代码只是核心能力,周围还需要一层工程系统把它变成稳定、可观测、可恢复的服务。
本文不讨论某一种具体算法,而是先建立一个通用判断框架,为后续的 FastAPI、Docker、Docker Compose 和业务系统接入做准备。
一、算法 Demo 和生产服务有什么区别?
可以把两者放在一张表里比较:
| 维度 | 本地算法 Demo | 生产算法服务 |
|---|---|---|
| 输入 | 默认输入,格式可控 | 用户输入,可能缺失、错误或恶意 |
| 模型 | 启动时手动加载 | 服务启动时加载并复用 |
| 配置 | 写死在代码里 | 按环境注入,可审计、可切换 |
| 依赖 | 当前机器恰好能运行 | 需要固定版本和可重复构建 |
| 调用 | 单次执行 | 并发、超时、重试和取消 |
| 输出 | 打印或返回对象 | 稳定 Schema、错误码和版本信息 |
| 数据 | 本地文件 | 对象存储、数据库或消息队列 |
| 运维 | 出问题就重新运行 | 日志、指标、告警和恢复 |
| 发布 | 手动复制代码 | 镜像、版本、灰度和回滚 |
因此,工程化不是给算法“套一个接口”这么简单,而是为算法补齐运行边界。
二、生产系统真正需要保证什么?
一个算法服务至少要回答下面八个问题:
- 输入数据不符合预期时,服务是否能安全返回错误?
- 模型是否只加载一次,而不是每个请求重复加载?
- 不同环境使用的模型、依赖和配置是否明确?
- 请求超时或客户端断开后,算法是否会继续占满资源?
- 服务重启后,已提交的任务和结果是否还能找到?
- 出现错误时,能否根据日志定位到具体请求和模型版本?
- 新版本效果变差时,能否快速切回旧版本?
- 服务扩容时,多个实例是否能够稳定处理请求?
如果这些问题没有答案,算法即使准确率很高,也还没有达到可交付状态。
三、先把算法代码拆成四层
不建议把模型加载、文件读取、业务判断和 HTTP 路由全部写在一个main.py中。可以先按下面的职责拆分:
algorithm-service/ ├── app/ │ ├── main.py # 服务启动和路由注册 │ ├── api/ # HTTP 接口层 │ ├── schemas/ # 请求和响应模型 │ ├── service/ # 业务流程编排 │ ├── inference/ # 模型加载和推理 │ ├── config.py # 配置读取 │ └── logging_config.py # 日志配置 ├── models/ # 本地开发用模型目录,不一定打进镜像 ├── tests/ # 单元、接口和回归测试 ├── requirements.txt # 或 pyproject.toml ├── Dockerfile ├── compose.yaml └── .dockerignore这样拆分有三个直接好处:
- 替换模型时,不必修改接口层;
- 本地调用和 HTTP 调用可以复用同一个推理服务;
- 后续放入 Docker、消息队列或 Worker 时,不需要重写全部代码。
四、模型加载应该和请求处理分开
常见错误是把模型加载写在接口函数里面:
@app.post("/predict")defpredict(request:PredictRequest):model=load_model("models/model.onnx")returnmodel.predict(request.features)这种写法每次请求都可能重复读取模型、初始化运行时和申请内存,延迟会非常高,也容易造成内存增长。
更合理的方式是让模型在服务启动阶段加载,并由后续请求复用:
fromfunctoolsimportlru_cache@lru_cache(maxsize=1)defget_model():model_path=settings.model_pathreturnload_model(model_path)@app.post("/predict")defpredict(request:PredictRequest):model=get_model()result=model.predict(request.features)return{"model_version":settings.model_version,"result":result,}生产环境还要考虑模型预热、启动失败、模型文件不存在、GPU 显存不足和多进程重复加载。模型加载失败时,服务应该处于未就绪状态,而不是启动后才在第一条请求中报错。
五、配置不能写死在算法代码里
下面这些内容不应该散落在 Python 文件中:
MODEL_PATH="/home/user/project/models/final-v3/model.onnx"THRESHOLD=0.72DATABASE_URL="postgresql://user:password@localhost/app"至少应区分三类配置:
代码默认值:适合本地开发 环境变量:适合部署时注入 密钥管理系统:适合密码、Token 和访问凭据例如:
frompydantic_settingsimportBaseSettings,SettingsConfigDictclassSettings(BaseSettings):model_path:str="models/demo.onnx"model_version:str="dev"score_threshold:float=0.72inference_timeout_seconds:float=10.0model_config=SettingsConfigDict(env_file=".env",env_prefix="ALGO_",extra="ignore",)settings=Settings()配置还需要记录版本。一次推理结果最好能够追溯到:
algorithm_name model_version config_version runtime_version request_id否则当结果发生变化时,很难判断究竟是模型换了、阈值换了,还是运行环境发生了变化。
六、输入和输出必须是稳定契约
算法函数可能接受一个 NumPy 数组,但业务系统更关心的是明确的请求和响应格式:
{"request_id":"req-001","image_url":"https://storage.example.com/input/a.jpg","options":{"threshold":0.72}}响应也不要只返回一个裸数组:
{"request_id":"req-001","status":"succeeded","model_version":"classifier-v3","result":{"label":"normal","score":0.9831},"elapsed_ms":86}输入输出契约至少应明确:
- 必填字段和字段类型;
- 文件大小、格式和数量限制;
- 分数、坐标和时间等字段的单位;
- 模型版本和结果版本;
- 失败时返回什么错误码;
- 结果是否保存,以及保存多久。
契约稳定后,Java、Node.js、前端或其他服务才能放心接入。接口文档可以由 FastAPI 根据类型定义生成,但业务字段的含义仍需要团队自己维护。
七、算法服务必须有资源边界
没有资源边界的算法服务,很容易因为一个异常请求拖垮整台机器。至少要限制:
单请求文件大小 单请求处理数量 最大输入尺寸 最大推理时间 最大并发数 队列长度 CPU、内存和 GPU 使用量例如,图像算法不能默认接受任意分辨率的图片;文本算法不能默认接受无限长文本;批量预测不能让一次请求提交几十万条记录。
资源限制要放在多个层次:
网关:请求大小、连接数、超时 API:参数校验、并发控制、任务配额 Worker:队列长度、单任务预算、重试次数 容器:CPU、内存、GPU 和文件系统限制OWASP 2025 的大模型风险清单也把“无限消耗”列为重要风险。即使当前项目不是大模型,算法服务同样需要防止输入洪泛、重复调用和资源失控。
八、错误要分为可处理和不可处理
不要把所有异常都返回成:
{"message":"预测失败"}建议至少区分:
| 错误类型 | 示例 | 是否适合重试 |
|---|---|---|
| 参数错误 | 图片格式不支持 | 否 |
| 权限错误 | 无权访问文件 | 否 |
| 资源不存在 | 模型文件缺失 | 否,应告警 |
| 临时依赖错误 | 对象存储超时 | 可以有限重试 |
| 资源不足 | GPU 显存不足 | 通常不应立即重试 |
| 服务超时 | 推理超过预算 | 视任务类型决定 |
错误响应中应包含用户可理解的错误码和request_id,但不要把堆栈、服务器路径、数据库连接串和密钥返回给调用方。
九、日志不是“多打印几行”
算法服务至少应该记录结构化日志:
{"event":"inference_finished","request_id":"req-001","algorithm":"image_classifier","model_version":"classifier-v3","status":"succeeded","elapsed_ms":86,"input_size":1,"created_at":"2026-10-28T10:00:00Z"}日志中不要直接保存完整图片、完整文本、访问 Token 或敏感业务字段。需要排查样本时,记录受控的对象 ID、哈希或短期授权地址,并设置访问权限和保留周期。
除了日志,还要关注几个基础指标:
- 请求量和成功率;
- P50、P95、P99 推理耗时;
- 超时、重试和拒绝数量;
- 当前并发和队列长度;
- CPU、内存和 GPU 使用率;
- 各模型版本的错误率和结果分布。
十、为什么 Docker 化是交付的关键一步?
如果算法只能在某位开发者电脑上运行,交付过程通常会变成:
复制代码 安装依赖 修改路径 手动下载模型 修改环境变量 重启服务 继续排查缺少的库Docker 的价值不是“把代码压缩成一个文件”,而是把运行环境、依赖、启动命令和部分配置变成可重复构建的镜像。之后可以在本地、测试服务器和生产平台使用同一个镜像,再通过环境配置改变外部连接。
一个最小的 Dockerfile 可以是:
FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app ./app EXPOSE 8000 CMD ["fastapi", "run", "app/main.py", "--port", "8000"]实际项目还应继续补充.dockerignore、固定依赖、非 root 用户、健康检查、模型文件策略和资源限制。Docker Compose 可以进一步把算法 API、数据库、Redis、对象存储和 Worker 组合成一个可启动的开发环境;但数据卷、密钥和健康检查必须单独设计,不能只写几个image就认为完成部署。Docker Compose 服务、健康检查、卷和 Secrets 文档
FastAPI 官方部署文档也建议根据实际环境自行构建应用镜像,并根据部署方式决定是使用容器副本还是进程 Worker,而不是依赖已经弃用的通用 FastAPI 基础镜像。FastAPI 容器部署文档
十一、上线前的最小检查表
在进入 Docker 和 Compose 之前,建议先完成下面的检查:
- 模型加载与请求处理已经分离;
- 模型、配置和运行时版本可以记录;
- 输入输出 Schema 已明确;
- 文件大小、输入长度和并发数有限制;
- 超时、重试和错误码已经定义;
- 日志包含 request_id,但没有泄露敏感内容;
- 模型文件的来源、校验和和保存位置明确;
- 算法函数有单元测试和固定样本回归测试;
- 服务可以在没有开发者本机环境的情况下启动;
- 已经准备 Dockerfile 和依赖清单。
结语
算法工程化的第一步,不是马上写 Dockerfile,而是先把算法从“个人电脑上的一段代码”变成“有输入契约、配置边界、资源限制、版本记录和错误处理的服务能力”。
完成这些准备后,Docker 才能真正发挥作用:把可重复的算法服务交付给测试环境、业务系统和生产平台。下一篇将从项目目录和依赖管理开始,搭建一个适合 Docker 化的 Python 算法服务骨架。
参考资料
- Docker 官方文档:Compose services
- FastAPI 官方文档:FastAPI in Containers
- OWASP:Top 10 for Large Language Model Applications 2025
本文为“码海寻道”原创技术文章。算法部署涉及模型许可、数据安全、资源容量和业务合规,正式上线前应结合实际环境进行测试和评审。