技能创建器
skill-creator
创建、改进和评估自定义技能,把专业流程转化为可重复使用的 AI 能力。
policy-conditioned-routing-demo【数据与研究】Interactive demo: Build a policy-conditioned support ticket routing AI function using hand-curated gold-labeled data, then evaluate and optimize cheaper models via GEPA optimization.
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Build an AI function that routes support tickets using company-specific policy context, then optimize cheaper models via GEPA optimization to match or beat a strong baseline.
This demo walks you through:
Correct routing requires interpreting company-specific policy language written in internal vocabulary — models must generalize policy semantics rather than rely on keyword matching.
Estimated time: 20-40 minutes
Explain to user:
Welcome to the Policy-Conditioned Routing Demo!
This demo uses a hard benchmark where correct routing depends on reading
company-specific policy text written in unfamiliar internal vocabulary.
Route labels:
- billing
- account_access
- bug_or_outage
- feature_request
- refund_or_cancel
- security_or_abuse
The dataset includes 4 companies, each with unique routing policies.
Most tickets have a "default" label that gets overridden by company
policy — a model that ignores policy context will score poorly.
Objects created: all prefixed with DEMO_ for easy cleanup.
Ask user:
Where would you like to create the demo objects?
Database: [e.g., TEMP]
Schema: [e.g., PUBLIC]
All objects will be prefixed with DEMO_ for easy cleanup.
Store the database and schema for use throughout the demo.
Explain to user:
I'll load the hand-curated gold-labeled dataset. This creates:
1. A company routing policy table (4 companies with unique policies)
2. A 24-row holdout set for evaluation (21 override rows)
3. A 96-row training set for optimization (84 override rows)
The data is pre-split — no train/test splitting needed.
⚠️ STOP: Wait for user confirmation before loading data.
Load create_support_ticket_v6_dataset.sql.j2 and render it as a Jinja2 template with database and schema set to the user's chosen values, then execute the resulting SQL. This creates and populates the policy, holdout, and training tables.
Verify creation:
SELECT 'policy' AS TBL, COUNT(*) AS ROWS
FROM {database}.{schema}.DEMO_COMPANY_ROUTING_POLICY_V6
UNION ALL
SELECT 'holdout', COUNT(*)
FROM {database}.{schema}.DEMO_TICKETS_HARD_GOLD_V6_SMALL
UNION ALL
SELECT 'train', COUNT(*)
FROM {database}.{schema}.DEMO_TICKETS_POLICY_TRAIN_V6_LARGE;
Expected: policy=4, holdout=24, train=96.
Also verify zero subject overlap between train and holdout:
SELECT COUNT(*) AS EXACT_SUBJECT_OVERLAP_COUNT
FROM {database}.{schema}.DEMO_TICKETS_POLICY_TRAIN_V6_LARGE t
JOIN {database}.{schema}.DEMO_TICKETS_HARD_GOLD_V6_SMALL h
ON t.SUBJECT = h.SUBJECT;
Expected: 0.
Present the function configuration:
Now I'll create a policy-aware routing function.
Function name: DEMO_ROUTE_TICKET
Default model: gemini-2.5-flash-lite
Inputs:
SUBJECT, BODY, CUSTOMER_TIER, COMPANY_NAME,
POLICY_PROFILE, POLICY_TEXT, ENTITLEMENT_TEXT
Output: route (string)
⚠️ STOP: Wait for user confirmation or modifications before creating the function.
Load create/SKILL.md and follow it from Step 7 onward, passing:
database, schemafunction_name: DEMO_ROUTE_TICKETfunction_intention: Route policy-aware support tickets using company context.model: gemini-2.5-flash-liteinputs: [{"name": "SUBJECT", "sql_type": "VARCHAR"}, {"name": "BODY", "sql_type": "VARCHAR"}, {"name": "CUSTOMER_TIER", "sql_type": "VARCHAR"}, {"name": "COMPANY_NAME", "sql_type": "VARCHAR"}, {"name": "POLICY_PROFILE", "sql_type": "VARCHAR"}, {"name": "POLICY_TEXT", "sql_type": "VARCHAR"}, {"name": "ENTITLEMENT_TEXT", "sql_type": "VARCHAR"}]outputs: [{"name": "route", "json_type": "string", "description": "Support ticket route"}]system_prompt: You are a support ticket router. Given a ticket subject, body, customer tier, company name, policy profile, company policy text, and entitlement notes, classify it into exactly one category: billing, account_access, bug_or_outage, feature_request, refund_or_cancel, or security_or_abuse. The company policy is written in internal handling language rather than the route labels themselves. Infer the best route from the ticket and company policy, and only use company context when it changes the default interpretation. Return only the label in the route field.user_prompt_template: Subject: {SUBJECT}\nBody: {BODY}\nCustomer tier: {CUSTOMER_TIER}\nCompany name: {COMPANY_NAME}\nPolicy profile: {POLICY_PROFILE}\nCompany policy: {POLICY_TEXT}\nEntitlement notes: {ENTITLEMENT_TEXT}Return here after the smoke test succeeds.
Create the eval table from the holdout set, renaming the gold label column to EXPECTED_OUTPUT:
CREATE OR REPLACE TABLE {database}.{schema}.DEMO_TICKETS_EVAL AS
SELECT
CASE_ID, CASE_GROUP, SUBJECT, BODY, CUSTOMER_TIER,
COMPANY_NAME, POLICY_PROFILE, POLICY_TEXT, ENTITLEMENT_TEXT,
DEFAULT_LABEL,
GOLD_LABEL_V6_SMALL AS EXPECTED_OUTPUT,
RULE_FAMILY, POLICY_EFFECT, REQUIRES_POLICY_CONTEXT,
CURATION_NOTE, CURATED_AT
FROM {database}.{schema}.DEMO_TICKETS_HARD_GOLD_V6_SMALL
ORDER BY CASE_ID;
Verify:
SELECT COUNT(*) AS ROWS, COUNT_IF(REQUIRES_POLICY_CONTEXT) AS OVERRIDE_ROWS
FROM {database}.{schema}.DEMO_TICKETS_EVAL;
Expected: 24 rows, 21 overrides.
Present to user:
We'll evaluate the routing function across multiple models on the
24-row holdout set. This establishes baselines before optimization.
Default comparison models:
- claude-sonnet-4-5 (strong reference)
- claude-haiku-4-5
- gemini-2.5-flash
- gemini-2.5-flash-lite
- llama3.1-8b
- mistral-7b
⚠️ STOP: Confirm the model list with the user before running.
If one of these models is unavailable, remove it from the list or load references/model_selection.md to choose substitutes.
Create the results table:
CREATE OR REPLACE TABLE {database}.{schema}.DEMO_ROUTE_TICKET_BASELINE_RESULTS (
MODEL_NAME VARCHAR, ROW_COUNT NUMBER, CORRECT NUMBER, ACCURACY NUMBER(10, 4)
);
Create a per-model UDF for each model, then run evaluation. The model and system prompt are baked into each function body — we create a separate function per model using create_udf.py, reusing the same configuration from Step 4 but with a different model and function_name.
Do not confirm with the user before creating each UDF — the user already confirmed the model list above.
For each model, derive a function suffix from the model name (replace - and . with _, uppercase). For example, claude-sonnet-4-5 → DEMO_ROUTE_TICKET__CLAUDE_SONNET_4_5.
For each model, create the function:
PYTHONPATH=<SKILL_DIRECTORY>/src uv run --project <SKILL_DIRECTORY> python <SKILL_DIRECTORY>/src/create_udf.py \
--execute --connection <CONNECTION_NAME> \
--database {database} \
--schema {schema} \
--function-name "DEMO_ROUTE_TICKET__{model_suffix}" \
--function-intention 'Route policy-aware support tickets using company context.' \
--model {model_name} \
--system-prompt '<same system_prompt from Step 4>' \
--user-prompt-template 'Subject: {SUBJECT}\nBody: {BODY}\nCustomer tier: {CUSTOMER_TIER}\nCompany name: {COMPANY_NAME}\nPolicy profile: {POLICY_PROFILE}\nCompany policy: {POLICY_TEXT}\nEntitlement notes: {ENTITLEMENT_TEXT}' \
--inputs '[{"name": "SUBJECT", "sql_type": "VARCHAR"}, {"name": "BODY", "sql_type": "VARCHAR"}, {"name": "CUSTOMER_TIER", "sql_type": "VARCHAR"}, {"name": "COMPANY_NAME", "sql_type": "VARCHAR"}, {"name": "POLICY_PROFILE", "sql_type": "VARCHAR"}, {"name": "POLICY_TEXT", "sql_type": "VARCHAR"}, {"name": "ENTITLEMENT_TEXT", "sql_type": "VARCHAR"}]' \
--outputs '[{"name": "route", "json_type": "string", "description": "Support ticket route"}]'
After all UDFs are created, run eval queries in parallel — one per model:
INSERT INTO {database}.{schema}.DEMO_ROUTE_TICKET_BASELINE_RESULTS
WITH preds AS (
SELECT
EXPECTED_OUTPUT,
{database}.{schema}.DEMO_ROUTE_TICKET__{model_suffix}(
SUBJECT, BODY, CUSTOMER_TIER, COMPANY_NAME,
POLICY_PROFILE, POLICY_TEXT, ENTITLEMENT_TEXT
) AS PREDICTED
FROM {database}.{schema}.DEMO_TICKETS_EVAL
)
SELECT
'{model_name}', COUNT(*), COUNT_IF(PREDICTED = EXPECTED_OUTPUT),
ROUND(COUNT_IF(PREDICTED = EXPECTED_OUTPUT) / COUNT(*), 4)
FROM preds;
After all results are collected, drop the per-model UDFs:
DROP FUNCTION IF EXISTS {database}.{schema}.DEMO_ROUTE_TICKET__{model_suffix}(VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR);
Show the baseline leaderboard:
SELECT MODEL_NAME, ROUND(ACCURACY * 100, 1) AS ACCURACY_PCT
FROM {database}.{schema}.DEMO_ROUTE_TICKET_BASELINE_RESULTS
ORDER BY ACCURACY DESC;
Highlight that cheap models typically score well below the strong reference on this hard benchmark because the policy vocabulary is unfamiliar.
Present the optimization configuration:
Now we'll optimize the cheap models using GEPA optimization.
The optimizer evolves the function body through multiple generations,
testing variations against the 96-row training set.
Default cheap models:
- llama3.1-8b
- mistral-7b
- gemini-2.5-flash
- gemini-2.5-flash-lite
- claude-haiku-4-5
Auto budget: demo (~5 minutes)
Experiment: DEMO_ROUTE_TICKET_OPT_EXP
⚠️ STOP: Wait for user confirmation or modifications before starting optimization.
Load optimize/SKILL.md and follow it from Step 6 onward, passing:
function_name: {database}.{schema}.DEMO_ROUTE_TICKETtraining_table: {database}.{schema}.DEMO_TICKETS_POLICY_TRAIN_V6_LARGEtest_table: {database}.{schema}.DEMO_TICKETS_EVALinput_columns: ['SUBJECT', 'BODY', 'CUSTOMER_TIER', 'COMPANY_NAME', 'POLICY_PROFILE', 'POLICY_TEXT', 'ENTITLEMENT_TEXT']label_column: EXPECTED_OUTPUTmetric: exact_matchmodels: the confirmed cheap-model listreflection_model: claude-sonnet-4-5auto_budget: demoexperiment_name: {database}.{schema}.DEMO_ROUTE_TICKET_OPT_EXPReturn here after optimization results are presented.
8.1. Join baseline results from DEMO_ROUTE_TICKET_BASELINE_RESULTS with the best optimized score per model from DEMO_ROUTE_TICKET_OPT_EXP. Show each model's baseline accuracy, optimized accuracy, gain, and whether it meets or exceeds the strong reference (claude-sonnet-4-5) baseline. The strong reference itself was not optimized — include it as the reference row.
8.2. Calculate relative cost using the Pareto filter script (src/filter_pareto.py). Include all models: optimized cheap models at their best optimized score and claude-sonnet-4-5 at its baseline score. Use the system prompt character length for --prompt-chars and average expected output length from the eval table for --avg-output-chars. Use the strong reference baseline as --seed-score. Present the Pareto-optimal table to the user.
8.3. Summarize key findings:
claude-sonnet-4-5 is dominated on the Pareto frontier (a cheaper model has equal or higher score), note that the strong model is no longer the best option at any price point.Ask user:
The Policy-Conditioned Routing demo is complete!
Would you like to clean up the demo objects?
This will drop:
- {database}.{schema}.DEMO_COMPANY_ROUTING_POLICY_V6
- {database}.{schema}.DEMO_TICKETS_HARD_GOLD_V6_SMALL
- {database}.{schema}.DEMO_TICKETS_POLICY_TRAIN_V6_LARGE
- {database}.{schema}.DEMO_TICKETS_EVAL
- {database}.{schema}.DEMO_ROUTE_TICKET
- {database}.{schema}.DEMO_ROUTE_TICKET_BASELINE_RESULTS
- {database}.{schema}.DEMO_ROUTE_TICKET_OPT_EXP
⚠️ STOP: Wait for user confirmation before cleanup.
If yes, execute:
DROP TABLE IF EXISTS {database}.{schema}.DEMO_COMPANY_ROUTING_POLICY_V6;
DROP TABLE IF EXISTS {database}.{schema}.DEMO_TICKETS_HARD_GOLD_V6_SMALL;
DROP TABLE IF EXISTS {database}.{schema}.DEMO_TICKETS_POLICY_TRAIN_V6_LARGE;
DROP TABLE IF EXISTS {database}.{schema}.DEMO_TICKETS_EVAL;
DROP FUNCTION IF EXISTS {database}.{schema}.DEMO_ROUTE_TICKET(VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR);
DROP TABLE IF EXISTS {database}.{schema}.DEMO_ROUTE_TICKET_BASELINE_RESULTS;
DROP EXPERIMENT IF EXISTS {database}.{schema}.DEMO_ROUTE_TICKET_OPT_EXP;
Explain to user:
You completed a policy-conditioned routing workflow:
1. Loaded labeled data with unfamiliar company routing policies
2. Built a policy-aware routing function
3. Measured baselines — cheap models scored well below the strong
reference because the policy vocabulary is intentionally unfamiliar
4. Optimized cheap models via GEPA optimization
5. Compared cost and quality side-by-side
Key takeaways:
Accuracy: GEPA optimization recovered large accuracy gains on
cheap models. On hard tasks where baselines are low, the room for
improvement is biggest.
Cost: The Pareto frontier shows which models offer the best
quality-per-dollar. When an optimized cheap model matches or beats
the strong reference, switching to it saves cost with no quality
penalty.
When to use GEPA optimization: Whenever baseline accuracy on
your task is disappointing, especially with cheaper models.
Optimization can close the gap without changing models or data.