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のモデル呼び出しにかかる料金を抑えることができそうです。

    また、テストを書く際のモック・スタブとしてもつかえるかもしれません。

    参考記事

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark