Agentic AI アプリケーションアーキテクチャ

15 minutes  

アプリケーション概要

このワークショップでは、旅行予約のための Agentic AI アプリケーションを使用します。 OpenTelemetry でアプリケーションを計装する前に、アプリケーションの仕組みを理解しておくと役立ちます。

Application Architecture Application Architecture

このアプリケーションは、旅行プランニングリクエストを受け付け、LangChain を活用した複数の LLM ノードで構成される LangGraph ワークフローを通じて処理する Flask API です。各ノードは特定の役割を持ち、共有ステートを更新して次のステップに引き渡します。

このパートでは、以下の内容を確認します

  • リクエストのライフサイクル
  • 共有ステートモデル
  • LangGraph ノードの動作方法
  • コード内で使用されている LangChain の抽象化
  • 後でオブザーバビリティが重要になる箇所

アプリケーションのアーキテクチャと実装の詳細については、サブセクションに移動してください。

Last Modified 2026/04/20

4. Agentic AI アプリケーションアーキテクチャのサブセクション

4.1 リクエストのライフサイクル

アプリケーションの動作

大まかに言うと、このアプリケーションはリクエストを受け取り、それを複数ステップのワークフローに変換します

  • coordinator
  • flight specialist
  • hotel specialist
  • activity specialist
  • synthesizer

メインのフローは次のようになっています

@app.route("/travel/plan", methods=["POST"])
def plan():
    data = request.get_json()

    origin = data.get("origin", "Seattle")
    destination = data.get("destination", "Paris")
    user_request = data.get(
        "user_request",
        f"Planning a week-long trip from {origin} to {destination}. "
        "Looking for boutique hotel, flights and unique experiences.",
    )
    travellers = int(data.get("travellers", 2))

    result = plan_travel_internal(
        origin=origin,
        destination=destination,
        user_request=user_request,
        travellers=travellers
    )

    return jsonify(result), 200

この流れを分かりやすく説明すると、次のようになります

  1. Flask がリクエストを受信します
  2. plan_travel_internal() がワークフローの状態を構築します
  3. LangGraph がノードを実行します
  4. 各ノードが状態を更新します
  5. 最終的な旅程が JSON として返されます

知識チェック

この API フローにおいて、LangGraph のワークフローは実際にどこで実行が開始されますか?

ここをクリックして回答を表示

plan_travel_internal() の内部で開始されます。Flask のルートはリクエストの受信と パラメータの抽出のみを行います。plan_travel_internal() がワークフローの状態を初期化し、 LangGraph のグラフを呼び出します。その後、ノード(coordinator、specialist、synthesizer)が 状態を更新しながら実行され、最終的な旅程が生成されます。

Last Modified 2026/04/20

4.2 共有ステート

LangGraph における共有ステート

このアプリで最も重要な LangGraph のコンセプトは、共有ステートオブジェクトです

class PlannerState(TypedDict):
    messages: Annotated[List[AnyMessage], add_messages]
    user_request: str
    session_id: str
    origin: str
    destination: str
    departure: str
    return_date: str
    travellers: int
    flight_summary: Optional[str]
    hotel_summary: Optional[str]
    activities_summary: Optional[str]
    final_itinerary: Optional[str]
    current_agent: str

このステートは、グラフ内のノードからノードへと移動します。

各ノードは以下を行います

  • ステートから値を読み取る
  • 何らかの処理を実行する
  • 新しい値をステートに書き戻す
  • current_agent を設定して次の処理を制御する

これは LangGraph の重要なメンタルモデルですステートフルなワークフローオーケストレーション

知識チェック

messages フィールドに使用されている構文をどのように説明しますか?

messages: Annotated[List[AnyMessage], add_messages]
ここをクリックして回答を確認

messages: Annotated[List[AnyMessage], add_messages] は2つのことを行います。

  • List[AnyMessage] はフィールドのを定義します:LangChain のメッセージオブジェクト(system、human、または AI メッセージ)のリストです。
  • Annotated[..., add_messages]LangGraph の動作を追加し、このフィールドの更新をどのように処理するかをグラフに指示します。

具体的には、add_messages はノードが新しいメッセージを書き込んだ際に、LangGraph が既存のリストを上書きするのではなく、追記することを意味します。 そのため、各ノードがメッセージを追加するたびに、会話履歴が増えていきます。

Last Modified 2026/04/20

4.3 オーケストレーション

実行が始まる場所

メインのオーケストレーションは plan_travel_internal() で行われます

def plan_travel_internal(
    origin: str,
    destination: str,
    user_request: str,
    travellers: int,
    ) -> Dict[str, object]:
    session_id = str(uuid4())
    departure, return_date = _compute_dates()

    initial_state: PlannerState = {
        "messages": [HumanMessage(content=user_request)],
        "user_request": user_request,
        "session_id": session_id,
        "origin": origin,
        "destination": destination,
        "departure": departure,
        "return_date": return_date,
        "travellers": travellers,
        "flight_summary": None,
        "hotel_summary": None,
        "activities_summary": None,
        "final_itinerary": None,
        "current_agent": "start",
    }

    workflow = build_workflow()
    compiled_app = workflow.compile()

    for step in compiled_app.stream(initial_state, config):
        node_name, node_state = next(iter(step.items()))
        final_state = node_state

