OpenAI 应用入门二:把解题说明变成页面能读取的学习卡
将原课方程转换成步骤卡结构,区分 Schema 约束、数值校验和教学质量。
本文目录 · 7 节
上一课返回的是一段文字。若页面希望把解题过程分成卡片,直接按换行或“第一步”拆分会很脆弱:模型换一种说法,解析就可能失效。本课选编 OpenAI Cookbook 的 Structured Outputs 数学辅导示例,保留原题 8x + 7 = -23,按当前 Responses 接口改写代码。
适用层级:Python 进阶,需要已有 API 账号,不是无代码入门。学习目标是设计一个简单结果结构,取得已解析对象,处理拒答与不完整结果,再做独立的数学检查。需要上一课的 Python 与 API 环境,并安装 pydantic。不想调用 API 的读者可以先完成下面的离线结构练习。文中样例输出是教学参考;接口部分只做资料核对,没有真实模型调用。
1. 有效 JSON 不等于符合业务结构
下面两段都是合法 JSON,却不能被同一个前端按相同字段读取:
{"answer": "x = -15/4"}{"steps": ["减去 7", "除以 8"], "result": -3.75}如果前端期待 steps 内每项包含 explanation 和 output,上面第二段依然不合格。Structured Outputs 的作用是约束结构,而不只是要求“请返回 JSON”。它不会自动证明解题步骤符合数学事实,所以结构与内容仍要分开验收。
本课定义:steps 是步骤数组,每项有一句解释和一个等式;final_answer 是可解析的分数字符串。这样页面可以先显示解释,再显示等式,最后显示结果。字段名是开发契约,不应该随中文标题变化而改变。
2. 先在本地验证结构
在上一课创建的同一练习目录和 PowerShell 会话中安装依赖:.\.venv\Scripts\python.exe -m pip install openai pydantic。把下面代码保存为 math_card_schema.py。它只处理教学数据,不发送任何网络请求:
from pydantic import BaseModel, ConfigDict
class Step(BaseModel):
model_config = ConfigDict(extra="forbid")
explanation: str
output: str
class MathCard(BaseModel):
model_config = ConfigDict(extra="forbid")
steps: list[Step]
final_answer: str
if __name__ == "__main__":
sample = {
"steps": [
{"explanation": "两边同时减去 7。", "output": "8x = -30"},
{"explanation": "两边同时除以 8。", "output": "x = -15/4"},
],
"final_answer": "-15/4",
}
card = MathCard.model_validate(sample)
print(card.steps[0].output)
print(card.final_answer)运行 .\.venv\Scripts\python.exe .\math_card_schema.py,预期依次打印 8x = -30 和 -15/4。把 final_answer 改名为 answer 后应校验失败,因为缺少必需字段且多出未声明字段。这一步证明的是你定义的本地数据契约,不能算成模型按结构输出的实测。
3. 用 Responses 解析助手取得对象
将下一段代码保存为同目录的 lesson_structured.py,使用 .\.venv\Scripts\python.exe .\lesson_structured.py 运行。它导入上一节定义的类型,因此文件名要一致。若已新开 PowerShell 窗口,先按上一课重新配置当前会话的密钥与模型;本课模型还需支持 Structured Outputs。API 请求可能产生费用,仍只使用虚构数学题:
import os
from fractions import Fraction
from openai import OpenAI
from math_card_schema import MathCard
if not os.environ.get("OPENAI_API_KEY") or not os.environ.get("OPENAI_MODEL"):
raise SystemExit("请配置 OPENAI_API_KEY 和 OPENAI_MODEL。")
client = OpenAI(timeout=30.0, max_retries=0)
response = client.responses.parse(
model=os.environ["OPENAI_MODEL"],
instructions=(
"用中文辅导初学者解方程,给出简短的教学步骤。"
"final_answer 只填分数字符串或整数,例如 -15/4,不加 x=。"
"只输出可核查的解题说明,不声称已经评估学生能力。"
),
input="解方程 8x + 7 = -23。",
text_format=MathCard,
max_output_tokens=1600,
store=False,
)
if response.status != "completed":
raise SystemExit(f"未完整完成:{response.status},不生成学习卡。")
for item in response.output:
if item.type == "message":
for content in item.content:
if content.type == "refusal":
raise SystemExit("模型拒答,不把拒答解析成正常学习卡。")
card = response.output_parsed
if card is None or not card.steps:
raise SystemExit("没有取得完整学习卡,请人工检查。")
try:
answer = Fraction(card.final_answer)
except (ValueError, ZeroDivisionError):
raise SystemExit("最终答案无法解析为有效分数,请人工检查。")
if 8 * answer + 7 != -23:
raise SystemExit("答案未通过代回检查,不展示为已验证结果。")
for index, step in enumerate(card.steps, start=1):
print(index, step.explanation, step.output)
print("最终值通过代回检查:", card.final_answer)本例使用 client.responses.parse 与 text_format,不要把旧 Chat Completions 示例的 response_format 参数名称原样搬过来。若使用原始 JSON Schema,则 Responses 对应 text.format;严格模式还要求对象禁用额外属性、声明必需字段,并遵循支持的 Schema 子集。
4. 读懂这段代码验证了什么
output_parsed 是 SDK 解析出的对象,不必再用正则从文字里提取字段。Fraction 使用精确分数代回这道固定方程,因此能拒绝错误的正负号或分母为零。最终值通过检查,不意味着每条解释都正确;代码没有逐条验证中间等式,也没有证明讲解适合每位学生。
更不能把这段固定算式当成通用数学验证器。换成另一道题时,代回逻辑也必须跟着改变。若让模型自己生成“验证程序”,然后用同一模型的错误假设去验证答案,就可能形成自我确认。生产系统可以引入独立的符号计算、题库标准答案或人工审核。
界面也应保留状态区别:正常学习卡、无法生成、等待人工检查,不能全部用绿色成功提示。模型拒答与网络失败不是同一种情况,输出 token 用完也不应显示一张只有前两步的完整卡片。
接到页面时,应该按字段渲染普通文字,而不是把模型输出直接当作可信 HTML 插入。数学表达式如果需要专门的排版库,也应限定可接受的语法。结构校验并不等于内容已经安全:一个字符串字段仍然可能包含不适合直接执行的内容。为每张卡保留题目编号与生成版本,教师指出错误时才能找到当时的输入和规则;不要只保留最后一段美化后的展示文本。修改字段结构后,也需要同步更新页面读取逻辑和旧数据兼容方式,避免后台成功保存、前端却因字段缺失而空白。
5. 为什么还需要处理异常
结构约束需要兼容模型和合法 Schema。字段类型、必需项或嵌套结构不符合规则时,请求可能被拒绝;网络和权限也会导致异常。上面集中展示结构化流程,实际运行时应结合上一课的连接错误与状态码处理,不要把 SDK 抛出的异常直接展示给公众。
不要在失败后盲目重试几十次。先区分是配置问题、预算不够、拒答、解析失败还是内容校验不通过。只有可重试且业务允许的错误,才考虑有限重试;记录时只保留必要的状态、模型标识和脱敏样例,不把完整学生资料写入日志。
如果希望某个字段允许“未知”,应在结构中明确表达,例如允许空值,同时保留字段存在;不要要求所有字段都必填字符串,再期待模型碰到没有依据的信息时自动省略字段。本课的固定数学题不需要这个分支,但信息抽取应用经常需要。
6. 练习与解析
练习一:在本地 sample 中加入 confidence 字段。预期触发禁止额外属性的校验。解析:如果需要这个字段,应同步修改类型、提示、前端和测试;不能让消费者猜测多出的字段是否可靠。尤其不能把模型自己报的置信度直接当作真实正确率。
练习二:保持结构完全正确,把 final_answer 改为 15/4。类型校验可能通过,但固定方程的代回检查必须失败。这个对照展示了 Schema 约束和事实验证的分工。
练习三:把答案写成 -30/8。它与 -15/4 等值,使用精确分数检查可以接受;如果采用字符串完全相等判分,反而可能误判。验收规则要对应你真正关心的数学含义,而不只是某一种排版。
最后为页面写一句真实的状态说明:“最终数值通过本题代回检查,步骤由 AI 生成,待教师审阅。”这比“AI 已保证全部正确”更精确。下一课给学习助手增加只读课程查询工具,学习模型提出请求与程序真正执行之间的区别。
来源说明:OpenAI Cookbook《Introduction to Structured Outputs》的数学辅导示例,MIT 许可;中文案例改编,保留原方程、步骤数组与最终答案教学结构,迁移到当前 Responses 解析接口,新增精确分数与失败分支。2026 年 9 月 4 日核对当前文档;未执行真实模型请求,教学输出不代表实测。
原教材许可声明
MIT License
Copyright (c) 2025 OpenAI
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
文字来源:OpenAI Cookbook;中文案例改编及独立补充练习;MIT 许可;当前官方文档用于接口事实核对,非官方认证课程