前面的基础主题已经覆盖了 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/activatepython -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 jsonimport osfrom pathlib import Pathfrom openai import OpenAISCHEMA = { "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 timedef 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 负责协议校验、重试、日志和文件写入。结构化输出让接口更容易检查,有限重试应对暂时性网络问题,而输入快照与固定测试让结果可以回归验证。继续扩展时,可以增加人工确认、批量处理和评估数据集,但每一步都应保持“先验证,后产生副作用”的顺序。