返回

安卓调用 Gemini 微调模型:解决 403 权限拒绝问题

Ai

在 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 就不行了呢?核心在于权限机制

  1. 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) 来进行认证和授权。
  2. 权限范围 (Scope):

    • 你为项目设置的访问权限,可能只是针对整个项目的通用权限,或者针对其他服务的权限。访问一个 特定 的 tuned model 资源,需要有指向该资源的明确权限角色。仅仅把 API Key 加到项目里,并不等同于这个 Key 就自动拥有了访问你名下所有细分资源的权限。
  3. SDK 的限制或使用方式:

    • 虽然 GenerativeModel 构造函数接受 modelNameapiKey,但可能对于 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 了。

操作步骤:

  1. 创建服务账号:

    • 登录 Google Cloud Console。
    • 导航到 "IAM & Admin" > "Service Accounts"。
    • 点击 "Create Service Account"。
    • 给它起个名字(比如 gemini-android-app-sa),提供一个。
    • 点击 "Create and Continue"。
  2. 授予权限:

    • 在 "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"。
  3. 创建密钥:

    • 在 "Grant users access to this service account" 步骤(可选,通常跳过),直接点击 "Done"。
    • 找到你刚创建的服务账号,点击进入详情。
    • 切换到 "Keys" 标签页。
    • 点击 "Add Key" > "Create new key"。
    • 选择 "JSON" 格式,点击 "Create"。浏览器会自动下载一个 .json 文件,这个文件非常重要,要妥善保管,不要泄露!
  4. 将密钥文件安全地集成到 Android 项目中:

    • 千万不要 直接把 JSON 文件内容硬编码到代码里,或者直接放在 assets 目录下。这是非常不安全的做法。
    • 推荐做法:
      • 将其存储在你的后台服务器上,App 启动时从后台安全地获取。
      • 或者,对于测试和开发,可以考虑放在 res/raw 目录下,并确保在构建最终 release 版本时通过其他安全方式提供。
  5. 修改 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 中,你(或者你使用的服务账号)确实被授予了访问该模型资源的权限。

操作步骤:

  1. 核对模型 ID:

    • 回到你创建 tuned model 的地方(可能是 Google AI Studio 或 Cloud Console 的 Vertex AI 部分)。
    • 仔细复制完整的模型 ID,格式一般是 tunedModels/your-model-name-and-hash。确保没有拼写错误或遗漏。
  2. 检查 IAM 权限:

    • 去 Google Cloud Console 的 "IAM & Admin" > "IAM" 页面。
    • 如果你在使用服务账号(方案一): 找到你的服务账号(例如 gemini-android-app-sa@YOUR_PROJECT_ID.iam.gserviceaccount.com),点击编辑按钮(铅笔图标)。检查它是否拥有 roles/generativeai.tunedmodeluserroles/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_IDYOUR_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 当前不支持,你可能需要考虑通过后端代理的方式来调用。