← Back to 文字

异常、文件与 JSON

本文来自《AI 应用开发课程》月份 1 课程文档,已整理为网站文章版本。

学习目标

学完本节后,你应当能够:

  • 使用 try/except 处理异常。
  • 读写文本文件和 JSON 文件。
  • 理解为什么 AI 应用开发中大量使用 JSON。
  • 为后面的配置管理、结构化输出和日志记录打基础。

前置知识

  • 已完成前 3 节 Python 课程

1. 为什么异常处理重要

月份 1 后面常见的失败点:

  • API Key 缺失
  • 网络请求失败
  • JSON 解析失败
  • 工具参数不合法

如果你不会做异常处理,代码通常会在最脆弱的位置直接崩掉。

2. try/except 基本写法

try:
    value = int("123")
except ValueError:
    print("无法转换为整数")

带错误对象

try:
    value = int("abc")
except ValueError as error:
    print(f"发生错误: {error}")

elsefinally

try:
    value = int("123")
except ValueError:
    print("失败")
else:
    print("成功")
finally:
    print("结束")

3. 什么时候应该主动抛出异常

def ensure_api_key(api_key: str | None) -> str:
    if not api_key:
        raise ValueError("DEEPSEEK_API_KEY 未配置")
    return api_key

主动抛异常的意义:

  • 让错误更早暴露
  • 让调用方知道失败原因
  • 避免错误静默传播

4. 文件读写

写文本文件

with open("note.txt", "w", encoding="utf-8") as file:
    file.write("Hello, Python!")

读文本文件

with open("note.txt", "r", encoding="utf-8") as file:
    content = file.read()
    print(content)

with 的作用是自动关闭文件资源。

5. JSON 读写

写 JSON

import json

data = {"role": "user", "content": "hello"}

with open("message.json", "w", encoding="utf-8") as file:
    json.dump(data, file, ensure_ascii=False, indent=2)

读 JSON

import json

with open("message.json", "r", encoding="utf-8") as file:
    data = json.load(file)
    print(data["content"])

6. 为什么 AI 应用里 JSON 如此重要

因为它经常用于:

  • API 请求体和响应体
  • 结构化输出
  • 工具参数
  • 配置文件
  • 实验记录

月份 1 后面你会多次处理 JSON,所以这里必须先熟悉。

7. 异常和 JSON 结合示例

import json


def load_json_file(path: str) -> dict:
    try:
        with open(path, "r", encoding="utf-8") as file:
            return json.load(file)
    except FileNotFoundError as error:
        raise FileNotFoundError(f"文件不存在: {path}") from error
    except json.JSONDecodeError as error:
        raise ValueError(f"JSON 格式错误: {path}") from error

这个模式在后面读取 Prompt 模板、工具结果和配置文件时都很常见。

8. 实操任务

  1. 写一个函数,把一条聊天消息保存为 JSON 文件。
  2. 写一个函数,从 JSON 文件读取聊天消息。
  3. 人为制造一个错误 JSON,观察异常内容。

9. 自测题

  1. 为什么不能所有异常都用一个裸 except 吞掉?
  2. 为什么文件操作推荐配合 with
  3. 为什么结构化输出通常会和 JSON 强绑定?

10. 作业与验收

作业:

  • 实现一个 save_messages(messages: list[dict], path: str) 函数。
  • 实现一个 load_messages(path: str) 函数。

验收标准:

  • 正常数据能成功保存和读取
  • 文件不存在时能给出清晰报错
  • JSON 格式错误时能定位问题

11. 常见错误

  • 打开文件时忘记指定编码
  • 用裸 except 把所有错误吞掉
  • JSON 文件里使用非法逗号或引号

12. 延伸阅读

  • Python 官方文档中的 exceptions
  • Python 官方文档中的 json 模块

13. 本章与前文关系

前几章让你具备了:

  • 基础语法能力
  • 模块组织能力
  • 结构化对象表达能力

这一章开始把这些能力放进更真实的工程场景里:程序不总会成功,数据也不总留在内存里。你需要学会:

  • 错误出现时怎么处理
  • 数据怎么保存到文件
  • 为什么 JSON 是后续 AI 应用里最常见的数据格式之一

14. 本章在研发助手项目中的位置

研发助手项目里,这一章的知识会反复出现:

  • 读取 .env 配置失败
  • 模型返回的 JSON 无法解析
  • 工具结果要写入日志或本地文件
  • 结构化输出要从文本变成对象

换句话说,本章学的不是“普通 Python 文件操作”,而是 AI 应用里最常见的失败与数据流转基础。

15. 为什么异常处理是月份 1 的硬能力

初学者很容易把异常处理看成“锦上添花”。但在月份 1,异常处理其实是基础能力,因为后面你会频繁遇到这些失败点:

  • API Key 缺失
  • 网络请求超时
  • JSON 结构不合法
  • 工具调用参数错误
  • 文件不存在

如果你不会做异常处理,就会出现两种糟糕结果:

  1. 程序直接崩掉,但你不知道为什么。
  2. 你把错误吞掉,程序看起来没报错,但结果已经不可靠。

