原语(问题)原语(问题)

TypeSafe 的原语是你在代码中组合使用的小型、带类型的构建块。它们成对出现:一个问题定义了让 System One 模型对某个状态做出的一项判断,而它的答案就是返回的类型化取值。你在代码中组合这些答案来做出决策。共有三种问题类型,各自返回不同形态的答案。

类型回答什么返回
Choice这些选项中是哪一个?choice、probabilities、confidence
Score处于哪个级别?score、legend、probabilities、confidence
Noul这是否为真?noul(0 到 1)

你可以只问一个问题,也可以把多个问题一起发送。请求中的每个问题看到的是同一个状态,各自独立评估,并在你选定的 ID 下返回一个类型化答案。

每个问题只要求一次即时判断

System One 模型为快速、聚焦的判断而生。要它做的是内行的人在给定恰当上下文后一秒钟内就能做出的判断。“这条消息是否传达了紧迫性?”是个好问题。“分析这条消息并确定最佳处理方案”则不是。后者需要慢速推理,而这正是应当把任务拆成多个小问题、再在代码中组合答案的信号。

如果你想要的判断取决于多个独立因素,就针对每个因素分别提问,再用你自己的逻辑组合答案。与其“给这份创业路演打个分”,不如分别询问市场规模、技术可行性和差异化程度,然后在代码中按相对重要性为它们加权。当优先级变化时,只需修改权重值,而不必重写提示词。一起提出多个问题展示了具体做法。

定义问题

每个问题都有一个 ID、一个 type 和 instructions。Choice 和 Score 问题还接受 criteria,它为 Choice 问题定义选项,为 Score 定义级别。Noul 问题可以接受 criteria,作为对“是”与“否”含义的可选澄清。

  • ID。你选定的键,例如 refund_requested。它用于在响应中标识对应的答案。
  • type。choice、score 或 noul 之一。
  • instructions。你要就状态提出的问题。你的评估逻辑就写在这里。把它写成一个清晰、具体的问题,或写成一个供模型判断的陈述。对大多数问题来说,一个字符串就够了。它也可以是对象或数组,把问题放在一个字段、把其所指的数据放在其他字段;参见在问题中使用结构。
  • criteria。可能的答案:Choice 问题是选项的映射,Score 问题是级别的有序列表,Noul 问题是关于“是”与“否”的可选描述。每种问题类型的页面会介绍其具体形态。

下面这个问题询问客户是否请求了退款:

python
from typesafe_sdk import Noul

questions = {
    "refund_requested": Noul(
        instructions="Does the customer request a refund?",
    ),
}

问题 ID 是给代码用的,不会发送给模型。请把完整的问题写进 instructions,即使 ID 本身看起来已经不言自明。

选择问题类型

选择与所需答案形态相匹配的类型。

  • Choice 适用于答案是已知选项集合之一、且选项之间没有顺序的情形:把工单路由到某个部门、分类文档类型、检测编程语言。给出完整的选项列表,并在列表可能无法覆盖所有输入时,添加一个 other 或 none of the above 选项。

  • Score 适用于答案落在一段连续区间上、且你能描述区间上每个点的含义的情形:缺陷严重程度、客户沮丧程度、技能水平。级别由你来定义,模型返回在这段级别上的一个位置。

  • Noul 适用于干净的是/否问题,且概率本身就是有用的信号:这条消息是否包含个人身份信息、客户是否在请求退款、简历是否提到了分布式系统。

用 Noul 做是/否判断,用 Score 衡量在一段连续区间上的位置。“这位候选人 Python 强吗?”需要先明确定义什么叫“强”。Noul 值为 0.5 表示模型给“是”和“否”相同的概率,并不表示候选人技能水平中等。定义不清会让这个概率难以解读。

如果你想衡量技能水平,请使用定义了级别的 Score,例如:没有经验、有一定了解、日常使用、深入专长。如果你需要的是是/否决策,就把条件定义清楚,例如“简历是否写明候选人在工作中使用过 Python?”

