Grounding:搜索增强生成,让回答更可靠

大型语言模型(LLM)的核心能力在于基于训练数据生成连贯、合理的文本,但其固有的知识截止(Knowledge Cutoff)和幻觉(Hallucination)问题在严肃的生产环境中是不可接受的。当用户询问"2025 年最新的 Go 语言版本特性"或"某家公司的最新财报数据"时,模型要么基于过时的知识给出错误答案,要么凭空编造看似合理实则虚假的信息。Grounding(搜索增强生成)正是为了解决这一根本性问题而设计的架构模式——它强制 Agent 在生成回答之前,先从外部权威信息源获取实时、可验证的事实依据,从而将生成过程锚定在真实世界之上。

为什么 Grounding 是生产级 Agent 的必备能力

在深入技术实现之前,我们需要理解 Grounding 在 AI 应用架构中的战略地位。未经过 Grounding 的 LLM 应用本质上是一个"封闭系统",其输出质量完全受限于预训练数据的边界。这在以下场景中会导致严重问题:

1. 时效性敏感领域 金融分析、新闻摘要、技术文档查询等场景要求信息必须是最新的。一个基于 2024 年初训练数据的模型在 2025 年讨论 Go 1.24 的新特性时,必然会出现知识盲区。

2. 事实准确性要求高的领域 医疗咨询、法律咨询、工程规范查询等场景中,事实错误可能导致严重后果。Grounding 通过引入可溯源的搜索结果,为回答提供了"证据链"。

3. 动态知识库场景 企业内部文档、产品手册、库存数据等每天都在变化。Grounding 允许 Agent 实时查询这些动态数据源,而不是依赖静态的训练知识。

从架构角度看,Grounding 实现了从"纯生成"(Pure Generation)到"检索增强生成"(Retrieval-Augmented Generation, RAG)的范式转变。ADK Go 提供的 Google Search Grounding 是一种特定实现,其原理与通用 RAG 架构一致:检索(Retrieve)→ 增强(Augment)→ 生成(Generate)。

核心架构与数据流

理解 Grounding 的完整数据流对于在生产环境中调试和优化至关重要。一个典型的 Grounding 请求在 ADK Go 内部经历了以下阶段:

用户输入
[意图分析] —— 模型判断是否需要外部信息
[查询生成] —— 从输入中提取或重构搜索查询
[搜索执行] —— 调用 Google Search API 获取实时结果
[结果筛选] —— 相关性排序、去重、截断
[上下文组装] —— 将搜索结果注入 Prompt 上下文
[生成回答] —— 基于增强后的上下文生成最终回复
[引用标注] —— 在回答中标注信息来源(如支持)

这个流水线中的每一个环节都可能成为性能瓶颈或质量衰减点。例如,查询生成阶段如果提取的关键词过于宽泛,会返回大量无关结果,稀释有效上下文;结果筛选阶段如果截断策略不当,可能丢失关键信息;上下文组装阶段如果不控制注入文本的长度,可能超出模型的上下文窗口限制。

在 ADK Go 中启用 Grounding

ADK Go 的 grounding 包提供了开箱即用的 Google Search Grounding 能力。以下是一个经过生产环境验证的完整配置示例:

package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    "google.golang.org/adk/agent"
    "google.golang.org/adk/grounding"
    "google.golang.org/adk/llm"
)

func main() {
    ctx := context.Background()

    // 初始化模型
    model, err := llm.NewGeminiModel(ctx, llm.GeminiConfig{
        APIKey:   os.Getenv("GOOGLE_API_KEY"),
        Model:    "gemini-2.0-flash",
        // 为 Grounding 预留足够的上下文窗口
        MaxTokens: 8192,
    })
    if err != nil {
        log.Fatalf("模型初始化失败: %v", err)
    }

    // 配置 Grounding 选项
    grounder, err := grounding.NewGoogleSearchGrounder(
        grounding.WithMaxResults(5),           // 最多使用 5 条搜索结果
        grounding.WithResultTimeout(10*time.Second), // 搜索超时 10 秒
        grounding.WithLanguageHint("zh-CN"),   // 优先中文结果
    )
    if err != nil {
        log.Fatalf("Grounding 初始化失败: %v", err)
    }

    // 创建 Agent
    agent, err := agent.New(agent.Config{
        Name:  "grounded-research-agent",
        Model: model,
        Instruction: `你是一个研究助手。在回答用户问题时,你必须:
1. 优先使用搜索结果中的事实
2. 如果搜索结果不足以回答问题,明确说明"根据现有搜索结果,无法确定..."
3. 在回答中引用信息来源
4. 对于时效性强的信息,注明信息的时间戳`,
        Grounding: grounder,
        // 启用引用标注,让用户可以追溯到原始来源
        EnableCitations: true,
    })
    if err != nil {
        log.Fatalf("Agent 创建失败: %v", err)
    }

    // 执行查询
    resp, err := agent.Run(ctx, "2025 年 Go 语言最新版本有哪些重要特性?")
    if err != nil {
        log.Fatalf("执行失败: %v", err)
    }

    fmt.Println(resp.Text)
    // 输出中可能包含类似 [1] 的引用标记,对应搜索结果
}

