Noul
当答案是是或否时,用 Noul。例如:这条消息是否要求退款?这份简历是否提到分布式系统?这条评论是否包含手机号?
Noul 的答案只有一个数:答案为"是"的概率,0 表示否,1 表示是。
名字的由来
Noul 可以理解为"带概率的布尔值":不是 true / false,而是 0 到 1 之间的一个数,数值本身就同时表达了答案和把握。
请求结构
请求体顶层同样是 state、model(可省略)和 questions。每个 Noul 问题包含:
type:固定为"noul";instructions:要回答的是非问题,或者一个让模型判断真假的陈述;criteria:可选。一个包含true和false两个字段的对象,分别说明"是"和"否"指什么。
下面的 state 是一条在线客服消息,两个问题分别是:顾客是否要求转人工?是否是重复来访?
{
"state": "我已经问了三遍了,每次机器人都答非所问。别再让机器人回复了,给我转人工客服!",
"model": "xiangxin-latest",
"questions": {
"wants_human": {
"type": "noul",
"instructions": "顾客是否要求由真人客服来处理?"
},
"repeat_contact": {
"type": "noul",
"instructions": "顾客之前是否已经就同一问题联系过客服?",
"criteria": {
"true": "消息中提到此前问过、反馈过或联系过",
"false": "看起来是第一次提出这个问题"
}
}
}
}wants_human 和 repeat_contact 是你起的 ID,不会发给模型。第一个问题只靠 instructions;第二个用 criteria 说清楚什么算"是"、什么算"否"。
用 Python SDK 写成 Noul:
from xiangxin import Noul, XiangxinClient
with XiangxinClient() as client:
resp = client.system_one(
state="我已经问了三遍了,每次机器人都答非所问。别再让机器人回复了,给我转人工客服!",
questions={
"wants_human": Noul(
instructions="顾客是否要求由真人客服来处理?",
),
"repeat_contact": Noul(
instructions="顾客之前是否已经就同一问题联系过客服?",
criteria={
"true": "消息中提到此前问过、反馈过或联系过",
"false": "看起来是第一次提出这个问题",
},
),
},
)
print(resp.answers["wants_human"].noul) # 0.98
print(resp.answers["repeat_contact"].noul) # 0.89响应结构
{
"model": "xiangxin-1.0.0",
"answers": {
"wants_human": {"type": "noul", "noul": 0.98},
"repeat_contact": {"type": "noul", "noul": 0.89}
},
"usage": {"input_tokens": 104, "output_tokens": 35}
}两个答案都接近 1:顾客明确说了"转人工客服";"已经问了三遍"正好符合 repeat_contact 的 true 描述。
解读 Noul
这一个数既是答案,也是把握程度。接近 1 是强烈的"是",接近 0 是强烈的"否",0.5 附近表示模型认为两种可能差不多。
同一个 wants_human 问题,换几条不同的消息:
| 顾客消息 | noul |
|---|---|
| "给我转人工!" | 0.98 |
| "能不能找个真人聊一下?" | 0.9 |
| "今天之内必须给我处理好" | 0.6 |
| "你是机器人吗?" | 0.3 |
| "密码怎么重置?" | 0.25 |
| "谢谢,问题解决了" | 0.24 |
头两条明确要求真人,得到 0.98 和 0.9。后四条都没有明说要真人,但概率并没有贴近 0:"今天之内必须给我处理好"情绪急迫,得到 0.6,甚至越过了 0.5;"你是机器人吗?"只是隐约暗示,得到 0.3;与真人毫无关系的"密码怎么重置?"和"谢谢,问题解决了"也只降到 0.25 左右。所以阈值要用你自己的数据来定,不要想当然地取 0.5;落在中间地带的消息,正是需要由你的代码或人来决定怎么处理的情况。
Noul 没有单独的 confidence。它的分布只有"是"和"否"两个结果,一个 noul 值就完整描述了它。离 0.5 越远,模型越有把握。Choice 和 Score 的概率分散在多个选项上,所以需要额外的 confidence 来概括分布形状。
用阈值转成布尔值
最常见的做法是给 noul 设一个阈值:
if resp.answers["wants_human"].noul >= 0.5:
transfer_to_agent()阈值放在哪里,取决于犯错的代价:
- 两种错误代价差不多:用 0.5;
- 误判为"是"代价很高(例如自动退款、半夜呼叫值班工程师):提高阈值,比如 0.85;
- 漏掉一个"是"代价很高(例如漏过包含身份证号的消息):降低阈值,比如 0.2。
也可以设两个阈值,中间地带交给人工复核,见下文 处理多个 Noul 答案。
Noul 不是刻度
noul 的取值在 0 到 1 之间,但它不是你所问事物的程度,而是"答案为是"的概率。
例如对四份简历问"候选人的 Python 是否很强?",一个 0.55 并不代表"中等水平",而是模型对"很强"这个说法成不成立拿不准。你当然可以在代码里把 0.3–0.7 定义为"有一定经验",但模型并没有见过这个定义,答案也不是依据它做出的。
需要程度,就用 Score 并写清楚每一档;需要是非,就把条件写得没有中间地带。
写好 Noul 问题
一个 Noul 只问一件事。 "顾客是否很生气并且要求退款?"要求模型同时判断两个条件,数值的含义就模糊了。拆成两个 Noul,在代码里用 and 组合。
让"高值"对应"是"。 "消息是否包含个人信息?"很清楚;"消息是否不含个人信息?"把含义反过来了,以后读代码的人很容易搞反。象信一号对否定句的处理基本一致(一个问题和它的否定形式,概率大致互补),但不保证严格互补,更不要依赖这种关系做算术,见 已知短板。
问句和陈述句都可以。 "顾客在要求退款"这样的陈述也行,接近 1 表示陈述为真。可以用自己的数据比较两种写法。
让"是"与"否"的边界清楚。 "候选人是否有任何 Python 经验?"效果很好,因为"任何"没有中间地带。边界微妙时,加上 criteria 的 true / false 描述,就像上面的 repeat_contact。
criteria 要和 instructions 一致。 不要让 true 描述"否"的情形。指令和标准彼此矛盾时,答案会变得不可靠。
一次问多个 Noul
对于一张检查清单,把每个条件写成一个 Noul,全部放进同一个请求,由代码决定组合起来意味着什么。问题是并行评估的,多加几个 Noul 几乎不增加响应时间。原理见 原语总览。
例如检查一条用户评论是否可以直接展示:
from xiangxin import Noul
REVIEW_CHECKS = {
"has_phone": Noul(instructions="评论中是否出现了手机号或微信号?"),
"has_ad": Noul(instructions="评论是否在为其他店铺或商品做广告引流?"),
"has_abuse": Noul(instructions="评论是否包含人身攻击或辱骂?"),
"is_about_product": Noul(instructions="评论内容是否与所购商品本身有关?"),
}同样的思路可以用来筛选:对一批候选条目各问一个 Noul("这一段是否回答了用户的问题?"),按 noul 排序或过滤,见 RAG 段落过滤。
处理多个 Noul 答案
上面转人工的两个问题已经足够给消息分流:顾客要求真人时转人工;重复来访时提高优先级。任一问题落在中间地带时,交给复核而不是猜测。
YES = 0.8 # 高于它视为"是"
NO = 0.3 # 低于它视为"否"
def decide(value: float) -> str:
if value >= YES:
return "yes"
if value <= NO:
return "no"
return "unsure"
def route(resp) -> dict:
human = decide(resp.answers["wants_human"].noul)
repeat = decide(resp.answers["repeat_contact"].noul)
if "unsure" in (human, repeat):
return {"queue": "review"}
if human == "yes":
return {"queue": "agent", "priority": "high" if repeat == "yes" else "normal"}
return {"queue": "bot"}对上面的消息,wants_human 为 0.98、repeat_contact 为 0.89,于是转人工且高优先级。"密码怎么重置?"在两个问题上分别为 0.25 和 0.2,都低于 0.3,交给机器人。NO 取 0.3 而不是更低,正是因为上表里与真人无关的消息也只降到 0.25 左右;"今天之内必须给我处理好"的 0.6 落在中间地带,进入复核。
阈值写在代码里。如果复核队列太多,缩小 NO 和 YES 之间的区间;如果误路由太多,扩大它。以后想知道消息是否涉及付款、是否包含个人信息,再加一个 Noul 即可。
结构化 instructions
instructions 也可以是对象:问题放在一个字段,被引用的数据放在其他字段。一个典型用法是"候选记录比对":问题文本固定,只替换记录。
from xiangxin import Noul, XiangxinClient
resume = "张伟,杭州,现就职于某电商公司,负责订单系统后端,5 年 Java 经验……"
candidates = [
{"id": 18, "name": "张玮", "city": "杭州", "employer": "某电商公司"},
{"id": 42, "name": "张伟", "city": "成都", "employer": "某银行"},
]
questions = {
f"same_person_{c['id']}": Noul(
instructions={
"record": c,
"question": "这份简历与 `record` 描述的是否是同一个人?",
},
)
for c in candidates
}
with XiangxinClient() as client:
resp = client.system_one(state=resume, questions=questions)
for c in candidates:
print(c["id"], resp.answers[f"same_person_{c['id']}"].noul)实际运行时,18 号记录名字写法不同("玮"与"伟"),但城市和雇主都吻合,得到 0.62:模型倾向于是同一个人,但并不确定;42 号同名但城市、雇主都不同,只有 0.02。更多写法见 进阶:结构化。

