Menganalisis log percakapan

Dokumen ini menunjukkan cara membuat kueri, mengevaluasi, dan memecahkan masalah log percakapan analisis percakapan di BigQuery menggunakan resep GoogleSQL, notebook Colab interaktif, dan SDK Python BigQuery Agent Analytics. Untuk mengetahui informasi selengkapnya tentang kolom tabel tujuan dan struktur JSON, lihat Skema log percakapan.

Menggunakan notebook Colab dan Python SDK

Anda dapat menjalankan resep SQL dalam dokumen ini bersama dengan bigquery-agent-analytics Python SDK (v0.5.4 atau yang lebih baru) menggunakan Customer Analytics, FinOps & Evaluation Starter Notebook (buka di Colaboratory | lihat di GitHub).

Untuk menggunakan Python SDK dengan tabel log percakapan, buat tampilan kompatibilitas v_bqca_sdk_events terlebih dahulu. Pada baris AGENT_RESPONSE, analisis percakapan menyimpan objek JSON di content.response, sedangkan evaluator LLMAsJudge SDK mengharapkan JSON_VALUE(content, '$.response') menampilkan string skalar. Tampilan ini menetapkan $.text_summary dan $.response ke jawaban teks biasa sambil mempertahankan objek asli di $.raw_response.

Dalam cuplikan SQL dan Python berikut, ganti placeholder ini:

  • PROJECT_ID: ID project yang berisi agen data dan set data log Anda.

  • LOG_DATASET: ID set data BigQuery yang menyimpan tabel log percakapan mentah Anda.

  • LOG_TABLE: ID tabel log percakapan, seperti agent_events.

  • ANALYTICS_DATASET: ID set data BigQuery di lokasi yang sama tempat Anda membuat tampilan analisis dan fungsi tabel.

  • LOCATION: lokasi BigQuery dari set data analisis, seperti US.

Membuat tampilan dan fungsi tabel di ANALYTICS_DATASET memerlukan akses tulis di ANALYTICS_DATASET, seperti peran BigQuery Data Editor (roles/bigquery.dataEditor), dan peran BigQuery Data Viewer (roles/bigquery.dataViewer) di LOG_DATASET. Analis yang hanya mengkueri tampilan memerlukan peran BigQuery Data Viewer di ANALYTICS_DATASET dan peran BigQuery Job User (roles/bigquery.jobUser) di project tempat mereka menjalankan kueri.

Kueri berikut membuat tampilan v_bqca_sdk_events, yang digunakan pada langkah berikutnya untuk memvalidasi skema tabel dan cakupan peristiwa Anda:

CREATE OR REPLACE VIEW `PROJECT_ID.ANALYTICS_DATASET.v_bqca_sdk_events` AS
WITH allowlisted AS (
  SELECT
    *,
    IF(
      event_type = 'AGENT_RESPONSE' AND content IS NOT NULL,
      COALESCE(
        NULLIF(JSON_VALUE(content, '$.text_summary'), ''),
        NULLIF(ARRAY_TO_STRING(ARRAY(
          SELECT JSON_VALUE(part, '$.markdown')
          FROM UNNEST(JSON_QUERY_ARRAY(content, '$.response.parts')) AS part WITH OFFSET AS part_offset
          WHERE JSON_VALUE(part, '$.markdown') IS NOT NULL
          ORDER BY part_offset
        ), '\n\n'), ''),
        NULLIF(JSON_VALUE(content, '$.response.clarifying_question'), ''),
        JSON_VALUE(content, '$.response')
      ),
      NULL
    ) AS answer_text
  FROM `PROJECT_ID.LOG_DATASET.LOG_TABLE`
  WHERE event_type IN (
    'INVOCATION_STARTING', 'USER_MESSAGE_RECEIVED', 'AGENT_RESPONSE',
    'INVOCATION_COMPLETED', 'LLM_RESPONSE', 'EMBEDDING_SUGGESTION',
    'INVOCATION_ERROR', 'AGENT_ERROR', 'LLM_ERROR'
  )
)
SELECT
  * EXCEPT(answer_text)
  REPLACE (
    CASE
      WHEN event_type = 'AGENT_RESPONSE' AND content IS NOT NULL THEN
        JSON_SET(
          content,
          '$.raw_response', JSON_QUERY(content, '$.response'),
          '$.text_summary', answer_text,
          '$.response', answer_text
        )
      ELSE content
    END AS content,
    JSON_SET(
      COALESCE(attributes, JSON '{}'),
      '$.root_agent_name',
      JSON_VALUE(attributes, '$.adk.app_name')
    ) AS attributes
  )
FROM allowlisted;

