部署、API 与模型网关
正在读取学习记录…
资料助手在个人电脑上运行后,下一步是让多个用户通过同一个入口使用。部署是把程序及其配置放到持续运行的服务环境;API是调用者与服务交换请求、响应的契约;模型网关是应用与模型服务之间的集中控制位置,负责路由、限额、超时和观测。
正常请求 /ask 带着经过验证的Alice身份和问题“报销上限?”,通过输入检查、就绪检查和并发限制后,选择 local-model 适配器,返回状态200、答案与模型标识。适配器将应用的统一请求转换为具体供应商接口;应用业务层便能保持同一套错误处理。
一次请求走过哪些位置
图从左到右表示处理顺序。入口层验证身份,业务层验证字段,调用层控制配额与截止时间;日志用请求ID串起这些环节。真实服务的身份应来自验证过的会话或令牌,不能信任客户端随意写入的 principal 字段。
| 位置 | 检查内容 | 提前退出后的结果 |
|---|---|---|
| 路由 | 是否存在/ask | 404,无模型调用 |
| 身份 | 是否验证成功 | 401,无模型调用 |
| 输入 | 问题为非空字符串且不超过100字符 | 400,无模型调用 |
| 就绪 | 是否接收新业务 | 503,无模型调用 |
| 并发 | 在途数是否小于2 | 429,无模型调用 |
| 下游 | 是否在1000毫秒内完成 | 504,已有1次调用 |
存活探针询问“进程是否还能回答”,就绪探针询问“现在能否接收业务”。Kubernetes 探针文档说明就绪失败会影响服务流量接入,存活失败用于判断重启;启动探针则为慢启动过程留出时间。把外部模型短暂不可用直接绑定进程存活,可能引起反复重启。
运行一个网关决策器
建立新文件夹 l8-deployment,分别保存 gateway.py 和 test_gateway.py。请求和响应在本地通过字典传递,downstream_ms 是给定耗时,calls 是调用轨迹。local_model 是实际执行的本地固定答案函数,函数进入时才把 local-model 写入轨迹;它作为模型适配器的教学替身,便于看清每个分支。接入HTTP服务器时,将这里的输入校验与返回结构放进处理函数,并由服务器验证真实身份。
代码按照路径、身份、输入、就绪、并发、下游顺序执行。先观察正常问答,再比较其他状态码的调用次数。
先在 PowerShell 创建并进入目录,再保存下方两份文件:
New-Item -ItemType Directory -Force l8-deployment
cd l8-deploymentmacOS 终端:
mkdir -p l8-deployment
cd l8-deployment"""用请求字典演示 API 网关的处理顺序与错误映射。"""
READY = True
LIMIT = 2
def local_model(text, calls):
# 本地固定答案适配器:函数执行时才记录调用,不接远端模型。
calls.append("local-model")
return {"answer": "报销上限200元", "model": "local-model"}
def gateway(request, *, ready=True, active=0, downstream_ms=200):
path = request.get("path")
# 健康探针独立于业务请求;存活表示进程能回答,就绪表示可接业务。
if path == "/live":
return 200, {"live": True}, []
if path == "/ready":
return (200 if ready else 503), {"ready": ready}, []
if path != "/ask":
return 404, {"error": "route"}, []
# 身份来自网关可信验证结果,示例不把模型输出当身份。
if request.get("principal") != "alice":
return 401, {"error": "identity"}, []
text = request.get("text")
if not isinstance(text, str) or not text.strip() or len(text) > 100:
return 400, {"error": "input"}, []
if not ready:
return 503, {"error": "not_ready"}, []
if active >= LIMIT:
return 429, {"error": "concurrency"}, []
# 调用次数可观测,超时后不自动重试,避免扩大下游压力。
calls = []
body = local_model(text, calls)
if downstream_ms > 1000:
return 504, {"error": "deadline"}, calls
return 200, body, calls
def main():
samples = [
("存活", {"path": "/live"}, {}),
("就绪", {"path": "/ready"}, {}),
("问答", {"path": "/ask", "principal": "alice", "text": "报销上限?"}, {}),
("缺少身份", {"path": "/ask", "text": "报销上限?"}, {}),
("并发已满", {"path": "/ask", "principal": "alice", "text": "报销上限?"}, {"active": 2}),
("下游超时", {"path": "/ask", "principal": "alice", "text": "报销上限?"}, {"downstream_ms": 1200}),
]
for label, request, options in samples:
status, body, calls = gateway(request, ready=READY, **options)
print(f"{label}: {status} / {body} / 调用{len(calls)}")
if __name__ == "__main__":
main()测试文件 test_gateway.py:
"""检查在调用前拒绝、阈值边界与健康探针的不同语义。"""
import unittest
from gateway import gateway, local_model
ASK = {"path": "/ask", "principal": "alice", "text": "报销上限?"}
class GatewayTests(unittest.TestCase):
def test_adapter_records_execution(self):
calls = []
body = local_model("报销上限?", calls)
self.assertEqual(calls, ["local-model"])
self.assertEqual(body["answer"], "报销上限200元")
def test_normal_and_invalid(self):
self.assertEqual(gateway(ASK)[0], 200)
self.assertEqual(gateway({**ASK, "text": " "}),
(400, {"error": "input"}, []))
def test_identity_before_call(self):
self.assertEqual(gateway({**ASK, "principal": "bob"})[2], [])
def test_readiness_and_liveness(self):
self.assertEqual(gateway({"path": "/live"}, ready=False)[0], 200)
self.assertEqual(gateway({"path": "/ready"}, ready=False)[0], 503)
self.assertEqual(gateway(ASK, ready=False)[2], [])
def test_slot_boundary(self):
self.assertEqual(gateway(ASK, active=1)[0], 200)
self.assertEqual(gateway(ASK, active=2)[0], 429)
def test_deadline_boundary(self):
self.assertEqual(gateway(ASK, downstream_ms=1000)[0], 200)
self.assertEqual(gateway(ASK, downstream_ms=1001)[0], 504)
def test_unknown_route(self):
self.assertEqual(gateway({"path": "/missing"})[0], 404)
if __name__ == "__main__":
unittest.main()文件保存后,在同一窗口核对文件名并运行:
ls
python -X utf8 gateway.py
python -X utf8 -m unittest -v test_gateway.pydir应显示这两个文件。运行程序后得到下面输出;测试末尾应显示 OK。
程序输出:
存活: 200 / {'live': True} / 调用0
就绪: 200 / {'ready': True} / 调用0
问答: 200 / {'answer': '报销上限200元', 'model': 'local-model'} / 调用1
缺少身份: 401 / {'error': 'identity'} / 调用0
并发已满: 429 / {'error': 'concurrency'} / 调用0
下游超时: 504 / {'error': 'deadline'} / 调用1服务尚未就绪时,哪些请求还能回答
在 gateway.py 找到 READY = True,改为 READY = False,保存并运行:
python -X utf8 gateway.py“存活”仍返回200;“就绪”返回503。“问答”“并发已满”“下游超时”都在就绪检查处提前结束,返回503且调用0次;“缺少身份”仍为401,因为身份检查更早。恢复 READY = True 后再次运行,得到上面的正常输出。
这说明状态码对应程序实际走到的分支。同一请求同时违反多个条件时,先检查的条件决定返回结果。测试还验证1000毫秒恰好通过、1001毫秒触发超时,避免把阈值写成模糊描述。
放入实际部署流程
构建阶段将应用、依赖清单与配置打成确定的制品。启动阶段加载配置并检查必要资源,准备好后转为就绪。停止阶段先取消就绪,再等正在执行的请求结束,最后关闭进程;这称为优雅停机,可减少发布时中断请求。
网关对模型别名、供应商凭证和允许工具统一管理。凭证由服务端读取,日志记录请求ID、模型别名、耗时、令牌数量和错误类别,敏感输入按保留规则处理。供应商返回格式仍须在适配器处校验,避免将不完整答案当成功。
| 场景 | 合适动作 | 原因 |
|---|---|---|
| 某模型短暂不可用 | 有预算和兼容性依据时切换适配器 | 同时记录切换原因与实际费用 |
| 高峰并发耗尽 | 有界排队或快速拒绝 | 防止请求无限堆积 |
| 请求已经超时 | 传播取消信号,核对下游状态 | 用户超时后下游仍可能执行 |
| 写工具返回超时 | 根据操作ID查询结果 | 自动重试可能重复写入 |
| 更新配置 | 审核、校验后发布 | 配置会改变真实行为 |
本地分支能检查处理顺序;连接池、TLS、负载均衡、HTTP传输和真实模型响应需要接入服务后验证。先把输入、输出、调用轨迹及失败映射定清楚,再扩大部署规模。
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 先判断边界
关于“部署、API 与模型网关”,哪种做法更稳妥?
对应知识点:L8-deployment
简答题 · 解释失败
写出一个失败输入并说明记录字段。
对应知识点:L8-deployment
代码题 · 边界测试
补充正常、边界、失败三组输入和预期结果。
对应知识点:L8-deployment
场景题 · 结果未知
证据不足时如何设计状态和人工接管?
对应知识点:L8-deployment
接着用容量、并发、缓存与FinOps估算入口限额与成本。
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。