😸

Langchain.rbの機能つまみ食い

2024/02/04に公開

僕が働くウォンテッドリー ではRubyをメインで使っている。

そのときChatGPTを使うならどういったライブラリがあるだろうと思い、Langchain.rbの機能を把握しておこうと思う。
(あと、ChatGPT周りのキャッチアップのため)
色々試している間のメモはこちら https://zenn.dev/rerost/scraps/b79890f9aa1623

読んだのは0.9.0 https://github.com/andreibondarev/langchainrb/releases/tag/0.9.0

機能

サポートしている機能

  • プロンプト管理
  • アシスタント作成(今でいうところのGPTsに近い)
  • Output Parser(JSON Modeのようなもの)
  • RAG (今回は対象外)

プロンプト管理

全体的にLLMの実装と切り離されており、取り回しがしやすい。ここだけ使うのもありだと思う。

LLM とは分離された形でTemplate機能が入っている。LLM特有っぽい機能はなさそうかも?
強いて言えば、

  • JSONベースでの管理ができる。多言語対応とかやりやすそう
  • Few Shot Learningがやりやすいらしい

Template

サンプルをそのまま実行している。

> prompt = Langchain::Prompt::PromptTemplate.new(template: "Tell me a {adjective} joke about {content}.", input_variables: ["adjective", "content"])
> prompt.format(adjective: "funny", content: "chickens") # "Tell me a funny joke about chickens."
=> "Tell me a funny joke about chickens."
> prompt.input_variables
=> ["adjective", "content"]

JSONでの管理

こんな感じで使えるらしい。

{
  "_type": "prompt",
  "input_variables": ["adjective", "content"],
  "template": "Tell me a {adjective} joke about {content}."
}

https://github.com/andreibondarev/langchainrb/blob/0.9.0/spec/fixtures/prompt/prompt_template.json

あたり。単体ではあまり嬉しさがわからない(強いて言うなら別ファイルなので、別のエンジニアと作業分担がし易いとかはありそう)。

Few Shot Learning

英語が得意ではないので日本語の例を入れた。
ありがちなFew Shot Learningを管理できる。

require "langchain"

prompt = Langchain::Prompt::FewShotPromptTemplate.new(
  prefix: "プレイヤー数を教えてください",
  suffix: "Input: {game_name}\nOutput:",
  example_prompt: Langchain::Prompt::PromptTemplate.new(
    input_variables: ["input", "output"],
    template: "Input: {input}\nOutput: {output}"
  ),
  examples: [
    { "input": "リバーシ", "output": "2人" },
    { "input": "テトリス", "output": "1~4人" },
  ],
   input_variables: ["game_name"]
)

p prompt.format(adjective: "テニス")

llm = Langchain::LLM::OpenAI.new(api_key: ENV["OPENAI_API_KEY"])

p llm.complete(prompt: prompt.format(adjective: "テニス")).completion
$ bundle exec ruby few_shot_learning.rb
"プレイヤー数を教えてください\n\nInput: リバーシ\nOutput: 2人\n\nInput: テトリス\nOutput: 1~4人\n\nInput: テニス\nOutput:"
"2人"

こんな感じで、Few Shot Learningの使い回しができる。

ちなみにsaveもできる

prompt.save(file_path: "./hoge.json")
$ cat hoge.json | jq .
{
  "_type": "few_shot",
  "input_variables": [
    "game_name"
  ],
  "prefix": "プレイヤー数を教えてください",
  "example_prompt": {
    "_type": "prompt",
    "input_variables": [
      "input",
      "output"
    ],
    "template": "Input: {input}\nOutput: {output}"
  },
  "examples": [
    {
      "input": "リバーシ",
      "output": "2人"
    },
    {
      "input": "テトリス",
      "output": "1~4人"
    }
  ],
  "suffix": "Input: {game_name}\nOutput:"
}

ちなみに_typefew_shotと素のtemplateのpromptの2つ。
https://github.com/andreibondarev/langchainrb/blob/76ce6332f5b6a330c20f9690ad099a789cd75ef1/lib/langchain/prompt/loading.rb#L9-L12

アシスタント

こんな感じでassistant(LLMにツールや事前の会話を付与したもの)を作れる。元から用意されているRubyCodeInterpreter を実行。

require "langchain"

llm = Langchain::LLM::OpenAI.new(api_key: ENV["OPENAI_API_KEY"])

thread = Langchain::Thread.new

assistant = Langchain::Assistant.new(
  llm: llm,
  thread: thread,
  tools: [
    Langchain::Tool::RubyCodeInterpreter.new
  ]
)

assistant.add_message content: "フィボナッチ数列の100個目を教えてください。またできるだけ効率的な探し方をしてください"
puts assistant.run(auto_tool_execution: true).map { |message| {role: message.role, content: message.content} }

