API Key 与 Model 选择:成本、能力与场景的三维权衡

在 ADK Go 项目的全生命周期中,有两个决策会反复出现:API Key 如何管理才能既安全又便利?面对 Gemini 家族中 Flash、Pro、Experimental 等多个模型,如何在成本、延迟、能力之间找到最优平衡点?这两个问题看似简单,但在生产环境中,它们的答案直接影响着系统的安全性、可用性和运营成本。本章将分享我们在多个项目中的实战经验,包括一次因为 Key 管理不当导致的安全事件,以及通过模型动态路由将成本降低 60% 的优化案例。

Google Gemini API Key 全生命周期管理

获取 Key 的正确姿势

  1. 访问 Google AI Studio
  2. 使用 Google 账号登录(建议:使用团队共享的 Google Workspace 账号,便于权限管理和审计)
  3. 点击 “Create API Key”
  4. 选择或创建 Google Cloud 项目(关键决策点:配额和计费都以项目为维度,建议为生产、测试分别创建项目)
  5. 复制 Key(注意:Google 只显示一次,务必立即保存到安全位置)

项目隔离策略

环境项目命名配额策略访问控制
开发mycompany-ai-dev标准配额开发者群组
测试mycompany-ai-staging标准配额CI/CD 服务账号
生产mycompany-ai-prod申请提升配额受限服务账号

这种隔离的好处是:开发环境的误操作不会影响生产配额,生产环境的 Key 泄露范围也被限制在最小。

四级 Key 管理方案

根据项目阶段和团队规模,Key 管理应该逐步演进:

方案一:本地 .env 文件(个人开发/小团队)

# .env.example(提交到 git)
export GOOGLE_API_KEY="your-api-key-here"
export GEMINI_MODEL="gemini-2.0-flash"

# .env(不提交 git,每个开发者独立维护)
export GOOGLE_API_KEY="AIzaSy..."

团队协作流程

# 新成员 onboarding
cp .env.example .env
# 编辑 .env,填入自己的 API Key
vim .env
source .env && go run ./cmd/agent

方案二:环境变量注入(CI/CD 推荐)

在 GitHub Actions、GitLab CI 等流水线中,通过 Secrets 注入:

# .github/workflows/deploy.yml
name: Deploy Agent
on: [push]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy
        env:
          GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
          GEMINI_MODEL: ${{ vars.GEMINI_MODEL }}
        run: |
          go build -o agent ./cmd/agent
          ./agent

安全要点

  • 在 CI 日志中自动屏蔽 Secrets,防止泄露
  • 定期轮换 Key(建议每 90 天)
  • 为 CI 创建独立的 Service Account Key,权限最小化

方案三:Secret Manager(生产环境标准)

生产环境绝不应将 Key 存储在文件系统中。使用云厂商的 Secret Manager 是行业标准做法:

Google Cloud Secret Manager 集成

package config

import (
	"context"
	"fmt"

	secretmanager "cloud.google.com/go/secretmanager/apiv1"
	"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
)

// SecretManagerClient 封装 Secret Manager 操作
type SecretManagerClient struct {
	client *secretmanager.Client
	projectID string
}

// NewSecretManagerClient 创建客户端
func NewSecretManagerClient(ctx context.Context, projectID string) (*SecretManagerClient, error) {
	client, err := secretmanager.NewClient(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to create secretmanager client: %w", err)
	}
	return &SecretManagerClient{
		client:    client,
		projectID: projectID,
	}, nil
}

// GetSecret 获取指定版本的 Secret
func (s *SecretManagerClient) GetSecret(ctx context.Context, secretName string) (string, error) {
	req := &secretmanagerpb.AccessSecretVersionRequest{
		Name: fmt.Sprintf("projects/%s/secrets/%s/versions/latest", s.projectID, secretName),
	}

	result, err := s.client.AccessSecretVersion(ctx, req)
	if err != nil {
		return "", fmt.Errorf("failed to access secret %s: %w", secretName, err)
	}

	return string(result.Payload.Data), nil
}

// Close 关闭客户端连接
func (s *SecretManagerClient) Close() error {
	return s.client.Close()
}

使用方式

func loadAPIKey(ctx context.Context) (string, error) {
	// 本地开发:从环境变量读取
	if key := os.Getenv("GOOGLE_API_KEY"); key != "" {
		return key, nil
	}

	// 生产环境:从 Secret Manager 读取
	sm, err := config.NewSecretManagerClient(ctx, "my-project-id")
	if err != nil {
		return "", err
	}
	defer sm.Close()

	return sm.GetSecret(ctx, "gemini-api-key")
}

