[从零构建多智能体 01] Python快速入门-从接管AI代码到多格式文件解析器实战

【从零构建多智能体 01】Python 快速入门:从接管 AI 代码到实现多格式文件解析器

系列总名称:从零构建多智能体:Harness & Hermes 项目实战系列

本文是系列第 1 篇,目标是帮助零基础读者先掌握智能体项目开发必需的 Python 基础能力。

一、为什么 AI 都能写代码了,我们还要学 Python?

现在很多同学写代码的方式已经变了:不再从空白文件一行一行敲,而是先让 AI 生成一个版本,再根据需求不断修改。

这确实提高了效率,但也带来了一个非常现实的问题:

AI 可以生成代码,但你必须能接管代码。

如果你完全看不懂 Python 语法,后面会遇到几个典型困境:

  1. AI 写了一段代码,你不知道程序从哪里开始执行。
  2. 代码报错了,你只能把报错复制给 AI,但无法判断 AI 的修复是否合理。
  3. 需求变了,你不知道应该改哪一行、哪个函数、哪个模块。
  4. AI 改完后表面能跑,但引入了新的隐藏问题。
  5. 代码无法接入真实项目,因为结构混乱、依赖不清、输入输出不稳定。

所以,学习 Python 的目的不是为了和 AI 比谁写得快,而是为了获得对代码的控制权。

在智能体项目里,Python 常常负责:

  • 调用大模型 API;
  • 处理 Prompt 和上下文;
  • 解析 PDF、Word、图片、音频等资料;
  • 编写 FastAPI 后端接口;
  • 管理数据库、缓存和任务队列;
  • 串联 LangChain、LangGraph、Pydantic 等工程工具。

如果你想做的不只是 Demo,而是能维护、能扩展、能上线的智能体项目,Python 基础就是绕不开的底座。

二、本文要解决什么问题?

本文不是一篇纯语法速查表,而是围绕智能体项目开发,帮助你建立一条实用路线:

  • 知道 Python 在 AI Agent 项目里的作用;
  • 掌握最短学习路径;
  • 搭好虚拟环境和依赖管理方式;
  • 理解变量、条件、循环、函数、模块、包等基础语法;
  • 学会用函数封装可复用逻辑;
  • 掌握文件读写、修改、删除和编码处理;
  • 理解类型注解、推导式、装饰器、OOP、Pydantic;
  • 最后实现一个统一文件解析器:一个函数解析 PDF、Word 和图片。

最终目标是从"能运行一段 Python 脚本",提升到"能写一个小型工程模块"。

三、跟做前先准备一个练习目录

为了让你能一步一步跟下来,建议先在电脑上新建一个干净目录。后面的所有代码都放在这个目录里。

假设目录叫:

text 复制代码
agent_python_start/

你可以手动创建,也可以在终端执行:

bash 复制代码
mkdir agent_python_start
cd agent_python_start

接下来我们会在这个目录中逐步创建下面这些文件:

text 复制代码
agent_python_start/
  01_word_count.py
  02_data_types.py
  03_functions.py
  04_file_tools.py
  easy_parser/

每学完一块知识,就会落到一个可运行的小文件上。这样你不是"看懂了",而是真的把代码跑起来了。

建议跟做顺序:

  1. 先运行最简单的词频统计,理解 Python 程序的输入、处理、输出。
  2. 再学习数据类型,知道字符串、列表、字典在项目中怎么用。
  3. 然后学习函数封装,把重复逻辑变成可复用函数。
  4. 接着学习文件处理,掌握读、写、改、删。
  5. 最后完成 easy_parser 小项目,把前面的基础串成一个工程化案例。

如果你是纯小白,不要一开始就追求写得很高级。先保证每一步都能运行,再逐步优化结构。

四、Python 学到什么程度才够做智能体项目?

初学者很容易陷入一个误区:想把 Python 全部学完再做项目。

这条路非常慢,也不适合 AI 项目开发。更合理的方式是围绕项目所需能力学习。

对于智能体项目,Python 基础至少要达到下面 4 个标准:

能力 具体表现
看懂代码结构 能知道入口文件、函数、模块、依赖之间的关系
能改关键逻辑 能根据需求修改输入、处理流程和输出格式
能定位报错 能根据异常信息判断是路径、依赖、类型还是业务逻辑问题
能封装模块 能把重复代码封装成函数、工具层或类,方便复用

如果你能做到这 4 点,AI 写代码反而会变成你的放大器。AI 负责快速生成初稿,你负责审查、修正、封装和验收。

五、Python 最短学习路径

面向智能体项目,Python 学习可以分成 3 个阶段。

阶段 1:能写小脚本

这个阶段的核心目标是跑通最小闭环:

text 复制代码
输入 -> 处理 -> 输出

例如:

  • 输入一段文本;
  • 按空格切词;
  • 用字典统计词频;
  • 打印统计结果。

请在 agent_python_start/ 目录下新建文件:

text 复制代码
01_word_count.py

写入下面这段代码:

python 复制代码
text = "python is good python is easy"

counts = {}
for word in text.split():
    counts[word] = counts.get(word, 0) + 1

print(counts)

然后在终端执行:

bash 复制代码
python 01_word_count.py

你应该看到类似输出:

text 复制代码
{'python': 2, 'is': 2, 'good': 1, 'easy': 1}

这段代码很小,但已经包含了 Python 入门最重要的几个知识点:

  • 变量;
  • 字符串;
  • 列表式遍历;
  • for 循环;
  • 字典;
  • dict.get()
  • 输出结果。

逐行解释一下:

python 复制代码
text = "python is good python is easy"

这行代码创建了一个字符串变量 text。你可以把它理解成程序的"输入数据"。真实项目里,这个输入可能来自用户问题、上传文件、接口请求或数据库。

