基于Node.js与Taotoken构建高可用AI服务层
环境准备与依赖选型
构建AI服务前,需确保Node.js版本不低于18.x。依赖方面除openai外,建议引入dotenv管理密钥,pino处理结构化日志:
npm install openai dotenv pino密钥配置采用双层防护:开发环境通过.env加载,生产环境则注入容器编排系统的Secret对象。关键变量如下:
TT_API_KEY=sk-xxxxxxxx
TT_ENDPOINT=https://taotoken.net/api
可复用客户端封装
创建core/llmClient.js,将实例化逻辑与配置解耦。此处采用单例模式避免重复创建连接池,同时预留代理配置入口:
import OpenAI from 'openai';
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const envPath = join(dirname(__filename), '../.env');
try {
const envConfig = readFileSync(envPath, 'utf-8')
.split('\n')
.filter(line => line.trim() && !line.startsWith('#'))
.reduce((acc, cur) => {
const [k, v] = cur.split('=');
acc[k.trim()] = v.trim();
return acc;
}, {});
Object.entries(envConfig).forEach(([k, v]) => {
if (!process.env[k]) process.env[k] = v;
});
} catch { /* 生产环境忽略 */ }
if (!process.env.TT_API_KEY) {
throw new Error('TT_API_KEY 未配置');
}
const client = new OpenAI({
apiKey: process.env.TT_API_KEY,
baseURL: process.env.TT_ENDPOINT,
timeout: 30000,
maxRetries: 0, // 由业务层自主控制重试
});
export { client };
弹性调用与熔断设计
网络抖动与平台限流要求调用层具备自愈能力。以下实现融入指数退避、抖动打散及熔断器思想:
// core/resilientCaller.js
import { client } from './llmClient.js';
import logger from './logger.js';
const CIRCUIT_STATES = { CLOSED: 0, OPEN: 1, HALF_OPEN: 2 };
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failures = 0;
this.threshold = threshold;
this.timeout = timeout;
this.state = CIRCUIT_STATES.CLOSED;
this.nextAttempt = Date.now();
}
canExecute() {
if (this.state === CIRCUIT_STATES.CLOSED) return true;
if (this.state === CIRCUIT_STATES.OPEN && Date.now() > this.nextAttempt) {
this.state = CIRCUIT_STATES.HALF_OPEN;
return true;
}
return false;
}
recordSuccess() {
this.failures = 0;
this.state = CIRCUIT_STATES.CLOSED;
}
recordFailure() {
this.failures += 1;
if (this.failures >= this.threshold) {
this.state = CIRCUIT_STATES.OPEN;
this.nextAttempt = Date.now() + this.timeout;
}
}
}
const globalBreaker = new CircuitBreaker();
export async function resilientChat(messages, modelSpec, options = {}) {
const {
maxAttempts = 3,
baseDelay = 500,
maxDelay = 16000,
signal,
} = options;
if (!globalBreaker.canExecute()) {
throw new Error('熔断器开启,服务暂时不可用');
}
let attempt = 0;
let lastErr;
while (attempt < maxAttempts) {
try {
const startAt = performance.now();
const resp = await client.chat.completions.create({
model: modelSpec,
messages,
stream: false,
...options.llmParams,
}, { signal });
globalBreaker.recordSuccess();
logger.info({
model: modelSpec,
latency: Math.round(performance.now() - startAt),
tokens: resp.usage?.total_tokens,
}, 'LLM调用成功');
return {
ok: true,
text: resp.choices[0]?.message?.content,
meta: {
promptTokens: resp.usage?.prompt_tokens,
completionTokens: resp.usage?.completion_tokens,
},
};
} catch (err) {
lastErr = err;
attempt += 1;
const abortive = err.status === 400 || err.status === 401 || err.status === 403;
if (abortive) break;
const retryable = err.status === 429 || err.status >= 500 || err.code === 'ECONNREFUSED';
if (!retryable || attempt >= maxAttempts) break;
const jitter = Math.random() * 0.3 + 0.85; // 0.85~1.15
const delay = Math.min(baseDelay * (2 ** (attempt - 1)) * jitter, maxDelay);
logger.warn({ attempt, delay: Math.round(delay), err: err.message }, '触发重试');
await new Promise(r => setTimeout(r, delay));
}
}
globalBreaker.recordFailure();
return {
ok: false,
error: lastErr?.message || '未知错误',
code: lastErr?.status || 'UNKNOWN',
};
}
多模型路由与动态调度
Taotoken聚合了多家厂商的模型能力,合理的调度策略能兼顾成本与效果。建立模型分级池,按任务特征自动匹配:
// config/modelPool.js
export const TIER_CONFIG = {
economy: {
models: ['qwen-turbo', 'gemini-flash'],
priority: 0,
maxTokens: 4096,
},
balanced: {
models: ['gpt-4o-mini', 'claude-3-haiku'],
priority: 1,
maxTokens: 8192,
},
premium: {
models: ['gpt-4o', 'claude-3-5-sonnet', 'deepseek-chat'],
priority: 2,
maxTokens: 32768,
},
};
export function selectModel(tier = 'balanced', hashSeed = '') {
const pool = TIER_CONFIG[tier];
if (!pool) throw new Error(`未知模型层级: ${tier}`);
// 简单一致性哈希,相同输入路由到相同模型
const idx = hashSeed.split('').reduce((a, c) => a + c.charCodeAt(0), 0) % pool.models.length;
return {
id: pool.models[idx],
...pool,
};
}
业务层调用时声明层级,由调度器透明处理模型选择:
import { selectModel } from '../config/modelPool.js';
import { resilientChat } from '../core/resilientCaller.js';
export async function generateSummary(longText, tier = 'balanced') {
const modelInfo = selectModel(tier, longText.slice(0, 50));
const messages = [
{
role: 'system',
content: '将以下长文本浓缩为200字以内的摘要,保留关键结论。'
},
{ role: 'user', content: longText.substring(0, 15000) }, // 预截断防超限
];
const result = await resilientChat(messages, modelInfo.id, {
llmParams: { max_tokens: 512, temperature: 0.3 },
});
if (!result.ok && tier !== 'economy') {
// 降级重试
logger.info({ from: tier, to: 'economy' }, '触发模型降级');
return generateSummary(longText, 'economy');
}
return result;
}
可观测性埋点
生产环境需全链路追踪Token消耗与延迟分布。以下示例集成Prometheus风格的指标采集:
// middleware/metrics.js
import { Counter, Histogram, register } from 'prom-client';
const llmLatency = new Histogram({
name: 'llm_request_duration_seconds',
help: 'LLM接口调用耗时',
labelNames: ['model', 'tier', 'status'],
buckets: [0.1, 0.3, 0.5, 1, 2, 5, 10],
});
const tokenUsage = new Counter({
name: 'llm_tokens_total',
help: 'Token消耗统计',
labelNames: ['model', 'type'], // type: prompt|completion
});
export function recordMetrics(model, tier, durationMs, tokenMeta, success) {
const status = success ? 'success' : 'failure';
llmLatency.observe({ model, tier, status }, durationMs / 1000);
if (tokenMeta) {
tokenUsage.inc({ model, type: 'prompt' }, tokenMeta.promptTokens || 0);
tokenUsage.inc({ model, type: 'completion' }, tokenMeta.completionTokens || 0);
}
}
export { register };
Express路由集成示例
将上述模块挂载到Web框架,完成端到端闭环:
// routes/ai.js
import { Router } from 'express';
import { generateSummary } from '../services/summaryService.js';
const router = Router();
router.post('/summarize', async (req, res, next) => {
const { text, tier = 'balanced' } = req.body;
if (!text || typeof text !== 'string') {
return res.status(400).json({ error: 'text字段必填' });
}
try {
const result = await generateSummary(text, tier);
if (!result.ok) {
return res.status(502).json({ error: '上游服务异常', detail: result.error });
}
res.json({ summary: result.text });
} catch (err) {
next(err);
}
});
export default router;
部署时建议配合express-rate-limit限制单用户调用频率,并在Nginx或Kong网关层配置全局并发配额,形成多层防护。