Gemini混合RAG教程:结合文件搜索与Google搜索

本教程面向有编程基础的开发者,深入讲解如何结合 Gemini API 的文件搜索(私有知识)与 Google 搜索接地(公共知识)构建混合 RAG 流水线。通过两种路由模式——确定性回退路由和原生多工具路由,实现知识源的灵活选择与融合。教程涵盖环境配置、文档上传、引用提取、元数据解析及清理流程,并强调 grounding_metadata 在溯源中的关键作用。同时提供常见错误排查与生产环境优化建议,帮助开发者构建可审计、可扩展的 AI 应用。

技术背景

在实际 AI 应用中,模型常需同时访问私有文档(如企业内部手册)和实时公共信息(如新闻、统计数据)。单一知识源无法满足复杂查询需求,而传统 RAG 仅处理私有数据,无法获取最新网络信息。Gemini API 提供文件搜索(托管 RAG)和 Google 搜索接地两种工具,分别解决私有与公共知识检索问题。混合 RAG 将两者结合,使模型能根据问题动态选择或融合知识源,提升回答的准确性和时效性。此方案适用于客服系统、研究助手、企业知识库等场景,但需注意 API 密钥权限管理、存储成本及路由策略的合理性。

核心原理

混合 RAG 的核心在于两种接地工具的协同:文件搜索将文档分块并嵌入向量,查询时检索相关块;Google 搜索接地则实时查询网络。两种工具均通过 grounding_metadata 返回引用来源,其中文件搜索填充 retrieved_context(文本、标题、页码),Google 搜索填充 web(URI、标题)。确定性回退路由由代码控制:先查询文件搜索,通过启发式规则(如无检索结果或模型明确表示文档未覆盖)判断是否回退到 Google 搜索。原生多工具路由则在同一请求中传递两个工具,由 Gemini 自主选择或组合调用,减少请求次数,但路由决策不透明。grounding_supports 提供文本片段与引用块的映射,用于行内引用。

架构分析

系统架构分为三层:客户端层(Python SDK)、Gemini API 层(模型与工具)和知识源层(文件搜索存储与 Google 搜索)。文件搜索存储通过异步摄取操作处理文档分块与嵌入,上传后需轮询状态直至完成。两种路由模式共享相同的工具定义,但调用方式不同:确定性路由分步调用,原生路由一次调用。响应处理层通过 extract_citations 函数统一规范化引用格式,便于展示。设计上强调可审计性:确定性路由可记录路由决策,原生路由通过 grounding_metadata 事后分析。生产环境可扩展为微服务,将文件搜索存储独立部署,并通过 API 网关管理密钥与限流。

完整实战教程

##### 版权所有 2026 Google LLC.

# @title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

Gemini API:混合 RAG,结合文件搜索与 Google 搜索

<a target="_blank" href="https://colab.research.google.com/github/google-gemini/cookbook/blob/main/examples/hybrid_file_search_and_google_search.ipynb">

</a>

大多数真实的助手需要同时具备两种知识:私有知识,存在于你自己的文档中;以及公共知识,存在于开放网络上且每日变化。Gemini 为每种知识都提供了一个接地工具:

* 文件搜索 是一个托管的 RAG 工具。你将文档上传到存储库,Gemini 在生成时检索相关的块。

* Google 搜索接地 将模型连接到网络上最新的公开信息。

本笔记本向你展示两种将它们组合成单一*混合*流水线的方法:

  1. 确定性回退路由 — 你先查询文件搜索,检查返回的内容,仅当你的文档无法回答问题时才回退到 Google 搜索。你完全控制路由决策,这使得它可审计且易于推理。
  2. 原生多工具路由 — 你在同一个请求中传递两个工具,让 Gemini 选择正确的工具,或在单个答案中组合两者。

在此过程中,你将提取每个响应中的 grounding_metadata,以便向用户准确展示答案来自哪个文档块或哪个网页。

