第1节 · 大模型基础与架构
大模型基础
LLM基础知识
【了解】课程安排
大模型课程整体安排:
- 大模型基础
- EduRAG项目
- Agent智能体
- 微调
1.后续使用大模型,几乎和提示词是绑定的。
2.垂直领域:RAG(检索,增强,生成,外挂知识库)
3.被动的,查询,辅助,信息参考。没办法帮我们干活。Agent(llm)
4.一定程度可以解决垂直领域(身份问题、角色问题)的问题,还可以在一些边缘设备使用。
黑马 -> qwen -> 微调 -> 黑马大模型
【了解】今日内容介绍
- 语言模型介绍
- 概念
- 发展历程
- 大语言模型介绍
- LLM三大主流架构
- LLM使用
- 语言模型的评估指标
- BLEU
- ROUGE
- PPL
- 扩展
【了解】语言模型介绍
什么是语言模型
语言模型(Language Model,LM)旨在建模词汇序列的生成概率,提升机器的语言智能水平,使机器能够模拟人类说话、写作的模式进行自动文本输出。
通俗理解: 用来计算一个句子的概率的模型,也就是判断一句话是否是人话的概率。
语言模型的发展历程
什么是语言模型
语言模型(Language Model,LM)旨在建模词汇序列的生成概率,提升机器的语言智能水平,使机器能够模拟人类说话、写作的模式进行自动文本输出。
通俗理解: 用来计算一个句子的概率的模型,也就是判断一句话是否是人话的概率.
标准定义:对于某个句子序列, 如S = {W1, W2, W3, …, Wn}, 语言模型就是计算该序列发生的概率, 即P(S). 如果给定的词序列符合语用习惯, 则给出高概率, 否则给出低概率.
这个概率计算可以基于概率论的 链式法则 进行分解:
$$
P(S) = P(W_1)*P(W_2|W_1)P(W_3|W_2,W_1)...*P(W_n|W_{n-1},W_{n-2})
$$
这个公式意味着,整个句子的概率等于每个词在给定其所有前文条件下出现的概率的乘积。
因此,语言模型的本质任务可以归结为 预测下一个词 :计算在已知前文 (w_1, ..., w*{i-1}) 的情况下,下一个词是 w_i 的概率 P(w_i|w_1, ..., w*{i-1})。
语言模型发展历程
语言模型的技术发展大致可以分为四个主要阶段:
- 基于规则和统计的语言模型 (Rule-based & Statistical LM)
- 神经网络语言模型 (Neural Network LM)
- 预训练语言模型 (Pre-trained LM)
- 大语言模型 (Large LM)
| 阶段 | 时间周期 | 主要特点 |
|---|---|---|
| 基于规则与统计的语言模型 | 20世纪80年代末 - 21世纪初 | 基于规则与概率:这类模型主要依赖于人工设计的规则和统计方法。如N-gram模型通过计算词语出现的频率来预测下一个词,但存在稀疏性问题,无法很好地处理未见过的词语组合。 |
| 神经网络语言模型 | 2000年代 - 2010年代中期 | 引入词向量:NNLM利用神经网络,将词语映射成连续的向量(词嵌入),从而捕捉词语间的语义关系。这解决了稀疏性问题,并能更好地理解上下文。然而,NNLM通常需要针对特定任务进行端到端训练。 |
| 预训练语言模型 | 2018年 - 2020年左右 | 预训练 + 微调:以Transformer架构为核心,PLM(如BERT、GPT-1/2)通过在海量文本上进行无监督预训练来学习通用语言知识,然后针对下游任务进行微调。这极大地提高了模型的泛化能力和任务表现。 |
| 大语言模型 | 2020年至今 | 规模化与涌现能力:LLM(如GPT-3/4、PaLM)在PLM的基础上,进一步扩大模型规模(参数量、数据量),从而展现出涌现能力,可以执行多样的任务,如问答、摘要、代码生成等,甚至不需要微调(in-context learning)。 |
基于规则和统计的语言模型
如果直接使用链式法则计算 P(W1,W2,….Wn) 会面临两个巨大挑战:
- 参数空间爆炸:随着上下文长度的增加,可能的词语组合会呈指数级增长,无法计算和存储。
- 数据稀疏性:在有限的语料库中,一个特定的长序列很可能从未出现过,导致其概率为零,这显然不合理。
为了解决上述问题,引入 马尔科夫假设 :一个词出现的概率只与它前面出现的有限的一个或者几个词有关。
- 如果一个词的出现与它周围的词是独立的,那么我们就称之为unigram也就是一元语言模型.
- 如果一个词的出现仅依赖于它前面出现的一个词,那么我们就称之为bigram.
- 如果一个词的出现仅依赖于它前面出现的两个词,那么我们就称之为trigram.
一般来说,N元模型就是假设当前词的出现概率只与它前面的N-1个词有关,而这些概率参数都是可以通过大规模语料库来计算,比如三元概率:
$$
P(Wi|Wi−1,Wi−2)=Count(Wi−2Wi−1Wi)/Count(Wi−2Wi−1)
$$
在实践中用的最多的就是bigram和trigram,接下来以bigram语言模型为例,理解其工作原理:
- 首先我们准备一个语料库(简单理解让模型学习的数据集),为了计算对应的二元模型的参数,即P(Wi|Wi−1),我们要先计数即C(Wi−1,Wi),然后计数 C(Wi−1) , 再用除法可得到概率。
- C(Wi−1,Wi) 计数结果如下:
- C(Wi−1) 的计数结果如下:
那么bigram语言模型针对上述语料的参数计算结果如何实现?假如,我想计算P(想|我)≈0.38 ,计算过程如下显示:(其他参数计算过程类似)
$$
P(想|我)=C(我,想)C(我)=\frac{800}{2100}≈0.38
$$
如果针对这个语料库的二元模型(bigram)建立好之后,就可以实现我们的目标计算。
计算一个句子的概率,举例如下:
$$
P(我想去打篮球)=P(想|我)∗P(去|想)∗P(打|去)∗P(篮球|打)=\frac{800}{2100}∗\frac{600}{900}∗\frac{690}{2000}∗\frac{20}{800}≈0.0022
$$
预测一句话最可能出现的下一个词汇,比如:我想去打【mask】? 思考:mask = 篮球 或者 mask = 晚饭
$$
P(我想去打篮球)=≈0.0022
$$
可以看出P(我想去打篮球)>P(我想去打晚饭),因此mask = 篮球,对比真实语境下,也符合人类习惯。
N-gram语言模型的特点:
- 优点:
- 原理简单,易于理解,可解释性强。
- 参数可以直接从语料库中统计得到,训练简单。
- 缺点:
- 上下文长度受限:无法捕捉长距离依赖关系。
- 数据稀疏性:即使有平滑技术(如拉普拉斯平滑),对于未见过的 N-gram 组合,模型效果依然很差 (OOV, Out-of-Vocabulary 问题)。 在 N-gram 模型中,当一个词语组合在训练语料中从未出现过时,其词频为零,导致其概率为零,所以会采用拉普拉斯平滑,即分字分母都加上一个 常数 α,通常 α=1 。
- 泛化能力差:模型完全依赖于语料库中的词频,无法理解词语之间的语义相似性。例如,模型不知道“喜欢”和“热爱”是近义词。
神经网络语言模型
为了解决传统统计语言模型(如N-gram)的“数据稀疏”(即无法处理未在训练语料中出现的词组)和泛化能力弱的问题,研究者们引入了神经网络。2003年由Bengio等人提出的NNLM是这一领域的开创性工作。其核心思想是:不仅要根据前文预测下一个词,还要在这个过程中学习到词与词之间内在的联系。
- 模型的输入:wt−n+1,…,wt−2,wt−1就是前n-1个词。现在需要根据这已知的n-1个词预测下一个词wt。C(w)表示w所对应的词向量.
- 网络的第一层(输入层)是将C(wt−n+1),…,C(wt−2),C(wt−1)这n-1个向量首尾拼接起来形成一个(n−1)∗m大小的向量,记作x.
- 网络的第二层(隐藏层)就如同普通的神经网络,直接使用一个全连接层, 通过全连接层后再使用tanh这个激活函数进行处理。
- 网络的第三层(输出层)一共有V个节点 (V 代表语料的词汇),本质上这个输出层也是一个全连接层。每个输出节点yi表示下一个词语为 i 的未归一化log 概率。最后使用 softmax 激活函数将输出值y进行归一化。得到最大概率值,就是我们需要预测的结果。
神经网络特点:
- 优点:
- 解决了数据稀疏性:通过词向量,模型可以泛化到训练时未见过的词语组合。
- 捕捉语义相似性:语义相近的词(如“猫”和“狗”)在向量空间中的位置也相近,使得模型具有更好的泛化能力。
- 缺点:
- 上下文长度固定:与 N-gram 类似,它仍然只能处理固定长度的上下文。
- 计算复杂度高:输出层的 Softmax 计算量巨大,与词表大小成正比。
- 长距离依赖问题:后续的 RNN/LSTM 模型虽然解决了变长上下文的问题,但仍面临梯度消失/爆炸,难以有效捕捉长距离依赖。
基于Transformer的预训练语言模型
2017年,Google 提出的 Transformer 模型彻底改变了 NLP 领域。其核心是 自注意力机制 (Self-Attention Mechanism),允许模型在处理一个词时,直接计算句子中所有其他词对该词的影响力,从而完美地解决了长距离依赖问题。并且可以对序列中的所有位置 并行计算,大幅提升训练速度。
Transformer模型由一些编码器和解码器层组成(见图),学习复杂语义信息的能力强,很多主流预训练模型在提取特征时都会选择Transformer结构,并产生了一系列的基于Transformer的预训练模型,包括GPT、BERT、T5等。这些模型能够从大量的通用文本数据中学习大量的语言表示,并将这些知识运用到下游任务中,获得了较好的效果.
预训练语言模型的使用方式:
- 预训练:预训练指建立基本的模型,先在一些比较基础的数据集、语料库上进行训练,然后按照具体任务训练,学习数据的普遍特征。
- 微调:微调指在具体的下游任务中使用预训练好的模型进行迁移学习,以获取更好的泛化效果。
预训练语言模型的特点:
- 优点:更强大的泛化能力,丰富的语义表示,可以有效防止过拟合。
- 缺点:计算资源需求大,可解释性差等。
【理解】大模型介绍
大模型简介
大模型有两层含义:
- 大规模模型(Large Scale Model)
这是最本质的含义。任何模型,只要参数量大,就可以称之为大模型。它可以有文本模型、语音模型、视觉模型等。
大:参数量大、数据量多、算力要求高。
- 大语言模型(Large Language Model)
本质上,它是大模型的子集。但是由于ChatGPT的兴起,这是是当下最流行的分支。默认情况下说的大模型就是大语言模型。
大语言模型
随着对预训练语言模型研究的开展,人们逐渐发现可能存在一种标度定律(Scaling Law),即随着预训练模型参数的指数级提升,其语言模型性能也会线性上升。2020年,OpenAI发布了参数量高达1750亿的GPT-3,首次展示了大语言模型的性能。
相较于此前的参数量较小的预训练语言模型,例如,3.3亿参数的Bert-large和17亿参数的GPT-2,GPT-3展现了在Few-shot语言任务能力上的飞跃,并具备了预训练语言模型不具备的一些能力。后续将这种现象称为能力涌现。例如,GPT-3能进行上下文学习,在不调整权重的情况下仅依据用户给出的任务示例完成后续任务。这种能力方面的飞跃引发研究界在大语言模型上的研究热潮,各大科技巨头纷纷推出参数量巨大的语言模型,例如,Meta公司1300亿参数量的LLaMA模型以及谷歌公司5400亿参数量的PaLM。国内如百度推出的文心一言ERNIE系列、清华大学团队推出的GLM系列,等等。这些模型的参数量极其庞大(千亿甚至万亿级别),因此被称为 大语言模型 (Large Language Model, LLM) 。
LLM通常都是具 大规模参数 (通常在十亿到万亿级别)的深度学习模型。这类模型通过在 大规模数据集 上进行训练,具备强大的泛化能力和复杂的任务处理能力,尤其在自然语言处理(NLP)、计算机视觉(CV)和多模态任务中表现突出。例如,GPT-3(1750亿参数)与传统的语言模型(如 N-gram)相比,LLM 具备以下革命性特点:
- 规模巨大 (Scale):参数量通常超过10亿(1B),例如 GPT-3 拥有1750亿参数,PaLM 拥有5400亿参数。巨大的规模是实现强大能力的基础。
- 涌现能力 (Emergent Abilities):当模型规模达到一定程度时,会涌现出一些意想不到的能力,如上下文学习 (In-Context Learning)、零样本/少样本任务处理 (Zero/Few-Shot Learning)、逻辑推理等,这些能力在小模型上并不存在。
- 通用性 (Generality):LLM 不再局限于单一的 NLP 任务(如文本分类、情感分析),而是能够通过自然语言指令 (Instruction) 完成多样化的、开放式的任务,展现出通用人工智能 (AGI) 的潜力。
- 生成式 (Generative):LLM 不仅能“理解”语言,更能“生成”流畅、连贯且富有创造性的文本内容,这也是其被称为“生成式AI”的核心原因。
大语言模型的特点:
- 优点:像“人类”一样智能,具备了能与人类沟通聊天的能力,甚至具备了使用插件进行自动信息检索的能力
- 缺点:参数量大,算力要求高、时效性低、知识滞后性、生成部分有害的、有偏见的内容等等
LLM三大主流架构
原始的 Transformer 包含编码器 (Encoder) 和解码器 (Decoder) 两部分。在后续的发展中,逐渐演化为三大主流架构,催生了如下图所示的“模型家族树”。
-
Encoder-Only架构
-
代表模型:BERT, RoBERTa, DeBERTa
-
特点:能够同时“看到”整个输入句子的上下文(双向理解),极其擅长深度理解。
-
适用任务:文本分类、情感分析、命名实体识别等需要对文本进行全面理解的任务。
-
-
Encoder-Decoder
-
代表模型:T5, BART, Flan-T5
-
特点:保留了原始 Transformer 的完整结构,适用于将一个文本序列转换为另一个文本序列的序列到序列任务。
-
适用任务:机器翻译、文本摘要、问答等。
-
-
Decoder-Only架构
-
代表模型:GPT 系列, PaLM, LLaMA, ChatGPT
-
特点:自回归(Auto-regressive)模型,根据前面的文本预测下一个词,极其擅长内容生成。
-
适用任务:文章写作、聊天对话、代码生成等需要创意和流畅表达的任务。
LLM主流任务类型
本部分从“用户要用模型来做什么”的角度出发,对 LLM 进行功能性分类。这种分类方式与模型架构紧密相关,因为特定的架构通常最适合执行特定的任务。
以下是详细的分类表格:
| 类别 | 核心能力与描述 | 关联架构 | 典型应用场景 | 代表模型 |
|---|---|---|---|---|
| 理解式/嵌入式模型 | 将文本转换为“语义指纹”向量,用于快速召回。 | 仅编码器 (双塔) encoder | 即语义理解任务:语义搜索(召回阶段)、文本聚类、推荐系统 | BERT, RoBERTa, BGE |
| 判别式/分类模型 | 对文本进行分析并输出一个明确的标签或分数。 | 仅编码器 encoder | 即分类模型任务:情感分析、意图识别、文本匹配 | BERT, ERNIE, ALBERT |
| 生成式模型 | 根据上文创作全新的文本内容。 | 仅解码器decoer | 即生成任务:对话机器人、内容写作、代码生成 | GPT 系列, LLaMA, PaLM |
| 重排/精准排序模型 (Rerank) | 对“查询-文档”对进行深度相关性打分,以优化排序。 | 仅编码器 (交叉) | 即排序任务:搜索引擎精准排序、RAG 系统优化、答案选择 | Cohere Rerank, bge-reranker-large, LongLM |
| 序列到序列模型 | 将一个输入文本序列转换为另一个输出文本序列。 | 编码器-解码器 | 即文本转换任务:机器翻译、文本摘要、生成式问答 | T5, BART, Flan-T5 |
LLM训练
大模型训练流程
- 其中 大模型的训练方式及产生的模型 包括:
| 阶段 | 方法 | 产生的模型类型 | 特点 |
|---|---|---|---|
| 预训练 | 在大规模数据集上进行无监督或自监督学习,学习通用特征 | 基础模型 (Base) | 通用知识强,但不会对齐 |
| 监督微调 | 在特定任务数据集上进行监督学习,优化模型性能。 | 指令微调模型 (SFT) | 能理解指令和任务 |
| 对齐与增强 | 训练奖励模型、用人类反馈做强化学习(如PPO/DPO)让模型更符合人类偏好与安全要求 | 对齐模型 (Chat/Aligned) | 更安全、更符合人类偏好 |
| 任务/领域微调 | LoRA/PEFT/全参微调 | 专用模型 (Domain/Task) | 适应行业和特定任务 |
大模型训练细节
| 列名(Column) | 中文含义 | 详细说明 |
|---|---|---|
| Model | 模型名称 | 表示大语言模型的名称,如 T5、mT5、CPM-2 等,通常附带文献引用编号(如 [73])。 |
| Release Time | 发布时间 | 模型首次公开发布的日期,格式为“月-年”,例如 "Oct-2019" 表示 2019 年 10 月。 |
| Size (B) | 模型规模(十亿参数) | 模型参数数量,单位为十亿(Billion)。例如,“11” 表示 110 亿参数,“175” 表示 1750 亿参数。 |
| Base Model | 基础模型 | 该模型所基于的原始架构或预训练模型,如 T5、mT5 等;“-” 表示从头训练或未指定。 |
| Adaptation IT / RLHF | 适应性训练方法 | 是否使用了: • IT:指令微调(Instruction Tuning) • RLHF:基于人类反馈的强化学习(Reinforcement Learning from Human Feedback) “✓” 表示使用,“-” 表示未使用。 |
| Pre-train Data Scale | 预训练数据规模 | 预训练阶段使用的数据总量,通常以 token 数量表示,如 “1T tokens”(1 万亿 token),也有以存储大小表示如 “1.1TB”。 |
| Latest Data Timestamp | 数据最新时间戳 | 训练数据截止的最晚时间,如 “Apr-2019” 表示数据截止到 2019 年 4 月;“-” 表示未知或未提供。 |
| Hardware (GPUs / TPUs) | 训练硬件资源 | 训练该模型所使用的计算设备,包括数量和型号,如 “96 40G A100” 表示使用了 96 块 40GB 的 NVIDIA A100 GPU。 |
| Training Time | 训练时长 | 完成模型训练所需的时间,单位如小时(h)、天(d),例如 “27 h” 表示 27 小时;“-” 表示未提供。 |
| Evaluation ICL / CoT | 评估方式支持 | 是否支持以下两种评估/推理方式: • ICL:上下文学习(In-context Learning) • CoT:思维链推理(Chain-of-Thought) “✓” 表示支持,“-” 表示不支持或未测试。 |
拓展:模型预训练时常用数据集:
- BookCorpus:BookCorpus将 11,000 本未出版书籍的爬取数据转化为一个 9.85 亿字的数据集。它最初创建的目的是将书籍中的故事情节与电影解读进行匹配。该数据集曾用于训练 RoBERTA、XLNET 和 T5 等法学硕士 (LLM)。
- C4:(Cleaned Common Crawl Corpus)是从Common Crawl数据集中精选并清理过的一个子集。Common Crawl数据集包含从数十亿个网页中提取的数 TB 级原始网络数据。它每月都会发布爬虫获取的新数据文件。包括 GPT-3、LLaMA、OpenLLaMa 和 T5 在内的多个大型语言模型都使用 CommonCrawl 进行了训练。
- Wikipedia:Wikipedia维基百科数据集由维基百科网站清理后的文本数据整理而成,涵盖所有语言版本。默认的英语维基百科数据集包含 19.88 GB 的海量完整文章示例,有助于完成语言建模任务。该数据集曾用于训练 Roberta、XLNet 和 LLaMA 等大型模型。
- ROOTS:ROOTS是一个 1.6TB 的多语言数据集,涵盖 59 种语言的文本。该数据集旨在训练 BigScience 大型开放科学开放获取多语言 (BLOOM) 语言模型。ROOTS 使用来自 Common Crawl、GitHub Code 和其他众包项目的经过大量去重和过滤的数据。
大模型权重的存储
大模型训练之后,会将模型进行保存。实际大模型权重的存储格式多种多样,每种格式都有其独特的特点和适用场景。以下是一些常见的大模型权重存储格式:
| 格式分类 (常见扩展名) | 一句话概括 | 主要优点 | 最适合的场景 | 关键点 / 备注 |
|---|---|---|---|---|
| PyTorch 格式 (.pth, .pt, .bin) | PyTorch框架的“官方标配”,训练和研究时最常用。 | 灵活,能保存任何东西,在PyTorch生态里用起来最顺手。 | 模型开发与训练:自己从头开始训练模型或做学术研究。 | 存在安全风险,加载不可信文件可能执行恶意代码。 |
| Safetensors 格式 (.safetensors) | Hugging Face推出的“安全卫士”。 | 绝对安全,加载速度飞快,是未来的趋势。 | 分享与下载模型:从网上下载别人训练好的模型时,这是首选。 | 只存储纯粹的权重数据,不包含任何可执行代码。 |
| GGUF 格式 (.gguf) | 在普通电脑CPU上跑大模型的“平民神器”。 | 对CPU特别友好,把模型所有信息打包成一个文件,方便管理。 | 本地化部署:在没有高端GPU的个人电脑或笔记本上运行大模型。 | 必须搭配量化模型才能发挥优势。技术上能存非量化模型,但这样做没意义且不会变快。 |
| 量化格式 (.gguf, ...q4_0.bin) | 模型的“减肥版”,通过降低精度给模型瘦身。 | 大幅减小模型体积和内存占用,让低功耗设备也能运行。 | 边缘计算:在手机、树莓派等计算资源非常有限的设备上部署模型。 | 量化是一种技术,GGUF 是承载这种技术最流行的格式之一。 |
| 分片格式 (...00001-of-00002.bin) | “化整为零”,把一个巨大到存不下的模型切成几块存放。 | 解决了单个文件过大的问题,便于存储和网络传输。 | 超大模型管理:处理和加载那些几十、上百GB的巨型模型。 | 通常是 .bin 或 .safetensors 格式,只是被切割成了多个部分。 |
| ONNX 格式 (.onnx) | 模型界的“通用护照”,让模型在不同框架间自由穿行。 | 跨平台、跨框架,一次转换,处处可用。 | 跨平台部署:把PyTorch训练的模型,部署到用TensorFlow或其他框架的环境中。 | 转换过程有时会遇到兼容性问题,需要调试。 |
| TensorFlow 格式 (.pb, .ckpt, .h5) | TensorFlow框架的“自家全家桶”,生态完整。 | 从训练到部署一条龙服务,功能全面。 | TensorFlow重度用户:全程使用TensorFlow生态进行模型开发和部署。 | 与 PyTorch 生态不直接兼容,需要转换 |
Tips:
每种格式的选择需根据具体应用场景(如安全性、推理效率、跨框架兼容性等)进行权衡。
Safetensors因其安全性和高效性正逐渐成为主流推荐格式,尤其在Hugging Face生态中。
量化格式(如GGUF和4-bit/8-bit量化)在边缘计算和资源受限环境中应用广泛,但需注意精度损失。
大模型精度类型
| 精度类型 | 描述 | 优点 | 缺点 | 适用场景 | 精度 | 数值范围 |
|---|---|---|---|---|---|---|
| FP32(单精度浮点数) | 32位浮点数,IEEE 754标准,高精度浮点表示 | 高精度,广泛硬件支持,适合复杂任务 | 存储和计算开销大,推理速度慢 | 科学研究、复杂模型训练 | 高,约7位十进制精度,误差极小 | ±1.18×10⁻³⁸ 至 ±3.4×10³⁸ |
| FP16(半精度浮点数) | 16位浮点数,减少一半存储和计算需求 | 推理速度快,内存占用低,精度损失较小 | 可能溢出或下溢,需硬件支持 | GPU加速推理,内存受限环境 | 中,约3-4位十进制精度,适合大多数任务 | ±6.1×10⁻⁵ 至 ±6.55×10⁴ |
| BF16(Brain Float 16) | 16位浮点数,指数范围与FP32相同,精度稍低 | 保持大动态范围,适合深度学习 | 精度低于FP16,需专用硬件(如TPU) | 深度学习推理与训练,硬件优化 | 中低,约2-3位十进制精度 | ±1.18×10⁻³⁸ 至 ±3.4×10³⁸ |
| INT8(8位整数) | 8位定点整数,通过量化映射浮点值 | 推理速度大幅提升,内存占用极低 | 精度损失较大,高维度数据表现差 | 边缘设备、移动设备 | 低,量化误差明显,依赖校准 | -128 至 127(有符号) 0 至 255(无符号) |
| 低比特量化(如2-bit、4-bit) | 极低位定点量化,极度压缩模型 | 极低存储和计算开销,超高推理速度 | 精度严重下降,数学任务表现差 | 超轻量推理,极低资源设备 | 极低,量化误差大,适合简单任务 | 4-bit:-8 至 7(有符号) 2-bit:-2 至 1(有符号) |
| 混合精度量化 | 权重用低精度(如INT8),梯度/偏差用高精度(如FP32) | 平衡精度与效率,适合复杂模型 | 实现复杂,需硬件和框架支持 | 高性能推理与训练,混合硬件环境 | 中高,权重低精度,整体精度较高 | 依赖组合(如INT8:-128 至 127,FP32:±1.18×10⁻³⁸ 至 ±3.4×10³⁸) |
| 动态量化 | 推理时动态调整精度,避免信息损失 | 精度较高,灵活性强 | 推理速度略慢,计算开销增加 | 精度敏感的动态推理场景 | 中高,接近FP16/INT8 | 动态变化(如FP16或INT8范围) |
| 静态量化 | 预先校准并固定低精度权重 | 精度较高,推理效率高 | 需额外校准,计算资源需求高 | 稳定推理,资源充足环境 | 中,优于无校准INT8 | 固定(如INT8:-128 至 127) |
精度与数值范围关系:
- 高精度(如FP32)提供更细腻的数值表示,误差小,适合复杂计算。
- 低精度(如INT8、4-bit)范围受限,量化误差大,适合简单任务或资源受限场景。
- 混合精度通过高精度梯度弥补低精度权重的不足,整体精度较高。
LLM使用
LLM分闭源大模型和开源大模型两大类,前者可以通过在线交互或者API的形式调用,后者则可以直接将模型下载下来,然后进行私有化部署使用。
优缺点:闭源大模型使用便捷、性能稳定且往往具备最新功能,但依赖厂商服务,成本受限于调用费用,数据也可能存在隐私和安全风险;开源大模型可自由下载和私有化部署,具备灵活性和可控性,能更好地满足定制化和数据安全需求,但部署和优化需要较高的算力与技术投入。
闭源大模型使用
下面以千问大模型为例进行讲解。
接入通义千问大模型
注册:https://bailian.console.aliyun.com/#/home
安装openai:
# 如果运行失败,您可以将pip替换成pip3再运行
pip install openai
python代码:
from openai import OpenAI
client = OpenAI(
# 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key
# api_key=os.getenv("DASHSCOPE_API_KEY"),
api_key="sk-e69e1fbxxxxxxxxxxxsb3d17ff",
# 以下是北京地域base_url,如果使用新加坡地域的模型,需要将base_url替换为:https://dashscope-intl.aliyuncs.com/compatible-mode/v1
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
completion = client.chat.completions.create(
# 模型列表:https://help.aliyun.com/zh/model-studio/getting-started/models
model="qwen-plus",
messages=[
{'role': 'system', 'content': 'You are a helpful assistant.'},
{'role': 'user', 'content': '你是谁?'}
]
)
print(completion)
print(completion.choices[0].message.content)
优化:dotenv 是一个用于从 .env 文件中加载环境变量的 Python 库。增加安全性和灵活性。
安装:
pip install python-dotenv
使用:
- 创建.env文件
DASHSCOPE_API_KEY=sk-e69exxxxxxxxx0834b3d17ff
DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
DASHSCOPE_MODEL_NAME=qwen-plus
- 调用代码
import os
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
client = OpenAI(
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url=os.getenv("DASHSCOPE_BASE_URL"),
)
completion = client.chat.completions.create(
# 模型列表:https://help.aliyun.com/zh/model-studio/getting-started/models
model=os.getenv("DASHSCOPE_MODEL_NAME"),
messages=[
{'role': 'system', 'content': 'You are a helpful assistant.'},
{'role': 'user', 'content': '你是谁?'}
]
)
print(completion)
print(completion.choices[0].message.content)
角色与作用
在大语言模型中,user、assistant、system 是三种核心角色,它们的定位和功能不同,共同构成对话的上下文结构。
以下是具体区别和用途:
1)system(系统角色)
- 定位:控制对话的全局方向和模型的行为模式
- 作用: 设定上下文:定义对话的背景、目标或规则(例如“你是一个专业翻译,只回答与翻译相关的问题”)。 调整风格:指定回复的语气(严肃、幽默)、格式(分点、Markdown)或内容限制(避免敏感话题)。 长期控制:在对话中持续影响模型的输出(即使后续对话中没有重复指令)。
- 典型场景: 初始化对话时设定角色(如医生、律师、代码助手)。 限制模型的回答范围(如“仅用英文回答”)。 设定复杂任务的流程(如分步骤完成任务)。
2)user(用户角色)
- 定位:代表真实用户输入,是驱动对话的核心。
- 作用: 提出问题或请求(如“帮我写一首关于夏天的诗”)。 提供补充信息(如“上一句翻译成法语”)。 修正模型行为(如“用更简单的语言解释”)。
- 典型场景: 直接交互:用户提问、追问或反馈。 间接控制:通过用户消息调整模型输出(例如在消息中附加指令)。
3)assistant(助手角色)
- 定位:模型生成的回复内容。
- 作用: 回答问题:基于上下文生成符合用户需求的回复。 自我修正:在后续对话中根据用户反馈调整回答(如“抱歉,之前的回答有误,正确的是…”)。 遵循指令:执行 system 或 user 指定的规则(如分点回答、使用特定格式)。
- 典型场景: 直接生成文本、代码、建议等。 通过历史 assistant 消息实现多轮对话连贯性。
4)三者间关系
- System 设定框架 → User 提供具体输入 → Assistant 生成回复。
- 优先级: 最近的 user 指令 > 初始 system 设定 > 历史 assistant 内容。 某些模型(如Claude)对 system 的权重更高,能更稳定地遵循长期指令。
- 示例:
System: 你是一位营养学家,用简洁的中文回答。
User: 香蕉适合减肥吗?
Assistant: 香蕉富含膳食纤维,适量食用有助于增加饱腹感...
扩展:OpenAI官方角色:
开源大模型使用
推理资源评估与计算
在部署模型之前,可靠评估所需资源(尤其是显存 VRAM)至关重要。这能帮助我们选择合适的模型或硬件,避免因资源不足导致的部署失败。
模型推理时的显存占用主要由以下三部分构成:
-
模型权重 (Model Weights): 加载模型本身参数所需的显存。
-
KV Cache: 存储模型计算过程中的键值对缓存,用于加速生成过程。这是最主要的可变开销。
-
激活值 (Activations): 模型在计算过程中产生的中间张量。
总推理显存占用估算公式:Total VRAM ≈ VRAM_weights + VRAM_kv_cache + VRAM_activations
最小显存简单计算:
M 是 GPU 内存,以 GB 为单位。
P 是模型中的参数(注意此处参数是以B为单位,如果是以个为单位,求得的M以bytes为单位,转成GB需要除以10^9)
4B 表示每个参数使用的 4 个字节(每个参数在内存中的占用)。
Q 是加载模型的位数(例如,16 位或 32 位)。
1.2 代表 20% 的额外开销,用于考虑额外的存储需求,例如缓存、元数据等。
假设我们有一个模型,参数数量P 为 3B(30亿参数),使用 16 位(即 2 字节)来加载模型,想要估算部署这个模型所需的 GPU 内存M。
我们可以将这些值代入公式:
M=P×Q/8×1.2 =3* 16 /8 *1.2 =7.2 GB
模型下载
1)使用Hugging Face下载
Hugging Face 是一个领先的开源人工智能平台,主要以提供海量的预训练机器学习模型(尤其是自然语言处理模型,如 Transformers)和数据集而闻名,支持开发者便捷地分享、发现和使用模型,推动了AI技术的开放与普及。
官网:https://huggingface.co/
2)使用魔搭社区下载【推荐】
魔搭(ModelScope)社区是由阿里云推出的模型开放平台,致力于提供大量经过优化和验证的机器学习与深度学习模型,支持模型的发现、学习、使用和二次开发,旨在降低AI技术的应用门槛,促进模型的共享与创新。官网:https://modelscope.cn/my/overview
在模型库中搜索想找的模型,然后点击下载,即可按照说明进行下载。
示例:
#安装modelscope
pip install modelscope
# 在本地创建D:\models文件夹,然后下载模型到本地
modelscope download --model Qwen/Qwen2.5-3B-Instruct --local_dir D:\models\Qwen2.5-3B-Instruct
3)通过官网或github下载
可以找到模型开源的网址进行下载。优点是可以找到最先的模型,可以了解模型的动态。缺点是一些历史版本模型、不同参数的模型、不同量化的模型可能不全。
例如:https://github.com/QwenLM
模型使用
模型下载完成后,可以在服务器、GPU 集群或本地电脑上运行,利用框架(如 Transformers、vLLM、FastChat 等)加载模型。
我们下载的模型是一个经过SFT后的模型,可以按照官网使用方式进行快速使用:
如:https://huggingface.co/Qwen/Qwen2.5-3B-Instruct
https://modelscope.cn/models/Qwen/Qwen2.5-3B-Instruct
from transformers import AutoModelForCausalLM, AutoTokenizer
# 模型的名称/路径(需要设置成本地存放的路径)
model_name = r"D:\models\Qwen2.5-3B-Instruct"
# model_name = r"D:\models\Qwen2.5-0.5B"
# 加载模型
# torch_dtype="auto":自动选择数据类型(如float16/float32),节省显存
# device_map="auto":自动将模型映射到可用的GPU/CPU上
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype="auto",
device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
prompt = "Give me a short introduction to large language model."
messages = [
{"role": "system", "content": "You are Qwen, created by Alibaba Cloud. You are a helpful assistant."},
{"role": "user", "content": prompt}
]
# 将对话转换成模型可识别的文本格式
# - tokenize=False 表示先不进行分词,只生成纯文本
# - add_generation_prompt=True 表示在末尾加上生成提示,引导模型继续生成
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
# 分词处理
model_inputs = tokenizer([text], return_tensors="pt").to(model.device)
# 调用模型进行文本生成
generated_ids = model.generate(
**model_inputs,
max_new_tokens=512 # 最大生成长度
)
# 去除掉输入部分,只保留模型新生成的内容
generated_ids = [
output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]
# 解码生成的 token 序列,转换成可读文本
# - skip_special_tokens=True 表示跳过特殊符号(如<eos>、<pad>)
response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
print(f'response-->{response}')
运行结果:
【了解】语言模型的评估指标
BLEU
BLEU:BLEU (双语评估替补)分数是评估一种语言翻译成另一种语言的文本质量的指标。用于评估生成文本的“正确性”和“流畅性”。
核心思想:一篇好的翻译,其用词和短语应该与专业译员的翻译(参考译文)有很高的重合度。BLEU 通过计算 生成文本 与 参考文本 之间 N-gram 的精确率 (Precision) 来衡量这种重合度。BLEU 的分数取值范围是 0~1,分数越接近1,说明翻译的质量越高。
计算步骤:
-
计算精确率。
分别计算candidate句和reference句的N-grams模型,然后统计其匹配的个数,计算精确率。
公式:candidate和reference中匹配的 n−gram 的个数 /candidate中n−gram 的个数
-
计算短句惩罚 (Brevity Penalty, BP)。
\[ BP = \begin{cases} 1 & c > r \\ e^{(1-r/c)} & c \leq r \end{cases} \]
其中,c:生成文本长度。r:参考文本长度。
- 计算最终 BLEU 分数:
示例:假设机器翻译的译文candidate和一个参考翻译reference如下
candidate: It is a nice day today
reference: Today is a nice day
5/6
- 使用1-gram进行匹配
candidate: {it, is, a, nice, day, today}
reference: {today, is, a, nice, day}
结果:
其中{today, is, a, nice, day}匹配,所以匹配度(精确率)为5/6
BLEU = BP * 匹配度(精确率) = 1 * 5/6 ≈ 0.833
Tips:
BLEU = BP * exp(1 * log(5/6)) = 1 * exp(log 5/6) = 5/6 = 0.833
- 使用2-gram进行匹配
candidate: {it is, is a, a nice, nice day, day today}
reference: {today is, is a, a nice, nice day}
结果:
其中{is a, a nice, nice day}匹配,所以匹配度(精确率)为3/5
2_gram = BP * 匹配度(准确率) = 0.6
blue_2 = 1 * exp(0.5*logP1 + 0.5*logP2) = 1 * exp^log(sqrt(0.5)) = sqrt(0.5) ≈ 0.7071
| 概念 | 解释 |
|---|---|
| $ \exp(\log x) = x $ | 指数与对数互为逆运算 |
| $ \log a + \log b = \log(ab) $ | 对数加法变乘法 |
| $ \frac{1}{2} \log p_1 + \frac{1}{2} \log p_2 = \log \sqrt{p_1 p_2} $ | 权重平均 → 对数形式 |
| $ \exp(\log \sqrt{p_1 p_2}) = \sqrt{p_1 p_2} $ | 回到原值 |
代入公式:
$$
BLEU-2 = 1 \cdot \sqrt{\frac 5{6} \cdot \frac 3{5}} = \sqrt{0.5} ≈ 0.7071
$$
- 使用3-gram进行匹配
candidate: {it is a, is a nice, a nice day, nice day today}
reference: {today is a, is a nice, a nice day}
结果:
其中{is a nice, a nice day}匹配,所以匹配度(精确率)为2/4
- 使用4-gram进行匹配
candidate: {it is a nice, is a nice day, a nice day today}
reference: {today is a nice, is a nice day}
结果:
其中{is a nice day}匹配,所以匹配度(精确率)为1/3
Python代码实现:
from nltk.translate.bleu_score import sentence_bleu
import warnings
warnings.filterwarnings("ignore")
candidate = ["it","is","a","nice","day","today"]
reference = [["today","is","a","nice","day"]]
print('1_gram-->', sentence_bleu(references=reference, hypothesis=candidate, weights=(1, 0, 0, 0)))
print('2_gram-->', sentence_bleu(references=reference, hypothesis=candidate, weights=(0.5, 0.5, 0, 0)))
print('3_gram-->', sentence_bleu(references=reference, hypothesis=candidate, weights=(0.33, 0.33, 0.33, 0)))
print('4_gram-->', sentence_bleu(references=reference, hypothesis=candidate, weights=(0.25, 0.25, 0.25, 0.25)))
BLUE:侧重准确率,看生成的文本和参考的文本像不像。值越大越好。
ROUGE
ROUGE,全称Recall-Oriented Understudy for Gisting Evaluation。ROUGE指标是在机器翻译、自动摘要、问答生成等领域常见的评估指标。用于衡量信息覆盖度。ROUGE通过将模型生成的摘要或者回答与参考答案(一般是人工生成的)进行比较计算,得到对应的得分。
核心思想:与强调“精确性”的 BLEU 不同,ROUGE 关注的是 召回率 (Recall) 。它衡量的是,一篇 “标准答案”文本(Reference)中的要点 ,有多少被你的 模型生成的文本(Candidate)给覆盖 了。
ROUGE分为四种方法:ROUGE-N,ROUGE-L,ROUGE-W,ROUGE-S。
下面举例说计算过程(这里以ROUGE_N为例)
基本步骤:
- Rouge-N实际上是将模型生成的结果和标准结果按N-gram拆分后,计算召回率
- 假设模型生成的文本candidate和一个参考文本reference如下:
candidate: It is a nice day today
reference: Today is a nice day
使用ROUGE-1进行匹配
candidate: {it, is, a, nice, day, today}
reference: {today, is, a, nice, day}
结果:
其中{today, is, a, nice, day}匹配,所以匹配度(精确率)为5/5=1,这说明生成的内容完全覆盖了参考文本中的所有单词,质量较高。
通过类似的方法,可以计算出其他ROUGE指标(如ROUGE-2、ROUGE-3、ROUGE-4)的评分,从而综合评估系统生成的文本质量。
Python代码实现:
from rouge import Rouge
# 生成文本
generated_text = "it is a nice day today"
# 参考文本列表
reference_text = "today is a nice day"
# 计算ROUGE分数
rouge = Rouge()
# 参数:生成文本,参考文本,直接传入句子的字符串即可
scores = rouge.get_scores(generated_text, reference_text)
print('scores-->', scores)
输出:
scores--> [{'rouge-1': {'r': 1.0, 'p': 0.8333333333333334, 'f': 0.9090909041322315}, 'rouge-2': {'r': 0.75, 'p': 0.6, 'f': 0.6666666617283951}, 'rouge-l': {'r': 0.8, 'p': 0.6666666666666666, 'f': 0.7272727223140496}}]
召回率(R):生成摘要包含多少参考摘要的内容
Recall = 5(预测)/5(真实) = 1
精确率(P):生成摘要中有多少是相关的内容
Precision = 5(真实)/6(预测) ≈ 0.8333
F1分数:召回率和精确率的调和平均
F1 = 2 * 0.8333 * 1 / (1 + 0.8333) ≈ 2 * 0.8333 / 1.8333 ≈ 0.9090
ROUGE:侧重召回率,看生成的文本有没有覆盖参考文本的关键信息。值越大越好。
PPL
困惑度(Perplexity, PPL)用来度量一个概率分布或概率模型预测样本的好坏程度。
PPL基本思想:一个好的语言模型,在预测下一个词时,应该给予正确答案更高的概率。困惑度衡量的就是模型在预测下一个词时的不确定性或“困惑”程度,本质上是对概率分布质量的评估。困惑度越低,说明模型对句子的预测越有信心,模型性能越好。
- 基本公式:
或者
$$
PPL = \exp \left( -\frac1{N} \sum_{i=1}^{N} \log p \left( w_i | context \right) \right)
$$
N:测试集的总token数。
由公式可知,句子概率越大,语言模型越好,迷惑度越小。
Python代码实现:
##############################单句话###############################
sentences = [['the', 'cat', 'sat']]
unigram = {'the': 0.5, 'cat': 0.4, 'sat': 0.1}
joint_prob = 1.0 # 初始化联合概率为 1.0
N = 0
# 遍历语料库中的每一个句子
for sentence in sentences:
N += len(sentence)
# 步骤 1: 计算整个句子的联合概率 P(W)
# 对应公式: P(W) = P(w_1) * P(w_2) * ... * P(w_N)
for word in sentence:
prob = unigram[word]
joint_prob *= prob # 将每个词的概率连乘起来
# 步骤 2: 计算该句子的困惑度 PPL(W)
# 对应公式: PPL(W) = P(W)^(-1/N)
# 使用 a ** b 表示 a的b次方, 这行代码是公式的直接翻译
ppl = joint_prob ** (-1.0 / N)
# 计算最终在整个测试集上的平均困惑度
print("ppl困惑度-->", ppl)
##############################多句话###############################
# 定义语料库
sentences = [
['I', 'have', 'a', 'pen'],
['He', 'has', 'a', 'book'],
['She', 'has', 'a', 'cat']
]
# 定义语言模型
unigram = {
'I': 1/12,
'have': 1/12,
'a': 3/12,
'pen': 1/12,
'He': 1/12,
'has': 2/12,
'book': 1/12,
'She': 1/12,
'cat': 1/12
}
# 计算困惑度
sentence_prob = 1
length = 0
# 当有多个样本的时候,我们可以将其视为一个大的序列,通过遍历和连乘的方式,计算整个序列的概率
for sentence in sentences:
length += len(sentence)
for word in sentence:
sentence_prob *= unigram[word]
# 根据公式计算困惑度
perplexity = pow(sentence_prob, -1/length)
print(f'困惑度-->{perplexity}') # 8.12323
PPL:模型对下一个词预测的"不确定性",PPL越低,模型预测越准确。
【扩展】大模型
环境
#1.创建环境
conda create -n llmbase python=3.10
#2.安装依赖
pip install -r requirements.txt的路径
Benchmark
Benchmark,基准。在 LLM 和 NLP 领域,通常指一个标准化数据集或任务集合,用于评估模型性能。包括:
数据集:提供输入数据、标签或参考答案。这是基准的基础,用于训练/测试模型。
任务定义:指明确描述要解决的问题类型(如分类、生成、推理),包括输入/输出格式和规则。这确保所有模型在相同任务上比较。
评估指标(Evaluation Metrics):量化模型性能的标准,如 Accuracy(准确率)、F1-score(精确率与召回率的调和平均)或 BLEU(用于生成任务)等提供客观分数。
业界主流大模型评测基准可以访问如下链接:https://www.datalearner.com/benchmarks
评估工具:lm-evaluation-harness 是 EleutherAI 开源的一个 大语言模型评测框架,主要用于统一、系统地评估不同语言模型在各类基准数据集上的表现。
安装:
pip install lm-eval
使用:
lm_eval --model hf --model_args pretrained=D:models\Qwen2.5-3B-Instruct --tasks hellaswag --batch_size auto --limit 1000
# --model hf : 指定模型类型为 Hugging Face 模型
# --model_args pretrained=... : 指定要评估的模型的 Hugging Face Hub ID或者本地模型路径
# --tasks hellaswag : 指定要运行的 Benchmark 任务
# --batch_size auto : 指定推理的 batch size。auto 表示自动选择一个合适的 batch size,避免显存不足。
# --limit 1000 : 限制评估数据的数量。只用前 1000 个样本来跑评测,而不是整个任务的全部数据。
运行结果:
结果解释:
显存推算
组成部分
模型推理时的显存占用主要由以下三部分构成:
- 模型权重 (Model Weights): 加载模型本身参数所需的显存。
- KV Cache: 存储模型计算过程中的键值对缓存,用于加速生成过程。这是最主要的可变开销。
- 激活值 (Activations): 模型在计算过程中产生的中间张量。
总推理显存占用估算公式:Total VRAM ≈ VRAM_weights + VRAM_kv_cache + VRAM_activations
计算模型权重显存 (VRAM_weights)
权重的显存占用取决于模型的参数量和参数精度。
公式: VRAM_weights = 参数量 × 单个参数大小。
比如Qwen2.5-3B-Instruct
参数量: 3 Billion (30亿)
精度: 通常使用半精度 (FP16),每个参数占 2 Bytes
计算: 3 * 10^9 * 2 Bytes = 6 * 10^9 Bytes = 6 GB
所以,模型权重本身大约需要 6 GB 显存。
计算 KV Cache 显存 (VRAM_kv_cache)
LLM 生成文本是一个“自回归”的过程,即生成第 N 个词,需要依赖前面 N-1 个词的信息。这个依赖关系是通过 Transformer 架构中的“自注意力(Self-Attention)”机制实现的。
假设你正在用模型逐字生成一句话:生成第一个 token 时,模型要计算它的 Q、K、V。生成第二个 token 时,模型不仅要计算新的 Q、K、V,还得重新计算第一步里所有 token 的注意力。生成第三个 token 时,又要重新算前两个 token 的注意力……这样,每生成一个新 token,模型都在重复计算历史部分,效率会非常慢。
KV Cache 就是用来避免重复计算的。 当第一个 token 生成时,模型会把它的 K 和 V 存到缓存里。当生成第二个 token 时,只需要算第二个 token 的 K、V,然后直接和缓存里的历史 K、V 拼起来,就能做注意力计算。这样,旧的部分(前面生成的 token)就不用反复算了。
总结来说,KV Cache 是在大模型推理阶段,缓存历史 token 的 Key 和 Value 向量,从而避免重复计算,显著提升生成速度的优化机制。它本质上是一种用空间(显存)换时间(计算速度)的策略。
KV Cache 是推理过程中最消耗显存的部分,尤其是在处理长序列或高并发请求时。
公式: VRAM_kv_cache = (batch_size × seq_len × num_layers × 2 × hidden_size) × Bytes_per_element
batch_size: 并发请求数seq_len: 每个请求的序列长度(prompt + generated tokens)。num_layers: 模型的层数,即Decoder层的数量。2: 分别代表 Key 和 Value。hidden_size: 隐藏层维度。Bytes_per_element: 通常是 2 (FP16)。
比如Qwen2.5-3B-Instruct
假设 batch_size = 8 (8个并发请求)
假设平均 seq_len = 2048
Qwen2-3B 模型通常有 num_layers ≈ 24, hidden_size ≈ 2048 (这些是典型值,具体需查模型配置)
计算: 8 * 2048 * 24 * 2 * 2048 * 2 Bytes ≈ 3.2 GB
所以,对于 8个并发、序列长度为2048的请求,KV Cache 大约需要 3.2 GB 显存。
估算激活值显存 (VRAM_activations)
激活值指的是输入经过某一层网络(比如线性层、注意力层、前馈层)计算后的 中间结果。这部分显存与模型架构和序列长度有关,计算复杂。但在现代推理框架(如 vLLM)中,由于其高效的内存管理,这部分开销相对固定且可控,通常可以估算为 1-2 GB。
最终资源推演
综合以上计算,我们来推演部署 Qwen2.5-3B-Instruct 在不同并发场景下的资源需求。
场景1:单个用户,中等长度对话
batch_size = 1, seq_len = 2048
VRAM_weights = 6 GB
VRAM_kv_cache = 1 * 2048 * 24 * 2 * 2048 * 2 Bytes ≈ 0.4 GB
VRAM_activations ≈ 1 GB
总计: 6 + 0.4 + 1 = 7.4 GB
结论: 一张 8GB 的消费级显卡(如 RTX 3070/4060)即可满足。
场景2:中等并发,长文本处理
batch_size = 16, seq_len = 4096
VRAM_weights = 6 GB
VRAM_kv_cache = 16 * 4096 * 24 * 2 * 2048 * 2 Bytes ≈ 12.8 GB
VRAM_activations ≈ 2 GB
总计: 6 + 12.8 + 2 = 20.8 GB
结论: 需要一张 24GB 的专业级显卡(如 RTX 3090/4090)
访问OpenAI
OpenAI官网:https://openai.com/
示例代码:
from dotenv import load_dotenv
from openai import OpenAI
import os
# 加载配置文件
load_dotenv()
# 创建客户端
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
# 访问ChatGPT
response = client.responses.create(
model="gpt-5-nano",
input="简单介绍一下你自己"
)
# 获取结果
print(response.output_text)
Tips:
该代码需要准备OpenAI的
API_KEY才能访问。
运行结果如下:
【作业】
#1.总结xmind文件
#2.OpenAI调用百炼平台大模型代码(闭源)
#3.复习/预习/加强内容
day01 代码(Qwen 加载 / ROUGE / PPL)
5 个文件📄 demo01_qwen.py 1.1KB
import os from openai import OpenAI from dotenv import load_dotenv load_dotenv() # 注意: 不同地域的base_url不通用(下方示例使用北京地域的 base_url) # - 华北2(北京): https://dashscope.aliyuncs.com/compatible-mode/v1 # - 美国(弗吉尼亚): https://dashscope-us.aliyuncs.com/compatible-mode/v1 # - 新加坡: https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1,请将WorkspaceId替换为业务空间ID # - 德国(法兰克福): https://{WorkspaceId}.eu-central-1.maas.aliyuncs.com/compatible-mode/v1,请将WorkspaceId替换为业务空间ID client = OpenAI( api_key=os.getenv("API_KEY"), base_url=os.getenv("BASE_URL"), ) completion = client.chat.completions.create( model=os.getenv("MODEL_NAME"), messages=[{'role': 'user', 'content': '你是谁?'}] ) print(completion.choices[0].message.content) """ 角色: (1)User:用户,也就是我们。 (2)assistant:助手,也就是大模型回复的内容。 (3)system:系统,也就是方向/定位。 """
📄 demo02_load_qwen.py 1019B
from modelscope import AutoModelForCausalLM, AutoTokenizer model_name = "D:\models\Qwen2.5-3B-Instruct" model = AutoModelForCausalLM.from_pretrained( model_name, torch_dtype="auto", device_map="auto" ) tokenizer = AutoTokenizer.from_pretrained(model_name) prompt = "Give me a short introduction to large language model." messages = [ {"role": "system", "content": "You are Qwen, created by Alibaba Cloud. You are a helpful assistant."}, {"role": "user", "content": prompt} ] text = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True ) model_inputs = tokenizer([text], return_tensors="pt").to(model.device) generated_ids = model.generate( **model_inputs, max_new_tokens=512 ) generated_ids = [ output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids) ] response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0] print("response-->", response)
📄 demo03_blue.py 615B
from nltk.translate.bleu_score import sentence_bleu candidate = ["it","is","a","nice","day","today"] reference = [["today","is","a","nice","day"]] blue1 = sentence_bleu(references=reference, hypothesis=candidate, weights=(1,)) print("blue1-->", blue1) blue2 = sentence_bleu(references=reference, hypothesis=candidate, weights=(0.5, 0.5)) print("blue2-->", blue2) blue3 = sentence_bleu(references=reference, hypothesis=candidate, weights=(1/3, 1/3, 1/3)) print("blue3-->", blue3) blue4 = sentence_bleu(references=reference, hypothesis=candidate, weights=(1/4, 1/4, 1/4, 1/4)) print("blue4-->", blue4)
📄 demo04_rouge.py 316B
from rouge import Rouge # 生成文本 generated_text = "it is a nice day today" # 参考文本列表 reference_text = "today is a nice day" rouge = Rouge() # hyps:生成的答案 # refs:参考的答案 result = rouge.get_scores(hyps=generated_text, refs=reference_text) print("result-->", result)
📄 demo05_ppl.py 722B
# sentences = [['the', 'cat', 'sat']] # unigram = {'the': 0.5, 'cat': 0.4, 'sat': 0.1} sentences = [ ['I', 'have', 'a', 'pen'], ['He', 'has', 'a', 'book'], ['She', 'has', 'a', 'cat'] ] # 定义语言模型 unigram = { 'I': 1/12, 'have': 1/12, 'a': 3/12, 'pen': 1/12, 'He': 1/12, 'has': 2/12, 'book': 1/12, 'She': 1/12, 'cat': 1/12 } # 初始化联合概率和N数量 uni_prob = 1.0 N = 0 # 外层遍历 for sentence in sentences: N += len(sentence) # 内层遍历 for word in sentence: # 得到每个单词的概率 prob = unigram[word] # 计算联合概率 uni_prob *= prob # 计算ppl ppl = uni_prob ** (- 1.0/N) print("ppl-->", ppl)
第2节 · RAG 开发(一):项目启动
EduRAG 笔记(day02)
EduRAG项目
【了解】今日内容介绍
- 项目介绍
- 环境安装
- ollama介绍
- Langchain介绍
【掌握】RAG介绍
RAG概念
通⽤的基础大模型存在一些问题:
- 幻觉问题,LLM有时会在回答中⽣成看似合理但实际上是错误的信息
- LLM的知识不是实时的,模型训练好后不具备自动更新知识的能力,会导致部分信息滞后
- LLM领域知识是缺乏的,大模型的知识来源于训练数据,这些数据主要来自公开的互联网和开源数据集,无法覆盖特定领域或高度专业化的内部知识
为解决或缓解上述问题,2020年Facebook发表了一篇论文——《Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks》,提出了RAG(Retrieval Augmented Generation,检索增强生成)技术。
RAG是一种将大规模语言模型(LLM)与外部知识源的检索相结合,以改进问答能力的工程框架。 它使用来自私有或专有数据源的信息来辅助文本生成,从而弥补LLM的局限性,特别是在解决幻觉问题和提升时效性方面。
RAG的作用
克服LLM“幻觉”问题:LLM在生成文本时有时会“一本正经地胡说八道”,即生成听起来合理但实际上不准确或捏造的信息,这被称为“幻觉”。RAG通过提供外部事实依据,显著减少了这种幻觉现象,让LLM的输出更具事实性和可靠性。
获取最新信息:LLM的训练数据通常是静态的,这意味着它们无法获取到训练截止日期之后发生的事件或更新的信息。RAG允许LLM连接到实时或定期更新的外部数据源(如新闻、数据库、内部文档等),从而提供最新、最及时的答案。
领域特定知识增强:对于特定行业或企业内部的知识,LLM的通用训练数据往往不足。RAG能够将LLM与企业内部的知识库文档或特定领域的数据连接起来,使LLM能够回答高度专业化的问题,并提供更符合上下文的答案。
降低模型微调成本:传统上,为了让LLM适应特定任务或数据,需要进行昂贵的微调(Fine-tuning)。RAG提供了一种更经济高效的替代方案,它无需修改LLM的底层参数,只需更新外部知识库即可,大大降低了维护和更新模型的成本。
提高答案的可解释性和溯源性:RAG可以引用其获取信息的来源,这意味着用户可以查看LLM答案所依据的原始文档或数据,增强了答案的透明度和用户信任度。
RAG通过将检索和生成相结合,既保留了传统检索问答的可靠性,又获得了LLM的灵活性和自然表达能力。它能让AI始终基于最新的、可信的知识来回答问题,同时保持对话的流畅自然。
RAG原理
R:Retrieve:检索。基于向量数据库检索相关的上下文信息。
A:Augment:增强。把问题和相关的上下文信息一起送给大模型。
G:Generate:生成。大模型基于增强后的上下文,生成回复内容。
【掌握】项目介绍
项目背景
为了解决人工客服效率低下的问题,公司决定采用LangChain和Qwen大模型,构建了一套智能化的学科在线答疑系统。
该系统基于RAG架构,能够通过实时检索相关知识库中的信息来增强大模型的生成能力,确保回答的准确性和时效性。
与此同时,系统通过自动化处理学生的答疑需求,极大地减轻了人工客服的工作压力,从而实现了高效、低成本的知识服务。
项目架构
- 第一步:将现有后台搜集的FQA数据集存储到MySQL数据库中
- 第二步:基于query实现MySQL数据库检索:将query和现有问题匹配(做相似度计算),如果阈值>=0.85,就认为问题比较明确,直接返回对应的答案;否则,进入RAG检索系统
- 第三步:搭建本地知识库:对本地文档加载读取;进行文档分割;文档向量化;存储向量数据库(Milvus)
- 第四步:基于query实现Milvus数据库检索:将query进行向量表示,并从Milvus数据库中检索出相似的top-k个文本段。
- 第五步:将query和检索出的top-k文本段拼接,送入大模型,实现预测。
【实践】环境安装
安装说明
Python虚拟环境
首先创建Python虚拟环境。从Anaconda Prompt命令行进入。
Python版本最好是:3.10.18
# 查看当前的虚拟环境
conda env list
# 创建虚拟环境, 安装3.10的Python解析器
conda create -n EduRAG python=3.10.18
# 切换虚拟环境
conda activate EduRAG
# 安装依赖包, 在requirements.txt对应路径下执行以下命令
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
安装Docker Desktop
前言
安装Docker的目的,主要是用于安装Milvus、minio软件的。因为Milvus官网,推荐使用Docker来安装Milvus。
而Docker对于Linux的支持很友好,但是对于Windows的支持不尽如意。
Docker官方说明:
安装WSL(Windows Subsystem for Linux)
启用WSL打开具有管理员权限的PowerShell,运行以下命令以安装WSL
安装完成后,重启电脑以完成配置
wsl --install
输入命令,进入等待中,直至安装完成。
注意:
下载过程可能耗时很长,期间别操作,等着就行。
安装完成后,重启时,可能会弹出如下对话框,代表WSL安装成功,关闭即可。
安装docker
下载并安装Docker Desktop
访问Docker官方网站,下载适用于Windows/Mac的Docker Desktop安装包。(已经提前下载了,大家直接用即可)
双击Docker Desktop安装文件(资料中有),按照提示一路下一步完成安装。
安装完成后,点击**==close and restart==**重启电脑以完成配置。
重启后,如果弹出如下对话框,点击**==Accept==**,如下图:
打开cmd窗口,运行以下命令,验证是否成功安装。
docker --version
docker compose version
配置Docker Compose文件
在本地磁盘(例如:D盘)创建文件夹milvus_data,用于存放Milvus和Redis等软件的配置文件和数据,且在milvus_data文件夹中,添加docker-compose.yml文件。该文件在预习资料中,如下图:
启动Milvus和Redis
拉取镜像并启动容器
进入上述自己创建的milvus_data目录下(docker-compose.yml文件目录),打开cmd终端,运行:
# -d 表示后台运行容器
docker compose up -d
注意:第一次拉取镜像时报错: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
上述报错主要是网络问题。
咱们当前是window环境,所以直接复制以下内容去docker软件中粘贴即可。
"registry-mirrors": [
"https://docker.211678.top",
"https://docker.1panel.live",
"https://hub.rat.dev",
"https://docker.m.daocloud.io",
"https://do.nark.eu.org",
"https://dockerpull.com",
"https://dockerproxy.cn",
"https://docker.awsl9527.cn"
]
打开 docker desktop 软件,第一次可能会看到如下页面,不需要注册,直接点击登录,如下图:
进来后,点击右上角的设置按钮,如下图:
找到左侧的Docker Engine入口,在下面的空白框内,添加上述的镜像地址,如下图:
配置好后,点击Apply & restart,重启Docker。
然后再次去 milvus_data 目录下,打开 cmd终端,运行:
启动成功后去docker看看,如下图:
验证Milvus和Redis
测试Milvus连接
from pymilvus import MilvusClient
# 连接milvus
client = MilvusClient("http://localhost:19530")
# 显示当前milvus的数据库
print('databases-->', client.list_databases())
测试Redis连接
import redis
# 连接到 Redis
client = redis.Redis(host="localhost", port=6379, password=1234, decode_responses=True)
# 测试读写
client.set("test_key", "Hello, Redis!")
value = client.get("test_key")
print(f"Redis value: {value}")
安装Milvus可视化工具Attu
双击Attn安装包进行安装,如出现不允许运行,点开详细进行安装即可。
不需要设置,直接安装结束。之后打开Attu,设置主机IP,如下图:
点击连接,就可以连接成功。
启动说明
启动Docker Desktop
在开始菜单中,找到Docker Desktop应用,单击该应用,即可启动Docker Desktop,如下图。
(启动过程需要等待片刻……)
温馨提示:
可以不在开始菜单,如果桌面上有Docker Desktop图标也可以。
启动Docker
把docker-compose配置文件(在当前目录下),放到一个没有中文,没有空格,没有特殊字符的目录下。
打开cmd终端,切换到docker-compose.yml文件目录下,运行如下命令:
docker compose up -d
如上图所示,我的docker-compose.yml文件是放在D:\code\workspace8\milvus_redis>目录下。
校验服务是否启动
在上述目录下,输入如下命令:
docker ps
如下图所示:
可以看到,STATUS列中并没有错误或者重启信息,目前已经运行一会儿了,说明服务都正常运行。
停止服务
当需要关停服务时,仍在docker-compose.yml文件路径下,输入如下命令:
docker compose down
结果如下图所示:
到此,EduRAG项目所需的服务就启动完成,后续只需要稍微配置即可使用。
【了解】工具介绍
Ollama:一个开源的大型语言模型服务工具,它帮助用户快速在本地运行大模型。
LangChain:为各种LLMs实现通用的接口,把LLMs相关的组件“链接”在一起,简化LLMs应用的开发难度,方便开发者快速地开发复杂的LLMs应用。
Redis:高性能缓存数据库,用于缓存高频问答对数据,实现高效检索。
MySQL:关系型数据库,用于存储高频问答对数据。
Milvus:一个开源向量数据库,帮助我们实现向量数据的存储和检索。
【了解】Ollama
简介
一个开源的大型语言模型服务工具,它帮助用户快速在本地运行大模型的能力。
安装
下载
https://ollama.com/ 官网下载,双击安装即可。
运行Ollama
参数量:
- 1.5B:简单的分类任务够用
- 7B:可以胜任绝大部分的简单任务,比如句子级别、段落级别的文本分类
- 32B:复杂任务需要32B
拓展:模型参数
-
bert: 440MB -> 1.1亿参数(104.904174805 * 4) -> 0.1B
-
B: 十亿
-
一个参数一般都是由一个32位浮点数(FP32 /float32/double, 4个字节)表示
-
完整的模型:7B(32位浮点数,4个字节) = 7 * 10 ^ 9 * 4个字节 = 1GB * 28
-
量化过的模型:7B(4位整数) = 7 * 0.5 *10^9 *1/1024/1024/1024= 3.3 GB
-
模型量化
-
FP32(4个字节) -> int4(0.5 个字节)
-
拓展:模型蒸馏
-
教师模型:在服务器上训练一个相对大的模型( 0.4B模型, 440MB, bert)
- 学生模型:用一个能在目标设备上运行的模型(0.01B模型, 10MB, LSTM ),学习教师模型
在Python中使用 Ollama API
安装
pip install ollama
三种方式:
- 根据API不同:
- chat api
- http api
- 根据输出形式不同:
- 全量(默认):生成内容全部就绪时,一次性返回
- 流式:生成一个token,就返回一个,直到结束
代码:
import ollama
# 聊天式
response = ollama.chat(model="deepseek-r1:1.5b", messages=[{"role": "user", "content": "为什么天空是蓝色的?"}])
# print('response1-->', response)
# print('response.message-->', response["message"])
## 远程调用的方式
from ollama import Client
client = Client(host="http://127.0.0.1:11434") # 虚拟机的需要改ip
response = client.chat(model="deepseek-r1:1.5b", messages=[{"role": "user", "content": "你是什么模型?"}])
# print('response2-->', response)
# print('response.message.content-->', response["message"]['content'])
## 流式调用
stream = client.chat(model="deepseek-r1:1.5b", messages=[{"role": "user", "content": "你叫什么名字?"}], stream=True)
for chunk in stream:
print(chunk["message"]['content'], end="")
LangChain调用
# 0.x版本导包
# from langchain_community.llms import Ollama
# 1.x版本导包
from langchain_ollama import OllamaLLM
llm = OllamaLLM(base_url="http://127.0.0.1:11434", model="deepseek-r1:1.5b")
result = llm.invoke("你叫什么名字?")
print("result-->", result)
requests调用
import requests
import json
url = "http://127.0.0.1:11434/api/chat"
headers = {"Content-Type": "application/json"}
data = {"model": "deepseek-r1:1.5b", "messages": [{"role": "user", "content": "你叫什么名字?"}], "stream": False}
response = requests.post(url, headers=headers, json=data)
# print('response1-->', response.json())
url = "http://127.0.0.1:11434/api/chat"
headers = {"Content-Type": "application/json"}
data = {"model": "deepseek-r1:1.5b", "messages": [{"role": "user", "content": "你叫什么名字?"}], "stream": True}
response = requests.post(url, headers=headers, json=data)
for chunk in response.iter_lines():
print(json.loads(chunk.decode("utf-8"))['message']['content'], end='')
OpenAI调用
from openai import OpenAI
client = OpenAI(base_url="http://localhost:11434/v1", api_key="xx") # vllm部署可以指定api_key
stream = client.chat.completions.create(
model="deepseek-r1:1.5b",
messages=[{"role": "user", "content": "写一首关于秋天的五言诗"}],
stream=True,
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
第3节 · 提示词工程进阶
大模型基础(提示词工程篇)
大模型基础
今日内容介绍
-
LLM主要架构类别
-
提示词工程原则
- 提示技术介绍
【了解】LLM主要结构类别
大语言模型架构基石
LLM本身基于Transformer架构。自2017年,《Attention Is All You Need》发表起,原始的transformer模型为不同领域的模型提供了灵感和启发。
Transformer 的核心创新是“自注意力 + 并行化”,让模型能高效捕捉全局依赖关系,并具备大规模扩展能力,因此成为大语言模型的基石。基于原始的Transformer框架,衍生出了一系列模型,一些模型仅仅使用encoder或decoder,有些模型同时使用encoder+decoder。
自编码模型
AutoEncoder Model,AE模型,代表作BERT,其特点为:Encoder-Only, 基本原理:是在输入中随机MASK掉一部分单词,根据上下文预测这个词。AE模型通常用于内容理解任务,比如自然语言理解(NLU)中的分类任务:情感分析、提取式问答。
代表模型 BERT
BERT是2018年10月由Google AI研究院提出的一种预训练模型。BERT的全称是Bidirectional Encoder Representation from Transformers。
BERT的架构
Embedding模块
双向Transformer模块
MLM(Masked Language Model):随机遮盖输入的部分词,让模型预测这些词。
NSP(Next Sentence Prediction):判断句子 B 是否是句子 A 的下一句。
预微调模块
BERT模型的特点
模型的一些关键参数为:
| 参数 | 取值 |
|---|---|
| transformer 层数 | 12 |
| 特征维度 | 768 |
| transformer head 数 | 12 |
| 总参数量 | 1.02 亿 |
AE模型总结
优点:BERT使用双向transformer,在语言理解相关的任务中表现很好。
缺点:
- 输入噪声:BERT在预训练过程中使用【mask】符号对输入进行处理,这些符号在下游的finetunning任务中永远不会出现,这会导致预训练-微调差异。而AR模型不会依赖于任何被mask的输入,因此不会遇到这类问题。
- 更适合用于语言嵌入表达, 语言理解方面的任务, 不适合用于生成式的任务
自回归模型
AutoRegressive Model,AR模型,代表作GPT,其特点为:Decoder-Only,基本原理:从左往右学习的模型,只能利用上文或者下文的信息,比如:AR模型从一系列time steps中学习,并将上一步的结果作为回归模型的输入,以预测下一个time step的值。AR模型通常用于生成式任务,在长文本的生成能力很强,比如自然语言生成(NLG)领域的任务:摘要、翻译或抽象问答。
代表模型 GPT
2018年6月, OpenAI公司发表了论文“Improving Language Understanding by Generative Pre-training”《用生成式预训练提高模型的语言理解力》, 推出了具有1.17亿个参数的GPT(Generative Pre-training , 生成式预训练)模型.
GPT模型架构
GPT模型的特点
模型的一些关键参数为:
| 参数 | 取值 |
|---|---|
| transformer 层数 | 12 |
| 特征维度 | 768 |
| transformer head 数 | 12 |
| 总参数量 | 1.17 亿 |
优点
- 在有监督学习的12个任务中, GPT在9个任务上的表现超过了state-of-the-art的模型。
- 利用Transformer做特征抽取, 能够捕捉到更长的记忆信息, 且较传统的 RNN 更易于并行化。
缺点
- GPT 最大的问题就是传统的语言模型是单向的。
- 针对不同的任务,需要不同的数据集进行模型微调,相对比较麻烦。
AR模型总结
优点:AR模型擅长生成式NLP任务。AR模型使用注意力机制,预测下一个token,因此自然适用于文本生成。此外,AR模型可以简单地将训练目标设置为预测语料库中的下一个token,因此生成数据相对容易。
缺点:AR模型只能用于前向或者后向建模,不能同时使用双向的上下文信息,不能完全捕捉token的内在联系。
序列到序列
Sequence to Sequence Model,encoder-decoder模型同时使用编码器和解码器。它将每个task视作序列到序列的转换/生成(比如,文本到文本,文本到图像或者图像到文本的多模态任务)。比如对于文本分类任务来说,编码器将文本作为输入,解码器生成文本标签。Encoder-decoder模型通常用于需要内容理解和生成的任务,比如机器翻译。
代表模型T5
T5 由谷歌的 Raffel 等人于 2020年7月提出,相关论文为“Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer”. 该模型的目的为构建任务统一框架:将所有NLP任务都视为文本转换任务。
T5模型架构
T5模型的特点
模型的一些关键参数为:
| 参数 | 取值 |
|---|---|
| transformer 层数 | 24 |
| 特征维度 | 768 |
| transformer head 数 | 12 |
| 总参数量 | 2.2 亿 |
encoder-decoder模型总结
优点:T5模型可以处理多种NLP任务,并且可以通过微调来适应不同的应用场景,具有良好的可扩展性;相比其他语言生成模型(如GPT-2、GPT-3等),T5模型的参数数量相对较少,训练速度更快,且可以在相对较小的数据集上进行训练。
缺点:由于T5模型使用了大量的Transformer结构,在训练时需要大量的计算资源和时间; 模型的可解释性不足。
同时使用了encoder-decoder,所以它的训练成本和推理成本相对于AE和AR模型就是双倍的。
| 架构 | 代表模型 | 主要特点 |
|---|---|---|
| Encoder-Only | BERT、RoBERTa | 双向注意力,擅长理解 |
| Encoder–Decoder | T5、BART | 编码输入 → 解码输出 |
| Decoder-Only | GPT、LLaMA、Qwen | 单向自回归生成 |
主流模型架构-Decoder-Only
LLM之所以主要都用Decoder-only架构,除了训练效率和工程实现上的优势外,在理论上是因为Encoder的双向注意力会存在低秩问题,这可能会削弱模型表达能力,就生成任务而言,引入双向注意力并无实质好处。而Encoder-Decoder架构之所以能够在某些场景下表现更好,大概只是因为它多了一倍参数。所以,在同等参数量、同等推理成本下,Decoder-only架构就是最优选择了。
结论:Decoder-Only 在“通用大模型”这个目标上,是目前性价比最高、最容易规模化的架构。
拓展:
Encoder的双向注意力会存在低秩问题,这里的低秩问题怎么理解?
由于双向注意力机制,在信息充分混合后,容易使不同 token 的表示趋于相似,区分度下降,导致表示矩阵的有效秩降低,从而削弱模型的表达能力和泛化性能。
【掌握】提示工程的原则
概念
提示工程(Prompt Engineering),也称为 In-Context Prompting,是指在不更新模型权重的情况下如何与 大模型交互以引导其行为以获得所需结果的方法。
在人工智能领域,Prompt指的是用户给大型语言模型发出的指令。
- 基于OpenAI官网文档,我们提炼出了5条大原则:
- 清晰的指令
- 文本参考
- 复杂任务拆分简单子任务
- 给模型"思考"的时间
- 借助外部工具
- 接下来,我们将对每一种具体的原则进行原理讲解以及举例实时,以帮助我们在日常工作准确的使用LLM。
提示工程原则
清晰的指令
详细的描述、让模型充当某个角色、使用分隔符标明输入的不同部分、提供例子、指定输出长度
文本参考
- 文本参考目的:基于文本文档,辅助大模型问答,降低模型"幻觉"(一本正经的胡说八道)问题。
复杂任务拆分为简单子任务
- 把复杂的任务给拆为更为简单的子任务,大模型会有更好的表现。
给模型"思考"的时间
-
给模型思考时间,本质为链式思考(Chain-of-Thought, CoT)。目的,让模型think step by step(一步步思考).
- 比如,直接问你一道数学应用题,你肯定不能立即回答出问题,但是如果给你时间让你一步步分析并计算,那么你就很有可能把题解出来.
-
Chain of Thought (思维链) 就是让大模型在输出答案时,不是直接给出结果,而是显式写出推理过程.
- 普通回答:直接给结论
- CoT回答:先一步一步推理,再得出结论
借助外部工具
- 大模型并不是万能的,比如一些实时问题等等大模型不能很好的回答,所以需要一些外部工具来帮助处理。接下来我们将学习如何使用外部工具来增强ChatGPT的功能。
- 在实际应用中,常常会通过 工具调用(Function Calling) 或 插件化扩展 来增强 ChatGPT 的能力。
【理解】提示词技术
概览
提示词技术非常多,如下列举的一些是常见且通用的提示技术。还有很多提示技术,在各大公司官网上:
https://api-docs.deepseek.com/zh-cn/prompt-library
https://docs.bigmodel.cn/cn/guide/platform/prompt
https://bailian.console.aliyun.com/cn-beijing/#/home
https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/
https://github.com/dair-ai/Prompt-Engineering-Guide
https://www.promptingguide.ai/zh
上述提示词技术,仅供编写提示词时做参考。
Zero-Shot
Zero-Shot 提示不提供示例,直接靠模型预训练知识完成任务。像问专家问题,他凭经验直接回答。
特点:
高效:无需准备示例,适合快速测试。
灵活:利用模型广博知识。
案例:
你是一个文本分类专家。将文本分类为Positive或Negative。
文本:我感觉《哪吒2》这部电影还可以。
Few-Shot
虽然大型语言模型展示了惊人的Zero-Shot 能力,但它们在更复杂的任务上仍然表现不佳。Few-Shot提示通过 1-3 个参考示例引导模型理解模式,使模型实现了更好的性能。
特点:
精准:示例减少歧义,提升一致性。
案例:
#1.
你是一个文本分类专家。将文本分类为Positive和Negative。
示例1:
输入:我爱北京天安门。
输出:Positive。
示例2:
输入:我不喜欢北京的天气,太冷了。
输出:Negative。
请对如下文本进行分类:
输入:我感觉《哪吒2》这部电影还可以。
==结论:如果没有提供示例,就叫Zero-Shot,如果给了一个示例,就叫One-Shot,给了多个示例就叫Few-Shot.==
Chain-of-Thought (CoT)
Chain-of-Thought 是一种提示技术,通过展示中间推理步骤来解决复杂问题。这种方法可以帮助模型更好得推理和生成答案。可以将其与少样本提示相结合,以获得更好的结果。
类型:
- Zero-shot-CoT (零样本思维链) 是指 不给任何示例,只在提示中加一句类似:“Let’s think step by step.”(让我们一步步思考)。模型在看到这句话时,就会自己展开推理链,而不是直接给结果。
- Few-shot-CoT (少样本思维链) 是指 在提示中给出几个带推理过程的示例。
特点:
准确:分解复杂问题,避免错误。
透明:推理过程可审查。
案例:
#1.没有思维链
你是一位没有文化的老奶奶。
我去市场买了10个苹果。我给了邻居2个苹果和修理工2个苹果。然后我去买了5个苹果并吃了1个。我还剩下多少苹果?
不要思考,不要算术,直接凭感觉说结果。
#2.有思维链
你是一位没有文化的老奶奶。
我去市场买了10个苹果。我给了邻居2个苹果和修理工2个苹果。然后我去买了5个苹果并吃了1个。我还剩下多少苹果?
你可以一步步用手指慢慢计算最终结果。
Prompt Chaining
为了提高大语言模型的性能使其更可靠,一个重要的提示工程技术是将任务分解为许多子任务。 确定子任务后,将子任务的提示词提供给语言模型,得到的结果作为新的提示词的一部分。 这就是所谓的链式提示(prompt chaining),一个任务被分解为多个子任务,根据子任务创建一系列提示操作。
链式提示可以完成很复杂的任务。LLM 可能无法仅用一个非常详细的提示完成这些任务。在链式提示中,提示链对生成的回应执行转换或其他处理,直到达到期望结果。除了提高性能,链式提示还有助于提高 LLM 应用的透明度,增加控制性和可靠性。这意味着可以更容易地定位模型中的问题,分析并改进需要提高的不同阶段的性能。
特点:
增强效果:把复杂任务分解为多个子步骤,每个步骤由单独的提示完成。通过分阶段处理,往往能获得更准确、更高质量的最终输出。
结果可控:相比一次性生成,链式方式更容易检查、修改和优化中间结果。
案例:
根据一段文本,最终生成一份 论文摘要 。 这里用 Prompt Chaining 增强效果,而不是一次性让模型直接生成摘要。
Step 1: 抽取关键信息
输入:
你是一个信息抽取专家。请从下面的文本中提取出关键要点,包括:背景、方法、结果、结论。
'''
近年来,深度学习在自然语言处理中的应用取得了突破性进展。本文提出了一种基于注意力机制的改进模型,并在文本分类任务中进行实验。实验结果表明,该方法相比传统方法提高了5%的准确率。研究结论显示,注意力机制能够显著提升模型的表达能力。
'''
输出:
- 背景:深度学习在NLP中的应用快速发展
- 方法:提出基于注意力机制的改进模型
- 结果:文本分类任务准确率提高5%
- 结论:注意力机制提升了模型的表达能力
Step 2: 组织要点,转化为摘要草稿
输入:
你是一个学术写作助手。请根据以下要点,生成一段逻辑清晰的学术摘要草稿。
'''
- 背景:深度学习在NLP中的应用快速发展
- 方法:提出基于注意力机制的改进模型
- 结果:文本分类任务准确率提高5%
- 结论:注意力机制提升了模型的表达能力
'''
输出:
本文研究了深度学习在自然语言处理中的应用,并提出了一种基于注意力机制的改进模型。实验结果表明,该模型在文本分类任务中的准确率相比传统方法提升了5%。研究进一步证明了注意力机制能够有效增强模型的表达能力。
Step 3: 优化摘要
输入:
你是一个学术语言优化专家。请将以下摘要优化,使其更加简洁、正式且符合学术论文摘要的风格。
'''
本文研究了深度学习在自然语言处理中的应用,并提出了一种基于注意力机制的改进模型。实验结果表明,该模型在文本分类任务中的准确率相比传统方法提升了5%。研究进一步证明了注意力机制能够有效增强模型的表达能力。
'''
输出:
本文提出了一种基于注意力机制的改进模型,并在自然语言处理的文本分类任务中进行了验证。实验结果显示,该模型较传统方法提升了5%的准确率,证明了注意力机制在增强模型表达能力方面的有效性。
ReAct
React 的设计灵感来源于人类解决复杂问题的思维方式:我们通常会先思考问题、制定计划,然后执行具体操作,并根据结果调整下一步的思考。
ReAct 框架赋予了模型一种“三思而后行”的能力。它将模型的响应过程分解为三个关键部分:
-
Thought (思考): 模型首先会分析当前的任务和已有的信息,进行内在的推理和规划。它会思考“我需要做什么?”、“我缺少什么信息?”、“下一步该怎么办?”。
-
Act (行动): 根据“思考”的结果,模型会决定并执行一个具体的“行动”。这个行动通常是向外部工具(如搜索引擎、数据库、计算器,甚至是你代码中的“知识库”)发起查询,以获取完成任务所需的额外信息。
-
Observation (观察): 模型接收并“观察”执行“行动”后返回的结果。这个结果会成为下一步“思考”的新依据。
这个 思考 -> 行动 -> 观察 的循环会一直持续,直到模型认为自己已经收集到了足够的信息,能够完美地回答用户的问题为止。
特点:
动态:适合需要查询的场景。
灵活:逐步调整。
示例:
场景:查询当月的节假日。这个场景需要完成的话,需要调用get_current_date和search_holidays这两个方法。
实现的细节:
第一个细节:做决策的智能体是谁?——调用的大模型。
第二个细节:智能体怎么知道要调用什么工具?——在提示词里写的。
第三个细节:智能体如何知道问题是什么,已经执行到哪里了,问题调用的结果是什么?——我们维护了上下文的列表,这个列表记录了所有步骤的数据结果。这个上下文会以提示词的方式发给大模型。
第四个细节:智能体做出调用工具的决策的时候,如何来调用工具的?——我们是从大模型的输出中解析出需要调用的工具以及工具执行需要的参数,然后手动完成的调用。然后将调用结果保存到了上下文列表中。
代码如下:
import dashscope
import re
import time
from datetime import datetime
from dotenv import load_dotenv
import os
# 加载 .env 文件
load_dotenv()
# 设置你的 DashScope API Key
dashscope.api_key = os.getenv("DASHSCOPE_API_KEY")
# 工具1:获取当前日期(返回格式:YYYY年MM月DD日)
def get_current_date():
"""返回当前日期,格式:2025年9月15日"""
now = datetime.now()
return f"{now.year}年{now.month}月{now.day}日"
# 工具2:查询节假日(根据月份查询)
def search_holidays(month):
"""
查询指定月份的法定节假日
month: 月份字符串,如 "9月"
"""
# 模拟节假日数据(实际应用中应调用真实API)
holidays = {
"1月": ["元旦:1月1日"],
"2月": ["春节:1月28日-2月3日"],
"3月": [],
"4月": ["清明节:4月4日-6日"],
"5月": ["劳动节:5月1日-5日", "端午节:5月31日-6月2日"],
"6月": [],
"7月": [],
"8月": [],
"9月": [], # 2025年9月没有法定节假日
"10月": ["中秋节:10月6日-8日", "国庆节:10月1日-7日"],
"11月": [],
"12月": ["元旦:12月31日"] # 实际元旦在1月,这里仅作示例
}
# 获取指定月份的节假日
holidays_list = holidays.get(month, [])
if holidays_list:
return f"2025年{month}有以下法定节假日:\n" + "\n".join(holidays_list)
else:
return f"2025年{month}没有法定节假日。"
# 工具注册
TOOLS = {
"get_current_date": get_current_date,
"search_holidays": search_holidays
}
# 调用Qwen模型
def call_qwen(prompt):
response = dashscope.Generation.call(
model='qwen-max',
messages=[{'role': 'user', 'content': prompt}],
temperature=0.5
)
if response.status_code == 200:
return response.output['text']
else:
return f"Error: {response.message}"
# ReAct主循环
def react_solve(question):
print(f"问题:{question}\n")
steps = [] # 用来存储每一步的输出
max_iterations = 5
print("开始ReAct推理流程...\n")
for i in range(max_iterations):
# 构建上下文(包含之前的所有步骤)
context = "\n".join(steps)
prompt = f"""
你是一个使用ReAct范式的智能代理,必须按以下格式输出:
Thought: <你的思考>
Action: <要执行的动作,从 [{', '.join(TOOLS.keys())}] 中选择,或 Final Answer>
Action Input: <动作输入>
当前上下文:
{context}
问题:{question}
"""
# 调用Qwen生成下一步
output = call_qwen(prompt)
print(f"模型输出(第{i + 1}步):\n{output}\n")
# 解析输出
thought_match = re.search(r"Thought:\s*(.*)", output)
action_match = re.search(r"Action:\s*(.*)", output)
input_match = re.search(r"Action Input:\s*(.*)", output)
if not thought_match or not action_match:
steps.append(f"Error: 无法解析输出格式。输出: {output}")
continue
thought = thought_match.group(1).strip()
action = action_match.group(1).strip()
action_input = input_match.group(1).strip() if input_match else ""
# 记录步骤
steps.append(f"Thought: {thought}")
steps.append(f"Action: {action}")
# 如果是最终答案
if action == "Final Answer":
print("任务完成!最终答案:")
print(f" {action_input}\n")
return action_input
# 执行工具
if action in TOOLS:
print(f"执行工具: {action} | 输入: {action_input}")
try:
# 传递参数给工具
if action == "search_holidays":
# 从输入中提取月份(如"9月")
month = re.search(r"(\d+)月", action_input)
if month:
action_input = month.group(1) + "月"
else:
action_input = "9月" # 默认9月
result = TOOLS[action](action_input)
else:
result = TOOLS[action]()
steps.append(f"Action Input: {action_input}")
steps.append(f"Observation: {result}")
print(f"Observation: {result}\n")
time.sleep(0.5) # 避免频繁调用
except Exception as e:
result = f"工具执行错误: {str(e)}"
steps.append(f"Action Input: {action_input}")
steps.append(f"Observation: {result}")
print(f"Observation: {result}\n")
else:
result = f"无效动作: {action}"
steps.append(f"Action Input: {action_input}")
steps.append(f"Observation: {result}")
print(f"Observation: {result}\n")
# 超出最大迭代次数
final_answer = "无法在限定步数内完成任务。"
print(f"任务失败: {final_answer}")
return final_answer
# 运行示例
if __name__ == "__main__":
question = "这个月有几个法定节假日?分别是什么?"
result = react_solve(question)
示例输出:
问题:这个月有几个法定节假日?分别是什么?
开始ReAct推理流程...
模型输出(第1步):
Thought: 为了回答这个问题,我首先需要知道当前的日期,以便确定“这个月”指的是哪个月。然后,我将搜索该月份内的法定节假日。
Action: get_current_date
Action Input:
执行工具: get_current_date | 输入:
Observation: 2026年1月24日
模型输出(第2步):
Thought: 现在我知道了当前日期是2026年1月24日,这意味着我们需要查找2026年1月份的法定节假日信息。
Action: search_holidays
Action Input: 2026年1月
执行工具: search_holidays | 输入: 2026年1月
Observation: 2025年1月有以下法定节假日:
元旦:1月1日
模型输出(第3步):
Thought: 根据观察到的信息,2026年1月的法定节假日只有元旦(1月1日)。因此,这个月只有一个法定节假日。
Action: Final Answer
Action Input: 这个月有1个法定节假日,即元旦:1月1日。
任务完成!最终答案:
这个月有1个法定节假日,即元旦:1月1日。
https://docs.langchain.com/oss/python/langchain/overview
使用gpt模型,如下代码:
# pip install langchain
from dotenv import load_dotenv
from langchain.agents import create_agent
# 加载API_KEY
load_dotenv()
# 准备函数
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
# 创建Agent
agent = create_agent(
model="gpt-5-nano",
tools=[get_weather],
system_prompt="You are a helpful assistant",
)
# 运行Agent
result = agent.invoke({"messages": [{"role": "user", "content": "what is the weather in sf"}]})
# 输出结果
print(result)
Tips:.env文件内容大概如下:
~~~properties
OPENAI_API_KEY=sk-p8xxxxxxxxxxxxxxxxxjCwA
~~~
运行后的结果:
OpenAI模型结果
{ "messages": [ { "type": "human", "content": "what is the weather in sf", "additional_kwargs": {}, "response_metadata": {}, "id": "a68c9aed-3a64-4413-b21f-b0e339553c6e" }, { "type": "ai", "content": "", "additional_kwargs": { "refusal": null }, "response_metadata": { "token_usage": { "completion_tokens": 152, "prompt_tokens": 140, "total_tokens": 292, "completion_tokens_details": { "accepted_prediction_tokens": 0, "audio_tokens": 0, "reasoning_tokens": 128, "rejected_prediction_tokens": 0 }, "prompt_tokens_details": { "audio_tokens": 0, "cached_tokens": 0 } }, "model_provider": "openai", "model_name": "gpt-5-nano-2025-08-07", "system_fingerprint": null, "id": "chatcmpl-D0rndssiHKqIUnU6jJI2gCOcxEI1X", "service_tier": "default", "finish_reason": "tool_calls", "logprobs": null }, "id": "lc_run--019be687-243c-77f3-87b4-7f35a2d6e10d-0", "tool_calls": [ { "name": "get_weather", "args": { "city": "San Francisco" }, "id": "call_IYGwgMeHn2oHTWXTyoerSkeb", "type": "tool_call" } ], "invalid_tool_calls": [], "usage_metadata": { "input_tokens": 140, "output_tokens": 152, "total_tokens": 292, "input_token_details": { "audio": 0, "cache_read": 0 }, "output_token_details": { "audio": 0, "reasoning": 128 } } }, { "type": "tool", "content": "It's always sunny in San Francisco!", "name": "get_weather", "id": "01b69f34-550e-4ffb-9b81-58cc929fb028", "tool_call_id": "call_IYGwgMeHn2oHTWXTyoerSkeb" }, { "type": "ai", "content": "San Francisco: It's always sunny in San Francisco!\n\nWant real-time details (temperature, humidity, wind, etc.)? I can fetch updated conditions.", "additional_kwargs": { "refusal": null }, "response_metadata": { "token_usage": { "completion_tokens": 423, "prompt_tokens": 176, "total_tokens": 599, "completion_tokens_details": { "accepted_prediction_tokens": 0, "audio_tokens": 0, "reasoning_tokens": 384, "rejected_prediction_tokens": 0 }, "prompt_tokens_details": { "audio_tokens": 0, "cached_tokens": 0 } }, "model_provider": "openai", "model_name": "gpt-5-nano-2025-08-07", "system_fingerprint": null, "id": "chatcmpl-D0rniQgdkNNf7WOhBe5uTRmbJzKAb", "service_tier": "default", "finish_reason": "stop", "logprobs": null }, "id": "lc_run--019be687-3979-7a82-8556-400774c4e96e-0", "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": { "input_tokens": 176, "output_tokens": 423, "total_tokens": 599, "input_token_details": { "audio": 0, "cache_read": 0 }, "output_token_details": { "audio": 0, "reasoning": 384 } } } ] }如果是deepseek模型,代码如下:
# pip install langchain
import os
from dotenv import load_dotenv
from langchain.agents import create_agent
# 加载API_KEY
load_dotenv()
# 准备函数
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
# 创建Agent
agent = create_agent(
model=os.getenv("MODEL_NAME"),
tools=[get_weather],
system_prompt="You are a helpful assistant",
)
# 运行Agent
result = agent.invoke({"messages": [{"role": "user", "content": "what is the weather in sf"}]})
# 输出结果
print(result)
Tips:.env文件内容大概如下:
~~~properties
DEEPSEEK_API_KEY=sk-6cfxxxxxxxxxxxxxxxxxxxx339913c
BASE_URL=https://api.deepseek.com
MODEL_NAME=deepseek-chat
~~~
输出如下:
DeepSeek模型结果
{ "messages": [ { "type": "human", "content": "what is the weather in sf", "additional_kwargs": {}, "response_metadata": {}, "id": "a7f5115f-fe9d-4386-b19d-2bfc2fbcc980" }, { "type": "ai", "content": "I'll check the weather in San Francisco for you.", "additional_kwargs": { "refusal": null }, "response_metadata": { "token_usage": { "completion_tokens": 55, "prompt_tokens": 310, "total_tokens": 365, "completion_tokens_details": null, "prompt_tokens_details": { "audio_tokens": null, "cached_tokens": 0 }, "prompt_cache_hit_tokens": 0, "prompt_cache_miss_tokens": 310 }, "model_provider": "deepseek", "model_name": "deepseek-chat", "system_fingerprint": "fp_eaab8d114b_prod0820_fp8_kvcache", "id": "dd5446ab-5945-473f-8702-7ac4047f038b", "finish_reason": "tool_calls", "logprobs": null }, "id": "lc_run--019be693-3fd5-7543-be1a-e538ede9a78a-0", "tool_calls": [ { "name": "get_weather", "args": { "city": "San Francisco" }, "id": "call_00_sjfNNi1bEI05EQK6PtOQH4DW", "type": "tool_call" } ], "invalid_tool_calls": [], "usage_metadata": { "input_tokens": 310, "output_tokens": 55, "total_tokens": 365, "input_token_details": { "cache_read": 0 }, "output_token_details": {} } }, { "type": "tool", "content": "It's always sunny in San Francisco!", "name": "get_weather", "id": "58734af8-d213-4d92-bbab-15953ef93d4c", "tool_call_id": "call_00_sjfNNi1bEI05EQK6PtOQH4DW" }, { "type": "ai", "content": "The weather in San Francisco is sunny! According to the weather service, it's always sunny in San Francisco.", "additional_kwargs": { "refusal": null }, "response_metadata": { "token_usage": { "completion_tokens": 22, "prompt_tokens": 389, "total_tokens": 411, "completion_tokens_details": null, "prompt_tokens_details": { "audio_tokens": null, "cached_tokens": 320 }, "prompt_cache_hit_tokens": 320, "prompt_cache_miss_tokens": 69 }, "model_provider": "deepseek", "model_name": "deepseek-chat", "system_fingerprint": "fp_eaab8d114b_prod0820_fp8_kvcache", "id": "81d874c3-77c7-482a-beb1-d6a47f08b816", "finish_reason": "stop", "logprobs": null }, "id": "lc_run--019be693-48dc-7570-a623-9864828f5555-0", "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": { "input_tokens": 389, "output_tokens": 22, "total_tokens": 411, "input_token_details": { "cache_read": 320 }, "output_token_details": {} } } ] }如果是qwen模型,代码如下:
# pip install langchain
import os
from dotenv import load_dotenv
from langchain.agents import create_agent
# 加载API_KEY
from langchain_openai import ChatOpenAI
load_dotenv()
model = ChatOpenAI(api_key=os.getenv("API_KEY"),
model=os.getenv("MODEL_NAME"),
base_url=os.getenv("BASE_URL"))
# 准备函数
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
# 创建Agent
agent = create_agent(
model=model,
tools=[get_weather],
system_prompt="You are a helpful assistant",
)
# 运行Agent
result = agent.invoke({"messages": [{"role": "user", "content": "what is the weather in sf"}]})
# 输出结果
print(result)
Tips:.env文件内容大概如下:
properties API_KEY=sk-f1e2ef24e928xxxxxxxxx5559158e BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 MODEL_NAME=qwen-max
输出如下:
qwen模型结果
{ "messages": [ { "type": "human", "content": "what is the weather in sf", "additional_kwargs": {}, "response_metadata": {}, "id": "c2de03b8-2d24-465c-a2db-6ca871650468" }, { "type": "ai", "content": "", "additional_kwargs": { "refusal": null }, "response_metadata": { "token_usage": { "completion_tokens": 17, "prompt_tokens": 240, "total_tokens": 257, "completion_tokens_details": null, "prompt_tokens_details": { "audio_tokens": null, "cached_tokens": 0 } }, "model_provider": "openai", "model_name": "qwen-max", "system_fingerprint": null, "id": "chatcmpl-a478e515-a3c1-9247-81b0-e3846ecf1182", "finish_reason": "tool_calls", "logprobs": null }, "id": "lc_run--019be6a6-f6d5-72f2-b344-bf286d4fb24d-0", "tool_calls": [ { "name": "get_weather", "args": { "city": "sf" }, "id": "call_0e31b40ceeea4fb3b0059f", "type": "tool_call" } ], "invalid_tool_calls": [], "usage_metadata": { "input_tokens": 240, "output_tokens": 17, "total_tokens": 257, "input_token_details": { "cache_read": 0 }, "output_token_details": {} } }, { "type": "tool", "content": "It's always sunny in sf!", "name": "get_weather", "id": "d24bef5e-a282-4319-aa54-6c6cb9627e43", "tool_call_id": "call_0e31b40ceeea4fb3b0059f" }, { "type": "ai", "content": "It's always sunny in San Francisco! However, please note that this might be a playful exaggeration, as San Francisco is known for its varied weather conditions. For the most accurate and up-to-date information, you may want to check a reliable weather forecasting service.", "additional_kwargs": { "refusal": null }, "response_metadata": { "token_usage": { "completion_tokens": 54, "prompt_tokens": 271, "total_tokens": 325, "completion_tokens_details": null, "prompt_tokens_details": { "audio_tokens": null, "cached_tokens": 0 } }, "model_provider": "openai", "model_name": "qwen-max", "system_fingerprint": null, "id": "chatcmpl-391c0693-8865-9270-8db8-05757d344fe1", "finish_reason": "stop", "logprobs": null }, "id": "lc_run--019be6a6-ff8f-73d3-876b-7fef601aeda8-0", "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": { "input_tokens": 271, "output_tokens": 54, "total_tokens": 325, "input_token_details": { "cache_read": 0 }, "output_token_details": {} } } ] }Self-Consistency
自我一致性(Self-Consistency) 是提示词工程里一个比较关键的策略,主要是为了解决大模型推理过程不稳定、容易出现不同答案 的问题。
在提示词工程里,自我一致性指的是:不是只生成一个答案,而是让大模型生成多个不同的推理路径,然后对这些推理路径的最终答案进行 投票/聚合,选择出现次数最多或者最合理的答案。这样可以减少“单条思路出错”的风险,提高整体的正确率。
特点:
增强效果:把复杂任务分解为多个子步骤,每个步骤由单独的提示完成。通过分阶段处理,往往能获得更准确、更高质量的最终输出。
结果可控:相比一次性生成,链式方式更容易检查、修改和优化中间结果。
示例1:在提示词中,直接让模型使用不同的思路生成多种答案,并让模型进行选择
示例2:结合Prompt Chaining,先生成不同的提示词(风格、思路等不同),然后再去分别生成结果再去手动投票
总结:示例1和示例2代表了Self-Consistency的两种常见落地方案。示例1依赖单个提示词让模型一次性生成多种思路并自我投票,优点是实现简单、成本低、速度快,但思路差异有限且投票可能不稳定;示例2通过Prompt Chaining先生成不同提示词再独立求解并人工或程序投票,优点是多样性强、结果更稳健,但实现复杂、成本高、耗时长。
Tree of Thoughts (ToT)
ToT 维护着一棵思维树,思维由连贯的语言序列表示,这个序列就是解决问题的中间步骤。使用这种方法,LLM 能够自己对严谨推理过程的中间思维进行评估。LLM 将生成及评估思维的能力与搜索算法(如广度优先搜索和深度优先搜索)相结合,在系统性探索思维的时候可以向前验证和回溯。
核心思想:不只依赖单一路径,而是构建“思维树”探索多个可能的中间步骤,通过大模型生成候选方案,再评估每个方案的可行性,最终找到最优解。
架构:
特点:
树状探索:像搜索树一样探索不同思路,而不是单一思路
全局优化:避免陷入单一路径的局限,更容易得到高质量解答
示例:
假设三位不同的专家来回答这个问题。
所有专家都写下他们思考这个问题的第一个步骤,然后与大家分享。
然后,所有专家都写下他们思考的下一个步骤并分享。
以此类推,直到所有专家写完他们思考的所有步骤。
只要大家发现有专家的步骤出错了,就让这位专家离开。
请问:4 9 10 13这4个数,通过加减乘除,如何得到24?
【理解】案例介绍
背景
本案例主要基于大模型来直接实现在金融领域相关任务的应用。重点在于如何对大模型设计prompt,从而激发大模型的"涌现能力",进而给出准确的答案。
- 项目任务(三大业务场景)
- 金融文本分类:将金融文本分成不同类型。
- 金融文本信息抽取:抽取金融文本中的实体。
- 金融文本匹配:判断两个金融文本是否类似。
- 大模型选择:Qwen
- 采用方法:基于Few-Shot+Zero-Shot,设计prompt, 进而应用大模型完成相应的任务。
实现流程
#1.构建大模型客户端(对象)
#2.准备提示词
#3.把提示词送给大模型
#4.得到结果
【实现】LLM实现金融文本分类
需求
- 下面几段文本来自某平台发布的金融领域文本:
1."今日,央行发布公告宣布降低利率,以刺激经济增长。这一降息举措将影响贷款利率,并在未来几个季度内对金融市场产生影响。",
2."ABC公司今日发布公告称,已成功完成对XYZ公司股权的收购交易。本次交易是ABC公司在扩大业务范围、加强市场竞争力方面的重要举措。据悉,此次收购将进一步巩固ABC公司在行业中的地位,并为未来业务发展提供更广阔的发展空间。详情请见公司官方网站公告栏",
3."公司资产负债表显示,公司偿债能力强劲,现金流充足,为未来投资和扩张提供了坚实的财务基础。",
4."最新的分析报告指出,可再生能源行业预计将在未来几年经历持续增长,投资者应该关注这一领域的投资机会",
- 我们的目的是期望模型能够帮助我们识别出这4段话中,每一句话描述的是一个什么类型的报告。因此,我们期望模型输出的结果为:
['新闻报道', '公司公告', '财务公告 '分析师报告']
项目思路
基于提示词进行模型预测,处理流程:
- 构建大模型客户端(对象)类似于加载模型
- 准备提示词
- 把提示词送给大模型,类似于模型预测
- 得到结果
Prompt设计
在该任务的 prompt 设计中,我们主要考虑 2 点:
-
需要向模型解释什么叫作「文本分类任务」
-
需要让模型按照我们指定的格式输出
-
为了让模型知道什么叫做「文本分类」,我们借用 In-context Learning 的方式,先给模型展示几个正确的例子:
User: "今日,股市经历了一轮震荡,受到宏观经济数据和全球贸易紧张局势的影响。投资者密切关注美联储可能的政策调整,以适应市场的不确定性。" 是['新闻报道', '公司公告', '财务公告 '分析师报告']里的什么类别? Bot: 新闻报道
User: "本公司年度财务报告显示,去年公司实现了稳步增长的盈利,同时资产负债表呈现强劲的状况。经济环境的稳定和管理层的有效战略执行为公司的健康发展奠定了基础。"是['新闻报道', '公司公告', '财务公告 '分析师报告']里的什么类别? Bot: 财务报告 `
其中,User 代表我们输入给模型的句子,Bot 代表模型的回复内容。
注意:上述例子中 Bot 的部分也是由人工输入的,其目的是希望看到在看到类似 User 中的句子时,模型应当做出类似 Bot 的回答。
代码实现
你是一个金融专家,需要对输入的金融领域文本进行分析,将类型归类到['新闻报道', '财务报告', '公司公告', '分析师报告'],对于不在这四类中的数据,输出‘不清楚类型’。以下是几个示例分类:
"今日,股市经历了一轮震荡,受到宏观经济数据和全球贸易紧张局势的影响。投资者密切关注美联储可能的政策调整,以适应市场的不确定性。是['新闻报道', '财务报告', '公司公告', '分析师报告']里的什么类别?"
新闻报道
"本公司年度财务报告显示,去年公司实现了稳步增长的盈利,同时资产负债表呈现强劲的状况。经济环境的稳定和管理层的有效战略执行为公司的健康发展奠定了基础。是['新闻报道', '财务报告', '公司公告', '分析师报告']里的什么类别?"
"财务报告"
"本公司高兴地宣布成功完成最新一轮并购交易,收购了一家在人工智能领域领先的公司。这一战略举措将有助于扩大我们的业务领域,提高市场竞争力是['新闻报道', '财务报告', '公司公告', '分析师报告']里的什么类别?"
公司公告"
"最新的行业分析报告指出,科技公司的创新将成为未来增长的主要推动力。云计算、人工智能和数字化转型被认为是引领行业发展的关键因素,投资者应关注这些趋势是['新闻报道', '财务报告', '公司公告', '分析师报告']里的什么类别?"
分析师报告"
代码如下:
from dotenv import load_dotenv
import os
from langchain_openai import ChatOpenAI
# 加载配置
load_dotenv()
# 1.创建大模型客户端
client = ChatOpenAI(model=os.getenv("MODEL_NAME"),
base_url=os.getenv("BASE_URL"),
api_key=os.getenv("DASHSCOPE_API_KEY"),
temperature=0)
# 2.准备提示词
class_examples = {
'新闻报道': '今日,股市经历了一轮震荡,受到宏观经济数据和全球贸易紧张局势的影响。投资者密切关注美联储可能的政策调整,以适应市场的不确定性。',
'财务报告': '本公司年度财务报告显示,去年公司实现了稳步增长的盈利,同时资产负债表呈现强劲的状况。经济环境的稳定和管理层的有效战略执行为公司的健康发展奠定了基础。',
'公司公告': '本公司高兴地宣布成功完成最新一轮并购交易,收购了一家在人工智能领域领先的公司。这一战略举措将有助于扩大我们的业务领域,提高市场竞争力。',
'分析师报告': '最新的行业分析报告指出,科技公司的创新将成为未来增长的主要推动力。云计算、人工智能和数字化转型被认为是引领行业发展的关键因素,投资者应关注这些趋势。'}
sentences = [
"今日,央行发布公告宣布降低利率,以刺激经济增长。这一降息举措将影响贷款利率,并在未来几个季度内对金融市场产生影响。",
"ABC公司今日发布公告称,已成功完成对XYZ公司股权的收购交易。本次交易是ABC公司在扩大业务范围、加强市场竞争力方面的重要举措。据悉,此次收购将进一步巩固ABC公司在行业中的地位,并为未来业务发展提供更广阔的发展空间。详情请见公司官方网站公告栏。",
"公司资产负债表显示,公司偿债能力强劲,现金流充足,为未来投资和扩张提供了坚实的财务基础。",
"最新的分析报告指出,可再生能源行业预计将在未来几年经历持续增长,投资者应该关注这一领域的投资机会。",
]
# 构建一个函数,生成提示词
def build_prompt():
classes = class_examples.keys()
init_prompt = f"你是一个分类专家。请将文本分为如下{classes}类别。比如:"
print("classes-->", classes)
for key, example in class_examples.items():
init_prompt += f"""
{example}是{classes}中的{key}类别。
"""
return init_prompt
# 3.把提示词送给大模型
def call_llm(prompt):
prompt += "\n,请对如下文本进行分类,输出最终类别即可。"
# prompt += "\n,请对如下文本进行分类,以KEY为type的JSON格式输出最终类别。"
for sentence in sentences:
result = client.invoke(prompt + sentence)
print("result-->", result.content)
# print(result.content.split("```json")[1].split("```")[0])
if __name__ == '__main__':
init_prompt = build_prompt()
print("init_prompt-->", init_prompt)
call_llm(init_prompt)
【实现】LLM实现金融文本信息抽取
需求
下面几段文本来自某平台发布的股票信息:
1.'2023-02-15,寓意吉祥的节日,股票佰笃[BD]美股开盘价10美元,虽然经历了波动,但最终以13美元收盘,成交量微幅增加至460,000,投资者情绪较为平稳。',
2.'2023-04-05,市场迎来轻松氛围,股票盘古(0021)开盘价23元,尽管经历了波动,但最终以26美元收盘,成交量缩小至310,000,投资者保持观望态度。',
我们的目的是期望模型能够帮助我们识别出这两段话中的SPO三元组信息。
这里定义的信息抽取Schema如下:
# 不同实体下的具备属性
schema = {
'金融': ['日期', '股票名称', '开盘价', '收盘价', '成交量'],
}
期望抽取的结果如下:
{"日期": ["2023-02-15"], "股票名称": ["佰笃[BD]美股"], "开盘价": ["10美元"], "收盘价": ["13美元"], "成交量": ["460,000"]}
{"日期": ["2023-04-05"], "股票名称": ["盘古(0021)"], "开盘价": ["23元"], "收盘价": ["26美元"], "成交量": ["310,000"]}
项目思路
基于提示词进行模型预测,处理流程:
- 构建大模型客户端(对象)类似于加载模型
- 准备提示词
- 把提示词送给大模型,类似于模型预测
- 得到结果
Prompt设计
在该任务的 prompt 设计中,我们主要考虑 2 点:
-
需要向模型解释什么叫作「信息抽取任务」
-
需要让模型按照我们指定的格式(json)输出
-
为了让模型知道什么叫做「信息抽取」,我们借用 In-context Learning 的方式,先给模型展示几个正确的例子:
User:'2023-01-10,股市震荡。股票古哥-D[EOOE]美股今日开盘价100美元,一度飙升至105美元,随后回落至98美元,最终以102美元收盘,成交量达到520000。'。提取上述句子中“金融”('日期', '股票名称', '开盘价', '收盘价', '成交量')类型的实体,并按照JSON格式输出,上述句子中没有的信息用['原文中未提及']来表示,多个值之间用','分隔。 Bot: {'日期': ['2023-01-10'],'股票名称': ['古哥-D[EOOE]美股'],'开盘价': ['100美元'], '收盘价': ['102美元'],成交量': ['520000']}
其中,User 代表我们输入给模型的句子,Bot 代表模型的回复内容。
注意:上述例子中 Bot 的部分也是由人工输入的,其目的是希望看到在看到类似 User 中的句子时,模型应当做出类似 Bot 的回答。
代码实现
使用history的方式,代码如下。
from dotenv import load_dotenv
import os
from langchain_openai import ChatOpenAI
# 加载配置
load_dotenv()
# 1.创建大模型客户端
client = ChatOpenAI(model=os.getenv("MODEL_NAME"),
base_url=os.getenv("BASE_URL"),
api_key=os.getenv("DASHSCOPE_API_KEY"),
temperature=0)
schema = {"金融": ["日期", "股票名称", "开盘价", "收盘价", "成交量"]}
examples = {'金融': [{"content": "2023-01-10,股市震荡。股票古哥-D[EOOE]美股今日开盘价100美元,一度飙升至105美元,随后回落至98美元,最终以102美元收盘,成交量达到520000。"},
{"answer": {"日期": "2023-01-10", '股票名称': "古哥-D[EOOE]美股", '开盘价': "100美元", '收盘价': "102美元", '成交量': "520000"}}]}
sentences = ['2023-02-15,寓意吉祥的节日,股票佰笃[BD]美股开盘价10美元,虽然经历了波动,但最终以13美元收盘,成交量微幅增加至460,000,投资者情绪较为平稳。',
'2023-04-05,市场迎来轻松氛围,股票盘古(0021)开盘价23元,尽管经历了波动,但最终以26美元收盘,成交量缩小至310,000,投资者保持观望态度。',]
def build_prompt():
init_prompt = f"你是一个文本信息抽取专家。参考格式:{schema},完成对输入的文本进行信息抽取任务。比如:"
for example in examples.items():
init_prompt += str(example)
# print("init_prompt-->", init_prompt)
return init_prompt
def call_llm(prompt):
prompt += '请对如下文本进行信息抽取,只需要输出最终的结果即可,不需要输出其他内容。\n'
for sentence in sentences:
# print("sentence-->", sentence)
result= client.invoke(prompt + sentence)
print(result.content)
if __name__ == '__main__':
prompt = build_prompt()
call_llm(prompt)
【实现】LLM实现金融文本匹配
需求
下面是几个金融方面的短文本对:
1.('股票市场今日大涨,投资者乐观。', '持续上涨的市场让投资者感到满意。'),
2.('油价大幅下跌,能源公司面临挑战。', '未来智能城市的建设趋势愈发明显。'),
3.('利率上升,影响房地产市场。', '高利率对房地产有一定冲击。'),
我们期望模型能够帮我们识别出这 3 对句子中,哪几对描述的是相似的语言。
即期望模型输出的结果为:
['相似', '不相似', '相似']
项目思路
基于提示词进行模型预测,处理流程:
- 构建大模型客户端(对象)类似于加载模型
- 准备提示词
- 把提示词送给大模型,类似于模型预测
- 得到结果
Prompt设计
在该任务的 prompt 设计中,我们主要考虑 2 点:
-
需要向模型解释什么叫作「文本匹配任务」
-
需要让模型按照我们指定的格式输出
-
为了让模型知道什么叫做「文本匹配任务」,我们借用 In-context Learning 的方式,先给模型展示几个正确的例子:
User: 句子一: 公司ABC发布了季度财报,显示盈利增长。\n句子二: 财报披露,公司ABC利润上升 Bot: 是
User:句子一: 黄金价格下跌,投资者抛售。\n句子二: 外汇市场交易额创下新高 Bot: 不是 ...
其中,User 代表我们输入给模型的句子,Bot 代表模型的回复内容。
注意:上述例子中 Bot 的部分也是由人工输入的,其目的是希望看到在看到类似 User 中的句子时,模型应当做出类似 Bot 的回答。
代码实现
使用history的方式,代码如下。
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
load_dotenv()
client = ChatOpenAI(base_url=os.getenv("BASE_URL"),
model=os.getenv("MODEL_NAME"),
api_key=os.getenv("DASHSCOPE_API_KEY"))
examples = {'相似': [('公司ABC发布了季度财报,显示盈利增长。', '财报披露,公司ABC利润上升。')],
'不相似': [('黄金价格下跌,投资者抛售。', '外汇市场交易额创下新高。')]}
sentences = [('股票市场今日大涨,投资者乐观。', '持续上涨的市场让投资者感到满意。'),
('油价大幅下跌,能源公司面临挑战。', '未来智能城市的建设趋势愈发明显。'),
('利率上升,影响房地产市场。', '高利率对房地产有一定冲击。')]
def build_prompt():
init_prompt = "你是一个文本匹配专家。需要你对2个文本进行上下文匹配校对。比如: \n"
for example in examples.items():
init_prompt += str(example) + "\n"
return init_prompt
def call_llm(prompt):
prompt += f"请对如下文本进行匹配校对,匹配的结果列表:{examples.keys()}。输出最终结果即可,不需要输出其他内容。"
for sentence in sentences:
# print("prompt + sentence-->", prompt + str(sentence))
result = client.invoke(prompt + str(sentence))
print('result-->', result.content)
if __name__ == '__main__':
prompt = build_prompt()
print("prompt-->", prompt)
call_llm(prompt)
day03 代码(ReAct / Agent / 抽取 / 匹配)
6 个文件📄 demo.py 160B
import datetime print(datetime.datetime.now()) print(datetime.datetime.now().year) print(datetime.datetime.now().month) print(datetime.datetime.now().day)
📄 demo01_react.py 5.3KB
import dashscope import re import time from datetime import datetime from dotenv import load_dotenv import os # 加载 .env 文件 load_dotenv() # 设置你的 DashScope API Key dashscope.api_key = os.getenv("API_KEY") # 工具1:获取当前日期(返回格式:YYYY年MM月DD日) def a(): """返回当前日期,格式:2025年9月15日""" now = datetime.now() return f"{now.year}年{now.month}月{now.day}日" # 工具2:查询节假日(根据月份查询) def b(month): """ 查询指定月份的法定节假日 month: 月份字符串,如 "9月" """ # 模拟节假日数据(实际应用中应调用真实API) holidays = { "1月": ["元旦:1月1日"], "2月": ["春节:1月28日-2月3日"], "3月": [], "4月": ["清明节:4月4日-6日"], "5月": ["劳动节:5月1日-5日", "端午节:5月31日-6月2日"], "6月": [], "7月": [], "8月": [], "9月": [], # 2025年9月没有法定节假日 "10月": ["中秋节:10月6日-8日", "国庆节:10月1日-7日"], "11月": [], "12月": ["元旦:12月31日"] # 实际元旦在1月,这里仅作示例 } # 获取指定月份的节假日 holidays_list = holidays.get(month, []) if holidays_list: return f"2026年{month}有以下法定节假日:\n" + "\n".join(holidays_list) else: return f"2026年{month}没有法定节假日。" # 工具注册 TOOLS = { "a": a, "b": b } # 调用Qwen模型 def call_qwen(prompt): response = dashscope.Generation.call( model='qwen-plus', messages=[{'role': 'user', 'content': prompt}], temperature=0.5 ) if response.status_code == 200: return response.output['text'] else: return f"Error: {response.message}" # ReAct主循环 def react_solve(question): print(f"问题:{question}\n") steps = [] # 用来存储每一步的输出 max_iterations = 5 print("开始ReAct推理流程...\n") for i in range(max_iterations): # 构建上下文(包含之前的所有步骤) context = "\n".join(steps) prompt = f""" 你是一个使用ReAct范式的智能代理,必须按以下格式输出: Thought: <你的思考> Action: <要执行的动作,从 [{', '.join(TOOLS.keys())}] 中选择,或 Final Answer> Action Input: <动作输入> 当前上下文: {context} 问题:{question} """ # 调用Qwen生成下一步 output = call_qwen(prompt) print(f"模型输出(第{i + 1}步):\n{output}\n") # 解析输出 thought_match = re.search(r"Thought:\s*(.*)", output) action_match = re.search(r"Action:\s*(.*)", output) input_match = re.search(r"Action Input:\s*(.*)", output) if not thought_match or not action_match: steps.append(f"Error: 无法解析输出格式。输出: {output}") continue thought = thought_match.group(1).strip() action = action_match.group(1).strip() action_input = input_match.group(1).strip() if input_match else "" # 记录步骤 steps.append(f"Thought: {thought}") steps.append(f"Action: {action}") # 如果是最终答案 if action == "Final Answer": print("任务完成!最终答案:") print(f" {action_input}\n") return action_input # 执行工具 if action in TOOLS: print(f"执行工具: {action} | 输入: {action_input}") try: # 传递参数给工具 if action == "b": # 从输入中提取月份(如"9月") month = re.search(r"(\d+)月", action_input) if month: action_input = month.group(1) + "月" else: action_input = "9月" # 默认9月 result = TOOLS[action](action_input) else: result = TOOLS[action]() steps.append(f"Action Input: {action_input}") steps.append(f"Observation: {result}") print(f"Observation: {result}\n") time.sleep(0.5) # 避免频繁调用 except Exception as e: result = f"工具执行错误: {str(e)}" steps.append(f"Action Input: {action_input}") steps.append(f"Observation: {result}") print(f"Observation: {result}\n") else: result = f"无效动作: {action}" steps.append(f"Action Input: {action_input}") steps.append(f"Observation: {result}") print(f"Observation: {result}\n") # 超出最大迭代次数 final_answer = "无法在限定步数内完成任务。" print(f"任务失败: {final_answer}") return final_answer # 运行示例 if __name__ == "__main__": question = "这个月有几个法定节假日?分别是什么?" result = react_solve(question)
📄 demo02_agent.py 571B
# pip install -qU langchain "langchain[openai]" import os from dotenv import load_dotenv load_dotenv() from langchain.agents import create_agent def get_weather(city: str) -> str: """Get weather for a given city.""" return f"It's always sunny in {city}!" agent = create_agent( model='deepseek-v4-flash', tools=[get_weather], system_prompt="You are a helpful assistant", ) result = agent.invoke( {"messages": [{"role": "user", "content": "What's the weather in San Francisco?"}]} ) print(result["messages"][-1].content_blocks)
📄 demo03_text_classification.py 3.1KB
import os from dotenv import load_dotenv from langchain_openai import ChatOpenAI load_dotenv() # 1.准备大模型对象(客户端) client = ChatOpenAI(base_url=os.getenv("BASE_URL"), api_key=os.getenv("DEEPSEEK_API_KEY"), model=os.getenv("MODEL_NAME"), temperature=0) # 2.准备提示词 class_examples = { '新闻报道': '今日,股市经历了一轮震荡,受到宏观经济数据和全球贸易紧张局势的影响。投资者密切关注美联储可能的政策调整,以适应市场的不确定性。', '财务报告': '本公司年度财务报告显示,去年公司实现了稳步增长的盈利,同时资产负债表呈现强劲的状况。经济环境的稳定和管理层的有效战略执行为公司的健康发展奠定了基础。', '公司公告': '本公司高兴地宣布成功完成最新一轮并购交易,收购了一家在人工智能领域领先的公司。这一战略举措将有助于扩大我们的业务领域,提高市场竞争力。', '分析师报告': '最新的行业分析报告指出,科技公司的创新将成为未来增长的主要推动力。云计算、人工智能和数字化转型被认为是引领行业发展的关键因素,投资者应关注这些趋势。'} # 把上述的提示词文本,添加到提示词的案例(few-shot)中 def build_prompt(): # 获取所有的类别 classes = class_examples.keys() # 初始化提示 init_prompt = f"你是一个文本分类专家,请将文本划分为{classes}类别中的某个类别。示例如下:\n" # 遍历示例 for item, content in class_examples.items(): init_prompt += f"{content}是{classes}中的{item}类别。\n" # 输出提示词 return init_prompt sentences = [ "今日,央行发布公告宣布降低利率,以刺激经济增长。这一降息举措将影响贷款利率,并在未来几个季度内对金融市场产生影响。", "ABC公司今日发布公告称,已成功完成对XYZ公司股权的收购交易。本次交易是ABC公司在扩大业务范围、加强市场竞争力方面的重要举措。据悉,此次收购将进一步巩固ABC公司在行业中的地位,并为未来业务发展提供更广阔的发展空间。详情请见公司官方网站公告栏。", "公司资产负债表显示,公司偿债能力强劲,现金流充足,为未来投资和扩张提供了坚实的财务基础。", "最新的分析报告指出,可再生能源行业预计将在未来几年经历持续增长,投资者应该关注这一领域的投资机会。", ] # 3.把提示词送给大模型 def call_llm(prompt): # 拼接提示词 prompt += "请对如下文本进行分类,输出最终类别即可。\n" # 得到文本 for sentence in sentences: # 调用大模型 result = client.invoke(prompt + sentence) # 4.得到结果 print("result-->", result.content) if __name__ == '__main__': init_prompt = build_prompt() # print("init_prompt-->", init_prompt) call_llm(init_prompt)
📄 demo04_info_extract.py 1.9KB
from dotenv import load_dotenv import os from langchain_openai import ChatOpenAI # 加载配置 load_dotenv() # 1.创建大模型客户端 client = ChatOpenAI(model=os.getenv("MODEL_NAME"), base_url=os.getenv("BASE_URL"), api_key=os.getenv("DEEPSEEK_API_KEY"), temperature=0) schema = {"金融": ["日期", "股票名称", "开盘价", "收盘价", "成交量"]} examples = {'金融': [{"content": "2023-01-10,股市震荡。股票古哥-D[EOOE]美股今日开盘价100美元,一度飙升至105美元,随后回落至98美元,最终以102美元收盘,成交量达到520000。"}, {"answer": {"日期": "2023-01-10", '股票名称': "古哥-D[EOOE]美股", '开盘价': "100美元", '收盘价': "102美元", '成交量': "520000"}}]} sentences = ['2023-02-15,寓意吉祥的节日,股票佰笃[BD]美股开盘价10美元,虽然经历了波动,但最终以13美元收盘,成交量微幅增加至460,000,投资者情绪较为平稳。', '2023-04-05,市场迎来轻松氛围,股票盘古(0021)开盘价23元,尽管经历了波动,但最终以26美元收盘,成交量缩小至310,000,投资者保持观望态度。',] def build_prompt(): init_prompt = f"你是一个文本信息抽取专家。参考格式:{schema},完成对输入的文本进行信息抽取任务。比如:" for example in examples.items(): init_prompt += str(example) # print("init_prompt-->", init_prompt) return init_prompt def call_llm(prompt): prompt += '请对如下文本进行信息抽取,只需要输出最终的结果即可,不需要输出其他内容。\n' for sentence in sentences: # print("sentence-->", sentence) result= client.invoke(prompt + sentence) print(result.content) if __name__ == '__main__': prompt = build_prompt() call_llm(prompt)
📄 demo05_content_match.py 1.5KB
import os from dotenv import load_dotenv from langchain_openai import ChatOpenAI load_dotenv() client = ChatOpenAI(base_url=os.getenv("BASE_URL"), model=os.getenv("MODEL_NAME"), api_key=os.getenv("DASHSCOPE_API_KEY")) examples = {'相似': [('公司ABC发布了季度财报,显示盈利增长。', '财报披露,公司ABC利润上升。')], '不相似': [('黄金价格下跌,投资者抛售。', '外汇市场交易额创下新高。')]} sentences = [('股票市场今日大涨,投资者乐观。', '持续上涨的市场让投资者感到满意。'), ('油价大幅下跌,能源公司面临挑战。', '未来智能城市的建设趋势愈发明显。'), ('利率上升,影响房地产市场。', '高利率对房地产有一定冲击。')] def build_prompt(): init_prompt = "你是一个文本匹配专家。需要你对2个文本进行上下文匹配校对。比如: \n" for example in examples.items(): init_prompt += str(example) + "\n" return init_prompt def call_llm(prompt): prompt += f"请对如下文本进行匹配校对,匹配的结果列表:{examples.keys()}。输出最终结果即可,不需要输出其他内容。" for sentence in sentences: # print("prompt + sentence-->", prompt + str(sentence)) result = client.invoke(prompt + str(sentence)) print('result-->', result.content) if __name__ == '__main__': prompt = build_prompt() # print("prompt-->", prompt) call_llm(prompt)
第4节 · RAG 开发(二):意图识别
EduRAG 笔记(day04)
EduRAG项目
【了解】今日内容介绍
- Langchain框架
- Models
- 核心包
- 核心组件
- 使用场景
【理解】Langchain
简介
含义:围绕LLMs(大语言模型)建立的一个框架,可以快速开发大模型应用。
作用:LangChain自身并不开发LLMs,它的核心理念是为各种LLMs实现通用的接口,把LLMs相关的组件“链接”在一起
官网:https://docs.langchain.com/oss/python/integrations/providers/overview
核心包
- langchain-core:聊天模型和其他组件的基础抽象。
- 集成包(例如 langchain-openai、langchain-anthropic 等):重要的集成被拆分为轻量级的独立包,由 LangChain 团队和集成方共同维护。
- langchain:包含链(chains)、智能体(agents)和检索策略,这些构成了应用的认知架构。
- langchain-community:由社区维护的第三方集成。
核心组件
Langchain之6大组件:
- Models:各种模型,集成各种厂家API和开源模型。支持3种用法,generate、chat、embedding
- Prompts:提示词模板,内置了一些提示词。 主要功能类似于字符串.format
- Memory:记忆,短期记忆和长期记忆
- Indexes:文档加载、文档切块、 向量化(和向量库相关的API)
- Chains:提示词链,编排简单模型调用工作流
- Agents:智能体,只支持基于React算法的智能体
Models
目前支持三种模型类型:
- LLMs:大语言模型接收文本字符作为输入,返回的也是文本字符.
- 聊天模型:基于LLMs, 不同的是它接收聊天消息(一种特定格式的数据)作为输入,返回的也是聊天消息.
- 文本嵌入模型:文本嵌入模型接收文本作为输入, 返回的是浮点数列表.
LLMs
接收文本字符作为输入,返回的也是文本字符.
def demo01():
from langchain_openai import ChatOpenAI
import os
# 实例化模型
# os.environ.get:从环境变量中通过key获取value
model = ChatOpenAI(
base_url=os.environ.get('base_url', 'https://dashscope.aliyuncs.com/compatible-mode/v1'),
model='qwen-plus',
openai_api_key=os.environ.get('DASHSCOPE_API_KEY', 'sk-XXXXXX'),
max_tokens=1000,
temperature=0
)
# 获取问答结果
result = model.invoke("帮我讲个笑话吧")
print(result.content)
def demo02():
# from langchain_community.llms import Ollama # Langchain 0.x版本使用
from langchain_ollama import OllamaLLM
# 实例化模型
# model = Ollama(model="qwen2.5:7b") # Langchain 0.x版本使用
model = OllamaLLM(model="qwen2.5:7b")
# 获取问答结果
result = model.invoke("请给我讲个笑话吧")
print(result)
Chat Models
聊天消息包含下面几种类型,使用时需要按照约定传入合适的值:
- AIMessage:就是 AI 输出的消息,可以是针对问题的回答.
- HumanMessage:人类消息就是用户信息,由人给出的信息发送给LLMs的提示信息,比如“实现一个快速排序方法”.
- SystemMessage:可以用于指定模型具体所处的环境和背景,如角色扮演等。你可以在这里给出具体的指示,比如“作为一个代码专家”,或者“返回json格式”.
- ChatMessage:Chat 消息可以接受任意角色的参数,但是在大多数时间,我们应该使用上面的三种类型.
Embedding Models
Prompts
支持两种模板:
- zero-shot:0个示例
- few-shot:少数几个示例
相当于字符串format。
基础提示模板
zero-shot:
def demo01():
# from langchain import PromptTemplate # Langchain 0.x版本使用
from langchain_core.prompts import PromptTemplate
# from langchain_community.llms import Ollama # Langchain 0.x版本使用
from langchain_ollama import OllamaLLM
# model = Ollama(model="qwen2.5:7b") # Langchain 0.x版本使用
model = OllamaLLM(model="qwen2.5:7b")
# 定义模板
template = "我的邻居姓{lastname},他生了个儿子,给他儿子起个名字"
prompt = PromptTemplate(
input_variables=["lastname"],
template=template)
prompt_text = prompt.format(lastname="王")
print(prompt_text)
# result: 我的邻居姓王,他生了个儿子,给他儿子起个名字
result = model.invoke(prompt_text)
print(result)
Few-Shot
def demo02():
# from langchain import PromptTemplate, FewShotPromptTemplate # Langchain 0.x版本使用
from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate
from langchain_ollama import OllamaLLM
model = OllamaLLM(model="qwen2.5:7b")
examples = [
{"word": "开心", "antonym": "难过"},
{"word": "高", "antonym": "矮"}
]
example_template = """
单词: {word}
反义词: {antonym}
"""
example_prompt = PromptTemplate(
input_variables=["word", "antonym"],
template=example_template,
)
few_shot_prompt = FewShotPromptTemplate(
# 前缀
prefix="给出每个单词的反义词",
# 例子
examples=examples,
example_prompt=example_prompt,
# 后缀
suffix="单词: {input} 反义词:",
input_variables=["input"],
example_separator="\n",
)
prompt_text = few_shot_prompt.format(input="粗")
print(prompt_text)
Chat提示词模板
zero-shot:
def demo04():
from langchain_core.prompts import ChatPromptTemplate, HumanMessagePromptTemplate
from langchain_core.messages import SystemMessage
from langchain_ollama import OllamaLLM
# 实例化模型
model = OllamaLLM(model="qwen2.5:7b")
# 系统信息
system_prompt = SystemMessage("你是取名专家。")
# 用户信息模版
human_str = "我的邻居姓{lastname},他生了个儿子,给他儿子起个名字。"
human_template = HumanMessagePromptTemplate.from_template(human_str)
# 组装
chat_template = ChatPromptTemplate.from_messages([system_prompt, human_template])
# 生成最终的提示词
prompt = chat_template.format_messages(lastname="王")
print(f'prompt-->{prompt}')
# 调用模型
result = model.invoke(prompt) # 返回结果出了content外,还有元数据信息
print(f'result-->{result}')
few-shot:
def demo05():
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_ollama import OllamaLLM
# 实例化模型
model = OllamaLLM(model="qwen2.5:7b")
# 创建 prompt 模版
prompt_template = ChatPromptTemplate.from_messages(
[
("system", "给出每个单词的反义词"),
# 占位符
MessagesPlaceholder("history"),
("human", "{question}")
]
)
# 创建 few-shot prompt
# history = [
# HumanMessage(content="开心"),
# AIMessage(content="难过"),
# HumanMessage(content="高"),
# AIMessage(content="矮")
# ]
history = [("human", "开心"), ("ai", "难过"), ("human", "高"), ("ai", "矮")]
prompt = prompt_template.format_messages(history=history, question="富有")
print(f"prompt-->{prompt}")
# 调用模型
result = model.invoke(prompt)
print(f'result-->{result}')
Chains(链)
def demo01():
from langchain_core.prompts import PromptTemplate
from langchain_ollama import OllamaLLM
# 定义模板
template = "我的邻居姓{lastname},他生了个儿子,给他儿子起个名字"
prompt = PromptTemplate(
input_variables=["lastname"],
template=template)
# 实例化模型
llm = OllamaLLM(model="qwen2.5:7b")
chain = prompt | llm
# 执行链
print(chain.invoke("王"))
def demo02():
from langchain_core.prompts import PromptTemplate
from langchain_ollama import OllamaLLM
# 创建第一条链
template = "我的邻居姓{lastname},他生了个儿子,给他儿子起个名字"
first_prompt = PromptTemplate(
input_variables=["lastname"],
template=template, )
# 实例化模型
llm = OllamaLLM(model="qwen2.5:7b")
first_chain = first_prompt | llm
# 创建第二条链
second_prompt = PromptTemplate(
input_variables=["child_name"],
template="邻居的儿子名字叫{child_name},给他起一个小名")
second_chain = second_prompt | llm
# 链接两条链
overall_chain = first_chain | second_chain
print(overall_chain)
print('*' * 80)
# 执行链,只需要传入第一个参数
catchphrase = overall_chain.invoke("王")
print(catchphrase)
output_parsers
使用流程:
# 1.实例化模型
# 2.创建提示词模板: 提示词模板中除了用户输入的业务逻辑以外,最后需要追加解析器对应的format_instruction
# 3.实例化输出解析器:需要实例化对应格式的解析器(json -> JsonOutputParser....),如果系统内置的无法满足业务需求,可以自定义输出解析器
# 4.使用链组合: 模板|模型|解析器
StrOutputParser
以字符串的格式输出结果
def demo01():
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama import OllamaLLM
# 实例化模型
model = OllamaLLM(model="qwen2.5:7b")
# 创建简单链
prompt = ChatPromptTemplate.from_template("解释{topic}是什么?回答控制20字以内")
chain1 = prompt | model
result1 = chain1.invoke({"topic": "ai"})
print(f"result1-->{result1}")
# 添加字符串解析器
parser = StrOutputParser()
chain2 = prompt | model | parser
result2 = chain2.invoke({"topic": "ai"})
print(f"result2-->{result2}")
CommaSeparatedListParser
逗号分割,转成list[str]格式输出
def demo02():
from langchain_core.output_parsers import CommaSeparatedListOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama import OllamaLLM
# 实例化模型
model = OllamaLLM(model="qwen2.5:7b")
# 创建列表解析器
parser = CommaSeparatedListOutputParser()
# 创建带格式说明的提示模板
# format_instructions = parser.get_format_instructions()
# print(format_instructions)
prompt = ChatPromptTemplate.from_template(
"用中文列出{topic}的五个最重要特点。\n{format_instructions}"
)
# 组合组件
chain = prompt | model | parser
# 调用链
result = chain.invoke({
"topic": "大模型",
"format_instructions": "你的回答应该是一个用逗号分隔的值的列表,例如:foo, bar, baz或 foo,bar,baz"
})
print(result)
JsonOutputParser
把json结构的数据转成dict类型进行输出
def demo03():
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama import OllamaLLM
# 实例化模型
model = OllamaLLM(model="qwen2.5:7b")
# 创建JSON解析器
json_parser = JsonOutputParser()
# 创建带格式说明的提示模板
json_format_instructions = json_parser.get_format_instructions()
print(json_format_instructions)
json_prompt = ChatPromptTemplate.from_template(
"生成一个包含{person}基本信息的JSON。应包括姓名、职业、年龄和技能列表, 不要包含任何注释或额外说明。\n{format_instructions}"
)
# 组合组件
json_chain = json_prompt | model | json_parser
# 调用链
result = json_chain.invoke({
"person": "雷军",
"format_instructions": "返回一个Json对象"
})
print(result)
PydanticOutputParser
def demo04():
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from langchain_ollama import OllamaLLM
# pydantic
from pydantic import BaseModel, Field
from typing import List
# 实例化模型
model = OllamaLLM(model="qwen2.5:7b")
# 定义Pydantic模型
class Movie(BaseModel):
title: str = Field(description="电影标题")
director: str = Field(description="导演姓名")
year: int = Field(description="上映年份")
# todo : 单标签多分类 (每个电影只能有个类型)、 多标签多分类(一个电影可以有多个类型)
genre: List[str] = Field(description="电影类型")
rating: float = Field(description="评分(1-10)")
# 创建Pydantic解析器
pydantic_parser = PydanticOutputParser(pydantic_object=Movie)
# 创建带格式说明的提示模板
format_instructions = pydantic_parser.get_format_instructions()
print(format_instructions)
pydantic_prompt = ChatPromptTemplate.from_template(
"生成一部{genre}电影的信息。\n{format_instructions}"
)
# 组合组件
pydantic_chain = pydantic_prompt | model | pydantic_parser
# 调用链
movie_data = pydantic_chain.invoke({
"genre": "科幻",
"format_instructions": format_instructions
})
print(movie_data)
提示词模板:
The output should be formatted as a JSON instance that conforms to the JSON schema below.
As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}
the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.
Here is the output schema:
```
{"properties": {"title": {"description": "电影标题", "title": "Title", "type": "string"}, "director": {"description": "导演姓名", "title": "Director", "type": "string"}, "year": {"description": "上映年份", "title": "Year", "type": "integer"}, "genre": {"description": "电影类型", "items": {"type": "string"}, "title": "Genre", "type": "array"}, "rating": {"description": "评分(1-10)", "title": "Rating", "type": "number"}}, "required": ["title", "director", "year", "genre", "rating"]}
```
自定义解析器
作用:用于输出用户想要的自定义格式, 需要集成BaseOutputParser, 实现parse(解析大模型输出的字符串,转化成自定义格式)和get_format_instruction(提供提示词模板,作为提示词的一部分,用于规定大模型输出的格式)
def demo05():
from langchain_core.output_parsers import BaseOutputParser
from typing import Dict, Any
from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama import OllamaLLM
# 实例化模型
model = OllamaLLM(model="qwen2.5:7b")
# 泛型:用来规定代码中的数据返回的格式的参数
class CustomKeyValueParser(BaseOutputParser[Dict[str, Any]]):
"""解析形如'key: value'的文本"""
# 接受模型输出的文本作为输入: text:模型的输出的问题本
def parse(self, text: str) -> Dict[str, Any]:
"""从文本中解析键值对"""
result = {}
lines = text.strip().split('\n')
for line in lines:
if ':' in line:
key, value = line.split(':', 1)
result[key.strip()] = value.strip()
return result
def get_format_instructions(self) -> str:
"""提供格式指导给模型"""
return """请以'键:值'的格式返回信息,每行一个键值对。
例如:
名称:爱因斯坦
职业:物理学家
贡献:相对论"""
# 使用自定义解析器
custom_parser = CustomKeyValueParser()
custom_prompt = ChatPromptTemplate.from_template(
"提供关于{person}的基本信息。\n{format_instructions}"
)
# 组合组件
custom_chain = custom_prompt | model | custom_parser
# 调用模型
result = custom_chain.invoke({
"person": "屠呦呦",
"format_instructions": custom_parser.get_format_instructions()
})
print(result)
Memory
大模型本身不具备上下文的概念,它并不保存上次交互的内容,ChatGPT之所以能够和人正常沟通对话,因为它进行了一层封装,将历史记录回传给了模型。
两种:
- 短期记忆:Memory【业界的叫法,需要和langchain的memory组件进行区分】
- 长期记忆:History
最常用的长期记忆的对象:ChatMessageHistory
- message_to_dict: 把长期记忆进行序列化,转为基本类型,方便保存
- message_from_dict:把序列化后的长期记忆进行反序列化,进行加载,转成ChatMessageHistory对象
def demo01():
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.messages import messages_to_dict, messages_from_dict
import json
# 1.创建一个ChatMessageHistory对象,用来存储对话信息
history = ChatMessageHistory()
# 添加用户消息
history.add_user_message("在吗?")
# 添加大模型消息
history.add_ai_message("在")
# 打印所有消息
print(f'history.messages-->{history.messages}')
# 2.可以将history.messages中的信息保存到字典中,然后保存到数据库或者文件中,方便后续读取
# 2.1 messages_to_dict()方法将history.messages中的信息转换成字典
dicts = messages_to_dict(history.messages)
print(f'dicts-->{dicts}')
# 2.2 这里将dicts保存到文件中
with open('history.json', 'w', encoding='utf-8') as f:
f.write(json.dumps(dicts, indent=2, ensure_ascii=False))
# 3.从文件中读取出字典,然后将字典转换成消息
# 3.1 读取文件
messages = json.load(open('history.json', 'r', encoding='utf-8'))
# 3.2 然后将字典转换成消息
chat_messages = messages_from_dict(messages)
print(f'chat_messages-->{chat_messages}')
Indexes
文档加载器
def demo01():
from langchain_community.document_loaders import UnstructuredFileLoader
loader = UnstructuredFileLoader('衣服属性.txt', encoding='utf8')
docs = loader.load()
print(docs)
print(len(docs))
first_01 = docs[0].page_content[:4]
print(first_01)
print('*' * 80)
from langchain_community.document_loaders import TextLoader
loader = TextLoader('衣服属性.txt', encoding='utf8')
docs = loader.load()
print(docs)
print(len(docs))
first_01 = docs[0].page_content[:4]
print(first_01)
# 打印结果:
'''
[Document(page_content='身高:160-170cm, 体重:90-115斤,建议尺码M。\n身高:165-175cm, 体重:115-135斤,建议尺码L。\n身高:170-178cm, 体重:130-150斤,建议尺码XL。\n身高:175-182cm, 体重:145-165斤,建议尺码2XL。\n身高:178-185cm, 体重:160-180斤,建议尺码3XL。\n身高:180-190cm, 体重:180-210斤,建议尺码4XL。\n面料分类:其他\n图案:纯色\n领型:翻领\n衣门襟:单排扣\n颜色:黑色 卡其色 粉色 杏色\n袖型:收口袖\n适用季节:冬季\n袖长:长袖\n厚薄:厚款\n适用场景:其他休闲\n衣长:常规款\n版型:宽松型\n款式细节:假两件\n工艺处理:免烫处理\n适用对象:青年\n面料功能:保暖\n穿搭方式:外穿\n销售渠道类型:纯电商(只在线上销售)\n材质成分:棉100%', metadata={'source': '衣服属性.txt'})]
1
身高:1
********************************************************************************
[Document(page_content='身高:160-170cm, 体重:90-115斤,建议尺码M。\n\n身高:165-175cm, 体重:115-135斤,建议尺码L。\n\n身高:170-178cm, 体重:130-150斤,建议尺码XL。\n\n身高:175-182cm, 体重:145-165斤,建议尺码2XL。\n\n身高:178-185cm, 体重:160-180斤,建议尺码3XL。\n\n身高:180-190cm, 体重:180-210斤,建议尺码4XL。\n\n面料分类:其他\n\n图案:纯色\n\n领型:翻领\n\n衣门襟:单排扣\n\n颜色:黑色 卡其色 粉色 杏色\n\n袖型:收口袖\n\n适用季节:冬季\n\n袖长:长袖\n\n厚薄:厚款\n\n适用场景:其他休闲\n\n衣长:常规款\n\n版型:宽松型\n\n款式细节:假两件\n\n工艺处理:免烫处理\n\n适用对象:青年\n\n面料功能:保暖\n\n穿搭方式:外穿\n\n销售渠道类型:纯电商(只在线上销售)\n\n材质成分:棉100%', metadata={'source': '衣服属性.txt'})]
1
身高:1
'''
文档分割器
CharacterTextSplitter
作用:通过字符长度进行分割, 需要3个参数:分割符号、块大小、块重叠字符数
def demo02():
from langchain_core.documents import Document
from langchain_text_splitters import CharacterTextSplitter
# 创建分词器 separator参数指的是分割的分隔符,chunk_size指的是分割出来的每个块的大小,chunk_overlap指的是每个块之间重叠的大小
text_splitter = CharacterTextSplitter(separator=" ", chunk_size=5, chunk_overlap=1)
# 一句话分割
result1 = text_splitter.split_text("a b c d e f")
print(f'result1--->{result1}')
# 多句话分割
result2 = text_splitter.create_documents(["a b c d e f", "e f g h"])
print(f'result2--->{result2}')
# 多句话分割
result3 = text_splitter.split_documents([Document(page_content="a b c d e f", metadata={"id": "1"})])
print(f'result3--->{result3}')
RecursiveCharacterTextSplitter
作用:按照指定的分隔符顺序递归分割文本,直到满足文本块大小为止。如果分割到最后一个分隔符未能满足块大小,直接截断。
需要传入4个参数: 块大小、块重叠、长度统计方式(需要把函数作为参数传入,默认用len)、分隔符(列表格式,严格按照内部顺序执行)
def demo03():
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 4个参数:
# 1. 块大小 2.重叠字符数 3. 长度衡量方式:传入一个函数进来,调用函数判断长度
# 4. 分割符号: 按照列表中的分割符号,从左到右依次进行分割,直到满足块大小限制。 如果分割到最后一个符号未满足块大小,直接截断
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=30, # 每个块最多 100 个字符
chunk_overlap=6, # 相邻分块会共享 20 个字符
# 使用len,中文和英文没有区别;byte
length_function=len, # 用字符数来衡量长度
separators=["\n\n", "\n", " ", ""] # 会优先尝试按 \n\n 分段,如果太长,再按 \n → 空格 → 逐字符切分。
)
text = """
人工智能正在快速发展,尤其是大语言模型的应用,正在改变人类的工作方式。
它们可以帮助人们进行写作、代码生成、甚至是科研探索。
相比之下,新能源的发展同样重要。
电动车和太阳能正在逐渐替代传统能源,减少碳排放,对全球环境保护至关重要。
"""
docs = text_splitter.split_text(text)
print(docs)
分割过程示意图:
SemanticChunker
作用: 把文本基于分隔符分割成多个段落,每个段落进行向量化,计算相似度,再基于分割点阈值进行切分 。 一共需要5个参数:嵌入模型、分割点阈值类型、分割点阈值(控制分割后的文本个数)、分隔符(正则表达式)、分割后每个片段的最小字符数。
优点:切分的效果比较好,尤其是在对语义要求比较高的场景
缺点:速度慢,成本高(要调用大模型)
def demo04():
from langchain_experimental.text_splitter import SemanticChunker
from langchain_ollama import OllamaEmbeddings
embed = OllamaEmbeddings(model="mxbai-embed-large")
text_splitter = SemanticChunker(
embeddings=embed,
breakpoint_threshold_type='percentile',
# 更低的百分位会更“积极”地切分(数值越小 => 切得越多)
breakpoint_threshold_amount=40.0,
# 句子拆分正则为同时识别中/英文终结符
sentence_split_regex=r'(?<=[。!?.!?])\s*',
# 每个切分后片段(chunk)至少要有多少个字符
min_chunk_size=10
)
text = """
人工智能正在快速发展,尤其是大语言模型的应用,正在改变人类的工作方式。
它们可以帮助人们进行写作、代码生成、甚至是科研探索。
相比之下,新能源的发展同样重要。
电动车和太阳能正在逐渐替代传统能源,减少碳排放,对全球环境保护至关重要。
"""
docs = text_splitter.split_text(text)
for i, d in enumerate(docs):
print(f"------ Chunk {i + 1} ------")
print(d.strip())
print()
VectorStores
作用:Langchain集成市面上常见的向量库api , 用于和向量库进行交互。 支持chroma、elasticsearch、milvus、faiss、pinecone等常见向量库。
def demo01():
from langchain_text_splitters import CharacterTextSplitter
from langchain_chroma import Chroma
from langchain_community.document_loaders import TextLoader
from langchain_ollama import OllamaEmbeddings
# 1.加载文档
# 创建 TextLoader 对象
loader = TextLoader('./data/pku.txt', encoding='utf-8')
# 加载文档
docs = loader.load()
# print(f'docs-->{docs}')
# 2.将文档进行分块
# 创建 CharacterTextSplitter 对象
text_splitter = CharacterTextSplitter(separator="\n\n", chunk_size=200, chunk_overlap=30)
# 分块
split_docs = text_splitter.split_documents(docs)
print(f'split_docs-->{split_docs}')
# 3.将分割后的文档存储到向量数据库中
# 加载embedding模型
embedding = OllamaEmbeddings(model="mxbai-embed-large")
# 创建向量数据库,需要指定 存储的文档和向量模型名称以及持久化目录
chromadaDB = Chroma.from_documents(documents=split_docs,
embedding=embedding,
persist_directory='./chroma_db')
# 假如你的向量数据库已经存在,那么可以直接加载
# chromadaDB = Chroma(persist_directory='./chroma_db', embedding_function=embedding)
# 4.使用向量数据库进行查询
query = "1937年北京大学发生了什么?"
result = chromadaDB.similarity_search(query, k=2)
print(f'result-->{result}')
if __name__ == '__main__':
demo01()
常见的向量存储引擎:

检索器
作用:用于检索文档库的数据,将用户的query和文档库中的文档进行相似度匹配。
def get_relevant_texts_demo():
loader = TextLoader('./pku.txt')
documents = loader.load()
text_splitter = CharacterTextSplitter(chunk_size=100, chunk_overlap=0)
texts = text_splitter.split_documents(documents)
embeddings = OllamaEmbeddings(model="mxbai-embed-large")
db = FAISS.from_documents(texts, embeddings)
retriever = db.as_retriever(search_kwargs={'k': 1})
docs = retriever.get_relevant_documents("北京大学什么时候成立的")
print(docs)
Agents
作用:根据用户的需求,来访问一些第三方工具(比如:搜索引擎或者数据库),进而来解决相关需求问题。
回忆:React的实现
.jpg)
循环执行思考 -> 行动 -> 观察,指导得到最终答案。(或触发结束循环的条件,比如次数)
"""
你是一个使用ReAct范式的智能代理,必须按以下格式输出:
Thought: <你的思考>
Action: <要执行的动作,从 [{', '.join(TOOLS.keys())}] 中选择,或 Final Answer>
Action Input: <动作输入>
当前上下文:
{context}
问题:{question}
"""
Langchain的Agent的内部架构:

agent的实现:
def demo02():
from langchain_core.messages import HumanMessage
from langgraph.checkpoint.memory import MemorySaver
from langchain.agents import create_agent
from langchain_community.agent_toolkits.load_tools import load_tools
from langchain_ollama import OllamaLLM
import time
# 初始化大模型,这里使用的是 Qwen 的聊天模型
import os
# 实例化模型
# TODO: 这里使用的模型必须支持function call,模型需要具备思考使用什么模型的能力
# {
# "tool_calls": [
# {
# "function": {
# "name": "get_weather",
# "arguments": "{\"location\": \"北京\", \"unit\": \"celsius\"}"
# },
# "id": "call_abc123",
# "type": "function"
# }
# ]
# }
model = ChatOllama(model="qwen2.5:7b")
#
# model = ChatOpenAI(
# base_url=os.environ.get('base_url', 'https://dashscope.aliyuncs.com/compatible-mode/v1'),
# model='qwen-plus',
# openai_api_key=os.environ.get('DASHSCOPE_API_KEY', 'sk-XXXXXXXX'),
# max_tokens=1000,
# temperature=0
# )
# Create the agent
memory = MemorySaver()
tools = load_tools(['wikipedia', 'llm-math'], llm=model)
agent_executor = create_agent(model, tools, checkpointer=memory)
# Use the agent
config = {"configurable": {"thread_id": "abc123"}}
# 1. 发送:计算一下300的25%是多少? 问题给到qwen-plus
for chunk in agent_executor.stream(
{"messages": [HumanMessage(content="计算一下300的25%是多少?")]}, config
):
print(chunk)
print("----")
# for chunk in agent_executor.stream(
# {"messages": [HumanMessage(content="大熊猫是哪个国家的国宝?")]}, config
# ):
# print(chunk)
# print("----")
案例代码三次调用大模型:
第一次:
{
"model": "qwen2.5:7b",
"stream": true,
"options": {},
"messages": [
{
"role": "user",
"content": "计算一下300的25%是多少?"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "wikipedia",
"description": "A wrapper around Wikipedia. Useful for when you need to answer general questions about people, places, companies, facts, historical events, or other subjects. Input should be a search query.",
"parameters": {
"type": "object",
"required": [
"query"
],
"properties": {
"query": {
"type": "string",
"description": "query to look up on wikipedia"
}
}
}
}
},
{
"type": "function",
"function": {
"name": "Calculator",
"description": "Useful for when you need to answer questions about math.",
"parameters": {
"type": "object",
"required": [
"__arg1"
],
"properties": {
"__arg1": {
"type": "string"
}
}
}
}
}
]
}
返回值:
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:13.74034Z","message":{"role":"assistant","content":"","tool_calls":[{"id":"call_jq4bhh1h","function":{"index":0,"name":"Calculator","arguments":{"__arg1":"300*25%"}}}]},"done":false}
第二次调用:
{
"model": "qwen2.5:7b",
"stream": true,
"options": {
"stop": [
"```output"
]
},
"messages": [
{
"role": "user",
"content": "Translate a math problem into a expression that can be executed using Python's numexpr library. Use the output of running this code to answer the question.\n\nQuestion: ${Question with math problem.}\n```text\n${single line mathematical expression that solves the problem}\n```\n...numexpr.evaluate(text)...\n```output\n${Output of running the code}\n```\nAnswer: ${Answer}\n\nBegin.\n\nQuestion: What is 37593 * 67?\n```text\n37593 * 67\n```\n...numexpr.evaluate(\"37593 * 67\")...\n```output\n2518731\n```\nAnswer: 2518731\n\nQuestion: 37593^(1/5)\n```text\n37593**(1/5)\n```\n...numexpr.evaluate(\"37593**(1/5)\")...\n```output\n8.222831614237718\n```\nAnswer: 8.222831614237718\n\nQuestion: 300*25%\n"
}
],
"tools": []
}
-----------
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.449323Z","message":{"role":"assistant","content":"```text"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.46917Z","message":{"role":"assistant","content":"\n"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.489589Z","message":{"role":"assistant","content":"3"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.509714Z","message":{"role":"assistant","content":"0"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.530932Z","message":{"role":"assistant","content":"0"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.553936Z","message":{"role":"assistant","content":" *"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.573914Z","message":{"role":"assistant","content":" "},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.594292Z","message":{"role":"assistant","content":"0"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.614514Z","message":{"role":"assistant","content":"."},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.634429Z","message":{"role":"assistant","content":"2"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.654519Z","message":{"role":"assistant","content":"5"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.674535Z","message":{"role":"assistant","content":"\n"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.717898Z","message":{"role":"assistant","content":"```\n"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.738706Z","message":{"role":"assistant","content":"..."},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.761338Z","message":{"role":"assistant","content":"num"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.781529Z","message":{"role":"assistant","content":"expr"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.803925Z","message":{"role":"assistant","content":".evaluate"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.826211Z","message":{"role":"assistant","content":"(\""},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.846939Z","message":{"role":"assistant","content":"3"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.868359Z","message":{"role":"assistant","content":"0"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.892578Z","message":{"role":"assistant","content":"0"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.914759Z","message":{"role":"assistant","content":" *"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.937478Z","message":{"role":"assistant","content":" "},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.959496Z","message":{"role":"assistant","content":"0"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:14.981581Z","message":{"role":"assistant","content":"."},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:15.001706Z","message":{"role":"assistant","content":"2"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:15.024193Z","message":{"role":"assistant","content":"5"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:15.044315Z","message":{"role":"assistant","content":"\")"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:15.066256Z","message":{"role":"assistant","content":"...\n"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:15.108846Z","message":{"role":"assistant","content":""},"done":true,"done_reason":"stop","total_duration":1312374083,"load_duration":66089916,"prompt_eval_count":277,"prompt_eval_duration":563196209,"eval_count":33,"eval_duration":668055375}
第三次调用:
{
"model": "qwen2.5:7b",
"stream": true,
"options": {},
"messages": [
{
"role": "user",
"content": "计算一下300的25%是多少?"
},
{
"role": "assistant",
"tool_calls": [
{
"function": {
"name": "Calculator",
"arguments": {
"__arg1": "300*25%"
}
}
}
]
},
{
"role": "tool",
"content": "Answer: 75.0"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "wikipedia",
"description": "A wrapper around Wikipedia. Useful for when you need to answer general questions about people, places, companies, facts, historical events, or other subjects. Input should be a search query.",
"parameters": {
"type": "object",
"required": [
"query"
],
"properties": {
"query": {
"type": "string",
"description": "query to look up on wikipedia"
}
}
}
}
},
{
"type": "function",
"function": {
"name": "Calculator",
"description": "Useful for when you need to answer questions about math.",
"parameters": {
"type": "object",
"required": [
"__arg1"
],
"properties": {
"__arg1": {
"type": "string"
}
}
}
}
}
]
}
----------------------
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:15.832853Z","message":{"role":"assistant","content":"3"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:15.853891Z","message":{"role":"assistant","content":"0"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:15.874864Z","message":{"role":"assistant","content":"0"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:15.899847Z","message":{"role":"assistant","content":"的"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:15.922601Z","message":{"role":"assistant","content":"2"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:15.942874Z","message":{"role":"assistant","content":"5"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:15.965128Z","message":{"role":"assistant","content":"%"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:15.98762Z","message":{"role":"assistant","content":"是"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:16.008236Z","message":{"role":"assistant","content":"7"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:16.031402Z","message":{"role":"assistant","content":"5"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:16.053328Z","message":{"role":"assistant","content":"。"},"done":false}
{"model":"qwen2.5:7b","created_at":"2026-03-06T10:12:16.074034Z","message":{"role":"assistant","content":""},"done":true,"done_reason":"stop","total_duration":925874792,"load_duration":89372459,"prompt_eval_count":292,"prompt_eval_duration":591315500,"eval_count":12,"eval_duration":235936335}
使用场景
- 个人助手
- 基于文档的问答系统
- 聊天机器人
- Tabular数据查询
- API交互
- 信息提取
- 文档总结
命名实体语料
- 📎 pku.txt1.7KB
- 📎 衣服属性.txt819B
ℹ️ 离线版不含附件原文件,需要请到在线网站对应课时下载。
第5节 · RAG 开发(三):文档处理与向量检索
EduRAG 笔记(day05)
EduRAG项目
【了解】今日内容介绍
- 向量数据库
- 什么是向量数据库
- 为什么需要向量数据库
- 有哪些向量数据库
- Milvus数据库
- 概述
- 名词
- 操作
【理解】向量数据库
什么是向量数据库
向量数据库是一种专门用于存储、索引和检索高维向量数据的数据库系统。它的核心能力是支持相似性搜索(比如找最相似的向量),而不是传统数据库中的精确匹配查询。
为什么需要向量数据库
在传统数据库中,我们通常查询“年龄=25”或“姓名=张三”这样精确匹配的数据。但在 AI 和机器学习应用中,数据往往以向量形式表示,例如:
- 文本(通过 Embedding 模型转为向量)
- 图片(通过 CNN 转为向量)
- 音频、视频等
这时常见的需求是:找到与某个给定向量最相似的 N 个向量(相似度计算)。传统数据库(如 MySQL、PostgreSQL)不适合做这种大规模、高维度的相似性搜索,因为全量比对的计算复杂度太高。所以才需要向量数据库。
核心功能
| 功能 | 说明 |
|---|---|
| 向量存储 | 存储固定维度的高维浮点数数组 |
| 相似性搜索 | 支持余弦相似度、欧氏距离、内积等距离度量 |
| 近似最近邻(ANN)搜索 | 通过索引技术(如 HNSW、IVF、PQ)加速查询,牺牲少量精度换取速度 |
| 混合搜索 | 结合标量过滤(如“只搜索 2024 年的文档”)和向量搜索 |
| CRUD 操作 | 增删改查、元数据管理、持久化 |
常见向量数据库对比
| 名称 | 特点 | 适用场景 |
|---|---|---|
| **Milvus ** | 功能最丰富,分布式,支持 GPU 加速 | 生产级大规模检索 |
| Faiss | 高效的相似性搜索算法库 | 轻量级 |
| Pinecone | 全托管云服务,简单易用 | 不想运维的团队 |
| Chroma | 轻量级,嵌入式,与 LangChain 集成友好 | 本地开发、小规模应用 |
典型应用场景
- 检索增强生成(RAG):将文档切成片段并向量化存入向量数据库,LLM 回答问题时先检索相关片段再生成答案。
- 推荐系统:用户向量与商品向量做相似性匹配。
- 图像/视频检索:以图搜图、以视频片段检索原视频。
- 语义搜索:将查询转为向量,匹配语义相似的文档。
- 异常检测:寻找与正常行为向量差异大的点。
【理解】Milvus数据库
概述
Milvus 是一款开源的向量数据库(2019年提出),其唯一目标是存储、索引和管理由深度神经网络和其他机器学习(ML)模型生成的大规模嵌入向量。
作为一个专门设计用于处理输入向量查询的数据库,它能够处理万亿级别的向量索引。与现有的关系型数据库主要处理遵循预定义模式的结构化数据不同,Milvus 从底层设计用于处理从非结构化数据转换而来的嵌入向量。
随着互联网的发展和演变,非结构化数据变得越来越常见,包括电子邮件、论文、物联网传感器数据、Facebook 照片、蛋白质结构等等。为了使计算机能够理解和处理非结构化数据,使用嵌入技术将它们转换为向量。Milvus 存储和索引这些向量。Milvus 能够通过计算它们的相似距离来分析两个向量之间的相关性。如果两个嵌入向量非常相似,则意味着原始数据源也很相似。
为什么选择Milvus
- 在处理大规模数据集的向量搜索时具有高性能。
- 开发者优先的社区,提供多语言支持和工具链。
- 云扩展性和高可靠性,即使出现故障也不会受到影响。
- 通过将标量过滤与向量相似度搜索配对,实现混合搜索。
名词
非结构化数据
非结构化数据包括图像、视频、音频和自然语言等信息,这些信息不遵循预定义的模型或组织方式。这种数据类型占据了世界数据的约 80%,可以使用各种人工智能(AI)和机器学习(ML)模型将其转换为向量。
嵌入向量
嵌入向量是对非结构化数据(如电子邮件、物联网传感器数据、Instagram 照片、蛋白质结构等)的特征抽象。数学上,嵌入向量是一个浮点数或二进制数的数组。现代的嵌入技术被用于将非结构化数据转换为嵌入向量。
向量相似度搜索
向量相似度搜索是将向量与数据库进行比较,以找到与查询向量最相似的向量的过程。使用近似最近邻搜索算法加速搜索过程。如果两个嵌入向量非常相似,那么原始数据源也是相似的。
Collection和Field
与传统数据库引擎类似,您也可以在 Milvus 中创建数据库,并为某些用户分配权限来管理它们。
在关系数据库中,表和字段的结构可以与Milvus中的Collection和Field进行对应:
| Milvus | 关系数据库 | 描述 |
|---|---|---|
| Collection | 表 | 集合相当于关系数据库中的表,用于组织数据 |
| Field | 字段 | 字段Schema相当于表中的列 |
| is_primary | 主键 | 在Field Schema中标记为主键对应该列的主键 |
| dtype | 数据类型 | 字段的数据类型,如INT, VARCHAR等 |
| max_length | 最大长度 | 对应VARCHAR类型字段的最大字符数 |
| dim | - | 向量字段的维度没有直接对应,但可以视为特殊数据处理 |
索引和度量
索引是数据的组织单位。在搜索或查询插入的实体之前,必须声明索引类型和相似度度量。如果您未指定索引类型,则 Milvus 将默认使用暴力搜索。
索引类型
大多数由 Milvus 支持的向量索引类型使用近似最近邻搜索(ANNS),包括:
- FLAT:FLAT最适合在小型、百万级数据集上寻求完全准确和精确搜索结果的场景。
这是最简单的索引方式,进行暴力搜索(brute-force),可以保证精确度,但效率低,尤其在数据量大时。适合场景:在小型、百万级数据集上寻求完全精确的搜索结果。
- IVF_FLAT: 是一种基于倒排的索引方法,广泛用于在大规模数据集上实现高效的近似最近邻搜索。它适用于在精度和查询速度之间寻求平衡的场景。
1. 聚类 :IVF_FLAT通过聚类算法(如k-means)将高维空间中的向量划分为多个子空间(簇)。每个簇包含一组相似的向量,并且每个簇会有一个代表向量,通常是簇的中心点。
2. 倒排索引 :为每个簇创建倒排索引。每个向量会被映射到它所属的簇,这样在查询时,系统只需关注与查询向量相似的簇,而不需要搜索整个高维空间,从而显著降低搜索的时间复杂度。
3. 查询处理 :
-
查询时,IVF_FLAT首先将查询向量分配到距离最近的簇中心(即子空间)。
-
然后在该簇内执行精确的线性搜索,从而查找与查询向量相似的向量。
-
为了优化查询,IVF_FLAT使用一个参数
nprobe来控制搜索的簇数。nprobe控制搜索时考虑的簇的数量,从而平衡查询精度和查询速度。 -
增大
nprobe可以搜索更多簇,返回更多候选向量,提高结果的精确度,但查询时间也会增加。减少nprobe可以缩小搜索范围,降低计算时间,查询速度更快,但可能会牺牲一些精度。 -
IVF_SQ8:是在 IVF_FLAT 基础上增加了量化步骤的一种索引方法,其核心思想与 IVF_FLAT 类似;IVF_SQ8通过 标量量化 (Scalar Quantization)将每个维度的 4 字节浮点数表示压缩为 1 字节整数表示。
- IVF_PQ:是一种高效的向量索引方式,结合了倒排文件索引和乘积量化(Product Quantization)技术,旨在加速大规模高维数据集的检索。
- 倒排文件索引:IVF_PQ首先将数据集划分为多个簇,每个簇由一个聚类中心表示。查询时,系统首先计算查询向量与这些聚类中心的距离,选择最接近的几个簇进行详细搜索,从而减少计算量。
- 乘积量化:在每个簇内,向量被进一步量化为多个子向量,这些子向量通过独立的量化过程进行编码。这样可以显著降低存储需求,并加快相似度计算。
-
存储与速度:IVF_PQ通过减少存储空间的占用,同时保持较高的查询速度和准确性,适用于处理大规模高维向量数据。
-
HNSW:HNSW是基于图的索引,最适合对搜索效率有高要求的场景。
相似度计算
在 Milvus 中,相似度度量用于衡量向量之间的相似性。选择一个好的距离度量方法可以显著提高分类和聚类的性能。根据输入数据的形式,选择特定的相似度度量方法可以获得最优的性能。
对于浮点嵌入,通常使用以下指标:
- 欧几里得距离(L2)
- 内积(IP)
- 余弦相似度 (COSINE)
Milvus操作
database操作
数据库的操作,基本上就是新增、查看、删除。
新增和查询的代码:
# 1 数据库的操作
def dm01():
# 创建名称为milvus_demo的数据库
databases = client.list_databases()
print("databases-->", databases)
if "milvus_demo" not in databases:
client.create_database(db_name="milvus_demo")
else:
client.using_database(db_name="milvus_demo")
# 创建已经存在的数据库会报错
# client.create_database(db_name="milvus_demo")
databases = client.list_databases()
print("databases-->", databases)
删除代码:
# 9.删除数据库
def dm09():
client.using_database("milvus_demo")
client.drop_database("milvus_demo")
Collection操作
增加
新增Collection的操作(含新增index)。
# 3.Entity实体数据操作-插入数据
def dm03():
client.using_database("milvus_demo")
# todo:1. 创建集合collection
# 这种方式: collection 只包括两个字段. id 作为主键, vector 作为向量字段,以及自动设置 auto_id、enable_dynamic_field 为 True
# auto_id 启用此设置可确保主键自动递增。在数据插入期间无需手动提供主键。
# enable_dynamic_field 启用后,要插入的数据中除 id 和 vector 之外的所有字段都将被视为动态字段。
# # 这些附加字段作为键值对保存在名为 $meta 的特殊字段中。此功能允许在数据插入期间包含额外的字段。
client.create_collection(collection_name='demo_v2', dimension=5, metric_type='IP')
# todo:2. 插入数据(也叫实体)
data = [
{"id": 0, "vector": [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354,
0.9029438446296592], "color": "pink_8682"},
{"id": 1, "vector": [0.19886812562848388, 0.06023560599112088, 0.6976963061752597, 0.2614474506242501,
0.838729485096104], "color": "red_7025"},
{"id": 2, "vector": [0.43742130801983836, -0.5597502546264526, 0.6457887650909682, 0.7894058910881185,
0.20785793220625592], "color": "orange_6781"},
{"id": 3, "vector": [0.3172005263489739, 0.9719044792798428, -0.36981146090600725, -0.4860894583077995,
0.95791889146345], "color": "pink_9298"},
{"id": 4, "vector": [0.4452349528804562, -0.8757026943054742, 0.8220779437047674, 0.46406290649483184,
0.30337481143159106], "color": "red_4794"},
{"id": 5, "vector": [0.985825131989184, -0.8144651566660419, 0.6299267002202009, 0.1206906911183383,
-0.1446277761879955], "color": "yellow_4222"},
{"id": 6, "vector": [0.8371977790571115, -0.015764369584852833, -0.31062937026679327, -0.562666951622192,
-0.8984947637863987], "color": "red_9392"},
{"id": 7, "vector": [-0.33445148015177995, -0.2567135004164067, 0.8987539745369246, 0.9402995886420709,
0.5378064918413052], "color": "grey_8510"},
{"id": 8, "vector": [0.39524717779832685, 0.4000257286739164, -0.5890507376891594, -0.8650502298996872,
-0.6140360785406336], "color": "white_9381"},
{"id": 9, "vector": [0.5718280481994695, 0.24070317428066512, -0.3737913482606834, -0.06726932177492717,
-0.6980531615588608], "color": "purple_4976"}
]
res = client.insert(collection_name='demo_v2', data=data)
print("res-->", res)
修改
# 5.Entity实体数据操作-更新数据
def dm05():
client.using_database("milvus_demo")
## todo:4. 更新插入数据
# 在 Milvus 中,upsert 操作执行数据级操作,根据集合中是否已存在主键来插入或更新实体。具体来说:
# 如果集合中已存在该实体的主键,则现有实体将被覆盖。
# 如果集合中不存在主键,则将插入一个新实体。
data = [
{"id": 0, "vector": [-0.619954382375778, 0.4479436794798608, -0.17493894838751745, -0.4248030059917294,
-0.8648452746018911], "color": "black_9898"},
{"id": 1, "vector": [0.4762662251462588, -0.6942502138717026, -0.4490002642657902, -0.628696575798281,
0.9660395877041965], "color": "red_7319"},
{"id": 2, "vector": [-0.8864122635045097, 0.9260170474445351, 0.801326976181461, 0.6383943392381306,
0.7563037341572827], "color": "white_6465"},
{"id": 3, "vector": [0.14594326235891586, -0.3775407299900644, -0.3765479013078812, 0.20612075380355122,
0.4902678929632145], "color": "orange_7580"},
{"id": 4, "vector": [0.4548498669607359, -0.887610217681605, 0.5655081329910452, 0.19220509387904117,
0.016513983433433577], "color": "red_3314"},
{"id": 5, "vector": [0.11755001847051827, -0.7295149788999611, 0.2608115847524266, -0.1719167007897875,
0.7417611743754855], "color": "black_9955"},
{"id": 6, "vector": [0.9363032158314308, 0.030699901477745373, 0.8365910312319647, 0.7823840208444011,
0.2625222076909237], "color": "yellow_2461"},
{"id": 7, "vector": [0.0754823906014721, -0.6390658668265143, 0.5610517334334937, -0.8986261118798251,
0.9372056764266794], "color": "white_5015"},
{"id": 8, "vector": [-0.3038434006935904, 0.1279149203380523, 0.503958664270957, -0.2622661156746988,
0.7407627307791929], "color": "purple_6414"},
{"id": 9, "vector": [-0.7125086947677588, -0.8050968321012257, -0.32608864121785786, 0.3255654958645424,
0.26227968923834233], "color": "brown_7231"}
]
res = client.upsert(collection_name='demo_v2', data=data)
print('res-->', res)
查询
# 7.数据查询
def dm07():
client.using_database("milvus_demo")
# todo: 1. 单向量搜索
res = client.search(collection_name='demo_v2',
data=[[0.19886812562848388, 0.06023560599112088, 0.6976963061752597, 0.2614474506242501,
0.838729485096104]],
limit=2,
search_params={"metric_type": "IP"},
output_fields=["id", 'vector']) # search_params是在查询时执行距离计算方式,如果定义索引的时候,已经制定了方式可以不写
print("res1-->", res)
print("\nres[0]-->", res[0])
print("\nres[0][0]-->", res[0][0])
删除
# 9.删除集合和数据库
def dm09():
client.using_database("milvus_demo")
# client.drop_collection(collection_name="demo_v3")
client.drop_database("milvus_demo")
第6节 · RAG 开发(四):混合检索与排序策略
EduRAG 笔记(day06)
EduRAG项目
【了解】今日内容介绍
- 日志格式设定
- BM25算法
- Redis
- MySQL问答
【理解】日志格式设定
概述
日志(Logging)是程序运行时记录关键信息的一种方式,例如操作成功、错误发生或调试信息。它在开发和维护中非常重要,因为:
-
调试:帮助找到代码中的问题。
-
监控:记录程序的运行状态。
- 审计:追踪用户或系统的行为。
Python的logging模块是一个内置工具,提供灵活的日志记录功能,比简单的print语句更强大。
核心概念
- 日志级别:表示日志的重要性,常见级别从低到高:
-DEBUG:调试信息(最低)。
-INFO:一般信息。
-WARNING:警告,可能有问题。
-ERROR:错误,已影响程序。
-CRITICAL:严重错误(最高)。 - 日志处理器(Handler):决定日志输出到哪里(如控制台或文件)。
- 日志格式(Formatter):定义日志的显示样式(如时间、级别、消息)。
使用
基础日志纪录
import logging
# 配置基本的日志设置
logging.basicConfig(level=logging.INFO)
# 获取日志记录器
logger = logging.getLogger("Example1")
# 记录不同级别的日志
logger.debug("这是调试信息,通常用于开发")
logger.info("程序运行正常")
logger.warning("注意,可能有小问题")
logger.error("发生错误")
logger.critical("严重错误,程序可能崩溃")
自定义日志格式
import logging
# 配置日志格式
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s'
)
# 获取日志记录器
logger = logging.getLogger("Example2")
# 记录日志
logger.debug("调试模式已开启")
logger.info("正在处理数据")
logger.error("数据处理失败")
将日志存储到文件
import logging
# 配置日志,输出到文件
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
filename='app.log', # 日志文件路径
filemode='a' # 'a'表示追加,'w'表示覆盖
)
# 获取日志记录器
logger = logging.getLogger("Example3")
# 记录日志
logger.info("程序启动")
logger.warning("内存使用率较高")
logger.error("无法连接数据库")
同时输出控制台和文件
import logging
# 创建日志记录器
logger = logging.getLogger("Example4")
logger.setLevel(logging.DEBUG) # 设置记录器级别
# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO) # 控制台显示INFO及以上级别
# 创建文件处理器
file_handler = logging.FileHandler('app.log', mode='a')
file_handler.setLevel(logging.DEBUG) # 文件记录DEBUG及以上级别
# 定义日志格式
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
# 为处理器设置格式
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)
# 将处理器添加到记录器
logger.addHandler(console_handler)
logger.addHandler(file_handler)
# 记录日志
logger.debug("调试信息,仅写入文件")
logger.info("程序运行正常")
logger.error("发生错误")
【理解】BM25算法
概述
BM25(Best Matching 25)是一种信息检索领域的排名算法,用于计算查询(Query)与文档(Document)之间的相关性得分。它改进了传统的TF-IDF算法,引入文档长度归一化和词频饱和机制,使检索结果更准确。
代码实现
from retrieval.bm25_search import BM25Search
import logging
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def main():
# 示例文档集合
documents = ["我喜欢编程", "编程很有趣"]
# 初始化BM25检索器
bm25_search = BM25Search(documents)
# 示例查询
query = "他喜欢编程"
# 执行检索
result, score = bm25_search.search(query)
if result:
logger.info(f"查询结果: {result}, 得分: {score}")
else:
logger.info("未找到匹配结果")
if __name__ == "__main__":
main()
【掌握】Redis
概述
Redis(Remote Dictionary Server)是一个高性能的键值对数据库,常用于缓存、会话管理等场景。它支持多种数据结构(如字符串、哈希、列表等),并提供快速的内存操作。
特性
- 高性能:数据存储在内存中,读写速度极快。
- 持久化:支持 RDB 和 AOF 两种持久化方式。
- 灵活性:支持多种数据类型和丰富命令。
- 简单易用:提供直观的 API,易于集成。
应用场景
- 缓存查询结果以减少数据库压力。
- 存储用户会话信息。
- 实现排行榜或计数器功能。
代码实现
from redis_client import RedisClient
import logging
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def main():
# 初始化 Redis 客户端
redis_client = RedisClient()
# 示例数据
key = "user:1"
value = {"name": "Alice", "age": 25}
# 存储数据
redis_client.set_data(key, value)
# 获取数据
result = redis_client.get_data(key)
if result:
logger.info(f"查询结果: {result}")
else:
logger.info("未找到数据")
# 示例查询缓存
query = "test_query"
answer = redis_client.get_answer(query)
if answer:
logger.info(f"缓存答案: {answer}")
else:
logger.info("未找到缓存答案")
if __name__ == "__main__":
main()
【掌握】MySQL问答
概述
本系统从MySQL数据库检索问答对,使用BM25算法计算相似度,并通过Softmax归一化将得分转换为概率值,阈值0.85判断答案可靠性。Redis仅缓存高可靠性结果(相似度>0.85且有答案)。若MySQL无可靠答案,则调用RAG系统检索。
流程
数据存储:MySQL存储FQA高频问答对数据。
问题检索:BM25计算相似度,Softmax归一化后判断阈值0.85。
缓存管理:Redis仅存储相似度>0.85且有答案的数据。
答案返回:
- 若MySQL返回可靠答案,直接返回。
- 否则,调用RAG系统检索。
代码实现
略。
【扩展】
format日志格式大全
| 占位符 | 含义 |
|---|---|
%(name)s |
当前日志记录器的名称(比如你用 logger = logging.getLogger("myLogger")) |
%(filename)s |
当前运行的脚本文件名(不包含路径) |
%(pathname)s |
当前运行脚本的完整路径 |
%(funcName)s |
打印日志的函数名 |
%(lineno)d |
打印日志所在的代码行号 |
%(module)s |
脚本模块名(去掉 .py 后缀的文件名) |
%(process)d |
当前进程的ID |
%(thread)d |
当前线程的ID |
TF-IDF和BM25的区别和联系
背景场景
假设我们有以下三份文档(Corpus):
Doc1: 我 喜欢 吃 苹果
Doc2: 我 今天 吃 了 一个 苹果
Doc3: 苹果 是 一种 水果 啊
现在你搜索的关键词是:
Query: 吃 苹果
我们要用 TF-IDF 和 BM25 分别计算每个文档与查询的相关性打分。
TF-IDF 原理+示例
- TF(Term Frequency):某词在文档中出现得越多,该词对该文档越重要。
- IDF(Inverse Document Frequency):某词在所有文档中越少见,区分度越高,重要性越高。
公示如下:
$$
TF\text{-}IDF(t, d) = TF(t, d) \times \log\left(\frac{N}{DF(t)} + 1\right)
$$
其中:
- t:词语
- d:文档
- N:文档总数
- TF(t,d):词项 t 在文档 d 中出现的频率(Term Frequency)
- DF(t):包含词 t的文档数
示例:计算“吃 苹果”在每个文档中的得分
第一步:统计词频(TF)
| 词语 | Doc1 | Doc2 | Doc3 |
|---|---|---|---|
| 吃 | 1 | 1 | 0 |
| 苹果 | 1 | 1 | 1 |
第二步:计算 DF 和 IDF
| 词语 | DF(包含它的文档数) | IDF |
|---|---|---|
| 吃 | 2 | log(3/2 + 1) ≈ 0.405 |
| 苹果 | 3 | log(3/3 + 1) = log(2) ≈ 0.693 |
第三步:TF-IDF 得分(简单相加)
| 文档 | 吃的TF-IDF | 苹果的TF-IDF | 总分 |
|---|---|---|---|
| Doc1 | 1 × 0.405 | 1 × 0.693 | 1.098 |
| Doc2 | 1 × 0.405 | 1 × 0.693 | 1.098 |
| Doc3 | 0 × 0.405 | 1 × 0.693 | 0.693 |
BM25 原理+示例
比 TF-IDF 更复杂,考虑了文档长度、词频饱和、查询词的重要性。用于计算查询(Query)与文档(Document)之间的相关性得分。
公式如下:
$$
BM25(D, Q) = \sum_{i=1}^{n} \frac{(k_1 + 1) \cdot f_i}{f_i + k_1 \cdot \left(1 - b + b \cdot \frac{|D|}{avgdl}\right)} \cdot \log\left(\frac{N - n_i + 0.5}{n_i + 0.5}\right)
$$
默认参数:
- D:文档
-
Q:查询
-
fi 是词项 qi在文档 D中的出现次数(TF),
- N是文档总数,
- ni是包含词项 qi的文档数,
- ∣D∣是文档 D的长度,
- avgdl 是所有文档的平均长度,
- k1 和 b是调整参数,通常设置为 k1=1.2和 b=0.75 。
示例参数
- Doc1 长度 = 4
- Doc2 长度 = 6
- Doc3 长度 = 5
- 平均长度 avgdl = (4+6+5)/3 = 5
用 BM25 公式代入计算(略去繁琐步骤,给出简化结果):
| 文档 | BM25 得分(吃 + 苹果) |
|---|---|
| Doc1 | ≈ 1.45 |
| Doc2 | ≈ 1.52 ✅(最高相关) |
| Doc3 | ≈ 0.68 |
由于 Doc2 更长、包含更多上下文信息,它在 BM25 中被认为更相关。
总结对比
| 特性 | TF-IDF | BM25(改进版) |
|---|---|---|
| 是否考虑文档长度 | ❌ 不考虑 | ✅ 考虑(更公平) |
| 词频处理 | 线性累加 | 非线性饱和处理(防止刷词) |
| 效果 | 中等,快速 | 更精确,现代搜索引擎常用 |
| 是否使用参数 | 无 | 有 k1,b参数可调节 |
MySQL
数据库有很多种,向量数据库、缓存数据库、关系型数据库、图数据库、宽列数据库等。
向量数据库:Milvus
缓存数据库:Redis(了解即可)
关系型数据库:MySQL(Oracle、SQLServer、PostgreSQL,用的非常广泛)
这里,重点说说关系型数据库。
先明确一点:既然是个数据库,就一定是增删改查的操作。
因此,学习MySQL,就是学习MySQL的增删改查操作。
这些操作,可以是纯SQL,也可以是代码(Java,Python等)。
SQL:
#0.前提
MySQL和Milvus差不多,都是有数据库(database),也有数据表(table)
新建库:
create database 库名
新建表:
create table 表名(字段)
# create table t1(
# id int primary key auto_increment,
# name varchar(20)
# )
#1.增加
insert into 表名 values (列)
insert into t1 values (null, 'yangguo');
#2.修改
update 表名 set 字段名=字段值.... where 条件
update t1 set name='yangkang' where id=6;
#3.删除
delete from 表名 where 条件;
delete from t1 where id=6;
#4.查询
select * from 表名;
select 列名1,列名2 ... from 表名;
select * from t1;
Python:
略。
专题:RRF 的理解
RRF的理解
倒数排序融合(Reciprocal Rank Fusion,简称 RRF)是一种融合多个独立排名列表的方法,目的是合并多个检索器(搜索系统)返回的结果,使最终结果能较好地反映所有检索器的优势,提高整体相关性和鲁棒性。
RRF 的核心思想和公式
RRF 利用排名位置的倒数加权累加来给每个文档打分
关键点:
- 排名越靠前,倒数越大,贡献分数越高。
- k 加上排名,有平滑作用,避免前几名分数差距过大。
- 出现在多个排名列表且位置靠前的文档,累积得分更高,最终排名优先。
通俗举例说明
假设有2个检索器,它们分别给出以下两个排名结果列表(排名从1开始):
| 文档ID | 检索器1排名 | 检索器2排名 |
|---|---|---|
| D1 | 1 | 4 |
| D2 | 2 | 1 |
| D3 | 3 | 6 |
| D4 | — (无排名) | 2 |
| D5 | 4 | — (无排名) |
取 k=60
| 文档ID | 检索器1得分 | 检索器2得分 | RRF总分 |
|---|---|---|---|
| D1 | 1/(60+1+1)= 0.01639 | 1/(60+4+1)= 0.01563 | 0.03202 |
| D2 | 1/(60+2+1)= 0.01613 | 1(60+1+1)= 0.01639 | 0.03252 |
| D3 | 1/(60+3+1)= 0.01587 | 1(60+6+1)= 0.01538 | 0.03125 |
| D4 | 0 (无排名,因此得分0) | 1(60+2+1)= 0.01613 | 0.01613 |
| D5 | 1/(60+4+1)= 0.01563 | 0 | 0.01563 |
最终根据 RRF 总分排序,得到的融合排名是:
D2(0.03252)>D1(0.03202)>D3(0.03125)>D4(0.01613)>D5(0.01563)
公式运算步骤总结
- 对每个检索器,第 i 个结果中排名为 ranki(d) 的文档 d,计算分数 1/(k+ranki(d)+1)。
- 若文档未出现在某检索器结果中,则贡献分数为0。
- 将所有检索器对应的分数相加,得到文档 d 的总 RRF 分数。
- 根据总分高低对所有文档重新排序。
专题:TF-IDF 和 BM25 的区别和联系
TF-IDF和BM25的区别和联系
🌟背景场景:
假设我们有以下三份文档(Corpus):
Doc1: 我 喜欢 吃 苹果
Doc2: 我 今天 吃 了 一个 苹果
Doc3: 苹果 是 一种 水果 啊
现在你搜索的关键词是:
Query: 吃 苹果
我们要用 TF-IDF 和 BM25 分别计算每个文档与查询的相关性打分。
1️⃣ TF-IDF 原理 + 示例
TF-IDF 核心思想:
- TF(Term Frequency):某词在文档中出现得越多,该词对该文档越重要。
- IDF(Inverse Document Frequency):某词在所有文档中越少见,区分度越高,重要性越高。
公式:
$$
TF-IDF(t, d) = TF * IDF = \frac{词t在文档d中的出现次数}{文档d的总词数} * log(\frac{1 + N}{1 + df(t)}) + 1
$$
其中:
- t:词语
- d:文档
- N:文档总数(3)
- DF(t):包含词 t的文档数
示例:计算“吃 苹果”在每个文档中的得分
➤ 第一步:统计词频(TF)
| 词语 | Doc1 | Doc2 | Doc3 |
|---|---|---|---|
| 吃 | 1 | 1 | 0 |
| 苹果 | 1 | 1 | 1 |
➤ 第二步:计算 DF 和 IDF
| 词语 | DF(包含它的文档数) | IDF |
|---|---|---|
| 吃 | 2 | log(3/2 + 1) ≈ 0.405 |
| 苹果 | 3 | log(3/3 + 1) = log(2) ≈ 0.693 |
➤ 第三步:TF-IDF 得分(简单相加)
| 文档 | 吃的TF-IDF | 苹果的TF-IDF | 总分 |
|---|---|---|---|
| Doc1 | 1 × 0.405 | 1 × 0.693 | 1.098 |
| Doc2 | 1 × 0.405 | 1 × 0.693 | 1.098 |
| Doc3 | 0 × 0.405 | 1 × 0.693 | 0.693 |
2️⃣ BM25 原理 + 示例
BM25 核心思想:
比 TF-IDF 更复杂,考虑了文档长度、词频饱和、查询词的重要性。用于计算查询(Query)与文档(Document)之间的相关性得分
公式
$$
BM25(D, Q) = \sum_{i=1}^{n} \frac{(k_1 + 1) \cdot f_i}{f_i + k_1 \cdot \left(1 - b + b \cdot \frac{|D|}{avgdl}\right)} \cdot \log\left(\frac{N - n_i + 0.5}{n_i + 0.5}\right)
$$
默认参数:
- DD:文档
-
QQ:查询(包含词项 q1,…,qnq1,…,qn)
-
fi 是词项 qi在文档 D中的出现次数(TF),
- N是文档总数,
- ni是包含词项 qi的文档数,
- ∣D∣是文档 D的长度,
- avgdl 是所有文档的平均长度,
- k1 和 b是调整参数,通常设置为 k1=1.2和 b=0.75 。
➤ 示例参数:
- Doc1 长度 = 4
- Doc2 长度 = 6
- Doc3 长度 = 5
- 平均长度 avgdl = (4+6+5)/3 = 5
用 BM25 公式代入计算(略去繁琐步骤,给出简化结果):
| 文档 | BM25 得分(吃 + 苹果) |
|---|---|
| Doc1 | ≈ 1.45 |
| Doc2 | ≈ 1.52 ✅(最高相关) |
| Doc3 | ≈ 0.68 |
由于 Doc2 更长、包含更多上下文信息,它在 BM25 中被认为更相关。
✅总结对比:
| 特性 | TF-IDF | BM25(改进版) |
|---|---|---|
| 是否考虑文档长度 | ❌ 不考虑 | ✅ 考虑(更公平) |
| 词频处理 | 线性累加 | 非线性饱和处理(防止刷词) |
| 效果 | 中等,快速 | 更精确,现代搜索引擎常用 |
| 是否使用参数 | 无 | 有 k1,b参数可调节 |
专题:加权排名策略
加权排名策略(WeightedRanker)
加权排名策略(WeightedRanker)是一种融合多个向量检索结果评分(score)的方法,核心是根据不同向量字段的重要性,为各路径搜索结果的得分分配权重,再将这些加权得分融合,得出最终排序。
WeightedRanker 原理和步骤
-
收集搜索得分
从不同的向量检索路径分别收集返回的结果及其得分(例如 score_1、score_2)。 -
得分归一化
不同路径的得分范围通常不同(如内积 IP 可能是 [-∞,+∞],欧氏距离 L2 是 [0,+∞]),直接比较不合适。
通过对分数使用如函数等数学变换,将各得分统一映射到 区间,方便统一秩序比较。 -
分配权重
根据不同向量字段的重要性,为每一路径的归一化得分分配权重 wi*,权重值在0-1之间。
例如图像特征权重 0.6,文本特征权重 0.4。 -
融合得分
按权重加权计算得分:
\(score^{final}\)=\(∑w^{i} *score^{i}\)
最后根据加权融合的得分从高到低排序,得到最终搜索结果的优先级。
举个具体例子(多模态搜索)
假设有两路检索:
| 文档ID | 图像检索得分 | 文本检索得分 |
|---|---|---|
| 101 | 0.92 | 0.87 |
| 203 | 0.88 | N/A |
| 150 | 0.85 | N/A |
| 198 | 0.83 | 0.91 |
| 175 | 0.80 | 0.82 |
| 110 | N/A | 0.85 |
| 250 | N/A | 0.78 |
假设权重为图像 0.6,文本 0.4,则加权得分计算为:
| 文档ID | 最终得分 = 图像得分×0.6 + 文本得分×0.4 |
|---|---|
| 101 | 0.92×0.6 + 0.87×0.4 = 0.552 + 0.348 = 0.90 |
| 203 | 0.88×0.6 + 0 = 0.528 |
| 150 | 0.85×0.6 + 0 = 0.51 |
| 198 | 0.83×0.6 + 0.91×0.4 = 0.498 + 0.364 = 0.86 |
| 175 | 0.80×0.6 + 0.82×0.4 = 0.48 + 0.328 = 0.81 |
| 110 | 0 + 0.85×0.4 = 0.34 |
| 250 | 0 + 0.78×0.4 = 0.312 |
最终排序就是:
101 (0.90) > 198 (0.86) > 175 (0.81) > 203 (0.528) > 150 (0.51) > 110 (0.34)> 250(0.312)
关键点总结
- WeightedRanker 适合多模态或多特征联合搜索,能体现不同特征的重要性和贡献度。
- 归一化步骤很重要,确保不同度量标准的得分能统一比较。
- 权重由用户设定,灵活调整各检测路径的优先级。
- 最终按加权得分降序排序,得到综合考虑所有搜索路径结果的最优排序。
第7节 · RAG 开发(五):重排与 Query 改写
下午笔记
EduRAG项目
【了解】今日内容介绍
- 文档读取和切分
- bge-m3和reranker模型
- argparse工具
【理解】文档读取与切分
概述
document_processor.py是EduRAG系统的核心模块之一,用于文档解析。主要负责加载多种格式的文档(如.txt、.pdf等),并对其进行分层切分,生成父块和子块,为后续的向量存储和检索做好准备。
OCR工具
# 质谱AI
GLM,OCR
# 百度
paddle OCR
# DeepSeek公司
deepseek OCR
# Google
Gemini OCR
#textin
付费进行图片识别的工具,类似于OCR,但是比OCR好,很好用。
【理解】bge-m3和reranker模型
BGEM3EmbeddingFunction 是 Milvus 向量数据库生态 中的一个类,用于将文本转换为多模态嵌入向量,它封装了 BAAI(智源研究院)发布的 BGE-M3 模型 的推理能力。
它来自库:milvus-model(原 milvus-sdk 的扩展),用于与 Milvus / Zilliz Cloud 等向量数据库无缝集成。
安装方式
pip install milvus-model
基本使用示例
bge-m3
def dm01():
# BGE-M3 模型本身就是为混合检索设计的,而这个函数则是它在 Milvus 生态中的最佳实践接口。将它和你之前了解的 RRFRanker # 或 WeightedRanker 结合,就能搭建一个开箱即用且效果优秀的混合搜索系统。
# BAAI 官方甚至推荐使用 BGE-M3 进行混合检索 + 重排模型 作为 RAG 的标准流程
from milvus_model.hybrid import BGEM3EmbeddingFunction
ef = BGEM3EmbeddingFunction()
texts = ["什么是人工智能?", "iPhone 价格是多少?"]
embeddings = ef(texts)
print(embeddings.keys()) # 输出:dict_keys(['dense', 'sparse'])
返回三种向量:
dense: 稠密向量(用于语义相似度)sparse: 稀疏向量(用于关键词匹配)多向量:项目中没有用
rerank
def dm02():
# WeightedRanker 和 RRFRanker 是 Milvus 进行混合搜索后,用于合并和重排多路召回结果的两种核心策略。它们的核心区别在 # 于:前者让你主动控制不同模态(如图片、文本)的重要性权重,后者则自动平衡各结果,让排序过程更"民主
# WeightedRanker 加权分数融合
from pymilvus import WeightedRanker
rerank= WeightedRanker(0.8, 0.3)
# RRFRanker 倒数排名融合
from pymilvus import RRFRanker
ranker = RRFRanker(100)
混合检索,也叫双路召回。指的是得到粗排的内容。
#1.稀疏向量
速度快,测试精确召回,差准。
#2.稠密向量
语义层面召回,查全。
混合检索(双路召回)得到粗排的内容。
使用rerank模型进行重排序,得到父文档topK上下文。
【了解】argparse工具
argparse 是 Python 标准库中用于解析命令行参数和选项的模块,使用它可以让脚本更灵活、更易于使用。下面是一个简单的使用说明,帮助你快速上手 argparse。
基本结构
import argparse
# 1. 创建解析器
parser = argparse.ArgumentParser(description='简要描述这个脚本的功能')
# 2. 添加参数
parser.add_argument('input', help='输入文件路径') # 位置参数
parser.add_argument('--output', help='输出文件路径') # 可选参数
parser.add_argument('--verbose', action='store_true', help='是否输出详细信息')
# 3. 解析参数
args = parser.parse_args()
# 4. 使用参数
print(f"输入文件: {args.input}")
if args.output:
print(f"输出文件: {args.output}")
if args.verbose:
print("详细模式已开启")
常见参数类型
位置参数(必须提供)
parser.add_argument('filename', help='输入文件名')
使用方式:python script.py myfile.txt
可选参数(以 -- 开头)
parser.add_argument('--name', help='用户姓名')
使用方式:python script.py --name Alice
带默认值的参数
parser.add_argument('--count', default=1, type=int, help='重复次数(默认1)')
布尔开关(使用 action='store_true')
parser.add_argument('--verbose', action='store_true', help='启用详细输出')
- 如果命令行中包含
--verbose,args.verbose为True,否则为False。
限制取值范围(choices)
parser.add_argument('--mode', choices=['fast', 'slow'], default='fast', help='运行模式')
专题:BGE-Reranker
BGE-Reranker
作为检索增强生成(RAG)流程中的关键组件,重排序模型负责对初步检索的大量候选文档进行精确排序,从而提高下游任务的质量。BGE-Reranker系列凭借其卓越的多语言处理能力和优异的性能表现,已成为当前最受关注的重排序模型之一。
官网:https://github.com/FlagOpen/FlagEmbedding
1、BGE-Reranker系列概述
1.1 开发背景与团队
BGE-Reranker系列模型由北京智源研究院(Beijing Academy of Artificial Intelligence,BAAI)开发,是继其成功推出BGE(BAAI General Embedding)嵌入模型之后的又一力作。该系列模型专注于解决多语言环境下的文档重排序问题,特别是针对中英文混合场景进行了优化。
BGE-Reranker的开发团队由来自北京智源研究院的NLP研究人员组成,他们致力于推动开源语言模型技术的发展。该项目得到了学术界和产业界的广泛关注和支持。
1.2 版本演进
BGE-Reranker系列模型经历了几个主要版本的演进:
-
BGE-Reranker-v1: 初代版本,初步实现了重排序功能,但性能和语言覆盖范围有限。
-
==采用交叉编码器(cross-encoder)架构,能够直接计算查询和文档之间的相关性得分==
-
基于大规模预训练语言模型,具有强大的语义理解能力
-
支持中英双语处理
-
相比嵌入模型,能够获得更高的排序精度
-
专为重排序任务优化,在各项基准测试中表现优异
-
-
BGE-Reranker-v2: 第二代模型,在v1基础上进行了全面升级,包括:
-
采用更强大的多语言预训练模型作为骨干网络
-
引入更高效的训练策略和数据增强技术
-
提供多种尺寸的模型版本,满足不同场景需求
-
-
BGE-Reranker-v2-M3: 当前最新版本,以"M3"命名表示其专注于多语言(Multilingual)、中等规模(Medium-sized)、现代化(Modern)的特点。该版本在模型架构、训练数据、优化策略等方面均有显著改进。
1.3 核心定位与应用场景
BGE-Reranker-v2-M3是一款轻量级多语言交叉编码器(Cross-Encoder),专为以下场景设计:
- 搜索引擎结果重排序:提高搜索结果的相关性和质量
- RAG(检索增强生成)系统:作为处理检索结果的关键组件
- 推荐系统:优化候选项排序,提升推荐准确性
- 问答系统:精确筛选可能包含答案的文档片段
该模型特别适合资源有限但需要高质量多语言处理能力的场景,在与初步检索(如向量检索)结合使用时效果最佳。
2、bge-reranker-large使用
- 加载模型
from sentence_transformers import CrossEncoder
# 加载模型
reranker = CrossEncoder('./bge-reranker-large', device='cuda:0')
# 输入格式必须是二维列表:[[query, document]]
score = reranker.predict([["人工智能的定义是什么?", "人工智能是使机器具有智能行为的技术"]])
print("相关性得分:", score)
输出结果:相关性得分: [0.9984816]
得分范围一般为 0~1,越接近 1 表示越相关。
- 批量重排序(Batch Rerank)
queries = ["什么是机器学习?"]
documents = [
"机器学习是一种让计算机从数据中自动学习规律的方法。",
"深度学习是机器学习的一个分支。",
"今天的天气很好。"
]
pairs = [[queries[0], doc] for doc in documents]
scores = reranker.predict(pairs)
print(f'scores-->{scores}')
# 输出排序结果
for doc, score in sorted(zip(documents, scores), key=lambda x: x[1], reverse=True):
print(f"{score:.4f} - {doc}")
输出结果:
scores-->[9.9982554e-01 3.1844237e-01 7.6328361e-05]
0.9998 - 机器学习是一种让计算机从数据中自动学习规律的方法。
0.3184 - 深度学习是机器学习的一个分支。
0.0001 - 今天的天气很好。
3、bge-reranker-large和bge-m3对比
| 模型名称 | 模型类型 | 主要用途 | 输入输出 | 特点 |
|---|---|---|---|---|
| bge-reranker-large | Cross-Encoder(重排序模型) | 重排序 / 精排:判断一对文本(query, passage)的相关性 | 输入一对句子 → 输出一个相关性得分(如 0.87) | 精度高,但速度慢;适合在召回后做 rerank |
| bge-m3 | Dual-Encoder + Sparse Hybrid(多模态混合向量模型) | 语义向量表示 / 向量召回 | 输入文本 → 输出 dense 向量(可选 sparse 向量) | 速度快,可用于 Milvus / Faiss 检索;支持稀疏+稠密混合检索 |
晚上笔记
EduRAG项目
【了解】今日内容介绍
- BERT意图识别
- 策略选择
- RAG问答
- RAG评估和优化
- RAG简历编写
【理解】意图识别
Prompt设计
prompts.py定义了RAGPrompts类,负责管理系统中使用的所有Prompt模板。这些模板用于指导大语言模型完成不同任务,例如生成最终答案、假设答案、子查询或简化问题。通过集中管理Prompt,系统能够确保输入的一致性和输出质量。
意图识别
QueryClassifier 是 EduRAG 系统的核心组件,负责将用户查询分为“通用知识”和“专业咨询”两类,以决定查询路由到知识库还是咨询接口。本模块介绍基于 BERT 的优化实现,替换传统 TF-IDF 模型,利用 5000 条混合数据集(training_dataset_hybrid_5000.json)进行训练,并解决评估中的标签处理问题。
【理解】策略选择
概述
strategy_selector.py定义了StrategySelector类,通过调用大语言模型根据用户查询选择最合适的检索策略。支持的策略包括直接检索、HyDE(假设问题检索)、子查询检索和回溯问题检索,旨在优化检索阶段的输入处理。
四种策略
直接检索(Direct Retrieval)
定义
对用户原始查询不作改写或增强,直接基于原始问题进行向量检索或全文检索。
适用场景
- 用户意图明确
- 查询结构简单
- 目标信息在知识库中直接可匹配
示例
- 查询: “AI 学科学费是多少?”
- 策略: 直接检索
- 查询: “JAVA的课程大纲是什么?”
- 策略: 直接检索
技术特征
- 不依赖 LLM 改写
- 检索效率高
- 召回范围小但精准
假设问题检索(HyDE: Hypothetical Document Embeddings)
定义
与其直接用问题去生成嵌入向量,不如让大模型先根据问题“编造”一个假设答案(hypothetical answer/document),然后再用这个答案去生成向量。
适用场景
- 查询偏抽象或开放式
- 原始问题很难匹配知识片段
- 无直接答案的探索性问题
示例
- 查询: “人工智能在教育领域的应用有哪些?”
- 策略: 假设问题检索
技术特征
- LLM 生成假设性上下文
- 提高语义覆盖面
- 更适合观点性或场景化问题
提示词内容
输入query进行转换
假设你是用户,想了解以下问题,请生成一个简短的假设答案:
问题: {query}
假设答案:
举例说明
- 用户提问:“如何改善睡眠?”
- 传统做法:把这个问题向量化去检索。
- HyDE 做法:先让大模型写一段“如何改善睡眠的建议”,然后把这段假设文档向量化,用它去检索相关文档。
为什么有效
- 用户的问题可能很简略或模糊,但大模型可以根据它生成一个详细、语义丰富的假设文档。
- 假设文档更像是你想找的答案,从而提高召回质量。
- 实测中 HyDE 在多种语义检索任务上比直接使用 query embeddings 效果更好,尤其是问答类检索。
子查询检索(Sub-Question Decomposition)
定义
将一个复杂查询分解为多个子问题,分别进行检索,最后融合各子问题的答案。
适用场景
- 多实体比较
- 多维度分析型问题
- 包含“对比”、“优缺点”等词语
示例
- 查询: “比较 Milvus 和 Zilliz Cloud 的优缺点。”
- 策略: 子查询检索
技术特征
- 拆解后检索精度更高
- 适合表格化或结构化合成
- 可并发处理多个子问题
提示词内容
将以下复杂查询分解为多个简单子查询,每行一个子查询:
查询: {query}
子查询:
回溯问题检索(Question Rewriting / Reframing)**
定义
将复杂或嵌套式查询转换为更基础的问题,以提高检索系统的理解能力和召回能力。
适用场景
- 包含前置知识或推理链的问题
- 提问方式过于口语或上下文依赖
- “是否可以”、“我该怎么做”类的用户意图不明确问题
示例
- 查询: “我有一个包含 100 亿条记录的数据集,想把它存储到 Milvus 中进行查询。可以吗?”
- 策略: 回溯问题检索
- 改写为: “Milvus 支持多大规模的数据集存储和检索?”
技术特征
- LLM 改写复杂问题
- 化繁为简
- 更适合进行技术类文档匹配
提示词内容
将以下复杂查询简化为一个更简单的问题:
查询: {query}
简化问题:
【掌握】RAG问答
rag_system.py定义了RAGSystem类,整合系统的各个模块,完成从查询输入到答案生成的完整流程。它通过查询分类选择处理路径,利用检索策略优化文档检索,并结合上下文生成最终答案。
从查询到回答的流程
- 输入处理:
QueryClassifier分类查询,决定是否需要检索。 - 策略选择:
StrategySelector根据查询选择最佳检索策略。 - 文档检索:根据策略调用
VectorStore的混合检索,获取相关文档。 - 答案生成:使用
RAGPrompts的模板,结合上下文调用大语言模型生成答案。 - 输出:返回最终答案,并记录日志。
【了解】RAG系统评估
评估框架
RAGAS (Retrieval Augmented Generation Assessment) 我们一般称为 Automated Evaluation of Retrieval Augmented Generation,即检索增强生成的自动评估。Ragas是一个大模型评测框架,可以评估检索增强生成(RAG)的效果,帮助分析模型的输出,了解模型在给定任务上的表现。
数据说明
最开始的 RAGAs 在评估数据集时,不必依赖人工标注的标准答案,而是通过底层的大语言模型 (LLM) 来进行评估。所以只需要一个带有问题-答案对的评估数据集(QA 对):
- question:作为 RAG 管道输入的用户查询,输入。
- answer:从 RAG 管道生成的答案,输出。
- contexts:从用于回答question外部知识源中检索的上下文。
- ground_truths:question的基本事实答案。这是唯一人工注释的信息。
评估指标
- **评估检索(context)**的指标:提供了上下文相关性(context_relevancy)和上下文召回率(context_recall),这些可以衡量你的检索系统的性能,即检索的段落是否相关。
- **评估生成(answer)**的指标:提供了忠实度(faithfulness),用以衡量生成的信息是否准确无误;以及答案相关性(answer_relevancy),用以衡量答案对问题的切题程度,即模型生成的答案是否恰当。
如下图所示:
上下文相关性
- 作用:指的是检索到的上下文应该只包含回答问题所需的信息,这个指标旨在惩罚包含冗余信息的情况。
- 比率越高,表示检索到的上下文与问题的相关性越强。
上下文召回率
- 作用:衡量检索到的上下文(contexts)与真实答案(ground_truths)的匹配程度。
- 该指标通过问题、标注答案和检索到的上下文计算,分数范围在0到1之间,得分越高表示性能更好。
- 要从真实答案中估计上下文召回率,需要分析真实答案中的每个声明(claim),以确定它是否可以归因于检索到的上下文。理想情况下,真实答案中的所有声明都应可归因于检索到的上下文。
忠实度
- 作用:指答案确实是根据给定的上下文得到的。这对于避免错觉并确保检索到的上下文可以用作生成答案的依据非常重要。
- 如果分数低,它表明 LLM 的回应没有遵循检索到的知识,提供幻觉式答案的可能性增加。
答案相关性
- 作用:生成的答案与查询之间的相关性。分数越高,相关性越好。
【理解】RAG系统优化
FQA和知识库查询
old_main.py 是一个融合 MySQL FAQ 和 RAG 系统的交互式问答程序,专注于快速、精准的答案生成。它通过 MySQL 提供结构化数据的高效查询,通过 RAG 系统处理复杂问题,同时配备日志记录和用户友好的命令行界面,适合教育场景的实时问答需求。
增强型FQA和知识库查询
new_main.py 是对 old_main.py 的优化版本,新增了对话历史管理和流式输出功能,增强了交互性和实时性。系统整合了 MySQL 的 FAQ 精确匹配、RAG 的知识库检索、MySQL 对话历史存储,以及流式答案生成,适用于教育场景下的实时问答需求。
基于FastAPI的知识库接口
app.py 是对 new_main.py 的进一步优化,基于 FastAPI 框架构建了 RESTful API 和 WebSocket 接口,集成了 MySQL FAQ 和 RAG 系统,支持流式输出、对话历史管理和日常问候处理。同时,通过静态文件服务支持前端页面交互,适用于教育场景下的实时问答系统。
【实践】RAG项目简历编写
项目名称:基于RAG的教育领域智能问答系统(EduRAG)
项目时间:2025.04 – 2025.010
技术栈:Python + Milvus + BERT + Sentence-BERT + Qwen2.5-7B + LoRA + RagAS + OCR + FastAPI
项目背景
针对教培行业高频、重复性咨询问题(如课程安排、退费政策、学习规划等),传统FAQ匹配准确率低、知识更新滞后,难以满足用户实时精准答疑需求。为此设计并实现了一套融合结构化数据检索与RAG技术的智能问答系统——EduRAG,显著提升回答准确率与用户体验。
主要职责
- 构建RAG知识库,整理并清洗PDF/Word/PPT等文档资料,累计构建 50万+条数据;
- 文档预处理:实现文本加载、去噪清洗、智能分块(按语义边界)、多模态内容处理
- 搭建 Milvus 向量数据库,设计多集合(3个collections)存储策略,支持课程、政策、服务等不同类型知识的隔离管理与高效检索;
- 基于 BERT 意图识别模块,支持4种意图分类如:(课程咨询、退费政策、学习规划、通用问题),F1-score达 90%;
- 完整RAG流程:根据意图路由至对应检索路径,结合检索结果拼接Prompt,基于 开源Qwen2.5-7B模型生成自然语言回答,支持上下文感知与格式化输出。
项目优化与技术创新
- 多模态文档处理增强
针对PDF/PPT中图片、表格等内容,引入 OCR工具(PaddleOCR)与多模态大模型提取图文信息,提升知识覆盖完整性。 - Query理解与重构优化
设计多策略query改写机制:
- 关键词扩展(同义词/领域词增强)
- 子查询分解(复杂问题拆解)
- 历史会话融合(上下文感知重写)
- 伪答案生成(HyDE策略)
- 通用去噪(去除口语化表达)
显著提升模糊查询的召回率。 - 文档索引与检索优化
- 构建 多级索引机制(支持父块/子块切分策略);
- 实现 稀疏向量 与 稠密向量 混合检索,通过加权融合提升召回准确率;
- 引入 (Rerank)模块,对Top-k结果进行精细化排序,Top-5相关性提升35%。 - 模型微调与生成优化
- 对 Qwen3-7B模型进行LoRA微调,适配教育领域术语与回答风格,显著提升回答专业性与流畅度;
- 设计结构化Prompt模板,控制生成格式,减少幻觉与冗余。 - 系统评估体系建设
- 使用 RagAS 自动评估框架对RAG全流程进行量化评测,关键指标:- 答案相关性(Answer Relevance)≥ 0.89
- 忠实性(Faithfulness)≥ 0.9
- 构建 1000条人工标注测试集,整体准确率(Accuracy)达 90%,具备良好泛化能力。
项目成果
- 系统上线后自动应答率超 80%,人工客服压力降低约50%;
- 回答准确率与用户满意度显著提升,获内部创新项目认可;
- 支持知识库动态更新,具备高可扩展性与私有化部署能力。
专题:RAG 中的 Query 改写
RAG中的Query改写
学习目标
- 理解query改写的意义
- 掌握qeury改写的实现方法
1 前言
在RAG(检索增强生成)流程中,第一步通常是对用户的提问(query)进行改写。这是因为用户提问的方式与他们期望的答案之间可能存在差距。由于每个用户的提问方式可能千差万别,因此对问题进行改写可以帮助系统更好地理解问题并返回更相关的答案,从而提升RAG系统的鲁棒性和扩展性。
用户提出的问题通常存在两类问题:
- 信息不完整:用户的提问没有表达清楚所有的关键信息
- 噪声问题:提问中可能包含了与答案无关的内容。
2 信息不完整
==2.1 历史会话改写==
在对话中,前后文是相互关联的。如果仅凭当前的query进行检索,可能会导致召回精度大幅下降,因为query中往往缺少重要的上下文信息。以下是一个具体的例子:
用户: 华为meta70手机的性能怎么样?
系统: 华为meta70手机搭载了强大的处理器和先进的摄像系统,性能表现非常优秀。
用户: 与上一代相比,它有哪些改进?
系统: 华为meta70相较于meta60在处理器性能、摄像头优化和电池续航方面都有显著提升。
用户: 摄像头方面具体改进了什么?
--改写前: 摄像头方面具体改进了什么?
--改写后: 华为meta70手机的摄像头相比meta60有哪些具体改进?
2.2 关键词扩写
用户在搜索时常常输入的关键词较为简短,并且缺乏足够的上下文信息,这会影响语义检索(向量检索)的效果,导致召回的相关性较低。因此,需要对用户的原始关键词进行扩展和丰富。
用户输入: “机器学习 实践”
改写后的 Query: “机器学习在实际应用中的案例有哪些?哪些工具和方法适用于机器学习实践?”
2.3 伪答案改写【假设问题检索】
伪答案改写通过在原始查询中加入一种假设性答案,来增强查询的语义丰富性,从而提高检索或回应的精准度。伪答案并非真实的答案,而是一个设想的内容,用于帮助系统更好地理解并检索相关信息。
用户输入: “如何提高企业的市场竞争力?”
改写后的 Query: “如何提高企业的市场竞争力?比如通过创新产品、优化营销策略或提升客户服务等手段。”
伪答案目的--> 通过提供假设性的提升方式,丰富查询的语义信息,从而增强系统在应对复杂问题时的检索能力
2.4 缩写词改写
用户在查询时常常使用缩写,而许多相关文档通常会使用完整的术语,因此需要对缩写进行扩展,以便更好地匹配相关内容。
用户输入: “VR 技术在教育中的应用”
改写后的 Query: “虚拟现实(Virtual Reality)技术在教育中的应用有哪些?可以举一些实际的应用案例吗?”
3 噪声问题
3.1 一般去噪改写【回溯问题检索】
通过去除查询中的无关成分(如多余的修饰语、模糊表达或不相关的背景信息),简化并优化查询,使其更加精确和可操作。这种方法有助于提高检索的准确性和效率。
用户输入: “我最近在准备面试,但对于算法的理解还不太够,能推荐一些有效的学习资源吗?”
改写后的 Query: “有哪些有效的学习资源可以帮助提高算法理解?”
分析:去除与问题无关的背景信息 “我最近在准备面试,但对于算法的理解还不太够”。 直接提取核心意图 “帮助提高算法理解”。
3.2 子查询改写【子查询检索】
当查询涉及对比多个实体时,可能会产生相互干扰的情况。对比类查询通常包含多个元素,这些元素如果直接放在一个查询中,可能会导致信息重叠,影响检索的准确性。为了避免干扰,可以将对比类查询拆分成多个独立的查询,每个查询聚焦于其中一个实体,这样可以减少信息混淆,获得更准确的结果。
用户输入: “C++ 和 Go 哪个更适合做系统编程?”
拆分后的查询:“C++ 适合做系统编程的优点有哪些?”;“Go 适合做系统编程的优点有哪些?”
拆分原因: • 直接对比 C++ 和 Go 的优劣,可能使得系统无法有效地提取每种语言的特点。拆分后,系统可以分别检索 C++ 和 Go 在系统编程中的优点,避免信息混乱
4 prompt示例
以下是一个开源rag系统,问题改写的示例,仅供参考:
您是查询扩展方面的专家,能够生成问题的释义。
我无法直接使用用户的问题从知识库中检索相关信息。
您需要通过多种方式扩展或释义用户的问题,例如使用同义词/短语、完整地写出缩写、添加一些额外的描述或解释、改变表达方式、将原始问题翻译成另一种语言(英语/中文)等。
并返回 5 个版本的问题,其中一个来自翻译。
只需列出问题。不需要其他单词。
本节小结
改写后的query质量在很大程度上依赖于提示(prompt)和大模型的能力。通常,查询改写可以提高召回的准确性,但有时仍可能发生改写失败的情况。因此,建议进行多次改写,或者将查询拆分为多个独立的查询,之后将召回结果输入到重排序(reranking)模型进行精确排序,再由大模型生成最终的答案。
第8节 · 加餐:RAG 串讲回顾
RAG 回顾和加强
RAG回顾和加强
LangChain
LangChain的几个核心组件?
#1.model模型
LLM
Chat
Embedding(类似于bge-m3,text-embedding-v3)
#2.Prompt提示词模板
PromptTemplate
#3.indexes 检索
初始化加载器对象(不同的加载类)
OCR:光学字符识别。识别图片中的文本(扫描件)。
PaddleOCR
DeepSeekOCR
GeminiOCR
textIn
多模态模型
知识库加载 loader.load()/lazy_load()
知识库切割
切割策略非常多。大小都是自定义。
为什么选择父子快切割,父块大小为什么是512,子块为什么是128?
重叠大小:经验值,建议给。
向量化(bge-m3模型)
#4.Memory 记忆
短期记忆:内存(会话)
长期记忆:外存(数据库,文件等)
面试题:如果检索的上下文内容过长/超过模型的上下文,怎么办?
会话压缩(让大模型总结一下)
#5.Chain 链
把多个任务串起来执行。
|:管道符。这种语法称之为:LCEL(LangChain Expression Language)
#6.Agent智能体
在LangChain中,也支持Agent。就是React模式的智能体。
Milvus
数据库:存取数据。
所有的数据库,基本上都是有数据存(insert/update)和取(select/delete)的功能。
Milvus是一个向量数据库。也支持这些操作。
upsert=insert+update,写入。
search:检索(查询)。
delete:删除。
但是,项目中主要是使用混合检索的能力。
client.hybrid_search(2个向量):
#1.稀疏向量:关键字检索
#2.稠密向量:语义检索
在混合检索过程中,如何取召回的内容?
它得指定排序算法:
#1.WeightedRanker
加权评分(加权排名器):适合明确知道权重的情况(稀疏向量权重、稠密向量权重)
#2.RRFRanker
互易等级融合(平权排名器):适合不知道权重的情况(同等对待所有向量权重)
RAG架构
通用RAG架构
项目中的RAG架构
核心代码
mysql_qa:sql_main.py模块
其中的核心方法是:bm25_search.search
rag_qa:rag_main.py模块
其中的核心方法是:grag_system.generate_answer(),其中,在generate_answer中的核心方法是:self.retrieve_and_merge()
📎 EduRAG 项目源码(integrated_qa_system)
FastAPI + React + MySQL/Milvus/Redis 全栈工程
75 个文件📁 base/
📄 __init__.py 52B
from base.config import * from base.logger import *
📄 config.py 3.4KB
# 导入配置解析库 import configparser # 导入路径操作库 import os class Config: # 初始化配置,加载 config.ini 文件 def __init__(self, config_file=r"D:\code\workspace7\integrated_qa_system\config.ini"): # 创建配置解析器 self.config = configparser.ConfigParser() # 读取配置文件 self.config.read(config_file, encoding='utf-8') # windows包gbk错误,增加encoding=utf-8 # MySQL 配置 # MySQL 主机地址 self.MYSQL_HOST = self.config.get('mysql', 'host', fallback='localhost') # MySQL 用户名 self.MYSQL_USER = self.config.get('mysql', 'user', fallback='root') # MySQL 密码 self.MYSQL_PASSWORD = self.config.get('mysql', 'password', fallback='123456') # MySQL 数据库名 self.MYSQL_DATABASE = self.config.get('mysql', 'database', fallback='subjects_kg') # Redis 配置 # Redis 主机地址 self.REDIS_HOST = self.config.get('redis', 'host', fallback='localhost') # Redis 端口 self.REDIS_PORT = self.config.getint('redis', 'port', fallback=6379) # Redis 密码 self.REDIS_PASSWORD = self.config.get('redis', 'password', fallback='1234') # Redis 数据库编号 self.REDIS_DB = self.config.getint('redis', 'db', fallback=0) # Milvus 配置 # Milvus 主机地址 self.MILVUS_HOST = self.config.get('milvus', 'host', fallback='localhost') # Milvus 端口 self.MILVUS_PORT = self.config.get('milvus', 'port', fallback='19530') # Milvus 数据库名 self.MILVUS_DATABASE_NAME = self.config.get('milvus', 'database_name', fallback='itcast') # Milvus 集合名 self.MILVUS_COLLECTION_NAME = self.config.get('milvus', 'collection_name', fallback='edurag_final') # LLM 配置 # LLM 模型名 self.LLM_MODEL = self.config.get('llm', 'model', fallback='qwen-plus') # DashScope API 密钥 self.DASHSCOPE_API_KEY = self.config.get('llm', 'dashscope_api_key') # DashScope API 地址 self.DASHSCOPE_BASE_URL = self.config.get('llm', 'dashscope_base_url', fallback='https://dashscope.aliyuncs.com/compatible-mode/v1') # 检索参数 # 父块大小 self.PARENT_CHUNK_SIZE = self.config.getint('retrieval', 'parent_chunk_size', fallback=512) # 子块大小 self.CHILD_CHUNK_SIZE = self.config.getint('retrieval', 'child_chunk_size', fallback=128) # 块重叠大小 self.CHUNK_OVERLAP = self.config.getint('retrieval', 'chunk_overlap', fallback=50) # 检索返回数量 self.RETRIEVAL_K = self.config.getint('retrieval', 'retrieval_k', fallback=5) # 最终候选数量 self.CANDIDATE_M = self.config.getint('retrieval', 'candidate_m', fallback=2) # 应用配置 # 有效来源列表 self.VALID_SOURCES = eval( self.config.get('app', 'valid_sources', fallback='["ai", "java", "test", "ops", "bigdata"]')) # 客服电话 self.CUSTOMER_SERVICE_PHONE = self.config.get('app', 'customer_service_phone', fallback='12345678') # 日志文件路径 self.LOG_FILE = self.config.get('logger', 'log_file', fallback='logs/app.log') if __name__ == '__main__': conf = Config() print(conf.CHILD_CHUNK_SIZE)
📄 logger.py 1.8KB
# 导入日志库 import logging # 导入路径操作库 import os # 导入配置类 from base.config import Config def setup_logging(log_file=Config().LOG_FILE): # 创建日志目录 os.makedirs(os.path.dirname(log_file), exist_ok=True) # 获取日志器 logger = logging.getLogger("EduRAG") # 设置日志级别 logger.setLevel(logging.INFO) # 避免重复添加处理器 if not logger.handlers: # 创建文件处理器 file_handler = logging.FileHandler(log_file, encoding='utf-8') # 设置文件处理器级别 file_handler.setLevel(logging.INFO) # 创建控制台处理器 console_handler = logging.StreamHandler() # 设置控制台处理器级别 console_handler.setLevel(logging.INFO) # 设置日志格式 formatter = logging.Formatter( '%(asctime)s - %(pathname)s[line:%(lineno)d] - %(name)s - %(levelname)s - %(message)s') # 为文件处理器设置格式 file_handler.setFormatter(formatter) # 为控制台处理器设置格式 console_handler.setFormatter(formatter) # 添加文件处理器 logger.addHandler(file_handler) # 添加控制台处理器 logger.addHandler(console_handler) # 返回日志器 return logger # 初始化日志器 logger = setup_logging() def process_data(data): logger.debug(f"开始处理数据: {data}") if not data: logger.error("数据为空,无法处理") return None logger.info("数据处理完成") return data.upper() def main(): logger.info("程序启动") result = process_data("hello") if result: logger.info(f"处理结果: {result}") else: logger.warning("处理失败") logger.info("程序结束") if __name__ == "__main__": main()
📁 docker/
📄 docker-compose.yml 2.1KB
version: "3.5" services: etcd: container_name: milvus-etcd image: quay.io/coreos/etcd:v3.5.5 environment: - ETCD_AUTO_COMPACTION_MODE=revision - ETCD_AUTO_COMPACTION_RETENTION=1000 - ETCD_QUOTA_BACKEND_BYTES=4294967296 - ETCD_SNAPSHOT_COUNT=50000 volumes: - ./etcd_data:/etcd ports: - "2379:2379" command: - etcd - --data-dir=/etcd - --listen-client-urls=http://0.0.0.0:2379 - --advertise-client-urls=http://milvus-etcd:2379 minio: container_name: milvus-minio image: minio/minio:RELEASE.2023-01-12T02-06-16Z environment: - MINIO_ACCESS_KEY=minioadmin - MINIO_SECRET_KEY=minioadmin command: server /data --console-address ":9001" volumes: - ./minio_data:/data ports: - "9000:9000" - "9001:9001" milvus: image: milvusdb/milvus:v2.4.10 container_name: milvus_standalone depends_on: - etcd - minio ports: - "19530:19530" - "9091:9091" volumes: - ./milvus_data:/var/lib/milvus environment: - ETCD_ENDPOINTS=milvus-etcd:2379 - MINIO_ADDRESS=milvus-minio:9000 - MINIO_ACCESS_KEY=minioadmin - MINIO_SECRET_KEY=minioadmin - ETCD_AUTO_COMPACTION_RETENTION=0 - MILVUS_LOG_LEVEL=info command: ["milvus", "run", "standalone"] restart: unless-stopped redis: image: redis:7.2 container_name: redis ports: - "6379:6379" volumes: - ./redis_data:/data command: redis-server --appendonly yes restart: unless-stopped mysql: image: mysql:8.0.43 container_name: mysql8 restart: always environment: MYSQL_ROOT_PASSWORD: 123456 MYSQL_DATABASE: subjects_kg MYSQL_USER: admin MYSQL_PASSWORD: 123456 ports: - "3306:3306" command: --local-infile=1 volumes: - ./init:/docker-entrypoint-initdb.d - ./mysql_data:/var/lib/mysql volumes: milvus_data: redis_data: etcd_data: minio_data: mysql_data:
📁 mysql_qa/
📁 cache/
📄 redis_client.py 2.9KB
# 导入 Redis 客户端 import redis # 导入 JSON 处理 import json # 导入配置和日志 from base import Config, logger class RedisClient: def __init__(self): # 初始化日志 self.logger = logger try: # 连接 Redis self.client = redis.StrictRedis( host=Config().REDIS_HOST, port=Config().REDIS_PORT, password=Config().REDIS_PASSWORD, db=Config().REDIS_DB, decode_responses=True ) # 记录连接成功 self.logger.info("Redis 连接成功") except redis.RedisError as e: # 记录连接失败 self.logger.error(f"Redis 连接失败: {e}") raise def set_data(self, key, value): # 存储数据到 Redis try: # 存储 JSON 数据 self.client.set(key, json.dumps(value, ensure_ascii=False)) # 记录存储成功 self.logger.info(f"存储数据到 Redis: {key}") except redis.RedisError as e: # 记录存储失败 self.logger.error(f"Redis 存储失败: {e}") def get_data(self, key): # 从 Redis 获取数据 try: # 获取数据 data = self.client.get(key) # 返回解析后的 JSON 数据或 None return json.loads(data) if data else None except redis.RedisError as e: # 记录获取失败 self.logger.error(f"Redis 获取失败: {e}") # 返回 None return None def get_answer(self, query): # 获取查询的缓存答案 try: # 从 Redis 获取答案 answer = self.client.get(f"answer:{query}") if answer: # 记录获取成功 self.logger.info(f"从 Redis 获取答案: {query}") # 返回答案 return answer # 返回 None return None except redis.RedisError as e: # 记录查询失败 self.logger.error(f"Redis 查询失败: {e}") # 返回 None return None import logging # 配置日志 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) def main(): # 初始化 Redis 客户端 redis_client = RedisClient() # 示例数据 key = "user:1" value = {"name": "Alice", "age": 25} # 存储数据 redis_client.set_data(key, value) # 获取数据 result = redis_client.get_data(key) if result: logger.info(f"查询结果: {result}") else: logger.info("未找到数据") # 示例查询缓存 query = "test_query" answer = redis_client.get_answer(query) if answer: logger.info(f"缓存答案: {answer}") else: logger.info("未找到缓存答案") if __name__ == "__main__": main()
📁 data/
- ⚙️ JP学科知识问答.csv 133.4KB
📁 db/
📄 mysql_client.py 3.8KB
# 导入 MySQL 连接库 import pymysql # 导入pandas import pandas as pd # 导入配置和日志 from base import Config, logger class MySQLClient: def __init__(self): # 初始化日志 self.logger = logger try: # 连接 MySQL 数据库 self.connection = pymysql.connect( host=Config().MYSQL_HOST, user=Config().MYSQL_USER, password=Config().MYSQL_PASSWORD, database=Config().MYSQL_DATABASE ) # 创建游标 self.cursor = self.connection.cursor() # 记录连接成功 self.logger.info("MySQL 连接成功") except pymysql.MySQLError as e: # 记录连接失败 self.logger.error(f"MySQL 连接失败: {e}") raise def create_table(self): create_table_query = ''' CREATE TABLE IF NOT EXISTS jpkb ( id INT AUTO_INCREMENT PRIMARY KEY, subject_name VARCHAR(20), question VARCHAR(100 ), answer VARCHAR(1000) ) \ ''' try: self.cursor.execute(create_table_query) self.connection.commit() self.logger.info("表创建成功") except pymysql.MySQLError as e: self.logger.error(f"表创建失败: {e}") raise def insert_data(self, csv_path): try: data = pd.read_csv(csv_path) for _, row in data.iterrows(): insert_query = "INSERT INTO jpkb (subject_name, question, answer) VALUES (%s, %s, %s)" self.cursor.execute(insert_query, (row['学科名称'], row['问题'], row['答案'])) self.connection.commit() self.logger.info("数据插入成功") except Exception as e: self.logger.error(f"数据插入失败: {e}") self.connection.rollback() raise def fetch_questions(self): # 获取所有问题 try: # 执行查询 self.cursor.execute("SELECT question FROM jpkb") # 获取结果 results = self.cursor.fetchall() # 记录获取成功 self.logger.info("成功获取问题") # 返回结果 return results except pymysql.MySQLError as e: # 记录查询失败 self.logger.error(f"查询失败: {e}") # 返回空列表 return [] def fetch_answer(self, question): # 获取指定问题的答案 try: # 执行查询 self.cursor.execute("SELECT answer FROM jpkb WHERE question=%s", (question,)) # 获取结果 result = self.cursor.fetchone() # 返回答案或 None return result[0] if result else None except pymysql.MySQLError as e: # 记录答案获取失败 self.logger.error(f"答案获取失败: {e}") # 返回 None return None def close(self): # 关闭数据库连接 try: # 关闭连接 self.connection.close() # 记录关闭成功 self.logger.info("MySQL 连接已关闭") except pymysql.MySQLError as e: # 记录关闭失败 self.logger.error(f"关闭连接失败: {e}") if __name__ == '__main__': # todo 必须先创建库:语句如下: # create database if not exists subjects_kg charset utf8; mysql_client = MySQLClient() mysql_client.create_table() mysql_client.insert_data('../data/JP学科知识问答.csv')
📁 retrieval/
📄 bm25_search.py 4.8KB
# 导入 BM25 算法 from rank_bm25 import BM25Okapi # 导入数值计算库 import numpy as np # 导入文本预处理 from mysql_qa.utils.preprocess import preprocess_text # 导入日志 from base import logger class BM25Search: def __init__(self, redis_client, mysql_client): # 初始化日志 self.logger = logger # 初始化 Redis 客户端 self.redis_client = redis_client # 初始化 MySQL 客户端 self.mysql_client = mysql_client # 初始化 BM25 模型 self.bm25 = None # 初始化问题列表 self.questions = None # 初始化原始问题 self.original_questions = None # 加载数据 self._load_data() def _load_data(self): # 加载数据 redis key original_key = "qa_original_questions" tokenized_key = "qa_tokenized_questions" # 从 Redis 获取原始问题 self.original_questions = self.redis_client.get_data(original_key) # 从 Redis 获取分词问题 tokenized_questions = self.redis_client.get_data(tokenized_key) # 如果 Redis 中没有数据,从 MySQL 加载 if not self.original_questions or not tokenized_questions: # 从 MySQL 获取问题 self.original_questions = self.mysql_client.fetch_questions() if not self.original_questions: # 记录无问题警告 self.logger.warning("未加载到问题") return # 分词问题 tokenized_questions = [preprocess_text(q[0]) for q in self.original_questions] # 存储原始问题到 Redis self.redis_client.set_data(original_key, [(q[0]) for q in self.original_questions]) # 存储分词问题到 Redis self.redis_client.set_data(tokenized_key, tokenized_questions) # 设置问题列表 self.questions = tokenized_questions # 初始化 BM25 模型 self.bm25 = BM25Okapi(self.questions) # 记录 BM25 初始化成功 self.logger.info("BM25 模型初始化完成") def _softmax(self, scores): # 计算 Softmax 分数 exp_scores = np.exp(scores - np.max(scores)) # 保证数值稳定性:减去每行的最大值 # 返回归一化分数, BM25分数 映射到0-1 return exp_scores / exp_scores.sum() def search(self, query, threshold=0.85): # 搜索查询 if not query or not isinstance(query, str): # 记录无效查询 self.logger.error("无效查询") # 返回 None 和 False return None, False # 检查 Redis 缓存 cached_answer = self.redis_client.get_answer(query) if cached_answer: # 返回缓存答案 return cached_answer, False try: # 分词查询 query_tokens = preprocess_text(query) # 计算 BM25 分数 scores = self.bm25.get_scores(query_tokens) # 计算 Softmax 分数 softmax_scores = self._softmax(scores) # 获取最高分索引 best_idx = softmax_scores.argmax() # 获取最高分 best_score = softmax_scores[best_idx] logger.info(f"搜索最高分 [{best_score:.3f}] 结果: {self.original_questions[best_idx]}") # 检查是否超过阈值 if best_score >= threshold: # 获取原始问题 original_question = self.original_questions[best_idx] # 获取答案 answer = self.mysql_client.fetch_answer(original_question) if answer: # 缓存答案 self.redis_client.set_data(f"answer:{query}", answer) # 记录搜索成功 self.logger.info(f"搜索成功,Softmax 相似度: {best_score:.3f}") # 返回答案和 False return answer, False # 记录无可靠答案 self.logger.info(f"未找到可靠答案,最高 Softmax 相似度: {best_score:.3f}") # 返回 None 和 True return None, True except Exception as e: # 记录搜索失败 self.logger.error(f"搜索失败: {e}") # 返回 None 和 True return None, True if __name__ == '__main__': # 测试BM25Search类的init方法 from mysql_qa.cache.redis_client import RedisClient from mysql_qa.db.mysql_client import MySQLClient redis_client = RedisClient() mysql_client = MySQLClient() bm25_search = BM25Search(redis_client=redis_client, mysql_client=mysql_client) # 测试 search方法 answer, flag = bm25_search.search('Windows如何安装redis') print("answer-->", answer) print('flag-->', flag)
📁 utils/
📄 preprocess.py 626B
# 导入分词库 import jieba # 导入日志 from base import logger from typing import List def preprocess_text(text: str) -> List[str]: # 预处理文本 # logger.info("开始预处理文本") try: # 分词并转换为小写 return jieba.lcut(text.lower()) except AttributeError as e: # 记录预处理失败 logger.error(f"文本预处理失败: {e}") # 返回空列表 return [] if __name__ == '__main__': text = "这是一个测试文本,我要吃白切鸡" # text = "This ia a text file" tokens = preprocess_text(text) print(tokens)
📄 __init__.py 130B
from .db.mysql_client import MySQLClient from .cache.redis_client import RedisClient from .retrieval.bm25_search import BM25Search
📄 sql_main.py 2.4KB
# 导入 MySQL 客户端 from db.mysql_client import MySQLClient # 导入 Redis 客户端 from cache.redis_client import RedisClient # 导入 BM25 搜索 from retrieval.bm25_search import BM25Search # 导入日志 from base import logger # 导入时间库 import time class MySQLQASystem: def __init__(self): # 初始化日志 self.logger = logger # 初始化 MySQL 客户端 self.mysql_client = MySQLClient() # 初始化 Redis 客户端 self.redis_client = RedisClient() # 初始化 BM25 搜索 self.bm25_search = BM25Search(self.redis_client, self.mysql_client) def query(self, query): # 查询 MySQL 系统 start_time = time.time() # 记录查询信息 self.logger.info(f"处理查询: '{query}'") # 执行 BM25 搜索 answer, _ = self.bm25_search.search(query, threshold=0.85) if answer: # 记录 MySQL 答案 self.logger.info(f"MySQL 答案: \n{answer}") else: # 记录无答案 self.logger.info("SQL中未找到答案, 需要调用RAG系统") # 设置默认答案 answer = "SQL未找到答案" # 计算处理时间 processing_time = time.time() - start_time # 记录处理时间 self.logger.info(f"查询处理耗时 {processing_time:.2f}秒") # 返回答案 return answer def main(): # 初始化 MySQL 系统 mysql_system = MySQLQASystem() try: # 打印欢迎信息 print("\n欢迎使用 MySQL 问答系统!") print("输入查询进行问答,输入 'exit' 退出。") while True: # 获取用户输入 query = input("\n输入查询: ").strip() if query.lower() == "exit": # 记录退出日志 logger.info("退出 MySQL 系统") # 打印退出信息 print("再见!") break # 执行查询 answer = mysql_system.query(query) # 打印答案 print(f"\n答案: {answer}") except Exception as e: # 记录系统错误 logger.error(f"系统错误: {e}") # 打印错误信息 print(f"发生错误: {e}") finally: # 关闭 MySQL 连接 mysql_system.mysql_client.close() if __name__ == "__main__": # 运行主程序 main()
📁 rag_qa/
📁 classify_data/
- ⚙️ model_generic_5000.json 38.7KB
📄 提示词模块.txt 1.8KB
你是一个高效的文本分类系统,任务是判断用户的查询属于“通用知识”还是“专业咨询”。 【任务说明】 - “通用知识”类主要包含数学计算、代码生成/纠错、概念与原理问题以及其他常识性问题。 - “专业咨询”类专指与IT教育培训相关的查询,如课程详情、师资介绍、项目内容、培训周期、地点、时间和费用等。 【要求】 1. 请仔细阅读以下查询文本,判断其所属类别。 2. 根据查询文本的内容和上下文,输出对应的分类标签: - 如果内容涉及IT培训(例如JAVA、AI、测试等课程介绍、培训服务、师资力量、学习周期等),请标注为“专业咨询”; - 如果内容主要涉及计算、代码处理或基础概念问题,请标注为“通用知识”。 3. 输出格式为JSON:包含"query"(原始查询文本)和"label"(分类结果)的键值对。 【示例】 输入示例: “什么AI?” 输出示例: { "query": "什么AI?", "label": "通用知识" } 输入示例: “JAVA的课程大纲是什么?” 输出示例: { "query": "JAVA的课程大纲是什么?", "label": "专业咨询" } 【注意事项】 - 请不要输出除结果之外的多余文字; - 如果存在歧义,请优先判断是否涉及具体的IT教育培训服务,如果是,则分类为“专业咨询”; - 输出时请保持标签仅为“通用知识”或“专业咨询”。 现在,请针对以下用户查询进行分类: <这里替换为实际用户查询文本> 一共需要生成5000条训练集,注意训练集之间一定要两个类别生成的数量是均衡的,而且要多样性。我需要你边生成边检查,保证数据的质量
📁 core/
📄 __init__.py 0B
📄 document_processor.py 8.7KB
# 这个脚本讲义的代码架构图没有体现,需要进行补充 import os from langchain_community.document_loaders import TextLoader from langchain_community.document_loaders.markdown import UnstructuredMarkdownLoader # NLTK from langchain_text_splitters import MarkdownTextSplitter from datetime import datetime import sys # 获取当前文件所在目录的绝对路径 current_dir = os.path.dirname(os.path.abspath(__file__)) # print(f'current_dir--》{current_dir}') # 获取core文件所在的目录的绝对路径 rag_qa_path = os.path.dirname(current_dir) # print(f'rag_qa_path--》{rag_qa_path}') sys.path.insert(0, rag_qa_path) # 获取根目录文件所在的绝对位置 project_root = os.path.dirname(rag_qa_path) sys.path.insert(0, project_root) from rag_qa.edu_document_loaders import OCRPDFLoader, OCRDOCLoader, OCRPPTLoader, OCRIMGLoader from rag_qa.edu_text_spliter import ChineseRecursiveTextSplitter from base import logger, Config conf = Config() # 定义支持的文件类型及其对应的加载器字典 document_loaders = { # 文本文件使用 TextLoader ".txt": TextLoader, # PDF 文件使用 OCRPDFLoader ".pdf": OCRPDFLoader, # Word 文件使用 OCRDOCLoader ".docx": OCRDOCLoader, # PPT 文件使用 OCRPPTLoader ".ppt": OCRPPTLoader, # PPTX 文件使用 OCRPPTLoader ".pptx": OCRPPTLoader, # JPG 文件使用 OCRIMGLoader ".jpg": OCRIMGLoader, # PNG 文件使用 OCRIMGLoader ".png": OCRIMGLoader, # Markdown 文件使用 UnstructuredMarkdownLoader ".md": UnstructuredMarkdownLoader } # 定义函数,从指定文件夹加载多种类型文件并添加元数据 def load_documents_from_directory(directory_path): # 初始化空列表,用于存储加载的文档 documents = [] # 获取支持的文件扩展名集合 supported_extensions = document_loaders.keys() # print(f'supported_extensions--》{supported_extensions}') # 从目录名提取学科类别(如 "ai_data" -> "ai") # print(f'1---》{os.path.basename(directory_path)}') source = os.path.basename(directory_path).replace("_data", "") # print(f'source-->{source}') # 遍历指定目录及其子目录 for root, _, files in os.walk(directory_path): # print(f'root---》{root}') # print(f'files---》{files}') # 遍历当前目录下的所有文件 for file in files: # 构造文件的完整路径 file_path = os.path.join(root, file) # print(f'file_path--》{file_path}') # print(os.path.splitext(file_path)) # 获取文件扩展名并转换为小写 file_extension = os.path.splitext(file_path)[1].lower() # print(f'file_extension--》{file_extension}') # 检查文件类型是否在支持的扩展名列表中 if file_extension in supported_extensions: # 使用 try-except 捕获加载过程中的异常 try: # 根据文件扩展名获取对应的加载器类 loader_class = document_loaders[file_extension] # 实例化加载器对象,传入文件路径 if file_extension == ".txt": loader = loader_class(file_path, encoding="utf-8") else: loader = loader_class(file_path) # 调用加载器加载文档内容,返回文档列表 loaded_docs = loader.load() # print(f'loaded_docs--》{loaded_docs}') # print(f'loaded_docs--》{len(loaded_docs)}') for doc in loaded_docs: # 为文档添加学科类别元数据 doc.metadata["source"] = source # 为文档添加文件路径元数据 doc.metadata["file_path"] = file_path # 为文档添加当前时间戳元数据 doc.metadata["timestamp"] = datetime.now().isoformat() # print(f'loaded_docs111--》{loaded_docs}') documents.extend(loaded_docs) # 记录成功加载文件的日志 logger.info(f"成功加载文件: {file_path}") except Exception as e: logger.error(f"加载文件 {file_path} 失败: {str(e)}") # 如果文件类型不在支持列表中 else: # 记录警告日志,提示不支持的文件类型 logger.warning(f"不支持的文件类型: {file_path}") # 返回加载的所有文档列表 return documents # 定义函数,处理文档并进行分层切分,返回子块结果 def process_documents(directory_path, parent_chunk_size=conf.PARENT_CHUNK_SIZE, child_chunk_size=conf.CHILD_CHUNK_SIZE, chunk_overlap=conf.CHUNK_OVERLAP): # 从指定目录加载所有文档 documents = load_documents_from_directory(directory_path) # 记录加载的文档总数日志 logger.info(f"加载的文档数量: {len(documents)}") # 初始化父块和子块分词器(通用) parent_splitter = ChineseRecursiveTextSplitter(chunk_size=parent_chunk_size, chunk_overlap=chunk_overlap) child_splitter = ChineseRecursiveTextSplitter(chunk_size=child_chunk_size, chunk_overlap=chunk_overlap) # 初始化 Markdown 专用分词器 markdown_parent_splitter = MarkdownTextSplitter(chunk_size=parent_chunk_size, chunk_overlap=chunk_overlap) markdown_child_splitter = MarkdownTextSplitter(chunk_size=child_chunk_size, chunk_overlap=chunk_overlap) # 初始化空列表,用于存储所有子块 child_chunks = [] # 遍历每个原始文档,带上索引 i for i, doc in enumerate(documents): # print(f'doc--》{doc}') # print(doc.metadata.get("file_path", '')) # # 获取文件的扩展名 # print(os.path.splitext(doc.metadata.get("file_path", ''))) file_extension = os.path.splitext(doc.metadata.get("file_path", ''))[1].lower() # print(f'file_extension--》{file_extension}') # 选择分词器 is_markdown = (file_extension == '.md') # print(f'is_markdown--》{is_markdown}') parent_splitter_to_use = markdown_parent_splitter if is_markdown else parent_splitter # print(f'parent_splitter_to_use-->{parent_splitter_to_use}') child_splitter_to_use = markdown_child_splitter if is_markdown else child_splitter logger.info( f"处理文档: {doc.metadata['file_path']}, 使用切分器: {'Markdown' if is_markdown else 'ChineseRecursive'}") # 使用父块切分器将文档切分为父块 parent_docs = parent_splitter_to_use.split_documents([doc]) # print(f'parent_docs--》{parent_docs}') # print(f'parent_docs--》{len(parent_docs)}') # 遍历每个父块,带上索引 j for j, parent_doc in enumerate(parent_docs): # 为父块生成唯一 ID,格式为 "doc_i_parent_j" parent_id = f"doc_{i}_parent_{j}" # # 将父块 ID 添加到元数据 parent_doc.metadata["parent_id"] = parent_id # # 将父块内容存储到元数据 parent_doc.metadata["parent_content"] = parent_doc.page_content # 使用子块分词器将父块切分为子块 sub_chunks = child_splitter_to_use.split_documents([parent_doc]) # print(f'sub_chunks--》{sub_chunks}') # print(f'sub_chunks--》{len(sub_chunks)}') # 遍历每个子块,为子块主要添加对应的父块文档 for k, sub_chunk in enumerate(sub_chunks): # print(f'原始的sub_chunk--》{sub_chunk}') # 为子块添加父块的ID sub_chunk.metadata["parent_id"] = parent_id # 为子块添加对应的父块文档(元数据) sub_chunk.metadata["parent_content"] = parent_doc.page_content # 为子块生成一个唯一的ID,格式为 "parent_id_child_k" sub_chunk.metadata["id"] = f"{parent_id}_child_{k}" # print(f'修改后的sub_chunk--》{sub_chunk}') # 将子块添加到子块列表中 child_chunks.append(sub_chunk) # 记录子块总数日志 logger.info(f"子块数量: {len(child_chunks)}") # 返回所有子块列表 return child_chunks if __name__ == '__main__': directory_path = r'D:/code/workspace7/integrated_qa_system/rag_qa/data/ai_data' # documents = load_documents_from_directory(directory_path) # print('documents-->', documents) child_chunks = process_documents(directory_path) print("child_chunks--", child_chunks)
📄 new_rag_system.py 14.0KB
from .prompts import RAGPrompts # 导入 time 模块,用于计算时间 import time from base import logger, Config from .query_classifier import QueryClassifier # 导入查询分类器 from .strategy_selector import StrategySelector # 导入策略选择器 conf = Config() # 定义 RAGSystem 类,封装 RAG 系统的核心逻辑 class RAGSystem: # 初始化方法,设置 RAG 系统的基本参数 def __init__(self, vector_store, llm): # 设置向量数据库对象 self.vector_store = vector_store # 设置大语言模型调用函数 self.llm = llm # 获取 RAG 提示模板 self.rag_prompt = RAGPrompts.rag_prompt() # 初始化查询分类器 self.query_classifier = QueryClassifier( model_path='D:/code/workspace7/integrated_qa_system/rag_qa/models/bert_query_classifier') # 初始化策略选择器 self.strategy_selector = StrategySelector() # 最大 4096 字符 self.max_prompt_length = 4096 # 定义私有方法,使用假设文档进行检索(HyDE) def _retrieve_with_hyde(self, query): logger.info(f"使用 HyDE 策略进行检索 (查询: '{query}')") # 获取假设问题生成的 Prompt 模板 hyde_prompt_template = RAGPrompts.hyde_prompt() # 使用 template 后缀区分 # 调用大语言模型生成假设答案 try: hypo_answer = self.llm(hyde_prompt_template.format(query=query)) hypo_answer = "".join(hypo_answer) logger.info(f"HyDE 生成的假设答案: '{hypo_answer}'") # 使用假设答案进行检索,并返回检索结果 # 注意:HyDE 通常只用于生成检索向量,不一定需要 rerank 这一步,但这里复用了 return self.vector_store.hybrid_search_with_rerank( hypo_answer, k=conf.RETRIEVAL_K # 使用 K 而非 M ) except Exception as e: logger.error(f"HyDE 策略执行失败: {e}") return [] # 定义私有方法,使用子查询进行检索 def _retrieve_with_subqueries(self, query): logger.info(f"使用子查询策略进行检索 (查询: '{query}')") # 获取子查询生成的 Prompt 模板 subquery_prompt_template = RAGPrompts.subquery_prompt() # 使用 template 后缀区分 try: # 调用大语言模型生成子查询列表 subqueries_text = self.llm(subquery_prompt_template.format(query=query)) subqueries_text = "".join(subqueries_text) subqueries = [q.strip() for q in subqueries_text.split("\n") if q.strip()] logger.info(f"生成的子查询: {subqueries}") if not subqueries: logger.warning("未能生成有效的子查询") return [] # 初始化空列表,用于存储所有子查询的检索结果 all_docs = [] # 遍历每个子查询 for sub_q in subqueries: # 使用子查询进行检索,并将结果添加到列表中 # 这里对每个子查询都执行了 hybrid search + rerank,开销可能较大 docs = self.vector_store.hybrid_search_with_rerank( sub_q, k=conf.RETRIEVAL_K # 使用 K ) all_docs.extend(docs) logger.info(f"子查询 '{sub_q}' 检索到 {len(docs)} 个文档") # 对所有检索结果进行去重 (基于对象内存地址,如果 Document 内容相同但对象不同则无法去重) # 更可靠的去重方式是基于文档内容或 ID unique_docs_dict = {doc.page_content: doc for doc in all_docs} # 基于内容去重 unique_docs = list(unique_docs_dict.values()) logger.info(f"所有子查询共检索到 {len(all_docs)} 个文档, 去重后剩 {len(unique_docs)} 个") # 返回去重后的文档,限制数量 (是否需要在此处限制? retrieve_and_merge 末尾会限制) # return unique_docs[: Config.CANDIDATE_M] return unique_docs # 返回所有唯一文档,让 retrieve_and_merge 处理数量 except Exception as e: logger.error(f"子查询策略执行失败: {e}") return [] # 定义私有方法,使用回溯问题进行检索 def _retrieve_with_backtracking(self, query): logger.info(f"使用回溯问题策略进行检索 (查询: '{query}')") # 获取回溯问题生成的 Prompt 模板 backtrack_prompt_template = RAGPrompts.backtracking_prompt() # 使用 template 后缀区分 try: # 调用大语言模型生成回溯问题 simplified_query = self.llm(backtrack_prompt_template.format(query=query)) simplified_query = "".join(simplified_query) logger.info(f"生成的回溯问题: '{simplified_query}'") # 使用回溯问题进行检索,并返回检索结果 return self.vector_store.hybrid_search_with_rerank( simplified_query, k=conf.RETRIEVAL_K # 使用 K ) except Exception as e: logger.error(f"回溯问题策略执行失败: {e}") return [] # 定义方法,检索并合并相关文档 def retrieve_and_merge(self, query, source_filter=None, strategy=None): # 新增 strategy 参数 # 如果未指定检索策略,则使用策略选择器选择 if not strategy: strategy = self.strategy_selector.select_strategy(query) # 根据检索策略选择不同的检索方式 ranked_sub_chunks = [] # 初始化 if strategy == "回溯问题检索": ranked_sub_chunks = self._retrieve_with_backtracking(query) elif strategy == "子查询检索": ranked_sub_chunks = self._retrieve_with_subqueries(query) # 返回的是唯一文档列表 # 注意:子查询返回的是已 rerank 过的父文档或子块列表,后续合并逻辑可能需要调整 # 当前实现中,子查询返回的是初步检索(可能已rerank)的块,再进行合并 elif strategy == "假设问题检索": ranked_sub_chunks = self._retrieve_with_hyde(query) else: # 默认或“直接检索” logger.info(f"使用直接检索策略 (查询: '{query}')") ranked_sub_chunks = self.vector_store.hybrid_search_with_rerank( query, k=conf.RETRIEVAL_K, source_filter=source_filter ) # 注意 hybrid_search_with_rerank 返回的是 rerank 后的父文档 logger.info(f"策略 '{strategy}' 检索到 {len(ranked_sub_chunks)} 个候选文档 (可能已是父文档)") final_context_docs = ranked_sub_chunks[:conf.CANDIDATE_M] logger.info(f"最终选取 {len(final_context_docs)} 个文档作为上下文") return final_context_docs # # 定义方法,生成答案 # def generate_answer(self, query, source_filter=None): # # 记录查询开始时间 # start_time = time.time() # logger.info(f"开始处理查询: '{query}', 学科过滤: {source_filter}") # # # 判断查询类型 # query_category = self.query_classifier.predict_category(query) # logger.info(f"查询分类结果:{query_category} (查询: '{query}')") # # # 如果查询属于“通用知识”类别,则直接使用 LLM 回答 # if query_category == "通用知识": # logger.info("查询为通用知识,直接调用 LLM") # prompt_input = self.rag_prompt.format( # context="", question=query, phone=conf.CUSTOMER_SERVICE_PHONE # ) # 不使用上下文 # try: # answer = self.llm(prompt_input) # except Exception as e: # logger.error(f"直接调用 LLM 失败: {e}") # answer = f"抱歉,处理您的通用知识问题时出错。请联系人工客服:{conf.CUSTOMER_SERVICE_PHONE}" # processing_time = time.time() - start_time # logger.info( # f"通用知识查询处理完成 (耗时: {processing_time:.2f}s, 查询: '{query}')" # ) # return answer # # # 否则,进行 RAG 检索并生成答案 # logger.info("查询为专业咨询,执行 RAG 流程") # # 选择检索策略 # strategy = self.strategy_selector.select_strategy(query) # # # 检索相关文档 # context_docs = self.retrieve_and_merge( # query, source_filter=source_filter, strategy=strategy # ) # 传递 strategy # # # 准备上下文 # if context_docs: # context = "\n\n".join([doc.page_content for doc in context_docs]) # 使用换行符分隔文档 # logger.info(f"构建上下文完成,包含 {len(context_docs)} 个文档块") # # logger.debug(f"上下文内容:\n{context[:500]}...") # Debug 日志可以打印部分上下文 # else: # context = "" # logger.info("未检索到相关文档,上下文为空") # # # 构造 Prompt,调用大语言模型生成答案 # prompt_input = self.rag_prompt.format( # context=context, question=query, phone=conf.CUSTOMER_SERVICE_PHONE # ) # # logger.debug(f"最终生成的 Prompt:\n{prompt_input}") # Debug 日志 # # try: # answer = self.llm(prompt_input) # except Exception as e: # logger.error(f"调用 LLM 生成最终答案失败: {e}") # answer = f"抱歉,处理您的专业咨询问题时出错。请联系人工客服:{conf.CUSTOMER_SERVICE_PHONE}" # # # # 记录查询处理完成的日志 # processing_time = time.time() - start_time # logger.info(f"查询处理完成 (耗时: {processing_time:.2f}s, 查询: '{query}')") # return answer # 添加完历史消息后,定义方法,生成答案 # 修改 rag_system.py 中的 generate_answer 方法 def generate_answer(self, query, source_filter=None, history=None): """生成答案,支持对话历史,限制提示长度,并支持流式输出""" start_time = time.time() logger.info(f"开始处理查询: '{query}', 学科过滤: {source_filter}") # 验证历史格式 if history is not None and not isinstance(history, list): logger.warning(f"无效的历史格式: {type(history)},忽略历史") history = [] elif history: history = history[-5:] # 限制最多5轮 for h in history: if not (isinstance(h, dict) and "question" in h and "answer" in h): logger.warning(f"无效的历史条目: {h},忽略历史") history = [] break # 构造历史上下文 history_context = "" if history: history_context = "\n".join( [f"Q: {h['question']}\nA: {h['answer']}" for h in history] ) logger.info(f"使用对话历史: {history_context[:100]}...") # 判断查询类型,通用知识还是专业知识 query_category = self.query_classifier.predict_category(query) logger.info(f"查询分类结果: {query_category} (查询: '{query}')") # 准备上下文 try: if query_category == "通用知识": logger.info("查询为通用知识,直接调用 LLM") context = "" else: logger.info("查询为专业咨询,执行 RAG 流程") strategy = self.strategy_selector.select_strategy(query) # 根据用户的query选择检索策略 # 根据检索策略,返回检索上下文 context_docs = self.retrieve_and_merge( query, source_filter=source_filter, strategy=strategy ) context = "\n\n".join([doc.page_content for doc in context_docs]) if context_docs else "" logger.info(f"构建上下文完成,包含 {len(context_docs)} 个文档块") # 构造提示 prompt_input = self.rag_prompt.format( context=context, history=history_context, question=query, phone=conf.CUSTOMER_SERVICE_PHONE ) # 截断提示(若超长) if len(prompt_input) > self.max_prompt_length: logger.warning(f"提示长度 {len(prompt_input)} 超过 {self.max_prompt_length},进行截断") # 保留问题和近期历史,截断上下文或早期历史 prompt_input = prompt_input[:self.max_prompt_length] logger.info(f"截断后提示长度: {len(prompt_input)}") processing_time = time.time() - start_time logger.info(f"检索时间 (耗时: {processing_time:.2f}s, 查询: '{query}')") start_time = time.time() # 调用 LLM(流式输出) try: # 使用 generator 获取流式输出 for token in self.llm(prompt_input): yield token # 直接将每个 token 传递给调用者 # 记录处理时间 processing_time = time.time() - start_time logger.info(f"LLM查询处理完成 (耗时: {processing_time:.2f}s, 查询: '{query}')") except Exception as e: logger.error(f"调用 LLM 失败: {e}") yield f"抱歉,处理您的问题时出错。请联系人工客服:{conf.CUSTOMER_SERVICE_PHONE}" except Exception as e: logger.error(f"答案生成失败: {e}") yield f"抱歉,处理您的问题时出错。请联系人工客服:{conf.CUSTOMER_SERVICE_PHONE}" # query 意图识别 使用bert 分类 # 改进: # 1. 用LLM做意图识别,考虑最近几轮历史问答 # 2. 最近几轮的query 拼接([CLS]current_query[SEP] history_query[SEP] ) 使用多轮对话数据训练Bert
📄 prompts.py 3.8KB
# 导入 PromptTemplate 类,用于创建 Prompt 模板 # 0.x版本导包 # from langchain.prompts import PromptTemplate # 1.x版本导包 from langchain_core.prompts import PromptTemplate # 定义 RAGPrompts 类,用于管理所有 Prompt 模板 class RAGPrompts: # 定义 RAG 提示模板 # @staticmethod # def rag_prompt(): # # 创建并返回 PromptTemplate 对象 # return PromptTemplate( # template=""" # 你是一个智能助手,帮助用户回答问题。 # 如果提供了上下文,请基于上下文回答;如果没有上下文,请直接根据你的知识回答。 # 如果答案来源于检索到的文档,请在回答中说明。 # # 上下文: {context} # 问题: {question} # # 如果无法回答,请回复:“信息不足,无法回答,请联系人工客服,电话:{phone}。” # 回答: # """, # # 定义输入变量 # input_variables=["context", "question", "phone"], # ) # 优化后再添加:如果添加了历史消息 @staticmethod def rag_prompt(): return PromptTemplate( template=""" 你是一个智能助手,负责帮助用户回答问题。请按照以下步骤处理: 1. **分析问题和上下文**: - 基于提供的上下文(如果有)和你的知识回答问题。 - 如果答案来源于检索到的文档,请在回答中明确说明,例如:“根据提供的文档,……”。 2. **评估对话历史**: - 检查对话历史是否与当前问题相关(例如,是否涉及相同的话题、实体或问题背景)。 - 如果对话历史与问题相关,请结合历史信息生成更准确的回答。 - 如果对话历史无关(例如,仅包含问候或不相关的内容),忽略历史,仅基于上下文和问题回答。 3. **生成回答**: - 提供清晰、准确的回答,避免无关信息。 - 如果上下文和历史消息均不足以回答问题,请回复:“信息不足,无法回答,请联系人工客服,电话:{phone}。” **上下文**: {context} **对话历史**: {history} **问题**: {question} **回答**: """, input_variables=["context", "history", "question", "phone"], ) # 定义假设问题生成的 Prompt 模板 @staticmethod def hyde_prompt(): # 创建并返回 PromptTemplate 对象 return PromptTemplate( template=""" 假设你是用户,想了解以下问题,请生成一个简短的假设答案: 问题: {query} 假设答案: """, # 定义输入变量 input_variables=["query"], ) # 定义子查询生成的 Prompt 模板 @staticmethod def subquery_prompt(): # 创建并返回 PromptTemplate 对象 return PromptTemplate( template=""" 将以下复杂查询分解为多个简单子查询,每行一个子查询: 查询: {query} 子查询: """, # 定义输入变量 input_variables=["query"], ) # 定义回溯问题生成的 Prompt 模板 @staticmethod def backtracking_prompt(): # 创建并返回 PromptTemplate 对象 return PromptTemplate( template=""" 将以下复杂查询简化为一个更简单的问题: 查询: {query} 简化问题: """, # 定义输入变量 input_variables=["query"], )
📄 query_classifier.py 8.1KB
# 导入标准库 import json import os # 导入 PyTorch import torch # 导入日志 from base import logger # 导入numpy import numpy as np # 导入 Transformers 库 from transformers import BertTokenizer, BertForSequenceClassification from transformers import Trainer, TrainingArguments # 导入train_test_split from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, confusion_matrix class QueryClassifier: def __init__(self, model_path="bert_query_classifier"): # 初始化模型路径 self.pre_trained_model_path = r'D:\code\workspace7\integrated_qa_system\rag_qa\models\bert-base-chinese' self.model_path = model_path # 加载 BERT 分词器 self.tokenizer = BertTokenizer.from_pretrained(self.pre_trained_model_path) # 初始化模型 self.model = None # 确定设备(GPU 或 CPU) self.device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu" # 记录设备信息 logger.info(f"使用设备: {self.device}") # 定义标签映射 self.label_map = {"通用知识": 0, "专业咨询": 1} # 加载模型 self.load_model() def load_model(self): # 检查模型路径是否存在 if os.path.exists(self.model_path): # 加载预训练模型 self.model = BertForSequenceClassification.from_pretrained(self.model_path, num_labels=2) # 将模型移到指定设备 self.model.to(self.device) # 记录加载成功的日志 logger.info(f"加载模型: {self.model_path}") else: # 初始化新模型 self.model = BertForSequenceClassification.from_pretrained(self.pre_trained_model_path, num_labels=2) # 将模型移到指定设备 self.model.to(self.device) # 记录初始化模型的日志 logger.info("初始化新 BERT 模型") def save_model(self): """保存模型""" logger.info(f"开始保存模型....") self.model.save_pretrained(self.model_path) self.tokenizer.save_pretrained(self.model_path) logger.info(f"模型保存至: {self.model_path}") def preprocess_data(self, texts, labels): """预处理数据为 BERT 输入格式""" encodings = self.tokenizer( texts, truncation=True, padding=True, max_length=128, return_tensors="pt" ) return encodings, [self.label_map[label] for label in labels] def create_dataset(self, encodings, labels): """创建 PyTorch 数据集""" class Dataset(torch.utils.data.Dataset): def __init__(self, encodings, labels): self.encodings = encodings self.labels = labels def __getitem__(self, idx): item = {key: val[idx] for key, val in self.encodings.items()} item["labels"] = torch.tensor(self.labels[idx]) return item def __len__(self): return len(self.labels) return Dataset(encodings, labels) def train_model(self, data_file="training_dataset_hybrid_5000.json"): """训练 BERT 分类模型""" # 加载数据集 if not os.path.exists(data_file): logger.error(f"数据集文件 {data_file} 不存在") raise FileNotFoundError(f"数据集文件 {data_file} 不存在") with open(data_file, "r", encoding="utf-8") as f: data = [json.loads(value) for value in f.readlines()] texts = [item["query"] for item in data] labels = [item["label"] for item in data] # 数据划分 train_texts, val_texts, train_labels, val_labels = train_test_split(texts, labels, test_size=0.2, random_state=42) # 预处理 train_encodings, train_labels = self.preprocess_data(train_texts, train_labels) val_encodings, val_labels = self.preprocess_data(val_texts, val_labels) # 创建数据集 train_dataset = self.create_dataset(train_encodings, train_labels) # print(f'train_dataset--》{train_dataset[0]}') val_dataset = self.create_dataset(val_encodings, val_labels) # # 设置训练参数 training_args = TrainingArguments( output_dir="./bert_results", num_train_epochs=1, per_device_train_batch_size=8, per_device_eval_batch_size=8, warmup_steps=500, weight_decay=0.01, logging_dir="./bert_logs", logging_steps=10, eval_strategy="epoch", save_strategy="epoch", load_best_model_at_end=True, save_total_limit=1, # 只保存一个检查点,即最优的模型 metric_for_best_model="eval_loss", fp16=False, # 禁用混合精度 ) # 初始化 Trainer trainer = Trainer( model=self.model, args=training_args, train_dataset=train_dataset, eval_dataset=val_dataset, compute_metrics=self.compute_metrics ) # 训练模型 logger.info("开始训练 BERT 模型...") trainer.train() self.save_model() # 评估模型 self.evaluate_model(val_texts, val_labels) def compute_metrics(self, eval_pred): """计算评估指标""" logits, labels = eval_pred predictions = np.argmax(logits, axis=-1) accuracy = (predictions == labels).mean() return {"accuracy": accuracy} def evaluate_model(self, texts, labels): """评估模型性能""" # 仅对 texts 进行分词,labels 已为数字 encodings = self.tokenizer( texts, truncation=True, padding=True, max_length=128, return_tensors="pt" ) dataset = self.create_dataset(encodings, labels) trainer = Trainer(model=self.model) predictions = trainer.predict(dataset) pred_labels = np.argmax(predictions.predictions, axis=-1) true_labels = labels # 直接使用数字标签 logger.info("分类报告:") logger.info(classification_report( true_labels, pred_labels, target_names=["通用知识", "专业咨询"] )) logger.info("混淆矩阵:") logger.info(confusion_matrix(true_labels, pred_labels)) def predict_category(self, query): # 检查模型是否加载 if self.model is None: # 模型未加载,记录错误 logger.error("模型未训练或加载") # 默认返回通用知识 return "通用知识" # 对查询进行编码 encoding = self.tokenizer(query, truncation=True, padding=True, max_length=128, return_tensors="pt") # 将编码移到指定设备 encoding = {k: v.to(self.device) for k, v in encoding.items()} # 不计算梯度,进行预测 self.model.eval() with torch.no_grad(): # 获取模型输出 outputs = self.model(**encoding) # 获取预测结果 prediction = torch.argmax(outputs.logits, dim=1).item() # 根据预测结果返回类别 return "专业咨询" if prediction == 1 else "通用知识" if __name__ == "__main__": # 初始化分类器 classifier = QueryClassifier(model_path="../models/bert_query_classifier") logger.info(f"classifier-->{classifier}") # 训练模型,如果放开下面训练模型的代码,则会在models目录下生成一个模型,名称为:bert_query_classifier # classifier.train_model(data_file='../classify_data/model_generic_5000.json') # logger.info("模型训练完成") # 示例预测 test_queries = [ "AI学科的课程大纲是什么", "JAVA课程费用多少?", "5乘以9等于多少?", "AI培训有哪些老师?" ] for query in test_queries: category = classifier.predict_category(query) print(f"查询: {query} -> 分类: {category}")
📄 rag_system.py 9.7KB
from rag_qa.core.prompts import RAGPrompts # 导入 time 模块,用于计算时间 import time from base import logger, Config from rag_qa.core.query_classifier import QueryClassifier # 导入查询分类器 from rag_qa.core.strategy_selector import StrategySelector # 导入策略选择器 conf = Config() # 定义 RAGSystem 类,封装 RAG 系统的核心逻辑 class RAGSystem: # 初始化方法,设置 RAG 系统的基本参数 def __init__(self, vector_store, llm): # 设置向量数据库对象 self.vector_store = vector_store # 设置大语言模型调用函数 self.llm = llm # 获取 RAG 提示模板 self.rag_prompt = RAGPrompts.rag_prompt() # 初始化查询分类器 self.query_classifier = QueryClassifier( model_path='D:/code/workspace7/integrated_qa_system/rag_qa/models/bert_query_classifier/') # 初始化策略选择器 self.strategy_selector = StrategySelector() # 最大 4096 字符 self.max_prompt_length = 4096 # 定义方法,使用假设文档进行检索(HyDE) def _retrieve_with_hyde(self, query): logger.info(f"使用 HyDE 策略进行检索 (查询: '{query}')") # 获取假设问题生成的 Prompt 模板 hyde_prompt_template = RAGPrompts.hyde_prompt() # 使用 template 后缀区分 # 调用大语言模型生成假设答案 try: hypo_answer = self.llm(hyde_prompt_template.format(query=query)).strip() logger.info(f"HyDE 生成的假设答案: '{hypo_answer}'") # 使用假设答案进行检索,并返回检索结果 # 注意:HyDE 通常只用于生成检索向量,不一定需要 rerank 这一步,但这里复用了 return self.vector_store.hybrid_search_with_rerank( hypo_answer, k=conf.RETRIEVAL_K # 使用 K 而非 M ) except Exception as e: logger.error(f"HyDE 策略执行失败: {e}") return [] # 定义方法,使用子查询进行检索 def _retrieve_with_subqueries(self, query): logger.info(f"使用子查询策略进行检索 (查询: '{query}')") # 获取子查询生成的 Prompt 模板 subquery_prompt_template = RAGPrompts.subquery_prompt() # 使用 template 后缀区分 try: # 调用大语言模型生成子查询列表 subqueries_text = self.llm(subquery_prompt_template.format(query=query)).strip() subqueries = [q.strip() for q in subqueries_text.split("\n") if q.strip()] logger.info(f"生成的子查询: {subqueries}") if not subqueries: logger.warning("未能生成有效的子查询") return [] # 初始化空列表,用于存储所有子查询的检索结果 all_docs = [] # 遍历每个子查询 for sub_q in subqueries: # 使用子查询进行检索,并将结果添加到列表中 # 这里对每个子查询都执行了 hybrid search + rerank,开销可能较大 docs = self.vector_store.hybrid_search_with_rerank( sub_q, k=conf.RETRIEVAL_K # 使用 K ) all_docs.extend(docs) logger.info(f"子查询 '{sub_q}' 检索到 {len(docs)} 个文档") # 对所有检索结果进行去重 (基于对象内存地址,如果 Document 内容相同但对象不同则无法去重) # 更可靠的去重方式是基于文档内容或 ID unique_docs_dict = {doc.page_content: doc for doc in all_docs} # 基于内容去重 unique_docs = list(unique_docs_dict.values()) logger.info(f"所有子查询共检索到 {len(all_docs)} 个文档, 去重后剩 {len(unique_docs)} 个") # 返回去重后的文档,限制数量 (是否需要在此处限制? retrieve_and_merge 末尾会限制) # return unique_docs[: Config.CANDIDATE_M] return unique_docs # 返回所有唯一文档,让 retrieve_and_merge 处理数量 except Exception as e: logger.error(f"子查询策略执行失败: {e}") return [] # 定义方法,使用回溯问题进行检索 def _retrieve_with_backtracking(self, query): logger.info(f"使用回溯问题策略进行检索 (查询: '{query}')") # 获取回溯问题生成的 Prompt 模板 backtrack_prompt_template = RAGPrompts.backtracking_prompt() # 使用 template 后缀区分 try: # 调用大语言模型生成回溯问题 simplified_query = self.llm(backtrack_prompt_template.format(query=query)).strip() logger.info(f"生成的回溯问题: '{simplified_query}'") # 使用回溯问题进行检索,并返回检索结果 return self.vector_store.hybrid_search_with_rerank( simplified_query, k=conf.RETRIEVAL_K # 使用 K ) except Exception as e: logger.error(f"回溯问题策略执行失败: {e}") return [] # 定义方法,检索并合并相关文档 def retrieve_and_merge(self, query, source_filter=None, strategy=None): # 新增 strategy 参数 # 如果未指定检索策略,则使用策略选择器选择 if not strategy: strategy = self.strategy_selector.select_strategy(query) # 根据检索策略选择不同的检索方式 ranked_sub_chunks = [] # 初始化 if strategy == "回溯问题检索": ranked_sub_chunks = self._retrieve_with_backtracking(query) elif strategy == "子查询检索": ranked_sub_chunks = self._retrieve_with_subqueries(query) # 返回的是唯一文档列表 # 注意:子查询返回的是已 rerank 过的父文档或子块列表,后续合并逻辑可能需要调整 # 当前实现中,子查询返回的是初步检索(可能已rerank)的块,再进行合并 elif strategy == "假设问题检索": ranked_sub_chunks = self._retrieve_with_hyde(query) else: # 默认或“直接检索” logger.info(f"使用直接检索策略 (查询: '{query}')") ranked_sub_chunks = self.vector_store.hybrid_search_with_rerank( query, k=conf.RETRIEVAL_K // 2, source_filter=source_filter ) # 注意 hybrid_search_with_rerank 返回的是 rerank 后的父文档 logger.info(f"策略 '{strategy}' 检索到 {len(ranked_sub_chunks)} 个候选文档 (可能已是父文档)") final_context_docs = ranked_sub_chunks[:conf.CANDIDATE_M] logger.info(f"最终选取 {len(final_context_docs)} 个文档作为上下文") return final_context_docs # 定义方法,生成答案 def generate_answer(self, query, source_filter=None): # 记录查询开始时间 start_time = time.time() logger.info(f"开始处理查询: '{query}', 学科过滤: {source_filter}") # 判断查询类型 query_category = self.query_classifier.predict_category(query) logger.info(f"查询分类结果:{query_category} (查询: '{query}')") # 如果查询属于“通用知识”类别,则直接使用 LLM 回答 if query_category == "通用知识": logger.info("查询为通用知识,直接调用 LLM") prompt_input = self.rag_prompt.format( context="", question=query, phone=conf.CUSTOMER_SERVICE_PHONE, history="", ) # 不使用上下文 try: answer = self.llm(prompt_input) except Exception as e: logger.error(f"直接调用 LLM 失败: {e}") answer = f"抱歉,处理您的通用知识问题时出错。请联系人工客服:{conf.CUSTOMER_SERVICE_PHONE}" processing_time = time.time() - start_time logger.info( f"通用知识查询处理完成 (耗时: {processing_time:.2f}s, 查询: '{query}')" ) return answer # 否则,进行 RAG 检索并生成答案 logger.info("查询为专业咨询,执行 RAG 流程") # 选择检索策略 strategy = self.strategy_selector.select_strategy(query) # 检索相关文档 context_docs = self.retrieve_and_merge( query, source_filter=source_filter, strategy=strategy ) # 传递 strategy # 准备上下文 if context_docs: context = "\n\n".join([doc.page_content for doc in context_docs]) # 使用换行符分隔文档 logger.info(f"构建上下文完成,包含 {len(context_docs)} 个文档块") # logger.debug(f"上下文内容:\n{context[:500]}...") # Debug 日志可以打印部分上下文 else: context = "" logger.info("未检索到相关文档,上下文为空") # 构造 Prompt,调用大语言模型生成答案 prompt_input = self.rag_prompt.format( context=context, question=query, phone=conf.CUSTOMER_SERVICE_PHONE, history="", ) logger.debug(f"最终生成的 Prompt:\n{prompt_input}") # Debug 日志 try: answer = self.llm(prompt_input) except Exception as e: logger.error(f"调用 LLM 生成最终答案失败: {e}") answer = f"抱歉,处理您的专业咨询问题时出错。请联系人工客服:{conf.CUSTOMER_SERVICE_PHONE}" # 记录查询处理完成的日志 processing_time = time.time() - start_time logger.info(f"查询处理完成 (耗时: {processing_time:.2f}s, 查询: '{query}')") return answer
📄 strategy_selector.py 4.2KB
# 导入 LangChain 提示模板 from langchain_core.prompts import PromptTemplate # 导入日志和配置 from base import logger, Config # 导入 OpenAI from openai import OpenAI class StrategySelector: def __init__(self): # 初始化 OpenAI 客户端 self.client = OpenAI(api_key=Config().DASHSCOPE_API_KEY, base_url=Config().DASHSCOPE_BASE_URL) # 获取策略选择提示模板 self.strategy_prompt_template = self._get_strategy_prompt() def call_dashscope(self, prompt): # 调用 DashScope API try: # 创建聊天完成请求 completion = self.client.chat.completions.create( model=Config().LLM_MODEL, messages=[ {"role": "system", "content": "你是一个有用的助手。"}, {"role": "user", "content": prompt} ], temperature=0.1 ) # 返回完成结果 return completion.choices[0].message.content if completion.choices else "直接检索" except Exception as e: # 记录 API 调用失败 logger.error(f"DashScope API 调用失败: {e}") # 默认返回直接检索 return "直接检索" def _get_strategy_prompt(self): # 定义私有方法,获取策略选择 Prompt 模板 return PromptTemplate( template=""" 你是一个智能助手,负责分析用户查询 {query},并从以下四种检索增强策略中选择一个最适合的策略,直接返回策略名称,不需要解释过程。 以下是几种检索增强策略及其适用场景: 1. **直接检索:** * 描述:对用户查询直接进行检索,不进行任何增强处理。 * 适用场景:适用于查询意图明确,需要从知识库中检索**特定信息**的问题,例如: * 示例: * 查询:AI 学科学费是多少? * 策略:直接检索 * 查询:JAVA的课程大纲是什么? * 策略:直接检索 2. **假设问题检索(HyDE):** * 描述:使用 LLM 生成一个假设的答案,然后基于假设答案进行检索。 * 适用场景:适用于查询较为抽象,直接检索效果不佳的问题,例如: * 示例: * 查询:人工智能在教育领域的应用有哪些? * 策略:假设问题检索 3. **子查询检索:** * 描述:将复杂的用户查询拆分为多个简单的子查询,分别检索并合并结果。 * 适用场景:适用于查询涉及多个实体或方面,需要分别检索不同信息的问题,例如: * 示例: * 查询:比较 Milvus 和 Zilliz Cloud 的优缺点。 * 策略:子查询检索 4. **回溯问题检索:** * 描述:将复杂的用户查询转化为更基础、更易于检索的问题,然后进行检索。 * 适用场景:适用于查询较为复杂,需要简化后才能有效检索的问题,例如: * 示例: * 查询:我有一个包含 100 亿条记录的数据集,想把它存储到 Milvus 中进行查询。可以吗? * 策略:回溯问题检索 根据用户查询 {query},直接返回最适合的策略名称,例如 "直接检索"。不要输出任何分析过程或其他内容。 """ , input_variables=["query"], ) # 定义方法,选择检索策略 def select_strategy(self, query): # 调用 LLM 获取检索策略 strategy = self.call_dashscope(self.strategy_prompt_template.format(query=query)).strip() logger.info(f"为查询 '{query}' 选择的检索策略:{strategy}") return strategy if __name__ == '__main__': ss = StrategySelector() ss.select_strategy('AI大模型应该怎么学习')
📄 vector_store.py 16.2KB
# -*- coding:utf-8 -*- # 导入 BGE-M3 嵌入函数,用于生成文档和查询的向量表示 import torch.cuda from langchain_core.documents import Document from milvus_model.hybrid import BGEM3EmbeddingFunction # 导入 Milvus 相关类,用于操作向量数据库 from pymilvus import MilvusClient, DataType, AnnSearchRequest, WeightedRanker # 导入 Document 类,用于创建文档对象 # from langchain.docstore.document import Document # 导入 CrossEncoder,用于重排序和 NLI 判断 from sentence_transformers import CrossEncoder # 导入 hashlib 模块,用于生成唯一 ID 的哈希值 import hashlib from rag_qa.core.document_processor import * import sys, os # 获取当前文件所在目录的绝对路径 current_dir = os.path.dirname(os.path.abspath(__file__)) # print(f'current_dir--》{current_dir}') # 获取core文件所在的目录的绝对路径 rag_qa_path = os.path.dirname(current_dir) # print(f'rag_qa_path--》{rag_qa_path}') sys.path.insert(0, rag_qa_path) # 获取根目录文件所在的绝对位置 project_root = os.path.dirname(rag_qa_path) sys.path.insert(0, project_root) from base import logger, Config conf = Config() # core/vector_store.py # 定义 VectorStore 类,封装向量存储和检索功能 class VectorStore: # 初始化方法,设置向量存储的基本参数 def __init__(self, collection_name=conf.MILVUS_COLLECTION_NAME, host=conf.MILVUS_HOST, port=conf.MILVUS_PORT, database=conf.MILVUS_DATABASE_NAME): # 设置 Milvus 集合名称 self.collection_name = collection_name # 设置 Milvus 主机地址 self.host = host # 设置 Milvus 端口号 self.port = port # 设置 Milvus 数据库名称 self.database = database # 设置日志记录器 self.logger = logger # 检查CUDA是否可用 self.device = 'cuda' if torch.cuda.is_available() else 'cpu' # 日志提醒使用的是什么设备 self.logger.info(f"使用设置:{self.device}") # 初始化 BGE-Reranker 模型,用于重排序检索结果 reranker_path = os.path.join(rag_qa_path, 'models', 'bge-reranker-large') # print(f'reranker_path--》{reranker_path}') self.reranker = CrossEncoder(reranker_path, device=self.device) # 初始化 BGE-M3 嵌入函数,使用 CPU 设备,不启用 FP16 m3_path = os.path.join(rag_qa_path, 'models', 'bge-m3') self.embedding_function = BGEM3EmbeddingFunction(model_name_or_path=m3_path, use_fp16=(self.device == 'cuda'), device=self.device) # 获取稠密向量的维度# 1024 self.dense_dim = self.embedding_function.dim["dense"] # 初始化 Milvus 客户端,连接到指定主机和数据库 self.client = MilvusClient(uri=f"http://{self.host}:{self.port}", db_name=self.database) # 调用方法创建或加载 Milvus 集合 self._create_or_load_collection() # 类私有化方法 def _create_or_load_collection(self): # 检查指定集合是否已经存在 if not self.client.has_collection(self.collection_name): # 创建集合 Schema,禁用自动 ID,启用动态字段 schema = self.client.create_schema(auto_id=False, enable_dynamic_field=True) # 添加 ID 字段,作为主键,VARCHAR 类型,最大长度 100 schema.add_field(field_name="id", datatype=DataType.VARCHAR, is_primary=True, max_length=100) # 添加文本字段,VARCHAR 类型,最大长度 65535 schema.add_field(field_name="text", datatype=DataType.VARCHAR, max_length=65535) # 添加稠密向量字段,FLOAT_VECTOR 类型,维度由嵌入函数指定 schema.add_field(field_name="dense_vector", datatype=DataType.FLOAT_VECTOR, dim=self.dense_dim) # 添加稀疏向量字段,SPARSE_FLOAT_VECTOR 类型 schema.add_field(field_name="sparse_vector", datatype=DataType.SPARSE_FLOAT_VECTOR) # 添加父块 ID 字段,VARCHAR 类型,最大长度 100 schema.add_field(field_name="parent_id", datatype=DataType.VARCHAR, max_length=100) # 添加父块内容字段,VARCHAR 类型,最大长度 65535 schema.add_field(field_name="parent_content", datatype=DataType.VARCHAR, max_length=65535) # 添加学科类别字段,VARCHAR 类型,最大长度 50 schema.add_field(field_name="source", datatype=DataType.VARCHAR, max_length=50) # 添加时间戳字段,VARCHAR 类型,最大长度 50 schema.add_field(field_name="timestamp", datatype=DataType.VARCHAR, max_length=50) # 创建索引参数对象 index_params = self.client.prepare_index_params() # 为稠密向量字段添加 IVF_FLAT 索引,度量类型为内积 (IP) index_params.add_index( field_name="dense_vector", index_name="dense_index", index_type="IVF_FLAT", metric_type="IP", params={"nlist": 128} ) # 为稀疏向量字段添加 SPARSE_INVERTED_INDEX 索引,度量类型为内积 (IP) index_params.add_index( field_name="sparse_vector", index_name="sparse_index", index_type="SPARSE_INVERTED_INDEX", metric_type="IP", params={"drop_ratio_build": 0.2} ) # 创建 Milvus 集合,应用定义的 Schema 和索引参数 self.client.create_collection(collection_name=self.collection_name, schema=schema, index_params=index_params) # 记录创建集合的日志 logger.info(f"已创建集合 {self.collection_name}") # 如果集合已存在 else: # 记录加载集合的日志 logger.info(f"已加载集合 {self.collection_name}") # 将集合加载到内存,确保可立即查询 self.client.load_collection(self.collection_name) # 定义方法,往向量存储添加文档 def add_documents(self, documents): # print(f'documents--》{documents[0]}') # 提取所有文档的内容列表 texts = [doc.page_content for doc in documents] # 使用 BGE-M3 嵌入函数生成文档的嵌入 embeddings = self.embedding_function(texts) # print(f'embeddings-->', embeddings) # print(f'embeddings.dense-->', embeddings['dense']) # print(f'embeddings.sparse-->', embeddings['sparse']) # 初始化空列表,存储插入的数据 data = [] # 遍历每个文档,带上索引i for i, doc in enumerate(documents): # 生成文档内容的哈希值作为唯一的ID text_hash = hashlib.md5(doc.page_content.encode('utf-8')).hexdigest() # print(f'text_hash--》{text_hash}') # print(f'text_hash--》{type(text_hash)}') # 初始化一个稀疏向量的字典(Milvus要求存储稀疏向量的格式) sparse_vector = {} try: # 新版本 milvus-model 使用 coo_array 格式 row = embeddings["sparse"][i] print('i-->',i, "row-->", row) if hasattr(row, 'col'): # coo_array 格式 indices = row.col values = row.data else: # csr_matrix 格式 indices = row.indices values = row.data except Exception as e: # 兼容旧版本 milvus-model row = embeddings["sparse"].getrow(0) indices = row.indices values = row.data # 获取第i行对应的稀疏向量数据[0.4, 0.2, 0, 0, 0.1] # row = embeddings["sparse"].getrow(i) # row = embeddings["sparse"][i] # 新版本milvus-model,支持这种获取稀疏向量的形式 # indices = row.row # print(f'row--》{row}') # print(f'row--》{row.shape}') # 获取稀疏向量的非零值的索引 # indices = row.col # print(f'indics--》{indics}') # 获取稀疏向量的非零值 # values = row.data # 将索引和值进行配对,存储到字典中 for idx, value in zip(indices, values): sparse_vector[idx] = value # print(f'sparse_vector--》{sparse_vector}') # print(f'sparse_vector--》{len(sparse_vector)}') # print(embeddings["dense"][i]) # print(embeddings["dense"][i].shape) # 创建数据字典,包含所有字段 data.append({ "id": text_hash, "text": doc.page_content, "dense_vector": embeddings["dense"][i], "sparse_vector": sparse_vector, "parent_id": doc.metadata["parent_id"], "parent_content": doc.metadata["parent_content"], "source": doc.metadata.get("source", "unknown"), "timestamp": doc.metadata.get("timestamp", "unknown") }) # print("data-->", data) # 检查是否有数据需要插入 if data: # 使用 upsert 操作插入数据,覆盖重复 ID self.client.upsert(collection_name=self.collection_name, data=data) # 记录插入或更新的文档数量日志 logger.info(f"已插入或更新 {len(data)} 个文档") # 定义方法,执行混合检索并重排序 def hybrid_search_with_rerank(self, query, k=conf.RETRIEVAL_K, source_filter=None): # 使用 BGE-M3 嵌入函数生成查询的嵌入 query_embeddings = self.embedding_function([query]) # 获取查询的稠密向量 dense_query_vector = query_embeddings["dense"][0] # print(f'dense_query_vector--》{dense_query_vector.shape}') # 初始化查询的稀疏向量字典 sparse_query_vector = {} # ==================== # 获取查询稀疏向量的第 0 行数据 # row = query_embeddings["sparse"][0] # # 获取稀疏向量的非零值索引 # indices = row.indices # # 获取稀疏向量的非零值 # values = row.data # ==================== try: # 新版本 milvus-model 使用 coo_array 格式 row = query_embeddings["sparse"][0] if hasattr(row, 'col'): # coo_array 格式 indices = row.col values = row.data else: # csr_matrix 格式 indices = row.indices values = row.data except Exception as e: # 兼容旧版本 milvus-model row = query_embeddings["sparse"].getrow(0) indices = row.indices values = row.data # 将索引和值配对,填充稀疏向量字典 for idx, value in zip(indices, values): sparse_query_vector[idx] = value # print(f'sparse_query_vector-->{sparse_query_vector}') # 初始化过滤表达式,默认不过滤 filter_expr = f"source == '{source_filter}'" if source_filter else "" # print(f'filter_expr--》{filter_expr}') # 创建稠密向量搜索请求 dense_request = AnnSearchRequest( data=[dense_query_vector], anns_field="dense_vector", param={"metric_type": "IP", "params": {"nprobe": 10}}, limit=k, expr=filter_expr ) # 创建稀疏向量搜索请求 sparse_request = AnnSearchRequest( data=[sparse_query_vector], anns_field="sparse_vector", param={"metric_type": "IP", "params": {}}, limit=k, expr=filter_expr ) # 创建加权排序器,稠密向量权重 1.0,稀疏向量权重 0.7 ranker = WeightedRanker(1.0, 0.7) # 执行混合搜索,返回 Top-K 结果 results = self.client.hybrid_search( collection_name=self.collection_name, reqs=[dense_request, sparse_request], ranker=ranker, limit=k, output_fields=["text", "parent_id", "parent_content", "source", "timestamp"] )[0] # print(f'results--》{results}') # print(f'results--》{type(results)}') # print(f'results--》{len(results)}') # 将上述搜索到的结果进行Document对象封装,便于查询使用 sub_chunks = [self._doc_from_hit(hit["entity"]) for hit in results] # print(f'sub_chunks--》{len(sub_chunks)}') # 从子块中提取去重的父文档 parent_docs = self._get_unique_parent_docs(sub_chunks) # print(f'parent_docs--》{parent_docs}') # print(f'parent_docs--》{len(parent_docs)}') # # 如果只有1个文档或者没有,直接返回跳过重排序 if len(parent_docs) < 2: return parent_docs[:conf.CANDIDATE_M] # 如果有父文档,进行重排序 if parent_docs: # 创建查询与文档内容的配对列表 pairs = [[query, doc.page_content] for doc in parent_docs] # 使用 BGE-Reranker 计算每个配对的得分 scores = self.reranker.predict(pairs) # print(f'scores--》{scores}') # 根据得分从高到低排序文档 ranked_parent_docs = [doc for _, doc in sorted(zip(scores, parent_docs), reverse=True)] # 如果没有父文档,返回空列表 else: ranked_parent_docs = [] # 返回前 m 个重排序后的文档 return ranked_parent_docs[:conf.CANDIDATE_M] def _get_unique_parent_docs(self, sub_chunks): # 初始化集合,用于存储已处理的父块内容(去重) parent_contents = set() # 初始化列表,用于存储唯一父文档 unique_docs = [] # 遍历所有子块 for chunk in sub_chunks: # 获取子块的父块内容,默认为子块内容 parent_content = chunk.metadata.get("parent_content", chunk.page_content) # 检查父块内容是否非空且未重复 if parent_content and parent_content not in parent_contents: # 创建新的 Document 对象,包含父块内容和元数据 unique_docs.append(Document(page_content=parent_content, metadata=chunk.metadata)) # 将父块内容添加到去重集合 parent_contents.add(parent_content) # 返回去重后的父文档列表 return unique_docs # 定义类似私有方法,从 Milvus 查询结果创建 Document 对象 def _doc_from_hit(self, hit): # 创建并返回 Document 对象,填充内容和元数据 return Document( page_content=hit.get("text"), metadata={ "parent_id": hit.get("parent_id"), "parent_content": hit.get("parent_content"), "source": hit.get("source"), "timestamp": hit.get("timestamp") } ) def test_bge_m3(): # 准备query query = "课程优势是什么" # 测试BGE-M3模型 bgem3 = BGEM3EmbeddingFunction(model_name_or_path=r"D:\code\workspace7\integrated_qa_system\rag_qa\models\bge-m3", use_fp16=False, device="cpu") # 把query送给模型,得到向量 result = bgem3([query]) # 打印结果 print("result-->", result) if __name__ == "__main__": # TODO 需要先在milvus中创建itcast数据库,否则执行代码会报错。 vector_store = VectorStore() # 把知识库文件加载到Milvus向量数据库中 # directory_path = 'D:/code/workspace7/integrated_qa_system/rag_qa/data/ai_data' # print(f"embedding_function.dim--》{vector_store.embedding_function.dim}") # documents = process_documents(directory_path) # print("documents-->", documents) # vector_store.add_documents(documents) # 测试代码 query = "课程新增了什么技术" results = vector_store.hybrid_search_with_rerank(query, source_filter='ai') print(f'results-->{results}') print(f'results-->{len(results)}')
📁 data/
📁 ai_data/
📄 人工智能就业课课程大纲(测试).md 2.1KB
# 人工智能就业课 【课程大纲】 ## 学科编辑要求说明 1.补充标题3《学科编辑请更新如下内容》,标题结构不可删减。 2.具体内容请根据学科实际填写和增删条数。 3.项目展示需配图。 ## 设计需求说明 3.学科编辑请更新如下内容 a. 一句话学科概括语 解锁「大模型」 新技能成就「高薪AI」人才 人工智能开发学科主要涵盖自然语言处理、大模型等领域的前沿技术与实战项目,旨在帮助学员掌握相关模型原理与应用,提升AI大模型专业技能与实践能力,以满足企业对高素质人才的需求。 b.课程介绍(一段话): 大模型时代来袭抢抓就业薪机遇 人工智能开发学科经过多年课程积淀与迭代,紧跟人工智能市场实际需求,不断对课程进行打磨升级,涵盖了互联网前沿技术及企业级实战项目,培养大模型应用型开发人才。 随着大模型时代的到来,人工智能技术和应用发展越来越快,但是以往的人工智能开 发门槛高、入门难,为了帮助梦想进入人工智能行业的学生找到更适宜的入门途径,人工智能学科全新升级——人工智能开发V6.0课程。 学习人工智能开发,以机器学习技术栈完成数据挖掘任务,以深度学习技术栈完成NLP 自然语言处理与大模型开发任务,成为懂 AI、能实战的大模型应用开发人才。 ### 课程新增技术 新增:大模型开发基础与项目 升级:机器学习 升级:自然语言处理项目1 升级:自然语言处理项目 整体内容目录: 第一部分:大模型语言基础 第二部分:大模型语言进阶 第三部分:数据处理与统计分析 第四阶段:机器学习 第五阶段:深度学习基础 第六阶段:NLP自然语言处理基础 第七部分:自然语言处理项目1 第八部分:自然语言处理项目2 第九部分:大模型开发基础与项目 赠送内容一:图像分析基础 赠送内容二:多模态大模型综合项目
📁 samples/
- ⚙️ ocr_01.pptx 3.3MB
- ⚙️ ocr_02.docx 102.9KB
- ⚙️ ocr_03.pdf 135.8KB
- ⚙️ ocr_04.png 185.5KB
- ⚙️ LLM基础知识.pdf 1.7MB
📄 人工智能就业课课程大纲.md 24.8KB
# 人工智能就业课 【课程大纲】 ## 1.学科编辑要求说明 1.补充标题3《学科编辑请更新如下内容》,标题结构不可删减。 2.具体内容请根据学科实际填写和增删条数。 3.项目展示需配图。 ## 2.设计需求说明 3.学科编辑请更新如下内容 a. 一句话学科概括语 解锁「大模型」 新技能成就「高薪AI」人才 人工智能开发学科主要涵盖自然语言处理、大模型等领域的前沿技术与实战项目,旨在帮助学员掌握相关模型原理与应用,提升AI大模型专业技能与实践能力,以满足企业对高素质人才的需求。 b.课程介绍(一段话): 大模型时代来袭抢抓就业薪机遇 人工智能开发学科经过多年课程积淀与迭代,紧跟人工智能市场实际需求,不断对课程进行打磨升级,涵盖了互联网前沿技术及企业级实战项目,培养大模型应用型开发人才。 随着大模型时代的到来,人工智能技术和应用发展越来越快,但是以往的人工智能开 发门槛高、入门难,为了帮助梦想进入人工智能行业的学生找到更适宜的入门途径,人工智能学科全新升级——人工智能开发V6.0课程。 学习人工智能开发,以机器学习技术栈完成数据挖掘任务,以深度学习技术栈完成NLP 自然语言处理与大模型开发任务,成为懂 AI、能实战的大模型应用开发人才。 ## 课程优势 优势一:热门岗位覆盖(匹配企业岗位需求,拓宽你的职业选择) 大模型方向:设计、实现和优化大规模深度学习模型,包括数据预处理、模型架构设计、训练调优、性能优化以及模型部署,以推动AI大模型技术在各种应用场景中的高效落地与规模化应用 自然语言处理(NLP)方向:通过处理和分析文本数据,实现语言翻译、情感分析、自动摘要、聊天机器人等功能。使计算机能够理解和生成人类语言。 优势二:与大厂深入合作(共建大模型课程,助你掌握前沿技术,增强就业竞争力) 优势三:定制垂直行业大模型专项领域赋能(打造就业速度快、就业薪资高的专精AI大模型人才) 优势四:覆盖NLP、大模型解决方案和技术栈(一个方案解决多个业务问题) ### 优势四 ### 覆盖NLP、大模型解决方案和经典技术栈 ### 课程新增技术 新增:大模型开发基础与项目 升级:机器学习 升级:自然语言处理项目1 升级:自然语言处理项目 整体内容目录: 第一部分:大模型语言基础 第二部分:大模型语言进阶 第三部分:数据处理与统计分析 第四阶段:机器学习 第五阶段:深度学习基础 第六阶段:NLP自然语言处理基础 第七部分:自然语言处理项目1 第八部分:自然语言处理项目2 第九部分:大模型开发基础与项目 赠送内容一:图像分析基础 赠送内容二:多模态大模型综合项目 ## 课程大纲 ### 第一部分:大模型语言基础 #### 可解决的现实问题: 熟练掌握人工智能Python 语言,建立编程思维,使学员能够熟练使用Python 技术完成基础程序编写。 #### 可掌握的核心竞争力: 1.掌握Python开发环境基本配置 2.掌握运算符、表达式、流程控制语句、容器等的使用 3.掌握字符串的基本操作 4.掌握函数的定义及使用 5.熟悉异常捕获的基本流程及使用方式 <html><body><table><tr><td>模块</td><td>技术要点</td></tr><tr><td>计算机原理与Python 基础 入门</td><td>1.变量2.注释3.标识符和关键字 4.输入输出5.数据类型转换</td></tr><tr><td>Python判断语句</td><td>1.if判断语句语法格式 2.比较/关系运算符</td></tr><tr><td>循环进阶与字符串容器</td><td>1.while语句语法格式2.while 循环嵌套 3.break 和 continue4.while 循环案例 5.for循环6.字符串定义语法格式 7.字符串遍历8.下标和切片9.字符串常见操作</td></tr><tr><td>Python容器</td><td>1.列表语法格式2.列表的遍历3.列表常见操作 4.列表嵌套5.列表推导式6.元组语法格式7.元组操作 8.字典语法格式9.字典常见操作10.字典的遍历</td></tr><tr><td>Python函数</td><td>1.函数概念和作用、函数定义、调用 2.函数的参数3.函数的返回值4.函数的注释 5.函数的嵌套调用6.可变和不可变类型7.局部变量 8.全局变量9.组包和拆包、引用</td></tr><tr><td>Python文件操作</td><td>1.文件的打开与关闭、文件的读写 2.文件、目录操作及案例 3.os 模块文件与目录相关操作</td></tr><tr><td>Python异常、模块与包</td><td>1.异常概念2.异常捕获3.异常的传递4.模块介绍 5.模块的导入6.包的概念7.包的导入8.模块中的__all_ 9.模块中_name_</td></tr><tr><td>聊天机器人实战</td><td>1.Streamlit 入门 2.Langchain 简介 3.聊天机器人搭建</td></tr></table></body></html> #### 大模型语言基础实战案例: 项目介绍: 本项目结合了Streamlit和LangChain,构建了一个具备上下文记忆和复杂任务处理能力的智能聊天机器人。Streamlit提供了简洁易用的界面,LangChain 负责对话逻辑和任务链执行,支持定制化对话流程和知识库查询。项目适用于客户服务、学习助手、知识管理等多种场景,具备快速开发、高度可定制和强扩展性的优势。 #### 可解决的现实问题: 1-基于Streamlit完成chatgpt界面设计 2-基于LangChain完成通义模型调用 3-基于LangChain $^+$ Streamlit完成chatgpt机器人案例开发 #### 可掌握的核心竞争力: 1.了解Streamlit作用及Streamlit框架的安装 2.能够基于Streamlit完成chatgpt界面设计 3.了解 LangChain 框架的作用及 LangChain 框架组成 4. 能够基于LangChain完成通义模型调用 5.能够基于LangChain $.+$ Streamlit完成chatgpt机器人案例开发 ### 第二部分:大模型语言进阶 #### 可解决的现实问题: 熟练使用Python,建立面向对象程序设计思想,掌握人工智能开发必备Python 高级语法。 #### 可掌握的核心竞争力: 1.初步建立面向对象的编程思维 2.掌握类和对象的基本使用方式 3.掌握网络编程技术,能够实现网络通讯 4.知道通讯协议原理 5.掌握开发中的多任务编程实现方式 6.知道多进程多线程的原理 <html><body><table><tr><td>模块</td><td></td></tr><tr><td>面向对象</td><td>1.面向对象介绍2.类的定义和对象的创建 3.添加和获取对象属性4.self参数5.init方法6.继承 7.子类方法重写8.类属性和实例属性 9.类方法、实例方法、静态方法</td></tr><tr><td>深拷贝浅拷贝</td><td>1.深浅拷贝</td></tr><tr><td>闭包和装饰器</td><td>1.闭包2.装饰器</td></tr><tr><td>网络编程</td><td>1.IP地址的介绍2.端口和端口号的介绍3.TCP的介绍 4.Socket的介绍5.TCP网络应用的开发流程 6.基于TCP通信程序开发</td></tr><tr><td>多线程和生成器</td><td>1.多任务介绍2.多进程的使用3.多线程的使用 4.线程同步5.生成器</td></tr><tr><td>正则表达式</td><td>1.正则表达式</td></tr></table></body></html> ### 第三部分:数据处理与统计分析 #### 可解决的现实问题: 掌握SQL及Pandas完成数据分析与可视化操作。 #### 可掌握的核心竞争力: 1.掌握Linux常用命令,为数据开发后续学习打下的良好基础 2.掌握MySQL数据库的使用 3. 掌握SQL语法 4. 掌握使用Python操作数据库 5. 掌握Pandas案例 6.知道绘图库使用 7.掌握Pandas数据ETL 8.掌握Pandas数据分析项目流程 <html><body><table><tr><td>模块</td><td>技术要点</td></tr><tr><td>Linux</td><td>1.Linux命令使用 2.Linux命令选项的使用 4.Linux权限管理</td></tr><tr><td>MySQL基础</td><td>1.数据库概念和作用 2.MySQL数据类型 3.数据完整性和约束4.数据库、表基本操作命令</td></tr><tr><td>单表查询VS多表查询</td><td>5.表数据操作命令 6.where子句 1.分组聚合 2.连接查询 3.外键的使用</td></tr><tr><td>Python数分入门</td><td>1.Anaconda 2.Jypyter 3.Python数据分析</td></tr><tr><td>Numpy 矩阵运算库</td><td>1.Numpy运算优势,数组的属性,数组的形状 2.Numpy实现数组基本操 3.Numpy实现数组运算,矩阵乘法</td></tr><tr><td>Pandas 数据清洗</td><td>1.数据组合:01_Pandas 数据组合_concat连接;02_Pandas 数据 组合_merge数据;03_Pandas数据组合_join 2.缺失值处理:01_缺失值处理介绍;02_缺失值处理_缺失值数量统 计;03_缺失值处理;04_缺失值处理_删除缺失值;05_缺失值处理_ 填充缺失值 3.Pandas数据类型 4.apply 函数: 01_Series 和 DataFrame 的 apply 方法; 02_apply 使用案例</td></tr><tr><td>Pandas数据整理</td><td>1.数据分组:01_单变量分组聚合;02_通过调用agg进行聚合;03_ 分组后 transform; 04_transform 练习 2.Pandas透视表:01_透视表概述&绘员存量增量分析;02_绘员增 量等级分布;03_增量等级占比分析&整体等级分布;04_线上线下增</td></tr></table></body></html> <html><body><table><tr><td></td><td>量分析 3.datetime数据类型:01_日期时间类型介绍;02_提取日期分组案 例;03_股票数据处理;04_datarange函数;05_综合案例</td></tr><tr><td>Pandas数据可视化</td><td>1.Matplotlib 可视化 2.Pandas可视化 3.Seaborn可视化</td></tr><tr><td>RFM项目</td><td>1.RFM概念介绍2.RFM项目_数据加载和数据处理 3.RFM项目_RFM计算 4.RFM项目_RFM可视化 5.RFM项目_业务解读和小结</td></tr></table></body></html> ### 第四部分:机器学习 #### 可解决的现实问题: 掌握机器学习基本概念,利用多场景案例强化机器学习建模。 #### 可掌握的核心竞争力: 1.掌握机器学习算法基本原理 2.掌握使用机器学习模型训练的基本流程 3.掌握Sklearn等常用机器学习相关开源库的使用 4.熟练使用机器学习相关算法进行预测分析 <html><body><table><tr><td>模块</td><td>技术要点</td></tr><tr><td>机器学习入门</td><td>1.人工智能概述 2.机器学习开发流程和用到的数据介绍 3.特征工程介绍4.机器学习算法分类 5.机器学习模型评估6.机器学习开发环境</td></tr><tr><td>KNN算法</td><td>1.K近邻算法基本原理2.K近邻算法进行分类预测 3.sklearn 实现knn4.训练集测试集划分 5.分类算法的评估6.归一化和标准化7.超参数搜索</td></tr><tr><td>线性回归算法</td><td>1.线性回归简介2.线性回归API使用初步 3.导数回顾4.线性回归的损失函数和优化方法 5.梯度下降推导6.波士顿房价预测案例 7.欠拟合和过拟合8.模型的保存和加载</td></tr></table></body></html> <html><body><table><tr><td></td><td>9.线性回归应用-回归分析</td></tr><tr><td>逻辑回归算法</td><td>1.逻辑回归简介2.逻辑回归API应用案例 3.分类算法评价方法4.逻辑回归应用_分类分析</td></tr><tr><td>决策树算法</td><td>1.决策树算法简介2.决策树分类原理 3.特征工程-特征提取4.决策树算法api5.决策树案例</td></tr><tr><td>集成学习算法</td><td>1.集成学习算法简介2.Bagging和随机森林 3.随机森林案例4.Boosting介绍 5.GBDT介绍6.XGBOOST介绍</td></tr><tr><td>KMeans算法</td><td>1.聚类算法的概念2.聚类算法API的使用 3.聚类算法实现原理4.聚类算法的评估5.聚类算法案例</td></tr><tr><td>数据挖掘案例-电力负荷预测</td><td>1.时序预测介绍2.实现电力负荷预测案例</td></tr></table></body></html> ### 第五部分:深度学习基础 #### 可解决的现实问题: 掌握深度学习基础及神经网络经典算法;掌握全球热门的PyTorch技术。 #### 可掌握的核心竞争力: 1.Pytorch工具处理神经网络涉及的关键点 2.掌握神经网络基础知识 3.掌握反向传播原理 4.了解深度学习正则化与算法优化 5.掌握卷积神经网络(CNN) 6.掌握循环神经网络(RNN) <html><body><table><tr><td>模块</td><td>技术要点</td></tr><tr><td>Pytorch深度学习 框架</td><td>2.深度学习发展历史</td></tr></table></body></html> <html><body><table><tr><td></td><td>6.张量运算 7.反向传播 8.梯度,自动梯度 9.参数更新 10.数据加载器</td></tr><tr><td>神经网络基础</td><td>1.神经网络基础:神经网络的构成、激活函数、损失函数、优化方法 及正则化 2.反向传播原理:梯度下降算法、链式法则、反向传播算法、改善反 向传播算法性能的迭代法 3.深度学习正则化与算法优化:L1、L2、 DropOut、BN、SGD、 RMSProp、Adagrad、Adam</td></tr><tr><td>卷积神经网络</td><td>4.实现多层神经网络案例 1.图像基础知识 2.卷积神经网络CNN 的构成 3.卷积层计算 4.全连接层的作用 5.池化层</td></tr><tr><td>循环神经网络</td><td>6.基于Cifar10的图像分类案例 1.自然语言处理概述 2.词嵌入的作用 3.RNN的原理 4.RNN实现 5.基于RNN实现文本生成案例</td></tr></table></body></html> ### 第六部分:NLP自然语言处理基础 #### 可解决的现实问题: 掌握自然语言处理基础算法,诸如 RNN、LSTM、GRU、Transformer、BERT 等技术;掌握NLP领域前沿的技术解决方案。 #### 可掌握的核心竞争力: 1.掌握NLP领域前沿的技术解决方案 2.了解NLP应用场景 3.掌握NLP相关知识的原理和实现 4.掌握传统序列模型的基本原理和使用 5.掌握非序列模型解决文本问题的原理和方案 6.能够使用pytorch搭建神经网络 7.构建基本的语言翻译系统模型 8.构建基本的文本生成系统模型 9.构建基本的文本分类器模型 10.使用FastText进行快速的文本分类 11.胜任多数企业的NLP工程师的职位 <html><body><table><tr><td>模块</td><td>技术要点</td></tr><tr><td>自然语言处理基础</td><td>1.自然语言处理的概念 2.自然语言处理的发展简史 3.自然语言处理的应用场景</td></tr><tr><td>文本处理的基本方法</td><td>1.虚拟环境的配置和安装 2.文本处理基本方法类型 3.jieba分词 4.命名实体识别的原理</td></tr><tr><td>文本张量的表示方法</td><td>1.one-hot词向量原理与实现 2.word2vec-cbow模型的原理 3.word2vec-skipgram 模型的原理 4.基于fasttext实现word2vec词向量的训练 5.word Embedding 的原理与实现</td></tr><tr><td>文本数据分析+特征处 理+数据增强</td><td>1.获取标签数量分布 2.获取句子长度分布 3.获取正负样本长度散点分布 4.获取不同词汇总数统计 5.词云 6. n-gram 特征 7.文本长度规范及其作用 8.回译数据增强方法实现文本扩增</td></tr></table></body></html> <html><body><table><tr><td>RNN系列模型</td><td>1.RNN模型原理、类别与结构分析 2.LSTM模型结构分析 3.Bi-LSTM模型原理介绍 4.使用Pytorch 构建LSTM 模型 5.GRU 模型结构分析 6.Bi-GRU 模型原理介绍</td></tr><tr><td>人名分类器案例</td><td>7.使用 Pytorch 构建 GRU 模型 1.文本分类(人名分类器案例)</td></tr><tr><td>注意力机制</td><td>1.注意力机制原理介绍 2.Attention 计算逻辑 3.有无Attention 模型对比 4.注意力计算规则 5.什么是深度神经网络注意力机制 6.注意力机制的作用</td></tr><tr><td>Seq2Seq英译法案例</td><td>7.注意力机制实现步骤 1.文本翻译(Seq2Seq英译法案例)</td></tr><tr><td>transformer 输入+编 码器架构</td><td>1.输入部分介绍 2.文本嵌入层的作用 3.位置编码器的作用 4.位置编码器的代码分析 5.掩码张量介绍 6.掩码张量的作用 7.生成掩码张量的代码分析 8.注意力计算规则的代码分析 9.带有mask的输入参数 10.多头注意力机制概念 11.多头注意力机制的代码实现 12.前馈全连接层 13.前馈全连接层的代码分析 14.规范化层的作用 15.规范化层的代码实现 16.子层连接结构</td></tr></table></body></html> <html><body><table><tr><td>1.解码器层的作用</td><td>18.编码器层的作用 19.编码器层的代码分析 20.编码器的作用 21编码器的代码分析</td></tr><tr><td>transformer 输出+解 码器架构</td><td>2.解码器层的代码实现 3.解码器的作用 4.解码器的代码分析 5.输出部分介绍 6.线性层的作用 7.softmax 层的作用 8.线性层和softmax层的代码分析 9.编码器-解码器结构的代码实现</td></tr><tr><td>FastText工具应用</td><td>10.Transformer 模型构建过程的代码分析 1.讲解FastText工具的概念和原理 2.安装FastText工具包 3.分析FastText模型的原理 4.讲解层次softmax方法的原理 5.讲解负采样方法的原理 6.基于FastText实现词向量的训练 7.基于FastText实现文本分类</td></tr><tr><td>迁移学习介绍</td><td>8.基于FastText实现词向量的迁移 1讲解迁移学习有关概念 2.理解什么是预训练模型(Pretrained model) 3.掌握预训练模型的应用方式微调(Fine-tuning) 4.了解NLP中常见评估数据集 5.了解NLP中的常见预训练模型</td></tr><tr><td>迁移学习案例实践</td><td>6.掌握transformers 库的三种使用方式 1.迁移学习案例实践(中文分类、中文填空、中文句子关系)</td></tr></table></body></html> ### 第七部分(项目阶段):自然语言处理项目1 项目配图: #### 可解决的现实问题: 海量文本快速分类基线模型解决方案 基于预训练模型优化的解决方案 模型量化优化的解决方案 模型剪枝优化的解决方案 模型知识蒸馏优化的解决方案 主流迁移学习模型微调优化的解决方案 #### 可掌握的核心竞争力: 1.基于大规模业务留存数据构建快速文本分类系统 2. 基于推荐系统内部分频道投递的需求,快速搭建短文本精准分类投递的模型 3. 基于随机森林和FastText搭建快速基线模型,验证业务通道的能力 4. 基于BERT的迁移学习优化模型搭建的能力 5. 实现神经网络量化的优化与测试 6. 实现神经网络剪枝的优化与测试 7.实现神经网络知识蒸馏的优化与测试 8. 更多主流预训练模型的优化与深度模型剖析 9. BERT模型在生成式任务和工程优化上的深入扩展 <html><body><table><tr><td>模块</td><td>技术要点</td></tr><tr><td>基线模型1.0</td><td>1.项目背景介绍</td></tr></table></body></html> <html><body><table><tr><td></td><td>2.实现基于随机森林的基线模型1.0</td></tr><tr><td>基线模型2.0</td><td>实现基于FastText的基线模型2.0</td></tr><tr><td>迁移学习优化</td><td>实现基于BERT的迁移学习模型搭建和训练,并对比模型关键 指标的提升</td></tr><tr><td>模型的量化</td><td>实现对大型预训练模型的量化,并对比原始模型与量化模型 的差异</td></tr><tr><td>模型的剪枝</td><td>实现对模型的剪枝的操作,包含主流的对特定网络模块的剪 枝,多参数模块的剪枝,全局剪枝,用户自定义剪枝,包含 处理细节和理论知识</td></tr><tr><td>迁移学习微调</td><td>BERT模型微调</td></tr><tr><td>模型的知识蒸馏</td><td>详细解析知识蒸馏的原理和意义,并实现知识蒸馏模型的搭 建,对比知识蒸馏后的新模型的优异表现,并做详细的对比 测试</td></tr></table></body></html> ### 第八部分(项目阶段):自然语言处理项目2 配图: #### 项目流程图: #### 可解决的现实问题: 1.智能搜索优化——利用语义理解与实体关联提升搜索准确性(图谱智能搜索) 2.医疗诊断与研发——构建医疗知识图谱,支持疾病诊断及药物研发 3.高效知识存储与查询——基于Neo4j存储与查询SPO三元组数据 4.智能问答系统——结合NLU与知识图谱实现精准问答 5.异构数据整合——通过实体抽取融合多源数据构建一致知识图谱 #### 可掌握的核心竞争力: 1.图谱全流程管理——掌握从数据获取到图谱构建的全生命周期。 2.图谱理论与应用——熟悉知识图谱定义、类型及多领域应用。 3.Neo4j技术——精通图数据库的存储与查询优化。 4.深度学习NER——掌握CRF、BERT、BiLSTM+CRF 等实体识别方法。 5.多样化关系抽取 —熟练运用规则、Pipeline与Casrel模型提取关系。 6.知识融合与消歧 一掌握实体消歧和关系对齐技术。 7.医疗问答开发 实现意图识别与对话管理的智能问答系统。 8.NLP语义处理—精通自然语言理解(NLU)与生成(NLG)。 9.模型工程化——掌握数据预处理与模型训练全流程。 10.垂直领域图谱——构建医疗、商业等领域的定制知识图谱。 <html><body><table><tr><td>模块</td><td>技术要点</td></tr><tr><td>知识图谱项目简介</td><td>1.项目背景介绍 2.知识图谱技术概括</td></tr><tr><td>项目工具介绍</td><td>1.项目应用工具概览 2.doccano 数据标注平台 3.知识查询语言 4.Neo4j图数据库</td></tr><tr><td>实体抽取</td><td>1.实体识别基本知识 2.基于规则实现NER 3.基于深度学习模型实现NER 4.Viterbi 算法介绍</td></tr><tr><td>关系抽取</td><td>1.关系抽取基本知识 2.规则方法实现关系抽 3.Pipeline方法实现关系抽取 4.Joint方法实现关系抽取</td></tr><tr><td>知识融合</td><td>1.知识融合基本知识 2.实体消歧任务</td></tr><tr><td>图谱搭建</td><td>1.Neo4j图数据库 2.医疗知识图谱的搭建</td></tr><tr><td>图谱应用</td><td>1.问答系统基础知识 2.问答系统关键技术 3.医疗KBQA系统架构 4.医疗KBQA系统实现</td></tr></table></body></html> ### 第九部分(项目阶段):大模型开发基础与项目 #### 可解决的现实问题: 了解大语言模型的主要方法与主要架构; 了解主流大模型; 理解向量知识库的基本概念和技术原理; 掌握Langchain开发框架;掌握RAG检索增强技术; 掌握大模型主要微调方法; 掌握大模型评价指标及模型部署上线 #### 可掌握的核心竞争力: 1.大模型 Prompt-Engineering 实践 2.基于Functioncall打造个人专属助手 3.基于AIAgent实现邮件的自动编写及发送 4.物流行业信息咨询智能问答系统(RAG检索) 5.基于GPT2模型搭建医疗问诊机器人 6.新零售行业决策评价系统 7.新媒体行业评论智能分类与信息抽取系统 <html><body><table><tr><td>模块</td><td>技术要点</td></tr><tr><td>大模型背景简介</td><td>1.大语言模型介绍 2.语言模型的基本定义 3.N-Gram 语言模型详解 4.transformer预训练和大语言模型 5.bleu 评估指标 6.rouge 评估指标 7.ppl 困惑度 8.大语言模型的主要类别 9.AE自编码模型-bert介绍 10.AR 自回归模型-gpt介绍</td></tr><tr><td>主流大模型介绍</td><td>11.Encoder-Decoder 模型-T5 介绍 1.ChatGPT的发展史 2.ChatGPT模型训练的原理 3.ChatGLM-6B模型原理详解 4.LLaMA模型原理详解 5.BLOOM模型的原理详解</td></tr><tr><td>大模型prompt-Tuning 技术</td><td>6.BaiChuan 模型的原理详解 1.大模型Prompt工程指南</td></tr></table></body></html> <html><body><table><tr><td></td><td>2.金融行业动态方问评估项自介绍 3.LLM实现金融文本分类 4.LLM实现金融文本信息抽取 5.LLM 实现金融文本匹配 1.LangChain 的概念介绍</td></tr><tr><td>LangChain 大模型应用框架</td><td>2.Models 组件的介绍 3.LLMs_MOdels 模型组件的使用 4.Chat_Models 模型组件的使用 5.Embedding_Models 模型组件的使用 6. Prompt-ZeroShot方法介绍 7. Prompt-FewShot 方法介绍 8.SimpleSequnetialChain 组件的应用实践 9.data_loader文档加载器的介绍 10.document_splitter 文档分割器的介绍 11.vector_database向量数据库介绍</td></tr><tr><td>大模型在金融业务领域的实践</td><td>12.LangChain 检索器的应用 1.提示词工程简介 2.清洗的指令形式构造方法 3.文本参考和拆分子任务 4.给模型思考和借助外部工具方法 5.Few-shot 和 Zero-shot 思想 6.基于ChatGLM实现金融文本分类 7.基于ChatGLM实现金融信息抽取</td></tr><tr><td>基于GPT2模型搭建聊天机器人</td><td>8.基于ChatGLM实现金融文本匹配 1.GPT2实现聊天机器人项目 2.基于Flask开发网页界面</td></tr><tr><td>新零售行业评价决策系统实战 基于大模型联合任务开发应用实战</td><td>1.实现新零售文本评价项目 2.PET方法 3.BERT+PET 模型 5.BERT+P-Tuning 1.基于ChatGLM+Lora 模型开发应用 4.P-Tuning</td></tr></table></body></html>
📁 edu_document_loaders/
📄 __init__.py 224B
from rag_qa.edu_document_loaders.edu_docloader import * from rag_qa.edu_document_loaders.edu_pptloader import * from rag_qa.edu_document_loaders.edu_imgloader import * from rag_qa.edu_document_loaders.edu_pdfloader import *
📄 demo_markdown.py 976B
""" TODO 温馨提示: 如果报错:zipfile.BadZipFile: File is not a zip file 则需要拷贝资料下的nltk_data到{项目的虚拟环境}/share目录下。 比如:我的项目环境是rag。目录是:C:\Programming\Anaconda3\envs\rag 则我需要拷贝到:C:\Programming\Anaconda3\envs\rag\share目录下。 """ # TODO 1.官网链接 # https://reference.langchain.com/python/integrations/langchain_unstructured/ from langchain_community.document_loaders import UnstructuredMarkdownLoader # 准备文件 mk_file_path = "D:/code/workspace7/integrated_qa_system/rag_qa/data/ai_data/人工智能就业课课程大纲.md" # 创建加载器对象 loader = UnstructuredMarkdownLoader(file_path=mk_file_path) print("loader-->", loader) docs = [] # 加载文件 docs_lazy = loader.lazy_load() # 遍历 for doc in docs_lazy: # print('doc-->', doc) docs.append(doc) print('docs[0].content-->', docs[0].page_content) print('docs[0].metadata-->', docs[0].metadata)
📄 edu_docloader.py 5.5KB
from typing import Iterator from rag_qa.edu_document_loaders.edu_ocr import get_ocr # 导入必要的模块 from tqdm import tqdm from docx.table import _Cell, Table # 用于处理表格 from docx.oxml.table import CT_Tbl # 用于处理表格XML结构 from docx.oxml.text.paragraph import CT_P # 用于处理段落XML结构 from docx.text.paragraph import Paragraph # 用于处理段落内容 from docx import Document as Docu1 from docx.document import Document as Docu2 from docx import ImagePart # 用于处理Word文档和图片 from PIL import Image # 用于处理图片 from io import BytesIO # 用于将字节流转换为图片 import numpy as np # 用于处理数组 from langchain_core.documents import Document from langchain_core.document_loaders import BaseLoader class OCRDOCLoader(BaseLoader): """An example document loader that reads a file line by line.""" def __init__(self, filepath: str) -> None: """Initialize the loader with a file path. Args: filepath_path: The path to the filepath to load. """ self.filepath = filepath def lazy_load(self) -> Iterator[Document]: # <-- Does not take any arguments """A lazy loader that reads a file line by line. When you're implementing lazy load methods, you should use a generator to yield documents one by one. """ line = self.doc2text(self.filepath) yield Document(page_content=line, metadata={"source": self.filepath}) def doc2text(self, filepath): # 创建OCR识别对象 ocr = get_ocr() # print(f'ocr--》{ocr}') # 输出OCR对象信息 # 读取Word文档 doc = Docu1(filepath) # print(f'doc-->{doc}') # 输出读取到的文档信息 # 定义一个空字符串用于存储最终的文本内容 resp = "" # 定义一个迭代器,用于遍历文档中的块(段落、表格等) def iter_block_items(parent): # 判断parent对象类型,如果是Document类型,则获取其元素 if isinstance(parent, Docu2): parent_elm = parent.element.body # 如果是表格单元格类型,获取单元格的XML元素 elif isinstance(parent, _Cell): parent_elm = parent._tc else: raise ValueError("OCRDOCLoader parse fail") # 如果都不是,则抛出错误 # print(f'parent_elm--》{parent_elm}') # print('*'*80) # 遍历parent_elm中的所有子元素 for child in parent_elm.iterchildren(): # print(f'child--》{child}') if isinstance(child, CT_P): # 如果是段落类型 yield Paragraph(child, parent) # 返回段落 elif isinstance(child, CT_Tbl): # 如果是表格类型 yield Table(child, parent) # 返回表格 # print(f'doc.paragraphs-->{doc.paragraphs}') # print(f'doc.tables-->{doc.tables}') # 创建进度条,表示文档处理的进度 b_unit = tqdm(total=len(doc.paragraphs) + len(doc.tables), desc="OCRDOCLoader block index: 0") # 遍历文档中的所有块(段落和表格) for i, block in enumerate(iter_block_items(doc)): # 更新进度条描述 b_unit.set_description("OCRDOCLoader block index: {}".format(i)) b_unit.refresh() # 刷新进度条 # 如果块是段落类型 if isinstance(block, Paragraph): resp += block.text.strip() + "\n" # 将段落文本加入到返回字符串中 # 获取段落中的所有图片 images = block._element.xpath('.//pic:pic') for image in images: # 遍历图片,获取图片ID for img_id in image.xpath('.//a:blip/@r:embed'): part = doc.part.related_parts[img_id] # 根据图片ID获取图片对象 if isinstance(part, ImagePart): # 如果该部分是图片 # BytesIO 是 Python 内置的 io 模块中的一个类,用于在内存中读写二进制数据 # part._blob 通常表示从某个文档(如 DOCX 文件)中提取的二进制内容。 image = Image.open(BytesIO(part._blob)) # 打开图片 result, _ = ocr(np.array(image)) # 使用OCR识别图片中的文字 if result: # 如果识别结果不为空 ocr_result = [line[1] for line in result] # 提取识别出的文字 resp += "\n".join(ocr_result) # 将识别结果加入返回文本中 # 如果块是表格类型 elif isinstance(block, Table): # 遍历表格中的所有行和单元格 for row in block.rows: for cell in row.cells: for paragraph in cell.paragraphs: resp += paragraph.text.strip() + "\n" # 将单元格内的段落文本加入返回文本中 # 更新进度条 b_unit.update(1) # 返回提取的文本内容 return resp if __name__ == '__main__': docx_loader = OCRDOCLoader(filepath='D:/code/workspace7/integrated_qa_system/rag_qa/data/samples/ocr_02.docx') doc = docx_loader.load() for i in doc: print(i.page_content)
📄 edu_imgloader.py 1.4KB
from typing import Iterator from rag_qa.edu_document_loaders.edu_ocr import get_ocr from langchain_core.documents import Document from langchain_core.document_loaders import BaseLoader class OCRIMGLoader(BaseLoader): """An example document loader that reads a file line by line.""" def __init__(self, img_path: str) -> None: """Initialize the loader with a file path. Args: img_path: The path to the img to load. """ self.img_path = img_path def lazy_load(self) -> Iterator[Document]: # <-- Does not take any arguments """A lazy loader that reads a file line by line. When you're implementing lazy load methods, you should use a generator to yield documents one by one. """ line = self.img2text() yield Document(page_content=line, metadata={"source": self.img_path}) def img2text(self): resp = "" ocr = get_ocr() result, _ = ocr(self.img_path) if result: ocr_result = [line[1] for line in result] resp += "\n".join(ocr_result) return resp if __name__ == '__main__': img_loader = OCRIMGLoader(img_path='D:/code/workspace7/integrated_qa_system/rag_qa/data/samples/ocr_04.png') docs = img_loader.load() for doc in docs: for page in doc.page_content.split("\n"): print(page)
📄 edu_ocr.py 1.2KB
''' paddleOCR:解析图片中的文字,也可以进行表格识别 rapidocr_paddle 和 rapidocr_onnxruntime 两种导入方式 主要区别在于它们所使用的推理引擎和硬件支持 选择哪种方式最合适取决于你的硬件环境和性能需求。 当你有 GPU 且追求速度时:使用 rapidocr_paddle。PaddlePaddle 原生支持在 GPU 上推理 PaddleOCR 模型,速度更快。 当只有 CPU 且需要高效推理时:使用 rapidocr_onnxruntime。它在 CPU 上进行了优化,资源占用较低. ''' def get_ocr(use_cuda: bool = True) -> "RapidOCR": try: from rapidocr_paddle import RapidOCR ''' det_use_cuda=True:启用检测模型的GPU加速。cls_use_cuda=True:启用分类模型的GPU加速。rec_use_cuda=True:启用识别模型的GPU加速。 ''' ocr = RapidOCR(det_use_cuda=use_cuda, cls_use_cuda=use_cuda, rec_use_cuda=use_cuda) except ImportError: # from rapidocr_onnxruntime import RapidOCR ocr = RapidOCR() return ocr if __name__ == '__main__': model = get_ocr() result, data = model("D:/code/workspace7/integrated_qa_system/rag_qa/data/samples/ocr_04.png") for each in result: print(each)
📄 edu_pdfloader.py 5.4KB
import cv2 import fitz # pyMuPDF里面的fitz包,不要与pip install fitz混淆 import numpy as np from PIL import Image from langchain_text_splitters import CharacterTextSplitter from tqdm import tqdm from typing import Iterator from rag_qa.edu_document_loaders.edu_docloader import get_ocr from langchain_core.documents import Document from langchain_core.document_loaders import BaseLoader # PDF OCR 控制:只对宽高超过页面一定比例(图片宽/页面宽,图片高/页面高)的图片进行 OCR。 # 这样可以避免 PDF 中一些小图片的干扰,提高非扫描版 PDF 处理速度 PDF_OCR_THRESHOLD = (0.6, 0.6) class OCRPDFLoader(BaseLoader): """An example document loader that reads a file line by line.""" def __init__(self, file_path: str) -> None: """Initialize the loader with a file path. Args: file_path: The path to the file to load. """ self.file_path = file_path def lazy_load(self) -> Iterator[Document]: # <-- Does not take any arguments """A lazy loader that reads a file line by line. When you're implementing lazy load methods, you should use a generator to yield documents one by one. """ line = self.pdf2text() yield Document(page_content=line, metadata={"source": self.file_path}) def pdf2text(self): ocr = get_ocr() # 打开pdf文件 doc = fitz.open(self.file_path) ## 获取页数 # print(f'len(doc)-->{len(doc)}') resp = "" b_unit = tqdm(total=doc.page_count, desc="OCRPDFLoader context page index: 0") for i, page in enumerate(doc): b_unit.set_description("OCRPDFLoader context page index: {}".format(i)) b_unit.refresh() # 提取文本:默认使用 "text" 模式提取文本。 text = page.get_text() resp += text + "\n" # print(f'resp-->{resp}') # 获取图片:获得所有显示的图像的元信息列表。 # 它适用于所有文档类型,不仅限于 PDF。 img_list = page.get_image_info(xrefs=True) # print(f'img_list--》{img_list}') # print(f'img_list--》{len(img_list)}') for img in img_list: # xref一种编号,指向该图像对象在PDF文件中的位置,程序可以通过这个编号快速定位和提取图像数据。 if xref := img.get("xref"): # 图像在页面上的位置和尺寸。 bbox = img["bbox"] # 检查图片尺寸是否超过设定的阈值 if ((bbox[2] - bbox[0]) / (page.rect.width) < PDF_OCR_THRESHOLD[0] or (bbox[3] - bbox[1]) / (page.rect.height) < PDF_OCR_THRESHOLD[1]): continue pix = fitz.Pixmap(doc, xref) # print(f'page.rotation-->{page.rotation}') if int(page.rotation) != 0: # 如果Page有旋转角度,则旋转图片 img_array = np.frombuffer(pix.samples, dtype=np.uint8).reshape(pix.height, pix.width, -1) tmp_img = Image.fromarray(img_array) ori_img = cv2.cvtColor(np.array(tmp_img), cv2.COLOR_RGB2BGR) rot_img = self.rotate_img(img=ori_img, angle=360 - page.rotation) img_array = cv2.cvtColor(rot_img, cv2.COLOR_RGB2BGR) else: img_array = np.frombuffer(pix.samples, dtype=np.uint8).reshape(pix.height, pix.width, -1) # result:包含了图像中检测到的所有文本框的位置、文本内容和置信度信息。 # _:它是一个包含了时间数据的列表,可以用于优化模型运行速度。 result, _ = ocr(img_array) if result: ocr_result = [line[1] for line in result] resp += "\n".join(ocr_result) # 更新进度 b_unit.update(1) return resp def rotate_img(self, img, angle): ''' img --image angle --rotation angle return--rotated img ''' h, w = img.shape[:2] rotate_center = (w / 2, h / 2) # 获取旋转矩阵 # 参数1为旋转中心点; # 参数2为旋转角度,正值-逆时针旋转;负值-顺时针旋转 # 参数3为各向同性的比例因子,1.0原图,2.0变成原来的2倍,0.5变成原来的0.5倍 M = cv2.getRotationMatrix2D(rotate_center, angle, 1.0) # 计算图像新边界 new_w = int(h * np.abs(M[0, 1]) + w * np.abs(M[0, 0])) new_h = int(h * np.abs(M[0, 0]) + w * np.abs(M[0, 1])) # 调整旋转矩阵以考虑平移 M[0, 2] += (new_w - w) / 2 M[1, 2] += (new_h - h) / 2 rotated_img = cv2.warpAffine(img, M, (new_w, new_h)) return rotated_img if __name__ == '__main__': pdf_loader = OCRPDFLoader( file_path="D:/code/workspace7/integrated_qa_system/rag_qa/data/ai_data/LLM基础知识.pdf") doc = pdf_loader.load() print("doc-->", doc) # print(type(doc)) # for doc in doc: # print(doc.page_content) text_spliter = CharacterTextSplitter(chunk_size=300, chunk_overlap=20) result = text_spliter.split_documents(doc) print(len(result)) print(result[0])
📄 edu_pptloader.py 4.4KB
from typing import Iterator from rag_qa.edu_document_loaders.edu_ocr import get_ocr from langchain_core.documents import Document from langchain_core.document_loaders import BaseLoader from pptx import Presentation from PIL import Image import numpy as np from io import BytesIO from tqdm import tqdm class OCRPPTLoader(BaseLoader): """An example document loader that reads a file line by line.""" def __init__(self, filepath: str) -> None: """Initialize the loader with a file path. Args: filepath: The path to the ppt to load. """ self.filepath = filepath def lazy_load(self) -> Iterator[Document]: # <-- Does not take any arguments """A lazy loader that reads a file line by line. When you're implementing lazy load methods, you should use a generator to yield documents one by one. """ line = self.ppt2text(self.filepath) yield Document(page_content=line, metadata={"source": self.filepath}) def ppt2text(self, filepath): # 打开指定路径的 PowerPoint 文件 prs = Presentation(filepath) print(f'prs-->{prs}') # 获取 OCR 功能的实例 ocr = get_ocr() # 初始化一个空字符串,用于存储提取的文本内容 resp = "" def extract_text(shape): # nonlocal指明resp非全局非局部,而是外部嵌套函数中的变量, # 允许内部函数访问和修改外部函数中定义的变量resp nonlocal resp # 检查形状是否有文本框 if shape.has_text_frame: # 将文本框中的文本添加到resp中,并去掉前后空格 resp += shape.text.strip() + "\n" # 检查形状是否为表格 if shape.has_table: # 遍历表格的每一行 for row in shape.table.rows: # 遍历每一行中的每个单元格 for cell in row.cells: # 遍历单元格中的每个段落 for paragraph in cell.text_frame.paragraphs: # 将单元格中的文本添加到resp中,并去掉前后空格 resp += paragraph.text.strip() + "\n" # 检查形状是否为图片(shape_type == 13) if shape.shape_type == 13: # 13 表示图片 # 使用 BytesIO 打开图片数据并转换为图像对象 image = Image.open(BytesIO(shape.image.blob)) # 使用 OCR 处理图像并获取结果 result, _ = ocr(np.array(image)) if result: # 如果 OCR 有结果 # 提取 OCR 结果中的文本行 ocr_result = [line[1] for line in result] # 将 OCR 提取的文本添加到resp中,以换行分隔 resp += "\n".join(ocr_result) # 检查形状是否为组合形状(shape_type == 6) elif shape.shape_type == 6: # 6 表示组合 # 遍历组合形状中的每个子形状,递归调用extract_text函数 for child_shape in shape.shapes: extract_text(child_shape) # 创建一个进度条,用于显示幻灯片处理进度,初始总数为幻灯片数量 b_unit = tqdm(total=len(prs.slides), desc="OCRPPTLoader slide index: 1") # 遍历所有幻灯片 for slide_number, slide in enumerate(prs.slides, start=1): # 更新进度条描述,显示当前处理的幻灯片索引 b_unit.set_description("OCRPPTLoader slide index: {}".format(slide_number)) b_unit.refresh() # 刷新进度条显示 # 按照从上到下、从左到右的顺序对形状进行排序遍历 sorted_shapes = sorted(slide.shapes, key=lambda x: (x.top, x.left)) for shape in sorted_shapes: extract_text(shape) # 调用extract_text函数提取当前形状的文本内容 b_unit.update(1) # 更新进度条,表示处理了一张幻灯片 return resp # 返回提取到的所有文本内容 if __name__ == '__main__': img_loader = OCRPPTLoader(filepath='D:/code/workspace7/integrated_qa_system/rag_qa/data/samples/ocr_01.pptx') doc = img_loader.load() for d in doc: print(d.page_content) print(d.metadata) print("-" * 50)
📁 edu_text_spliter/
📄 __init__.py 88B
from .edu_model_text_spliter import * from .edu_chinese_recursive_text_splitter import *
📄 edu_chinese_recursive_text_splitter.py 6.4KB
import re from typing import List, Optional, Any from langchain_text_splitters import RecursiveCharacterTextSplitter, CharacterTextSplitter import logging logger = logging.getLogger(__name__) def _split_text_with_regex_from_end(text: str, separator: str, keep_separator: bool) -> List[str]: # Now that we have the separator, split the text if separator: if keep_separator: # The parentheses in the pattern keep the delimiters in the result. _splits = re.split(f"({separator})", text) splits = ["".join(i) for i in zip(_splits[0::2], _splits[1::2])] if len(_splits) % 2 == 1: splits += _splits[-1:] # splits = [_splits[0]] + splits else: splits = re.split(separator, text) else: splits = list(text) return [s for s in splits if s != ""] class ChineseRecursiveTextSplitter(RecursiveCharacterTextSplitter): def __init__( self, separators: Optional[List[str]] = None, keep_separator: bool = True, is_separator_regex: bool = True, **kwargs: Any, ) -> None: """Create a new TextSplitter.""" super().__init__(keep_separator=keep_separator, **kwargs) self._separators = separators or [ "\n\n", "\n", "。|!|?", "\.\s|\!\s|\?\s", ";|;\s", ",|,\s" ] self._is_separator_regex = is_separator_regex def _split_text(self, text: str, separators: List[str]) -> List[str]: """Split incoming text and return chunks.""" final_chunks = [] # Get appropriate separator to use separator = separators[-1] new_separators = [] for i, _s in enumerate(separators): _separator = _s if self._is_separator_regex else re.escape(_s) if _s == "": separator = _s break if re.search(_separator, text): separator = _s new_separators = separators[i + 1:] break _separator = separator if self._is_separator_regex else re.escape(separator) splits = _split_text_with_regex_from_end(text, _separator, self._keep_separator) # Now go merging things, recursively splitting longer texts. _good_splits = [] _separator = "" if self._keep_separator else separator for s in splits: if self._length_function(s) < self._chunk_size: _good_splits.append(s) else: if _good_splits: merged_text = self._merge_splits(_good_splits, _separator) final_chunks.extend(merged_text) _good_splits = [] if not new_separators: final_chunks.append(s) else: other_info = self._split_text(s, new_separators) final_chunks.extend(other_info) if _good_splits: merged_text = self._merge_splits(_good_splits, _separator) final_chunks.extend(merged_text) return [re.sub(r"\n{2,}", "\n", chunk.strip()) for chunk in final_chunks if chunk.strip() != ""] if __name__ == "__main__": text_splitter = ChineseRecursiveTextSplitter( keep_separator=True, is_separator_regex=True, chunk_size=150, chunk_overlap=10 ) char_splitter = CharacterTextSplitter(chunk_size=150, chunk_overlap=10) doc = [ """中国对外贸易形势报告(75页)。前 10 个月,一般贸易进出口 19.5 万亿元,增长 25.1%, 比整体进出口增速高出 2.9 个百分点,占进出口总额的 61.7%,较去年同期提升 1.6 个百分点。其中,一般贸易出口 10.6 万亿元,增长 25.3%,占出口总额的 60.9%,提升 1.5 个百分点;进口8.9万亿元,增长24.9%,占进口总额的62.7%, 提升 1.8 个百分点。加工贸易进出口 6.8 万亿元,增长 11.8%, 占进出口总额的 21.5%,减少 2.0 个百分点。其中,出口增 长 10.4%,占出口总额的 24.3%,减少 2.6 个百分点;进口增 长 14.2%,占进口总额的 18.0%,减少 1.2 个百分点。此外, 以保税物流方式进出口 3.96 万亿元,增长 27.9%。其中,出 口 1.47 万亿元,增长 38.9%;进口 2.49 万亿元,增长 22.2%。前三季度,中国服务贸易继续保持快速增长态势。服务 进出口总额 37834.3 亿元,增长 11.6%;其中服务出口 17820.9 亿元,增长 27.3%;进口 20013.4 亿元,增长 0.5%,进口增 速实现了疫情以来的首次转正。服务出口增幅大于进口 26.8 个百分点,带动服务贸易逆差下降 62.9%至 2192.5 亿元。服 务贸易结构持续优化,知识密集型服务进出口 16917.7 亿元, 增长 13.3%,占服务进出口总额的比重达到 44.7%,提升 0.7 个百分点。 二、中国对外贸易发展环境分析和展望 全球疫情起伏反复,经济复苏分化加剧,大宗商品价格 上涨、能源紧缺、运力紧张及发达经济体政策调整外溢等风 险交织叠加。同时也要看到,我国经济长期向好的趋势没有 改变,外贸企业韧性和活力不断增强,新业态新模式加快发 展,创新转型步伐提速。产业链供应链面临挑战。美欧等加快出台制造业回迁计 划,加速产业链供应链本土布局,跨国公司调整产业链供应 链,全球双链面临新一轮重构,区域化、近岸化、本土化、 短链化趋势凸显。疫苗供应不足,制造业“缺芯”、物流受限、 运价高企,全球产业链供应链面临压力。 全球通胀持续高位运行。能源价格上涨加大主要经济体 的通胀压力,增加全球经济复苏的不确定性。世界银行今年 10 月发布《大宗商品市场展望》指出,能源价格在 2021 年 大涨逾 80%,并且仍将在 2022 年小幅上涨。IMF 指出,全 球通胀上行风险加剧,通胀前景存在巨大不确定性。""", ] # text = """""" print("ChineseRecursiveTextSplitter") for inum, text in enumerate(doc): print(inum) chunks = text_splitter.split_text(text) for chunk in chunks: print(f"chunk size [{len(chunk)}] : {chunk}") print("CharacterTextSplitter") for inum, text in enumerate(doc): chunks = char_splitter.split_text(text) for chunk in chunks: print(f"chunk size [{len(chunk)}] : {chunk}")
📄 edu_model_text_spliter.py 2.4KB
""" 使用nlp_bert_document-segmentation_chinese-base模型,实现文本的语义级别分割 """ from langchain_text_splitters import CharacterTextSplitter import re from typing import List from modelscope.pipelines import pipeline class AliTextSplitter(CharacterTextSplitter): def __init__(self, pdf: bool = False, **kwargs): super().__init__(**kwargs) self.pdf = pdf def split_text(self, text: str) -> List[str]: # use_document_segmentation参数指定是否用语义切分文档,此处采取的文档语义分割模型为达摩院开源的nlp_bert_document-segmentation_chinese-base,论文见https://arxiv.org/abs/2107.09278 # 如果使用模型进行文档语义切分,那么需要安装modelscope[nlp]:pip install "modelscope[nlp]" -f https://modelscope.oss-cn-beijing.aliyuncs.com/releases/repo.html # 考虑到使用了三个模型,可能对于低配置gpu不太友好,因此这里将模型load进cpu计算,有需要的话可以替换device为自己的显卡id if self.pdf: text = re.sub(r"\n{3,}", r"\n", text) text = re.sub('\s', " ", text) text = re.sub("\n\n", "", text) p = pipeline( task="document-segmentation", model='D:/code/workspace7/integrated_qa_system/rag_qa/models/nlp_bert_document-segmentation_chinese-base', device="cpu") result = p(documents=text) sent_list = [i for i in result["text"].split("\n\t") if i] return sent_list if __name__ == '__main__': model_split = AliTextSplitter() result = model_split.split_text( text='移动端语音唤醒模型,检测关键词为“小云小云”。模型主体为4层FSMN结构,使用CTC训练准则,参数量750K,适用于移动端设备运行。模型输入为Fbank特征,输出为基于char建模的中文全集token预测,测试工具根据每一帧的预测数据进行后处理得到输入音频的实时检测结果。模型训练采用“basetrain + finetune”的模式,basetrain过程使用大量内部移动端数据,在此基础上,使用1万条设备端录制安静场景“小云小云”数据进行微调,得到最终面向业务的模型。后续用户可在basetrain模型基础上,使用其他关键词数据进行微调,得到新的语音唤醒模型,但暂时未开放模型finetune功能。') for chunk in result: print(f"chunk size [{len(chunk)}]: {chunk}")
📁 rag_assessment/
- ⚙️ img.png 27.4KB
📄 ollama_ragas.py 3.3KB
# 导入pandas库,用于数据处理和保存CSV文件 import pandas as pd # 导入ragas库的evaluate函数,用于执行RAG评估 from ragas import evaluate # 导入ragas的评估指标,包括忠实度、答案相关性、上下文相关性和上下文召回率 from ragas.metrics import ( faithfulness, answer_relevancy, context_precision, context_recall ) # 导入datasets库的Dataset类,用于构建RAGAS所需的数据格式 from datasets import Dataset # 导入langchain_community的Ollama聊天模型和嵌入模型,用于本地模型调用 from langchain_community.chat_models import ChatOllama from langchain_community.embeddings import OllamaEmbeddings # 导入json库,用于加载JSON格式的评估数据集 import json # 1. 加载生成的数据集 # 使用with语句打开JSON文件,确保文件正确关闭,指定编码为utf-8 with open("rag_evaluate_data_small.json", "r", encoding="utf-8") as f: # 将JSON文件内容加载到data变量中,data为包含多个数据条目的列表 data = json.load(f) # 2. 转换为RAGAS格式 # 创建字典eval_data,将JSON数据转换为RAGAS要求的字段格式 eval_data = { # 提取每个数据条目的question字段,组成问题列表 "question": [item["question"] for item in data], # 提取每个数据条目的answer字段,组成答案列表 "answer": [item["answer"] for item in data], # 提取每个数据条目的context字段,组成上下文列表(每个context为列表) "contexts": [item["context"] for item in data], # 提取每个数据条目的ground_truth字段,组成真实答案列表 "ground_truth": [item["ground_truth"] for item in data] } # 使用Dataset.from_dict将字典转换为RAGAS所需的Dataset对象 dataset = Dataset.from_dict(eval_data) # 3. 配置RAGAS评估环境 # 初始化ChatOllama模型,指定使用qwen2.5:0.5b模型,设置本地Ollama服务地址 llm = ChatOllama(model="qwen2.5:0.5b", base_url="http://localhost:11434") # 初始化Ollama嵌入模型,用于计算语义相似度,指定mxbai-embed-large模型 embeddings = OllamaEmbeddings(model="mxbai-embed-large:latest", base_url="http://localhost:11434") print("embeddings-->", embeddings) print("embeddings.hello-->", embeddings.embed_query('hello')) # 4. 执行评估 # 调用evaluate函数,传入数据集、评估指标、LLM模型和嵌入模型 result = evaluate( # 传入转换好的Dataset对象 dataset=dataset, # 指定使用的评估指标列表 metrics=[ faithfulness, # 忠实度:答案是否基于上下文 answer_relevancy, # 答案相关性:答案与问题的匹配度 context_precision, # 上下文相关性:上下文是否仅包含相关信息 context_recall # 上下文召回率:上下文是否包含所有必要信息 ], # 传入配置好的Ollama LLM模型 llm=llm, # 传入配置好的Ollama嵌入模型 embeddings=embeddings ) # 5. 输出并保存结果 # 打印评估结果标题 print("RAGAS评估结果:") # 打印评估结果,包含各指标的分数 print(result) # 将评估结果转换为pandas DataFrame,便于保存 result_df = pd.DataFrame([result]) # 将DataFrame保存为CSV文件,文件名为ragas_evaluation_results.csv,不保存索引 result_df.to_csv("ragas_evaluation_results2.csv", index=False)
- ⚙️ rag_evaluate_data.json 14.7KB
- ⚙️ rag_evaluate_data_small.json 2.2KB
📄 ragas_evaluate.py 3.4KB
# 导入pandas库,用于数据处理和保存CSV文件 import pandas as pd # 导入ragas库的evaluate函数,用于执行RAG评估 from ragas import evaluate # 导入ragas的评估指标,包括忠实度、答案相关性、上下文相关性和上下文召回率 from ragas.metrics import ( faithfulness, answer_relevancy, context_precision, context_recall ) # 导入datasets库的Dataset类,用于构建RAGAS所需的数据格式 from datasets import Dataset # 导入langchain_openai的嵌入模型和聊天模型,用于评估时的语义计算和推理 from langchain_openai import OpenAIEmbeddings, ChatOpenAI from langchain_community.chat_models import ChatTongyi from langchain_community.embeddings import DashScopeEmbeddings # 导入json库,用于加载JSON格式的评估数据集 import json import os from dotenv import load_dotenv, find_dotenv load_dotenv(find_dotenv()) # 1. 加载生成的数据集 # 使用with语句打开JSON文件,确保文件正确关闭,指定编码为utf-8 with open("rag_evaluate_data_small.json", "r", encoding="utf-8") as f: # 将JSON文件内容加载到data变量中,data为包含多个数据条目的列表 data = json.load(f) # 2. 转换为RAGAS格式 # 创建字典eval_data,将JSON数据转换为RAGAS要求的字段格式 eval_data = { # 提取每个数据条目的question字段,组成问题列表 "question": [item["question"] for item in data], # 提取每个数据条目的answer字段,组成答案列表 "answer": [item["answer"] for item in data], # 提取每个数据条目的context字段,组成上下文列表(每个context为列表) "contexts": [item["context"] for item in data], # 提取每个数据条目的ground_truth字段,组成真实答案列表 "ground_truth": [item["ground_truth"] for item in data] } # 使用Dataset.from_dict将字典转换为RAGAS所需的Dataset对象 dataset = Dataset.from_dict(eval_data) # 3. 配置RAGAS评估环境 # 初始化ChatOpenAI模型,指定使用gpt-4模型,并设置OpenAI API密钥 # llm = ChatOpenAI(model="gpt-3.5-turbo", openai_api_key=os.environ['OPENAI_API_KEY']) # 初始化OpenAI嵌入模型,用于计算语义相似度,设置API密钥 # embeddings = OpenAIEmbeddings(openai_api_key=os.environ['OPENAI_API_KEY']) llm = ChatTongyi(model=os.getenv("MODEL_NAME"), api_key=os.getenv("DASHSCOPE_API_KEY")) embeddings = DashScopeEmbeddings() # 4. 执行评估 # 调用evaluate函数,传入数据集、评估指标、LLM模型和嵌入模型 result = evaluate( # 传入转换好的Dataset对象 dataset=dataset, # 指定使用的评估指标列表 metrics=[ faithfulness, # 忠实度:答案是否基于上下文 answer_relevancy, # 答案相关性:答案与问题的匹配度 context_precision, # 上下文相关性:上下文是否仅包含相关信息 context_recall # 上下文召回率:上下文是否包含所有必要信息 ], # 传入配置好的LLM模型 llm=llm, # 传入配置好的嵌入模型 embeddings=embeddings ) # 5. 输出并保存结果 # 打印评估结果标题 print("RAGAS评估结果:") # 打印评估结果,包含各指标的分数 print(result) # 将评估结果转换为pandas DataFrame,便于保存 result_df = pd.DataFrame([result]) # 将DataFrame保存为CSV文件,文件名为ragas_evaluation_results.csv,不保存索引 result_df.to_csv("ragas_evaluation_results1.csv", index=False)
- ⚙️ ragas_evaluation_results.csv 478.8KB
📄 __init__.py 85B
from .core.vector_store import VectorStore from .core.new_rag_system import RAGSystem
📄 rag_main.py 7.1KB
import os from base import Config, logger from core.document_processor import process_documents # 导入处理文档的函数 from core.vector_store import VectorStore # TODO 新的rag_system模块,流式输出,正式运行用它 # from core.new_rag_system import RAGSystem # TODO 老的rag_system模块,该模块测试用它 from core.rag_system import RAGSystem from openai import OpenAI # 使用 OpenAI 接口 conf = Config() def main(query_mode=True, directory_path="data"): # 初始化 DashScope API 客户端 (通过 OpenAI 接口) # 确保环境变量 DASHSCOPE_API_KEY 和 DASHSCOPE_BASE_URL 已设置 try: client = OpenAI(api_key=conf.DASHSCOPE_API_KEY, base_url=conf.DASHSCOPE_BASE_URL) except Exception as e: logger.error(f"初始化 OpenAI 客户端失败 (请检查 API Key 和 Base URL): {e}") # 如果客户端初始化失败,可能无法继续,取决于模式 if query_mode: # 查询模式下必须要有 LLM print("错误:无法初始化语言模型客户端,无法进入查询模式。") return # 数据处理模式可能不需要 LLM,可以继续,但最好记录错误 client = None # 标记客户端不可用 # 定义 LLM 调用函数 (仅在需要时定义和使用) def call_dashscope(prompt): if not client: # 检查客户端是否可用 logger.error("LLM 客户端未初始化,无法调用 call_dashscope") return f"错误: LLM客户端不可用" try: completion = client.chat.completions.create( model=conf.LLM_MODEL, messages=[ {"role": "system", "content": "你是一个有用的助手."}, {"role": "user", "content": prompt}, ] # 可以添加 temperature 等参数 ) if completion.choices and completion.choices[0].message: return completion.choices[0].message.content else: logger.error("LLM API 调用返回无效响应或空消息") return "错误: LLM返回无效响应" except Exception as e: logger.error(f"LLM API (call_dashscope) 调用失败: {e}") return f"错误: 调用LLM失败 - {e}" # 初始化 VectorStore try: vector_store = VectorStore( collection_name=conf.MILVUS_COLLECTION_NAME, host=conf.MILVUS_HOST, port=conf.MILVUS_PORT, database=conf.MILVUS_DATABASE_NAME, ) except Exception as e: logger.error(f"初始化 VectorStore 失败 (请检查 Milvus 连接配置): {e}") print("错误:无法连接到向量数据库,程序无法继续。") return # 根据模式执行不同操作 if not query_mode: # --- 数据处理模式 --- logger.info("进入数据处理模式...") total_chunks_added = 0 for source_dir in conf.VALID_SOURCES: dir_path = os.path.join(directory_path, f"{source_dir}_data") if os.path.exists(dir_path): logger.info(f"开始处理目录: {dir_path}") try: chunks = process_documents( dir_path, conf.PARENT_CHUNK_SIZE, conf.CHILD_CHUNK_SIZE, conf.CHUNK_OVERLAP, ) if chunks: vector_store.add_documents(chunks) total_chunks_added += len(chunks) logger.info(f"成功处理目录 {dir_path},添加了 {len(chunks)} 个文档块") else: logger.info(f"目录 {dir_path} 未发现有效文档或处理结果为空") except Exception as e: logger.error(f"处理目录 {dir_path} 时出错: {e}") else: logger.warning(f"目录 {dir_path} 不存在,跳过处理") logger.info(f"数据处理完成,共添加了 {total_chunks_added} 个文档块到向量存储") else: # --- 交互式查询模式 --- if not client: # 再次检查 LLM 客户端是否必须且可用 print("错误:查询模式需要语言模型客户端,但初始化失败。") return logger.info("进入交互式查询模式...") try: rag_system = RAGSystem(vector_store, call_dashscope) except Exception as e: logger.error(f"初始化 RAGSystem 失败: {e}") print("错误:无法初始化 RAG 系统,无法进入查询模式。") return valid_sources = conf.VALID_SOURCES print("\n欢迎使用 EduRAG 交互式查询系统!") print(f"支持的学科类别:{valid_sources}") print("输入您的问题,或输入 'exit' 退出。") while True: query = input("\n请输入您的问题:") if query.lower() == "exit": logger.info("用户退出查询模式") print("再见!") break source_filter_input = input(f"请输入学科类别 ({'/'.join(valid_sources)}) (直接回车默认不过滤):").strip() source_filter = None # 默认不过滤 if source_filter_input: if source_filter_input in valid_sources: source_filter = source_filter_input logger.info(f"用户选择了学科过滤: {source_filter}") else: logger.warning( f"无效的学科类别 '{source_filter_input}',将不过滤" ) print(f"提示:输入的学科 '{source_filter_input}' 无效,将不过滤。") try: print("正在生成答案,请稍候...") answer = rag_system.generate_answer(query, source_filter=source_filter) print("-" * 30) print(f"问题: {query}") print(f"回答: {answer}") print("-" * 30) except Exception as e: logger.error(f"处理查询 '{query}' 时失败: {str(e)}") print(f"抱歉,处理您的问题时遇到了错误,请稍后重试或联系管理员。\n") if __name__ == "__main__": # 模式一:默认进入查询模式 # 若要执行数据处理,可以修改调用方式,例如: # main(query_mode=False) # 或者通过命令行参数控制 # 模式二:查询模式 import argparse parser = argparse.ArgumentParser(description="EduRAG System Main Entry Point") # store_true:表示存储bool值 parser.add_argument('--data-processing', action='store_true', help='Run in data processing mode instead of query mode.') # 字符串类型,默认值:./data/ai_data parser.add_argument('--data-dir', type=str, default='./data/ai_data', help='Path to the data directory.') args = parser.parse_args() print('args-->', args) main(query_mode=(not args.data_processing), directory_path=args.data_dir)
📁 static/
📁 src/
📄 App.jsx 6.0KB
import React, { useState, useEffect, useRef } from 'react'; const App = () => { const [query, setQuery] = useState(''); const [sourceFilter, setSourceFilter] = useState(''); const [sessionId, setSessionId] = useState(null); const [answer, setAnswer] = useState(''); const [history, setHistory] = useState([]); const [isLoading, setIsLoading] = useState(false); const answerRef = useRef(null); // 学科类别(从后端获取或硬编码) const validSources = ['数学', '物理', '化学', '生物', '历史', '地理']; // 初始化会话ID useEffect(() => { fetch('http://localhost:8000/query', { method: 'POST' }) .then(res => res.json()) .then(data => setSessionId(data.session_id)) .catch(err => console.error('初始化会话失败:', err)); }, []); // 自动滚动到答案底部 useEffect(() => { if (answerRef.current) { answerRef.current.scrollTop = answerRef.current.scrollHeight; } }, [answer]); // 提交查询 const handleSubmit = async (e) => { e.preventDefault(); if (!query.trim()) return; setIsLoading(true); setAnswer(''); const requestBody = { query, source_filter: sourceFilter || null, session_id: sessionId }; try { const response = await fetch('http://localhost:8000/query', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (response.headers.get('content-type').includes('text/event-stream')) { // 流式响应(RAG) const reader = response.body.getReader(); const decoder = new TextDecoder(); let accumulatedAnswer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n\n'); for (const line of lines) { if (line.startsWith('data: ')) { const token = line.slice(6); if (token) { accumulatedAnswer += token; setAnswer(accumulatedAnswer); } } } } } else { // JSON响应(MySQL) const data = await response.json(); setAnswer(data.answer); } // 更新历史 const historyRes = await fetch(`http://localhost:8000/history/${sessionId}`); const historyData = await historyRes.json(); setHistory(historyData.history); } catch (err) { setAnswer('错误:无法获取答案'); console.error('查询失败:', err); } finally { setIsLoading(false); } setQuery(''); setSourceFilter(''); }; // 清除历史 const handleClearHistory = async () => { try { await fetch(`http://localhost:8000/history/${sessionId}`, { method: 'DELETE' }); setHistory([]); setAnswer(''); alert('历史记录已清除'); } catch (err) { console.error('清除历史失败:', err); alert('清除历史失败'); } }; return ( <div className="max-w-3xl mx-auto"> <h1 className="text-3xl font-bold text-center mb-6">集成问答系统</h1> <p className="text-center mb-4">会话ID: {sessionId || '加载中...'}</p> {/* 输入表单 */} <div className="bg-white p-6 rounded-lg shadow-md mb-6"> <form onSubmit={handleSubmit}> <div className="mb-4"> <label className="block text-sm font-medium text-gray-700">请输入您的问题</label> <input type="text" value={query} onChange={(e) => setQuery(e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" placeholder="输入问题..." disabled={isLoading} /> </div> <div className="mb-4"> <label className="block text-sm font-medium text-gray-700">学科类别(可选)</label> <select value={sourceFilter} onChange={(e) => setSourceFilter(e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" disabled={isLoading} > <option value="">不限</option> {validSources.map((source) => ( <option key={source} value={source}>{source}</option> ))} </select> </div> <button type="submit" className="w-full bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 disabled:bg-gray-400" disabled={isLoading} > {isLoading ? '处理中...' : '提交'} </button> </form> </div> {/* 答案显示 */} {answer && ( <div className="bg-white p-6 rounded-lg shadow-md mb-6"> <h2 className="text-xl font-semibold mb-2">答案</h2> <div 喧嚣 ref={answerRef} className="max-h-96 overflow-y-auto prose prose-sm" dangerouslySetInnerHTML={{ __html: answer.replace(/\n/g, '<br>') }} ></div> </div> )} {/* 历史记录 */} {history.length > 0 && ( <div className="bg-white p-6 rounded-lg shadow-md"> <div className="flex justify-between items-center mb-4"> <h2 className="text-xl font-semibold">最近对话历史</h2> <button onClick={handleClearHistory} className="text-red-600 hover:text-red-800" > 清除历史 </button> </div> <div className="space-y-4"> {history.map((entry, idx) => ( <div key={idx}> <p className="font-medium">问: {entry.question}</p> <p className="text-gray-600">答: {entry.answer}</p> </div> ))} </div> </div> )} </div> ); }; ReactDOM.createRoot(document.getElementById('root')).render(<App />);
📄 index.html 25.0KB
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>传智教育EduRAG智慧问答系统</title> <!-- 引入 Font Awesome 图标库 --> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet"> <!-- 引入 marked.js 用于解析 Markdown --> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <style> :root { --primary-color: #4a6fa5; /* 主要颜色 */ --secondary-color: #6c757d; /* 次要颜色 */ --accent-color: #61dafb; /* 强调颜色 */ --bg-color: #f8f9fa; /* 背景颜色 */ --text-color: #343a40; /* 文本颜色 */ --border-color: #dee2e6; /* 边框颜色 */ --hover-color: #e9ecef; /* 悬停颜色 */ } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: var(--text-color); background-color: var(--bg-color); display: flex; flex-direction: column; height: 100vh; } header { background-color: var(--primary-color); color: white; padding: 1rem; text-align: center; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .container { max-width: 1200px; margin: 0 auto; padding: 1rem; display: flex; flex: 1; flex-direction: column; } .chat-container { display: flex; height: calc(100vh - 180px); border: 1px solid var(--border-color); border-radius: 8px; overflow: hidden; background-color: white; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .sidebar { width: 250px; background-color: #f1f3f5; padding: 1rem; border-right: 1px solid var(--border-color); display: flex; flex-direction: column; } .main-content { flex: 1; display: flex; flex-direction: column; position: relative; } .chat-history { flex: 1; padding: 1rem; overflow-y: auto; scroll-behavior: smooth; } .chat-input-container { padding: 1rem; border-top: 1px solid var(--border-color); background-color: white; display: flex; flex-direction: column; } .source-filter { display: flex; align-items: center; margin-bottom: 0.5rem; } .source-filter select { margin-left: 0.5rem; padding: 0.3rem; border-radius: 4px; border: 1px solid var(--border-color); } .input-group { display: flex; } .chat-input { flex: 1; padding: 0.75rem; border: 1px solid var(--border-color); border-radius: 4px 0 0 4px; font-size: 1rem; resize: none; height: 45px; max-height: 150px; overflow-y: auto; } .chat-input:focus { outline: none; border-color: var(--primary-color); } .send-btn { background-color: var(--primary-color); color: white; border: none; padding: 0 1.25rem; border-radius: 0 4px 4px 0; cursor: pointer; font-size: 1rem; transition: background-color 0.2s; } .send-btn:hover { background-color: #3a5a8c; } .send-btn:disabled { background-color: var(--secondary-color); cursor: not-allowed; } .message { margin-bottom: 1rem; display: flex; flex-direction: column; } .message-content { padding: 0.75rem 1rem; border-radius: 8px; max-width: 80%; word-break: break-word; } .user-message { align-self: flex-end; } .user-message .message-content { background-color: var(--primary-color); color: white; align-self: flex-end; } .system-message { align-self: flex-start; } .system-message .message-content { background-color: #e9ecef; align-self: flex-start; } /* 代码块样式 */ .message-content pre { background-color: #f4f4f4; padding: 1rem; border-radius: 4px; overflow-x: auto; } .message-content code { font-family: 'Courier New', Courier, monospace; font-size: 0.9rem; } .typing-indicator { display: inline-block; padding: 0.75rem 1rem; background-color: #e9ecef; border-radius: 8px; margin-bottom: 1rem; } .typing-indicator span { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background-color: var(--secondary-color); margin-right: 4px; animation: typing 1s infinite; } .typing-indicator span:nth-child(2) { animation-delay: 0.2s; } .typing-indicator span:nth-child(3) { animation-delay: 0.4s; margin-right: 0; } @keyframes typing { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } } .session-controls { display: flex; justify-content: space-between; margin-bottom: 1rem; } .new-session-btn, .clear-history-btn { background-color: var(--secondary-color); color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; font-size: 0.9rem; transition: background-color 0.2s; } .new-session-btn:hover, .clear-history-btn:hover { background-color: #5a6268; } .session-info { font-size: 0.9rem; color: var(--secondary-color); margin-top: 1rem; } .history-title { font-weight: bold; margin-bottom: 1rem; color: var(--primary-color); } .history-list { flex: 1; overflow-y: auto; } .history-item { padding: 0.75rem; border-bottom: 1px solid var(--border-color); cursor: pointer; transition: background-color 0.2s; } .history-item:hover { background-color: var(--hover-color); } .history-question { font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .history-answer { color: var(--secondary-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 0.85rem; } .loading-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(255, 255, 255, 0.8); display: flex; justify-content: center; align-items: center; z-index: 10; visibility: hidden; } .spinner { width: 40px; height: 40px; border: 4px solid rgba(0, 0, 0, 0.1); border-radius: 50%; border-left-color: var(--primary-color); animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* 响应式设计 */ @media (max-width: 768px) { .chat-container { flex-direction: column; } .sidebar { width: 100%; border-right: none; border-bottom: 1px solid var(--border-color); max-height: 200px; } } </style> </head> <body> <header> <h1>传智教育EduRAG智慧问答系统</h1> </header> <div class="container"> <div class="chat-container"> <div class="sidebar"> <!-- 会话控制按钮 --> <div class="session-controls"> <button class="new-session-btn" id="newSessionBtn"> <i class="fas fa-plus"></i> 新会话 </button> <button class="clear-history-btn" id="clearHistoryBtn"> <i class="fas fa-trash"></i> 清除历史 </button> </div> <div class="history-title">历史会话</div> <div class="history-list" id="historyList"> <!-- 历史会话将动态添加到这里 --> </div> <div class="session-info" id="sessionInfo"></div> </div> <div class="main-content"> <div class="chat-history" id="chatHistory"> <!-- 消息将动态添加到这里 --> <div class="message system-message"> <div class="message-content"> 您好!我是智能问答助手,有什么我可以帮您的吗? </div> </div> </div> <div class="chat-input-container"> <div class="source-filter"> <label for="sourceFilter">学科类别:</label> <select id="sourceFilter"> <option value="">全部</option> <!-- 学科类别选项将动态添加 --> </select> </div> <div class="input-group"> <textarea class="chat-input" id="chatInput" placeholder="请输入您的问题..." rows="1" oninput="this.style.height='auto';this.style.height=this.scrollHeight+'px'"></textarea> <button class="send-btn" id="sendBtn"> <i class="fas fa-paper-plane"></i> </button> </div> </div> <div class="loading-overlay" id="loadingOverlay"> <div class="spinner"></div> </div> </div> </div> </div> <script> // 全局变量 let currentSessionId = null; // 当前会话 ID let isResponseInProgress = false; // 是否正在处理响应 let socket = null; // WebSocket 连接 // DOM 元素 const chatHistory = document.getElementById('chatHistory'); // 聊天历史容器 const chatInput = document.getElementById('chatInput'); // 输入框 const sendBtn = document.getElementById('sendBtn'); // 发送按钮 const sourceFilter = document.getElementById('sourceFilter'); // 学科过滤器 const historyList = document.getElementById('historyList'); // 历史列表 const sessionInfo = document.getElementById('sessionInfo'); // 会话信息 const newSessionBtn = document.getElementById('newSessionBtn'); // 新会话按钮 const clearHistoryBtn = document.getElementById('clearHistoryBtn'); // 清除历史按钮 const loadingOverlay = document.getElementById('loadingOverlay'); // 加载遮罩 // API URL 前缀,根据部署环境调整 const API_BASE_URL = ''; // 如果 API 和前端在同一域,可以留空 // 页面加载完成后执行 document.addEventListener('DOMContentLoaded', async () => { // 创建新会话 await createNewSession(); // 加载学科类别 await loadSources(); // 事件监听器 sendBtn.addEventListener('click', sendMessage); // 发送按钮点击事件 chatInput.addEventListener('keydown', (e) => { // 输入框键盘事件 if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); newSessionBtn.addEventListener('click', createNewSession); // 新会话按钮点击事件 clearHistoryBtn.addEventListener('click', clearHistory); // 清除历史按钮点击事件 }); // 创建新会话 async function createNewSession() { try { const response = await fetch(`${API_BASE_URL}/api/create_session`, { method: 'POST' }); if (!response.ok) { throw new Error('创建会话失败'); } const data = await response.json(); currentSessionId = data.session_id; // 更新当前会话 ID // 更新会话信息 sessionInfo.textContent = `会话ID: ${currentSessionId.slice(0, 8)}...`; // 清空聊天历史 clearChatHistory(); // 加载会话历史 await loadHistory(); } catch (error) { console.error('创建会话错误:', error); alert('创建会话失败,请刷新页面重试'); } } // 加载学科类别 async function loadSources() { try { const response = await fetch(`${API_BASE_URL}/api/sources`); if (!response.ok) { throw new Error('获取学科类别失败'); } const data = await response.json(); // 清空现有选项 sourceFilter.innerHTML = '<option value="">全部</option>'; // 添加学科类别选项 data.sources.forEach(source => { const option = document.createElement('option'); option.value = source; option.textContent = source; sourceFilter.appendChild(option); }); } catch (error) { console.error('加载学科类别错误:', error); } } // 加载历史消息(支持多轮 QA) async function loadHistory() { if (!currentSessionId) return; try { const response = await fetch(`${API_BASE_URL}/api/history/${currentSessionId}`); if (!response.ok) { throw new Error('获取历史记录失败'); } const data = await response.json(); // 清空历史列表 historyList.innerHTML = ''; // 将历史记录按会话分组(每组最多 10 轮) const sessionItem = document.createElement('div'); sessionItem.className = 'history-item'; sessionItem.innerHTML = ` <div class="history-question">会话 ${currentSessionId.slice(0, 8)}...</div> <div class="history-answer">共 ${data.history.length} 轮对话</div> `; // 点击历史记录项显示整个会话 sessionItem.addEventListener('click', () => { // 清空现有聊天记录 clearChatHistory(); // 添加整个会话的 QA 对 data.history.forEach(item => { addMessage('user', item.question); // 添加用户问题 addMessage('system', item.answer); // 添加系统回答 }); // 滚动到底部 scrollToBottom(); }); historyList.appendChild(sessionItem); } catch (error) { console.error('加载历史记录错误:', error); } } // 清除历史记录 async function clearHistory() { if (!currentSessionId) return; try { const response = await fetch(`${API_BASE_URL}/api/history/${currentSessionId}`, { method: 'DELETE' }); if (!response.ok) { throw new Error('清除历史记录失败'); } // 清空历史列表 historyList.innerHTML = ''; // 清空聊天历史 clearChatHistory(); // 添加欢迎消息 chatHistory.innerHTML = ` <div class="message system-message"> <div class="message-content"> 历史记录已清除,有什么我可以帮您的吗? </div> </div> `; } catch (error) { console.error('清除历史记录错误:', error); alert('清除历史记录失败'); } } // 清空聊天历史 function clearChatHistory() { chatHistory.innerHTML = ` <div class="message system-message"> <div class="message-content"> 您好!我是智能问答助手,有什么我可以帮您的吗? </div> </div> `; } // 发送消息 async function sendMessage() { const query = chatInput.value.trim(); const filter = sourceFilter.value; if (!query || isResponseInProgress) return; // 重置输入框 chatInput.value = ''; chatInput.style.height = 'auto'; // 添加用户消息到聊天窗口 addMessage('user', query); // 设置正在响应标志 isResponseInProgress = true; // 添加正在输入指示器 const typingIndicator = document.createElement('div'); typingIndicator.className = 'typing-indicator'; typingIndicator.innerHTML = '<span></span><span></span><span></span>'; chatHistory.appendChild(typingIndicator); // 滚动到底部 scrollToBottom(); try { // 发送查询请求 const response = await fetch(`${API_BASE_URL}/api/query`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: query, source_filter: filter, session_id: currentSessionId }) }); if (!response.ok) { throw new Error('查询失败'); } const data = await response.json(); // 移除输入指示器 if (typingIndicator.parentNode) { chatHistory.removeChild(typingIndicator); } // 如果需要流式输出,使用 WebSocket if (data.is_streaming) { // 创建系统消息容器,但内容为空 const messageElement = document.createElement('div'); messageElement.className = 'message system-message'; const contentElement = document.createElement('div'); contentElement.className = 'message-content'; contentElement.innerHTML = ''; // 初始化为空 messageElement.appendChild(contentElement); chatHistory.appendChild(messageElement); // 建立 WebSocket 连接进行流式输出 await handleStreamingResponse(query, filter, contentElement); } else { // 直接添加格式化后的答案 addMessage('system', data.answer); } // 重新加载历史记录 await loadHistory(); } catch (error) { console.error('发送消息错误:', error); // 移除输入指示器 if (typingIndicator.parentNode) { chatHistory.removeChild(typingIndicator); } // 添加错误消息 addMessage('system', `抱歉,发生错误: ${error.message}`); } finally { // 清除正在响应标志 isResponseInProgress = false; // 滚动到底部 scrollToBottom(); } } // 处理流式响应 async function handleStreamingResponse(query, filter, contentElement) { // 关闭现有连接 if (socket && socket.readyState === WebSocket.OPEN) { socket.close(); } return new Promise((resolve, reject) => { // WebSocket URL const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${wsProtocol}//${window.location.host}${API_BASE_URL}/api/stream`; // 创建 WebSocket 连接 socket = new WebSocket(wsUrl); let accumulatedContent = ''; // 累积流式内容 // 连接建立时 socket.onopen = function() { console.log('WebSocket 连接已建立'); // 发送查询请求 socket.send(JSON.stringify({ query: query, source_filter: filter, session_id: currentSessionId })); }; // 接收消息时 socket.onmessage = function(event) { const data = JSON.parse(event.data); switch (data.type) { case 'start': console.log('流式响应开始'); break; case 'token': // 追加 token 到内容 accumulatedContent += data.token; // 使用 marked.js 解析 Markdown 格式 contentElement.innerHTML = marked.parse(accumulatedContent); scrollToBottom(); break; case 'end': console.log('流式响应结束'); socket.close(); resolve(); break; case 'error': console.error('流式响应错误:', data.error); contentElement.innerHTML += `<p>[错误: ${data.error}]</p>`; socket.close(); reject(new Error(data.error)); break; } }; // 发生错误时 socket.onerror = function(error) { console.error('WebSocket 错误:', error); contentElement.innerHTML += '<p>[连接错误]</p>'; reject(error); }; // 连接关闭时 socket.onclose = function() { console.log('WebSocket 连接已关闭'); resolve(); }; }); } // 添加消息到聊天窗口(支持 Markdown 格式) function addMessage(type, content) { const messageElement = document.createElement('div'); messageElement.className = `message ${type === 'user' ? 'user-message' : 'system-message'}`; const contentElement = document.createElement('div'); contentElement.className = 'message-content'; // 使用 marked.js 解析 Markdown 格式(支持换行和代码块) contentElement.innerHTML = marked.parse(content); messageElement.appendChild(contentElement); chatHistory.appendChild(messageElement); // 滚动到底部 scrollToBottom(); } // 滚动到底部 function scrollToBottom() { chatHistory.scrollTop = chatHistory.scrollHeight; } </script> </body> </html>
📄 old_index.html 23.5KB
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>智能问答系统</title> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet"> <style> :root { --primary-color: #4a6fa5; --secondary-color: #6c757d; --accent-color: #61dafb; --bg-color: #f8f9fa; --text-color: #343a40; --border-color: #dee2e6; --hover-color: #e9ecef; } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: var(--text-color); background-color: var(--bg-color); display: flex; flex-direction: column; height: 100vh; } header { background-color: var(--primary-color); color: white; padding: 1rem; text-align: center; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .container { max-width: 1200px; margin: 0 auto; padding: 1rem; display: flex; flex: 1; flex-direction: column; } .chat-container { display: flex; height: calc(100vh - 180px); border: 1px solid var(--border-color); border-radius: 8px; overflow: hidden; background-color: white; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .sidebar { width: 250px; background-color: #f1f3f5; padding: 1rem; border-right: 1px solid var(--border-color); display: flex; flex-direction: column; } .main-content { flex: 1; display: flex; flex-direction: column; position: relative; } .chat-history { flex: 1; padding: 1rem; overflow-y: auto; scroll-behavior: smooth; } .chat-input-container { padding: 1rem; border-top: 1px solid var(--border-color); background-color: white; display: flex; flex-direction: column; } .source-filter { display: flex; align-items: center; margin-bottom: 0.5rem; } .source-filter select { margin-left: 0.5rem; padding: 0.3rem; border-radius: 4px; border: 1px solid var(--border-color); } .input-group { display: flex; } .chat-input { flex: 1; padding: 0.75rem; border: 1px solid var(--border-color); border-radius: 4px 0 0 4px; font-size: 1rem; resize: none; height: 45px; max-height: 150px; overflow-y: auto; } .chat-input:focus { outline: none; border-color: var(--primary-color); } .send-btn { background-color: var(--primary-color); color: white; border: none; padding: 0 1.25rem; border-radius: 0 4px 4px 0; cursor: pointer; font-size: 1rem; transition: background-color 0.2s; } .send-btn:hover { background-color: #3a5a8c; } .send-btn:disabled { background-color: var(--secondary-color); cursor: not-allowed; } .message { margin-bottom: 1rem; display: flex; flex-direction: column; } .message-content { padding: 0.75rem 1rem; border-radius: 8px; max-width: 80%; word-break: break-word; } .user-message { align-self: flex-end; } .user-message .message-content { background-color: var(--primary-color); color: white; align-self: flex-end; } .system-message { align-self: flex-start; } .system-message .message-content { background-color: #e9ecef; align-self: flex-start; } .typing-indicator { display: inline-block; padding: 0.75rem 1rem; background-color: #e9ecef; border-radius: 8px; margin-bottom: 1rem; } .typing-indicator span { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background-color: var(--secondary-color); margin-right: 4px; animation: typing 1s infinite; } .typing-indicator span:nth-child(2) { animation-delay: 0.2s; } .typing-indicator span:nth-child(3) { animation-delay: 0.4s; margin-right: 0; } @keyframes typing { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } } .session-controls { display: flex; justify-content: space-between; margin-bottom: 1rem; } .new-session-btn, .clear-history-btn { background-color: var(--secondary-color); color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; font-size: 0.9rem; transition: background-color 0.2s; } .new-session-btn:hover, .clear-history-btn:hover { background-color: #5a6268; } .session-info { font-size: 0.9rem; color: var(--secondary-color); margin-top: 1rem; } .history-title { font-weight: bold; margin-bottom: 1rem; color: var(--primary-color); } .history-list { flex: 1; overflow-y: auto; } .history-item { padding: 0.75rem; border-bottom: 1px solid var(--border-color); cursor: pointer; transition: background-color 0.2s; } .history-item:hover { background-color: var(--hover-color); } .history-question { font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .history-answer { color: var(--secondary-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 0.85rem; } .loading-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(255, 255, 255, 0.8); display: flex; justify-content: center; align-items: center; z-index: 10; visibility: hidden; } .spinner { width: 40px; height: 40px; border: 4px solid rgba(0, 0, 0, 0.1); border-radius: 50%; border-left-color: var(--primary-color); animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* 响应式设计 */ @media (max-width: 768px) { .chat-container { flex-direction: column; } .sidebar { width: 100%; border-right: none; border-bottom: 1px solid var(--border-color); max-height: 200px; } } </style> </head> <body> <header> <h1>智能问答系统</h1> </header> <div class="container"> <div class="chat-container"> <div class="sidebar"> <div class="session-controls"> <button class="new-session-btn" id="newSessionBtn"> <i class="fas fa-plus"></i> 新会话 </button> <button class="clear-history-btn" id="clearHistoryBtn"> <i class="fas fa-trash"></i> 清除历史 </button> </div> <div class="history-title">历史消息</div> <div class="history-list" id="historyList"> <!-- 历史消息将动态添加到这里 --> </div> <div class="session-info" id="sessionInfo"></div> </div> <div class="main-content"> <div class="chat-history" id="chatHistory"> <!-- 消息将动态添加到这里 --> <div class="message system-message"> <div class="message-content"> 您好!我是智能问答助手,有什么我可以帮您的吗? </div> </div> </div> <div class="chat-input-container"> <div class="source-filter"> <label for="sourceFilter">学科类别:</label> <select id="sourceFilter"> <option value="">全部</option> <!-- 学科类别选项将动态添加 --> </select> </div> <div class="input-group"> <textarea class="chat-input" id="chatInput" placeholder="请输入您的问题..." rows="1" oninput="this.style.height='auto';this.style.height=this.scrollHeight+'px'"></textarea> <button class="send-btn" id="sendBtn"> <i class="fas fa-paper-plane"></i> </button> </div> </div> <div class="loading-overlay" id="loadingOverlay"> <div class="spinner"></div> </div> </div> </div> </div> <script> // 全局变量 let currentSessionId = null; let isResponseInProgress = false; let socket = null; // DOM元素 const chatHistory = document.getElementById('chatHistory'); const chatInput = document.getElementById('chatInput'); const sendBtn = document.getElementById('sendBtn'); const sourceFilter = document.getElementById('sourceFilter'); const historyList = document.getElementById('historyList'); const sessionInfo = document.getElementById('sessionInfo'); const newSessionBtn = document.getElementById('newSessionBtn'); const clearHistoryBtn = document.getElementById('clearHistoryBtn'); const loadingOverlay = document.getElementById('loadingOverlay'); // API URL前缀,根据部署环境调整 const API_BASE_URL = ''; // 如果API和前端在同一域,可以留空 // 页面加载完成后执行 document.addEventListener('DOMContentLoaded', async () => { // 创建新会话 await createNewSession(); // 加载学科类别 await loadSources(); // 事件监听器 sendBtn.addEventListener('click', sendMessage); chatInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); newSessionBtn.addEventListener('click', createNewSession); clearHistoryBtn.addEventListener('click', clearHistory); }); // 创建新会话 async function createNewSession() { try { const response = await fetch(`${API_BASE_URL}/api/create_session`, { method: 'POST' }); if (!response.ok) { throw new Error('创建会话失败'); } const data = await response.json(); currentSessionId = data.session_id; // 更新会话信息 sessionInfo.textContent = `会话ID: ${currentSessionId.slice(0, 8)}...`; // 清空聊天历史 clearChatHistory(); // 加载会话历史 await loadHistory(); } catch (error) { console.error('创建会话错误:', error); alert('创建会话失败,请刷新页面重试'); } } // 加载学科类别 async function loadSources() { try { const response = await fetch(`${API_BASE_URL}/api/sources`); if (!response.ok) { throw new Error('获取学科类别失败'); } const data = await response.json(); // 清空现有选项 sourceFilter.innerHTML = '<option value="">全部</option>'; // 添加学科类别选项 data.sources.forEach(source => { const option = document.createElement('option'); option.value = source; option.textContent = source; sourceFilter.appendChild(option); }); } catch (error) { console.error('加载学科类别错误:', error); } } // 加载历史消息 async function loadHistory() { if (!currentSessionId) return; try { const response = await fetch(`${API_BASE_URL}/api/history/${currentSessionId}`); if (!response.ok) { throw new Error('获取历史记录失败'); } const data = await response.json(); // 清空历史列表 historyList.innerHTML = ''; // 添加历史记录到侧边栏 data.history.forEach((item, index) => { const historyItem = document.createElement('div'); historyItem.className = 'history-item'; historyItem.innerHTML = ` <div class="history-question">${item.question}</div> <div class="history-answer">${item.answer.substring(0, 50)}${item.answer.length > 50 ? '...' : ''}</div> `; // 点击历史记录项显示对话 historyItem.addEventListener('click', () => { // 清空现有聊天记录 clearChatHistory(); // 添加用户问题 addMessage('user', item.question); // 添加系统回答 addMessage('system', item.answer); // 滚动到底部 scrollToBottom(); }); historyList.appendChild(historyItem); }); } catch (error) { console.error('加载历史记录错误:', error); } } // 清除历史记录 async function clearHistory() { if (!currentSessionId) return; try { const response = await fetch(`${API_BASE_URL}/api/history/${currentSessionId}`, { method: 'DELETE' }); if (!response.ok) { throw new Error('清除历史记录失败'); } // 清空历史列表 historyList.innerHTML = ''; // 清空聊天历史 clearChatHistory(); // 添加欢迎消息 chatHistory.innerHTML = ` <div class="message system-message"> <div class="message-content"> 历史记录已清除,有什么我可以帮您的吗? </div> </div> `; } catch (error) { console.error('清除历史记录错误:', error); alert('清除历史记录失败'); } } // 清空聊天历史 function clearChatHistory() { chatHistory.innerHTML = ` <div class="message system-message"> <div class="message-content"> 您好!我是智能问答助手,有什么我可以帮您的吗? </div> </div> `; } // 发送消息 async function sendMessage() { const query = chatInput.value.trim(); const filter = sourceFilter.value; if (!query || isResponseInProgress) return; // 重置输入框 chatInput.value = ''; chatInput.style.height = 'auto'; // 添加用户消息到聊天窗口 addMessage('user', query); // 设置正在响应标志 isResponseInProgress = true; // 添加正在输入指示器 const typingIndicator = document.createElement('div'); typingIndicator.className = 'typing-indicator'; typingIndicator.innerHTML = '<span></span><span></span><span></span>'; chatHistory.appendChild(typingIndicator); // 滚动到底部 scrollToBottom(); try { // 发送查询请求 const response = await fetch(`${API_BASE_URL}/api/query`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: query, source_filter: filter, session_id: currentSessionId }) }); if (!response.ok) { throw new Error('查询失败'); } const data = await response.json(); // 移除输入指示器 if (typingIndicator.parentNode) { chatHistory.removeChild(typingIndicator); } // 如果需要流式输出,使用WebSocket if (data.is_streaming) { // 创建系统消息容器,但内容为空 const messageElement = document.createElement('div'); messageElement.className = 'message system-message'; const contentElement = document.createElement('div'); contentElement.className = 'message-content'; contentElement.textContent = ''; messageElement.appendChild(contentElement); chatHistory.appendChild(messageElement); // 建立WebSocket连接进行流式输出 await handleStreamingResponse(query, filter, contentElement); } else { // 直接添加完整答案 addMessage('system', data.answer); } // 重新加载历史记录 await loadHistory(); } catch (error) { console.error('发送消息错误:', error); // 移除输入指示器 if (typingIndicator.parentNode) { chatHistory.removeChild(typingIndicator); } // 添加错误消息 addMessage('system', `抱歉,发生错误: ${error.message}`); } finally { // 清除正在响应标志 isResponseInProgress = false; // 滚动到底部 scrollToBottom(); } } // 处理流式响应 async function handleStreamingResponse(query, filter, contentElement) { // 关闭现有连接 if (socket && socket.readyState === WebSocket.OPEN) { socket.close(); } return new Promise((resolve, reject) => { // WebSocket URL const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${wsProtocol}//${window.location.host}${API_BASE_URL}/api/stream`; // 创建WebSocket连接 socket = new WebSocket(wsUrl); // 连接建立时 socket.onopen = function() { console.log('WebSocket连接已建立'); // 发送查询请求 socket.send(JSON.stringify({ query: query, source_filter: filter, session_id: currentSessionId })); }; // 接收消息时 socket.onmessage = function(event) { const data = JSON.parse(event.data); switch (data.type) { case 'start': console.log('流式响应开始'); break; case 'token': // 追加token到内容 contentElement.textContent += data.token; scrollToBottom(); break; case 'end': console.log('流式响应结束'); socket.close(); resolve(); break; case 'error': console.error('流式响应错误:', data.error); contentElement.textContent += `\n[错误: ${data.error}]`; socket.close(); reject(new Error(data.error)); break; } }; // 发生错误时 socket.onerror = function(error) { console.error('WebSocket错误:', error); contentElement.textContent += '\n[连接错误]'; reject(error); }; // 连接关闭时 socket.onclose = function() { console.log('WebSocket连接已关闭'); resolve(); }; }); } // 添加消息到聊天窗口 function addMessage(type, content) { const messageElement = document.createElement('div'); messageElement.className = `message ${type === 'user' ? 'user-message' : 'system-message'}`; const contentElement = document.createElement('div'); contentElement.className = 'message-content'; contentElement.textContent = content; messageElement.appendChild(contentElement); chatHistory.appendChild(messageElement); // 滚动到底部 scrollToBottom(); } // 滚动到底部 function scrollToBottom() { chatHistory.scrollTop = chatHistory.scrollHeight; } </script> </body> </html>
📁 test/
📄 demo00_env.py 552B
def dm01(): from pymilvus import MilvusClient # 连接milvus client = MilvusClient("http://localhost:19530") # 显示当前milvus的数据库 print('databases-->', client.list_databases()) def dm02(): import redis # 连接到 Redis client = redis.Redis(host="localhost", port=6379, decode_responses=True) # 测试读写 client.set("test_key", "Hello, Redis!") value = client.get("test_key") print(f"Redis value: {value}") if __name__ == '__main__': # dm01() dm02()
📄 demo01_ollama_utils.py 2.7KB
# TODO 1.python中使用ollama def dm01(): import ollama # 聊天式 response = ollama.chat(model="deepseek-r1:1.5b", messages=[{"role": "user", "content": "为什么天空是蓝色的?"}]) print('response1-->', response) print('response.message-->', response["message"]) ## 远程调用的方式 from ollama import Client client = Client(host="http://127.0.0.1:11434") # 虚拟机的需要改ip response = client.chat(model="deepseek-r1:1.5b", messages=[{"role": "user", "content": "你是什么模型?"}]) print('response2-->', response) print('response.message.content-->', response["message"]['content']) ## 流式调用 stream = client.chat(model="deepseek-r1:1.5b", messages=[{"role": "user", "content": "你叫什么名字?"}], stream=True) for chunk in stream: print(chunk["message"]['content'], end="") # TODO 2.langchain调用 def dm02(): # 0.x版本导包 # from langchain_community.llms import Ollama # 1.x版本导包 from langchain_ollama import OllamaLLM llm = OllamaLLM(base_url="http://127.0.0.1:11434", model="deepseek-r1:1.5b") result = llm.invoke("你叫什么名字?") print("result-->", result) # TODO 3.HTTP调用 def dm03(): import requests import json url = "http://127.0.0.1:11434/api/chat" headers = {"Content-Type": "application/json"} data = {"model": "deepseek-r1:1.5b", "messages": [{"role": "user", "content": "你叫什么名字?"}], "stream": False} response = requests.post(url, headers=headers, json=data) # print('response1-->', response.json()) url = "http://127.0.0.1:11434/api/chat" headers = {"Content-Type": "application/json"} data = {"model": "deepseek-r1:1.5b", "messages": [{"role": "user", "content": "你叫什么名字?"}], "stream": True} response = requests.post(url, headers=headers, json=data) for chunk in response.iter_lines(): print(json.loads(chunk.decode("utf-8"))['message']['content'], end='') # TODO 4.openai sdk调用Ollama def dm04(): from openai import OpenAI client = OpenAI(base_url="http://localhost:11434/v1", api_key="xx") # vllm部署可以指定api_key stream = client.chat.completions.create( model="deepseek-r1:1.5b", messages=[{"role": "user", "content": "写一首关于春天的七言绝句"}], stream=True, ) for chunk in stream: if chunk.choices and chunk.choices[0].delta.content: print(chunk.choices[0].delta.content, end="", flush=True) if __name__ == '__main__': # dm01() # dm02() # dm03() dm04()
📄 demo02_langchain_utils.py 19.1KB
""" langchain支持3种模型: * LLM * ChatModel * Embedding model """ ####################LangChain组件:Models#################### # LLM def dm01(): # 0.x版本 # from langchain.llms import Ollama # 1.x版本 方式一 from langchain_community.llms import Ollama model = Ollama(model="deepseek-r1:1.5b", temperature=0) # 1.x版本 方式二 # from langchain_ollama import OllamaLLM # model = OllamaLLM(model="deepseek-r1:1.5b", temperature=0) result = model.invoke("请给我讲个鬼故事") print("result-->", result) # ChatModel def dm02(): from langchain_core.messages import HumanMessage, SystemMessage # 0.x版本 # from langchain.chat_models import ChatOllama # 1.x版本 # from langchain_community.chat_models import ChatOllama from langchain_ollama import ChatOllama model = ChatOllama(model="deepseek-r1:1.5b", temperature=0) messages = [ SystemMessage(content="现在你是一个著名的歌手"), HumanMessage(content="给我写一首歌词") ] result = model.invoke(messages) print('result-->', result) print('result.content-->', result.content) # embedding model def dm03(): # from langchain_community.embeddings import OllamaEmbeddings from langchain_ollama import OllamaEmbeddings model = OllamaEmbeddings(model="mxbai-embed-large", temperature=0) res1 = model.embed_query('这是第一个测试文档') print('res1-->', res1, len(res1)) res2 = model.embed_documents(['这是第一个测试文档', '这是第二个测试文档']) print('res2-->', res2, len(res2)) ####################LangChain组件:Prompt#################### # TODO 掌握:提示模板(非Chat版本) def dm04(): # from langchain import PromptTemplate # Langchain 0.x版本使用 from langchain_core.prompts import PromptTemplate # from langchain_community.llms import Ollama # Langchain 0.x版本使用 from langchain_ollama import OllamaLLM # model = Ollama(model="qwen2.5:0.5b") # Langchain 0.x版本使用 model = OllamaLLM(model="qwen2.5:0.5b") # 提示词模板 prompt = PromptTemplate.from_template("我的邻居姓{lastname}, 他最近生了个儿子,请帮他儿子取个名字") prompt_str = prompt.format(lastname="王") print("result1-->", prompt_str) prompt = PromptTemplate(template="我的邻居姓{lastname}, 他最近生了个儿子,请帮他儿子取个名字", input_variables=['lastname']) prompt_str = prompt.format(lastname='王') print("result2-->", prompt_str) result = model.invoke(prompt_str) print("result-->", result) # 了解:Langchain源码写法 def dm05(): from langchain_core.prompts import ChatPromptTemplate template = ChatPromptTemplate( [ ("system", "You are a helpful AI bot. Your name is {name}."), ("human", "Hello, how are you doing?"), ("ai", "I'm doing well, thanks!"), ("human", "{user_input}"), ]) prompt_value = template.invoke( { "name": "Bob", "user_input": "What is your name?", }) print("prompt_value-->", prompt_value) # TODO 掌握:Chat版本提示词模板 def dm06(): from langchain_community.chat_models import ChatOllama # 0.x版本 # from langchain.prompts import ChatPromptTemplate # 1.x版本 from langchain_core.prompts import ChatPromptTemplate # 创建原始模板 template_str = """您是一位专业的鲜花店文案撰写员。\n 对于售价为 {price} 元的 {flower_name} ,您能提供一个吸引人的简短描述吗?""" # 把字符串传给提示词模板 prompt = ChatPromptTemplate.from_template(template=template_str) # 给模板中的变量赋值 prompt_result = prompt.format_messages(price='68', flower_name='玫瑰') print("prompt_result-->", prompt_result) # 把提示词送给大模型 model = ChatOllama(model="deepseek-r1:1.5b", temperature=0) result = model.invoke(prompt_result) print("result-->", result) # 了解:FewShot提示词模板 def dm07(): from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate from langchain_ollama.llms import OllamaLLM model = OllamaLLM(model="qwen2.5:0.5b") examples = [ {"word": "开心", "antonym": "难过"}, {"word": "高", "antonym": "矮"}, ] example_template = """ 单词: {word} 反义词: {antonym} """ example_prompt = PromptTemplate( input_variables=["word", "antonym"], template=example_template, ) few_shot_prompt = FewShotPromptTemplate( examples=examples, example_prompt=example_prompt, prefix="给出每个单词的反义词", suffix="单词: {input}\n反义词:", input_variables=["input"], example_separator="\n", ) prompt_text = few_shot_prompt.format(input="粗") print(prompt_text) print('*' * 80) print(model.invoke(prompt_text)) ####################LangChain组件:Chains#################### # TODO 掌握:Chains def dm08(): from langchain_core.prompts import PromptTemplate from langchain_community.llms import Ollama from langchain_classic.chains.llm import LLMChain # 定义模板 template = "你是一个取名专家,我的邻居姓{lastname},他生了个儿子,请给他儿子起个名字。" prompt = PromptTemplate( input_variables=["lastname"], template=template, ) llm = Ollama(model="qwen2.5:0.5b") # 调用方式一:普通调用 chain = LLMChain(llm=llm, prompt=prompt) print('1-->', chain.invoke("王")) # 调用方式二:LCEL(Langchain 表达式语言) chain = prompt | llm # 执行链 print('2-->', chain.invoke("王")) # 了解:Chains 多条链 def dm09(): from langchain_core.prompts import PromptTemplate from langchain_ollama import OllamaLLM # 创建第一条链 template = "我的邻居姓{lastname},他生了个儿子,给他儿子起个名字" first_prompt = PromptTemplate( input_variables=["lastname"], template=template, ) # 实例化模型 llm = OllamaLLM(model="deepseek-r1:1.5b") # LCEL表达式 first_chain = first_prompt | llm # 创建第二条链 second_prompt = PromptTemplate( input_variables=["child_name"], template="邻居的儿子名字叫{child_name},给他起一个小名") second_chain = second_prompt | llm # 链接两条链 overall_chain = first_chain | second_chain print(overall_chain) print('*' * 80) # 执行链,只需要传入第一个参数 catchphrase = overall_chain.invoke("王") print(catchphrase) ####################OutputParser:对大模型输出结果进行解析(转换)#################### # 结果美化:字符串 def dm10(): from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import ChatPromptTemplate from langchain_ollama import OllamaLLM # 实例化模型 model = OllamaLLM(model="qwen2.5:0.5b") # 创建简单链 prompt = ChatPromptTemplate.from_template("解释{topic}是什么?回答控制20字以内") chain1 = prompt | model result1 = chain1.invoke({"topic": "ai"}) print(f"result1-->{result1}") # 添加字符串解析器 parser = StrOutputParser() chain2 = prompt | model | parser result2 = chain2.invoke({"topic": "ai"}) print(f"result2-->{result2}") # 结果美化:列表 def dm11(): from langchain_core.output_parsers import CommaSeparatedListOutputParser from langchain_core.prompts import ChatPromptTemplate from langchain_ollama import OllamaLLM # 实例化模型 model = OllamaLLM(model="qwen2.5:0.5b") # 创建列表解析器 parser = CommaSeparatedListOutputParser() # 创建带格式说明的提示模板 # format_instructions = parser.get_format_instructions() # print(format_instructions) prompt = ChatPromptTemplate.from_template( "用中文列出{topic}的五个最重要特点。\n{format_instructions}" ) # 组合组件 chain = prompt | model | parser # 调用链 result = chain.invoke({ "topic": "大模型", "format_instructions": "你的回答应该是一个用逗号分隔的值的列表,例如:foo, bar, baz或 foo,bar,baz" }) print(result) # 结果美化:JSON def dm12(): from langchain_core.output_parsers import JsonOutputParser from langchain_core.prompts import ChatPromptTemplate from langchain_ollama import OllamaLLM # 实例化模型 model = OllamaLLM(model="qwen2.5:0.5b") # 创建JSON解析器 json_parser = JsonOutputParser() # 创建带格式说明的提示模板 json_format_instructions = json_parser.get_format_instructions() print(json_format_instructions) json_prompt = ChatPromptTemplate.from_template( "生成一个包含{person}基本信息的JSON。应包括姓名、职业、年龄和技能列表, 不要包含任何注释或额外说明。\n{format_instructions}" ) # 组合组件 json_chain = json_prompt | model | json_parser # 调用链 result = json_chain.invoke({ "person": "雷军", "format_instructions": "返回一个JSON对象" }) print(result) # 结果美化:Pydantic类型校验 def dm13(): from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import PydanticOutputParser from langchain_ollama import OllamaLLM # pydantic from pydantic import BaseModel, Field from typing import List # 实例化模型 model = OllamaLLM(model="qwen2.5:0.5b") # 定义Pydantic模型 class Movie(BaseModel): title: str = Field(description="电影标题") director: str = Field(description="导演姓名") year: int = Field(description="上映年份") # todo : 单标签多分类 (每个电影只能有个类型)、 多标签多分类(一个电影可以有多个类型) genre: List[str] = Field(description="电影类型") rating: float = Field(description="评分(1-10)") # 创建Pydantic解析器 pydantic_parser = PydanticOutputParser(pydantic_object=Movie) # 创建带格式说明的提示模板 format_instructions = pydantic_parser.get_format_instructions() print('1-->', format_instructions) pydantic_prompt = ChatPromptTemplate.from_template( "生成一部{genre}电影的信息。\n{format_instructions}" ) # 组合组件 pydantic_chain = pydantic_prompt | model | pydantic_parser # 调用链 movie_data = pydantic_chain.invoke({ "genre": "科幻", "format_instructions": format_instructions }) print('2-->', movie_data) ####################LangChain组件:Memory#################### def dm14(): from langchain_community.chat_message_histories import ChatMessageHistory from langchain_core.messages import messages_to_dict, messages_from_dict import json # 1.创建一个ChatMessageHistory对象,用来存储对话信息 history = ChatMessageHistory() # 添加用户消息 history.add_user_message("在吗?") # 添加大模型消息 history.add_ai_message("在") # 打印所有消息 print(f'history.messages1-->{history.messages}') # 2.可以将history.messages中的信息保存到字典中,然后保存到数据库或者文件中,方便后续读取 # 2.1 messages_to_dict()方法将history.messages中的信息转换成字典 dicts = messages_to_dict(history.messages) print(f'dicts-->{dicts}') # 2.2 这里将dicts保存到文件中 with open('history.json', 'w', encoding='utf-8') as f: f.write(json.dumps(dicts, indent=2, ensure_ascii=False)) # 3.从文件中读取出字典,然后将字典转换成消息 # 3.1 读取文件 messages = json.load(open('history.json', 'r', encoding='utf-8')) # 3.2 然后将字典转换成消息 chat_messages = messages_from_dict(messages) print(f'chat_messages2-->{chat_messages}') ####################LangChain组件:VectorStore#################### def dm15(): from langchain_text_splitters import CharacterTextSplitter from langchain_chroma import Chroma from langchain_community.document_loaders import TextLoader from langchain_ollama import OllamaEmbeddings # 1.加载文档 # 创建 TextLoader 对象 loader = TextLoader('./pku.txt', encoding='utf-8') # 加载文档 # metadata:描述数据的数据。数据大小,路径,创建日期,作者,权限... docs = loader.load() print(f'docs-->{docs}') # 2.将文档进行分块 # 创建 CharacterTextSplitter 对象 text_splitter = CharacterTextSplitter(separator="\n\n", chunk_size=200, chunk_overlap=30) # 分块 split_docs = text_splitter.split_documents(docs) print(f'split_docs-->{split_docs}') # 3.将分割后的文档存储到向量数据库中 # 加载embedding模型 embedding = OllamaEmbeddings(model="mxbai-embed-large") # 创建向量数据库,需要指定 存储的文档和向量模型名称以及持久化目录 chromadaDB = Chroma.from_documents(documents=split_docs, embedding=embedding, persist_directory='./chroma_db') print("chromadaDB-->", chromadaDB) # 假如你的向量数据库已经存在,那么可以直接加载 # chromadaDB = Chroma(persist_directory='./chroma_db', embedding_function=embedding) # 4.使用向量数据库进行查询 query = "1937年北京大学发生了什么?" result = chromadaDB.similarity_search(query, k=1) print(f'result-->{result}') ####################LangChain组件:检索器#################### def dm16(): from langchain_community.vectorstores import FAISS from langchain_community.embeddings import OllamaEmbeddings from langchain_text_splitters import CharacterTextSplitter from langchain_community.document_loaders import TextLoader loader = TextLoader('./pku.txt', encoding='utf-8') documents = loader.load() text_splitter = CharacterTextSplitter(chunk_size=100, chunk_overlap=0) texts = text_splitter.split_documents(documents) print("texts-->", texts) embeddings = OllamaEmbeddings(model="mxbai-embed-large") print("embeddings-->", embeddings) db = FAISS.from_documents(texts, embeddings) print("db-->", db) result = db.similarity_search("北京大学什么时候成立的", k=2) print("result-->", result) ####################LangChain组件:Indexes加载器#################### def dm17(): from langchain_community.document_loaders import UnstructuredFileLoader loader = UnstructuredFileLoader('衣服属性.txt', encoding='utf8') docs = loader.load() print('1-->', docs) print(len(docs)) first_01 = docs[0].page_content[:4] print('2-->', first_01) print('*' * 80) from langchain_community.document_loaders import TextLoader loader = TextLoader('衣服属性.txt', encoding='utf8') docs = loader.load() print('3-->', docs) print(len(docs)) first_01 = docs[0].page_content[:4] print('4-->', first_01) ####################LangChain组件:Indexes分割器#################### def dm18(): from langchain_core.documents import Document from langchain_text_splitters import CharacterTextSplitter # 创建分词器 separator参数指的是分割的分隔符,chunk_size指的是分割出来的每个块的大小,chunk_overlap指的是每个块之间重叠的大小 text_splitter = CharacterTextSplitter(separator=" ", chunk_size=5, chunk_overlap=1) # 一句话分割 result1 = text_splitter.split_text("a b c d e f") print(f'result1--->{result1}') # 多句话分割 result2 = text_splitter.create_documents(["a b c d e f", "e f g h"]) print(f'result2--->{result2}') # 多句话分割 result3 = text_splitter.split_documents([Document(page_content="a b c d e f", metadata={"id": "1"})]) print(f'result3--->{result3}') def dm19(): from langchain_text_splitters import RecursiveCharacterTextSplitter # 4个参数: # 1. 块大小 2.重叠字符数 3. 长度衡量方式:传入一个函数进来,调用函数判断长度 # 4. 分割符号: 按照列表中的分割符号,从左到右依次进行分割,直到满足块大小限制。 如果分割到最后一个符号未满足块大小,直接截断 text_splitter = RecursiveCharacterTextSplitter( chunk_size=30, # 每个块最多 30 个字符 chunk_overlap=6, # 相邻分块会共享 6 个字符 # 使用len,中文和英文没有区别;byte length_function=len, # 用字符数来衡量长度 separators=["\n\n", "\n", " ", ""] # 会优先尝试按 \n\n 分段,如果太长,再按 \n → 空格 → 逐字符切分。 ) text = """ 人工智能正在快速发展,尤其是大语言模型的应用,正在改变人类的工作方式。 它们可以帮助人们进行写作、代码生成、甚至是科研探索。 相比之下,新能源的发展同样重要。 电动车和太阳能正在逐渐替代传统能源,减少碳排放,对全球环境保护至关重要。 """ docs = text_splitter.split_text(text) print('docs-->', docs) print('docs-->', len(docs)) # 4 def dm20(): from langchain_experimental.text_splitter import SemanticChunker from langchain_ollama import OllamaEmbeddings embed = OllamaEmbeddings(model="mxbai-embed-large") text_splitter = SemanticChunker( embeddings=embed, breakpoint_threshold_type='percentile', # 更低的百分位会更“积极”地切分(数值越小 => 切得越多) breakpoint_threshold_amount=40.0, # 句子拆分正则为同时识别中/英文终结符 sentence_split_regex=r'(?<=[。!?.!?])\s*', # 每个切分后片段(chunk)至少要有多少个字符 min_chunk_size=10 ) text = """ 人工智能正在快速发展,尤其是大语言模型的应用,正在改变人类的工作方式。 它们可以帮助人们进行写作、代码生成、甚至是科研探索。 相比之下,新能源的发展同样重要。 电动车和太阳能正在逐渐替代传统能源,减少碳排放,对全球环境保护至关重要。 """ docs = text_splitter.split_text(text) print("docs-->", docs) if __name__ == '__main__': # dm01() # dm02() # dm03() # dm04() # dm05() # dm06() # dm07() # dm08() # dm09() # dm10() # dm11() # dm12() # dm13() # dm14() # dm15() # dm16() # dm17() # dm18() # dm19() dm20()
📄 demo03_milvus_utils.py 21.0KB
# 启动milvus向量数据库的服务。之前的环境搭建已经讲过 # docker compose up -d from pymilvus import MilvusClient # 如果uri为链接地址,代表Milvus属于单机服务,需要开启Milvus后台服务操作 client = MilvusClient(uri="http://localhost:19530") # TODO Milvus数据库操作 # 1 数据库的操作 def dm01(): # 创建名称为milvus_demo的数据库 databases = client.list_databases() print("databases-->", databases) if "milvus_demo" not in databases: client.create_database(db_name="milvus_demo") else: client.using_database(db_name="milvus_demo") # 创建已经存在的数据库会报错 # client.create_database(db_name="milvus_demo") # databases = client.list_databases() # print("databases-->", databases) # 2 collection集合的操作 def dm02(): # 定义schema # from pymilvus import DataType # client.using_database("milvus_demo") # # 注意:在定义集合 Schema 时,enable_dynamic_field=True 使得您可以插入未定义的字段。一般动态字段以 JSON 格式存储,通常命名为 $meta。 # # 在插入数据时,所有未定义的字段及其值将被保存为键值对。 # # 在定义集合 Schema 时,auto_id=True 可以对主键自动增长id。 # schema = client.create_schema(auto_id=True, enable_dynamic_field=True) # # # schema添加字段id, vector # schema.add_field(field_name='id', datatype=DataType.INT64, is_primary=True) # schema.add_field(field_name='vector', datatype=DataType.FLOAT_VECTOR, dim=5) # schema.add_field(field_name='scalar1', datatype=DataType.VARCHAR, max_length=256, description='标量字段') # # 创建集合 # client.create_collection(collection_name='demo_v1', schema=schema) # 切换数据库 client.using_database("milvus_demo") # # 设置索引 index_params = client.prepare_index_params() # # 在向量字段vector上面添加一个索引; # field_name:字段名 # metric_type:索引类型,cosine(余弦相似度) # index_name:索引名称,用户自定义 # index_type='', # 留空以使用自动索引 # 对于向量字段,常见的默认索引类型包括IVF_FLAT或HNSW等,具体取决于数据的特性和查询需求。 # 对于标量字段,常见的默认索引可能是INVERTED等。 index_params.add_index(field_name='vector', metric_type="COSINE", index_type='', index_name="vector_index") client.create_index(collection_name='demo_v1', index_params=index_params) # 查看索引信息 res = client.list_indexes(collection_name='demo_v1') print(f'索引信息--》{res}') res = client.describe_index(collection_name='demo_v1', index_name='vector_index') print(f'指定索引详细信息-->{res}') # 查看索引状态 client.load_collection(collection_name='demo_v1') print('load_state-->', client.get_load_state(collection_name='demo_v1')) # 如果不需要索引,可以删除相关索引 # client.release_collection(collection_name='demo_v1') # client.drop_index(collection_name='demo_v1', index_name='vector_index') # 检索标量字段 index_params1 = client.prepare_index_params() index_params1.add_index(field_name='scalar1', index_type='', index_name='default_index') client.create_index(collection_name='demo_v1', index_params=index_params1) # 查看索引信息 res = client.list_indexes(collection_name='demo_v1') print(f'索引信息--》{res}') # res = client.describe_index(collection_name='demo_v1', index_name='vector_index') print(f'指定索引详细信息-->{res}') # 3.Entity实体数据操作-插入数据 def dm03(): client.using_database("milvus_demo") # todo:1. 创建集合collection # 这种方式: collection 只包括两个字段. id 作为主键, vector 作为向量字段,以及自动设置 auto_id、enable_dynamic_field 为 True # auto_id 启用此设置可确保主键自动递增。在数据插入期间无需手动提供主键。 # enable_dynamic_field 启用后,要插入的数据中除 id 和 vector 之外的所有字段都将被视为动态字段。 # # 这些附加字段作为键值对保存在名为 $meta 的特殊字段中。此功能允许在数据插入期间包含额外的字段。 client.create_collection(collection_name='demo_v2', dimension=5, metric_type='IP') # todo:2. 插入数据(也叫实体) data = [ {"id": 0, "vector": [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592], "color": "pink_8682"}, {"id": 1, "vector": [0.19886812562848388, 0.06023560599112088, 0.6976963061752597, 0.2614474506242501, 0.838729485096104], "color": "red_7025"}, {"id": 2, "vector": [0.43742130801983836, -0.5597502546264526, 0.6457887650909682, 0.7894058910881185, 0.20785793220625592], "color": "orange_6781"}, {"id": 3, "vector": [0.3172005263489739, 0.9719044792798428, -0.36981146090600725, -0.4860894583077995, 0.95791889146345], "color": "pink_9298"}, {"id": 4, "vector": [0.4452349528804562, -0.8757026943054742, 0.8220779437047674, 0.46406290649483184, 0.30337481143159106], "color": "red_4794"}, {"id": 5, "vector": [0.985825131989184, -0.8144651566660419, 0.6299267002202009, 0.1206906911183383, -0.1446277761879955], "color": "yellow_4222"}, {"id": 6, "vector": [0.8371977790571115, -0.015764369584852833, -0.31062937026679327, -0.562666951622192, -0.8984947637863987], "color": "red_9392"}, {"id": 7, "vector": [-0.33445148015177995, -0.2567135004164067, 0.8987539745369246, 0.9402995886420709, 0.5378064918413052], "color": "grey_8510"}, {"id": 8, "vector": [0.39524717779832685, 0.4000257286739164, -0.5890507376891594, -0.8650502298996872, -0.6140360785406336], "color": "white_9381"}, {"id": 9, "vector": [0.5718280481994695, 0.24070317428066512, -0.3737913482606834, -0.06726932177492717, -0.6980531615588608], "color": "purple_4976"} ] res = client.insert(collection_name='demo_v2', data=data) print("res-->", res) # 4.Entity实体数据操作-分区插入数据 def dm04(): client.using_database("milvus_demo") ## todo:2.1 将数据插入到特定分区,可以在插入请求中指定分区名称,如下所示: data = [ {"id": 10, "vector": [-0.5570353903748935, -0.8997887893201304, -0.7123782431855732, -0.6298990746450119, 0.6699215060604258], "color": "red_1202"}, {"id": 11, "vector": [0.6319019033373907, 0.6821488267878275, 0.8552303045704168, 0.36929791364943054, -0.14152860714878068], "color": "blue_4150"}, {"id": 12, "vector": [0.9483947484855766, -0.32294203351925344, 0.9759290319978025, 0.8262982148666174, -0.8351194181285713], "color": "orange_4590"}, {"id": 13, "vector": [-0.5449109892498731, 0.043511240563786524, -0.25105249484790804, -0.012030655265886425, -0.0010987671273892108], "color": "pink_9619"}, {"id": 14, "vector": [0.6603339372951424, -0.10866551787442225, -0.9435597754324891, 0.8230244263466688, -0.7986720938400362], "color": "orange_4863"}, {"id": 15, "vector": [-0.8825129181091456, -0.9204557711667729, -0.935350065513425, 0.5484069690287079, 0.24448151140671204], "color": "orange_7984"}, {"id": 16, "vector": [0.6285586391568163, 0.5389064528263487, -0.3163366239905099, 0.22036279378888013, 0.15077052220816167], "color": "blue_9010"}, {"id": 17, "vector": [-0.20151825016059233, -0.905239387635804, 0.6749305353372479, -0.7324272081377843, -0.33007998971889263], "color": "blue_4521"}, {"id": 18, "vector": [0.2432286610792349, 0.01785636564206139, -0.651356982731391, -0.35848148851027895, -0.7387383128324057], "color": "orange_2529"}, {"id": 19, "vector": [0.055512329053363674, 0.7100266349039421, 0.4956956543575197, 0.24541352586717702, 0.4209030729923515], "color": "red_9437"} ] client.create_partition(collection_name='demo_v2', partition_name='partitionA') res = client.insert(collection_name='demo_v2', data=data, partition_name='partitionA') print("res-->", res) # 5.Entity实体数据操作-更新数据 def dm05(): client.using_database("milvus_demo") ## todo:4. 更新插入数据 # 在 Milvus 中,upsert 操作执行数据级操作,根据集合中是否已存在主键来插入或更新实体。具体来说: # 如果集合中已存在该实体的主键,则现有实体将被覆盖。 # 如果集合中不存在主键,则将插入一个新实体。 # data = [ # {"id": 0, "vector": [-0.619954382375778, 0.4479436794798608, -0.17493894838751745, -0.4248030059917294, # -0.8648452746018911], "color": "black_9898"}, # {"id": 1, "vector": [0.4762662251462588, -0.6942502138717026, -0.4490002642657902, -0.628696575798281, # 0.9660395877041965], "color": "red_7319"}, # {"id": 2, "vector": [-0.8864122635045097, 0.9260170474445351, 0.801326976181461, 0.6383943392381306, # 0.7563037341572827], "color": "white_6465"}, # {"id": 3, "vector": [0.14594326235891586, -0.3775407299900644, -0.3765479013078812, 0.20612075380355122, # 0.4902678929632145], "color": "orange_7580"}, # {"id": 4, "vector": [0.4548498669607359, -0.887610217681605, 0.5655081329910452, 0.19220509387904117, # 0.016513983433433577], "color": "red_3314"}, # {"id": 5, "vector": [0.11755001847051827, -0.7295149788999611, 0.2608115847524266, -0.1719167007897875, # 0.7417611743754855], "color": "black_9955"}, # {"id": 6, "vector": [0.9363032158314308, 0.030699901477745373, 0.8365910312319647, 0.7823840208444011, # 0.2625222076909237], "color": "yellow_2461"}, # {"id": 7, "vector": [0.0754823906014721, -0.6390658668265143, 0.5610517334334937, -0.8986261118798251, # 0.9372056764266794], "color": "white_5015"}, # {"id": 8, "vector": [-0.3038434006935904, 0.1279149203380523, 0.503958664270957, -0.2622661156746988, # 0.7407627307791929], "color": "purple_6414"}, # {"id": 9, "vector": [-0.7125086947677588, -0.8050968321012257, -0.32608864121785786, 0.3255654958645424, # 0.26227968923834233], "color": "brown_7231"} # ] # res = client.upsert(collection_name='demo_v2', data=data) data = [ {"id": 10, "vector": [-0.619954382375778, 0.4479436794798608, -0.17493894838751745, -0.4248030059917294, -0.8648452746018911], "color": "black_9898"}, {"id": 20, "vector": [-0.009954382375778, 0.4479436794798608, -0.17493894838751745, -0.4248030059917294, -0.8648452746018911], "color": "pink_998"} ] # upsert:update + insert,更新+插入 res = client.upsert(collection_name='demo_v2', data=data, partition_name='partitionA') print('res-->', res) # 6.Entity实体数据操作-删除数据 def dm06(): client.using_database("milvus_demo") # 注意如果分区中不存在更新数据的id,就不会受影响,但是会影响集合里已经存在的相同id的实体 # res = client.upsert(collection_name='demo_v2', data=data, partition_name="partitionA") # todo:5. 删除实体(数据) # 按照过滤器删除;如果不指定分区,默认情况下会在整个集合中进行删除 # res = client.delete(collection_name='demo_v2', filter='id in [12, 5, 6]') # print('res-->', res) # 按照id进行删除;指定分区删除数据 res = client.delete(collection_name='demo_v2', ids=[1, 2, 3, 14], partition_name='partitionA') print('res-->', res) # 7.数据查询 def dm07(): client.using_database("milvus_demo") # todo: 1. 单向量搜索 res = client.search(collection_name='demo_v2', data=[[0.19886812562848388, 0.06023560599112088, 0.6976963061752597, 0.2614474506242501, 0.838729485096104]], limit=2, search_params={"metric_type": "IP"}, output_fields=["id", 'vector']) # search_params是在查询时执行距离计算方式,如果定义索引的时候,已经制定了方式可以不写 print("res1-->", res) print("\nres[0]-->", res[0]) print("\nres[0][0]-->", res[0][0]) # todo: 2. 批量向量搜索 res = client.search(collection_name='demo_v2', data=[[0.19886812562848388, 0.06023560599112088, 0.6976963061752597, 0.2614474506242501, 0.838729485096104], [0.3172005263489739, 0.9719044792798428, -0.36981146090600725, -0.4860894583077995, 0.95791889146345]], limit=2, search_params={"metric_type": "IP"}, output_fields=["id", 'vector']) # search_params是在查询时执行距离计算方式,如果定义索引的时候,已经制定了方式可以不写 print("res2-->", res) print("\nres[0]-->", res[0]) print("\nres[0][0]-->", res[0][0]) print("=================") print("\nres[1]-->", res[1]) print("\nres[1][0]-->", res[1][0]) # todo: 3. 分区搜索 # 要进行分区搜索,只需在搜索请求的 partition_names 中包含目标分区的名称即可。这指定search操作仅考虑指定分区内的向量。 res = client.search( collection_name="demo_v2", data=[[0.02174828545444263, 0.058611125483182924, 0.6168633415965343, -0.7944160935612321, 0.5554828317581426]], limit=5, search_params={"metric_type": "IP", "params": {}}, partition_names=["partitionA"], # 这里指定搜索的分区 output_fields=['vector'] ) print('res3-->', res) # todo: 4.指定输出字段进行搜索 # 使用输出字段进行搜索允许您指定搜索结果中应包含匹配向量的哪些属性或字段。 res = client.search( collection_name="demo_v2", data=[[0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592]], limit=5, search_params={"metric_type": "IP", "params": {}}, output_fields=['vector', "color"] # 返回定义的字段 ) print('res4-->', res) # todo: 5.过滤搜索 # 过滤器搜索:筛选搜索将标量筛选器应用于矢量搜索,允许我们根据特定条件优化搜索结果。 # 例如,要根据字符串模式优化搜索结果,可以使用 like 运算符。此运算符通过考虑前缀、中缀和后缀来启用字符串匹配: # 筛选颜色以红色为前缀的结果: res = client.search( collection_name="demo_v2", data=[[0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592]], limit=2, search_params={"metric_type": "IP", "params": {}}, output_fields=["color"], filter='color like "red%"' ) print('res5-->', res) # todo: 6.范围搜索 # 范围搜索允许查找距查询向量指定距离范围内的向量。 # 范围搜索:radius:定义搜索空间的外边界。只有距查询向量在此距离内的向量才被视为潜在匹配。 # range_filter:虽然radius设置搜索的外部限制,但可以选择使用range_filter来定义内部边界,创建一个距离范围,在该范围内向量必须落下才被视为匹配。 search_params = { "metric_type": "IP", "params": {} } res = client.search( collection_name="demo_v2", data=[[0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592]], limit=3, # 返回的搜索结果最大数量 search_params=search_params, output_fields=["color"], ) print('res6-->', res) # 8.复杂查询 # 1.混合检索:要对两组 ANN 搜索结果进行合并和重新排序,有必要选择适当的重新排序策略。 # 支持两种重排策略:加权排名策略(WeightedRanker)和重排序策略(RRFRanker)。 # 在选择重排策略时,需要考虑的一个问题是,在向量场中是否需要强调一个或多个基本 ANN 搜索。 # 2.加权排名(WeightedRanker):如果您要求结果强调特定的向量场,建议使用该策略。 # 通过 WeightedRanker,您可以为某些向量场分配更高的权重,从而更加强调这些向量场。 # 如果认为所有向量权重都同等对待,建议使用RRFRanker。 # 例如,在多模态搜索中,图片的文字描述可能比图片的颜色更重要。 def dm08(): from pymilvus import RRFRanker, AnnSearchRequest, DataType # import random # # client.using_database("milvus_demo") # # # 定义schema # schema = client.create_schema(enable_dynamic_field=False) # schema.add_field(field_name='film_id', datatype=DataType.INT64, is_primary=True) # schema.add_field(field_name='filmVector', datatype=DataType.FLOAT_VECTOR, dim=5) # 向量字段 # schema.add_field(field_name="posterVector", datatype=DataType.FLOAT_VECTOR, dim=5) # 向量字段 # # # 定义索引 # index_params = client.prepare_index_params() # index_params.add_index(field_name='filmVector', index_type="IVF_FLAT", # metric_type="L2", params={"nlist": 128}) # index_params.add_index(field_name='posterVector', index_type="", # metric_type="COSINE") # # # 创建集合 # client.create_collection(collection_name='demo_v3', schema=schema, index_params=index_params) # # # 向量库中插入实体 # entities = [] # for _ in range(1000): # # 构造实体 # film_id = random.randint(1, 10000) # film_vector = [random.random() for _ in range(5)] # poster_vector = [random.random() for _ in range(5)] # entity = {"film_id": film_id, "filmVector": film_vector, "posterVector": poster_vector} # entities.append(entity) # # client.insert(collection_name='demo_v3', data=entities) client.using_database("milvus_demo") # 多向量查询(注意和批量向量查询不同) # 多向量搜索使用 hybrid_search() API 在一次调用中执行多个 ANN 搜索请求。每个 AnnSearchRequest 代表特定矢量场上的单个搜索请求。 # 示例创建两个 AnnSearchRequest 实例以对两个向量字段执行单独的相似性搜索。 # 创建多搜索请求 filmVector query_filmVector = [ [0.8896863042430693, 0.370613100114602, 0.23779315077113428, 0.38227915951132996, 0.5997064603128835]] dense_search_params = {"data": query_filmVector, "anns_field": "filmVector", # 该参数值必须与集合模式中使用的值相同。 "param": {"metric_type": "L2", "nprobe": 10}, # nprobe代表访问簇的数量 "limit": 2} request_1 = AnnSearchRequest(**dense_search_params) print("request_1-->", request_1) # 创建多搜索请求 posterVector query_posterVector = [ [0.02550758562349764, 0.006085637357292062, 0.5325251250159071, 0.7676432650114147, 0.5521074424751443]] sparse_search_params = {"data": query_posterVector, "anns_field": "posterVector", # 该参数值必须与集合模式中使用的值相同。 "param": {"metric_type": "COSINE"}, "limit": 2 } request_2 = AnnSearchRequest(**sparse_search_params) print("request_2-->", request_2) reqs = [request_1, request_2] ranker = RRFRanker(100) res = client.hybrid_search( collection_name="demo_v3", reqs=reqs, ranker=ranker, limit=3 ) print("res-->", res) # 9.删除集合和数据库 def dm09(): client.using_database("milvus_demo") client.drop_collection(collection_name="demo_v3") client.drop_database("milvus_demo") if __name__ == '__main__': # dm01() # dm02() # dm03() # dm04() # dm05() # dm06() # dm07() dm08() # dm09()
📄 demo04_logging.py 426B
import logging # 基本配置 logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(levelname)s - %(message)s - %(lineno)d', filename='app.log') # 日志记录器 logger = logging.getLogger("ABC") # 不同日志级别 logger.debug("debug log") logger.info("info log") logger.warning("warning log") logger.error("error log") logger.critical("critical log")
📄 demo05_logging.py 903B
import logging # 创建日志记录器 logger = logging.getLogger("Example4") logger.setLevel(logging.DEBUG) # 设置记录器级别 # 创建控制台处理器 console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) # 控制台显示INFO及以上级别 # 创建文件处理器 file_handler = logging.FileHandler('file.log', encoding='utf-8') file_handler.setLevel(logging.DEBUG) # 文件记录DEBUG及以上级别 # 定义日志格式 formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') # 为处理器设置格式 console_handler.setFormatter(formatter) file_handler.setFormatter(formatter) # 将处理器添加到记录器 logger.addHandler(console_handler) logger.addHandler(file_handler) # 记录日志 logger.debug("调试信息,仅写入文件") logger.info("程序运行正常") logger.error("发生错误")
📄 demo06_redis_milvus.py 356B
#测试连接milvus和redis的脚本 #注意: # 1. 启动redis的时候,是否配置了密码 # 2. 如果docker安装在虚拟机,需要修改下面的IP地址 import redis # 连接到 Redis client = redis.Redis(host="localhost", port=6379, decode_responses=False) # 测试读写 client.set("def", "你好呀") # 关闭客户端 client.close()
📄 demo07_bm25.py 677B
from rank_bm25 import BM25Okapi # 文本语料库 corpus = [ "Hello there good man!", "It is quite windy in London", "How is the weather today?" ] # 遍历每一个文档,并对文本进行切词 tokenized_corpus = [doc.split(" ") for doc in corpus] print("tokenized_corpus-->", tokenized_corpus) # 创建BM25对象 bm25 = BM25Okapi(tokenized_corpus) print("bm25-->", bm25) # 准备query query = "windy London" # query = "today is a nice day" tokenized_query = query.split(" ") print("tokenized_query-->", tokenized_query) # 得到相似度得分 doc_scores = bm25.get_scores(tokenized_query) print("doc_scores-->", doc_scores)
📄 demo08_mysql.py 1.1KB
import pymysql # 1.创建连接对象 connection = pymysql.connect(host='localhost', user='root', password='123456', database='subjects_kg', port=3306, charset='utf8') print("connection-->", connection) # 2.获得cursor(游标) cursor = connection.cursor() # 3.执行SQL语句 # create table t1( # id int primary key auto_increment, # name varchar(20) # ) # result = cursor.execute("insert into t1 values(null, 'zhangwuji')") result = cursor.execute("select * from t1") # result = cursor.execute("select * from t1 where id = 2") # TODO pymysql模块,默认开启了事务,所以必须提交后才会生效。 # 提交事务 connection.commit() # 4.输出结果 print("result-->", result) # 获取查询的结果 results = cursor.fetchall() # results = cursor.fetchone() print("result-->", results) # for row in results: # print(row) # 5.关闭cursor cursor.close() # 6.关闭连接对象 connection.close()
📄 demo09_read_data.py 338B
import pandas as pd data = pd.read_csv("../mysql_qa/data/JP学科知识问答.csv") for index, row in data.iterrows(): print('index-->', index) print('row-->', row) print('row.学科名称-->', row['学科名称']) print('row.问题-->', row['问题']) print('row.答案-->', row['答案']) break
📄 demo10_hasattr.py 212B
class A(): def __init__(self): self.name = '张三' self.age = 18 if __name__ == '__main__': a = A() if hasattr(a, 'name'): print("1") else: print("2")
📄 demo11_sorted.py 377B
scores = [0.08682564, 0.10919417] parent_docs = ['学习人工智能开发', '人工智能就业课'] result = sorted(zip(scores, parent_docs), reverse=True) for _, doc in result: print(_) print(doc) print("*" * 100) ranked_parent_docs = [doc for _, doc in sorted(zip(scores, parent_docs), reverse=True)] print("ranked_parent_docs-->", ranked_parent_docs)
📄 demo12_walk.py 1.3KB
import os # directory_path = r'D:/code/workspace7/integrated_qa_system/rag_qa/data' # for root, dirs, files in os.walk(directory_path): # print(root) # print(dirs) # print(files) # file_path = r'D:\code\workspace7\integrated_qa_system\rag_qa\data\ai_data\LLM基础知识.pdf' # file_extension = os.path.splitext(file_path)[1].lower() # print(file_extension) # import hashlib # page_content = "ab" # result = hashlib.md5(page_content.encode('utf-8')).hexdigest() # print("result-->", result) # 面向对象有三种方法: # 1.对象方法 不适用任何装饰器调用,方法参数是self # 2.类方法 使用classmethod装饰器调用,方法参数是cls # 3.静态方法 使用staticmethod装饰器调用,方法参数什么都没有 # 对象方法,类方法,静态方法都可以使用对象调用。 # 类方法,静态方法还可以使用类名调用。 class RAGPrompts: # 静态方法:使用类名调用,而不需要对象名。 @staticmethod def rag_prompt(): pass @classmethod def xxx(cls): pass def yyy(self): pass class RAGSystem: def __init__(self): pass self.rag_prompt = RAGPrompts.rag_prompt() pass x = RAGSystem() print(x)
📄 demo13_argparse.py 647B
import argparse # 1. 创建解析器 # Argument:参数 # # Augment:增强 parser = argparse.ArgumentParser(description='简要描述这个脚本的功能') # 2. 添加参数 parser.add_argument('input', help='输入文件路径') # 位置参数 parser.add_argument('--output', help='输出文件路径') # 可选参数 parser.add_argument('--verbose', action='store_true', help='是否输出详细信息') # 3. 解析参数 args = parser.parse_args() # 4. 使用参数 print(f"输入文件: {args.input}") if args.output: print(f"输出文件: {args.output}") if args.verbose: print("详细模式已开启")
📄 demo14_history.py 920B
# eval方法解释 # b = "10" # result = eval(b) # print(type(result)) # print(result) from dotenv import load_dotenv import os from openai import OpenAI load_dotenv() # 定义一个变量,保存历史纪录 history = [] client = OpenAI( api_key=os.getenv("DASHSCOPE_API_KEY"), base_url=os.getenv("DASHSCOPE_BASE_URL"), ) # 1.多轮对话 # 2.历史管理 # 本质:保存用户和模型输入的上下文信息 while True: # 接收用户输入 prompt = input("请输入:") # 添加用户的输入 history.append({'role': 'user', 'content': prompt}) completion = client.chat.completions.create( model=os.getenv("MODEL_NAME"), messages=history ) result = completion.choices[0].message.content # 封装成模式输出的JSON格式 history.append({'role': 'assistant', 'content': result}) print(result)
📄 demo15_uuid.py 160B
import uuid session_id = str(uuid.uuid4()) print(session_id) # hash值:同一个str,哈希值一定相同。 # UUID:每一次运行都不一样。
📄 demo16_coroutine.py 1.6KB
######################################################## # 同步:线程执行 # import time # # def dm01(): # print("1") # time.sleep(3) # 真的要休眠3秒钟 # print("2") # # # def dm02(): # print("3") # time.sleep(3) # 真的要休眠3秒钟 # print("4") # # # start_time = time.time() # dm01() # dm02() # end_time = time.time() # spend_time = end_time - start_time # print("spend_time-->", spend_time) ######################################################## # 异步:协程执行 import time # io:input-output,输入输出 # 同步/异步 # 同步:同频 # 异步:不同频。 # 举例:外卖(送你手上就是同步,放到外卖架上就是异步) # 协程:让程序等待的时候去干别的事(提升效率)。 # 应用场景:在一些IO密集型任务中使用。 # IO密集型任务:文件上传,下载,读取等。 import asyncio async def dm01(): print("1") await asyncio.sleep(3) # 休眠3秒钟,但是不是一直等待,而是在这休眠期间可以干别的事 print("2") async def dm02(): print("3") await asyncio.sleep(3) # 休眠3秒钟,但是不是一直等待,而是在这休眠期间可以干别的事 print("4") async def dm03(): start_time = time.time() # 方式二 await asyncio.gather(dm01(),dm02()) # 方式一 # t1 = asyncio.create_task(dm01()) # t2 = asyncio.create_task(dm02()) # await t1 # await t2 end_time = time.time() spend_time = end_time - start_time print("spend_time-->", spend_time) asyncio.run(dm03())
📄 demoxx_bert_models.py 1.2KB
# 导入包 from transformers import BertForSequenceClassification, AutoTokenizer # 加载模型和分词器 model_path = "../rag_qa/models/bert_query_classifier" model = BertForSequenceClassification.from_pretrained(model_path) # 加载模型 tokenizer = AutoTokenizer.from_pretrained(model_path) # 单个句子编码 text = "黑马的Java课程安排有哪些?" inputs = tokenizer(text, return_tensors="pt") # 返回pytorch print('inputs-->', inputs) # 句子对编码 texts = ["黑马的Java课程安排有哪些?", "黑马的Python课程安排有哪些?"] inputs = tokenizer(texts[0], texts[1], return_tensors="pt") print("inputs-->", inputs) texts = ["黑马的Java课程安排有哪些?", "黑马的Python有哪些?"] inputs = tokenizer(texts, return_tensors="pt", padding=True) print("inputs-->", inputs) # 测试模型输出 text = "Java数据的基本类型" # 意图分类 专业知识 inputs = tokenizer(text, return_tensors="pt") outputs = model(**inputs) # **表示将字典中的键值对作为参数传递给函数 print("outputs-->", outputs) # 获取预测结果 result = outputs.logits.argmax(dim=-1) print("result-->", result)
📄 demoxx_fastapi.py 360B
from fastapi import FastAPI app = FastAPI() @app.get("/") def read_root(): return {"Hello": "World"} @app.get("/items/{item_id}") def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "q": q} if __name__ == '__main__': import uvicorn uvicorn.run(app=app, host='0.0.0.0', port=8003, reload=False)
📄 demoxx_models.py 1.3KB
# bge-m3模型 def dm01(): from milvus_model.hybrid import BGEM3EmbeddingFunction m3_path = "../rag_qa/models/bge-m3" embedding_function = BGEM3EmbeddingFunction(model_name_or_path=m3_path, use_fp16=False, device='cpu') # 对文本进行Embedding embed = embedding_function(["黑马JAVA", "黑马大模型"]) # 打印Embedding后的结果 print("embed-->", embed) print("embed.dense-->", embed["dense"]) print("embed.sparse-->", embed["sparse"]) row = embed["dense"][0] print("row-->", row) # reranker 模型 def dm02(): from sentence_transformers import CrossEncoder reranker_path = "../rag_qa/models/bge-reranker-large" reranker = CrossEncoder(reranker_path) queries = ["什么是机器学习?"] documents = [ "深度学习是机器学习的一个分支。", "今天的天气很好。", "机器学习是一种让计算机从数据中自动学习规律的方法。", ] pairs = [[queries[0], doc] for doc in documents] print("pairs-->", pairs) scores = reranker.predict(pairs) print(f'scores-->', scores) # 输出排序结果 for doc, score in sorted(zip(documents, scores), key=lambda x: x[1], reverse=True): print(f"{score:.4f} - {doc}") if __name__ == '__main__': # dm01() dm02()
📄 pku.txt 1.7KB
北京大学创办于1898年,是戊戌变法的产物,也是中华民族救亡图存、兴学图强的结果,初名京师大学堂,是中国近现代第一所国立综合性大学,辛亥革命后,于1912年改为现名。 在悠久的文明历程中,古代中国曾创立太学、国子学、国子监等国家最高学府,在中国和世界教育史上具有重要影响。北京大学“上承太学正统,下立大学祖庭”,既是中华文脉和教育传统的传承者,也标志着中国现代高等教育的开端。其创办之初也是国家最高教育行政机关,对建立中国现代学制作出重要历史贡献。 作为新文化运动的中心和五四运动的策源地,作为中国最早传播马克思主义和民主科学思想的发祥地,作为中国共产党最初的重要活动基地,北京大学为民族的振兴和解放、国家的建设和发展、社会的文明和进步做出了突出贡献,在中国走向现代化的进程中起到了重要的先锋作用。爱国、进步、民主、科学的精神和勤奋、严谨、求实、创新的学风在这里生生不息、代代相传。 1917年,著名教育家蔡元培就任北京大学校长,他“循思想自由原则,取兼容并包主义”,对北京大学进行了卓有成效的改革,促进了思想解放和学术繁荣。陈独秀、李大钊、毛泽东以及鲁迅、胡适、李四光等一批杰出人士都曾在北京大学任教或任职。 1937年卢沟桥事变后,北京大学与清华大学、南开大学南迁长沙,共同组成国立长沙临时大学。1938年,临时大学又西迁昆明,更名为国立西南联合大学。抗日战争胜利后,北京大学于1946年10月在北平复员。
📄 衣服属性.txt 819B
身高:160-170cm, 体重:90-115斤,建议尺码M。 身高:165-175cm, 体重:115-135斤,建议尺码L。 身高:170-178cm, 体重:130-150斤,建议尺码XL。 身高:175-182cm, 体重:145-165斤,建议尺码2XL。 身高:178-185cm, 体重:160-180斤,建议尺码3XL。 身高:180-190cm, 体重:180-210斤,建议尺码4XL。 面料分类:其他 图案:纯色 领型:翻领 衣门襟:单排扣 颜色:黑色 卡其色 粉色 杏色 袖型:收口袖 适用季节:冬季 袖长:长袖 厚薄:厚款 适用场景:其他休闲 衣长:常规款 版型:宽松型 款式细节:假两件 工艺处理:免烫处理 适用对象:青年 面料功能:保暖 穿搭方式:外穿 销售渠道类型:纯电商(只在线上销售) 材质成分:棉100%
📄 app.py 8.3KB
from fastapi import FastAPI, WebSocket, HTTPException, Query, Depends from fastapi.responses import StreamingResponse, FileResponse from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from starlette.websockets import WebSocketDisconnect import os from pydantic import BaseModel import asyncio import json import uuid from typing import Optional, List, Dict, Any import time import re # 导入现有的系统 from new_main import IntegratedQASystem # 创建应用实例 app = FastAPI(title="问答系统API", description="集成MySQL和RAG的智能问答系统") # 配置CORS,允许前端访问 app.add_middleware( CORSMiddleware, allow_origins=["*"], # 在生产环境中应该限制为特定域名 allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 创建静态文件目录 os.makedirs("static", exist_ok=True) # 创建全局QA系统实例 qa_system = IntegratedQASystem() # 定义日常问候用语模式和回复 GREETING_PATTERNS = [ { "pattern": r"^(你好|您好|hi|hello)", "response": "你好!我是黑马程序员,专注于为学生答疑解惑,很高兴为你服务!" }, { "pattern": r"^(你是谁|您是谁|你叫什么|你的名字|who are you)", "response": "我是黑马程序员,你的智能学习助手,致力于提供 IT 教育相关的解答!" }, { "pattern": r"^(在吗|在不在|有人吗)", "response": "我在!我是黑马程序员,随时为你解答问题!" }, { "pattern": r"^(干嘛呢|你在干嘛|做什么)", "response": "我正在待命,随时为你解答 IT 学习相关的问题!有什么我可以帮你的?" } ] # 定义请求模型 class QueryRequest(BaseModel): query: str source_filter: Optional[str] = None session_id: Optional[str] = None # 定义响应模型 class QueryResponse(BaseModel): answer: str is_streaming: bool session_id: str processing_time: float # 添加静态文件服务 app.mount("/static", StaticFiles(directory="static"), name="static") # 根路径重定向到index.html @app.get("/") async def read_root(): return FileResponse("static/index.html") # 创建新会话 @app.post("/api/create_session") async def create_session(): session_id = str(uuid.uuid4()) return {"session_id": session_id} # 查询历史消息 @app.get("/api/history/{session_id}") async def get_history(session_id: str): try: history = qa_system.get_session_history(session_id) return {"session_id": session_id, "history": history} except Exception as e: raise HTTPException(status_code=500, detail=f"获取历史记录失败: {str(e)}") # 清除历史消息 @app.delete("/api/history/{session_id}") async def clear_history(session_id: str): success = qa_system.clear_session_history(session_id) if success: return {"status": "success", "message": "历史记录已清除"} else: raise HTTPException(status_code=500, detail="清除历史记录失败") # 检查是否为日常问候用语并返回模板回复 def check_greeting(query: str) -> Optional[str]: query_text = query.strip() for pattern_info in GREETING_PATTERNS: if re.match(pattern_info["pattern"], query_text, re.IGNORECASE): return pattern_info["response"] return None # 非流式查询接口 @app.post("/api/query") async def query(request: QueryRequest): start_time = time.time() session_id = request.session_id or str(uuid.uuid4()) # 检查是否为日常问候 greeting_response = check_greeting(request.query) if greeting_response: return { "answer": greeting_response, "is_streaming": False, "session_id": session_id, "processing_time": time.time() - start_time } # 判断是否需要流式处理(基于 need_rag) answer, need_rag = qa_system.bm25_search.search(request.query, threshold=0.85) if need_rag: # 需要 RAG 和 LLM 处理,返回流式响应提示 return { "answer": "请使用WebSocket接口获取流式响应", "is_streaming": True, "session_id": session_id, "processing_time": time.time() - start_time } # 非流式查询,直接返回 BM25 检索的答案 return { "answer": answer, "is_streaming": False, "session_id": session_id, "processing_time": time.time() - start_time } # 流式查询WebSocket接口 @app.websocket("/api/stream") async def websocket_endpoint(websocket: WebSocket): await websocket.accept() try: while True: # 接收消息 data = await websocket.receive_text() request_data = json.loads(data) query = request_data.get("query") source_filter = request_data.get("source_filter") session_id = request_data.get("session_id", str(uuid.uuid4())) start_time = time.time() # 发送开始标志 if websocket.client_state == websocket.client_state.CONNECTED: await websocket.send_json({ "type": "start", "session_id": session_id }) # 检查是否为日常问候 greeting_response = check_greeting(query) if greeting_response: if websocket.client_state == websocket.client_state.CONNECTED: await websocket.send_json({ "type": "token", "token": greeting_response, "session_id": session_id }) await websocket.send_json({ "type": "end", "session_id": session_id, "is_complete": True, "processing_time": time.time() - start_time }) break # 调用QA系统进行查询,流式返回结果 collected_answer = "" for token, is_complete in qa_system.query(query, source_filter=source_filter, session_id=session_id): collected_answer += token if is_complete and not collected_answer: if websocket.client_state == websocket.client_state.CONNECTED: await websocket.send_json({ "type": "end", "session_id": session_id, "is_complete": True, "processing_time": time.time() - start_time }) break if token and websocket.client_state == websocket.client_state.CONNECTED: await websocket.send_json({ "type": "token", "token": token, "session_id": session_id }) if is_complete: if websocket.client_state == websocket.client_state.CONNECTED: await websocket.send_json({ "type": "end", "session_id": session_id, "is_complete": True, "processing_time": time.time() - start_time }) break await asyncio.sleep(0.01) except WebSocketDisconnect as e: print(f"WebSocket disconnected: code={e.code}, reason={e.reason}") except Exception as e: print(f"WebSocket error: {str(e)}") if websocket.client_state == websocket.client_state.CONNECTED: await websocket.send_json({ "type": "error", "error": str(e) }) finally: try: if websocket.client_state == websocket.client_state.CONNECTED: await websocket.close() except Exception as e: print(f"Error closing WebSocket: {str(e)}") # 健康检查端点 @app.get("/health") async def health_check(): return {"status": "healthy"} # 获取有效的学科类别 @app.get("/api/sources") async def get_sources(): return {"sources": qa_system.config.VALID_SOURCES} if __name__ == "__main__": import uvicorn uvicorn.run(app=app, host="0.0.0.0", port=8003, reload=False)
📄 config.ini 766B
# MySQL 配置 [mysql] host = localhost user = root password = 123456 database = subjects_kg # Redis 配置 [redis] host = localhost port = 6379 password = db = 0 # Milvus 配置 [milvus] host = 127.0.0.1 port = 19530 database_name = itcast collection_name = edurag # LLM 配置 [llm] model = qwen-plus dashscope_api_key = sk-e19a5da0294a4fe99e50f41e92521582 dashscope_base_url = https://dashscope.aliyuncs.com/compatible-mode/v1 # 检索参数配置 [retrieval] parent_chunk_size = 512 child_chunk_size = 128 chunk_overlap = 50 retrieval_k = 5 candidate_m = 2 # 日志配置 [logger] log_file = D:\code\workspace7\integrated_qa_system\logs\app.log # 应用配置 [app] valid_sources = ["ai", "java", "test", "ops", "bigdata"] customer_service_phone = 12345678
📄 new_main.py 10.1KB
# 导入MySQL和Redis客户端,管理数据库和缓存 from mysql_qa import MySQLClient, RedisClient, BM25Search # 导入RAG系统组件 from rag_qa import VectorStore, RAGSystem # 导入配置和日志 from base import logger, Config # 导入OpenAI客户端,用于DashScope API from openai import OpenAI # 导入时间库,记录处理时间 import time # 导入UUID生成唯一会话ID import uuid # 导入pymysql错误处理 import pymysql class IntegratedQASystem: def __init__(self): # 初始化日志 self.logger = logger # 初始化配置 self.config = Config() # 初始化MySQL客户端 self.mysql_client = MySQLClient() # 初始化Redis客户端 self.redis_client = RedisClient() # 初始化BM25搜索 self.bm25_search = BM25Search(self.redis_client, self.mysql_client) try: # 初始化OpenAI客户端,连接DashScope API self.client = OpenAI(api_key=self.config.DASHSCOPE_API_KEY, base_url=self.config.DASHSCOPE_BASE_URL) except Exception as e: self.logger.error(f"OpenAI客户端初始化失败: {e}") raise # 初始化向量存储 self.vector_store = VectorStore() # 初始化RAG系统 self.rag_system = RAGSystem(self.vector_store, self.call_dashscope) # 初始化对话历史表 self.init_conversation_table() def init_conversation_table(self): """初始化MySQL中的conversations表,用于存储对话历史""" try: self.mysql_client.cursor.execute(""" CREATE TABLE IF NOT EXISTS conversations ( id INT AUTO_INCREMENT PRIMARY KEY, session_id VARCHAR(36) NOT NULL, question TEXT NOT NULL, answer TEXT NOT NULL, timestamp DATETIME NOT NULL, INDEX idx_session_id (session_id) ) """) self.mysql_client.connection.commit() self.logger.info("对话历史表初始化成功") except pymysql.MySQLError as e: self.logger.error(f"初始化对话历史表失败: {e}") raise # 调用模型 def call_dashscope(self, prompt): """调用DashScope API生成答案(流式输出)""" try: completion = self.client.chat.completions.create( model=self.config.LLM_MODEL, messages=[ {"role": "system", "content": "你是一个有用的助手。"}, {"role": "user", "content": prompt}, ], timeout=30, stream=True # 启用流式输出 ) # 收集流式输出的结果 collected_content = "" for chunk in completion: if chunk.choices and chunk.choices[0].delta.content: content = chunk.choices[0].delta.content collected_content += content # 这里可以通过回调函数向前端发送每个chunk yield content return collected_content except Exception as e: self.logger.error(f"LLM调用失败: {e}") return f"错误:LLM调用失败 - {e}" def _fetch_recent_history(self, session_id: str) -> list: """获取最近5轮对话历史""" try: self.mysql_client.cursor.execute(""" SELECT question, answer FROM conversations WHERE session_id = %s ORDER BY timestamp DESC LIMIT %s """, (session_id, 5)) history = [{"question": row[0], "answer": row[1]} for row in self.mysql_client.cursor.fetchall()] return history[::-1] except pymysql.MySQLError as e: self.logger.error(f"获取对话历史失败: {e}") return [] def update_session_history(self, session_id: str, question: str, answer: str) -> list: """更新会话历史到MySQL,保留最近5轮对话""" try: self.mysql_client.cursor.execute(""" INSERT INTO conversations (session_id, question, answer, timestamp) VALUES (%s, %s, %s, NOW()) """, (session_id, question, answer)) history = self._fetch_recent_history(session_id) self.mysql_client.cursor.execute(""" DELETE FROM conversations WHERE session_id = %s AND id NOT IN ( SELECT id FROM ( SELECT id FROM conversations WHERE session_id = %s ORDER BY timestamp DESC LIMIT %s ) AS sub ) """, (session_id, session_id, 5)) self.mysql_client.connection.commit() self.logger.info(f"会话 {session_id} 历史更新成功") return history except pymysql.MySQLError as e: self.logger.error(f"更新会话历史失败: {e}") self.mysql_client.connection.rollback() raise except Exception as e: self.logger.error(f"更新会话历史意外错误: {e}") self.mysql_client.connection.rollback() raise def get_session_history(self, session_id: str) -> list: """从MySQL获取会话历史""" return self._fetch_recent_history(session_id) def clear_session_history(self, session_id: str) -> bool: """清除指定会话历史""" try: self.mysql_client.cursor.execute(""" DELETE FROM conversations WHERE session_id = %s """, (session_id,)) self.mysql_client.connection.commit() self.logger.info(f"会话 {session_id} 历史已清除") return True except pymysql.MySQLError as e: self.logger.error(f"清除会话历史失败: {e}") self.mysql_client.connection.rollback() return False # 修改 main.py 中的 query 方法 def query(self, query, source_filter=None, session_id=None): """查询集成系统,支持对话历史和流式输出""" start_time = time.time() self.logger.info(f"处理查询: '{query}' (会话ID: {session_id})") history = self.get_session_history(session_id) if session_id else [] # 首先尝试MySQL精确查询 answer, need_rag = self.bm25_search.search(query, threshold=0.85) if answer: # MySQL找到精确答案,直接返回 self.logger.info(f"MySQL答案: {answer}") if session_id: self.update_session_history(session_id, query, answer) processing_time = time.time() - start_time self.logger.info(f"查询处理耗时 {processing_time:.2f}秒") # 对于MySQL答案,一次性返回 yield answer, True # 第二个参数表示这是完整答案 elif need_rag: # 需要使用RAG系统,支持流式输出 self.logger.info("无可靠MySQL答案,回退到RAG") # 收集完整答案以便存储 collected_answer = "" # 从RAG系统获取流式输出 for token in self.rag_system.generate_answer(query, source_filter=source_filter, history=history): collected_answer += token yield token, False # 第二个参数表示这是部分答案 # 完整答案收集完毕,更新会话历史 if session_id: self.update_session_history(session_id, query, collected_answer) processing_time = time.time() - start_time self.logger.info(f"查询处理耗时 {processing_time:.2f}秒") # 标记结束 yield "", True # 空字符串表示流结束,True表示完成 else: # 未找到答案 self.logger.info("未找到答案") processing_time = time.time() - start_time self.logger.info(f"查询处理耗时 {processing_time:.2f}秒") yield "未找到答案", True # 一次性返回 def main(): qa_system = IntegratedQASystem() session_id = str(uuid.uuid4()) print("\n欢迎使用集成问答系统!") print(f"会话ID: {session_id}") print(f"支持的学科类别:{qa_system.config.VALID_SOURCES}") print("输入查询进行问答,输入 'exit' 退出。") try: while True: query = input("\n输入查询: ").strip() if query.lower() == "exit": logger.info("退出系统") print("再见!") break source_filter = input(f"请输入学科类别 ({'/'.join(qa_system.config.VALID_SOURCES)}) (直接回车默认不过滤): ").strip() if source_filter and source_filter not in qa_system.config.VALID_SOURCES: logger.warning(f"无效的学科类别 '{source_filter}',将不过滤") source_filter = None print("\n答案: ", end="", flush=True) # 开始打印答案 answer = "" # 用于累积完整答案 # 迭代 query 方法的生成器 for token, is_complete in qa_system.query(query, source_filter=source_filter, session_id=session_id): if token: # 仅当 token 非空时打印 print(token, end="", flush=True) # 流式打印 answer += token # 累积答案 if is_complete: # 如果是完整答案或流结束,换行并退出循环 print() # 换行 break # 打印对话历史 history = qa_system.get_session_history(session_id) print("\n最近对话历史:") for idx, entry in enumerate(history, 1): print(f"{idx}. 问: {entry['question']}\n 答: {entry['answer']}") except Exception as e: logger.error(f"系统错误: {e}") print(f"发生错误: {e}") finally: qa_system.mysql_client.close() if __name__ == "__main__": main()
📄 old_main.py 5.1KB
# 导入 MySQL 系统组件 from mysql_qa import MySQLClient, RedisClient, BM25Search # 导入 RAG 系统组件 from rag_qa import VectorStore, RAGSystem # 导入配置和日志 from base import logger, Config # 导入 OpenAI from openai import OpenAI # 导入时间库 import time class IntegratedQASystem: def __init__(self): # 初始化日志 self.logger = logger # 初始化配置 self.config = Config() # 初始化 MySQL 客户端 self.mysql_client = MySQLClient() # 初始化 Redis 客户端 self.redis_client = RedisClient() # 初始化 BM25 搜索 self.bm25_search = BM25Search(self.redis_client, self.mysql_client) try: # 初始化 OpenAI 客户端 self.client = OpenAI(api_key=self.config.DASHSCOPE_API_KEY, base_url=self.config.DASHSCOPE_BASE_URL) except Exception as e: # 记录 OpenAI 初始化失败 self.logger.error(f"OpenAI 客户端初始化失败: {e}") raise # 初始化向量存储 self.vector_store = VectorStore() # 初始化 RAG 系统 self.rag_system = RAGSystem(self.vector_store, self.call_dashscope) def call_dashscope(self, prompt): # 调用 DashScope API try: # 创建聊天完成请求 completion = self.client.chat.completions.create( model=self.config.LLM_MODEL, messages=[ {"role": "system", "content": "你是一个有用的助手。"}, {"role": "user", "content": prompt}, ] ) # 返回完成结果 return completion.choices[0].message.content if completion.choices else "错误:无效的 LLM 响应" except Exception as e: # 记录 LLM 调用失败 self.logger.error(f"LLM 调用失败: {e}") # 返回错误信息 return f"错误:LLM 调用失败 - {e}" def query(self, query, source_filter=None): # 查询集成系统 start_time = time.time() # 记录查询信息 self.logger.info(f"处理查询: '{query}'") # 首先尝试 MySQL 搜索 answer, need_rag = self.bm25_search.search(query, threshold=0.85) if answer: # 记录 MySQL 答案 self.logger.info(f"MySQL 答案: {answer}") # 计算处理时间 processing_time = time.time() - start_time # 记录处理时间 self.logger.info(f"查询处理耗时 {processing_time:.2f}秒") # 返回答案 return answer elif need_rag: # 记录需要 RAG self.logger.info("无可靠 MySQL 答案,回退到 RAG") # 调用 RAG 系统 answer = self.rag_system.generate_answer(query, source_filter=source_filter) # 记录 RAG 答案 self.logger.info(f"RAG 答案: {answer}") # 计算处理时间 processing_time = time.time() - start_time # 记录处理时间 self.logger.info(f"查询处理耗时 {processing_time:.2f}秒") # 返回答案 return answer else: # 记录无答案 self.logger.info("未找到答案") # 计算处理时间 processing_time = time.time() - start_time # 记录处理时间 self.logger.info(f"查询处理耗时 {processing_time:.2f}秒") # 返回默认答案 return "未找到答案" def main(): # 初始化集成系统 qa_system = IntegratedQASystem() try: # 打印欢迎信息 print("\n欢迎使用集成问答系统!") print(f"支持的来源: {qa_system.config.VALID_SOURCES}") print("输入查询进行问答,输入 'exit' 退出。") while True: # 获取用户输入 query = input("\n输入查询: ").strip() if query.lower() == "exit": # 记录退出日志 logger.info("退出系统") # 打印退出信息 print("再见!") break # 获取来源过滤 source_filter = input(f"输入来源过滤 ({'/'.join(qa_system.config.VALID_SOURCES)}) (按 Enter 跳过): ").strip() if source_filter and source_filter not in qa_system.config.VALID_SOURCES: # 记录无效来源 logger.warning(f"无效来源 '{source_filter}',忽略过滤") # 打印无效来源信息 print(f"无效来源 '{source_filter}',继续无过滤。") source_filter = None # 执行查询 answer = qa_system.query(query, source_filter) # 打印答案 answer = "".join(answer) print(f"\n答案: {answer}") except Exception as e: # 记录系统错误 logger.error(f"系统错误: {e}") # 打印错误信息 print(f"发生错误: {e}") finally: # 关闭 MySQL 连接 qa_system.mysql_client.close() if __name__ == "__main__": # 运行主程序 main()
📎 项目环境与架构图
安装 / 启动说明、docker-compose、requirements
- 📎 docker-compose.yml2.1KB
- 📎 requirements.txt3.7KB
ℹ️ 离线版不含附件原文件,需要请到在线网站对应课时下载。
架构图
- 📎 RAG流程图.pdf192.8KB
- 📎 项目主流程.pdf183.1KB
ℹ️ 离线版不含附件原文件,需要请到在线网站对应课时下载。