はじめに
Genie CodeとGenie Oneのやり取りを、Unity Gatewayの統合トレーステーブルに記録できるようになりました。現時点ではベータです。
統合トレーステーブルについては、以前の記事で試しました。そのときに見ていたのは、Unity Gatewayを経由したモデルやMCPの呼び出しでした。今回はそこに、Genie CodeとGenie Oneの1回のやり取りがまるごと入ります。誰が何を聞き、Genieが裏でどのSQLを実行し、何を返したのか。それをUnity Catalogのテーブルとして、SQLで追えるようになります。
実際に有効にしてみると、トレースが入るまでに少し手間取りました。フィードバックを記録するための設定も別に必要でした。そのあたりも含めて書いていきます。
Genieのトレースに記録されるもの
Genie CodeとGenie Oneでは、1回のやり取り (ターン) が1つのトレースになります。トレースの中には、次のようなスパンが入ります。
| スパン | 内容 | 対象 |
|---|---|---|
| ルートスパン | ターン全体。プロンプト、回答、結果、所要時間 | 両方 |
| ツールのスパン | SQLの実行やダッシュボードの編集など、1回のツール呼び出し | 両方 |
| モデル呼び出しのスパン | 1回のモデル呼び出し | Genie Codeのみ |
トレースは、Unity Gatewayのほかのトラフィックと同じ統合トレーステーブルに入ります。どちらの製品のトレースかは、service_name で見分けます。
| 製品 | service_name |
|---|---|
| Genie Code | genie-code-agent |
| Genie One | Genie One |
高評価・低評価のフィードバックは、トレーステーブルとは別の注釈テーブルに入ります。注釈テーブルの名前は、トレーステーブルの名前の _otel_spans を _otel_annotations に置き換えたものです。
ドキュメントによると、Genie Agent (旧Genieスペース) との会話はトレースされません。システムプロンプトやコストも、トレースには含まれません。
有効にする
前提
ドキュメントでは、次の4つがそろっている必要があるとされています。
- アカウントでUnity Gatewayが有効になっている
- アカウント管理者が、アカウントコンソールのプレビューで「Enhanced Unity Gateway」を有効にしている
- メタストア管理者が、統合トレーステーブルを設定している
- ワークスペース管理者が、Genie OneトレースとGenie Codeトレースのプレビューを有効にしている
今回のワークスペースでは、統合トレーステーブルはすでに設定済みでした。そのため、残りはワークスペースのプレビューだけです。
プレビューを2つオンにする
ワークスペースのプレビューページで、Genie OneトレースとGenie Codeトレースの両方をオンにします。それぞれ別のプレビューなので、トレースしたい製品の分をオンにします。
ドキュメントにあるとおり、記録されるのはオンにしたあとのターンだけです。それより前のやり取りは、さかのぼって記録されません。
すぐには記録されなかった
ここが最初のハマりどころです。プレビューをオンにしてすぐ、Genie OneとGenie Codeで何度かやり取りしましたが、トレーステーブルには1件も入りませんでした。トレーステーブル自体は動いていて、ほかのモデル呼び出しのトレースは入っている状態です。
しばらく時間を置いてからやり取りすると、Genie Oneのトレースが入るようになりました。Genie Codeも同じで、さらに時間を置いたやり取りから記録が始まりました。
オンにしてから何分経てば記録されるのかは、今回は確かめられていません。ドキュメントでも、配信はベストエフォートとされています。有効にした直後に入らなくても、少し時間を置いてから試し直すのがよさそうです。
Unity Gatewayの画面で見る
Unity Gatewayの「ガバナンス」にある「トレース」タブで、サービスの種類を「Genie」に絞り込むと、Genieのトレースだけが一覧に出ます。
サービスの列には、「Genie Code」と「Genie One」が表示されています。プリンシパルの列に出るのは、やり取りしたユーザーです。
トレースを開くと、スパンが時刻の順に並びます。次は、Genie Codeに「ダッシュボードのテーマを整えて」と頼んだターンです。
モデルの呼び出し (chat) が2回あり、そのあとでダッシュボードを編集するツール (execute_tool edit_dashboard) を2回呼んでいます。最後にもう一度モデルを呼んで、回答を書いています。右側には、入力のプロンプトと出力の回答が表示されます。
SQLで1ターンを読み解く
ここからが本題です。画面で見られる内容は、テーブルのSQLでも読めます。SQLで読めれば、ターンをまとめて集計したり、条件で絞り込んだりできます。
ターン単位に整える
ドキュメントには、ルートスパンだけを取り出して1行1ターンに整える genie_turns ビューの定義が載っています。今回はビューを作らず、同じ定義を WITH 句に埋め込んで使いました。
ビューを作らなかったのには理由があります。ドキュメントにもあるとおり、このビューはトレーステーブルを、ビューの所有者の権限で読み取ります。そのため、ビューへの SELECT を誰かに付与すると、その人はビューが返すトレースをすべて読めるようになります。統合トレーステーブルのスキーマには、ほかの人のトレースも入っています。共有のスキーマにビューを置く前に、誰がそのビューを読めるのかを確かめておく必要があります。
WITH turns AS (
SELECT
trace_id,
time,
CASE WHEN service_name = 'genie-code-agent' THEN 'Genie Code' ELSE 'Genie One' END AS product,
-- 名前にドットを含む属性は、ブラケットで指定する
attributes:['enduser.id']::string AS requester,
attributes:['gen_ai.input.messages']::string AS prompt,
attributes:['gen_ai.output.messages']::string AS response,
attributes:['databricks.genie.channel']::string AS channel,
attributes:['databricks.genie_code.run.outcome']::string AS run_outcome,
status.code AS status_code,
(end_time_unix_nano - start_time_unix_nano) / 1e6 AS duration_ms
FROM <catalog>.<schema>.<table_name>
-- ルートスパンだけを取り出すと、1行が1ターンになる
WHERE (parent_span_id IS NULL OR parent_span_id = '')
AND service_name IN ('genie-code-agent', 'Genie One')
-- テーブルにはほかの人のターンも入っているので、自分のターンに絞る
AND attributes:['enduser.id']::string = current_user()
)
SELECT * FROM turns ORDER BY time DESC;
<table_name> には、統合トレーステーブルの名前を入れます。今回の環境では unity_gateway_otel_spans でした。
前回の記事では、モデルサービスのスパンには parent_span_id が入っていて、parent_span_id IS NULL ではリクエストを取り出せませんでした。Genieのトレースでは、この条件でターンごとのルートスパンが取り出せています。また、名前にドットを含む属性は、ドキュメントのとおり attributes:['enduser.id'] のようにブラケットで指定すると値が取れました。
プロンプトと回答はメッセージのJSONで入っている
ここで少しつまずきました。prompt と response には、プロンプトや回答の文章がそのまま入っているわけではありません。メッセージの配列をJSONにした文字列が入っています。
[{"role":"user","parts":[{"type":"text","content":"samples.nyctaxi.trips で平均乗車距離が一番長い曜日は?"}]}]
回答のほうはさらに複雑で、配列の中にツールの呼び出し (tool_call) とテキスト (text) が混ざっています。先頭だけを表示すると、ツールの呼び出しの部分しか見えません。
そこで、type が text の部分だけを取り出しました。回答は最後の text を、最終的な回答とみなしています。
SELECT
time, product, channel, run_outcome,
ROUND(duration_ms / 1000, 1) AS duration_sec,
try_element_at(filter(flatten(transform(
from_json(prompt, 'array<struct<role:string,parts:array<struct<type:string,content:string>>>>'),
m -> m.parts)), p -> p.type = 'text'), 1).content AS prompt_text,
-- 回答の配列にはツール呼び出しも入るので、最後の text を最終的な回答とみなす
try_element_at(filter(flatten(transform(
from_json(response, 'array<struct<role:string,parts:array<struct<type:string,content:string>>>>'),
m -> m.parts)), p -> p.type = 'text'), -1).content AS answer_text,
trace_id
FROM turns
ORDER BY time DESC;
これで、質問と回答を文章として読めるようになります。
| product | channel | run_outcome | duration_sec | prompt_text |
|---|---|---|---|---|
| Genie Code | (空) | success |
31.7 | ダッシュボードのテーマを整えて |
| Genie One | webapp |
(空) | 42.3 | samples.nyctaxi.trips で平均乗車距離が一番長い曜日は? |
Genie Oneでは channel に webapp が入り、Genie Codeでは run_outcome に success が入っていました。製品によって、入る属性が少し違います。
Genie Oneの1ターン: 質問から回答まで
Genie Oneのターンを開いてみます。trace_id で絞り、スパンを時刻の順に並べます。
SELECT
name,
attributes:['gen_ai.tool.name']::string AS tool_name,
-- 引数はJSON文字列で、日本語はエスケープされた形で入っている。JSONとして読むと元の文字に戻る
COALESCE(
from_json(attributes:['gen_ai.tool.call.arguments']::string, 'map<string,string>')['query'],
attributes:['gen_ai.tool.call.arguments']::string
) AS tool_arguments,
attributes:['gen_ai.tool.call.result']::string AS tool_result,
status.code AS status_code,
ROUND((end_time_unix_nano - start_time_unix_nano) / 1e6) AS duration_ms
FROM <catalog>.<schema>.<table_name>
WHERE trace_id = '<trace_id>'
ORDER BY start_time_unix_nano;
ツールの引数は、そのまま読むと日本語が 日曜日 のようにエスケープされた形で出ます。from_json でJSONとして読み直すと、元の文字に戻ります。
結果は3行でした。
| name | tool_name | duration_ms |
|---|---|---|
invoke_agent GenieOne |
42264 | |
execute_tool execute_sql_query |
execute_sql_query |
1654 |
execute_tool execute_sql_query |
execute_sql_query |
1186 |
Genie OneはSQLを2回実行していました。1回目は、samples.nyctaxi.trips 全体で曜日ごとの平均乗車距離を出すSQLです。
SELECT dayofweek(`tpep_pickup_datetime`) AS weekday_num,
CASE dayofweek(`tpep_pickup_datetime`) WHEN 1 THEN '日曜日' WHEN 2 THEN '月曜日' WHEN 3 THEN '火曜日' WHEN 4 THEN '水曜日' WHEN 5 THEN '木曜日' WHEN 6 THEN '金曜日' WHEN 7 THEN '土曜日' END AS weekday,
ROUND(AVG(`trip_distance`), 3) AS avg_trip_distance,
COUNT(*) AS trips
FROM `samples`.`nyctaxi`.`trips`
GROUP BY 1, 2
ORDER BY avg_trip_distance DESC
この結果では、月曜日が2.987マイルで1位でした。2回目のSQLでは、乗車距離と運賃に条件を付けて外れ値を除いています。こちらの結果では、日曜日が2.276マイルで1位になり、月曜日は6位に下がっていました。ツールの結果の列には、どちらのSQLについても、返ってきた7行の表がMarkdownの形で入っています。
最終的な回答を読むと、この2つの結果が両方使われていました。回答に書かれていたのは次の3点です。
- 曜日による差は小さい
- 外れ値を除くと順位が入れ替わる
- 月曜日の平均は、一部の長距離の乗車に押し上げられている可能性が高い
回答の文章だけを見ても、どういう計算でこの結論になったのかは分かりません。トレースを見れば、どのSQLを実行し、どんな結果を受け取って回答を書いたのかを、1ターンの中でたどれます。誤った回答の原因を調べるときに、これが一番効いてくる部分だと思います。
Genie Codeの1ターン: モデルとツールの呼び出し
Genie Codeのターンを、同じSQLで開きます。
| name | tool_name | tool_arguments | duration_ms |
|---|---|---|---|
invoke_agent genieCodeAgent |
31689 | ||
chat |
9041 | ||
chat |
8928 | ||
execute_tool edit_dashboard |
edit_dashboard |
{"dashboard_id":"01f1d80a..."} |
534 |
execute_tool edit_dashboard |
edit_dashboard |
{"dashboard_id":"01f1d80a..."} |
182 |
chat |
7340 |
Genie Codeには、Genie Oneにはなかったモデル呼び出しのスパン (chat) が入っています。ターン全体の31.7秒のうち、大半はモデルの呼び出しでした。時間はいずれも今回の環境で1回測っただけの値です。
edit_dashboard の引数と結果には、ダッシュボードのIDと {"status":"edited"} が入っていました。どのダッシュボードを編集したかはトレースから分かります。配色やフォントをどう変えたのかは、回答の本文に書かれています。
ツールごとの呼び出し回数
ツールのスパンを、製品とツール名ごとに数えます。
SELECT
CASE WHEN service_name = 'genie-code-agent' THEN 'Genie Code' ELSE 'Genie One' END AS product,
attributes:['gen_ai.tool.name']::string AS tool_name,
COUNT(*) AS calls,
COUNT_IF(status.code = 'STATUS_CODE_ERROR') AS failed_calls
FROM <catalog>.<schema>.<table_name>
WHERE (
(service_name = 'Genie One' AND name LIKE 'execute_tool %')
-- 今回の環境では、Genie Code のツールのスパンも execute_tool で始まる名前だった
OR (service_name = 'genie-code-agent' AND (name LIKE 'tool.execute:%' OR name LIKE 'execute_tool %'))
)
-- ツールのスパンには依頼者の属性がないので、自分のターンの trace_id で絞る
AND trace_id IN (SELECT trace_id FROM turns)
GROUP BY ALL
ORDER BY calls DESC;
ドキュメントのSQLの例では、Genie Codeのツールのスパンを tool.execute: で始まる名前で数えています。今回の環境では、Genie Codeのツールのスパンは execute_tool edit_dashboard という名前でした。例のSQLのままだとGenie Codeの分が0件になったので、両方の名前を数えています。
| product | tool_name | calls | failed_calls |
|---|---|---|---|
| Genie One | execute_sql_query |
2 | 0 |
| Genie Code | edit_dashboard |
2 | 0 |
ターンの数が増えてくれば、どのツールがよく使われ、どのツールでよく失敗しているかを、この集計で追えます。
フィードバックをトレースにつなげる
フィードバックのロギングは別にオンにする
ここが2つ目のハマりどころです。Genie Oneの回答に低評価を付けたあとで注釈テーブルを探しましたが、スキーマにテーブル自体がありませんでした。
Unity Gatewayのトレースタブを開くと、上部に「フィードバックのロギングを有効にする」というバナーが出ていました。
ここをオンにすると、注釈テーブル (unity_gateway_otel_annotations) が、トレーステーブルと同じスキーマに作られました。オンにする前に付けていた低評価は、注釈テーブルに入っていませんでした。オンにしたあとで付け直した低評価は記録されています。今回の環境では、Genieのトレースのプレビューとは別に、トレースタブでフィードバックのロギングもオンにする必要がありました。
なお、注釈テーブルはトレーステーブルと同じスキーマに作られます。トレーステーブルを自分以外の人が管理している場合は、オンにする前に管理者に確認しておくのがよいと思います。
低評価のターンを取り出す
注釈テーブルの target_id には、フィードバックを付けたターンの trace_id が入ります。value は低評価なら false です。
SELECT target_id, name, value, annotation_change_type, updated_at
FROM <catalog>.<schema>.<annotations_table_name>
ORDER BY updated_at DESC;
| target_id | name | value | annotation_change_type |
|---|---|---|---|
a2a6ad58... |
user_feedback |
false |
insert |
target_id は、先ほどのGenie Oneのターンの trace_id と一致しています。そのため、ターンの一覧と結合すれば、低評価を付けたターンを質問と回答と一緒に取り出せます。
WITH feedback AS (
SELECT
target_id AS trace_id,
value::boolean AS thumbs_up,
annotation_change_type,
updated_at,
-- 同じフィードバックが更新されることがあるので、最新の状態だけを使う
ROW_NUMBER() OVER (PARTITION BY annotation_id ORDER BY updated_at DESC) AS rn
FROM <catalog>.<schema>.<annotations_table_name>
WHERE name = 'user_feedback' AND target_type = 'TRACE'
)
SELECT f.updated_at AS feedback_at, t.product, t.prompt, t.response, t.trace_id
FROM feedback f
JOIN turns t ON t.trace_id = f.trace_id
WHERE f.rn = 1
AND f.annotation_change_type <> 'delete'
AND NOT f.thumbs_up;
これで、低評価を付けた「平均乗車距離が一番長い曜日は?」のターンが取り出せます。取り出した trace_id でスパンを開けば、その回答の裏で実行された2本のSQLとその結果まで追えます。低評価が付いた回答から、どのSQLが原因だったのかまでを、テーブルだけでたどれるということです。
トレーステーブルを読める人に注意する
最後に、運用する側として気をつけたい点です。
今回のトレーステーブルには、自分以外の人のGenieのやり取りも入っていました。プロンプトと回答は全文で記録されています。先ほどのSQLで current_user() に絞ったのは、このためです。
ドキュメントによると、行フィルターがない状態では、トレーステーブルの SELECT 権限を持つ人は、メタストアにつながるすべてのワークスペースのトレースを読めます。トレーステーブルを共有する場合は、ABACの行フィルターで workspace_id などに応じて読める範囲を絞ることが推奨されています。
Genieのトレースを有効にすると、Genieとのやり取りの中身がこのテーブルに入るようになります。有効にする前に、誰がこのテーブルを読めるのか、保持期間はどうなっているのかを確認しておくのがよいと思います。
まとめ
Genie CodeとGenie Oneのトレースを、統合トレーステーブルで試して分かったことをまとめます。
- Genie CodeとGenie Oneの1ターンが、統合トレーステーブルに1つのトレースとして入る。
service_nameはgenie-code-agentとGenie One - Genie Oneのトレースでは、実行したSQLの全文と結果の表まで読める。回答がどのSQLの結果から書かれたのかを、1ターンの中でたどれる
- Genie Codeのトレースには、モデルの呼び出し (
chat) とツールの呼び出しが時刻の順に並ぶ - プロンプトと回答はメッセージのJSONで入っている。
typeがtextの部分を取り出すと文章として読める - プレビューをオンにしてすぐのやり取りは記録されなかった。時間を置いてからのやり取りが記録された
- フィードバックは、トレースタブで「フィードバックのロギングを有効にする」をオンにしたあとで記録された。オンにする前の低評価は入っていなかった
- 低評価は注釈テーブルに入り、
target_idでトレースと結合できる - 今回の環境では、Genie Codeのツールのスパン名は
execute_toolで始まっていた - トレーステーブルにはほかの人のやり取りも全文で入る。共有するなら行フィルターで読める範囲を絞る
一番の収穫は、低評価を付けた回答から、その裏で実行されたSQLと結果までをテーブルだけでたどれると分かったことでした。回答の文章だけでは、どういう計算でその結論になったのかを確かめられません。トレースが残っていれば、そのときに実際に何が実行されたのかをあとから確かめられます。その分、トレーステーブルには利用者のやり取りの中身がそのまま残ります。有効にする前に、誰がこのテーブルを読めるのかを決めておくことが、この機能を使ううえでの前提になると感じました。
参考リンク
- 統一トレーステーブルで Genie Code と Genie One を監視する
- 統合されたトレーステーブルを使用して、すべての AI アクティビティを監視します
- Unity Gateway の統合トレーステーブルを試す: 計装なしでプロンプトと応答まで記録される