注意: 本笔记本使用 generateContent API 而非更新的
Interactions API,因为
grounding_metadata(混合路由所依赖的引用)是在 generateContent 响应中返回的。
工具定义本身在两个 API 中是相同的。

安装依赖

安装 google-genai Python SDK。

%pip install -U -q "google-genai>=2.9.0"

设置你的 API 密钥

要运行以下单元格,你的 API 密钥必须存储在名为 GEMINI_API_KEY 的 Colab Secret 中。

如果你还没有 API 密钥,或者不确定如何创建 Colab Secret,请参阅

身份验证

获取详细指南。

重要提示: 文件搜索会将上传的文档与 API 密钥的云项目关联,因此你的

API 密钥也会授予对你上传的所有内容的访问权限。请妥善保管,并遵循 Google 的

API 密钥最佳实践。

from google import genai
from google.colab import userdata
from google.genai import types

GEMINI_API_KEY = userdata.get("GEMINI_API_KEY")
client = genai.Client(api_key=GEMINI_API_KEY)

选择模型

当前 Gemini 模型系列均支持 Grounding 工具。在本指南中,请选择您要使用的模型。

MODEL_ID = "gemini-3.7-flash" # @param ["gemini-3.1-pro-preview", "gemini-3.7-flash", "gemini-3.5-flash-lite", "gemini-2.5-pro"] {"allow-input":true, isTemplate: true}

创建示例文档

为了让笔记本自包含,你为一家虚构公司生成一份小型内部手册,而不是下载任何内容。本文档中的任何内容都不存在于公共网络上,因此它可以很好地代表你自己的私有语料库:Gemini 能够回答关于它的问题的唯一方式是通过文件搜索检索它。

HANDBOOK = """
Zephyr Robotics — Employee Handbook (Internal)

About the company
Company Name: Zephyr Robotics
Founded: 2019
Headquarters: San Francisco, CA

Time off
PTO Policy: Every full-time employee receives 20 days of paid time off per year.
Unused PTO does not roll over into the next calendar year.

Benefits
Health Insurance: Zephyr Robotics covers 90% of medical, dental, and vision premiums
for employees and their dependents.

Working arrangements
Remote Work: Zephyr Robotics runs a hybrid schedule of 3 days remote and 2 days in the
San Francisco office. Teams pick their own in-office days.
"""

with open("sample_handbook.txt", "w") as f:
    f.write(HANDBOOK)

print(HANDBOOK.strip())

创建文件搜索存储并上传文档

文件搜索存储用于保存您的文档及其嵌入向量。上传会启动一个长时间运行的摄取操作(分块和嵌入),因此请轮询该操作,直到其状态报告为 done。

file_search_store = client.file_search_stores.create(
    config=types.CreateFileSearchStoreConfig(
        display_name="Zephyr Robotics Handbook Store"
    )
)

# Keep the store name around; every File Search call and the cleanup step needs it.
FILE_SEARCH_STORE_NAME = file_search_store.name
print(f"Created store: {FILE_SEARCH_STORE_NAME}")
import time

upload_op = client.file_search_stores.upload_to_file_search_store(
    file_search_store_name=FILE_SEARCH_STORE_NAME,
    file="sample_handbook.txt",
    config=types.UploadToFileSearchStoreConfig(
        display_name="Zephyr Robotics Employee Handbook",
    ),
)

print(f"Ingestion started: {upload_op.name}")

# Wait for chunking and embedding to finish before querying the store.
while not (upload_op := client.operations.get(upload_op)).done:
    time.sleep(2)
    print(".", end="")

print()
print("Ingestion complete.")

从响应中读取引用

两种 grounding 工具都以相同的方式报告其来源,即通过 response.candidates[0].grounding_metadata。区别在于 grounding_chunks 中每个条目的哪个字段被填充:

* File Search 填充 chunk.retrieved_context —— 检索到的 text、文档 title,通常还有 page_number。

* Google Search 填充 chunk.web —— 页面的 uri 和 title。

编写一个小辅助函数,将两种形状规范化为普通字典,再编写另一个辅助函数来打印它们。你将在两种路由模式中复用这些函数。

