踩坑记录与性能调优:常见问题与解决方案

本章汇总了在真实生产环境中使用 Go ADK 开发 Agent 系统时遇到的典型问题、踩坑经验和性能调优方案。这些内容来源于多个项目的实战总结,涵盖了从开发调试到生产运维的全生命周期。每一个问题都附有根因分析、解决方案和预防措施,力求让读者"踩一次坑,长一辈子智"。

API 层常见问题

错误 1:UNAUTHORIZED —— 认证失败

现象:Agent 启动或运行时返回 UNAUTHORIZED 错误,提示 API Key 无效。

根因分析

  1. 环境变量未加载.env 文件存在但未正确加载到进程环境
  2. Key 格式错误:复制时包含了多余空格或换行符
  3. Key 权限不足:API Key 未开通对应模型或服务的访问权限
  4. Key 已过期/被撤销:在 Google Cloud Console 中手动撤销或达到使用期限
  5. 多项目配置冲突GOOGLE_APPLICATION_CREDENTIALSGOOGLE_API_KEY 指向不同项目

解决方案

package main

import (
    "fmt"
    "os"
    "strings"
)

func validateAPIKey() error {
    key := os.Getenv("GOOGLE_API_KEY")
    
    if key == "" {
        return fmt.Errorf("GOOGLE_API_KEY 未设置")
    }
    
    // 去除首尾空白字符
    key = strings.TrimSpace(key)
    
    // 基础格式校验(Gemini API Key 通常为 39 位)
    if len(key) < 20 {
        return fmt.Errorf("GOOGLE_API_KEY 格式异常,长度过短: %d", len(key))
    }
    
    // 检查是否包含非法字符
    if strings.ContainsAny(key, "\n\r\t") {
        return fmt.Errorf("GOOGLE_API_KEY 包含非法空白字符")
    }
    
    // 设置回环境变量(去除空白后)
    os.Setenv("GOOGLE_API_KEY", key)
    
    return nil
}

func main() {
    // 优先加载 .env 文件
    if err := loadEnvFile(".env"); err != nil {
        log.Printf("加载 .env 文件失败(可能不存在): %v", err)
    }
    
    if err := validateAPIKey(); err != nil {
        log.Fatalf("API Key 验证失败: %v", err)
    }
    
    // 继续初始化...
}

func loadEnvFile(path string) error {
    data, err := os.ReadFile(path)
    if err != nil {
        return err
    }
    
    for _, line := range strings.Split(string(data), "\n") {
        line = strings.TrimSpace(line)
        if line == "" || strings.HasPrefix(line, "#") {
            continue
        }
        
        parts := strings.SplitN(line, "=", 2)
        if len(parts) != 2 {
            continue
        }
        
        key := strings.TrimSpace(parts[0])
        value := strings.TrimSpace(parts[1])
        
        // 去除可能的引号包裹
        value = strings.Trim(value, `"'`)
        
        os.Setenv(key, value)
    }
    
    return nil
}

预防措施

  • 在 CI/CD 流程中加入 API Key 格式校验步骤
  • 使用 Secret Manager(如 Google Secret Manager、AWS Secrets Manager)而非环境变量存储密钥
  • 实现 Key 健康检查端点,定期验证 Key 有效性

错误 2:RESOURCE_EXHAUSTED —— 配额耗尽

现象:请求返回 RESOURCE_EXHAUSTED,提示已达到速率限制或配额上限。

根因分析

  1. RPM(Requests Per Minute)超限:突发流量超过 API 的每分钟请求限制
  2. TPD(Tokens Per Day)超限:日 Token 消耗达到上限
  3. 并发连接数超限:同时建立的连接数超过限制
  4. 项目级配额未调整:默认配额较低,未根据业务需求申请提升

解决方案

package ratelimit

