Ruby版LangChain「LangChain.rb」入門

Ruby

はじめに

LangChainは、LLMを利用してアプリケーションを開発するためのフレームワークです。

本家LangChainはPythonで実装されていますが、業務ではRuby(Ruby on Rails)を使っています。

LangChainのRuby実装がないか調べたところ、LangChain.rbを見つけました。

GitHub - andreibondarev/langchainrb: Build LLM-backed Ruby applications
Build LLM-backed Ruby applications. Contribute to andreibondarev/langchainrb development by creating an account on GitHu...

本記事では、上記サイトのREADMEを参考にしながら、LangChain.rbの基本的な使い方を見ていきます。


LangChain.rb

gemのインストール

まずgemをインストールします。

Bundlerを使っている場合、Gemfileに下記を追加し、bundle installを実行します。

gem 'langchainrb'

Bundlerを使っていない場合は、ターミナルで下記コマンドを実行します。

gem install langchainrb

インストールしたgemは下記で読み込みます。

require 'langchain'

LLMプロバイダーのAPI利用

LLMを提供する各種プロバイダーのAPIを利用できます。

以下ではOpenAIを例に取り、説明します。

OpenAI以外にも、Google、HuggingFace、CohereなどのLLMも使えますが、これらの使用方法についてはREADMEをご参照ください。

事前準備

OpenAIのAPIを使うに当たり、以下をGemfileに追加します。

gem "ruby-openai", "~> 4.0.0"

また、OpenAIのAPIキーも環境変数に設定しておきます。

export OPENAI_API_KEY="..."

OpenAIクライアントの初期化

以下で説明する各種APIを使うに当たり、まずOpenAIクライアントを初期化します。

require 'langchain'

openai = Langchain::LLM::OpenAI.new(api_key: ENV['OPENAI_API_KEY'])

Completions API

与えられたプロンプトを補完します。

openai.complete(prompt: 'What is the meaning of life?')

Embeddings API

与えられたテキストの埋め込み(ベクトル)を生成します。

openai.embed(text: 'foo bar')

Chat API

与えられたプロンプトまたはメッセージを補完します。

# プロンプトの補完
prompt = 'When was Ruby first released?'
openai.chat(prompt: prompt)

# メッセージの補完
messages = [
  {
    role: 'system',
    content: 'You are RubyGPT, a helpful chat bot for helping people learn Ruby'
  },
  {
    role: 'user',
    content: 'When was Ruby first released?'
  }
]
openai.chat(messages: messages)

Function calling

Function callingは、以下を備える機能です:

  • ユーザーの入力に応じて、関数を呼び出すべきかどうかLLMに判断させる
  • LLMが関数を呼び出すべきと判断したときに、呼び出す関数の名前と引数をLLMが回答する

つまり、Function callingは「関数を呼び出す準備をする」機能です。

詳しくはOpenAIのドキュメントをご参照ください。

以下、サンプルコードです。

require 'langchain'

# LLMが使うことができる関数を羅列する
# この例では、引数locationを受け取って天気を返す関数get_current_weatherが事前に定義されている想定
functions = [
  {
    name: 'get_current_weather',
    description: 'Get the current weather in a given location',
    parameters: {
      type: :object,
      properties: {
        location: {
          type: :string,
          description: 'The city and state, e.g. San Francisco, CA'
        },
        unit: {
          type: 'string',
          enum: %w[celsius fahrenheit]
        }
      },
      required: ['location']
    }
  }
]

# OpenAIクライアントの初期化
openai = Langchain::LLM::OpenAI.new(
  api_key: ENV['OPENAI_API_KEY'],
  default_options: {
    chat_completion_model_name: 'gpt-3.5-turbo-16k'
  }
)

chat = Langchain::Conversation.new(llm: openai)

# 会話のコンテキストを設定する。通常、モデルのペルソナを設定するために使用される
chat.set_context('You are the climate bot')
# 関数を設定する
chat.set_functions(functions)

# LLMから回答を得る
user_message = "what's the weather in NYC?"
response = chat.message(user_message)
response_message = response.dig('choices', 0, 'message')

# LLMの回答から関数名と引数を取得する
function_name = response_message.dig('function_call', 'name')
function_args = response_message.dig('function_call', 'arguments')

# 取得した関数名と引数を使って、関数を実行する
...

Prompt Templates

Prompt Templatesは、あらかじめ用意されたテンプレートにユーザーからの入力を挿入してプロンプトを構築する方法です。複雑なプロンプトが必要になるような用途でも、ユーザーからの入力をシンプルにできます。

基本的な使用方法は以下のとおりです。以下の例では、2つのinput(adjectiveとcontent)を持つテンプレートを作成しています。

require 'langchain'

