基础路线完成后,可以用一个小项目练习“模型生成、程序约束、人来复核”的完整闭环。本篇构建一个研究笔记整理器:输入一段资料,模型提取主题、要点和原文证据,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/activatepython -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 jsonimport osimport sysfrom pathlib import Pathfrom openai import OpenAIMODEL = 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 做确定性校验,最后再保存或展示。模型适合提出语言层面的整理结果,程序适合执行边界检查;两者分工清楚,错误才容易被发现和修复。