最近在做 AI 网关相关的东西时,踩过一个不算明显但影响很大的坑:网关配置上游模型时写的是“模型 A”,实际请求转发过去后,返回的模型上下文、计费信息和审计记录都和预期对不上。排查了很久才意识到,问题出在“模型身份”没有被校验。
这类问题在 AI 网关场景里越来越多。像 XTokenChecker 这样一个面向 AI 网关模型身份校验的工具思路,刚好可以作为我们自研校验模块的设计参考。本文会从模型身份是什么、为什么容易出问题讲起,再用一套可直接运行的 Python 示例,从零实现一个轻量的模型身份检查层,覆盖响应校验、模型名比对、身份令牌签发与验签等关键环节。
适合正在做 LLM 网关、企业内部 AI 平台、或者是想给现有网关补一层安全审计的同学。全文偏工程落地,代码都可以直接复制改着用。
1. 背景与核心概念
1.1 什么是 AI 网关中的模型身份
在传统微服务架构里,网关负责路由、鉴权和流量控制。到了 AI 网关阶段,事情又多了一层:网关需要根据用户请求中的模型名,把请求转发到不同的模型供应商,或者在多个同名模型中做负载均衡。
这里的“模型身份”,指的是一个响应或者一次模型调用,到底来自哪个模型。它不能只理解为“模型名字”,而应该是一组可验证的属性,至少包括:
- 模型供应商是谁;
- 上游模型标识是什么;
- 实际调用的模型版本是什么;
- 请求方申请的模型别名是什么;
- 校验发生时的时间戳、请求 ID 等上下文。
例如用户请求写的是gpt-4o-mini,网关经过别名映射后,实际转发的上游模型可能是gpt-4o-mini-2024-07-18。那么在这次调用中,模型身份就不只是gpt-4o-mini,而是包含了上游版本序列号的完整信息。
1.2 模型身份与模型名称的区别
很多同学会把“模型身份”等同于“响应里的 model 字段”。这其实是一个容易踩坑的地方。
响应体中的model字段,有一个典型问题:它是由模型服务方返回的,本身不携带任何密码学证明。也就是说,只要控制返回内容的服务端愿意,它完全可以把gpt-4o-mini的返回内容,标记成gpt-4o的模型名,或者反过来。
此外,网关层也可能因为缓存、负载均衡、配置漂移等原因,把请求转发到错误的模型上。这个时候如果只依赖响应体中的 model 字符串做统计或审计,结果可能就是错的。
所以我们要讨论的模型身份校验,重点并不只是“字符串是否相等”,而是“响应中的模型标识,是否与网关预期转发的模型标识一致,并且这个过程有可验证的证据”。
1.3 不校验模型身份会带来什么问题
在真实项目中,缺少模型身份校验通常会引发几类问题:
- 成本核算错误:不同模型单价不同。如果网关把贵价模型的调用错误记录成便宜模型,成本大盘就会失真。
- 审计不合规:企业内外部审计往往要求每条调用记录可追溯。模型身份无法置信,审计证据链就断了。
- A/B 实验判断错误:有些团队会同时接入多个模型做效果对比。模型身份错了,实验结论就可能完全反转。
- 模型逃逸与管理风险:上游配置被篡改,或者网关路由配置与预期不一致时,调用可能被悄悄路由到其他模型,甚至未经授权的模型。
这些问题不是靠“等出事了再查日志”就能解决的,更重要的是在入口链路加一道校验动作。XTokenChecker 这个名字里有两个关键词:Token 和 Checker,本质上就是通过 Token 这一类可验证对象,来检查 AI 网关中模型身份是否真实可信。
2. XTokenChecker 的设计思路
2.1 身份校验的本质
模型身份校验的核心可以拆成三个问题:
- 身份信息从哪来;
- 身份信息如何传递;
- 身份信息如何证明。
第一个问题决定我们校验什么。通常情况下,身份信息来自模型服务的响应元数据、网关自身的路由配置、上游模型注册表。第二个问题决定我们如何把这些信息编排到一起。第三个问题,则依赖签名、HMAC、甚至上游模型服务返回的可验证元数据。
XTokenChecker 这类工具的通用做法,并不是去修改模型供应商的接口协议,而是在网关与模型服务之间增加一个“校验层”,在请求转发完成后、响应返回给调用方之前,完成对模型身份信息的提取、核对、签名与审计。
2.2 三层校验
按防护强度从低到高,可以分为三层:
- 第一层:字段级校验。检查响应中是否存在 model 字段,字段非空,且字段是合法的字符串。
- 第二层:配置级校验。把响应中的模型标识与网关当前生效的模型白名单做比对,确认本次响应中的模型确实属于预期范围。
- 第三层:令牌级校验。网关校验完模型身份后,生成一个带签名的身份令牌,随响应返回给调用方。调用方或审计服务可以通过验签,确认该令牌确实由当前网关签发且未被篡改。
三层校验不是互斥的,生产环境建议两层或三层叠加使用。
2.3 校验流程
我们可以把一次请求的模型身份校验流程拆成以下几个步骤:
- 调用方通过 AI 网关发起模型调用请求。
- 网关根据请求中的模型别名,查询模型映射配置。
- 网关将请求转发给上游模型服务。
- 模型服务返回响应,响应中携带模型标识和生成结果。
- 校验模块从响应中提取模型标识。
- 校验模块对比网关配置中的允许模型列表。
- 校验通过后,生成模型身份令牌,并随响应返回。
- 校验失败时,网关按策略阻断或降级,并记录审计日志。
3. 环境准备与项目结构
3.1 运行环境
本文示例使用 Python 3.10+,核心依赖如下:
fastapi==0.115.6 uvicorn==0.34.0 httpx==0.28.1 pydantic==2.10.4版本可以根据你的项目实际情况调整,本文以常见环境为例,重点演示配置思路。
除了 Python 环境,你还需要准备:
- 一个可访问的 AI 网关或模型服务接口,用于模拟上游响应;
- 如果没有实际模型服务,可以使用 Mock 协议本地模拟响应。
3.2 项目结构
建议按下面的目录结构组织示例工程:
xtokenchecker-demo/ ├── app.py # FastAPI 入口,模拟网关入口 ├── verifier/ │ ├── __init__.py │ ├── identity.py # 模型身份对象 │ ├── checker.py # 模型名校验逻辑 │ ├── token.py # HMAC 身份令牌 │ └── models.py # Pydantic 模型 ├── config.yaml # 模型映射配置 ├── requirements.txt └── tests/ └── test_checker.py下面我们按文件逐个实现。
4. 核心实现:从配置到校验
4.1 定义模型身份对象
先创建verifier/identity.py,用 dataclass 定义模型身份。
# 文件路径:verifier/identity.py from dataclasses import dataclass from datetime import datetime, timezone from typing import Optional @dataclass class ModelIdentity: requested_model: str # 调用方请求的模型名 model: str # 响应中实际返回的模型名 provider: str # 模型供应商标识 version: Optional[str] = "" # 模型版本 checked_at: str = "" # 校验时间 request_id: str = "" # 网关请求 ID def __post_init__(self): if not self.checked_at: self.checked_at = datetime.now(timezone.utc).isoformat() def to_dict(self) -> dict: return { "requested_model": self.requested_model, "model": self.model, "provider": self.provider, "version": self.version, "checked_at": self.checked_at, "request_id": self.request_id, }这里把requested_model和model分开,是因为网关层存在模型别名映射。例如业务侧请求fast-chat,但上游实际模型是gpt-4o-mini。两个字段分别记录“业务想要什么”和“实际得到什么”,对后续审计会清晰很多。
4.2 定义接口请求与响应模型
创建verifier/models.py,用于定义网关校验接口的请求与响应结构。
# 文件路径:verifier/models.py from typing import Optional from pydantic import BaseModel, Field class UpstreamResponse(BaseModel): id: str = "" model: str = "" object: str = "" choices: list = Field(default_factory=list) class CheckResult(BaseModel): passed: bool reason: str identity: Optional[dict] = None token: Optional[str] = None在实际网关场景中,响应体结构可能远比这个复杂,但校验层关心的字段其实很少:id、model、choices。所以这里只摘出关键字段,避免无关字段影响校验逻辑。
4.3 模型名校验逻辑
创建verifier/checker.py,实现配置级校验。
# 文件路径:verifier/checker.py from .identity import ModelIdentity class ModelIdentityError(Exception): """模型身份校验失败时抛出""" class ModelIdentityChecker: def __init__(self, allowed_models: list[str], provider: str): self.allowed_models = set(allowed_models) self.provider = provider def check_model_field(self, response_model: str) -> bool: """第一层:字段级校验""" if not response_model: return False if not isinstance(response_model, str): return False return True def check_allowed_model(self, response_model: str) -> bool: """第二层:配置级校验""" if not self.check_model_field(response_model): return False return response_model in self.allowed_models def verify(self, response_model: str, requested_model: str, request_id: str) -> ModelIdentity: if not self.check_model_field(response_model): raise ModelIdentityError("model field is empty or invalid") if not self.check_allowed_model(response_model): raise ModelIdentityError( f"model {response_model} is not in allowed models: {sorted(self.allowed_models)}" ) return ModelIdentity( requested_model=requested_model, model=response_model, provider=self.provider, request_id=request_id, )这里有两个容易出错的地方:
allowed_models如果转成 list 再判断,性能在大流量下不好,应转成 set;- 比较模型名时不要做模糊匹配,比如
gpt-4o不能匹配gpt-4o-mini,否则白名单就失去了意义。
4.4 基于 HMAC 的身份令牌
有了模型身份对象后,还需要一种方式让调用方或审计服务验证“这个身份是网关生成且没有被篡改的”。这里采用 HMAC 签名方式实现verifier/token.py。
# 文件路径:verifier/token.py import base64 import hashlib import hmac import json class TokenVerificationError(Exception): """身份令牌校验失败""" class IdentityTokenManager: def __init__(self, secret_key: str): self.secret_key = secret_key def build_token(self, identity: dict) -> str: """将身份信息签名后生成 token""" payload_bytes = json.dumps( identity, sort_keys=True, separators=(",", ":") ).encode("utf-8") payload_b64 = base64.urlsafe_b64encode(payload_bytes).decode("ascii") signature = hmac.new( self.secret_key.encode("utf-8"), payload_bytes, hashlib.sha256, ).hexdigest() return f"{payload_b64}.{signature}" def verify_token(self, token: str) -> dict: """校验 token,返回原始身份信息""" try: payload_b64, signature = token.rsplit(".", 1) payload_bytes = base64.urlsafe_b64decode(payload_b64.encode("ascii")) expected = hmac.new( self.secret_key.encode("utf-8"), payload_bytes, hashlib.sha256, ).hexdigest() if not hmac.compare_digest(expected, signature): raise TokenVerificationError("signature mismatch") return json.loads(payload_bytes) except (ValueError, TypeError) as exc: raise TokenVerificationError("invalid token format") from exc需要注意,HMAC 的比较必须使用hmac.compare_digest,不能直接比较两个字符串。这样可以在一定程度上避免时间侧信道攻击。
5. 完整接入示例
上面的模块已经覆盖了模型身份的核心能力。下面通过一个 FastAPI 应用,把这些模块串起来,演示一次完整的模型身份校验。
5.1 依赖清单
先创建requirements.txt:
fastapi==0.115.6 uvicorn==0.34.0 httpx==0.28.1 pydantic==2.10.45.2 模型映射配置
创建config.yaml,这里的配置是示例思路,请按实际网关的上游信息调整:
gateway: provider: "openai-demo" allowed_models: - "gpt-4o-mini" - "gpt-4o" token_secret: "please-change-this-secret" enable_token: true其中token_secret在真实环境中不能出现在配置文件里,应通过环境变量或密钥管理服务注入。
5.3 网关校验入口
创建app.py,模拟网关收到上游响应后,先进行模型身份校验,再返回给调用方。
# 文件路径:app.py import os from fastapi import FastAPI, HTTPException from pydantic import BaseModel from verifier.checker import ModelIdentityChecker, ModelIdentityError from verifier.models import UpstreamResponse, CheckResult from verifier.token import IdentityTokenManager app = FastAPI(title="XTokenChecker Demo") ALLOWED_MODELS = ["gpt-4o-mini", "gpt-4o"] PROVIDER = "openai-demo" TOKEN_SECRET = os.environ.get("TOKEN_SECRET", "dev-only-secret") checker = ModelIdentityChecker(allowed_models=ALLOWED_MODELS, provider=PROVIDER) token_manager = IdentityTokenManager(secret_key=TOKEN_SECRET) ENABLE_TOKEN = True class CheckRequest(BaseModel): request_id: str requested_model: str upstream_response: UpstreamResponse @app.post("/check", response_model=CheckResult) def check_model_identity(req: CheckRequest): try: identity = checker.verify( response_model=req.upstream_response.model, requested_model=req.requested_model, request_id=req.request_id, ) except ModelIdentityError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc result = CheckResult( passed=True, reason="model identity verified", identity=identity.to_dict(), ) if ENABLE_TOKEN: result.token = token_manager.build_token(identity.to_dict()) return result这段代码通过一个/check端点,接收模拟的上游响应和调用方请求信息,然后执行:
- 字段级校验;
- 配置级校验;
- 通过后生成模型身份对象;
- 按配置决定是否签发身份令牌。
5.4 运行与验证
在项目根目录启动服务:
pip install -r requirements.txt uvicorn app:app --reload --port 8000接着用 curl 发送一个模拟请求:
curl -X POST http://127.0.0.1:8000/check \ -H "Content-Type: application/json" \ -d '{ "request_id": "req-001", "requested_model": "gpt-4o-mini", "upstream_response": { "id": "chatcmpl-abc123", "model": "gpt-4o-mini", "object": "chat.completion", "choices": [ { "index": 0, "message": {"role": "assistant", "content": "hello"} } ] } }'正常情况下,预期返回大致如下:
{ "passed": true, "reason": "model identity verified", "identity": { "requested_model": "gpt-4o-mini", "model": "gpt-4o-mini", "provider": "openai-demo", "version": "", "checked_at": "2025-01-01T12:00:00.123456+00:00", "request_id": "req-001" }, "token": "eyJtb2RlbCI6ICJncHQt...abc.signature" }如果响应中的模型名是gpt-4o-ornot,不在允许列表中,接口会返回类似下面的错误:
{ "detail": "model gpt-4o-ornot is not in allowed models: ['gpt-4o', 'gpt-4o-mini']" }这就是最基础的模型身份拦截能力。
6. 常见问题与排查思路
在实际接入过程中,模型身份校验会遇到不少问题。下面按高频问题整理了一份排查表。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| model 字段为空 | 上游服务返回结构不兼容 | 检查上游响应字段大小写与嵌套结构 |
| 模型名与白名单不一致 | 网关别名映射配置错误 | 核对网关模型映射配置,打印实际转发的模型名 |
| 校验通过但成本账单仍不准 | 只校验了 model 字符串,未校验版本 | 在身份对象中增加 model version 字段 |
| token 验签失败 | 网关密钥与消费方密钥不一致 | 统一密钥来源,避免硬编码到多个服务 |
| 流式响应中身份信息缺失 | 流式响应结构里没有 model 字段 | 在首个 chunk 或流结束元数据中获取模型字段 |
| 缓存命中的响应模型标识过期 | 网关缓存了上游响应,未同步更新模型映射 | 缓存 key 加入模型映射版本号 |
| 校验层拖慢请求延时 | 每次请求都做额外网络调用 | 本地白名单缓存 + 异步审计日志 |
下面展开两个典型问题。
第一个是上游响应结构不兼容。不同模型供应商的响应字段并不完全一致,有的是model,有的是model_id,有的是data.model。如果上游响应结构发生变化,简单取值就可能取到 None。建议在校验入口增加统一的响应结构解析层,而不是直接访问原始字典。
第二个是流式响应。Stream 场景下,响应以多个 chunk 陆续到达。模型身份信息可能只在第一个 chunk 里出现。这时候需要在流开始阶段提取模型标识并完成校验,而不能等到整个流结束后再校验。
7. 工程化与生产落地建议
7.1 密钥管理与最小权限
身份令牌的本质是可信关系。如果签名密钥泄露,攻击者就可以自行签发“合法”的模型身份令牌。
在生产环境要注意:
- 不使用默认密钥,不把密钥提交到代码仓库;
- 优先使用 KMS、Vault 等密钥管理服务;
- 密钥定期轮换,并保留一段新旧密钥并存窗口;
- 校验层只读所需的模型映射配置,不授予不必要的高权限。
7.2 身份校验层如何放置
模型身份校验层可以放在两个位置:
- 网关插件或中间件。优点是覆盖范围广,不用改业务代码。
- 网关之后的独立校验服务。优点是职责单一,方便灰度、升级和审计。
如果团队已有 LiteLLM、Kong、APISIX 等网关,建议优先选择插件或中间件方案。如果是自研网关,则可以在网关核心处理链路中预留一个校验 Hook。
7.3 流式响应与异步审计
对于流式请求,最好把“模型身份校验”和“流内容转发”做异步解耦:
- 在流开始时同步完成身份校验;
- 身份校验结果写入审计日志;
- 后续流的转发不再阻塞在身份校验上。
这样既满足审计要求,又不会明显增加用户等待时间。
7.4 可观测性
模型身份校验是安全治理的一部分,建议将校验结果暴露成指标:
- 校验总次数;
- 校验失败次数;
- 校验失败原因分布;
- 身份令牌签发成功率;
- 上游模型响应延迟。
这样当模型路由出现异常时,可以通过指标快速定位是配置问题、上游问题还是校验逻辑问题。
8. 总结与下一步学习建议
本文围绕 XTokenChecker 的模型身份校验思路,从 AI 网关的实际痛点出发,实现了以下能力:
- 模型身份对象的定义与序列化;
- 模型名字段校验和白名单校验;
- 基于 HMAC 的身份令牌签发与验签;
- 一个可运行的 FastAPI 接入示例。
你可以把这段代码继续扩展成:
- 支持多供应商的模型注册表;
- 支持数据库持久化的审计记录;
- 支持 Prometheus 指标的校验层;
- 支持流式响应的异步身份校验。
后续建议继续深入学习几个方向:一是网关流量治理,特别是模型路由与限流;二是可验证凭证与签名体系,比如 JWT 与 HMAC 的适用边界;三是成本治理,把模型身份校验结果与计费系统打通。
如果你正准备给 AI 网关补一套安全审计能力,可以先从本文的模型名白名单校验开始。不要一上来就上签名令牌,先把“身份信息能不能拿到、白名单配置是否可靠”这两个基础问题解决掉,再逐步叠加令牌验证,会让整个落地过程更稳。希望这篇能给你一些可落地的参考。
如果觉得本文对你有帮助,可以收藏备用,也欢迎在评论区聊聊你在 AI 网关中遇到的模型身份问题。