こんな感じでRubyのコードを生成して実行させることができる。

~/go/src/github.com/rerost/tmp/langchainrb_test rerost/langchainrb* 7s
(arm64) $  bundle exec ruby assistants.rb
I, [2024-02-04T21:17:50.254519 #81332]  INFO -- : [LangChain.rb] [Langchain::Tool::RubyCodeInterpreter]: Executing "def fibonacci(n)
  fib = [0, 1]
  (2..n).each do |i|
    fib[i] = fib[i-1] + fib[i-2]
  end
  fib[n]
end

fibonacci(100)"
{:role=>"user", :content=>"フィボナッチ数列の100個目を教えてください。またできるだけ効率的な探し方をしてください"}
{:role=>"assistant", :content=>""}
{:role=>"tool", :content=>"354224848179261915075"}
{:role=>"assistant", :content=>"フィボナッチ数列の100個目は、354224848179261915075です。この結果は、効率的な方法で計算されました。"}

ちなみにtoolsはdescriptionを書いておいて、それに合わせてLLMがInputを渡してくれるのでそれを実行する、という形なので、気軽に好きな機能が追加できそう。期待するフォーマットを渡してくれるように調整するのが大変そうだが。

Output Parser

もし、ChatGPTを利用するなら、こちらよりもJSON Modeとかを利用するのが良さそうだとは思う。

処理の流れとして

  1. JSON Schemaを与えてそれに従ってもらうプロンプトを作る
  2. 帰ってきたレスポンスをパース

という流れ。

追加されたのが、2023/06とかなので事情が変わっているかも。
https://github.com/andreibondarev/langchainrb/pull/208

JSON ModeがないLLMでやるときとかは便利そう。あと、逆にJSON Modeを使いたいときは直に叩くのが良いかも?

require "langchain"

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: ["interest", "levelOfInterest"],
        additionalProperties: false
      },
      minItems: 1,
      maxItems: 3,
      description: "A list of the person's interests"
    }
  },
  required: ["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: ["description", "format_instructions"])
prompt_text = prompt.format(description: "Korean chemistry student", format_instructions: parser.get_format_instructions)
puts "---prompt_text--"
puts prompt_text

llm = Langchain::LLM::OpenAI.new(api_key: ENV["OPENAI_API_KEY"])
llm_response = llm.complete(prompt: prompt_text).completion
puts "---llm_response--"
puts llm_response

puts "---parse_result--"
puts parser.parse(llm_response)
~/go/src/github.com/rerost/tmp/langchainrb_test rerost/langchainrb*
(arm64) $  bundle exec ruby output_parser.rb
---prompt_text--
Generate details of a fictional character.
You must format your output as a JSON value that adheres to a given "JSON Schema" instance.

"JSON Schema" is a declarative language that allows you to annotate and validate JSON documents.

For example, the example "JSON Schema" instance {"properties": {"foo": {"description": "a list of test words", "type": "array", "items": {"type": "string"}}, "required": ["foo"]}
would match an object with one required property, "foo". The "type" property specifies "foo" must be an "array", and the "description" property semantically describes it as "a list of test words". The items within "foo" must be strings.
Thus, the object {"foo": ["bar", "baz"]} is a well-formatted instance of this example "JSON Schema". The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.

Your output will be parsed and type-checked according to the provided schema instance, so make sure all fields in your output match the schema exactly and there are no trailing commas!

Here is the JSON Schema instance your output must adhere to. Include the enclosing markdown codeblock:
```json
{"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":["interest","levelOfInterest"],"additionalProperties":false},"minItems":1,"maxItems":3,"description":"A list of the person's interests"},"required":["name","age","interests"],"additionalProperties":false}
```

Character description: Korean chemistry student
---llm_response--
{
  "name": "Ji-hyun Kim",
  "age": 21,
  "interests": [
    {
      "interest": "Organic Chemistry",
      "levelOfInterest": 85
    },
    {
      "interest": "Physical Chemistry",
      "levelOfInterest": 70
    },
    {
      "interest": "Analytical Chemistry",
      "levelOfInterest": 60
    }
  ]
}
---parse_result--
{"name"=>"Ji-hyun Kim", "age"=>21, "interests"=>[{"interest"=>"Organic Chemistry", "levelOfInterest"=>85}, {"interest"=>"Physical Chemistry", "levelOfInterest"=>70}, {"interest"=>"Analytical Chemistry", "levelOfInterest"=>60}]}

それっぽくなっているし、JSONにパースするところまでやってくれる。

まとめ

Output Parserは良い機能だと思うが、ChatGPT側でサポートするなどがリスクとしてある。
自分としては利用するなら、

  • プロンプト管理
  • アシスタント

とかは依存してほかはChatGPTの進化を待つ、みたいな感じだとちょうどいい距離感なのかなと思う。

Discussion