1. 为什么 Google Docs 读取总是卡在 credentials.json 这一步
如果你正在做 RAG 或者知识库问答,大概率会遇到这样的场景:团队文档全在 Google Docs 里,想用 llama-index 的 GoogleDocsReader 把它们拉下来做索引,结果第一步就卡住了。不是FileNotFoundError,就是ValueError: Invalid document ID,再不然就是 LLM 调用那一步连接超时。我自己第一次跑这个链路的时候,光授权就折腾了大半天。
GoogleDocsReader 这个组件本身不复杂,它的职责很单一:拿着你的服务账号凭证,去 Google Docs API 把指定文档的正文拉回来,转成 llama-index 的 Document 对象。真正麻烦的是两件事——一是 credentials.json 的授权配置,二是读取完之后做查询时 LLM 通道怎么接。前者是 Google 服务账号的权限体系,后者在国内网络环境下需要换一个稳定的 API 通道。
这篇文章要解决的就是这两个卡点。我会给你一份可以直接复制的 credentials.json 结构说明、一份完整的读取+查询代码,以及用 TaoToken 统一 Key 接入 llama-index 的配置方式。目标很明确:一次跑通从 Google Docs 读取到查询的完整链路。适合已经装了 llama-index、手里有 Google 服务账号、但被授权和通道问题卡住的开发者。读完你至少能拿到一个能跑的最小可复现示例,而不是一堆零散的 API 文档。
先说清楚整体链路:GoogleDocsReader 负责「读」,llama-index 的 SummaryIndex 负责「索引」,query_engine 负责「查」,而查询这一步背后要调 LLM,这个 LLM 的 Base URL 和 Key 就是我们要用 TaoToken 统一接进来的地方。四段链路,任何一段断了都跑不通,所以我会逐段给配置和验证方法。
2. 前置准备:TaoToken 统一 Key 与 Google 服务账号
在写代码之前,有两套凭证要准备好,很多人会把它们搞混。一套是 Google 服务账号的 credentials.json,用来访问 Google Docs API;另一套是 LLM 的 API Key,用来做查询。这两套是完全独立的,前者管「读文档」,后者管「问问题」。
先说 Google 服务账号这一侧。你需要去 Google Cloud Console 创建一个服务账号,生成 JSON 格式的密钥文件,然后把这个服务账号的邮箱地址添加到目标 Google Docs 文档的共享列表里,权限给「查看者」就够了。这一步是最容易被忽略的:很多人创建了服务账号、下载了 JSON,但忘了把服务账号邮箱加进文档共享,结果读取时报权限错误。服务账号邮箱长这样:xxx@xxx.iam.gserviceaccount.com,在 JSON 文件里的client_email字段就能找到。
再说 LLM 这一侧。llama-index 默认会去读 OpenAI 的地址,国内直连经常超时。我用 TaoToken 的统一 Key 来接管这一层,好处是一个 Key 能覆盖多个模型,Base URL 固定,不用每个项目改一遍。你可以在 TaoToken 的控制台创建一个 API Key,然后在代码里把api_base指向https://taotoken.net/api。注意这里不要带任何多余路径,llama-index 的 OpenAI 兼容层会自动拼接/v1/chat/completions。
安装依赖这块,llama-index 的包拆分比较细,Google Docs 读取器是单独一个包。你需要装三个:
pip install llama-index pip install llama-index-readers-google pip install llama-index-llms-openai第一个是核心框架,第二个是 GoogleDocsReader 所在包,第三个是 LLM 接入层。如果你用的是比较新的 llama-index 版本,llama-index-llms-openai是必须单独装的,否则设置Settings.llm的时候会报找不到类。装完之后可以用pip show llama-index确认版本,建议 0.10 以上,因为 0.10 之后 API 结构变化比较大,老教程里的ServiceContext已经废弃了。
关于 credentials.json 的存放位置,我建议放在项目根目录,然后用环境变量或者绝对路径引用,不要依赖「当前工作目录」。因为 Jupyter 和脚本的工作目录经常不一致,这是FileNotFoundError的高发原因。下面这段是 credentials.json 的字段结构,你下载下来的文件应该长这样,重点确认type、project_id、private_key、client_email四个字段齐全:
{ "type": "service_account", "project_id": "your-project-id", "private_key_id": "xxxxxxxxxxxxxxxx", "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEv...\n-----END PRIVATE KEY-----\n", "client_email": "your-service-account@your-project-id.iam.gserviceaccount.com", "client_id": "1234567890", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/your-service-account%40your-project-id.iam.gserviceaccount.com" }这份文件不要提交到 Git,建议加进.gitignore。如果你在团队里协作,用环境变量注入整个 JSON 字符串也是常见做法,但本地调试阶段直接放文件最省事。
3. 可复制配置:credentials.json 与 Base URL 接入 llama-index
这一节给你可以直接抄的配置。核心是把两件事配好:GoogleDocsReader 怎么找到 credentials.json,以及 llama-index 的全局 LLM 怎么指向 TaoToken。
先看 GoogleDocsReader 的初始化。它接受一个credentials参数,可以传文件路径,也可以传字典。传路径最直观:
from llama_index.readers.google import GoogleDocsReader loader = GoogleDocsReader( credentials="credentials.json" ) documents = loader.load_data(document_ids=["你的文档ID"])这里有个细节:document_ids是列表,可以一次传多个文档 ID。文档 ID 就是 Google Docs URL 里/d/和/edit之间那一串字符。比如https://docs.google.com/document/d/1AbCdEfGhIjKlMnOpQrStUvWxYz/edit,ID 就是1AbCdEfGhIjKlMnOpQrStUvWxYz。复制的时候别把后面的/edit带进去,这是Invalid document ID的常见原因。
然后是 LLM 的接入。llama-index 0.10 之后用Settings做全局配置,把 LLM 和 Embedding 都设好:
from llama_index.core import Settings from llama_index.llms.openai import OpenAI from llama_index.embeddings.openai import OpenAIEmbedding Settings.llm = OpenAI( model="gpt-4o-mini", api_key="你的TaoToken Key", api_base="https://taotoken.net/api", temperature=0.1 ) Settings.embed_model = OpenAIEmbedding( model="text-embedding-3-small", api_key="你的TaoToken Key", api_base="https://taotoken.net/api" )注意api_base写https://taotoken.net/api就行,不要自己加/v1。llama-index 的 OpenAI 兼容层内部会处理路径拼接,你手动加了反而会变成/api/v1/v1/chat/completions,直接 404。这个坑我踩过,报错信息是NotFoundError: Error code: 404,看起来像 Key 的问题,其实是路径重复了。
如果你不想把 Key 硬编码在代码里,用环境变量更规范:
export TAOTOKEN_API_KEY="你的Key" export GOOGLE_APPLICATION_CREDENTIALS="/绝对路径/credentials.json"然后代码里用os.getenv("TAOTOKEN_API_KEY")读取。GOOGLE_APPLICATION_CREDENTIALS这个环境变量是 Google 官方 SDK 认的,设了之后 GoogleDocsReader 甚至可以不用显式传 credentials 参数,它会自动去读。但为了可读性,我还是建议显式传,出问题好排查。
把上面几段拼起来,一个完整的配置初始化大概是这样:
import os from llama_index.core import Settings from llama_index.llms.openai import OpenAI from llama_index.embeddings.openai import OpenAIEmbedding from llama_index.readers.google import GoogleDocsReader API_KEY = os.getenv("TAOTOKEN_API_KEY") BASE_URL = "https://taotoken.net/api" Settings.llm = OpenAI( model="gpt-4o-mini", api_key=API_KEY, api_base=BASE_URL ) Settings.embed_model = OpenAIEmbedding( model="text-embedding-3-small", api_key=API_KEY, api_base=BASE_URL ) loader = GoogleDocsReader(credentials="credentials.json")这段配置跑通之后,后面读取和查询就都是顺水推舟了。如果你还想在多个模型之间切换,TaoToken 的模型对话页面可以直接试不同模型的返回效果,确认哪个模型在你的文档问答场景下更稳,再写回代码里的model字段。
4. 验证请求:一次完整的文档读取与查询
配置好了,现在跑一次完整链路。我会把读取、索引、查询三步拆开,每步都给你验证点,这样哪一步出问题一眼就能看出来。
第一步,读取文档。先只做读取,不要急着建索引:
import logging import sys logging.basicConfig(stream=sys.stdout, level=logging.INFO) logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout)) from llama_index.readers.google import GoogleDocsReader document_ids = ["1AbCdEfGhIjKlMnOpQrStUvWxYz"] # 换成你的真实文档ID loader = GoogleDocsReader(credentials="credentials.json") documents = loader.load_data(document_ids=document_ids) print(f"读取到 {len(documents)} 个文档") print(documents[0].text[:300])这一步的验证点是:len(documents)应该等于你传入的 ID 数量,documents[0].text应该能看到文档正文的前几百字。如果text是空的,说明文档权限没给对,或者文档本身是空的。如果直接抛异常,往下看第 5 节的排错。
第二步,建索引。这里用 SummaryIndex 做演示,因为它对单文档问答最直接:
from llama_index.core import SummaryIndex index = SummaryIndex.from_documents(documents) query_engine = index.as_query_engine()SummaryIndex.from_documents这一步会调用 Embedding 模型,也就是会走 TaoToken 的通道。如果这一步卡住或者报连接错误,说明 Base URL 或 Key 有问题。正常情况下几秒内就建好了。
第三步,查询:
response = query_engine.query("这份文档主要讲了什么?") print(response)query_engine.query会先做检索,再把检索到的上下文和你的问题一起发给 LLM。返回的response是一个 Response 对象,直接 print 会输出文本答案。如果你想在 Notebook 里显示得好看点,可以用:
from IPython.display import Markdown, display display(Markdown(f"**{response}**"))实测下来,整个链路从读取到出答案,单文档大概 10 到 20 秒,取决于文档长度和模型响应速度。如果超过一分钟还没返回,大概率是网络通道的问题,检查一下api_base是否写对。
这里有个小技巧:第一次跑的时候,把Settings.llm的temperature设成 0,这样答案更稳定,方便你判断链路是否正常。等确认跑通了,再根据业务需要调温度。
如果你要读多个文档,document_ids列表里多传几个就行,SummaryIndex 会把它们合并成一个索引。但要注意,SummaryIndex 对多文档的问答效果一般,因为它不做精细的向量检索。多文档场景建议换成VectorStoreIndex:
from llama_index.core import VectorStoreIndex index = VectorStoreIndex.from_documents(documents) query_engine = index.as_query_engine(similarity_top_k=3)similarity_top_k=3表示每次检索最相关的 3 个片段。这个参数对答案质量影响很大,文档长的时候可以调到 5。
5. 常见报错排查:401、local proxy failed、reading choices
这一节把我在实际项目里遇到的报错整理出来,对照着看基本能定位问题。
报错一:FileNotFoundError: [Errno 2] No such file or directory: 'credentials.json'
这是最高频的。原因就一个:代码运行的工作目录里没有这个文件。解决方法是用绝对路径,或者先print(os.getcwd())看看当前目录在哪,再把 credentials.json 放过去。Jupyter Notebook 的工作目录通常是 notebook 所在目录,而脚本是执行命令的目录,两者经常不一样。
报错二:ValueError: Invalid document ID
文档 ID 复制错了。检查两点:一是 ID 里不能有空格或换行,二是别把 URL 里的/edit、?usp=sharing带进来。最稳的办法是直接从浏览器地址栏/d/后面复制到/edit前面。
报错三:google.auth.exceptions.RefreshError: ('invalid_grant: Invalid JWT Signature', ...)
credentials.json 内容损坏或者被截断了。常见于手动复制粘贴 JSON 的时候漏了字符。重新从 Google Cloud Console 下载一份,别手动改。另外确认private_key字段里的\n是转义字符而不是真实换行。
报错四:openai.AuthenticationError: Error code: 401 - {'error': {'message': 'Invalid API key'}}
TaoToken 的 Key 没配对。检查api_key是不是完整复制了,有没有多余空格。另外确认api_base写的是https://taotoken.net/api,不是别的地址。401 基本都是 Key 的问题,跟网络无关。
报错五:APIConnectionError: Connection error或local proxy failed
这类是网络层的问题。如果你本地设了代理环境变量,llama-index 底层的 httpx 会去读HTTP_PROXY/HTTPS_PROXY,代理不通就会报local proxy failed。解决方法是临时清掉代理环境变量:
unset HTTP_PROXY unset HTTPS_PROXY unset ALL_PROXY然后重新跑。如果你确实需要走代理,确保代理地址是通的。但更推荐直接用 TaoToken 的通道,省掉代理这一层。
报错六:KeyError: 'choices'或reading choices相关错误
这个通常出现在流式响应或者模型返回格式异常的时候。llama-index 期望返回体里有choices字段,如果通道返回了非标准格式就会报这个。检查api_base是否指向了正确的兼容端点,以及model字段是不是 TaoToken 支持的模型名。模型名写错有时候不会直接报 404,而是返回一个奇怪的响应体,导致解析choices失败。
报错七:googleapiclient.errors.HttpError: <HttpError 403 when requesting ... returned "The caller does not have permission">
服务账号没有文档的访问权限。回到 Google Docs,点「共享」,把服务账号邮箱(credentials.json 里的client_email)加进去,权限给「查看者」。加完之后可能要等一两分钟权限才生效。
排查的时候有个通用思路:把读取和查询分开验证。先只跑loader.load_data,确认文档能读出来;再单独测 LLM 通道,用一个最简单的Settings.llm.complete("你好")看能不能返回。两段都通了,再合起来跑。这样能把问题范围缩小一半。
6. 把 Key 和通道固定下来,后续接入更省事
链路跑通之后,建议把配置固化下来,别每次新建项目都重新折腾一遍。我的做法是建一个config.py,把 Base URL、模型名、credentials 路径都集中管理:
# config.py import os TAOTOKEN_BASE_URL = "https://taotoken.net/api" TAOTOKEN_API_KEY = os.getenv("TAOTOKEN_API_KEY") DEFAULT_LLM_MODEL = "gpt-4o-mini" DEFAULT_EMBED_MODEL = "text-embedding-3-small" GOOGLE_CREDENTIALS_PATH = os.path.join( os.path.dirname(os.path.abspath(__file__)), "credentials.json" )这样不管在哪个脚本里,from config import *就能拿到统一配置,credentials.json 的路径也不会因为工作目录变化而失效。这个os.path.dirname(os.path.abspath(__file__))的写法是关键,它让路径相对于 config.py 本身,而不是当前工作目录。
如果你后面要做更复杂的 Agent 或者多轮对话,llama-index 的 query engine 可以进一步封装成工具。但那是下一步的事,当前阶段先把「读取-索引-查询」这条最小链路跑稳。我自己的经验是,这条链路里 80% 的问题都出在 credentials.json 的权限和 Base URL 的写法上,把这两个点确认死,后面基本不会再有意外。
另外提醒一句,Google Docs 的 API 有配额限制,免费额度是每分钟 60 次读取请求。如果你要批量读取大量文档,记得加个time.sleep做限流,不然会触发 429。单文档调试阶段不用管这个,但上生产之前一定要考虑。
最后,如果你在接入过程中遇到模型选择的问题,可以先去 TaoToken 的模型对话页面手动试几个模型,看看哪个在你的文档问答场景下回答质量更好,再写回代码。通道和 Key 是固定的,模型可以随时换,这个灵活性对调试很有帮助。