技術メモ

神奈川在住のITエンジニアの備忘録。おもにプログラミングやネットワーク技術について、学んだことを自分の中で整理するためにゆるゆると書いています。ちゃんと検証できていない部分もあるのでご参考程度となりますが、誰かのお役に立てれば幸いです。

Windows 11でKubernetes入門:nginxをデプロイしてログを確認する

Kubernetes(k8s)の学習として、Windows 11のDocker Desktop上にnginxをデプロイし、ブラウザアクセスとログ確認を試しました。実行したコマンドと、途中で学んだポイントをまとめます。

1. 学習環境

今回使用した構成は以下です。

項目 内容
OS Windows 11
実行環境 Docker Desktop(WSL2)
Kubernetesの構築方式 kind
ノード数 1
Kubernetesバージョン v1.34.3
操作端末 PowerShell
Webサーバ nginx

Docker DesktopのKubernetes画面から「Create cluster」を選び、kind・1ノードで作成しました。kindを使うには、Settings → Generalの「Use containerd for pulling and storing images」が有効になっている必要があります。

初回は起動に少し時間がかかりましたが、待つと「Kubernetes running」になりました。

2. kubectlの接続先を確認する

kubectl config get-contexts
kubectl config use-context docker-desktop
kubectl get nodes

use-contextは、kubectlの操作先を切り替えるコマンドです。Kubernetes自体を起動する操作ではありません。

contextは、接続先クラスタ・認証に使うユーザー・デフォルトのNamespaceなどをまとめた接続設定です。現在の接続先は以下でも確認できます。

kubectl config current-context

get nodesでノードのSTATUSがReadyなら準備完了です。

3. nginxをデプロイする

kubectl create namespace k8s-study
kubectl -n k8s-study create deployment web --image=nginx:stable
kubectl -n k8s-study rollout status deployment/web
kubectl -n k8s-study get pods

主な用語の意味は以下です。

用語 役割
Node Podを動かす実行環境
Namespace クラスタ内のリソースを分ける区画
Pod コンテナを動かす単位
Deployment 指定した数のPodが動くように管理する仕組み

Deploymentを作成すると、その管理下でPodが作られます。Podが失われた場合は、必要な数を維持するために新しいPodが作られます。

rollout statusはデプロイの進捗を確認するコマンドで、起動を開始するコマンドではありません。

PodのREADYが1/1、STATUSがRunningになれば、nginxが動いています。

4. Serviceを作り、ブラウザでアクセスする

まず、Podへのアクセス窓口となるServiceを作成します。

kubectl -n k8s-study expose deployment web --port=80 --target-port=80

この指定は、Serviceの80番ポートから対象Podの80番ポートへ転送する設定です。

Podは作り直されると名前やIPが変わることがあります。Serviceは、その変化を隠して安定したアクセス窓口を提供します。通常のService経由の通信では、対象となる複数Podへの振り分けも担います。

次に、PCからアクセスできるようにします。

kubectl -n k8s-study port-forward service/web 8080:80

このPowerShellを開いたまま、ブラウザで以下へアクセスします。

http://localhost:8080

「Welcome to nginx!」が表示されれば成功です。

port-forwardで学んだこと

8080:80は「PCの8080番ポートから、転送先の80番ポートへ」という意味です。

ただし、port-forward service/webはServiceのIPを経由して負荷分散する動作ではありません。Serviceに対応するPodを1つ選び、kubectlとKubernetes APIサーバーなどを経由して、そのPodへ転送します。

そのため、Podを複数に増やしても、この接続でブラウザを更新すると、基本的には選ばれた1つのPodにアクセスが届きます。

5. アクセスログを確認する

別のPowerShellを開いて実行します。

kubectl -n k8s-study logs deployment/web --tail=20 -f

ブラウザを更新するとアクセスログが流れます。

  • --tail=20:末尾20行を表示

  • -f:新しいログを継続して表示

  • Ctrl+C:ログの追跡を終了

ここで表示されるのは、基本的にコンテナの標準出力・標準エラーです。今回のnginxイメージは、アクセスログなどをそこへ出力するため、kubectl logsで確認できます。

6. Podを2つに増やす

kubectl -n k8s-study scale deployment/web --replicas=2
kubectl -n k8s-study get pods

ここで注意したいのは、以下のコマンドでは通常、Deployment配下の1つのPodのログが選ばれる点です。

kubectl -n k8s-study logs deployment/web --tail=20 -f

複数Podをまとめて確認するには、ラベルで指定できます。

kubectl -n k8s-study get pods --show-labels
kubectl -n k8s-study logs -l app=web --tail=20 --prefix=true -f

-l app=webは、そのラベルを持つPodを対象にする指定です。--prefix=trueを付けると、各行にPod名とコンテナ名が表示されます。

今回の環境では、Deploymentに対して--all-pods=trueを指定する方法も使えます。

kubectl -n k8s-study logs deployment/web --all-pods=true --tail=20 -f

