LoginSignup
5
3

More than 1 year has passed since last update.

Rails+AI: ChatGPTとllm_memoryで構築するFAQチャットシステム

Last updated at Posted at 2023-05-13

Railsで動くWebサービスとChatGPTを連携したいと思ったことがありますよね。今回は、サービスのFAQページの内容に応答するチャットボットを作成し、電話の問い合わせを減らす施策を考えてみましょう。

RubyにはLangChainやLlamaIndexのようなライブラリがありません。なので、llm_memoryというGemを作りました。LlamaIndexを大いに参考にしています。ただし、LlamaIndexは大きなバンドルであり、科学的な要素も含まれています。簡単に利用できる一方で、詳細に調べようとすると難易度が上がります。さらに、Python製なのでRailsとの統合が難しいです。

llm_memoryについて

llm_memoryは、LlmaIndexと同じくin-context learningを利用しています。具体的には、データをチャンクに分割し、vector databaseにembeddingしたものを突っ込みます。ユーザの質問もベクター化し、類似度の高いコンテントを取得。その結果をプロンプトに入れてchatgptに問い合わせます。これにより、gpt3.5でも4Kのトークンリミットに収まり、情報をもとにユーザの質問に返答できるようになります。(今後、トークンリミットも大きくなっていく思いますが、その分コストがかかると思います)

llm_memoryのコンポーネントは以下の3つから成り立っています。

  • LlmMemory::Wernicke: 外部データをロードする役割を持ちます。現在はファイルからのロードのみ対応していますが、今後はさまざまなローダータイプを追加する予定です。
  • LlmMemory::Hippocampus: ベクターデータベースとのインタラクションを管理し、クエリに基づいて関連情報を取得します。現在は、Redisearchモジュールを使用したRedisをベクターデータベースとして使用しています。
  • LlmMemory::Broca: Hippocampusコンポーネントから提供されるメモリを使用してクエリに応答します。プロンプトテンプレートにはERBを使用し、オンライン(例えば、LangChain Hub)でさまざまなテンプレートを見つけることができます。

コンポーネント名は、脳の言語・記憶に関する用語に由来しています。ウェルニッケ野は他人の言語を理解する働き、海馬は記憶、ブローカ野は運動性言語中枢で言語の産出に関わる

FAQチャットのバックエンドの作成

それでは、FAQチャットのバックエンドを作成してみましょう。
(この記事ではChatUIは語りません!)

1. インフラサービスの準備

まずは、openaiのキーとRediscloudのURLを取得します。RedisearchというRedisのモジュールを利用してvector dbとして使います。これは、rediscloudのプロプラエタリなものなので標準のRedisには入っていません。RedisCloudにサインアップして取得してください。RedisCouldにはフリープランがありますので、Herokuではaddonとして提供されているので、すぐに導入できます。

2. モジュールのインストール

llm_memoryをGemに追加し、bundle installを行います。

gem "llm_memory"

3. OPEN AIのキーとRedisのURIを指定

次に、環境変数(OPENAI_ACCESS_TOKEN, REDISCLOUD_URL)、またはinitializerで下記のようにセットします。

LlmMemory.configure do |c|
  c.openai_access_token = "xxxxx"
  c.redis_url = "redis://xxxx:6379"
end

4. データの取得

ここまででLLM Memoryを使う準備ができました。データを作成しましょう。Wernickeコンポーネントを利用せずに、ActiveRecordから直接データを作ります。FAQは、CMS(comfortable_mexican_sofa gem)を利用しています。該当データを取得します。今回、このデータは、WSYWIGのHTMLなので、textを取得する必要がありました。

以下にそのためのコードを示します。

def extract_text_from_html(html)
  utf8_html = html.force_encoding("UTF-8")
  document = Nokogiri::HTML(utf8_html)
  document.css("style").remove
  document.css("br").each { |br| br.replace("\n") }
  document.css("a").each do |a|
    link = a["href"]
    text = a.text
    a.replace("#{text} (#{link})") # Replace the <a> tag with its text content and link
  end
  document.text.gsub(/ /, " ").gsub(/\s+/, " ").gsub(/(\r\n)+|\n+/, "\n").strip
end

docs = []
Comfy::Cms::Page.where(is_published: true, parent_id: 408).each do |page|            
  trans = page.translations.find_by(locale: "ja")
  next if trans.blank?
  raw = trans&.content_cache
  next if raw.blank?
  label = trans&.label
  content = extract_text_from_html(raw) 
  docs.push({
    content: content,
    metadata: {
      path: "https://xxx.com#{page.full_path}",
      label: label || page.label        
    }
  })
end

ポイントとして、docsの出力が下記のような:content:metadata(シンボル!)を含むハッシュの配列になっていることです。

[{
 content: "...",
 metadata: {}
},,,,] 

5. 記憶

次に、データをVector DB(Redis)に格納します。日本語の場合、1tokenあたり、0.7~0.9文字 (参考:https://zenn.dev/microsoft/articles/dcf32f3516f013 )なので、max_chunkを512程度にしてRedisに記憶しています。

以下のコードでデータをRedisに格納します。

hippocampus = LlmMemory::Hippocampus.new(
  chunk_size: 512, 
  index_name: "faq"
)
hippocampus.memorize(docs)

そして、質問を入れて結果を確認します。

query_str = "宿題の提出方法を教えてください"
related_docs =  hippocampus.query(query_str, limit: 5)

512文字のmax_tokenを指定したので、大体400トークンぐらいが一つのcontentになります。

6. プロンプトを用意

次に、プロンプトを用意します。ERBで書きます。

prompt = <<-TEMPLATE
Context information is below.
---------------------
<% related_docs.each do |doc| %>
<%= doc[:content] %>
URL: <%= doc[:metadata]["path"] %>

<% end %>
---------------------
Given the context information and not prior knowledge,
answer the question: <%= query_str %>
output format:
<answer>
URL: <referred URL>
TEMPLATE

7. 出力

最後に、respondの引数がERBに展開されるようにします。

broca = LlmMemory::Broca.new(
  prompt: prompt, 
  model: "gpt-3.5-turbo", 
  temperature: 0, 
  max_token: 4096
)
message = broca.respond(query_str: query_str, related_docs: related_docs)
puts message
A. 宿題の提出方法は、生徒アカウントのトップナビゲーションバーから「ホームワーク」を選択して、ご希望のコース名をクリックすると表示されます。レッスン後に提出することをお勧めします。

いい感じですね!

次の質問に行く時は、下記のようにqueryを問い合わせて、出力する形になります。

query_str = "連絡先を教えてください"
related_docs = hippocampus.query(query_str, limit:  5)
message = broca.respond(query_str: query_str, related_docs: related_docs)

まとめ

元のデータの質と量にもよりますが、max_chunkとlimitの調整をすれば意外とうまく答えてくれるように思います。Promptエンジニアリングも少しは必要ですね。

以上で、ChatGPTとllm_memoryを用いたRails FAQチャットシステムの作り方についての説明を終わります。あとはchatUIをhowire/turbo streamなどで作れば、カスタマーサポートのコストも下がって効率化が期待できます!

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3