[LLM] microsoft/guidanceを使ってAgentを実装しToolを実行させてみる

こんにちは。@_mkazutakaです。
今回は、先日Microsoftから発表された microsoft/guidance を使ってAgentを実装しToolを実行させてみたのでその紹介と、ついでにLangChainのAgentと比較してみました。ぜひご参照ください。

microsoft/guidanceとは

ガイダンスは、従来のプロンプトやチェイニングよりも効果的かつ効率的に最新の言語モデルを制御することができます。ガイダンスプログラムでは、生成、プロンプト、論理制御を、言語モデルが実際にテキストを処理する方法と一致する単一の連続したフローに織り交ぜることができます。Chain of Thoughtやその多くのバリエーション(ART、Auto-CoTなど)のような単純な出力構造は、LLMのパフォーマンスを向上させることが示されている。GPT-4のようなより強力なLLMの出現は、さらに豊かな構造を可能にし、ガイダンスはその構造をより簡単かつ安価にする。

Readmeをはじめの方の文章をDeepLで翻訳

とのことです。以下の記事が日本語でまとまっていて大変勉強になりました。

Agentを実装する

Agentは、LLMがユーザの要求に従ってToolを選択し、実行するという機能です。今回は、Agentに2つのツールを渡して、要求に応じてそれぞれ実行してもらうようにします。

プロンプトの実装

なにはともあれ、Agentの用のプロンプトを用意する必要があります。LangChainのChatAgentのプロンプトをベースにプロンプトを書いていきます。

LangChainのChatAgentのプロンプトは以下の様になっています(コード)。
PREFIX、FORMAT_INSTRUCTIONS、SUFFIXと本体で別れています。実行時にライブラリ内で結合され使用されます。

SYSTEM_MESSAGE_PREFIX = """Answer the following questions as best you can. You have access to the following tools:"""
FORMAT_INSTRUCTIONS = """The way you use the tools is by specifying a json blob.
Specifically, this json should have a `action` key (with the name of the tool to use) and a `action_input` key (with the input to the tool going here).

The only values that should be in the "action" field are: {tool_names}

The $JSON_BLOB should only contain a SINGLE action, do NOT return a list of multiple actions. Here is an example of a valid $JSON_BLOB:

```
{{{{
  "action": $TOOL_NAME,
  "action_input": $INPUT
}}}}
```

ALWAYS use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action:
```
$JSON_BLOB
```
Observation: the result of the action
... (this Thought/Action/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question"""
SYSTEM_MESSAGE_SUFFIX = """Begin! Reminder to always use the exact characters `Final Answer` when responding."""
HUMAN_MESSAGE = "{input}\n\n{agent_scratchpad}"

こちらをベースにGuidanceのプロンプトを書いていきます。実際の出来上がったプロンプトが以下のようになります。上から順番に説明してきます。

Answer the following questions as best you can. You have access to the following tools:

