前面的基础主题已经覆盖了 API 调用、消息管理、结构化输出、错误处理、评估和工程边界。本篇用一个小项目把这些知识串起来:读取一条网页书签,让模型提取标题、摘要、标签和分类,再由 Python 校验结果并生成 Markdown。它不是“让模型直接改文件”,而是把模型当作候选内容生成器,把格式和写入权限留在程序中。

先划清输入和输出

程序接收一个 JSON 文件,内容只包括网页标题、URL 和用户备注;输出是一个 Markdown 文件。模型可以建议分类和标签,但不能改变 URL,也不能写入任意路径。这样即使模型返回了奇怪内容,程序仍然能在保存前拒绝它。

我们采用如下数据协议:

1
2
3
4
5
{
"title": "Python pathlib 实用指南",
"url": "https://example.com/pathlib",
"note": "整理路径拼接、文件读取和目录遍历的要点"
}

为了让示例可以迁移,模型名和密钥均从环境变量读取。请在虚拟环境中安装官方 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="替换为你可用的模型名称"

不要把密钥写进源码、JSON 或 Git。示例的 URL 使用占位地址,不代表程序会真的抓取网页;书签内容由本地输入提供。

让模型只负责提议

Responses API 可以用 instructions 描述任务,并用 text.format 请求 JSON Schema 形式的结构化输出。下面的 Schema 是程序和模型之间的契约:

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
import json
import os
from pathlib import Path

from openai import OpenAI

SCHEMA = {
"type": "object",
"properties": {
"summary": {"type": "string"},
"category": {"type": "string"},
"tags": {"type": "array", "items": {"type": "string"}},
},
"required": ["summary", "category", "tags"],
"additionalProperties": False,
}


def classify(bookmark: dict) -> dict:
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
response = client.responses.create(
model=os.environ["MODEL_NAME"],
instructions=(
"你是书签整理助手。只根据输入内容提出分类建议。"
"摘要不超过80字,标签使用简短中文词语。"
),
input=json.dumps(bookmark, ensure_ascii=False),
text={"format": {
"type": "json_schema",
"name": "bookmark_metadata",
"strict": True,
"schema": SCHEMA,
}},
)
return json.loads(response.output_text)

response.output_text 是 SDK 汇总后的文本,json.loads 把它转换为 Python 字典。结构化输出能降低格式漂移,但不能替代业务校验:模型仍可能给出过长摘要、空标签或不符合站点分类约定的内容。

在写文件前做校验

校验函数只接受预期类型,并限制长度和数量。错误直接抛出,让调用方决定是否重试或人工修改,而不是悄悄修剪模型结果:

1
2
3
4
5
6
7
8
9
10
11
12
def validate_metadata(data: dict) -> None:
if not isinstance(data.get("summary"), str):
raise ValueError("summary 必须是字符串")
if not 1 <= len(data["summary"]) <= 80:
raise ValueError("summary 长度必须在 1 到 80 之间")
if data.get("category") not in {"Python", "AI", "工具"}:
raise ValueError("category 不在允许列表中")
tags = data.get("tags")
if not isinstance(tags, list) or not 1 <= len(tags) <= 5:
raise ValueError("tags 必须是 1 到 5 个标签")
if not all(isinstance(tag, str) and 1 <= len(tag) <= 20 for tag in tags):
raise ValueError("每个标签都必须是短字符串")

生成 Markdown 时,路径和 URL 使用本地输入,模型只能提供元数据。json.dumps 还可以作为日志的安全序列化方式,但生产环境仍应根据敏感程度脱敏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def render(bookmark: dict, metadata: dict) -> str:
validate_metadata(metadata)
tags = " ".join(f"`{tag}`" for tag in metadata["tags"])
return (
f"# {bookmark['title']}\n\n"
f"- 分类:{metadata['category']}\n"
f"- 标签:{tags}\n"
f"- 摘要:{metadata['summary']}\n"
f"- 链接:[{bookmark['title']}]({bookmark['url']})\n\n"
f"> 备注:{bookmark.get('note', '')}\n"
)


def main() -> None:
source = Path("bookmark.json")
bookmark = json.loads(source.read_text(encoding="utf-8"))
metadata = classify(bookmark)
output = render(bookmark, metadata)
Path("bookmark.md").write_text(output, encoding="utf-8")
print("已生成 bookmark.md")

运行 python bookmarker.py 后,真正可验证的结果是:进程成功退出、bookmark.md 存在、URL 与输入一致、内容符合校验规则。不要把某次模型回复硬编码成预期输出,因为模型服务、模型版本和输入都会影响自然语言文本。

加上有限重试和日志

网络请求可能因超时或限流失败。重试必须有上限,并逐步增加等待时间;认证失败和输入校验失败通常不应盲目重试。最小实现可以把调用包在循环中:

1
2
3
4
5
6
7
8
9
10
11
12
13
import time


def classify_with_retry(bookmark: dict, attempts: int = 3) -> dict:
last_error = None
for attempt in range(attempts):
try:
return classify(bookmark)
except Exception as error:
last_error = error
if attempt + 1 < attempts:
time.sleep(2 ** attempt)
raise RuntimeError(f"模型调用失败,已尝试 {attempts} 次") from last_error

实际项目应使用 SDK 文档列出的具体异常类型,分别处理超时、限流和认证错误,并记录请求耗时、尝试次数和错误类别。日志不要写入 API 密钥,也不要默认保存包含个人信息的完整输入。指数退避不是保证成功的魔法,只是避免在服务暂时拥堵时立即连续撞击接口。

常见问题

为什么有了 JSON Schema 还要校验? Schema 主要约束返回形状,业务规则如允许的分类、长度和标签数量仍需由应用决定。两层校验解决的是不同问题。

模型能不能直接抓取 URL? 本例不允许。网络抓取涉及权限、超时、 robots 规则和不可信网页内容。若未来增加抓取工具,应把抓取和整理拆成独立步骤,并限制域名、大小和超时。

失败时是否覆盖原来的 Markdown? 不应覆盖。先在内存中完成请求、解析和校验,全部成功后再一次写入临时文件并替换目标文件;更重要的资料还应保留备份或走人工审批。

如何测试而不消耗 API 额度? 把 classify 设计成可注入函数,测试时传入固定的假实现,覆盖合法结果、缺字段、超长摘要和非法分类等情况。真实 API 只保留少量集成测试。

小结

这个书签整理器展示了一条可靠的 AI 应用边界:模型负责理解和提出建议,Python 负责协议校验、重试、日志和文件写入。结构化输出让接口更容易检查,有限重试应对暂时性网络问题,而输入快照与固定测试让结果可以回归验证。继续扩展时,可以增加人工确认、批量处理和评估数据集,但每一步都应保持“先验证,后产生副作用”的顺序。