环境这一步最不出成果,也最容易把人卡住一整天。 卡住的地方几乎不是「不会装」,而是装上了却跑不起来——版本对不上。 这一节把 pip 干的那件事拆开看:它其实是一个回溯求解器,在几十个版本里找一组互相不冲突的组合; 找不着,就报一段谁都读不懂的错。
这一节是全书唯一一节纯工程环境的内容,可以跳过去先跑 Demo,回头再看。 依赖与虚拟环境的一般原理,17 课 编程原理 讲过; 这一节讲的是大模型项目特有的那部分:两套轮子、几百兆的包、拆成十几个子包。
一、三条硬约束,先记住再动手
第一条,Python 版本。 LangChain 这类框架对版本有明确下限,老版本 Python 装不上新包, 新版本 Python 又可能还没有预编译轮子。 第二条,必须用虚拟环境。系统 Python 里装的东西,谁装的、什么时候装的、被谁升级过,都查不清。 项目一多,两个项目要的 numpy 版本不一样,就开始互相踩。 第三条,CPU 和 GPU 是两套轮子。 同样一个 torch,装 CPU 版和装 CUDA 版是两个不同的包,装错了不报错,只是跑起来用不上显卡。
这条最坑人:torch.cuda.is_available() 返回 False,你以为是驱动没装,
实际是当初 pip 装的是 CPU 版。装错和没装,表现一模一样。
二、虚拟环境:venv 与 conda 的取舍
venv 是 Python 自带的,一条命令建一个目录,激活之后 pip 装的东西全落在这个目录里。 够用、干净、没有额外依赖。 conda 多管一件事:它连 Python 解释器本身、以及 cuda 工具链这类非 Python 的库也能装。 代价是体积大、慢,而且 pip 和 conda 混用容易把依赖图搞乱。 经验上:只跑 Python 代码,用 venv;要在同一台机器上切不同 CUDA 版本,用 conda。
最忌讳的是「先 conda 装一半,再 pip 装一半」。 两套包管理器各记各的账,出问题时谁都不知道是谁覆盖了谁。 要混就定规矩:conda 只装 Python 和 cuda,其余一律 pip。
三、pip 到底在干什么:一次回溯求解
每个包声明的是区间,不是确定版本:pydantic 要 1.x 到 2.x,requests 要 2.x 到 3.x。 装的时候要把所有区间求交集,还要给每个包挑一个具体版本。 演示里五个包(七个版本条目)就能造出冲突:老的 langchain 要 pydantic 1.x 到 2.x, langchain-core 只要 pydantic 2.x。两个都要,就只能选 2.x。 求解的方式是回溯:先给第一个包挑个版本,往下推,推不下去就退回来换一个。 演示里解出那组解用了 5 次尝试;无解的那一组,1 次就撞死了。
这里有个必须说清的事实: 求解器给出的是「第一个能装上的组合」,不是「最好的组合」。演示里它先试 0.1.0,一试就成, 于是老版本压根没轮到。 所以「pip 装完能跑」只说明这组版本自洽,不说明它是最新、最稳、或者官方推荐的那一组。
四、依赖报错怎么读:只看两行
那段又长又吓人的报错,实际有用的只有两处:谁要什么、谁跟谁冲突了。
中间那一大堆「以下版本都被排除」,是求解器的过程记录。
看到冲突,处理顺序是固定的:先把报错里点名的那两个包的版本区间抄下来,看交集是不是空的。
空了就是真冲突,不空就是其中一个包被别的东西顶住了。
真正的解法只有三条:换一个能对上的版本、把顶住它的那个包降级、或者干脆换个不依赖它的方案。
没有第四条——不要靠 --force-reinstall 硬装。
硬装能装上,运行时才崩,而且崩在 import 或第一次调用的地方,离真正的原因隔了十万八千里。
五、0.1 之后拆包:import 路径会变
这一点是照着半年前教程装包时最常踩的坑。
早期的 LangChain 是一个大包,from langchain.prompts import ... 这样直接引。
后来它被拆成几层:核心抽象单独一个包,社区集成(各种向量库、各种加载器)单独一个包,
各家模型厂商再各出一个 partner 包。
拆包的好处是装得少、装得快——只接一个模型厂商,不用把几十家 SDK 全拖下来。
代价是老代码的 import 要改。
判断方法很简单:装完之后 pip list 看一眼装了什么。如果只有两三个包,说明是拆包之后的结构;
如果一个大包加一堆依赖,那是拆包之前。
遇到 ImportError 先去查这个包实际叫什么,别急着改代码——多半是路径变了,不是逻辑错了。
六、接本地 Llama 2:改两行配置
上一节说过模型这层的收益在「换得动」。 换到本地,靠的是接口兼容:推理服务把 Llama 2 包装成 OpenAI 那样的一问一答接口。 于是同一份业务代码,只要把 base_url 指向本机端口、api_key 随便填一个占位串, 就能从云 API 切到本地 7B。 这一步最容易漏的是 base_url 结尾:有的服务要带路径后缀,有的不要。 写错了不报连接错误,而是返回 404,报错信息还长得像「模型不存在」。
第二个容易漏的是超时。 本地 7B 在 CPU 上生成一段话要几十秒,默认超时往往等不到,表现成「请求失败」,其实是没等完。
七、动手 Demo:五个包的小依赖图,跑一次回溯求解
造一张只有五个包(七个版本条目)的依赖图,约束写成主版本区间。 版本号是代表值,只为演示约束关系,不代表任何真实发行版。 用三十行写一个回溯求解器,跑三种情况:能解、解出来是哪一组、以及解不出来时报什么。
PKGS = {
u'langchain': {
u'0.0.350': {u'pydantic': (1, 3), u'requests': (2, 3)},
u'0.1.0': {u'pydantic': (1, 3), u'langchain-core': (0, 1)},
},
u'langchain-core': {u'0.1.0': {u'pydantic': (2, 3), u'jsonpatch': (1, 2)}},
u'pydantic': {u'1.10.13': {}, u'2.6.1': {}},
}
def solve(want): # 回溯:推不下去就退回来换版本
for ver in sorted(PKGS[name], reverse=True):
lo = max(merged[k][0], r[0])
hi = min(merged[k][1], r[1])
if lo >= hi: # 区间交空了
continue
实跑输出:
--------------------------------------------------------------------
五个包的小依赖图,跑一次回溯求解
--------------------------------------------------------------------
① 依赖图
--------------------------------------------------------------------
| 包 | 版本 | 依赖 |
|---|---|---|
| jsonpatch | 1.33 | 无 |
| langchain | 0.0.350 | pydantic(1, 3)、requests(2, 3) |
| langchain | 0.1.0 | pydantic(1, 3)、langchain-core(0, 1) |
| langchain-core | 0.1.0 | pydantic(2, 3)、jsonpatch(1, 2) |
| pydantic | 1.10.13 | 无 |
| pydantic | 2.6.1 | 无 |
| requests | 2.31.0 | 无 |
约束写成「主版本区间」,比如 pydantic (1, 3) 就是 >=1 且 <3。真实 pip 的约束更细。
! 这张图小,但已经能造出冲突:langchain 0.0.350 要 pydantic 1.x 到 2.x,
langchain-core 0.1.0 只要 pydantic 2.x。两个都装,只能选 2.x。
② 能解的情况
--------------------------------------------------------------------
| 装上的包 | 版本 |
|---|---|
| jsonpatch | 1.33 |
| langchain | 0.1.0 |
| langchain-core | 0.1.0 |
| pydantic | 2.6.1 |
回溯了 5 次,得到一组可行解。
! 这组解里没有 requests——只有老的 0.0.350 才依赖它,0.1.0 只要 langchain-core。
! 而之所以走到 0.1.0,是因为求解器按版本号倒序试,0.1.0 排在前头,一试就成,老版本压根没轮到。
第一个可行解长什么样,全看候选怎么排序——所以 pip 装出来的组合未必是最新的,也未必是最稳的,
只是最先撞上的那个。
③ 解不出来的情况
--------------------------------------------------------------------
要 pydantic 1.x,但同时要 langchain-core——它只支持 pydantic 2.x。尝试 1 次,无解。
┌─ pip 会这么报
│ ERROR: Cannot install ... because these package versions
│ have conflicting dependencies:
│ pydantic 1.10.13 depends on ...
│ langchain-core 0.1.0 depends on pydantic>=2.0
└─
! 读这种报错,只看两行:「谁要什么」和「谁冲突了」。
中间那一大堆「以下版本都被排除」是求解器的过程记录,不用看。
④ 版本对齐的三条经验
--------------------------------------------------------------------
| 做法 | 理由 |
|---|---|
| 先装框架,再装插件 | 框架定版本,插件跟着走 |
| 锁文件进版本库 | requirements.txt 要写死版本号 |
| 一次只升一个包 | 一次升三个,出问题不知道是谁 |
第二条最容易被跳过。不锁版本的 requirements,三个月后换台机器就装不上了。
! langchain 这类框架版本迭代很快,0.0.x 之间就会有破坏性改动。所以锁版本不是保守,是必需。
⑤ 真机上的命令
--------------------------------------------------------------------
[真机命令 · 本沙箱不执行]
| pip install -r requirements.txt
| pip check # 检查已装包的依赖是否自洽
| pip list --outdated
要 pip 与真环境,本沙箱不执行;逻辑就是上面那个求解器。
①先把依赖图看清楚。
注意 langchain 有两个版本,它们要的 pydantic 区间不一样:老的 0.0.350 要 1.x 到 2.x(宽的),
新的 0.1.0 把它交给 langchain-core 去要 2.x(窄的)。
约束写成主版本区间是为了演示方便。
真实 pip 的约束细到小版本,所以真报错里的版本号会长得多,读法一样。
②解出来的这一组里,langchain 是 0.1.0。为什么不是老的 0.0.350?
因为求解器按版本号倒序试,0.1.0 排在前头,一试就通,老版本压根没轮到。
这组解里也没有 requests——只有老的 0.0.350 才依赖它。换一个候选排序,装出来的包会完全不同。
所以「pip 装完能跑」只说明这组版本自洽,不说明它是最新、最稳、或者官方推荐的那一组。
③无解那一格最有教学价值:要 pydantic 1.x,同时要 langchain-core,而它只认 pydantic 2.x。
求解器试了 1 次就撞死,说明冲突在第一个包上就暴露了。
注意它撞死得这么快:需求里 pydantic 已经是 1.x,langchain-core 一加进来就把上界压到 2 以下。
交集空了,没有版本可选,直接返回无解——不用往下试。
真 pip 这时会打一大段「以下版本都被排除」,那是求解器把每个候选版本的失败原因全列了一遍。
读法只看两行:谁要什么、谁跟谁冲突。中间那堆过程记录,一个字都不用看。
④三条经验里,第二条最容易被跳过:把版本号写死进 requirements。
不锁版本的代价不是当天崩,是三个月后换台机器装不上——那时候你已经不记得当初装的是什么。
⑤真机上的 pip check 比 pip freeze 有用:前者查已装包的依赖是否自洽,后者只是把列表导出来。
装完先跑一次 check,比跑起来再报错省事。
30 秒实验
- 把求解器的
reverse=True去掉,改成按版本号正序试,重跑第 ② 步。 装上的 langchain 变成哪个版本?requests 会不会出现? - 给 pydantic 加一个 3.0.0 版本(无依赖),重跑第 ② 步。求解器会不会选它?为什么?
- 在第 ③ 步的需求里把 pydantic 区间从 1.x 放宽到「1 到 3」,冲突消失了吗? 这说明修冲突的哪一条办法在起作用?
常见误区
- 在系统 Python 里直接 pip install。 装的时候一路顺,第二天另一个项目跑不起来,而且完全想不到是昨天那个包干的。
- 遇到冲突就
--force-reinstall。 装是装上了,运行时崩在 import,报错指向的是一个跟冲突毫无关系的地方。 - conda 装一半、pip 装一半,两套包管理器各记各的账。 查依赖树的时候,两边都显示「没问题」,合起来就是跑不起来。
- 照着半年前的教程抄 import。 0.1 之后包被拆过,路径变了,这时候该查的是「这个类现在在哪个包里」,不是改自己写的逻辑。
- base_url 少写或多写一段路径后缀。 不报连接失败,返回 404,而报错文本长得像「模型不存在」,于是开始怀疑模型名写错了。
自测三问(点开对答案)
1. 为什么说「pip 装完能跑」不等于「装对了」? —— 因为求解器只保证这组版本互相不冲突,取的是第一个能走通的组合。 候选的尝试顺序一变,装出来的可能是完全不同的一组——都「能跑」,但都不是你想要的那个。 2. 看到一大段依赖冲突报错,第一眼看哪里? —— 看「谁要什么」和「谁冲突了」这两处。剩下那一大堆是求解器逐个候选版本的失败记录,对你没用。 拿到那两个包的区间,先算交集是不是空的。 3. 为什么 CPU 版和 GPU 版的 torch 装错了不报警? —— 因为它们是两个名字不同、依赖不同的包,都能正常 import、正常算。差别只在有没有 CUDA 支持。 装错的唯一表现是「用不上显卡」,而报错信息长得跟「没装驱动」一模一样。环境装好,最后一步是把这些东西拼成一个能答文档问题的系统, 并且说清楚它到底有多准——下一节算这笔账。
- ☐ (本篇完,下一篇 6.4 Llama 2+LangChain 文档问答模型评估)
相关笔记
- 主要概念:八件零件逐个认识
- 17 课 编程原理:模块、包与依赖
- 文档问答评估:把它拼成文档问答系统
- 🦙 Llama 大模型实践 · 索引