{{~! 使用可能なツールを記述する ~}}
{{~#each tool_explanations}}
{{this.name}}: {{this.description}}
{{~/each}}

ALWAYS use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action:
```
{
  "action": $TOOL_NAME,
  "action_input": $INPUT_BY_JSON
}
```
Observation: the result of the action
Answer: Determines whether to continue running or if the task is complete
... (this Thought/Action/Observation/Answer can repeat N times)

### Input:
{{question}}

### Response:
{{~#geneach 'conversation' stop=False~}}
Question:{{question}}
Thought:{{gen 'thought' temperature=0 max_tokens=100 stop='\\n'}}
Action:
```json
{
    "action":"{{select 'action' options=valid_tools}}",
    "action_input": {{gen 'action_input' stop='}'}}
}
```
Observation: {{run_tool action action_input}}
Thought: {{gen 'thought2' temperature=0 max_tokens=100 stop='\\n'}}
Answer: {{select 'answer' options=valid_answers}}
{{#if (contains answer "FINISH")}}{{break}}{{/if}}
{{~/geneach}}

プロンプトの実装: PREFIX部

Answer the following questions as best you can. You have access to the following tools:

{{~! 使用可能なツールを記述する ~}}
{{~#each tool_explanations}}
{{this.name}}: {{this.description}}
{{~/each}}

まず、LangChainのプロンプトのPREFIXを参考にAgent内で使用するツールの説明をしています。Guidanceでは `{{~#each 変数名}}` から `{{~/each}}` までの範囲で囲むことにより、リスト形式で渡した変数を展開してくれます。このリストは、promptの引数を通じて渡すことができます。

プロンプトの実装: 本体部 (前半)

ALWAYS use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action:
```
{
  "action": $TOOL_NAME,
  "action_input": $INPUT_BY_JSON
}
```
Observation: the result of the action
Answer: Determines whether to continue running or if the task is complete
... (this Thought/Action/Observation/Answer can repeat N times)

LangChainのプロンプトを参考に構成しています。通常、LangChainのプロンプトでは`Final Answer`という形で最終的な回答を得ますが、今回は`Answer`という形にしています。LangChainのChatAgentは、LLMからの出力に`FinalAnswer`という文字列が含まれていれば、Agentの実行を終了するという動作をします。しかし、この方法をGuidanceで実装することができなかった(わからなかった)ため、異なる方法で実行を終了させる処理にしています(後述)。

プロンプトの実装: 本体部 (後半)

### Input:
{{question}}

### Response:
{{~#geneach 'conversation' stop=False~}}
Question:{{question}}
Thought:{{gen 'thought' temperature=0 max_tokens=100 stop='\\n'}}
Action:
```json
{
    "action":"{{select 'action' options=valid_tools}}",
    "action_input": {{gen 'action_input' stop='}'}}
}
```
Observation: {{run_tool action action_input}}
Thought: {{gen 'thought2' temperature=0 max_tokens=100 stop='\\n'}}
Answer: {{select 'answer' options=valid_answers}}
{{#if (contains answer "FINISH")}}{{break}}{{/if}}
{{~/geneach}}

実際にAgentが思考し、ツールを実行する部分です。
Input部分では、`{{question}}`という形式のプレースホルダーが使われています。これは実行時に引数で渡される値に置き換えられます。
応答部分は、`{{~#geneach 'conversation' stop=False~}}`というコードで開始されます。geneachは、上限回数を実施するまでは思考を止めません。geneachは、以下の要素を含みます:

  1. Question:  `{{question}}`という形式のプレースホルダーを使っています。

  2. Thought: `{{gen 'thought' temperature=0 max_tokens=100 stop='\\n'}}`を使用して、思考過程を表現するテキストを生成します。不必要に長い文章は不要なのでトークン数を制限しています。

  3. Action: JSON形式でのアクション表現が含まれています。アクション自体は`{{select 'action' options=valid_tools}}`で選択し、その入力は`{{gen 'action_input' stop='}'}}`で生成します。optionsの値は、実行時に引数で渡します。

  4. Observation: `{{run_tool action action_input}}`を用いてアクションの実行結果を観測します。run_toolは関数名なので変更できます。run_toolも同様に引数で渡します。

  5. Thought: `{{gen 'thought2' temperature=0 max_tokens=100 stop='\\n'}}`を用いて、次の思考過程を表現するテキストを生成します

  6. Answer: `{{select 'answer' options=valid_answers}}`を使用して、可能な答えの中から一つを選択します。

  7. 終了判定: 最後に`{{#if (contains answer "FINISH")}}{{break}}{{/if}}`をし、answerの値がFINISHならループを終わるようにしています。

以上がプロンプトの説明になります。次にツール及び、プロンプトの実行部のコードを書いていきます。

実行部の実装

ツールの実装

Agent内で実行されるツールを書いていきます。LangChainのカスタムツールを使います。弊社がもともとSlackbotを開発しているのもあってその際に使ったツールを再利用しています。以下のツールを定義しています。名前等はとくに今回の実装とは関係ありません。
・ChannelConfigurationThreadModeTool: チャンネルごとの返答方法を指定する。入力`thread_mode`
・ChannelConfigurationPromptTool: チャンネルごとのプロンプトを設定する。入力は`prompt`

from langchain.tools import BaseTool

class ChannelConfigurationThreadModeTool(BaseTool):
    name = "ChannelConfigurationThreadModeTool"
    description = f"""A wrapper Channel Configuration Tool that can be used to set up thread mode.
    Input should be "True" or "False" string with one keys: "thread_mode".
    The value of "thread_mode" should be a "True" if the user wants to use thread, otherwise "False".
    Call only if Thread mode is specified.
    """
    def _run(self, thread_mode: str) -> str:
        return "Successfully set up thread mode"
    async def _arun(self, thread_mode: str) -> str:
        pass

class ChannelConfigurationPromptTool(BaseTool):
    name = "ChannelConfigurationPromptTool"
    description = f"""A wrapper Channel Configuration Tool that can be used to set up prompt.
    Input should be a string with one keys: "prompt".
    The value of "prompt" should be a string. It is used for prompt on llm.
    Call only if Prompt is specified.
    """
    def _run(self, prompt: str) -> str:
        return "Successfully set up Prompt"
    async def _arun(self, prompt: str) -> str:
        pass

必要な要素の準備

プロンプトから以下のものを用意する必要があります
・tools: nameとdescriptionをキー値に持つ辞書のリスト
・question: ユーザの要求
・valid_tools: actionで実行されるツール名、toolsから取得する。
・run_tool: action結果を受け取って実際にコードを実行できる関数
・valid_answers: 最終的な出力

tools = [
    ChannelConfigurationThreadModeTool(),
    ChannelConfigurationPromptTool(),
]
tool_explanations = [{"name": tool.name, "description": tool.description} for tool in tools]
valid_tools = [tool.name for tool in tools]
tool_dict = { tool.name: tool for tool in tools }
def run_tool(name: str, value: str):
    value = json.loads(value + '}')
    return tool_dict[name].run(tool_input=value)
valid_answers = ["CONTINUE", "FINISH"]

実行部の実装

guidanceのReadmeに従って実装していきます。

guidance.llm = guidance.llms.OpenAI("text-davinci-003")
guidance.llms.OpenAI.cache.clear()
prompt = guidance(prompt_template)
result = prompt(
    question='スレッドモードに設定し、プロンプトを「小学生で元気よく挨拶する。」に設定してください。',
    tool_explanations=tool_explanations,
    valid_tools=valid_tools,
    valid_answers=valid_answers,
    run_tool=run_tool,
)

実行結果

以下のように実行されます。動画ではわかりにくいですが、定義したツールクラスがきちんと実行されています。

感想

LLMからの出力を制御できるのは、かなりいい印象です。Answerの形式を指定して、特定の値が来たらAgentを終了する処理は、LangChainでも苦労する部分なので、すんなりできたのは感動しました。
プロダクションで使うには、LangChainよりmicrosoft/guidanceのほうが制御ができる分使いやすいそうな印象ですね。Streamingの部分等はまだだめしてないですが…

今回は、gpt-3.5-turboは使わなかった(一通り書いたあとにguidance側が制限していることに気づいた)ので、そのあたりを使ってAgent等が実装できないか引き続き調査していきたいです。

以上参考になれば幸いです。

参考

この記事が気に入ったらサポートをしてみませんか?