拆.
协议与适配——AI 怎么对接不同 LLM · 第 04

Gemini 的协议

functionDeclarations、systemInstruction

4500读完约 23 分钟craft:B+发布于 2026-06-21

一、原理:Google 的 LLM API 跟 Anthropic / OpenAI 的差别

Google 的 Gemini API 设计有自己的风格——既不像 OpenAI 也不像 Anthropic。它的差别不止"字段名换了"——是根本设计思路不同

我们看几个关键差别。

差别 1:role 命名

Anthropic / OpenAI 用 user / assistant

Gemini 用 user / model

为什么 "model" 而不是 "assistant"?Google 的判断——Gemini 是"模型"不是"助手"。这种命名暗示了 Google 对 LLM 的产品定位——它是技术产品(模型)而不是用户产品(助手)。

实际效果差不多——但每次写 Gemini 代码都要记得 "用 model 不用 assistant"。这是个常见的 adapter bug 来源。

差别 2:messages 嵌套

Anthropic / OpenAI:

{
  "messages": [
    {"role": "user", "content": "Hello"}
  ]
}

Gemini:

{
  "contents": [
    {
      "role": "user",
      "parts": [
        {"text": "Hello"}
      ]
    }
  ]
}

注意——

  • messagescontents(字段名换了)
  • content 是字符串 → parts 是数组
  • 每个 part 必须是 { text: "..." } 这种 wrapper

更 verbose。每个文本都要包装成 { text: "..." }

差别 3:system 是独立字段

Gemini 的 system prompt 字段叫 systemInstruction

{
  "systemInstruction": {
    "parts": [{"text": "You are a helpful assistant."}]
  },
  "contents": [...]
}

这跟 Anthropic 一样——独立字段。

注意 parts 嵌套——又是 { text: "..." } 包装。

差别 4:tools 用 functionDeclarations

Gemini 的 tools 结构:

{
  "tools": [
    {
      "functionDeclarations": [
        {
          "name": "get_weather",
          "description": "...",
          "parameters": {...}
        }
      ]
    }
  ]
}

注意 functionDeclarations 是数组——里面是 function 定义。多了一层嵌套。

为什么?Google 的设想是"tools 数组里可以有不同类型的 tools"——functionDeclarations 是 function 类型、可能还有其他类型。

但实际上其他类型很少用——这层嵌套是 overhead。

差别 5:function call 用 functionCall

response 里 LLM 调用 function:

{
  "candidates": [
    {
      "content": {
        "parts": [
          {
            "functionCall": {
              "name": "get_weather",
              "args": {"city": "Shanghai"}
            }
          }
        ]
      }
    }
  ]
}

注意几件事:

  • response 包在 candidates 数组里(每个 candidate 是个可能的输出,但通常只有 1 个)
  • function call 在 part 的 functionCall 字段
  • args对象(不是 OpenAI 的字符串)—— Google 直接给对象,更好用

差别 6:function response 用 functionResponse

{
  "role": "user",  // 还是 user role
  "parts": [
    {
      "functionResponse": {
        "name": "get_weather",
        "response": {"weather": "sunny", "temp": 25}
      }
    }
  ]
}

注意——response 也是对象,不是字符串。Google 在结构化数据上比 OpenAI 友好。

没有 functionCallId——这是个大问题!

OpenAI / Anthropic 的每次 function call 都有唯一 ID——response 里要带这个 ID 告诉 LLM "我这是哪次调用的回复"。

Gemini 没有 ID——只有 function name。如果 LLM 在一次响应里调了 2 次 get_weather——你给 2 个 functionResponse 回去,LLM 不知道哪个对应哪个调用。

这是 Gemini 协议的重大缺陷。adapter 要自己处理(比如禁止并行调用相同 function)。

差别 7:tool schema 必须 sanitize

