🐷

【🦜️🔗 LangChain + Next.js Starter Template】ReActエージェントを読み解く

に公開

概要

LangGraphのアプリケーションを作成するとき、CLIツールを使えばアプリケーションを素早く立ち上げられます。
このアプリケーションには、NextjsまたはViteでセットアップされたボイラーテンプレートがあります。
(個人的にはフロントはVue、サーバーはpythonが一番やりやすいと思うのですが、世の中の案件としてはNextjsが圧倒的に多いです)
https://github.com/langchain-ai/create-agent-chat-app

言うて私が「俺俺フレームワーク」で作ったものよりも、どこかのエラいエンジニアが集合知で作ったもののほうが優れているでしょうし、属人的な書き方にこだわるよりFit to Standardを目指すべく、その設計思想を学ぶことにしました

今回はその中でReActエージェントのテンプレートの設計思想を読み解きます
https://github.com/langchain-ai/react-agent-js

全体像

  • LangGraph.jsを用いたReActエージェントのサンプル構成
  • 会話履歴や設定値を型安全に管理し、柔軟な拡張・運用が可能
  • モデル・プロンプト・ツールを動的に切り替えられる設計
  • テストやA/Bテスト、運用時のパラメータ調整が容易

各ファイル、関数の設計思想解説

1. configuration.ts

役割

  • エージェントの「設定値(Config)」の型・初期値・必須項目を一元管理するファイルです。
  • "型"はConfigurationSchemaに記述することで、Agentに渡すべきプロパティがわかります。
  • ensureConfigurationは、型で指定した各プロパティの初期値を決めています。ConfigurationSchemaと比較しながら初期値を設定できます。

ConfigurationSchema

export const ConfigurationSchema = Annotation.Root({
  systemPromptTemplate: Annotation<string>,
  model: Annotation<string>,
});
  • エージェントの設定値(例:モデル名やプロンプト)の「型・必須項目・初期値・説明」を宣言するものです。
  • これをStateGraph作成時に渡すことで、グラフ全体で「どんな設定値が使えるか」を型安全に管理できます。
  • 型定義にはtype/interfaceではなく、LangGraphのAnnotationを用いています。
    type/interfaceは開発時の補助であり、TypeScriptがJavaScriptにトランスパイルされる時に型情報は消えます。一方でLangGraphのAnnotationは実行時にも情報を保持します。
  • またAnnotationには、型定義だけでなく初期値や説明、バリデーションも一元管理でき、LangGraph Studio等のUI連携や自動ドキュメント生成にも活かされます。
  • この例では、systemPromptTemplateとmodelが設定されています。つまり、graphを設定・実行する時には最低限systemPromptTemplateとmodelを定義せよという意味です。必要に応じて、temperatureやmaxTokens、enableWebSearch、userIdなどといったプロパティを追加で設定すると良いでしょう。

ensureConfiguration

export function ensureConfiguration(config: RunnableConfig,): typeof ConfigurationSchema.State {
  const configurable = config.configurable ?? {};
  return {
    systemPromptTemplate:
      configurable.systemPromptTemplate ?? SYSTEM_PROMPT_TEMPLATE,
    model: configurable.model ?? "claude-3-7-sonnet-latest",
  };
}
  • 実行時に、設定値が足りない場合に初期値で自動補完するための関数です。
  • 外部から渡されたConfigが不完全でも、必ず全項目が揃った状態に整形してからエージェントに渡します。
  • これにより、ノードやモデル呼び出し時に「undefined」や「型不一致」で落ちるリスクを排除し、運用やテスト時のトラブルを防ぎます。

補足:ConfigurationSchemaとensureConfigurationの関係

  • 「ensureConfigurationで保証されるんなら、これだけでええやん。ConfigurationSchemaも書くのは二度手間ちゃうん?」と思ったので、もうちょっと深掘りしました。
  • 位置づけ:ConfigurationSchema=設計図(型・説明・期待値)、ensureConfiguration=実行時補完(初期値適用)。
  • 両者を併用することで「設計時の型安全」と「実行時の安全(未指定時の補完)」を両立できます。
  • まあそれでも二度手間感はあります。ゆくゆく統合されるかもしれません、ってchatgptがゆってた

