前面的文章已经介绍了模型调用、错误处理、评估、日志和性能优化。到了部署阶段,难点通常不在“把代码放到服务器”,而在于让程序能够被稳定启动、配置、检查和停止。本篇只聚焦一个核心知识点:如何为 AI 应用建立最小的生产部署边界。示例使用 Python 标准库模拟模型调用,不需要真实密钥,也不依赖 Web 框架;它可以直接运行,帮助我们先理解部署契约,再替换为实际的模型客户端。

本地能运行不等于可以部署

本地原型往往把配置写在代码里,调用失败时直接打印异常,程序退出也没有明确规则。这样的程序可以验证想法,却很难交给进程管理器或容器运行。生产部署至少要把以下职责说清楚:

  1. 配置从哪里来:模型名、超时和密钥通过环境变量注入,代码只提供安全的非敏感默认值。
  2. 进程如何工作:启动后持续运行,收到停止信号时完成收尾,不把一个请求的失败变成整个服务的无声崩溃。
  3. 是否健康:健康检查要能区分“进程还活着”和“已经可以接收请求”。
  4. 如何观察:日志输出到标准输出,包含请求结果和耗时,交给平台统一采集。

这四项是部署边界,而不是某个云平台的专属功能。容器、虚拟机或进程管理器都可以据此启动同一个程序。真正的模型 SDK 只负责调用模型,部署层仍然负责进程、配置和运行状态。

一个最小的可运行服务

下面的程序提供一个简单的 HTTP 服务。/healthz 返回进程状态,/ask 接收 JSON 中的 question,并用本地函数模拟模型调用。为了让示例可验证,模拟函数会把问题转换为固定格式;接入真实服务时,只替换 call_model,不要把密钥写进源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import json
import logging
import os
import signal
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
HOST = os.getenv("AI_HOST", "127.0.0.1")
PORT = int(os.getenv("AI_PORT", "8080"))
MODEL_NAME = os.getenv("AI_MODEL", "demo-model")


def call_model(question: str) -> str:
"""用确定性结果模拟一次模型调用。"""
return f"[{MODEL_NAME}] 已收到:{question}"


class Handler(BaseHTTPRequestHandler):
def send_json(self, status: int, payload: dict) -> None:
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)

def do_GET(self) -> None:
if self.path == "/healthz":
self.send_json(200, {"status": "ok"})
else:
self.send_json(404, {"error": "not found"})

def do_POST(self) -> None:
if self.path != "/ask":
self.send_json(404, {"error": "not found"})
return

started = time.perf_counter()
try:
length = int(self.headers.get("Content-Length", "0"))
data = json.loads(self.rfile.read(length))
question = data.get("question")
if not isinstance(question, str) or not question.strip():
raise ValueError("question 必须是非空字符串")
answer = call_model(question.strip())
self.send_json(200, {"answer": answer})
except (ValueError, json.JSONDecodeError) as exc:
self.send_json(400, {"error": str(exc)})
except Exception:
logging.exception("request failed")
self.send_json(502, {"error": "model request failed"})
finally:
elapsed = time.perf_counter() - started
logging.info("path=%s elapsed=%.3fs", self.path, elapsed)

def log_message(self, format: str, *args: object) -> None:
logging.info("http " + format, *args)


def main() -> None:
server = ThreadingHTTPServer((HOST, PORT), Handler)

def stop(signum: int, _frame: object) -> None:
logging.info("received signal=%s, shutting down", signum)
server.shutdown()

signal.signal(signal.SIGTERM, stop)
signal.signal(signal.SIGINT, stop)
logging.info("listening on http://%s:%s", HOST, PORT)
try:
server.serve_forever()
finally:
server.server_close()
logging.info("server stopped")


if __name__ == "__main__":
main()

保存为 app.py 后运行:

1
AI_MODEL=demo-model python app.py

另开终端验证:

1
2
3
4
curl -i http://127.0.0.1:8080/healthz
curl -i -X POST http://127.0.0.1:8080/ask \
-H 'Content-Type: application/json' \
-d '{"question":"什么是上下文?"}'

