基础路线完成后,适合用一个边界清晰的小项目把知识串起来。本篇构建课程大纲检查器:读取 Markdown 草稿,让模型找出目标、先修知识和章节顺序上的问题,再由 Python 校验结果并输出报告。它只提出检查意见,不自动修改或发布内容,重点是练习“模型负责发现候选问题,程序负责约束格式,人负责最终判断”的协作方式。

先定义输入、输出和边界

输入是一个课程大纲文本,例如包含课程目标、章节标题和简短说明的 Markdown 文件。输出使用 JSON,包含总体结论和问题列表。每个问题必须指向一个章节,给出问题类型、严重程度和建议;模型不得凭空补充课程事实,也不能把建议当成已经完成的修改。

把任务限制为四类问题:目标不清、顺序不合理、前置知识缺失、内容重复。严重程度只有 low、medium、high 三个值。这样的枚举方便程序拒绝拼写错误,也让审核者能优先查看高严重度问题。工具不会写回原文件,因此即使模型判断错误,原始大纲仍然安全。

准备环境和配置

使用独立虚拟环境安装官方 Python SDK:

1
2
3
4
5
python -m venv .venv
source .venv/bin/activate
python -m pip install openai
export OPENAI_API_KEY="替换为你的真实密钥"
export MODEL_NAME="替换为你可用的模型名称"

密钥只从环境变量读取,不要写进源码、Markdown 或提交记录。不同服务的模型名和兼容接口可能不同,应以服务商当前文档为准;下面示例使用 OpenAI Python SDK 的 Responses API 写法。

编写检查器

新建 outline_checker.py。提示词要求返回 JSON,但真正的边界仍然是 json.loads 和后面的类型、枚举、长度检查:

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
import json
import os
import sys
from pathlib import Path

from openai import OpenAI

MODEL = os.environ["MODEL_NAME"]
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
KINDS = {"目标不清", "顺序不合理", "前置知识缺失", "内容重复"}
SEVERITIES = {"low", "medium", "high"}


def check_outline(markdown: str) -> dict:
instructions = """你是课程设计审核员。只根据输入的大纲提出检查意见,不要改写大纲。
只返回 JSON,不要 Markdown 代码围栏,格式必须是:
{"summary":"不超过80字的总体结论","issues":[{"section":"章节标题","kind":"目标不清","severity":"low","suggestion":"不超过100字的建议"}]}
kind 只能是目标不清、顺序不合理、前置知识缺失、内容重复;severity 只能是 low、medium、high。
如果没有明显问题,issues 返回空数组。不要臆测输入中没有出现的课程信息。"""
response = client.responses.create(
model=MODEL,
instructions=instructions,
input=markdown,
)
result = json.loads(response.output_text)
validate(result)
return result


def validate(result: object) -> None:
if not isinstance(result, dict) or set(result) != {"summary", "issues"}:
raise ValueError("顶层字段不符合约定")
if not isinstance(result["summary"], str) or len(result["summary"]) > 80:
raise ValueError("summary 无效或过长")
if not isinstance(result["issues"], list):
raise ValueError("issues 必须是数组")
for issue in result["issues"]:
if not isinstance(issue, dict):
raise ValueError("问题项必须是对象")
if set(issue) != {"section", "kind", "severity", "suggestion"}:
raise ValueError("问题项字段不完整")
if not isinstance(issue["section"], str) or not issue["section"]:
raise ValueError("section 必须是非空字符串")
if issue["kind"] not in KINDS or issue["severity"] not in SEVERITIES:
raise ValueError("kind 或 severity 不在允许集合中")
if not isinstance(issue["suggestion"], str) or len(issue["suggestion"]) > 100:
raise ValueError("suggestion 无效或过长")


if __name__ == "__main__":
if len(sys.argv) != 2:
raise SystemExit("用法:python outline_checker.py outline.md")
text = Path(sys.argv[1]).read_text(encoding="utf-8")
print(json.dumps(check_outline(text), ensure_ascii=False, indent=2))

将示例大纲保存为 outline.md,执行 python outline_checker.py outline.md。不要预先写死某个模型返回结果,因为输出会受模型、输入和服务状态影响。可验证的结果是:输出能被 JSON 解析,顶层字段固定,问题项的枚举和值类型全部通过检查。

为什么要保留程序校验

模型可能返回额外字段、把 medium 拼成其他形式,或者把一段解释包在 JSON 外面。提示词只能提高符合约定的概率,不能替代类型系统。validate 先检查整体结构,再逐项检查字段;任何失败都应让命令以错误结束,而不是把不完整报告交给下游。

还可以在校验前增加数量限制,例如最多保留 30 个问题,防止异常输出导致报告过大。若报告要保存到文件,应使用临时文件写完后再替换正式文件,避免进程中断留下半份结果。输入文件和输出报告分开,也便于比较每次提示词或模型调整后的变化。

加入离线测试和人工复核

校验逻辑不需要联网,可以直接准备几组返回值测试:合法的空问题列表、缺少 severity、未知的 kind、超长建议,以及问题项不是对象。测试应断言这些结果分别被接受或拒绝。这样即使没有 API 密钥,也能验证程序边界。

模型通过校验不代表意见正确。人工复核时至少检查三点:问题是否真的能从大纲文本中得到依据;建议是否具体而不是泛泛而谈;high 是否被滥用。可以把人工决定记录为 accepted、rejected 或 needs_review,但不要让模型自己把意见标记为最终结论。后续评估可以准备一小组人工标注的大纲,比较问题类别的命中情况,而不是只看返回 JSON 是否合格。

常见问题

为什么不让程序自动改 Markdown? 检查和修改是两个风险不同的动作。自动改写可能改变原意;先生成报告并人工确认,能保留原文,也更容易审计。

没有问题时为什么要返回空数组? 固定结构比返回“没有发现问题”更适合程序处理。调用方可以统一遍历 issues,无需猜测自然语言表达。

所有建议都很笼统怎么办? 在输入中提供章节标题和说明,明确要求引用已有信息,并在测试集中加入边界案例。若仍不稳定,应把报告降级为人工参考,而不是扩大自动化权限。

小结

这个小项目没有引入新的框架,却完整串起了文件输入、环境变量、Responses API、结构化解析、枚举校验、离线测试和人工复核。可靠的 AI 功能不是“模型说了算”,而是把模型限制在可审查的任务内,再用代码和流程守住边界。掌握这种闭环后,才适合把相同方法迁移到其他内容检查任务中。