import marimo

__generated_with = "0.23.16"
app = marimo.App(width="medium", app_title="微調工具實戰：Unsloth、TRL 與託管微調（實驗場）")


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
    # 🧪 微調工具實戰：Unsloth、TRL 與託管微調（實驗場）

    這是本課的**實驗場**。教學頁讀到哪，就回到這裡對應的段落動手——
    每一段都有**選單與滑桿**，選了就立刻重算。

    這裡的訓練紀錄全是**真的**：同一份 480 筆客服資料、同一組超參數，
    在一張 RTX 4090 上分別用 **TRL＋PEFT** 和 **Unsloth** 各跑一次 QLoRA SFT
    （Qwen3-1.7B，2026-09-24）——每一步的 loss、秒數、峰值 VRAM，
    以及訓練前後模型對 40 則沒看過的訊息的真實回答，都打包在這份 notebook 裡。
    你的瀏覽器沒有在訓練模型：它在**重播與重算**那次實測。
    """
    )
    return


@app.cell
def _():
    import marimo as mo
    return (mo,)


@app.cell
def _():
    # 科學套件集中在這格 import，回傳給全 notebook 用
    import html as html_mod
    import json

    import matplotlib.pyplot as plt
    import numpy as np
    return html_mod, json, np, plt


@app.cell
def _(json):
    # ── 實測資料：由 _spikes/spike_genai_finetune_collect.py 從 spike 結果注入，請勿手改 ──
    # <FT_DATA>
    FT = json.loads(r"""{"meta":{"date":"2026-09-24","gpu":{"name":"NVIDIA GeForce RTX 4090","total_gb":23.52,"bf16":true},"model":"unsloth/Qwen3-1.7B-bnb-4bit","hp":{"lora_r":16,"lora_alpha":16,"lora_dropout":0.0,"lr":0.0002,"batch":8,"grad_accum":1,"epochs":1,"warmup_steps":5,"scheduler":"linear","optim":"adamw_8bit","weight_decay":0.0,"max_length":512,"seed":3407,"max_new_tokens":160},"versions":{"trl":{"trl":"1.13.0","peft":"0.21.0","transformers":"5.17.0","torch":"2.14.0","bitsandbytes":"0.50.2","datasets":"5.0.1","accelerate":"1.15.0"},"unsloth":{"unsloth":"2026.9.11","unsloth_zoo":"2026.9.7","trl":"0.24.0","peft":"0.21.0","transformers":"5.5.0","torch":"2.12.1","bitsandbytes":"0.50.2","datasets":"4.3.0","accelerate":"1.15.0","xformers":"0.0.35"}},"n_train":480,"n_test":40,"tok_per_ex":{"short":99.7,"long":1178.8},"trainable":17432576},"runs":{"short":{"trl":[{"loss":[2.8648,2.9606,2.958,2.6274,2.0034,1.6583,1.0955,0.8267,0.6766,0.3938,0.3495,0.3127,0.1999,0.2426,0.1495,0.0761,0.09,0.1865,0.0394,0.0708,0.0456,0.043,0.0427,0.0486,0.0364,0.0392,0.041,0.0397,0.0461,0.0216,0.048,0.0179,0.0187,0.0101,0.0169,0.0123,0.0268,0.0134,0.0161,0.0093,0.0225,0.0053,0.0072,0.0215,0.0111,0.0184,0.0028,0.0039,0.0044,0.0038,0.0063,0.0044,0.0048,0.0029,0.0049,0.007,0.0052,0.0027,0.0143,0.0043],"step_s":[0.5327,0.3475,0.3499,0.5353,0.3357,0.3553,0.3396,0.3264,0.4036,0.3402,0.3961,0.3598,0.4371,0.3489,0.3488,0.3454,0.3545,0.3539,0.3126,0.4147,0.3321,0.3441,0.3811,0.4681,0.2872,0.3259,0.3327,0.3438,0.4084,0.3469,0.4735,0.3619,0.3236,0.339,0.2857,0.2711,0.3148,0.3027,0.3387,0.3378,0.432,0.4225,0.3455,0.3576,0.3521,0.3272,0.3193,0.3441,0.4564,0.6936,0.3248,0.1917,0.2257,0.2091,0.1949,0.247,0.2771,0.1891,0.1857,0.2598],"train_s":21.15,"peak":2.18,"load":1.257,"file":"r_trl_1.json"},{"loss":[2.8648,2.9606,2.9567,2.6498,2.0358,1.6821,1.1047,0.8286,0.679,0.3922,0.3513,0.314,0.2051,0.2455,0.1509,0.082,0.095,0.1932,0.0417,0.0693,0.0478,0.0391,0.0429,0.0502,0.0383,0.043,0.0424,0.0416,0.0512,0.0242,0.0485,0.0204,0.0215,0.0113,0.0181,0.0134,0.0304,0.0143,0.017,0.0113,0.0245,0.0057,0.009,0.0231,0.0121,0.0191,0.0037,0.0048,0.0047,0.0046,0.0072,0.0049,0.0053,0.0036,0.0052,0.0075,0.0062,0.0028,0.0159,0.0049],"step_s":[0.4322,0.3658,0.3572,0.3691,0.3463,0.346,0.3663,0.3647,0.3723,0.3712,0.3857,0.371,0.3618,0.3698,0.3674,0.377,0.3859,0.3752,0.377,0.3606,0.378,0.3859,0.3702,0.3811,0.4866,0.3761,0.3777,0.3799,0.3721,0.3746,0.3877,0.3837,0.3735,0.3703,0.3813,0.3789,0.3883,0.3788,0.3797,0.3713,0.3805,0.3789,0.3599,0.3797,0.3782,0.3715,0.3662,0.386,0.377,0.369,0.4768,0.3774,0.3861,0.3904,0.379,0.38,0.3959,0.3638,0.3815,0.3802],"train_s":23.07,"peak":2.18,"load":1.257,"file":"r_trl_2.json"},{"loss":[2.8648,2.9606,2.9563,2.6216,2.0065,1.6488,1.0802,0.821,0.6676,0.3927,0.3483,0.3115,0.1999,0.2377,0.1464,0.0752,0.0909,0.1863,0.039,0.0648,0.0454,0.0389,0.0411,0.0483,0.0357,0.0384,0.0409,0.0407,0.0471,0.0214,0.0459,0.0173,0.0183,0.0108,0.0165,0.0126,0.0271,0.0131,0.0166,0.011,0.0227,0.0056,0.0082,0.0214,0.0113,0.0257,0.0027,0.004,0.0037,0.0039,0.0058,0.0038,0.0049,0.0029,0.0045,0.0077,0.0051,0.0025,0.0132,0.0042],"step_s":[0.9328,0.3692,0.3515,0.364,0.3272,0.3812,0.3567,0.355,0.3579,0.3629,0.3747,0.366,0.3383,0.3448,0.3185,0.3053,0.3568,0.3853,0.3377,0.3536,0.3539,0.3597,0.3522,0.4601,0.3431,0.3335,0.361,0.3564,0.3637,0.3217,0.3488,0.3566,0.3054,0.3135,0.3075,0.3636,0.3579,0.3626,0.3559,0.3552,0.3616,0.3551,0.3544,0.3674,0.3931,0.3669,0.355,0.3579,0.3643,0.4669,0.3732,0.3522,0.3715,0.37,0.3583,0.3468,0.3721,0.3548,0.3588,0.3501],"train_s":22.46,"peak":2.188,"load":1.257,"file":"r_trl_3.json"}],"unsloth":[{"loss":[2.6171,2.7177,2.7159,2.3801,1.8311,1.5905,1.0191,0.7867,0.6404,0.389,0.3394,0.3029,0.1994,0.2385,0.1555,0.0779,0.0898,0.1907,0.038,0.0636,0.0463,0.0405,0.0405,0.0491,0.0371,0.0391,0.0384,0.0414,0.0539,0.0244,0.045,0.0188,0.0169,0.01,0.0165,0.0111,0.0346,0.0143,0.0165,0.0102,0.0197,0.005,0.0067,0.0217,0.0128,0.0219,0.0033,0.0046,0.0044,0.0043,0.0053,0.0052,0.0046,0.0035,0.0041,0.0072,0.0045,0.0036,0.0137,0.0035],"step_s":[2.7336,0.2746,0.264,0.2653,0.2724,0.2543,0.2967,0.3331,0.2668,0.2783,0.2642,0.2012,0.2343,0.2612,0.2487,0.2635,0.2572,0.2286,0.2094,0.2635,0.3355,0.345,0.3428,0.3477,0.376,0.3324,0.3573,0.3523,0.2634,0.2657,0.3257,0.2867,0.3447,0.3195,0.2629,0.2677,0.3011,0.284,0.2242,0.2052,0.1992,0.3085,0.2634,0.2827,0.3451,0.2628,0.3326,0.3113,0.2661,0.2619,0.2273,0.2575,0.3003,0.2265,0.2548,0.2626,0.2166,0.2389,0.2653,0.3878],"train_s":20.5,"peak":2.0,"load":1.3,"file":"r_uns_1.json"},{"loss":[2.6171,2.7177,2.7159,2.3801,1.8311,1.5905,1.0191,0.7867,0.6404,0.389,0.3394,0.3029,0.1994,0.2385,0.1555,0.0779,0.0898,0.1907,0.038,0.0636,0.0463,0.0405,0.0405,0.0491,0.0371,0.0391,0.0384,0.0414,0.0539,0.0244,0.045,0.0188,0.0169,0.01,0.0165,0.0111,0.0346,0.0143,0.0165,0.0102,0.0197,0.005,0.0067,0.0217,0.0128,0.0219,0.0033,0.0046,0.0044,0.0043,0.0053,0.0052,0.0046,0.0035,0.0041,0.0072,0.0045,0.0036,0.0137,0.0035],"step_s":[0.9602,0.298,0.2834,0.2715,0.2777,0.2748,0.2991,0.2938,0.2963,0.2929,0.282,0.2843,0.2984,0.2752,0.2776,0.2514,0.275,0.2864,0.2868,0.2846,0.2926,0.2876,0.2855,0.2859,0.2877,0.2749,0.2767,0.2829,0.2815,0.2771,0.2667,0.2513,0.2659,0.2374,0.2346,0.2566,0.2671,0.2632,0.2722,0.2752,0.2759,0.2919,0.2747,0.2784,0.2801,0.2775,0.2824,0.2751,0.2709,0.2699,0.2769,0.2763,0.2678,0.2755,0.2775,0.266,0.2404,0.2666,0.27,0.2805],"train_s":18.19,"peak":2.0,"load":1.3,"file":"r_uns_2.json"},{"loss":[2.6171,2.7177,2.7159,2.3801,1.8311,1.5905,1.0191,0.7867,0.6404,0.389,0.3394,0.3029,0.1994,0.2385,0.1555,0.0779,0.0898,0.1907,0.038,0.0636,0.0463,0.0405,0.0405,0.0491,0.0371,0.0391,0.0384,0.0414,0.0539,0.0244,0.045,0.0188,0.0169,0.01,0.0165,0.0111,0.0346,0.0143,0.0165,0.0102,0.0197,0.005,0.0067,0.0217,0.0128,0.0219,0.0033,0.0046,0.0044,0.0043,0.0053,0.0052,0.0046,0.0035,0.0041,0.0072,0.0045,0.0036,0.0137,0.0035],"step_s":[1.0333,0.263,0.2603,0.2713,0.2695,0.249,0.2864,0.2778,0.2505,0.2776,0.286,0.286,0.2846,0.2776,0.2959,0.2734,0.259,0.2788,0.2763,0.2743,0.2799,0.2828,0.2796,0.2682,0.3566,0.2726,0.2695,0.2781,0.2807,0.2772,0.2599,0.2736,0.2763,0.2648,0.2885,0.2845,0.2808,0.276,0.2727,0.2768,0.272,0.2686,0.2815,0.2762,0.2851,0.277,0.2654,0.2912,0.2745,0.2695,0.2763,0.2768,0.2715,0.2852,0.2736,0.2817,0.2799,0.2727,0.2668,0.263],"train_s":18.3,"peak":2.0,"load":1.3,"file":"r_uns_3.json"}]},"long":{"trl":[{"loss":[3.0147,2.9268,2.9174,2.7952,2.085,1.891,1.0878,0.8689,0.7042,0.447,0.3645,0.3665,0.2522,0.2724,0.1753,0.0698,0.0821,0.1465,0.0568,0.0555,0.06,0.0479,0.0763,0.0437,0.0493,0.093,0.0754,0.038,0.0311,0.0235,0.03,0.0223,0.0133,0.0132,0.0158,0.0221,0.0247,0.0159,0.0212,0.0166,0.0268,0.0169,0.0078,0.0178,0.0174,0.0137,0.0055,0.0051,0.0052,0.0093,0.0021,0.0046,0.0053,0.0038,0.0048,0.0072,0.0026,0.0079,0.0093,0.0033],"step_s":[3.8218,3.2175,3.1706,3.2159,3.1612,3.0932,3.2028,3.1527,3.2567,3.0734,3.2604,3.2023,3.2791,3.2573,3.3464,3.1526,3.3366,3.3934,3.2708,3.1704,3.3209,3.4377,3.3411,3.177,3.127,3.0954,3.1923,3.0946,3.0479,3.1516,3.2496,3.1418,2.9185,2.9329,2.9928,3.0432,2.6287,3.0247,3.0151,3.057,3.1864,3.0605,3.0888,3.0518,2.6729,1.5052,1.5306,1.5689,1.5108,1.7059,1.8183,1.5246,1.579,1.5256,1.512,1.5372,1.5374,1.9517,1.8938,2.6527],"train_s":167.78,"peak":4.086,"load":1.257,"file":"r_trl_long.json"},{"loss":[3.0147,2.9268,2.9206,2.7775,2.0867,1.8929,1.0925,0.8674,0.7024,0.4404,0.3598,0.3607,0.2508,0.2676,0.1691,0.0653,0.0776,0.1415,0.0594,0.0546,0.058,0.0484,0.0778,0.0406,0.0482,0.0908,0.0782,0.0362,0.0368,0.0191,0.0293,0.0135,0.0124,0.0154,0.0173,0.0252,0.0231,0.0142,0.017,0.0151,0.0293,0.0151,0.0081,0.0182,0.0159,0.0118,0.0034,0.0053,0.0043,0.0104,0.0015,0.0043,0.0043,0.0031,0.0041,0.0058,0.0018,0.0082,0.006,0.0039],"step_s":[3.8598,3.2701,3.1873,3.2684,3.2129,3.2844,3.3047,3.2909,3.3738,3.3335,3.3777,3.3423,3.373,3.3542,3.3212,3.2902,3.3632,3.4057,3.249,3.3219,3.3118,3.4684,3.4817,3.3664,3.3455,3.3296,3.3271,3.2567,3.2382,3.239,3.4002,3.2774,3.2854,3.2984,3.2176,3.4524,3.3043,3.3495,3.3226,3.3565,3.3627,3.1788,3.2351,3.3224,3.3542,3.3086,3.3686,3.4484,3.3074,3.3754,3.2837,3.3367,3.4416,3.2331,3.2842,3.3483,3.3043,3.3138,3.2623,3.4069],"train_s":200.25,"peak":4.086,"load":1.257,"file":"r_trl_long_2.json"},{"loss":[3.0147,2.9268,2.9163,2.7918,2.0751,1.8879,1.0823,0.8717,0.705,0.4382,0.3581,0.3629,0.2531,0.2698,0.176,0.0693,0.0825,0.1487,0.0609,0.0581,0.0625,0.05,0.0808,0.0439,0.0486,0.0804,0.0721,0.0394,0.0332,0.0206,0.0386,0.0156,0.0159,0.0161,0.0161,0.0246,0.0257,0.0134,0.0186,0.0164,0.0298,0.0173,0.0079,0.0203,0.0175,0.0148,0.0046,0.0046,0.0051,0.013,0.002,0.0038,0.0054,0.0039,0.005,0.0072,0.0025,0.0092,0.0087,0.0038],"step_s":[3.825,3.1639,3.201,3.2002,2.9897,2.9919,2.9777,3.0884,3.131,3.125,3.1594,3.1292,3.1166,3.0943,3.1552,2.9333,3.0423,3.0882,3.0638,3.0176,2.7817,3.1547,3.0953,3.0003,3.008,3.0138,2.9897,2.7001,2.9393,2.9278,3.0649,2.9811,2.9874,2.9614,2.8701,2.904,3.0183,3.0128,3.0318,3.0131,2.9597,2.9718,2.7027,2.8888,2.8395,2.9941,2.9504,2.9854,2.8403,2.982,2.3102,2.4864,2.829,2.8463,2.9294,2.8335,2.8953,2.9498,2.7616,2.6996],"train_s":178.96,"peak":4.086,"load":1.257,"file":"r_trl_long_3.json"}],"unsloth":[{"loss":[2.9002,2.7975,2.814,2.7103,2.0656,1.7825,0.8953,0.8356,0.6716,0.4102,0.3329,0.3345,0.2217,0.2422,0.151,0.0565,0.077,0.1267,0.0576,0.051,0.0536,0.042,0.0737,0.0383,0.0471,0.0745,0.0688,0.0381,0.0362,0.0176,0.0331,0.0148,0.0164,0.0242,0.0177,0.0268,0.0304,0.0152,0.0212,0.0185,0.03,0.0126,0.0085,0.0171,0.0194,0.0101,0.0042,0.0068,0.0048,0.0106,0.0022,0.0048,0.0055,0.0036,0.0049,0.0074,0.0023,0.0075,0.0076,0.0033],"step_s":[2.3882,0.8983,0.9201,0.9406,0.9032,0.9223,0.9206,0.8957,0.9309,0.8926,0.9245,0.9243,0.9414,0.9298,0.9063,0.926,0.9316,0.9244,0.9046,0.9032,0.9222,0.9121,0.923,0.9256,0.9319,0.906,0.9329,0.9235,0.9043,0.9245,0.9079,0.9074,0.9274,0.9062,0.9084,0.9341,0.9069,0.9281,0.9084,1.3936,1.8177,1.8038,1.8109,1.7577,1.8184,1.7707,1.8414,1.9336,1.9179,1.9961,1.9607,1.9178,1.9292,1.974,1.9941,1.9452,1.9741,1.9983,2.0068,1.9419],"train_s":77.76,"peak":2.492,"load":1.3,"file":"r_uns_long.json"},{"loss":[2.9002,2.7975,2.7948,2.7007,2.0651,1.7793,0.8924,0.8336,0.6707,0.4108,0.3281,0.3399,0.2205,0.2415,0.1518,0.0579,0.0745,0.1279,0.0547,0.0515,0.058,0.0509,0.0744,0.0381,0.0497,0.0831,0.078,0.0369,0.0388,0.0177,0.0332,0.0143,0.0157,0.0235,0.0183,0.0258,0.0295,0.0157,0.0197,0.0201,0.0299,0.0149,0.0092,0.0169,0.0192,0.0114,0.0042,0.0066,0.0057,0.011,0.0023,0.005,0.0059,0.0041,0.0051,0.0064,0.0024,0.0083,0.0064,0.0029],"step_s":[2.7311,2.028,2.024,2.0661,2.0102,2.0286,2.0432,1.9787,1.9768,1.9557,1.947,1.9938,2.091,2.0737,2.0307,2.0617,2.0813,2.0443,2.0289,2.0109,2.0565,2.0377,2.0413,2.0618,2.0832,2.0118,2.0772,2.0428,2.0236,2.0557,2.0333,2.1103,2.0545,2.0271,2.0501,2.067,2.0278,2.0625,2.0254,2.0404,2.0309,2.0272,2.0621,2.0268,2.0636,1.9899,2.031,2.0262,2.0154,2.0586,2.0533,2.0335,2.0101,2.0528,2.0601,2.0111,2.0275,2.046,1.983,2.0063],"train_s":123.86,"peak":2.492,"load":1.3,"file":"r_uns_long_2.json"},{"loss":[2.9002,2.7975,2.8101,2.7039,2.054,1.792,0.8923,0.8349,0.6716,0.4078,0.3267,0.3381,0.2199,0.2439,0.1515,0.0597,0.0749,0.1277,0.0576,0.0487,0.0582,0.0473,0.0757,0.0368,0.0499,0.0881,0.0783,0.035,0.0353,0.0179,0.0349,0.0158,0.0157,0.0228,0.0185,0.0249,0.0278,0.0141,0.0196,0.0184,0.0294,0.0152,0.009,0.0169,0.0182,0.0106,0.004,0.0059,0.0061,0.0102,0.0024,0.0062,0.0056,0.0039,0.0054,0.0061,0.0023,0.0085,0.0068,0.0028],"step_s":[2.4903,1.7509,1.7327,1.7483,1.6944,1.7783,1.8044,1.7538,1.695,1.6866,1.7754,1.2772,0.9424,0.9299,0.9062,0.9252,0.9322,0.9251,0.9076,0.9047,0.9247,0.9145,0.9244,0.9273,0.9347,0.9086,0.9351,0.9258,0.9073,0.9272,1.5307,1.8354,1.7918,1.7996,1.7655,1.9291,1.8489,1.9102,1.8124,1.7859,1.8636,1.8946,1.8339,1.841,1.8883,1.8561,1.8368,1.8502,1.8271,1.8561,1.8202,1.8456,1.8322,1.8666,1.8685,1.8144,1.8616,1.8775,1.8581,1.8232],"train_s":93.84,"peak":2.492,"load":1.3,"file":"r_uns_long_3.json"}]}},"eval":{"msgs":["請問 FitBand 5 螢幕不會亮是正常的嗎？是不是壞掉了？這已經是第三次了，請馬上處理。","MeshLink AX 我不想要了，用了三天覺得不適合，要怎麼辦理退貨？","關於 FitBand 5：被重複扣款兩次，麻煩確認。麻煩了，謝謝。","MeshLink AX 有辦法改 Wi-Fi 密碼嗎？說明書看不太懂。","查一下 PureAir 300 的物流，訂了五天還沒收到。麻煩了，謝謝。","請問 MeshLink AX 5G 訊號消失是正常的嗎？是不是壞掉了？明天就要出國了，拜託盡快！","AirWave 3 我不想要了，買錯型號，要怎麼辦理退貨？麻煩了，謝謝。","關於 NovaWatch 2：被重複扣款兩次，麻煩確認。","ZoomPad 11 有辦法分割畫面嗎？說明書看不太懂。不急，有空再回覆就好。","查一下 MeshLink AX 的物流，收到的數量少一個。不急，有空再回覆就好。","請問 PureAir 300 運轉聲音很大是正常的嗎？是不是壞掉了？這已經是第三次了，請馬上處理。","MeshLink AX 我不想要了，用了三天覺得不適合，要怎麼辦理退貨？急！今天一定要處理。","關於 FitBand 5：分期期數跟說好的不一樣，麻煩確認。明天就要出國了，拜託盡快！","NovaWatch 2 有辦法開啟睡眠偵測嗎？說明書看不太懂。明天就要出國了，拜託盡快！","查一下 AirWave 3 的物流，物流狀態卡住不動。麻煩了，謝謝。","請問 NovaWatch 2 心率一直抓不到是正常的嗎？是不是壞掉了？麻煩了，謝謝。","CleanBot S2 我不想要了，收到時外盒破損，要怎麼辦理退貨？工作要用，拜託越快越好！","關於 MeshLink AX：發票一直沒收到，麻煩確認。小孩明天考試要用，很急。","AirWave 3 有辦法配對新手機嗎？說明書看不太懂。麻煩了，謝謝。","查一下 MeshLink AX 的物流，想改收件地址。急！今天一定要處理。","請問 SwiftMouse X 滾輪亂跳是正常的嗎？是不是壞掉了？","MeshLink AX 我不想要了，收到時外盒破損，要怎麼辦理退貨？","關於 AirWave 3：被重複扣款兩次，麻煩確認。明天就要出國了，拜託盡快！","MeshLink AX 有辦法改 Wi-Fi 密碼嗎？說明書看不太懂。","查一下 PureAir 300 的物流，物流狀態卡住不動。小孩明天考試要用，很急。","請問 MeshLink AX 一直斷網是正常的嗎？是不是壞掉了？麻煩了，謝謝。","PureAir 300 我不想要了，用了三天覺得不適合，要怎麼辦理退貨？急！今天一定要處理。","關於 NovaWatch 2：被重複扣款兩次，麻煩確認。","ZoomPad 11 有辦法分割畫面嗎？說明書看不太懂。","查一下 NovaWatch 2 的物流，收到的數量少一個。不急，有空再回覆就好。","請問 FitBand 5 螢幕不會亮是正常的嗎？是不是壞掉了？","MeshLink AX 我不想要了，收到時外盒破損，要怎麼辦理退貨？","關於 FitBand 5：分期期數跟說好的不一樣，麻煩確認。小孩明天考試要用，很急。","MeshLink AX 有辦法開訪客網路嗎？說明書看不太懂。急！今天一定要處理。","查一下 PureAir 300 的物流，訂了五天還沒收到。不急，有空再回覆就好。","請問 MeshLink AX 指示燈閃紅燈是正常的嗎？是不是壞掉了？麻煩了，謝謝。","FitBand 5 我不想要了，買錯型號，要怎麼辦理退貨？麻煩了，謝謝。","關於 NovaWatch 2：發票一直沒收到，麻煩確認。工作要用，拜託越快越好！","PowerCube 20K 有辦法看剩餘電量嗎？說明書看不太懂。","查一下 NovaWatch 2 的物流，物流狀態卡住不動。急！今天一定要處理。"],"labels":[{"category":"repair","product":"FitBand 5","urgent":true,"summary":"FitBand 5 螢幕不會亮"},{"category":"return","product":"MeshLink AX","urgent":false,"summary":"MeshLink AX 申請退換：用了三天覺得不適合"},{"category":"billing","product":"FitBand 5","urgent":false,"summary":"FitBand 5 訂單被重複扣款兩次"},{"category":"howto","product":"MeshLink AX","urgent":false,"summary":"詢問 MeshLink AX 如何改 Wi-Fi 密碼"},{"category":"shipping","product":"PureAir 300","urgent":false,"summary":"PureAir 300 訂了五天還沒收到"},{"category":"repair","product":"MeshLink AX","urgent":true,"summary":"MeshLink AX 5G 訊號消失"},{"category":"return","product":"AirWave 3","urgent":false,"summary":"AirWave 3 申請退換：買錯型號"},{"category":"billing","product":"NovaWatch 2","urgent":false,"summary":"NovaWatch 2 訂單被重複扣款兩次"},{"category":"howto","product":"ZoomPad 11","urgent":false,"summary":"詢問 ZoomPad 11 如何分割畫面"},{"category":"shipping","product":"MeshLink AX","urgent":false,"summary":"MeshLink AX 收到的數量少一個"},{"category":"repair","product":"PureAir 300","urgent":true,"summary":"PureAir 300 運轉聲音很大"},{"category":"return","product":"MeshLink AX","urgent":true,"summary":"MeshLink AX 申請退換：用了三天覺得不適合"},{"category":"billing","product":"FitBand 5","urgent":true,"summary":"FitBand 5 訂單分期期數跟說好的不一樣"},{"category":"howto","product":"NovaWatch 2","urgent":true,"summary":"詢問 NovaWatch 2 如何開啟睡眠偵測"},{"category":"shipping","product":"AirWave 3","urgent":false,"summary":"AirWave 3 物流狀態卡住不動"},{"category":"repair","product":"NovaWatch 2","urgent":false,"summary":"NovaWatch 2 心率一直抓不到"},{"category":"return","product":"CleanBot S2","urgent":true,"summary":"CleanBot S2 申請退換：收到時外盒破損"},{"category":"billing","product":"MeshLink AX","urgent":true,"summary":"MeshLink AX 訂單發票一直沒收到"},{"category":"howto","product":"AirWave 3","urgent":false,"summary":"詢問 AirWave 3 如何配對新手機"},{"category":"shipping","product":"MeshLink AX","urgent":true,"summary":"MeshLink AX 想改收件地址"},{"category":"repair","product":"SwiftMouse X","urgent":false,"summary":"SwiftMouse X 滾輪亂跳"},{"category":"return","product":"MeshLink AX","urgent":false,"summary":"MeshLink AX 申請退換：收到時外盒破損"},{"category":"billing","product":"AirWave 3","urgent":true,"summary":"AirWave 3 訂單被重複扣款兩次"},{"category":"howto","product":"MeshLink AX","urgent":false,"summary":"詢問 MeshLink AX 如何改 Wi-Fi 密碼"},{"category":"shipping","product":"PureAir 300","urgent":true,"summary":"PureAir 300 物流狀態卡住不動"},{"category":"repair","product":"MeshLink AX","urgent":false,"summary":"MeshLink AX 一直斷網"},{"category":"return","product":"PureAir 300","urgent":true,"summary":"PureAir 300 申請退換：用了三天覺得不適合"},{"category":"billing","product":"NovaWatch 2","urgent":false,"summary":"NovaWatch 2 訂單被重複扣款兩次"},{"category":"howto","product":"ZoomPad 11","urgent":false,"summary":"詢問 ZoomPad 11 如何分割畫面"},{"category":"shipping","product":"NovaWatch 2","urgent":false,"summary":"NovaWatch 2 收到的數量少一個"},{"category":"repair","product":"FitBand 5","urgent":false,"summary":"FitBand 5 螢幕不會亮"},{"category":"return","product":"MeshLink AX","urgent":false,"summary":"MeshLink AX 申請退換：收到時外盒破損"},{"category":"billing","product":"FitBand 5","urgent":true,"summary":"FitBand 5 訂單分期期數跟說好的不一樣"},{"category":"howto","product":"MeshLink AX","urgent":true,"summary":"詢問 MeshLink AX 如何開訪客網路"},{"category":"shipping","product":"PureAir 300","urgent":false,"summary":"PureAir 300 訂了五天還沒收到"},{"category":"repair","product":"MeshLink AX","urgent":false,"summary":"MeshLink AX 指示燈閃紅燈"},{"category":"return","product":"FitBand 5","urgent":false,"summary":"FitBand 5 申請退換：買錯型號"},{"category":"billing","product":"NovaWatch 2","urgent":true,"summary":"NovaWatch 2 訂單發票一直沒收到"},{"category":"howto","product":"PowerCube 20K","urgent":false,"summary":"詢問 PowerCube 20K 如何看剩餘電量"},{"category":"shipping","product":"NovaWatch 2","urgent":true,"summary":"NovaWatch 2 物流狀態卡住不動"}],"answers":["{\"category\": \"repair\", \"product\": \"FitBand 5\", \"urgent\": true, \"summary\": \"FitBand 5 螢幕不會亮\"}","{\"category\": \"return\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 申請退換：用了三天覺得不適合\"}","{\"category\": \"billing\", \"product\": \"FitBand 5\", \"urgent\": false, \"summary\": \"FitBand 5 訂單被重複扣款兩次\"}","{\"category\": \"howto\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"詢問 MeshLink AX 如何改 Wi-Fi 密碼\"}","{\"category\": \"shipping\", \"product\": \"PureAir 300\", \"urgent\": false, \"summary\": \"PureAir 300 訂了五天還沒收到\"}","{\"category\": \"repair\", \"product\": \"MeshLink AX\", \"urgent\": true, \"summary\": \"MeshLink AX 5G 訊號消失\"}","{\"category\": \"return\", \"product\": \"AirWave 3\", \"urgent\": false, \"summary\": \"AirWave 3 申請退換：買錯型號\"}","{\"category\": \"billing\", \"product\": \"NovaWatch 2\", \"urgent\": false, \"summary\": \"NovaWatch 2 訂單被重複扣款兩次\"}","{\"category\": \"howto\", \"product\": \"ZoomPad 11\", \"urgent\": false, \"summary\": \"詢問 ZoomPad 11 如何分割畫面\"}","{\"category\": \"shipping\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 收到的數量少一個\"}","{\"category\": \"repair\", \"product\": \"PureAir 300\", \"urgent\": true, \"summary\": \"PureAir 300 運轉聲音很大\"}","{\"category\": \"return\", \"product\": \"MeshLink AX\", \"urgent\": true, \"summary\": \"MeshLink AX 申請退換：用了三天覺得不適合\"}","{\"category\": \"billing\", \"product\": \"FitBand 5\", \"urgent\": true, \"summary\": \"FitBand 5 訂單分期期數跟說好的不一樣\"}","{\"category\": \"howto\", \"product\": \"NovaWatch 2\", \"urgent\": true, \"summary\": \"詢問 NovaWatch 2 如何開啟睡眠偵測\"}","{\"category\": \"shipping\", \"product\": \"AirWave 3\", \"urgent\": false, \"summary\": \"AirWave 3 物流狀態卡住不動\"}","{\"category\": \"repair\", \"product\": \"NovaWatch 2\", \"urgent\": false, \"summary\": \"NovaWatch 2 心率一直抓不到\"}","{\"category\": \"return\", \"product\": \"CleanBot S2\", \"urgent\": true, \"summary\": \"CleanBot S2 申請退換：收到時外盒破損\"}","{\"category\": \"billing\", \"product\": \"MeshLink AX\", \"urgent\": true, \"summary\": \"MeshLink AX 訂單發票一直沒收到\"}","{\"category\": \"howto\", \"product\": \"AirWave 3\", \"urgent\": false, \"summary\": \"詢問 AirWave 3 如何配對新手機\"}","{\"category\": \"shipping\", \"product\": \"MeshLink AX\", \"urgent\": true, \"summary\": \"MeshLink AX 想改收件地址\"}","{\"category\": \"repair\", \"product\": \"SwiftMouse X\", \"urgent\": false, \"summary\": \"SwiftMouse X 滾輪亂跳\"}","{\"category\": \"return\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 申請退換：收到時外盒破損\"}","{\"category\": \"billing\", \"product\": \"AirWave 3\", \"urgent\": true, \"summary\": \"AirWave 3 訂單被重複扣款兩次\"}","{\"category\": \"howto\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"詢問 MeshLink AX 如何改 Wi-Fi 密碼\"}","{\"category\": \"shipping\", \"product\": \"PureAir 300\", \"urgent\": true, \"summary\": \"PureAir 300 物流狀態卡住不動\"}","{\"category\": \"repair\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 一直斷網\"}","{\"category\": \"return\", \"product\": \"PureAir 300\", \"urgent\": true, \"summary\": \"PureAir 300 申請退換：用了三天覺得不適合\"}","{\"category\": \"billing\", \"product\": \"NovaWatch 2\", \"urgent\": false, \"summary\": \"NovaWatch 2 訂單被重複扣款兩次\"}","{\"category\": \"howto\", \"product\": \"ZoomPad 11\", \"urgent\": false, \"summary\": \"詢問 ZoomPad 11 如何分割畫面\"}","{\"category\": \"shipping\", \"product\": \"NovaWatch 2\", \"urgent\": false, \"summary\": \"NovaWatch 2 收到的數量少一個\"}","{\"category\": \"repair\", \"product\": \"FitBand 5\", \"urgent\": false, \"summary\": \"FitBand 5 螢幕不會亮\"}","{\"category\": \"return\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 申請退換：收到時外盒破損\"}","{\"category\": \"billing\", \"product\": \"FitBand 5\", \"urgent\": true, \"summary\": \"FitBand 5 訂單分期期數跟說好的不一樣\"}","{\"category\": \"howto\", \"product\": \"MeshLink AX\", \"urgent\": true, \"summary\": \"詢問 MeshLink AX 如何開訪客網路\"}","{\"category\": \"shipping\", \"product\": \"PureAir 300\", \"urgent\": false, \"summary\": \"PureAir 300 訂了五天還沒收到\"}","{\"category\": \"repair\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 指示燈閃紅燈\"}","{\"category\": \"return\", \"product\": \"FitBand 5\", \"urgent\": false, \"summary\": \"FitBand 5 申請退換：買錯型號\"}","{\"category\": \"billing\", \"product\": \"NovaWatch 2\", \"urgent\": true, \"summary\": \"NovaWatch 2 訂單發票一直沒收到\"}","{\"category\": \"howto\", \"product\": \"PowerCube 20K\", \"urgent\": false, \"summary\": \"詢問 PowerCube 20K 如何看剩餘電量\"}","{\"category\": \"shipping\", \"product\": \"NovaWatch 2\", \"urgent\": true, \"summary\": \"NovaWatch 2 物流狀態卡住不動\"}"],"base_short":["以下是將您提供的訊息轉換為 JSON 工單的結果：\n\n```json\n{\n  \"工單類別\": \"產品問題\",\n  \"產品名稱\": \"FitBand 5\",\n  \"問題描述\": \"屏幕不会亮，已发生三次，请求立即处理\",\n  \"問題嚴重程度\": \"高\",\n  \"建議處理方式\": \"立即更换屏幕或联系售后\",\n  \"狀態\": \"待處理\",\n  \"創建時間\": \"2023-09-25T14:30:00\",\n  \"處理人\": \"客服工單助理\",\n  \"處理狀態\": \"待處理\"\n}\n```\n\n請根據實際情況調整相關字段內容。","以下是將您的訊息轉換為 JSON 格式的工單資訊：\n\n```json\n{\n  \"order_number\": \"MeshLink_AX_20250405\",\n  \"product_name\": \"MeshLink AX\",\n  \"user_message\": \"我不想要了，用了三天覺得不適合，要怎麼辦理退貨？\",\n  \"order_status\": \"未處理\",\n  \"contact_method\": \"電話/郵寄\",\n  \"product_type\": \"MeshLink\",\n  \"usage_duration\": \"3天\",\n  \"reason_for_return\": \"不適合\",\n  \"return_policy\": \"請聯繫客服或前往指定退貨中心辦理退貨\",\n  \"contact_info\": {\n    \"phone\": \"請提供電話號碼\",\n    \"","```json\n{\n  \"工單類別\": \"支付問題\",\n  \"產品名稱\": \"FitBand 5\",\n  \"問題描述\": \"被重複扣款兩次，麻煩確認。\",\n  \"聯絡人\": \"使用者\",\n  \"聯絡方式\": \"電話/郵件（請補充）\",\n  \"狀態\": \"待處理\",\n  \"備註\": \"請協助確認扣款情況，謝謝。\"\n}\n```","```json\n{\n  \"工單類別\": \"設備設置\",\n  \"設備名稱\": \"MeshLink AX\",\n  \"問題描述\": \"MeshLink AX 有辦法改 Wi-Fi 密碼嗎？說明書看不太懂。\",\n  \"詳細說明\": \"請問 MeshLink AX 是否有提供修改 Wi-Fi 密碼的設定方式？說明書中是否有相關說明？\",\n  \"狀態\": \"待確認\",\n  \"聯絡方式\": \"請提供更多細節，例如是否需要協助設定或修改 Wi-Fi 密碼。\"\n}\n```","```json\n{\n  \"工單類別\": \"物流查詢\",\n  \"產品名稱\": \"PureAir 300\",\n  \"訂單狀態\": \"未收到\",\n  \"訂單天數\": 5,\n  \"聯絡方式\": \"謝謝\",\n  \"問題描述\": \"查一下 PureAir 300 的物流，訂了五天還沒收到。\"\n}\n```","以下是將您提供的訊息轉換為 JSON 工單的結果：\n\n```json\n{\n  \"工單類別\": \"技術問題\",\n  \"產品名稱\": \"MeshLink AX 5G\",\n  \"問題描述\": \"MeshLink AX 5G 訊號消失，是否正常？是否壞掉？\",\n  \"嚴重程度\": \"高\",\n  \"處理需求\": \"請盡快診斷並處理，以便明天出國前準備好。\",\n  \"聯絡人\": \"請提供聯絡方式\",\n  \"備註\": \"請盡快處理，感謝協助。\"\n}\n```\n\n請將此 JSON 转換為工單系統所需的格式，並盡快回覆以確保行程順利。","{\n  \"order_number\": \"AirWave 3\",\n  \"product_type\": \"AirWave\",\n  \"issue_type\": \"退貨\",\n  \"reason\": \"買錯型號\",\n  \"contact_method\": \"電話/郵寄\",\n  \"shipping_address\": \"請提供正確的退貨地址\",\n  \"shipping_cost\": \"退貨費用由買方承擔\",\n  \"expected_delivery_time\": \"退貨後 3-5 天內處理\",\n  \"status\": \"待處理\"\n}","```json\n{\n  \"工單類別\": \"支付問題\",\n  \"問題描述\": \"NovaWatch 2 被重複扣款兩次，麻煩確認。\",\n  \"客戶資訊\": {\n    \"姓名\": \"使用者\",\n    \"帳號\": \"NovaWatch 2\",\n    \"電話\": \"請提供\",\n    \"信箱\": \"請提供\"\n  },\n  \"問題細節\": \"帳號 NovaWatch 2 連續被扣款兩次，疑似重複扣款，請確認是否正確。\",\n  \"狀態\": \"待處理\",\n  \"處理人\": \"客服工單助理\",\n  \"處理時間\": \"2023-10-05T14:30:00\",\n  \"備","```json\n{\n  \"工單類別\": \"產品功能詢問\",\n  \"產品名稱\": \"ZoomPad 11\",\n  \"問題內容\": \"ZoomPad 11 有辦法分割畫面嗎？說明書看不太懂。\",\n  \"問題類型\": \"功能詢問\",\n  \"處理狀態\": \"待處理\",\n  \"回應方式\": \"文字回應\",\n  \"回應內容\": \"感謝您的詢問。ZoomPad 11 本身並沒有內建的畫面分割功能。不過，您可以透過調整螢幕顯示模式或使用外接顯示器來實現畫面分割。建議您參考說明書中的螢幕設定部分，或聯繫 ZoomPad 官方客服進一步","```json\n{\n  \"工单编号\": \"20231015-001\",\n  \"客户姓名\": \"未知\",\n  \"产品型号\": \"MeshLink AX\",\n  \"问题描述\": \"查一下 MeshLink AX 的物流，收到的數量少一個。不急，有空再回覆就好。\",\n  \"物流状态\": \"待处理\",\n  \"物流信息\": {\n    \"物流单号\": \"未知\",\n    \"物流状态\": \"未确认\",\n    \"预计到达时间\": \"未知\",\n    \"当前状态\": \"未收到\"\n  },\n  \"备注\": \"客户表示不急，可后续处理\"\n}\n```","以下是將您提供的訊息轉換為 JSON 格式的工單：\n\n```json\n{\n  \"工單類別\": \"設備問題\",\n  \"設備名稱\": \"PureAir 300\",\n  \"問題描述\": \"運轉聲音很大，已發生三次，請馬上處理\",\n  \"問題嚴重程度\": \"嚴重\",\n  \"建議處理\": \"請立即檢查設備是否正常，並考慮是否需要維修或換新設備\",\n  \"聯絡方式\": \"請聯繫客服或前往官方維修中心\"\n}\n```\n\n請注意，此 JSON 工單為簡化版本，實際情況可能需要進一步確認。如需更詳細的處理建議，建議聯繫專業客服或技術支持。","以下是根據您的訊息轉換成的 JSON 工單格式：\n\n```json\n{\n  \"order_number\": \"MeshLink_AX_20231015_001\",\n  \"product_name\": \"MeshLink AX\",\n  \"user_message\": \"我不想要了，用了三天覺得不適合，要怎麼辦理退貨？急！今天一定要處理。\",\n  \"urgency\": \"急\",\n  \"deadline\": \"今天\",\n  \"product_type\": \"MeshLink AX\",\n  \"status\": \"待處理\",\n  \"contact_info\": {\n    \"name\": \"使用者\",\n    \"phone\": \"未提供\",\n    \"email\": \"未提供\"\n  }\n}\n```\n\n請注意：\n- 這是一份示例格式，實際","以下是將您提供的訊息轉換為 JSON 工單的結果：\n\n```json\n{\n  \"工單類別\": \"客服工單\",\n  \"產品名稱\": \"FitBand 5\",\n  \"問題內容\": \"關於 FitBand 5：分期期數跟說好的不一樣，麻煩確認。明天就要出國了，拜託盡快！\",\n  \"問題類型\": \"訂單問題\",\n  \"問題細節\": \"客戶反映 FitBand 5 的分期期數與預期不符，且客戶明天就要出國，需及時確認並處理。\",\n  \"處理狀態\": \"待處理\",\n  \"處理人\": \"客服專線\",\n  \"處理時間\": \"2023-0","以下是將您的訊息轉換為 JSON 工單的結果：\n\n```json\n{\n  \"工單類別\": \"產品使用\",\n  \"產品名稱\": \"NovaWatch 2\",\n  \"問題描述\": \"NovaWatch 2 有辦法開啟睡眠偵測嗎？說明書看不太懂。明天就要出國了，拜託盡快！\",\n  \"需求內容\": \"請協助說明 NovaWatch 2 是否支持睡眠偵測功能，並提供相關說明書或設定方法，以便明天出國前準備好。\",\n  \"狀態\": \"待處理\",\n  \"處理人\": \"客服工單助理\",\n  \"處理時間\": \"2023-05-15T12:00:00","{\n  \"工單類別\": \"物流查詢\",\n  \"產品名稱\": \"AirWave 3\",\n  \"問題描述\": \"物流狀態卡住不動\",\n  \"聯絡人\": \"使用者\",\n  \"聯絡方式\": \"電話/郵件\",\n  \"狀態\": \"待處理\",\n  \"備註\": \"請查驗物流狀態，確認是否卡住，並提供相關物流單號或資訊\"\n}","以下是將您的訊息轉換成的 JSON 工單格式：\n\n```json\n{\n  \"工單類別\": \"技術支持\",\n  \"問題描述\": \"NovaWatch 2 心率一直抓不到，是否正常？是否壞掉了？\",\n  \"狀態\": \"待處理\",\n  \"優先級\": \"高\",\n  \"聯絡人\": \"使用者\",\n  \"問題細節\": \"NovaWatch 2 心率一直抓不到，是否正常？是否壞掉了？\",\n  \"建議行動\": \"請確認設備是否正常連接，檢查軟體設定，重置設備，若問題持續請聯繫技術支援。\",\n  \"回應時間\": \"2025-03-15T10:0","```json\n{\n  \"工單號\": \"20231015-001\",\n  \"客戶姓名\": \"使用者\",\n  \"產品名稱\": \"CleanBot S2\",\n  \"問題描述\": \"收到時外盒破損\",\n  \"退貨要求\": \"工作要用，拜託越快越好\",\n  \"退貨處理\": {\n    \"處理方式\": \"退貨\",\n    \"處理時間\": \"越快越好\",\n    \"處理人\": \"客服\",\n    \"處理狀態\": \"進行中\"\n  }\n}\n```","```json\n{\n  \"工單類別\": \"發票問題\",\n  \"問題描述\": \"關於 MeshLink AX：發票一直沒收到，麻煩確認。小孩明天考試要用，很急。\",\n  \"聯絡人\": \"使用者\",\n  \"聯絡方式\": \"未提供\",\n  \"問題細節\": \"MeshLink AX 的發票未收到，需立即確認處理，因小孩明天考試，急需處理。\",\n  \"狀態\": \"待處理\",\n  \"優先級\": \"高\",\n  \"創建時間\": \"2023-09-25T14:30:00\",\n  \"處理人\": \"客服工單助理\",\n  \"處理狀態\": \"待處理\"\n}\n```","以下是將您的訊息轉換成的 JSON 工單格式：\n\n```json\n{\n  \"工單類別\": \"客服工單\",\n  \"問題描述\": \"AirWave 3 有辦法配對新手機嗎？說明書看不太懂。\",\n  \"建議解決方案\": \"建議您先確認 AirWave 3 的配對方式，通常需要透過藍牙或 Wi-Fi 配對。請查看說明書中關於配對的步驟，或提供更詳細的說明。若仍不清楚，建議聯繫 AirWave 3 的官方支持取得進一步協助。\",\n  \"狀態\": \"待處理\",\n  \"處理人\": \"客服工單助理\",\n  \"處理時間\": \"2023-04","以下是將您的訊息轉換為 JSON 工單的格式：\n\n```json\n{\n  \"工單類別\": \"物流查詢與地址更改\",\n  \"問題描述\": \"查一下 MeshLink AX 的物流，想改收件地址。急！今天一定要處理。\",\n  \"處理狀態\": \"待處理\",\n  \"處理人\": \"客服工單助理\",\n  \"處理時間\": \"2023-10-05T12:00:00\",\n  \"備註\": \"請協助處理，感謝配合！\"\n}\n```\n\n請注意，此 JSON 工單為示例格式，實際處理時需根據實際情況調整。","```json\n{\n  \"工單類別\": \"硬件問題\",\n  \"產品名稱\": \"SwiftMouse X\",\n  \"問題描述\": \"SwiftMouse X 滚輪亂跳，是否正常？是否壞掉？\",\n  \"狀態\": \"待處理\",\n  \"優先級\": \"高\",\n  \"詳細資訊\": {\n    \"設備資訊\": {\n      \"品牌\": \"SwiftMouse\",\n      \"產品名稱\": \"X\",\n      \"模型\": \"X\",\n      \"版本\": \"未提供\"\n    },\n    \"問題現象\": {\n      \"描述\": \"滾輪亂跳\",\n      \"是否正常\": \"未明確說明，需進一步確認\",\n      \"可能原因\": \"可能因滾輪損壞、驅","以下是將您提供的訊息轉換為 JSON 格式的工單資訊：\n\n```json\n{\n  \"訂單號\": \"MeshLink AX\",\n  \"產品名稱\": \"MeshLink AX\",\n  \"問題描述\": \"收到時外盒破損\",\n  \"退貨要求\": \"請辦理退貨\",\n  \"退貨原因\": \"外盒破損\",\n  \"聯絡方式\": \"請提供聯絡方式以進行退貨處理\",\n  \"狀態\": \"待處理\"\n}\n```\n\n請提供您的聯絡方式，以便我們協助您辦理退貨流程。","```json\n{\n  \"工單類別\": \"支付問題\",\n  \"問題描述\": \"AirWave 3 被重複扣款兩次，麻煩確認。\",\n  \"狀態\": \"待處理\",\n  \"聯絡人\": \"使用者\",\n  \"聯絡方式\": \"電話/電子郵件\",\n  \"處理期限\": \"明天\",\n  \"備註\": \"使用者將於明天出國，請盡快處理。\"\n}\n```","```json\n{\n  \"工單類別\": \"設備設置\",\n  \"設備名稱\": \"MeshLink AX\",\n  \"問題描述\": \"MeshLink AX 有辦法改 Wi-Fi 密碼嗎？說明書看不太懂。\",\n  \"詳細說明\": \"請問 MeshLink AX 是否有提供修改 Wi-Fi 密碼的設定方式？說明書中是否有相關說明？\",\n  \"狀態\": \"待確認\",\n  \"聯絡方式\": \"請提供更多細節，例如是否需要協助設定或修改 Wi-Fi 密碼。\"\n}\n```","{\n  \"工單類別\": \"物流查詢\",\n  \"產品名稱\": \"PureAir 300\",\n  \"狀態\": \"卡住不動\",\n  \"客戶需求\": \"小孩明天考試要用，很急\",\n  \"聯絡人\": \"使用者\",\n  \"聯絡方式\": \"未提供\",\n  \"備註\": \"請協助處理物流狀態，盡快安排配送\"\n}","以下是將您的訊息轉換成 JSON 工單的結果：\n\n```json\n{\n  \"工單類別\": \"網路連線問題\",\n  \"設備名稱\": \"MeshLink AX\",\n  \"問題描述\": \"MeshLink AX 一直斷網，是否正常？是否壞掉了？\",\n  \"狀態\": \"待處理\",\n  \"建議行動\": \"請檢查網路連線狀態，確認路由器與模组之間的連線是否正常，並嘗試重啟設備。若問題持續，建議聯繫專業技術人員或確認設備是否正常。\",\n  \"回應時間\": \"待定\",\n  \"處理人\": \"客服工單助理\",\n  \"狀態更新\": \"待處理\"\n}\n```\n\n如有進一步資訊，","{\n  \"order_no\": \"20231015001\",\n  \"product_name\": \"PureAir 300\",\n  \"user_message\": \"我不想要了，用了三天覺得不適合，要怎麼辦理退貨？急！今天一定要處理。\",\n  \"status\": \"待处理\",\n  \"priority\": \"高\",\n  \"contact_method\": \"电话/邮件/在线客服\",\n  \"deadline\": \"2023-10-15\",\n  \"product_type\": \"空气清新器\",\n  \"product_model\": \"PureAir 300\",\n  \"user_location\": \"台湾\",\n  \"user_contact\": \"未提供\",\n  \"product_status\": \"已使用3天\",\n  \"reason\": \"","```json\n{\n  \"工單類別\": \"支付問題\",\n  \"問題描述\": \"NovaWatch 2 被重複扣款兩次，麻煩確認。\",\n  \"客戶資訊\": {\n    \"姓名\": \"使用者\",\n    \"帳號\": \"NovaWatch 2\",\n    \"金額\": \"未提供\",\n    \"扣款次數\": 2\n  },\n  \"狀態\": \"待處理\",\n  \"聯絡方式\": \"請使用者提供更多細節或聯繫客服\",\n  \"創建時間\": \"2023-09-25T14:30:00\",\n  \"處理人\": \"客服工單助理\"\n}\n```","```json\n{\n  \"工單類別\": \"產品功能問詢\",\n  \"產品名稱\": \"ZoomPad 11\",\n  \"問題內容\": \"ZoomPad 11 有辦法分割畫面嗎？說明書看不太懂。\",\n  \"問題類型\": \"功能詢問\",\n  \"狀態\": \"待處理\",\n  \"處理人\": \"客服工單助理\",\n  \"處理時間\": \"2023-09-20T14:30:00\",\n  \"回應內容\": \"感謝您提出問題。ZoomPad 11 本身並沒有內建的畫面分割功能，但您可以在使用時透過滑鼠或觸控操作來調整視窗大小與位置，","```json\n{\n  \"工單類別\": \"退換貨\",\n  \"問題描述\": \"NovaWatch 2 的物流，收到的數量少一個\",\n  \"狀態\": \"待處理\",\n  \"聯絡方式\": \"請提供訂單號或購買資訊以進一步處理\",\n  \"備註\": \"不急，有空再回覆就好\"\n}\n```","以下是將您提供的訊息轉換為 JSON 工單的結果：\n\n```json\n{\n  \"工單類別\": \"技術支持\",\n  \"問題描述\": \"FitBand 5 螢幕不會亮\",\n  \"狀態\": \"待處理\",\n  \"嚴重程度\": \"中度\",\n  \"建議行動\": \"請檢查電源是否正常、螢幕保護模式是否啟用、並嘗試重啟設備。若問題持續，建議聯繫官方支持或送修。\",\n  \"回應時間\": \"2023-10-25T14:30:00\",\n  \"處理人\": \"客服工單助理\",\n  \"處理狀態\": \"待處理\"\n}\n```\n\n請","以下是將您提供的訊息轉換為 JSON 格式的工單資訊：\n\n```json\n{\n  \"訂單號\": \"MeshLink AX\",\n  \"產品名稱\": \"MeshLink AX\",\n  \"問題描述\": \"收到時外盒破損\",\n  \"退貨要求\": \"請辦理退貨\",\n  \"退貨原因\": \"外盒破損\",\n  \"聯絡方式\": \"請提供聯絡方式以進行退貨處理\",\n  \"狀態\": \"待處理\"\n}\n```\n\n請提供您的聯絡方式，以便我們協助您辦理退貨流程。","```json\n{\n  \"工單類別\": \"產品問題\",\n  \"產品名稱\": \"FitBand 5\",\n  \"問題內容\": \"關於 FitBand 5：分期期數跟說好的不一樣，麻煩確認。小孩明天考試要用，很急。\",\n  \"聯絡人\": \"使用者\",\n  \"聯絡方式\": \"電話/郵件/其他\",\n  \"處理狀態\": \"待處理\",\n  \"優先級\": \"高\",\n  \"備註\": \"小孩明天考試，需及時處理，請加快處理速度。\"\n}\n```","以下是將您的訊息轉換為 JSON 工單的結果：\n\n```json\n{\n  \"工單類別\": \"技術支持\",\n  \"問題描述\": \"MeshLink AX 有辦法開訪客網路嗎？說明書看不太懂。急！今天一定要處理。\",\n  \"設備名稱\": \"MeshLink AX\",\n  \"問題類型\": \"網路設定\",\n  \"狀態\": \"待處理\",\n  \"優先級\": \"高\",\n  \"聯絡人\": \"使用者\",\n  \"備註\": \"說明書看不太懂，急！今天一定要處理\"\n}\n```\n\n請將此 JSON 转換為工單系統所需的格式，並安排技術人員回覆。","```json\n{\n  \"工單類別\": \"物流查詢\",\n  \"產品名稱\": \"PureAir 300\",\n  \"訂單狀態\": \"已下單\",\n  \"物流狀態\": \"未收到\",\n  \"預計到貨時間\": \"五天後\",\n  \"備註\": \"不急，有空再回覆就好\"\n}\n```","以下是將您的訊息轉換為 JSON 工單的結果：\n\n```json\n{\n  \"工單類別\": \"設備異常\",\n  \"設備名稱\": \"MeshLink AX\",\n  \"問題描述\": \"指示燈閃紅燈\",\n  \"問題類型\": \"異常\",\n  \"問題嚴重度\": \"中度\",\n  \"建議處理\": \"建議檢查設備連線狀態及電源供應，若問題持續，建議聯繫專業維修人員。\",\n  \"狀態\": \"待處理\"\n}\n```\n\n請問是否需要進一步協助處理此工單？","{\n  \"order_no\": \"20231015001\",\n  \"product_name\": \"FitBand 5\",\n  \"user_message\": \"FitBand 5 我不想要了，買錯型號，要怎麼辦理退貨？麻煩了，謝謝。\",\n  \"status\": \"待處理\",\n  \"contact_method\": \"電話/郵寄\",\n  \"expected_response\": \"請聯繫客服中心，提供訂單號與購買細節，我們將協助您辦理退貨。\"\n}","```json\n{\n  \"工單類別\": \"發票問題\",\n  \"問題描述\": \"NovaWatch 2 的發票一直沒收到，工作要用，拜託越快越好！\",\n  \"狀態\": \"待處理\",\n  \"聯絡人\": \"使用者\",\n  \"聯絡方式\": \"電話/電子郵件（未提供）\",\n  \"備註\": \"此筆工單需快速處理，建議優先處理。\"\n}\n```","```json\n{\n  \"工單類別\": \"產品功能詢問\",\n  \"產品名稱\": \"PowerCube 20K\",\n  \"問題內容\": \"PowerCube 20K 有辦法看剩餘電量嗎？說明書看不太懂。\",\n  \"問題類型\": \"功能詢問\",\n  \"處理狀態\": \"待處理\",\n  \"聯絡方式\": \"email or phone\"\n}\n```","{\n  \"工單類別\": \"物流狀態\",\n  \"問題描述\": \"NovaWatch 2 的物流狀態卡住不動，急！今天一定要處理。\",\n  \"產品名稱\": \"NovaWatch 2\",\n  \"狀態\": \"卡住不動\",\n  \"處理需求\": \"立即處理，今天內解決\",\n  \"聯絡方式\": \"請聯繫客服中心\",\n  \"狀態更新時間\": \"2023-10-05T12:00:00Z\"\n}"],"base_detailed":["{\"category\": \"repair\", \"product\": \"FitBand 5\", \"urgent\": true, \"summary\": \"FitBand 5 屏幕不亮是否正常？是否壞掉？已發生三次，請馬上處理。\"}","{\"category\": \"return\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"退貨請求，產品使用三天，不適合繼續使用\"}","{\"category\": \"billing\", \"product\": \"FitBand 5\", \"urgent\": true, \"summary\": \"FitBand 5 被重複扣款兩次，請確認。\"}","{\"category\": \"howto\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 有辦法改 Wi-Fi 密碼嗎？\"}","{\"category\": \"shipping\", \"product\": \"PureAir 300\", \"urgent\": true, \"summary\": \"PureAir 300 的物流狀況，訂了五天還沒收到。\"}","{\"category\": \"repair\", \"product\": \"MeshLink AX 5G\", \"urgent\": true, \"summary\": \"MeshLink AX 5G 訊號消失是否正常？是否壞掉？\"}","{\"category\": \"return\", \"product\": \"AirWave 3\", \"urgent\": true, \"summary\": \"客戶表示不想要AirWave 3，認為買錯型號，要求辦理退貨。\"}","```json\n{\n  \"category\": \"billing\",\n  \"product\": \"NovaWatch 2\",\n  \"urgent\": true,\n  \"summary\": \"NovaWatch 2 被重複扣款兩次，請確認。\"\n}\n```","{\"category\": \"howto\", \"product\": \"ZoomPad 11\", \"urgent\": false, \"summary\": \"ZoomPad 11 是否有辦法分割畫面？\"}","{\"category\": \"shipping\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 的物流資訊顯示收到的數量少一個。\"}","{\"category\": \"repair\", \"product\": \"PureAir 300\", \"urgent\": true, \"summary\": \"PureAir 300運轉聲音很大是正常的嗎？是不是壞掉了？\"}","{\"category\": \"return\", \"product\": \"MeshLink AX\", \"urgent\": true, \"summary\": \"退貨申請，產品為 MeshLink AX，急需處理，今天必須完成。\"}","```json\n{\n  \"category\": \"return\",\n  \"product\": \"FitBand 5\",\n  \"urgent\": true,\n  \"summary\": \"FitBand 5 的分期期數與說好的不一樣，麻煩確認。明天就要出國了，拜託盡快！\"\n}\n```","{\"category\": \"howto\", \"product\": \"NovaWatch 2\", \"urgent\": true, \"summary\": \"NovaWatch 2 是否有辦法開啟睡眠偵測？說明書看不太懂。\"}","{\"category\": \"shipping\", \"product\": \"AirWave 3\", \"urgent\": true, \"summary\": \"查一下 AirWave 3 的物流，物流狀態卡住不動。\"}","{\"category\": \"repair\", \"product\": \"NovaWatch 2\", \"urgent\": true, \"summary\": \"NovaWatch 2 心率一直抓不到是否正常？是否壞掉了？\"}","{\"category\": \"return\", \"product\": \"CleanBot S2\", \"urgent\": true, \"summary\": \"客戶表示不想要CleanBot S2，因外盒破損，要求退貨，並希望越快越好。\"}","```json\n{\n  \"category\": \"return\",\n  \"product\": \"MeshLink AX\",\n  \"urgent\": true,\n  \"summary\": \"發票一直沒收到，小孩明天考試要用，很急。\"\n}\n```","{\"category\": \"howto\", \"product\": \"AirWave 3\", \"urgent\": false, \"summary\": \"AirWave 3 是否有辦法配對新手機？說明書看不太懂。\"}","{\"category\": \"shipping\", \"product\": \"MeshLink AX\", \"urgent\": true, \"summary\": \"查一下 MeshLink AX 的物流，想改收件地址。急！今天一定要處理。\"}","{\"category\": \"howto\", \"product\": \"SwiftMouse X\", \"urgent\": false, \"summary\": \"SwiftMouse X 滾輪亂跳是否正常，是否壞掉？\"}","{\"category\": \"return\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 外盒破損，要求退貨\"}","{\"category\": \"billing\", \"product\": \"AirWave 3\", \"urgent\": true, \"summary\": \"AirWave 3 被重複扣款兩次，請確認。\"}","{\"category\": \"howto\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 有辦法改 Wi-Fi 密碼嗎？\"}","{\"category\": \"shipping\", \"product\": \"PureAir 300\", \"urgent\": true, \"summary\": \"PureAir 300 的物流狀態卡住不動，小孩明天考試要用，很急。\"}","{\"category\": \"repair\", \"product\": \"MeshLink AX\", \"urgent\": true, \"summary\": \"MeshLink AX 一直斷網是否正常？是否壞掉了？\"}","{\"category\": \"return\", \"product\": \"PureAir 300\", \"urgent\": true, \"summary\": \"PureAir 300 三天使用，需辦理退貨，急！今天處理。\"}","```json\n{\n  \"category\": \"billing\",\n  \"product\": \"NovaWatch 2\",\n  \"urgent\": true,\n  \"summary\": \"NovaWatch 2 被重複扣款兩次，請確認。\"\n}\n```","{\"category\": \"howto\", \"product\": \"ZoomPad 11\", \"urgent\": false, \"summary\": \"ZoomPad 11 是否有分割畫面的功能？\"}","{\"category\": \"shipping\", \"product\": \"NovaWatch 2\", \"urgent\": false, \"summary\": \"NovaWatch 2 的物流收到數量少一個\"}","{\"category\": \"repair\", \"product\": \"FitBand 5\", \"urgent\": true, \"summary\": \"FitBand 5 螢幕不亮是否正常？是否壞掉？\"}","{\"category\": \"return\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 外盒破損，要求退貨\"}","```json\n{\n  \"category\": \"return\",\n  \"product\": \"FitBand 5\",\n  \"urgent\": true,\n  \"summary\": \"FitBand 5 的分期期數與說好的不同，小孩明天考試要用，很急。\"\n}\n```","```json\n{\n  \"category\": \"howto\",\n  \"product\": \"MeshLink AX\",\n  \"urgent\": true,\n  \"summary\": \"MeshLink AX 是否有開訪客網路的功能？說明書看不太懂，急！今天一定要處理。\"\n}\n```","{\"category\": \"shipping\", \"product\": \"PureAir 300\", \"urgent\": false, \"summary\": \"PureAir 300 的物流狀況，訂了五天還沒收到。\"}","{\"category\": \"repair\", \"product\": \"MeshLink AX\", \"urgent\": true, \"summary\": \"MeshLink AX 指示燈閃紅燈是否正常，是否壞掉\"}","{\"category\": \"return\", \"product\": \"FitBand 5\", \"urgent\": true, \"summary\": \"FitBand 5 買錯型號，需辦理退貨請協助。\"}","{\"category\": \"billing\", \"product\": \"NovaWatch 2\", \"urgent\": true, \"summary\": \"NovaWatch 2 发票一直没收到，工作要用，拜託越快越好！\"}","{\"category\": \"howto\", \"product\": \"PowerCube 20K\", \"urgent\": false, \"summary\": \"PowerCube 20K 是否有辦法查看剩餘電量？\"}","{\"category\": \"shipping\", \"product\": \"NovaWatch 2\", \"urgent\": true, \"summary\": \"NovaWatch 2 的物流狀態卡住不動，急！今天一定要處理。\"}"],"trl":["{\"category\": \"repair\", \"product\": \"FitBand 5\", \"urgent\": true, \"summary\": \"FitBand 5 螢幕不會亮\"}","{\"category\": \"return\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 申請退換：用了三天覺得不適合\"}","{\"category\": \"billing\", \"product\": \"FitBand 5\", \"urgent\": false, \"summary\": \"FitBand 5 被重複扣款兩次\"}","{\"category\": \"howto\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"詢問 MeshLink AX 如何修改 Wi-Fi 密碼\"}","{\"category\": \"shipping\", \"product\": \"PureAir 300\", \"urgent\": false, \"summary\": \"PureAir 300 訂了五天還沒收到\"}","{\"category\": \"repair\", \"product\": \"MeshLink AX 5G\", \"urgent\": true, \"summary\": \"MeshLink AX 5G 訊號消失\"}","{\"category\": \"return\", \"product\": \"AirWave 3\", \"urgent\": false, \"summary\": \"AirWave 3 訂單買錯型號\"}","{\"category\": \"billing\", \"product\": \"NovaWatch 2\", \"urgent\": false, \"summary\": \"NovaWatch 2 訂單被重複扣款兩次\"}","{\"category\": \"howto\", \"product\": \"ZoomPad 11\", \"urgent\": false, \"summary\": \"詢問 ZoomPad 11 是否有辦法分割畫面\"}","{\"category\": \"shipping\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 收到的數量少一個\"}","{\"category\": \"repair\", \"product\": \"PureAir 300\", \"urgent\": true, \"summary\": \"PureAir 300 運轉聲音很大\"}","{\"category\": \"return\", \"product\": \"MeshLink AX\", \"urgent\": true, \"summary\": \"MeshLink AX 申請退換：用了三天覺得不適合\"}","{\"category\": \"billing\", \"product\": \"FitBand 5\", \"urgent\": true, \"summary\": \"FitBand 5 申請分期期數跟說好的不一樣\"}","{\"category\": \"howto\", \"product\": \"NovaWatch 2\", \"urgent\": true, \"summary\": \"詢問 NovaWatch 2 如何開啟睡眠偵測\"}","{\"category\": \"shipping\", \"product\": \"AirWave 3\", \"urgent\": false, \"summary\": \"AirWave 3 物流狀態卡住不動\"}","{\"category\": \"repair\", \"product\": \"NovaWatch 2\", \"urgent\": false, \"summary\": \"NovaWatch 2 心率一直抓不到\"}","{\"category\": \"return\", \"product\": \"CleanBot S2\", \"urgent\": true, \"summary\": \"CleanBot S2 申請退換：收到時外盒破損\"}","{\"category\": \"billing\", \"product\": \"MeshLink AX\", \"urgent\": true, \"summary\": \"MeshLink AX：發票一直沒收到\"}","{\"category\": \"howto\", \"product\": \"AirWave 3\", \"urgent\": false, \"summary\": \"詢問 AirWave 3 是否可以配對新手機\"}","{\"category\": \"shipping\", \"product\": \"MeshLink AX\", \"urgent\": true, \"summary\": \"MeshLink AX 訂單要改收件地址\"}","{\"category\": \"repair\", \"product\": \"SwiftMouse X\", \"urgent\": false, \"summary\": \"SwiftMouse X 滾輪亂跳\"}","{\"category\": \"return\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 申請退換：收到時外盒破損\"}","{\"category\": \"billing\", \"product\": \"AirWave 3\", \"urgent\": true, \"summary\": \"AirWave 3 訂單被重複扣款兩次\"}","{\"category\": \"howto\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"詢問 MeshLink AX 如何修改 Wi-Fi 密碼\"}","{\"category\": \"shipping\", \"product\": \"PureAir 300\", \"urgent\": true, \"summary\": \"PureAir 300 物流狀態卡住不動\"}","{\"category\": \"repair\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 一直斷網\"}","{\"category\": \"return\", \"product\": \"PureAir 300\", \"urgent\": true, \"summary\": \"PureAir 300 申請退換：用了三天覺得不適合\"}","{\"category\": \"billing\", \"product\": \"NovaWatch 2\", \"urgent\": false, \"summary\": \"NovaWatch 2 被重複扣款兩次\"}","{\"category\": \"howto\", \"product\": \"ZoomPad 11\", \"urgent\": false, \"summary\": \"詢問 ZoomPad 11 是否有辦法分割畫面\"}","{\"category\": \"shipping\", \"product\": \"NovaWatch 2\", \"urgent\": false, \"summary\": \"NovaWatch 2 收到的數量少一個\"}","{\"category\": \"repair\", \"product\": \"FitBand 5\", \"urgent\": false, \"summary\": \"FitBand 5 螢幕不會亮\"}","{\"category\": \"return\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 申請退換：收到時外盒破損\"}","{\"category\": \"billing\", \"product\": \"FitBand 5\", \"urgent\": true, \"summary\": \"FitBand 5 申請分期期數跟說好的不一樣\"}","{\"category\": \"howto\", \"product\": \"MeshLink AX\", \"urgent\": true, \"summary\": \"詢問 MeshLink AX 如何開訪客網路\"}","{\"category\": \"shipping\", \"product\": \"PureAir 300\", \"urgent\": false, \"summary\": \"PureAir 300 訂了五天還沒收到\"}","{\"category\": \"repair\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 指示燈閃紅燈\"}","{\"category\": \"return\", \"product\": \"FitBand 5\", \"urgent\": false, \"summary\": \"FitBand 5 申請退換：買錯型號\"}","{\"category\": \"billing\", \"product\": \"NovaWatch 2\", \"urgent\": true, \"summary\": \"NovaWatch 2：發票一直沒收到\"}","{\"category\": \"howto\", \"product\": \"PowerCube 20K\", \"urgent\": false, \"summary\": \"詢問 PowerCube 20K 如何看剩餘電量\"}","{\"category\": \"shipping\", \"product\": \"NovaWatch 2\", \"urgent\": true, \"summary\": \"NovaWatch 2 物流狀態卡住不動\"}"],"unsloth":["{\"category\": \"repair\", \"product\": \"FitBand 5\", \"urgent\": true, \"summary\": \"FitBand 5 螢幕不會亮\"}","{\"category\": \"return\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 申請退換：用了三天覺得不適合\"}","{\"category\": \"billing\", \"product\": \"FitBand 5\", \"urgent\": false, \"summary\": \"FitBand 5 被重複扣款兩次\"}","{\"category\": \"howto\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"詢問 MeshLink AX 如何修改 Wi-Fi 密碼\"}","{\"category\": \"shipping\", \"product\": \"PureAir 300\", \"urgent\": false, \"summary\": \"PureAir 300 訂了五天還沒收到\"}","{\"category\": \"repair\", \"product\": \"MeshLink AX 5G\", \"urgent\": true, \"summary\": \"MeshLink AX 5G 訊號消失\"}","{\"category\": \"return\", \"product\": \"AirWave 3\", \"urgent\": false, \"summary\": \"AirWave 3 訂單買錯型號\"}","{\"category\": \"billing\", \"product\": \"NovaWatch 2\", \"urgent\": false, \"summary\": \"NovaWatch 2 訂單被重複扣款兩次\"}","{\"category\": \"howto\", \"product\": \"ZoomPad 11\", \"urgent\": false, \"summary\": \"詢問 ZoomPad 11 是否有辦法分割畫面\"}","{\"category\": \"shipping\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 收到的數量少一個\"}","{\"category\": \"repair\", \"product\": \"PureAir 300\", \"urgent\": true, \"summary\": \"PureAir 300 運轉聲音很大\"}","{\"category\": \"return\", \"product\": \"MeshLink AX\", \"urgent\": true, \"summary\": \"MeshLink AX 申請退換：用了三天覺得不適合\"}","{\"category\": \"billing\", \"product\": \"FitBand 5\", \"urgent\": true, \"summary\": \"FitBand 5 申請分期期數跟說好的不一樣\"}","{\"category\": \"howto\", \"product\": \"NovaWatch 2\", \"urgent\": true, \"summary\": \"詢問 NovaWatch 2 如何開啟睡眠偵測\"}","{\"category\": \"shipping\", \"product\": \"AirWave 3\", \"urgent\": false, \"summary\": \"AirWave 3 物流狀態卡住不動\"}","{\"category\": \"repair\", \"product\": \"NovaWatch 2\", \"urgent\": false, \"summary\": \"NovaWatch 2 心率一直抓不到\"}","{\"category\": \"return\", \"product\": \"CleanBot S2\", \"urgent\": true, \"summary\": \"CleanBot S2 申請退換：收到時外盒破損\"}","{\"category\": \"billing\", \"product\": \"MeshLink AX\", \"urgent\": true, \"summary\": \"MeshLink AX：發票一直沒收到\"}","{\"category\": \"howto\", \"product\": \"AirWave 3\", \"urgent\": false, \"summary\": \"詢問 AirWave 3 是否可以配對新手機\"}","{\"category\": \"shipping\", \"product\": \"MeshLink AX\", \"urgent\": true, \"summary\": \"MeshLink AX 想改收件地址\"}","{\"category\": \"repair\", \"product\": \"SwiftMouse X\", \"urgent\": false, \"summary\": \"SwiftMouse X 滾輪亂跳\"}","{\"category\": \"return\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 申請退換：收到時外盒破損\"}","{\"category\": \"billing\", \"product\": \"AirWave 3\", \"urgent\": true, \"summary\": \"AirWave 3 訂單被重複扣款兩次\"}","{\"category\": \"howto\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"詢問 MeshLink AX 如何修改 Wi-Fi 密碼\"}","{\"category\": \"shipping\", \"product\": \"PureAir 300\", \"urgent\": true, \"summary\": \"PureAir 300 物流狀態卡住不動\"}","{\"category\": \"repair\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 一直斷網\"}","{\"category\": \"return\", \"product\": \"PureAir 300\", \"urgent\": true, \"summary\": \"PureAir 300 申請退換：用了三天覺得不適合\"}","{\"category\": \"billing\", \"product\": \"NovaWatch 2\", \"urgent\": false, \"summary\": \"NovaWatch 2 訂單被重複扣款兩次\"}","{\"category\": \"howto\", \"product\": \"ZoomPad 11\", \"urgent\": false, \"summary\": \"詢問 ZoomPad 11 是否有辦法分割畫面\"}","{\"category\": \"shipping\", \"product\": \"NovaWatch 2\", \"urgent\": false, \"summary\": \"NovaWatch 2 收到的數量少一個\"}","{\"category\": \"repair\", \"product\": \"FitBand 5\", \"urgent\": false, \"summary\": \"FitBand 5 螢幕不會亮\"}","{\"category\": \"return\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 申請退換：收到時外盒破損\"}","{\"category\": \"billing\", \"product\": \"FitBand 5\", \"urgent\": true, \"summary\": \"FitBand 5 申請分期期數跟說好的不一樣\"}","{\"category\": \"howto\", \"product\": \"MeshLink AX\", \"urgent\": true, \"summary\": \"詢問 MeshLink AX 如何開訪客網路\"}","{\"category\": \"shipping\", \"product\": \"PureAir 300\", \"urgent\": false, \"summary\": \"PureAir 300 訂了五天還沒收到\"}","{\"category\": \"repair\", \"product\": \"MeshLink AX\", \"urgent\": false, \"summary\": \"MeshLink AX 指示燈閃紅燈\"}","{\"category\": \"return\", \"product\": \"FitBand 5\", \"urgent\": false, \"summary\": \"FitBand 5 訂單買錯型號\"}","{\"category\": \"billing\", \"product\": \"NovaWatch 2\", \"urgent\": true, \"summary\": \"NovaWatch 2 申請發票一直沒收到\"}","{\"category\": \"howto\", \"product\": \"PowerCube 20K\", \"urgent\": false, \"summary\": \"詢問 PowerCube 20K 如何看剩餘電量\"}","{\"category\": \"shipping\", \"product\": \"NovaWatch 2\", \"urgent\": true, \"summary\": \"NovaWatch 2 物流狀態卡住不動\"}"]},"mask":{"trl":[{"loss":false,"text":"<|im_start|>system\n你是客服工單助理。把使用者訊息轉成 JSON 工單。<|im_end|>\n<|im_start|>user\n我的 ZoomPad 11 螢幕有一條綠線，可以幫忙修嗎？不急，有空再回覆就好。<|im_end|>\n<|im_start|>assistant\n","n":61},{"loss":true,"text":"<think>\n\n</think>\n\n{\"category\": \"repair\", \"product\": \"ZoomPad 11\", \"urgent\": false, \"summary\": \"ZoomPad 11 螢幕有一條綠線\"}<|im_end|>\n","n":45}],"unsloth":[{"loss":false,"text":"<|im_start|>system\n你是客服工單助理。把使用者訊息轉成 JSON 工單。<|im_end|>\n<|im_start|>user\n我的 ZoomPad 11 螢幕有一條綠線，可以幫忙修嗎？不急，有空再回覆就好。<|im_end|>\n<|im_start|>assistant\n","n":61},{"loss":true,"text":"<think>\n\n</think>\n\n{\"category\": \"repair\", \"product\": \"ZoomPad 11\", \"urgent\": false, \"summary\": \"ZoomPad 11 螢幕有一條綠線\"}<|im_end|>","n":44},{"loss":false,"text":"\n<|im_start|>system\n你是客服工單助理。把使用者訊息轉成 JSON 工單。<|im_end|>\n<|im_start|>user\n新買的平板 ZoomPad 11，不知道怎麼分割畫面。這已經是第三次了，請馬上處理。<|im_end|>\n<|im_start|>assistant\n","n":58},{"loss":true,"text":"<think>\n\n</think>\n\n{\"category\": \"howto\", \"product\": \"ZoomPad 11\", \"urgent\": true, \"summary\": \"詢問 ZoomPad 11 如何分割畫面\"}<|im_end|>","n":44}]},"cal":[{"model":"Qwen3-1.7B","method":"qlora","tool":"trl","scn":"short","seq":100,"batch":8,"batch_tokens":797,"peak":2.18,"load":1.257,"role":"fit"},{"model":"Qwen3-1.7B","method":"qlora","tool":"unsloth","scn":"short","seq":100,"batch":8,"batch_tokens":797,"peak":2.0,"load":1.3,"role":"fit"},{"model":"Qwen3-1.7B","method":"qlora","tool":"trl","scn":"long","seq":1179,"batch":8,"batch_tokens":9430,"peak":4.086,"load":1.257,"role":"fit"},{"model":"Qwen3-1.7B","method":"qlora","tool":"unsloth","scn":"long","seq":1179,"batch":8,"batch_tokens":9430,"peak":2.492,"load":1.3,"role":"fit"},{"model":"Qwen3-0.6B","method":"qlora","tool":"trl","scn":"short","seq":100,"batch":8,"batch_tokens":797,"peak":1.307,"load":0.502,"role":"fit"},{"model":"Qwen3-0.6B","method":"qlora","tool":"unsloth","scn":"short","seq":100,"batch":8,"batch_tokens":797,"peak":1.162,"load":0.545,"role":"fit"},{"model":"Qwen3-1.7B","method":"lora16","tool":"trl","scn":"short","seq":100,"batch":8,"batch_tokens":797,"peak":4.264,"load":3.205,"role":"check"},{"model":"Qwen3-1.7B","method":"lora16","tool":"unsloth","scn":"short","seq":100,"batch":8,"batch_tokens":797,"peak":3.934,"load":3.248,"role":"check"},{"model":"Qwen3-0.6B","method":"full","tool":"trl","scn":"short","seq":100,"batch":8,"batch_tokens":797,"peak":6.258,"load":1.111,"role":"calib"},{"model":"Qwen3-0.6B","method":"qlora","tool":"trl","scn":"long","seq":1179,"batch":8,"batch_tokens":9430,"peak":2.221,"load":0.502,"role":"fit"},{"model":"Qwen3-0.6B","method":"qlora","tool":"unsloth","scn":"long","seq":1179,"batch":8,"batch_tokens":9430,"peak":1.498,"load":0.545,"role":"fit"},{"model":"Qwen3-4B","method":"qlora","tool":"trl","scn":"short","seq":100,"batch":8,"batch_tokens":797,"peak":3.604,"load":2.471,"role":"check"},{"model":"Qwen3-4B","method":"qlora","tool":"unsloth","scn":"short","seq":100,"batch":8,"batch_tokens":797,"peak":3.396,"load":2.514,"role":"check"},{"model":"Qwen3-4B","method":"qlora","tool":"trl","scn":"long","seq":1179,"batch":8,"batch_tokens":9430,"peak":8.16,"load":2.471,"role":"check"},{"model":"Qwen3-4B","method":"qlora","tool":"unsloth","scn":"long","seq":1179,"batch":8,"batch_tokens":9430,"peak":4.34,"load":2.514,"role":"check"}],"sys_tokens":{"short":17,"detailed":132}}""")
    # </FT_DATA>
    return (FT,)


@app.cell
def _(json):
    # 與 spike 同一套嚴格評分：整段輸出必須「就是 JSON」，欄位與列舉值都要對
    CATS = ["repair", "return", "billing", "howto", "shipping"]
    KEYS = ["category", "product", "urgent", "summary"]

    def score(output, label):
        res = {"json_ok": False, "schema_ok": False, "category": False, "product": False,
               "urgent": False, "all": False}
        try:
            obj = json.loads(output.strip())
        except ValueError:
            return res
        if not isinstance(obj, dict):
            return res
        res["json_ok"] = True
        res["schema_ok"] = (sorted(obj) == sorted(KEYS) and obj.get("category") in CATS
                            and isinstance(obj.get("urgent"), bool))
        res["category"] = obj.get("category") == label["category"]
        res["product"] = obj.get("product") == label["product"]
        res["urgent"] = obj.get("urgent") is label["urgent"]
        res["all"] = res["schema_ok"] and res["category"] and res["product"] and res["urgent"]
        return res
    return CATS, KEYS, score


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
    ## 1️⃣ 你的情況 → 用哪個工具

    2026 年 9 月的微調工具多到眼花，但真正決定選擇的只有四件事：
    **手上有什麼 GPU、要不要寫程式、要做哪種訓練、資料能不能離開公司**。
    選選看（工具版本與價格查證日：2026-09-24）：
    """
    )
    return