关键配置参数解析

参数作用生产建议
MaxResults控制注入上下文的搜索结果数量3-5 条为宜,过多会稀释注意力且增加 Token 消耗
ResultTimeout搜索 API 的超时时间5-15 秒,需配合整体请求超时设计
LanguageHint搜索语言偏好根据用户群体设置,多语言场景可动态调整
EnableCitations是否在回答中添加引用标记建议开启,提升可信度

高级模式:混合 Grounding 策略

在生产环境中,单一的 Google Search Grounding 往往不足以覆盖所有场景。一个更健壮的架构是"混合 Grounding"——根据查询类型动态选择信息源:

type HybridGrounder struct {
    webGrounder     *grounding.GoogleSearchGrounder
    internalRAG     *InternalRAGClient      // 企业内部文档检索
    knowledgeBase   *KnowledgeBaseClient    // 结构化知识库
}

func (h *HybridGrounder) Ground(ctx context.Context, query string) (*grounding.Result, error) {
    // 1. 查询分类:判断应该使用哪种信息源
    category := classifyQuery(query)

    switch category {
    case CategoryInternalDoc:
        // 内部文档查询:优先使用企业 RAG
        return h.internalRAG.Search(ctx, query)
    case CategoryStructuredData:
        // 结构化数据查询:使用知识库
        return h.knowledgeBase.Query(ctx, query)
    case CategoryGeneralKnowledge:
        // 通用知识查询:使用 Google Search
        return h.webGrounder.Ground(ctx, query)
    default:
        // 默认策略:并行查询所有源,合并去重
        return h.fallbackGround(ctx, query)
    }
}

func classifyQuery(query string) QueryCategory {
    // 基于关键词或小型分类模型进行路由
    // 例如:包含"内部"、"我司"、"产品手册"等词 → CategoryInternalDoc
    // 包含"股价"、"财报"、"汇率" → CategoryStructuredData
    // 其他 → CategoryGeneralKnowledge
    // 生产环境中建议使用轻量级分类器或规则引擎
    lower := strings.ToLower(query)
    if strings.Contains(lower, "内部") || strings.Contains(lower, "手册") {
        return CategoryInternalDoc
    }
    if strings.Contains(lower, "股价") || strings.Contains(lower, "价格") {
        return CategoryStructuredData
    }
    return CategoryGeneralKnowledge
}

这种混合策略的核心优势在于:不同信息源有不同的延迟、成本和准确性特征。例如,企业内部 RAG 基于向量数据库,延迟通常在 100ms 以内,但覆盖范围有限;Google Search 覆盖全网,但延迟可能达到 1-3 秒,且按调用计费。通过智能路由,可以在效果、成本和性能之间取得平衡。

生产环境中的陷阱与最佳实践

陷阱 1:Grounding 导致的延迟失控

Grounding 引入了一个外部网络调用(搜索 API),这在高并发场景下可能成为系统瓶颈。我们曾经在一个客服 Agent 项目中遇到这样的问题:高峰期 P99 延迟从 2 秒飙升到 15 秒,原因是搜索 API 的并发配额耗尽,请求开始排队。

解决方案

type CachedGrounder struct {
    inner   grounding.Grounder
    cache   *ristretto.Cache  // 使用高性能本地缓存
    ttl     time.Duration
}

func (c *CachedGrounder) Ground(ctx context.Context, query string) (*grounding.Result, error) {
    // 对标准化后的查询进行缓存
    normalized := normalizeQuery(query)
    
    if val, found := c.cache.Get(normalized); found {
        return val.(*grounding.Result), nil
    }

    result, err := c.inner.Ground(ctx, query)
    if err != nil {
        return nil, err
    }

    // 缓存时间不宜过长,Grounding 的价值在于实时性
    c.cache.SetWithTTL(normalized, result, 1, c.ttl)
    return result, nil
}

func normalizeQuery(q string) string {
    // 去除多余空格、统一大小写、去除标点
    // 提高缓存命中率的同时避免过度归一化导致语义丢失
    q = strings.ToLower(strings.TrimSpace(q))
    q = regexp.MustCompile(`\s+`).ReplaceAllString(q, " ")
    return q
}

缓存 TTL 建议设置为 5-15 分钟,既能缓解热点查询的压力,又不会让信息过于陈旧。

陷阱 2:搜索结果质量不可控

搜索引擎返回的结果并不总是可靠的。低质量网站、过时页面、SEO 垃圾内容都可能被注入到上下文中,导致模型被"污染"。

解决方案:实施多层级质量过滤