この関数は以下のアプリケーションライフサイクルを実装しています

  • 初期ステートを構築する
  • グラフを構築する
  • コンパイルする
  • ステップごとにストリーム実行する

知識チェック

質問 1

なぜコードは単にグラフを一度呼び出して最終結果を取得するのではなく、compiled_app.stream(initial_state, config) を使用しているのですか?

ここをクリックして回答を確認

ストリーミングはワークフローを各ノードの実行ごとにステップバイステップで実行するためです。これにより、アプリケーションは中間ステートを観察し、どのノードが実行中かを追跡し、最終出力を待つだけでなくリアルタイムでワークフローを監視できます。

質問 2

なぜグラフを実行する前に initial_state を作成するのですか?

ここをクリックして回答を確認

LangGraph のワークフローは共有ステートオブジェクト上で動作するためです。initial_state は、ワークフローの進行に伴ってノードが読み取り、更新し、受け渡す開始データを提供します。

Last Modified 2026/04/20

4.4 グラフの定義

グラフの定義方法

グラフは build_workflow() で明示的に構築されます

def build_workflow() -> StateGraph:
    graph = StateGraph(PlannerState)
    graph.add_node("coordinator", lambda state: coordinator_node(state))
    graph.add_node("flight_specialist", lambda state: flight_specialist_node(state))
    graph.add_node("hotel_specialist", lambda state: hotel_specialist_node(state))
    graph.add_node("activity_specialist", lambda state: activity_specialist_node(state))
    graph.add_node("plan_synthesizer", lambda state: plan_synthesizer_node(state))
    graph.add_conditional_edges(START, should_continue)
    graph.add_conditional_edges("coordinator", should_continue)
    graph.add_conditional_edges("flight_specialist", should_continue)
    graph.add_conditional_edges("hotel_specialist", should_continue)
    graph.add_conditional_edges("activity_specialist", should_continue)
    graph.add_conditional_edges("plan_synthesizer", should_continue)
    return graph

ルーティングロジックは以下の通りです

def should_continue(state: PlannerState) -> str:
    mapping = {
    "start": "coordinator",
    "flight_specialist": "flight_specialist",
    "hotel_specialist": "hotel_specialist",
    "activity_specialist": "activity_specialist",
    "plan_synthesizer": "plan_synthesizer",
    }
    return mapping.get(state["current_agent"], END)

条件付きエッジを使用していますが、ワークフローは実質的にリニア(直線的)です

  • start
  • coordinator
  • flight specialist
  • hotel specialist
  • activity specialist
  • synthesizer
  • end

理解度チェック

ワークフローが実質的にリニアであるなら、なぜグラフは add_conditional_edgesshould_continue() ルーターを使用しているのでしょうか?

ここをクリックして回答を表示

ワークフローを柔軟で拡張可能にするためです。現在のフローはリニアですが、ルーティング関数によりグラフはステートに基づいて次のノードを動的に決定できます。これにより、グラフを再設計することなく、分岐、リトライ、異なる実行パスを後から簡単に追加できます。

Last Modified 2026/04/20

4.5 ノードの定義

ノードの仕組み

このアプリにおける LangGraph のノードは、state を受け取り、更新された state を返す Python 関数です。

例えば、flight specialist は以下のようになります:

def flight_specialist_node(state: PlannerState) -> PlannerState:
    llm = _create_llm(
    "flight_specialist", temperature=0.4, session_id=state["session_id"]
    )

    step = (
        f"Find an appealing flight from {state['origin']} to {state['destination']} "
        f"departing {state['departure']} for {state['travellers']} travellers."
    )

    messages = [
        SystemMessage(content="You are a flight booking specialist. Provide concise options."),
        HumanMessage(content=step),
    ]

    result = llm.invoke(messages)
    state["flight_summary"] = result.content
    state["messages"].append(result)
    state["current_agent"] = "hotel_specialist"
    return state

これは一般的なノードパターンを示しています:

  1. LLM を作成またはアクセスする
  2. 構造化された state からプロンプトを構築する
  3. モデルを呼び出す
  4. 結果を state に保存する
  5. 次のノードを設定する

hotel ノードと activity ノードも同じ構造に従っているため、ワークフローの説明が容易です。

知識チェック

flight_specialist ノードの LLM を作成する際に、temperature を 0.4 に指定しました。これはどういう意味でしょうか?

ここをクリックして回答を確認

Temperature は、モデルの応答がどの程度ランダムまたは創造的になるかを制御します。

  • 低い temperature(例: 0.0〜0.3): より決定論的で一貫した応答になります
  • 中程度(約 0.4〜0.7): 正確性と創造性のバランスが取れています
  • 高い(0.8 以上): より多様で創造的ですが、予測しにくくなります

つまり、temperature=0.4 に設定すると、flight_specialist エージェントはおおむね一貫性があり信頼できる応答を生成しつつ、わずかなバリエーションを持たせることができます。これは正確性が求められるが、完全に固定的な回答は不要なタスクに適しています。

