1. 从tool_calls回填失败说起:Gemini 3.8 Live 异步工具调用要先把 Key 放进 .env
在本地调试 Gemini 3.8 Live 异步函数调用时,tool_calls返回后下一轮 400 很常见:工具结果没带对tool_call_id,或 Key 没进.env。如果你准备把 Key 写进环境变量,先到 TaoToken 官网 拿 TaoToken Key,并把 Base URL 设置为 https://taotoken.net/api。Google 近期把 Gemini Live 系列推进到支持异步函数调用和视觉上下文,对工具调用开发者来说,重点不只是“模型会不会调工具”,而是异步回填是否可追踪、环境变量是否可复现、视觉输入是否和工具结果分层。很多 demo 在单轮对话里能跑,一旦进入 Live 风格的多轮事件流,就会出现三个典型断点:第一,工具声明和实际回填的函数名不一致;第二,多个异步工具并行执行后,结果顺序和tool_call_id映射错位;第三,视觉上下文塞进错误的消息位置,模型看到了图,却不知道它对应哪一次工具执行。本文按工具调用开发者视角,给出一套可复现的.env、异步工具调用代码和请求日志,并说明 Claude Code、Codex、CC Switch 在 TaoToken 下各自应该怎么配。你最终要拿到的是三样东西:一份不泄露 Key 的环境变量样例、一段能并发执行工具并回填function_response的异步代码、一份能看出finish_reason与工具调用链的请求日志。
2. 先拿 Key 再写 .env:TaoToken 侧准备与 Base URL 固定写法
在把任何 Key 写入项目之前,先完成 TaoToken 侧准备。打开 TaoToken 官网,进入控制台创建 API Key。创建后通常只完整显示一次,复制后放进本地密码管理器或临时终端变量。这里有一个容易忽略的顺序:不要先在代码里硬编码YOUR_API_KEY再回头找 Key,也不要把 Key 提交到 Git。正确顺序是:先拿 Key,再写.env,再用python-dotenv或系统环境变量读取,最后把.env加入.gitignore。
Base URL 只需要写https://taotoken.net/api。注意,这个 Base URL 在工具配置里不加 UTM 参数,UTM 只用于官网入口和转化链接。很多 404 不是模型名错,而是 Base URL 被写成了已经包含/chat/completions的完整路径,SDK 再拼一次就重复了。建议在.env中只保留根地址,让 SDK 或 HTTP 客户端自己拼路径。
一份最小.env样例可以这样写:
# TaoToken 凭证,不要把真实值提交到仓库 TAOTOKEN_API_KEY=YOUR_API_KEY # 工具配置里的 Base URL,不要追加 /chat/completions TAOTOKEN_BASE_URL=https://taotoken.net/api # 模型名建议以模型对话页或控制台当前可用列表为准 GEMINI_LIVE_MODEL=gemini-3.8-live # 异步工具调用的超时与重试,按本地网络情况调整 TAOTOKEN_TIMEOUT=60 TAOTOKEN_MAX_RETRIES=2项目里读取时,可以用python-dotenv:
import os from dotenv import load_dotenv load_dotenv() api_key = os.environ["TAOTOKEN_API_KEY"] base_url = os.environ["TAOTOKEN_BASE_URL"] model = os.getenv("GEMINI_LIVE_MODEL", "gemini-3.8-live") assert api_key != "YOUR_API_KEY", "请先把 .env 里的占位符替换为真实 TaoToken Key" assert base_url == "https://taotoken.net/api", "Base URL 不要带 UTM,也不要手动拼 /chat/completions"如果你在 shell 里临时验证,可以这样做,但不要把真实 Key 写进命令历史:
export TAOTOKEN_API_KEY="YOUR_API_KEY" export TAOTOKEN_BASE_URL="https://taotoken.net/api" export GEMINI_LIVE_MODEL="gemini-3.8-live"到这里,环境变量层已经可复现。下一步才是写异步工具调用代码。顺序反过来,就会出现代码能跑、换机器就 401 的情况。
3. 异步函数调用最小闭环:AsyncOpenAI + tools + await 工具结果
下面这段代码用 OpenAI 兼容的异步客户端调用 TaoToken 的 Base URL,演示 Gemini 3.8 Live 风格的工具调用链。如果你的 Live 入口是原生 WebSocket,工具执行部分仍然可以保持同样的 await 语义:模型返回工具调用事件,本地异步执行工具,再把带tool_call_id的结果回填。关键点有三个:工具声明放在tools,工具结果用role="tool"回填,回填时必须带tool_call_id。
import asyncio import json import os from dotenv import load_dotenv from openai import AsyncOpenAI load_dotenv() client = AsyncOpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], timeout=float(os.getenv("TAOTOKEN_TIMEOUT", "60")), max_retries=int(os.getenv("TAOTOKEN_MAX_RETRIES", "2")), ) MODEL = os.getenv("GEMINI_LIVE_MODEL", "gemini-3.8-live") TOOLS = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的当前天气和出行建议", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名,例如:北京、上海、深圳", } }, "required": ["city"], }, }, }, { "type": "function", "function": { "name": "search_train", "description": "查询两座城市之间的列车班次", "parameters": { "type": "object", "properties": { "from_city": {"type": "string"}, "to_city": {"type": "string"}, "date": {"type": "string", "description": "YYYY-MM-DD"}, }, "required": ["from_city", "to_city", "date"], }, }, }, ] async def get_weather(city: str) -> dict: # 这里替换为你自己的业务查询,不要直接连生产库 await asyncio.sleep(0.3) data = { "北京": {"condition": "晴", "temp_c": 26, "advice": "适合步行"}, "上海": {"condition": "多云", "temp_c": 24, "advice": "带薄外套"}, "深圳": {"condition": "阵雨", "temp_c": 29, "advice": "带伞"}, } return data.get(city, {"condition": "未知", "temp_c": None, "advice": "暂无数据"}) async def search_train(from_city: str, to_city: str, date: str) -> dict: await asyncio.sleep(0.4) return { "from": from_city, "to": to_city, "date": date, "trains": [ {"no": "G101", "depart": "08:00", "arrive": "12:30"}, {"no": "G105", "depart": "10:15", "arrive": "14:50"}, ], } async def execute_tool(call) -> dict: name = call.function.name args = json.loads(call.function.arguments or "{}") if name == "get_weather": result = await get_weather(**args) elif name == "search_train": result = await search_train(**args) else: result = {"error": f"unknown tool: {name}"} return { "tool_call_id": call.id, "name": name, "content": json.dumps(result, ensure_ascii=False), } async def main(): messages = [ { "role": "user", "content": "我明天要从北京去上海,先帮我查一下北京天气,再查高铁,最后给出行建议。", } ] first = await client.chat.completions.create( model=MODEL, messages=messages, tools=TOOLS, tool_choice="auto", ) msg = first.choices[0].message print("first finish_reason:", first.choices[0].finish_reason) print("first message:", msg.model_dump_json(indent=2, ensure_ascii=False)) if not msg.tool_calls: print("模型没有调用工具,直接输出:", msg.content) return messages.append(msg) # 多个工具可以并发执行,但回填时必须按 tool_call_id 映射 tool_results = await asyncio.gather(*(execute_tool(call) for call in msg.tool_calls)) for item in tool_results: messages.append( { "role": "tool", "tool_call_id": item["tool_call_id"], "name": item["name"], "content": item["content"], } ) final = await client.chat.completions.create( model=MODEL, messages=messages, tools=TOOLS, tool_choice="auto", ) final_msg = final.choices[0].message print("final finish_reason:", final.choices[0].finish_reason) print("final content:", final_msg.content) if __name__ == "__main__": asyncio.run(main())这段代码跑通后,你会看到第一轮finish_reason是tool_calls,第二轮才是最终自然语言回复。如果模型一次返回多个工具调用,asyncio.gather会并发执行,但每个结果都带着自己的tool_call_id。不要让结果按执行完成顺序直接 append,否则在并发场景下很容易把天气结果塞给列车调用。
4. 视觉上下文不要挤在最后一条消息:图片、工具结果与异步事件分层
Gemini 3.8 Live 支持视觉上下文,这对工具调用开发者很有吸引力:模型可以先看一张图,再决定调用哪个工具;也可以在工具返回后,再看新的一帧图像继续判断。但视觉输入不能随便塞。一个常见错误是把图片和工具结果放在同一条消息里,导致模型无法区分“这张图是用户原始输入”还是“工具执行后的新证据”。更稳的做法是分层:用户原始消息里放第一张图;工具调用回填时只放role="tool"和结构化结果;如果工具执行后产生了新图像,再追加一条新的user消息,并在文本里说明这张图和哪个工具调用相关。
一个可复制的视觉追加片段如下:
import base64 def image_to_data_url(path: str, mime: str = "image/jpeg") -> str: with open(path, "rb") as f: raw = f.read() b64 = base64.b64encode(raw).decode("utf-8") return f"data:{mime};base64,{b64}" async def append_visual_evidence(messages, image_path: str, note: str): data_url = image_to_data_url(image_path) messages.append( { "role": "user", "content": [ {"type": "text", "text": note}, { "type": "image_url", "image_url": {"url": data_url}, }, ], } )使用时可以这样写:
await append_visual_evidence( messages, "./frames/after_weather_check.jpg", "这是工具返回后的最新画面,请结合天气结果和列车班次,给出是否需要带伞的建议。", )如果你用的是原生 Live 事件流,视觉帧通常以事件形式到达。此时建议在本地维护一个事件队列,把“工具调用事件”“工具结果事件”“视觉帧事件”分开记录时间戳和关联 ID。不要把所有事件压成一个大字符串,否则模型无法稳定追踪上下文。图片过大时先压缩,尤其是 base64 内联场景。视觉上下文和异步工具调用组合时,最贵的往往不是模型推理,而是你把错误的数据结构反复发送。
5. Claude Code 侧:settings.json 里只放 ANTHROPIC_*,不要混 Codex 变量
如果你在 Claude Code 里做辅助开发,配置文件和 Gemini Live 工具调用不是一回事。Claude Code 使用settings.json和ANTHROPIC_*系列环境变量。典型写法是:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "YOUR_CLAUDE_MODEL", "ANTHROPIC_SMALL_FAST_MODEL": "YOUR_FAST_MODEL" } }这里要特别注意:ANTHROPIC_*只用于 Claude Code 这一侧,不要把它写进 Codex 的config.toml。相反,Codex 使用自己的config.toml和 provider 配置,不要指望ANTHROPIC_AUTH_TOKEN会被 Codex 读取。如果你同时使用 Claude Code、Codex 和项目脚本,建议把公共 Key 放在项目.env的TAOTOKEN_API_KEY,Claude Code 的settings.json单独引用同一 Key,但变量名保持各自生态的规范。
还有一个排查点:Claude Code 的 Base URL 同样使用https://taotoken.net/api,不要带 UTM,也不要带多余路径。如果你在settings.json里写了ANTHROPIC_BASE_URL=https://taotoken.net/api/,末尾斜杠通常问题不大,但保持统一更稳。切换配置后,重启 Claude Code 或重新加载环境,确认它读取的是你刚改的settings.json,而不是 shell 里残留的旧变量。
6. Codex 与 CC Switch 三件套:config.toml、.env、settings.json 各管各的
Codex 的配置走config.toml,与 Claude Code 的settings.json完全分开。一个兼容 TaoToken 的 provider 片段可以这样写:
model = "YOUR_CODEX_MODEL" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat"这里env_key = "TAOTOKEN_API_KEY"表示 Codex 会从环境变量读取 Key,所以你的.env或系统环境里要有TAOTOKEN_API_KEY=YOUR_API_KEY。注意不要再写ANTHROPIC_AUTH_TOKEN,Codex 不认识它。Base URL 仍然只写https://taotoken.net/api。
CC Switch 三件套可以理解为三条配置线,各自负责不同工具:
| 配置线 | 典型路径 | 负责内容 | 不要混用 |
|---|---|---|---|
| 项目环境变量 | 项目根目录.env | TAOTOKEN_API_KEY、TAOTOKEN_BASE_URL、模型名 | 不要把真实 Key 提交到 Git |
| Claude Code | ~/.claude/settings.json | ANTHROPIC_BASE_URL、ANTHROPIC_AUTH_TOKEN、模型名 | 不要把ANTHROPIC_*塞给 Codex |
| Codex | ~/.codex/config.toml | model_provider、base_url、env_key | 不要期待 Codex 读取 Claude 变量 |
当你用 CC Switch 做多供应商切换时,核心是切换 provider 片段,而不是把不同工具的变量名混在一起。切换后做三步自检:第一,TAOTOKEN_BASE_URL是否为https://taotoken.net/api;第二,当前工具读取的 Key 变量名是否与配置文件一致;第三,模型名是否来自当前账号可用的模型列表。只要这三步一致,大部分 401 和 404 都能提前排除。
7. 请求日志逐字段排查:从 tool_calls 到最终回答
可复现的请求日志不需要打印完整 Key,只需要记录关键字段。下面是一份排障日志样例:
POST https://taotoken.net/api/chat/completions headers: Authorization: Bearer YOUR_API_KEY*** body: { "model": "gemini-3.8-live", "messages": [ {"role": "user", "content": "先查北京天气,再查明天北京到上海高铁"} ], "tools": [{"type": "function", "function": {"name": "get_weather"}}], "tool_choice": "auto" } response: choices[0].finish_reason = tool_calls choices[0].message.tool_calls[0].id = call_001 choices[0].message.tool_calls[0].function.name = get_weather choices[0].message.tool_calls[0].function.arguments = {"city":"北京"} local tool: await get_weather(city="北京") -> {"condition":"晴","temp_c":26,"advice":"适合步行"} POST https://taotoken.net/api/chat/completions body.messages 追加: { "role": "tool", "tool_call_id": "call_001", "name": "get_weather", "content": "{\"condition\":\"晴\",\"temp_c\":26,\"advice\":\"适合步行\"}" } response: choices[0].finish_reason = stop choices[0].message.content = "北京明天晴,建议步行到站,高铁可选 G101 或 G105。"看日志时重点核对五个字段:
finish_reason:第一轮是不是tool_calls,第二轮是不是stop。如果第一轮就是stop,说明模型没触发工具,检查工具描述和用户意图是否匹配。tool_call_id:回填消息里的 ID 必须和模型返回的一致。缺失或拼错会导致 400。function.name:工具声明名和回填名必须一致,大小写也要一致。get_weather和getWeather是两个不同的名字。arguments:必须是 JSON 字符串。异步并发时先用json.loads解析,再按参数签名调用本地函数。content:工具结果建议用 JSON 字符串回填,并加ensure_ascii=False方便中文日志阅读。
常见错误可以对照处理:401 invalid_api_key通常是.env没加载或 Key 变量名不对;404 not_found通常是 Base URL 写多了路径;400 function_response.name通常是工具名不一致;tool_call_id missing通常是回填时漏字段;timeout通常是本地工具执行太久,应该给工具单独设超时,并把非幂等操作做成可重试队列,而不是无限等待。
8. 文末 CTA:按模型对话 → Coding Plan → 创建 Key → Claude Code 文档走一遍
如果你还没有完成 TaoToken 侧的 Key 和环境变量准备,可以先回到 TaoToken 官网 完成账号入口确认,然后按下面顺序验证你的 Gemini 3.8 Live 异步工具调用链路。
第一步,用模型对话页快速验证模型和工具调用意图是否正常:模型对话。在这里先发一个“查天气并调用工具”的简单请求,观察是否能返回tool_calls。
第二步,如果你要把这套能力放进长期编码或自动化工作流,查看 Coding Plan 的可用范围:Coding Plan。把工具调用、日志和重试策略固定成项目级配置,而不是每次手工改环境变量。
第三步,创建并管理 API Key:API Keys。拿到 Key 后写入.env,使用YOUR_API_KEY占位,Base URL 固定为https://taotoken.net/api,不要把 UTM 参数写进工具配置。
第四步,如果你同时使用 Claude Code,按官方文档配置settings.json与ANTHROPIC_*:Claude Code 文档。记住 Claude Code 用 Claude Code 的变量,Codex 用config.toml,CC Switch 三件套各管各的。完成这四步后,再用本文的异步工具调用代码跑一遍,日志里应该能稳定看到tool_calls、tool_call_id回填和最终stop。