$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

コーディングエージェントの挙動を理解するためReActの動きを追いかける

前回の自分の知りたい領域をDeep Researchでハンズオン資料を作って学ぶの記事でDeep ResearchでReasoningを理解するためのハンズオン資料を作った。その実践の中でReActの動きを深掘りしたら、ClineやCursor Agentのようなコーディングエージェントの動きの基本を少し理解できたように感じた。

そこで今回はReActの動きを深掘りした内容をまとめておく。もし間違ったことを書いていたら指摘してほしい。

AIエージェントにおけるReActとは

ReAct Prompting | Prompt Engineering Guideに詳しい情報が載っている。

簡単に言うと、「推論(Reasoning)」と「行動(Acting)」を組み合わせて、AIがより複雑なタスクを遂行できるようにするためのフレームワークである。最初のステップから以下の3ステップをどんどん繰り返していき、最終的に答えを導き出すようになっている。

  • 推論 (Reasoning): 観察した内容に基づき「次に何をすべきか」をモデルが考え、解決策を計画する
  • 行動 (Action): 推論の結果、必要ならモデルが外部のツールやデータにアクセスする行動を起こす(たとえば電卓を使う、ウェブ検索をするなど)
  • 結果の観察: 行動によって得られた結果を観察し、それを踏まえて次の推論に活かす

CoTプロンプティングをさらに発展させて、段階的な推論や外部ツールとの連携をできるようにしたものと言えそう。

挙動を詳しく追いかける

この定義だけだと実際に何をしているか分からないので、LangChainを用いた簡単なReActエージェントを作り、さらにdebugログを有効にして挙動を詳しく追いかけた。https://github.com/shibayu36/playground/blob/main/ai-reasoning-study/AI_Reasoning_Study.ipynb の「実験2: LangChainを用いた簡単なReActエージェント」の部分で試している。

まずgpt-4o-miniを使い、電卓ツールであるllm-mathをロードしたものを用意する。さらに詳細な実行ログを得るために、langchain.debug = True を設定する。

from langchain_openai import ChatOpenAI
from langchain.agents import load_tools, initialize_agent, AgentType
import langchain

# デバッグログを有効にする
langchain.debug = True

# OpenAIのLLM(GPT-4o-mini)を用意
llm = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
# 電卓ツールをロード
tools = load_tools(["llm-math"], llm=llm)
# ReAct (Zero-shot) エージェントを初期化
agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)

そしてagent.runで実行を行う。質問文は「ある店舗が製品を100ドルで販売しています。20%割引した後10%値上げしました。最終価格はいくら?」となっていて、これを解くには電卓ツールを使うのが楽という状況になっている。

question = "ある店舗が製品を100ドルで販売しています。20%割引した後10%値上げしました。最終価格はいくら?"
response = agent.run(question)
print("最終的なエージェントからの回答:", response)

ではこのdebugログを見ながら、ReActの具体的な挙動を詳しく追いかけていこう。

初期プロンプト

初期プロンプトには以下のとおり。

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

Calculator(*args: Any, callbacks: Union[list[langchain_core.callbacks.base.BaseCallbackHandler], langchain_core.callbacks.base.BaseCallbackManager, NoneType] = None, tags: Optional[list[str]] = None, metadata: Optional[dict[str, Any]] = None, **kwargs: Any) -> Any - Useful for when you need to answer questions about math.

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 [Calculator]
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: ある店舗が製品を100ドルで販売しています。20%割引した後10%値上げしました。最終価格はいくら?
Thought:

これを見ると次のようにプロンプトで命令を出している。

  • toolの一覧と使い方を初期プロンプトで提供する
  • Question, Thought, Action, Action Input, Observationの出力のフォーマットを指定する
  • Thought/Action/Action Input/Observationを何回もループしてFinal Answerまで辿り着いたら終了するという計画を提供する
  • そして最初のQuestion:とThought:を始める