16. 错误示例 vs 正确示例

错误示例:裸 except

try:
    data = json.load(file)
except:
    print("something went wrong")

问题:

  • 你不知道到底是哪种错误
  • 错误上下文丢失
  • 后面几乎无法排查

正确示例:按具体异常类型处理

import json


try:
    data = json.load(file)
except FileNotFoundError as error:
    raise FileNotFoundError("目标文件不存在") from error
except json.JSONDecodeError as error:
    raise ValueError("JSON 格式错误") from error

这种方式的核心价值是:错误更可见,更可解释,也更容易在日志里分类。

17. 完整文件级示例:json_store.py

"""消息保存与加载示例。

本文件展示三件事:
1. 如何把结构化消息保存为 JSON
2. 如何从 JSON 读取回来
3. 如何在失败时给出清晰异常
"""

from __future__ import annotations

import json
from pathlib import Path


def save_messages(messages: list[dict[str, str]], path: str) -> None:
    """将消息列表写入 JSON 文件。"""

    file_path = Path(path)
    file_path.parent.mkdir(parents=True, exist_ok=True)

    with file_path.open("w", encoding="utf-8") as file:
        json.dump(messages, file, ensure_ascii=False, indent=2)


def load_messages(path: str) -> list[dict[str, str]]:
    """从 JSON 文件中读取消息列表。"""

    file_path = Path(path)

    if not file_path.exists():
        raise FileNotFoundError(f"文件不存在: {file_path}")

    try:
        with file_path.open("r", encoding="utf-8") as file:
            data = json.load(file)
    except json.JSONDecodeError as error:
        raise ValueError(f"JSON 格式错误: {file_path}") from error

    if not isinstance(data, list):
        raise TypeError("消息文件的顶层结构必须是列表")

    return data


def main() -> None:
    """演示保存、读取和打印流程。"""

    demo_messages = [
        {"role": "system", "content": "你是研发助手"},
        {"role": "user", "content": "请总结这个错误日志"},
    ]

    path = "data/messages.json"
    save_messages(demo_messages, path)

    loaded_messages = load_messages(path)
    print(loaded_messages)


if __name__ == "__main__":
    main()

18. 逐段解释这份完整示例

Path 的使用

这里没有直接用字符串拼路径,而是使用 pathlib.Path。这是 Python 工程里非常常见的文件路径处理方式,后面做配置、日志和测试时也很实用。

mkdir(parents=True, exist_ok=True)

这行代码意味着:如果目录不存在,就自动创建。它能让保存动作更稳定,而不是要求你手工先建目录。

先检查文件是否存在,再读

这能让错误更早暴露,也更容易给出清晰提示。

读取后检查顶层结构

即使 JSON 能成功解析,也不代表内容一定是你想要的结构。所以还需要做结构检查。

19. 为什么 JSON 在月份 1 会被反复使用

因为它恰好处在“人类可读”和“程序可处理”的交叉点:

  • API 请求和响应经常是 JSON
  • 结构化输出经常要落成 JSON
  • 工具调用参数本质上也是结构化 JSON 风格数据
  • 调试时把中间结果保存成 JSON 非常方便

月份 1 学会 JSON,不只是为了做文件读写,而是为了理解后面所有结构化数据流。

20. 一个更接近后续项目的增强版示例

你可以在 json_store.py 基础上继续增强:

  1. 增加日志打印
  2. 增加时间戳字段
  3. 在读取后校验每条消息是否都包含 rolecontent

增强版思路:

def validate_message(message: dict[str, str]) -> None:
    if "role" not in message or "content" not in message:
        raise ValueError("每条消息都必须包含 role 和 content")

这一步很重要,因为它已经在向后面的 Pydantic 校验思维靠近。

21. 调试与排错:本章最值得刻意练习的 4 类错误

练习一:文件不存在

把路径改成一个不存在的文件,看你是否能得到清晰错误。

练习二:JSON 写错格式

手动修改 JSON,故意删掉一个引号或逗号,再观察报错。

练习三:顶层结构不对

把原本的列表改成对象,看看你的结构检查是否能拦住。

练习四:字段缺失

删掉某条消息的 content,思考后续如果不拦住会有什么后果。

22. 本章完成后你应该具备的能力

完成本章后,你应当做到:

  1. 能写出最小 try/except 结构。
  2. 能解释为什么不应使用裸 except
  3. 能读写 UTF-8 文本和 JSON 文件。
  4. 能理解为什么解析成功后仍然需要进一步校验结构。

23. 如果你卡在这里,先回看哪几章

24. 从本章过渡到下一章的桥接说明

接下来进入 05-asyncio基础.md

这一步的逻辑非常自然:你已经知道本地文件如何读写,也知道失败时如何处理。下一步要进入的是月份 1 后面最常见的“等待型操作”场景,也就是网络请求和异步执行。后面的 DeepSeek API 调用和 FastAPI 异步接口都会直接建立在这里。

Fin