前面的系列已经分别介绍了模型调用、上下文、提示词、结构化输出、错误处理、安全边界和评估。本篇在路线完成后做一个新的综合练习:输入一段原文,让模型提出改写稿和修改说明,程序负责校验结果、保存审计记录,并把最终决定留给人。这个项目不追求“自动发布”,而是练习如何把不确定的模型输出放进可追踪、可回退的程序流程。

先定义输入、输出和边界

工具只做一种改写:把技术说明改得更清晰,但不改变事实、数字、命令和专有名词。模型返回三个字段:rewritten_text 是改写稿,changes 是修改点数组,warnings 是模型认为需要人工确认的事项。程序还会保存输入原文、输出结果、模型名和时间戳。

这里有三个重要边界。第一,原文只读,改写结果写入新文件,失败或不满意时可以直接丢弃。第二,模型不能自行补充未提供的事实,提示词中的要求仍然不是安全边界,所以程序会做基本校验。第三,模型结果只是建议,是否采用必须由人确认;命令行工具不会执行发布、发送或覆盖操作。

准备环境

在独立虚拟环境中安装官方 Python SDK 和环境变量加载库:

1
2
3
python -m venv .venv
source .venv/bin/activate
python -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 json
import os
import sys
from datetime import datetime, timezone
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 = """
你是技术文档编辑。只返回 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 json
from pathlib import Path

record = 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 负责协议、长度、文件和副作用边界。综合项目的重点不是让模型自动完成更多动作,而是让每个动作都能被检查、追踪和回退。