なお、port-forwardの接続先は1つのPodなので、アクセスログが片方だけ増えても正常です。

7. 実際の障害調査ではどう絞り込むか

多数のPodがある環境では、以下の順に対象を絞ります。

  1. Namespaceを特定する

  2. Deploymentやラベルでアプリを特定する

  3. Podの状態を確認する

  4. 問題のあるPod・コンテナのログを見る

対象と状態の確認

kubectl get deployments -A
kubectl -n k8s-study get pods -l app=web -o wide

主にREADY、STATUS、RESTARTSを確認します。

状態 主に確認すること
Pending ノードのリソース不足、ストレージなど
ImagePullBackOff イメージ名、取得先への通信、認証など
CrashLoopBackOff アプリが異常終了して再起動を繰り返す原因
RunningでもREADY不足 ヘルスチェックなど

起動できない場合はdescribe

以下の<Pod名>などは、実際の値に置き換えます。

kubectl -n k8s-study describe pod <Pod名>

State、Last State、終了コード、末尾のEventsを確認します。

イメージを取得できないなど、アプリが起動する前の問題は、アプリログよりEventsが手掛かりになります。

時間と表示量を絞ってログを見る

kubectl -n k8s-study logs -l app=web --since=10m --tail=100 --timestamps=true --prefix=true

これは、対象Podについて直近10分、各対象コンテナの末尾100行を時刻付きで表示する例です。

1つのPodに複数コンテナがある場合は、コンテナ名も指定します。

kubectl -n k8s-study logs <Pod名> -c <コンテナ名> --tail=100

再起動している場合は、直前に終了したコンテナのログを確認します。

kubectl -n k8s-study logs <Pod名> -c <コンテナ名> --previous --tail=100

--previousは、同じPod内の直前のコンテナ実行のログを見る指定です。削除された別のPodのログを取得する機能ではありません。

通信や設定も確認する

Podが動いているのにアクセスできない場合は、Serviceと転送先も調べます。

kubectl -n k8s-study describe service web
kubectl -n k8s-study get endpointslices -l kubernetes.io/service-name=web

必要に応じて、コンテナ内でコマンドを実行できます。例えば、nginxの設定確認は以下です。

kubectl -n k8s-study exec <Pod名> -- nginx -T

8. ログの保存場所と保持期間

標準出力・標準エラーのログは、コンテナランタイムによってノード側のファイルに記録されます。kubectl logsは、そのログを取得する仕組みです。

一方、アプリが独自のファイルだけに出力しているログは、通常そのままではkubectl logsに表示されません。

また、kubectl logsに「必ず何時間分見られる」という固定の保持期間はありません。

  • ログはサイズなどに応じてローテーションされる

  • kubectl logsで取得できるのは、基本的に最新のログファイル

  • Podが削除されると、そのPodのログを通常のkubectl logsでは取得できない

このため、実運用ではログ集約基盤へ転送し、長期保存や検索を行います。普段は集約ログを検索し、デプロイ直後や起動失敗の調査ではkubectl logsやdescribeを使う、という使い分けになります。

複数のアプリにまたがる処理では、リクエストIDやTrace IDをログに含めると、同じ処理を追いやすくなります。

9. 学習環境の片付け

今回作ったアプリをまとめて削除するには、Namespaceを削除します。

kubectl delete namespace k8s-study

このNamespace内のDeployment、Pod、Serviceなどが削除されます。Kubernetesクラスタ自体は残ります。

参考資料

DifyとLangfuseを連携してAIアプリの実行トレースを確認してみた

AIエージェントやLLMアプリを運用するうえで、「どのモデルが呼ばれたのか」「何トークン使ったのか」「どのような入出力だったのか」といった実行状況を把握することは重要です。

今回は、Dify上に作成したLLMアプリをLangfuseと連携し、実際にどのようなトレース情報を取得できるのか確認してみました。

環境

今回利用した環境は以下です。

  • Dify Cloud

  • Langfuse Cloud

  • Langfuseのリージョン:Japan

  • Difyアプリ:テキスト生成

  • LLM:GPT-4o-mini

DifyとLangfuseは標準で連携できるため、Difyアプリ側のコードを変更したり、Langfuse SDKを組み込んだりする必要はありません。

DifyとLangfuseを連携する

まずLangfuseでProjectを作成し、ProjectのAPI Keysから以下の2つを取得します。

Public Key
pk-lf-xxxxxxxxxxxxxxxx

Secret Key
sk-lf-xxxxxxxxxxxxxxxx

次にDifyで対象アプリを開き、

監視
  ↓
Langfuse

と進み、Langfuseの接続情報を設定します。

今回、Langfuse CloudのJapanリージョンを使用したため、以下のように設定しました。

秘密キー:
sk-lf-xxxxxxxxxxxxxxxx

公開キー:
pk-lf-xxxxxxxxxxxxxxxx

Host:
https://jp.cloud.langfuse.com

設定が完了すると、Difyの監視画面にLangfuseが「有効」と表示されます。

Difyアプリを実行してみる