type ResultFilter struct {
    blockedDomains map[string]bool
    minContentLen  int
    maxAge         time.Duration
}

func (f *ResultFilter) Filter(results []grounding.SearchResult) []grounding.SearchResult {
    var filtered []grounding.SearchResult
    for _, r := range results {
        // 域名黑名单过滤
        if f.blockedDomains[r.Domain] {
            continue
        }
        // 内容长度过滤(排除空页面或纯导航页)
        if len(r.Snippet) < f.minContentLen {
            continue
        }
        // 时效性过滤
        if time.Since(r.LastModified) > f.maxAge {
            continue
        }
        filtered = append(filtered, r)
    }
    return filtered
}

陷阱 3:上下文窗口溢出

Grounding 将搜索结果拼接到 Prompt 中,如果结果过长,可能超出模型的上下文窗口限制,导致请求失败或早期上下文被截断。

解决方案:实施智能截断与摘要

func truncateResults(results []grounding.SearchResult, maxTokens int) []grounding.SearchResult {
    // 粗略估算:1 个中文汉字 ≈ 1.5 tokens,英文单词 ≈ 1.3 tokens
    currentTokens := 0
    var truncated []grounding.SearchResult

    for _, r := range results {
        estimatedTokens := len(r.Snippet) * 3 / 2  // 保守估算
        if currentTokens+estimatedTokens > maxTokens {
            break
        }
        truncated = append(truncated, r)
        currentTokens += estimatedTokens
    }
    return truncated
}

建议为 Grounding 预留不超过总上下文窗口 30%-40% 的容量,确保历史对话和系统指令有足够的空间。

陷阱 4:成本失控

Google Search Grounding 通常按调用次数计费,在高流量场景下成本可能迅速累积。一个未加防护的 Agent 如果遭遇恶意刷量或异常流量,可能产生巨额账单。

解决方案

  1. 请求限流:在 Agent 入口处实施 Rate Limiting
  2. 查询去重:相同或高度相似的查询复用缓存结果
  3. 降级策略:当 Grounding 成本或延迟过高时,允许回退到纯模型生成(需明确告知用户)
type RateLimitedGrounder struct {
    inner    grounding.Grounder
    limiter  *rate.Limiter  // golang.org/x/time/rate
}

func (r *RateLimitedGrounder) Ground(ctx context.Context, query string) (*grounding.Result, error) {
    if !r.limiter.Allow() {
        return nil, fmt.Errorf("grounding rate limit exceeded")
    }
    return r.inner.Ground(ctx, query)
}

Grounding 与 RAG 的关系

很多开发者会混淆 Grounding 和 RAG 的概念。简单来说:

  • Grounding 是 RAG 的一种具体实现,特指通过搜索引擎(如 Google Search)获取实时网络信息来增强生成。
  • RAG 是更广泛的架构模式,信息源可以是搜索引擎、向量数据库、关系型数据库、API 等任何外部数据源。

在 ADK Go 的语境中,grounding.NewGoogleSearchGrounder() 提供的是前者。如果你的应用场景需要检索企业内部文档,你需要自行实现基于向量数据库的 RAG 管道,并将其封装为 Tool 或自定义 Grounder 集成到 Agent 中。

调试与可观测性

在生产环境中,Grounding 的调试往往比纯模型生成更复杂,因为故障点可能分布在搜索、结果处理、上下文组装等多个环节。建议实施以下可观测性措施:

// 在 Grounder 中注入日志和指标
func (g *ObservableGrounder) Ground(ctx context.Context, query string) (*grounding.Result, error) {
    start := time.Now()
    
    result, err := g.inner.Ground(ctx, query)
    
    duration := time.Since(start)
    
    // 记录指标
    metrics.RecordHistogram("grounding_latency_ms", float64(duration.Milliseconds()))
    metrics.IncrementCounter("grounding_total")
    
    if err != nil {
        metrics.IncrementCounter("grounding_errors")
        log.Printf("[Grounding] 查询失败 | query=%s | error=%v | duration=%v", query, err, duration)
        return nil, err
    }
    
    log.Printf("[Grounding] 查询成功 | query=%s | results=%d | duration=%v", 
        query, len(result.Snippets), duration)
    
    return result, nil
}

关键监控指标包括:

  • Grounding 调用延迟:P50、P95、P99
  • 搜索结果数量分布:判断是否存在大量空结果
  • Grounding 失败率:区分网络超时、配额耗尽、API 错误等不同原因
  • 缓存命中率:评估缓存策略的有效性

下一步

Grounding 解决了 Agent"知道什么"的问题——通过实时搜索让 Agent 的回答锚定在真实世界之上。接下来我们将探讨 Artifacts——如何让 Agent 生成结构化、可重用的内容输出,这对于代码生成、文档编写等场景至关重要。

跨语言协作实战 | Artifacts →


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