
OpenAI Decisions API 教程:用 predicate、choice、score 三种决策类型构建判定型 Agent
OpenAI Decisions API 教程:用 predicate、choice、score 三种决策类型构建判定型 Agent
Decisions API 是 OpenAI 面向「判定」场景提供的接口,通过 /v1/decisions 端点让模型返回概率、选项或评分,而不是开放式文本。本文介绍它的三种问题类型(predicate、choice、score)、文本与图像输入方式,并给出从环境准备到完整可运行示例的操作步骤,适合需要做自动审核、分类与评分打分的开发者参考。
很多 AI Agent 真正要做的不是「写一段话」,而是「下一个判断」:这条客户投诉要不要转人工、这条咨询该分给哪个部门、司机提交的送达照片算不算有效凭证。这类判断一旦出错,代价并不对称——误判为「需要升级」会白白消耗人工,漏判则会让真正的问题溜过去。过去开发者要在 LLM 外面自己搭一套分类规则、置信度阈值和评测流水线,才能把开放式输出收敛成可用的判定结果。
OpenAI 的 Decisions API 把这件事收进了接口本身:调用方不再拿一段自由文本,而是先声明「要评估什么」,再指定模型该返回一个概率、从预设选项里挑一个,还是按有序等级打分。它同时支持文本与图像输入。本文按实际操作顺序讲清这套接口的用法,覆盖三种决策类型,并给出一个从头到尾能跑通的最小示例。
准备工作
账号与密钥
使用前需要一个 OpenAI 平台的 API Key。密钥在 OpenAI 平台的账号后台生成,属于敏感凭据,不要写进代码仓库或 Notebook 的明文单元格里。示例中通过 getpass 在运行时输入,避免密钥落盘。
Python 环境与 SDK 版本
Decisions API 需要较新的 OpenAI Python SDK 才能调用,示例使用 3.26.0 或更高版本。在 Notebook 或虚拟环境里安装:
%pip install openai==3.26.0
如果本机已经装过旧版本,建议先确认实际生效的版本,避免出现「代码写法正确但客户端没有 decisions 属性」的情况。
运行环境
示例在 Jupyter Notebook 中运行,因为要展示逐步输出。用普通 Python 脚本同样可以,只需把 %pip 换成命令行安装、把 getpass 换成环境变量读取即可。图像示例还需要准备一张本地图片文件。
导入与客户端初始化
先导入所需模块并创建客户端。图像输入需要把本地文件转成 base64 数据 URL,因此还要用到 base64 与 pathlib:
from getpass import getpass
from openai import OpenAI
openai_api_key = getpass("OpenAI API key: ")
import base64
from pathlib import Path
client = OpenAI(api_key=openai_api_key)
客户端初始化完成后即可调用 client.decisions.create(...)。这个调用对应的是 POST /v1/decisions 端点。
操作步骤
第一步:理解请求的三个核心参数
一次决策请求主要由三个参数构成:
- model:要使用的模型。当前 Decisions API 只支持最新的
gpt-6-luna,其他模型(例如gpt-6-astra)暂不支持用于决策请求。 - input:待评估的内容,可以是纯文本字符串,也可以是包含文本与图像的消息数组。
- questions:声明要评估什么。它的作用类似系统提示词,但结构是固定的——必须为每个问题指定
type,并给出允许的选项集合或阈值说明。
把「要评估什么」从自由提示词改成结构化声明,是这套接口与通用对话接口最本质的区别:模型不再自由发挥,而是在你给定的判定空间里给出结果。
第二步:选择问题类型
questions 里的 type 决定返回结果的形态,共有三种:
| 类型 | 用途 | 返回内容 |
|---|---|---|
predicate |
判断某个条件是否成立 | 一个概率值 |
choice |
从无序的预设选项中选一个 | 每个选项的概率,以及最终选项与置信度 |
score |
按有序等级打分 | 每个等级的概率,以及加权平均分 |
选择依据很简单:条件成立与否用 predicate;选项之间没有高低之分(体育、监管、科技、市场)用 choice;选项本身有顺序(Incorrect、Partial、Correct,或 S、A、B、C)用 score。
第三步:用 predicate 校验图像凭证
第一个场景是判断配送员提交的照片能否作为送达凭证。由于要传图片,先写一个把本地文件转成数据 URL 的辅助函数:
def to_data_url(path: str, mime: str = "image/jpeg") -> str:
encoded = base64.b64encode(Path(path).read_bytes()).decode("ascii")
return f"data:{mime};base64,{encoded}"
然后构造请求。input 是一个消息数组,消息的 content 里可以同时放文本块和图像块:
decision = client.decisions.create(
model="gpt-6-luna",
input=[
{
"role": "user",
"content": [
{
"type": "input_text",
"text": "Proof-of-delivery photo submitted by the driver.",
},
{
"type": "input_image",
"image_url": to_data_url("delivery_photo.jpg"),
},
],
}
],
questions=[
{
"type": "predicate",
"name": "package_at_door",
"instructions": (
"Does the photo clearly show a package placed at a residential or office "
"entrance, such as a doorstep, porch, or mailroom? Answer false if the package "
"is still inside a vehicle, held in someone's hands, or not visible."
),
}
],
)
注意 instructions 的写法:它不仅说明要判断什么,还明确列出了应当判为否定的几种情形。对判定类任务来说,把「什么算不成立」写清楚,往往比把「什么算成立」写清楚更能减少歧义。
返回结果需要按类型分支处理。答案对象可能是拒绝(refusal),也可能是 predicate:
answer = decision.answers[0]
if answer.type == "refusal":
print(f"Refused: {answer.name}")
elif answer.type == "predicate":
if answer.probability >= 0.85:
print("Delivery confirmed automatically.")
else:
print(f"Sent for manual review (Probability={answer.probability:.2f})")
这里用 0.85 作为自动确认的阈值,低于阈值则转人工复核。阈值不是接口强制的,而是调用方根据自己的误判成本设定的:如果漏判代价高,就把阈值调低;如果误升级代价高,就调高。
在示例场景中,模型返回的概率为 0.00,照片被标记为需要人工复核——这正是期望行为,因为照片里无法确认包裹是否放在门口。
第四步:用 choice 做无序分类
当选项之间没有等级关系时使用 choice。下面用一条新闻标题做主题分类:
decision = client.decisions.create(
model="gpt-6-luna",
input="Apple shares fall after EU fines company over App Store rules",
questions=[
{
"type": "choice",
"name": "topic",
"instructions": "What is this headline about?",
"choices": [
{"value": "sports", "description": "Games, players, tournaments."},
{"value": "regulations", "description": "Government and regulation."},
{"value": "tech", "description": "Technology and science."},
{"value": "markets", "description": "Markets and companies."},
{"value": "other", "description": "None of the above."},
],
}
],
)
每个选项由 value 和 description 组成。value 是程序里用来分支的标识,description 帮助模型区分相近选项。建议始终保留一个兜底选项(如 other),否则模型会被迫在都不合适的选项里硬选一个。
处理返回结果时,choice 类型会给出最终选项、置信度,以及每个选项各自的概率:
answer = decision.answers[0]
if answer.type == "refusal":
print(f"Refused: {answer.name}")
elif answer.type == "choice":
print(f"Top pick: {answer.choice} (confidence: {answer.confidence:.2f})")
for option in answer.probabilities:
print(f" {option.value}: {option.probability:.2f}")
示例中这条标题同时涉及市场和监管,模型给出的结果是 markets,置信度 0.52,而 regulations 拿到 0.36。这种「两个选项都不低」的情况正是需要关注的地方:如果业务上要求高确定性,可以在置信度低于某个值时把请求转人工,而不是直接采信最高分选项。
第五步:用 score 按评分标准打分
当选项本身有顺序时使用 score。它接收一组有序等级,并返回加权平均分。下面用一道问答题的答案做评分:
decision = client.decisions.create(
model="gpt-6-luna",
input=(
"Question: What's an AI agent?\n"
"Answer: An AI agent is a system where an LLM works toward a goal by "
"deciding its own next steps, calling tools, and using the results "
"in a loop until the task is done."
),
questions=[
{
"type": "score",
"name": "grade",
"instructions": "Grade the answer.",
"levels": [
{"label": "Incorrect", "description": "Wrong or no attempt."},
{"label": "Partial", "description": "Some correct ideas."},
{"label": "Correct", "description": "Fully correct."},
],
}
],
)
等级按升序排列,每个等级带一个 label 和 description。返回对象包含每个等级的概率、对应的数值,以及最终得分:
answer = decision.answers[0]
print(answer)
示例输出如下:
AnswerAnswerResourceScore(
confidence=1.0,
name='grade',
probabilities=[
AnswerAnswerResourceScoreProbability(label='Incorrect', probability=0.0, value=0),
AnswerAnswerResourceScoreProbability(label='Partial', probability=0.0, value=1),
AnswerAnswerResourceScoreProbability(label='Correct', probability=1.0, value=2),
],
score=2.0,
type='score'
)
加权平均的计算方式是各等级概率乘以对应数值再求和:0 × 0 + 1 × 0 + 2 × 1 = 2,满分即为 2。这种打分方式适合对描述性答案做批改,因为它不是简单地在几个标签里挑一个,而是给出一个连续的分值。
一个完整示例
把上面的步骤串起来,下面是一个从初始化到输出判定结果的最小可运行脚本,覆盖三种决策类型。运行前请把图片路径替换为本地实际文件。
from getpass import getpass
from openai import OpenAI
import base64
from pathlib import Path
openai_api_key = getpass("OpenAI API key: ")
client = OpenAI(api_key=openai_api_key)
def to_data_url(path: str, mime: str = "image/jpeg") -> str:
encoded = base64.b64encode(Path(path).read_bytes()).decode("ascii")
return f"data:{mime};base64,{encoded}"
# 1. predicate:图像条件判断
predicate_decision = client.decisions.create(
model="gpt-6-luna",
input=[
{
"role": "user",
"content": [
{"type": "input_text", "text": "Proof-of-delivery photo submitted by the driver."},
{"type": "input_image", "image_url": to_data_url("delivery_photo.jpg")},
],
}
],
questions=[
{
"type": "predicate",
"name": "package_at_door",
"instructions": (
"Does the photo clearly show a package placed at a residential or office "
"entrance, such as a doorstep, porch, or mailroom? Answer false if the package "
"is still inside a vehicle, held in someone's hands, or not visible."
),
}
],
)
answer = predicate_decision.answers[0]
if answer.type == "refusal":
print(f"Refused: {answer.name}")
elif answer.type == "predicate":
if answer.probability >= 0.85:
print("Delivery confirmed automatically.")
else:
print(f"Sent for manual review (Probability={answer.probability:.2f})")
# 2. choice:无序分类
choice_decision = client.decisions.create(
model="gpt-6-luna",
input="Apple shares fall after EU fines company over App Store rules",
questions=[
{
"type": "choice",
"name": "topic",
"instructions": "What is this headline about?",
"choices": [
{"value": "sports", "description": "Games, players, tournaments."},
{"value": "regulations", "description": "Government and regulation."},
{"value": "tech", "description": "Technology and science."},
{"value": "markets", "description": "Markets and companies."},
{"value": "other", "description": "None of the above."},
],
}
],
)
answer = choice_decision.answers[0]
if answer.type == "refusal":
print(f"Refused: {answer.name}")
elif answer.type == "choice":
print(f"Top pick: {answer.choice} (confidence: {answer.confidence:.2f})")
for option in answer.probabilities:
print(f" {option.value}: {option.probability:.2f}")
# 3. score:按评分标准打分
score_decision = client.decisions.create(
model="gpt-6-luna",
input=(
"Question: What's an AI agent?\n"
"Answer: An AI agent is a system where an LLM works toward a goal by "
"deciding its own next steps, calling tools, and using the results "
"in a loop until the task is done."
),
questions=[
{
"type": "score",
"name": "grade",
"instructions": "Grade the answer.",
"levels": [
{"label": "Incorrect", "description": "Wrong or no attempt."},
{"label": "Partial", "description": "Some correct ideas."},
{"label": "Correct", "description": "Fully correct."},
],
}
],
)
print(score_decision.answers[0])
三段调用彼此独立,可以按需只保留其中一段。实际接入业务时,通常会把判定结果映射到后续动作:predicate 的结果决定是否自动通过,choice 的结果决定工单流向哪个队列,score 的结果决定答案是否需要重写。
注意事项
模型支持范围有限
当前 Decisions API 只支持 gpt-6-luna 这一个模型。即使其他模型在通用任务上表现更好,也不能用于决策请求。这意味着模型选型上没有回旋余地,如果业务对模型有特定要求,需要先确认这一点是否可接受。
计费方式与长上下文
该接口按输入 token 计费,示例中提到的价格是每 100 万输入 token 0.10 美元,输出与缓存不额外收费。但需要注意,长上下文输入会推高成本。价格与计费规则属于易变信息,实际以 OpenAI 官网当前公布的信息为准。
必须显式声明问题类型
questions 中的 type 是必填项,不能省略。缺少类型声明时,接口无法判断该返回概率、选项还是评分。三种类型的字段结构也各不相同:predicate 用 instructions,choice 额外需要 choices,score 额外需要 levels。混用字段会导致请求失败。
返回结果要按类型分支处理
答案对象可能是 refusal,也可能是 predicate、choice 或 score。直接访问 answer.probability 或 answer.choice 而不先判断类型,在模型拒绝回答时会抛错。稳妥的写法是先判断 answer.type,再读取对应字段。
阈值由调用方决定
接口只给出概率和置信度,不替你决定「多少算通过」。示例中 predicate 用了 0.85 作为自动确认阈值,这只是演示值。真实业务里应当结合误判成本来定:漏判代价高的场景把阈值调低,误升级代价高的场景把阈值调高。
低置信度结果需要兜底
在 choice 示例中,最高选项的置信度只有 0.52,第二选项为 0.36。这类结果说明输入本身存在多义性。如果业务不能接受这种模糊性,应当设置置信度下限,低于下限时转人工或要求补充信息,而不是直接采用最高分选项。
图像输入需要编码
图像以 base64 数据 URL 的形式传入,需要自行完成编码。图片体积越大,占用的输入 token 越多,成本也越高。传入前建议先压缩到满足判定需要的最小尺寸。
等级顺序影响得分
score 的等级按升序排列,加权平均分由概率乘以对应数值求和得到。如果等级顺序写反,得分会失去意义。定义等级时应当从最差到最好依次排列,并确保每个等级的 description 边界清晰,避免相邻等级含义重叠。