Grounding:搜索增强生成,让回答更可靠
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 如果遭遇恶意刷量或异常流量,可能产生巨额账单。
解决方案:
- 请求限流:在 Agent 入口处实施 Rate Limiting
- 查询去重:相同或高度相似的查询复用缓存结果
- 降级策略:当 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 编程实战干货。
