LangChain.jsの動作確認コストを抑えるため、FakeLLMを利用する
LangChainを使用すると、複数のモデル呼び出しやベクトル検索、データ処理などのワークフローを簡単に作ることができます。しかし、LLMのAPIを呼び出すたびにコストが心配になる場合もあります。そこで、FakeLLMがLangChain / LangChain.jsに用意されており、ダミーのLLMとして使用できます。FakeLLMでは言語モデルをLLMまたはChatモデルとして利用できます。また、FakeListLLMを使用することで、ダミーの返答文章を配列で設定し、呼び出すたびに順番に返すことができます。さらに、sleepを使用することでレスポンスの遅延をシミュレートすることも可能です。
目次
LangChainを使うことで、複数のモデル呼び出しやベクトル検索、データ処理といったワークフローを簡単に作ることができます。しかしその一方で、さまざまなフローやデータ処理をテストしたい場合、実行するたびにLLMのAPIを呼び出す関係上コストが心配になることもあります。そんな時に使えるダミーのLLMとして、FakeLLMがLangChain / LangChain.jsに用意されていましたので、こちらを紹介します。
FakeLLMをLLMとして利用する
LangChainでは言語モデルをLLMまたはChatモデルとして利用できます。まずはワンショットの文章生成に利用できるLLMからみてみましょう。
FakeLLMをLLMとして使う場合、FakeListLLM
を利用します。responses
にダミーの返答文章を配列で設定しておくと、呼び出されるたびに配列にセットされた順に返すように動作します。
import { FakeListLLM } from "langchain/llms/fake";
import { Hono } from "hono";
export const fakeApp = new Hono()
fakeApp.get('llm', async c => {
const llm = new FakeListLLM({
responses: ["I'll callback later."],
});
const firstResponse = await llm.call("You want to hear a JavasSript joke?");
return c.json(firstResponse);
})
これを呼び出すと、セットしたテキストが返ってきます。
[
"I'll callback later.",
]
複数回呼び出すと、セットした順に返ってきます。
fakeApp.get('llm', async c => {
const llm = new FakeListLLM({
responses: ["I'll callback later.", "You 'console' them!"],
});
return c.json([
await llm.call("How was your coding?"),
await llm.call("How was your coding?"),
await llm.call("How was your coding?"),
await llm.call("How was your coding?"),
await llm.call("How was your coding?"),
await llm.call("How was your coding?")
])
})
配列の数よりも多い場合、ループする様子でした。
[
"You 'console' them!",
"I'll callback later.",
"You 'console' them!",
"I'll callback later.",
"You 'console' them!",
"I'll callback later."
]
sleep
で遅延をシュミレートする
LLMを利用する際に気になるのは遅延ですが、こちらもシュミレートできます。sleep
でmsを設定すると、その時間llm.call
などのレスポンスが遅延するようになります。
fakeApp.get('llm', async c => {
const llm = new FakeListLLM({
responses: ["I'll callback later.", "You 'console' them!"],
sleep: 10000,
});
const firstResponse = await llm.call("You want to hear a JavasSript joke?");
return c.json(firstResponse)
})
sleep
有り無しでレスポンスを簡単に見たのですが、Time Total
の秒数が変化していることがわかります。
$ curl http://localhost:3000/fake/llm | jq .
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 22 100 22 0 0 3392 0 --:--:-- --:--:-- --:--:-- 22000
"I'll callback later."
$ curl http://localhost:3000/fake/llm | jq .
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 22 100 22 0 0 2 0 0:00:11 0:00:10 0:00:01 4
"I'll callback later."
sleep
と組み合わせたStreamのシュミレーション
sleep
を使っている場合、stream
と組み合わせて少しずつテキストを出す動作を試すこともできました。
fakeApp.get('llm', async c => {
const llm = new FakeListLLM({
responses: ["I'll callback later.", "You 'console' them!"],
sleep: 100,
});
const slowStream = await llm.stream(
"How do you cheer up a JavaScript developer?"
);
return c.streamText(async stream => {
for await (const chunk of slowStream) {
await stream.write(chunk);
}
});
})
curl -sN
でシュミレートできます。
$ curl http://localhost:3000/fake/llm -sN
I
I'
I'l
I'll
...
I'll callback later.
ChatモデルでFakeLLMを試す
Chatモデルの場合、FakeListChatModel
を利用します。こちらもresponses
に配列でテキストを入れればOKです。
fakeApp.get('chat', async c => {
const chat = new FakeListChatModel({
responses: ["I'll callback later.", "You 'console' them!"],
});
const firstMessage = new HumanMessage("You want to hear a JavasSript joke?");
const secondMessage = new HumanMessage(
"How do you cheer up a JavaScript developer?"
);
const firstResponse = await chat.call([firstMessage]);
const secondResponse = await chat.call([secondMessage]);
return c.json([firstResponse, secondResponse])
})
こちらはChatモデルですので、レスポンスがJSONになります。
[
{
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"messages",
"AIMessage"
],
"kwargs": {
"content": "I'll callback later.",
"additional_kwargs": {}
}
},
{
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"messages",
"AIMessage"
],
"kwargs": {
"content": "You 'console' them!",
"additional_kwargs": {}
}
}
]
もちろんChainやOutput Parserを組み合わせることもできます。
fakeApp.get('chat', async c => {
const chat = new FakeListChatModel({
responses: ["I'll callback later.", "You 'console' them!"],
});
const llmChain = new LLMChain({
llm: chat,
prompt: new PromptTemplate({
template: "You want to hear a {lang} joke?",
inputVariables: ["lang"],
outputParser: new StringOutputParser()
})
});
const result = await llmChain.run({lang: 'Ruby'})
return c.json(result);
})
StringOutputParser
を入れると、LLMと同じようにテキストだけ取得できます。
"I'll callback later."
使ってみての感想
まだあまり長いChainを作ったり、Parserを組み合わせたりする流れを試していないのですが、LangChainを使い込むと「LLMの挙動とは関係ない動作の開発」が増えていくことが考えられます。その場合にFakeLLMを利用すると、LLMのモデル呼び出しにかかる料金を抑えることができそうです。
また、テストを書く際のモック・スタブとしてもつかえるかもしれません。