原语(问题)Noul

当答案是“是”或“否”时,使用 Noul。例如,这条消息是否在请求退款、这份简历是否提到了分布式系统、这条评论是否包含个人数据。如果答案是多个选项中的一项,请使用 Choice。如果是连续区间上的一个位置,请使用 Score。选择问题类型对三者做了比较。

Noul 的答案是一个单独的数字,表示答案为“是”的概率,其中 0 表示“否”,1 表示“是”。

请求结构

发往 TypeSafe API 的 POST 请求体与其他问题类型一样,有三个顶层字段:state,要评估的内容;model;以及 questions。每个 Noul 问题包含以下字段:

  • type:始终为 "noul"。
  • instructions:模型要回答的是/否问题,或供它判断的陈述。
  • criteria:可选。一个包含 true 和 false 描述的对象,说明“是”与“否”各自的含义。

下面这个请求的状态是一条客服消息,两个问题分别是:客户是否想要人工服务、此前是否联系过客服:

request
json
{
  "state": "I have asked three times now. Can I please just talk to a real person?",
  "questions": {
    "is_human_escalation": {
      "type": "noul",
      "instructions": "Is the customer asking for a human agent?"
    },
    "is_repeat_contact": {
      "type": "noul",
      "instructions": "Has the customer contacted support about this before?",
      "criteria": {
        "true": "Mentions a prior attempt, ticket, or that they have asked before",
        "false": "No sign of any previous contact"
      }
    }
  }
}

问题 ID 由你选定,这里是 is_human_escalation 和 is_repeat_contact。这些 ID 不会发送给模型。每个答案会以同样的 ID 返回。第一个问题只依靠 instructions。第二个问题增加了 criteria,说明什么算“是”、什么算“否”。

在 Python SDK 中,同样的问题是 Noul 对象:

python
from typesafe_sdk import Noul, NoulCriteria, TypeSafeClient

with TypeSafeClient() as client:
    response = client.system_one(
        model="jev-latest",
        state="I have asked three times now. Can I please just talk to a real person?",
        questions={
            "is_human_escalation": Noul(
                instructions="Is the customer asking for a human agent?",
            ),
            "is_repeat_contact": Noul(
                instructions="Has the customer contacted support about this before?",
                criteria=NoulCriteria(
                    true="Mentions a prior attempt, ticket, or that they have asked before",
                    false="No sign of any previous contact",
                ),
            ),
        },
    )

    print(response.answers["is_human_escalation"].noul)
    print(response.answers["is_repeat_contact"].noul)

system_one 方法和 https://api.typesafe.ai/v1/systemone 端点都以 TypeSafe 的 AI 模型 System One 命名。如何用 TypeSafe 构建介绍了在代码中的何处使用它。

如果你在使用编程代理,请先安装 TypeSafe agent skill,让它了解请求和响应的结构。

instructions 可以是字符串、对象或数组。先从字符串开始。当问题需要附带数据(例如用来与状态比对的记录),或问题的一部分由代码生成时,再使用对象。在问题中使用结构解释了结构何时有帮助,下面的示例则展示了与代码生成的问题搭配使用的情形。

响应结构

响应的 answers 中,每个问题各有一条记录,键为请求中的 ID:

json
{
  "model": "jev-1.13.0",
  "answers": {
    "is_human_escalation": {
      "type": "noul",
      "noul": 0.99
    },
    "is_repeat_contact": {
      "type": "noul",
      "noul": 0.93
    }
  },
  "usage": {
    "input_tokens": 360,
    "output_tokens": 39
  }
}

这里的两个答案都接近 1。客户说了“和真人交谈”,因此 is_human_escalation 为 0.99。“我已经问过三次了”与 is_repeat_contact 的 true 描述相符,因此它是 0.93。

解读 Noul

这个数字同时是答案和确定性。接近 1 的值是强烈的“是”,接近 0 的值是强烈的“否”,接近 0.5 表示模型给“是”和“否”相近的概率。

下表显示了 jev-1.13.0 对不同客户消息在 is_human_escalation 问题上的真实记录答案:

Statenoul
Thanks, that fixed it!0.02
How do I reset my password?0.07
I need this sorted today, whatever it takes.0.26
Are you a bot?0.40
Is there any way to speak to someone about my invoice?0.84
I have asked three times now. Can I please just talk to a real person?0.99

前两条和后两条都很清楚。“I need this sorted today” 很紧急但从未要求人工,得到 0.26。“Are you a bot?” 暗示想要人工却并未直说,模型几乎对半分配,得到 0.40。这两条都属于需要依据代码中的阈值来做决策的消息。

与 Choice 或 Score 不同,Noul 没有单独的 confidence 值。Noul 的概率分布只有“是”和“否”两种结果,因此单个 noul 值就完整地描述了它。Choice 或 Score 把概率分散到多个选项或级别上,confidence 概括了这种分散程度。

大多数情况下,你的代码会把 noul 按阈值转换成布尔值:

python
wants_human = response.answers["is_human_escalation"].noul > 0.9

if wants_human:
    route_to_agent(ticket)
else:
    route_to_bot(ticket)

阈值设在哪里取决于犯错的代价。当“是”与“否”的处置难度相同时,用 0.5。当对假“是”采取行动的代价很高时调高阈值,例如叫醒值班的人或发起退款。当漏掉真“是”的代价很高时调低阈值,例如未能标记安全问题。中间的值可以交给人工处理,而不是走任何一条代码路径。这与置信度页面为 Choice 和 Score 答案描述的三分法相同。

Noul 值的取值范围是 0 到 1,但它并不是你所问事物本身的量表。它是答案为“是”的概率。如果问题实际上关乎程度,这个值并不能衡量程度。下面对四位候选人提问“这位候选人 Python 强吗?”,旁边是一个有四个级别的 Score:没有经验、有一定了解、在工作中日常使用、深入专长。

CandidateNoul: "Is the candidate strong in Python?"Score: "How much Python experience does the candidate have?"
My experience is in Java and Go. I have not used Python.0.030.0 (No experience)
I have used Python occasionally for small scripts alongside my main Java work.0.141.0 (Some familiarity)
I used Python every day for two years in my last job, mostly data pipelines.0.812.05 (Regular use in a job)
I have written Python daily for eight years, including maintaining a large Django codebase.0.922.89 (Deep expertise)

Noul 判断的是一个命题——“强”,这些值就是该命题成立的可能性。你当然可以在代码中在 0 到 1 的范围内划分层级,比如把 0.3 到 0.7 定义为“有一定经验”,但模型看不到这些层级,因此答案中的任何内容都不是依据它们判断的。一个中间值可能意味着中等经验,也可能意味着情形不明,而且候选人之间的间距也不是你设定的。Score 会单独判断每条级别描述,因此每个候选人都落在或接近你写下的某个级别,返回的概率则显示模型如何在各级别之间分配判断。如果你不认可,改写某个级别再跑一次即可。选择问题类型解释了这一区别。

编写 Noul 问题

每个 Noul 只问一个是/否问题。如果一个问题包含两个条件,例如“客户是否在生气并请求退款?”,模型就必须同时判断两者,这个值的意义也随之减弱。改为提问两个 Noul,在代码中组合。

问题的措辞要让高值意味着“是”。“这条消息是否包含个人数据?”很清楚。“这条消息是否不含个人数据?”则颠倒了含义,之后读取它的代码会把它弄反。

陈述句和疑问句同样有效。对于“客户正在请求退款”,接近 1 的值意味着该陈述为真。用你自己的数据试试两种措辞,看哪种效果更好。

让“是”与“否”的边界毫无歧义。“这位候选人是否有任何 Python 经验?”效果很好,因为“任何”不给中间地带留余地。当边界比较微妙时,添加带 true 和 false 描述的 criteria,就像上面的 is_repeat_contact 问题那样。对大多数 Noul 来说 instructions 就够了,所以请分别尝试带与不带 criteria 的版本,保留在你的文档上给出更好答案的那个。

最佳实践:每次调用提出多个问题

对于一份条件清单,在一次请求中提出多个 Noul 问题:每个条件一个问题,由代码决定组合的含义。问题是并行评估的,因此增加 Noul 几乎不会改变响应时间。一起提出多个问题有更详细的说明。

在代码中处理多个 Noul 答案

上面的两问题请求已经足以让代码完成消息路由。下面的示例在客户要求人工时转接人工,并在客户此前联系过时提高优先级。任何一个问题的值落在中间时,都交给审核人员而不是走代码路径:

python
from typesafe_sdk import Noul, NoulCriteria, TypeSafeClient

SUPPORT_QUESTIONS = {
    "is_human_escalation": Noul(
        instructions="Is the customer asking for a human agent?",
    ),
    "is_repeat_contact": Noul(
        instructions="Has the customer contacted support about this before?",
        criteria=NoulCriteria(
            true="Mentions a prior attempt, ticket, or that they have asked before",
            false="No sign of any previous contact",
        ),
    ),
}

YES = 0.8
NO = 0.2


def route(message: str) -> None:
    with TypeSafeClient() as client:
        response = client.system_one(
            model="jev-latest",
            state=message,
            questions=SUPPORT_QUESTIONS,
        )
    answers = response.answers

    wants_human = answers["is_human_escalation"].noul
    repeat = answers["is_repeat_contact"].noul

    if NO < wants_human < YES or NO < repeat < YES:
        # The model isn't sure either way. Let a person decide.
        send_to_review(message)
        return

    priority = "high" if repeat > YES else "normal"
    if wants_human > YES:
        route_to_agent(message, priority=priority)
    else:
        route_to_bot(message, priority=priority)

对上面那条消息,is_human_escalation 的 noul 值为 0.99,is_repeat_contact 为 0.93,因此代码以高优先级把它路由给人工客服。消息“How do I reset my password?”在两个问题上都是 0.07,被路由给机器人。