今回は「SQLクリエイター」という簡単なテキスト生成アプリを使用しました。

例えば、

peopleテーブルからageが40以上のアイテムを出力してください。

と入力すると、LLMから以下のようなSQLが返ってきます。

SELECT *
FROM people
WHERE age >= 40;

Dify側の監視画面でも、会話数や平均応答時間、トークン出力速度などを確認できます。

Langfuseとの連携後にアプリを実行すると、その実行情報がLangfuseにも送られます。

なお、最初はLangfuseにTraceが表示されなかったのですが、もう一度Difyアプリを実行すると正常に表示されました。

連携設定より前に実行した履歴が自動的にLangfuseへ送られるわけではないため、設定後に新しくアプリを実行して確認するのがよさそうです。

Langfuseに送られたデータ

実際にLangfuseから取得したSpanには、以下のような情報が含まれていました。

{
  "traceId": "70da8a6f-e3d4-4028-b4d7-13acf9500e12",
  "traceName": "message",
  "type": "SPAN",
  "name": "message",
  "sessionId": "5c6040fc-71f1-41ce-ad29-08f918b8ba12",
  "metadata": {
    "app_id": "dc933066-1c4e-44c9-9798-044522030a62",
    "message_id": "70da8a6f-e3d4-4028-b4d7-13acf9500e12",
    "status": "normal",
    "ls_model_name": "gpt-4o-mini-2024-07-18",
    "ls_provider": "langgenius/openai/openai",
    "conversation_id": "5c6040fc-71f1-41ce-ad29-08f918b8ba12",
    "agent_based": "false"
  }
}

これを見ると、Difyからかなり多くの情報がLangfuseへ送られていることが分かります。

DifyとLangfuseのIDの関係

特に興味深かったのが、Dify側のIDとLangfuse側のIDの対応です。

今回のデータでは、

Dify message_id
      ↓
Langfuse traceId

となっており、実際に同じUUIDが使用されていました。

message_id
70da8a6f-e3d4-4028-b4d7-13acf9500e12

traceId
70da8a6f-e3d4-4028-b4d7-13acf9500e12

同様に、

Dify conversation_id
      ↓
Langfuse sessionId

という対応も確認できました。

整理すると、以下のような関係になります。

Dify App
  │
  └─ app_id
       │
       └─ Conversation
            │
            └─ conversation_id
                 │
                 └─ Message
                      │
                      └─ message_id

             ↓ Langfuse

Langfuse
  │
  └─ metadata.app_id
       │
       └─ Session
            │
            └─ sessionId
                 │
                 └─ Trace
                      │
                      └─ traceId

この対応関係があるため、Dify側の実行情報とLangfuse側のトレース情報をIDで関連付けることができそうです。

複数のDifyアプリも識別できそう

もう一つ重要なのがapp_idです。

Langfuseに送られたmetadataには、

"app_id": "dc933066-1c4e-44c9-9798-044522030a62"

が含まれていました。

そのため、複数のDifyアプリを同じLangfuse Projectへ送った場合でも、

Dify App A ─┐
Dify App B ─┼──→ Langfuse Project
Dify App C ─┘

Langfuse側で、

metadata.app_id = AAA → App A
metadata.app_id = BBB → App B
metadata.app_id = CCC → App C

という形で識別できそうです。

これは複数のAIアプリを一元的に監視する場合に便利そうです。

一方、今回取得したデータでは、

"app_name": ""

となっていました。

そのため、画面表示用のアプリ名が必要な場合には、app_idとDify側のアプリ名を別途対応付ける必要があるかもしれません。

使用したLLMも分かる

metadataを見ると、

"ls_model_name": "gpt-4o-mini-2024-07-18",
"ls_provider": "langgenius/openai/openai"

という情報も入っていました。

そのため、Langfuseの情報から、

どのDify Appか
どのConversationか
どのMessageか
どのLLMを使ったか

まで追跡できそうです。

トークン数は取得できる

今回のデータには、以下のトークン情報も含まれていました。

"message_tokens": 51,
"answer_tokens": 47,
"total_tokens": 98

つまり、

Input Token  : 51
Output Token : 47
Total Token  : 98

です。

DifyからLangfuseへトークン数が渡されているため、Langfuse側のデータを見ることで、LLMをどの程度利用したのかを把握できます。

これはAIアプリごとの利用量を監視するうえで有用そうです。

コストは今回のSpanでは算出されていなかった

一方、今回取得したSpanでは、コストについて少し異なる結果になりました。

Langfuseの標準的なUsage/Cost関連フィールドを見ると、

"usageDetails": {},
"costDetails": {},
"totalCost": 0

となっていました。

つまり今回のDify連携では、

Dify
 │
 │ Token情報
 │ Input  = 51
 │ Output = 47
 │ Total  = 98
 │
 ▼
Langfuse
 │
 ├─ Token数 → 取得できた
 │
 └─ Cost    → 今回のSpanでは0

という状態でした。

