AB
AiBoss
チュートリアル

Jev

チュートリアル

Jev 类型安全判断 API 入门:Noul、Choice、Score 三种问题类型与 Python 调用

Jev 是面向软件调用的判断型模型,用 Noul、Choice、Score 三种问题类型返回带类型的判断值与概率。本文介绍 state 与 questions 的组织方式、三种类型的输入输出结构、HTTP API 与 Python 调用方法,以及阈值设定、概率校准等落地注意事项。

Jev 是一个面向软件调用的判断型模型:它不负责写文章,而是把「这段文本属于哪一类」「这件事是否成立」「程度落在哪一档」这类判断,以带类型的值和概率返回。它适合已经能处理 JSON API 的工程师,用来做工单分流、紧急度判定、内容审核、贡献度评估等需要把结果直接接进代码分支的场景。站内也有对应的工具页可以对照查看:Jev。本文按公开信息整理三种问题类型的用法、请求结构、Python 调用方式与实测中值得注意的细节。

准备工作

在动手写代码之前,需要先确认几件事。

  • 账号与 API 密钥:在控制台的「API keys」页面创建密钥。密钥按组织为单位管理,创建后显示的密文只出现一次,需要立刻保存。把密钥放进环境变量或密钥管理服务,不要写进代码仓库。
  • 请求头:调用时把密钥作为 Bearer 令牌放进 HTTP 请求头。
  • 模型版本:请求体里用 model 字段指定版本。为了结果可复现,建议固定一个具体版本号,而不是依赖默认值。本文示例统一使用 jev-1.13.0。
  • 输入形态:Jev 只接受文本输入,state 可以是字符串、JSON 对象或数组。它不直接处理图片、音频、视频。
  • 语言:公开文档以英语作为主要训练语言,其他语言建议用真实数据自行评估效果,不要假设英语上的表现可以直接迁移。
  • 费用:价格与配额属于易变信息,务必以官网当前公布的信息为准,不要按本文或任何二手资料里的数字做预算。

另外要区分两件事:Jev 返回的是「判断结果与概率」,不是业务上的正确答案。概率经过校准,指的是在大量同类样本上预测概率与实际发生比例大致吻合,而不是「这一条判断有 80% 的概率是对的」。阈值怎么定、中间地带要不要转人工,都由调用方决定。

操作步骤

第一步:把输入拆成 state 与 questions

一次请求由两部分组成:state 是判断对象以及判断所需的上下文,questions 是针对同一个 state 的一组问题。组织输入时,把「判断什么」和「按什么标准判断」分开,结构会清楚很多。

字段作用工单分类场景示例
state判断对象与必要上下文工单正文、退款政策文本
questions针对同一 state 的问题集合负责部门、紧急程度、不满强度
每个问题的 type返回值类型noul、choice、score
每个问题的 instructions要判断什么应由哪个团队处理
每个问题的 criteria候选或评价标准技术团队处理缺陷与故障

几个关键机制需要先理解:

  • state 只读一次,同一请求内的各个问题针对同一份 state 并行评估。
  • 问题之间相互独立,同一个请求里的问题不会互相参考答案。所以不要设计「先判断 A,再根据 A 判断 B」的链式问题,而是拆成独立问题,在应用层组合结果。
  • 问题 ID 由调用方指定,返回值按同样的 ID 组织,方便和原字段对应。ID 不会发送给模型,也不参与推理,所以 q1 这类机械命名也能跑,但取一个能读懂判断意图的名字更利于维护。
  • instructions 与 criteria 里可以用对象和数组,不只是字符串。条件复杂时,把判断拆成更小的问题,再用代码合并输出,通常比堆一段长描述更可靠。

第二步:选择问题类型

类型问的是什么主要返回值典型场景
Noul是或否0 到 1 之间的 noul 值是否在要求紧急处理
Choice候选中的哪一个选中项、各候选概率、confidence属于账单、技术还是销售
Score落在定义好的哪一档档位期望值、各档概率、legend、confidence不满强度处于三档中的哪一档

第三步:写 Noul 问题

Noul 用于真伪判断,返回的 noul 表示「为真」的概率,越接近 1 越偏向「是」,越接近 0 越偏向「否」。Noul 没有单独的 confidence 字段。

下面这个例子把冰淇淋三明治的定义放进 state,问它是不是三明治,并在 criteria 里分别定义 true 和 false 的含义:

{
  "state": {
    "food": "Ice cream sandwich",
    "definition": "An ice cream sandwich is a frozen dessert with a layer of ice cream between two cookies, wafers, or thin pieces of cake."
  },
  "model": "jev-1.13.0",
  "questions": {
    "is_sandwich": {
      "type": "noul",
      "instructions": "Is `food` a sandwich?",
      "criteria": {
        "true": "A sandwich is a food dish where a filling, such as meat, cheese, vegetables, or spread, is placed between structural starch",
        "false": "The food has no bread enclosing a filling or uses only a single slice of bread, or uses a non-bread wrapper such as a tortilla, wafer, or cookie."
      }
    }
  }
}

返回结构如下:

{
  "answers": {
    "is_sandwich": {
      "type": "noul",
      "noul": 0.33
    }
  }
}

这个输入得到的结果是 0.33。因为 criteria.false 里明确把「用威化或饼干夹起来」也算作非三明治,结果偏向「不是」这一侧是符合定义的。同一个问题重复执行时数值不会完全一致,实测中在 0.33 到 0.35 之间浮动。

在代码里使用 Noul 时,不要把返回值直接当成布尔值。应当根据业务上的损失来设定阈值:如果误判为「是」的代价很高,就把阈值定高一些,并把落在中间地带的结果转给人工处理。

第四步:写 Choice 问题

Choice 用于从给定候选中选一个。返回的 choice 是概率最高的候选,probabilities 是每个候选的概率,confidence 用 0 到 1 概括分布的集中程度。注意 confidence 既不是「选中项的概率」,也不是「正确率」,公开资料没有给出它的具体计算公式,不要按某个公式去反推。

候选可以只写名字,也可以附带描述信息。下面这个例子同时提了两个问题,一个只给候选名,另一个给候选配上说明和色值:

{
  "state": { "condition": "Tornado's a-comin" },
  "model": "jev-1.13.0",
  "questions": {
    "sky_color": {
      "type": "choice",
      "instructions": "What color is the sky?",
      "criteria": {
        "indigo": "",
        "electric blue": "",
        "baby blue": "",
        "gray": "",
        "lavender": "",
        "salmon": "",
        "seafoam green": ""
      }
    },
    "sky_color_with_desc": {
      "type": "choice",
      "instructions": {
        "object": "sky",
        "question": "what color is `object`?"
      },
      "criteria": {
        "indigo": {
          "description": "very dark, deep blue, like fountain-pen ink",
          "hex": "#280868",
          "rgb": { "red": 40, "green": 8, "blue": 104 },
          "hsl": { "hue": 260, "saturation": "86%", "lightness": "22%" }
        },
        "electric blue": {
          "description": "bright, punchy blue, like a swimming pool",
          "hex": "#1585E7",
          "rgb": { "red": 21, "green": 133, "blue": 231 },
          "hsl": { "hue": 208, "saturation": "83%", "lightness": "49%" }
        },
        "baby blue": {
          "description": "a very pale, soft blue, like a robin's egg",
          "hex": "#C2E4F1",
          "rgb": { "red": 194, "green": 228, "blue": 241 },
          "hsl": { "hue": 197, "saturation": "63%", "lightness": "85%" }
        },
        "gray": {
          "description": "a pale slate, like a heron's feathers",
          "hex": "#B1CBD1",
          "rgb": { "red": 177, "green": 203, "blue": 209 },
          "hsl": { "hue": 191, "saturation": "26%", "lightness": "76%" }
        },
        "lavender": {
          "description": "a dusty blue-purple, like wisteria blossoms",
          "hex": "#847CBF",
          "rgb": { "red": 132, "green": 124, "blue": 191 },
          "hsl": { "hue": 247, "saturation": "34%", "lightness": "62%" }
        },
        "salmon": {
          "description": "a warm, almost-orange pink, like coral",
          "hex": "#FB7A65",
          "rgb": { "red": 251, "green": 122, "blue": 101 },
          "hsl": { "hue": 8, "saturation": "95%", "lightness": "69%" }
        },
        "seafoam green": {
          "description": "a muted, grayish green, like dried eucalyptus leaves",
          "hex": "#94BDB5",
          "rgb": { "red": 148, "green": 189, "blue": 181 },
          "hsl": { "hue": 168, "saturation": "24%", "lightness": "66%" }
        }
      }
    }
  }
}

返回结果:

{
  "answers": {
    "sky_color": {
      "type": "choice",
      "choice": "seafoam green",
      "confidence": 0.48,
      "probabilities": {
        "electric blue": 0,
        "seafoam green": 0.56,
        "gray": 0.4,
        "baby blue": 0,
        "indigo": 0.03,
        "lavender": 0,
        "salmon": 0
      }
    },
    "sky_color_with_desc": {
      "type": "choice",
      "choice": "indigo",
      "confidence": 0.33,
      "probabilities": {
        "electric blue": 0,
        "seafoam green": 0.36,
        "gray": 0.21,
        "indigo": 0.43,
        "baby blue": 0,
        "lavender": 0,
        "salmon": 0
      }
    }
  }
}