@app.cell
def _(mo):
    dd_gpu = mo.ui.dropdown(
        options={
            "只有筆電（沒有 NVIDIA GPU）": "none",
            "免費 Colab／Kaggle 的 T4（16 GB）": "t4",
            "自己有一張 24 GB 卡（RTX 4090 級）": "one",
            "多張資料中心 GPU（A100／H100）": "multi",
        },
        value="免費 Colab／Kaggle 的 T4（16 GB）", label="GPU",
    )
    rd_code = mo.ui.radio(options=["會寫 Python", "不想寫程式"], value="會寫 Python", label="寫程式")
    dd_method = mo.ui.dropdown(
        options=["SFT（教格式／語氣）", "DPO（好壞成對）", "GRPO／強化學習（可驗證的獎勵）"],
        value="SFT（教格式／語氣）", label="訓練方法",
    )
    rd_data = mo.ui.radio(options=["可以上雲", "不能離開公司"], value="可以上雲", label="資料")
    mo.vstack([mo.hstack([dd_gpu, dd_method], wrap=True, justify="start"),
               mo.hstack([rd_code, rd_data], wrap=True, justify="start", gap=2)])
    return dd_gpu, dd_method, rd_code, rd_data


@app.cell
def _(dd_gpu, dd_method, mo, rd_code, rd_data):
    _gpu, _code = dd_gpu.value, rd_code.value == "會寫 Python"
    _m = dd_method.value.split("（")[0]
    _private = rd_data.value == "不能離開公司"
    _notes = []
    if _private and _gpu in ("none", "t4"):
        _pick = "先解決算力：公司內部要有自己的 GPU"
        _why = ("免費 Colab／Kaggle 和所有託管微調服務，都是把資料上傳到別人的機器——"
                "「不能離開公司」的資料兩條路都不能走。一張 24 GB 的卡就夠 QLoRA 微調 8B 級模型"
                "（本課實測：1.7B 峰值約 2 GiB；更大的模型用 5️⃣ 估算）。")
        _alt = "有卡之後：寫程式用 **Unsloth** 或 **TRL＋PEFT**；不寫程式用 **Unsloth Studio** 或 **LLaMA-Factory WebUI**，全部在內網跑。"
    elif _gpu == "none":
        if _code:
            _pick = "先用免費的：Colab／Kaggle T4 ＋ Unsloth 官方 notebook（\\$0）"
            _alt = ("不想管機器、或模型大到 T4 裝不下：**Tinker**（你寫訓練迴圈、它出 GPU，按 token 計費）；"
                    "只想上傳資料就好：**Together／Fireworks**（上傳 JSONL，按訓練 token 計費）。")
        else:
            _pick = "Unsloth 官方的免費 Colab notebook（照順序按執行）或託管服務的網頁介面"
            _alt = "**Together／Fireworks** 上傳 JSONL 就能開訓，按訓練 token 計費（價格見 6️⃣）。"
        _why = "沒有 GPU 不代表要花錢：Google Colab 免費版常配到 T4（不保證、最長 12 小時），Kaggle 每週也有免費 GPU 時數。"
    elif _gpu in ("t4", "one"):
        if _code:
            _pick = "**Unsloth**（單卡最省、最快）"
            _why = ("單卡正是 Unsloth 的主場：本課同一份資料實測，短資料兩者差不多，"
                    "資料一變長（每筆約 1,180 tokens）Unsloth 的峰值 VRAM 少約四成、每步快約 1.6–3.3 倍（見 2️⃣）。")
            _alt = "要用最新演算法或 TRL 1.x 才有的功能：直接用 **TRL＋PEFT**（Unsloth 2026.9.11 把 trl 釘在 ≤0.24.0）。"
        else:
            _pick = "**Unsloth Studio**（網頁介面）或 **LLaMA-Factory WebUI**"
            _why = "兩者都能點選模型、上傳資料、按開始——底下跑的是同一套 LoRA／QLoRA。LLaMA-Factory 0.9.5 支援 100+ 種模型。"
            _alt = "想要可重現的設定檔：**Axolotl**（一份 YAML 描述整個訓練）。"
        if _gpu == "t4":
            _notes.append("T4 沒有原生 bf16，要走 fp16（Unsloth 會自動處理；自己寫 TRL 要注意精度設定，見教學頁測驗 Q2）；"
                          "本課 4090 的秒數在 T4 上會慢好幾倍。")
    else:
        _pick = "**Axolotl**（YAML 設定、多卡）或 **TRL＋accelerate**（FSDP／DeepSpeed）"
        _why = "多卡要的是可重現與分散式：Axolotl 0.19.0（2026-09-10）用一份 YAML 管整個訓練；TRL 走 Hugging Face 的 accelerate 生態。"
        _alt = "不寫程式：**LLaMA-Factory** 也支援多卡。"
    if _m == "DPO":
        _notes.append("DPO：TRL 的 `DPOTrainer`、Unsloth、Axolotl、LLaMA-Factory 都支援；託管的 Together 有 DPO 價目（≤16B 每百萬 token \\$0.84–0.94）。")
    if _m == "GRPO／強化學習":
        _notes.append("GRPO：TRL 的 `GRPOTrainer`（可接 vLLM 生成）與 Unsloth 都支援；Tinker 讓你自己寫 RL 迴圈。"
                      "OpenAI 的 RFT 只支援 o4-mini，而且官方文件寫明微調平台**不再開放新用戶**。")
    _notes.append("別選 **torchtune**：官方 README 寫著已停止維護（development wound down in 2025）。")
    _li = "\n".join(f"    - {_n}" for _n in _notes)
    mo.md(
        f"""
    **首選：** {_pick}

    **為什麼：** {_why}

    **備選：** {_alt}

    **要注意：**

{_li}
    """
    )
    return


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
    ## 2️⃣ 實測：同一份資料，兩個工具

    兩次訓練的**一切都一樣**：同一份 480 筆資料（固定 seed 產生）、同一份 4-bit 權重
    （`unsloth/Qwen3-1.7B-bnb-4bit`）、LoRA r=16／α=16 掛七個投影、學習率 2e-4、batch 8、
    1 個 epoch＝60 步、只對回答算 loss。唯一的差別是**用哪個工具跑**。

    切換兩種資料情境——短訊息（每筆約 100 tokens）與墊了歷史對話的長訊息（每筆約 1,180 tokens）：
    """
    )
    return


@app.cell
def _(mo):
    rd_scn = mo.ui.radio(options={"短訊息（每筆約 100 tokens）": "short",
                                  "長對話紀錄（每筆約 1,180 tokens）": "long"},
                         value="短訊息（每筆約 100 tokens）", label="資料情境")
    rd_run = mo.ui.radio(options={"第 1 次": 0, "第 2 次": 1, "第 3 次": 2}, value="第 1 次",
                         label="哪一次實測", inline=True)
    mo.hstack([rd_scn, rd_run], wrap=True, justify="start", gap=2)
    return rd_run, rd_scn


@app.cell
def _(FT, np, plt, rd_run, rd_scn):
    _runs = FT["runs"][rd_scn.value]
    _i = min(rd_run.value, len(_runs["trl"]) - 1, len(_runs["unsloth"]) - 1)
    _t, _u = _runs["trl"][_i], _runs["unsloth"][_i]
    _fig, (_a1, _a2) = plt.subplots(2, 1, figsize=(6.4, 5.8), sharex=True)
    _x = np.arange(1, len(_t["loss"]) + 1)
    _a1.plot(_x, _t["loss"], color="#4C72B0", lw=2.2, label="TRL + PEFT (trl 1.13)")
    _a1.plot(_x, _u["loss"], color="#DD8452", lw=2.2, ls="--", label="Unsloth 2026.9.11")
    _a1.set_yscale("log")
    _a1.set_ylabel("training loss (log)")
    _a1.set_title("same data, same hyper-parameters: the curves almost overlap")
    _a1.legend(frameon=False)
    _a1.grid(alpha=0.3)
    _a2.plot(_x, _t["step_s"], color="#4C72B0", lw=1.8, label="TRL + PEFT")
    _a2.plot(_x, _u["step_s"], color="#DD8452", lw=1.8, ls="--", label="Unsloth")
    _a2.set_ylabel("seconds per step")
    _a2.set_xlabel("training step")
    _a2.set_ylim(0, max(max(_t["step_s"]), max(_u["step_s"])) * 1.1)
    _a2.grid(alpha=0.3)
    _fig.tight_layout()
    _fig
    return


@app.cell
def _(FT, mo, np, rd_run, rd_scn):
    _runs = FT["runs"][rd_scn.value]
    _i = min(rd_run.value, len(_runs["trl"]) - 1, len(_runs["unsloth"]) - 1)
    _t, _u = _runs["trl"][_i], _runs["unsloth"][_i]

    def _med(r):
        return float(np.median(r["step_s"][3:]))

    _all_ratio = [float(np.median(a["step_s"][3:]) / np.median(b["step_s"][3:]))
                  for a, b in zip(_runs["trl"], _runs["unsloth"], strict=False)]
    mo.md(
        f"""
    | 這次實測 | TRL＋PEFT | Unsloth | 比一比 |
    | --- | ---: | ---: | --- |
    | 60 步總秒數 | {_t['train_s']:.1f} s | {_u['train_s']:.1f} s | Unsloth 第 1 步含暖機 {_u['step_s'][0]:.1f} s |
    | 每步秒數（中位數） | {_med(_t):.2f} s | {_med(_u):.2f} s | TRL ÷ Unsloth ＝ **{_med(_t) / _med(_u):.1f} 倍** |
    | 峰值 VRAM（PyTorch reserved） | {_t['peak']:.2f} GiB | {_u['peak']:.2f} GiB | 少 **{(1 - _u['peak'] / _t['peak']) * 100:.0f}%** |
    | 載入 4-bit 權重後 | {_t['load']:.2f} GiB | {_u['load']:.2f} GiB | 同一份權重 |
    | 最後一步 loss | {_t['loss'][-1]:.4f} | {_u['loss'][-1]:.4f} | 學到同一件事 |

    秒數曲線上突然的跳動（例如長資料第 1 次，Unsloth 在第 40 步附近變慢、TRL 在第 46 步附近變快）
    是主機上其他服務的負載在變——兩次訓練不是同時跑的。

    這個情境跑了 {len(_all_ratio)} 次，「每步快幾倍」依序是
    {"、".join(f"{r:.1f}" for r in _all_ratio)} 倍——**時間會飄**：這張 4090 同時有別的服務在用
    （GPU 使用率約 45%），所以秒數只看比例與量級；**峰值 VRAM 每次幾乎一樣**（差不到 0.01 GiB），那是確定的。
    """
    )
    return


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
    看懂這組數字的三個重點：

    - **loss 曲線幾乎疊在一起**：工具不改變模型「學到什麼」——兩者最後都落在 0.00x。
      起點不同（2.9 vs 2.6）是因為 Unsloth 自動開了 *padding-free*（把一個 batch 的樣本接成一條），
      loss 的平均方式不同；形狀與終點一致。
    - **短資料差距小**：每筆才 100 tokens，活化值本來就小，Unsloth 省記憶體的招數沒什麼可省
      （VRAM 只少 8%）；每步快約 1.3 倍，但它第一步要暖機，60 步總時間只差 3–27%。
    - **資料一長，差距才出來**：Unsloth 用自己的 Triton 核心、把梯度檢查點**搬到 CPU**
      （log 寫著 *smartly offload gradients*）、自動 padding-free——序列越長，活化值越大，省下的越多。
    """
    )
    return


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
    ## 3️⃣ 訓練前後：60 步教會了它什麼

    訓練完，拿 **40 則訓練時沒看過的訊息**考它——句型是新的、一半的產品型號也是新的。
    評分很嚴格：輸出必須**整段就是 JSON**（程式直接 `json.loads` 吃得下），欄位與類別都要對。
    四種做法同場比較：
    """
    )
    return


@app.cell
def _(FT, mo):
    _opts = {f"#{i + 1:02d} {m[:30]}{'…' if len(m) > 30 else ''}": i
             for i, m in enumerate(FT["eval"]["msgs"])}
    dd_case = mo.ui.dropdown(options=_opts, value=next(iter(_opts)), label="測試訊息")
    dd_case
    return (dd_case,)


@app.cell
def _(FT, dd_case, html_mod, mo, score):
    _i = dd_case.value
    _ev = FT["eval"]
    _lab = _ev["labels"][_i]
    _cols = [
        ("底模＋一句話 prompt", "#C44E52", _ev["base_short"][_i], f"{FT['sys_tokens']['short']} tokens 的 system prompt"),
        ("底模＋寫滿規則的 prompt", "#8172B2", _ev["base_detailed"][_i], f"{FT['sys_tokens']['detailed']} tokens 的 system prompt"),
        ("微調後（TRL＋PEFT）", "#4C72B0", _ev["trl"][_i], "一句話 prompt"),
        ("微調後（Unsloth）", "#DD8452", _ev["unsloth"][_i], "一句話 prompt"),
    ]

    def _badges(s):
        _items = [("JSON", s["json_ok"]), ("schema", s["schema_ok"]), ("類別", s["category"]),
                  ("產品", s["product"]), ("急件", s["urgent"])]
        return " ".join(
            f'<span style="display:inline-block;font-size:11px;font-weight:700;padding:1px 6px;border-radius:6px;'
            f'margin:1px;background:{"#E4F2E7" if ok else "#FBEAEA"};color:{"#2E7042" if ok else "#C44E52"}">'
            f'{"✓" if ok else "✗"} {name}</span>' for name, ok in _items)

    _cards = []
    for _title, _color, _out, _sub in _cols:
        _s = score(_out, _lab)
        _txt = _out if len(_out) < 700 else _out[:700] + "…（後略）"
        _cards.append(
            f'<div style="flex:1;min-width:250px;border:2px solid {_color};border-radius:12px;padding:10px 12px;'
            f'font-family:system-ui,sans-serif"><div style="font-size:12px;font-weight:800;color:{_color}">{_title}</div>'
            f'<div style="font-size:11px;color:#5A6A72;margin-bottom:6px">{_sub}</div>{_badges(_s)}'
            f'<div style="font-size:12.5px;line-height:1.6;white-space:pre-wrap;word-break:break-word;margin-top:6px;'
            f'font-family:ui-monospace,monospace">{html_mod.escape(_txt)}</div></div>'
        )
    mo.Html(
        f'<div style="font-size:13.5px;margin-bottom:8px"><b>客人說：</b>{html_mod.escape(_ev["msgs"][_i])}</div>'
        f'<div style="font-size:12.5px;margin-bottom:10px;color:#5A6A72">標準答案：'
        f'<code>{html_mod.escape(_ev["answers"][_i])}</code></div>'
        '<div style="display:flex;gap:10px;flex-wrap:wrap">' + "".join(_cards) + "</div>"
        '<div style="font-size:12px;color:#5A6A72;margin-top:8px">實測紀錄（Qwen3-1.7B 4-bit，貪婪解碼，'
        "RTX 4090，2026-09-24）；綠紅標籤是這份 notebook 用同一套評分規則現場算的。</div>"
    )
    return


@app.cell
def _(FT, mo, score):
    _ev = FT["eval"]
    _rows = []
    for _name, _key, _p in [("底模＋一句話 prompt", "base_short", "short"),
                            ("底模＋寫滿規則的 prompt", "base_detailed", "detailed"),
                            ("微調後（TRL＋PEFT）", "trl", "short"),
                            ("微調後（Unsloth）", "unsloth", "short")]:
        _ss = [score(o, l) for o, l in zip(_ev[_key], _ev["labels"], strict=True)]
        _c = {k: sum(s[k] for s in _ss) for k in _ss[0]}
        _rows.append(f"| {_name} | {FT['sys_tokens'][_p]} | {_c['json_ok']} | {_c['schema_ok']} | "
                     f"{_c['category']} | {_c['urgent']} | **{_c['all']} / {len(_ss)}** |")
    _tbl = "\n    ".join(_rows)
    mo.md(
        f"""
    | 做法 | system prompt tokens | 整段是 JSON | 欄位正確 | 類別對 | 急件對 | 全對 |
    | --- | ---: | ---: | ---: | ---: | ---: | ---: |
    {_tbl}

    - 底模只拿到一句話時，**自己發明欄位**（「工單類別」「處理人」「創建時間」…）、
      還會混進簡體字與 markdown 程式碼框——它不知道你要的格式。
    - 把規則寫滿 132 tokens，34 題輸出了合法 JSON——另外 6 題包了 ```` ```json ```` 框，程式直接 `json.loads` 會失敗；
      「急不急」只判對 25 題，**判錯的 11 題全是把不急的判成急**（連「麻煩了，謝謝」都當急件）。
    - 微調後只剩一句話 prompt，**每次呼叫少送一百多個 token**，而且格式穩定。
      兩個工具練出來的模型答對的題目完全一樣——它們學到的是同一件事。
    - 唯一答錯的那題（#06）值得看：`MeshLink AX 5G 訊號消失`——型號後面緊接著「5G」，
      模型把「5G」當成型號的一部分。**沒看過的產品＋模糊的邊界**，就是資料該補的地方。
    """
    )
    return


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
    ## 4️⃣ 資料格式：messages、chat template、loss masking

    三個工具吃的資料長得都一樣——**一行一筆的 JSONL**，每筆是一段 `messages` 對話。
    選一個範例（或直接改框裡的文字），驗證器會即時檢查，
    再把它套上 Qwen3 的 chat template，標出**哪些字會算 loss**：
    """
    )
    return


@app.cell
def _(json, mo):
    _SYS = "你是客服工單助理。把使用者訊息轉成 JSON 工單。"
    _ANS = json.dumps({"category": "repair", "product": "ZoomPad 11", "urgent": False,
                       "summary": "ZoomPad 11 螢幕有一條綠線"}, ensure_ascii=False)
    _USER = "我的 ZoomPad 11 螢幕有一條綠線，可以幫忙修嗎？"

    def _line(msgs):
        return json.dumps({"messages": msgs}, ensure_ascii=False)

    PRESETS = {
        "✅ 正確的一筆": _line([{"role": "system", "content": _SYS}, {"role": "user", "content": _USER},
                                {"role": "assistant", "content": _ANS}]),
        "❌ role 拼錯（assistent）": _line([{"role": "system", "content": _SYS},
                                           {"role": "user", "content": _USER},
                                           {"role": "assistent", "content": _ANS}]),
        "❌ 最後一則不是 assistant": _line([{"role": "system", "content": _SYS},
                                         {"role": "user", "content": _USER}]),
        "❌ Alpaca 舊格式（instruction／output）": json.dumps(
            {"instruction": _SYS, "input": _USER, "output": _ANS}, ensure_ascii=False),
        "❌ 回答不是合法 JSON（本課任務規則）": _line([
            {"role": "system", "content": _SYS}, {"role": "user", "content": _USER},
            {"role": "assistant", "content": "類別：repair，產品：ZoomPad 11，不急"}]),
        "❌ 整行 JSON 壞掉（單引號）": "{'messages': [{'role': 'user', 'content': '你好'}]}",
    }
    dd_preset = mo.ui.dropdown(options=list(PRESETS), value="✅ 正確的一筆", label="範例")
    dd_preset
    return PRESETS, dd_preset


@app.cell
def _(PRESETS, dd_preset, mo):
    ta_line = mo.ui.text_area(value=PRESETS[dd_preset.value], label="JSONL（一行一筆，可以直接改）",
                              rows=5, full_width=True)
    ta_line
    return (ta_line,)


@app.cell
def _(CATS, KEYS, html_mod, json, mo, ta_line):
    _lines = [ln for ln in ta_line.value.splitlines() if ln.strip()]
    _checks, _render = [], []
    _ROLES = {"system", "user", "assistant"}
    for _n, _ln in enumerate(_lines[:5], 1):
        _tag = f"第 {_n} 行" if len(_lines) > 1 else "這一行"
        try:
            _obj = json.loads(_ln)
        except json.JSONDecodeError as _e:
            _checks.append(("err", f"{_tag}不是合法 JSON：`{_e.msg}`（第 {_e.colno} 個字）——JSON 字串要用雙引號"))
            continue
        _msgs = _obj.get("messages") if isinstance(_obj, dict) else None
        if not isinstance(_msgs, list):
            _keys = "、".join(_obj) if isinstance(_obj, dict) else type(_obj).__name__
            _checks.append(("err", (f"{_tag}沒有 `messages` 清單（看到的欄位：{_keys}）——"
                                    "舊的 instruction／output 格式要先轉成 messages")))
            continue
        _bad = [m for m in _msgs if not isinstance(m, dict) or m.get("role") not in _ROLES
                or not isinstance(m.get("content"), str)]
        if _bad:
            _checks.append(("err", (f"{_tag}有 {len(_bad)} 則訊息的 role 不在 system／user／assistant 之內"
                                    f"（看到 `{_bad[0].get('role') if isinstance(_bad[0], dict) else _bad[0]}`）")))
            continue
        if not _msgs or _msgs[-1]["role"] != "assistant":
            _checks.append(("err", f"{_tag}最後一則不是 assistant——沒有「標準答案」，模型沒東西可學"))
            continue
        _body = [m["role"] for m in _msgs if m["role"] != "system"]
        if any(_body[i] == _body[i + 1] for i in range(len(_body) - 1)):
            _checks.append(("warn", f"{_tag}有連續兩則同一個 role——多數 chat template 預期 user／assistant 輪流"))
        if _msgs[0]["role"] != "system":
            _checks.append(("warn", f"{_tag}沒有 system——可以，但推論時也要用一樣的開頭，訓練與推論才對得上"))
        try:
            _ans = json.loads(_msgs[-1]["content"])
            if sorted(_ans) != sorted(KEYS) or _ans.get("category") not in CATS:
                _checks.append(("warn", f"{_tag}的回答是 JSON，但欄位或類別跟本課任務的 schema 不一致"))
            else:
                _checks.append(("ok", f"{_tag}格式正確，回答符合本課任務的 schema"))
        except (json.JSONDecodeError, AttributeError, TypeError):
            _checks.append(("err", f"{_tag}的回答不是合法 JSON——本課任務要模型只輸出 JSON，示範資料就得先做到"))
        # 套上 Qwen3 chat template（與實測時 TRL 資料整理器吐出來的 labels 同一套切法）
        _parts = []
        _last_a = max(i for i, m in enumerate(_msgs) if m["role"] == "assistant")
        for _k, _m in enumerate(_msgs):
            if _m["role"] == "assistant":
                _parts.append((False, "<|im_start|>assistant\n"))
                _think = "<think>\n\n</think>\n\n" if _k == _last_a else ""
                _parts.append((True, _think + _m["content"] + "<|im_end|>"))
                _parts.append((False, "\n"))
            else:
                _parts.append((False, f"<|im_start|>{_m['role']}\n{_m['content']}<|im_end|>\n"))
        _render.append(_parts)

    _icon = {"ok": ("✅", "#2E7042"), "warn": ("⚠️", "#B36B00"), "err": ("❌", "#C44E52")}
    _ck = "".join(f'<div style="color:{_icon[k][1]};font-size:13px;margin:2px 0">{_icon[k][0]} {html_mod.escape(t)}</div>'
                  for k, t in _checks) or '<div style="font-size:13px">（框裡是空的）</div>'
    _rd = ""
    for _parts in _render:
        _n_loss = sum(len(t) for on, t in _parts if on)
        _n_all = sum(len(t) for _, t in _parts)
        _spans = "".join(
            f'<span style="background:{"#E4F2E7" if on else "#EEF0F2"};color:{"#1E5C30" if on else "#7A8791"};'
            f'{"font-weight:700;" if on else ""}">{html_mod.escape(t)}</span>' for on, t in _parts)
        _rd += (f'<div style="font-family:ui-monospace,monospace;font-size:12px;line-height:1.7;white-space:pre-wrap;'
                f'word-break:break-word;border:1px solid #D5DBDF;border-radius:10px;padding:10px 12px;margin-top:8px">{_spans}</div>'
                f'<div style="font-size:12px;color:#5A6A72;margin-top:4px">灰＝模型看得到、但<b>不算 loss</b>；綠＝要學會寫出來的字'
                f'（{_n_loss} / {_n_all} 個字元，約 {_n_loss / _n_all:.0%}）</div>')
    mo.Html(_ck + _rd)
    return


@app.cell
def _(FT, html_mod, mo):
    def _spans_html(spans, limit):
        _out = ""
        for _s in spans[:limit]:
            _out += (f'<span style="background:{"#E4F2E7" if _s["loss"] else "#EEF0F2"};'
                     f'color:{"#1E5C30" if _s["loss"] else "#7A8791"};{"font-weight:700;" if _s["loss"] else ""}">'
                     f'{html_mod.escape(_s["text"])}</span>')
        return _out

    _box = ('font-family:ui-monospace,monospace;font-size:11.5px;line-height:1.65;white-space:pre-wrap;'
            'word-break:break-word;border:1px solid #D5DBDF;border-radius:10px;padding:8px 10px;margin:6px 0 12px')
    _t = FT["mask"]["trl"]
    _u = FT["mask"]["unsloth"]
    mo.Html(
        '<div style="font-size:13.5px;font-weight:800;margin-top:6px">實測原文：兩個工具的資料整理器真的吐出了什麼</div>'
        '<div style="font-size:12.5px;color:#5A6A72">訓練開始前從 data collator 拿出第一個 batch，'
        '把 <code>labels = -100</code>（不算 loss）與其餘 token 分色——這不是示意圖，是那一次訓練的真實輸入。</div>'
        f'<div style="font-size:12.5px;margin-top:8px"><b>TRL＋PEFT</b>（prompt／completion 格式，'
        f'第 1 筆：灰 {_t[0]["n"]} tokens、綠 {_t[1]["n"]} tokens）</div>'
        f'<div style="{_box}">{_spans_html(_t, 3)}</div>'
        f'<div style="font-size:12.5px"><b>Unsloth</b>（<code>train_on_responses_only</code>；'
        'padding-free 把 8 筆接成同一條，下面是開頭兩筆）</div>'
        f'<div style="{_box}">{_spans_html(_u, 4)}…</div>'
        '<div style="font-size:12.5px;color:#5A6A72">差別只在邊角：Unsloth 那條不訓練 <code>&lt;|im_end|&gt;</code> 後面的換行。'
        "兩者都有 <code>&lt;think&gt;…&lt;/think&gt;</code> 空殼：Qwen3 的 template 對最後一則回答自動補上，"
        "推論時用 <code>enable_thinking=False</code> 也會補同一段——訓練與推論長得一樣，模型才不會錯亂。</div>"
    )
    return


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
    ## 5️⃣ VRAM 帳：估算 vs 實測

    「我的卡裝得下嗎？」是選工具之前的第一個問題。下面的**估算**把峰值 VRAM 拆成三塊：

    - **權重**：bf16 每參數 2 bytes；4-bit（NF4）用實測校準約 0.516 bytes，embedding 仍留 16-bit
    - **訓練狀態**：全參數＝梯度＋AdamW 兩份狀態＋更新時的暫存（用 0.6B 全參數實測校準，約 8 bytes／參數）；
      LoRA 只有 adapter 自己的權重、梯度與 8-bit Adam（可訓練參數才 1% 上下）
    - **活化值與雜項**：跟「一個 batch 的 token 數 × 層數 × MLP 寬度」成正比，
      係數用本課的 QLoRA 實測點**現場擬合**（TRL 與 Unsloth 各一組）

    選模型、長度與工具，看三種做法各要多少：
    """
    )
    return


@app.cell
def _(mo):
    dd_model = mo.ui.dropdown(options=["Qwen3-0.6B", "Qwen3-1.7B", "Qwen3-4B", "Qwen3-8B", "Qwen3-14B",
                                       "Qwen3-32B", "Llama-3.3-70B"], value="Qwen3-8B", label="模型")
    rd_tool = mo.ui.radio(options=["TRL＋PEFT", "Unsloth"], value="TRL＋PEFT", label="工具", inline=True)
    sl_seq = mo.ui.slider(steps=[64, 100, 256, 512, 1024, 1179, 2048, 4096], value=512,
                          label="每筆 token 數", show_value=True)
    sl_bs = mo.ui.slider(steps=[1, 2, 4, 8, 16], value=8, label="batch size", show_value=True)
    mo.vstack([mo.hstack([dd_model, rd_tool], wrap=True, justify="start", gap=2),
               mo.hstack([sl_seq, sl_bs], wrap=True, justify="start", gap=2)])
    return dd_model, rd_tool, sl_bs, sl_seq


@app.cell
def _(FT, np):
    # 模型結構（各模型 config.json 的實際值，2026-09 查）
    MODELS = {
        "Qwen3-0.6B": {"h": 1024, "L": 28, "I": 3072, "nq": 16, "nkv": 8, "hd": 128, "V": 151936, "tied": True},
        "Qwen3-1.7B": {"h": 2048, "L": 28, "I": 6144, "nq": 16, "nkv": 8, "hd": 128, "V": 151936, "tied": True},
        "Qwen3-4B": {"h": 2560, "L": 36, "I": 9728, "nq": 32, "nkv": 8, "hd": 128, "V": 151936, "tied": True},
        "Qwen3-8B": {"h": 4096, "L": 36, "I": 12288, "nq": 32, "nkv": 8, "hd": 128, "V": 151936, "tied": False},
        "Qwen3-14B": {"h": 5120, "L": 40, "I": 17408, "nq": 40, "nkv": 8, "hd": 128, "V": 151936, "tied": False},
        "Qwen3-32B": {"h": 5120, "L": 64, "I": 25600, "nq": 64, "nkv": 8, "hd": 128, "V": 151936, "tied": False},
        "Llama-3.3-70B": {"h": 8192, "L": 80, "I": 28672, "nq": 64, "nkv": 8, "hd": 128, "V": 128256, "tied": False},
    }
    GIB = 2**30
    Q4 = 0.516  # NF4＋雙重量化，bytes/參數（用 Qwen3-1.7B 載入後的實測 1.257 GiB 反推）

    def counts(name, r=16):
        c = MODELS[name]
        q, kv = c["h"] * c["nq"] * c["hd"], c["h"] * c["nkv"] * c["hd"]
        lin = c["L"] * (2 * q + 2 * kv + 3 * c["h"] * c["I"])
        emb = c["V"] * c["h"] * (1 if c["tied"] else 2)
        lora = c["L"] * r * (2 * (c["h"] + c["nq"] * c["hd"]) + 2 * (c["h"] + c["nkv"] * c["hd"])
                             + 3 * (c["h"] + c["I"]))
        return lin, emb, lora, c

    def weights_state(name, method):
        lin, emb, lora, _c = counts(name)
        if method == "qlora":
            w = lin * Q4 + emb * 2
        else:
            w = (lin + emb) * 2
        # 全參數：梯度＋AdamW 兩份狀態＋更新時的暫存 ≈ 8 bytes/參數（用 Qwen3-0.6B 全參數實測校準）
        # LoRA：只有 adapter 自己——fp32 權重 4＋梯度 4＋8-bit Adam 2 ＝ 10 bytes/可訓練參數
        s = (lin + emb) * 8 if method == "full" else lora * (4 + 4 + 2)
        return w / GIB, s / GIB

    # 活化值係數：用實測的 QLoRA 點現場擬合  act ≈ a0 + a1 × (batch tokens × 層數 × MLP 寬度)
    FIT = {}
    for _tool in ("trl", "unsloth"):
        _pts = [p for p in FT["cal"] if p["tool"] == _tool and p["role"] == "fit"]
        _X = np.array([[1.0, p["batch_tokens"] * MODELS[p["model"]]["L"] * MODELS[p["model"]]["I"] / 1e9]
                       for p in _pts])
        _y = np.array([p["peak"] - p["load"] - weights_state(p["model"], p["method"])[1] for p in _pts])
        FIT[_tool] = np.linalg.lstsq(_X, _y, rcond=None)[0]

    def estimate(name, method, tool, batch_tokens):
        w, s = weights_state(name, method)
        a0, a1 = FIT[tool]
        c = MODELS[name]
        return w, s, float(a0 + a1 * batch_tokens * c["L"] * c["I"] / 1e9)
    return FIT, MODELS, counts, estimate


@app.cell
def _(FT, dd_model, estimate, plt, rd_tool, sl_bs, sl_seq):
    _tool = "trl" if rd_tool.value == "TRL＋PEFT" else "unsloth"
    _bt = sl_seq.value * sl_bs.value
    _names = [("full", "Full FT (bf16 + AdamW)"), ("lora", "LoRA (bf16 base)"), ("qlora", "QLoRA (4-bit base)")]
    _parts = [estimate(dd_model.value, m, _tool, _bt) for m, _ in _names]
    _tot = [sum(p) for p in _parts]
    _fig, _ax = plt.subplots(figsize=(6.4, 4.2))
    _y = range(len(_names))
    _left = [0.0] * 3
    for _k, (_lab, _col) in enumerate([("weights", "#4C72B0"), ("training state", "#DD8452"),
                                       ("activations etc.", "#8C9BA5")]):
        _vals = [p[_k] for p in _parts]
        _ax.barh(list(_y), _vals, left=_left, color=_col, edgecolor="#1C2B33", lw=0.8, label=_lab, zorder=3)
        _left = [a + b for a, b in zip(_left, _vals, strict=True)]
    _xmax = max(max(_tot) * 1.15, 27)
    for _k, (_cap, _name) in enumerate([(15, "T4"), (24, "4090"), (80, "A100")]):
        if _cap < _xmax:
            _ax.axvline(_cap, color="#C44E52", ls=":", lw=1.4, zorder=4)
            _ax.text(_cap, len(_names) - 0.42 + 0.22 * (_k % 2), f" {_name} ", color="#C44E52", fontsize=8,
                     ha="center", va="bottom", bbox={"fc": "white", "ec": "none", "pad": 0.5}, zorder=5)
    for _i, _t in enumerate(_tot):
        _ax.text(min(_t, _xmax * 0.86) + _xmax * 0.01, _i, f"{_t:.1f} GiB", va="center", fontsize=9, zorder=5)
    # 這個組合有實測點就畫上去（黑點＝實測峰值）
    for _p in FT["cal"]:
        if (_p["model"] == dd_model.value and _p["tool"] == _tool and _p["batch"] == sl_bs.value
                and abs(_p["seq"] - sl_seq.value) <= 2):
            _row = [m for m, _ in _names].index(_p["method"])
            _ax.plot(_p["peak"], _row, "o", color="black", ms=7, zorder=6)
            _ax.text(_p["peak"], _row - 0.33, "measured", fontsize=8, ha="center", zorder=6)
    _ax.set_yticks(list(_y), [n for _, n in _names])
    _ax.set_xlim(0, _xmax)
    _ax.set_ylim(-0.6, len(_names) + 0.1)
    _ax.set_xlabel("estimated peak VRAM (GiB)\nred lines: T4 16GB / RTX 4090 24GB / A100 80GB")
    _ax.set_title(f"{dd_model.value}, {sl_bs.value} x {sl_seq.value} tokens per batch ({rd_tool.value.replace('＋', ' + ')})")
    _ax.legend(frameon=False, fontsize=8, loc="upper center", bbox_to_anchor=(0.5, -0.3), ncol=3)
    _ax.grid(axis="x", alpha=0.3)
    _fig.tight_layout()
    _fig
    return


@app.cell
def _(FT, FIT, estimate, mo):
    _ROLE = {"fit": "擬合", "calib": "校準", "check": "**驗證**"}
    _MNAME = {"qlora": "QLoRA", "lora16": "LoRA（bf16）", "full": "全參數"}
    _rows = []
    for _p in FT["cal"]:
        _est = sum(estimate(_p["model"], _p["method"], _p["tool"], _p["batch_tokens"]))
        _err = (_est - _p["peak"]) / _p["peak"] * 100
        _rows.append(f"| {_p['model']} | {_MNAME[_p['method']]} | "
                     f"{'TRL＋PEFT' if _p['tool'] == 'trl' else 'Unsloth'} | {_p['batch']}×{_p['seq']} | "
                     f"{_ROLE[_p['role']]} | {_p['peak']:.2f} | {_est:.2f} | {_err:+.0f}% |")
    _tbl = "\n    ".join(_rows)
    mo.md(
        f"""
    **估算 vs 實測**（實測＝RTX 4090 上 PyTorch 的峰值 reserved。「擬合」的點決定活化值係數、
    「校準」的點決定全參數的狀態係數；**「驗證」的點沒參與任何擬合**，拿來看估算準不準）：

    | 模型 | 做法 | 工具 | batch×長度 | 用途 | 實測 GiB | 估算 GiB | 誤差 |
    | --- | --- | --- | --- | --- | ---: | ---: | ---: |
    {_tbl}

    擬合出的活化值係數：TRL＋PEFT 每 10 億「token × 層 × MLP 寬度」約 **{FIT['trl'][1]:.2f} GiB**、
    Unsloth 約 **{FIT['unsloth'][1]:.2f} GiB**——Unsloth 把梯度檢查點搬到 CPU，
    所以序列越長，這一塊差越多。驗證點的誤差大多在幾 % 以內，最差的是 4B 長資料的 TRL（低估約 7%）。
    這是**估算**：8B 以上沒有實測點，只看量級；真的要租卡前，先用小 batch 跑幾步看峰值。
    另一個實測：Qwen3-1.7B **全參數**微調在 9.5 GB 的額度內直接 OOM——估算約 {sum(estimate('Qwen3-1.7B', 'full', 'trl', 797)):.1f} GiB。
    """
    )
    return


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
    ## 6️⃣ 託管微調：不用自己的 GPU，要花多少

    沒有 GPU、又不想跟免費額度搶卡，就把資料交給託管服務——多數**按訓練 token 計費**
    （訓練 token ＝ 資料 token 數 × epoch 數）。拉拉看你的資料量要多少錢
    （牌價查證日 2026-09-24，以 ≤16B 的模型、LoRA SFT 為準）：
    """
    )
    return


@app.cell
def _(mo):
    sl_n = mo.ui.slider(steps=[100, 480, 1000, 5000, 20000, 50000, 200000], value=5000,
                        label="資料筆數", show_value=True)
    sl_tok = mo.ui.slider(steps=[100, 300, 600, 1180, 2000, 4000], value=600, label="每筆 token 數",
                          show_value=True)
    sl_ep = mo.ui.slider(start=1, stop=5, step=1, value=3, label="epoch", show_value=True)
    mo.hstack([sl_n, sl_tok, sl_ep], wrap=True, justify="start", gap=2)
    return sl_ep, sl_n, sl_tok


@app.cell
def _(mo, sl_ep, sl_n, sl_tok):
    _m = sl_n.value * sl_tok.value * sl_ep.value / 1e6

    def _usd(x):
        return f"\\${x:,.2f}" if x < 100 else f"\\${x:,.0f}"

    _tg_lo, _tg_hi = max(_m * 0.34, 4.0), max(_m * 0.38, 4.0)
    _tg = _usd(_tg_lo) if _tg_hi - _tg_lo < 0.005 else f"{_usd(_tg_lo)}–{_usd(_tg_hi)}"
    mo.md(
        rf"""
    訓練 token：{sl_n.value:,} 筆 × {sl_tok.value:,} tokens × {sl_ep.value} epoch ＝ **{_m:,.2f} 百萬**

    | 服務 | 牌價（每百萬訓練 token） | 這筆資料 | 備註 |
    | --- | --- | ---: | --- |
    | Together AI | \$0.34–0.38（SFT） | {_tg} | 每個 job 有最低收費（文件寫 \$4 起）；DPO \$0.84–0.94 |
    | Fireworks AI | \$0.50（LoRA SFT） | {_usd(_m * 0.50)} | 全參數、DPO 另有價目 |
    | Tinker（Thinking Machines） | \$0.44（Qwen3-8B train） | {_usd(_m * 0.44)} | 你自己寫訓練迴圈（SFT／RL 都行），它出 GPU；取樣另計 |
    | OpenAI fine-tuning | — | — | 官方文件：平台逐步收掉、**不再開放新用戶** |
    | 自己的 Colab／Kaggle T4 | \$0 | \$0 | 免費但不保證有卡、有時數上限；資料會上傳到 Google |

    本課那次實驗（480 筆 × 約 100 tokens × 1 epoch ≈ 0.05 百萬 token）在 4090 上跑 20 秒——
    **小實驗自己跑幾乎免費，託管的價值在「大模型、不想管機器」**。
    """
    )
    return


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
    ## 7️⃣ 換你動手

    挑戰題在教學頁的「換你動手」，全部用上面的選單與滑桿就做得到。
    做完再打開下面的解答對照：
    """
    )
    return


@app.cell(hide_code=True)
def _(mo):
    mo.accordion(
        {
            "💡 LEVEL 1 參考解答": mo.md(
                r"""
    在 2️⃣ 切到「長對話紀錄」，三次實測的「每步秒數中位數」TRL ÷ Unsloth 分別是 **3.3、1.6、1.7 倍**；
    60 步總時間 TRL 168／200／179 s、Unsloth 78／124／94 s。峰值 VRAM 則三次都是 **4.09 → 2.49 GiB**。

    為什麼？**VRAM 由「要存哪些張量」決定**——同一份資料、同一組設定，每次配置的張量一模一樣，
    所以一次就量得準。**時間則受「GPU 同時在忙什麼」影響**：這張 4090 上還跑著別的服務
    （GPU 使用率約 45%），誰搶到算力就飄。所以看 benchmark：VRAM 看一次、時間看多次的範圍與比例。
    """
            ),
            "💡 LEVEL 2 參考解答": mo.md(
                r"""
    5️⃣ 選 Qwen3-14B、每筆 2048 tokens、batch 8（估算）：

    - QLoRA＋TRL 約 **24.9 GiB**——超過 T4 的 15 線，連 4090 的 24 線都擠不進。
    - QLoRA＋Unsloth 約 **14.2 GiB**——壓在 T4 的 15 線下，很緊但裝得下。
    - 堅持用 TRL：batch 降到 4 約 17.7 GiB（還是不行）、降到 **2 約 14.1 GiB** 才進得去。
      降 batch 的同時把 `gradient_accumulation_steps` 設 4，等效 batch 還是 8（代價是時間）。

    權重（QLoRA 約 9 GiB）三種組合都一樣，差的全在活化值——這正是長序列時工具差距的來源。
    提醒：14B 沒有實測點，這是估算；真的上 T4 之前先跑幾步看 `torch.cuda.max_memory_reserved()`。
    """
            ),
            "💡 LEVEL 3 提示": mo.md(
                r"""
    **格式修好長這樣**（貼進 4️⃣ 的框，驗證器應該全綠）：

    ```
    {"messages": [{"role": "system", "content": "你是客服工單助理。把使用者訊息轉成 JSON 工單。"}, {"role": "user", "content": "我的 ZoomPad 11 螢幕有一條綠線，可以幫忙修嗎？"}, {"role": "assistant", "content": "{\"category\": \"repair\", \"product\": \"ZoomPad 11\", \"urgent\": false, \"summary\": \"ZoomPad 11 螢幕有一條綠線\"}"}]}
    ```

    重點是三個欄位的對應：`instruction` → system、`input` → user、`output` → assistant；
    assistant 的內容本身是一段 JSON 字串，裡面的雙引號要跳脫成 `\"`。

    **補資料的方向**：答錯的是 #06「MeshLink AX 5G 訊號消失」——型號後面緊接著規格詞，模型把「5G」吞進型號。
    可以補「型號＋規格詞」的示範（例如「ZoomPad 11 4G 版本收不到訊號」→ product 只寫 `ZoomPad 11`），
    或乾脆把產品清單交給程式查表／RAG，不靠模型記。

    怎麼驗證：這一步要真的重新訓練——用教學頁第 6 節的腳本（免費 T4 就夠），加資料後重跑，
    看 #06 有沒有轉對、其他 39 題有沒有退步（腳本會印出同一套嚴格評分）。
    """
            ),
        }
    )
    return


if __name__ == "__main__":
    app.run()
