前面的基础系列已经覆盖了模型调用、提示词、上下文、结构化输出、错误处理、工具和评估。接下来用一个小项目把这些知识串起来:输入一段自然语言需求,让模型生成带优先级和依赖关系的任务清单,再由 Python 校验结果,避免把“看起来像 JSON”的文本直接交给后续程序。这个项目不追求让模型替你管理完整项目,而是展示一个可靠 AI 功能应有的边界:模型负责提出候选计划,代码负责约束格式和业务规则。

先定义输入和输出

规划器接收一段需求,例如“为个人博客增加搜索功能,先调研方案,再实现和测试”。输出固定为一个 JSON 对象,其中 tasks 是任务数组。每个任务必须包含唯一的 id、任务名 title、优先级 priority 和依赖列表 depends_on:

1
2
3
4
5
6
7
8
9
10
{
"tasks": [
{
"id": "task-1",
"title": "确认搜索需求和验收条件",
"priority": "high",
"depends_on": []
}
]
}

这里的关键不是字段数量,而是先把契约写清楚。priority 只允许 low、medium、high;依赖只能引用清单中已经存在的任务 ID,不能依赖自己。模型可能漏字段、重复 ID,甚至把解释文字混在 JSON 前后,因此必须在本地校验,而不是相信提示词能够百分之百约束模型。

准备环境和密钥

在项目目录创建虚拟环境并安装 OpenAI 官方 Python SDK:

1
2
3
python -m venv .venv
source .venv/bin/activate
python -m pip install openai

通过环境变量提供密钥和模型名称,示例不包含真实凭据:

1
2
export OPENAI_API_KEY="替换为你的真实密钥"
export MODEL_NAME="替换为服务商文档中的可用模型"

模型名称、接口能力和可用参数应以实际服务商的当前文档为准。下面使用 SDK 的 Responses API,并读取 response.output_text;如果使用其他兼容服务,应先确认它是否支持相同的响应字段。

编写最小规划器

新建 planner.py。提示词要求只返回 JSON,但真正的安全边界仍在 validate_plan:

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 typing import Any

from openai import OpenAI


SYSTEM_PROMPT = """你是任务规划助手。
只返回一个合法 JSON 对象,不要 Markdown 代码围栏或额外说明。
对象格式为 {\"tasks\": [{\"id\": string, \"title\": string,
\"priority\": \"low\" | \"medium\" | \"high\", \"depends_on\": [string]}]}。
任务数量控制在 2 到 8 个,依赖关系必须合理且不能形成循环。"""


def validate_plan(data: Any) -> dict[str, list[dict[str, Any]]]:
if not isinstance(data, dict) or not isinstance(data.get("tasks"), list):
raise ValueError("结果必须包含 tasks 数组")
tasks = data["tasks"]
if not 2 <= len(tasks) <= 8:
raise ValueError("任务数量必须在 2 到 8 个之间")

ids: set[str] = set()
for task in tasks:
if not isinstance(task, dict):
raise ValueError("每个任务必须是对象")
required = {"id", "title", "priority", "depends_on"}
if set(task) != required:
raise ValueError("任务字段必须正好是 id、title、priority、depends_on")
if not isinstance(task["id"], str) or not task["id"].strip():
raise ValueError("id 必须是非空字符串")
if task["id"] in ids:
raise ValueError(f"重复的任务 id:{task['id']}")
ids.add(task["id"])
if not isinstance(task["title"], str) or not task["title"].strip():
raise ValueError("title 必须是非空字符串")
if task["priority"] not in {"low", "medium", "high"}:
raise ValueError("priority 不合法")
if not isinstance(task["depends_on"], list):
raise ValueError("depends_on 必须是数组")

for task in tasks:
dependencies = task["depends_on"]
if any(dep not in ids for dep in dependencies):
raise ValueError(f"{task['id']} 引用了不存在的依赖")
if task["id"] in dependencies:
raise ValueError(f"{task['id']} 不能依赖自己")
return {"tasks": tasks}


