这次的教程,我将带大家一步步用 Google 文件搜索(Google File Search)来搭建一个医疗文档助手。我们会从头开始,讲讲如何设置环境、实现查询功能,以及玩转一些高级特性,比如自定义分块和元数据过滤。文章读完后,相信你会对何时选择托管式 RAG 而非自建方案,有个更清晰的认识。我个人认为,托管式服务在很多场景下能大幅提高开发效率,减少运维负担。
Google 文件搜索工具到底是什么?
通常情况下,构建 RAG 应用要应对向量数据库、嵌入生成管道以及大量基础设施。但 Google 在 2025 年 11 月推出的文件搜索工具(File Search Tool),彻底改变了这一局面。它将一个完全托管的 RAG 系统直接整合到了 Gemini API 中,大大简化了开发的复杂性。
这个工具替我们处理了那些烦人的技术细节:它把文档分块的、生成嵌入,再管理语义搜索,所有这些都不需要外部工具,比如 Pinecone 或 ChromaDB。它的工作流程非常简单——上传文件,创建储存库,然后就可以开始查询了。更棒的是,它还内置了引用功能,能让你轻松核实答案的来源。
理解 RAG:为何 Google 让它变得如此简单
Gemini 文件搜索将自己定位为一个托管式 RAG 系统。要用好这个工具,并判断它是否适合你的应用场景,首先得深入理解 RAG 的核心理念。
检索增强生成(RAG)的核心思想是,让语言模型能连接到外部知识库。在模型生成回复之前,它会从你的文档中检索相关信息,从而让答案基于你实际的数据,而不是仅仅依赖模型自身的训练数据。这极大提升了回答的准确性和相关性。
自建 RAG 的挑战
RAG 的概念听起来挺直观,但自己动手构建一个 RAG 管道,意味着要管理一系列复杂的组件:
- 向量数据库:你需要部署和维护像 Pinecone、ChromaDB 或 Weaviate 这样的服务来存储嵌入向量。
- 嵌入生成管道:将文档转换为数值向量,并且当内容更新时,还得处理嵌入的同步更新。
- 分块策略:如何将文档拆分成大小适中、既能保留上下文又能精确检索的片段,这是一个精细活。
- 基础设施:性能监控、参数调优,以及随着数据增长而进行扩展,这些都是持续性的运维工作。
每一个环节都需要专业的知识和持续的维护。无论你是想构建一个需要高可靠性的生产系统,还是一个追求快速迭代的原型,基础设施的复杂性始终是一个瓶颈。
托管式 RAG 为何重要
像 Google 文件搜索这样的托管服务,恰好解决了这个瓶颈。你不再需要去调优检索系统,而是直接编写查询语句;不用再调试嵌入管道,而是专注于验证结果。所有复杂的底层基础设施都在幕后运行,让你能把精力完全放在应用逻辑上。
Gemini 文件搜索承担了技术复杂性,而你则能掌控关键环节:哪些文档需要索引,如何查询它们,以及如何利用查询结果。这种平衡对于既需要生产级质量又不想背负沉重运维负担的团队来说,是个不错的在选择。如果你想深入了解 RAG 的基础知识,我推荐大家看看 DataCamp 上关于 Agentic RAG 的教程。

