分析對話記錄
本文說明如何使用 GoogleSQL 方案、互動式 Colab 筆記本和 BigQuery 代理分析 Python SDK,在 BigQuery 中查詢、評估對話式分析對話記錄,並排解相關問題。如要進一步瞭解目的地資料表資料欄和 JSON 結構,請參閱「對話記錄結構定義」。
使用 Colab 筆記本和 Python SDK
您可以使用 Customer Analytics、FinOps 和 Evaluation Starter Notebook (在 Colaboratory 中開啟 | 在 GitHub 中查看),搭配 bigquery-agent-analytics Python SDK (0.5.4 以上版本) 執行本文中的 SQL 方案。
如要搭配對話記錄資料表使用 Python SDK,請先建立 v_bqca_sdk_events 相容性檢視表。在 AGENT_RESPONSE 資料列中,對話數據分析會將 JSON 物件儲存在 content.response,但 SDK 的 LLMAsJudge 評估器預期 JSON_VALUE(content, '$.response') 會傳回純量字串。這個檢視畫面會將 $.text_summary 和 $.response 設為純文字答案,同時保留 $.raw_response 中的原始物件。
在下列 SQL 和 Python 程式碼片段中,請替換這些預留位置:
PROJECT_ID:資料代理和記錄資料集所在專案的 ID。LOG_DATASET:儲存原始對話記錄資料表的 BigQuery 資料集 ID。LOG_TABLE:對話記錄資料表的 ID,例如agent_events。ANALYTICS_DATASET:BigQuery 資料集的 ID,位於您建立分析檢視表和資料表函式的相同位置。LOCATION:數據分析資料集的 BigQuery 位置,例如US。
如要在 ANALYTICS_DATASET 中建立檢視表和資料表函式,您必須具備 ANALYTICS_DATASET 的寫入權限,例如 BigQuery 資料編輯者 (roles/bigquery.dataEditor) 角色,以及 LOG_DATASET 的 BigQuery 資料檢視者 (roles/bigquery.dataViewer) 角色。如果分析師只查詢檢視表,則需要 ANALYTICS_DATASET 的 BigQuery 資料檢視者角色,以及執行查詢的專案 BigQuery 工作使用者 (roles/bigquery.jobUser) 角色。
下列查詢會建立 v_bqca_sdk_events 檢視表,用於在下一個步驟驗證資料表結構定義和事件涵蓋範圍:
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;
建立相容性檢視區塊後,請初始化 SDK Client。您可以視需要執行 client.doctor(),驗證資料表結構定義和事件涵蓋範圍:
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)
client.doctor() 函式會傳回字典,用於驗證資料表結構定義,並比較事件涵蓋率與標準 Agent Development Kit (ADK) 事件類型。由於 v_bqca_sdk_events 相容性檢視畫面只會顯示許可清單中的對話式數據分析事件類型,因此系統會顯示缺少 AGENT_COMPLETED、AGENT_STARTING、LLM_REQUEST、TOOL_COMPLETED 或 TOOL_STARTING 事件的警告。如果出現錯誤或缺少資料欄警告,表示 Client 參數與檢視表建立位置不符,或您沒有資料集的讀取權限。如果 event_coverage 結果空白,表示檢視表沒有過去 7 天的對話。
如要進一步瞭解如何使用 SDK 評估代理品質、重建追蹤記錄及監控偏移,請參閱「使用 BigQuery 代理分析 SDK」和本節連結的互動式 Colab 筆記本。
SQL 食譜
在下列 GoogleSQL 查詢中,請使用「使用 Colab 筆記本和 Python SDK」一節所述的 PROJECT_ID、LOG_DATASET、LOG_TABLE 和 ANALYTICS_DATASET 預留位置。
首先,請建立跨代理事件檢視表 (v_ca_events),並建立對話輪次資料表函式 (ca_turns)。下列大部分查詢都會使用 ca_turns 資料表函式或 v_ca_events 檢視表;AI.GENERATE 錯誤摘要查詢和簡化查詢則會直接讀取記錄資料表。
建立跨代理事件檢視畫面 (v_ca_events)
這個檢視表會為每個事件傳回一列,其中包含代理 ID、對話 ID、問題、答案文字、洞察、權杖和延遲時間等純文字資料欄。timestamp 上的篩選器會傳遞至記錄資料表,因此分區剪除功能仍可運作。檢視表不會移除重複事件;寫入重試的情況很少見,但如果您想依 event_id 檢查或篩選重複事件,請參閱「移除重複資料列」:
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`;
建立對話表格函式 (ca_turns):每個常見問答各占一列
這個資料表函式會採用時間範圍,並為每個回合傳回一個資料列。由於系統會將時間篩選器套用至記錄資料表,因此您只需支付所要求天數的費用:
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;
一位使用者提出的所有問題和答案 (過去 30 天)
將 target_user 設為使用者的電子郵件地址,或沒有電子郵件地址的呼叫端內部金鑰:
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;
所有代理的問答範例,供您查看
這項查詢會傳回過去 24 小時內約 10% 的對話。變更 0.10 以設定審查率:
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;
每日採用率
當 user_id 為 NULL 時 (例如某些聯合身分),internal_user_key 欄位會改用 attributes.session_metadata.user_id,因此每位使用者的每日使用者計數會保持一致:
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;
使用者採用率:每人使用的代理、對話和問題數量
如要計算過去 30 天內,每位使用者不重複的代理、對話和問題數量,請執行下列查詢:
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;
各代理的採用情形和健康狀態
如要比較專案中所有資料代理的使用者、對話、問題數量、平均延遲時間、錯誤率和總權杖數,請執行下列查詢:
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;
對話摘要
如要匯總過去 7 天的多輪對話,請執行下列查詢:
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;
各模型和代理的詞元用量 (過去 7 天)
如要依日期、data_agent_id 和 model_version 細分過去 7 天的模型呼叫次數和詞元用量 (包括思考詞元),請查詢 v_ca_events 的 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;
依代理顯示錯誤,並提供 AI 輔助的根本原因摘要
如要查看過去 7 天內,各代理和每天的錯誤計數,請執行下列查詢:
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;
如要總結近期錯誤的根本原因,請使用 AI.GENERATE 函式。
CTE 會在呼叫模型前限制資料列數量,因此費用可預測:
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;
移除重複的資料列
您可以依需求選擇執行這個步驟。重試寫入作業有時可能會產生兩次相同的事件。下列查詢示範如何選取過去 24 小時內每個 event_id 的不重複資料列:
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;
排解對話記錄問題
如果對話記錄表格遺失、空白或缺少特定使用者的對話,或遇到目的地驗證錯誤,請參閱「對話記錄疑難排解」。