KOA技术分享

专注 Koa.js 框架的编程知识分享

Koa.js 与大语言模型集成实践

引言

随着大语言模型(LLM)的快速发展,将 AI 能力集成到 Web 应用已成为趋势。本文将介绍如何在 Koa.js 框架中集成大语言模型,构建智能化的 Web 服务。

架构设计

Koa.js 与 LLM 集成的整体架构:

组件 职责 技术选型
API 网关 请求路由、认证、限流 Koa + koa-router
提示词管理 模板管理、上下文管理 自研/ LangChain
模型适配层 多模型切换、成本优化 OpenAI/Anthropic/本地模型
缓存层 响应缓存、会话管理 Redis

核心代码实现

LLM 服务封装:

// LLM 服务基类
class LLMService {
  constructor(config) {
    this.config = config;
    this.client = null;
    this.cache = new Map();
  }

  // 发送聊天请求
  async chat(messages, options = {}) {
    const cacheKey = this.getCacheKey(messages, options);

    // 检查缓存
    if (options.useCache !== false) {
      const cached = await this.getFromCache(cacheKey);
      if (cached) {
        return cached;
      }
    }

    // 调用模型
    const response = await this.sendRequest(messages, options);

    // 缓存响应
    if (options.useCache !== false) {
      await this.setCache(cacheKey, response, options.cacheTTL);
    }

    return response;
  }

  // 发送请求(子类实现)
  async sendRequest(messages, options) {
    throw new Error('must be implemented by subclass');
  }

  // 获取缓存键
  getCacheKey(messages, options) {
    const content = JSON.stringify({ messages, options });
    return crypto.createHash('md5').update(content).digest('hex');
  }

  // 获取缓存
  async getFromCache(key) {
    const cached = this.cache.get(key);
    if (cached && cached.expire > Date.now()) {
      return cached.data;
    }
    this.cache.delete(key);
    return null;
  }

  // 设置缓存
  async setCache(key, data, ttl = 3600000) {
    this.cache.set(key, {
      data,
      expire: Date.now() + ttl
    });
  }
}

// OpenAI 服务实现
class OpenAIService extends LLMService {
  constructor(config) {
    super(config);
    this.client = new OpenAI({
      apiKey: config.apiKey,
      baseURL: config.baseURL // 支持代理/自定义端点
    });
  }

  async sendRequest(messages, options = {}) {
    const params = {
      model: options.model || this.config.defaultModel,
      messages: messages.map(m => ({
        role: m.role,
        content: m.content
      })),
      temperature: options.temperature || 0.7,
      max_tokens: options.maxTokens || 2048,
      top_p: options.topP || 1,
      frequency_penalty: options.frequencyPenalty || 0,
      presence_penalty: options.presencePenalty || 0
    };

    // 流式响应
    if (options.stream) {
      return this.streamChat(params);
    }

    const response = await this.client.chat.completions.create(params);
    return this.parseResponse(response);
  }

  // 流式响应处理
  async *streamChat(params) {
    const stream = await this.client.chat.completions.create({
      ...params,
      stream: true
    });

    for await (const chunk of stream) {
      const content = chunk.choices[0]?.delta?.content;
      if (content) {
        yield content;
      }
    }
  }

  parseResponse(response) {
    return {
      content: response.choices[0].message.content,
      role: response.choices[0].message.role,
      usage: {
        prompt: response.usage.prompt_tokens,
        completion: response.usage.completion_tokens,
        total: response.usage.total_tokens
      },
      model: response.model,
      id: response.id
    };
  }
}

// 多模型路由
class ModelRouter {
  constructor(config) {
    this.services = new Map();
    this.routingRules = config.routingRules || [];
    this.defaultService = config.defaultService;
    this.initServices(config.providers);
  }

  initServices(providers) {
    for (const [name, provider] of Object.entries(providers)) {
      switch (provider.type) {
        case 'openai':
          this.services.set(name, new OpenAIService(provider.config));
          break;
        case 'anthropic':
          this.services.set(name, new AnthropicService(provider.config));
          break;
        case 'local':
          this.services.set(name, new LocalLLMService(provider.config));
          break;
      }
    }
  }

  // 选择合适的服务
  selectService(context) {
    for (const rule of this.routingRules) {
      if (this.matchRule(rule, context)) {
        return this.services.get(rule.service);
      }
    }
    return this.services.get(this.defaultService);
  }

  matchRule(rule, context) {
    if (rule.condition === 'always') return true;

    if (rule.condition === 'model') {
      return rule.models.includes(context.model);
    }

    if (rule.condition === 'prompt_tokens') {
      return context.promptTokens > rule.threshold;
    }

    return false;
  }

  // 统一聊天接口
  async chat(messages, options = {}) {
    const service = this.selectService({
      model: options.model,
      promptTokens: this.countTokens(messages)
    });

    return await service.chat(messages, options);
  }

