文档目录

Noul(是非题)

Noul(是非题)问题要求 TypeSafe 模型对一个是非问题进行判断,并返回答案为"是"的概率。

非官方中文翻译 · 内容以 英文原文 ↗ 为准 · 同步于 2026-10-15

当答案只有"是"或"否"时,请使用 Noul。例如:这条消息是否要求退款、这份简历是否提到过分布式系统、这条评论是否包含个人数据。如果答案是若干个选项之一,请使用 Choice。如果答案是某个连续区间上的某个位置,请使用 Score。选择题型 一文对三者进行了比较。

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

Request structure

向 TypeSafe API 发出的 POST 请求正文与任何其他题型一样,包含三个顶层字段:state,即需要评估的内容;model;以及 questions。每个 Noul 问题包含以下字段:

  • type:始终为 "noul"。
  • instructions:模型回答的是非题,或需要它判断的陈述句。
  • criteria:可选。一个对象,包含 true 和 false 描述,分别说明"是"和"否"的含义。

下面是一个请求示例,其中 state 是一条客户支持消息,两个问题分别是客户是否想要真人服务,以及客户之前是否联系过客服:

json
{
  "model": "jev-latest",
  "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"
      }
    }
  }
}

这里的 question 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 接口都以 System One(TypeSafe 的 AI 模型)命名。如何使用 TypeSafe 构建 一文介绍了在代码中何时使用它。

如果你正在使用编程智能体(coding agent),请先安装 TypeSafe agent skill,以便它了解请求和响应的结构。

Response structure

响应中每个问题对应 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。客户说 "talk to a real person"(我要和真人说话),因此 is_human_escalation 为 0.99。"I have asked three times now"(我已经问过三次了)符合 is_repeat_contact 的 true 描述,因此该值为 0.93。

Reading a Noul

这个数字既代表了答案,也体现了置信度(confidence)。接近 1 表示强烈的"是",接近 0 表示强烈的"否",接近 0.5 则意味着模型对"是"和"否"赋予了相近的概率。

下表展示了在不同客户消息下,jev-1.13.0 对 is_human_escalation 问题给出的实际答案:

state noul
谢谢,已经解决了! 0.02
我该怎么重置密码? 0.07
今天必须把这件事搞定,无论如何都行。 0.26
你是机器人吗? 0.40
有没有办法让我找个人聊聊我的发票问题? 0.84
我已经问过三次了,求求你让我跟真人说句话可以吗? 0.99

前后两组消息的答案都是明确的。"今天必须把这件事搞定"虽然语气很急,但并未要求真人介入,因此得分为 0.26。"你是机器人吗?"虽然流露出希望与真人沟通的意味,但并未明确提出要求,模型在 0.40 处分成了几乎均等的两半。这两类消息都属于需要由你的代码根据阈值来做出决策的情形。

Noul 没有像 Choice 或 Score 那样单独的 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。如果误判为"是"的代价很高(例如触发寻呼或发放退款),则应提高阈值。如果漏掉一个真正的"是"代价很高(例如未能标记出一个安全隐患),则应降低阈值。处于中间区间的值可以转交给人工处理,而不是走任一代码分支。这与 置信度(Confidence) 一文中针对 Choice 和 Score 答案所描述的三分法相同。

Noul 的取值范围是 0 到 1,但它并不是对所问事物程度的度量,而是答案为"是"的概率。如果问题真正关心的是某种程度,那么这个值就无法衡量那种程度。下面针对四位候选人同时提出 "Is the candidate strong in Python?"(该候选人是否精通 Python?),并附带一个分为四档的 Score:无经验、有一定了解、在工作中经常使用、精通。

候选人 Noul:「该候选人是否精通 Python?」 Score:「该候选人有多少 Python 经验?」
我的经验主要在 Java 和 Go 上,没有用过 Python。 0.03 0.0(无经验)
在主要从事 Java 工作的同时,我偶尔会用 Python 写一些小脚本。 0.14 1.0(有一定了解)
上一份工作里我每天使用 Python,持续了两年,主要做数据管道。 0.81 2.05(在工作中经常使用)
我已连续八年每天用 Python 写代码,包括维护一个大型 Django 代码库。 0.92 2.89(精通)