Gemini 的 tool schema 对 JSON Schema 支持不完整

  • 不支持 $ref(引用)
  • 不支持 additionalProperties
  • enum 必须是 string 类型(不能 integer)
  • 某些复杂类型不支持

如果你直接把标准 JSON Schema 发给 Gemini——会报错。

opencode 的 gemini.ts adapter 有 150 行专门的 schema sanitize 代码——把标准 JSON Schema 转成 Gemini 能接受的形式。

这是个纯适配开销——Gemini 协议的限制让你不得不写这么多代码。

把这些差别合起来——Gemini API 设计有自己的逻辑,但实际用起来比 Anthropic 和 OpenAI 都麻烦

  • 字段名独特(role 是 model)
  • 结构 verbose(parts 嵌套)
  • function call 没 ID(并行调用难处理)
  • schema 需要 sanitize(150 行专门代码)

我们看 opencode 怎么处理。

二、案例:opencode gemini.ts 的 schema sanitize 150 行

R6-02 阶段的研究告诉我们——opencode 的 gemini.ts 大概 487 行——比 OpenAI Chat 短点、比 Anthropic 长得多。

主要复杂度来自两件事——schema sanitize(约 150 行)和 没有 functionCallId 的处理

Schema sanitize 实现

简化版逻辑:

function sanitizeForGemini(schema) {
  if (!schema) return schema
  
  const result = {...schema}
  
  // 1. 删 $ref
  if (result.$ref) {
    // 内联引用
    result = resolveRef(result, rootSchema)
  }
  
  // 2. 删 additionalProperties
  delete result.additionalProperties
  
  // 3. 把 integer enum 转 string
  if (result.type === "integer" && result.enum) {
    result.type = "string"
    result.enum = result.enum.map(String)
  }
  
  // 4. 删不支持的字段
  delete result.title
  delete result.examples
  delete result.default  // 实际可能保留
  
  // 5. 递归处理嵌套
  if (result.type === "object" && result.properties) {
    result.properties = Object.fromEntries(
      Object.entries(result.properties)
        .map(([k, v]) => [k, sanitizeForGemini(v)])
    )
  }
  
  if (result.type === "array" && result.items) {
    result.items = sanitizeForGemini(result.items)
  }
  
  // 6. 处理 anyOf / oneOf
  if (result.anyOf) {
    // Gemini 只接受 "type" 数组
    result.anyOf = result.anyOf.map(sanitizeForGemini)
  }
  
  return result
}

每一步都对应 Gemini 协议的一个限制。

为什么 Gemini 这么严格?两个理由:

理由 a:Vertex AI 的历史包袱

Gemini API 是从 Google Cloud Vertex AI 演化来的——继承了 Vertex 的 schema 规则。Vertex 当年支持的 schema 是子集——延续到 Gemini。

理由 b:Google 的 protobuf 偏好

Google 内部喜欢用 protobuf——schema 要"严格类型"。JSON Schema 的灵活性($ref、anyOf、灵活 enum)在 protobuf 里不存在。

Gemini 接受的 schema 实际上是"protobuf-friendly JSON Schema"——比标准 JSON Schema 严格。

没有 functionCallId 的处理

Gemini 不给 function call ID——opencode 怎么对应 call 和 response?

简化版逻辑:

function trackFunctionCalls(response) {
  const calls = []
  for (const part of response.candidates[0].content.parts) {
    if (part.functionCall) {
      // 用 name + 顺序作为隐式 ID
      calls.push({
        id: `${part.functionCall.name}_${calls.length}`,
        name: part.functionCall.name,
        args: part.functionCall.args
      })
    }
  }
  return calls
}

opencode 用 name + 顺序索引 作为隐式 ID。然后给 Gemini 发 functionResponse 时按顺序对应。

这种 hack 有限制——Gemini 上 opencode 禁止并行同名调用。如果 LLM 想同时调 get_weather("Shanghai")get_weather("Beijing")——opencode 强制串行。

