TS TypeSafe 文档中文版 原文 ↗

原语(问题)

三种 TypeSafe 问题类型(Choice、Score、Noul)、它们返回的有类型答案、如何在它们之间做选择,以及如何一次提出多个问题。

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。

返回什么

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

类型 答案字段 如何解读
Choice choice, probabilities, confidence choice 是被选中的选项。probabilities 是在所有选项上的分布。confidence 概括该分布的尖峰程度。
Score score, legend, probabilities, confidence score 是沿你定义的等级所处的位置,可能落在两个等级之间。legend 按编号复述这些等级。probabilities 是在各等级上的分布。
Noul noul 答案为「是」的概率。接近 1 是很强的「是」,接近 0 是很强的「否」,接近 0.5 则不确定。Noul 没有单独的 confidence。

这些答案有两点性质使它们可以组合:

  • 每个答案都被约束在你提供的选项之内。 模型返回的是在你的选项或等级之上的概率分布,绝不会是它们之外的值。你的代码永远不需要从生成的散文里恢复出一个值。
  • 每个答案都是独立的。 一个问题的答案不会成为另一个问题的隐藏上下文。你可以增删问题,而不改变其他问题的结果。

置信度 解释了 confidence 如何由 probabilities 推导出来,以及如何用它来决定何时自动行动、何时升级给人工。

引用具体字段

被求值的内容,即 状态,往往是一个由若干部分组成的 JSON 对象:一段对话、一条记录、一份策略。当某个问题针对其中某一部分时,在 instructions 里用一个带点号和索引的路径指向它的键,并保留反引号。这样模型就知道该判断状态的哪一部分。

以 State 页面中的客服对话为例:

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`?"
        ),
    },
}
 

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

一次提出多个问题

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

下面这次请求一次性对客户消息分类、检查紧迫性并给沮丧程度打分:

request
{
  "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"
      ]
    }
  }
}

这个示例是可交互的;到官网原页面可以直接在 Playground 里运行。

我们的 客户端 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 智能体技能 会告诉你的智能体在每次调用中放入多个问题,包括那些只对部分输入才有意义的问题。

把复杂判断拆成多个问题

一项取决于若干因素的判断,最好按因素拆成每个因素一个问题。在代码中组合答案,按相对重要性给每个答案一个权重。权重由你决定。当组合结果与你团队的判断不一致时,在代码里改动权重再跑一次。增加问题几乎不改变响应时间,因为它们在同一次请求内并行运行。这种拆分只多花几个问题所需的 token。

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

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

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

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

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

后续步骤

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