python 复制代码
counts = {}

这行代码创建了一个空字典。字典适合保存"键值对",这里的键是单词,值是单词出现的次数。

python 复制代码
for word in text.split():

text.split() 会把字符串按空格拆成多个单词:

text 复制代码
["python", "is", "good", "python", "is", "easy"]

for word in ... 的意思是:每次从这个列表里取出一个单词,放到变量 word 中,然后执行缩进里面的代码。

python 复制代码
counts[word] = counts.get(word, 0) + 1

这行是词频统计的核心。counts.get(word, 0) 表示:

  • 如果 word 已经在字典里,就取出原来的次数;
  • 如果 word 不在字典里,就先当作 0 次;
  • 最后再加 1。

例如第一次遇到 python

text 复制代码
counts.get("python", 0) -> 0
0 + 1 -> 1

第二次遇到 python

text 复制代码
counts.get("python", 0) -> 1
1 + 1 -> 2
python 复制代码
print(counts)

这行把最终结果打印到终端。对于新手来说,print() 是最重要的调试工具之一。你可以在不理解某个变量时先打印出来,看程序运行到那里时它到底是什么。

你可以试着把第一行改成:

python 复制代码
text = "agent agent python fastapi python"

再运行一次,观察输出结果如何变化。学习编程最有效的方法就是"小改动 + 立即运行 + 看结果"。

很多 AI 项目里的复杂代码,本质上也是这个模型:

text 复制代码
用户输入 / 文件 / 接口参数 -> 清洗、解析、调用模型 -> 返回文本、JSON 或写入数据库

阶段 2:能做小项目

当脚本变长后,就要考虑代码组织。

这个阶段你需要掌握:

  • 函数封装;
  • 模块拆分;
  • 包导入;
  • 虚拟环境;
  • 依赖管理;
  • 文件读写;
  • 异常处理;
  • 日志输出。

比如"文件解析器"就不应该全部写在一个 main.py 里,而应该拆成:

text 复制代码
easy_parser/
  main.py
  src/
    __init__.py
    core.py
    parsers/
      __init__.py
      base.py
      pdf_parser.py
      docx_parser.py
      image_parser.py

这样代码才有扩展空间。

阶段 3:能写 API 和服务

后续进入智能体项目后,Python 会进一步承担后端服务能力。

你会接触:

  • FastAPI;
  • Pydantic;
  • JWT 鉴权;
  • 数据库建模;
  • Redis 缓存;
  • LangChain 调用链路;
  • Prompt 模板管理;
  • 文件上传和资源管理。

所以这篇 Python 基础不是孤立知识,而是给后面的 FastAPI、文件解析、智能体编排打基础。

六、开发环境:先把项目跑稳

很多 Python 报错不一定是代码错了,而是环境不一致。

常见问题包括:

  • 你电脑装了某个库,别人电脑没装;
  • 你用的是新版库,代码依赖旧版 API;
  • 多个项目共用一个全局 Python 环境,依赖互相污染;
  • pip install 安装到了另一个 Python 解释器下面。

解决方式是:每个项目一个虚拟环境,每个项目一份依赖文件

1. 创建虚拟环境

先进入前面创建的练习目录:

bash 复制代码
cd agent_python_start

然后确认 Python 能正常使用:

bash 复制代码
python --version

如果你看到类似下面的输出,说明 Python 已经安装成功:

text 复制代码
Python 3.11.8

如果提示 python 不是内部或外部命令,说明 Python 没有安装,或者安装时没有加入环境变量。此时建议重新安装 Python,并勾选 Add Python to PATH

Windows PowerShell:

bash 复制代码
python -m venv .venv
.\.venv\Scripts\Activate.ps1

Windows CMD:

bash 复制代码
python -m venv .venv
.\.venv\Scripts\activate.bat

macOS / Linux:

bash 复制代码
python3 -m venv .venv
source .venv/bin/activate

激活成功后,命令行前面通常会出现:

text 复制代码
(.venv)

这表示你现在已经进入当前项目自己的 Python 环境。后面安装的第三方库会优先安装到 .venv 目录中,不会污染全局环境。

退出虚拟环境:

bash 复制代码
deactivate

2. 安装依赖

例如安装请求库:

bash 复制代码
pip install requests

这里的 requests 是一个常见的 HTTP 请求库。后面智能体项目调用大模型接口、请求后端服务时,经常会用到类似的网络请求能力。

查看当前环境已安装的包:

bash 复制代码
pip list

如果安装成功,列表里应该能看到 requests

卸载依赖:

bash 复制代码
pip uninstall requests

3. 锁定依赖版本

把当前环境中的依赖写入 requirements.txt

bash 复制代码
pip freeze > requirements.txt

生成后,当前目录下会多出一个 requirements.txt 文件。这个文件的作用是记录项目依赖版本,方便别人复现同样的运行环境。

在另一台机器恢复环境:

bash 复制代码
pip install -r requirements.txt

可以把它理解为:别人拿到你的项目后,不需要一个一个猜要装什么库,只要执行这一条命令,就能把依赖装齐。

对于本文的 easy_parser 示例,依赖大致包括:

text 复制代码
PyMuPDF>=1.23.0
python-docx>=0.8.11
paddlepaddle>=2.6.0
paddleocr>=2.7.0
Pillow>=10.0.0

如果在国内网络环境下安装较慢,可以使用镜像源:

bash 复制代码
pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/

注意:OCR 相关库体积较大,首次运行还可能下载模型文件,安装和初始化时间会更长。

七、Python 基础语法:先掌握项目里最常用的部分

1. 变量和基础数据类型

Python 常见数据类型如下:

类型 示例 常见用途
str "hello" 文本、Prompt、文件内容
int 10 数量、页码、状态码
float 0.85 分数、相似度、模型温度
bool True 开关、判断结果
list [1, 2, 3] 有序集合
dict {"name": "Ada"} JSON、接口参数、配置
set {"pdf", "docx"} 去重、集合判断
tuple (1, "ok") 固定结构数据

示例:

请新建文件:

text 复制代码
02_data_types.py

写入:

python 复制代码
name = "Ada"
nums = [1, 2, 3]
user = {"id": 1, "name": "Ada"}
tags = {"python", "ai"}
pair = (1, "ok")

print(name.upper())
print(nums[0])
print(user["name"])
print("python" in tags)
print(pair[1])

运行:

bash 复制代码
python 02_data_types.py

你会看到:

text 复制代码
ADA
1 Ada True ok

这段代码里:

  • name.upper() 表示把字符串转成大写;
  • nums[0] 表示取列表第 1 个元素,Python 下标从 0 开始;
  • user["name"] 表示从字典里取出 name 对应的值;
  • "python" in tags 表示判断集合里是否包含某个元素;
  • pair[1] 表示取元组第 2 个元素。

在智能体项目中,dict 尤其重要,因为接口参数、大模型响应、配置项、消息结构通常都可以表示成字典或 JSON。

2. 条件判断

python 复制代码
score = 86

if score >= 90:
    level = "A"
elif score >= 80:
    level = "B"
else:
    level = "C"

print(level)

Python 使用缩进表示代码块,不使用 JavaScript 那样的大括号。

JavaScript 写法:

javascript 复制代码
if (score >= 90) {
  level = "A";
}

Python 写法:

python 复制代码
if score >= 90:
    level = "A"

这也是很多新手最容易出错的地方:缩进不一致会直接导致语法错误或逻辑错误。

3. 循环

遍历列表:

python 复制代码
files = ["a.pdf", "b.docx", "c.png"]

for file in files:
    print(file)

遍历字典:

python 复制代码
counts = {"python": 2, "agent": 1}

for word, count in counts.items():
    print(word, count)

在文件解析器中,我们经常需要遍历目录下的文件:

python 复制代码
from pathlib import Path

input_dir = Path("assets")
files = [file for file in input_dir.iterdir() if file.is_file()]

for file in files:
    print(file.name)

4. 函数

函数的作用是把一段逻辑封装起来,减少重复代码。

请新建文件:

text 复制代码
03_functions.py

写入:

python 复制代码
def normalize_text(text: str) -> str:
    return " ".join(text.strip().split()).lower()

print(normalize_text("  Hello   Python  "))

运行:

bash 复制代码
python 03_functions.py

输出:

text 复制代码
hello python

这个函数做了 3 件事:

  • 去掉首尾空格;
  • 把多个连续空格压缩成一个;
  • 转成小写。

代码拆开看:

python 复制代码
def normalize_text(text: str) -> str:

这行定义了一个函数:

  • def 表示定义函数;
  • normalize_text 是函数名;
  • text 是参数,也就是调用函数时传进来的数据;
  • text: str 表示参数应该是字符串;
  • -> str 表示这个函数最终会返回字符串。
python 复制代码
text.strip()

去掉字符串首尾多余空格。

python 复制代码
text.strip().split()

把字符串按空白字符拆成列表。多个连续空格会被自动处理。

python 复制代码
" ".join(...)

把列表重新拼成一个字符串,中间用一个空格连接。

python 复制代码
.lower()

把结果转成小写。

函数调用时,程序会先执行括号里的内容:

python 复制代码
normalize_text("  Hello   Python  ")

函数返回结果后,print() 再把结果打印出来。

函数命名建议采用"动词 + 名词"的结构,例如:

  • read_file
  • parse_pdf
  • validate_user
  • normalize_text
  • build_prompt

这样的函数名更容易表达意图。

八、函数封装:代码从"能跑"到"好维护"

很多初学者写代码时,会把所有逻辑都堆到一个文件里。

例如:

python 复制代码
name = input("请输入用户名:")
if len(name) < 3:
    print("用户名太短")

password = input("请输入密码:")
if len(password) < 6:
    print("密码太短")

这段代码可以运行,但维护性不好。因为长度校验逻辑重复出现了。

更好的写法是封装函数:

python 复制代码
def check_length(value: str, min_length: int, field_name: str) -> bool:
    if len(value) < min_length:
        print(f"{field_name}长度不能少于 {min_length}")
        return False
    return True

name = input("请输入用户名:")
password = input("请输入密码:")

check_length(name, 3, "用户名")
check_length(password, 6, "密码")

这个函数的参数含义如下:

参数 含义 示例
value 要检查的字符串 用户名、密码、标题
min_length 最小长度 3、6、10
field_name 字段中文名,用来提示用户 用户名、密码

返回值是 bool

  • 返回 True 表示校验通过;
  • 返回 False 表示校验失败。

如果你希望失败时直接停止程序,也可以改成抛异常:

python 复制代码
def require_min_length(value: str, min_length: int, field_name: str) -> None:
    if len(value) < min_length:
        raise ValueError(f"{field_name}长度不能少于 {min_length}")

这类校验逻辑在后端接口里非常常见。例如用户注册时,要检查用户名、密码、手机号、邮箱是否合法。前期先用函数写,后面进入 FastAPI 后可以交给 Pydantic 模型做统一校验。

高质量函数的 4 个原则

原则 说明
单一职责 一个函数只做一件事
命名清晰 函数名要体现行为和目标
输入输出明确 参数和返回值要清楚,尽量加类型注解
少副作用 不要在函数里偷偷修改外部状态

比如文件读取函数可以这样写:

python 复制代码
from pathlib import Path

def read_text_file(file_path: Path) -> str:
    return file_path.read_text(encoding="utf-8")

它只负责读取文件,不负责打印、不负责解析、不负责写数据库。这样后续要测试、复用、替换都更简单。

