生成式 AI 应用上线后,最先暴露的往往不是 prompt 写得好不好,而是系统能不能扛住流量、限额和依赖波动:突发访问把模型配额打满,单一区域推理不可用,多租户场景里某个客户把大家的吞吐吃光。围绕 Amazon Bedrock,可以从托管能力开始做韧性设计,再逐步引入 LLM gateway,把多模型、多区域、多租户策略收敛到统一入口。
下面按工程落地顺序拆成五个模式。
1. 先把 Bedrock 调用做成“可失败”的客户端
很多团队第一次接入 LLM 时,会把模型调用当成普通 HTTP API:请求、等待、返回。但 LLM 推理的失败形态更多:限流、超时、上游繁忙、输出过长、模型暂时不可用。客户端层应该显式处理这些情况。
可以这样实践:
- 设置超时,避免请求长时间挂住。
- 对可重试错误做指数退避。
- 区分用户错误和系统错误,prompt 太长、模型 ID 错误不应盲目重试。
- 记录 tenant、model、region、latency、retry_count,后续才能做容量判断。
下面是一个最小可改造的 Python 示例:优先调用主区域模型,遇到限流或临时故障时退避重试,再切到备用区域。
运行前需要:
- 配好 AWS 凭证和权限。
- 安装
boto3。 - 把
MODEL_ID换成你账号已启用的 Bedrock 模型 ID。
import json
import random
import time
from typing import Dict, Any
import boto3
from botocore.config import Config
from botocore.exceptions import ClientError, ReadTimeoutError, ConnectTimeoutError
MODEL_ID = 'anthropic.claude-3-haiku-20240307-v1:0'
REGIONS = ['us-east-1', 'us-west-2']
RETRYABLE_CODES = {
'ThrottlingException',
'TooManyRequestsException',
'ServiceUnavailableException',
'InternalServerException',
'ModelTimeoutException',
}
def bedrock_client(region: str):
return boto3.client(
'bedrock-runtime',
region_name=region,
config=Config(
connect_timeout=3,
read_timeout=30,
retries={'max_attempts': 1}, # 自己控制重试,避免多层重试放大流量
),
)
def build_body(prompt: str) -> Dict[str, Any]:
# 示例以 Anthropic Claude Messages API 形态演示;其他模型需要替换 body schema。
return {
'anthropic_version': 'bedrock-2023-05-31',
'max_tokens': 300,
'messages': [
{
'role': 'user',
'content': [{'type': 'text', 'text': prompt}],
}
],
}
def invoke_with_resilience(prompt: str, max_attempts_per_region: int = 3) -> str:
last_error = None
for region in REGIONS:
client = bedrock_client(region)
for attempt in range(1, max_attempts_per_region + 1):
started = time.time()
try:
resp = client.invoke_model(
modelId=MODEL_ID,
body=json.dumps(build_body(prompt)),
contentType='application/json',
accept='application/json',
)
payload = json.loads(resp['body'].read())
latency_ms = int((time.time() - started) * 1000)
print(f'region={region} attempt={attempt} latency_ms={latency_ms}')
return payload['content'][0]['text']
except (ReadTimeoutError, ConnectTimeoutError) as exc:
last_error = exc
retryable = True
except ClientError as exc:
last_error = exc
code = exc.response.get('Error', {}).get('Code', 'Unknown')
retryable = code in RETRYABLE_CODES
print(f'region={region} attempt={attempt} error_code={code}')
if not retryable:
raise
if attempt < max_attempts_per_region and retryable:
sleep_seconds = min(2 ** attempt, 8) + random.random()
time.sleep(sleep_seconds)
print(f'failover_to_next_region from={region}')
raise RuntimeError(f'All Bedrock regions failed: {last_error}')
if __name__ == '__main__':
answer = invoke_with_resilience('用三句话解释为什么 LLM 调用需要重试和降级。')
print(answer)
这个示例不是完整网关,但它体现了韧性设计的底线:失败是正常路径,不是异常路径。
2. 用多区域推理降低单点风险
当流量突然上涨时,单一区域的模型配额可能先被打满。摘要中提到的一个核心问题就是 quota exhaustion:不是代码坏了,而是你在某个区域、某个模型、某个账号维度的可用吞吐不够。
在 AWS 上可以优先利用 Amazon Bedrock 的原生能力做地域分布和推理弹性;如果业务对可用性要求更高,还可以在应用层维护区域优先级和健康状态。
工程上建议把区域策略做成配置,而不是写死在代码里:
# llm-routing.yaml
models:
chat-fast:
primary:
provider: bedrock
region: us-east-1
model_id: anthropic.claude-3-haiku-20240307-v1:0
fallbacks:
- provider: bedrock
region: us-west-2
model_id: anthropic.claude-3-haiku-20240307-v1:0
timeout_ms: 30000
retry:
max_attempts: 2
backoff_ms: 500
这种配置化的好处是:
- 变更备用区域时不需要重新发版。
- 可以给不同业务线配置不同模型和区域。
- 方便后续接入 LLM gateway,由网关读取同一套路由策略。
需要注意,多区域不是免费午餐。跨区域调用会引入额外延迟、数据驻留和合规问题。对含敏感数据的 prompt,应先确认数据是否允许离开当前区域。
3. 用容量隔离处理“吵闹邻居”
多租户应用里最容易出现 noisy neighbor:一个租户做批量任务,导致其他租户的在线请求也被限流。仅靠全局重试会让问题更糟,因为所有租户都在失败后继续放大请求。
更稳妥的做法是把限额放在业务边界上:
- 按 tenant 设置 QPS、并发数或 token 预算。
- 在线请求和后台批处理分池。
- 高优先级租户使用独立路由或预留容量。
- 当租户超过预算时,返回明确的 429,而不是拖慢所有人。
可以这样在网关前做一个简单的租户限流配置:
# tenant-limits.yaml
tenants:
free:
max_concurrent_requests: 2
requests_per_minute: 30
monthly_token_budget: 1000000
pro:
max_concurrent_requests: 10
requests_per_minute: 300
monthly_token_budget: 20000000
enterprise-a:
max_concurrent_requests: 50
requests_per_minute: 2000
monthly_token_budget: 500000000
preferred_route: chat-fast-dedicated
这类策略的关键不是 YAML,而是观测维度:每次调用都要带上 tenant id、route id、model id、region 和 token usage。否则你只能看到“Bedrock 被限流了”,看不到是谁制造了尖峰。
4. 把降级路径设计成产品能力,而不是事故脚本
LLM 应用的降级不应该只有“报错”。根据场景不同,可以准备不同级别的退路:
- 从大模型降到小模型,牺牲部分质量换取可用性。
- 从跨区域备用模型返回结果。
- 对非实时任务进入队列,稍后处理。
- 对可缓存问题返回最近一次结果。
- 对低优先级功能关闭生成,只保留检索或规则结果。
例如客服总结、标签生成、文档分类这类任务,很多时候可以接受延迟或较小模型;但交易确认、医疗建议、合规审查等场景不能随意降级,需要更严格的策略和人工兜底。
一个实用判断标准是:如果降级结果可能被用户当成高置信事实,就不要静默降级。要么标注“系统繁忙,已使用简化模型”,要么直接失败并提示重试。
5. 用 LLM Gateway 统一多模型编排
当应用只有一个服务、一个模型时,客户端重试足够。但一旦进入多模型、多区域、多租户阶段,韧性逻辑散落在各个业务服务里会非常难维护。
LLM gateway 的价值在这里体现出来:它把调用策略从业务代码中抽离出来,集中处理:
- 模型路由和 fallback。
- per-tenant 限流和预算。
- 熔断、重试、超时。
- 请求日志、token 计量和审计。
- 多模型供应商或多区域调度。
可以这样设计一个内部调用协议,让业务服务不直接关心 Bedrock 细节:
curl -X POST http://localhost:8080/v1/chat/completions \
-H 'Content-Type: application/json' \
-H 'X-Tenant-Id: enterprise-a' \
-d '{
"route": "chat-fast",
"messages": [
{"role": "user", "content": "把这段客服对话总结成三个要点。"}
],
"max_tokens": 300
}'
网关后面可以先只接 Amazon Bedrock。等业务增长后,再把多区域路由、不同模型档位、租户隔离逐步加进去。不要一开始就做成复杂平台,否则网关本身会成为新的单点和运维负担。
落地检查清单
把 Bedrock 应用做稳,可以按这个顺序推进:
- 客户端有超时、退避重试和错误分类。
- 模型、区域、超时、fallback 都配置化。
- 关键指标按 tenant、model、region 维度打点。
- 对 quota exhaustion 有预案:备用区域、备用模型或排队。
- 多租户系统有租户级预算和并发隔离。
- 降级行为经过产品确认,不让用户误解结果质量。
- 当调用策略开始复制到多个服务时,引入 LLM gateway。
韧性不是把错误全部藏起来,而是让系统在配额、区域和模型波动时仍然可控。Amazon Bedrock 提供了托管推理基础,LLM gateway 则把更复杂的路由和隔离策略变成工程化能力。两者配合,生成式 AI 应用才更接近可生产、可运营的系统。