Ruby 版 LangChain (?) の Boxcars を触ってみた


ChatGPT API などを利用する際に色々面倒な処理を賄ってくれるツールとして [LangChain](https://github.com/hwchase17/langchain) や [LangChain.js](https://github.com/hwchase17/langchainjs) がありますが、 Ruby でこれらに近いものないかな、と思って探したら Boxcars というのを見つけました。

https://github.com/BoxcarsAI/boxcars

ざっくり触ってみた & コードに目を通してみたので、そのへんをまとめつつ紹介します。

## Boxcars の概要

Boxcars は LangChain を参考にして作られた Ruby ライブラリです。 [BoxCars, Inc.](https://www.boxcars.ai/blog/hello-world/) というスタートアップが開発している OSS です。
LangChain とは作者も異なり、よりユーザーフレンドリー (?) にということで、完全なコピーではなく、コンポーネント名も異なったりしています。

> Boxcars is a gem that enables you to create new systems with AI composability, using various concepts such as OpenAI, Search, SQL, Rails Active Record and more. This can even be extended with your concepts as well.(including your concepts).
>
> This gem was inspired by the popular Python library Langchain. However, we wanted to give it a Ruby spin and make it more user-friendly for beginners to get started.
> https://github.com/BoxcarsAI/boxcars

https://www.boxcars.ai/blog/

## どう動かすか

Boxcars では、 特定の種類の問い合わせ (Boxcar: LangChain Tools みたいなもの) が予め用意されていて、それに問い合わせることができます。

```ruby
require "dotenv/load"
require "boxcars"

Boxcars::Openai.new.run("1 + 2")
# 1 + 2
# 3
# => "3"

Boxcars::Calculator.new.run("1 + 2")
# > Entering Calculator#run
# 1 + 2
# {"status":"ok","answer":"3","explanation":"Answer: 3"}
# < Exiting Calculator#run
# => "3"

Boxcars::Calculator.new.run("what is pi to the forth power divided by 22.1?")
# > Entering Calculator#run
# what is pi to the forth power divided by 22.1?
# RubyREPL: puts (Math::PI ** 4 / 22.1)
# Answer: 4.407651178009159
#
# {"status":"ok","answer":"4.407651178009159","explanation":"Answer: 4.407651178009159","code":"puts (Math::PI ** 4 / 22.1)"}
# < Exiting Calculator#run
# => "4.407651178009159"

Boxcars::GoogleSearch.new.run("Ruby programming language creator")
# Question: Ruby programming language creator
# Answer: Yukihiro Matsumoto
# => "Yukihiro Matsumoto"
```

実際にこれらの多くは、特定の用途に特化したプロンプトを構築し、 ChatGPT に問い合わせることで、その用途に特化した回答を得ています。
設定でログを出力するようにすると、どのようなプロンプトを ChatGPT に問い合わせているかがわかります。

<details>
<summary>ChatGPT に問い合わせているプロンプト</summary>

```ruby
Boxcars.configuration.log_prompts = true

Boxcars::Openai.new.run("1 + 2")
# >>>>>> Role: assistant <<<<<<
# 1 + 2
# 3
# => "3"


Boxcars::Calculator.new.run("1 + 2")
# > Entering Calculator#run
# 1 + 2
# >>>>>> Role: system <<<<<<
# You can do basic math, but for any hard calculations that a human could not do in their head, use the following approach instead. Return code written in the Ruby programming language that prints the results. If anyone gives you a hard math problem, just use the following format and we’ll take care of the rest:
# ${{Question with hard calculation.}}
# reply only with the following format:
# ```ruby
# ${{only Ruby code that prints the answer}}
# ```
# ```output
# ${{Output of your code}}
# ```
#
# Otherwise, you should use this simpler format:
# ${{Question without hard calculation}}
# Answer: ${{Answer}}
#
# Do not give an explanation of the answer and make sure your answer starts with either 'Answer:' or '```ruby'.
# >>>>>> Role: system <<<<<<
# here is a hard example:
# the user asks: What is 37593 * 67?
# your answer: ```ruby
# puts(37593 * 67)
# ```
# ```output
# 2518731
# ```
# Answer: 2518731
# >>>>>> Role: system <<<<<<
# basic example:
# user asks: What is 2518731 + 0?
# you answer: Answer: 2518731
# >>>>>> Role: system <<<<<<
# Begin.
# >>>>>> Role: user <<<<<<
# 1 + 2
# {"status":"ok","answer":"3","explanation":"Answer: 3"}
# < Exiting Calculator#run
# => "3"

Boxcars::Calculator.new.run("what is pi to the forth power divided by 22.1?")
# > Entering Calculator#run
# what is pi to the forth power divided by 22.1?
# >>>>>> Role: system <<<<<<
# You can do basic math, but for any hard calculations that a human could not do in their head, use the following approach instead. Return code written in the Ruby programming language that prints the results. If anyone gives you a hard math problem, just use the following format and we’ll take care of the rest:
# ${{Question with hard calculation.}}
# reply only with the following format:
# ```ruby
# ${{only Ruby code that prints the answer}}
# ```
# ```output
# ${{Output of your code}}
# ```
#
# Otherwise, you should use this simpler format:
# ${{Question without hard calculation}}
# Answer: ${{Answer}}
#
# Do not give an explanation of the answer and make sure your answer starts with either 'Answer:' or '```ruby'.
# >>>>>> Role: system <<<<<<
# here is a hard example:
# the user asks: What is 37593 * 67?
# your answer: ```ruby
# puts(37593 * 67)
# ```
# ```output
# 2518731
# ```
# Answer: 2518731
# >>>>>> Role: system <<<<<<
# basic example:
# user asks: What is 2518731 + 0?
# you answer: Answer: 2518731
# >>>>>> Role: system <<<<<<
# Begin.
# >>>>>> Role: user <<<<<<
# what is pi to the forth power divided by 22.1?
# RubyREPL: puts (Math::PI ** 4 / 22.1)
# Answer: 4.407651178009159
#
# {"status":"ok","answer":"4.407651178009159","explanation":"Answer: 4.407651178009159","code":"puts (Math::PI ** 4 / 22.1)"}
# < Exiting Calculator#run
# => "4.407651178009159"

Boxcars::GoogleSearch.new.run("Ruby programming language creator")
# Question: Ruby programming language creator
# Answer: Yukihiro Matsumoto
# => "Yukihiro Matsumoto"
```

</details>

## 複数の Boxcar を利用して、問い合わせに対応できる

複数の Boxcar を組み合わせて、[LangChain Agent](https://python.langchain.com/en/latest/modules/agents/getting_started.html) のように、様々な問い合わせに対応することもできます。

```ruby
boxcars = [Boxcars::Calculator.new, Boxcars::GoogleSearch.new]
train = Boxcars.train.new(boxcars: boxcars)
train.run("Ruby の作者は誰?")
# > Entering Zero Shot#run
# Ruby の作者は誰?
# Thought: I don't know the answer to this question, but it seems like a factual question that can be answered through a search.
# Question: Ruby programming language creator
# Answer: Yukihiro Matsumoto
# Observation: Yukihiro Matsumoto
# I have found the answer to the question.
#
# Final Answer: Yukihiro Matsumoto is the creator of the Ruby programming language.
#
# Next Actions:
# 1. Who developed the Ruby programming language?
# 2. What year was the Ruby programming language created?
# 3. What are some notable features of the Ruby programming language?
# < Exiting Zero Shot#run
# => "Yukihiro Matsumoto is the creator of the Ruby programming language.\n\nNext Actions:\n1. Who developed the Ruby programming language?\n2. What year was the Ruby programming language created?\n3. What are some notable features of the Ruby programming language?"
```

実際にどのようなプロンプトを送っているかを見ると、 [ReAct Agent](https://python.langchain.com/en/latest/modules/agents/agents/examples/react.html) と同じように ReAct フレームワークに沿ったプロンプトから、利用する Boxcar とその入力を生成し、副問合せを行っている事がわかります。

![プロンプト](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/bd06c0fc-2a14-b5f1-b72d-64e45dd7e2b2.png)

他にもいくつかの動作例が https://github.com/BoxcarsAI/boxcars や https://github.com/BoxcarsAI/boxcars/blob/main/notebooks/boxcars_examples.ipynb にあります。

## LangChain に比べると実装されているプロンプトは少ない

ここまで、 LangChain Agent 相当の機能を持っていることはわかりました。必要最小限の機能はある印象ですが、後発であること、 LangChain に比べると実装されているプロンプト自体は少ないです。

* Boxcar (LangChain Tools あたりに相当)
  * 計算
  * SQL を生成しての問い合わせ
  * ActiveRecord を利用するコードを生成しての問い合わせ
  * (SerpAPI を使った) Google 検索
* Train (LangChain Agents あたりに相当)
  * zero-shot ReAct のみ
* 利用できる LLM は現時点では、OpenAI API のみ
* Indexes, Memory に相当する機能はない

## 所感

ざっくり Ruby 版の LangChain (?) である Boxcars を触ってみました。

流石に LangChain に比べると機能や実装済みのプロンプトは少ないので、実際にいろんなユースケースに活用するには、自前でプロンプトや Boxcar を実装する必要がありそうです。
ただ、 実装が面倒そうなロジック (ReAct を利用したツールの選択など) が用意されていたり、複数のプロンプトを組み合わせて問い合わせる枠組み自体は出来ているので、必要なデータを提供するなどのコアロジックの実装に集中しやすいのかな、という印象です。

Python や JavaScript が使える環境なら LangChain を使うのが良いと思いますが、 Ruby を使っている場面、使いたい場面では Boxcars を選択肢を持っておくとよいかも、という印象です。
そういった意味では Ruby であることは大きいかなと思っていて、 Ruby プログラマが、実際に手を動かしてみたり、実装を読んだりして (実装自体はコンパクトで把握しやすい印象です) プロンプトエンジニアリングを学ぶ題材としても面白いかな、と思います。

個人的には、 Ruby で書いているアプリケーションと組み合わせたプロトタイプを作ってみたり、色々 Boxcar を作ってみたり、[LangChainHub](https://github.com/hwchase17/langchain-hub) のプロンプトを活用してみたり、もうちょっとこれで遊んでみようかなと思います。