分析对话日志
本文档介绍了如何使用 GoogleSQL 配方、交互式 Colab 笔记本和 BigQuery Agent Analytics Python SDK 在 BigQuery 中查询、评估和排查对话式分析对话日志。如需详细了解目标表列和 JSON 结构,请参阅对话日志架构。
使用 Colab 笔记本和 Python SDK
您可以使用 Customer Analytics、FinOps 和 Evaluation Starter 笔记本(在 Colaboratory 中打开 | 在 GitHub 上查看),将本文档中的 SQL 配方与 bigquery-agent-analytics Python SDK(v0.5.4 或更高版本)搭配使用。
如需将 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 Data Editor [roles/bigquery.dataEditor] 角色),以及对 LOG_DATASET 具有 BigQuery Data Viewer (roles/bigquery.dataViewer) 角色。仅查询视图的分析师需要对 ANALYTICS_DATASET 拥有 BigQuery Data Viewer 角色,并对运行查询的项目拥有 BigQuery Job User (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() 函数会返回一个字典,用于验证表架构,并将事件覆盖率与标准智能体开发套件 (ADK) 事件类型进行比较。由于 v_bqca_sdk_events 兼容性视图仅公开列入许可清单的对话分析事件类型,因此预计会出现有关缺少 AGENT_COMPLETED、AGENT_STARTING、LLM_REQUEST、TOOL_COMPLETED 或 TOOL_STARTING 事件的警告。错误或缺少列警告表示 Client 参数与创建视图的位置不匹配,或者您缺少对数据集的读取权限,而空的 event_coverage 结果表示视图在过去 7 天内没有对话。
如需详细了解如何使用 SDK 评估代理质量、重构轨迹和监控偏移,请参阅使用 BigQuery Agent Analytics 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`;
创建 turns 表函数 (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;
按智能体划分的采用情况和健康状况
如需比较项目中所有数据智能体的用户数、对话数、问题量、平均延迟时间、错误率和总 token 数,请运行以下查询:
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;
按模型和代理统计的 token 用量(过去 7 天)
如需按日期、data_agent_id 和 model_version 细分过去 7 天的模型调用次数和 token 使用量(包括思考 token),请查询 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;
排查对话日志记录问题
如果对话日志表缺失、为空或缺少特定用户的对话轮次,或者您遇到目标位置验证错误,请参阅对话日志记录问题排查。