这是个Gemini 限制的 leak 到产品——并行加速能力被砍。但没办法——Gemini API 设计如此。

其他 Gemini quirks

  • response 在 candidates[0]——总是数组,但实际只用第 1 个
  • 每个 text 要 {text: "..."} 包装——内部代码要做转换
  • stop_reason 字段叫 finishReason——值是 STOP / MAX_TOKENS / SAFETY / RECITATION(大写)——跟 Anthropic 的小写、OpenAI 的混合都不一样
  • safety filter 强——某些内容会被过滤、API 返回 SAFETY finishReason

每个 quirk 都是 adapter 层要处理的。把这些加起来——Gemini adapter 复杂度仅次于 Anthropic adapter——但复杂度的性质不同

  • Anthropic 的复杂——为了利用 advanced 特性(thinking、cache、parallel)
  • Gemini 的复杂——为了克服协议限制(schema sanitize、没有 ID、强 safety)

两种复杂的 ROI 不一样——前者带来质量提升、后者只是"勉强能用"。

三、设计启示:每个 LLM 都有"奇葩 quirk"

这一章的核心论点:LLM API 不是"标准化协议"——每家有自己的奇葩。adapter 层要吃下这些奇葩

如果你做 AI 产品考虑 Gemini API,下面几条原则有用:

1. schema sanitize 是必做

如果你不 sanitize——你的 tool 调用大概率失败。

写一个独立的 sanitize 函数——把标准 JSON Schema 转 Gemini 兼容。

2. 接受没有 function call ID

Gemini 上你不能"完美追踪"function call。

要么禁并行同名调用——要么自己用 name + 顺序做隐式 ID。

opencode 选了前者——简单可靠。

3. parts 嵌套要包装

不要忘了——每个 text 都要 {text: "..."} 包装。

写一个 helper:toGeminiParts(text)

4. response 在 candidates[0]

总是取第一个 candidate——其他基本不用。

5. 注意 finishReason 大写

不要假设小写——Gemini 用大写。adapter 要处理大小写转换。

6. 注意 safety filter

Gemini 的 safety filter 比其他 provider 强——某些"边界内容"会被拒。

如果你的产品涉及边界场景(医疗、法律、安全研究)——Gemini 上可能频繁触发 safety、不可用。

7. Gemini 的优势

虽然有这些 quirk——Gemini 也有优势:

  • 长 context(1M tokens)——对某些场景有用
  • 多模态原生支持(图片、音频、视频)——比 OpenAI 强
  • 价格便宜——比 Claude / GPT-4 便宜

如果你的场景需要这些优势——Gemini 值得用,吃下 quirk 是代价。

8. 不要在 Gemini 上做精细 tool 工作

Gemini 的 function call 限制(没 ID、schema 严格)让它不适合复杂 tool 编排场景。

如果你的 AI Agent 重度依赖 tool calling——Gemini 体验比 Anthropic / OpenAI 差。

考虑:

  • 用 Gemini 做"长 context 阅读"
  • 用 Claude 做"tool calling 重场景"
  • 不要强行让 Gemini 做它不擅长的事

最后一个观察。Gemini API 反映了 Google 的工程文化——严格、protobuf 化、内部一致性优先。

但 LLM 应用场景需要的是灵活性——LLM 输出不严格、tool schema 灵活、并行调用常见。Gemini 协议的"严格"在这种场景下是负担。

opencode 通过 487 行的 adapter 吃下这些负担——让 Gemini 能在 opencode 里"勉强工作"。但能力受限是事实——某些 opencode 特性(并行 task、复杂 tool schema)在 Gemini 上表现差。

这是 multi-provider AI 产品的真实成本——不是"加个 adapter 就行",是接受每个 provider 的真实限制

下一章 6.5 我们看一个特别的 provider——Amazon Bedrock。它用 AWS 自家协议而不是 SSE,是基础设施厂商"传统"的代价。