  countTokens(messages) {
    // 简单估算
    return messages.reduce((sum, m) =>
      sum + Math.ceil(m.content.length / 4), 0);
  }
}

提示词工程

提示词模板管理:

// 提示词管理器
class PromptManager {
  constructor() {
    this.templates = new Map();
    this.defaultTemplate = null;
  }

  // 注册提示词模板
  register(name, template, description = '') {
    this.templates.set(name, {
      name,
      template,
      description,
      variables: this.extractVariables(template),
      examples: []
    });
  }

  // 提取变量
  extractVariables(template) {
    const matches = template.match(/\{\{(\w+)\}\}/g);
    if (!matches) return [];
    return [...new Set(matches.map(m => m.replace(/\{\{|\}\}/g, '')))];
  }

  // 渲染模板
  render(name, context) {
    const template = this.templates.get(name);
    if (!template) {
      throw new Error(`Template ${name} not found`);
    }

    // 检查必需变量
    const missing = template.variables.filter(v => !(v in context));
    if (missing.length > 0) {
      throw new Error(`Missing variables: ${missing.join(', ')}`);
    }

    let prompt = template.template;

    // 替换变量
    for (const [key, value] of Object.entries(context)) {
      prompt = prompt.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
    }

    return prompt;
  }

  // 构建对话消息
  buildMessages(name, context, history = []) {
    const systemPrompt = this.render(name, context);

    const messages = [
      { role: 'system', content: systemPrompt }
    ];

    // 添加历史对话
    messages.push(...history.map(h => ({
      role: h.role,
      content: h.content
    })));

    // 添加当前用户输入
    messages.push({
      role: 'user',
      content: context.userInput || context.input || ''
    });

    return messages;
  }

  // 添加示例
  addExample(name, input, output) {
    const template = this.templates.get(name);
    if (template) {
      template.examples.push({ input, output });
    }
  }

  // 生成带示例的提示词
  renderWithExamples(name, context) {
    const template = this.templates.get(name);
    let prompt = this.render(name, context);

    if (template.examples.length > 0) {
      prompt += '\n\n请参考以下示例:\n\n';
      for (const example of template.examples) {
        prompt += `用户输入:${example.input}\n`;
        prompt += `助手回复:${example.output}\n\n`;
      }
    }

    return prompt;
  }
}

// 对话上下文管理
class ConversationContext {
  constructor(config) {
    this.maxHistory = config.maxHistory || 10;
    this.maxTokens = config.maxTokens || 4000;
    this.conversations = new Map();
  }

  // 创建会话
  createSession(sessionId, systemPrompt = '') {
    this.conversations.set(sessionId, {
      systemPrompt,
      messages: [],
      metadata: {
        createdAt: Date.now(),
        updatedAt: Date.now()
      }
    });
    return sessionId;
  }

  // 添加消息
  addMessage(sessionId, role, content) {
    const conversation = this.conversations.get(sessionId);
    if (!conversation) {
      throw new Error(`Session ${sessionId} not found`);
    }

    conversation.messages.push({
      role,
      content,
      timestamp: Date.now()
    });

    // 修剪过长的历史
    this.trimHistory(sessionId);

    conversation.metadata.updatedAt = Date.now();
  }

  // 修剪历史
  trimHistory(sessionId) {
    const conversation = this.conversations.get(sessionId);

    // 限制消息数量
    while (conversation.messages.length > this.maxHistory) {
      conversation.messages.shift();
    }

    // 限制 token 数量
    while (this.countTokens(conversation.messages) > this.maxTokens) {
      conversation.messages.shift();
    }
  }

  // 计算 token 数
  countTokens(messages) {
    return messages.reduce((sum, m) =>
      sum + Math.ceil(m.content.length / 4), 0);
  }

  // 获取对话历史
  getMessages(sessionId, includeSystem = true) {
    const conversation = this.conversations.get(sessionId);
    if (!conversation) return [];

    const messages = [];
    if (includeSystem && conversation.systemPrompt) {
      messages.push({
        role: 'system',
        content: conversation.systemPrompt
      });
    }

    return messages.concat(conversation.messages);
  }

  // 会话摘要
  async summarize(sessionId, llmService) {
    const messages = this.getMessages(sessionId, false);

    const summaryPrompt = `请简洁总结以下对话的要点:\n\n${
      messages.map(m => `${m.role}: ${m.content}`).join('\n')
    }`;

    const result = await llmService.chat([
      { role: 'user', content: summaryPrompt }
    ]);

    return result.content;
  }
}

最佳实践建议

总结

Koa.js 与大语言模型集成为应用带来智能化的同时,也需要关注:

通过本文介绍的技术方案,可以在 Koa.js 中快速构建 AI 驱动的应用服务。

← 下一篇:Koa.js 响应式编程与高性能并发处理