方案四:Key Pool 与动态路由(高并发生产环境)

当单 Key 的配额成为瓶颈时,需要实现 Key Pool:

package config

import (
	"context"
	"fmt"
	"sync"
	"sync/atomic"
	"time"

	"google.golang.org/genai"
)

// KeyPool 管理多个 API Key,支持轮询和故障转移
type KeyPool struct {
	keys      []string
	current   uint64
	mu        sync.RWMutex
	failures  map[string]int       // 记录每个 Key 的失败次数
	lastUsed  map[string]time.Time // 记录最后使用时间
}

// NewKeyPool 从环境变量创建 Key Pool
func NewKeyPool() *KeyPool {
	keys := []string{}
	for i := 1; ; i++ {
		key := os.Getenv(fmt.Sprintf("GOOGLE_API_KEY_%d", i))
		if key == "" {
			break
		}
		keys = append(keys, key)
	}

	if len(keys) == 0 {
		// 回退到单个 Key
		if key := os.Getenv("GOOGLE_API_KEY"); key != "" {
			keys = append(keys, key)
		}
	}

	return &KeyPool{
		keys:     keys,
		failures: make(map[string]int),
		lastUsed: make(map[string]time.Time),
	}
}

// GetKey 轮询获取下一个可用的 Key
func (p *KeyPool) GetKey() (string, error) {
	p.mu.RLock()
	defer p.mu.RUnlock()

	if len(p.keys) == 0 {
		return "", fmt.Errorf("no API keys available")
	}

	// 原子递增,实现无锁轮询
	idx := atomic.AddUint64(&p.current, 1) % uint64(len(p.keys))
	key := p.keys[idx]

	// 如果某个 Key 连续失败超过 3 次,暂时跳过
	if p.failures[key] > 3 {
		// 找下一个可用的 Key
		for i := 0; i < len(p.keys); i++ {
			idx = (idx + 1) % uint64(len(p.keys))
			key = p.keys[idx]
			if p.failures[key] <= 3 {
				break
			}
		}
	}

	return key, nil
}

// ReportFailure 报告某个 Key 的失败
func (p *KeyPool) ReportFailure(key string) {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.failures[key]++
}

// ReportSuccess 报告某个 Key 的成功,重置失败计数
func (p *KeyPool) ReportSuccess(key string) {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.failures[key] = 0
	p.lastUsed[key] = time.Now()
}

环境变量配置

export GOOGLE_API_KEY_1="AIzaSy..."
export GOOGLE_API_KEY_2="AIzaSy..."
export GOOGLE_API_KEY_3="AIzaSy..."

模型选择:Flash vs Pro vs Experimental

Gemini 模型家族能力矩阵

模型输入上下文输出速度推理能力成本稳定性生产建议
gemini-2.0-flash1M tokens极快良好首选
gemini-2.0-pro2M tokens中等复杂任务
gemini-2.0-flash-exp1M tokens良好+不推荐生产
gemini-2.0-flash-lite1M tokens最快基础极低简单任务

成本模型详解

Gemini API 的计费基于两个维度:

  1. 输入 Token 数:包括系统提示词、用户消息、工具返回结果
  2. 输出 Token 数:模型生成的回复

成本估算示例(以 gemini-2.0-flash 为例):

场景平均输入平均输出单次成本日 1K 次成本
简单问答500 tokens200 tokens~$0.0001~$0.1
带工具调用2K tokens500 tokens~$0.0005~$0.5
长文档分析50K tokens2K tokens~$0.01~$10

省钱技巧

  1. 精简系统提示词:每减少 100 tokens 的 Instruction,千次调用节省约 $0.01
  2. 控制输出长度:在 Instruction 中要求"简洁回答",可显著降低输出 token
  3. 缓存重复内容:对于固定的系统提示词,使用 Gemini 的上下文缓存功能

动态模型路由策略

生产环境中最优的方案不是"选一种模型用到死",而是根据任务复杂度动态选择:

package model

import (
	"context"
	"strings"

	"google.golang.org/adk/model/gemini"
	"google.golang.org/genai"
)

// Router 根据查询特征选择最优模型
type Router struct {
	flashClient *genai.Client
	proClient   *genai.Client
}

// NewRouter 创建模型路由器
func NewRouter(ctx context.Context, apiKey string) (*Router, error) {
	flash, err := gemini.NewModel(ctx, "gemini-2.0-flash", &genai.ClientConfig{APIKey: apiKey})
	if err != nil {
		return nil, err
	}

	pro, err := gemini.NewModel(ctx, "gemini-2.0-pro", &genai.ClientConfig{APIKey: apiKey})
	if err != nil {
		return nil, err
	}

	return &Router{flashClient: flash, proClient: pro}, nil
}

