Serverless Operations, inc

>_cd /blog/id_681rezoq6mf5

title

LangGraph (TypeScript) の基本的な使い方を可能な限り分かりやすくした参考実装を作ってみた

summary

昨年10月に LangChain 1.0 がリリースされ、AI エージェント開発に必要な機能および概念が整ってきています。それによってより複雑で高度な対応ができるようになったものが作れるのではないかという期待が持てると同時に、状態管理や複雑なワークフローの実装に悩まされます。LangGraph はこのような問題を解決する LangChain の拡張パッケージです。

昨年10月に LangChain 1.0 がリリースされ、AI エージェント開発に必要な機能および概念が整ってきています。それによってより複雑で高度な対応ができるようになったものが作れるのではないかという期待が持てると同時に、状態管理や複雑なワークフローの実装に悩まされます。LangGraph はこのような問題を解決する LangChain の拡張パッケージです。

状態管理・分岐・ループ・その他エージェントのオーケストレーションを視覚化できる形のワークフローとして実装できるという点で、LangGraph を利用することはとても良い選択になります。ただし、便利になっている分、高度に抽象化されているレイヤーが多く、エージェント開発に必要な新しい概念を理解する必要があるため、公式ドキュメントを読んでも初見でそれらの概念を理解することが難しいと感じられるかもしれません。

