Note_of_Learning(CS&AI)
LangChain v0.3.x与python版本问题
Python 3.8无法使用最新的LangChain v0.3.x,为了获得最佳兼容性,避免潜在问题,推荐使用3.9及其以上的Python版本
问题
在conda中安装langchain及其相关组件时,发现无法安装最新版的LangChain v0.3.x,而是相对过时的LangChain v0.2.x,就算使用下面的命令强制安装最新版得到的也只是0.2.x的版本;
1 | pip install --upgrade langchain langchain-community langchain-core langchain-openai langchain-text-splitters langsmith |
当指定版本时也无法找到对应版本的安装包,ERROR: No matching distribution found for langchain==0.3.25
1 | pip install --upgrade --force-reinstall langchain==0.3.25 |
查阅LangChain v0.3的官网,发现LangChain v0.3对python版本是有要求的,在2024 年 10 月之前可能是支持python3.8的,但是现在是2025-05-10,python3.8已经不受官方支持了,要求python≥3.9,所以如果要使用最新版的LangChain v0.3,要求python版本≥3.9,我现在的虚拟环境是python3.8,不符合要求所以无法安装最新版的模块包
不同python版本对应的的LangChain
解决方法
- 在anaconda中升级python版本,将原来的python3.8升级到python3.9版本
1 | conda install python==3.10 |
- 升级langchain及其相关组件的版本;使用下列命令会尝试升级所列出的这几个langchain包到当前环境和可用软件源下能找到的最新兼容版本,为了满足这些核心包新版本的依赖要求,它也会升级其依赖的包,只处理指定的以及为了满足指定包的依赖而必须升级的包,不会影响没有依赖的包,其他项目指定版本的包也还能正常使用
1 | pip install --upgrade langchain langchain-community langchain-core langchain-openai langchain-text-splitters langsmith |
Python语法
三元表达式
三元表达式提供了一种简洁的方式来根据条件在两个值之间进行选择。它不是一个运算符,而是一种条件表达式(Conditional Expression)。之所以叫“三元”表达式,是因为它涉及到三个操作数:一个条件、条件为真时的结果、条件为假时的结果。主要用于在一个简单的 if-else 逻辑中,根据条件给一个变量赋值,或者在一个表达式中直接根据条件得到一个值。相比传统的 if-else 语句,它更紧凑,适合简单的场景。
语法格式:
1 | value_if_true if condition else value_if_false |
value_if_true: 如果condition为True,整个表达式的值就是这个。if: 关键字。condition: 一个布尔表达式,它的结果是True或False。else: 关键字。value_if_false: 如果condition为False,整个表达式的值就是这个。
1 | # 示例1:找出两个数中的最大值 |
海象运算符(Walrus Operator)
海象运算符是 Python 3.8 引入的一个新特性,正式名称是赋值表达式(Assignment Expression),符号是 :=(因为 := 的形状有点像海象的眼睛和獠牙),作用是可以在表达式内部对变量赋值,同时返回该值
语法格式:
1 | variable := expression |
variable:要赋值的变量expression:计算结果将被赋值给变量的表达式- 整个 := 表达式的值是 expression 的结果
在没有海象运算符之前,如果想在表达式中使用一个值并同时保存它,通常需要两步操作。海象运算符允许一步完成
1 | # 示例一:传统写法,计算一个值并使用它 |
多行字符串''' 的作用
在 Python 中,'''(或 """)可以用来表示多行字符串,但它们是否被视为注释取决于上下文。LangChain 中经常使用多行字符串作为提示词(Prompt)内容,而不是单行字符串(如 “”)
Python 中 ''' 的作用多行字符串,不是直接的注释;'''(三单引号)或 """(三双引号)用于定义多行字符串,而不是专门的注释符号(# 才是专门用作注释的)。如果多行字符串被赋值给变量或在代码中有实际用途,它就是字符串值的一部分。如果多行字符串出现在代码中但未被赋值或使用,Python 解释器会忽略它,因此它可以“间接”用作多行注释。如果 ''' 包裹的内容被赋值给变量或传递给函数,它是实际的字符串数据。
当内容跨越多行时,使用
'''可以保留原来的字符格式(包括换行),增强可读性。
1 | # 未赋值时(作为多行注释) |
JSON格式与 Python 字典的区别
本质:
- JSON 是数据交换格式,基于文本,跨语言支持;用于数据传输/存储(如 API、文件)
- Python 字典是内存中的数据结构,仅用于 Python;用于程序内部操作
语法差异:
- JSON 的键必须是字符串,且用双引号 (“key”)。
- Python 字典键可以是任何可哈希对象(如字符串、数字、元组)。
- JSON 值仅支持:字符串、数字、布尔、null、对象、数组。
- Python 字典值支持任意 Python 对象。
- JSON 使用 true, false, null;Python 用 True, False, None。
序列化/反序列化:
- JSON 需要通过 json.dumps()(序列化)或 json.loads()(反序列化)与 Python 字典互转
- Python 字典可直接操作
1 | # JSON示例 |
{"name": "Alice", "age": 25, "active": true} 在特定情况下既可以看作 JSON,也可以看作 Python 字典
作为 JSON:
- 它是合法的 JSON 文本格式,符合 JSON 规范(键是双引号字符串,值是字符串/数字/布尔)。
- 通常用于数据传输或存储(例如 API 响应、文件)。
- 需要通过 json.loads() 解析成 Python 字典才能在 Python 中操作。
作为 Python 字典:
- 它在 Python 中可以直接作为字典字面量运行({“name”: “Alice”, “age”: 25, “active”: True}),因为语法兼容。
- 但注意,JSON 的 true 在 Python 中需解释为 True(Python 不认 true)。
- 如果直接写在 Python 代码中,它就是内存中的字典对象,无需解析。
1 | import json |
JSON 用
[]包住表示数组,通常用于表示多条数据;不带[]的是单个JSON对象。[]常用于数据传输多涉及列表或集合。
有时候会发现json数据前面被[]包住,表示它是一个 JSON 数组,而非单个 JSON 对象。JSON 的顶层结构可以是对象 {} 或数组 [],具体取决于数据设计;[] 用于存储一组有序的数据(列表),通常表示多个记录或条目。例如,API 返回多条数据时,常以数组形式组织。
1 | # 表示多个对象(例如多条用户信息),对应 Python 的 list 包含多个 dict。 |
LangChain中的LLM和ChatModel有什么不同
在 LangChain 框架中,LLM(Large Language Model)和 ChatModel 都是用于与大型语言模型交互的抽象接口,但它们的设计目的和输入/输出格式有着本质的区别;LLM 和 ChatModel 是 LangChain 中实现调用方式的具体类
“补全型”和“对话型”描述的是模型交互的方式,是更抽象的概念,适用于任何支持这些交互的模型或框架。
LLM 和 ChatModel 是 LangChain 中的具体实现类,专为 LangChain 生态设计,封装了补全型和对话型的调用逻辑。
输入/输出格式不同:
LLM:主要处理字符串的输入和输出。给它一个文本字符串作为提示词(prompt),它返回一个文本字符串作为补全或生成的结果。更像是一个“文本补全”或“文本生成”引擎(补全型),使用单一 Prompt,适合单次文本生成。。- 输入: 单个字符串 (
"请写一篇关于人工智能的短文。") - 输出: 单个字符串 (
"人工智能是...")
- 输入: 单个字符串 (
ChatModel:主要处理消息对象列表作为输入,并返回一个消息对象作为输出。它理解“对话轮次”和“角色”(如用户消息、AI 消息、系统消息),更适合进行多轮对话(对话型),使用消息列表(SystemMessage、HumanMessage 等),适合多轮对话和角色明确的任务。- 输入: 消息对象列表 (
[SystemMessage(content="你是一个乐于助人的助手。"), HumanMessage(content="你好!")]) - 输出: 消息对象 (
AIMessage(content="你好!有什么可以帮助你的吗?"))
- 输入: 消息对象列表 (
**LLM:**更适合单轮的文本生成、提取、翻译、摘要等任务,其中输入是一个独立的文本块,不需要考虑之前的对话历史。如根据一段文本生成标题,或者对一个问题进行一次性回答。
ChatModel: 专门为多轮对话和构建聊天机器人而设计。它能够自然地处理对话历史,理解不同角色的发言,从而生成更连贯、上下文相关的回复。构建需要记忆和上下文的复杂 Agent 时,通常会使用 ChatModel。
1 | # 使用LLM方式 |
| 特性 | LLM |
ChatModel |
|---|---|---|
| 输入类型 | 字符串 (String) | 消息对象列表 (List of Message) |
| 输出类型 | 字符串 (String) | 消息对象 (Message) |
| 核心交互 | 文本补全 / 单轮生成 | 基于消息的多轮对话 |
| 适用场景 | 单轮任务、文本生成、提取、摘要 | 聊天机器人、Agent、多轮对话 |
| 记忆/上下文 | 不内置处理对话历史 | 内置处理对话轮次和角色 |
LangChain中补全型(Completion)与对话型(Chat)的区别
类似于上面的
LLM与ChatModel的区别. 补全型 ≈ LLM 类;对话型 ≈ ChatModel 类;
补全型
补全型指的是向语言模型提供一段文本(称为“提示”或 Prompt),模型根据这段文本生成后续的文本,完成内容的“补全”。不支持多轮对话,单次交互
这种方式通常是单次交互,模型不维护对话上下文,输入和输出是一次性的。
没有明确的角色(如用户、助手),输入和输出是“连续的文本”。
适合场景:文本生成、文章续写、代码补全等。
对话型
对话型(Chat)指的是以对话的形式与模型交互,输入是一系列明确角色的消息(比如 System、User、Assistant),模型根据这些消息生成回复。
这种方式通常支持多轮对话,模型可以理解上下文,适合模拟人机交互。
输入:由多个消息组成,每个消息有明确的角色(例如 System 设置背景,User 提出问题,Assistant 回复)。
输出:模型以对话形式返回回复,通常是 Assistant 角色的消息。
支持上下文管理,适合多轮对话。支持多轮对话,维护上下文
适合场景:问答、聊天机器人、任务助手等。
LangChain 封装了多种语言模型,支持与补全型和对话型模型的交互,通过不同的接口和类来处理这两种调用方式,在下面的代码中体现为提示词的prompt与messages的区别;在导包时分别导入的是OpenAI与ChatOpenAI
1 | # 补全型调用 |
Langchain 消息类型:角色与消息类的区别与用法
'user', 'assistant', 'system', 'tool' 是消息的role。
HumanMessage, AIMessage, SystemMessage, ToolMessage 是 Langchain 中用来表示这些role的 Python 类。两种方式都能实现工具调用,但消息类形式更直观。
本质:
- 消息是结构化的数据,可以通过字典(包含 role 和 content)或消息类(如 SystemMessage、AIMessage)来表示。
两种写法的不同:
字典形式:直接用 {‘role’: ‘user’, ‘content’: ‘文本’}的方式相对更加灵活,常用于直接与底层LLM模型的 API 进行交互或动态构造消息;适合低层次操作、调试,或直接传递给不支持 LangChain 消息类的模型。更接近底层 API,最好不在 Langchain 代码中常用
消息类形式:如 SystemMessage(content=’文本’)、AIMessage(content=’文本’)是 LangChain 的高级封装,更结构化,更加适合 LangChain 的生态。尽量在 LangChain 框架内使用消息类的形式( 链、工具调用、聊天历史管理、ChatPromptTemplate、LCEL等),因为 LangChain 的组件更适配消息类。
为什么两种形式都存在,且在langchain中两种形式都可以使用?
字典形式是模型底层的通用格式(例如 OpenAI 的 API 接受 role 和 content 的字典列表)。
消息类是 LangChain 为了简化开发、增强类型安全和功能(如 tool_calls、response_metadata)提供的封装。
1 | # 字典形式(使用 role 和 content) |
角色 (role) |
消息类 | 说明 |
|---|---|---|
system |
SystemMessage |
系统指令,定义模型行为 |
user |
HumanMessage |
用户输入 |
assistant |
AIMessage |
模型回复,可包含 tool_calls |
tool |
ToolMessage |
工具调用结果 |
文本分割器
从高层次来看,文本分割器的工作原理如下:
- 将文本拆分成小的、语义上有意义的块(通常是句子)。
- 开始将这些小块组合成一个更大的块,直到达到某个大小(通过某个函数来衡量)。
- 一旦达到该大小,将该块作为独立的文本片段,然后开始创建一个新的文本块,并保持一些重叠(以保持块之间的上下文)。
模型参数不同导致的训练时长和显存占用问题
我使用llama-factory来微调一个模型,跟着教程来做的 所有步骤都一样,只有一项不同:我用的是7B的模型,他用的是0.5B的模型,都是Qwen2.5模型 但是他的训练很快就完成了 我的训练很慢很慢 ,是什么原因?所有参数设置和数据集都是一样的
- 主要原因在于模型大小差异巨大。
- 模型参数量是关键: 我使用的是一个 70 亿参数 (7B) 的模型,而教程使用的是一个 5 亿参数 (0.5B) 的模型。7B 模型比 0.5B 模型大了整整 14倍
- 训练计算量与参数量直接相关: 模型训练(尤其是前向传播和反向传播计算梯度)的核心过程是对模型的所有参数进行大量的数学运算。参数量越大,每次训练迭代(一步)需要进行的计算就越多。
- 每一步都更慢: 即使使用完全相同的数据集和参数设置,由于 7B 模型每一步需要处理的参数量是 0.5B 模型的 14 倍,因此 7B 模型完成每一步训练所需的时间会比 0.5B 模型长得多。
- 总时间累积: 训练总时间是每一步训练所需时间乘以总的训练步数。即使总步数一样(取决于数据集大小和 epoch 数),由于每一步都慢很多,累积起来的总训练时间就会变得非常非常长。
所以即使数据集和所有其他参数都相同,仅仅因为训练的模型从 0.5B 变成了 7B,训练速度变慢是完全正常的,也是预期的结果。训练一个 7B 模型所需的计算资源(时间和硬件要求)远高于训练一个 0.5B 模型。
还有一个问题,就是我在设置batch_size=30 截断长度=230时,别人能正常运行,而且显存占用不高 ,我同样的配置和参数设置,却提示了爆显存,只是模型选择不同,为什么会这样?
原因依然是 模型参数大小不同
显卡内存在模型训练时主要用于存储以下几个关键部分:
- 模型参数 (Model Parameters): 这是模型本身的权重和偏置。7B 模型有 70 亿个参数,0.5B 模型有 5 亿个参数。存储这些参数本身就需要大量的显存。7B 模型仅仅加载到显存中,所占用的空间就比 0.5B 模型大很多倍。
- 优化器状态 (Optimizer States): 大多数现代优化器(如 Adam, AdamW)会为模型的每一个参数存储额外的状态信息(例如动量、方差等)。这些状态信息占用的显存通常是模型参数本身的 1 到 4 倍。因此,7B 模型的优化器状态占用的显存也是 0.5B 模型的许多倍。
- 中间激活值 (Intermediate Activations): 在前向传播计算时,模型每一层都会产生输出(称为激活值),这些激活值需要在反向传播时用于计算梯度。激活值的大小与 Batch Size、序列长度以及模型的内部维度有关。虽然 Batch Size 和序列长度相同,但 7B 模型通常有更多的层和/或更大的隐藏层维度,所以 即使 Batch Size 和序列长度相同,7B 模型产生的中间激活值通常也会比 0.5B 模型更大。
- 梯度 (Gradients): 在反向传播时计算出的梯度需要存储起来,用于更新模型参数。梯度的数量与模型参数数量一致,所以 7B 模型的梯度占用的显存也是 0.5B 模型的很多倍。
为什么 7B 模型会爆显存而 0.5B 不会?
- 0.5B 模型: 模型参数和优化器状态占用的基础显存相对较小。Batch Size=30, 截断长度=230 产生的激活值和梯度占用的显存加在上面,总和还在GPU 的容量范围内。
- 7B 模型: 模型参数和优化器状态占用的基础显存就已经非常巨大了。 当再加入 Batch Size=30 和截断长度=230 产生的激活值和梯度所需的显存时,这些额外的显存是叠加在一个已经非常庞大的基础之上的。即使 Batch Size 和截断长度带来的额外显存需求(激活值、梯度)比例上看起来相似,但由于基础占用巨大,总和很容易就超过了 GPU 的总显存容量,从而导致了爆显存。
所以,即使保持了相同的 Batch Size 和截断长度,仅仅因为模型本身大小(参数量、内部维度)巨大,它所需要的总显存(尤其是参数、优化器状态、激活值和梯度的总和)就远超小模型,从而导致了显存不足。
Qwen打包部署(大模型转换为 GGUF 以及使 用 ollama 运行)
RAG中完成向量匹配之后传入LLM的数据类型是什么?
一般的RAG流程会先把源文档使用Embedding Model处理成向量的形式然后存储到向量数据库中,然后当用户提出问题query时,会使用同一个Embedding Model对问题也做一次向量化,然后把问题和向量数据库中的内容做相似度对比,找出top_k个相似片段,然后把问题和找到的相似片段都传给LLM做推理和分析,找出符合问题的答案。
需要注意的是,传入LLM的数据是经过分片的原始文本表示,而不是向量化后的高维数据。
RAG流程如下
步骤 1:文档处理(Offline/Indexing)
- 原始文档被切分成小的、有意义的文本片段(chunks)。
- 每个文本片段通过嵌入模型转换为向量。
- 这些文本片段的向量和它们的原始文本内容一起存储在向量数据库中。通常,向量数据库会存储向量,并关联一个指向原始文本(或直接存储原始文本)的ID或元数据。
步骤 2:用户查询(Online/Inference)
- 用户提出问题。
- 用户的查询通过同样的嵌入模型转换为向量。
- 这个查询向量用于在向量数据库中进行相似度搜索,查找与查询最相似的
top_K个文档向量。
步骤 3:检索并获取原始文本:
- 从向量数据库中检索到的不仅仅是向量,更重要的是与这些
top_K向量关联的原始文本片段(content)。
步骤 4:构建LLM提示:
- 用户的原始问题和检索到的
top_K个原始文本片段被组织成一个结构化的提示(prompt),然后发送给LLM。
步骤 5:LLM生成答案:
- LLM接收这个包含原始问题和相关上下文的提示,然后基于这些信息生成最终的答案。
能不能直接使用LLM作为RAG项目的嵌入模型?
硬要用的话,其实是可以用的;但是一般不会这么用。RAG项目通常会使用一个单独的嵌入模型(Embedding Model)来将文档和用户查询转换为向量,而不是直接用生成式大模型(LLM)来做这件事。
嵌入模型的本质
嵌入模型是一种将离散数据(如文本、图像)映射到连续向量空间的技术。通过高维向量表示(常见的为768维~3072维),模型可捕捉数据的语义信息,使得语义相似的文本在向量空间中距离更近。例如,“忘记密码”和“账号锁定”、“河流”和“瀑布”会被编码为相近的向量,从而支持语义检索而非仅关键词匹配。
主要是效率和成本问题
计算效率:将文档转换为向量并存储在向量数据库中需要一个高效的嵌入过程。专门的嵌入模型通常比LLM更小、更快、更节省资源。如果用LLM来做嵌入,每次调用(无论是文档预处理还是用户查询)都会消耗大量的计算资源和时间,成本会非常高。
存储效率:生成式LLM内部的表示可能维度非常高,而且不一定是最紧凑有效的语义表示,可能导致向量数据库的存储开销更大。
实时性要求:当用户提问时,需要实时地将查询转换为向量,然后快速地在向量数据库中进行相似度搜索。如果使用生成式LLM来做这个,延迟会非常高,影响用户体验。
模型的职责分离和优化:
- 嵌入模型(Embedding Model):这类模型是专门为了将文本映射到高维向量空间而训练的。它们的目标是让语义相似的文本在向量空间中距离更近。例如,Sentence-BERT、OpenAI的
text-embedding-ada-002、各种开源的通用嵌入模型(如bge-large-en、all-MiniLM-L6-v2等)。它们通常是双编码器(Bi-Encoder)架构,即查询和文档分别独立编码,然后计算相似度。这种架构在检索效率上非常高。 - 生成式大模型(Generative LLM):像GPT-3/4、Llama、Mistral这类模型,它们的主要任务是理解并生成文本。虽然它们内部也有将文本编码为向量的过程,但这些内部向量(通常是最后一个隐藏层的输出)是针对生成任务优化的,并不直接适合作为通用语义相似度匹配的嵌入。它们通常是交叉编码器(Cross-Encoder)或解码器架构,需要同时输入查询和文档来计算相关性分数,这在检索大规模文档时效率极低。








