前面的系列已经覆盖了 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/activate
python -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 json
import os
import sys
from datetime import date
from pathlib import Path

from openai import OpenAI


SYSTEM = """你是任务整理器。只返回一个 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 json
from pathlib import Path

path = 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 严格校验和持久化,状态修改则完全交给确定性代码。结构化输出能减少解析成本,却不能替代校验;重试能提高成功率,却不能替代错误边界;模型可以提出建议,但程序和用户才应决定哪些数据真正生效。把这三点落实后,后续接数据库、命令行界面或其他业务系统都会更安全、更容易测试。