Last Modified 2026/04/20

4.6 Message Abstractions

LangChain Message Abstractions

このアプリケーションは、1つの長いプロンプト文字列ではなく、LangChain のメッセージ抽象化を使用しています。

from langchain_core.messages import (
    AIMessage,
    BaseMessage,
    HumanMessage,
    SystemMessage,
)

これは、各ノードで以下を分離できるため重要です

  • システムロール
  • ユーザータスク
  • モデルの応答

messages = [
    SystemMessage(content="You are a flight booking specialist. Provide concise options."),
    HumanMessage(content=step),
]
result = llm.invoke(messages)

理解度チェック

system、human、AI メッセージをどのように定義しますか?

ここをクリックして回答を表示

LangChain と LangGraph では、メッセージは通常、誰が話しているか、および会話を導く上でどのような役割を果たしているかによって分類されます

  • System message: AI の動作に関するルールとコンテキストを設定します。インタラクション全体を通じてモデルがどのように応答すべきかを導く指示、制約、トーン、および目標を定義します。
  • Human message: ユーザーからの入力です。AI が応答すべき質問、リクエスト、または情報を含みます。
  • AI message: モデルの応答です。システムの指示とユーザーの入力に基づいて、アシスタントが生成した出力を表します。
Last Modified 2026/04/20

4.7 LLMの作成

LLMの作成

LLM自体はここで作成されます

def _create_llm(agent_name: str, *, temperature: float, session_id: str) -> AzureChatOpenAI:
    azure_deployment_name = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME")
    azure_openai_api_version = os.getenv("AZURE_OPENAI_API_VERSION")

    return AzureChatOpenAI(
        azure_deployment=azure_deployment_name,
        openai_api_version=azure_openai_api_version,
        temperature=temperature,
        model_name = azure_deployment_name,
        # AZURE_OPENAI_API_KEY and AZURE_OPENAI_ENDPOINT environment variables will be used to connect to the LLM
    )

このアプローチでは、モデルの設定をワークフローロジックから分離しています。 各ノードは、どの程度決定論的または創造的であるべきかに応じて、異なるtemperatureを使用できます。

理解度チェック

OpenAI(Azure OpenAIではなく)用のLLMをどのように作成しますか?

クリックして回答を表示

OpenAI用のLLMの作成にはいくつかの違いがあります。関数は AzureChatOpenAI の代わりに ChatOpenAI オブジェクトを返します。

OpenAIを直接使用する場合、Azure固有のパラメータ(azure_deploymentopenai_api_versionAzure endpoint)は使用しません。代わりに、モデル名を指定し、標準の OPENAI_API_KEY 環境変数を使用します。

以下は例です

def _create_llm(agent_name: str, *, temperature: float, session_id: str) -> ChatOpenAI:
    model_name = os.getenv("OPENAI_MODEL_NAME", "gpt-4o-mini")

    return ChatOpenAI(
        model=model_name,
        temperature=temperature,
        # Uses OPENAI_API_KEY automatically from environment
    )
Last Modified 2026/04/20

4.8 Decomposition Pattern

シンセサイザーが分解パターンを示します

最後のノードは、各専門エージェントの出力を1つの回答にまとめます。

def plan_synthesizer_node(state: PlannerState) -> PlannerState:
    llm = _create_llm(
    "plan_synthesizer", temperature=0.3, session_id=state["session_id"]
    )

    content = json.dumps(
        {
            "flight": state["flight_summary"],
            "hotel": state["hotel_summary"],
            "activities": state["activities_summary"],
        },
        indent=2,
    )

    response = llm.invoke(
        [
            SystemMessage(
                content="You are the travel plan synthesiser. Combine the specialist insights into a concise, structured itinerary."
            ),
            HumanMessage(
                content=(
                    f"Traveller request: {state['user_request']}\n\n"
                    f"Origin: {state['origin']} | Destination: {state['destination']}\n"
                    f"Dates: {state['departure']} to {state['return_date']}\n\n"
                    f"Specialist summaries:\n{content}"
                )
            ),
        ]
    )
    state["final_itinerary"] = response.content
    state["messages"].append(response)
    state["current_agent"] = "completed"
    return state

これはエージェントアプリの典型的なパターンです

  • 作業を専門エージェントに分解する
  • 中間出力を収集する
  • 最終的な応答に統合する

これは、この概要から得るべき主要なアーキテクチャのアイデアの1つです。

理解度チェック

アプリが1つのエージェントに旅行プラン全体を生成させるのではなく、個別の plan_synthesizer ノードを使用するのはなぜですか?

ここをクリックして回答を表示

システムはまず問題を専門的なタスク(フライト、ホテル、アクティビティ)に分解するためです。各専門エージェントが焦点を絞った要約を生成し、plan_synthesizer ノードがそれらの出力を1つのまとまった旅程に統合します。

このパターンはモジュール性、信頼性、オブザーバビリティを向上させます。各エージェントがより小さな問題を処理し、最後のノードが結果を統合するためです。