はじめに
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. ... "}
アーキテクチャ図
