前面的系列已经覆盖了模型调用、上下文、生成参数、结构化输出、错误处理、评估和可观测性。本篇用一个小项目把这些知识串起来:读取本地 Markdown 文件,请模型生成摘要,再经过程序校验后写入结果文件,同时记录每次调用的状态。它不是新的框架教程,而是一次工程化练习,帮助你看清“输入、调用、验证、落盘、复盘”的完整链路。
先确定边界和数据流
工具只处理一个目录中的 .md 文件,不执行文件里的代码,也不把摘要直接覆盖原文。每篇文章生成一个 JSON 对象,包含源文件名、摘要和关键点;校验通过后写入 summaries.jsonl,每次请求的成功或失败写入 run.log。模型只负责生成候选文本,文件读写、格式检查和失败重试都由 Python 控制。
数据流可以画成五步:读取文件 → 组装提示词 → 调用 Responses API → 校验摘要 → 保存结果。把模型放在中间而不是让它控制整个流程,后续才能替换模型、增加人工审核或接入数据库。
准备环境和配置
建议在独立虚拟环境中安装官方 Python SDK 和环境变量加载库:
1 2 3
| python -m venv .venv source .venv/bin/activate python -m pip install openai python-dotenv
|
在项目目录创建 .env,只放占位配置;真实密钥由你自己的环境提供,不能提交到 Git:
1 2 3
| OPENAI_API_KEY=替换为你的真实密钥 MODEL_NAME=替换为你可用的模型名称 OPENAI_BASE_URL=
|
下面示例采用 OpenAI Python SDK 当前文档推荐的 Responses API。使用兼容接口时,可以按服务商文档填写 OPENAI_BASE_URL,并确认它支持该接口及所选模型。
编写最小可运行程序
创建 summarize.py,把待处理文件放到 notes/ 目录,然后运行 python summarize.py notes:
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 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| import json import os import sys import time from pathlib import Path
from dotenv import load_dotenv from openai import OpenAI
load_dotenv() MODEL = os.environ["MODEL_NAME"] client = OpenAI( api_key=os.environ.get("OPENAI_API_KEY"), base_url=os.environ.get("OPENAI_BASE_URL") or None, )
INSTRUCTIONS = ( "你是中文技术编辑。根据用户提供的 Markdown 原文生成摘要。" "只返回一个 JSON 对象,字段为 summary 和 key_points。" "summary 是 80 到 160 字的客观摘要,key_points 是 2 到 4 条简短要点。" "不要补充原文没有的事实。" )
def call_model(text: str) -> str: last_error = None for attempt in range(3): try: response = client.responses.create( model=MODEL, instructions=INSTRUCTIONS, input=text, ) if not response.output_text: raise ValueError("模型返回了空内容") return response.output_text except Exception as exc: last_error = exc if attempt < 2: time.sleep(2**attempt) raise RuntimeError(f"调用失败:{last_error}") from last_error
def validate(raw: str) -> dict: try: result = json.loads(raw) except json.JSONDecodeError as exc: raise ValueError("返回内容不是合法 JSON") from exc if set(result) != {"summary", "key_points"}: raise ValueError("字段不符合约定") if not isinstance(result["summary"], str): raise ValueError("summary 必须是字符串") points = result["key_points"] if not isinstance(points, list) or not 2 <= len(points) <= 4: raise ValueError("key_points 必须包含 2 到 4 项") if not all(isinstance(item, str) and item.strip() for item in points): raise ValueError("要点必须是非空字符串") return result
def main() -> None: if len(sys.argv) != 2: raise SystemExit("用法:python summarize.py notes") source_dir = Path(sys.argv[1]) files = sorted(source_dir.glob("*.md")) if not files: raise SystemExit("目录中没有 Markdown 文件")
with open("summaries.jsonl", "a", encoding="utf-8") as output, open( "run.log", "a", encoding="utf-8" ) as log: for path in files: event = {"file": path.name} try: result = validate(call_model(path.read_text(encoding="utf-8"))) record = {"file": path.name, **result} output.write(json.dumps(record, ensure_ascii=False) + "\n") event["status"] = "ok" except (OSError, RuntimeError, ValueError) as exc: event.update(status="error", error=str(exc)) log.write(json.dumps(event, ensure_ascii=False) + "\n") log.flush() print(event)
if __name__ == "__main__": main()
|
这里的 responses.create 是一次请求;instructions 放稳定的编辑规则,input 放本次文件内容,output_text 是 SDK 提供的文本汇总属性。call_model 只负责网络调用和有限重试,validate 只负责解析和约束,main 负责文件和日志。职责分开后,测试 validate 时不需要消耗 API 配额。
如何验证结果而不是只看屏幕输出
首次运行前,可以先准备一篇很短的 notes/demo.md。程序真实调用模型后,summaries.jsonl 每行应是一个 JSON 对象,run.log 会记录 ok 或 error。摘要内容、模型延迟和费用都可能变化,不能把某次回复当成固定测试结果。
最低限度的验证包括三项。第一,用 Python 读取 summaries.jsonl,确认每行都能解析且字段完整;第二,故意把 validate 的输入替换成缺字段、非法 JSON 和空要点,确认程序拒绝它们;第三,临时把模型名改成不可用值,确认失败会记录日志,并且不会写入一条伪造的成功结果。真实项目还应保存一组人工审核过的样例,比较摘要是否遗漏主题、混入原文没有的结论。
示例按追加模式写入结果,因此重复运行会产生重复记录。生产版本应为每个源文件保存内容哈希,或先写临时文件再原子替换;若请求在写入后进程崩溃,也要用唯一 ID 或幂等策略避免重复。重试只包住模型调用,没有包住落盘动作,正是为了降低重复写文件的风险。
常见问题
为什么提示词要求 JSON,却还要校验? 提示词是约束,不是类型系统。模型可能输出 Markdown 围栏、缺少字段或返回错误类型,解析和字段检查才是程序的信任边界。
为什么不把整个目录一次发给模型? 文件多或内容长时容易超过上下文限制,也难以定位失败文件。逐文件处理更容易重试、限流、记录和估算成本;后续可再增加分块摘要。
所有异常都应该重试吗? 不应该。示例为了短小捕获了通用异常,实际应用应区分认证失败、参数错误、限流、超时和服务端错误;密钥无效和提示词参数错误通常重试没有意义。
日志里能不能记录原文和密钥? 不要。日志只记录文件名、状态、错误摘要和必要的请求标识,绝不写入 API 密钥;原文也可能包含个人信息,应按敏感等级脱敏或限制访问。
小结
这个摘要器的重点不是“让模型写一段话”,而是把一次不确定的生成请求放进可检查的程序流程:环境变量管理密钥,有限重试应对暂时性故障,JSON 校验阻止坏数据进入结果,日志和样例让问题能够复盘。综合项目完成后,继续扩展时应一次只增加一个能力,例如并发、人工审核或成本统计,并为新增行为补一条可重复验证的测试。这样,前面学过的 API、提示词、可靠性和评估知识才能真正落到代码里。