またそれぞれのラベルごとの役割を簡単にまとめると

  • Question: 質問文
  • Thought: 次に何をすべきかを考えてLLMに出力させるためのラベル
  • Action: 実行するアクションを指定するラベル。今回だったらCalculatorを使うときに使う
  • Action Input: アクションに渡す入力。Calculatorを使うなら計算式を渡す
  • Observation: アクションの実行結果
  • Final Answer: 最終的な回答。この出力があったら実行が完了したとみなす

初回のCalculator Actionを実行しろという命令を受け取る

初期プロンプトから開始したとき、まず最初のLLMからのレスポンスはこのようになっていた。

最初の価格は100ドルです。まず、20%の割引を計算し、その後10%の値上げを計算する必要があります。

Action: Calculator
Action Input: 100 * 0.2

つまりLLMからどのActionをどの引数で実行するかを受け取って、ここで出力が止まっている。

なぜここで出力が止まっているかというと、LLMにプロンプトを渡すときにstop wordsとしてObservationを指定しているためだ。最初に指定したフォーマットではAction Inputの後はObservation(=Action実行結果を出力)するため、LLMがObservationを出力した瞬間に停止し、stop wordsの前のAction Inputまででレスポンスを返している。

Entering Chain run with input:
{
  "input": "ある店舗が製品を100ドルで販売しています。20%割引した後10%値上げしました。最終価格はいくら?",
  "agent_scratchpad": "",
  "stop": [
    "\nObservation:",
    "\n\tObservation:"
  ]
}

Calculator実行のための引数をさらに詳細に得る

LLMからCalculatorを使えという指令が返ってきたが、続いてクライアント側がPythonのnumexpr libraryにそのまま与えられる計算式を得るために、次のプロンプトを追加発行してLLMにリクエストしている。

このプロンプトをどこで定義しているかというと、llm_math側の https://github.com/langchain-ai/langchain/blob/1ebcbf1d11578cb55db26013c745dc1e5722966e/libs/langchain/langchain/chains/llm_math/prompt.py#L4-L44 の部分。CalculatorのActionを検知すると、このプロンプトを使ってLLMにリクエストを行い、Actionを実行できる引数をさらに得ようとしている。FewShotを活用してフォーマットを固定している。

その後のLLMからのレスポンスはこちら。

レスポンスではnumexprの実行コードが返ってきている。先ほどのFewShotのフォーマットにしたがっているが、outputという部分が出てこないのは、こちらでもstopを活用しているから。

{
  "question": "100 * 0.2",
  "stop": [
    "```output"
  ]
}

Calculatorの実行

