【从零构建多智能体 01】Python 快速入门:从接管 AI 代码到实现多格式文件解析器
系列总名称:从零构建多智能体:Harness & Hermes 项目实战系列
本文是系列第 1 篇,目标是帮助零基础读者先掌握智能体项目开发必需的 Python 基础能力。
一、为什么 AI 都能写代码了,我们还要学 Python?
现在很多同学写代码的方式已经变了:不再从空白文件一行一行敲,而是先让 AI 生成一个版本,再根据需求不断修改。
这确实提高了效率,但也带来了一个非常现实的问题:
AI 可以生成代码,但你必须能接管代码。
如果你完全看不懂 Python 语法,后面会遇到几个典型困境:
- AI 写了一段代码,你不知道程序从哪里开始执行。
- 代码报错了,你只能把报错复制给 AI,但无法判断 AI 的修复是否合理。
- 需求变了,你不知道应该改哪一行、哪个函数、哪个模块。
- AI 改完后表面能跑,但引入了新的隐藏问题。
- 代码无法接入真实项目,因为结构混乱、依赖不清、输入输出不稳定。
所以,学习 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/
每学完一块知识,就会落到一个可运行的小文件上。这样你不是"看懂了",而是真的把代码跑起来了。
建议跟做顺序:
- 先运行最简单的词频统计,理解 Python 程序的输入、处理、输出。
- 再学习数据类型,知道字符串、列表、字典在项目中怎么用。
- 然后学习函数封装,把重复逻辑变成可复用函数。
- 接着学习文件处理,掌握读、写、改、删。
- 最后完成
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_fileparse_pdfvalidate_usernormalize_textbuild_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的协议编程。
这样后面新增 TxtParser、ExcelParser、AudioParser 时,也能保持同一套结构。
逐行解释:
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)
这段代码的运行逻辑:
fitz.open(file_path)打开 PDF 文件;for page_num, page in enumerate(doc, start=1)一页一页遍历;page.get_text()提取当前页文本;text_content.append(...)把每一页文本放进列表;"\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.pdf 和 sample.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 基础能力。
你需要重点掌握:
- AI 可以写代码,但人必须能接管代码。
- Python 学习应围绕项目最小闭环:输入、处理、输出。
- 虚拟环境和
requirements.txt是保证项目可复现的基础。 - 函数封装和工具层设计决定代码是否好维护。
- 文件处理是智能体处理资料、日志、知识库的前置能力。
- 类型注解、装饰器、OOP、Pydantic 是工程化代码的重要基础。
- 统一文件解析器展示了从基础语法走向小型工程模块的完整过程。
如果你已经能看懂并改造本文中的统一文件解析器,那么后续学习 FastAPI、数据库建模、LangChain 和多智能体协作时,就不会只是"跟着敲",而是能理解每一层代码为什么这么设计。