ניתוח יומני שיחות
במאמר הזה מוסבר איך לשלוח שאילתות, להעריך ולפתור בעיות ביומני השיחות של ניתוח נתוני השיחות ב-BigQuery באמצעות מתכוני GoogleSQL, נוטבוק של Colab אינטראקטיבי ו-Python SDK של BigQuery Agent Analytics. למידע נוסף על העמודות בטבלת היעד ועל מבנה ה-JSON, קראו את המאמר סכימת יומן השיחות.
שימוש בנוטבוק של Colab וב-Python SDK
אתם יכולים להריץ את מתכוני ה-SQL שבמסמך הזה לצד ה-SDK של bigquery-agent-analytics ב-Python (גרסה 0.5.4 ואילך) באמצעות Customer Analytics, FinOps & Evaluation Starter Notebook (פתיחה ב-Colaboratory | צפייה ב-GitHub).
כדי להשתמש ב-Python SDK עם טבלת יומן השיחות, קודם צריך ליצור את תצוגת התאימות של v_bqca_sdk_events. בשורות של AGENT_RESPONSE, ניתוח שיחות מאחסן אובייקט JSON ב-content.response, ואילו מעריכי LLMAsJudge של ה-SDK מצפים ש-JSON_VALUE(content, '$.response') יחזיר מחרוזת סקלרית. בתצוגה הזו, הערכים של $.text_summary ו-$.response מוגדרים לתשובה בטקסט פשוט, והאובייקט המקורי נשמר ב-$.raw_response.
בקטעי הקוד הבאים ב-SQL וב-Python, מחליפים את ה-placeholder הבאים:
PROJECT_ID: מזהה הפרויקט שמכיל את סוכן הנתונים ואת מערך הנתונים של היומנים.
LOG_DATASET: המזהה של קבוצת הנתונים ב-BigQuery שבה מאוחסנת טבלת יומן השיחות הגולמי.
LOG_TABLE: המזהה של טבלת יומן השיחות, למשלagent_events.
ANALYTICS_DATASET: המזהה של מערך הנתונים ב-BigQuery באותו מיקום שבו יוצרים תצוגות של ניתוח נתונים ופונקציות טבלה.
LOCATION: המיקום ב-BigQuery של קבוצת הנתונים של Analytics, למשלUS.
כדי ליצור תצוגות ופונקציות של טבלאות ב-ANALYTICS_DATASET, צריך גישת כתיבה ב-ANALYTICS_DATASET, כמו התפקיד עריכת נתונים ב-BigQuery (roles/bigquery.dataEditor) והתפקיד צפייה בנתונים ב-BigQuery (roles/bigquery.dataViewer) ב-LOG_DATASET.
מנתחי נתונים שרק שולחים שאילתות לתצוגות נתונים צריכים את התפקיד "צפייה בנתונים ב-BigQuery" ב-ANALYTICS_DATASET ואת התפקיד 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 כדי להעריך את איכות הנציג, לשחזר יומני מעקב ולעקוב אחרי סחף נתונים מופיע במאמר שימוש ב-SDK של BigQuery Agent Analytics וב-נוטבוק של Colab האינטראקטיבי שמקושר בקטע הזה.
מתכוני SQL
בשאילתות הבאות ב-GoogleSQL, משתמשים באותם פלייסהולדרים של PROJECT_ID, LOG_DATASET, LOG_TABLE ו-ANALYTICS_DATASET שמתוארים בקטע שימוש בנוטבוק של Colab וב-Python SDK.
מתחילים עם יצירת התצוגה של אירועים בין נציגים (v_ca_events) ויצירת הפונקציה של טבלת התורות (ca_turns). רוב השאילתות הבאות משתמשות בפונקציית הטבלה ca_turns או בתצוגה v_ca_events. השאילתה AI.GENERATE לסיכום שגיאות והשאילתה לביטול כפילויות קוראות ישירות את טבלת היומן.
יצירת תצוגת אירועים של כמה סוכנים (v_ca_events)
התצוגה הזו מחזירה שורה אחת לכל אירוע, עם מזהה הנציג, מזהה השיחה, השאלה, טקסט התשובה, התובנות, האסימונים וזמן האחזור כעמודות פשוטות. מסננים ב-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;
שאלות לדוגמה ותשובות של כל הסוכנים לבדיקה
השאילתה הזו מחזירה כ-10% מהתורות ב-24 השעות האחרונות. כדי להגדיר את קצב הבדיקה, משנים את 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;
היקף השימוש לפי יום
השדה internal_user_key חוזר לערך attributes.session_metadata.user_id כש-user_id הוא NULL (לדוגמה, בחלק מהזהויות המאוחדות), כך שמספר המשתמשים היומי נשאר עקבי לכל אדם:
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;
תסיר שורות כפולות
השלב הזה הוא אופציונלי. ניסיון חוזר של כתיבה יכול לפעמים ליצור את אותו אירוע פעמיים. השאילתה הבאה מדגימה איך לבחור שורות ייחודיות לכל event_id ב-24 השעות האחרונות:
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;
פתרון בעיות ברישום ביומן של השיחות
אם טבלת יומן השיחות חסרה, ריקה או חסרים בה תורות של משתמשים ספציפיים, או אם נתקלתם בשגיאות אימות של היעד, עיינו במאמר פתרון בעיות ברישום ביומן של שיחות.