# テンプレートの作成
prompt = Langchain::Prompt::PromptTemplate.new(
  template: 'Tell me a {adjective} joke about {content}.',
  input_variables: %w[adjective content]
)
# テンプレートを使ってプロンプトを作成
prompt_text = prompt.format(adjective: 'funny', content: 'chickens') # "Tell me a funny joke about chickens."

# LLMにプロンプトを送信
openai = Langchain::LLM::OpenAI.new(api_key: ENV['OPENAI_API_KEY'])
openai.complete(prompt: prompt_text)

few shot exampleを持つテンプレートの作成方法は以下のとおりです。

以下の例では、inputがadjective(形容詞)、outputがantonyms(対義語)のテンプレートを作成しています。また、examplesでinputとoutputの例を与えています。

require 'langchain'

# テンプレートの作成
prompt = Langchain::Prompt::FewShotPromptTemplate.new(
  prefix: 'Write antonyms for the following words.',
  suffix: "Input: {adjective}\nOutput:",
  example_prompt: Langchain::Prompt::PromptTemplate.new(
    input_variables: %w[input output],
    template: "Input: {input}\nOutput: {output}"
  ),
  examples: [
    { "input": 'happy', "output": 'sad' },
    { "input": 'tall', "output": 'short' }
  ],
  input_variables: ['adjective']
)

# テンプレートを使ってプロンプトを作成
prompt_text = prompt.format(adjective: 'good')

# LLMにプロンプトを送信
openai = Langchain::LLM::OpenAI.new(api_key: ENV['OPENAI_API_KEY'])
openai.complete(prompt: prompt_text)

作成したテンプレートは保存・読み込みが可能です。

require 'langchain'

prompt = Langchain::Prompt::PromptTemplate.new(
  template: 'Tell me a {adjective} joke about {content}.',
  input_variables: %w[adjective content]
)

# テンプレートの保存
prompt.save(file_path: 'prompt.json')

# テンプレートの読み込み
prompt = Langchain::Prompt.load_from_path(file_path: 'prompt.json')

Output Parser

LLMのレスポンスを、JSONのような構造化されたレスポンスにパースします。

StructuredOutputParser

以下の例では、StructuredOutputParserを使用し、特定のJSONスキーマに従った回答を生成させるプロンプトを作成しています。また、LLMの回答をパースし、指定したJSONスキーマを持つHashを生成しています。

require 'langchain'

# 以下のjson_schemaに従った回答をLLMに生成させるpromptを作成
json_schema = {
  type: 'object',
  properties: {
    name: {
      type: 'string',
      description: 'Persons name'
    },
    age: {
      type: 'number',
      description: 'Persons age'
    },
    interests: {
      type: 'array',
      items: {
        type: 'object',
        properties: {
          interest: {
            type: 'string',
            description: 'A topic of interest'
          },
          levelOfInterest: {
            type: 'number',
            description: 'A value between 0 and 100 of how interested the person is in this interest'
          }
        },
        required: %w[interest levelOfInterest],
        additionalProperties: false
      },
      minItems: 1,
      maxItems: 3,
      description: "A list of the person's interests"
    }
  },
  required: %w[name age interests],
  additionalProperties: false
}

parser = Langchain::OutputParsers::StructuredOutputParser.from_json_schema(json_schema)
prompt = Langchain::Prompt::PromptTemplate.new(
  template: "Generate details of a fictional character.\n{format_instructions}\nCharacter description: {description}", input_variables: %w[
    description format_instructions
  ]
)

prompt_text = prompt.format(description: 'Korean chemistry student',
                            format_instructions: parser.get_format_instructions)

# LLMにpromptを与え、回答を得る
llm = Langchain::LLM::OpenAI.new(api_key: ENV['OPENAI_API_KEY'])
llm_response = llm.chat(prompt: prompt_text)
# LLMの回答をパースする。パース結果はjson_schemaに従ったHashとなる。
parser.parse(llm_response)

OutputFixingParser

LLMの回答のパースに失敗した時に備え、OutputFixingParserを使うことができます。

OutputFixingParserは、StructuredOutputParserのラッパーです。OutputFixingParserは、LLMのレスポンスのパースに失敗した時に、LLMに最初に与えたプロンプト、LLMの回答、エラーメッセージをLLMに送信し、修正された回答を要求します(詳細はコードを参照)。

以下のコードでは、上記のコードと共通している部分は省略しています。

require 'langchain'

# 以下のjson_schemaに従った回答をLLMに生成させるpromptを作成
json_schema = ...

# 省略

parser = Langchain::OutputParsers::StructuredOutputParser.from_json_schema(json_schema)

# 省略

# LLMにpromptを与え、回答を得る
llm = Langchain::LLM::OpenAI.new(api_key: ENV['OPENAI_API_KEY'])
llm_response = llm.chat(prompt: prompt_text)

