0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LangChain 1.0(α)のAgent MiddlewareをDatabricks上で試す

Posted at

導入

LangChain/LangGraph v1.0のアルファ版がリリースされましたが、その中でエージェントに対するMiddlewareという概念が導入されました。

*以下で同記事を機械翻訳しています。

ざっくり言えば、エージェント内で利用するコンテキストをさらに柔軟に制御するために、エージェントの実行ステップにおいて処理を挿入できるようにする機能です。
詳細は上記Blog記事を確認いただきたいのですが、現状は以下の図で示すbefore_modelmodify_model_requestafter_modelの場所に処理を挿入できます。

image.png

LangGraphのReAct Agentでもモデル入出力において処理を差し込む機能はあったのですが、それをMiddlewareという形で抽象化して使えるようにしたものという理解です。

まだ仕様は変わる可能性がありますが、2025年9月上旬時点のバージョン(ver.1.0.0a5)をDatabricks上で試してみたいと思います。
検証はDatabricks on AWSで行いました。おそらくFee Editionでも問題なく動くと思います。

Agent Middlewareとは

以下のドキュメントより一部抜粋。

ミドルウェアは、エージェント内部で何が起こるかをより厳密に制御する方法を提供します。

コアエージェントループは、modelを呼び出し、実行するtoolsを選択させ、それ以上ツールを呼び出さなくなったら終了するというものです。

ミドルウェアは、これらのステップの前後に何が起こるかを制御します。
各ミドルウェアは、3つの異なるタイプの修飾子を追加できます。

  • Middleware.before_model: モデル実行前に実行されます。状態を更新したり、別のノード(modeltools__end__)にジャンプしたりできます。
  • Middleware.modify_model_request: モデル実行前に実行され、モデルリクエストオブジェクトを準備します。現在のモデルリクエストオブジェクトのみを変更でき(永続的な状態更新は不可)、別のノードにジャンプすることはできません。
  • Middleware.after_model: モデル実行後、ツール実行前に実行されます。状態を更新したり、別のノード(modeltoolsEND)にジャンプしたりできます。

エージェントは、before_modelmodify_model_request、またはafter_modelミドルウェアを含むことができます。これら3つすべてを実装する必要はありません。

(中略)

LangChainは、すぐに使用できるいくつかの組み込みミドルウェアを提供しています。

  • 要約
  • ヒューマン・イン・ザ・ループ
  • Anthropicプロンプトキャッシュ

今回は組み込みミドルウェアである「要約」と「ヒューマンインザループ」を試します。

動かす

準備

Databricks上でノートブックを作成し、まずはLangChain 1.0(α)関連パッケージをインストール。
MLflowも併せてインストールしておきます。

%pip install --pre -U langchain langchain_openai mlflow

%restart_python

Databricksの基盤モデルをLLMとして使う準備をします。

from langchain_openai import ChatOpenAI
import mlflow

mlflow.langchain.autolog()

creds = mlflow.utils.databricks_utils.get_databricks_host_creds()

llm = ChatOpenAI(
    model="databricks-gpt-oss-20b",
    api_key=creds.token,
    base_url=creds.host + "/serving-endpoints",
)

ヒューマンインザループ ミドルウェアを試す

組み込みミドルウェアHumanInTheLoopMiddlewareは、現時点でエージェントのツール実行時に承認を人に求める割込みをかけることができるミドルウェアです。

まずは適当なツールを準備し、それを使うエージェントを作成します。
また、ミドルウェアとしてHumanInTheLoopMiddlewareを指定します。

from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware, HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver
from langchain_core.messages import HumanMessage, AIMessage
from langgraph.types import Command

def get_weather(city: str) -> str:
    """指定した都市の天気を取得します"""
    return f"It's always sunny in {city}!"

def calculator_tool(n1:int, n2:int) -> int:
    """与えられた数字同士を加算します"""
    return n1 + n2

