NestJS – LLMアダプタを抽象化するようにリファクタリング

はじめに

DIを使っているのに、OpenAIとの結びつきが強いので、先に抽象化したコードを検討する

LLM 抽象化アーキテクチャ

1. LLMの抽象インターフェースを定義

libにadaptersを生成する

% npx nest g lib adapters
✔ What prefix would you like to use for the library (default: @app or 'defaultLibraryPrefix' setting value)? 
CREATE libs/adapters/tsconfig.lib.json (222 bytes)
CREATE libs/adapters/src/index.ts (71 bytes)
CREATE libs/adapters/src/adapters.module.ts (202 bytes)
CREATE libs/adapters/src/adapters.service.spec.ts (474 bytes)
CREATE libs/adapters/src/adapters.service.ts (92 bytes)
UPDATE nest-cli.json (1821 bytes)
UPDATE package.json (2382 bytes)
UPDATE tsconfig.json (1073 bytes)

「どの LLM でも共通して持つべき機能」をインターフェースに切り出す。

export interface LlmAdapter {
  generateChat(input: { message: string }): Promise<string>;
}

2. LLMインタフェースを実装

OpenAI用

import { Injectable } from '@nestjs/common';
import OpenAI from 'openai';
import { LlmAdapter } from '@app/adapter/llm.adapter';

@Injectable()
export class OpenAiLlmAdapter implements LlmAdapter {
  private client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

  async generateChat(input: { message: string }): Promise<string> {
    const res = await this.client.chat.completions.create({
      model: 'gpt-4o-mini',
      messages: [{ role: 'user', content: input.message }],
    });
    return res.choices[0].message.content ?? '';
  }
}

Gemini用

import { Injectable } from '@nestjs/common';
import { LlmAdapter } from '@app/adapter/llm.adapter';

@Injectable()
export class GeminiLlmAdapter implements LlmAdapter {
  async generateChat(input: { message: string }): Promise<string> {
    // TODO: Gemini SDK 呼び出しに置き換え
    return `Gemini response to: ${input.message}`;
  }
}

@app/commonという参照は、tsconfig.jsonの以下の設定から可能となっている

{
  "compilerOptions": {

    "paths": {
      "@app/adapter": [
        "libs/adapter/src"
      ],
      "@app/adapter/*": [
        "libs/adapter/src/*"
      ],
      "@app/database": [
        "libs/database/src"
      ],
      "@app/database/*": [
        "libs/database/src/*"
      ]
    }
  }
}

DIによるLLM adapterの切り替え

import { Module } from '@nestjs/common';
import { OpenAiLlmAdapter } from '@app/adapter/openai-llm.adapter';
import { GeminiLlmAdapter } from '@app/adapter/gemini-llm.adapter';

const llmProvider = process.env.LLM_PROVIDER === 'gemini'
  ? { provide: 'LLM_ADAPTER', useClass: GeminiLlmAdapter }
  : { provide: 'LLM_ADAPTER', useClass: OpenAiLlmAdapter };

@Module({
  providers: [llmProvider],
  exports: [llmProvider],
})
export class LlmModule {}

ChatServiceの変更

  • ChatServiceのインスタンス生成時に、DIコンテナから ‘LLM_ADAPTER’ トークンとして登録されたLlmAdapter型のインスタンスがllmプロパティに自動でセットされます。
  • これにより、ChatService内でthis.llmとして利用できます。
import { Inject, Injectable } from '@nestjs/common';
import { LlmAdapter } from '@app/adapters/llm.adapter';

@Injectable()
export class ChatService {
  constructor(
    @Inject('LLM_ADAPTER') private readonly llm: LlmAdapter,
  ) {}

  async getReply(message: string): Promise<string> {
    return this.llm.generateChat({
      message,
    });
  }
}

ChatModuleの変更

LlmModuleをインポートする

import { Module } from '@nestjs/common';
import { ChatService } from './chat.service';
import { ChatController } from './chat.controller';
import { LlmModule } from '@app/adapters/llm.module';

@Module({
  imports: [LlmModule], 
  controllers: [ChatController],
  providers: [ChatService],
})
export class ChatModule {}

動作確認

% pnpm start api
% curl -X POST http://localhost:3000/chat \
  -H "Content-Type: application/json" \
  -d '{"message":"Hello, can you help me find hair loss products?"}'

{"reply":"Of course! There are various hair loss products available, 
ranging from shampoos and conditioners to topical treatments and supplements. ... "}

アーキテクチャ図

関連記事

カテゴリー

アーカイブ

Lang »