この記事では、そのようなことを考えている方を対象に、LangGraph の基本的な使い方について、公式ドキュメントの Quickstart ( https://docs.langchain.com/oss/javascript/langgraph/quickstart ) の内容を少しアレンジした上で詳細に解説いたします。 利用するモデルは Claude 4.5 Sonnect で、Amazon Bedrock 経由(Amazon Bedrock Converse API)で利用する形にしますが、モデルやモデルへのアクセス方法は適宜に切り替えていただいても問題ありません。

作りたいもの

サンプルの要件としては、以下のようになります。

① 2つの入力値(整数値)の四則演算が行える
② 計算結果をまとめて出力する

実際の業務では応用が必要なので、そのための感覚を捉えることにフォーカスしてください。

ワークフローグラフ

実装したワークフローは Mermaid 図として簡単に出力することができます。今回は、上記要件を満たすために以下のようなワークフローを作ります。 SELECT_OPERATION で入力内容に応じて CALCULATE に進み、結果を REPORT でまとめる流れです。

ディレクトリ構成

以下のようなディレクトリ構成を想定しています。

langgraph-quickstart-sample/
├── src/
│   │
│   ├── actions/
│   │   └── CalculatorAgentAction.ts  # ユーザー入力を受けるアプリケーションのエントリーポイント
│   │
│   ├── libs/
│   │   └── LangChain.ts              # LangChain/LangGraph のユーティリティ関数
│   │
│   ├── services/
│   │   └── CalculatorAgentService.ts # ワークフロー本体とエージェントのコアロジック実装
│   │
│   └── tools/
│       └── CalculatorAgentTools.ts   # 四則演算を行うためのツール定義
│
├── package.json
└── tsconfig.json

実装手順

以下の流れで実装を進めていきます。

必要なモジュールおよびパッケージ構成(package.json)

本記事のサンプルは Node.js を使った TypeScript を利用するため、必要最低限の内容で package.json を作成し、 npm install を実行しておきます。

{
  "name": "calculator-agent-sample",
  "version": "0.0.0",
  "scripts": {
    "tsx": "tsx"
  },
  "dependencies": {
    "@langchain/aws": "^1.1.27",
    "langchain": "^1.2.3",
    "zod": "^4.3.5"
  },
  "overrides": {
    "@langchain/core": "^1.1.17"
  },
  "devDependencies": {
    "@types/node": "^22.9.0",
    "tsx": "^4.7.0",
    "typescript": "^5.4.5"
  }
}

パッケージ構成の特記事項として、以下の内容も併せてご参考ください。

  • langchian
    • langchain パッケージで、 @langchain/core など下層パッケージを含む
  • @langchain/core
    • 統合モジュール( @langchain/xxx )を利用する場合、すべての統合モジュールで @langchain/core のバージョンを合わせなければならない
    • package.jsonresolutions または overrides フィールドを追加して解決

本サンプルで統合モジュールのユースケースはないため overrides は未定義でも良いですが、今後拡張していくとなると必要になってきますので予め一緒に入れておくと良いです。

LangChain ユーティリティモジュールの作成

インストールしておいた LangChain のモジュールを直接読み込んでも進められますが、特定のファイルにロジックが集中しすぎず適宜分散できるように、ラッパー関数を作成し利用するようにします。

import { initChatModel, AIMessage } from 'langchain'
import { MessagesAnnotation, Annotation } from '@langchain/langgraph'

// モデルのオブジェクトを取得します
export const getBedrockChatModel = async (params: { modelName: string }) => {
  // Amazon Bedrock 経由(Amazon Bedrock Converse API)でモデルを利用する方法の詳細はこちらのドキュメントをご参考ください
  // https://docs.langchain.com/oss/javascript/langchain/models#bedrock-converse
  const model = await initChatModel('bedrock:' + params.modelName, {
    timeout     : 60,
    temperature : 0.7,
    max_tokens  : 4096
  })
  return model
}

// 状態(state)を定義し、状態のオブジェクトを取得します
export const MessagesState = Annotation.Root({
  ...MessagesAnnotation.spec,
  llmCalls: Annotation<number>({
    reducer: (x, y) => x + y,
    default: () => 0,
  })
})
// 状態(state)の型を取得します
export type MessagesStateType = typeof MessagesState.State

// 最新(直前)の状態メッセージを取得
// ワークフローの途中で state の中身を確認し、分岐やループなどをコントロールする際によく利用します
export const getLastMessage = (state: MessagesStateType) => state.messages.at(-1)

// AI メッセージかどうかを判定
// getLastMessage(state) とペアでよく使います
// ワークフローの途中で state の中身を確認し、分岐やループなどをコントロールする際に使われます
export const isAiMessage = (lastMessage: ReturnType<typeof getLastMessage>) => {
  return AIMessage.isInstance(lastMessage)
}

// 最新(直前)の状態メッセージが tool_calls (ツールの使用であったかどうか)を判定
// ワークフローの途中で state の中身を確認し、分岐やループなどをコントロールする際に使われます
export const isNextToolCall = (params: {state: MessagesStateType, toolNames: string[] }) => {
  const lastMessage = getLastMessage(params.state)
  if (!lastMessage || !isAiMessage(lastMessage)) {
    return false
  }
  if (!lastMessage.tool_calls || lastMessage.tool_calls.length < 1) {
    return false
  }
  return lastMessage.tool_calls?.filter(toolCall => params.toolNames.includes(toolCall.name)).length > 0
}

ワークフローの中で利用されるツールを実装

四則演算を行うツールを定義していきます。ツールの名前・説明・スキーマに関しては toolMeta という変数で定義し、ツールのロジックとなる部分は toolLogic で分離しておくと構造が把握しやすくなります。ワークフローな中ではtoolMeta で定義した内容を基に LLM 判断でどのツールが使われるかが決まる仕組みになります。

import { tool, DynamicStructuredTool} from 'langchain'
import * as z from 'zod'

export const getAddTool = () => {
  const toolMeta = {
    name: 'add',
    description: '2つの数値を加算する',
    schema: z.object({
      a: z.number().describe('最初の数値'),
      b: z.number().describe('2番目の数値'),
    })
  }

  const toolLogic = (params: z.infer<typeof toolMeta.schema>) => {
    return params.a + params.b
  }

  return tool(toolLogic, toolMeta)
}

export const getSubtractTool = () => {
  const toolMeta = {
    name: 'subtract',
    description: '2つの数値を減算する',
    schema: z.object({
      a: z.number().describe('最初の数値'),
      b: z.number().describe('2番目の数値'),
    })
  }

  const toolLogic = (params: z.infer<typeof toolMeta.schema>) => {
    return params.a - params.b
  }

  return tool(toolLogic, toolMeta)
}

export const getMultiplyTool = () => {
  const toolMeta = {
    name: 'multiply',
    description: '2つの数値を乗算する',
    schema: z.object({
      a: z.number().describe('最初の数値'),
      b: z.number().describe('2番目の数値'),
    })
  }

  const toolLogic = (params: z.infer<typeof toolMeta.schema>) => {
    return params.a * params.b
  }

  return tool(toolLogic, toolMeta)
}

export const getDivideTool = () => {
  const toolMeta = {
    name: 'divide',
    description: '2つの数値を除算する',
    schema: z.object({
      a: z.number().describe('最初の数値'),
      b: z.number().describe('2番目の数値'),
    })
  }

  const toolLogic = (params: z.infer<typeof toolMeta.schema>) => {
    return params.a / params.b
  }

  return tool(toolLogic, toolMeta)
}

export const ALL_TOOLS = [
  getAddTool(),
  getSubtractTool(),
  getMultiplyTool(),
  getDivideTool()
] as DynamicStructuredTool[]

コアロジックとなるワークフロー本体を実装する

エージェントのコアとなるロジックとワークフローを実装していきます。State を定義して状態を管理できるようにし、StateGraph でワークフローを定義します。StateGraph で理解しておくポイントを簡単に表現すると以下になります。

  • State:状態・履歴
  • Node:作業の実行を担当
  • Edge:次の作業を示す

Node は同期処理・非同期処理のノードが作成でき、Edge は固定経路( addEdge )・条件によて他の Node に向けさせる( addConditionalEdges )といったことが可能です。Node の実行が完了するとリターンされた内容が State に登録され、次の Node または条件付き Edge に渡されるので、その内容を把握することで実装が進めあれます。また、Checkpoint と呼ばれるオプションに ID を渡せば、一度中断または過去の履歴を基にワークフローを再開または複数回実行する仕組みが実装できます。

本記事では、分かりやすく用件が満たせる最小限のワークフローを作ってみます。

import * as CalculatorAgentTools from '../tools/CalculatorAgentTools'
import * as LangChain from '../libs/LangChain'

import { HumanMessage, SystemMessage, DynamicStructuredTool, ToolMessage } from 'langchain'
import { StateGraph, START, END } from '@langchain/langgraph'

export class CalculatorAgentService {

  public async run(params: { humanMessage: string }) {
    // まずは state 空間を確保
    // Model や Tool の呼び出し結果、および、次に何を呼べばよいかを判定するための情報が格納される
    const messageState = LangChain.MessagesState

    const MODEL = {
      // モデルを利用する作業を「Model Node」として定義
      // ワークフローの中へ組み込むため、モデル呼び出しを Node 型として定義して抽象化する
      // state に入っている情報をコンテキストとして、計算タスクを遂行するためのプロンプトを作成
      // プロンプトは過剰に細かくする必要はなく、手順と利用するツールをシンプルな形でガイドラインを与えておく
      SELECT_OPERATION: {
        name: 'SELECT_OPERATION',
        node: this.selectOperationModelNode({
          systemPrompt: `
            ユーザー入力からちょうど2つの数値を抽出し、
            add, subtract, multiply, devide のツールを使って計算を実行。

            注意:
            説明文を出力してください。`
        })
      },
      REPORT: {
        name: 'REPORT',
        node: this.reportModelNode({
          systemPrompt: `
            計算結果を、以下のフォーマットで整理して出力
            
            <example>
            ユーザー入力①:{最初の数字}
            ユーザー入力②:{2番目の数字}
            利用したツール:{ツール名}
            結果の値:{結果の値}
            </example>

            注意:
            説明文を出力してください。`
        })
      }
    }

    const TOOL = {
      // 利用するツールを「Tool Node」として定義
      // ワークフローの中へ組み込むため、ツールの呼び出しを Node 型として定義して抽象化する
      // モデルが実行された後、タスクを遂行するためにはツールの利用が必要と判断する場合、State には「tool_call」という配列に呼び出しが必要なツール情報が詰まれる
      // その tool_call を実行(例えば、詳細情報の検索やDBから値を読み込むなど)して State に積むことでコンテキストに反映される
      CALCULATE: {
        name: 'CALCULATE',
        node: this.calculateToolNode()
      },
    }

    const agent = new StateGraph(messageState)

    // nodes: 必要な Node を定義しておく
      .addNode(MODEL.SELECT_OPERATION.name, MODEL.SELECT_OPERATION.node)
      .addNode(TOOL.CALCULATE.name,         TOOL.CALCULATE.node)
      .addNode(MODEL.REPORT.name,           MODEL.REPORT.node)

    // edges: Node のワークフローを定義する
      .addEdge(START, MODEL.SELECT_OPERATION.name)
      // conditionalEdges: 最新のステート(lastMessage)をみて、次にどのノードに進めば良いかのカスタムロジックを記載できる
      .addConditionalEdges(MODEL.SELECT_OPERATION.name, (state: LangChain.MessagesStateType) => {
        // Model Node で、タスク遂行のためにはツールの利用が必要だと判断すると、
        // Model Node が呼ばれた後の State に tool_call で該当ツールの情報が指定される
        // まずは次の作業が「ツール呼び出しか否か」を判定し、ツール呼び出しの場合はそちらへルーティングする、
        if (LangChain.isNextToolCall({ state, toolNames: ['add', 'subtract', 'multiply', 'divide'] })) {
          return TOOL.CALCULATE.name
        }
        // ツールを呼び出す必要がないと判断された婆愛(直近の State に tool_call が入っていない場合)
        // レポート作成にルーティングする
        return MODEL.REPORT.name
      }, {
        // 第2引数のカスタムロジックで return した文字列と実際のツールの名前をマッピング
        [TOOL.CALCULATE.name] : TOOL.CALCULATE.name,
        [MODEL.REPORT.name]   : MODEL.REPORT.name
      })
      .addEdge(TOOL.CALCULATE.name, MODEL.REPORT.name)
      .addEdge(MODEL.REPORT.name,   END)
    // compile: Graph 定義完了
      .compile()

    const graph = await agent.getGraphAsync()
    console.log('## ワークフロー図', graph.drawMermaid()) // mermaid 記法でワークフロー図が出力される

    // この結果(result) が最終 State になります
    const result = await agent.invoke(
      {
        messages: [ new HumanMessage(params.humanMessage) ]
      },
      {
        recursionLimit: 5 // 最大ループ回数を指定(デフォルトは25)
      }
    )
    console.log('## 全ての state messages', result.messages)
  }

  private selectOperationModelNode(params: { systemPrompt: string }) {
    const { systemPrompt } = params

    // Model/Tool Node は State を受ける関数で定義する必要がある
    const modelNode = async (state: LangChain.MessagesStateType) => {
      const model = await LangChain.getBedrockChatModel({
        modelName: 'jp.anthropic.claude-sonnet-4-5-20250929-v1:0'
      })

      //  ツールの説明や各種情報を読み込ませるためバインドしておく
      const modelWithTools = model.bindTools([
        ...CalculatorAgentTools.ALL_TOOLS
      ])

      // Model Node は指定されたシステムプロンプトでモデルを呼び出す
      // State 情報を一緒に送ってコンんテキストを維持する
      const result = await modelWithTools.invoke([
        new SystemMessage(systemPrompt),
        ...state.messages,
      ])

      // 呼び出し結果を messages に積んでおけば、State に積まれる(appendされる)
      return {
        llmCalls: 1,
        messages: [ result ],
        ...state.messages
      }
    }

    return modelNode
  }

  private calculateToolNode() {
    // Model/Tool Node は State を受ける関数で定義する必要がある
    // ツールはモデル呼び出しではなく関数実行のため別途システムプロンプトを渡さない
    // ツールの実行結果は State に詰まれ、Model Node にて Context として活用される
    const toolNode = async (state: LangChain.MessagesStateType) => {

      const lastMessage = LangChain.getLastMessage(state)
      if (!LangChain.isAiMessage(lastMessage)) {
        return { messages: [] }
      }

      const tools = [ ...CalculatorAgentTools.ALL_TOOLS ] as DynamicStructuredTool[]
      const toolMap = new Map(tools.map(t => [t.name, t]))

      // 順番にツールを実行
      const result: ToolMessage[] = []
      for (const toolCall of lastMessage.tool_calls ?? []) {
        const tool = toolMap.get(toolCall.name)
        if (tool) {
          const observation = await tool.invoke(toolCall)
          result.push(observation)
        }
      }

      // ツール実行結果が State.messages に反映される
      return { messages: result }
    }

    return toolNode
  }

  private reportModelNode(params: { systemPrompt: string }) {
    const { systemPrompt } = params

    // Model/Tool Node は State を受ける関数で定義する必要がある
    const modelNode = async (state: LangChain.MessagesStateType) => {
      const model = await LangChain.getBedrockChatModel({
        modelName: 'jp.anthropic.claude-sonnet-4-5-20250929-v1:0'
      })

      // Amazon Bedrock Converse API では
      // メッセージ履歴にツール実行結果(ToolMessageに toolUse や toolResult など)が含まれている場合、toolConfig が必須のため、再度モデルを定義
      const modelWithTools = model.bindTools([
        ...CalculatorAgentTools.ALL_TOOLS
      ])

      // Model Node は指定されたシステムプロンプトでモデルを呼び出す
      // State 情報を一緒に送ってコンテキストを維持する
      const result = await modelWithTools.invoke([
        new SystemMessage(systemPrompt),
        ...state.messages,
      ])

      // 呼び出し結果を messages に積んでおけば、State に積まれる(appendされる)
      return {
        llmCalls: 1,
        messages: [ result ],
        ...state.messages
      }
    }

    return modelNode
  }
}

ユーザー入力を受け、ワークフローを実行する部分の実装

actions レイヤーを実装します。サンプルなのでシンプルな形ですが、実際にはチャットでのユーザー入力を想定します。

import { CalculatorAgentService } from '../services/CalculatorAgentService'

(async () => {

  const service = new CalculatorAgentService()
  await service.run({ humanMessage: '100 足す 200 は?' })

})()

実行

以下のコマンドで実行します。Amazon Bedrock を利用するので、環境変数 AWS_DEFAULT_REGION および AWS 認証情報がセット済みの状態の想定です。

npm run tsx -- src/actions/CalculatorAgentAction.ts

実行結果の確認

console.log で2つのログを出しているため、それぞれ確認します。

  • StateGraph の mermaid 図(ワークフロー図):preview ツールなどでワークフローが確認できる
  • ワークフロー実行後の State の内容

最終結果として、State の内容を見ると以下のようになっています(※重要ではない項目をかなり省略しています)。この中から適宜必要な情報を取得することで結果や途中過程を確認することが可能です。

[
  // ユーザー入力
  HumanMessage {
    "content": "100 足す 200 は?"
  },

  // SELECT_OPERATION モデルノードの実行結果
  AIMessage {
    "content": [
      {
        "type": "text",
        "text": "100 足す 200 の計算を実行します。"
      }
    ],
    "tool_calls": [
      {
        "name": "add",
        "args": { "a": 100, "b": 200 },
      }
    ]
  },

  // CALCULATE ツールノードの実行結果
  ToolMessage {
    "content": "300",
    "name": "add"
  },

  // REPORT モデルノードの実行結果
  AIMessage {
    "content": "ユーザー入力①:100\nユーザー入力②:200\n利用したツール:add(加算)\n結果の値:300\n\n100に200を足すと、300になります。",
    "tool_calls": [],
  }

おわりに

いかがだったでしょうか。公式ドキュメントだけでは掴みにくかった部分について詳細にフォローアップした形で書いてみました。この記事が LangGraph に入門する方々のお役に立てると幸いです。

Written by
COO

金 仙優

Sonu Kim

  • Facebook->
  • X->
  • GitHub->

Share

Facebook->X->
Back
to list
<-