agent = create_agent(
    model=llm,
    tools=[get_weather, calculator_tool],
    middleware=[
        HumanInTheLoopMiddleware(
            tool_configs={
                "get_weather": {
                    "require_approval": False, ## 許可を求めない
                },
                "calculator_tool": {
                    "require_approval": True,
                    "description": "🚨 計算には承認が必要です",
                },
            },
            message_prefix="ツール実行のオペレーション確認:",
        ),
    ],
    checkpointer=InMemorySaver(),
)

では、動かしてみましょう。

まず、calculator_toolを呼び出すケース。


config = {"configurable": {"thread_id": 1}}

# 初回実行
result = agent.invoke(
    {
        "messages": [HumanMessage("5+123は?")],
    },
    config
)

## Human in the Loopの確認
state = agent.get_state(config)
if state.next:
    request = state.tasks[0].interrupts[0].value[0]

    print("--- Check ---")
    print(request["config"])
    print(request["description"])

    # ツール実行を承認して継続
    result = agent.invoke(
        Command(
            resume=[{"type": "accept"}]  # 承認
        ),
        config=config
    )

    print()
    print("--- Result after accept ---")
    for message in result["messages"]:
      if isinstance(message, (AIMessage, HumanMessage)):
        print(message.content)

結果は以下の通り。

出力
--- Check ---
{'require_approval': True, 'description': '🚨 計算には承認が必要です'}
ツール実行のオペレーション確認:

Tool: calculator_tool
Args: {'n1': 5, 'n2': 123}

--- Result after accept ---
5+123は?

128

一度目のinvokeは、呼び出すツールの情報出力までで停止し、2回目のinvokeで承認のCommandを渡すことで継続実行されます。

もう一例。


config = {"configurable": {"thread_id": 2}}

# 初回実行
result = agent.invoke(
    {
        "messages": [HumanMessage("大阪の天気は?")],
    },
    config
)

## Human in the Loopの確認
state = agent.get_state(config)
if state.next:
    request = state.tasks[0].interrupts[0].value[0]

    print("--- accept ---")
    print(request["config"])
    print(request["description"])

    result = agent.invoke(
        Command(
            resume=[{"type": "accept"}]  # 承認
        ),
        config=config
    )

    print()
    print("--- Result after accept ---")
    for message in result["messages"]:
      if isinstance(message, (AIMessage, HumanMessage)):
        print(message.content)
出力
--- accept ---
{'require_approval': False}
ツール実行のオペレーション確認:

Tool: get_weather
Args: {'city': 'Osaka'}

--- Result after accept ---
大阪の天気は?

大阪の天気は、いつも晴れです!

get_weatherツールを呼び出す以外は最初の例と同じです。
ただ、このツールはrequire_approvalをFalseに設定していました。

もしかしたらそのままツール実行までされるかと思っていましたが、acceptコマンドを渡して継続処理する必要があります。
このあたりのハンドリングはエージェント外で実装対応が必要そうですね。

要約 ミドルウェアを試す

組み込みミドルウェアSummarizationMiddlewareは、長くなったコンテキストを圧縮するためのミドルウェアです。
チャットを繰り返してコンテキストサイズが膨らんだ際に、過去の会話履歴を除去したり要約したりするときに有用だと思います。

では、このミドルウェアを使うエージェントを作成します。
今回はツール呼び出しをせず、ただの会話エージェントとしました。

# 要約処理に使うLLM
summarization_llm = ChatOpenAI(
    model="databricks-gemma-3-12b",
    api_key=creds.token,
    base_url=creds.host + "/serving-endpoints",
)

agent = create_agent(
    model=llm,
    tools=[],
    middleware=[
        SummarizationMiddleware(
            model=summarization_llm, # 要約用LLM
            max_tokens_before_summary=100,  # 100トークンを越えたら要約する
            messages_to_keep=3,  # 要約後に最後の3メッセージはそのままキープする
        ),
    ],
    checkpointer=InMemorySaver(),
)

要約用のLLMにはdatabricks-gemma-3-12bを利用しました。

