基础路线已经覆盖了 API 调用、消息组织、提示词、结构化输出、错误处理、日志和评估。本篇用一个独立的小项目把这些知识串起来:输入目的地、天数和偏好,模型返回一个可被 Python 解析和检查的旅行行程草案。项目不查询实时价格、不购买票券,也不把模型输出直接当成最终计划,重点是练习“模型生成,程序约束”的协作边界。
先定义输入和边界 行程规划看似只是让模型写一段文字,但自然语言结果很难被程序继续使用。本项目规定输出必须包含 destination、days 和 itinerary 三个字段;每天的安排包含日期序号、地点列表和备注。程序还会检查天数范围、数组长度和字符串类型。
边界同样重要:模型只能提出草案,不能声称已经完成预订,也不能编造实时营业时间、库存和价格。涉及签证、交通、天气或安全的内容,应在后续由用户和可靠来源确认。把这些限制写进提示词是第一层防线,代码校验是第二层防线。
准备环境和配置 在独立虚拟环境中安装官方 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="替换为你可用的模型名称"
不要把真实密钥写进源码、配置文件或文章。模型名不是所有服务都相同,运行前应查看所用服务的官方文档;下面的代码沿用 Responses API 的 Python SDK 写法。
编写最小生成器 创建 itinerary.py。先用标准库定义校验函数,再调用模型。这样即使模型返回了格式正确但内容不合约的 JSON,也会在程序边界处失败,而不会悄悄流入后续业务。
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 import jsonimport osimport sysfrom openai import OpenAIMODEL = os.environ["MODEL_NAME" ] client = OpenAI(api_key=os.environ["OPENAI_API_KEY" ]) INSTRUCTIONS = """ 你是旅行行程草案助手。只返回 JSON,不要 Markdown 代码围栏。 格式为: {"destination": "城市", "days": 2, "itinerary": [{"day": 1, "places": ["地点"], "note": "备注"}]} 只能根据用户给出的信息规划,不要声称查询了实时价格、库存或营业时间。 不要执行预订。信息不确定时,在 note 中明确提醒用户核实。 """ .strip()def validate (data: dict , expected_days: int ) -> dict : if set (data) != {"destination" , "days" , "itinerary" }: raise ValueError("顶层字段不符合约定" ) if not isinstance (data["destination" ], str ) or not data["destination" ].strip(): raise ValueError("destination 必须是非空字符串" ) if data["days" ] != expected_days: raise ValueError("days 与用户输入不一致" ) plan = data["itinerary" ] if not isinstance (plan, list ) or len (plan) != expected_days: raise ValueError("itinerary 长度不正确" ) for number, item in enumerate (plan, start=1 ): if not isinstance (item, dict ): raise ValueError("每日安排必须是对象" ) if item.get("day" ) != number: raise ValueError("day 必须从 1 连续编号" ) if (not isinstance (item.get("places" ), list ) or not all (isinstance (x, str ) and x.strip() for x in item["places" ])): raise ValueError("places 必须是字符串列表" ) if not isinstance (item.get("note" ), str ): raise ValueError("note 必须是字符串" ) return data def plan (destination: str , days: int , preference: str ) -> dict : if not 1 <= days <= 7 : raise ValueError("天数必须在 1 到 7 之间" ) user_input = ( f"目的地:{destination} \n" f"天数:{days} \n" f"偏好:{preference} " ) response = client.responses.create( model=MODEL, instructions=INSTRUCTIONS, input =user_input, ) data = json.loads(response.output_text) return validate(data, days) if __name__ == "__main__" : if len (sys.argv) != 4 : raise SystemExit("用法:python itinerary.py 目的地 天数 偏好" ) result = plan(sys.argv[1 ], int (sys.argv[2 ]), sys.argv[3 ]) print (json.dumps(result, ensure_ascii=False , indent=2 ))
运行示例:
1 python itinerary.py "杭州" 2 "喜欢博物馆和步行,节奏不要太紧"
这里没有预先写死一份“模型应该返回的结果”,因为真实文本会受模型、提示词和服务状态影响。可验证的结果包括:进程是否正常结束、输出能否被 JSON 解析、每天是否恰好有一项,以及字段是否通过 validate。如果服务支持更严格的结构化输出能力,可以按其当前官方文档配置;即使如此,业务字段仍值得在本地校验。
为什么校验不能只靠提示词 提示词能够描述期望,却不是 Python 类型系统。模型可能加上解释文字、遗漏某天、把天数写成字符串,或者返回一个看似合理但与输入天数不同的计划。json.loads 负责语法解析,validate 负责项目自己的数据契约,两者解决的是不同问题。
如果解析或校验失败,不应直接把原始文本展示为“成功结果”。可以记录错误类型和请求标识,随后有限次重试,或提示用户人工整理。重试也不应无限进行,否则格式问题会变成额外费用和延迟。生产程序还应捕获认证失败、超时和限流错误,并对日志中的目的地、偏好等可能含有个人信息的内容进行脱敏。
给项目加一个离线测试 不调用模型也能测试大部分逻辑。新建 test_itinerary.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 import unittestfrom itinerary import validateclass ItineraryTest (unittest.TestCase): def test_valid_plan (self ): data = { "destination" : "杭州" , "days" : 1 , "itinerary" : [ {"day" : 1 , "places" : ["西湖" ], "note" : "出发前核实开放信息" } ], } self .assertEqual(validate(data, 1 )["days" ], 1 ) def test_day_count_must_match (self ): data = {"destination" : "杭州" , "days" : 2 , "itinerary" : []} with self .assertRaises(ValueError): validate(data, 2 ) if __name__ == "__main__" : unittest.main()
运行:
1 python -m unittest -v test_itinerary.py
这组测试不验证模型“聪不聪明”,而是验证程序是否守住数据边界。真实 API 调用属于集成测试,应单独运行并控制次数;如果要测试 plan,可以把客户端作为参数传入,再注入一个返回固定 output_text 的假客户端,避免每次测试都消耗额度。
常见问题 为什么不直接让模型输出 Markdown? Markdown 适合人阅读,但不便于稳定检查字段和天数。先得到结构化数据,再由程序渲染成文本,通常更容易测试和扩展。
模型推荐的地点可靠吗? 不一定。这个工具生成的是草案,不是事实核验服务。营业时间、预约要求、交通变化和价格都应通过可靠的实时来源确认。
输入偏好里能不能放隐私? 尽量不要。把最少必要信息发送给模型,并在日志中避免保存完整原文。使用第三方服务前,还要确认数据保留和合规政策。
小结 这个小项目把一次模型调用变成了一个有边界的 Python 功能:输入被限制,提示词声明职责,模型返回候选 JSON,程序解析并校验,离线测试负责守住回归。真正可维护的 AI 应用并不是让模型独自完成全部工作,而是让模型擅长的自然语言生成与代码擅长的确定性验证彼此配合。后续扩展时,可以加入用户确认、可靠信息源和成本统计,但不要跳过输入校验与失败处理这两个基础环节。