当前位置:首页 > 技术 > 正文内容

基于Node.js与Taotoken构建高可用AI服务层

访客 技术 2026年9月3日 1

环境准备与依赖选型

构建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网关层配置全局并发配额,形成多层防护。

相关文章

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

linux screen 用法详情 (nohup 的替代方案)

一、screen 是什么?能干嘛?screen 是一个终端复用器,可以:在一个 SSH 会话中开多个“虚拟终端”SSH 断线后,程序仍然在后台运行随时重新连接到原来的会话特别适合:nohup 的替代方案跑脚本 / 爬虫 / 训练模型运维、远程开发二、安装 screen# CentOS / Rocky / Almayum install -y screen# Debian / Ubuntuapt i...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。