文档目录
怎样用 TypeSafe 构建
通过让代码掌握控制权,并交给 System One 做出窄而结构化的判断,来设计 AI 驱动的软件。
非官方中文翻译 · 内容以 英文原文 ↗ 为准 · 同步于 2026-10-15
System One 是 TypeSafe 用来构建 AI 驱动软件的模型——而不是智能体。它不生成代码,也不自己选择下一步动作。它提供可嵌入软件的 AI 题型,让代码始终掌握控制权,同时由模型在非结构化数据上完成常识判断。
三种软件架构
TypeSafe 面向构建 AI 驱动的软件 而设计:代码掌握工作流,AI 处理窄而结构化的判断。
传统软件
传统代码是由简单的软件原语组成的复杂决策树。由于每个原语都很可靠,开发者可以把它们组合成更高层的抽象。
LLM 智能体
智能体处理指令并自主选择下一步。当有人在监控流程时,这种方式很有效,但每多一轮循环就多一次跑偏的可能。
AI 驱动的软件
由代码处理确定性的工作并掌握控制流。模型只在系统需要可编程的常识或需要解读非结构化数据时出现。每个 AI 任务都保持原子化并且受限。
System One 可组合的原因
- 结构化:System One 在构造上就是类型安全的。判断结果和概率与代码所期望的结构化软件类型和 JSON schema 完全匹配,因此永远不需要从生成的文字里反推出值。
- 并行:问题相互独立并并行求值。一个原语的结果不会变成隐藏的上下文,去影响另一个原语的结果。
- 可比较:输出可排序,可以驱动智能的
if判断、阈值和比较。 - 快速:多数查询在 100 ms 左右完成。System One 足够快,可以跑在实时请求路径和用户界面上。
- 校准过的置信度:RLCD 通过校准过的概率传达不确定性,而不是倾向于过度自信。
- 自一致:System One 设计上能在重复求值时给出稳定的答案。参见 自一致实战示例。
由于每个输出都被限制在给定的选项范围内,模型返回的是这些选项上的完整概率分布,而不是在 schema 之外自己造一个值。TypeSafe 的目标是实现超过 100 倍的「智能/速度 + 成本」比;其底层押注是,更便宜的智能会催生出远大于当前的巨大需求。
设计一个 System One 工作流
能写代码时就写代码
把确定性的工作保留在代码中。它既可靠又便宜。如果一段软件工作流就能表达相同的行为,就不要用智能体的
while循环。示例:把确定性的规则保留在代码中
days_overdue = (today - invoice.due_date).days if days_overdue > 30: route_to_collections(invoice)浏览 System One 设计模式,了解把模型判断与代码组合的有界方式。
拆分输入 state
只包含与当前问题相关的上下文。这能帮助模型避免分心和上下文腐烂。当自有知识库能提供最新信息时,不要依赖存储在模型权重里的知识。
示例:只发送相关的上下文
request
{ "model": "jev-latest", "state": { "ticket_message": "My flight was cancelled. Can I get a refund?", "refund_policy": "Cancelled flights are eligible for a full refund." }, "questions": { "policy_supports_refund": { "type": "noul", "instructions": "Does the refund policy support the refund requested in the ticket?" } } }在输入 state 中使用结构
对
state和questions字段使用嵌套的 JSON。当指向具体的值能消除歧义时,让问题指向具体的值,并在问题内用反引号把路径括起来。示例:引用一个嵌套值
用反引号包裹的点与下标路径,让问题指向具体的嵌套值,例如
support.tickets[0].message。request
{ "model": "jev-latest", "state": { "support": { "tickets": [ { "message": "I was charged twice for order A-104." }, { "message": "How do I reset my password?" } ] }, "commerce": { "orders": [ { "id": "A-104", "charges": [ { "amount_usd": 49, "status": "captured" }, { "amount_usd": 49, "status": "captured" } ] } ] }, "account": { "security": { "password_reset": "Email a reset link to the address on file." } } }, "questions": { "duplicate_charge": { "type": "noul", "instructions": "Do `support.tickets[0].message` and `commerce.orders[0].charges` indicate a duplicate charge?" }, "password_reset_supported": { "type": "noul", "instructions": "Can `account.security.password_reset` resolve the request in `support.tickets[1].message`?" } } }拆分问题
尽可能问最明确、最窄、最具体、最原子化的问题。把复杂或定义不清的问题拆成多个问题,每个问题只评估一个属性。
示例:拆分垃圾邮件识别
一个宽泛的问题(不好的写法)
{ "model": "jev-latest", "state": { "message": { "sender": { "display_name": "Acme Payroll", "email": "rewards@claim-bonus.example" }, "subject": "Urgent: claim your employee bonus", "body": "You have been selected for a $1,000 bonus. Confirm your payroll password today to receive it.", "links": [ { "text": "Claim bonus", "url": "http://claim-bonus.example/acme" } ] } }, "questions": { "is_spam": { "type": "noul", "instructions": "Is `message` spam?" } } }拆分后的问题(好的写法)
{ "model": "jev-latest", "state": { "message": { "sender": { "display_name": "Acme Payroll", "email": "rewards@claim-bonus.example" }, "subject": "Urgent: claim your employee bonus", "body": "You have been selected for a $1,000 bonus. Confirm your payroll password today to receive it.", "links": [ { "text": "Claim bonus", "url": "http://claim-bonus.example/acme" } ] } }, "questions": { "requests_credentials": { "type": "noul", "instructions": "Does `message.body` ask the recipient to provide a password or other login credential?" }, "offers_unexpected_reward": { "type": "noul", "instructions": "Does `message.body` claim the recipient received an unexpected prize, payment, or reward?" }, "creates_time_pressure": { "type": "noul", "instructions": "Does `message.subject` or `message.body` pressure the recipient to act quickly?" }, "sender_identity_mismatch": { "type": "noul", "instructions": "Does the organization named in `message.sender.display_name` conflict with the domain in `message.sender.email`?" }, "link_domain_mismatch": { "type": "noul", "instructions": "Does the domain in `message.links[0].url` conflict with the organization named in `message.sender.display_name`?" }, "disguises_link_destination": { "type": "noul", "instructions": "Does `message.links[0].text` conceal or misrepresent the destination in `message.links[0].url`?" } } }示例:核对一次工具调用 trace
一个宽泛的问题(不好的写法)
{ "model": "jev-latest", "state": { "request": { "text": "What's the weather in Seattle tomorrow in Fahrenheit?", "location": "Seattle, WA", "date": "2026-09-03", "unit": "fahrenheit" }, "available_tools": { "geocode_city": { "description": "Resolve a city to latitude and longitude.", "parameters": { "city": "string" } }, "get_weather": { "description": "Get the forecast for coordinates and a date.", "parameters": { "latitude": "number", "longitude": "number", "date": "YYYY-MM-DD", "unit": [ "fahrenheit", "celsius" ] } } }, "trace": { "tool_calls": [ { "id": "call_1", "name": "geocode_city", "arguments": { "city": "Seattle, WA" } }, { "id": "call_2", "name": "get_weather", "arguments": { "latitude": 47.6062, "longitude": -122.3321, "date": "2026-09-03", "unit": "celsius" } } ], "tool_results": [ { "tool_call_id": "call_1", "output": { "latitude": 47.6062, "longitude": -122.3321 } } ] } }, "questions": { "tool_calls_are_correct": { "type": "noul", "instructions": "Is `trace.tool_calls` correct for `request` and `available_tools`?" } } }拆分后的问题(好的写法)
{ "model": "jev-latest", "state": { "request": { "text": "What's the weather in Seattle tomorrow in Fahrenheit?", "location": "Seattle, WA", "date": "2026-09-03", "unit": "fahrenheit" }, "available_tools": { "geocode_city": { "description": "Resolve a city to latitude and longitude.", "parameters": { "city": "string" } }, "get_weather": { "description": "Get the forecast for coordinates and a date.", "parameters": { "latitude": "number", "longitude": "number", "date": "YYYY-MM-DD", "unit": [ "fahrenheit", "celsius" ] } } }, "trace": { "tool_calls": [ { "id": "call_1", "name": "geocode_city", "arguments": { "city": "Seattle, WA" } }, { "id": "call_2", "name": "get_weather", "arguments": { "latitude": 47.6062, "longitude": -122.3321, "date": "2026-09-03", "unit": "celsius" } } ], "tool_results": [ { "tool_call_id": "call_1", "output": { "latitude": 47.6062, "longitude": -122.3321 } } ] } }, "questions": { "geocode_tool_is_relevant": { "type": "noul", "instructions": "Is `trace.tool_calls[0].name` an appropriate tool for resolving `request.location`?" }, "geocode_location_matches": { "type": "noul", "instructions": "Does `trace.tool_calls[0].arguments.city` match `request.location`?" }, "geocode_arguments_match_schema": { "type": "noul", "instructions": "Does `trace.tool_calls[0].arguments` conform to `available_tools.geocode_city.parameters`?" }, "geocode_result_matches_call": { "type": "noul", "instructions": "Does `trace.tool_results[0].tool_call_id` match `trace.tool_calls[0].id`?" }, "weather_tool_is_relevant": { "type": "noul", "instructions": "Is `trace.tool_calls[1].name` an appropriate tool for answering `request.text`?" }, "weather_arguments_match_schema": { "type": "noul", "instructions": "Does `trace.tool_calls[1].arguments` conform to `available_tools.get_weather.parameters`?" }, "weather_uses_geocoded_coordinates": { "type": "noul", "instructions": "Do the coordinates in `trace.tool_calls[1].arguments` match those in `trace.tool_results[0].output`?" }, "weather_date_matches": { "type": "noul", "instructions": "Does `trace.tool_calls[1].arguments.date` match `request.date`?" }, "weather_unit_matches": { "type": "noul", "instructions": "Does `trace.tool_calls[1].arguments.unit` match `request.unit`?" } } }在问题中使用结构
问题要简短。
instructions和criteria通常是字符串,对于一个简短且不含歧义的问题,一个字符串就够用了。它们也可以是对象或数组。把问题放在一个字段里,把引导问题的数据放在其他字段里。在以下场景下,结构化能帮上忙:
- 问题需要上下文或示例。一长段背景信息或一组示例输入应该放在问题旁边的具名字段里,方便你的代码往里追加或替换,而无需重写问题本身。
- 问题的某一部分来自你的代码。当一个值来自数据库时,把它放在自己的字段里,而不是拼接到字符串模板里。
- 多个问题有类似的
instructions。一次请求只有一个 state,但可以包含多个问题。补充数据有助于让问题彼此区分开来。
示例:引用来自代码的一条记录
这个 Noul 把 state 中的简历与来自候选人数据库的一条记录进行比较。该记录原样放入
potential_duplicate,问题通过名字引用它。questions
{ "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": "John Smith", "location": "Oakland, California", "last_employer": "Google" }, "question": "Is the resume for the same person as `potential_duplicate`?" } } } }来自代码的
potential_duplicate数据会随时间变化。question用反引号引用它。criteria内部的描述同样可以是对象。对于 Choice,每个选项的描述可以是一个对象,说明该选项覆盖什么、不覆盖什么,并给出几个示例。跨选项使用相同的字段名,让模型可以直接进行比较。示例:定义对比式的 Choice criteria
questions
{ "model": "jev-latest", "state": "How many disposable virtual cards can I make per day?", "questions": { "card_help_topic": { "type": "choice", "instructions": { "question": "Which disposable virtual card topic is the user asking about?", "focus": "Classify the information the user wants." }, "criteria": { "get_disposable_virtual_card": { "what": "Purpose, eligibility, or setup", "not_for": "Quantity, transaction, or merchant restrictions", "examples": [ "How can I get a disposable virtual card?", "What are disposable cards for?" ] }, "disposable_card_limits": { "what": "Quantity, transaction, or merchant restrictions", "not_for": "Purpose, eligibility, or setup", "examples": [ "How many disposable cards can I make per day?", "Where can I use a disposable card?" ] } } } } }每种题型的页面都有一个完整示例:
- Noul 把一份简历与多条候选人记录对比,每条记录一个问题,问题在代码中构建。
- Choice 用「覆盖什么 / 不覆盖什么 / 示例」描述两个容易混淆的选项。
- Score 为每个档位给出描述和示例情境。
结构化数据提取级联实战示例 展示了「共享措辞」的情况:对抽取记录里的每个字段问同一组问题。
简短且不含歧义的问题或判定标准可以继续用字符串。当结构能拆分原本会混在一起的指引时,就加入结构。完整可接受结构的位置集合见 高级:结构。
多问几个问题
在同一次请求里围绕同一个 state 大量问出窄而独立的问题。这就是你用 API 把「每美元的有效性和智能」最大化的方式:问题并行运行,代码可以在不增加串行模型往返的前提下组合它们的信号。
在代码中合并问题的输出(或送入经典的 ML 模型)
用确定性的规则或加权和把独立的答案组合起来。如果希望学到更复杂的组合方式,可以把这些概率作为下游经典机器学习模型的特征。
示例:用加权和组合信号
answers = response.answers # Combine independent signals into one application-specific score. quality = ( 0.4 * answers["answers_request"].noul + 0.4 * answers["citations_are_supported"].noul + 0.2 * (1 - answers["contradicts_context"].noul) )组合打分 展示了在合并的同时保留各个独立判断的方法。如果你没有用于下游模型的标签,可以用一组昂贵的推理模型来生成标签;AutoResearch 实战示例 展示了如何基于 System One 的输出训练一个经典模型。
根据不确定性进行路由
让代码对高置信和低置信的答案采取不同的动作。把不确定的用例升级到人工,或者交给一个更昂贵的推理模型。通过在你自己的数据上绘制置信度与准确率的关系曲线来测试阈值。
示例:按置信度路由
answer = response.answers["card_help_topic"] if answer.confidence < 0.8: route_to_human_review(ticket) else: route_to_handler(answer.choice, ticket)
整合到一起
这个客服工单工作流把确定性的工作保留在代码中,只发送相关的结构化上下文,在一次请求中评估多个原子问题,并通过显式的置信度阈值来组合答案。
from typesafe_sdk import Choice, Noul, NoulCriteria, Score, TypeSafeClient
def triage_ticket(ticket, customer):
# Handle deterministic states without calling a model.
if ticket["status"] == "closed":
return "no_action"
open_orders = [
order for order in customer["orders"] if order["status"] != "delivered"
]
# Include only the structured context needed by the questions below.
state = {
"ticket": {
"message": ticket["message"],
"sender": ticket["sender"],
"links": ticket["links"],
},
"customer": {
"plan": customer["plan"],
"open_orders": open_orders,
},
"policy": {
"sensitive_credentials": ["password", "security code", "API key"],
},
}
# Ask structured, atomic questions together so they run in parallel.
questions = {
"topic": Choice(
instructions={
"question": "Which team should handle `ticket.message`?",
"focus": "Classify the customer's primary request.",
},
criteria={
"billing": {
"what": "Charges, invoices, refunds, or subscriptions",
"not_for": "Order tracking or account access",
"examples": ["I was charged twice", "Where is my refund?"],
},
"orders": {
"what": "Order status, delivery, cancellation, or returns",
"not_for": "Charges or account access",
"examples": ["Where is my order?", "Cancel my shipment"],
},
"account": {
"what": "Login, profile, permissions, or security",
"not_for": "Charges or order tracking",
"examples": ["Reset my password", "I cannot sign in"],
},
},
),
"requests_credentials": Noul(
instructions={
"question": "Does the message request a sensitive credential?",
"compare": [
"`ticket.message`",
"`policy.sensitive_credentials`",
],
"focus": "Look for a request to disclose the credential itself.",
},
criteria=NoulCriteria(
true={
"what": "Asks the recipient to disclose a listed credential",
"examples": [
"Reply with your password",
"Send us your API key",
],
},
false={
"what": "Does not ask the recipient to disclose a credential",
"not_for": "A legitimate instruction to reset a credential",
"examples": ["Use this link to reset your password"],
},
),
),
"sender_identity_mismatch": Noul(
instructions={
"question": "Does the claimed sender identity conflict with its domain?",
"compare": [
"`ticket.sender.display_name`",
"`ticket.sender.email`",
],
"focus": "Compare the named organization with the email domain.",
},
criteria=NoulCriteria(
true={
"what": "Claims an organization unrelated to the email domain",
"examples": ["Acme Payroll sent from claim-bonus.example"],
},
false={
"what": "The identity and domain agree or make no conflicting claim",
"examples": ["Acme Payroll sent from acme.example"],
},
),
),
"unexpected_reward": Noul(
instructions={
"question": "Does the message announce an unexpected reward?",
"inspect": "`ticket.message`",
"focus": "Look for an unsolicited prize, payment, or reward claim.",
},
criteria=NoulCriteria(
true={
"what": "Announces an unrequested prize, payment, or reward",
"examples": ["You were selected for a $1,000 bonus"],
},
false={
"what": "Contains no reward claim or discusses an expected payment",
"not_for": "A customer asking about a known refund or payroll deposit",
"examples": ["When will my approved refund arrive?"],
},
),
),
"refund_requested": Noul(
instructions={
"question": "Does the customer explicitly request a refund or credit?",
"inspect": "`ticket.message`",
"focus": "Require a requested remedy, not a billing complaint alone.",
},
criteria=NoulCriteria(
true={
"what": "Directly asks for money back or an account credit",
"examples": ["Please refund the duplicate charge"],
},
false={
"what": "Does not ask for a refund or credit",
"not_for": "A complaint or billing question without a requested remedy",
"examples": ["Why was I charged twice?"],
},
),
),
"mentions_open_order": Noul(
instructions={
"question": "Does the message refer to a supplied open order?",
"compare": [
"`ticket.message`",
"`customer.open_orders`",
],
"focus": "Match an order id or other identifying details.",
},
criteria=NoulCriteria(
true={
"what": "Refers to an open order by id or identifying details",
"examples": ["Where is order A-104?"],
},
false={
"what": "Does not identify any supplied open order",
"not_for": "A generic order question with no matching details",
"examples": ["How long does shipping usually take?"],
},
),
),
"frustration": Score(
instructions={
"question": "How frustrated does the customer appear?",
"inspect": "`ticket.message`",
"focus": "Judge expressed frustration, not issue severity.",
},
criteria=[
{
"what": "Calm and matter-of-fact",
"signals": ["Neutral wording", "No complaint about the experience"],
},
{
"what": "Frustrated but civil",
"signals": ["Expresses annoyance", "Remains constructive"],
},
{
"what": "Very angry or threatening to leave",
"signals": ["Hostile language", "Threatens cancellation or churn"],
},
],
),
}
with TypeSafeClient() as client:
response = client.system_one(
state=state,
questions=questions,
)
# Compose independent spam signals with weights controlled by code.
answers = response.answers
spam_risk = (
0.45 * answers["requests_credentials"].noul
+ 0.30 * answers["sender_identity_mismatch"].noul
+ 0.25 * answers["unexpected_reward"].noul
)
# Escalate uncertain judgments instead of guessing.
spam_is_uncertain = 0.4 < spam_risk < 0.6
if spam_is_uncertain or answers["topic"].confidence < 0.75:
return route_to_human_review(ticket)
if spam_risk >= 0.6:
return quarantine_as_spam(ticket)
# Let code decide which speculative answers matter on this path.
if answers["topic"].choice == "billing":
return route_to_billing(
ticket,
refund_requested=answers["refund_requested"].noul >= 0.7,
)
if answers["topic"].choice == "orders":
return route_to_orders(
ticket,
mentions_open_order=answers["mentions_open_order"].noul >= 0.7,
)
priority = (
"high"
if answers["frustration"].confidence >= 0.7
and answers["frustration"].score >= 1.5
else "normal"
)
return route_to_account_support(ticket, priority=priority)本文档由 Mintlify 构建并托管,Mintlify 是一个开发者文档平台。