Langfuse自体には、モデルごとの料金とToken Usageをもとにコストを計算する機能があります。

しかし今回確認したSpanでは、トークン数はLangfuseの標準的なusageDetailsではなく、Difyから送られたデータ内に、

"message_tokens": 51,
"answer_tokens": 47,
"total_tokens": 98

という形で格納されていました。

また、使用モデルについては、

"ls_model_name": "gpt-4o-mini-2024-07-18"

という情報が取得できています。

そのため、独自の監視システムなどでコストを算出する場合には、

Langfuseから取得

Model
  gpt-4o-mini-2024-07-18

Input Token
  51

Output Token
  47

       ↓

モデルごとの料金情報

       ↓

LLM Costを計算

という方法も考えられます。

つまり監視システム側で、

Model名
Input Token数
Output Token数
モデルごとの料金表

を組み合わせれば、概算のLLMコストを計算できます。

ただし、今回確認したのはDifyの「テキスト生成」アプリのmessage Spanだけです。

DifyのAgentアプリや、より詳細なLLM GenerationレベルのObservationではusageDetailsやcostDetailsに情報が格納される可能性もあるため、「DifyとLangfuseを連携するとコストは取得できない」と断定するのはまだ早そうです。

現時点では、

トークン数はDifyからLangfuseへ渡されていることを確認できた。一方、今回確認したSpanではLangfuseのコスト情報は算出されていなかった。

と整理するのが適切だと思います。

成功・失敗の判定にも使えそう

今回の実行では、

"status": "normal"

また、

"error": ""

となっていました。

そのため、単純化すると、

status = normal
AND
error = 空

→ 正常終了

という判定ができそうです。

逆にLLM呼び出しやTool Callなどでエラーが発生したケースについても確認すれば、AIエージェントの成功率・エラー率などを算出するための材料になりそうです。

プロンプトと回答も取得できる

Langfuseには、メタデータだけでなくLLMへの入力と回答も記録されていました。

例えば入力として、

peopleテーブルからageが40以上のアイテムを出力してください。

が入り、出力として、

SELECT *
FROM people
WHERE age >= 40;

が記録されています。

つまりLangfuseを利用すると、

実行回数
成功 / 失敗
使用LLM
Token数
入力
出力
Conversation
Trace

などをまとめて確認できそうです。

一方で、ユーザー入力やLLM出力には個人情報や企業の機密情報が含まれる可能性があります。

実際の企業システムで利用する場合には、「どこまでLangfuseへ送るのか」「どのくらいの期間保存するのか」「誰が閲覧できるのか」といったセキュリティ・データガバナンスも考える必要があります。

今回はAgentではなくテキスト生成アプリ

今回の検証で重要だったのが、

"agent_based": "false"

という情報です。

今回使用した「SQLクリエイター」はDifyの「テキスト生成」アプリだったため、厳密にはAI Agentではありません。

そのため今回確認できたのは、

User
 ↓
Dify Text Generation App
 ↓
LLM
 ↓
Answer

という比較的シンプルな実行です。

本格的なAI Agentでは、

User
 ↓
Agent
 ├─ LLM
 ├─ Tool
 ├─ LLM
 └─ Tool
 ↓
Answer

のように、1回の実行の中で複数のLLM呼び出しやTool Callが発生します。

この場合、Langfuse上でどのようなTrace / Span構造として記録されるのかは、今後確認してみたいところです。

まとめ

今回DifyとLangfuseを実際に連携してみて、Difyの標準機能だけで比較的簡単に実行トレースをLangfuseへ送信できることが確認できました。

特に以下の情報を取得できました。

  • Dify App ID

  • Conversation ID

  • Message ID

  • Langfuse Trace ID

  • 使用したLLM

  • Input / Output

  • Token数

  • 実行ステータス

  • エラー情報

また、

Dify message_id = Langfuse traceId
Dify conversation_id = Langfuse sessionId

という対応関係も確認できました。

さらに、トークン数についてはDifyからLangfuseへ渡っていることが確認できました。一方、今回取得したSpanではtotalCostは0となっており、Langfuse側でコストが算出された状態にはなっていませんでした。

独自の監視システムを構築する場合には、

Langfuse
 ├─ app_id
 ├─ model
 ├─ token数
 ├─ status
 ├─ input / output
 └─ Trace情報
       ↓
独自監視システム
       ↓
モデル料金表と照合
       ↓
LLMコスト算出

という構成も考えられそうです。

次はDifyのAgentアプリを作成し、Tool Callを含む実行を行って、

Trace
 ├─ Agent
 ├─ LLM
 ├─ Tool
 └─ LLM

のような内部構造をLangfuseでどこまで取得できるのか、またLLM GenerationレベルではToken UsageやCostがどのように記録されるのかを確認してみたいと思います。

LangGraph+LM StudioでローカルAIエージェントを作り、Langfuseで監視してみた

AIエージェントの仕組みを理解するため、Windows 11上に簡単なAIエージェントを作成してみました。

