文档导航

原语(问题)

原语(问题)

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 则是对「是」和「否」的可选描述。每种问题类型各自的页面会讲它的形状。

下面这个问题问的是客户是否要求退款:

from typesafe_sdk import Noul

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

选择问题类型

挑那个和你需要的答案形状匹配的类型。

  • Choice 适合答案是一组已知选项之一、且选项之间没有顺序的情况:把工单路由到某个部门、给文档类型分类、识别编程语言。给出完整的选项列表;当列表未必能覆盖所有输入时,加上一个 other 或 none of the above 选项。

  • Score 适合答案落在一个谱系上、且你能描述谱系上每个点分别代表什么的情况:bug 严重程度、客户挫败感、技能水平。档位由你定义,模型返回一个落在这条谱系上的位置。

  • Noul 适合一个干净的是非题,其中概率本身就是有用的信号:这条消息是否包含个人身份信息、客户是否在要求退款、简历里是否提到分布式系统。

如果两种类型看起来都合适,就选那个答案能直接被你的代码所用的。在 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 里用点和下标的路径写出它的键名,记得带上反引号。这样模型就知道该判断状态里的哪一部分。

拿状态页面里那段客服对话举例:

{
  "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."
}

下面两个问题用路径指向客户消息、政策和扣款记录:

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
{
  "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(...)。下面这次请求把一张工单和一份退款政策发一次,就能拿到每个问题的类型化答案:

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。

提一些推测性的问题

把代码可能需要的每个问题都问出来,包括那些只对部分输入才有意义的,再让代码决定用哪些答案。如果一张工单最后发现不是 bug 报告,忽略严重程度那个答案就是了。我们把这叫做推测式扇出模式。并行提问 cookbook展示了把 13 个问题并进一次调用,比 13 次单独调用便宜 11.5 倍、快 9.6 倍,而答案没有任何变化。

把复合判断拆成几个问题

一个依赖好几件事的判断,最好拆成每件事一个问题。在代码里组合这些答案,并按相对重要性给每个答案一个权重。权重由你定。当组合结果和你们团队的判断不一致时,就在代码里改权重再跑一遍。增加问题几乎不改变响应时间,因为它们在同一次请求里并行执行。拆分只多花几个问题的 token。

比如,工单优先级可以由三个 Score 问题搭出来:bug 有多严重、客户有多不满、报告给了工程师多少可用信息。Score 那一页在把复合判断拆成几个 Score一节里完整演示了这次请求,以及把答案归一化并加权的那段代码。这个技巧叫做组合评分模式。

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

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

两次请求是例外,不是惯例。如果第二次请求里的问题本来可以对原始状态提出,那就放在第一次请求里问,再让代码忽略用不到的那些。有三个 cookbook 出于真正必要的理由才发第二次请求。技能建议在一次请求里给 182 个技能排序,然后取回前三名的全文,再拿这份更好的证据重新判断它们。结构还原先问每个换行是否切断了句子,再根据这些答案把行合并成块,然后给这些块分类 —— 而这些块在第一次请求给出答案之前根本不存在。层级分类用每个 Choice 答案来决定下一次请求提供哪些选项。

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

后续步骤

Choice

从固定列表里挑一个选项。

Score

按有序档位给状态评分。

Noul

得到某个陈述为真的概率。

想看它们如何组合成系统架构,去架构模式。