工具层 utils 怎么设计?

当通用函数越来越多时,不建议全部放在 main.py

可以建立工具包:

text 复制代码
project/
  main.py
  utils/
    __init__.py
    string_utils.py
    file_utils.py
    validators.py

示例:

python 复制代码
# utils/string_utils.py
def normalize_text(text: str) -> str:
    return " ".join(text.strip().split()).lower()
python 复制代码
# utils/__init__.py
from .string_utils import normalize_text

__all__ = ["normalize_text"]

业务代码中就可以这样导入:

python 复制代码
from utils import normalize_text

print(normalize_text("  Hello  Python "))

这里的关键是:utils 只放通用工具函数,不放具体业务逻辑。比如"解析 PDF"可以是通用能力,"生成某个用户的面试报告"就更偏业务逻辑,不适合随便放进工具层。

九、文件处理:智能体项目必须掌握的基础能力

智能体项目经常需要处理文件:

  • 用户上传简历 PDF;
  • 上传 Word 学习资料;
  • 上传截图或扫描件;
  • 保存聊天记录;
  • 保存模型输出结果;
  • 记录日志;
  • 生成报告。

所以文件读写是 Python 基础里非常关键的一块。

1. 文件操作的底层逻辑

文件操作可以理解成 3 步:

text 复制代码
打开文件 -> 读写文件 -> 关闭文件

如果文件打开后没有关闭,就可能造成资源泄露。

不推荐:

python 复制代码
file = open("data.txt", "r", encoding="utf-8")
content = file.read()
file.close()

更推荐使用 with

python 复制代码
with open("data.txt", "r", encoding="utf-8") as file:
    content = file.read()

with 会在代码块执行完后自动关闭文件。

2. open 的常见模式

模式 含义 注意事项
r 只读 文件不存在会报错
w 覆盖写 文件存在会清空原内容
a 追加写 在文件末尾追加
rb 二进制读 读取图片、音频等
wb 二进制写 写入二进制文件

尤其注意 w 模式,它会直接清空原文件。

3. 优先使用 pathlib

现代 Python 项目里,推荐优先使用 pathlib 处理路径。

python 复制代码
from pathlib import Path

file_path = Path(__file__).parent / "data.txt"
content = file_path.read_text(encoding="utf-8")

print(content)

相比字符串拼接路径,Path 对象更安全,也更容易跨平台。

错误示例:

python 复制代码
file_path = "data" + "/" + "a.txt"

推荐示例:

python 复制代码
file_path = Path("data") / "a.txt"

4. 写入文件

覆盖写:

python 复制代码
from pathlib import Path

file_path = Path("output.txt")
file_path.write_text("hello\n", encoding="utf-8")

追加写:

python 复制代码
with file_path.open("a", encoding="utf-8") as file:
    file.write("new line\n")

写文件时要自己加换行符 \n,否则多次写入可能会挤在同一行。

5. 修改文件

文本文件的"修改"通常不是直接改磁盘中的某几个字,而是:

text 复制代码
读取原内容 -> 在内存中替换 -> 覆盖写回
python 复制代码
from pathlib import Path

def replace_text(file_path: Path, old: str, new: str) -> None:
    content = file_path.read_text(encoding="utf-8")
    content = content.replace(old, new)
    file_path.write_text(content, encoding="utf-8")

如果是重要文件,建议先备份:

python 复制代码
def replace_text_safely(file_path: Path, old: str, new: str) -> None:
    backup_path = file_path.with_suffix(file_path.suffix + ".bak")
    backup_path.write_text(file_path.read_text(encoding="utf-8"), encoding="utf-8")

    content = file_path.read_text(encoding="utf-8")
    file_path.write_text(content.replace(old, new), encoding="utf-8")

6. 删除文件

删除文件使用 unlink()

python 复制代码
from pathlib import Path

def delete_file(file_path: Path) -> None:
    if file_path.exists() and file_path.is_file():
        file_path.unlink()

注意:unlink() 删除后不会进入回收站,真实项目里要谨慎使用。

7. 完整跟做:封装一个文件工具脚本

请新建文件:

text 复制代码
04_file_tools.py

写入下面的完整代码:

python 复制代码
from pathlib import Path


def read_file(file_path: Path) -> str:
    """读取文本文件内容。"""
    if not file_path.exists():
        return ""
    return file_path.read_text(encoding="utf-8")


def append_file(file_path: Path, content: str) -> None:
    """向文件末尾追加内容。"""
    with file_path.open("a", encoding="utf-8") as file:
        file.write(content)


def replace_file_text(file_path: Path, old: str, new: str) -> None:
    """替换文件中的文本,并在替换前生成备份文件。"""
    if not file_path.exists():
        raise FileNotFoundError(f"文件不存在:{file_path}")

    backup_path = file_path.with_suffix(file_path.suffix + ".bak")
    backup_path.write_text(file_path.read_text(encoding="utf-8"), encoding="utf-8")

    content = file_path.read_text(encoding="utf-8")
    file_path.write_text(content.replace(old, new), encoding="utf-8")


def delete_file(file_path: Path) -> None:
    """删除文件。真实项目中要谨慎调用。"""
    if file_path.exists() and file_path.is_file():
        file_path.unlink()


if __name__ == "__main__":
    current_dir = Path(__file__).resolve().parent
    data_path = current_dir / "demo_data.txt"

    data_path.write_text("Python is useful.\n", encoding="utf-8")
    print("第一次读取:")
    print(read_file(data_path))

    append_file(data_path, "Agent needs Python.\n")
    print("追加后读取:")
    print(read_file(data_path))

    replace_file_text(data_path, "Python", "Python programming")
    print("替换后读取:")
    print(read_file(data_path))

    print(f"原文件路径:{data_path}")
    print(f"备份文件路径:{data_path.with_suffix(data_path.suffix + '.bak')}")