def extract_citations(response):
    """Normalizes File Search and Google Search grounding chunks into dicts."""
    citations = []
    if response is None:
        return citations

    candidates = response.candidates or []
    if not candidates:
        return citations

    metadata = candidates[0].grounding_metadata
    if metadata is None:
        return citations

    for chunk in metadata.grounding_chunks or []:
        if chunk.retrieved_context is not None:
            snippet = chunk.retrieved_context.text or ""
            page_num = chunk.retrieved_context.page_number
            location = (
                f"page {page_num}" if page_num is not None else "retrieved chunk"
            )
            citations.append({
                "source_type": "document",
                "title": chunk.retrieved_context.title,
                "location": location,
                "snippet": snippet[:200],
            })
        elif chunk.web is not None:
            citations.append({
                "source_type": "web",
                "title": chunk.web.title,
                "location": chunk.web.uri,
                "snippet": "",
            })
    return citations


def show(label, response):
    """Prints a response together with its normalized citations."""
    if response is None:
        print("\n[no response returned]")
        return

    print(f"=== {label} ===")
    print((response.text or "").strip())

    citations = extract_citations(response)
    if not citations:
        print("\n[no grounding citations returned]")
        return

    print("\nSources:")
    for i, citation in enumerate(citations, start=1):
        print(f"  [{i}] ({citation['source_type']}) {citation['title']}")
        print(f"      {citation['location']}")
        if citation["snippet"]:
            print(f"      \"{citation['snippet']}...\"")

模式 1:确定性回退路由

在此模式中,*你的代码*决定哪个来源回答问题,分两步:

  1. 仅启用文件搜索工具询问 Gemini。
  2. 用简单的启发式规则判断结果。如果你的文档明显未覆盖问题,则仅启用 Google 搜索工具重新提问。

下面的启发式规则刻意保持基础——没有评分模型,只有两个检查,既便宜又易于解释:

* 无检索结果。 如果 grounding_chunks 为空,说明文件搜索未找到相关内容。

* 明确的不回答。 如果模型检索到了内容,但其回复称文档中不包含答案,也视为不足。

当你需要保证私有文档始终被优先查阅、希望记录路由决策以供审计,或当网络访问必须是明确、可观察的升级而非模型选择时,确定性路由值得额外调用。

NO_ANSWER_PHRASES = (
    "does not contain",
    "doesn't contain",
    "no information",
    "not mentioned",
    "not specified",
    "unable to answer",
    "cannot answer",
    "i don't know",
)


def is_sufficient(response):
    """Simple heuristic: did File Search actually ground this answer?"""
    if response is None:
        return False, "No response returned."

    citations = extract_citations(response)
    if not citations:
        return False, "File Search returned no chunks."

    text = (response.text or "").lower()
    if any(phrase in text for phrase in NO_ANSWER_PHRASES):
        return False, "The model reported that the documents do not cover the question."

    return True, f"Grounded in {len(citations)} document chunk(s)."


def ask_with_fallback(question, store_name=None, verbose=True):
    """Queries File Search first, then falls back to Google Search if needed."""
    store_name = store_name or FILE_SEARCH_STORE_NAME

    try:
        doc_response = client.models.generate_content(
            model=MODEL_ID,
            contents=question,
            config=types.GenerateContentConfig(
                tools=[types.Tool(
                    file_search=types.FileSearch(
                        file_search_store_names=[store_name]
                    )
                )],
            ),
        )
    except Exception as e:  # Surface API errors without stopping the notebook.
        print(f"File Search call failed: {type(e).__name__}: {e}")
        return None, "error"

    sufficient, reason = is_sufficient(doc_response)
    if verbose:
        print(f"Router: File Search -> {'sufficient' if sufficient else 'insufficient'}")
        print(f"        {reason}")

    if sufficient:
        return doc_response, "file_search"

    if verbose:
        print("Router: falling back to Google Search.")

    try:
        web_response = client.models.generate_content(
            model=MODEL_ID,
            contents=question,
            config=types.GenerateContentConfig(
                tools=[types.Tool(google_search=types.GoogleSearch())],
            ),
        )
    except Exception as e:
        print(f"Google Search call failed: {type(e).__name__}: {e}")
        return None, "error"

    return web_response, "google_search"