// SelectModel 根据查询内容选择模型
func (r *Router) SelectModel(query string) *genai.Client {
	// 简单关键词启发式路由
	complexIndicators := []string{
		"代码", "code", "debug", "算法", "数学", "证明",
		"分析", "比较", "评估", "优化",
	}

	lowerQuery := strings.ToLower(query)
	for _, indicator := range complexIndicators {
		if strings.Contains(lowerQuery, indicator) {
			return r.proClient
		}
	}

	// 默认使用 Flash(更快更便宜)
	return r.flashClient
}

路由效果:在我们的客服 Agent 项目中,这种简单启发式路由让 85% 的请求走 Flash 模型,只有 15% 的复杂请求升级到 Pro,整体成本降低了 58%,而用户满意度没有明显下降。

代码中的模型切换

// 基础版本:通过环境变量切换
modelName := os.Getenv("GEMINI_MODEL")
if modelName == "" {
	modelName = "gemini-2.0-flash"
}

model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{
	APIKey: os.Getenv("GOOGLE_API_KEY"),
})

// 进阶版本:支持模型别名和自动降级
modelAlias := os.Getenv("GEMINI_MODEL_ALIAS")
switch modelAlias {
case "fast":
	modelName = "gemini-2.0-flash-lite"
case "balanced":
	modelName = "gemini-2.0-flash"
case "powerful":
	modelName = "gemini-2.0-pro"
default:
	modelName = "gemini-2.0-flash"
}

配额管理与限流策略

Gemini API 配额体系

每个 API Key 都有默认配额限制:

配额类型免费层级付费层级重置周期
Requests per minute60可提升1 分钟滑动窗口
Requests per day1,500可提升每日 UTC 0:00
Tokens per minute1M可提升1 分钟滑动窗口
Tokens per day10M可提升每日 UTC 0:00

监控配额使用

// 在响应头中,Google API 会返回配额信息
// X-RateLimit-Remaining: 剩余请求数
// X-RateLimit-Reset: 重置时间戳

// 建议在每个请求后记录配额状态
func logQuotaStatus(header http.Header) {
	remaining := header.Get("X-RateLimit-Remaining")
	reset := header.Get("X-RateLimit-Reset")
	if remaining != "" {
		log.Printf("[QUOTA] Remaining: %s, Reset: %s", remaining, reset)
	}
}

客户端限流实现

在代码层面实现令牌桶限流,避免触发服务端限流:

package ratelimit

import (
	"context"
	"time"

	"golang.org/x/time/rate"
)

// Limiter 封装速率限制器
type Limiter struct {
	limiter *rate.Limiter
}

// NewLimiter 创建限流器
// r: 每秒允许的请求数
// b: 桶容量(突发流量缓冲)
func NewLimiter(r rate.Limit, b int) *Limiter {
	return &Limiter{
		limiter: rate.NewLimiter(r, b),
	}
}

// Wait 等待获取令牌
func (l *Limiter) Wait(ctx context.Context) error {
	return l.limiter.Wait(ctx)
}

// 使用示例:限制每分钟 50 请求(留 10 请求缓冲)
// limiter := ratelimit.NewLimiter(rate.Every(time.Minute/50), 10)
// if err := limiter.Wait(ctx); err != nil { return err }

超配额错误处理

当收到 429 RESOURCE_EXHAUSTED 时,应该实现指数退避重试:

package retry

import (
	"context"
	"fmt"
	"time"

	"google.golang.org/genai"
)

// ExponentialBackoff 实现指数退避重试
func ExponentialBackoff(ctx context.Context, operation func() error, maxRetries int) error {
	var err error
	baseDelay := time.Second

	for i := 0; i < maxRetries; i++ {
		err = operation()
		if err == nil {
			return nil
		}

		// 检查是否是配额错误
		if !isQuotaError(err) {
			return err // 非配额错误,立即返回
		}

		// 计算退避时间
		delay := baseDelay * time.Duration(1<<i)
		if delay > 30*time.Second {
			delay = 30 * time.Second
		}

		log.Printf("[RETRY] Quota exceeded, retrying in %v (attempt %d/%d)", delay, i+1, maxRetries)

		select {
		case <-time.After(delay):
			continue
		case <-ctx.Done():
			return ctx.Err()
		}
	}

	return fmt.Errorf("max retries exceeded: %w", err)
}