两点值得注意。第一,返回的概率保留两位小数,前一个问题的概率合计是 0.99,不要把它当成严格归一化的分布去做校验。第二,这两个问题不仅候选描述不同,instructions 的结构和附加的色值也不同,所以不能据此得出「加了描述所以答案变了」的结论。同一个 state 下,只要问题和候选的定义变了,输出分布就可能变。

第五步:写 Score 问题

Score 按定义好的有序档位评估状态。返回值包含 score、各档位的 probabilities、把编号和说明对应起来的 legend,以及 confidence。score 是档位编号的期望值,也就是每个编号乘以对应概率再求和,因此可能是小数。它不是「概率最高的那一档」。

下面这个例子用五档评估两个主体对一件作品的贡献程度:

{
  "state": {
    "scenario": "Naruto is a Celebes crested macaque living in the Tangkoko nature reserve in North Sulawesi, Indonesia. David Slater, a wildlife photographer shooting macaques in the reserve, leaves his camera unattended, and Naruto repeatedly activates the shutter, producing hundreds of images, including a remarkably sharp, grinning self-portrait reminiscent of a human selfie. Slater later processes and publishes the best photographs in a book that names him as the copyright owner.",
    "subject": "Naruto",
    "human": "Slater",
    "creative_work": "the grinning self-portrait"
  },
  "model": "jev-1.13.0",
  "questions": {
    "subject_contribution": {
      "type": "score",
      "instructions": "How much did `subject` contribute to `creative_work`?",
      "criteria": ["None", "Minorly", "Moderately", "Majorly", "Completely"]
    },
    "human_contribution": {
      "type": "score",
      "instructions": "How much did `human` contribute to `creative_work`?",
      "criteria": ["None", "Minorly", "Moderately", "Majorly", "Completely"]
    }
  }
}

返回结果:

{
  "answers": {
    "subject_contribution": {
      "type": "score",
      "score": 3.43,
      "confidence": 0.56,
      "legend": {
        "0": "None",
        "1": "Minorly",
        "2": "Moderately",
        "3": "Majorly",
        "4": "Completely"
      },
      "probabilities": {
        "0": 0,
        "1": 0,
        "2": 0.03,
        "3": 0.48,
        "4": 0.49
      }
    }
  }
}

另一个问题的结果是 1.19,confidence 为 0.69。两个问题都返回了 0 到 4 这五个编号上的概率分布。

这里有一个容易踩的坑:用返回的两位小数概率去反算 score,结果和返回值对不上。上面这个例子按显示概率算是 3.46,实际返回 3.43;另一个问题按显示概率算是 1.21,实际返回 1.19。仅凭这些记录无法确定差异来源,所以实现里应当同时使用返回的 score 和概率,不要自己用显示值重算一遍再覆盖它。

第六步:调用 HTTP API

接口是 POST,请求体包含 state、model、questions 三部分。下面用 Python 标准库 urllib 演示,一次请求同时判断「负责部门」「不满强度」「紧急程度」三件事:

import json
import os
from urllib.request import Request, urlopen

api_key = os.environ["JEV_API_KEY"]

payload = {
    "state": "今朝からAPIが500エラーを返し続けていて、本番サービスが止まっています。至急対応してください!",
    "model": "jev-1.13.0",
    "questions": {
        "department": {
            "type": "choice",
            "instructions": "この問い合わせはどのチームが担当すべきか",
            "criteria": {
                "billing": "請求・支払い・契約に関する問い合わせ",
                "technical": "不具合・障害・仕様に関する問い合わせ",
                "sales": "導入検討・見積もりに関する問い合わせ"
            }
        },
        "urgency": {
            "type": "noul",
            "instructions": "この問い合わせは緊急の対応を求めているか",
            "criteria": {
                "true": "サービス停止やデータ損失など、即時の対応が必要な状態",
                "false": "即時の対応を要しない状態"
            }
        },
        "dissatisfaction": {
            "type": "score",
            "instructions": "この問い合わせに表れている不満の強さ",
            "criteria": ["None", "Mild", "Moderate", "Strong", "Severe"]
        }
    }
}

request = Request(
    "https://api.typesafe.ai/v1/jev",
    data=json.dumps(payload).encode("utf-8"),
    headers={
        "Content-Type": "application/json",
        "Authorization": f"Bearer {api_key}"
    },
    method="POST"
)

with urlopen(request) as response:
    result = json.loads(response.read().decode("utf-8"))

answers = result["answers"]

# Choice:取最高概率候选
department = answers["department"]["choice"]

# Noul:按业务损失设定阈值,中间地带转人工
urgency_score = answers["urgency"]["noul"]
if urgency_score >= 0.8:
    route = "oncall"
