Claude のプロンプトエンジニアリングテクニックを駆使して LangChain の ReACT Agent を構築してみた

この記事は [Anthropic Advent Calendar](https://qiita.com/advent-calendar/2023/anthropic-claude) の7日目の投稿です。

こんにちは、生成 AI に仕事を自動化させて楽をしたいですよね。

今回は仕事の自動化に活用できるプロンプトエンジニアリングテクニックの ReACT を Claude で実装したのでその Tips を紹介します！

## ReACT とは

ReACT とは、[Yao 2022](https://arxiv.org/abs/2210.03629) で提唱された、LLM が思考のプロセスとタスクに応じたアクションを交互に生成する Chain of Thought の派生プロンプトエンジニアリングテクニックです。

LLM が思考プロセスを生成することで、アクションプランを導き出し、追跡、更新することができます。また、例外が発生した場合の処理も可能です。

・・・といっても初めて聞く場合はイメージが湧かないと思うので具体例も交えて解説します。

まず思考フェイズではユーザーからの指示や現在の状況から何が必要かを思考させるために Thought を LLM に出力させます。

```
Thought: X についての情報得るために Web 検索をする必要がある
```

次のアクションステップでは、外部の知識ベースや API と連携し、必要な情報を収集します。Thought のステップの出力から続きを生成させ、事前に定義されたツールの名前と入力パラメータを LLM に出力させます。

```
Action: Web 検索
Action Input: X
```

ここまでで一旦 LLM の出力は止めさせ、これらの出力をスクリプトでパースし、データソースや API への連携を行います。
そしてその結果を Obsevation としてこれまでの文章に Append し LLM に渡すことで次の Thought から続きを生成させ、これまでの考えと連携されたデータをもとに次のアクションへとループしていきます。

```
Observation: 検索結果（スクリプトで追加）・・・・
Thought: X についてより深掘りして検索をする必要がある
（以下ループ）
```

そして最終的に必要なデータが集まったと LLM が判断したら、Thought の次は Action ではなく Final Answer を出力します。

```
Thought: 必要な情報が揃いました。
Final Answer: X は・・・・・
```

このように、推論とアクションを組み合わせることで、LLM の能力が大きく向上することが示唆されています。

この考え方をベースとした派生として、GPT や Claude の Function calling 機能や Amazon Bedrock の Agent for Bedrock 機能があり、どちらも外部 API やデータソースと生成 AI を連携させてタスクを実行する仕組みです。

Amazon Bedrock ではすでに Agent for Bedrock 機能が利用できるようになっており、エラーハンドリング含め、Agent の実装をマネージドサービスとして提供しているため実サービスに組み込んだりして運用するのに向いていますが、今回は Agent の仕組みを学ぶために、ローカルで LangChain を使用し Agent を手作りしてみます。

## Amazon Bedrock Claude と LangChain での ReACT

大まかな構築方法についてはすでにこちらの記事で紹介されているため、今回はこちらをベースにより Claude に最適化された Agent を作成します。

https://qiita.com/moritalous/items/60bcae924812a18d65b0

今回は Web から情報を収集して記事を作成する Agent を作成していきます。

まずは必要なライブラリをインストールします。

```bash
!pip install "langchain==0.0.309" langchainhub -qU
!pip install "duckduckgo-search>=3.9.5" -qU
```

次に必要なライブラリをインポートし、LLM とツールを定義します。今回は無料で API キーなしでも利用できる DuckDuckGo を検索エンジンとして利用します。Google 検索ツールも利用できますが、1日100リクエスト以上は有料になるためここでは DuckDuckGo を使用しています。

```python
import langchain
from langchain.llms.bedrock import Bedrock
from langchain.tools import DuckDuckGoSearchRun
from langchain.tools.render import render_text_description
from langchain.agents import Tool, AgentExecutor
from langchain.agents.output_parsers import ReActSingleInputOutputParser
from langchain.utilities import DuckDuckGoSearchAPIWrapper
from langchain import hub
import boto3

# Initialize Model
bedrock_client = boto3.client("bedrock-runtime")
llm = Bedrock(
    model_id="anthropic.claude-instant-v1",
    client=bedrock_client,
    model_kwargs={ 'max_tokens_to_sample': 1024 }
)

# Initialize Tool
wrapper = DuckDuckGoSearchAPIWrapper(region="jp-jp", safesearch="strict")
search = DuckDuckGoSearchRun(api_wrapper=wrapper)
tools = [
    Tool(
        name="duckduckgo-search",
        func=search.run,
        description="日本語もしくは英語でウェブ検索が可能",
    )
]
```

さて、ここまでは LLM を Claude に置き換えただけで、GPT の ReACT とは大きく変わりません。

このままモデルだけ置き換えて ReACT を実行することはできますが、一定確率で Parse Error になります。
なぜなら、デフォルトのプロンプトテンプレートは GPT 向けのプロンプトであり他のモデルに応用が効かないからです。また、[GPT 以外の OSS モデルで Parse Error が発生することや gpt-3.5-turbo だと Parse Error が発生するため GPT4 を使用している](https://github.com/langchain-ai/langchain/issues/1358) といった報告がされています。（ReACT のプロンプトは過去のコンテキストなどを引き継いでループし長文になりがちなので GPT 4 を使うとコストが高くついてしまいます・・・）

```
# デフォルトのプロンプトテンプレート
Answer the following questions as best you can. You have access to the following tools:

{tools}

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 [{tool_names}]
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: {input}
Thought:{agent_scratchpad}
```

今回は [Claude Prompt Guide](https://docs.anthropic.com/claude/docs/introduction-to-prompt-design) を参考にこのプロンプトテンプレートを Cluade 向けに最適化しより安定して高い精度の出力が出せるようにしていきます。試した限りでは、Claude Instant v1 でも Parse Error にならず十分な精度が出ますので GPT 4 と比較してもかなり低コストでエージェントの運用が可能です。

まず Agent で利用する出力の Parser を Claude 向けに最適化します。[元の ReACT の実装](https://github.com/langchain-ai/langchain/blob/master/libs/langchain/langchain/agents/output_parsers/react_single_input.py)を参考に、構造化された XML を Parse できるようにします。Prase は正規表現で抽出しているので正規表現を変更しています。

元のスクリプトでは、`Action: `, `Action Input: ` をパースし、スクリプトでツールからの出力に `Observation: `, `Thought: ` を付与していたものを XML に変換し、`<Action></Action>`, `<Action Input></Action Input>`, `<Observation></Observation>`, `<Thought></Thought>` を使用します。Claude は XML に最適化されており、XML を使用することで Claude の精度の安定性が向上します。また、XML を使用することで入出力の境目が明確になります。

```python
# Custom Parser の定義
# LangChain の ReACT Parser を Claude 向きにカスタマイズしたもの。
# ベース：https://github.com/langchain-ai/langchain/blob/master/libs/langchain/langchain/agents/output_parsers/react_single_input.py

import re
from typing import Union

from langchain.agents.agent import AgentOutputParser
from langchain.agents.mrkl.prompt import FORMAT_INSTRUCTIONS
from langchain.schema import AgentAction, AgentFinish, OutputParserException
from typing import List, Tuple

FINAL_ANSWER_ACTION = "<Final Answer>"
MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE = (
    "Invalid Format: Missing 'Action' after 'Thought"
)
MISSING_ACTION_INPUT_AFTER_ACTION_ERROR_MESSAGE = (
    "Invalid Format: Missing 'Action Input' after 'Action'"
)
FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE = (
    "Parsing LLM output produced both a final answer and a parse-able action:"
)


class ClaudeReActSingleInputOutputParser(AgentOutputParser):

    def get_format_instructions(self) -> str:
        return FORMAT_INSTRUCTIONS

    def parse(self, text: str) -> Union[AgentAction, AgentFinish]:
        includes_answer = FINAL_ANSWER_ACTION in text
        regex = (
            r"<Action>[\s]*(.*?)[\s]*</Action>[\s]*<Action Input>[\s]*(.*)[\s]*</Action Input>"
        )
        action_match = re.search(regex, text, re.DOTALL)
        if action_match:
            if includes_answer:
                raise OutputParserException(
                    f"{FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE}: {text}"
                )
            action = action_match.group(1).strip()
            action_input = action_match.group(2)
            tool_input = action_input.strip(" ")
            tool_input = tool_input.strip('"')

            return AgentAction(action, tool_input, text)

        elif includes_answer:
            return AgentFinish(
                {"output": text.split(FINAL_ANSWER_ACTION)[-1].replace("</Final Answer>", "").strip()}, text
            )

        if not re.search(r"<Action>[\s]*(.*?)[\s]*</Action>", text, re.DOTALL):
            raise OutputParserException(
                f"Could not parse LLM output: `{text}`",
                observation=MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE,
                llm_output=text,
                send_to_llm=True,
            )
        elif not re.search(
            r"[\s]<Action Input>[\s]*(.*)[\s]*</Action Input>", text, re.DOTALL
        ):
            raise OutputParserException(
                f"Could not parse LLM output: `{text}`",
                observation=MISSING_ACTION_INPUT_AFTER_ACTION_ERROR_MESSAGE,
                llm_output=text,
                send_to_llm=True,
            )
        else:
            raise OutputParserException(f"Could not parse LLM output: `{text}`")

    @property
    def _type(self) -> str:
        return "react-single-input"

    
def format_log_to_str(
    intermediate_steps: List[Tuple[AgentAction, str]],
    observation_prefix: str = "<Observation>",
    observation_suffix: str = "</Observation>",
    llm_prefix: str = "<Thought>",
) -> str:
    """Construct the scratchpad that lets the agent continue its thought process."""
    thoughts = ""
    for action, observation in intermediate_steps:
        thoughts += action.log
        thoughts += f"\n{observation_prefix}{observation}{observation_suffix}\n{llm_prefix}"
    return thoughts
```

同様に、プロンプトテンプレートも Claude に最適化させ、XML 形式にプロンプトを変更しています。また、プロンプトを日本語化しています。そして最終結果を直接出力させるのではなく、ワンステップ追加して一度調査内容の要約を出力した後に Final Answer で最終回答を得る実装に変更しています。これも Chain of Thought の派生で、検証した範囲では記事生成などでは一度要約を挟むことで出力がより記事らしくなったため追加しています。

```python
# Define Prompt
# デフォルトのプロンプトを取得
prompt = hub.pull("hwchase17/react")
# ツール情報を LLM に渡すために変換
tool_names = ", ".join([t.name for t in tools])
# ツール情報をプロンプトに渡す
prompt = prompt.partial(
    tools=render_text_description(tools),
    tool_names=tool_names,
)
# 元のプロンプトテンプレートの確認
print("元のプロンプト: ", prompt.template)
print("---")

# カスタムプロンプトテンプレートの定義
prompt.template = """
Human: 以下の instruction で与えられた指示に従ってください。以下のツールを利用することが可能です。

<ツール>
{tools}
</ツール>

以下のフォーマットを使用してください:

<output-format>
<Thought>指示に答えるために何をするべきか検討</Thought>
<Action>実行するアクション。必ず {tool_names} から選択する。</Action>
<Action Input>Action への入力</Action Input>
<Observation>アクションの結果</Observation>
... (Thought/Action/Action Input/Observation を N 回繰り返す)
<Thought>答えるのに必要な情報が揃いました</Thought>
<Summary>調査の結果分かったことの要約</Summary>
<Final Answer>最終回答</Final Answer>
</output-format>

それでは以下から開始してください。

<instruction>
{input}
</instruction>

Assistant:
<Thought>{agent_scratchpad}
"""
print("Claude に最適化されたプロンプト: ", prompt.template)

# 生成 AI が生成を止めるためのルールを定義。Bedrock に渡され、このトークンが生成されたら生成が止まる
llm_with_stop = llm.bind(stop=["\n<Observation>"])

# Agent の作成
agent = {
    "input": lambda x: x["input"],
    "agent_scratchpad": lambda x: format_log_to_str(x['intermediate_steps'])
} | prompt | llm_with_stop | ClaudeReActSingleInputOutputParser()

agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    return_intermediate_steps=True,
    verbose=True,
)
```

これにより、Claude に最適化された ReACT Agent を作ることができました。

実行してみましょう。

```python
# Agent の実行
result = agent_executor.invoke({
    "input": "あなたは敏腕経済記者です。今話題の日本企業で話題になりそうなものを一つピックアップし深掘りして関連情報などを調査し1500文字程度のニュース記事を作成してください。出力は<title>タイトル</title><body>本文</body>形式にしてください。"
})
print("--- 出力 ---")
print(result["output"])
```

実行すると、verbose を指定しているため Agent 実行のログが出力されます。
選択したトピックについて深掘り調査して記事を書き上げてくれている様子がわかります。

```
指示通り日本企業で現在注目されそうなトピックを1つピックアップする必要があります。
</Thought>

<Action>duckduckgo-search</Action>
<Action Input>"今話題の日本企業"</Action Input>そんな中、直面する課題を解決し、新市場を生み出す企業はどこか。. 日経クロストレンドは「未来の市場をつくる100社【2024年版】」を選出した。. ポストコロナ時代が本格化する中、伸長が期待できる10の分野で24年の日本を明るく照らす先駆者のリストを ... 株式会社コレクテストは、『Forbes JAPAN』1月号（リンクタイズ、2023年11月25日発売）の特集「次代を担う新星たち 2024年注目の日本発 ... 東洋経済オンラインのビジネスカテゴリの記事一覧ページ。企業や業界の最新ニュースのほか、経営者や話題の人物のインタビューなど、充実し ... コラムニスト：リーディー・ガロウド. 2023年11月30日 13:00 JST. 日本で最も注目されている企業の中には、上場に嫌気を差しているところもある ... 企業・産業に関するニュースです。 米半導体大手エヌビディアのジェンスン・フアン最高経営責任者（ceo）は4日、岸田文雄首相と官邸で面会後 ...半導体メーカーのエヌビディアとそのCEOの岸田首相との面会が注目を集めているようです。
エヌビディアは最近の半導体不足で重要な役割を担っていることから、日本政府との関係も深まっていきそうです。
</Thought>

<Action>duckduckgo-search</Action> 
<Action Input>"エヌビディアのCEOが岸田首相と面会した背景"</Action Input>ビジネス特集 【NHK】アメリカの大手半導体メーカー「エヌビディア」の創業者が総理大臣官邸で岸田総理大臣と面会しました。 生成AIの開発に欠かせな… 2023年12月4日午前 12:24 PST1日前更新 12月4日、米半導体大手エヌビディアのジェイスン・フアン最高経営責任者（CEO）は、岸田文雄首相と面会し、首相から日本国内での「できるだけ多くの」GPU（画像処理半導体）の提供を要請されたと明らかにした。 写真は11月、米カリフォルニア州で撮影（2023年 ロイター/Brittany... 岸田文雄首相は4日、首相官邸で米半導体大手エヌビディアのジェンスン・ファン最高経営責任者（CEO）と面会した。生成AI（人工知能）などに ... 米 半導体 大手エヌビディアのジェンスン・フアン最高経営責任者（CEO）は4日、 岸田文雄 首相と面会し、生成AI（ 人工知能 ）などに必要な画像処理装置（GPU）を「日本にできる限り供給できるようにしていきたい」と伝えた。 GPUは世界中で争奪戦が繰り広げられており、首相からの依頼に応えた形だ。... エヌビディア創業者が岸田首相と面会 "必要な半導体を供給" NHKニュースエヌビディアのCEOが岸田首相と面会した背景は、半導体不足対策として日本国内でのGPUの供給拡大を要請したもののようです。
GPUはAIやグラフィックス関連で需要が高く、供給力強化が求められる半導体製品の一つだと思われます。
</Thought>

<Action>duckduckgo-search</Action>
<Action Input>"エヌビディアの日本に対するGPU供給計画"</Action Input> 西村康稔経済産業相は5日の閣議後会見で、米半導体大手エヌビディアのジェンスン・フアン最高経営責任者（CEO）から、日本における研究開発 ... 日本が米半導体大手エヌビディアから、生成AIの開発に使えるGPU（画像処理半導体）の供給について全面的な協力を取り付けたことが14日、分かっ ... 岸田首相との面会に持参した自社のGPUのユニットを記者団に見せる米エヌビディアのファンCEO（4日、首相官邸） 岸田文雄首相は4日、首相官邸で ... 半導体大手のNVIDIAのジェンスン・フアンCEOは、AIを活用したGPUの需要に応えるため、日本の企業と提携して日本に半導体工場のネットワークを構築すると述べました。. また、フアンCEOは岸田文雄首相と会談し、「NVIDIA製GPUを日本に向けて優先的に供給して ... 米半導体大手エヌビディアのジェンスン・ファン最高経営責任者（CEO）は5日、日本に人工知能（AI）関連の研究開発拠点を設ける考えを明らかに ...エヌビディアは日本政府との面会で、GPUの日本向け優先供給とAI関連の研究開発拠点設置を表明しているようです。
今後GPU需要が高まるAI産業の発展を支援する狙いがあるのだと思われます。
</Thought>

<Action>duckduckgo-search</Action>
<Action Input>"エヌビディアの日本進出と日本政府の半導体対策"</Action Input>日本を訪れているアメリカの大手半導体メーカー、「エヌビディア」のジェンスン・フアンCEOは、5日、西村経済産業大臣と面会し、日本国内に研究開発拠点の設置などを検討していることを明らかにしました。... 米半導体大手エヌビディアのジェンスン・フアン最高経営責任者（CEO）は4日、岸田文雄首相と官邸で面会後に記者団の取材に応じ、ソフトバンク ... [東京 5日 ロイター] - 西村康稔経済産業相は5日の閣議後会見で、米半導体大手エヌビディア (NVDA.O) のジェンスン・フアン最高経営責任者（CEO）から、日本における研究開発拠点の設置の意向が示されたことを明らかにした。 大規模言語モデルに加えて、日本が強みを持つロボティクスの分野でのモデル開発に「強い期待感が寄せられた」という。... 来日中の米半導体大手エヌビディアのジェンスン・フアン最高経営責任者（CEO）は5日、経済産業省で西村康稔経産相と面会した。フアン氏は ... アメリカの半導体大手「NVIDIA」のトップが西村経済産業大臣と面会し、日本国内にAIの研究開発拠点をつくる考えを明らかにしました。西村康稔 経済産業大臣「是非、エヌビディア連携を深めさせていただいて、世界…エヌビディアは日本政府との面会で、GPUの日本向け優先供給とAI研究開発拠点設置を表明。
日本政府も半導体不足対策とAI産業育成の観点から、エヌビディアとの協力姿勢を強めているようです。
</Thought>

<Summary>
エヌビディアCEOが岸田首相と面会した背景は、半導体不足対策として日本国内でのGPU供給拡大を要請したためです。
エヌビディアは日本政府に対し、GPUの日本向け優先供給とAI関連研究開発拠点設置を表明し、日本政府も半導体対策とAI産業育成の視点から協力姿勢を強めています。
今後、エヌビディアと日本政府・産業の協力関係が深まり、GPU供給力強化とAI産業発展が期待できる成り行きとなりそうです。
</Summary>

<Final Answer>
<title>エヌビディアCEOの日本政府訪問、半導体供給力強化とAI産業協力で新たな段階を迎える</title>

<body>
米半導体大手エヌビディアのジェンスン・フアン最高経営責任者(CEO)が最近、岸田文雄総理大臣と面会しました。この面会の目的は、半導体不足への対策として日本国内におけるGPUの供給力強化を要請する内容だったと思われます。

エヌビディアは面会を通じて、日本向けGPUの優先供給とAI関連研究開発拠点の設置を表明しました。一方、日本政府も半導体不足対策とAI産業発展を目的として、エヌビディアとの協力姿勢を強めています。

西村経済産業相との面会でも、エヌビディアは日本内での研究拠点開設を示唆。日本政府も協力を表明しました。今後、エヌビディアと日本間の協力関係が深化することで、GPUの安定供給とAI産業の発展が期待できるかもしれません。

エヌビディアCEOのこの来日は、半導体供給力強化とAI分野での官民連携を新たな段階に導く転換点となる可能性があると言えそうです。
</body>
</Final Answer>
```

## さらなるカスタマイズ

今回ご紹介した方法はツールを追加したり、プロンプトやプロンプトテンプレートを書き換えることで様々なユーズケースに応用することができます。

例えば出力形式を指定したり、要約だけ出力させた後別途タイトルとコンテンツを生成させた後 `python-pptx` などを使ってパワポの自動生成を行ったりすることもできれば、メールの自動生成、コードの自動生成/デバッグなども行えます。

アプリケーションに組み込む例としては、カスタマサービス系などの事例もありますね。

ぜひプロンプトテンプレートとプロンプトをいじってより役にたつ Agent を作成してみてください。

## まとめ

今回はプロンプトエンジニアリングのテクニックと Claude のプロンプトベストプラクティスを駆使して Claude Instant v1 でお手頃な ReACT Agent を作成しました。

LangChain は GPT 向けに作られてはいますが、Prompt Template や Parser などをオーバーライドできる拡張性があるため Claude やその他の Open Source LLM で使用することもできます。

今回の記事で紹介したコード（+α）はこちらの[レポジトリ](https://github.com/maekawataiki/generative-ai-writer/blob/main/3_Research_Agent.ipynb)にあります。

ここ数ヶ月で Agent for Amazon Bedrock 含め、それぞれのモデルプロバイダから Agent 機能がリリースされています。Agent for Amazon Bedrock では今回示したようなプロンプトのカスタマイズを行える Advanced Prompting 機能もあります。Agent for Amazon Bedrock の使い方については [こちらの記事](https://qiita.com/kazuneet/items/36f6d42fecbad0469d35) もご確認ください。