この後すぐに計算が実行されてAnswer 20.0というのが返っている。これはllm_math側で```textという出力がきたら、その結果をnumexpr.evaluateに通すという実装になっているからっぽい。

https://github.com/langchain-ai/langchain/blob/703fce7972084a325ddd480fa7c261452d57f717/libs/langchain/langchain/chains/llm_math/base.py#L228-L233

これによりクライアント側でCalculatorが実行されて、次のプロンプトに活用される。

次のThoughtへ

ここまででThoughの1loopが終わったので、これまでの情報をすべて入れてLLMへもう一度リクエストを投げる。

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

(ここに最初のプロンプトが全て入っている)

Question: ある店舗が製品を100ドルで販売しています。20%割引した後10%値上げしました。最終価格はいくら?
Thought:最初の価格は100ドルです。まず、20%の割引を計算し、その後10%の値上げを計算する必要があります。

Action: Calculator
Action Input: 100 * 0.2
Observation: Answer: 20.0
Thought:

先ほどのCalculatorの結果がObservationの部分にAnswer: 20.0として含まれ、もう一度Thoughtフェーズに入ってという命令を送っている。

その後のLLMからのレスポンスはこちら。

20ドルの割引が適用されるので、最初の価格から20ドルを引きます。

Action: Calculator
Action Input: 100 - 20

またCalculatorを使えという指令がやってくる。この結果、「Calculator実行のための引数をさらに詳細に得る」のフェーズに戻ってループする。

ループを繰り返し、Final AnswerをLLMがレスポンスするまで続ける

あとはこのThought -> Action -> ObservationのループをFinal Answerが出るまで繰り返す。

最終的にこのようなプロンプトでリクエストされ

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

(ここに最初のプロンプトが全て入っている)

Question: ある店舗が製品を100ドルで販売しています。20%割引した後10%値上げしました。最終価格はいくら?
Thought:最初の価格は100ドルです。まず、20%の割引を計算し、その後10%の値上げを計算する必要があります。

Action: Calculator
Action Input: 100 * 0.2
Observation: Answer: 20.0
Thought:20ドルの割引が適用されるので、最初の価格から20ドルを引きます。

Action: Calculator
Action Input: 100 - 20
Observation: Answer: 80
Thought:割引後の価格は80ドルです。次に、この価格に10%の値上げを計算します。

Action: Calculator
Action Input: 80 * 0.1
Observation: Answer: 8.0
Thought:10%の値上げは8ドルです。次に、割引後の価格にこの値上げを加えます。

Action: Calculator
Action Input: 80 + 8
Observation: Answer: 88
Thought:

LLMがFinal Answerを出力し

最終価格は88ドルです。
Final Answer: 88ドル

これで結果が確定して終わる。

挙動の図

最後に調べた挙動の図を簡単にまとめる。

[AI Client]                    [LLM]                    
     |                          |                        
     |--初期プロンプト送信-------->|                        
     |  (Question + format)     |                      
     |                          |                        
     |<--Thought + Action指令----|                        
     |  (Calculator, "100*0.2") |                        
     |                          |                        
     |--詳細計算式要求----------->|                        
     |  (llm_math prompt)       |                        
     |                          |                        
     |<--numexpr形式コード--------|                        
     |  ("100 * 0.2")           |                        
     |                          |                        
     |--llm_math実行             |                        
     | ↓                        |                        
     | |numexpr.evaluate        |                        
     | |("100 * 0.2")           |                        
     | ↓                        |                        
     |<-結果: "Answer: 20.0"     |                        
     |                          |                        
     |--全履歴+Observation-----> |                        
     |  送信(次のThought要求)     |                        
     |                          |                        
     |<--次のThought+Action----- |                        
     |  (Calculator, "100-20")  |                        
     |                          |                        
     |        ↓ ループ継続 ↓      |                        
     |                          |                        
     |--全履歴+最終Observation--->|                        
     |                          |                        
     |<--Final Answer-----------|                        
     |  ("88ドル")               |                        
     |                          |                        
    終了                         |                        

まとめ

ReActはこのように初期プロンプトで「推論」と「行動」を繰り返す方法が示され、それにしたがってLLMにリクエストを投げ、Action実行指令がレスポンスされたときはクライアントで適切にActionを実行してさらにLLMへリクエストを投げるということを繰り返していることが分かった。

この流れは最近のAIエージェントによる実行とMCP serverへのtool callの流れと非常に似ている。おそらくClaude Desktop, Cline, Cursor Agentでは初期プロンプトで同じようなやり方を指定して実現しているのだろう。

またこれを見るとThoughtのループに入るたびにこれまでのコンテキスト全てを渡すようになっている。そのため非常に多くのトークンを消費しそう。この辺りの話がPrompt Cachingの話に繋がってくるのかなと思った。また調べてみたい。

自分の知りたい領域をDeep Researchでハンズオン資料を作って学ぶ

CursorやClineなどで生成AIを使ってコーディングをしていると、Reasoningモデルとか、Thinkingモードとか、そういったワードに出会うことが多い。これらが意味することについて理解できてないと感じていた。

何かを学ぶとき、資料を読むだけではなく手を動かしてみると理解が早くなる。そこでChatGPTのDeep Researchでハンズオン資料を作り、それを元に学ぶと効率が良いのではないかと考えた。今回の記事ではハンズオンの作成と実施の過程をまとめておく。

ハンズオン資料をDeep Researchで作る

次のように質問してDeep Researchでハンズオン資料を作る。

すると https://chatgpt.com/share/6819d686-5ae0-8010-8196-d532528dc82a のような資料が作られた。

  • 序文
  • 実験の章の一部

見出しはこういう感じ。

  • はじめに
  • LLMの思考・推論に関する重要なコアコンセプト
    • チェーン・オブ・ソート(Chain-of-Thought)プロンプティング
    • ReAct(推論+行動)フレームワーク
    • ツールの活用による拡張 (LLM Tool Use)
    • メタ認知プロンプティング(自己認識・自己評価プロンプト)
  • ハンズオン:推論パターンの実践(Google Colab)
    • 準備: Colab環境セットアップ (目安: 0.5時間)
    • 実験1: チェーン・オブ・ソートで数学問題を解く (目安: 2時間)
    • 実験2: LangChainを用いた簡単なReActエージェント (目安: 4時間)
    • 実験3: モデルの自己反省プロンプトを試す (目安: 1.5時間)
  • おわりに:学習計画のまとめと今後の展望

Google Colabで実践する

資料ができたので、コアコンセプトの資料を読んだ後、ハンズオンに取り組んだ。

実施すると、ハンズオン資料のコードのままだとうまく動かなかったり、実際に理解したいことと微妙に外れていたりした。今回学びたい領域が新しい領域なため、Deep Researchでも細かい部分はうまくいかなかったのかなと思う。

そこで資料自体を批判的に捉え、違和感があった時やうまく動かない時は自分で調査をし、学びたいことを適切に行うための実装に変更しながら進めていった。このやり方はむしろ理解を深められて良かった。

またハンズオン資料をそのままやるだけでなく、あえて脱線して気になったことを深掘りすることも大切だった。僕の場合ReActフレームワークが具体的にどういう動きをするかが非常に気になったため、debugログを出して1つずつ動きを確認していった。結構時間がかかったが、今のコーディングエージェントがどう動いているかの基本部分を理解できた気がする。

最終的にできたものは https://github.com/shibayu36/playground/blob/main/ai-reasoning-study/AI_Reasoning_Study.ipynb

学んだこと

  • LLMにおけるReasoningが何かというのは難しいが、LLMの推論能力や推論させる(論理的に考えさせる)ための方法的な意味で使われている
  • 論理的に考えさせるためには、ステップを区切って一つずつ思考させるためのCoT、思考とアクションを繰り返すReAct、出力に対し自己反省させて正しいものに修正する自己反省プロンプトなどいろんな手法がある
  • ReActについては、今のコーディングエージェントのtool callの仕組みの理解に繋がった。また改めて記事にしたい

まとめ

今回はLLMのReasoningという自分の知りたい領域について、Deep Researchでハンズオン資料を作って学ぶという方法を試してみた。入門するにはなかなか良い資料ができたし、ハンズオン資料を使いながらその間違いを修正していくくらいの気持ちでやっていくと理解が深まった。

ReAct周りを深掘りして追いかけたことでコーディングエージェントがやっていることの一部を理解できた。これについては別で記事を書こうと思う。

AIによりコードレビューや対話による新しい発見のような学習が増えた

ソフトウェアエンジニアリングの学び方には、書籍を読む、サンプルコードを写経するなど色々とある。その中で、同僚とのコードレビューや対話による新しい発見も1つの学習方法として非常に役立っていた。他人との対話による学習は、他の学習と違って自分が想像していなかった視点をもらえたり、固定観念に気付かされたりと、とても有益だった。ただしこれまではこの学習は他人がいないと成立しないものだった。

ChatGPTのようなAIの登場で、このような対話による新しい発見を得られる機会が非常に増えた。ChatGPTなどと会話を通して調査・設計していると、なるほどそういう解決策があるのかと驚かされる瞬間が頻繁に訪れる。

とはいえ、新たな気づきを得るためには、AIの使い方にも工夫が必要だ。たとえば細部まで自分で考えた後で細かい部分に対して質問してしまうと、自分の視点の回答だけを引き出す結果になってしまうことが多い。そうならないように僕は次のような工夫をしている。

  • いきなりHowを聞くのではなく、その要件について考える時に考慮すべき観点が何かを先に聞き、自分が想定していなかった観点に気づけるようにする
  • 解決策について複数案考えてもらい、それぞれのメリット・デメリットも合わせて聞く

このようにAIが進歩したことで対話による新たな発見が加速したのでありがたい。一方で使い方によっては自分の視点だけを強化することになるので気をつけたいと感じた。