Gemini 的协议
functionDeclarations、systemInstruction
一、原理: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"}
]
}
]
}
注意——
messages→contents(字段名换了)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,是基础设施厂商"传统"的代价。