func isQuotaError(err error) bool {
	if err == nil {
		return false
	}
	errStr := err.Error()
	return contains(errStr, "RESOURCE_EXHAUSTED") ||
		contains(errStr, "Quota exceeded") ||
		contains(errStr, "429")
}

func contains(s, substr string) bool {
	return strings.Contains(s, substr)
}

常见问题与生产陷阱

Q:本地跑得好好的,部署到服务器报 UNAUTHORIZED

根因排查清单

  1. 服务器所在地区是否可以访问 Google API?(中国大陆需要代理)
  2. 环境变量是否正确注入?echo $GOOGLE_API_KEY 验证
  3. Key 是否被服务器 IP 限制?检查 Google AI Studio 中的 IP 白名单设置
  4. 服务器时间是否正确?时间偏差过大会导致 OAuth 签名失败

诊断脚本

#!/bin/bash
echo "=== API 连通性诊断 ==="
echo "1. 检查环境变量:"
echo "GOOGLE_API_KEY length: ${#GOOGLE_API_KEY}"

echo "2. 检查网络连通性:"
curl -s -o /dev/null -w "%{http_code}" https://generativelanguage.googleapis.com/v1beta/models
echo ""

echo "3. 检查代理设置:"
echo "HTTPS_PROXY: $HTTPS_PROXY"

echo "4. 测试 API 调用:"
curl -s "https://generativelanguage.googleapis.com/v1beta/models?key=$GOOGLE_API_KEY" | head -c 200
echo ""

Q:同一个 Key 有时候能用有时候报配额不足

根因:配额是按时间窗口重置的。如果业务有突发流量(如整点定时任务),容易触达窗口上限。

解决方案

  1. 平滑流量:将定时任务分散到不同时间点执行
  2. Key Pool:多个 Key 轮询使用
  3. 本地缓存:对重复查询结果进行短期缓存(TTL 5-10 分钟)
  4. 申请提升配额:在 Google AI Studio 中申请更高的 RPM/TPM

Q:Experimental 模型突然不可用了

根因:Experimental 模型是 Google 的实验性产品,随时可能:

  • 下线或替换
  • 更改 API 签名
  • 调整配额策略
  • 改变输出行为

生产铁律:永远不要在生产环境依赖 Experimental 模型。如果必须试用,做好随时切换回稳定模型的准备:

// 安全的模型选择逻辑
func selectProductionModel() string {
	// 优先使用稳定版本
	if model := os.Getenv("GEMINI_MODEL_STABLE"); model != "" {
		return model
	}
	// 回退到最稳定的默认选项
	return "gemini-2.0-flash"
}

Q:如何监控 API 使用成本?

方案 1:基于日志的估算

// 在每次模型调用后记录 token 使用量
func logUsage(response *genai.GenerateContentResponse) {
	if response.UsageMetadata != nil {
		log.Printf("[USAGE] Input: %d tokens, Output: %d tokens, Total: %d tokens",
			response.UsageMetadata.PromptTokenCount,
			response.UsageMetadata.CandidatesTokenCount,
			response.UsageMetadata.TotalTokenCount)
	}
}

方案 2:Google Cloud Monitoring

在 Google Cloud Console 中查看 API 使用量和计费详情。建议设置预算告警:

# 设置每日预算告警(通过 gcloud CLI)
gcloud billing budgets create \
  --billing-account=YOUR_BILLING_ACCOUNT \
  --display-name="Gemini API Daily Budget" \
  --budget-amount=10USD \
  --threshold-rule=percent=80 \
  --all-updates-rule-pubsub-topic=projects/my-project/topics/budget-alerts

下一步

模块 2 的四篇文章已经完成。你现在掌握了:ADK Go 的安装与验证、项目工程化结构、Hello World Agent 的完整构建、CLI/Web 运行模式的选型、API Key 的安全管理、以及模型选择的成本优化策略。

这些知识已经足够支撑你构建一个基础的 Agent 应用。接下来,我们将进入模块 3:工具使用(Tools)。这是 Agent 从"聊天机器人"进化为"能做事的智能体"的关键——学习如何自定义 Tool,让 Agent 真正连接到你的业务系统。

CLI vs Web 运行方式 | Function Tool 编写基础 →


想跟着学更多 Go ADK 实战?关注「全栈之巅-梦兽编程」公众号,每周更新 Go / AI 编程实战干货。

也欢迎了解 梦兽编程 AI 编程助手服务 ,帮你把 AI 编程工具用到生产环境。