def make_plan(requirement: str) -> dict[str, list[dict[str, Any]]]:
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
response = client.responses.create(
model=os.environ["MODEL_NAME"],
instructions=SYSTEM_PROMPT,
input=requirement,
)
try:
data = json.loads(response.output_text)
except json.JSONDecodeError as exc:
raise ValueError("模型没有返回合法 JSON") from exc
return validate_plan(data)


def main() -> None:
if len(sys.argv) != 2 or not sys.argv[1].strip():
raise SystemExit("用法:python planner.py '描述你的需求'")
plan = make_plan(sys.argv[1])
print(json.dumps(plan, ensure_ascii=False, indent=2))


if __name__ == "__main__":
main()

运行方式如下:

1
python planner.py "为个人博客增加搜索功能,要求有方案调研、实现、测试和上线检查"

程序成功时会打印经过本地校验的任务 JSON。具体任务名称、数量和模型文本取决于实际请求,不能预先伪造一个固定的“运行结果”;可以确定的是,任何未通过 validate_plan 的响应都会在交给调用方前抛出异常。

为什么校验要放在模型之外

提示词是意图说明,不是类型系统。即使明确写了“只返回 JSON”,模型也可能因为上下文、服务差异或生成被截断而返回无效内容。json.loads 负责语法层校验,validate_plan 负责业务层校验,两者解决的是不同问题:前者判断文本能否解析,后者判断解析后的对象是否符合项目契约。

实际项目可以把校验函数单独测试,不需要 API 密钥。例如加入下面的测试数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
valid = {"tasks": [
{"id": "task-1", "title": "调研", "priority": "high", "depends_on": []},
{"id": "task-2", "title": "实现", "priority": "medium", "depends_on": ["task-1"]},
]}
assert validate_plan(valid) == valid

invalid = {"tasks": [
{"id": "task-1", "title": "调研", "priority": "urgent", "depends_on": []},
{"id": "task-1", "title": "重复", "priority": "low", "depends_on": []},
]}
try:
validate_plan(invalid)
except ValueError:
pass
else:
raise AssertionError("非法计划没有被拒绝")

注意,这个示例检查了字段和引用,却没有实现完整的循环检测。若需要允许多层依赖,应使用深度优先搜索或拓扑排序检查环;不要因为“模型通常不会生成环”就省略这一层规则。

常见问题

模型返回了 Markdown 代码围栏怎么办? 不要盲目用字符串替换后接受。可以记录原始响应用于调试,再增加一次受限修复请求,修复后仍必须重新执行 json.loads 和业务校验。修复失败就明确报错,不能把不可信文本当成计划。

为什么不让模型直接执行任务? 规划和执行是两个风险等级。当前程序只生成候选计划,执行前仍可人工确认,也不会自动运行命令、发送邮件或修改文件。以后接工具时,应为每个工具定义参数模型、权限和超时,并保留审批边界。

如何判断计划质量? 格式合法不等于计划有用。准备一组固定需求,由人工定义任务覆盖率、依赖正确率、重复率和人工修改次数,保存模型、提示词版本和失败原因。修改提示词后重新运行同一组输入,才能观察是否真的改进。

密钥会不会被打印出来? 示例只从 OPENAI_API_KEY 环境变量读取,没有打印环境变量,也没有把它写入输入。日志中同样不要记录完整密钥、敏感需求或未经脱敏的模型响应。

小结

这个综合项目把一次自然语言规划拆成了四步:用提示词说明输出契约,用 Responses API 请求模型,用 JSON 解析区分语法错误,再用 Python 规则校验业务约束。模型适合提出候选方案,代码适合做确定性检查;把两者边界分开,才能在后续加入重试、人工审批、评估和工具执行时保持可控。你可以先替换需求文本和校验规则,再逐步把这个最小程序扩展为真正的开发辅助工具。