问题1:由你的文档回答

PTO政策只存在于员工手册中,因此文件搜索应返回相关片段,路由器应在步骤1后停止。

question_1 = "What is Zephyr Robotics' PTO policy?"

response_1, route_1 = ask_with_fallback(question_1)
print()

if response_1 is not None:
    show(f"Route: {route_1}", response_1)

问题2:你的文档未回答的问题

手册中没有任何内容提到宏观经济学,因此启发式方法应将文件搜索尝试标记为不足,并升级到谷歌搜索。请注意,返回的引用是网页而不是文档片段。

question_2 = "What is the current US inflation rate?"

response_2, route_2 = ask_with_fallback(question_2)
print()

if response_2 is not None:
    show(f"Route: {route_2}", response_2)

模式 2:原生多工具路由

与其在自己的代码中进行路由,不如在同一请求中将两个工具都交给 Gemini,让它自行决定。

模型会读取问题,调用合适的工具,并且当问题同时涉及私有和公共知识时,可以同时调用两个工具。

这样只需一次请求,而不是最多两次,并且能够处理确定性路由需要手动拆分的混合问题。

代价是路由决策现在由模型内部做出,因此你需要事后通过阅读 grounding_metadata 来了解发生了什么,而不是事先决定。

HYBRID_TOOLS = [
    types.Tool(
        file_search=types.FileSearch(
            file_search_store_names=[FILE_SEARCH_STORE_NAME]
        )
    ),
    types.Tool(google_search=types.GoogleSearch()),
]


def ask_hybrid(question):
    """Passes both grounding tools and lets Gemini choose."""
    try:
        return client.models.generate_content(
            model=MODEL_ID,
            contents=question,
            config=types.GenerateContentConfig(tools=HYBRID_TOOLS),
        )
    except Exception as e:
        print(f"Hybrid call failed: {type(e).__name__}: {e}")
        return None

再次提出同样两个问题。答案应与模式1产生的结果一致,但现在每个问题只需一次请求即可完成。

for question in (question_1, question_2):
    response = ask_hybrid(question)
    if response is not None:
        show(question, response)
    print()

一个需要两种来源的问题

由于两个工具同时在线,模型可以将答案的一部分基于你的手册,另一部分基于网络。提出一个刻意跨越这两者的问题。

blended_question = (
    "Zephyr Robotics covers 90% of health insurance premiums. "
    "How does that compare to the average employer contribution in the US?"
)

blended_response = ask_hybrid(blended_question)
if blended_response is not None:
    show("Blended question", blended_response)

检查原始 grounding 元数据

show 辅助函数为了可读性简化了引用,但值得查看一次原始对象,以便了解还有哪些其他信息可用。除了 grounding_chunks 之外,元数据还包含:

* grounding_supports — 生成文本中映射回支持它们的块索引的片段,这是渲染行内脚注标记所需的。

* web_search_queries — Gemini 实际向 Google 搜索发出的查询。

* retrieval_queries — 针对你的文件搜索存储发出的查询。

这些信息共同告诉你模型使用了*什么*以及*它是如何查找的*。

import json


def describe_grounding(response):
    """Dumps the interesting parts of grounding_metadata."""
    if response is None:
        print("No response returned.")
        return

    candidates = response.candidates or []
    if not candidates or candidates[0].grounding_metadata is None:
        print("No grounding metadata on this response.")
        return

    metadata = candidates[0].grounding_metadata

    print(f"web_search_queries: {metadata.web_search_queries}")
    print(f"retrieval_queries:  {metadata.retrieval_queries}")
    print(f"grounding_chunks:   {len(metadata.grounding_chunks or [])}")
    print(f"grounding_supports: {len(metadata.grounding_supports or [])}")

    print("\nFirst chunk, as JSON:")
    if metadata.grounding_chunks:
        print(json.dumps(
            metadata.grounding_chunks[0].model_dump(exclude_none=True),
            indent=2,
            default=str,
        )[:800])

    print("\nText spans and the chunks that support them:")
    for support in (metadata.grounding_supports or [])[:5]:
        segment_text = (support.segment.text or "")[:80]
        print(f"  {support.grounding_chunk_indices} <- \"{segment_text}\"")


