文档目录

怎样用 TypeSafe 构建

通过让代码掌握控制权,并交给 System One 做出窄而结构化的判断,来设计 AI 驱动的软件。

非官方中文翻译 · 内容以 英文原文 ↗ 为准 · 同步于 2026-10-15

System One 是 TypeSafe 用来构建 AI 驱动软件的模型——而不是智能体。它不生成代码,也不自己选择下一步动作。它提供可嵌入软件的 AI 题型,让代码始终掌握控制权,同时由模型在非结构化数据上完成常识判断。

三种软件架构

TypeSafe 面向构建 AI 驱动的软件 而设计:代码掌握工作流,AI 处理窄而结构化的判断。

传统软件

传统代码是由简单的软件原语组成的复杂决策树。由于每个原语都很可靠,开发者可以把它们组合成更高层的抽象。

LLM 智能体

智能体处理指令并自主选择下一步。当有人在监控流程时,这种方式很有效,但每多一轮循环就多一次跑偏的可能。

AI 驱动的软件

由代码处理确定性的工作并掌握控制流。模型只在系统需要可编程的常识或需要解读非结构化数据时出现。每个 AI 任务都保持原子化并且受限。

传统软件、智能体和 AI 驱动的软件展示为三种不同的系统架构。 传统软件、智能体和 AI 驱动的软件展示为三种不同的系统架构。

System One 可组合的原因

  • 结构化:System One 在构造上就是类型安全的。判断结果和概率与代码所期望的结构化软件类型和 JSON schema 完全匹配,因此永远不需要从生成的文字里反推出值。
  • 并行:问题相互独立并并行求值。一个原语的结果不会变成隐藏的上下文,去影响另一个原语的结果。
  • 可比较:输出可排序,可以驱动智能的 if 判断、阈值和比较。
  • 快速:多数查询在 100 ms 左右完成。System One 足够快,可以跑在实时请求路径和用户界面上。
  • 校准过的置信度:RLCD 通过校准过的概率传达不确定性,而不是倾向于过度自信。
  • 自一致:System One 设计上能在重复求值时给出稳定的答案。参见 自一致实战示例。

由于每个输出都被限制在给定的选项范围内,模型返回的是这些选项上的完整概率分布,而不是在 schema 之外自己造一个值。TypeSafe 的目标是实现超过 100 倍的「智能/速度 + 成本」比;其底层押注是,更便宜的智能会催生出远大于当前的巨大需求。

设计一个 System One 工作流

  1. 能写代码时就写代码

    把确定性的工作保留在代码中。它既可靠又便宜。如果一段软件工作流就能表达相同的行为,就不要用智能体的 while 循环。

    示例:把确定性的规则保留在代码中

    python
    days_overdue = (today - invoice.due_date).days
    
    if days_overdue > 30:
        route_to_collections(invoice)

    浏览 System One 设计模式,了解把模型判断与代码组合的有界方式。

  2. 拆分输入 state

    只包含与当前问题相关的上下文。这能帮助模型避免分心和上下文腐烂。当自有知识库能提供最新信息时,不要依赖存储在模型权重里的知识。

    示例:只发送相关的上下文

    request

    json
    {
      "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?"
        }
      }
    }
  3. 在输入 state 中使用结构

    对 state 和 questions 字段使用嵌套的 JSON。当指向具体的值能消除歧义时,让问题指向具体的值,并在问题内用反引号把路径括起来。

    示例:引用一个嵌套值

    用反引号包裹的点与下标路径,让问题指向具体的嵌套值,例如 support.tickets[0].message。

    request

    json
    {
      "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`?"
        }
      }
    }
  4. 拆分问题

    尽可能问最明确、最窄、最具体、最原子化的问题。把复杂或定义不清的问题拆成多个问题,每个问题只评估一个属性。

    示例:拆分垃圾邮件识别

    一个宽泛的问题(不好的写法)

    json
    {
      "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?"
        }
      }
    }

    拆分后的问题(好的写法)

    json
    {
      "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

    一个宽泛的问题(不好的写法)

    json
    {
      "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`?"
        }
      }
    }

    拆分后的问题(好的写法)

    json
    {
      "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`?"
        }
      }
    }
  5. 在问题中使用结构

    问题要简短。instructions 和 criteria 通常是字符串,对于一个简短且不含歧义的问题,一个字符串就够用了。它们也可以是对象或数组。把问题放在一个字段里,把引导问题的数据放在其他字段里。

    在以下场景下,结构化能帮上忙:

    • 问题需要上下文或示例。一长段背景信息或一组示例输入应该放在问题旁边的具名字段里,方便你的代码往里追加或替换,而无需重写问题本身。
    • 问题的某一部分来自你的代码。当一个值来自数据库时,把它放在自己的字段里,而不是拼接到字符串模板里。
    • 多个问题有类似的 instructions。一次请求只有一个 state,但可以包含多个问题。补充数据有助于让问题彼此区分开来。

    示例:引用来自代码的一条记录

    这个 Noul 把 state 中的简历与来自候选人数据库的一条记录进行比较。该记录原样放入 potential_duplicate,问题通过名字引用它。

    questions

    json
    {
      "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

    json
    {
      "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 为每个档位给出描述和示例情境。

    结构化数据提取级联实战示例 展示了「共享措辞」的情况:对抽取记录里的每个字段问同一组问题。

    简短且不含歧义的问题或判定标准可以继续用字符串。当结构能拆分原本会混在一起的指引时,就加入结构。完整可接受结构的位置集合见 高级:结构。

  6. 多问几个问题

    在同一次请求里围绕同一个 state 大量问出窄而独立的问题。这就是你用 API 把「每美元的有效性和智能」最大化的方式:问题并行运行,代码可以在不增加串行模型往返的前提下组合它们的信号。

    参见 推测式扇出(speculative fan-out)设计模式 和 并行问题实战示例。

  7. 在代码中合并问题的输出(或送入经典的 ML 模型)

    用确定性的规则或加权和把独立的答案组合起来。如果希望学到更复杂的组合方式,可以把这些概率作为下游经典机器学习模型的特征。

    示例:用加权和组合信号

    python
    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 的输出训练一个经典模型。

  8. 根据不确定性进行路由

    让代码对高置信和低置信的答案采取不同的动作。把不确定的用例升级到人工,或者交给一个更昂贵的推理模型。通过在你自己的数据上绘制置信度与准确率的关系曲线来测试阈值。

    示例:按置信度路由

    python
    answer = response.answers["card_help_topic"]
    
    if answer.confidence < 0.8:
        route_to_human_review(ticket)
    else:
        route_to_handler(answer.choice, ticket)

    参见 置信度 和 置信度分流,了解如何选择阈值,以及如何让阈值与每种动作的风险相匹配。

整合到一起

这个客服工单工作流把确定性的工作保留在代码中,只发送相关的结构化上下文,在一次请求中评估多个原子问题,并通过显式的置信度阈值来组合答案。

python
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 是一个开发者文档平台。