Datadogのサンプルで LLM Observabilityを体験してみた

## Datadog LLM Observabilityとは
[Datadog](https://www.datadoghq.com/ja/product/)はObservabilityツールで、そのプロダクトに[Datadog LLM Observability](https://www.datadoghq.com/ja/product/llm-observability/)があります。

https://www.datadoghq.com/ja/product/llm-observability/

LLMアプリを分析するためのプロダクトで、トレースの結果やメトリクスを可視化することができます。

また、LLM Observabilityそのものは[こちら](https://qiita.com/MK_Tech/items/4f7dd716560b26699145)で紹介した書籍がすごく分かりやすいです。

https://qiita.com/MK_Tech/items/4f7dd716560b26699145

LLM Observabilityは新しい領域のようで、様々なツールが誕生しています。

https://langfuse.com/

https://www.langchain.com/langsmith

私もLLMを使ったRAGを構築しています。開発中にはアプリの性能をあげるために、ドキュメントを改善するべきか、チャンク戦略を見直すべきか、モデルを変えるべきか、通常のアプリ開発アプローチとは別の視点の評価や判断が必要だなと感じていました。

LLMは進化が早く、より優秀なモデルが誕生し性能やコスト変動も激しいです。

モデルを変更した際に、その判断が良かったのか。アプリの挙動を改善したいとき、どのような指標をもとに何を改善するのか。悩ましいです。

今回はDatadogのLLM Observabilityを試す機会があったので、公式サンプルをベースに体験してみました。

## 公式サンプル
DatadogのリポジトリにはJupyter Notebookから実行してLLM Observabilityを体験できるサンプルが公開されています。

https://github.com/DataDog/llm-observability

それ利用してLLM Observabilityを体験してみます。

### セットアップ
[リポジトリ](https://github.com/DataDog/llm-observability)をダウンロードし、python環境をセットアップ、環境変数を設定するだけで動作します。

#### 環境変数の設定
.envファイルを作成し環境変数を設定します。
```sh
OPENAI_API_KEY=Open AIのAPI Key
DD_API_KEY=DatadogのAPI Key
DD_SITE=ap1.datadoghq.com
DD_LLMOBS_AGENTLESS_ENABLED=1
DD_LLMOBS_ML_APP=onboarding-quickstart
```
アプリはOpen AIを呼び出すのでAPI Keyが必要です。

DD_LLMOBS_AGENTLESS_ENABLEDを指定することでDatadog Agentなしで利用できるようです。

### リポジトリの構成
リポジトリは4つのNoteBookで構成されています。

- 1-llm-span.ipynb
- 2-workflow-span.ipynb
- 3-agent-span.ipynb
- 4-custom-evaluations.ipynb

それぞれためしてみます。

### 1-llm-span.ipynb
このNoteBookでは基本的なLLMスパンのトレースを体験できます。

コード前半で環境変数の読み取りとddtraceのllmobsを有効にしています。
```python
from dotenv import load_dotenv

load_dotenv()

from ddtrace.llmobs import LLMObs

LLMObs.enable()
```
このサンプルではOpen AIのchat.completions.createでLLMを呼び出しています。

```python
oai_client.chat.completions.create(
            messages=messages,
            model="gpt-3.5-turbo",
            response_format={"type": "json_object"},
        )
```


Open AIを利用している場合は、特に追加の作業なしにトレースすることができます。


![01-llm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3831737/88f55ca8-a7c6-47fc-99da-401e82e2b091.png)

実行時間や呼び出したモデル、トークン数やコストが確認できます。

![02-llm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3831737/826dffa2-57ad-4c4a-930f-8b9ccbc2aa3f.png)

モデルへのインプット、モデルからのレスポンスも記録されています。

![03-llm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3831737/f8177df7-dd47-4051-81db-47ec7366fe47.png)

プロンプトも見やすく整理されています。タグ付けもされていますね。

対応した言語やモデルプロバイダの場合は、追加設定なしにトレースできるのはすばらしいですね。

[ドキュメント](https://docs.datadoghq.com/llm_observability/setup/auto_instrumentation/?tab=python)に対応しているプロバイダやモデルの情報があります。

### 2-workflow-span.ipynb
このサンプルでは[function calling](https://platform.openai.com/docs/guides/function-calling)を使ったLLMの関数呼び出しのトレースを体験できます。

DatadogのLLM Observabilityではスパンに種類が定義されており、一連の呼び出しや関数の実行をまとめ可視化することができます。

スパンの設定にはpythonのデコレータを利用することができます。

```python
from ddtrace.llmobs.decorators import *
```

function callingで利用する関数にToolデコレータが指定されおり、関数中にLLMOs.annotateを利用してInputやOutputの内容を指定しています。

```python
@tool()
def fetch_met_urls(query_parameters):
    # We annotate the tool call with input_data here
    LLMObs.annotate(
        input_data=query_parameters,
    )
    response = requests.get(SEARCH_ENDPOINT, params=query_parameters)
    response.raise_for_status()
    object_ids = response.json().get("objectIDs")
    objects_to_return = object_ids[:MAX_RESULTS] if object_ids else []
    urls = [
        f"https://www.metmuseum.org/art/collection/search/{objectId}"
        for objectId in objects_to_return
    ]
    # We annotate the tool call with output_data here
    LLMObs.annotate(
        output_data=urls,
    )
    return urls
```

一連の処理をまとめた関数定義にworkflowデコレータが指定されています。

```python
@workflow()
def find_artworks(question):
    # We annotate the workflow span with input_data here
    LLMObs.annotate(
        input_data=question,
    )
    query = parse_query(question)
    print("Parsed query parameters", query)
    urls = fetch_met_urls(query)
    # We annotate the workflow span with output_data here
    LLMObs.annotate(
        output_data=urls,
    )
    return urls
```

トレース全体がworkflowスパンにまとまっていることが確認できます。

![04-workflow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3831737/574fbf5f-2d95-4b67-9289-25dee8ad50d1.png)

LLM呼び出しも記録されています。

![05-wofkflow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3831737/9351bf29-10ab-4e94-b69f-f9f592bd3c23.png)

関数を実行する際のInput情報やレスポンス、実行時間などが記録されています。

![06-workflow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3831737/a3291abf-ffed-4977-ae6a-6f6ba6c227d0.png)

デコレーターだけで簡単にスパンの種類をつけられるのはよいですね。またスパンの名前も関数名で自動的に記録されています。

簡単に処理をまとめて可視化できます。

スパンの種類は他にもありますが、親スパンにできるものが限られています。詳しくは[ドキュメント](https://docs.datadoghq.com/llm_observability/terms/#span-kinds
)に記載があります。

### 3-agent-span.ipynb
ReActを使ったAgentの実行をトレースを体験できます。コードを見るだけでも勉強になるサンプルです。

ReAct Agentのロジック全体の記載がある関数をworkflowデコレータを指定しています。
```python
@workflow()
def execute_loop_step(messages, calls_left=MAX_CALLS):
```

全体をまとめた関数にagentデコレータを指定しています。
```python
@agent()
def call_weather_assistant(question_prompt):
    LLMObs.annotate(
        input_data=question_prompt,
    )
    messages = get_initial_messages(question_prompt)
    answer = execute_loop_step(messages)
    LLMObs.annotate(
        output_data=answer,
    )
    return answer
```

複雑なReAct Agentの実行がトレースできています。表示が崩れているように見えなくもないですが、新しいプロダクトなのでまだバグがあるのかもしれません。

![07-agent.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3831737/ecb3351f-b372-4c1d-b548-c5b12c7e0db4.png)


Toolが3回実行さています。関数名がわかりやすいですね。

![08-agent.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3831737/6c897c3a-6238-489f-8e97-5f9c926f3a46.png)

Agentのロジックは複雑になりがちなので、スパンで区切って可視化されているとLLMが実行を選択した関数の妥当性や、レスポンスのボトルネックになっている関数などが特定できそうです。

### 4-custom-evaluations.ipynb
最後はRAGのトレースとカスタム評価を体験できます。最後のサンプルはボリューム満点です。

このサンプルでは[llama-index](https://www.llamaindex.ai)でRAGを構築しています。

RAGを呼び出す関数にretrivalデコレータを使用しています。

```python
@retrieval
def retrieve_nodes(query, retriever=base_retriever):
    nodes = retriever.retrieve(query)
    LLMObs.annotate(
        tags={"retriever": retriever.__class__.__name__},
        metadata={"top_k": TOP_K},
        input_data=query,
        output_data=[
            {"text": node.text, "id": node.node_id, "score": node.score}
            for node in nodes
        ],
    )
    return nodes
```

呼び出し全体にworkflowデコレータを指定しています。お約束ですね。

```python
@workflow
def ask_docs(
    query, retriever=base_retriever, response_synthesizer=response_synthesizer_compact
):
    LLMObs.annotate(input_data=query)
    nodes = retrieve_nodes(query, retriever=retriever)
    response = response_synthesizer.synthesize(query, nodes=nodes)
    LLMObs.annotate(output_data=str(response))
    return response, LLMObs.export_span()
```

RAGの呼び出しがまとまって見やすく可視化されています。

![09-rag.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3831737/eeb13419-b52b-4661-bab1-aa174b007d75.png)

検索されたドキュメントの内容やスコアなどがまとめて可視化されて見やすいです。

![10-rag.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3831737/0800d2e8-3e9e-4d50-9e5f-8906f2850dbc.png)

Embeddingのトークンやコストも可視化されています。

![11-rag.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3831737/7a54e882-34cb-4061-a3b8-c32ca8e23f08.png)

また、複数のRAG設定での呼び出し結果の違いを評価するために[RAGAS](https://docs.ragas.io/en/latest/)というツールを使いRAGのカスタム評価をDatadogに送信しています。

![スクリーンショット 2025-02-28 15.08.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3831737/01fa6c77-fa51-45ef-b65d-477d0817319f.png)

RAGアプリを作っていると、質問に対して何でこんな回答なんだっけ？と思うことがあるので、参照したドキュメントやレスポンスが見える化できるのは良いです。

カスタム評価指標も取り込めるので、柔軟なメトリクスの取り込みができそうです。

### イチオシの機能 Trace Cluster Map

[Trace Cluster Map](https://docs.datadoghq.com/llm_observability/cluster_map/)はトレースしたデータをトピックごとにグループ化しクラスタリングできます。

![13-cluster.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3831737/54e8af91-d2e7-48c0-a46e-f1d3ce83109c.png)

ユーザーのインプット、LLMアウトプットをそれぞれ可視化することができ、表示を切り替えることができます。

![スクリーンショット 2025-02-28 17.30.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3831737/5b419f89-3519-4e9f-824e-114e27bd2ede.png)

RAGアプリの開発ではユーザーのインプットの傾向を調べやすく、ユーザーが求めている回答ができておらず、RAGに足りないドキュメントがあることに気がつく方ができました。大変重宝した機能です。

### まとめ
#### よかった点
Datadog LLM Observabilityを利用すると対応している言語やプロバイダであれば最低限のコードで可視化することができ、ツールの恩恵を受けるまでの時間が短縮できました。

普段使い慣れているツールで操作できるのも良く、Dataodgのおしゃれで親しみのあるUIで分析できたのは良かったです。

検証中にもアプリ改善に繋がるインサイトを見つけることができ、今後も必須のツールになるなと感じています。

#### 改善を感じた点

実際に開発していたアプリはJavascriptで構築しており、バックエンドにPythonやLangChainを使っておらず、トレースのための実装が多くなり、導入に苦労しました。

最新のプロダクトなのであまり情報がなかったです。

Pythonが優遇されがちなので、対応言語が増えていくと便利になると思います。

Open AIが前提になっている機能などもあるので、柔軟にプロバイダを選択できることや機能をオミットするなどできると良いと感じます。

### 最後に
他にもたくさんの機能があるのですが、もっとじっくり使ってから記事化できたらと思います。