运行:

bash 复制代码
python 04_file_tools.py

运行后,当前目录会出现两个文件:

text 复制代码
demo_data.txt
demo_data.txt.bak

逐块解释:

python 复制代码
current_dir = Path(__file__).resolve().parent

这行用于获取当前脚本所在目录。这样无论你从哪里运行脚本,它都能找到和脚本同级的文件。

python 复制代码
data_path = current_dir / "demo_data.txt"

这行用 / 拼接路径,比字符串拼接更安全。

python 复制代码
data_path.write_text("Python is useful.\n", encoding="utf-8")

这行创建并写入文件。如果文件已经存在,会覆盖原内容,所以真实业务里要谨慎使用。

python 复制代码
append_file(data_path, "Agent needs Python.\n")

这行调用我们封装好的追加函数,把新内容写到文件末尾。

python 复制代码
replace_file_text(data_path, "Python", "Python programming")

这行会把文件里的 Python 替换成 Python programming。替换前会先创建 .bak 备份文件,防止误改后无法恢复。

为什么 delete_file() 没有在主流程里调用?因为删除操作不可逆。对于新手教程,默认不执行删除更稳妥。你确认理解后,可以手动加一行测试:

python 复制代码
delete_file(data_path)

但请只在练习文件上测试,不要对真实资料执行删除。

十、进阶语法:让代码更适合工程化

1. 类型注解

类型注解不是强制类型检查,而是给人和编辑器看的"类型说明"。

python 复制代码
def add(a: int, b: int) -> int:
    return a + b

容器类型也可以标注:

python 复制代码
names: list[str] = ["Ada", "Bob"]
scores: dict[str, int] = {"Ada": 90}
phone: str | None = None

类型注解的价值:

  • 提升可读性;
  • 让编辑器提供更准确的提示;
  • 减少团队协作时的理解成本;
  • 为 Pydantic 参数校验打基础。

2. 推导式

普通写法:

python 复制代码
nums = []
for x in range(10):
    if x % 2 == 0:
        nums.append(x * 2)

推导式写法:

python 复制代码
nums = [x * 2 for x in range(10) if x % 2 == 0]

推导式适合简单的数据转换。如果逻辑过于复杂,还是应该写成普通循环,否则可读性会下降。

3. 装饰器

装饰器可以在不修改原函数代码的前提下,给函数增加额外能力。

例如给函数加日志:

python 复制代码
from functools import wraps

def log_call(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"[start] {func.__name__}")
        result = func(*args, **kwargs)
        print(f"[end] {func.__name__}")
        return result
    return wrapper

@log_call
def parse_pdf(file_path: str) -> str:
    return "pdf content"

parse_pdf("demo.pdf")

在后端和智能体项目里,装饰器常用于:

  • 日志;
  • 计时;
  • 权限校验;
  • 缓存;
  • 重试;
  • 统一异常处理。

4. 面向对象 OOP

当数据和行为需要绑定在一起时,可以使用类。

python 复制代码
class Student:
    def __init__(self, name: str, score: float = 60):
        self.name = name
        self.score = score

    def update_score(self, new_score: float) -> None:
        if not 0 <= new_score <= 100:
            raise ValueError("score must be between 0 and 100")
        self.score = new_score

student = Student("Ada")
student.update_score(95)

在本文的统一文件解析器里,PDF、Word、图片解析器都适合用类封装,因为它们拥有相同的行为:接收文件路径,返回解析文本。

5. Pydantic 参数校验

类型注解只提示,不强制校验。Pydantic 可以在运行时校验数据。

安装:

bash 复制代码
pip install pydantic

示例:

python 复制代码
from pydantic import BaseModel, Field, ValidationError

class Query(BaseModel):
    text: str = Field(min_length=1)
    top_k: int = Field(default=3, ge=1, le=20)

try:
    query = Query(text="Python Agent", top_k=5)
    print(query)
except ValidationError as exc:
    print(exc)

Pydantic 在 FastAPI 里非常常见。后面写接口时,请求体、查询参数、响应结构都可以用 Pydantic 模型约束。

十一、实战项目:统一文件解析器

这一节是本文最接近智能体项目的部分。

需求是:

不管用户上传的是 PDF、Word 还是图片,都通过同一个函数解析出文本。

理想调用方式:

python 复制代码
from src import parse_file

content = parse_file("assets/sample.pdf")
content = parse_file("assets/sample.docx")
content = parse_file("assets/sample.png")

用户不需要关心文件类型,也不需要关心底层用了哪个库。

1. 项目结构

先在 agent_python_start/ 目录下创建一个新项目:

text 复制代码
easy_parser/
  assets/
    sample.pdf
    sample.docx
    sample.png
  outputs/
  src/
    __init__.py
    core.py
    parsers/
      __init__.py
      base.py
      pdf_parser.py
      docx_parser.py
      image_parser.py
  main.py
  requirements.txt

如果你不熟悉命令行,可以直接在编辑器里手动创建这些文件夹和文件。对新手来说,先把结构建对,比纠结命令更重要。

每个目录的职责:

路径 职责
assets/ 存放待解析文件
outputs/ 保存解析结果
src/core.py 统一调度中心
src/parsers/base.py 定义解析器抽象协议
src/parsers/pdf_parser.py 解析 PDF
src/parsers/docx_parser.py 解析 Word
src/parsers/image_parser.py OCR 解析图片
main.py 扫描文件、调用解析器、保存结果

2. 第一步:编写 requirements.txt

easy_parser/requirements.txt 中写入:

text 复制代码
PyMuPDF>=1.23.0
python-docx>=0.8.11
paddlepaddle>=2.6.0
paddleocr>=2.7.0
Pillow>=10.0.0

