基础路线完成后,可以用一个小项目练习“模型生成、程序约束、人来复核”的完整闭环。本篇构建一个研究笔记整理器:输入一段资料,模型提取主题、要点和原文证据,Python 校验每个结论是否确实绑定了证据,最后保存为 JSON。它不负责搜索网页,也不把模型意见当作事实,重点是学习如何让 AI 输出可追踪、可检查。

先定义数据边界

输入是一段已经由用户提供的 Markdown 或纯文本。输出包含标题、摘要和要点列表。每个要点必须有 claim(整理后的陈述)、evidence(资料中的原文摘录)和 confidence(low、medium、high)。程序还会检查证据长度,并要求证据原文出现在输入文本中。

这里的“证据”只是可追溯的文本片段,不等于外部事实核验。模型可能误读资料,也可能选择不能充分支持结论的句子,因此最终笔记仍需要人工阅读。把这条边界写进提示词和校验代码,比笼统要求“不要幻觉”更可靠。

准备环境和密钥

使用独立虚拟环境安装官方 Python SDK。SDK 的具体参数和模型名称会随服务变化,实际使用前应以服务商当前文档为准:

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

密钥只从环境变量读取,不能写进源码、笔记或 Git。下面示例采用 OpenAI Python SDK 的 Responses API,并读取响应对象的 output_text;如果使用兼容服务,应确认它支持同样的接口和返回字段。

编写最小整理器

新建 note_organizer.py,先让模型返回约定的 JSON,再由本地代码解析和校验。模型负责语言理解,validate 负责程序可以确认的事实:字段类型、枚举值、数量和证据是否来自原文。

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
import json
import os
import sys
from pathlib import Path

from openai import OpenAI

MODEL = os.environ["MODEL_NAME"]
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
CONFIDENCE = {"low", "medium", "high"}


INSTRUCTIONS = """
你是研究笔记整理助手。只根据用户提供的资料整理,不要补充外部知识。
只返回 JSON,不要 Markdown 代码围栏,格式必须是:
{"title":"不超过40字","summary":"不超过120字","points":[
{"claim":"不超过100字的陈述","evidence":"资料中的连续原文摘录",
"confidence":"low|medium|high"}
]}
每条 claim 都必须能被 evidence 直接支持;资料没有说过的内容不要推断。
最多返回 8 条 points;没有可靠证据的内容不要列出。
""".strip()


def validate(result: object, source: str) -> None:
if not isinstance(result, dict):
raise ValueError("顶层结果必须是对象")
if set(result) != {"title", "summary", "points"}:
raise ValueError("顶层字段不符合约定")
if not isinstance(result["title"], str) or not result["title"]:
raise ValueError("title 必须是非空字符串")
if not isinstance(result["summary"], str):
raise ValueError("summary 必须是字符串")
points = result["points"]
if not isinstance(points, list) or len(points) > 8:
raise ValueError("points 必须是最多 8 项的数组")
for index, point in enumerate(points, start=1):
if not isinstance(point, dict):
raise ValueError(f"第 {index} 条要点不是对象")
if set(point) != {"claim", "evidence", "confidence"}:
raise ValueError(f"第 {index} 条字段不符合约定")
if not all(isinstance(point[key], str) and point[key].strip()
for key in ("claim", "evidence")):
raise ValueError(f"第 {index} 条的 claim 或 evidence 为空")
if point["confidence"] not in CONFIDENCE:
raise ValueError(f"第 {index} 条 confidence 无效")
if point["evidence"] not in source:
raise ValueError(f"第 {index} 条 evidence 不在原文中")


def organize(source: str) -> dict:
response = client.responses.create(
model=MODEL,
instructions=INSTRUCTIONS,
input=source,
)
result = json.loads(response.output_text)
validate(result, source)
return result


def main() -> None:
if len(sys.argv) != 2:
raise SystemExit("用法:python note_organizer.py notes.md")
source_path = Path(sys.argv[1])
source = source_path.read_text(encoding="utf-8")
if not source.strip():
raise SystemExit("输入文件为空")
result = organize(source)
output_path = source_path.with_suffix(".organized.json")
output_path.write_text(
json.dumps(result, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
print(f"已写入 {output_path}")


if __name__ == "__main__":
main()

运行方式如下:

1
python note_organizer.py notes.md

示例程序只打印实际写入的文件路径,不伪造模型返回内容。为了在没有密钥时也能测试 validate,可以单独写一个本地测试:把 organize 替换为固定字典,传入包含该 evidence 的文本,确认合法结果能通过;再把证据改成原文不存在的句子,确认程序抛出 ValueError。这样测试的是不依赖网络的安全边界。

为什么要同时使用提示词和校验

提示词中的 JSON 格式、字段说明和“只根据资料”属于软约束,能降低错误概率,但不能替代解析。模型仍可能输出围栏、遗漏字段或改写证据。json.loads 负责判断是否是合法 JSON;validate 进一步限制字段集合、枚举值和证据归属。只有两层都通过,结果才会保存。

证据检查使用 evidence in source,它是一个有意保持简单的最小实现。它能发现模型凭空生成、改写或混入原文之外的摘录,却无法判断证据是否真正支持 claim,也无法处理复杂的空白差异。生产代码可以保存证据在原文中的字符区间,或先按句子切分并使用稳定的片段 ID;不要仅靠字符串包含就宣称完成事实核验。

常见问题

模型输出不是 JSON 怎么办? 先记录原始响应但不要直接落盘为可信笔记。可以增加一次“修复格式”的请求,但修复后仍必须重新执行 json.loads 和完整校验,不能因为重试成功就跳过安全检查。

为什么不让模型直接写 Markdown? Markdown 适合阅读,JSON 更适合程序验证和后续处理。可以在所有检查通过后,再由 Python 把 JSON 渲染成 Markdown;这样格式层和内容层分离,模板变化不会影响证据校验。

证据很长或完全匹配失败怎么办? 这是输入规范问题,不要简单放宽为“证据大致相似”。可以限制摘录长度、先把原文按句编号,并要求模型返回句子 ID。若要做模糊匹配,应明确阈值并保留人工复核状态。

摘要是否也需要证据? 本示例只校验要点,因为摘要可能是多条证据的压缩表达。对需要审计的场景,应让摘要也返回引用的要点编号,或者把摘要当作普通展示字段,不把它当作独立事实。

小结

这个项目没有引入新的 Agent 框架,却完整演示了一个可靠 AI 功能的基本结构:明确输入输出,要求结构化结果,保留原文证据,用普通 Python 做确定性校验,最后再保存或展示。模型适合提出语言层面的整理结果,程序适合执行边界检查;两者分工清楚,错误才容易被发现和修复。