如果两种类型看起来都合适,优先选择答案能被你的代码直接处置的那种。在 refund、rebook 和 information 之间做选择的 Choice 可以直接对应三条代码路径。衡量客户沮丧程度的 Score 对应一个阈值。Noul 对应一个 if。

返回什么

答案也是原语。每种问题类型都返回一个类型化取值,你的代码可以对它进行比较、设阈值、排序、传入后续逻辑,或放入下一次请求的状态中(参见当一个问题依赖另一个问题时)。

类型答案字段如何解读
Choicechoice、probabilities、confidencechoice 是选中的选项。probabilities 是所有选项上的分布。confidence 概括该分布的峰值程度。
Scorescore、legend、probabilities、confidencescore 是在你的级别上的位置,可以落在两个级别之间。legend 按编号重复列出各级别。probabilities 是各级别上的分布。
Noulnoul答案为“是”的概率。接近 1 是强烈的“是”,接近 0 是强烈的“否”,接近 0.5 表示不确定。Noul 没有单独的 confidence。

这些答案有两个特性,使其可以组合:

  • 每个答案都被限制在你提供的选项之内。 模型返回的是你的选项或级别上的概率分布,绝不会返回它们之外的值。你的代码永远不必从生成的文字中解析出取值。
  • 每个答案都是独立的。 一个问题的答案不会成为另一个问题的隐藏上下文。你可以增加或删除问题,而不改变其他问题的结果。

置信度页面解释了 confidence 如何由 probabilities 推导而来,以及如何用它决定何时自动执行、何时上报给人工。

引用特定字段

被评估的内容,即状态,通常是一个包含多个部分的 JSON 对象:一段对话、一条记录、一份保单。当问题只针对其中某个部分时,在 instructions 中用带点和索引的路径指向它的键来指明该部分,并保留反引号。这样模型就知道要判断状态的哪个部分。

来看“状态”页面中的客服对话:

json
{
  "ticket": {
    "subject": "Duplicate charge",
    "messages": [
      {"from": "customer", "text": "I was charged twice for order A-104. Please refund the duplicate."},
      {"from": "support", "text": "We are checking the charges."}
    ]
  },
  "order": {
    "id": "A-104",
    "charges": [
      {"amount_usd": 49, "status": "captured"},
      {"amount_usd": 49, "status": "captured"}
    ]
  },
  "refund_policy": "Duplicate charges are eligible for a refund."
}

下面两个问题通过路径指向客户的消息、保单和扣款记录:

python
questions = {
    "refund_requested": {
        "type": "noul",
        "instructions": "Does `ticket.messages[0].text` request a refund?",
    },
    "policy_supports_refund": {
        "type": "noul",
        "instructions": (
            "Does `refund_policy` support the refund requested "
            "in `ticket.messages[0].text`, given `order.charges`?"
        ),
    },
}

显式路径清楚地表明,结构化状态中的哪些部分应当参与每项判断。关于如何组织输入,参见状态。

一起提出多个问题

把使用同一个状态的所有问题放进一次请求发送。你可以自由混用问题类型。System One 模型会并行评估请求中的每个问题。增加问题几乎不会改变响应时间,只会消耗这些额外问题的 token,而这很便宜。问一个可能用不上的问题几乎是免费的。

下面这个请求一次性完成对客户消息的分类、紧急程度判断和沮丧程度评分:

request
json
{
  "state": "Our API integration started returning 500 errors on every request about 20 minutes ago, and we can't process any customer orders until this is fixed.",
  "questions": {
    "department": {
      "type": "choice",
      "instructions": "Which team should handle this",
      "criteria": {
        "billing": "Payment or subscription issues",
        "technical": "Bugs or integration problems",
        "sales": "Pricing or account questions"
      }
    },
    "is_urgent": {
      "type": "noul",
      "instructions": "The message conveys urgency or time-sensitivity"
    },
    "frustration": {
      "type": "score",
      "instructions": "How frustrated the customer appears",
      "criteria": [
        "Calm, just stating facts",
        "Frustrated but civil",
        "Very angry, strong language"
      ]
    }
  }
}

