前面的基础系列已经覆盖了模型调用、提示词、上下文、结构化输出、错误处理、工具和评估。接下来用一个小项目把这些知识串起来:输入一段自然语言需求,让模型生成带优先级和依赖关系的任务清单,再由 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/activatepython -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 jsonimport osimport sysfrom typing import Any from openai import OpenAISYSTEM_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) == validinvalid = {"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 规则校验业务约束。模型适合提出候选方案,代码适合做确定性检查;把两者边界分开,才能在后续加入重试、人工审批、评估和工具执行时保持可控。你可以先替换需求文本和校验规则,再逐步把这个最小程序扩展为真正的开发辅助工具。