import (
    "context"
    "fmt"
    "sync"
    "time"

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

// AdaptiveRateLimiter 自适应限流器
type AdaptiveRateLimiter struct {
    limiter      *rate.Limiter
    mu           sync.RWMutex
    currentRPM   int
    minRPM       int
    maxRPM       int
    backoffUntil time.Time
}

func NewAdaptiveRateLimiter(minRPM, maxRPM int) *AdaptiveRateLimiter {
    return &AdaptiveRateLimiter{
        limiter:    rate.NewLimiter(rate.Every(time.Minute/time.Duration(maxRPM)), maxRPM),
        currentRPM: maxRPM,
        minRPM:     minRPM,
        maxRPM:     maxRPM,
    }
}

func (a *AdaptiveRateLimiter) Wait(ctx context.Context) error {
    a.mu.RLock()
    backoff := a.backoffUntil
    a.mu.RUnlock()
    
    if time.Now().Before(backoff) {
        select {
        case <-time.After(time.Until(backoff)):
        case <-ctx.Done():
            return ctx.Err()
        }
    }
    
    return a.limiter.Wait(ctx)
}

func (a *AdaptiveRateLimiter) OnSuccess() {
    a.mu.Lock()
    defer a.mu.Unlock()
    
    // 成功时缓慢恢复速率
    if a.currentRPM < a.maxRPM {
        a.currentRPM = min(a.currentRPM+1, a.maxRPM)
        a.limiter.SetLimit(rate.Every(time.Minute / time.Duration(a.currentRPM)))
    }
}

func (a *AdaptiveRateLimiter) OnRateLimit() {
    a.mu.Lock()
    defer a.mu.Unlock()
    
    // 触发限流时,降低速率并进入退避期
    a.currentRPM = max(a.currentRPM/2, a.minRPM)
    a.limiter.SetLimit(rate.Every(time.Minute / time.Duration(a.currentRPM)))
    a.backoffUntil = time.Now().Add(30 * time.Second)
    
    log.Printf("触发速率限制,降级至 %d RPM,退避 30 秒", a.currentRPM)
}

// 在 Agent 调用处使用
func callWithRateLimit(ctx context.Context, limiter *AdaptiveRateLimiter, fn func() error) error {
    if err := limiter.Wait(ctx); err != nil {
        return err
    }
    
    err := fn()
    if err != nil {
        if isRateLimitError(err) {
            limiter.OnRateLimit()
        }
        return err
    }
    
    limiter.OnSuccess()
    return nil
}

长期策略

  1. 申请提升配额(Google Cloud Console → Quotas)
  2. 实现多层缓存减少对 API 的依赖
  3. 使用模型降级策略:高峰期自动切换到更轻量的模型

错误 3:context deadline exceeded —— 请求超时

现象:请求超时,上下文被取消。

根因分析

  1. LLM 推理耗时过长:复杂推理或长上下文导致模型响应慢
  2. Tool 执行阻塞:数据库查询、外部 API 调用未设置超时
  3. 级联超时:外层超时设置过短,内层操作未完成就被取消
  4. 网络抖动:与 LLM API 之间的网络不稳定

解决方案

package timeout

import (
    "context"
    "fmt"
    "time"
)

// LayeredTimeout 分层超时管理
type LayeredTimeout struct {
    TotalTimeout    time.Duration  // 用户感知的总超时
    ModelTimeout    time.Duration  // LLM 调用超时
    ToolTimeout     time.Duration  // 单个 Tool 超时
    StreamChunkTimeout time.Duration // 流式输出单块超时
}

func DefaultLayeredTimeout() LayeredTimeout {
    return LayeredTimeout{
        TotalTimeout:       30 * time.Second,
        ModelTimeout:       20 * time.Second,
        ToolTimeout:        5 * time.Second,
        StreamChunkTimeout: 2 * time.Second,
    }
}

func (lt LayeredTimeout) Execute(ctx context.Context, fn func(context.Context) error) error {
    ctx, cancel := context.WithTimeout(ctx, lt.TotalTimeout)
    defer cancel()
    
    done := make(chan error, 1)
    go func() {
        done <- fn(ctx)
    }()
    
    select {
    case err := <-done:
        return err
    case <-ctx.Done():
        return fmt.Errorf("请求总超时 (%v): %w", lt.TotalTimeout, ctx.Err())
    }
}

// Tool 调用包装器
func CallToolWithTimeout(ctx context.Context, toolName string, timeout time.Duration, fn func(context.Context) (string, error)) (string, error) {
    ctx, cancel := context.WithTimeout(ctx, timeout)
    defer cancel()
    
    result, err := fn(ctx)
    if err != nil {
        if ctx.Err() == context.DeadlineExceeded {
            return fmt.Sprintf("[%s 查询超时,请稍后重试]", toolName), nil
        }
        return "", err
    }
    
    return result, nil
}

关键原则:总超时 > 模型超时 + Tool 超时 × 最大 Tool 调用次数,并预留 20% 缓冲。

性能问题诊断与调优

问题 1:响应延迟高

诊断流程

用户感知延迟高
[分解延迟] 使用分布式追踪(OpenTelemetry)拆解各阶段耗时
├─ 网络传输延迟?→ 检查 CDN、就近部署、连接池
├─ 模型推理延迟?→ 模型降级、Context Caching、Prompt 优化
├─ Tool 调用延迟?→ Tool 并行化、缓存、超时优化
├─ 路由决策延迟?→ 简化分类逻辑、缓存路由结果
└─ 序列化/反序列化?→ 使用更高效的协议(如 protobuf)

模型推理优化

// 1. Context Caching:缓存系统指令和重复上下文
func (a *Agent) buildPromptWithCache(ctx context.Context, userInput string) (string, error) {
    cacheKey := a.config.Instruction + a.session.GetSummary()
    
    if cached, ok := a.promptCache.Get(cacheKey); ok {
        // 复用缓存的上下文前缀,只追加用户输入
        return cached.(string) + "\nUser: " + userInput, nil
    }
    
    fullPrompt := a.config.Instruction + "\n" + a.session.GetHistory() + "\nUser: " + userInput
    a.promptCache.Set(cacheKey, fullPrompt, 10*time.Minute)
    return fullPrompt, nil
}

// 2. 模型选择策略:根据查询复杂度动态选择模型
func selectModel(query string, history []Message) string {
    // 简单查询使用轻量模型
    if isSimpleQuery(query) && len(history) < 3 {
        return "gemini-2.0-flash"  // 更快、更便宜
    }
    
    // 复杂推理使用专业模型
    if requiresComplexReasoning(query) {
        return "gemini-2.0-pro"
    }
    
    // 默认使用平衡型
    return "gemini-2.0-flash"
}

Tool 调用并行化

func (a *Agent) callToolsParallel(ctx context.Context, toolCalls []ToolCall) []ToolResult {
    var wg sync.WaitGroup
    results := make([]ToolResult, len(toolCalls))
    
    for i, tc := range toolCalls {
        wg.Add(1)
        go func(index int, call ToolCall) {
            defer wg.Done()
            
            result, err := a.executeTool(ctx, call)
            results[index] = ToolResult{
                ToolName: call.Name,
                Result:   result,
                Error:    err,
            }
        }(i, tc)
    }
    
    wg.Wait()
    return results
}

问题 2:内存暴涨与 OOM

根因分析

  1. Session 堆积:未清理的会话对象持续累积
  2. State 膨胀:用户在 State 中存储大量数据(如完整对话历史)
  3. ** goroutine 泄漏**:未正确关闭的流式连接或后台任务
  4. 缓存无限增长:未设置容量上限的缓存

解决方案

package memory

import (
    "runtime"
    "time"

    "github.com/shirou/gopsutil/mem"
)

// MemoryGuard 内存守护
type MemoryGuard struct {
    maxMemoryMB      uint64
    gcThreshold      float64
    sessionLimit     int
    checkInterval    time.Duration
}

func (g *MemoryGuard) Start(sessionManager *SessionManager) {
    ticker := time.NewTicker(g.checkInterval)
    go func() {
        for range ticker.C {
            v, _ := mem.VirtualMemory()
            usedPercent := v.UsedPercent
            
            if usedPercent > g.gcThreshold {
                log.Printf("内存使用率 %.1f%% 超过阈值 %.1f%%,触发清理", usedPercent, g.gcThreshold)
                
                // 1. 强制 GC
                runtime.GC()
                
                // 2. 清理过期 Session
                evicted := sessionManager.EvictExpired(30 * time.Minute)
                log.Printf("已清理 %d 个过期会话", evicted)
                
                // 3. 如果仍然紧张,清理最久未使用的 Session
                if v, _ := mem.VirtualMemory(); v.UsedPercent > g.gcThreshold {
                    lruEvicted := sessionManager.EvictLRU(g.sessionLimit / 2)
                    log.Printf("LRU 清理 %d 个会话", lruEvicted)
                }
            }
        }
    }()
}

// SessionManager 实现
type SessionManager struct {
    sessions    map[string]*Session
    mu          sync.RWMutex
    maxSize     int
    maxStateSize int  // 单个 State 最大大小(字节)
}

func (sm *SessionManager) SetState(sessionID string, key string, value interface{}) error {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    
    session, ok := sm.sessions[sessionID]
    if !ok {
        return fmt.Errorf("会话不存在: %s", sessionID)
    }
    
    // 检查 State 大小限制
    data, _ := json.Marshal(value)
    if len(data) > sm.maxStateSize {
        return fmt.Errorf("State 数据过大: %d bytes,最大允许 %d", len(data), sm.maxStateSize)
    }
    
    session.State[key] = value
    session.LastActive = time.Now()
    
    return nil
}

问题 3: goroutine 泄漏

常见泄漏场景

// 错误示例:未关闭的流式通道
func badStream() {
    ch := make(chan string)
    go func() {
        for {
            select {
            case msg := <-someSource:
                ch <- msg  // 如果消费者已离开,这里会永远阻塞
            }
        }
    }()
    return ch
}

// 正确示例:使用 context 控制生命周期
func goodStream(ctx context.Context) <-chan string {
    ch := make(chan string)
    go func() {
        defer close(ch)
        for {
            select {
            case msg := <-someSource:
                select {
                case ch <- msg:
                case <-ctx.Done():
                    return
                }
            case <-ctx.Done():
                return
            }
        }
    }()
    return ch
}

检测方法

import "runtime"

// 监控 goroutine 数量
func monitorGoroutines() {
    ticker := time.NewTicker(1 * time.Minute)
    go func() {
        baseline := runtime.NumGoroutine()
        for range ticker.C {
            current := runtime.NumGoroutine()
            if current > baseline*2 {
                log.Printf("[ALERT] goroutine 数量异常: %d (基线: %d)", current, baseline)
                // 输出 goroutine 堆栈用于分析
                buf := make([]byte, 1<<20)
                n := runtime.Stack(buf, true)
                log.Printf("当前 goroutine 堆栈:\n%s", buf[:n])
            }
        }
    }()
}

调试技巧与工具

结构化日志

package logging

import (
    "context"
    "encoding/json"
    "log"
    "os"
    "time"
)

type AgentLogger struct {
    logger *log.Logger
    level  LogLevel
}

type LogLevel int

const (
    DEBUG LogLevel = iota
    INFO
    WARN
    ERROR
)

type LogEntry struct {
    Timestamp   time.Time              `json:"timestamp"`
    Level       string                 `json:"level"`
    Agent       string                 `json:"agent"`
    SessionID   string                 `json:"session_id"`
    Phase       string                 `json:"phase"`
    Message     string                 `json:"message"`
    Duration    int64                  `json:"duration_ms,omitempty"`
    Error       string                 `json:"error,omitempty"`
    Metadata    map[string]interface{} `json:"metadata,omitempty"`
}

func (l *AgentLogger) Log(ctx context.Context, level LogLevel, phase, message string, metadata map[string]interface{}) {
    if level < l.level {
        return
    }
    
    entry := LogEntry{
        Timestamp: time.Now(),
        Level:     level.String(),
        Agent:     ctx.Value("agent_name").(string),
        SessionID: ctx.Value("session_id").(string),
        Phase:     phase,
        Message:   message,
        Metadata:  metadata,
    }
    
    if duration, ok := metadata["duration"]; ok {
        entry.Duration = duration.(int64)
        delete(metadata, "duration")
    }
    
    if err, ok := metadata["error"]; ok {
        entry.Error = err.(error).Error()
        delete(metadata, "error")
    }
    
    data, _ := json.Marshal(entry)
    l.logger.Println(string(data))
}

// 使用示例
func init() {
    logger := &AgentLogger{
        logger: log.New(os.Stdout, "", 0),
        level:  INFO,
    }
    
    // 在 Callback 中使用
    callback.BeforeRun(func(ctx context.Context, input *callback.RunInput) {
        logger.Log(ctx, INFO, "before_run", "开始处理用户请求", map[string]interface{}{
            "input_length": len(input.Text),
        })
    })
}

本地调试配置

// debug.go
// +build debug

package main

import (
    "net/http"
    _ "net/http/pprof"
)

func init() {
    go func() {
        log.Println("启动 pprof 调试服务: http://localhost:6060")
        http.ListenAndServe("localhost:6060", nil)
    }()
}

启动调试版本:go run -tags debug ./cmd/server

请求追踪

func traceMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        requestID := r.Header.Get("X-Request-ID")
        if requestID == "" {
            requestID = generateRequestID()
        }
        
        ctx := context.WithValue(r.Context(), "request_id", requestID)
        ctx = context.WithValue(ctx, "start_time", time.Now())
        
        w.Header().Set("X-Request-ID", requestID)
        
        next.ServeHTTP(w, r.WithContext(ctx))
        
        duration := time.Since(ctx.Value("start_time").(time.Time))
        log.Printf("[%s] %s %s - %v", requestID, r.Method, r.URL.Path, duration)
    })
}

