前面的系列已经覆盖了 API 调用、提示词、结构化输出、错误处理和评估。本篇在路线完成后做一个小型综合项目:读取一段技术笔记,请模型生成问答卡片,程序严格校验后保存为 JSON。它不负责替你判断知识是否正确,也不会把模型输出直接当成学习结论;重点是把一次生成请求放进可测试、可复用的程序流程。
先确定输入和输出
程序只处理一份纯文本笔记,输出若干张卡片。每张卡片包含 question、answer 和 difficulty 三个字段,其中难度只能是 easy、normal 或 hard。问题应能根据原文回答,答案不能凭空补充事实;无法从原文得到可靠答案时,模型应减少卡片,而不是猜测。
数据流分成五步:读取文件、请求模型、解析 JSON、校验字段、保存结果。模型只负责提出候选卡片,文件读写和格式边界由 Python 控制。这样,后续更换模型或提示词时,不会把不合格结果静默写入资料库。
准备环境和配置
在独立虚拟环境中安装 SDK:
1 2 3
| python -m venv .venv source .venv/bin/activate python -m pip install openai python-dotenv
|
创建 .env,真实密钥只通过环境变量提供,下面的内容只是占位符:
1 2 3
| OPENAI_API_KEY=替换为你的真实密钥 MODEL_NAME=替换为你可用的模型名称 OPENAI_BASE_URL=
|
不要把 .env 提交到 Git。若使用兼容服务,只有在服务商文档确认支持相同接口时,才配置 OPENAI_BASE_URL。
编写最小程序
新建 cards.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| import json import os import sys from pathlib import Path
from dotenv import load_dotenv from openai import OpenAI
load_dotenv() MODEL = os.environ["MODEL_NAME"] client = OpenAI( api_key=os.environ.get("OPENAI_API_KEY"), base_url=os.environ.get("OPENAI_BASE_URL") or None, )
INSTRUCTIONS = """ 你是技术学习卡片编辑。根据用户提供的笔记生成 3 到 8 张问答卡片。 只返回 JSON 数组,不要 Markdown 围栏或解释。每项只能有 question、answer、difficulty。 question 和 answer 必须是非空字符串,difficulty 只能是 easy、normal、hard。 只使用原文明确提供的信息;原文无法支持的问题不要生成。 """.strip()
def validate(raw: str) -> list[dict]: try: cards = json.loads(raw) except json.JSONDecodeError as exc: raise ValueError("模型返回的不是合法 JSON") from exc if not isinstance(cards, list) or not 3 <= len(cards) <= 8: raise ValueError("卡片数量必须在 3 到 8 张之间")
checked = [] for card in cards: if not isinstance(card, dict): raise ValueError("卡片必须是对象") if set(card) != {"question", "answer", "difficulty"}: raise ValueError("卡片字段不完整或包含未知字段") if card["difficulty"] not in {"easy", "normal", "hard"}: raise ValueError("difficulty 无效") for key in ("question", "answer"): if not isinstance(card[key], str) or not card[key].strip(): raise ValueError(f"{key} 必须是非空字符串") checked.append(card) return checked
def generate(note: str) -> list[dict]: note = note.strip() if not note: raise ValueError("笔记不能为空") if len(note) > 12000: raise ValueError("示例只接受不超过 12000 个字符的笔记") response = client.responses.create( model=MODEL, instructions=INSTRUCTIONS, input=note, ) return validate(response.output_text)
def main() -> None: if len(sys.argv) != 2: raise SystemExit("用法:python cards.py note.txt") note = Path(sys.argv[1]).read_text(encoding="utf-8") cards = generate(note) Path("study-cards.json").write_text( json.dumps(cards, ensure_ascii=False, indent=2), encoding="utf-8" ) print(f"已生成 {len(cards)} 张卡片:study-cards.json")
if __name__ == "__main__": main()
|
准备一份 note.txt 后运行:
1
| python cards.py note.txt
|
responses.create 负责一次模型请求,instructions 放稳定规则,input 放本次笔记,output_text 取出文本结果。validate 不依赖网络,可以单独测试。只有校验通过,main 才会写文件;因此不能把某次模型的具体回复当成固定运行结果,真正稳定的是程序对输入和输出边界的处理。
增加离线测试
模型调用昂贵且结果不固定,先测试纯校验函数。创建 test_cards.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import json import unittest
from cards import validate
class CardValidationTest(unittest.TestCase): def test_valid_cards_are_accepted(self): raw = json.dumps([ {"question": "什么是变量?", "answer": "用于保存值的名称。", "difficulty": "easy"}, {"question": "为什么校验输出?", "answer": "模型输出不是类型系统。", "difficulty": "normal"}, {"question": "怎样限制输入?", "answer": "在请求前检查长度。", "difficulty": "hard"}, ]) self.assertEqual(len(validate(raw)), 3)
def test_unknown_field_is_rejected(self): cards = [{"question": "q", "answer": "a", "difficulty": "easy", "source": "x"}] * 3 with self.assertRaises(ValueError): validate(json.dumps(cards))
if __name__ == "__main__": unittest.main()
|
运行 python -m unittest -v test_cards.py。这个测试不会调用模型,能稳定验证字段集合和合法数据路径。还可以补充非法 JSON、空答案、错误难度和超过数量上限的案例。测试的目标不是证明模型一定生成好卡片,而是保证坏结果不会直接落盘。
常见问题
为什么不让模型直接写 Markdown? Markdown 适合阅读,但字段结构不稳定。先保存 JSON,后续可以由确定性的 Python 代码渲染成 Markdown、网页或其他格式。
卡片内容错误怎么办? 程序只能校验类型和枚举值,不能证明答案符合事实。应保留原笔记、抽样人工复核,并把发现的错误样例加入评估集。
笔记很长怎么办? 不要无限增大单次请求。先按标题或段落分块,再分别生成卡片,并在保存记录中保留来源片段;否则既难控制上下文,也难定位错误。
为什么没有自动去重? 问题相似度需要额外规则或 embedding。初版先保持边界简单,后续增加去重时,应同时补充测试,避免把不同角度的问题误删。
小结
这个项目把“笔记转卡片”拆成读取、调用、解析、校验和保存五步。模型提供候选内容,Python 负责格式、长度和文件副作用,离线测试负责保护确定性逻辑。综合项目的价值不在于堆叠功能,而在于建立清晰的信任边界:生成结果必须经过程序检查和人工复核,才能成为真正可用的学习资料。