1. 为什么要在 Koa2 项目里统一收敛模型调用用 koa-generator 搭骨架这件事本身没什么难度一条命令就能跑起来。真正容易埋坑的是后面项目里一旦要接大模型能力很多人会在每个路由、每个 service 里各写一份请求逻辑base_url 散落在三四个文件api_key 直接硬编码在代码里换一个模型供应商就要全局搜索替换。等到要加日志、加超时、加重试的时候发现根本没有统一的入口可以改。这篇就聚焦这个场景用 koa-generator 生成 Koa2 骨架之后把模型调用统一收敛到 TaoToken 的 Key 和 API 通道上。TaoToken 是一个模型 API 聚合平台你可以把它理解成一个 Key 打通多家模型的入口base_url 和 api_key 都从环境变量读取业务代码只依赖一个封装好的 client。适合谁适合正在用 Node.js Koa2 写后端、准备接入模型能力、又不想把项目搞成一团乱麻的开发者。我会从零走一遍生成骨架、装依赖、改模板、写 config、封装请求、启动验证最后把几个高频报错单独拎出来讲。每一步都有可复制的代码你跟着敲就能跑通。2. 用 koa-generator 生成项目骨架2.1 全局安装与生成koa-generator 是 Koa 官方生态里的脚手架作用和 express-generator 一样帮你把目录结构和基础文件一次性铺好。全局装一次就行npm install -g koa-generator然后生成项目注意命令是koa2而不是koakoa2 mykoa cd mykoa npm install生成出来的目录大致是这样mykoa ├── bin │ └── www # 启动入口配置端口 ├── public # 静态资源 │ ├── images │ ├── javascripts │ └── stylesheets ├── routes # 路由前后端分离时主要写 API │ ├── index.js │ └── users.js ├── views # 模板文件 │ ├── error.pug │ ├── index.pug │ └── layout.pug ├── app.js # 主入口 ├── package.json └── package-lock.json默认模板引擎是 pug。如果你更习惯 ejs装一下再改 app.js 里的配置npm i ejs --save在app.js里找到这段把pug改成ejsapp.use(views(__dirname /views, { extension: ejs }));然后把views下的 pug 文件换成对应的 ejs 文件重启后访问localhost:3000就能看到页面。这一步不是本篇重点但如果你后面要在同一个项目里既提供 API 又渲染页面模板引擎顺手配好会省事。2.2 先跑通默认启动在动模型接入之前先确认骨架本身是活的npm start浏览器打开http://localhost:3000看到 Koa 的欢迎页就说明基础环境没问题。这一步别跳过因为后面报错时你需要能区分是骨架本身有问题还是接入配置写错了。3. 接入 TaoToken 的前置准备3.1 拿到 API Key 和 base_urlTaoToken 的接入方式和主流模型 API 一致核心就两个东西一个 base_url一个 api_key。base_url 固定是https://taotoken.net/apiapi_key 需要你去控制台创建。打开 API Keys 页面https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite创建一个 Key复制出来。注意这个 Key 只显示一次丢了就得重建。创建好之后先别急着写代码把 Key 放进环境变量这是后面所有配置的基础。如果你还没决定用哪个模型可以先到模型对话页面试一下通道是否通https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite在页面上发一条消息能正常返回就说明你的账号和 Key 是有效的。这一步相当于先验证账号再验证代码能帮你排除掉一半的排查工作。3.2 为什么用环境变量而不是硬编码我见过太多项目把 api_key 直接写在app.js或者某个config.js里然后提交到 Git。一旦仓库是公开的Key 就等于泄露了。正确做法是.env文件存真实值加入.gitignore代码里只读process.env.XXX提供一个.env.example给协作者参考Koa2 项目里读.env最省事的方式是用dotenvnpm i dotenv然后在app.js最顶部加一行require(dotenv).config();这样项目启动时就会自动把.env里的变量注入到process.env。4. 可复制的配置骨架4.1 .env 示例在项目根目录新建.env# TaoToken 统一通道配置 TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_API_KEYsk-你的真实key TAOTOKEN_MODELgpt-4o-mini TAOTOKEN_TIMEOUT30000同时在.gitignore里加上.env node_modules再建一个.env.example把值留空方便别人 clone 后知道要配哪些变量TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_API_KEY TAOTOKEN_MODEL TAOTOKEN_TIMEOUT300004.2 config 骨架在项目根目录新建config/taotoken.js把读取逻辑集中在这里// config/taotoken.js const config { baseURL: process.env.TAOTOKEN_BASE_URL || https://taotoken.net/api, apiKey: process.env.TAOTOKEN_API_KEY, defaultModel: process.env.TAOTOKEN_MODEL || gpt-4o-mini, timeout: Number(process.env.TAOTOKEN_TIMEOUT) || 30000, }; if (!config.apiKey) { console.warn([taotoken] 未检测到 TAOTOKEN_API_KEY请检查 .env 文件); } module.exports config;这个文件的作用是所有和 TaoToken 相关的配置只在这里读一次业务代码require它就行。以后要换 base_url 或者加新的默认参数只改这一处。4.3 封装请求 clientKoa2 项目里发 HTTP 请求用 Node 18 自带的fetch就够了不用额外装 axios。新建utils/taotokenClient.js// utils/taotokenClient.js const config require(../config/taotoken); async function chatCompletion(messages, options {}) { const { model config.defaultModel, temperature 0.7, max_tokens 1024, } options; const controller new AbortController(); const timer setTimeout(() controller.abort(), config.timeout); try { const res await fetch(${config.baseURL}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${config.apiKey}, }, body: JSON.stringify({ model, messages, temperature, max_tokens, }), signal: controller.signal, }); if (!res.ok) { const text await res.text(); throw new Error(TaoToken 请求失败: ${res.status} ${text}); } const data await res.json(); return data.choices?.[0]?.message?.content ?? ; } finally { clearTimeout(timer); } } module.exports { chatCompletion };这里有几个设计点值得说明。第一超时用AbortController控制避免请求卡死拖垮整个 Koa 进程。第二错误信息里带上状态码和响应体排查时能直接看到是 401 还是 429。第三返回值只取content业务层不用关心 OpenAI 格式的嵌套结构。4.4 在路由里调用打开routes/index.js加一个测试路由const router require(koa-router)(); const { chatCompletion } require(../utils/taotokenClient); router.get(/, async (ctx, next) { await ctx.render(index, { title: Koa 2 }); }); router.post(/api/chat, async (ctx) { const { message } ctx.request.body; if (!message) { ctx.status 400; ctx.body { ok: false, error: message 不能为空 }; return; } try { const reply await chatCompletion([ { role: user, content: message }, ]); ctx.body { ok: true, reply }; } catch (err) { ctx.status 500; ctx.body { ok: false, error: err.message }; } }); module.exports router;注意 Koa2 默认不解析 POST body需要装koa-bodyparsernpm i koa-bodyparser在app.js里注册const bodyParser require(koa-bodyparser); app.use(bodyParser());到这里配置骨架就完整了.env存值config/taotoken.js读值utils/taotokenClient.js封装请求路由只负责业务逻辑。5. 启动并验证通道可用5.1 启动项目npm start看到localhost:3000的日志就说明起来了。如果config/taotoken.js里没读到 Key控制台会打印那条 warn这时候先回去检查.env是否在根目录、dotenv是否在app.js顶部被 require。5.2 发一条验证请求用 curl 直接打你刚写的接口curl -X POST http://localhost:3000/api/chat \ -H Content-Type: application/json \ -d {message:用一句话解释什么是 Koa2}预期返回类似{ ok: true, reply: Koa2 是一个基于 async/await 的轻量级 Node.js Web 框架通过中间件洋葱模型处理请求。 }如果你在浏览器里测可以用fetchfetch(http://localhost:3000/api/chat, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ message: 你好 }), }) .then((r) r.json()) .then(console.log);能拿到ok: true和一段回复就说明从 Koa 路由到 TaoToken 通道整条链路是通的。这一步验证通过之后你就可以放心地在其他路由里复用chatCompletion不用再关心 Key 和 base_url 的细节。5.3 验证模型是否可用如果你不确定某个模型名在当前账号下是否可用可以到模型对话页面手动选一下https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite在页面上切换模型发消息能返回就说明这个模型在你的账号下是可调用的然后把模型名填回.env的TAOTOKEN_MODEL即可。6. 本篇常见报错排查6.1 401 Unauthorized最常见的原因是 Key 没读到或者写错了。排查顺序确认.env在项目根目录不是放在config/里确认app.js顶部有require(dotenv).config()且在所有其他 require 之前打印一下console.log(process.env.TAOTOKEN_API_KEY?.slice(0, 8))看前几位对不对确认 Key 没有多余空格复制时容易带上换行6.2 404 Not Found大概率是 base_url 拼错了。TaoToken 的 base_url 是https://taotoken.net/api请求路径是/v1/chat/completions拼起来是https://taotoken.net/api/v1/chat/completions。如果你在.env里把 base_url 写成了带/v1的就会变成/v1/v1/...直接 404。6.3 请求超时默认 30 秒超时如果模型响应慢或者网络抖动会触发AbortError。可以在.env里把TAOTOKEN_TIMEOUT调大比如 60000。但更推荐的做法是在业务层做重试而不是无限加大超时。6.4 ctx.request.body 为 undefined忘了注册koa-bodyparser或者注册顺序在路由之后。中间件顺序在 Koa2 里很关键bodyParser必须在路由之前app.use。6.5 模型名不存在报错信息里通常会带model not found之类的字样。回到模型对话页面确认一下当前账号可用的模型名填到.env里。不同账号可用的模型列表可能不一样别直接抄别人的配置。7. 下一步把通道用在长期编码和 Agent 场景单次请求验证通过只是起点。如果你打算把这个 Koa2 项目做成一个长期跑的编码助手后端或者接 Agent 工作流建议把 Key 管理升级到 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewriteCoding Plan 适合需要持续调用、多模型切换、额度可控的场景比单次按量更适合长期项目。接入文档在这里里面有更完整的参数说明和错误码对照https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite如果你用的是 Claude Code 这类工具也有对应的接入方式https://taotoken.net/ClaudeCodeAnthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite回到项目本身接下来你可以做的几件事把chatCompletion加上重试和日志、把模型调用抽成独立的 service 层、在config/taotoken.js里支持多模型配置。这些都不需要改路由因为入口已经收敛好了。骨架搭对后面加功能就是往里填东西而不是到处救火。