部署与运维陷阱

陷阱:配置漂移

开发环境、测试环境和生产环境使用不同的配置方式(环境变量、配置文件、硬编码),导致"在我机器上能跑"的问题。

解决方案:使用统一的配置管理

type Config struct {
    Environment     string `env:"ENV" envDefault:"development"`
    Port            int    `env:"PORT" envDefault:"8080"`
    GoogleAPIKey    string `env:"GOOGLE_API_KEY" required:"true"`
    RedisURL        string `env:"REDIS_URL" envDefault:"redis://localhost:6379"`
    LogLevel        string `env:"LOG_LEVEL" envDefault:"info"`
    MaxConcurrency  int    `env:"MAX_CONCURRENCY" envDefault:"100"`
    
    // 模型配置
    Model           string `env:"MODEL" envDefault:"gemini-2.0-flash"`
    ModelTimeout    int    `env:"MODEL_TIMEOUT_SEC" envDefault:"30"`
    
    // 限流配置
    RateLimitRPM    int    `env:"RATE_LIMIT_RPM" envDefault:"60"`
}

func LoadConfig() (*Config, error) {
    var cfg Config
    if err := env.Parse(&cfg); err != nil {
        return nil, fmt.Errorf("配置解析失败: %w", err)
    }
    return &cfg, nil
}