現時点では、要約にdatabricks-gpt-oss-20bを使うと、このモデルの出力構造の違いでエラーが出るようです。

では、エージェントを動かしてみましょう。

config = {"configurable": {"thread_id": 1}}

# Initial invocation
agent.invoke(
    {
        "messages": [HumanMessage("東京の観光名所を10か所紹介して")],
    },
    config
)

agent.invoke(
    {
        "messages": [HumanMessage("大阪の観光名所を10か所紹介して")],
    },
    config
)

agent.invoke(
    {
        "messages": [HumanMessage("名古屋の観光名所を10か所紹介して")],
    },
    config
)

result = agent.invoke(
    {
        "messages": [HumanMessage("これまで紹介した日本の観光名所をまとめて")],
    },
    config
)

result
出力
{'messages': [HumanMessage(content='Here is a summary of the conversation to date:\n\n## 大阪のおすすめ観光名所 10箇所\n\n| # | 名称 | 所在地 | 特色・見どころ |\n|---|------|--------|----------------|\n| 1 | **大阪城** | 大阪市中央区 | 16世紀の石垣と櫓が残る本格的な日本城。天守閣からは大阪市内を一望でき、春には桜、秋には紅葉が美しい。 |\n| 2 | **道頓堀** | 大阪市浪速区 | グリコ看板やかに道具屋、くいだおれグリコなど、食い倒れ文化が息づく人気商業街。高台のミケバルやトルコ大橋も必見。 |\n| 3 | **大阪国立博物館** | 大阪市北区 | 日本美術・人文資料のコレクションが充実。特に国宝の土佐市祭りの山形や再現江戸館などが魅力。 |\n| 4 | **ユニバーサル・スタジオ・ジャパン (USJ)** | 大阪市此花区 | 映画やテーマパーク好き必須。ハリーポッター、ミニオンズなどのアトラクションが人気。季節ごとのイベントも楽しめる。 |\n| 5 | **天保山ハーバービレッジ** | 大阪市港区 | 大型フェリシア・ザ・タワーや海遊館などを備えた港周辺のレジャー複合施設。夜はライトアップも美しい。 |\n| 6 | **大阪水族館「海遊館」** | 大阪市港区 | 世界最大級のタンクをはじめ、グレートバリアリーフ再現や訪れる魚の舞台が見事。夜のイルミネーションもおすすめ。 |\n| 7 | **梅田スカイビル** | 大阪市北区 | 空中庭園展望台から見える大阪の夜景が一等席。北梅田の豊富な飲食店やショッピングも楽しめる。 |\n| 8 | **住吉大社** | 大阪市住之寺区 | 全国神社の中でもとりわけ重要な神社。池の水・木曽川の御神酒を注ぐ祭事が見どころ。 |\n| 9 | **黒門市場** | 大阪市中央区 | 新鮮な寿司や海鮮、名物お好み焼きが並ぶ食品市場。観光客との交流が活発で食い歩きに最適。 |\n|10 | **心斎橋筋商店街** | 大阪市中央区 | 大阪のショッピングとグルメの心臓部。ファッションビルや古着屋、土産物ショップが連なる長い通路は観光客多数。 |\n\n**ポイントまとめ**  \n- **歴史と現代が混在**:大阪城や住吉大社など歴史的名所と、USJ・海遊館のような近代的レジャー施設をバランス良く巡ると奥行きが出ます。  \n- **食い倒れの町を体験**:道頓堀・黒門市場・心斎橋で地元のグルメを堪能すると、観光の味覚が更に深まります。  \n- **観光のピーク時**:日本の祭り期間や週末は混雑するため、主要スポットは朝早くか夕方遅くに訪れると人が少ないです。', additional_kwargs={}, response_metadata={}, id='cd022f66-70a1-4246-83bd-b78852e48b5d'),
  HumanMessage(content='名古屋の観光名所を10か所紹介して', additional_kwargs={}, response_metadata={}, id='f647eabb-899b-407d-b1f1-17fb30c4df5f'),
  AIMessage(content=[{'type': 'reasoning', 'summary': [{'type': 'summary_text', 'text': "User wants the list of 10 attractions in Nagoya. They probably want Japanese names, English, location, features. Should format a table. Provide concise descriptions. Also might mention transport tips. Should be in Japanese. Provide 10 items. Royal. Let's draft."}]}, {'type': 'text', 'text': '## 名古屋市で必訪の観光名所 10選\n\n| # | 日本語名 | 英語表記 | 所在地 | 特色・おすすめポイント |\n|---|----------|----------|--------|--------------------------|\n| 1 | 名古屋城 | Nagoya Castle | 名古屋市中区 | 徳川家康の城跡に金の鯱(しゃちょう)と著名な桂離宮を持つ、白壁の本丸。桜の季節は「金鯱レオニ」が光りつけて絶景。 |\n| 2 | オアシス21 | Oasis 21 | 名古屋市中区 | 未来感溢れる“ぐらいとら”と呼ばれる巨大水槽が特徴の未来型ビル群。展望デッキやカッパックアートが楽しめます。 |\n| 3 | 名古屋テレビ塔 | Nagoya TV Tower | 名古屋市中区 | 高さ147\u202fmのタワーは、軽食店や展望台、レストランがあり、夜景は市内を一望。 |\n| 4 | 名古屋市科学館 | Nagoya City Science Museum | 名古屋市中区 | 巨大な天文観測室と分子体験や量子物理の展示など、子どもから大人まで学べるインタラクティブ施設。 |\n| 5 | 熱田神宮 | Atsuta Shrine | 名古屋市熱田区 | 剣の神として有名、特に月光祭(五月)や正月の門前祭りが人気。 |\n| 6 | 東山動植物園 | Higashiyama Zoo & Botanical Gardens | 名古屋市港区 | ワニ館・巨乳ゆり小屋や珍しい樹木が多数。ゴールデンウィークは混雑に注意。 |\n| 7 | 名古屋港水族館 | Port of Nagoya Public Aquarium | 名古屋市港区 | 日本最大級のタンク「いわかがい”の猫の水槽」を持ち、イルカショーやアザラシショーが人気。 |\n| 8 | 未来館(名古屋市) | Aichi Prefectural Science Museum | 名古屋市中区 | 宇宙・交通・生物などをテーマにした展示と、体験型ワークショップが充実。 |\n| 9 | 伏見稲荷神社(名古屋) | Fushimi Inari Shrine (Nagoya) | 名古屋市安治区 | 本宮ではなく、裾野に広がる「白い稲荷の社」も有名。 |\n|10 | 栄 | Sakae | 名古屋市中区 | 名古屋最大の商業・オフィスエリア。デパ地下やオフィスビルの屋上展望台、夜はネオン街としても人気。 |\n\n### 交通でのアクセスヒント\n- **地下鉄**: 名古屋市営地下鉄は大半の観光スポットを網羅。名古屋城・熱田神宮は「城東駅」・「熱田駅」が近い。\n- **JR**: JR 名古屋駅は多数の路線が乗り入れ、オアシス21・名古屋塔・モールセンター内の未来館と直結。  \n- **バス**: 観光バス(名鉄観光バス)で主要スポット間を効率的に巡ることも可能。  \n- **移動時間**: 早朝の8〜9時、夕方の19〜20時は混雑がひどくなるため、余裕をもって計画を立てると快適です。  \n\nこれらを基に、歴史・自然・科学・ショッピングとバランス良く巡れば、名古屋の多彩な魅力を存分に体感できます。ぜひご活用ください!'}], additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 970, 'prompt_tokens': 1746, 'total_tokens': 2716, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_provider': 'openai', 'model_name': 'gpt-oss-20b-080525', 'system_fingerprint': None, 'id': 'chatcmpl_b8cd3474-c245-4278-b881-416a4e082365', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--7fe0b221-df2d-4724-ba8b-a70db0a54988-0', usage_metadata={'input_tokens': 1746, 'output_tokens': 970, 'total_tokens': 2716, 'input_token_details': {}, 'output_token_details': {}}),
  HumanMessage(content='これまで紹介した日本の観光名所をまとめて', additional_kwargs={}, response_metadata={}, id='50a7f365-f36a-4aea-a5ba-403204c3ab49'),
  AIMessage(content=[{'type': 'reasoning', 'summary': [{'type': 'summary_text', 'text': 'User says: "これまで紹介した日本の観光名所をまとめて" - likely wants a summary list that includes the previously introduced places: Osaka and Nagoya. The conversation: first user gave a summary of Osaka tourist spots. Assistant responded with Osaka list. Then user gave a prompt to provide 10 Nagoya spots. Assistant responded with that. Now user wants "これまで紹介した日本の観光名所をまとめて" i.e., a combined summary of all Japanese tourist spots mentioned so far.\n\nSo we need to produce a summarized list, maybe a table, combining Osaka and Nagoya places. Could also include the total of 20 places. Might present them by city with maybe # numbers. Should be in Japanese. Provide bullet points or table.\n\nAlso we can mention some points like categories: history, modern, culinary, etc.\n\nLet\'s produce a summary table with city columns maybe. Use # from each city. Could provide each with their # but there are two sets of #1-10 each. So maybe unify with overall numbering. Or just separate by city. Let\'s present a combined list with columns: City, #, 名称, 所在地, 特色・見どころ. Let\'s include 20 rows. That addresses "まとめて". Also maybe provide a short summary paragraph.\n\nLet\'s produce in Japanese.'}]}, {'type': 'text', 'text': '## これまでに紹介した日本国内の主要観光名所(大阪・名古屋)まとめ\n\n| No. | 都市 | 名称 | 所在地 | 特色・見どころ |\n|-----|-----|------|--------|----------------|\n| 1 | 大阪 | **大阪城** | 大阪市中央区 | 16世紀の石垣と櫓が残る本格城郭。天守閣からは大阪市内を一望。春桜・秋紅葉も美しい。 |\n| 2 | 大阪 | **道頓堀** | 大阪市浪速区 | グリコ看板・くいだおれ有名。熱燗の屋台やトルコ大橋、ミケバルなど食文化と商業のハブ。 |\n| 3 | 大阪 | **大阪国立博物館** | 大阪市北区 | 日本美術・人文資料コレクション。土佐市祭り・再現江戸館など見所満載。 |\n| 4 | 大阪 | **ユニバーサル・スタジオ・ジャパン (USJ)** | 大阪市此花区 | 映画テーマパーク。ハリーポッター・ミニオンズなどアトラクション。季節イベントも豊富。 |\n| 5 | 大阪 | **天保山ハーバービレッジ** | 大阪市港区 | フェリシア・ザ・タワー・海遊館等レジャー複合施設。夜のライトアップが美しい。 |\n| 6 | 大阪 | **大阪水族館「海遊館」** | 大阪市港区 | 世界最大級タンク・グレートバリアリーフ再現。夜のイルミネーションもおすすめ。 |\n| 7 | 大阪 | **梅田スカイビル** | 大阪市北区 | 空中庭園展望台。大阪夜景の絶景と北梅田の飲食・ショッピング。 |\n| 8 | 大阪 | **住吉大社** | 大阪市住之寺区 | 全国重要神社。池水・木曽川御神酒の祭事。 |\n| 9 | 大阪 | **黒門市場** | 大阪市中央区 | 新鮮寿司・海鮮・お好み焼きの食品市場。観光客と接客も活発。 |\n|10 | 大阪 | **心斎橋筋商店街** | 大阪市中央区 | 賞典・ファッションビル・古着屋・土産物ショップで賑わう商店街。 |\n| 11 | 名古屋 | **名古屋城** | 名古屋市中区 | 徳川家康築城。金鯱の晴れ姿と桂離宮。春桜シーズンは絶景。 |\n| 12 | 名古屋 | **オアシス21** | 名古屋市中区 | 未来型ビル群。巨大水槽「グレーリター」等のファッション。 |\n| 13 | 名古屋 | **名古屋テレビ塔** | 名古屋市中区 | 高さ147m。展望台・レストラン・夜景鑑賞。 |\n| 14 | 名古屋 | **名古屋市科学館** | 名古屋市中区 | 天文観測室・インタラクティブ展示。子・大人問わず学び体験。 |\n| 15 | 名古屋 | **熱田神宮** | 名古屋市熱田区 | 剣神。月光祭・正月門前祭りの賑わい。 |\n| 16 | 名古屋 | **東山動植物園** | 名古屋市港区 | ワニ館・珍木。大人も子供も楽しめる。 |\n| 17 | 名古屋 | **名古屋港水族館** | 名古屋市港区 | 「いわかがい」猫タンクの水槽が有名。イルカ・アザラシショー。 |\n| 18 | 名古屋 | **未来館(名古屋市)** | 名古屋市中区 | 宇宙・交通・生物テーマ。体験型ワークショップ多数。 |\n| 19 | 名古屋 | **伏見稲荷神社(名古屋)** | 名古屋市安治区 | 白い稲荷社・裾野に広がる小社群。訪問者の来訪頻度が多い。 |\n| 20 | 名古屋 | **栄** | 名古屋市中区 | 商業・オフィスエリア。デパ地下・屋上展望台・ネオン街が栄える。 |\n\n### 要点まとめ\n\n| 観光テーマ | 概括 |\n|------------|------|\n| **歴史と文化** | 大阪城・名古屋城・住吉大社・熱田神宮は日本の城郭・神社文化を代表 |\n| **現代エンタメ** | USJ・大阪水族館・名古屋港水族館・未来館などが近代的な楽しみを提供 |\n| **食のタワー** | 道頓堀・黒門市場・心斎橋・栄で地元グルメと市場文化を体験 |\n| **観光のフロー** | 大阪は港区・中央区を中心にコンパクトに巡れる。名古屋は市内中心部と港区を結ぶ動線が整備 |\n| **おすすめ時期** | 春(桜)、秋(紅葉)、熱帯夜は熱燗やショーで最適。日中の混雑を避けるなら早朝・夕方遅めに観光 |\n| **交通** | 大阪・名古屋とも地下鉄が充実しており、主要施設に駅が直結。JR・名鉄バスでローカル観光も容易 |\n\nこの20箇所を組み合わせた旅程を作ることで、歴史・科学・エンタメ・グルメをバランス良く体験できる日本国内の代表的観光ルートが完成します。ぜひご参照ください。'}], additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 1703, 'prompt_tokens': 1871, 'total_tokens': 3574, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_provider': 'openai', 'model_name': 'gpt-oss-20b-080525', 'system_fingerprint': None, 'id': 'chatcmpl_e75be931-8755-4aad-ac2f-c5cc8c4b16ea', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--72dfc8e2-afe8-4c57-9a17-9ad513460973-0', usage_metadata={'input_tokens': 1871, 'output_tokens': 1703, 'total_tokens': 3574, 'input_token_details': {}, 'output_token_details': {}})]}

少しわかりづらいのですが、最初のHumanMessageは要約された大阪の観光名所情報が含まれています。(実際はほとんど要約されていませんでしたが)また、一番最初の東京の観光名所情報は含まれませんでした。
直近の3メッセージまではそのまま保持されています。

今回は要約のトリガーとなるトークン数をかなり小さい値にしましたが、実際にはもう少し大きい値で運用し、履歴が長くなってくると適切にコンテキストを圧縮(要約)して利用できるようになるかと思います。

おわりに

LangChain 1.0から登場予定のMiddlewareを試してみました。
カスタムミドルウェアももちろん作成できますので、コンテキストに対する加工処理など割込みが必要な場合に活用できると思います。

最終仕様がどうなるかはまだわかりませんが、よくあるパターンが他にも組み込みとして公開されるとありがたいなあ。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?