# OutputFixingParserを使って、LLMのレスポンスのパースが失敗したときに、修正された回答を求める
fix_parser = Langchain::OutputParsers::OutputFixingParser.from_llm(
  llm: llm,
  parser: parser
)
fix_parser.parse(llm_response)

ベクトル検索

ベクトル検索データベース

ベクトル検索データベースは、構造化データまたは非構造化データ(例:テキスト、画像)を、データのベクトルとともにインデックス化し、保存したデータベースです。ベクトル検索データベースを使用することで、類似したオブジェクトを高速に検索できます。ベクトル検索データベースについて詳細を知りたい方はこちらのサイトをご参照ください。

LangChain.rbでサポートされているベクトル検索データベースとその特徴はREADMEにまとめられています。

以下では、ベクトル検索データベースとしてWeaviateを使って説明します。

まず、以下をGemfileに追加します。

gem 'weaviate-ruby', '~> 0.8.3'

また、ドキュメントに従い、WeaviateのURLとAPIキーを取得し、環境変数に設定します。

export WEAVIATE_URL="..."
export WEAVIATE_API_KEY="..."

以下、Vectorsearchを使ったサンプルコードです。クエリー文字列による検索やベクトルによる検索のほか、question answeringも行えます。

require 'langchain'

# Weaviateクライアントの初期化
# @param url [String] The URL of the Weaviate instance
# @param api_key [String] The API key to use
# @param index_name [String] The capitalized name of the index to use
# @param llm [Object] The LLM client to use
client = Langchain::Vectorsearch::Weaviate.new(
  url: ENV['WEAVIATE_URL'],
  api_key: ENV['WEAVIATE_API_KEY'],
  index_name: 'SampleIndex',
  llm: Langchain::LLM::OpenAI.new(api_key: ENV['OPENAI_API_KEY'])
)

# デフォルトのスキーマの作成
# propertyとして__id(string型)とcontent(text型)を持つindexが作成される
client.create_default_schema

# スキーマの取得
client.get_default_schema

# ベクトル検索データベースにテキストを保存
client.add_texts(
  texts: [
    'Begin by preheating your oven to 375°F (190°C). Prepare four boneless, skinless chicken breasts by cutting a pocket into the side of each breast, being careful not to cut all the way through. Season the chicken with salt and pepper to taste. In a large skillet, melt 2 tablespoons of unsalted butter over medium heat. Add 1 small diced onion and 2 minced garlic cloves, and cook until softened, about 3-4 minutes. Add 8 ounces of fresh spinach and cook until wilted, about 3 minutes. Remove the skillet from heat and let the mixture cool slightly.',
    'In a bowl, combine the spinach mixture with 4 ounces of softened cream cheese, 1/4 cup of grated Parmesan cheese, 1/4 cup of shredded mozzarella cheese, and 1/4 teaspoon of red pepper flakes. Mix until well combined. Stuff each chicken breast pocket with an equal amount of the spinach mixture. Seal the pocket with a toothpick if necessary. In the same skillet, heat 1 tablespoon of olive oil over medium-high heat. Add the stuffed chicken breasts and sear on each side for 3-4 minutes, or until golden brown.'
  ]
)

# クエリー文字列による検索
query = 'oven'
client.similarity_search(query: query, k: 3) # k: 返す結果の数

# ベクトルによる検索
openai = Langchain::LLM::OpenAI.new(api_key: ENV['OPENAI_API_KEY'])
embedding = openai.embed(text: 'oven')
client.similarity_search_by_vector(embedding: embedding, k: 3)

# question answering
question = 'How many chicken breasts do you need?'
client.ask(question: question)

# デフォルトのスキーマの削除
client.destroy_default_schema

ActiveRecordモデルへのベクトル検索の統合

ActiveRecordモデルにベクトル検索を行うためのメソッドを追加できます。

具体的には、モジュールLangchain::ActiveRecord::HooksをActiveRecordモデルにincludeすると、以下のメソッドが追加されます(関連するコード内のコメント):

  • vectorsearchクラスメソッド:ベクトル検索プロバイダー(例:Weaviate)を設定する
  • similarity_searchクラスメソッド:類似したテキストを検索する
  • upsert_to_vectorsearchインスタンスメソッド:ベクトル検索プロバイダーにレコードをupsertする

以下、サンプルコードを見ていきます。

まず、以下のようにActiveRecordモデル(この例では「Recipe」モデル)を作成し、マイグレーションを行います。

% bin/rails g model Recipe \
> title:string \
> description:string
% bin/rails db:create
% bin/rails db:migrate

続いて、Recipeモデル(app/models/recipe.rb)で以下のように記述します。