陷阱:优雅退出缺失

Agent 服务在部署更新时直接终止,导致进行中的请求被中断。

解决方案

func main() {
    // ... 初始化 ...
    
    srv := &http.Server{
        Addr:    fmt.Sprintf(":%d", cfg.Port),
        Handler: router,
    }
    
    // 启动服务
    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("服务启动失败: %v", err)
        }
    }()
    
    // 等待中断信号
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    
    log.Println("正在优雅关闭服务...")
    
    // 给进行中的请求 30 秒完成
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    if err := srv.Shutdown(ctx); err != nil {
        log.Printf("服务关闭异常: %v", err)
    }
    
    // 关闭 Plugin
    for _, p := range plugins {
        p.Shutdown(ctx)
    }
    
    log.Println("服务已安全关闭")
}

小结

踩坑记录完成。本章涵盖的核心经验:

  1. API 层:认证、配额、超时是最常见的三类错误,需要系统性的防御机制
  2. 性能层:延迟优化需要端到端的追踪分析,内存管理需要主动监控和清理
  3. 调试层:结构化日志、分布式追踪、pprof 是排查问题的三大利器
  4. 运维层:统一配置管理和优雅退出是生产部署的基本要求

接下来进入最后一个主题——Evaluation:Agent 效果评估。

端到端项目实战 | Evaluation →


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