阈值就在你的代码里。如果审核人员看到的消息太多,就缩小 NO 和 YES 之间的间距;如果有太多错误路由漏过去,就加大间距。如果之后你还需要知道消息是否提到了付款、是否包含个人数据,就向 SUPPORT_QUESTIONS 再加一个 Noul。请求数保持为一次。

结构化 instructions

Instructions 可以是对象而不是字符串,把问题放在一个字段、补充数据放在其他字段。在问题中使用结构介绍了这何时有帮助。这里用它构造一个由代码生成的问题:把刚收到的一份简历与候选人数据库中可能是同一个人的记录比对。每条记录原样放进 potential_duplicate 字段,每条记录的 question 都相同,所有记录在一次请求中完成检查。由代码生成的问题键中包含每条记录的数据库 ID:

request
json
{
  "state": {
    "resume": {
      "name": "John Smith",
      "location": "Oakland, CA",
      "summary": "Backend engineer with eight years of Python and Go experience.",
      "experience": [
        {
          "employer": "Google",
          "title": "Senior Backend Engineer",
          "years": "2021-2025"
        },
        {
          "employer": "Microsoft",
          "title": "Software Engineer",
          "years": "2017-2021"
        }
      ]
    }
  },
  "questions": {
    "same_as_record_18": {
      "type": "noul",
      "instructions": {
        "potential_duplicate": {
          "name": "Jon Smith",
          "location": "Oakland, CA",
          "last_employer": "Google"
        },
        "question": "Is the resume for the same person as `potential_duplicate`?"
      }
    },
    "same_as_record_42": {
      "type": "noul",
      "instructions": {
        "potential_duplicate": {
          "name": "John Smith",
          "location": "Austin, TX",
          "last_employer": "Lone Star Freight"
        },
        "question": "Is the resume for the same person as `potential_duplicate`?"
      }
    },
    "same_as_record_77": {
      "type": "noul",
      "instructions": {
        "potential_duplicate": {
          "name": "John Smithers",
          "location": "Oakland, CA",
          "last_employer": "Bay Health Clinic"
        },
        "question": "Is the resume for the same person as `potential_duplicate`?"
      }
    }
  }
}

响应:

json
{
  "model": "jev-1.13.0",
  "answers": {
    "same_as_record_18": {
      "type": "noul",
      "noul": 0.74
    },
    "same_as_record_42": {
      "type": "noul",
      "noul": 0.09
    },
    "same_as_record_77": {
      "type": "noul",
      "noul": 0.08
    }
  },
  "usage": {
    "input_tokens": 535,
    "output_tokens": 58
  }
}

每个答案都是“这份简历属于该记录中那个人”的概率。记录 18 的名字拼写不同,但地点和雇主都吻合,得到 0.74。记录 42 名字相同,但城市和雇主都不同,得到 0.09。记录 77 名字相近、地点相同、雇主不同,得到 0.08。请在你的代码中为每个值设置阈值,做法参见在代码中处理多个 Noul 答案,并把中间值交给人工。

在 Python SDK 中,这些问题由候选人记录构建。问题文本固定,记录变化:

python
from typesafe_sdk import Noul, TypeSafeClient

SAME_PERSON = "Is the resume for the same person as `potential_duplicate`?"


def duplicate_questions(candidates: list[dict]) -> dict[str, Noul]:
    """One Noul per candidate record, all asking the same question."""
    return {
        f"same_as_record_{candidate['id']}": Noul(
            instructions={
                "potential_duplicate": {
                    "name": candidate["name"],
                    "location": candidate["location"],
                    "last_employer": candidate["last_employer"],
                },
                "question": SAME_PERSON,
            },
        )
        for candidate in candidates
    }


def find_duplicates(resume: dict, candidates: list[dict]) -> list[str]:
    with TypeSafeClient() as client:
        response = client.system_one(
            model="jev-latest",
            state={"resume": resume},
            questions=duplicate_questions(candidates),
        )
    return [
        question_id
        for question_id, answer in response.answers.items()
        if answer.noul > 0.7
    ]

结构化数据抽取级联实战手册使用结构化 instructions 来校验抽取出的记录。每个字段都会被问到同一组问题。每个问题的 instructions 对象在 main_question 属性中存放问题文本,另有随字段变化的 field_spec 和 extracted_field 属性。

实战手册中的 Noul

欢迎查看我们的实战手册,了解使用 Noul 问题的应用:

  • 并行问题在单次请求中对一篇文章执行 13 项合规检查清单。
  • 自洽性:noul按 15 个问题的评分标准对一条保险理赔评分,并测量这些值在多次运行间的稳定性。
  • 重排序直接使用概率本身而非阈值:每个查询-候选对一个 Noul,然后按值对候选排序。
  • 逐行搜索把“找出匹配行”的 Choice 与“检查文档是否包含答案”的 Noul 配合使用。
  • 结构恢复对每一对相邻行各问一个 Noul——换行是否截断了一个句子——从而从纯文本重建段落。