JEV - 兩個 demo:回來的只是一張分佈

兩個例子都只問封閉的單選:客服裡切不切人工,合併前放不放行。回來的是分佈。公開延遲仍是大約 70 到 500 毫秒,不是某一次演示的毫秒數。

JEV - 兩個 demo:回來的只是一張分佈

聯合作者:folkbench.com (Folkbench 是面向 AI API、模型服務及相關站點的可覆核評測與選擇平台,依據已發布的服務資料、價格、可用性、延遲與證據窗口,幫助用戶比較並選擇使用路徑。)

這一組共十篇:

兩個 demo 都是同一件事:不給自由文字,只給封閉題,拿分佈做分支。下面的 JSON 和程式碼是呼叫形狀,不是一份計時報告。公開的端到端大約 70 到 500 毫秒。不要把某一次 18 毫秒寫成產品指標。值得看的是返回裡沒有逐 token 的句子。

客服:意圖和熔斷

場景

客服最怕使用者已經在發火,系統還在打字。用生成式模型做意圖,常常為了準確而先寫一段 JSON,首包和生成都等在那裡。用純正則又讀不懂繞著說的怒氣。要的是封閉分類,然後立刻決定切不切人工。

狀態和選項

這是一道 Choice,不是 Noul。Noul 只回答是非。這裡選項有三項。

[State]
scene: live support entry
rule: assign probability across the given options from the message text

[user]
I have waited three days. The shipment has not moved. Are you even alive? Refund me now, or I escalate.
{
  "question": "choice",
  "options": [
    {"id": "GENERAL", "description": "ordinary question or tracking, no strong emotion"},
    {"id": "REFUND", "description": "a clear return or refund, stated flatly"},
    {"id": "BREAKER", "description": "insult, threat, or a complaint that should break to a person"}
  ]
}

回來的是分佈

沒有游標在吐字。形狀是一張表,不是一段評語。

{
  "top": "BREAKER",
  "scores": [
    {"choice": "BREAKER", "score": 0.9241},
    {"choice": "REFUND", "score": 0.0715},
    {"choice": "GENERAL", "score": 0.0044}
  ]
}

數字是這個例子裡的分佈,用來看分支怎麼寫。它不是一次公開基準。模型是在給定選項上分配機率,不是在預測下一個字元。

程式碼怎麼接

async function handleIncomingCustomerMessage(userMessage: string) {
  const res = await jev.choice("cs_intent_v1", { input: userMessage });
  const breaker = scoreOf(res, "BREAKER");
  if (breaker > 0.85) {
    return triggerHumanOverride({ priority: "CRITICAL", queue: "RETENTION" });
  }
  if (scoreOf(res, "REFUND") > 0.70) {
    return launchWorkflow("AUTOMATED_REFUND_FLOW");
  }
  return launchWorkflow("DEFAULT_FAQ_BOT");
}

沒有為了解析 Markdown 再寫一輪重試。閾值寫在程式碼裡。0.85 和 0.70 是這個流程自己的政策,不是模型給的保證。

合併前的審計

場景

靜態掃描靠語法樹和死規則,變體能繞過,誤報也多。讓聊天模型讀 500 行 diff 再寫評語,常常要十幾秒,流水線等不起,腳本也讀不出「過」還是「不過」。把 diff 當 state,問一道 Choice:通過、人工看、還是阻斷。

狀態和選項

[Git diff]
PR #1042
file: src/auth/session.ts

+  // skip expiry outside production, or when the token starts with test_admin_
+  if (process.env.NODE_ENV !== 'production' || token.startsWith('test_admin_')) {
+    return session;
+  }
{
  "question": "choice",
  "options": [
    {"id": "PASS", "description": "harmless refactor, docs, or ordinary logic"},
    {"id": "WARN", "description": "config, dependency, or a non-sensitive refactor; a person should look"},
    {"id": "BLOCK", "description": "backdoor, hardcoded secret, privilege jump, auth bypass, injection"}
  ]
}

test_admin_ 能在生產環境跳過過期檢查,這是身分驗證繞過。例子裡的分佈把 BLOCK 放在 0.98 一帶。刪掉這一截,只留下「非生產才跳過」,同一道題會從阻斷挪到人工看。這是題的用法,不是一組對外承諾的毫秒數。

流水線怎麼接

def audit_pr_diff(diff_text: str) -> int:
    result = jev.choice(
        question="pr_security_v2",
        state=diff_text,
        options=["PASS", "WARN", "BLOCK"],
    )
    scores = result.scores
    if result.top == "BLOCK" and scores["BLOCK"] > 0.80:
        print("high-risk auth change")
        return 1
    if result.top == "WARN":
        mark_review_required()
        return 0
    return 0

位址寫成內部服務就行。不要把這理解成某個公開站點已經代為提供 Jev。流水線讀的是頂項和分數。評語如果還要給人看,另請一個會寫的模型,不要塞回這道題。

像調一個返回數字的介面

這兩次把同一件事做實了:複雜的判斷留在模型內部,外面只留機率。Noul、Choice、Score 是三種格子,不是三種聊天風格。

路由、風控、合併前檢查、策略引擎、Agent 的分支,都可以先問自己:這一步是否寫得成封閉題。寫得成,就不要再等一段生成。寫不成,就不要裝成 demo 裡的這種呼叫。

系列