程序收到 Ctrl-C 或终止信号后会退出 serve_forever,执行 server_close。这里的健康检查只表示进程已经能响应;真实项目还可以在启动阶段检查必要配置,并增加依赖服务检查,但不要让健康检查本身触发一次昂贵的模型调用。

接入真实模型时,边界应该放在哪里

call_model 替换为 SDK 调用时,至少保留三层边界。第一层是配置边界:使用类似 os.environ["PROVIDER_API_KEY"] 的方式读取密钥,部署平台负责注入,日志中绝不打印密钥和完整用户输入。第二层是请求边界:为网络调用设置连接和读取超时,限制输入长度,并将供应商异常转换为应用自己的错误类型。第三层是输出边界:即使模型返回了内容,也要经过解析、校验和必要的安全过滤后再交给调用方。

不要把“服务启动成功”理解成“模型调用一定成功”。启动时可以校验密钥变量是否存在,但网络、配额、上游限流和模型临时故障仍然可能在运行中发生。因此 /healthz 与请求错误处理应当分开:健康检查反映服务进程状态,请求处理则按超时、重试和降级策略返回结果。

部署清单:从脚本到进程

实际部署时,可以按下面顺序检查:

  • 固定运行方式:明确 Python 版本、启动命令和工作目录,依赖文件锁定版本,避免“在我的机器上可以”。
  • 配置外置:区分非敏感配置和密钥;密钥使用环境变量或平台的密钥管理服务,不提交到 Git。
  • 限制资源:为并发数、请求体大小、单次输入长度和超时设上限。AI 请求通常比普通接口更慢,不能默认无限等待。
  • 标准输出日志:至少记录请求类型、耗时、状态和可关联的请求 ID;不要记录原始密钥、完整隐私文本或模型内部思维内容。
  • 可停止:处理 SIGTERM,停止接收新任务并等待正在执行的任务收尾。若调用可能很久,还需要设置最大等待时间。
  • 健康检查:平台应能通过固定路径判断进程是否响应;重启策略要避免故障时无限快速重启。
  • 分离后台任务:批量摘要、索引和定时处理不要阻塞在线请求,必要时使用独立 worker 和队列。

初次上线不必一开始就引入复杂平台。先让同一份程序在本地、测试环境和生产环境通过环境变量获得不同配置,再由进程管理器或容器负责拉起、停止和重启。部署工具可以变化,但应用的输入、输出和退出行为应保持稳定。

常见问题

为什么不把密钥直接写进配置文件? 配置文件很容易被提交、复制或进入镜像层。环境变量也不是万能的,但配合平台密钥管理和最小权限,泄露范围更容易控制。

健康检查返回 200 就代表模型服务正常吗? 不代表。它通常只验证当前进程能响应。若把上游模型也纳入检查,可能造成额外费用和级联故障,应根据系统目标拆成轻量存活检查和受控的依赖检查。

为什么一个请求失败不能让进程退出? 临时网络错误只影响当前请求,进程退出会把一个局部问题扩大成全站不可用。只有无法恢复的启动配置错误或进程级故障,才应交给外部管理器重启。

ThreadingHTTPServer 能直接承载生产流量吗? 这里的重点是展示部署边界,不是推荐某个 Web 服务器。正式环境应选择经过验证的服务栈,并在前面配置 TLS、认证、限流和网关;这些都不应由这个最小示例承担。

小结

从原型走向生产,第一步不是增加更多模型能力,而是定义稳定的运行契约:配置外置、请求有边界、日志可观察、健康状态可检查、进程能够优雅停止。本文的标准库示例刻意省略了真实供应商细节,因此可以先验证启动、健康检查、错误响应和信号处理。接入具体模型后,只需把模型调用放回明确的函数边界,并继续保留超时、校验和密钥管理。这样,AI 能力只是服务中的一个可替换部件,而不是部署系统的全部。