我们的客户端 SDK 提供类型化的问题和答案。在 Python 中,把由 Choice、Noul 和 Score 对象组成的 questions 字典传给 client.system_one(...)。下面的请求只发送一次工单和退款政策,就为每个问题拿到一个类型化答案:

python
from typesafe_sdk import Choice, Noul, Score, TypeSafeClient

state = {
    "ticket_message": "My flight was cancelled. Can I get a refund?",
    "refund_policy": "Cancelled flights are eligible for a full refund.",
}

with TypeSafeClient() as client:
    response = client.system_one(
        state=state,
        questions={
            "refund_requested": Noul(
                instructions="Does `ticket_message` request a refund?",
            ),
            "request_type": Choice(
                instructions="What is the main request in `ticket_message`?",
                criteria={
                    "refund": "The customer wants money returned.",
                    "rebooking": "The customer wants a replacement flight.",
                    "information": "The customer is asking for information only.",
                },
            ),
            "frustration": Score(
                instructions="How frustrated does the customer appear in `ticket_message`?",
                criteria=[
                    "Calm and neutral.",
                    "Concerned but civil.",
                    "Very angry or using strong language.",
                ],
            ),
        },
    )

print(response.answers["refund_requested"].noul)
print(response.answers["request_type"].choice)
print(response.answers["frustration"].score)

关于各语言的安装与用法,参见客户端 SDK。

提出投机性问题

把代码可能用到的每个问题都问出来,包括那些答案只对部分输入有意义的,再让代码决定使用哪些答案。如果工单最终并不是缺陷报告,就忽略严重程度那个答案。我们称之为投机性扇出模式。并行问题实战手册展示了把 13 个问题批量合并为一次调用,比 13 次单独调用便宜 11.5 倍、快 9.6 倍,而答案毫无变化。

编程代理比人更容易养成“每次调用只问一个问题”的习惯。TypeSafe agent skill 会告诉你的代理在每次调用中放入大量问题,包括那些只对部分输入有意义的问题。

把复杂判断拆分为多个问题

取决于多个因素的判断,最好拆成每个因素一个问题。在代码中组合答案,并按相对重要性给每个答案一个权重。权重由你决定。当组合结果与你团队的判断不符时,在代码里修改权重再跑一次。由于问题在同一个请求内并行运行,增加问题几乎不会改变响应时间。拆分只多花一点额外的问题 token。

例如,工单优先级可以由三个 Score 问题构成:缺陷有多严重、客户有多沮丧、报告给工程师提供了多少可用的信息。Score 页面在把复杂判断拆分为多个 Score中逐步讲解了这个请求以及对答案做归一化和加权的代码。这种技巧称为组合评分模式。

当一个问题依赖另一个问题时

同一请求中的问题是相互独立的:一个答案不会成为另一个问题的上下文。如果后面的判断依赖前面的答案,就在代码中发起第二次请求。只有当你的代码在拿到第一个答案之前无法构造第二个请求时,这种依赖才是真实存在的:比如需要用答案为状态获取更多数据、决定状态由什么构成,或选择下一个问题的选项。否则,就把问题放在一起提问,然后在代码中组合答案。

两次请求是例外而非常规。如果第二个请求的问题本来可以针对原始状态提问,就把它们放进第一个请求,让代码忽略用不上的答案。有三篇实战手册是出于真实原因发起第二次请求的。技能推荐在一次请求中对 182 个技能排序,然后取前三个的完整文本,用更好的证据再次评判它们。结构恢复先询问每个换行是否截断了一个句子,再根据这些答案把多行合并成块,然后对这些块进行分类——在第一次请求回答之前,这些块并不存在。层级分类用每个 Choice 答案来决定下一次请求提供哪些选项。

关于如何把工作流拆解为聚焦的判断,参见如何用 TypeSafe 构建。

下一步

想了解它们如何组合成系统架构,请前往模式。