LangChain Agent ワークフロー

前のセクションではcreate_agent()のパラメータについて学びました。

このセクションでは Agent の内部を詳しく見て、どのように動作するのか——モデルとツールがどのように連携するのか、Agent がいつ停止するのかを理解します。


Agent 実行ループ

Agent の核心は単純なループです:モデルを呼び出し → ツールが必要か確認 → ツールを実行 → 繰り返す。モデルがツール呼び出しを要求しなくなるまで繰り返し、Agent は停止して最終結果を返します。

以下では Agent の各ステップを追跡して、このプロセスを理解します。

例

from dotenv import load_dotenv
load_dotenv()

from langchain.tools import tool
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage


@tool
def get_weather(city: str) -> str:
    """指定された都市の天気を取得します。

    Args:
city: 都市名
    """

    weather_data = {
        "杭州": "晴れ、25°C",
        "北京": "曇り、18°C",
    }
    return weather_data.get(city, f"{city} の天気データが見つかりません")


@tool
def get_time(city: str) -> str:
    「指定された都市の現在時刻を取得します。

    Args:
city: 都市名
    """

    time_data = {
        "杭州": "14:30",
        "北京": "14:30",
        "ニューヨーク": "02:30",
    }
    return time_data.get(city, f"{city} の時刻データが見つかりません")


model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
    model=model,
    tools=[get_weather, get_time],
    system_prompt="あなたは親切なアシスタントです。",
)

# stream_mode="updates" を使用すると各ステップを確認できます
print("=== Agent 実行プロセスの追跡 ===\n")
step = 0
for chunk in agent.stream(
    {"messages": [HumanMessage(content="杭州の今の天気はどうですか?何時ですか?")]},
    stream_mode="updates",
):
    step += 1
    print(f"--- ステップ {step} ---")
    for node_name, update in chunk.items():
        print(f"ノード: {node_name}")
        if "messages" in update:
            for msg in update["messages"]:
                if hasattr(msg, 'tool_calls') and msg.tool_calls:
                    # AI メッセージにはツール呼び出しが含まれます
                    for tc in msg.tool_calls:
                        print(f" → ツール呼び出しを要求: {tc['name']}({tc['args']})")
                elif msg.type == "tool":
                    print(f" → ツール結果 [{msg.name}]: {msg.content}")
                elif msg.type == "ai" and msg.content:
                    print(f" → AI 応答: {msg.content[:100]}")

実行結果:

=== Agent 执行过程追踪 ===

--- 步骤 1 ---
ノード: model
  → 请求调用工具: get_weather({'city': '杭州'})
  → 请求调用工具: get_time({'city': '杭州'})

--- 步骤 2 ---
ノード: tools
  → 工具结果 [get_weather]: 晴,25°C
  → 工具结果 [get_time]: 14:30

--- 步骤 3 ---
ノード: model
  → AI 回复: 杭州现在天气晴朗,气温25°C,当前时间是14:30。

この追跡から Agent が実行した3 つのステップ:

  1. ステップ 1(model ノード):モデルが質問を受け取り、get_weather と get_time の 2 つのツールを呼び出す必要があると判断し、2 つの tool_call を返します
  2. ステップ 2(tools ノード):2 つのツールを実行し、天気と時刻の結果を取得します
  3. ステップ 3(model ノード):モデルがツール結果を受け取り、情報が十分であると判断し、最終応答を生成します

stream_mode の詳細

stream() は複数の stream_mode をサポートしており、それぞれ異なる粒度の情報を提供します:

モード戻り値の内容適用シーン
updates各ノード実行後の状態更新Agent の実行ステップを追跡し、中間結果を表示
values各ノード実行後の完全な状態各ステップで完全なメッセージ履歴を確認する必要がある
messagesToken 単位のメッセージストリームフロントエンドで AI のタイピング効果をストリーミング表示
customカスタムイベントMiddleware が stream_writer を通じてカスタムイベントを送信

stream_mode="values"——完全な状態変化を確認する

例

print("=== stream_mode='values' ===\n")
for i, chunk in enumerate(agent.stream(
    {"messages": [HumanMessage(content="杭州の天気はどうですか?")]},
    stream_mode="values",
)):
    messages = chunk.get("messages", [])
    print(f"状態 {i}: {len(messages)} 件のメッセージ")
    for msg in messages:
        print(f"  [{msg.type}] {str(msg.content)[:80]}")
    if i >= 3:  # 最初のいくつかの状態だけを見る
        break

実行結果:

=== stream_mode='values' ===

状態0: 1 条消息
  [human] 杭州天气怎么样?

状態1: 2 条消息
  [human] 杭州天气怎么样?
  [ai]

状態2: 3 条消息
  [human] 杭州天气怎么样?
  [ai]
  [tool] 晴,25°C

状態3: 4 条消息
  [human] 杭州天气怎么样?
  [ai]
  [tool] 晴,25°C
  [ai] 杭州今天天气晴朗,气温25°C,适合出门活动。

stream_mode="messages"——Token 単位のストリーミング出力

例

print("=== stream_mode='messages'(ストリーミングタイピング効果)===\n")
for msg_chunk, metadata in agent.stream(
    {"messages": [HumanMessage(content="サンプル を一言で紹介してください")]},
    stream_mode="messages",
):
    # msg_chunk は AIMessageChunk で、それぞれが 1 つの Token のみを含みます
    if hasattr(msg_chunk, 'content') and msg_chunk.content:
        print(msg_chunk.content, end="", flush=True)
print()

Agent の終了条件

Agent はいつ停止するのでしょうか?主に以下のケースがあります:

終了条件説明例
ツール呼び出しなしモデルが返した AIMessage の tool_calls が空モデルがタスク完了と判断し、直接応答
return_direct=Trueツールが直接返却としてマークされ、実行後すぐに終了クエリ型ツールで、結果がそのまま最終回答
structured_responseモデルが構造化出力を生成response_format で指定された構造化出力が完了
jump_to="end"Middleware が状態制御を通じて能動的に終了権限超過の問題を検出し、早期終了

例

from langchain.tools import tool
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage


# ケース 1:モデルが直接応答(ツール不要)
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
    model=model,
    tools=[],  # ツールなし
)

result = agent.invoke({
    "messages": [HumanMessage(content="サンプル を一言で紹介してください")]
})
# モデルが直接応答し、ループは 1 回だけ実行
print(f"ツールなしシナリオ、メッセージ数: {len(result['messages'])}")
print(f"応答: {result['messages'][-1].content[:80]}...")


# ケース 2:ツール呼び出しが必要(複数回のループ)
@tool
def search_course(keyword: str) -> str:
    """サンプル のコースを検索"""
    return f"{keyword} 関連コースを 3 件見つけました"


agent_with_tools = create_agent(
    model=model,
    tools=[search_course],
)

result = agent_with_tools.invoke({
    "messages": [HumanMessage(content="Python コースを検索")]
})
print(f"\nツールありシナリオ、メッセージ数: {len(result['messages'])}")
# 通常は:human, ai(tool_call), tool(result), ai(final) の順になります
for msg in result["messages"]:
    print(f"  [{msg.type}]", end="")
    if hasattr(msg, 'tool_calls') and msg.tool_calls:
        print(f" tool_calls: {[tc['name'] for tc in msg.tool_calls]}")
    else:
        print(f" {str(msg.content)[:50]}")

実行結果:

无工具场景,消息数: 2
返答: サンプルは初心者向けのプログラミング学習プラットフォームで、豊富なチュートリアルとサンプルを提供しています...

有工具场景,消息数: 4
  [human] 搜索 Python 课程
  [ai] tool_calls: ['search_course']
  [tool] 找到 python 相关课程 3 门
  [ai] サンプルで「Pythonコース」を検索し、関連コースを3件見つけました...

invoke と stream の比較

メソッド戻り値のタイミング適用シーンユーザー体験
invoke()すべて完了後に一括で返却スクリプト、API インターフェース、バッチ処理待機後に完全な結果を確認
stream()中間状態を段階的に返却チャットインターフェース、プロセス表示が必要な場合進行状況をリアルタイムで確認
ainvoke()非同期ですべて完了後に返却Web サービス、非同期フレームワークイベントループをブロックしない
astream()非同期で段階的に返却WebSocket、SSE プッシュサーバー側からのリアルタイムプッシュ

with_config でスレッド ID を渡す

checkpointer(後続の章で詳しく説明します)を使用している場合は、config で thread_id を渡して会話スレッドを管理する必要があります:

例

# config はランタイム設定を渡すために使用
# thread_id は異なる会話スレッドを区別するために使用
config = {"configurable": {"thread_id": "conversation-001"}}

# invoke 方式
result = agent.invoke(
    {"messages": [HumanMessage(content="こんにちは")]},
    config=config,
)

# stream 方式も config をサポート
for chunk in agent.stream(
    {"messages": [HumanMessage(content="こんにちは")]},
    config=config,
    stream_mode="updates",
):
    print(chunk)
その他の拡張