生成AIにランダム要素のある文章を頼むと似通ったものが出てくるので、先に大量にアイデアを上げてもらうようにしたい。

langchainのagentにあるtoolsでもOpenAIのfunction-callingが使えるようなのでそれを使ってやってみます。

準備

pyenvを使ってインストールした後に、venvで仮想環境にしています。

-- brew で pyenvをインストール --
brew install pyenv

-- pyenv で python をインストール
pyenv install 3.11.4
pyenv global 3.11.4

-- 作業用のフォルダでvenv
python -m venv ai-chat.venv
. ./ai-chat.venv/bin/activate

openaiとlangchainのインストールです。

pip install openai
pip install langchain

これでインストール完了です

以前に書いた記事も参考にどうぞ。

LangChainのToolについて

LangChainのカスタムツールを使用することによって、自分自身のエージェントにユニークな機能を追加することが可能になります。以下にその詳細をまとめています:

  • エージェントに提供するツールのセット内でユニークである必要がある名前(name)を指定します。これは必須の項目です。OpenAIのfunction callingを使う場合、「^[a-zA-Z0-9_-]{1,64}$」という縛りがあるようです。
  • ツールの使用方法をエージェントが判断するために使用される説明(description)を指定することが推奨されています。これはオプションです。
  • さらに、パラメータの詳細情報や検証を提供するためにPydantic BaseModelを使用したargs_schemaを指定することができます。これもオプションです​。

このカスタムツールを作成するには、基本的に2つの方法があります:

  1. Tool dataclassを使用する方法:この方法では、一つの文字列入力を受け取り、一つの文字列出力を返す関数をラップするためにTool dataclassを使用します。あるいは、より詳細な入力情報を提供するためにカスタムargs_schemaを定義することも可能です​。
  2. BaseTool classをサブクラス化する方法:より多くの制御を必要とする場合や、ネストされたチェインや他のツールへのコールバックを伝播させたい場合は、BaseToolを直接サブクラス化すると便利です。例えば、特定のツールを非同期に動作させたい場合や、特定の動作をカスタマイズしたい場合に有用です​。

これらの方法を使えば、エージェントが特定のタスクを達成するために必要な情報を検索したり、複雑な計算を行ったりといった特定の機能を実装することができます。これにより、エージェントの対応能力や対応範囲を拡大することが可能になります。

参考:https://python.langchain.com/docs/modules/agents/tools/how_to/custom_tools

今回のプログラム

今回は、BaseTool classをサブクラス化して自作の関数をToolに登録して使っていきます。また、agent=AgentType.OPENAI_FUNCTIONSとすることで、OpenAIのfunction callingを使うようにしています。

中身的には、冒頭で少し書きましたが、生成AIに大量にアイデア(今回は花の名前)をあげてもらって、その後に自作関数でランダムにチョイスするというプログラムになっています。

from langchain.tools import BaseTool
from math import pi
import random

from langchain.chat_models import ChatOpenAI
from langchain.chains.conversation.memory import ConversationBufferWindowMemory
from langchain.agents import AgentType
  

class FirstWord(BaseTool):
    name = "get_random_item_from_csv_string"
    description = "渡された、csv形式の文字列から、ランダムに抽出したカラムの要素を返します。"

    def _run(self, words: str):
        # random choice of three items
        items = words.split(',')
        return [random.choice(items) for _ in range(3)]
    def _arun(self, words: str):
        raise NotImplementedError("This tool does not support async")


# initialize LLM (we use ChatOpenAI because we'll later define a `chat` agent)
llm = ChatOpenAI(
        temperature=0,
        model_name='gpt-3.5-turbo-0613'
)

# initialize conversational memory
conversational_memory = ConversationBufferWindowMemory(
        memory_key='chat_history',
        k=5,
        return_messages=True
)

from langchain.agents import initialize_agent

tools = [FirstWord()]

# initialize agent with tools
agent = initialize_agent(
    agent=AgentType.OPENAI_FUNCTIONS,
    tools=tools,
    llm=llm,
    verbose=True,
    max_iterations=3,
    early_stopping_method='generate',
    memory=conversational_memory
)

# run agent
agent.run("花の名前を日本語で30個リストアップしてください。その30個をリストからランダムに要素を取得する関数を使って、選ばれた要素を3つ教えてください。")

実行結果

実行した結果です。こんな感じで期待通り動いてくれました。

> Entering new  chain...

Invoking: `get_random_item_from_csv_string` with `さくらんぼ,ひまわり,ばら,たんぽぽ,すみれ,あじさい,ふじ,あさがお,ひつじぐさ,あやめ,ひょうたん,すずらん,ひめゆり,あさがお,ひまわり,さくら,たんぽぽ,すみれ,あじさい,ふじ,あやめ,ひょうたん,すずらん,ひめゆり,さくらんぼ,ばら,ひつじぐさ,あさがお,ひまわり,たんぽぽ`
responded: {content}

['ひょうたん', 'あじさい', 'ひょうたん']ランダムに選ばれた花の名前は、「ひょうたん」、「あじさい」、「ひょうたん」です。

> Finished chain.

課題

関数を途中に挟むことで、期待した形式のデータがクリーニング済みで渡ってくるのはとても便利ですね。本当は配列で受け取りたかったのですが、関数を配列にするとエラーになることが多いので、その辺りについては色々知りたいと思っています。