然后在 easy_parser/ 目录下执行:

bash 复制代码
pip install -r requirements.txt

如果网络慢,可以使用镜像源:

bash 复制代码
pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/

每个依赖的作用:

依赖 作用
PyMuPDF 读取 PDF 文件内容,代码中通过 import fitz 使用
python-docx 读取 Word .docx 文件中的段落和表格
paddlepaddle PaddleOCR 运行所需的底层框架
paddleocr 对图片做 OCR 文字识别
Pillow 图片处理基础库,很多 OCR 场景会间接用到

如果你暂时不想安装 OCR 相关依赖,可以先只实现 PDF 和 Word 解析,把图片解析器留到后面再补。

3. 第二步:定义抽象基类 base.py

easy_parser/src/parsers/base.py 中写入:

python 复制代码
from abc import ABC, abstractmethod
from pathlib import Path


class BaseParser(ABC):
    @abstractmethod
    def parse(self, file_path: Path) -> str:
        pass

这段代码的意义是建立统一协议:

  • PDF 解析器必须有 parse()
  • Word 解析器必须有 parse()
  • 图片解析器也必须有 parse()
  • 调度器只需要面向 BaseParser 的协议编程。

这样后面新增 TxtParserExcelParserAudioParser 时,也能保持同一套结构。

逐行解释:

python 复制代码
from abc import ABC, abstractmethod

abc 是 Python 标准库,专门用来定义抽象类。抽象类可以理解成"接口规范"。

python 复制代码
class BaseParser(ABC):

这表示 BaseParser 是一个抽象基类,不是给你直接使用的,而是给其他解析器继承的。

python 复制代码
@abstractmethod
def parse(self, file_path: Path) -> str:

@abstractmethod 表示子类必须实现这个方法。也就是说,只要某个类继承了 BaseParser,它就必须写自己的 parse()

这样做的好处是:不同文件类型虽然解析细节不同,但对外都统一成一个动作:传入文件路径,返回文本内容。

4. 第三步:编写 PDF 解析器

easy_parser/src/parsers/pdf_parser.py 中写入:

python 复制代码
import fitz
from pathlib import Path
from .base import BaseParser


class PdfParser(BaseParser):
    def parse(self, file_path: Path) -> str:
        text_content = []

        with fitz.open(file_path) as doc:
            for page_num, page in enumerate(doc, start=1):
                text = page.get_text()
                text_content.append(f"--- Page {page_num} ---\n{text}")

        return "\n".join(text_content)

这段代码的运行逻辑:

  1. fitz.open(file_path) 打开 PDF 文件;
  2. for page_num, page in enumerate(doc, start=1) 一页一页遍历;
  3. page.get_text() 提取当前页文本;
  4. text_content.append(...) 把每一页文本放进列表;
  5. "\n".join(text_content) 把所有页的文本合并成一个大字符串。

这里用 with fitz.open() 是为了确保 PDF 文件资源能被正确关闭。打开 PDF 和打开普通文本文件一样,都属于占用系统资源的操作,处理完后要释放。

为什么要加 --- Page 1 --- 这种页码标记?因为真实文档可能有很多页,后续你把解析结果交给大模型时,保留页码有助于定位内容来源。

5. 第四步:编写 Word 解析器

easy_parser/src/parsers/docx_parser.py 中写入:

python 复制代码
import docx
from pathlib import Path
from .base import BaseParser


class DocxParser(BaseParser):
    def parse(self, file_path: Path) -> str:
        document = docx.Document(str(file_path))
        content = []

        for paragraph in document.paragraphs:
            if paragraph.text.strip():
                content.append(paragraph.text)

        for table in document.tables:
            for row in table.rows:
                row_text = [
                    cell.text.strip()
                    for cell in row.cells
                    if cell.text.strip()
                ]
                if row_text:
                    content.append(" | ".join(row_text))

        return "\n".join(content)

这段代码分两部分读取 Word:

第一部分读取段落:

python 复制代码
for paragraph in document.paragraphs:
    if paragraph.text.strip():
        content.append(paragraph.text)

paragraph.text.strip() 的作用是去掉空白。如果某个段落是空行,就不加入结果。

第二部分读取表格:

python 复制代码
for table in document.tables:
    for row in table.rows:
        row_text = [
            cell.text.strip()
            for cell in row.cells
            if cell.text.strip()
        ]

很多 Word 文档里不只有段落,还有表格。如果只读取段落,会漏掉表格内容。所以这里额外遍历 document.tables,把每一行的单元格内容用 | 拼起来。

例如 Word 表格中一行是:

text 复制代码
姓名 | 年龄 | 技能

解析后也会尽量保留类似结构,方便后续阅读或交给大模型处理。

6. 第五步:编写图片解析器

easy_parser/src/parsers/image_parser.py 中写入:

python 复制代码
from paddleocr import PaddleOCR
from pathlib import Path
from .base import BaseParser


class ImageParser(BaseParser):
    def __init__(self):
        self.ocr = PaddleOCR(
            use_angle_cls=True,
            lang="ch",
            ocr_version="PP-OCRv3",
            enable_mkldnn=False,
        )

    def parse(self, file_path: Path) -> str:
        result = self.ocr.ocr(str(file_path))
        text_content = []

        if result:
            for page_result in result:
                if not page_result:
                    continue

                for line in page_result:
                    if isinstance(line, list) and len(line) >= 2:
                        text_info = line[1]
                        if isinstance(text_info, (list, tuple)) and text_info:
                            text_content.append(str(text_info[0]))

        return "\n".join(text_content)

这里有几个参数需要解释:

python 复制代码
use_angle_cls=True

表示开启文字方向分类。比如图片里的文字有一点倾斜,OCR 会尝试自动识别方向。

python 复制代码
lang="ch"