if blended_response is not None:
    describe_grounding(blended_response)

清理

文件搜索存储会持久保存并计入存储空间,因此完成后请删除该存储。本地示例文件也会随之消失。

import os

try:
    client.file_search_stores.delete(
        name=FILE_SEARCH_STORE_NAME,
        config=types.DeleteFileSearchStoreConfig(force=True),
    )
    print(f"Deleted store: {FILE_SEARCH_STORE_NAME}")
except Exception as e:
    print(f"Could not delete store: {type(e).__name__}: {e}")

if os.path.exists("sample_handbook.txt"):
    os.remove("sample_handbook.txt")
    print("Deleted local file: sample_handbook.txt")

print("Cleanup complete.")

下一步

将本教程适配到您自己的文档

将生成的示例手册替换为您的真实语料库,笔记本的其余部分无需更改即可正常工作:

使用 upload_to_file_search_store 上传每个文件,并附加自定义元数据(如团队或文档类型),以便您可以通过 metadata_filter 缩小检索范围。如果您的回退触发过于频繁,请收紧 is_sufficient —— 要求最小块数,或检查检索到的片段是否与问题共享关键词,这些都是在使用评分模型之前合理的下一步。

了解更多

* 文件搜索指南及其定价详情

* Google 搜索接地指南,包括内联引用归属

* 文件搜索快速入门,了解元数据过滤和多模态存储

* 接地快速入门,了解地图、YouTube 和 URL 上下文

* 搜索接地示例,了解更长的研究报告工作流程

* 在 Gemini API Cookbook 中查看更多想法

常见问题

常见错误:1) API 密钥未设置或权限不足,导致认证失败;2) 文件搜索存储未创建或名称错误,引发 NotFound;3) 上传文档格式不支持,需检查文件类型;4) 摄取未完成即查询,返回空结果;5) 启发式规则误判,导致不必要的回退或遗漏。排查方法:检查 API 密钥环境变量,确认存储名称正确,轮询摄取状态,打印 grounding_metadata 调试。生产优化:使用元数据过滤缩小检索范围,调整 is_sufficient 阈值(如要求最小块数),采用评分模型替代简单启发式,缓存高频查询结果,监控 API 调用成本。

企业应用扩展

可扩展方向:1) 将示例手册替换为真实语料库,利用自定义元数据(如团队、文档类型)进行过滤;2) 结合 Interactions API 实现多轮对话中的动态路由;3) 集成其他接地工具(如地图、YouTube)丰富知识源;4) 构建评估集,量化路由准确率与回答质量;5) 实现流式输出,提升用户体验;6) 部署为 REST 服务,支持高并发。参考官方文档:文件搜索指南、Google 搜索接地指南、文件搜索快速入门、接地快速入门。

总结

本教程面向有编程基础的开发者,深入讲解如何结合 Gemini API 的文件搜索(私有知识)与 Google 搜索接地(公共知识)构建混合 RAG 流水线。通过两种路由模式——确定性回退路由和原生多工具路由,实现知识源的灵活选择与融合。教程涵盖环境配置、文档上传、引用提取、元数据解析及清理流程,并强调 grounding_metadata 在溯源中的关键作用。同时提供常见错误排查与生产环境优化建议,帮助开发者构建可审计、可扩展的 AI 应用。

内容来源与验证

继续系统学习

站内还有更多完整实战教程

加入会员,解锁全部教程、完整代码与持续更新内容。

加入会员
查看全部RAG 与知识库教程