2. graph.ts

役割

  • グラフを定義する、いわゆるLangGraphの本体です。
  • ここでは、「エージェント呼び出し」と「ツール実行」をノードとして定義し、状態や設定値を受け渡しながらAIの推論・行動を制御します。

callModel

async function callModel(
  state: typeof MessagesAnnotation.State,
  config: RunnableConfig,
): Promise<typeof MessagesAnnotation.Update> {

  const configuration = ensureConfiguration(config);

  const model = (await loadChatModel(configuration.model)).bindTools(TOOLS);

  const response = await model.invoke([
    // 冒頭にはシステムメッセージを入れる
    {
      role: "system",
      content: configuration.systemPromptTemplate.replace(
        "{system_time}",
        new Date().toISOString(),
      ),
    },
    ...state.messages,
  ]);
  // responseはAIの回答しか含まれないので、会話履歴を付け加える必要がある。
  // MessagesAnnotationなので、[response]と書くことで、「会話履歴 + AIレスポンス」の形にできる
  return { messages: [response] };
}
  • AIモデルを呼び出し、設定値や会話履歴をもとに応答を生成します。
  • モデル名は直接設定せずloadChatModelという独自関数を用いて、Configから取得するようにしています。これにより、configrationに文字列でモデル名を指定さえすれば、graph実行時に動的にモデルを設定することができるようになり、A/Bテストが容易に書けるメリットがあります。
  • 同様にプロンプトもConfigから動的に取得し、プロンプト変更に対応しやすい設計となっています。
  • 会話履歴やシステムプロンプトをAIモデルに渡すことで、文脈を維持した応答生成が可能です。

routeModelOutput

function routeModelOutput(state: typeof MessagesAnnotation.State): string {
  const messages = state.messages;
  const lastMessage = messages[messages.length - 1];

  if (((lastMessage as AIMessage)?.tool_calls?.length ?? 0) > 0) {
    return "tools";
  } else {
    return "__end__";
  }
}
  • conditionalEdgeを設定する際、次にどのノードへ遷移するか(ツール呼び出し or 終了)を返すための関数です。
  • ReActエージェントはレスポンスとして、「ツール呼び出し」か「AIメッセージ」を返します。
     (どちらか一方ではなく、一度のレスポンスで両方ともを配列形式で返すこともあります)
    ツール呼び出しを含む場合はツールノードへ、そうでなければ終了ノードへと遷移させます。
  • これにより、ReActパターンの「推論→行動→観察→再推論」のループを実現し、複雑な分岐やカスタムロジックも追加しやすいです。

workflow

const workflow = new StateGraph(MessagesAnnotation, ConfigurationSchema)
  .addNode("callModel", callModel)
  .addNode("tools", new ToolNode(TOOLS))
  .addEdge("__start__", "callModel")
  .addConditionalEdges(
    "callModel",
    routeModelOutput,
  )
  .addEdge("tools", "callModel");
  • グラフ全体の「状態(State)」と「設定値(Config)」の型・構造をLangGraphに伝え、型安全なワークフローを構築します。
  • すべてのノードで「どんな状態・設定値が使えるか」を明確にし、型安全・自動バリデーション・一元管理を実現します。
  • 状態や設定値の追加・変更が全体に即時反映され、拡張や保守が容易です。
  • LangGraphの強みである「宣言的・型安全なグラフ設計」の基盤となります。

graph

export const graph = workflow.compile({
  interruptBefore: [],
  interruptAfter: [],
});
  • interruptBefore/interruptAfterは、各ノードの実行前後で処理を一時停止し、状態の確認・編集や人手介入(human interrupt)を挟むための割り込みポイントです。

Discussion