表示主要识别中文,也能兼容一部分英文。

python 复制代码
ocr_version="PP-OCRv3"

表示使用指定版本的 OCR 模型。

python 复制代码
enable_mkldnn=False

在部分 Windows CPU 环境下,关闭 mkldnn 可以减少兼容性问题。

为什么 ImageParser 要写 __init__()?因为 OCR 模型初始化比较慢。如果每解析一张图片都重新初始化一次,程序会很慢。把 OCR 对象放到 __init__() 中,只在创建解析器时初始化一次,后续可以复用。

真实项目里,OCR 返回结构可能因为版本差异有所不同,所以这里对 result 做了多层判断,避免某些图片识别失败时程序直接崩掉。

7. 第六步:导出 parsers 包

easy_parser/src/parsers/__init__.py 中写入:

python 复制代码
from .pdf_parser import PdfParser
from .docx_parser import DocxParser
from .image_parser import ImageParser

__all__ = ["PdfParser", "DocxParser", "ImageParser"]

这个文件的作用是统一导出解析器类。这样在 core.py 里可以写:

python 复制代码
from .parsers import PdfParser, DocxParser, ImageParser

而不用分别写三个导入语句。

8. 第七步:编写核心调度器 core.py

easy_parser/src/core.py 中写入:

python 复制代码
from pathlib import Path
from .parsers import PdfParser, DocxParser, ImageParser


class UnifiedParser:
    def __init__(self):
        self.pdf_parser = PdfParser()
        self.docx_parser = DocxParser()
        self.image_parser = ImageParser()

        self._parser_map = {
            ".pdf": self.pdf_parser,
            ".docx": self.docx_parser,
            ".jpg": self.image_parser,
            ".jpeg": self.image_parser,
            ".png": self.image_parser,
            ".bmp": self.image_parser,
        }

    def parse(self, file_path: str | Path) -> str:
        path = Path(file_path).resolve()

        if not path.exists():
            return f"Error: File not found: {path}"

        parser = self._parser_map.get(path.suffix.lower())
        if parser is None:
            return f"Error: Unsupported file format: {path.suffix}"

        return parser.parse(path)

_parser_instance = UnifiedParser()

def parse_file(file_path: str | Path) -> str:
    return _parser_instance.parse(file_path)

这里有几个工程化亮点:

设计 作用
字典映射 用后缀匹配解析器,避免大量 if/elif
统一入口 外部只调用 parse_file()
单例实例 避免每次调用都重复初始化 OCR
Path.resolve() 统一路径处理
返回字符串 对上层调用者保持简单

再拆开看核心逻辑:

python 复制代码
self._parser_map = {
    ".pdf": self.pdf_parser,
    ".docx": self.docx_parser,
    ".jpg": self.image_parser,
    ".jpeg": self.image_parser,
    ".png": self.image_parser,
    ".bmp": self.image_parser,
}

这就是"策略映射"。程序不需要写一堆 if file.endswith(".pdf"),而是直接通过后缀找到对应解析器。

python 复制代码
parser = self._parser_map.get(path.suffix.lower())

path.suffix 取文件后缀,.lower() 转成小写,避免 .PDF.Pdf 这种大小写问题。

python 复制代码
return parser.parse(path)

调度器并不关心 PDF、Word、图片具体怎么解析。它只负责把任务转交给对应解析器。

这就是工程化里很重要的思想:调度层负责分发,具体实现层负责干活。

9. 第八步:导出统一入口 src/init.py

easy_parser/src/__init__.py 中写入:

python 复制代码
from .core import parse_file

__all__ = ["parse_file"]

这样外部就可以直接写:

python 复制代码
from src import parse_file

而不用知道 parse_file 实际定义在 src/core.py 里。

这也是很多 Python 包常用的写法:内部结构可以复杂,但对外暴露的入口要简单。

10. 第九步:编写主程序 main.py

easy_parser/main.py 中写入:

python 复制代码
from pathlib import Path
from src import parse_file

project_root = Path(__file__).resolve().parent
input_dir = project_root / "assets"
output_dir = project_root / "outputs"
output_dir.mkdir(parents=True, exist_ok=True)

files = [file for file in input_dir.iterdir() if file.is_file()]

for file_path in files:
    content = parse_file(file_path)
    output_path = output_dir / f"{file_path.name}_result.txt"
    output_path.write_text(content, encoding="utf-8")
    print(f"saved: {output_path}")

运行前,请把测试文件放入:

text 复制代码
easy_parser/assets/

例如:

text 复制代码
easy_parser/assets/sample.pdf
easy_parser/assets/sample.docx
easy_parser/assets/sample.png

然后在 easy_parser/ 目录执行:

bash 复制代码
python main.py

运行后,解析结果会保存到:

text 复制代码
easy_parser/outputs/

这段代码体现了一个完整的批处理流程:

text 复制代码
扫描输入目录 -> 逐个解析文件 -> 保存结果 -> 打印进度

这就是智能体项目里文件上传、资料入库、知识库构建的基础形态。

主程序也拆开解释一下:

python 复制代码
project_root = Path(__file__).resolve().parent

获取当前项目根目录,也就是 main.py 所在目录。

python 复制代码
input_dir = project_root / "assets"
output_dir = project_root / "outputs"

定义输入目录和输出目录。assets 放原始文件,outputs 放解析后的文本结果。

python 复制代码
output_dir.mkdir(parents=True, exist_ok=True)

如果 outputs 不存在,就自动创建。exist_ok=True 表示目录已经存在时不要报错。

python 复制代码
files = [file for file in input_dir.iterdir() if file.is_file()]

遍历 assets 目录,只保留文件,排除子目录。

python 复制代码
content = parse_file(file_path)

调用统一入口。这里主程序完全不关心文件是 PDF、Word 还是图片。

