基础路线已经覆盖了 API 调用、消息组织、提示词、结构化输出、错误处理、日志和评估。本篇用一个独立的小项目把这些知识串起来:输入目的地、天数和偏好,模型返回一个可被 Python 解析和检查的旅行行程草案。项目不查询实时价格、不购买票券,也不把模型输出直接当成最终计划,重点是练习“模型生成,程序约束”的协作边界。

先定义输入和边界

行程规划看似只是让模型写一段文字,但自然语言结果很难被程序继续使用。本项目规定输出必须包含 destination、days 和 itinerary 三个字段;每天的安排包含日期序号、地点列表和备注。程序还会检查天数范围、数组长度和字符串类型。

边界同样重要:模型只能提出草案,不能声称已经完成预订,也不能编造实时营业时间、库存和价格。涉及签证、交通、天气或安全的内容,应在后续由用户和可靠来源确认。把这些限制写进提示词是第一层防线,代码校验是第二层防线。

准备环境和配置

在独立虚拟环境中安装官方 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="替换为你可用的模型名称"

不要把真实密钥写进源码、配置文件或文章。模型名不是所有服务都相同,运行前应查看所用服务的官方文档;下面的代码沿用 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 json
import os
import sys

from openai import OpenAI

MODEL = 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 unittest

from itinerary import validate


class 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 应用并不是让模型独自完成全部工作,而是让模型擅长的自然语言生成与代码擅长的确定性验证彼此配合。后续扩展时,可以加入用户确认、可靠信息源和成本统计,但不要跳过输入校验与失败处理这两个基础环节。