前面的系列已经覆盖了模型调用、提示词、结构化输出、错误处理、评估和安全边界。本篇把这些知识组合成一个小项目:读取两份文字方案,让模型提取差异、标出未知信息,并给出带证据编号的比较结果。项目不让模型直接替人做不可逆决定,而是把它限制在“整理和解释输入材料”这一步,最终结果仍由程序校验和人来判断。

先定义可复核的结果

方案比较最容易出错的地方,是模型说出输入材料里没有的优点。为此,程序先给两份文本的每一行编号:方案 A 使用 A#1、A#2,方案 B 使用 B#1、B#2。模型比较每个维度时,必须引用这些编号;程序再检查引用是否真实存在。

输出只允许包含四个字段:

  • summary:一句话概括主要差异。
  • criteria:比较维度列表,每项有名称、胜出方和证据编号。
  • unknowns:材料中无法判断、需要补充的信息。
  • recommendation:只能是 A、B、tie 或 need_more_info。

这个设计并不能证明模型的结论一定正确,但它把“凭空补充事实”的风险变成了可以检测的格式错误,也让读者能够沿着编号回看原文。

创建最小项目

当前 OpenAI Python SDK 的基本调用方式是 Responses API。先创建虚拟环境并安装官方 SDK:

1
2
3
4
5
python -m venv .venv
source .venv/bin/activate
python -m pip install -U openai
export OPENAI_API_KEY="替换为你的真实密钥"
export AI_MODEL="替换为你可用的模型名称"

密钥只通过环境变量读取,不要写进 Python 文件、提交到 Git,也不要把完整请求内容写入公共日志。如果使用兼容接口,可额外设置 OPENAI_BASE_URL;程序会在该变量存在时传给客户端。

编写方案比较器

把下面代码保存为 compare.py。它没有依赖 Web 框架,输入是两个文本文件,输出是经过校验的 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
79
80
81
82
import json
import os
import re
import sys
from pathlib import Path

from openai import OpenAI


def numbered(label: str, text: str) -> tuple[str, int]:
lines = [line.strip() for line in text.splitlines() if line.strip()]
return "\n".join(f"{label}#{i}: {line}" for i, line in enumerate(lines, 1)), len(lines)


def validate_result(result: dict, counts: dict[str, int]) -> dict:
required = {"summary", "criteria", "unknowns", "recommendation"}
if set(result) != required:
raise ValueError("结果字段不完整,或包含未允许的字段")
if not isinstance(result["summary"], str) or not result["summary"].strip():
raise ValueError("summary 必须是非空字符串")
if result["recommendation"] not in {"A", "B", "tie", "need_more_info"}:
raise ValueError("recommendation 不是允许的值")
if not isinstance(result["criteria"], list) or not result["criteria"]:
raise ValueError("criteria 必须是非空列表")
reference_pattern = re.compile(r"^([AB])#([1-9][0-9]*)$")
for item in result["criteria"]:
if set(item) != {"name", "winner", "evidence"}:
raise ValueError("比较维度字段不正确")
if item["winner"] not in {"A", "B", "tie", "unknown"}:
raise ValueError("winner 不是允许的值")
if not isinstance(item["evidence"], list) or not item["evidence"]:
raise ValueError("每个比较维度都必须有证据编号")
for reference in item["evidence"]:
match = reference_pattern.fullmatch(reference)
if not match or int(match.group(2)) > counts[match.group(1)]:
raise ValueError(f"不存在的证据编号:{reference}")
if not isinstance(result["unknowns"], list) or not all(
isinstance(item, str) for item in result["unknowns"]
):
raise ValueError("unknowns 必须是字符串列表")
return result


def main() -> None:
if len(sys.argv) != 3:
raise SystemExit("用法:python compare.py plan_a.txt plan_b.txt")

text_a = Path(sys.argv[1]).read_text(encoding="utf-8").strip()
text_b = Path(sys.argv[2]).read_text(encoding="utf-8").strip()
if not text_a or not text_b:
raise SystemExit("两份方案都不能为空")

plan_a, count_a = numbered("A", text_a)
plan_b, count_b = numbered("B", text_b)
instructions = """
你是一个谨慎的方案比较助手。只能根据用户提供的方案文本作比较,不能补充外部事实。
返回严格 JSON,不要 Markdown,不要解释 JSON 之外的内容。
JSON 必须包含 summary、criteria、unknowns、recommendation 四个字段。
criteria 的每项必须包含 name、winner、evidence;winner 只能是 A、B、tie、unknown。
证据必须使用输入中真实存在的行号,例如 A#1 或 B#2,不得创造编号。
如果信息不足,winner 使用 unknown,recommendation 使用 need_more_info。
""".strip()
prompt = f"方案 A:\n{plan_a}\n\n方案 B:\n{plan_b}"

client_options = {"api_key": os.environ["OPENAI_API_KEY"]}
if os.environ.get("OPENAI_BASE_URL"):
client_options["base_url"] = os.environ["OPENAI_BASE_URL"]
client = OpenAI(**client_options)
response = client.responses.create(
model=os.environ["AI_MODEL"],
instructions=instructions,
input=prompt,
text={"format": {"type": "json_object"}},
)

result = json.loads(response.output_text)
checked = validate_result(result, {"A": count_a, "B": count_b})
print(json.dumps(checked, ensure_ascii=False, indent=2))


if __name__ == "__main__":
main()

numbered 是确定性预处理:同一份输入每次都会得到相同编号。responses.create 负责生成候选比较结果,response.output_text 取出文本后,程序先用 json.loads 解析,再调用 validate_result 检查字段、枚举值和证据引用。只有通过校验的对象才会打印出来;这里没有把模型返回值直接当成可信数据。

准备两份测试材料:

1
2
3
printf '月费 99 元\n包含基础报表\n不支持导出原始数据\n' > plan_a.txt
printf '月费 129 元\n包含基础报表和原始数据导出\n需要单独购买多人协作\n' > plan_b.txt
python compare.py plan_a.txt plan_b.txt

实际输出会随模型和输入变化,不能预先写死。你可以检查每个 evidence 是否能在对应文件中找到,也可以故意把返回结果中的某个编号改成 A#99,确认校验器会拒绝它。这样测试的是程序的边界,而不是某一次模型恰好给出的句子。

常见问题

有 JSON 格式还需要校验吗?

需要。JSON 只说明文本能被解析,不代表字段完整、枚举值正确或证据存在。text.format 可以减少格式偏差,但业务规则仍应由普通 Python 代码执行。

为什么不让模型直接选出最好的方案?

“最好”通常依赖预算、合规、团队规模等未必出现在材料中的条件。比较器只输出差异、证据和未知项,把最终选择留给人,能避免把缺失信息伪装成确定结论。

证据编号能保证内容没有被误解吗?

不能。编号只能证明模型引用了哪一行,不能证明它正确理解了这一行。重要场景仍要人工复核,必要时把每个比较维度拆成独立测试样例。

如何处理 API 失败?

本示例先保持主流程最小。实际使用时,应围绕网络超时、限流和服务端错误增加有限重试,并把解析失败与业务校验失败分开记录;认证失败和输入为空通常不应盲目重试。

小结

这个项目展示了一条可复核的 AI 数据流:给输入建立稳定边界,让模型生成带证据的候选结果,再由程序执行确定性校验。模型适合做文本理解和差异归纳,编号、字段检查和失败处理应交给 Python。以后无论把输入换成合同、配置说明还是产品方案,都可以沿用“证据引用 + 结构校验 + 人工决定”的边界,而不是把模型输出直接当成事实或操作指令。