elif urgency_score <= 0.2:
    route = "normal"
else:
    route = "human_review"

# Score:使用返回的期望值,不要用显示概率重算
dissatisfaction = answers["dissatisfaction"]["score"]

print(department, route, dissatisfaction)

要点是:state 只发一次,三个问题并行评估;每个问题的 ID 在返回的 answers 里原样出现,直接按 ID 取值即可;Noul 的阈值由业务决定,不要写死在库里。

一个完整示例

把上面的步骤串起来,做一个最小可运行的工单分流脚本。它读取环境变量里的密钥,发一次请求,然后按结果决定路由。

import json
import os
from urllib.request import Request, urlopen

API_URL = "https://api.typesafe.ai/v1/jev"
MODEL = "jev-1.13.0"


def judge(ticket_text):
    payload = {
        "state": ticket_text,
        "model": MODEL,
        "questions": {
            "department": {
                "type": "choice",
                "instructions": "Which team should handle this inquiry?",
                "criteria": {
                    "billing": "Questions about invoices, payments, or contracts",
                    "technical": "Questions about defects, outages, or behavior",
                    "sales": "Questions about evaluation, pricing, or procurement"
                }
            },
            "urgent": {
                "type": "noul",
                "instructions": "Does this inquiry require urgent handling?",
                "criteria": {
                    "true": "The service is down or data is at risk, requiring immediate action",
                    "false": "No immediate action is required"
                }
            },
            "severity": {
                "type": "score",
                "instructions": "How strong is the dissatisfaction expressed in this inquiry?",
                "criteria": ["None", "Mild", "Moderate", "Strong", "Severe"]
            }
        }
    }

    request = Request(
        API_URL,
        data=json.dumps(payload).encode("utf-8"),
        headers={
            "Content-Type": "application/json",
            "Authorization": "Bearer " + os.environ["JEV_API_KEY"]
        },
        method="POST"
    )

    with urlopen(request) as response:
        return json.loads(response.read().decode("utf-8"))["answers"]


def route(answers):
    department = answers["department"]["choice"]
    urgent = answers["urgent"]["noul"]
    severity = answers["severity"]["score"]

    if urgent >= 0.8:
        queue = "oncall"
    elif urgent <= 0.2:
        queue = "standard"
    else:
        queue = "human_review"

    return {
        "department": department,
        "queue": queue,
        "severity": severity,
        "raw": answers
    }


if __name__ == "__main__":
    text = "Our production API has been returning 500 errors since this morning and the service is down. Please respond immediately."
    answers = judge(text)
    print(json.dumps(route(answers), ensure_ascii=False, indent=2))

运行前先设置环境变量:

export JEV_API_KEY="你的密钥"
python ticket_router.py

输出里会包含选中的部门、按阈值决定的路由队列、不满强度的期望值,以及完整的原始返回,便于排查。如果要把结果写进数据库或日志,建议把 answers 整体保留,因为概率分布和 confidence 在事后分析阈值是否合理时很有用。

注意事项

  • 概率是校准过的,但不是单条正确率。校准说的是在大量样本上预测概率与实际发生比例大致吻合。不要把它读成「这一条有 80% 的概率是对的」。
  • 重复执行结果不会完全一致。同一个输入多次调用,数值会在小范围内浮动。需要可复现时固定 model 版本,并在业务上容忍这个波动。
  • 不要用显示概率反算 Score。返回的概率保留两位小数,按它重算期望值会和返回的 score 有偏差,直接用返回值。
  • Choice 的概率合计可能不是 1。同样是显示精度造成的,不要拿它做严格校验。
  • confidence 的含义要自己确认。公开资料没有给出它的计算公式,不要假设它等于选中项概率或正确率,业务阈值应当基于自己的数据来定。
  • 问题之间相互独立。同一请求内的问题不会互相参考答案,需要链式判断时拆成多个问题,在应用层组合。
  • 问题 ID 不参与推理。它只用于把返回值和调用方字段对应起来,命名可以自由,但建议取可读的名字。
  • 只支持文本。图片、音频、视频不能直接作为输入。
  • 非英语输入需要自行评估。公开文档以英语为主要训练语言,其他语言建议用真实数据验证效果。
  • 复杂条件要拆小。与其写一段很长的判断说明,不如拆成多个小问题,再用代码合并输出,例如把「是否要求退款」和「按规则是否符合退款条件」分开问。
  • 阈值由调用方决定。Jev 不会替你决定多高的概率算「是」,要按误判成本来设定,并为中间地带设计人工兜底。
  • 价格与配额以官网为准。计费方式、可用模型版本、限额都可能调整,接入前请查阅当前公布的信息。