Noul 只判断"精通"这一个命题,得到的值是该命题为真的概率。你可以在自己的代码里人为地把 0 到 1 划分成几个区间,比如用 0.3 到 0.7 表示"有一定经验",但模型并不会看到这些区间,因此答案中没有任何部分是依据这些区间评判的。一个中间值可能表示中等程度的经验,也可能表示一个拿不准的案例,而且不同候选人之间的数值间距也不是由你设定的。Score 则会针对每一档的描述独立进行判断,因此每位候选人都会落点在你所写的某一档附近,返回的概率反映了模型在各个档位之间如何分配判断。如果你不同意它的结论,可以修改某一档的措辞后再次运行。选择题型 一文详细解释了两者的区别。

Writing a Noul question

每个 Noul 只问一个是非问题。如果一个问题里包含两个条件(例如 "Is the customer angry and asking for a refund?"——客户是否生气且要求退款?),模型就不得不同时判断这两件事,得到的数值含义也会被稀释。正确做法是使用两个 Noul,然后在代码中将它们的结论组合起来。

问题的措辞应当让数值越大表示答案为"是"。"Does the message contain personal data?"(这条消息是否包含个人数据?)表达得很清楚。而 "Is the message free of personal data?"(这条消息是否不包含个人数据?)则把含义反过来了,后续读取这个值的代码很容易把它理解反。

用陈述句提问和用疑问句提问效果相同。对于 "The customer is requesting a refund"(客户正在申请退款)这样的陈述,接近 1 的值表示该陈述为真。建议用你自己的数据分别尝试两种措辞,看看哪种效果更好。

"是"和"否"之间的边界要尽量明确。"Does this candidate have any Python experience?"(这位候选人是否有过任何 Python 经验?)就很好,因为 "any"(任何)排除了中间地带。当边界较为模糊时,可以像上面 is_repeat_contact 那样,加入带有 true 和 false 描述的 criteria。对大多数 Noul 而言,仅使用 instructions 就足够了,所以最好在你的文档上同时测试有 criteria 和没有 criteria 两个版本,保留答案质量更好的那一个。

最佳实践:每次请求中包含多个问题

对于条件清单,建议在同一次请求中提出多个 Noul 问题:每个条件对应一个问题,由代码决定这种组合的含义。问题会被并行评估,因此增加 Noul 几乎不会影响响应时间。Ask multiple questions together 详细介绍了这种做法。

在代码中处理多个 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 除了是字符串外,也可以是一个对象,其中一个问题字段中存放问题本身,其他字段存放补充数据。Use structure in the questions 介绍了在哪些场景下这种做法会有帮助。这里展示的是用代码构建的问题:将刚刚收到的简历与候选人数据库中可能是同一个人的记录进行比对。每条记录都按原样放入 potential_duplicate 字段,question 对每条记录都相同,所有记录在一次请求中一并检查。由代码生成的问题键包含每条记录的数据库 ID:

json
{
  "model": "jev-latest",
  "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。正如 Handling multiple Noul answers in code 所示,在代码中对每个值设置阈值,并将中间值交给人工处理。

在使用 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
    ]

structured-data-extraction cascade cookbook 使用结构化指令来验证已抽取的记录。每个字段都会收到同一组问题。每个问题的 instructions 对象将问题文本放在 main_question 属性中。还有 field_spec 和 extracted_field 属性,会随着字段的不同而变化。

实战示例中的 Noul

查看我们的实战示例,了解使用 Noul 问题的应用:

  • Parallel questions 在一次请求中针对一篇文章运行包含 13 个问题的合规清单。
  • Self-consistency: nouls 根据包含 15 个问题的评分表对保险理赔进行打分,并衡量多次运行之间分值的稳定性。
  • Re-ranking 直接使用概率本身,而不是阈值:每个查询-候选对对应一个 Noul,然后按分值对候选项进行排序。
  • Line-by-line search 将一个用于定位匹配行的 Choice 与一个用于检查文档是否包含答案的 Noul 配对使用。
  • Structure recovery 针对每一对相邻行问一个 Noul,判断换行符是否打断了句子,从而从纯文本中重建段落。

This documentation is built and hosted on Mintlify, a developer documentation platform.