Setelah membuat tampilan kompatibilitas, lakukan inisialisasi SDK Client. Anda dapat menjalankan client.doctor() secara opsional untuk memvalidasi skema tabel dan cakupan peristiwa:

from datetime import datetime, timedelta, timezone
from bigquery_agent_analytics import Client, TraceFilter

client = Client(
    project_id="PROJECT_ID",
    dataset_id="ANALYTICS_DATASET",
    table_id="v_bqca_sdk_events",
    location="LOCATION",
)

# Optionally run diagnostic checks over recent partitions:
report = client.doctor(
    filters=TraceFilter(start_time=datetime.now(timezone.utc) - timedelta(days=7))
)
print(report)

Fungsi client.doctor() menampilkan kamus yang memvalidasi skema tabel dan membandingkan cakupan peristiwa dengan jenis peristiwa Agent Development Kit (ADK) standar. Karena tampilan kompatibilitas v_bqca_sdk_events hanya mengekspos jenis peristiwa analisis percakapan yang diizinkan, peringatan tentang peristiwa AGENT_COMPLETED, AGENT_STARTING, LLM_REQUEST, TOOL_COMPLETED, atau TOOL_STARTING yang tidak ada adalah hal yang wajar. Peringatan error atau kolom yang tidak ada berarti parameter Client tidak cocok dengan tempat tampilan dibuat atau Anda tidak memiliki akses baca ke set data, sedangkan hasil event_coverage kosong berarti tampilan tidak memiliki percakapan dari 7 hari terakhir.

Untuk mengetahui informasi selengkapnya tentang cara menggunakan SDK untuk mengevaluasi kualitas agen, merekonstruksi trace, dan memantau drift, lihat Menggunakan SDK BigQuery Agent Analytics dan notebook Colab interaktif yang ditautkan di bagian ini.

Resep SQL

Dalam kueri GoogleSQL berikut, gunakan placeholder PROJECT_ID, LOG_DATASET, LOG_TABLE, dan ANALYTICS_DATASET yang sama seperti yang dijelaskan dalam Menggunakan notebook Colab dan Python SDK.

Mulailah dengan Membuat tampilan peristiwa lintas agen (v_ca_events) dan Membuat fungsi tabel giliran (ca_turns). Sebagian besar kueri berikut menggunakan fungsi tabel ca_turns atau tampilan v_ca_events; kueri ringkasan error AI.GENERATE dan kueri penghapusan duplikat membaca tabel log secara langsung.

Membuat tampilan peristiwa lintas agen (v_ca_events)

Tampilan ini menampilkan satu baris per peristiwa, dengan ID agen, ID percakapan, pertanyaan, teks jawaban, insight, token, dan latensi sebagai kolom biasa. Filter pada timestamp diteruskan ke tabel log, sehingga pruning partisi tetap berfungsi. Tampilan tidak menghapus peristiwa duplikat; percobaan ulang penulisan jarang terjadi, tetapi jika Anda ingin memeriksa atau memfilter peristiwa duplikat berdasarkan event_id, lihat Menghapus baris duplikat:

CREATE OR REPLACE VIEW `PROJECT_ID.ANALYTICS_DATASET.v_ca_events` AS
SELECT
  timestamp,
  DATE(timestamp) AS event_date,
  event_id,
  event_type,
  status,
  error_message,
  user_id,
  COALESCE(NULLIF(user_id, ''), JSON_VALUE(attributes, '$.session_metadata.user_id')) AS internal_user_key,
  JSON_VALUE(attributes, '$.session_metadata.state."data-agent-id"') AS data_agent_id,
  JSON_VALUE(attributes, '$.adk.app_name') AS data_agent_resource_name,
  JSON_VALUE(attributes, '$.session_metadata.state."conversation-id"') AS conversation_id,
  session_id AS turn_id,
  invocation_id,
  IF(event_type = 'USER_MESSAGE_RECEIVED', JSON_VALUE(content, '$.text_summary'), NULL) AS user_question,
  IF(event_type = 'AGENT_RESPONSE',
    COALESCE(
      NULLIF(JSON_VALUE(content, '$.text_summary'), ''),
      NULLIF(ARRAY_TO_STRING(ARRAY(
        SELECT JSON_VALUE(p, '$.markdown')
        FROM UNNEST(JSON_QUERY_ARRAY(content, '$.response.parts')) AS p WITH OFFSET AS o
        WHERE JSON_VALUE(p, '$.markdown') IS NOT NULL
        ORDER BY o), '\n\n'), ''),
      NULLIF(JSON_VALUE(content, '$.response.clarifying_question'), ''),
      JSON_VALUE(content, '$.response')),
    NULL) AS agent_response,
  IF(event_type = 'AGENT_RESPONSE',
    NULLIF(ARRAY_TO_STRING(ARRAY(
      SELECT JSON_VALUE(p, '$.insight')
      FROM UNNEST(JSON_QUERY_ARRAY(content, '$.response.parts')) AS p WITH OFFSET AS o
      WHERE JSON_VALUE(p, '$.insight') IS NOT NULL
      ORDER BY o), '\n'), ''),
    NULL) AS agent_insights,
  IF(event_type = 'AGENT_RESPONSE',
    NULLIF(ARRAY_TO_STRING(ARRAY(
      SELECT JSON_VALUE(p, '$.assumption')
      FROM UNNEST(JSON_QUERY_ARRAY(content, '$.response.parts')) AS p WITH OFFSET AS o
      WHERE JSON_VALUE(p, '$.assumption') IS NOT NULL
      ORDER BY o), '\n'), ''),
    NULL) AS agent_assumptions,
  IF(event_type = 'AGENT_RESPONSE',
    NULLIF(ARRAY_TO_STRING(ARRAY(
      SELECT JSON_VALUE(q)
      FROM UNNEST(JSON_QUERY_ARRAY(content, '$.response.followup_questions')) AS q WITH OFFSET AS o
      WHERE JSON_VALUE(q) IS NOT NULL
      ORDER BY o), '\n'), ''),
    NULL) AS followup_questions,
  IF(event_type = 'AGENT_RESPONSE',
    NULLIF(ARRAY_TO_STRING(ARRAY(
      SELECT artifact_name
      FROM UNNEST(JSON_QUERY_ARRAY(content, '$.response.parts')) AS p WITH OFFSET AS o,
        UNNEST([JSON_VALUE(p, '$.data_result_artifact_name'), JSON_VALUE(p, '$.chart_artifact_name')]) AS artifact_name WITH OFFSET AS a_o
      WHERE artifact_name IS NOT NULL
      ORDER BY o, a_o), ', '), ''),
    NULL) AS result_artifact_names,
  IF(event_type = 'LLM_RESPONSE', JSON_VALUE(attributes, '$.model_version'), NULL) AS model_version,
  IF(event_type = 'LLM_RESPONSE', SAFE_CAST(JSON_VALUE(content, '$.usage.prompt') AS INT64), NULL) AS prompt_tokens,
  IF(event_type = 'LLM_RESPONSE', SAFE_CAST(JSON_VALUE(content, '$.usage.completion') AS INT64), NULL) AS completion_tokens,
  IF(event_type = 'LLM_RESPONSE', SAFE_CAST(JSON_VALUE(content, '$.usage.total') AS INT64), NULL) AS total_tokens,
  IF(event_type = 'LLM_RESPONSE', SAFE_CAST(JSON_VALUE(attributes, '$.usage_metadata.thoughts_token_count') AS INT64), NULL) AS thoughts_tokens,
  IF(event_type = 'INVOCATION_COMPLETED', SAFE_CAST(JSON_VALUE(latency_ms, '$.total_ms') AS INT64), NULL) AS turn_latency_ms,
  is_truncated,
  trace_id,
  span_id,
  parent_span_id
FROM `PROJECT_ID.LOG_DATASET.LOG_TABLE`;

Buat fungsi tabel giliran (ca_turns): satu baris per pertanyaan dan jawaban

Fungsi tabel ini mengambil periode waktu dan menampilkan satu baris per giliran. Karena menerapkan filter waktu ke tabel log, Anda hanya membayar untuk hari yang Anda minta:

CREATE OR REPLACE TABLE FUNCTION `PROJECT_ID.ANALYTICS_DATASET.ca_turns`(start_ts TIMESTAMP, end_ts TIMESTAMP) AS
SELECT
  turn_id,
  MIN(timestamp) AS turn_start,
  MAX(timestamp) AS turn_end,
  DATE(MIN(timestamp)) AS turn_date,
  NULLIF(MAX(user_id), '') AS user_id,
  MAX(internal_user_key) AS internal_user_key,
  MAX(data_agent_id) AS data_agent_id,
  MAX(conversation_id) AS conversation_id,
  ARRAY_AGG(user_question IGNORE NULLS ORDER BY timestamp, event_id LIMIT 1)[SAFE_OFFSET(0)] AS user_question,
  ARRAY_AGG(agent_response IGNORE NULLS ORDER BY timestamp DESC, event_id DESC LIMIT 1)[SAFE_OFFSET(0)] AS agent_response,
  ARRAY_AGG(agent_insights IGNORE NULLS ORDER BY timestamp DESC, event_id DESC LIMIT 1)[SAFE_OFFSET(0)] AS agent_insights,
  ARRAY_AGG(agent_assumptions IGNORE NULLS ORDER BY timestamp DESC, event_id DESC LIMIT 1)[SAFE_OFFSET(0)] AS agent_assumptions,
  ARRAY_AGG(followup_questions IGNORE NULLS ORDER BY timestamp DESC, event_id DESC LIMIT 1)[SAFE_OFFSET(0)] AS followup_questions,
  ARRAY_AGG(result_artifact_names IGNORE NULLS ORDER BY timestamp DESC, event_id DESC LIMIT 1)[SAFE_OFFSET(0)] AS result_artifact_names,
  MAX(turn_latency_ms) AS turn_latency_ms,
  SUM(total_tokens) AS total_tokens,
  COUNTIF(status = 'ERROR') AS error_events,
  LOGICAL_OR(IFNULL(is_truncated, FALSE)) AS any_truncated
FROM `PROJECT_ID.ANALYTICS_DATASET.v_ca_events`
WHERE timestamp >= start_ts
  AND timestamp < end_ts
  AND turn_id IS NOT NULL
GROUP BY turn_id;

Setiap pertanyaan dan jawaban untuk satu pengguna (30 hari terakhir)

Tetapkan target_user ke alamat email pengguna, atau ke kunci internal untuk penelepon tanpa alamat email:

DECLARE target_user STRING DEFAULT 'analyst@example.com';

SELECT turn_start, data_agent_id, conversation_id, user_question, agent_response, agent_insights
FROM `PROJECT_ID.ANALYTICS_DATASET.ca_turns`(
  TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY), CURRENT_TIMESTAMP())
WHERE user_id = target_user
   OR internal_user_key = target_user
ORDER BY turn_start;

Contoh pertanyaan dan jawaban di semua agen untuk ditinjau

Kueri ini menampilkan sekitar 10% giliran dari 24 jam terakhir. Ubah 0.10 untuk menetapkan rasio ulasan:

SELECT data_agent_id, turn_start, user_question, agent_response, agent_assumptions
FROM `PROJECT_ID.ANALYTICS_DATASET.ca_turns`(
  TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY), CURRENT_TIMESTAMP())
WHERE user_question IS NOT NULL
  AND RAND() < 0.10
ORDER BY data_agent_id, turn_start;

Adopsi berdasarkan hari

Kolom internal_user_key akan kembali ke attributes.session_metadata.user_id jika user_id adalah NULL (misalnya, untuk beberapa identitas gabungan), sehingga jumlah pengguna harian tetap konsisten per orang:

SELECT
  turn_date,
  COUNT(DISTINCT internal_user_key) AS active_users,
  COUNT(DISTINCT data_agent_id) AS active_agents,
  COUNT(DISTINCT conversation_id) AS conversations,
  COUNT(*) AS questions
FROM `PROJECT_ID.ANALYTICS_DATASET.ca_turns`(
  TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY), CURRENT_TIMESTAMP())
GROUP BY turn_date
ORDER BY turn_date DESC;

Adopsi oleh pengguna: agen, percakapan, dan pertanyaan per orang

Untuk menghitung agen, percakapan, dan pertanyaan yang berbeda per pengguna selama 30 hari terakhir, jalankan kueri berikut:

SELECT
  t.internal_user_key,
  COUNT(DISTINCT t.data_agent_id) AS agents_used,
  COUNT(DISTINCT t.conversation_id) AS conversations,
  COUNT(*) AS questions,
  MIN(t.turn_start) AS first_seen,
  MAX(t.turn_start) AS last_seen
FROM `PROJECT_ID.ANALYTICS_DATASET.ca_turns`(
  TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY), CURRENT_TIMESTAMP()) AS t
GROUP BY internal_user_key
ORDER BY questions DESC;

Adopsi dan kondisi menurut agen

Untuk membandingkan pengguna, percakapan, volume pertanyaan, latensi rata-rata, tingkat error, dan total token di semua agen data dalam project Anda, jalankan kueri berikut:

SELECT
  data_agent_id,
  COUNT(DISTINCT internal_user_key) AS users,
  COUNT(DISTINCT conversation_id) AS conversations,
  COUNT(*) AS questions,
  ROUND(AVG(turn_latency_ms)) AS avg_turn_latency_ms,
  ROUND(100 * SAFE_DIVIDE(COUNTIF(error_events > 0), COUNT(*)), 2) AS pct_turns_with_errors,
  SUM(total_tokens) AS total_tokens