理解 Google 文件搜索的最好方式,就是亲自动手尝试。接下来,我们将构建一个完整的医疗文档助手,演示从文档上传到获取带有引用的扎实回复的完整工作流程。
利用 Google 文件搜索构建医疗文档助手
免责声明:本教程仅出于教育目的,使用 FDA 药品说明书演示文件搜索功能。您将构建的助手不适用于临床用途、患者护理决策或医疗诊断。如有医疗建议,请务必咨询合格的医疗专业人员。即使有源文档的支撑,AI 系统也可能生成不正确的信息。
本节将引导您使用文件搜索功能,构建一个完整的医疗文档助手。我们将处理三份 FDA 批准的常用药物说明书,创建一个系统来回答关于药物相互作用、副作用和禁忌症的问题。该助手通过引用源文档中的特定段落,提供可验证的答案。
文件搜索分两个阶段运行:您只需索引文档一次,然后可以重复查询。我们首先设置索引基础设施,然后将全部精力放在提问和解释带引用的回复上。
步骤 1:安装 API 并配置认证
您需要 Python 3.9 或更高版本。请安装 Google Generative AI SDK 和相关依赖:
pip install google-genai python-dotenv
从 Google AI Studio 获取您的 API 密钥。将其存储在项目目录的 .env 文件中:
GOOGLE_API_KEY=your_api_key_here
设置导入并初始化客户端:
from google import genai
from google.genai import types
import time
from dotenv import load_dotenv
load_dotenv()
client = genai.Client()
genai.Client() 会自动使用您的环境变量进行身份验证。您将使用此客户端对象执行所有文件搜索操作。
步骤 2:创建文件搜索储存库
创建一个储存库来存储您已索引的文档:
file_search_store = client.file_search_stores.create(
config={"display_name": "fda-drug-labels"}
)
print(f"Created store: {file_search_store.name}")
文件搜索储存库作为您索引文档的容器。与 48 小时后过期的临时文件上传不同,储存库会永久存在。这意味着您可以一次索引文档,然后查询它们成千上万次,而无需重新上传或重新处理。
file_search_store.name 包含一个唯一的标识符,您将在查询时引用它。它的格式类似于 fileSearchStores/fdadruglabels-abc123。如果您需要从不同的会话查询该储存库,请保存此值。
3. 上传并索引 PDF 文档
在本教程中,您将使用三份 FDA 批准的药物说明书。从 FDA 网站下载这些 PDF 文件:
- 二甲双胍 (Metformin/Glucophage) - 糖尿病药物
- 阿托伐他汀 (Atorvastatin/Lipitor) - 降胆固醇药物
- 赖诺普利 (Lisinopril/Zestril) - 降血压药物
将它们保存在您的项目目录中,然后上传到您的文件搜索储存库:
pdf_files = ["metformin.pdf", "atorvastatin.pdf", "lisinopril.pdf"]
for pdf_file in pdf_files:
operation = client.file_search_stores.upload_to_file_search_store(
file=pdf_file,
file_search_store_name=file_search_store.name,
config={"display_name": pdf_file.replace(".pdf", "")},
)
# Wait for indexing to complete
while not operation.done:
time.sleep(3)
operation = client.operations.get(operation)
print(f"{pdf_file} indexed")
在上传过程中,文件搜索会将每个 PDF 文档分块,并使用 gemini-embedding-001 模型将这些片段转换为嵌入。这些嵌入是捕捉语义含义的数值表示,即使你的问题与文档中的确切措辞不匹配,系统也能找到相关的段落。
轮询模式 (while not operation.done) 处理了索引的异步特性。大型文档处理时间较长,因此 API 会立即返回,你需要定期检查完成状态。对于生产系统,最好添加超时逻辑以防止无限循环。
每个分块都保留了元数据,将其链接回其源文档和位置。这些元数据在你稍后访问引用时会变得很重要。
步骤 4:查询单个文档信息
现在,你可以查询你的已索引文档了:
query1 = "What are the contraindications for metformin?"
response1 = client.models.generate_content(
model="gemini-2.5-flash",
contents=query1,
config=types.GenerateContentConfig(
tools=[
types.Tool(
file_search=types.FileSearch(
file_search_store_names=[file_search_store.name]
)
)
]
),
)
print(response1.text)
这会打印出生成的答案:
Metformin is contraindicated in several conditions:
* Severe renal impairment (eGFR below 30 mL/min/1.73 m2)
* Acute or chronic metabolic acidosis
* Hypersensitivity to metformin
文件搜索会检索你文档中最具语义相似性的分块,并将它们作为上下文提供给 gemini-2.5-flash 模型,然后由模型生成答案。tools 数组配置告诉模型在生成过程中使用文件搜索。你可以在同一个请求中,将文件搜索与其他工具(如代码执行或 Google 搜索)结合使用。
步骤 5:访问引用和基础元数据
提取哪些文档为答案提供了信息:
print("Sources used:")
for i, chunk in enumerate(response1.candidates[0].grounding_metadata.grounding_chunks, 1):
source_name = chunk.retrieved_context.title
print(f" [{i}] {source_name}")
输出:
Sources used:
[1] metformin
[2] atorvastatin
基础元数据中的每个分块都包含源文档标题和为答案提供信息的特定文本段落。这建立了一个从生成回复到原始文档的验证路径——这对于医疗、法律或金融等对准确性要求极高的应用至关重要。
grounding_chunks 数组包含了所有检索到的段落,按相关性排序。即使查询是专门关于二甲双胍的,文件搜索也检索了阿托伐他汀文档中的内容,这很可能是因为它包含了相关的禁忌症信息。这展示了语义检索的方法:系统寻找概念上相关的内容,而不仅仅是关键词匹配。
步骤 6:跨多个文档查询
测试一个涉及多文档的药物相互作用问题:
query2 = "Can a patient take both atorvastatin and metformin together? Are there any drug interactions?"
response2 = client.models.generate_content(
model="gemini-2.5-flash",
contents=query2,
config=types.GenerateContentConfig(
tools=[
types.Tool(
file_search=types.FileSearch(
file_search_store_names=[file_search_store.name]
)
)
]
),
)
print(response2.text)
相同的 API 模式现在可以从多个文档中提取信息并进行综合。访问检索到的文本片段:
print("Sources used:")
for i, chunk in enumerate(response2.candidates[0].grounding_metadata.grounding_chunks, 1):
source_name = chunk.retrieved_context.title
source_text = chunk.retrieved_context.text[:100] + "..."
print(f" [{i}] {source_name}")
print(f" {source_text}")
输出显示了两种药物说明书的摘录:
Sources used:
[1] atorvastatin
Concomitant use with diabetes medications is generally safe but monitor glucose levels...
[2] metformin
Carbonic anhydrase inhibitors may increase the risk of lactic acidosis...
文件搜索检索了两个文档中的相关部分,模型将它们综合成一个连贯的答案。retrieved_context.text 属性为你提供了确切使用的段落,让你能够验证模型没有产生幻觉信息。
步骤 7:运行跨文档比较
提出一个需要比较所有三个文档的分析性问题:
query3 = "Which medications have muscle-related side effects?"
response3 = client.models.generate_content(
model="gemini-2.5-flash",
contents=query3,
config=types.GenerateContentConfig(
tools=[
types.Tool(
file_search=types.FileSearch(
file_search_store_names=[file_search_store.name]
)
)
]
),
)
print(response3.text)
# Check which documents were consulted
metadata = response3.candidates[0].grounding_metadata
for i, chunk in enumerate(metadata.grounding_chunks, 1):
print(f" [{i}] {chunk.retrieved_context.title}")
输出结果表明,阿托伐他汀有肌肉相关的副作用(肌痛、肌病、横纹肌溶解),并确认其他药物没有列出此类副作用。基础元数据显示,文件搜索咨询了所有三个文档来回答这个比较问题。
你现在已经构建了一个可用的医疗文档助手。核心工作流程保持一致:在 generate_content() 调用中配置文件搜索工具,获取响应文本,并访问基础元数据进行验证。储存库存储在 Google 的服务器上,因此你可以在未来的会话中查询它,无需重新索引。
接下来,我们将探索自定义分块配置和元数据过滤等高级功能,这些功能可以让你更精细地控制检索行为。
Google 文件搜索工具的高级功能与自定义
基础的文件搜索工作流已经能应对大多数用例,但生产系统往往需要更精细地控制检索行为。本节将展示如何自定义分块策略、使用元数据过滤文档、优化性能以及管理多个储存库以适应不同用例。
自定义分块配置
文件搜索在索引期间会自动将文档分块。默认情况下,它使用针对通用文档优化的分块策略,但当特定文档类型需要不同处理时,你可以自定义此行为。
以医疗助手为例。药品说明书包含表格和短段落中的密集技术信息。较小的分块可以让你检索精确的信息,例如特定剂量或禁忌症,而不会引入不相关的上下文。较大的分块更适合需要更多上下文才能正确理解的叙述性部分。
在上传文档时配置分块参数:
operation = client.file_search_stores.upload_to_file_search_store(
file="metformin.pdf",
file_search_store_name=file_search_store.name,
config={
"display_name": "metformin",
"chunking_config": {
"white_space_config": {
"max_tokens_per_chunk": 200,
"max_overlap_tokens": 20
}
}
}
)
chunking_config 参数控制文件搜索如何拆分你的文档。max_tokens_per_chunk 参数设置每个分块的最大大小,而 max_overlap_tokens 确定连续分块之间有多少内容重叠。这种重叠确保了跨分块边界的信息在检索时不会丢失。
其中的权衡很重要:更短的分块可以提供更精确的检索,但可能会错过更广泛的上下文;而更大的分块能保留更多含义,但也可能包含不相关的信息。
对于具有清晰节边界的技术文档,我通常建议使用较小的分块(150-250 个词元)。而对于研究论文或报告等叙述性文档,较大的分块(400-600 个词元)能更好地保持论证的流畅性和上下文。官方的文件搜索文档提供了关于针对不同文档类型选择分块大小的更多指导。
元数据过滤
当你的储存库包含几十甚至几百个文档时,元数据过滤可以在语义搜索运行之前缩小检索范围。这能提高精度并减少处理时间。
在文档上传期间添加元数据以启用后续过滤:
operation = client.file_search_stores.upload_to_file_search_store(
file="metformin.pdf",
file_search_store_name=file_search_store.name,
config={
"display_name": "metformin",
"custom_metadata": [
{"key": "category", "string_value": "diabetes"},
{"key": "year", "numeric_value": 2017},
{"key": "drug_class", "string_value": "biguanide"}
]
}
)
custom_metadata 参数接受一个键值对数组。对于文本元数据,比如类别或药物分类,使用 string_value;对于年份、版本或其他数值数据,则使用 numeric_value。
使用元数据过滤器进行查询,只搜索相关文档:
query = "What are the common side effects?"
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=query,
config=types.GenerateContentConfig(
tools=[
types.Tool(
file_search=types.FileSearch(
file_search_store_names=[file_search_store.name],
metadata_filter="category=diabetes"
)
)
]
)
)
metadata_filter 参数将检索限制在符合指定条件的文档。在这个例子中,文件搜索只考虑 category=diabetes 的文档,即使同一储存库中存在降血压和降胆固醇药物,也会被忽略。
当储存库包含异构文档时,这一点变得至关重要。一个医疗知识库可能包含药品说明书、研究论文和临床指南。按文档类型过滤可确保您从说明书中获取剂量信息,而不是从研究摘要中获取。
你可以将元数据过滤与完整的语义搜索功能结合使用。过滤器首先运行以选择候选文档,然后语义搜索在这些文档中找到最相关的段落。
性能优化
文件搜索的性能取决于储存库大小、查询复杂度和模型选择。遵循以下准则可以保持检索速度并控制成本。
储存库大小限制:将单个储存库保持在 20GB 以下以获得最佳检索延迟。文件搜索将嵌入向量与您的文档一起存储,嵌入向量的大小大约是原始文件的三倍。一个 7GB 的 PDF 文件集在索引后会生成大约 21GB 的存储数据,这将超出建议限制。
当你接近这个限制时,可以按类别、时间段或访问模式创建单独的储存库。对于医疗助手,你可能需要为不同的药物类别创建单独的储存库,而不是将所有可用的药物都索引到一个储存库中。
成本结构:文件搜索对索引每 100 万个词元收取 0.15 美元。一旦索引完成,你可以运行数千次查询而无需额外支付索引费用。这种定价模式有利于读密集型工作负载,即你反复查询相同的文档。
模型选择:对于大多数查询,请使用 gemini-2.5-flash。它通常在 1-2 秒内处理请求,并且成本显著低于 gemini-2.5-pro。将 gemini-2.5-pro 保留给那些需要跨多个来源进行深度推理或处理极其复杂合成任务的查询。对于高吞吐量应用程序,模型之间的成本差异比索引成本更重要。
添加文档时,请务必监控储存库大小。你可以通过 API 检查此信息,尽管大小计算发生在 Google 的后端,可能不会在上传后立即反映。有关完整的技术规范和限制,请参阅 Gemini 文件 API 文档。
管理多个储存库
每个 Google Cloud 项目最多支持 10 个文件搜索储存库。多个储存库可以让你按访问控制、性能要求或逻辑组织来分离文档。
为不同的用例创建专门的储存库:
# Create separate stores for different document categories
diabetes_store = client.file_search_stores.create(
config={"display_name": "diabetes-medications"}
)
cardio_store = client.file_search_stores.create(
config={"display_name": "cardiovascular-medications"}
)
在单个请求中查询多个储存库:
response = client.models.generate_content(
model="gemini-2.5-flash",
contents="What medications treat both diabetes and heart disease?",
config=types.GenerateContentConfig(
tools=[
types.Tool(
file_search=types.FileSearch(
file_search_store_names=[
diabetes_store.name,
cardio_store.name
]
)
)
]
)
)
文件搜索将从所有指定的储存库中检索并综合结果。基础元数据会识别每个引用来自哪个储存库,从而保持完整的可追溯性。
储存库会永久存在,不再需要时需要手动删除。这使得它们适用于文档在会话和部署之间保持可查询的生产应用程序。
接下来,你将看到文件搜索与其他 RAG 解决方案的比较,以及何时选择托管式方法与自建方法。
Google 文件搜索工具与其他文件搜索及 RAG 工具的比较
文件搜索并非构建 RAG 应用的唯一选择。了解它与替代方案的对比,有助于你选择合适的工具。让我们比较一下 Google 的方法、OpenAI 的产品和传统的自定义构建。
| 功能 | Google 文件搜索 | OpenAI 文件搜索 | 自定义 RAG (LangChain) |
|---|---|---|---|
| 定价模式 | 0.15 美元/百万词元 (仅索引) | 0.10 美元/GB 每日存储 | 基础设施 + 开发成本 |
| 分块控制 | 自动,带基本配置 | 可配置 (默认 800 词元, 400 重叠) | 完全控制策略 |
| 搜索类型 | 语义 (仅向量) | 混合 (向量 + 关键词) | 任何你实现的方法 |
| 文件格式 | 150+ 种 (PDF, DOCX, 代码等) | 6 种 (TXT, MD, HTML, DOCX, PPTX, PDF) | 取决于使用的解析器 |
| 设置时间 | 几分钟 | 几分钟 | 几天到几周 |
| 引用 | 内置,带基础元数据 | 内置 | 必须自行实现 |
| 最适合用途 | 高查询量,快速部署 | 关键词密集查询,适度控制 | 复杂需求,完全自定义 |
Google 文件搜索 vs OpenAI 文件搜索
两家公司都提供托管式 RAG 服务,但它们在定价和功能上采取了不同的路径。
定价:Google 在索引期间收费一次(每千次查询 2.50 美元,加上每日每 GB 存储 0.10 美元)。如果你运行大量查询但很少更新文档,Google 的模型可以省钱。如果你经常重新索引,那么计算方式就变得有趣了。
配置控制:Google 通过自动化分块和有限的配置来保持简单。OpenAI 给你更多控制权。你可以设置分块大小(默认 800 个词元)和重叠(400 个词元)。OpenAI 还运行结合向量和关键词匹配的混合搜索,而 Google 则纯粹依赖语义搜索。当你的查询包含特定技术术语或产品代码时,这一点很重要。
文件格式:Google 处理 150 多种文件类型,包括代码文件和各种文档格式。OpenAI 支持六种:TXT、MD、HTML、DOCX、PPTX 和 PDF。两者都不能很好地处理 CSV 或 JSONL 等结构化数据。这正是自定义构建的优势所在。
集成:Google 与 Gemini 模型和 Google Cloud 服务绑定。OpenAI 连接到他们的模型家族和 Azure。两者都提供引用和来源追踪。
真正的区别在于简单性与控制力。Google 将一切都封装在一个 API 调用中。OpenAI 让你以更复杂的代价来调整检索。这里没有绝对的赢家。这取决于你的项目是需要速度还是自定义。
自定义 RAG 能力
使用 LangChain 等工具构建自己的 RAG 系统,可以解锁托管服务无法提供的能力。DataCamp 的 RAG with LangChain 课程详细介绍了这种方法。
自定义构建能够实现高级技术:
- 语义分割:检测主题何时发生变化,而不是按固定长度截断。
- 感知词元的分块:精确遵守模型上下文窗口。
- 混合检索:将 BM25 关键词搜索与密集向量混合使用。
- 查询转换:例如 HyDE,通过生成假设性答案来改进搜索。
- 图 RAG:将文档表示为实体和关系的网络。
DataCamp 上关于提高 RAG 性能的教程通过示例探讨了这些技术,展示了可衡量的质量改进。但其代价是操作复杂性:你需要监控数据库性能、调优嵌入模型,以及处理多个服务之间的更新。
何时采用每种方法
选择像文件搜索这样的托管工具,当:
- 构建原型或概念验证,速度是关键时。
- 你的用例符合标准模式(文档问答、知识库、文档搜索)时。
- 你的团队缺乏深厚的 RAG 专业知识时。
- 你想要可预测的成本和最小的运维开销时。
在以下情况下,选择自定义构建:
- 你需要高级分块或专门的检索方法时。
- 处理结构化数据或不寻常的文件格式时。
- 构建结合多种策略的 Agentic RAG 系统时。
- 在海量规模下优化成本,证明工程投入是值得的。
- 合规性要求特定的基础设施或模型时。
大多数项目都会从托管解决方案开始,只有当需求确实需要时,才会转向自定义构建。来自自定义 RAG 的技术(智能分块、混合搜索、查询优化)仍然会指导你如何使用托管工具。理解整个技术版图,有助于你在需求演变时做出更好的选择。
结论
你已经使用 Google 文件搜索工具构建了一个完整的 RAG 系统,从索引 FDA 药品说明书到带引用的查询。这个医疗助手演示了托管服务如何处理基础设施,而你则可以专注于应用逻辑。
文件搜索在你需要可靠的 RAG 而又不想管理向量数据库或嵌入管道时,表现得非常出色。免费的存储和查询嵌入让成本变得可预测。持久性储存库消除了重新索引的开销,让你可以在不增加基础设施维护成本的情况下扩展查询。
在部署到生产环境之前,别忘了添加本教程为求简洁而省略的一些关键防护措施。例如,为上传操作实现带超时的错误处理,并在 API 调用周围加上 try-catch 块。考虑到将文档上传到 Google 服务器时可能涉及数据隐私问题,特别是敏感内容。添加验证,确保在访问引用之前,基础元数据确实存在。最重要的是,要与领域专家进行彻底测试,以捕获模型即使有数据支撑也可能生成看似合理但实际不正确的答案的情况。
作为下一步,你可以尝试通过元数据过滤来扩展你的助手,按类别组织文档。试验不同的分块大小,以匹配你的文档类型。你在本教程中学到的技术,无论你是构建支持机器人、文档搜索,还是知识助手,都能派上用场。
关于
关注我获取更多资讯