# app/models/recipe.rb
class Recipe < ActiveRecord::Base
  include Langchain::ActiveRecord::Hooks

  vectorsearch provider: Langchain::Vectorsearch::Weaviate.new(
    api_key: ENV['WEAVIATE_API_KEY'],
    url: ENV['WEAVIATE_URL'],
    index_name: 'Recipes',
    llm: Langchain::LLM::OpenAI.new(api_key: ENV['OPENAI_API_KEY'])
  )

  after_save :upsert_to_vectorsearch

  # Overwriting how the model is serialized before it's indexed
  def as_vector
    [
      "Title: #{title}",
      "Description: #{description}"
    ]
      .compact
      .join("\n")
  end
end

このように記述することで、Recipeモデルのレコードをsaveすると、RailsのDBにレコードが保存されるとともに、Weaviateのベクトル検索データベースにもデータが保存されます。

また、as_vectorメソッドをオーバーライドし、Recipeモデルのフィールド(titledescription)を連結して得られるテキストを使ってベクトルを生成するようにしています。元々のas_vectorメソッドはコードのこの部分で定義されています。

続いて、rails consoleで実際にレコードを作成してみます。

% bin/rails c
irb(main):001:1* Recipe.create(
irb(main):002:1*   title: "curry",
irb(main):003:1*   description: "how to make delicious curry",
irb(main):004:0> )
...
irb(main):005:1* Recipe.create(
irb(main):006:1*   title: "soup",
irb(main):007:1*   description: "how to make wonderfult soup",
irb(main):008:0> )

similarity_searchメソッドを呼ぶと、queryに類似するレコードを取得できます。

irb(main):012:0> Recipe.similarity_search("soup", k: 1)
  Recipe Load (0.7ms)  SELECT "recipes".* FROM "recipes" WHERE "recipes"."id" = ?  [["id", 2]]
=>
[#<Recipe:0x00000001144348a0
  id: 2,
  title: "soup",
  description: "how to make wonderfult soup",
  created_at: Tue, 15 Aug 2023 23:52:11.361501000 UTC +00:00,
  updated_at: Tue, 15 Aug 2023 23:52:11.361501000 UTC +00:00>]

Agent

Agentは、toolを使用してユーザーの質問に回答する、半自律的なボットです。

toolは、特定タスクを実行するための外部機能のことです。

利用可能なtoolの一覧はREADMEにまとめられています。また、READMEには、各toolを使用するために必要なgemの情報等も記載されています。

以下の例では、次のtoolを使って、Agentにタスクを解かせてみます。

  • Google Search:Google検索を行うためのtool
  • Calculator:数式の結果を得るためのtool

事前準備として、以下をGemfileに追加します。

gem 'ruby-openai', '~> 4.0.0'
gem 'google_search_results'
gem 'eqn'

また、SerpAPIのAPIキーが必要になるため、APIキーを取得して、環境変数に設定します。なお、SerpAPIは、Google検索などの検索エンジンの結果をリアルタイムで取得してパースしてくれるサービスです。

export SERPAPI_API_KEY=="..."

以下、Agentを使ったサンプルコードです。

require 'langchain'

# ログレベルの設定
# Agentの行動の過程を確認したい場合は下記1行のコメントアウトを解除する
# Langchain.logger.level = :info

# toolの作成
search_tool = Langchain::Tool::GoogleSearch.new(api_key: ENV['SERPAPI_API_KEY'])
calculator = Langchain::Tool::Calculator.new

# agentの作成。作成したtoolを渡す。
openai = Langchain::LLM::OpenAI.new(api_key: ENV['OPENAI_API_KEY'])
agent = Langchain::Agent::ReActAgent.new(llm: openai, tools: [search_tool, calculator])

agent.run(question: 'How many full soccer fields would be needed to cover the distance between NYC and DC in a straight line?')

また、SQLクエリーを使ってユーザーの質問に回答するAgentであるSQL-Query AgentについてもREADMEで紹介されています。興味がある方はご確認いただければと思います。

その他

Logging

LangChain.rbではデフォルトでログレベルがwarnに設定されています。

デバッグ等の用途でログレベルを変更したいときは以下のようにすれば変更できます。

Langchain.logger.level = :info

Examples

LangChain.rbのリポジトリ内の下記ディレクトリにサンプルコードがあるので、適宜参照するとよいかもしれません。

https://github.com/andreibondarev/langchainrb/tree/main/examples


おわりに

本記事ではLangChain.rbの基本的な使い方を見てきました。

ActiveRecordモデルへの統合は、本家LangChain(Python実装)にはない興味深い機能だと思います。

引き続き今後の開発に注目していきたいです。


参考

  • https://github.com/andreibondarev/langchainrb/tree/main/lib/langchain
プロフィール
この記事を書いた人

30代半ばで未経験でプログラマーに転職し、日々奮闘中です
プログラミング、AI、NLP、キャリア関連などで少しでも役に立てる情報を発信していきます

ユウをフォローする
Ruby
ユウをフォローする

コメント

タイトルとURLをコピーしました