前面的系列已经分别介绍了模型调用、上下文、提示词、结构化输出、错误处理、安全边界和评估。本篇在路线完成后做一个新的综合练习:输入一段原文,让模型提出改写稿和修改说明,程序负责校验结果、保存审计记录,并把最终决定留给人。这个项目不追求“自动发布”,而是练习如何把不确定的模型输出放进可追踪、可回退的程序流程。
先定义输入、输出和边界 工具只做一种改写:把技术说明改得更清晰,但不改变事实、数字、命令和专有名词。模型返回三个字段:rewritten_text 是改写稿,changes 是修改点数组,warnings 是模型认为需要人工确认的事项。程序还会保存输入原文、输出结果、模型名和时间戳。
这里有三个重要边界。第一,原文只读,改写结果写入新文件,失败或不满意时可以直接丢弃。第二,模型不能自行补充未提供的事实,提示词中的要求仍然不是安全边界,所以程序会做基本校验。第三,模型结果只是建议,是否采用必须由人确认;命令行工具不会执行发布、发送或覆盖操作。
准备环境 在独立虚拟环境中安装官方 Python SDK 和环境变量加载库:
1 2 3 python -m venv .venv source .venv/bin/activatepython -m pip install openai python-dotenv
在项目目录创建 .env,密钥和模型名只通过环境变量提供:
1 2 3 OPENAI_API_KEY=替换为你的真实密钥 MODEL_NAME=替换为你可用的模型名称 OPENAI_BASE_URL=
不要把 .env 提交到 Git。只有在服务商文档明确说明兼容时才设置 OPENAI_BASE_URL。下面的程序使用 OpenAI Python SDK 当前的 Responses API 写法;如果你使用其他服务,先以该服务的官方文档为准确认接口和返回字段。
编写最小改写器 新建 rewriter.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 66 67 68 69 70 71 72 73 74 75 76 77 78 import jsonimport osimport sysfrom datetime import datetime, timezonefrom pathlib import Pathfrom dotenv import load_dotenvfrom openai import OpenAIload_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 = """ 你是技术文档编辑。只返回 JSON 对象,不要 Markdown 围栏或额外解释。 字段必须是 rewritten_text、changes、warnings。 rewritten_text 只能改进输入的清晰度,不能改变数字、命令、事实或专有名词,长度不超过 4000 字。 changes 是字符串数组,最多 10 项;warnings 是字符串数组,最多 10 项。 如果无法确认某个事实,把风险写入 warnings,不要自行补全。 """ .strip()def validate_result (raw: str ) -> dict : try : value = json.loads(raw) except json.JSONDecodeError as exc: raise ValueError("模型返回的不是合法 JSON" ) from exc required = {"rewritten_text" , "changes" , "warnings" } if set (value) != required: raise ValueError("返回字段不完整或包含未知字段" ) if not isinstance (value["rewritten_text" ], str ): raise ValueError("rewritten_text 必须是字符串" ) if not value["rewritten_text" ].strip() or len (value["rewritten_text" ]) > 4000 : raise ValueError("改写稿为空或超过长度限制" ) for name in ("changes" , "warnings" ): items = value[name] if not isinstance (items, list ) or len (items) > 10 : raise ValueError(f"{name} 必须是不超过 10 项的数组" ) if any (not isinstance (item, str ) or not item.strip() for item in items): raise ValueError(f"{name} 中存在无效说明" ) return value def rewrite (source: str ) -> dict : if not source.strip() or len (source) > 4000 : raise ValueError("输入为空或超过 4000 字" ) response = client.responses.create( model=MODEL, instructions=INSTRUCTIONS, input =source, ) return validate_result(response.output_text) def main () -> None : if len (sys.argv) != 3 : raise SystemExit("用法:python rewriter.py input.txt result.json" ) source_path, result_path = map (Path, sys.argv[1 :]) original = source_path.read_text(encoding="utf-8" ) suggestion = rewrite(original) record = { "created_at" : datetime.now(timezone.utc).isoformat(), "model" : MODEL, "source" : original, **suggestion, } result_path.write_text( json.dumps(record, ensure_ascii=False , indent=2 ) + "\n" , encoding="utf-8" , ) print (f"已生成建议,请人工确认后再使用:{result_path} " ) if __name__ == "__main__" : main()
运行 python rewriter.py input.txt result.json。responses.create 负责请求,output_text 取出文本结果;但程序没有直接信任它,而是先检查 JSON、字段集合、字符串类型和数量限制,最后才写入新文件。source 和 rewritten_text 同时保存在审计记录中,后续可以比较两者,也能知道这次使用了哪个模型。
为什么要保留审计记录 只保存改写稿会丢失三个问题的答案:原文是什么、模型为什么这样改、这次运行使用了什么配置。审计记录不一定要很复杂,至少应包含输入、输出、时间、模型、提示词版本和人工决定。正式系统还可以增加请求编号、错误信息和成本数据。
示例把原文直接写进 JSON,是为了突出流程。若原文包含密码、个人信息或内部代码,应在发送前按业务规则脱敏,并评估第三方服务的数据处理政策。脱敏不能只依赖提示词;程序要明确哪些字段禁止发送,必要时在调用前直接拒绝。
给“人工确认”一个明确状态 生成结果后不要马上覆盖原文件。可以把 result.json 交给人工检查,确认以下项目:事实和数字是否保持不变,命令是否仍可复制,是否出现原文没有的结论,warnings 是否已经处理。确认后再由另一个明确的脚本把 rewritten_text 导出到目标位置;这个导出动作应当是单独的、有备份的,并记录操作者和时间。
如果暂时只做命令行练习,可以用下面的检查代码快速查看待确认内容:
1 2 3 4 5 6 7 8 9 10 11 12 import jsonfrom pathlib import Pathrecord = json.loads(Path("result.json" ).read_text(encoding="utf-8" )) print ("=== 改写稿 ===" )print (record["rewritten_text" ])print ("=== 修改点 ===" )for item in record["changes" ]: print ("-" , item) print ("=== 风险提示 ===" )for item in record["warnings" ]: print ("-" , item)
这段代码只读取和展示建议,不会自动发布。把“生成”和“采用”拆成两个阶段,是降低误改风险的关键。
常见问题 模型仍然改动了事实怎么办? 把输入和输出交给确定性检查,例如提取数字、命令片段或版本号后逐项比较;发现不一致就标记为失败,不要只依赖人工浏览。对于复杂事实,仍需要领域人员审核。
为什么不让模型返回 Markdown? Markdown 适合展示,却不适合作为程序协议。先返回可解析的数据,再由程序或模板负责展示,可以减少围栏、说明文字和字段缺失带来的解析问题。JSON 校验也不能证明内容正确,只能保护结构。
调用失败时会不会留下半个结果? 当前写入发生在请求和校验成功之后;网络异常会让程序抛错,不会写出新的结果文件。若目标文件已经存在,生产代码还应使用临时文件和原子替换,避免进程中断造成半成品。
如何测试而不消耗 API 额度? 把 validate_result 单独测试,准备合法 JSON、缺字段、未知字段、超长文本和非法数组等样例。rewrite 可以通过依赖注入替换成返回固定 JSON 的假客户端,测试流程逻辑,而不是每次都调用真实模型。
小结 这个小项目把一次改写请求变成了一个可审计的建议流程:输入和原文保持不变,模型输出经过本地校验,结果带有时间与模型信息,最终采用由人工决定。模型适合提出语言层面的候选方案,Python 负责协议、长度、文件和副作用边界。综合项目的重点不是让模型自动完成更多动作,而是让每个动作都能被检查、追踪和回退。