[LLM] microsoft/guidanceを使ってAgentを実装しToolを実行させてみる
こんにちは。@_mkazutakaです。
今回は、先日Microsoftから発表された microsoft/guidance を使ってAgentを実装しToolを実行させてみたのでその紹介と、ついでにLangChainのAgentと比較してみました。ぜひご参照ください。
microsoft/guidanceとは
とのことです。以下の記事が日本語でまとまっていて大変勉強になりました。
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は、以下の要素を含みます:
Question: `{{question}}`という形式のプレースホルダーを使っています。
Thought: `{{gen 'thought' temperature=0 max_tokens=100 stop='\\n'}}`を使用して、思考過程を表現するテキストを生成します。不必要に長い文章は不要なのでトークン数を制限しています。
Action: JSON形式でのアクション表現が含まれています。アクション自体は`{{select 'action' options=valid_tools}}`で選択し、その入力は`{{gen 'action_input' stop='}'}}`で生成します。optionsの値は、実行時に引数で渡します。
Observation: `{{run_tool action action_input}}`を用いてアクションの実行結果を観測します。run_toolは関数名なので変更できます。run_toolも同様に引数で渡します。
Thought: `{{gen 'thought2' temperature=0 max_tokens=100 stop='\\n'}}`を用いて、次の思考過程を表現するテキストを生成します
Answer: `{{select 'answer' options=valid_answers}}`を使用して、可能な答えの中から一つを選択します。
終了判定: 最後に`{{#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等が実装できないか引き続き調査していきたいです。
以上参考になれば幸いです。