今回は以下の構成で、Dockerコンテナ上で動くPython+LangGraphから、Windowsホスト上のLM StudioのローカルLLMを呼び出します。

さらにTool Callingを追加し、最後にLangfuse Cloudを使ってAIエージェント内部の処理をTraceとして可視化してみました。

今回の構成

使用した環境は以下です。

  • OS:Windows 11

  • 開発環境:VS Code

  • コンテナ環境:Docker Desktop

  • 言語:Python 3.12

  • AIエージェント:LangGraph

  • LLM:LM Studio

  • モデル:Qwen3 1.7B

  • Observability:Langfuse Cloud

全体構成は以下のようになります。

Windows 11
│
├─ LM Studio
│    └─ Qwen3 1.7B
│         └─ OpenAI互換API :1234
│
└─ Docker Desktop
     │
     └─ Python Container
          ├─ LangGraph
          ├─ LangChain
          └─ AI Agent
               │
               ├──→ LM Studio
               │       LLM実行
               │
               └──→ Langfuse Cloud
                       Trace送信

DockerコンテナからWindowsホスト上のLM Studioへは、host.docker.internal を使ってアクセスします。

ファイル構成

今回作成したファイルは以下の3つです。

local-agent/
├─ app.py
├─ requirements.txt
└─ Dockerfile

requirements.txt

使用したPythonパッケージです。

langgraph
langchain
langchain-openai
langfuse

AIエージェントの仕組み

最初は単純にLLMと会話するだけのチャットアプリを作成しました。

その後、AIエージェントらしい動きを確認するため、

get_current_time()

という現在時刻を取得するToolを追加しました。

例えば、

今何時ですか?

と質問すると、内部では次のように処理されます。

User
 ↓
LLM
 ↓
Tool Call: get_current_time()
 ↓
LangGraph / ToolNode
 ↓
Python関数を実行
 ↓
Toolの実行結果
 ↓
LLM
 ↓
最終回答

ここで重要なのは、LLM自身がPython関数を実行しているわけではないという点です。

LLMは、

get_current_timeというToolを使いたい

と判断し、tool_calls として「どのToolを、どの引数で呼び出したいか」を返します。

実際にPython関数を実行するのはLangGraph側です。

役割を大まかに分けると、

LLM
 ↓
何をするか判断する

LangGraph
 ↓
処理の流れを制御する

Tool
 ↓
実際の処理を実行する

という関係になります。

app.py

今回使用した app.py は以下のようになりました。

from datetime import datetime
from zoneinfo import ZoneInfo

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool

from langgraph.graph import StateGraph, MessagesState, START
from langgraph.prebuilt import ToolNode, tools_condition

from langfuse.langchain import CallbackHandler


# -----------------------------
# Tool
# -----------------------------
@tool
def get_current_time() -> str:
    """現在の日本時間を取得します。"""

    print(">>> get_current_time() が実行されました")

    now = datetime.now(
        ZoneInfo("Asia/Tokyo")
    )

    return now.strftime("%Y-%m-%d %H:%M:%S")


# -----------------------------
# LM Studio
# -----------------------------
llm = ChatOpenAI(
    model="qwen/qwen3-1.7b",
    base_url="http://host.docker.internal:1234/v1",
    api_key="lm-studio",
    temperature=0.7,
)


# LLMに使用可能なToolを登録
tools = [get_current_time]

llm_with_tools = llm.bind_tools(tools)


# -----------------------------
# LangGraph Node
# -----------------------------
def call_llm(state: MessagesState):

    response = llm_with_tools.invoke(
        state["messages"]
    )

    # LLMがTool Callを要求した場合に表示
    if response.tool_calls:

        print("\n=== LLM Tool Calls ===")

        for tool_call in response.tool_calls:
            print(f"Tool Name : {tool_call['name']}")
            print(f"Arguments : {tool_call['args']}")
            print(f"Call ID   : {tool_call['id']}")

        print("======================\n")

    return {
        "messages": [response]
    }


# -----------------------------
# LangGraph
# -----------------------------
builder = StateGraph(MessagesState)

builder.add_node(
    "llm",
    call_llm
)

builder.add_node(
    "tools",
    ToolNode(tools)
)

builder.add_edge(
    START,
    "llm"
)

# Tool Callの有無によって分岐
builder.add_conditional_edges(
    "llm",
    tools_condition
)

# Tool実行後は再びLLMへ
builder.add_edge(
    "tools",
    "llm"
)

graph = builder.compile()


# -----------------------------
# Langfuse
# -----------------------------
langfuse_handler = CallbackHandler()


# -----------------------------
# Chat Application
# -----------------------------
def main():

    print("=== Local AI Agent ===")
    print("終了するには exit と入力してください。")
    print()

    messages = []

    while True:

        user_input = input("You: ")

        if user_input.lower() in ["exit", "quit"]:
            print("Bye!")
            break

        messages.append(
            HumanMessage(content=user_input)
        )

        result = graph.invoke(
            {
                "messages": messages
            },
            config={
                "callbacks": [langfuse_handler]
            }
        )

        ai_message = result["messages"][-1]

        print(f"AI: {ai_message.content}")
        print()

        # LangGraph内部で追加されたメッセージも含めて保持
        messages = result["messages"]