python 复制代码
output_path = output_dir / f"{file_path.name}_result.txt"

生成输出文件路径。例如:

text 复制代码
sample.pdf -> sample.pdf_result.txt

保留原始后缀的好处是避免同名文件互相覆盖。例如 sample.pdfsample.docx 都会有独立结果文件。

十二、如何给解析器新增一种文件格式?

假设我们想支持 .txt 文件。

第一步,新建 txt_parser.py

python 复制代码
from pathlib import Path
from .base import BaseParser

class TxtParser(BaseParser):
    def parse(self, file_path: Path) -> str:
        return file_path.read_text(encoding="utf-8")

第二步,在 parsers/__init__.py 中导出:

python 复制代码
from .pdf_parser import PdfParser
from .docx_parser import DocxParser
from .image_parser import ImageParser
from .txt_parser import TxtParser

第三步,在 core.py 中注册:

python 复制代码
self.txt_parser = TxtParser()

self._parser_map = {
    ".pdf": self.pdf_parser,
    ".docx": self.docx_parser,
    ".png": self.image_parser,
    ".txt": self.txt_parser,
}

这样外部调用方式不变:

python 复制代码
content = parse_file("assets/readme.txt")

这就是模块化设计的价值:新增能力时,不需要推翻原来的调用方式。

十三、常见坑和排查方式

1. 虚拟环境没有激活

现象:

text 复制代码
ModuleNotFoundError: No module named 'fitz'

排查:

bash 复制代码
where python
pip list

确认当前终端使用的是 .venv 里的 Python。

2. fitz 安装错误

PyMuPDF 的导入名是 fitz,但安装包名是 PyMuPDF

正确安装:

bash 复制代码
pip install PyMuPDF

代码中导入:

python 复制代码
import fitz

3. 中文乱码

读写文本时尽量明确指定编码:

python 复制代码
content = file_path.read_text(encoding="utf-8")
file_path.write_text(content, encoding="utf-8")

不要依赖系统默认编码。

4. 相对路径找不到文件

错误写法:

python 复制代码
file_path = Path("assets/sample.pdf")

如果从不同目录运行脚本,路径可能失效。

更稳的写法:

python 复制代码
project_root = Path(__file__).resolve().parent
file_path = project_root / "assets" / "sample.pdf"

5. w 模式误清空文件

下面代码会清空原文件:

python 复制代码
open("data.txt", "w", encoding="utf-8")

如果只是追加日志,应使用:

python 复制代码
open("data.txt", "a", encoding="utf-8")

6. OCR 初始化慢

PaddleOCR 第一次运行可能会下载模型,初始化时间较长。这不是代码卡死,通常等待即可。

真实项目里,不建议每次解析图片都重新创建 OCR 实例。应该像本文示例一样,在解析器初始化时创建一次,然后复用。

十四、本文知识如何连接后续智能体项目?

第一篇看起来是 Python 基础,但它已经在为后面的项目做铺垫。

本文能力 后续项目用途
虚拟环境和依赖管理 FastAPI、LangChain、OCR 等依赖稳定运行
函数封装 Prompt 构造、文件解析、接口逻辑复用
模块和包 后端分层架构、服务拆分
文件处理 简历、资料、图片、日志、报告处理
类型注解 提升大型项目可读性
Pydantic FastAPI 请求参数和响应结构校验
OOP Agent、Parser、Service、Repository 等对象建模
统一解析器 多模态智能体的资料输入基础

换句话说,后面的智能体项目不是突然变复杂的,而是从这些基础能力逐步搭起来的。

十五、练习任务

如果你想真正掌握本文内容,建议完成下面几个练习。

练习 1:词频统计升级版

要求:

  • data.txt 读取文本;
  • 统一转小写;
  • 去掉常见标点;
  • 统计词频;
  • 按出现次数从高到低输出。

提示:

python 复制代码
from pathlib import Path

def count_words(file_path: Path) -> dict[str, int]:
    text = file_path.read_text(encoding="utf-8").lower()
    for char in ",.!?;:":
        text = text.replace(char, " ")

    counts = {}
    for word in text.split():
        counts[word] = counts.get(word, 0) + 1
    return counts

练习 2:封装文件工具函数

实现下面 4 个函数:

python 复制代码
def read_file(file_path: Path) -> str:
    ...

def append_file(file_path: Path, content: str) -> None:
    ...

def replace_file_text(file_path: Path, old: str, new: str) -> None:
    ...

def delete_file(file_path: Path) -> None:
    ...

要求:

  • 全部使用 pathlib
  • 明确指定 utf-8
  • 删除前判断文件是否存在;
  • 修改前自动备份。

练习 3:给统一解析器添加 TXT 支持

要求:

  • 新增 TxtParser
  • 继承 BaseParser
  • core.py 注册 .txt
  • 保持外部调用方式不变。

完成后应该可以这样调用:

python 复制代码
content = parse_file("assets/demo.txt")

十六、总结

本文的重点不是"背 Python 语法",而是建立智能体项目开发所需的 Python 基础能力。

你需要重点掌握:

  1. AI 可以写代码,但人必须能接管代码。
  2. Python 学习应围绕项目最小闭环:输入、处理、输出。
  3. 虚拟环境和 requirements.txt 是保证项目可复现的基础。
  4. 函数封装和工具层设计决定代码是否好维护。
  5. 文件处理是智能体处理资料、日志、知识库的前置能力。
  6. 类型注解、装饰器、OOP、Pydantic 是工程化代码的重要基础。
  7. 统一文件解析器展示了从基础语法走向小型工程模块的完整过程。

如果你已经能看懂并改造本文中的统一文件解析器,那么后续学习 FastAPI、数据库建模、LangChain 和多智能体协作时,就不会只是"跟着敲",而是能理解每一层代码为什么这么设计。