ניתוח יומני שיחות

במאמר הזה מוסבר איך לשלוח שאילתות, להעריך ולפתור בעיות ביומני השיחות של ניתוח נתוני השיחות ב-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;

פתרון בעיות ברישום ביומן של השיחות

אם טבלת יומן השיחות חסרה, ריקה או חסרים בה תורות של משתמשים ספציפיים, או אם נתקלתם בשגיאות אימות של היעד, עיינו במאמר פתרון בעיות ברישום ביומן של שיחות.

המאמרים הבאים