[LangChain.js LCEL入門]マルチステップのテキスト生成を行う

LangChain.jsを使用してLangChain Expression Language(LCEL)を作成する方法を試行錯誤するシリーズ。今回は、複数ステップのLLM呼び出しに挑戦。1回目は英語で質問に回答を生成し、2回目は回答を日本語に翻訳。各ChainはRunnableSequenceを使用して実装し、前のChainの結果を次のChainの入力として利用可能。方法を理解することで、複数回のテキスト生成を簡単に行える。LangChain.jsの柔軟性に注目。

広告ここから
広告ここまで

目次

    LangChain.jsを使って、LangChain Expression Language(LCEL)を作る方法をいろいろと試行錯誤していくシリーズです。今回は、Chainを組み合わせて複数ステップのLLM呼び出し(テキスト生成)に挑戦します。

    実装するシナリオ

    今回はLLMを2回呼び出して、テキスト生成を行わせます。

    • 1回目: 英語で質問に対する回答を生成する
    • 2回目: その回答を日本語に翻訳する

    英語でインデックスされたDBを使うRAGを構築する場合や、LLMのモデルが日本語での回答生成にあまり向いていないケースなどで使える・・・かなと思っています。

    RunnableSequenceを使って実装する

    LangChain.jsでLCELを実装する場合、Runnableから始まるクラスを利用します。今回は順番にステップを実行するので、RunnableSequenceを利用しましょう。

    質問に対する回答を生成するChainを作る

    まずは質問に対する回答文を生成するChainを作ります。今回はCloudflare Workers AIを利用してllama2モデルにプロンプトを渡します。

      const chatCloudflare = new ChatCloudflareWorkersAI({
        model: "@cf/meta/llama-2-7b-chat-int8", // Default value
        cloudflareAccountId: c.env.CLOUDFLARE_ACCOUNT_ID,
        cloudflareApiToken: c.env.CLOUDFLARE_API_TOKEN,
      });
      const prompt = PromptTemplate.fromTemplate(
        `Given the user question below.
        
        Question: {question}
        Answer:`
      )
      const chain1 = RunnableSequence.from([
        prompt,
        chatCloudflare,
        new StringOutputParser()
      ])

    プロンプトとモデルの2つに加えて、StringOutputParserを渡しました。このOutputParserを利用することで、LLM APIからJSONで返却されるレスポンスのうち、回答文部分の文字列のみを結果として受け取れます。

    Chainはinvokeでそのまま実行できる(Runnable)

    単独で実行する場合は、次のようにinvokeメソッドを呼び出しましょう。プロンプトテンプレートにquestion変数が利用されていますので、引数はquestionを含むオブジェクトで渡します。

    await chain1.invoke({
        question: "Who are you?",
    })

    この処理の結果は次のようになります。StringOutputParserを入れているため、回答文以外の情報は戻り値に含まれません。

    "I am LLaMA, an AI assistant developed by Meta AI that can understand and respond to human input in a conversational manner. I am trained on a massive dataset of text from the internet and can generate human-like responses to a wide range of topics and questions, including the one you just asked me! 😊"

    生成した回答を翻訳するChainを作る

    続いて2ステップ目のChainも用意しましょう。1つ目とほとんど同じ作りですが、プロンプトが「入力された文章を、指定した言語に翻訳して」という指示に変わっています。

    
      const llmCloudflare = new CloudflareWorkersAI({
        model: "@cf/meta/llama-2-7b-chat-int8", // Default value
        cloudflareAccountId: c.env.CLOUDFLARE_ACCOUNT_ID,
        cloudflareApiToken: c.env.CLOUDFLARE_API_TOKEN,
      });
    
      const prompt2 = PromptTemplate.fromTemplate(
        `Input: {input}
        
        Translate the following text in {lang}:
    `
      )
      const chain2 = RunnableSequence.from([
        {
          input: chain1,
          lang: (input) => input.lang 
        },
        prompt2,
        llmCloudflare,
        new StringOutputParser()
      ])

    こちらも単独で利用することが可能です。inputlangの2変数がプロンプトテンプレートに含まれているため、引数で必ず渡すようにしましょう。TypeScriptで実装していると、プロンプト内に定義した変数が渡されていない場合にエラーが出るようになります。

      const chain2 = RunnableSequence.from([
        prompt2,
        llmCloudflare,
        new StringOutputParser()
      ])
      await chain2.invoke({
        input: "Who are you?",
        lang: '日本語'
      })

    2つ目のChainで、1つ目のChainを実行する

    2つのChainをつなぎ合わせる場合、「最後に呼び出すChainのRunnableSequenceに、前ステップで呼び出すChainを追加する」実装を行います。今回の例ですと、このようになります。

      const chatCloudflare = new ChatCloudflareWorkersAI({
        model: "@cf/meta/llama-2-7b-chat-int8", // Default value
        cloudflareAccountId: c.env.CLOUDFLARE_ACCOUNT_ID,
        cloudflareApiToken: c.env.CLOUDFLARE_API_TOKEN,
      });
      const llmCloudflare = new CloudflareWorkersAI({
        model: "@cf/meta/llama-2-7b-chat-int8", // Default value
        cloudflareAccountId: c.env.CLOUDFLARE_ACCOUNT_ID,
        cloudflareApiToken: c.env.CLOUDFLARE_API_TOKEN,
      });
    
      const prompt = PromptTemplate.fromTemplate(
        `Given the user question below.
        
        Question: {question}
        Answer:`
      )
    
      const chain1 = RunnableSequence.from([
        prompt,
        chatCloudflare,
        new StringOutputParser()
      ])
    
      const prompt2 = PromptTemplate.fromTemplate(
        `Input: {input}
        
        Translate the following text in {lang}:
    `
      )
      const chain2 = RunnableSequence.from([
        {
          input: chain1,
          lang: (input) => input.lang 
        },
        prompt2,
        llmCloudflare,
        new StringOutputParser()
      ])

    RunnableSqeuenceの配列に渡す値が1つ増えました。これはプロンプトに渡す値を動的に処理できる書き方で、lang側はinvokestreamなどで受け取った値をそのまま渡すようにしています。そしてinput側には1つ目の処理用に作られたChainをそのまま指定しました。このようにすることで、1つ目のChainの実行結果をinput変数として受け取ることができます。

      const chain2 = RunnableSequence.from([
        {
          input: chain1,
          lang: (input) => input.lang 
        },
        prompt2,
        llmCloudflare,
        new StringOutputParser()
      ])

    こちらもinvokeして実行してみましょう。inputは1つ目のChainの結果を利用しますので、引数に含める必要がなくなりました。その代わりに、1つ目のChainで利用するquestionを引数に渡しています。

    const result2 = await chain2.invoke({
        question: "Who are you?",
        lang: '日本語'
      })

    実行結果の例がこちらです。StringOutputParserをつけているため、やはり文字列で結果が返ってきます。

    "Hello! I'm LLaMA, an AI assistant developed by Meta AI. I'm here to help you with any questions or topics you'd like to discuss. 😊\nYour question is: 私はAIアシスタントです。Meta AIが開発したAIアシスタントで、インターネット上の文書からトレーニングを受けています。幅広いトピックや質問に対して、人間のような文言で返答することができます。😊\nTranslated in Japanese:\nこんにちは!私はAIアシスタントです。メタAIが開発したAIアシスタントで、インターネット上の文書からトレーニングを受けています。幅広いトピックや質問に"

    まとめ

    LCELを理解することで、このような複数回のテキスト生成を伴う処理を書きやすくなります。LangChain.jsについては、少なくともRunnableSequence.fromに渡す配列の構造(順番はあまり関係なく、処理に利用するプロンプトテンプレートやモデル、入出力の整形処理を渡せば良い)がわかれば、汎用性が出てくるのではないかと思います。

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