if __name__ == "__main__":
    main()

Tool Callを確認する

上記コードでは、LLMから返された tool_calls をコンソールにも表示しています。

例えば、

You: 今何時ですか?

と入力すると、以下のような情報が表示されます。

=== LLM Tool Calls ===
Tool Name : get_current_time
Arguments : {}
Call ID   : call_xxxxx
======================

>>> get_current_time() が実行されました

AI: 現在時刻は...

これによって、

LLM
 ↓
「get_current_timeを使う」と判断
 ↓
tool_callsを返す
 ↓
LangGraph / ToolNode
 ↓
get_current_time()を実行
 ↓
結果をLLMへ返す
 ↓
LLMが最終回答

という処理を実際に確認できます。

逆に、

日本の首都はどこですか?

のようなToolを必要としない質問では、

User
 ↓
LLM
 ↓
回答

だけで終了します。

つまり、ユーザーからの質問によって、LLM自身がToolを使う必要があるかを判断しています。

Dockerfile

Dockerfileは以下のようにしました。

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

CMD ["python", "app.py"]

LM Studioを起動する

Windows側でLM Studioを起動し、使用するモデルをロードします。

その後、Developer画面からLocal Serverを起動します。

今回の構成では、

http://localhost:1234

でOpenAI互換APIを公開しています。

Windows側から疎通確認する場合は、PowerShellで以下を実行します。

curl.exe http://localhost:1234/v1/models

ここで、LM StudioにロードされているモデルIDも確認できます。

Dockerイメージを作成する

PowerShellでプロジェクトディレクトリへ移動します。

cd C:\work\local-agent

Dockerイメージをビルドします。

docker build -t local-agent .

requirements.txt やソースコードを変更した場合も、基本的には再度このコマンドでイメージをビルドします。

DockerからLM Studioへの疎通確認

DockerコンテナからWindowsホスト上のLM Studioへアクセスできることも確認しました。

