安卓调用 Gemini 微调模型:解决 403 权限拒绝问题
2025-04-21 22:28:34
在 Android 应用中接入 Gemini Tuned Model?解决权限问题是关键
咱们在开发一个安卓应用,比如一个急救助手 App,想用上谷歌的 Gemini API 来提供智能问答。基础的 API 调用跑通了,API Key 也拿到了,一切看起来挺顺利。
但是,基础模型啥都回,不够“专一”。咱希望它只回答急救相关的问题。于是,我们准备了一堆急救数据,训练(或者说微调,tune)了一个专门的模型。高高兴兴地把代码里的模型名字,从 gemini-1.5-flash
换成了我们自己的 tuned model ID,比如 tunedModels/first-aid-123example123
。
// 原来的代码
GenerativeModel gm = new GenerativeModel("gemini-1.5-flash", "YOUR_API_KEY");
// 修改后的代码,试图使用 tuned model
GenerativeModel gm = new GenerativeModel("tunedModels/first-aid-123example123", "YOUR_API_KEY");
model = GenerativeModelFutures.from(gm);
改完一运行,坏了,模型不理我了,啥反应都没有。或者,更糟的是,直接报错。
遇到问题
就像上面说的,把 GenerativeModel
初始化时的 modelName
参数,从标准的模型名称(如 "gemini-1.5-flash"
)换成我们自己微调模型的 ID(格式通常是 "tunedModels/YOUR_TUNED_MODEL_ID"
)之后,应用就无法从模型那里获取任何响应了。
进一步查看应用的日志或者抓包,可能会看到类似下面的错误信息:
{
"error": {
"code": 403,
"message": "You do not have permission to access tuned model tunedModels/first-aid-123example123.",
"status": "PERMISSION_DENIED"
}
}
kotlinx.serialization.MissingFieldException: Field 'details' is required for type with serial name 'com.google.ai.client.generativeai.common.server.GRpcError', but it was missing at path: $.error
这个 PERMISSION_DENIED
(403 错误) 非常明确地告诉我们:没权限访问这个 tuned model 。即使你在 Google Cloud 项目里好像已经给了某些权限(比如截图中显示的 Project Access),但问题依旧。
问题在哪儿?
为什么简单的 API Key 能用标准模型,换成 tuned model 就不行了呢?核心在于权限机制 。
-
API Key vs OAuth 2.0:
- API Key :通常用于访问公开或者半公开的 API 资源。它像一个简单的通行证,证明“我是这个项目的某个应用”,但不精细地区分“我是谁”以及“我具体能干啥”。对于通用的 Gemini 模型(比如
gemini-1.5-flash
),使用 API Key 通常足够了。 - Tuned Model :这是你自己创建的、非公开的资源。访问这种私有资源,Google Cloud 需要更严格的身份验证和授权机制,确认“你是谁”(Authentication)并且“你有没有权限访问这个特定的模型”(Authorization)。这时,光有 API Key 就不够了,通常需要用到 OAuth 2.0 协议,通过用户账户或者服务账号 (Service Account) 来进行认证和授权。
- API Key :通常用于访问公开或者半公开的 API 资源。它像一个简单的通行证,证明“我是这个项目的某个应用”,但不精细地区分“我是谁”以及“我具体能干啥”。对于通用的 Gemini 模型(比如
-
权限范围 (Scope):
- 你为项目设置的访问权限,可能只是针对整个项目的通用权限,或者针对其他服务的权限。访问一个 特定 的 tuned model 资源,需要有指向该资源的明确权限角色。仅仅把 API Key 加到项目里,并不等同于这个 Key 就自动拥有了访问你名下所有细分资源的权限。
-
SDK 的限制或使用方式:
- 虽然
GenerativeModel
构造函数接受modelName
和apiKey
,但可能对于tunedModels/
这种格式的模型 ID,SDK 内部会判断需要更强的认证方式,而不仅仅是 API Key。或者说,SDK 期望在这种情况下,开发者能提供 OAuth 凭证,而不是 API Key。单纯替换modelName
而不改变认证方式,自然就走不通了。
- 虽然
错误日志里的 PERMISSION_DENIED
再次印证了这一点:问题不在于模型 ID 写错了,而是调用者(你的 App,通过 API Key 发起请求)缺乏访问那个特定 tunedModels/first-aid-123example123
资源的权限。
解决方案来了
要解决这个问题,关键在于改变应用的认证方式 ,从简单的 API Key 切换到更安全的、能够证明具体身份和权限的 OAuth 2.0。对于服务端或移动应用,最常用的方式是使用服务账号 (Service Account) 。
方案一:使用 OAuth 2.0 和服务账号 (Service Account)
这是最推荐也是最可能解决问题的方案。
原理:
服务账号是 Google Cloud 里的一个特殊“用户”,代表你的应用程序。你可以给这个服务账号授予访问特定资源的权限(比如你的 tuned model),然后你的应用使用这个服务账号的凭证(一个 JSON 密钥文件)来向 Google API 证明自己的身份并获取访问令牌 (Access Token)。用这个令牌去调用 Gemini API,就能访问授权过的 tuned model 了。
操作步骤:
-
创建服务账号:
- 登录 Google Cloud Console。
- 导航到 "IAM & Admin" > "Service Accounts"。
- 点击 "Create Service Account"。
- 给它起个名字(比如
gemini-android-app-sa
),提供一个。 - 点击 "Create and Continue"。
-
授予权限:
- 在 "Grant this service account access to project" 步骤中,你需要添加一个允许访问 tuned model 的角色。最相关的角色可能是:
roles/aiplatform.user
(AI Platform User): 如果你的模型是通过 AI Platform 管理的。roles/generativeai.tunedmodeluser
(Generative AI Tuned Model User): 这个看起来更针对 Gemini 的 tuned model。优先考虑这个,如果没有,roles/aiplatform.user
也值得尝试。- 或者更宽泛的编辑或拥有者角色(但不推荐,权限过大)。
- 选择合适的角色,点击 "Continue"。
- 在 "Grant this service account access to project" 步骤中,你需要添加一个允许访问 tuned model 的角色。最相关的角色可能是:
-
创建密钥:
- 在 "Grant users access to this service account" 步骤(可选,通常跳过),直接点击 "Done"。
- 找到你刚创建的服务账号,点击进入详情。
- 切换到 "Keys" 标签页。
- 点击 "Add Key" > "Create new key"。
- 选择 "JSON" 格式,点击 "Create"。浏览器会自动下载一个
.json
文件,这个文件非常重要,要妥善保管,不要泄露!
-
将密钥文件安全地集成到 Android 项目中:
- 千万不要 直接把 JSON 文件内容硬编码到代码里,或者直接放在
assets
目录下。这是非常不安全的做法。 - 推荐做法:
- 将其存储在你的后台服务器上,App 启动时从后台安全地获取。
- 或者,对于测试和开发,可以考虑放在
res/raw
目录下,并确保在构建最终 release 版本时通过其他安全方式提供。
- 千万不要 直接把 JSON 文件内容硬编码到代码里,或者直接放在
-
修改 Android 代码以使用服务账号凭证:
-
你需要引入 Google Auth Library for Java。在你的
build.gradle (app)
文件中添加依赖:implementation 'com.google.auth:google-auth-library-oauth2-http:1.19.0' // Check for the latest version implementation 'com.google.api-client:google-api-client-android:2.0.0' // Check for the latest version, handles Android specifics implementation 'com.google.http-client:google-http-client-gson:1.42.3' // Check for the latest version
(版本号请根据实际情况选择最新稳定版)
-
获取访问令牌 (Access Token): 在你的代码中(比如
AssistanceActivity
或者一个专门的认证类),你需要读取 JSON 密钥文件,然后用它来获取一个有时效性的 Access Token。import com.google.auth.oauth2.GoogleCredentials; import com.google.auth.oauth2.ServiceAccountCredentials; import java.io.InputStream; import java.util.Arrays; import java.util.List; // ... inside your activity or a helper class private String getAccessToken() throws IOException { // 假设你把 service account JSON 文件放在 res/raw/service_account.json InputStream serviceAccountStream = getResources().openRawResource(R.raw.service_account); // 定义需要的权限范围 (Scope) - 对于 Gemini API,通常是 cloud-platform List<String> scopes = Arrays.asList("https://www.googleapis.com/auth/cloud-platform"); GoogleCredentials credentials = ServiceAccountCredentials.fromStream(serviceAccountStream) .createScoped(scopes); // 刷新凭证以获取最新的 Access Token credentials.refreshIfExpired(); return credentials.getAccessToken().getTokenValue(); }
-
使用 Access Token 初始化
GenerativeModel
:
目前 (截至写这篇文章时),Gemini Android SDK 的GenerativeModel
构造函数似乎没有直接接受GoogleCredentials
或 Access Token 的选项。它主要设计为使用 API Key。这意味着你可能需要稍微变通一下。可能的 workaround (需要确认 SDK 是否支持自定义 Header):
如果 SDK 允许你自定义网络请求的 Header,你可以尝试在每个请求中手动添加Authorization: Bearer YOUR_ACCESS_TOKEN
Header。更可能的 SDK 设计方向 (未来或检查文档):
查看最新的 Gemini Android SDK 文档,看是否有新的初始化方式,比如:// 伪代码,检查 SDK 是否提供类似方法 // GenerativeModel gm = GenerativeModel.builder() // .setModelName("tunedModels/first-aid-123example123") // .setCredentials(googleCredentials) // 传入 GoogleCredentials 对象 // .build(); // 或者接受一个 TokenProvider // GenerativeModel gm = new GenerativeModel("tunedModels/first-aid-123example123", yourTokenProvider);
如果 SDK 实在不支持:
这可能意味着当前版本的 Android SDK 对 tuned model 的支持还不完善,或者官方推荐通过后端服务来调用 tuned model (后端使用服务账号验证,App 调用你的后端)。但是,让我们先假设可以通过某种方式(可能是未来的SDK更新,或未公开的初始化选项)将认证信息传递给模型。
修改
onCreate
中的初始化部分 (概念性):@Override protected void onCreate(Bundle savedInstanceState) { // ... 其他代码 ... // !! 注意:下面的初始化方式是假设性的,你需要根据 SDK 的实际能力调整 !! // 你需要找到一种方式将获取到的 Access Token (或者 Credentials 对象) // 传递给 GenerativeModel 或其底层的网络请求机制。 // 方案 A: 如果 SDK 未来支持 Credentials 对象 (理想情况) // try { // InputStream saStream = getResources().openRawResource(R.raw.service_account); // List<String> scopes = Arrays.asList("https://www.googleapis.com/auth/cloud-platform"); // GoogleCredentials credentials = ServiceAccountCredentials.fromStream(saStream).createScoped(scopes); // // // 假设有这样的构造器或 Builder (需查证 SDK 文档) // GenerativeModel gm = new GenerativeModel( // "tunedModels/first-aid-123example123", // credentials // 或者其他认证参数 // ); // model = GenerativeModelFutures.from(gm); // // } catch (IOException e) { // // 处理异常,例如显示错误信息 // responseView.setText("认证失败,无法加载模型。"); // Log.e("GeminiAuth", "Failed to initialize with Service Account", e); // sendButton.setEnabled(false); // 禁用发送按钮 // } // 方案 B: 如果只能用 API Key,并且 SDK 不支持其他方式 // 那就说明直接在 Android 端使用带服务账号认证的 Tuned Model 可能有限制 // 你可能需要考虑通过你的后端服务器来代理这个调用。 // 这里的代码就还是用 API Key,但模型名改为 tuned model ID 会继续失败。 // **这种情况下,下面的原始 API Key 初始化逻辑实际上解决不了问题** GenerativeModel gm = new GenerativeModel( "tunedModels/first-aid-123example123", // 你的 tuned model ID "YOUR_API_KEY" // 这个 API Key 在这里对于 tuned model 很可能无效 ); model = GenerativeModelFutures.from(gm); // ... 按钮点击监听器等其他代码保持不变 ... } // modelCall 方法保持不变,因为它处理的是拿到 model 对象之后的事 // private void modelCall(String query) { ... }
-
安全建议:
- 严禁将
service_account.json
文件提交到版本控制系统 (如 Git)。 将其添加到.gitignore
文件中。 - 不要在客户端代码中硬编码任何密钥信息。 考虑使用更安全的密钥管理方案,比如 Google Secret Manager,或者从受信任的后端获取。在 Android 中,也可以使用
Keystore
系统来存储敏感信息,但 JSON 文件本身可能太大,不适合直接存入。 - 最小权限原则: 给服务账号授予必需的最小权限集。如果只需要读取 tuned model,就只给读取权限 (
roles/generativeai.tunedmodeluser
或类似角色),不要给 Owner 或 Editor 权限。
进阶使用技巧:
- 令牌缓存与刷新: Access Token 是有有效期的(通常 1 小时)。
GoogleCredentials
库会自动处理令牌的刷新逻辑。你不需要手动管理它,只需确保在使用前调用refreshIfExpired()
(或者库在后台自动做)。 - 异步获取令牌: 在 Android 主线程执行网络请求(比如获取令牌)或文件读取是不推荐的。应该在后台线程执行
getAccessToken()
方法,可以使用AsyncTask
,ExecutorService
, 或者 Kotlin Coroutines。
方案二:确认模型 ID 和 IAM 权限配置
虽然可能性较小,但还是检查一下基础配置。
原理:
确保你使用的 tuned model ID 完全正确,并且在 Google Cloud IAM 中,你(或者你使用的服务账号)确实被授予了访问该模型资源的权限。
操作步骤:
-
核对模型 ID:
- 回到你创建 tuned model 的地方(可能是 Google AI Studio 或 Cloud Console 的 Vertex AI 部分)。
- 仔细复制完整的模型 ID,格式一般是
tunedModels/your-model-name-and-hash
。确保没有拼写错误或遗漏。
-
检查 IAM 权限:
- 去 Google Cloud Console 的 "IAM & Admin" > "IAM" 页面。
- 如果你在使用服务账号(方案一): 找到你的服务账号(例如
gemini-android-app-sa@YOUR_PROJECT_ID.iam.gserviceaccount.com
),点击编辑按钮(铅笔图标)。检查它是否拥有roles/generativeai.tunedmodeluser
或roles/aiplatform.user
角色。如果没有,添加它。 - 如果你(错误地)试图用自己的 Google 账号直接测试(不推荐用于生产应用): 确保你的 Google 账号邮箱也具有上述角色。
- 关键点: 权限需要授予给发起 API 调用的那个身份 。如果你用服务账号,就给服务账号授权;如果用 API Key(理论上不行,但如果 SDK 有特殊机制),权限可能需要与 API Key 的项目关联,但更可能是 Google 判断你需要一个明确授权的身份,而非简单的 API Key。
命令行示例 (gcloud CLI):
如果你安装了 gcloud
命令行工具,可以用它来检查或添加权限:
# 查看服务账号当前的角色
gcloud projects get-iam-policy YOUR_PROJECT_ID \
--flatten="bindings[].members" \
--format='table(bindings.role)' \
--filter="bindings.members:serviceAccount:YOUR_SERVICE_ACCOUNT_EMAIL"
# 为服务账号添加角色 (例如 tunedmodeluser)
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
--member="serviceAccount:YOUR_SERVICE_ACCOUNT_EMAIL" \
--role="roles/generativeai.tunedmodeluser"
# 或者 --role="roles/aiplatform.user"
把 YOUR_PROJECT_ID
和 YOUR_SERVICE_ACCOUNT_EMAIL
替换成你自己的。
安全建议:
再次强调最小权限原则 。不要为了省事就给 roles/owner
。
总的来说,PERMISSION_DENIED
(403) 错误几乎总是指向认证授权问题 。对于访问 Google Cloud 上的私有资源(如你的 tuned model),标准的解决方案就是使用服务账号和 OAuth 2.0 。你需要创建服务账号,授予正确权限,安全地将密钥集成到你的应用中,并修改代码以使用服务账号凭证(或其生成的 Access Token)来初始化和调用 Gemini API。你需要仔细查阅最新的 Gemini Android SDK 文档,确定它支持哪种基于 OAuth 的认证方式来连接 tuned model。如果 SDK 当前不支持,你可能需要考虑通过后端代理的方式来调用。