1. 先把链路画清楚:微信 AI 调小程序时,Token 到底在哪一层被消耗
在微信云开发的云函数里接大模型,卡住人的从来不是业务代码,而是那两行配置:API Key填什么、请求地址填哪个。我见过最常见的两种翻车方式——把 Key 直接写进小程序前端app.js,或者把baseURL写成某个国外官方域名,结果云函数调用时直接 401 或超时。这篇从接口配置视角把整条链路拆开:先在 TaoToken 官网(https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=cloud_fn_env_intro )拿到 Key 和请求地址,请求地址统一写成https://taotoken.net/api,然后回到微信云开发控制台填环境变量。整篇产出三样可复现的东西:环境变量键值表、接口地址的填法对照、以及一张 Token 消耗归属表。
先把链路讲明白,否则后面填什么都是猜。
微信生态里「AI 调用个人小程序」这件事,落到工程上其实是一条单向数据流:
微信侧 AI 意图 / 用户在小程序内触发 │ ▼ 小程序前端(wxml / js) ← 这里绝对不能放 Key │ wx.cloud.callFunction ▼ 微信云函数(Node.js,运行在腾讯云侧) │ HTTPS POST ▼ https://taotoken.net/api → 模型 │ ▼ 返回 usage(prompt_tokens / completion_tokens)→ 云函数日志关键判断点有三个:
第一,Token 消耗发生在云函数发出的那一次 HTTPS 请求上,不在小程序前端,也不在微信 AI 的意图识别环节。所以「谁的 Key」决定了「谁扣费」,这直接引出后面的 Token 消耗归属表。
第二,云函数是服务端环境,它拿不到小程序前端的wx.request环境,必须走 Node.js 的https或axios。这一点决定了配置文件的写法和你本地调试的方式完全不同。
第三,请求地址是一个整体,不是两段。很多人把它拆成「域名 + 路径」分别填,填着填着就多出一个/v1或者少一个/v1,于是 404。正确做法是:环境变量里只存BASE_URL = https://taotoken.net/api,路径在代码里拼接,永远不在配置文件里硬编码完整 endpoint。
搞清楚这三点,下面的配置才有意义。
2. 填环境变量之前:先去 TaoToken 拿 Key 与请求地址
顺序很重要。原文里那种「先写代码再想办法找 Key」的流程,会在联调阶段浪费大量时间。正确顺序是:
- 打开https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=key_and_base_url,进入控制台体系;
- 在API Keys页面创建一个 Key,命名建议带上用途,例如
wx-cloudfn-prod、wx-cloudfn-dev; - 在模型列表里复制你要用的模型 ID(它是字符串,不是显示名称);
- 记下请求地址:
https://taotoken.net/api。
创建 Key 的直达入口在这里:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=key_create_inline
这里有三个工程习惯值得养:
按环境拆 Key。本地调试用wx-cloudfn-dev,线上云函数用wx-cloudfn-prod。一旦线上出现异常调用量,你可以直接把线上那个 Key 停掉,而不影响本地。
按云函数拆 Key。如果你不止一个云函数在调模型(比如aiChat、aiSummary、ocrHelper),给每个云函数单独建 Key。这是后面 Token 消耗归属表能对上账的前提。
Key 只出现在两个地方:云开发控制台的环境变量里,以及你本地.env(且.env必须在.gitignore里)。它不应该出现在小程序代码包、Git 提交历史、截图、聊天记录里。
Key 拿到后先做一次握手验证,别等云函数部署完再排查。本地开个终端:
export TAOTOKEN_API_KEY="YOUR_API_KEY" export TAOTOKEN_BASE_URL="https://taotoken.net/api" curl -sS -X POST "$TAOTOKEN_BASE_URL/v1/chat/completions" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -d '{ "model": "YOUR_MODEL_ID", "messages": [{"role": "user", "content": "ping"}], "max_tokens": 16 }'返回体里如果能看到choices和usage,说明 Key 与地址这一对组合是对的。这一步必须在填云函数环境变量之前完成——否则云函数报错时,你无法区分是 Key 错、地址错,还是云函数网络出不去。
3. 微信云函数的环境变量与最小可运行代码
微信云开发的环境变量,实际生效位置是云开发控制台 → 云函数 → 选中函数 → 配置 → 环境变量。云函数目录下的config.json在不同版本控制台里对字段支持不完全一致,所以这里的原则是:环境变量以控制台配置为准,config.json只放权限与触发器。
控制台里需要填的键值表如下:
| 键名 | 值 | 说明 |
|---|---|---|
TAOTOKEN_API_KEY | YOUR_API_KEY | 从 API Keys 页面创建,按云函数独立 |
TAOTOKEN_BASE_URL | https://taotoken.net/api | 末尾不要带/,不要带/v1 |
TAOTOKEN_MODEL | YOUR_MODEL_ID | 模型 ID 字符串 |
TAOTOKEN_TIMEOUT_MS | 20000 | 云函数侧超时兜底 |
云函数目录下的config.json可以这样写,用于声明权限和触发方式:
{ "permissions": { "openapi": [] }, "triggers": [] }真正干活的是index.js。下面这份代码是可直接跑的最小实现,用 Node 内置https模块,不依赖额外 npm 包,避免云函数依赖安装失败:
// cloudfunctions/aiChat/index.js const https = require('https'); const { URL } = require('url'); const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const BASE_URL = (process.env.TAOTOKEN_BASE_URL || 'https://taotoken.net/api') .replace(/\/+$/, ''); const API_KEY = process.env.TAOTOKEN_API_KEY; const MODEL = process.env.TAOTOKEN_MODEL || 'YOUR_MODEL_ID'; const TIMEOUT_MS = Number(process.env.TAOTOKEN_TIMEOUT_MS || 20000); function postJson(pathname, payload) { return new Promise((resolve, reject) => { const target = new URL(BASE_URL + pathname); const body = JSON.stringify(payload); const req = https.request( { hostname: target.hostname, port: 443, path: target.pathname + target.search, method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${API_KEY}`, 'Content-Length': Buffer.byteLength(body) }, timeout: TIMEOUT_MS }, (res) => { let raw = ''; res.setEncoding('utf8'); res.on('data', (chunk) => { raw += chunk; }); res.on('end', () => { if (res.statusCode < 200 || res.statusCode >= 300) { return reject( Object.assign(new Error('UPSTREAM_' + res.statusCode), { raw }) ); } try { resolve(JSON.parse(raw)); } catch (e) { reject(Object.assign(new Error('BAD_JSON'), { raw })); } }); } ); req.on('timeout', () => req.destroy(new Error('UPSTREAM_TIMEOUT'))); req.on('error', reject); req.write(body); req.end(); }); } exports.main = async (event) => { const prompt = (event && event.prompt) || ''; if (!API_KEY) return { ok: false, code: 'MISSING_KEY' }; if (!prompt) return { ok: false, code: 'EMPTY_PROMPT' }; const startedAt = Date.now(); try { const data = await postJson('/v1/chat/completions', { model: MODEL, messages: [ { role: 'system', content: '你是小程序内的助手,回答简短、直接。' }, { role: 'user', content: prompt } ], max_tokens: 512, temperature: 0.6 }); const choice = data.choices && data.choices[0]; const text = choice && choice.message ? choice.message.content : ''; // 关键:把用量写进日志,后面归属表靠它对账 console.log('[TAOTOKEN_USAGE]', JSON.stringify({ requestId: data.id || null, model: data.model || MODEL, usage: data.usage || null, latencyMs: Date.now() - startedAt })); return { ok: true, text, usage: data.usage || null, requestId: data.id || null }; } catch (err) { console.error('[TAOTOKEN_ERROR]', err.message, String(err.raw || '').slice(0, 500)); return { ok: false, code: err.message, detail: String(err.raw || '').slice(0, 500) }; } };小程序端这样调,注意参数只有业务数据,没有任何密钥:
// pages/chat/chat.js Page({ data: { answer: '' }, async onAsk() { const res = await wx.cloud.callFunction({ name: 'aiChat', data: { prompt: '帮我用一句话解释云函数是什么' } }); const payload = res.result || {}; this.setData({ answer: payload.ok ? payload.text : `调用失败:${payload.code}` }); } });部署时还有两件容易漏的事:一是云函数的超时时间要在控制台调大(默认值对模型调用往往不够,建议 20 秒起);二是Node 版本要选支持你所写语法的版本,别用太老的运行时。
4. 接口地址的三种正确写法,和三种必错写法
「接口地址怎么填」这个问题,90% 的答案取决于你在写哪一种兼容协议。TaoToken 的请求地址是https://taotoken.net/api,但拼到不同端点上,形式不一样。
正确写法一:OpenAI 兼容(云函数最常用)
BASE_URL = https://taotoken.net/api ENDPOINT = BASE_URL + /v1/chat/completions 最终请求 = https://taotoken.net/api/v1/chat/completions正确写法二:Anthropic 兼容(Claude Code 用)
ANTHROPIC_BASE_URL = https://taotoken.net/api注意这里不加/v1,客户端会自己在后面拼/v1/messages。你手动加上去,就会变成/api/v1/v1/messages,直接 404。
正确写法三:Codex 供应商配置
base_url = https://taotoken.net/api/v1Codex 的model_providers里要带/v1,这一点和 Claude Code 恰好相反。这就是为什么「不要把一个工具的 base_url 直接抄给另一个工具」是一条硬规矩。
必错写法一:把 Key 拼到 URL 里。
https://taotoken.net/api?key=YOUR_API_KEY ← 错误Key 一律走Authorization: Bearer YOUR_API_KEY请求头。
必错写法二:把 endpoint 塞进环境变量。
TAOTOKEN_BASE_URL = https://taotoken.net/api/v1/chat/completions ← 错误这样后面再拼路径就重复了。环境变量只存到/api这一层。
必错写法三:混用不同工具的地址习惯。
把 Claude Code 的https://taotoken.net/api直接粘进 Codex 的base_url,或者反过来给 Claude Code 加上/v1,都是典型症状:配置看着没问题,请求全是 404。
为了少踩坑,建议在代码里加一个启动自检,把最终拼出来的地址打印一次:
console.log('[TAOTOKEN_ENDPOINT]', `${BASE_URL}/v1/chat/completions`);云函数日志里出现的那一行,就是排障时第一个要看的东西。
5. Token 消耗归属表:把每一笔调用对上账
线上跑起来之后,真正难的不是「能不能调通」,而是「这个月的消耗是谁花的」。个人小程序往往同时存在多个触发入口:用户主动提问、微信侧 AI 意图触发、定时任务预生成、后台管理页测试。如果没有 Key 隔离和日志字段,这些消耗会全糊在一起。
先按入口拆 Key,再按 Key 记账。这张表建议直接抄进你的项目文档:
| 调用入口 | 载体 | 建议 Key 名 | 请求地址 | Token 归属 | 必须记录的日志字段 |
|---|---|---|---|---|---|
| 用户小程序内提问 | aiChat云函数 | wx-cloudfn-prod-chat | https://taotoken.net/api | 线上用户交互消耗 | requestId/usage/openid |
| 微信 AI 意图触发 | aiChat云函数(同函数分流) | wx-cloudfn-prod-chat | https://taotoken.net/api | 线上用户交互消耗 | requestId/usage/scene |
| 定时预生成内容 | aiCron云函数 | wx-cloudfn-prod-cron | https://taotoken.net/api | 运营成本,单独核算 | requestId/usage/taskName |
| 本地调试 | 本地 Node 脚本 | wx-cloudfn-dev | https://taotoken.net/api | 开发成本,不计入线上 | requestId/usage/env=dev |
| 后台管理测试 | aiAdmin云函数 | wx-cloudfn-admin | https://taotoken.net/api | 管理成本,单独核算 | requestId/usage/operator |
落地这张表只需要三件事:
第一,每一个云函数的前 20 行里,把 Key 从环境变量读出来,不要有兜底默认值。缺 Key 就返回MISSING_KEY,让它明确失败,而不是偷偷用别的 Key 跑起来。
第二,每一次调用都打一行结构化日志,形如:
console.log('[TAOTOKEN_USAGE]', JSON.stringify({ fn: 'aiChat', env: 'prod', scene: event.scene || 'direct', openid: (wxContext && wxContext.OPENID) || 'anonymous', requestId: data.id, usage: data.usage }));第三,按月做一次对账。把云函数日志里[TAOTOKEN_USAGE]的行捞出来,按fn聚合usage.total_tokens,与控制台上的用量记录比对。差值大的一栏,通常就是某个漏了日志的分支,或者某个还在用旧 Key 的历史版本。
这套做法在个人小程序阶段看起来有点重,但它的收益很直接:当某天发现消耗异常上涨时,你能在两分钟内定位到是哪个入口,而不是从零开始加日志。
6. 排障手册:401、404、超时、串号
401 / 403:Key 的问题。先确认云函数里读到的TAOTOKEN_API_KEY不是空字符串。控制台的环境变量有个隐蔽坑——改完环境变量必须重新部署/重启云函数才会生效。另外检查是不是复制 Key 时带上了首尾空格,Authorization头里多一个空格就会鉴权失败。
404:地址的问题。把最终请求地址完整打出来看一遍。常见三种:多了一层/v1、少了一层/v1、BASE_URL末尾带了/导致出现//v1。前两种在上一节已经列过,第三种在代码里用.replace(/\/+$/, '')兜住。
超时:分两段排查。第一段是云函数本身的超时时间,默认值偏短,要在控制台调大。第二段是模型响应的耗时,max_tokens给得太大、prompt 太长都会拉长响应。建议在代码里显式设置timeout并在超时时主动destroy,同时把max_tokens压到一个你业务真正需要的量级。
结果串号:并发场景下的上下文污染。如果你的云函数把对话历史存在模块级变量里(let history = []),并发请求会互相污染。云函数实例是复用的,必须把历史存在数据库或每次请求显式传入,不要把状态挂在模块作用域上。
日志里看不到 usage:有些返回路径没有usage字段,比如流式响应的中间分片。如果你用的是流式,需要在最后一个 chunk 里取用量,或者干脆用非流式做主链路、流式只做体验优化。
这里有一个通用的排查顺序,建议固化下来:
① 本地 curl 通不通 → 不通:Key 或地址问题 ② curl 通、云函数不通 → 云开发网络/环境变量未生效问题 ③ 云函数通、返回慢 → 超时配置 / max_tokens 问题 ④ 返回通、结果乱 → 并发状态污染问题按这个顺序走,基本不会在错误的方向上浪费时间。
7. 本机 AI 工具同步改供应商:Claude Code、Codex 与 CC Switch 三件套
云函数跑通之后,很多人的下一步是把本机的开发工具也切到同一个供应商,这样 Key 管理、用量查看都在一处。这里要特别强调:不同工具用不同配置文件,字段不能互相套用。
Claude Code 用settings.json,走ANTHROPIC_*系列变量:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "YOUR_MODEL_ID" } }注意ANTHROPIC_BASE_URL保持到/api,不要加/v1。
Codex 用config.toml,走model_providers配置块:
model = "YOUR_MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api/v1" env_key = "TAOTOKEN_API_KEY" wire_api = "chat"注意这里的base_url是带/v1的,和 Claude Code 的写法正好相反。把ANTHROPIC_*那套变量名抄进 Codex 配置里,是完全没有效果的——Codex 不读这些变量名。
CC Switch 三件套:所谓三件套,指的是同时维护三份配置——Claude Code 的settings.json、Codex 的config.toml、以及一份通用的环境变量文件(系统环境变量或.env)。用 CC Switch 这类工具在多个供应商之间切换时,三份都要一起指向同一家,否则会出现「Claude Code 切过去了、Codex 还在用旧的」这种半切换状态。
切换后做一次验证,别只看工具启动成功:
# 通用环境变量(Codex 侧读取) export TAOTOKEN_API_KEY="YOUR_API_KEY" # 快速验证地址与 Key 是否配对 curl -sS -o /dev/null -w "%{http_code}\n" \ -X POST "https://taotoken.net/api/v1/chat/completions" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"YOUR_MODEL_ID","messages":[{"role":"user","content":"ping"}],"max_tokens":8}'返回200就说明这一侧的配对是对的。Claude Code 的具体字段与常见问题,官方文档里写得更细:https://taotoken.net/doc/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude_code_doc_inline
8. 上线前 Checklist 与下一步
把上面所有内容压成一张上线前自查表,逐条打勾:
- [ ] 云函数环境变量只有 4 个键:
TAOTOKEN_API_KEY、TAOTOKEN_BASE_URL、TAOTOKEN_MODEL、TAOTOKEN_TIMEOUT_MS - [ ]
TAOTOKEN_BASE_URL的值是https://taotoken.net/api,无尾斜杠、无/v1 - [ ] 小程序前端代码包里搜不到
YOUR_API_KEY、sk-、Bearer - [ ]
.env已加入.gitignore,Git 历史里没有 Key - [ ] 每个云函数使用独立 Key,命名带环境与用途
- [ ] 每次调用都打
[TAOTOKEN_USAGE]结构化日志 - [ ] 云函数超时时间已从默认值调大
- [ ] 已用本地 curl 完成一次握手验证
- [ ] Token 消耗归属表已落进项目文档,按月对账
完成这些之后,链路的稳定性就不再依赖运气了。真正剩下的变量只有两个:模型选型,和 prompt 质量。
如果你的下一步是把本机开发工作流也统一过来,建议按这个顺序推进:先在模型对话页面直接试几个模型,确认哪个在中文短回答和长上下文两个场景下都稳(https://taotoken.net/models/detail/chat?utm_source=taotoken_aicg_blog_end&utm_content=cta_chat );确定模型后看Coding Plan的额度结构是否符合你的日常调用量(https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=cta_coding_plan );然后按第 2 节的做法为云函数和本机工具分别创建 Key(https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=cta_create_key );最后照着文档把本机 AI 工具的供应商配置改过来(https://taotoken.net/doc/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_content=cta_claude_code_doc )。
回到最开始那个问题——云函数耗 Token 的接口地址怎么填?答案就一句话:环境变量存到https://taotoken.net/api这一层,路径在代码里拼,Key 走请求头,工具之间不互相抄地址。剩下的,都是把日志和对账做扎实。