FROM `PROJECT_ID.ANALYTICS_DATASET.ca_turns`(
  TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY), CURRENT_TIMESTAMP())
GROUP BY data_agent_id
ORDER BY questions DESC;

Ringkasan percakapan

Untuk menggabungkan percakapan multi-turn selama 7 hari terakhir, jalankan kueri berikut:

SELECT
  conversation_id,
  MAX(data_agent_id) AS data_agent_id,
  MAX(internal_user_key) AS internal_user_key,
  COUNT(*) AS turns,
  MIN(turn_start) AS started_at,
  TIMESTAMP_DIFF(MAX(turn_end), MIN(turn_start), SECOND) AS duration_seconds,
  ARRAY_AGG(user_question IGNORE NULLS ORDER BY turn_start LIMIT 1)[SAFE_OFFSET(0)] AS first_question
FROM `PROJECT_ID.ANALYTICS_DATASET.ca_turns`(
  TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY), CURRENT_TIMESTAMP())
WHERE conversation_id IS NOT NULL
GROUP BY conversation_id
ORDER BY started_at DESC
LIMIT 100;

Penggunaan token menurut model dan menurut agen (7 hari terakhir)

Untuk mengelompokkan jumlah panggilan model dan penggunaan token (termasuk token penalaran) berdasarkan tanggal, data_agent_id, dan model_version selama 7 hari terakhir, buat kueri v_ca_events untuk baris LLM_RESPONSE:

SELECT
  event_date,
  data_agent_id,
  model_version,
  COUNT(*) AS model_calls,
  SUM(prompt_tokens) AS prompt_tokens,
  SUM(completion_tokens) AS completion_tokens,
  SUM(thoughts_tokens) AS thoughts_tokens,  -- thinking tokens, already included in total_tokens
  SUM(total_tokens) AS total_tokens
FROM `PROJECT_ID.ANALYTICS_DATASET.v_ca_events`
WHERE event_type = 'LLM_RESPONSE'
  AND timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
GROUP BY event_date, data_agent_id, model_version
ORDER BY event_date DESC, total_tokens DESC;

Error menurut agen, dengan ringkasan akar masalah yang didukung AI

Untuk memeriksa jumlah error menurut agen dan hari selama 7 hari terakhir, jalankan kueri berikut:

SELECT
  event_date,
  data_agent_id,
  COUNT(*) AS error_events,
  COUNT(DISTINCT turn_id) AS turns_with_errors,
  ANY_VALUE(error_message) AS sample_error
FROM `PROJECT_ID.ANALYTICS_DATASET.v_ca_events`
WHERE status = 'ERROR'
  AND timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
GROUP BY event_date, data_agent_id
ORDER BY event_date DESC, error_events DESC;

Untuk meringkas akar masalah error terbaru, gunakan fungsi AI.GENERATE.

CTE membatasi baris sebelum model dipanggil, sehingga biaya tetap dapat diprediksi:

WITH recent_errors AS (
  SELECT
    timestamp,
    JSON_VALUE(attributes, '$.session_metadata.state."data-agent-id"') AS data_agent_id,
    session_id AS turn_id,
    error_message
  FROM `PROJECT_ID.LOG_DATASET.LOG_TABLE`
  WHERE status = 'ERROR'
    AND timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY)
  ORDER BY timestamp DESC
  LIMIT 10
)
SELECT
  timestamp,
  data_agent_id,
  turn_id,
  error_message,
  AI.GENERATE(
    CONCAT(
      'Analyze this BigQuery Conversational Analytics error log and summarize the likely root cause in two sentences. Error: ',
      IFNULL(error_message, '(none)'))
  ).result AS ai_root_cause_summary
FROM recent_errors;

Hapus baris duplikat

Langkah ini opsional. Penulisan yang dicoba ulang terkadang dapat menghasilkan peristiwa yang sama dua kali. Kueri berikut menunjukkan cara memilih baris unik per event_id selama 24 jam terakhir:

SELECT * EXCEPT (row_num)
FROM (
  SELECT
    *,
    ROW_NUMBER() OVER (
      PARTITION BY event_id
      ORDER BY timestamp) AS row_num
  FROM `PROJECT_ID.LOG_DATASET.LOG_TABLE`
  WHERE timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY)
)
WHERE row_num = 1 OR event_id IS NULL;

Memecahkan masalah logging percakapan

Jika tabel log percakapan Anda tidak ada, kosong, atau tidak memiliki giliran dari pengguna tertentu, atau jika Anda mengalami error validasi tujuan, lihat Memecahkan masalah logging percakapan.

Langkah berikutnya