同一句"调模型",在官方 API 上是三行 HTTP,在云平台上是一套鉴权链、一个区域概念、一份配额单。Bedrock(AWS)和 Vertex AI(GCP)都不是模型本身,而是模型的托管入口:把多家模型放进同一个账单、同一个 IAM、同一个 VPC。
这篇不比较模型能力,只比较"接进来要付出什么",以及什么时候该加一层抽象、什么时候不该。
1. 云厂商模型服务在解决什么
| 诉求 | 云平台的做法 | 直连官方 API 的做法 |
|---|---|---|
| 合规与数据边界 | 走 VPC 私有端点、区域固定、审计日志 | 依赖厂商区域策略,审计在对方侧 |
| 网络 | 与业务同区域,低延迟、可走内网 | 公网出口,受限于出口带宽与稳定性 |
| 计费 | 与云账单合并,统一预算与成本分摊 | 单独账户、单独发票 |
| 多模型池 | 一个 SDK 入口切多家模型 | 每家一套 SDK 与鉴权 |
| 权限 | 复用 IAM / 服务账号,按角色授权 | 各自发 key,散落在各环境变量 |
| 配额 | 按区域按模型统一申请 | 按账号申请,口径不同 |
对照结论:云平台卖的是"治理",不是"模型"。 如果团队已经在用这套云(IAM、VPC、账单都在里面),接入的边际成本很低;如果只是想在个人项目里调一次模型,多出来的鉴权链纯属负担。
2. 鉴权:两套完全不同的思路
| 维度 | AWS Bedrock | GCP Vertex AI |
|---|---|---|
| 身份 | IAM 用户 / 角色 | 服务账号 / 用户账号 |
| 凭证形态 | Access Key + Secret,或临时凭证(STS) | 服务账号 JSON key,或 ADC |
| 请求签名 | SigV4(由 SDK 完成) | OAuth2 Bearer token(由 SDK 完成) |
| 权限粒度 | IAM policy,可精确到模型 ARN | IAM role,按资源与 API |
| 有效期 | 临时凭证有有效期,需刷新 | token 短期有效,ADC 自动刷新 |
| 本地开发 | aws configure / SSO 登录 |
gcloud auth application-default login |
# AWS:SDK 自动做 SigV4,凭证来自环境链(环境变量 → profile → 实例角色)
import boto3, json
client = boto3.client("bedrock-runtime", region_name="us-east-1")
resp = client.invoke_model(
modelId="MODEL_ID", # 具体模型标识,按当前版本核对可用列表
body=json.dumps({"prompt": "你好", "max_gen_len": 256}),
accept="application/json",
)
print(json.loads(resp["body"].read()))
# GCP:走应用默认凭证,避免把 key 文件塞进仓库
gcloud auth application-default login
export GOOGLE_CLOUD_PROJECT="PROJECT_ID"
export GOOGLE_CLOUD_REGION="us-central1"
临时凭证最典型的坑:在容器里挂了一个带有效期的 STS 凭证,进程跑得比凭证久,几小时后开始报鉴权失败,而重启后又正常。判断方法是看错误码是不是"过期/无效凭证"类,而不是权限不足类。
3. 区域与模型可用性
同一家模型,在不同区域的可选性并不一致,而且模型标识本身带区域语义。这带来两个工程约束:
| 约束 | 表现 | 处理 |
|---|---|---|
| 模型不在该区域 | 调用直接报"模型不存在/不可用" | 启动时做一次可用性探测,别等线上 |
| 跨区域延迟 | 请求绕路,P95 明显变差 | 业务与模型同区域部署 |
| 数据驻留要求 | 某些行业要求数据不出境 | 固定区域,禁用跨区回退 |
| 新模型上线节奏 | 各区域不同步 | 把模型标识做成配置项,不硬编码 |
工程上的做法是把"区域 + 模型标识"当成一对配置,而不是两个独立配置:
# application.yml(示意,值按当前版本核对)
llm:
provider: bedrock
region: ${LLM_REGION:us-east-1}
model-id: ${LLM_MODEL_ID:MODEL_ID}
fallback:
enabled: false # 跨区回退会违反数据驻留要求时关掉
4. 调用形态:同步、流式、批处理
| 形态 | 适用 | 注意点 |
|---|---|---|
| 同步(invoke) | 短请求、结果要立刻用 | 超时要设,别用 SDK 默认值 |
| 流式(stream) | 对话、首字延迟敏感 | 断流要能续,前端要能处理半截响应 |
| 批处理(batch job) | 离线大规模推理 | 输入输出走对象存储,格式有约定 |
| 异步长任务 | 视频/大文档 | 轮询或回调,别同步等 |
流式的实现差异会被 SDK 抹平一半,但事件结构不一样:Bedrock 返回的是带类型标签的事件流,Vertex 侧是增量文本块。做统一封装时,要在这一层做归一化,否则上层得为每家写一份解析。
5. 统一抽象层:什么时候值得加
| 方案 | 抽象层级 | 适合 |
|---|---|---|
| Spring AI | ChatClient + 模型实现 | Java/Spring 技术栈,想换模型只改配置 |
| LangChain4j | ChatLanguageModel + AiServices | 需要工具调用、RAG 组件的 Java 项目 |
| 自建薄封装 | 一个接口 + 两个实现 | 只用到一两个模型,不想引入大依赖 |
| 直接用厂商 SDK | 无 | 只用一家,且要用厂商特有能力 |
// LangChain4j 的形态:业务代码只依赖接口
ChatLanguageModel model = BedrockChatModel.builder()
.region(REGION)
.modelId(MODEL_ID)
.build();
String answer = model.generate("用三句话解释什么是重排");
// 换到 Vertex 只需替换实现类,业务代码不动
// ChatLanguageModel model = VertexGeminiChatModel.builder()...build();
抽象层的代价也要认清:它只能抹平"共同子集"。厂商特有的能力(比如某种结构化输出约束、某个专用工具协议)穿过抽象层时会被削平,这时候要么在抽象层开逃生口,要么就承认这层不值得。
6. 配额与限流
| 维度 | 常见现象 | 处理 |
|---|---|---|
| 按区域 | 同模型在 A 区够用、B 区一调就 429 | 配额按区域申请,配置里标明 |
| 按模型 | 换了模型标识,配额是另一套 | 上线前核对目标模型的配额 |
| RPM / TPM 双限 | 请求数没超,token 先超 | 限流器要同时算请求与 token |
| 突发流量 | 自动扩缩容把配额打满 | 客户端限流 + 队列,别指望云侧兜底 |
| 共享账号 | 多个服务抢同一配额 | 按服务分账号或分区域 |
限流不要在业务代码里散着写,统一放到模型客户端外层:一个令牌桶按 token 估重、一个按请求数,两个都过才放行。
7. 计费口径
| 计费方式 | 形态 | 适合 | 陷阱 |
|---|---|---|---|
| 按 token | 输入/输出分别计价 | 调用量波动大 | 长上下文与重试放大成本 |
| 按实例/时长 | 独占推理端点,按小时 | 稳定高并发 | 低峰期空转也在计费 |
| 按批处理任务 | 离线作业按量 | 大规模离线 | 结果延迟,不适合在线 |
| 按预置吞吐 | 预留容量 | 有明确峰值 | 预留不足仍会限流 |
成本归因的关键是在每个请求上打业务标签(租户、功能、版本),否则月底只能看到一个总数。云平台的标签体系能用就用,用不上就在应用侧记账。
8. 换厂商要改什么
| 改动点 | 具体内容 | 容易漏的地方 |
|---|---|---|
| 模型标识 | 名称体系完全不同 | 配置里散落多份,改漏一处就回退到默认 |
| 参数映射 | max_tokens / top_p / 停止符命名不同 |
语义相近但边界不同(默认值、上限) |
| 消息结构 | system 是独立字段还是消息角色 | 迁移后系统提示失效 |
| 工具调用 | schema 格式与回填方式不同 | 参数嵌套层数不一致 |
| 流式事件 | 事件类型与结束标记不同 | 结束判断依赖了某家的特定字段 |
| 嵌入维度 | 向量维度与归一化方式不同 | 向量库直接不兼容 |
| 错误码 | 限流/超限/内容策略的码不同 | 重试策略把不可重试的错也重试了 |
# 参数映射要显式做一层,别在调用点各写各的
def to_provider_params(p: dict, provider: str) -> dict:
if provider == "bedrock":
return {"max_gen_len": p["max_tokens"], "temperature": p["temperature"],
"top_p": p["top_p"]}
if provider == "vertex":
return {"max_output_tokens": p["max_tokens"],
"temperature": p["temperature"], "top_p": p["top_p"]}
raise ValueError(provider)
嵌入维度这一条是最容易被低估的:换嵌入模型等于换向量空间,旧向量全部失效,必须整库重建并重新索引。切模型前先确认这件事的代价。
9. 排错:七种接入后的典型报错
| 现象 | 原因 | 处理 |
|---|---|---|
| 报"模型不存在" | 模型在该区域不可用,或标识写错 | 按当前版本核对区域与标识 |
| 报鉴权失败且重启后恢复 | 临时凭证过期,进程长于凭证 | 用 SDK 的凭证链自动刷新,别硬编码 |
| 429 但请求数远低于预期 | token 维度先超限 | 限流器加 token 估算 |
| 流式响应中途断了 | 超时/网关缓冲/心跳缺失 | 检查中间层是否缓冲,补心跳与重试 |
| 从官方 API 迁过来参数报错 | 参数名与语义不同 | 走显式参数映射层 |
| 检索结果突然全变差 | 嵌入模型换了,向量空间不兼容 | 整库重建,别混用新旧向量 |
| 账单暴涨但 QPS 没变 | 重试放大或上下文变长 | 按请求打标签,看是哪类请求变贵 |
10. 边界:什么情况别上云平台
| 场景 | 更合适 | 理由 |
|---|---|---|
| 个人验证一个想法 | 官方 API | 鉴权链的成本大于收益 |
| 只用一家模型、不用其云资源 | 官方 SDK | 抽象层与云治理都白付 |
| 数据不能出内网 | 私有化部署的开源模型 | 托管服务无法满足边界要求 |
| 极低延迟要求(毫秒级) | 本地小模型 | 网络往返本身就是成本 |
| 需要模型厂商的最新特性 | 官方 API | 云平台上新通常滞后 |
一句话定位:Bedrock 和 Vertex 是"把模型纳入云治理"的入口,不是"更好的模型"。选它是因为你已经在治理体系里,不是因为它的模型更强。
11. 自测
- 云厂商模型服务真正卖的是什么,和直连官方 API 的取舍在哪?
- AWS 与 GCP 的鉴权在"凭证有效期"上的差异会引发什么线上问题?
- 为什么"区域"和"模型标识"应该当成一对配置?
- 统一抽象层能抹平什么,抹不平什么?
- 换嵌入模型时,向量库为什么必须整库重建?
- 什么情况下不该引入云平台模型服务?