自然言語によるタスク指示で、所定の処理に所定のAPIを使うよう誘導する

# 背景

一連の処理から成るタスクを自然言語でLLMに指示する場合、「この部分はLLMのchain of thoughts / planningに任せず、所定のpython関数や所定のAPIコールで行いたい」という場合があると思います。そんなときに同様の話を

参考issue:
https://github.com/hwchase17/langchain/issues/832  

で見つけたので、自分用にまとめてみました。
LangChain × OpenAI の枠組みで検証していますが、一般化すれば「LLM promptingとplanningの話」だと思っています。

# 概要

langchainカスタムツールのdescriptionをうまくpromptingすることで、

- 所定処理だけをそのtoolに誘導し
- 処理の入出力をそのtool func用の形式（例えばJSON化したdict）に誘導できる
- tool funcの中で好きなAPIを呼べばよい

# 題材

本関係のサービスを構築中のため、「指定した本の書誌情報を取得する」というタスクをagentで解決する場合を例に取ります。  
例えばこれをweb検索=「google-search」ツールだけで解決しようとすると、しばしば間違った回答が返るため、専用APIコールに誘導することで正確性を上げたい、というモチベーションです。
ちなみにクエリしている「千葉ルー」とは[この本](https://www.amazon.co.jp/%E5%8D%83%E8%91%89%E3%81%8B%E3%82%89%E3%81%BB%E3%81%A8%E3%82%93%E3%81%A9%E5%87%BA%E3%81%AA%E3%81%84%E5%BC%95%E3%81%8D%E3%81%93%E3%82%82%E3%82%8A%E3%81%AE%E4%BF%BA%E3%81%8C%E3%80%81%E4%B8%80%E5%BA%A6%E3%82%82%E6%B5%B7%E5%A4%96%E3%81%AB%E8%A1%8C%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8%E3%81%8C%E3%81%AA%E3%81%84%E3%81%BE%E3%81%BE%E3%83%AB%E3%83%BC%E3%83%9E%E3%83%8B%E3%82%A2%E8%AA%9E%E3%81%AE%E5%B0%8F%E8%AA%AC%E5%AE%B6%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%9F%E8%A9%B1-%E6%B8%88%E6%9D%B1%E9%89%84%E8%85%B8/dp/4865283501)の通称です。

# 詳細

## 1) 所定のAPIを呼ぶ関数を実装

```python
import json
import requests


def search_book(message_json: str):
    """return book info for given ISBN
    :param message_json: JSON string like {"isbn": "<ISBN>"}
    """

    try:
        isbn = json.loads(message_json)["isbn"]
    except ValueError as e:
        return "invalid input format. format should be JSON string having 'isbn' key"

    url = f"https://api.openbd.jp/v1/get?isbn={isbn}&pretty"
    response = requests.get(url)

    if response.status_code != 200:
        return "Failed to request to OpenBD API."

    book_data = response.json()
    if len(book_data) == 0:
        return "No book info found for given ISBN."

    return book_data[0]["summary"] # dict having "author", "title", "publisher"
```
エラーハンドリングの不明点・課題：
- 例外を投げるとagentがそこで落ちてしまうので、投げるべきではない
- エラーメッセージをそのまま返すのが良さそう？
  - LLM処理の都合上、結局入出力とも文字列化する必要があるので、正常戻り値もdictではなくjson.dumpすべき？
- しかしそうすると自然言語でエラー判定を依頼していることになる。エラーレスポンスフォーマットもdescription（＝prompt）で規定してしまった方がメリットあるのでは？

## 2) 作成した関数をTool化

```python
mytool = Tool(
        name="get book information",
        func=search_book,
        description="""helps to get book information(title, author, publisher) from its ISBN number.
    Before using this API, you must get ISBN of the book.
    Input should be json in the following format: `{{"isbn": "<ISBN number>"}}`
    Output is a JSON in the following format: `{{"title": "<title>", "author": "<author>"}}`
    """
)
```
ポイント：
- 入出力内容をdescriptionで詳細に記載する
  - langchainのprompt engineeringにより、LLMがその情報をplanningに使用する
- カスタムツール関数の入出力フォーマットをdescriptionで指定する
  - 特に入力フォーマットの一致は大事
  - langchainのprompt engineeringにより、LLMがそのフォーマットで中間返答を返してくれ、それがカスタムツール関数に入力される
  - 入力は文字列にする必要があるので、例えばdictのJSON表現を採用すればよい

## 3) 作成したToolを使うAgentを作成

```python
from langchain.agents import load_tools
from dotenv import load_dotenv
from langchain import OpenAI

# loading OPENAI_API_KEY, GOOGLE_CSE_ID, GOOGLE_API_KEY from .env
load_dotenv()

# OpenAIのLLMを初期化
llm = OpenAI(temperature=0, verbose=True)

# 実験のため、google-search toolとカスタムツールを併用 
tools = load_tools(["google-search"], llm=llm)
tools.append(mytool)

executor = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)
```
不明点：
- llm:OpenAIだと、modelはデフォルトでtext-davinch-003。model: gpt-3.5-turboを使うにはllm:ChatOpenAIにすべきということだったが、変えてみたところ後段処理でエラーが出た
- TODO ChatModelを使う場合の書き方変更点を整理


## 4) Agentに質問

```python
question = "千葉ルーという本の詳細を教えてください。著者や出版社など。"
react.run(question)
```

## 出力

```
> Entering new AgentExecutor chain...
 ISBNを検索して本の情報を取得する
Action: Google Search
Action Input: 千葉ルー ISBN
Observation: より深くルーマニアの文化に潜るための巻末資料付き！ ◉ルーマニアックの本棚／言語、食文化、思想といった幅広いジャンルから選び抜いた、魂のブックリスト. ◉ルー ... 千葉の片隅から、魂の故郷・ルーマニアへの愛を叫ぶ—— ... ISBN: 9784-86528-350-1 ... ルーマニアックの本棚／言語、食文化、思想といった幅広いジャンルから選び抜い ... ルーマニア映画に惚れ、ルーマニア語を学び、ルーマニア文学の一部になったルーマニア馬鹿の全記録！この #千葉ルー を宜しくお願いします！ 出版社 : 岩崎書店 (2008/3/1) 発売日 : 2008/3/1 言語 : 日本語 ISBN-10 : 4265104517 ISBN-13 : 978-4265104512 ... 着色料不使用でこんなに綺麗な色のルーでした。 八方塞がりの苦しみから、ルーマニア語が救ってくれた。暑苦しくって切実で、好奇心みなぎるノンフィクションエッセイ。 【来たるべきルーマニアックのための巻末資料】 ... Jan 11, 2023 ... 著者Twitterよりこの #千葉ルー は彼方から極東の私を支えてくれたルーマニア文化とルーマニアの人々への感謝の書 ... ISBN：978-4-86528-350-1 C0095 シルバー金具 2000年のど飴 /ルー出版/2000年問題研究会. 著者：2000年問題研究会出版社：ルー出版サイズ：単行本. ISBN-10：489778090X ISBN, 978-4-03-528390-4. NDC, 913. 発売日, 2009年10月 ... 絵描きのルーちゃんが話してくれた、少しふうがわりなお話。 ... ＜高楼方子×千葉史子＞原画展. 2018.8.3. 発送元の地域千葉県 ... トゥバペンライト カップルードル ロボタイマー シーフードヌードル スターバックス さくら 2023 ハンディーステンレスボトル パープル500ml. ウォータールー大学（英語: University of Waterloo）は、オンタリオ州ウォータールー市に本部を ... 京都大学 · 東京工業大学 · 東北大学 · 熊本大学 · 鳥取大学 · 千葉大学 ...
Thought: ISBNを取得して本の情報を取得する
Action: get book information
Action Input: {"isbn": "978-4-86528-350-1"}
Observation: {'isbn': '9784865283501', 'title': '千葉からほとんど出ない引きこもりの俺が、一度も海外に行ったことがないままルーマニア語の小説家になった話', 'volume': '', 'series': '', 'publisher': '左右社', 'pubdate': '20230207', 'cover': 'https://cover.openbd.jp/9784865283501.jpg', 'author': '済東鉄腸／著'}
Thought: 本の情報を取得した
Final Answer: 千葉ルーという本の詳細は、著者が済東鉄腸、出版社が左右社、発売日が2023年2月7日、ISBNが978-4-86528-350-1です。

> Finished chain.
```

## debug 

最初にLLMに投げているクエリ全文
```
Answer the following questions as best you can. You have access to the following tools:

Google Search: normally use this.
get book information: helps to get book information(title, author, publisher) from its ISBN number.
    Before using this API, you must get ISBN of the book.
    Input should be json in the following format: `{"isbn": "<ISBN number>"}`
    Output is a JSON in the following format: `{"title": "<title>", "author": "<author>"}`
    

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [Google Search, get book information]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: 千葉ルーという本の詳細を教えてください。著者や出版社など。
Thought:
```

これはagentが最初に[planningを行う](https://python.langchain.com/en/latest/_modules/langchain/agents/agent.html#Agent.plan)中でのLLMへのprompt。  
質問文とtool情報を材料にして、
「クエリ文（=goal）と使用可能なtool一覧（機能説明、入出力の内容）を渡すので、goal達成に向けたactionをplanningしてください」
というpromptに成形し、LLMに送っている。
その結果として最初のactionが、

> ISBNを検索して本の情報を取得する

として決定されている。

# 感想

今回の方法以外にも色々方法はありそう。

例：目的APIのOpenAPIドキュメントを用意し（URL指定など）、それに基づいてLLMに目的処理のpythonコードを出力させ、それをlangchainのpython REPLへ接続して実行させる。

注意点としては、

- OpenAPIドキュメント全体をLLM promptに含めてしまうと、長い場合には（GPT4以外は）token数オーバーになるため工夫が必要。
- カスタムツールをそもそも作らず、「まずISBNを調べて、OpenBD APIを使って書誌情報を取得して」と全てをAgentに丸投げすることもできる（実証済）が、そのあたりの情報取得をgoogle-searchに任せると誤情報にぶつかってタスクが完遂しない確率が上がっていくので要注意。

ちなみにlangchainには、[ニュース・映画・天候情報などをAPIで取得するtool群](https://github.com/hwchase17/langchain/blob/v0.0.135/docs/modules/agents/tools/getting_started.md)が備わっており、その中では各APIのOpenAPIドキュメントを元にしてAPIを呼んでいる様子。  

以上、個人的メモレベルのため、誤りやアドバイスがありましたら、ご指摘お願いします！