前面的系列已经覆盖了 API 调用、结构化输出、错误处理、日志和基础评估。本篇用一个小项目把这些知识串起来:输入一段自然语言任务描述,让模型返回统一的任务清单,再由 Python 校验、保存和追踪状态。重点不是让模型“直接操作任务”,而是把模型当作生成候选数据的组件,把边界、格式和修改权限留在程序中。
先定义可验证的边界 例如,用户输入“周五前整理发布清单,优先检查链接,再让同事复核”,我们希望得到若干条任务。每条任务只包含四个字段:title、priority、due 和 status。priority 只能是 low、medium、high,status 初始只能是 todo;日期使用 YYYY-MM-DD,无法确定时使用 null。
这几个限制很重要。自然语言适合表达意图,却不适合直接作为数据库记录。模型可能漏字段、增加额外字段,或者把“尽快”猜成某个日期。因此流程应当是:读取输入、请求模型、解析 JSON、校验字段、保存候选结果。校验失败时重试或报告错误,绝不能把未经检查的文本当成任务数据。
准备最小输入和环境 在虚拟环境中安装官方 Python SDK,并通过环境变量配置服务信息:
1 2 3 4 5 python -m venv .venv source .venv/bin/activatepython -m pip install openai export OPENAI_API_KEY="替换为你的真实密钥" export MODEL_NAME="替换为你可用的模型名称"
密钥只存在于环境变量,不写入源码、输入文件或日志。新建 tasks.txt,内容可以是:
1 周五前整理发布清单,优先检查链接,然后请同事复核。下周一提醒我归档结果。
不同服务对结构化输出参数的命名可能不同,使用前应以所选服务的官方文档为准。下面示例沿用 Responses API 的 Python SDK 写法,并把模型返回的文本当作 JSON 字符串处理;这样即使更换服务,也只需要替换请求适配层。
编写请求和解析函数 创建 task_tracker.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 import jsonimport osimport sysfrom datetime import datefrom pathlib import Pathfrom openai import OpenAISYSTEM = """你是任务整理器。只返回一个 JSON 对象,不要 Markdown。格式: {"tasks": [{"title": "字符串", "priority": "low|medium|high", "due": "YYYY-MM-DD 或 null", "status": "todo"}]} 不要添加其他字段;不确定日期时使用 null。""" def validate (payload: object ) -> list [dict ]: if not isinstance (payload, dict ) or not isinstance (payload.get("tasks" ), list ): raise ValueError("顶层必须包含 tasks 数组" ) allowed = {"title" , "priority" , "due" , "status" } result = [] for item in payload["tasks" ]: if not isinstance (item, dict ) or set (item) != allowed: raise ValueError("任务字段不符合约定" ) if not isinstance (item["title" ], str ) or not item["title" ].strip(): raise ValueError("title 必须是非空字符串" ) if item["priority" ] not in {"low" , "medium" , "high" }: raise ValueError("priority 取值无效" ) if item["status" ] != "todo" : raise ValueError("新任务的 status 必须是 todo" ) if item["due" ] is not None : date.fromisoformat(item["due" ]) result.append(item) return result def extract_tasks (text: str ) -> list [dict ]: client = OpenAI(api_key=os.environ["OPENAI_API_KEY" ]) response = client.responses.create( model=os.environ["MODEL_NAME" ], instructions=SYSTEM, input =text, ) payload = json.loads(response.output_text) return validate(payload) def main () -> None : if len (sys.argv) != 3 : raise SystemExit("用法:python task_tracker.py tasks.txt tasks.json" ) source, target = map (Path, sys.argv[1 :]) tasks = extract_tasks(source.read_text(encoding="utf-8" )) target.write_text( json.dumps({"tasks" : tasks}, ensure_ascii=False , indent=2 ) + "\n" , encoding="utf-8" , ) print (f"已保存 {len (tasks)} 条候选任务:{target} " ) if __name__ == "__main__" : main()
这里有三个值得注意的边界。SYSTEM 规定了输出协议,但它不是安全校验;真正的约束在 validate 中。set(item) != allowed 会拒绝多余字段,避免模型悄悄增加程序没有处理的属性。date.fromisoformat 能验证日期格式,错误会直接中断保存,而不是留下一个看似合法的文件。
运行命令如下:
1 python task_tracker.py tasks.txt tasks.json
程序打印的是文件路径和任务数量,不伪造模型生成的具体内容。要检查真实结果,可打开 tasks.json,确认每项只有四个字段、日期能被解析,并人工判断任务是否忠实于输入。
增加状态追踪而不是让模型改状态 整理完成后,状态变化应由确定性的 Python 代码完成。例如,把指定标题标记为完成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import jsonfrom pathlib import Pathpath = Path("tasks.json" ) data = json.loads(path.read_text(encoding="utf-8" )) target = "检查链接" changed = 0 for task in data["tasks" ]: if task["title" ] == target: task["status" ] = "done" changed += 1 if changed != 1 : raise SystemExit("未找到唯一目标任务,未写入文件" ) path.write_text(json.dumps(data, ensure_ascii=False , indent=2 ) + "\n" , encoding="utf-8" ) print ("状态已更新" )
这是一个简单但重要的职责分离:模型负责从文本提取候选任务,程序负责识别目标、检查修改数量和写入文件。若未来接入数据库或日历,也应先通过明确的函数边界和人工确认,再执行外部副作用。
常见问题 为什么不直接让模型返回 Python 列表? 因为模型输出仍是文本,Python 表示法可能使用单引号、缺少引号或夹杂说明文字。JSON 加 json.loads 是清晰的交换协议,但仍需要字段校验。
解析失败时要不要无限重试? 不要。可在请求层设置有限次数,例如最多两次;第二次提示“上次输出无法解析,请只返回 JSON”,仍失败就保存错误类型和请求时间,交给人工处理。无限重试可能放大费用和限流问题。
日期由模型决定可靠吗? 不可靠。“周五”依赖当前日期和时区,模型也可能误解上下文。更稳妥的做法是把当前日期作为明确上下文传入,并对高风险日期要求用户确认;本示例只把日期当候选值。
如何测试而不调用模型? 把 extract_tasks 拆成“请求”和“解析校验”两层。测试时直接给 validate 传入固定字典,覆盖缺字段、额外字段、错误枚举值和非法日期。真实接口只需少量集成测试,并且不要把测试密钥提交到仓库。
小结 这个任务清单器展示了一个可维护 AI 功能的最小闭环:自然语言输入经过模型转换为候选 JSON,再由 Python 严格校验和持久化,状态修改则完全交给确定性代码。结构化输出能减少解析成本,却不能替代校验;重试能提高成功率,却不能替代错误边界;模型可以提出建议,但程序和用户才应决定哪些数据真正生效。把这三点落实后,后续接数据库、命令行界面或其他业务系统都会更安全、更容易测试。