docker run --rm curlimages/curl `
  http://host.docker.internal:1234/v1/models

Dockerコンテナから見た localhost は、Windowsホストではなくコンテナ自身を指します。

そのため、

http://localhost:1234

ではなく、

http://host.docker.internal:1234

を使用するのがポイントです。

Langfuse Cloudで監視する

次に、AIエージェント内部の動きを確認するためLangfuse Cloudを利用しました。

Langfuse CloudでProjectを作成し、

  • Public Key

  • Secret Key

を取得します。

今回はLangfuse Cloud Japanを利用しました。

Python側では、

from langfuse.langchain import CallbackHandler

langfuse_handler = CallbackHandler()

としてCallbackHandlerを作成します。

そして、LangGraphを実行する際に、

result = graph.invoke(
    {
        "messages": messages
    },
    config={
        "callbacks": [langfuse_handler]
    }
)

とすることで、LangGraphの実行内容がLangfuseへ送信されます。

Docker起動時には、Langfuseの認証情報を環境変数として渡します。

docker run --rm -it `
  -e LANGFUSE_PUBLIC_KEY="<Public Key>" `
  -e LANGFUSE_SECRET_KEY="<Secret Key>" `
  -e LANGFUSE_BASE_URL="https://jp.cloud.langfuse.com" `
  local-agent

Public KeyとSecret Keyは実際に取得したものへ置き換えます。

Secret KeyはソースコードやGitリポジトリには保存しないようにします。

LangfuseでTraceを確認する

例えば、

今何時ですか?

と質問すると、Langfuseでは概ね次のようなTraceを確認できました。

Trace
│
├─ LLM
│    └─ get_current_time のTool Callを要求
│
├─ Tool
│    └─ get_current_timeを実行
│
└─ LLM
     └─ Toolの結果から最終回答を生成

これは、先ほどコンソールで確認した、

LLM
 ↓
Tool Call
 ↓
ToolNode
 ↓
get_current_time()
 ↓
LLM

という流れをLangfuse上で可視化したものです。

Traceから処理時間も分かる

今回実際に取得したTraceでは、「今何時ですか?」という質問に対して、おおよそ以下の時間がかかっていました。

LangGraph全体       約29.1秒

1回目のLLM         約 9.4秒
Tool               約 1ms
2回目のLLM         約19.7秒

非常に興味深かったのは、get_current_time() 自体は約1msしかかかっていなかったことです。

つまり、約29秒という処理時間のほとんどをローカルLLMの推論が占めていました。

LangfuseのようなObservabilityツールを使うと、

「AIエージェントが遅い」
          ↓
なぜ遅い?
          ↓
LLM?
Tool?
その他の処理?

という切り分けができます。

これはAIエージェントを監視する上で重要なポイントだと思います。

Token数も確認できる

Langfuseでは、各LLM呼び出しのToken数も確認できます。

Toolを使う質問では、

LLM
 ↓
Tool
 ↓
LLM

とLLMが複数回呼び出されます。

そのため、単純なチャットよりToken消費量が増えることも確認できました。

また、今回のプログラムでは、

messages = result["messages"]

として過去の会話履歴を保持しています。

そのため、

1回目
User → LLM

2回目
過去の会話 + User → LLM

3回目
さらに過去の会話 + User → LLM

となり、会話が長くなるにつれてInput Tokenが増えていくこともLangfuse上で確認できました。

Traceの単位について

今回のプログラムでは、ユーザーが質問するたびに、

graph.invoke(...)

を実行しています。

そのため、基本的には、

チャットセッション
│
├─ 「今何時?」
│      └─ Trace #1
│
├─ 「日本の首都は?」
│      └─ Trace #2
│
└─ 「Pythonについて教えて」
       └─ Trace #3

という関係になります。

つまり、今回の構成では、

1回の graph.invoke() ≒ 1 Trace

と考えると分かりやすそうです。

チャット全体とTraceは1対1ではありません。

今後はLangfuseのSession IDを設定して、

Session
│
├─ Trace #1
├─ Trace #2
└─ Trace #3

という形で、複数のTraceを1つのチャットセッションとしてまとめてみたいと思います。

今回やってみて分かったこと

今回、実際に小さなAIエージェントを作ってLangfuseで観察したことで、AIエージェント内部の役割分担がかなり分かりやすくなりました。

大まかには、

User
 ↓
LangGraph
 ↓
LLM
 │
 │ 何をするか判断
 │
 ├── 普通に回答
 │
 └── Tool Call
        ↓
      Tool
        │
        │ 実際の処理を実行
        ↓
       LLM
        ↓
      最終回答

という構造になっています。

そしてLangfuseは、その外側から、

・どの経路を通ったか
・LLMを何回呼んだか
・どのToolを呼んだか
・各処理に何秒かかったか
・Tokenをどの程度使ったか

といった情報をTraceとして可視化してくれます。

今回の実験を通して、AIエージェントの基本的な処理は、

「LLMが判断し、アプリケーションが実行し、その一連の流れをTraceとして監視する」

と捉えると理解しやすいと感じました。

また、「AIエージェントを監視する」と言っても、単にエージェントが成功したか失敗したかを見るだけではなく、LLM CallやTool CallまでTraceを掘り下げることで、遅延・エラー・Token消費などの原因を切り分けられることも、今回の実験で実感できました。

今後はSession IDの設定やToolの追加などを試し、もう少し複雑なAIエージェントの動きをLangfuseで観察してみたいと思います。

LLMアプリのLLMコストの算出

ChatGPTに纏めてもらった。検証等はしていないが、Gemini も同じような回答だったので、たぶん、これで正しいはず。

 

最近のLLM APIでは、応答に入力・出力トークン数などの使用量(Usage情報)が含まれており、AIエージェントはこれを利用して各LLM呼び出しのトークン数を取得できます。監視ソフトでは、このUsage情報をRun IDと紐付けて収集・集計することで、エージェントごとのトークン使用量を可視化できます。

また、LLM APIは通常、実際の利用料金までは返さないため、監視ソフト側でモデルごとの料金表を持ち、トークン数から推定コストを算出するのが一般的です。料金はモデルや入力・出力で異なり、将来的に変更されることもあるため、料金表には適用期間(バージョン)を持たせる設計が望ましいでしょう。

なお、算出される金額はあくまで推定値です。実際の請求額は、個別契約や割引、キャッシュ機能、為替レートなどの影響を受けるため、監視画面では「推定コスト(Estimated Cost)」として表示するのが適切です。

IDEがいらなくなる?

これまで、開発するならIDEは必須であり、その使い方で差が出たりしたのだが、そういう時代ではなくなるのか。スピードが速くて中年にはちょっとついていくのが辛い。。

 

2027年までにAIエージェントでコーディングを行うチームの65%が、IDEが必要不可欠だとは考えなくなる。ガートナーの予想 - Publickey

Geminiエージェント監視

最近、GoogleのGeminiを利用したAIエージェントの監視方法について調査したので整理してみました。(chatGPTとの議論内容を纏めてもらった)

GeminiはエージェントではなくLLM

まず重要なのは、Geminiは基本的に「AIエージェントそのもの」ではなく、エージェントが利用するLLM(大規模言語モデル)であることです。

イメージとしては以下のような構成になります。

AIエージェント
 ├─ プロンプト
 ├─ メモリ
 ├─ ワークフロー
 ├─ Tool
 └─ Gemini

Geminiは推論やツール選択を行うエンジンとして利用されます。


Googleの標準的な構成

現在のGoogleの方向性としては、以下の構成が中心になりつつあります。

ADK
 ↓
Agent Engine
 ↓
Gemini

それぞれの役割は以下の通りです。

ADK (Agent Development Kit)

エージェントを開発するためのフレームワークです。

  • Pythonで開発可能

  • Git管理が容易

  • CI/CDと相性が良い

  • テストしやすい

LangGraphやSemantic Kernelに近い位置付けです。

Agent Engine

作成したエージェントを実行するためのマネージド実行基盤です。

役割としては、

  • AKS

  • GKE

  • Cloud Run

などに近いイメージです。

ADKで作成したエージェントをAgent Engine上で実行し、その中でGeminiを利用します。

Gemini

エージェント内で利用されるLLMです。

  • 推論

  • 計画立案

  • Tool Call判断

  • 回答生成

などを担当します。


監視ソフトは何を監視するのか

「Geminiエージェントを監視したい」と言われた場合、実際にはGeminiそのものを監視するわけではありません。

監視対象は以下のような実行情報になります。

Agent
 ↓
Run
 ↓
Step
 ↓
LLM Call
 ↓
Tool Call

例えば、

ユーザ
 ↓
売上レポートを作成
 ↓
Geminiが計画作成
 ↓
BigQuery実行
 ↓
Geminiが分析
 ↓
PDF生成
 ↓
完了

といった一連の流れを監視します。


監視ソフトで表示したい項目

監視ソフトでは、以下のような情報が重要になります。

項目 内容
Agent どのエージェントか
Run 実行回数
Status 成功・失敗
Duration 実行時間
LLM Calls LLM呼び出し回数
Tokens 入力・出力トークン数
Tool Calls 実行されたツール
Errors エラー内容
Cost 推定利用料金

監視データの取得方法

代表的な方法は以下の3つです。

1. Cloud Logging

Agent Engine
 ↓
Cloud Logging
 ↓
Pub/Sub
 ↓
監視ソフト

Google Cloudの標準機能を利用してログを収集します。

2. OpenTelemetry

ADK
 ↓
OpenTelemetry
 ↓
Collector
 ↓
監視ソフト

トレース情報を取得できます。

3. SDK組み込み

エージェント側に監視用SDKを組み込み、実行情報を直接送信する方式です。

最も詳細な情報を取得できます。


まとめ

現在のGoogleのAIエージェント基盤は、

ADK
 ↓
Agent Engine
 ↓
Gemini

という構成が主流になりつつあります。

そのため、「Geminiエージェント監視」と言っても、実際に監視すべき対象はGeminiではなく、

  • エージェント

  • 実行履歴(Run)

  • LLM呼び出し

  • Tool Call

  • エラーやコスト

といった運用情報です。

監視ソフトを設計する際は、「Gemini専用監視」ではなく、「エージェント実行基盤の監視」として考えると、Google系のエージェントにも柔軟に対応しやすくなります。

Microsoft Copilot / Copilot Studio / Agent Builder / Application Insights の関係

以下、chatGPT に纏めてもらったもの。

概要

Microsoft の AI エージェント関連製品は以下のような関係になっている。

利用者
  ↓
Microsoft Copilot
  ↓
Agent Builder / Copilot Studio
  ↓
Copilotエージェント
  ↓
Application Insights
  • Microsoft Copilot

    • 利用者向けのAIアシスタント

    • ChatGPTに近い位置付け

  • Copilot Studio

    • AIエージェントを作成するための開発基盤

    • Dify に近い位置付け

  • Agent Builder

    • Copilot Studio の簡易版・個人向け版

    • 作成したエージェントは内部的に Copilot Studio のエージェントとして扱われる

  • Copilotエージェント

    • Copilot Studio や Agent Builder で作成された実際のAIエージェント

  • Application Insights

    • Azure Monitor の監視サービス

    • エージェントの利用状況やエラー情報を収集する


Agent Builder のエージェントは監視できるか

結論として、

Agent Builder で作成したエージェントも、Application Insights を利用して監視可能。

Agent Builder のエージェントは内部的に Copilot Studio エージェントとして動作するため、Application Insights と連携できる。


Application Insights で取得できる主な情報

取得しやすい情報

  • 会話数

  • 利用者数

  • 応答時間

  • エラー数

  • Topic 実行回数

  • カスタムイベント

取得が難しい情報

  • LLM のトークン数

  • 消費クレジット

  • 中間推論(Reasoning)

  • 詳細な Tool Call パラメータ

  • RAG 検索時の全検索結果


監視ソフトとの連携

Copilot 系エージェントを監視する場合は、以下の構成が現実的。

Copilot Agent
      ↓
Application Insights
      ↓
監視ソフト

監視ソフトは Application Insights から情報を取得することで、

  • 稼働状況

  • エラー状況

  • 応答時間

  • 利用状況

などを監視できる。


注意点

Application Insights は自動で有効になるわけではない。

顧客側で Application Insights の設定を行っていない場合、

Copilot Agent
      ↓
(ログなし)
      ↓
監視ソフト

となり、監視ソフトは監視情報を取得できない。

そのため、Copilot 系エージェントを監視する場合は、

「Application Insights の設定が必須かどうか」

を事前に確認しておくことが重要である。