day16-28 (一期讲过)机器学习+深度学习+迁移学习+NLP通识+大模型基本概述+API调用+langchain+Functioncalling+streamlit


目录

  1. day16开班
  2. Day17 day17机器学习通知
  3. Day18 day18深度学习通知
  4. day19-NLP通识
  5. day20 大模型基本概述+API调用
  6. day21大模型基本概述+API调用
  7. day22 langchain框架基础
  8. day23langchain进阶
  9. day24langgraph+RAG理论
  10. day25-RAG实操
  11. day26大模型程序开发day01-Functioncalling技术栈
  12. day27大模型程序开发day02-sql解释器+streamlit基础
  13. day28大模型程序开发day03-项目实战

day16开班

文件: day16开班\开班.md

预习部分:

  • day15-机器学习通知(了解) (1期开班---自我介绍+大家基本要求)
  • day16-深度学习通知(了解)
  • day17-NLP通识(了解)
  • day18-大模型基本概述+API调用(重点!重点!)
  • day19-大模型基本概述+API调用(重点!重点!)
  • day25-langchain框架基础(了解)
  • day26-langchain框架进阶(重点!)
  • day27-langc+RAG(重点!)
  • day28-RAG实战(重点!)

直播内容:

  • Functioncalling技术实现
  • Streamlit+大模型应用
  • RAG+Agent+Streamlit项目实战
  • MCP深度应用
  • OpenAI Agents SDK(openai在25年推出的企业级Multi-Agent开发框架)
    • Agents SDK接入MCP
  • 模型本地部署+调用(ollama+企业级)
  • 模型微调
  • 微软-GraphRAG应用开发

Day17 机器学习

文件: Day17 day17机器学习通知\day01\机器学习通识.md

有言在先

  • 授课风格:
    • 没有鸡汤、没有段子。有认真负责的态度、有传道受业解惑的育人之志启智之能
  • 学习方法
    • 课上动脑听、课下用心练、笔记常翻译、消化在心中
  • 授课相关:好的授课过程就好比是谈一场恋爱,从来都不是一个人的事
    • 节奏快慢
    • 内容动态补充
    • 授课内容+顺序
    • ......
  • Finally:
    • 人无完人,每个人都有不同的短板和不足之处。互相理解、互相适应、搞定AI、成就大计!

核心内容

  • 参考企培方案

  • 大致关键词:

    大模型接口调用、模型部署、模型微调、Agent智能体开发、RAG和graphRAG、MCP开发、Langchain开发框架(LangGraph)......
    

开发环境搭建

Anaconda

定义:Anaconda是一个集成的Python发行版,专为数据科学、机器学习和AI开发而设计。它包含了常用的Python库、包管理工具(Conda)和Jupyter Notebook等开发工具。

在机器学习和AI开发中使用的原因

  • 环境管理便捷:可以通过Conda轻松创建和管理虚拟环境,为不同的项目设置独立的环境,避免Python依赖冲突。
  • 内置丰富资源:自带了众多常用的AI库,无需手动安装,节省了大量时间和精力。这使得开发者能够快速搭建起工作环境,立即开始项目开发。

Jupyter Notebook

定义:Jupyter Notebook是一个基于Web的交互式开发环境,支持Python代码编写、调试、运行以及数据可视化。它被广泛应用于AI研究、数据分析和机器学习模型训练。

在机器学习和AI开发中使用的原因

  • 交互式执行代码:支持分块执行代码,便于调试和数据分析。在机器学习和AI开发中,经常需要对代码进行反复调试和修改,Jupyter Notebook的这种特性可以让开发者快速查看代码执行结果,及时调整代码逻辑。
  • 文档与代码结合:可以在Notebook中添加Markdown文档,方便记录研究过程、注释代码功能和撰写项目报告。对于机器学习和AI项目,这种文档与代码相结合的方式非常有利于知识的整理和分享。
  • 与Anaconda完美兼容:Anaconda内置Jupyter Notebook,开箱即用,两者结合可以为机器学习和AI开发提供一个高效、便捷的工作环境1。

网盘下载安装:

通过网盘分享的文件:Anaconda
链接: https://pan.baidu.com/s/1v2TkkKPMOu5j2sOQ9viuNQ?pwd=udkg 提取码: udkg 
--来自百度网盘超级会员v5的分享

清华源下提供下载:https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/

Conda简介

Conda是一个开源的软件包管理系统和环境管理系统。它最初是为Python程序创建的,但现在已经可以打包和分发其他软件,适用于Linux、OS X和Windows系统。以下是关于conda的详细信息:

主要功能

  • 包管理:可以快速安装、运行和更新包及其依赖项,类似于Node.js的npm或Python的pip,但功能更强大,能更好地处理依赖关系。
  • 环境管理:能够轻松地在计算机上创建、保存、加载和切换环境,使得在同一台机器上管理多个软件包版本成为可能,这对于需要不同版本的Python或特定依赖项的项目非常有用。

与pip的区别

  • 包管理范围:conda不仅可以管理Python包,还可以管理其他语言的包;而pip主要用于管理Python包。
  • 环境隔离方式:conda本身可以创建和管理独立的环境,而pip一般要与虚拟环境(如venv)结合使用来实现类似的隔离效果。
  • 依赖冲突解决:conda能够解决包之间的依赖关系冲突,确保安装兼容的包;pip在安装包时如果存在依赖冲突,可能需要手动解决。

虚拟环境创建

基本命令创建

  • 语法conda create -n <环境名称> [选项]
  • 示例:创建一个名为myenv且指定Python版本为3.8的虚拟环境,命令为conda create -n myenv python=3.102。

指定路径创建

  • 使用--prefix参数:例如conda create --prefix /path/to/env python=3.8,这会在/path/to/env目录下创建虚拟环境。

激活与退出虚拟环境

  • 激活:使用命令conda activate <环境名称>,激活后命令行提示符会显示当前环境名称,表示已切换到该环境。
  • 退出:使用命令conda deactivate可退出当前激活的虚拟环境。

查看虚拟环境列表:使用命令conda env listconda info -e可以查看已有的虚拟环境列表。

删除虚拟环境:使用命令conda remove -n <环境名称> --all可根据环境名删除虚拟环境。

人工智能背景介绍

初始人工智能

  • 概述:

    • 人工智能(Artificial Intelligence),英文缩写 AI。AI 第一次被提出来是在 1956 年,是由四位图灵奖得主、信息论创始人和一位诺贝尔得主在美国达特茅斯会议(DartmouthConference)上一同定义出来的。

    • 人工智能只是一个抽象概念,它不是任何具体的机器或算法。任何类似于人的智能或高于人的智能的机器或算法都可以称为人工智能。人工智能可以对人的意识、思维进行模拟,但又不是人的智能。有时候我们还会把人工智能分为弱人工智能(Weak AI)和强人工智能(Strong AI)

      • 弱人工智:
        • 弱人工智能是擅长于单个方面技能的人工智能。比如 AlphaGo 能战胜了众多世界围棋冠军的,在围棋领域所向披靡,但它只会下围棋,做不了其他事情。我们目前的人工智能相关的技术,比如图像识别,语言识别,自然语言处理等等,基本都是处于弱人工智能阶段。
      • 强人工智:
        • 强人工智能指的是在各方面都能和人类智能差不多的人工智能,人类能干的脑力劳动它都能干。创造强人工智能比创造弱人工智能难度要大很多,我们现阶段还做不到,只有在一些科幻电影中才能看到。

人工智能技术演变

人工智能早期阶段,迅速解决了一些对于人类来说比较困难,但是对于计算机来说相对容易的问题,比如下棋,推理,路径规划等等。但是也有早期人工智能解决不了的问题,比如图像识别、语音识别等。接下来,我们从如下3个维度展开讲解:

  • 计算速度:

    • 我们下象棋的时候,通常需要思考很久才能推算出几步棋之后棋盘战局的变化,并且经常还会有看错看漏的情况。
    • 而计算机能在一瞬间计算出七八步棋甚至十几步棋之后棋盘的情况,并从中选出对自己最有利的下法来与对手对弈。面对如此强大的对手,人类早在 20 年前就已经输了。
  • 算法:

    • 可能有人会想到人工智能在象棋领域早就战胜了人类最顶尖的选手,为什么在围棋领域一直到 2016 年才出了个 AlphaGo 把人类顶级棋手击败呢?

    • 比起象棋,围棋的局面发展的可能性要复杂得多。

    • 或许我们在设计象棋 AI 的时候可以使用暴力计算的方法,把几步之内所有可能的走法都遍历一次,然后选一个最优下法。同样的方法放到围棋上通了,围棋每一步的可能性都太多了,用暴力计算法设计出来的围棋AI,它的棋力是很差的。

    • 虽然 AlphaGo 的计算非常快,可以在短时间完成大量运算,但是AlphaGo 比其他棋类 AI 强的地方并不是计算能力,而是它的算法,也可以理解为它拥有更强大的“智慧”。

      • 就像是进行小学速算比赛,题目是 100 以内的加减法,10 个小学生为一队 1个数学系的博士为另一队。如果比赛内容是 1 分钟哪个队做的正确题目多,小学生队肯定是能够战胜数学博士的。如果是进行大学生数学建模比赛,那 10000 个小学生也赢不了 1 个数学博士。对于解决复杂的问题,需要的往往不只是计算速度,更多的应该是智慧。
  • 识别规则:

    • 对于一些人类比较擅长的任务,比如图像识别,语音识别,自然语言处理等,计算机却完成得很差。以视觉为例,人类的视觉从眼睛采集信息开始,但起到主要作用的是大脑。人类的大脑就像是一台超级计算机,可以轻松处理非常复杂的图像问题。人类的视觉能力是通过几亿年地不断进化,不断演变最终才得到的。
    • 在人工智能的早期阶段,计算机的智能通常是基于人工制定的“规则”,我们可以通过详细的规则去定义下棋的套路,推理的方法,以及路径规划的方案。但是我们却很难用规则去详细描述图片中的物体,比如我们要判断一张图片中是否存在猫。那我们先要通过规则去定义一只猫,如图 1.1 所示。
    Snip20230808_13
    • 观察图 1.1 中的猫,我们可以知道猫有一个圆脑袋,两个三角形的耳朵,又胖又长的身体,和一条长尾巴,然后可以定义一套规则在图片中寻找猫。这看起来好像是可行的,但是如果我们遇到的是图 1.2,图 1.3 中的猫该怎么办?
    Snip20230808_14
    • 猫可能只露出身体的一部分,可能会摆出奇怪的造型,那么我们又要针对这些情况定义新的规则。
    • 从这个例子中大家应该能看得出来,即使是一只很普通的家养宠物,都可能会出现无数种不同的外形。如果我们使用人工定义的规则去定义这个物体,那么可能需要设置非常大量的规则,并且效果也不一定会很好。仅仅一个物体就这么复杂,而现实中常见的各种物体成千上万,所以在图像识别领域,使用使用人为定义的规则去做识别肯定是行不通的。很多其他的领域也同样存在这种问题。

    由于人们没有办法设计出足够复杂的规则来精确描述世界,所以 AI 系统需要具备自我学习的能力,即从原始数据中获取有用的知识。这种能力被称为机器学习(Machine Learning)。

机器学习

  • 概念:

    • 机器学习就是从数据中自动分析获得规律,并利用规律对未知数据进行预测、分类或者决策的过程。
  • 举个例子:

    • 假如我们现在都是原始人,并不知道太阳和月亮是什么东西。
    • 但是我们可以观察天上的太阳和月亮,并且把太阳出来时候的光线和温度记录下来,把月亮出来时候的光线和温度记录下来(这就相当于是收集数据)
    • 观察了 100天之后,我们进行思考,总结这 100 天的规律我们可以发现,太阳和月亮是交替出现的。出太阳的时候光线比较亮,温度比较高。月亮出来的时候光线比较暗,温度比较低(这相当于是分析数据,建立模型)
    • 之后我们看到太阳准备落山,月亮准备出来的时候我们就知道温度要降低可能要多穿树叶或毛皮(预测未来的情况)
    • 因此,机器学习可以利用已有的数据进行学习,获得一个训练好的模型,然后可以利用此模型预测未来的情况。
  • 机器学习和人类思维的对比:

    • 我们可以使用历史数据来训练一个机器学习的模型,模型训练好之后,再放入新的数据,模型就可以对新的数据进行预测分析。人类也善于从以往的经验中总结规律,当遇到新的问题时,我们可以根据之前的经验来预测未来的结果。

    Snip20230808_15

  • 深入理解机器学习:

    • 算法模型对象:

      • 一种特殊的对象,特殊之处在于,该对象内部集成/封装了某种形式的算法/方程。该算法/方程用于找寻数据间的规律。假设某一个模型内部封装的算法/方程如下:
        • y = w * x + b,这是一个还没有求出解的方程式。
    • 样本数据:

      • 特征数据:自变量(一个样本的描述信息)
      • 标签数据:因变量(一个样本数据的结果)
      Snip20230808_16
    • 模型的训练:

      • 将样本数据带入到算法模型对象内部的算法/方程中,对算法/方程进行求解操作。
      • 在该算法/方程中 y = w * x + b,如果求出了w和b则方程就可有解。
      • 模型训练就是在使用算法/方程找寻样本数据之间的规律。
    • 模型的作用:

      • 对未知样本实现预测、分类或者决策。
      • 算法/方程的解就是模型实现分类或者预测的结果。
    • 算法模型的分类:

      • 有监督类别:
        • 有监督学习是指使用带有标签的样本数据来训练模型
      • 无监督类别:
        • 无监督学习是指使用没有标签的样本的数据训练模型

机器学习基础操作

sklearn的数据集接口介绍

sklearn.datasets.load_*():获取小规模的数据集

sklearn.datasets.fetch_*():获取大规模的数据集

数据集切分

  • 前提:
    • 机器学习就是从数据中自动分析获得规律,并利用规律对未知数据进行预测。换句话说,机器学习的算法模型一定是要经过样本数据对其进行训练,才可以对未知数据进行预测。
  • 问题:
    • 当我们有了一组样本数据后,是否要是有所有的样本数据对模型进行训练呢?
  • 思考:
    • 一个算法模型被训练好之后,我们如何获知该模型在具体应用中的性能和效果呢?
    • 好比一个学生,当进行了一个学期的系统学习后,如何获知该名学生学习后的综合效果呢?
  • 数据集切分
    • 当有了一组完整的样本数据后,一定会将该样本数据一分为二,生成训练集数据和测试集数据。
      • 训练集数据:通常80%作为训练集数据,用于进行模型的训练,让模型在训练集数据中学习/找寻训练样本中的规律。
      • 测试集数据:剩下20%的数据作为测试集数据,用于测试和评估训练好模型的综合表现效果。
    • 如果模型的训练和测试全都用同一组数据,则通常来讲,模型评估的效果一般都会很好,就好比是用一名同学见过的/学过的题对其进行考试,则考试结果一般都会很好,模型的评估也是一样的道理。
  • 数据集切分API:
from sklearn.model_selection import train_test_split
train_test_split(x,y,test_size,random_state)

参数介绍:
  x:特征
  y:目标
  test_size:测试集的比例
  random_state:打乱的随机种子
返回值:
	训练特征,测试特征,训练标签,测试标签

KNN算法模型

KNN简介

KNN(k-Nearest Neighbor)也叫做K近邻算法,个人认为该模型是机器学习中算法原理最简单的一种分类模型。KNN常用于样本量较小或特征空间/维度较小的分类问题中。

什么是分类问题?

所谓的分类,就是将一个未知类别的样本归属的一个已知的类别中。机器学习中,分类问题的目标/标签数据是离散型的类别数据,表示一些具体的类别。下面是一些常见的分类问题:

1.邮件分类:根据邮件的内容作为样本特征,将其分类为垃圾邮件或非垃圾邮件。其中的垃圾邮件类型和非垃圾邮件类型就是分类问题中的离散型类别的标签/目标数据。

2.信用评估:根据个人的收入、信用历史、负债情况等特征,将申请人的信用风险进行分类,例如将申请人分为高风险或低风险。

3.疾病诊断:根据患者的症状、体征和检查结果,将患者的疾病进行分类,例如将患者的病情分为正常或患有某种疾病。

4.图像分类:根据图像的特征和内容,将图像分类为不同的物体、场景或模式。例如,将图像中的动物分类为猫、狗、鸟类等不同类别。

KNN分类算法原理

简单地说,KNN算法是采用测量不同特征值之间的距离方法进行分类。大家可以类别:近朱者赤近墨者黑这句话进行理解。

下面,我们就详细来理解下KNN的分类原理,先看下图:w1(猫)、w2(狗)和w3(兔子)是三个已知类群,X则是一个未知类别的图片样本,现在要基于KNN算法将X样本分到w1、w2和w3其中的一个类别中,以确定X图片中的动物到底是猫、狗还是兔子。

Snip20230815_53

根据我们的直接感受,应该是衡量X样本距离w1、w2和w3哪个类群最近,则X样本就应该被分到哪个类别中。这个是不是就好比与:近朱者赤近墨者黑呢。那么,KNN究竟是如何实现的分类呢?

实现步骤:

  1. 算距离:KNN算法会计算X样本到其余所有样本之间的距离。(有几个其余样本就会计算几次距离)
  2. 找近邻:定义一个k值,找出离X最近的k个样本最为X最近的k个邻居。注意,k值是需要认为定义的一个数值。
  3. 投票:根据k个最近的邻居样本的类别标签进行投票,哪个类别的标签得票最多(在k个样本中哪个类别样本数量最多),则X样本就归属到该类别中。

注意:不同的k,可能会造成不同的分类结果

在下图中,如果k为3则小球的分类结果为三角形,k为5则分类结果为正方形。因此,k值的最优选择在KNN中是比较重要的一个环节,稍后会详细进行讲解说明。

Snip20230815_56

距离计算方式:

可以是欧式距离、曼卡顿距离或者闵可夫斯基距离等方式。

Snip20230815_59

电影分类

众所周知,电影可以按照题材分类,然而题材本身是如何定义的?由谁来判定某部电影属于哪个题材?也就是说同一题材的电影具有哪些公共特征?这些都是在进行电影分类时必须要考虑的问题。下面我们就一起来探究下电影如何实现分类?

电影名称 打斗镜头 接吻镜头 电影类型
California Man 3 104 爱情片
He Not Really into Dudes 2 100 爱情片
Beautiful Woman 1 81 爱情片
Kevin Longblade 101 10 动作片
Robo Slayer 3000 99 5 动作片
Amped 2 98 2 动作片
18 90 未知
采集到了一组电影的样本数据,每一个电影样本有两个特征维度:打斗镜头和接吻镜头,电影类型为目标数据,有爱情和动作两种类别。其中有一部未知类别的电影"?",并且提取到了该电影的打斗和接吻镜头的数量。接下来,使用KNN来计算电影 “?” 的特征到其他已知类型电影特征之间的距离。

下面可以,观测下具体的距离显示:

Snip20230815_62

根据欧式距离,进行距离计算结果如下:

电影名称 与未知类型电影“?”的距离
California Man 20.5
He Not Really into Dudes 18.7
Beautiful Woman 19.2
Kevin Longblade 115.3
Robo Slayer 3000 117.4
Amped 2 118.9

制定k的值,找到电影 “ ?”周围最近的k个邻居,假定k的值为3,则离其最近的3个邻居是:

电影名称 电影类型
California Man 爱情片
He Not Really into Dudes 爱情片
Beautiful Woman 爱情片

投票:最近的3个邻居种,爱情类别的得票最多,因此 “ ?”电影的类别被KNN划分到了爱情片类别。

模型超参数

  • n_neighbors(K值):表示KNN算法选择的最近邻样本的数量。

  • weights(权重):用于指定最近邻样本的投票权重。常见的选择有"uniform"(所有样本的权重相等)和"distance"(给更近的邻居更高的权重)。"distance"考虑了距离更近的邻居对分类的重要性,可以在样本数量较少或数据类别分布不平衡的情况下提高模型性能。

  • metric(距离度量):用于计算样本之间的距离。常见的选择有"euclidean"(欧氏距离)、"manhattan"(曼哈顿距离)和"minkowski"(闵可夫斯基距离)等。

    • 欧氏距离:欧氏距离是最常用的距离度量方式。在欧几里得空间中,欧氏距离是计算两点之间的直线距离。当数据的特征具有连续性且各特征之间的量纲相同时,可以使用欧氏距离作为距离度量方式。
    • 曼哈顿距离:曼哈顿距离衡量的是两个点之间在各个坐标轴上的绝对距离总和。当特征具有明显不同的量纲,或者数据具有离散性特征时,可以使用曼哈顿距离作为距离度量方式。
    • 闵可夫斯基距离:闵可夫斯基距离是欧氏距离和曼哈顿距离的一般化。它通过参数p控制距离的倾向性。当p等于1时,就是曼哈顿距离;当p等于2时,就是欧氏距离。因此,闵可夫斯基距离可以用来平衡欧氏距离和曼哈顿距离的效果。
    一般来说,欧氏距离适用于连续性且量纲相同的数据,曼哈顿距离适用于具有离散性特征或有不同量纲的数据,闵可夫斯基距离可以根据具体情况灵活选择参数p来平衡距离衡量的效果。
    

KNN模型应用

电影分类

  • 部分样本数据展示

Snip20230820_71

  • 代码实操
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
import warnings
warnings.filterwarnings("ignore")

#1.提取样本数据
data = pd.read_excel('./datasets/my_films.xlsx') 
feature = data[['Action Lens','Love Lens']]#特征
target = data['target']#标签

#2.数据集划分
x_train,x_test,y_train,y_test = train_test_split(feature,target,test_size=0.2,random_state=2023)

#3.建模:在创建模型的时候就需要人为指定k的值
model = KNeighborsClassifier(n_neighbors=5)

#4.训练模型
model.fit(x_train,y_train)#参数:X表示二维形状的特征,y表示标签

#5.对模型进行评估
model.score(x_test,y_test)

#6.使用训练好的模型进行分类任务
movie = [10,90]
model.predict([movie])

线性回归模型

何为回归?

  • 回归问题判定:
    • 回归问题对应的样本数据的标签数据是连续性的值,而分类问题对应的是离散型的值。
    • 在社会中产生的数据必然是离散型或者是连续型的数据,那么企业针对数据所产生的需求也无非是分类问题或者回归问题。
  • 常见的回归问题:
    • 预测房价
    • 销售额的预测
    • 贷款额度指定
    • ......

线性回归在生活中的映射

  • 学生期末成绩制定
    • 总成绩 = 0.7 * 考试成绩 + 0.3 * 平时成绩
    • 则该例子中,特征值为考试成绩和平时成绩,目标值为总成绩。从此案例中大概可以感受到
      • 回归算法预测出来的结果其实就是经过相关的算法计算出来的结果值!
      • 每一个特征需要有一个权重的占比,这个权重的占比明确后,则就可以得到最终的计算结果,也就是获取了最终预测的结果了。
        • 那么这个特征对应的权重如何获取或者如何制定呢?

现在有一组售房数据:

面积 售价
55 110
76 152
80 160
100 200
120 240
150 300

对售房数据的分布情况进行展示

Snip20230822_76

问题:假如现在有一套房子,面积为76.8平米,那么这套房子应该卖多少钱呢?也就是如何预测该套房子的价钱呢?上图中散点的分布情况就是面积和价钱这两个值之间的关系,那么如果该关系可以用一种分布趋势来表示的话,那么是不是就可以通过这分布趋势预测出新房子的价格呢?

Snip20230822_78

在上图中使用了一条直线来表示了房子的价格和面积对应的分布趋势,那么该趋势找到后,就可以基于该趋势根据新房子的面积预测出新房子的价格。

线性回归的作用:找出特征和目标之间存在的某种趋势,在二维平面中,该种趋势可以用一条线段来表示,该条线段用一元一次线性方程来表示:y = w * x + b。

将上述的售房数据,带入到线性方程中,经过求解,w和b变为了已知,现在方程为:y = 2 * x + 0。则发现,在上述售房数据中,面积和价格之间的关系是二倍的关系,其实就可以映射成:价格 = 2 * 面积 ,这个方程就是价格和面积的分布趋势,也就是说根据该方程就可以进行新房子价格的预测。

多元回归

思考:上述的线性方程y=wx+b其中x为特征y为目标,这种方程作为线性关系模型的预测依据的话是否可以满足所有的预测场景呢?

比如,现在房价受影响的因素不光是面积了,加入了采光率和楼层了,那么就意味着特征变成了3种。在原始的一元线性方程y=wx+b中只可以有一个特征,则该方程不具备通用性。可以使用多元方程!

多元方程为:y = w1 * x1 + w2 * x2 + wn * xn 存在多个自变量特征x
则房价预测可为:售价 = (w1 * 面积 + w2 * 采光率 + w3 * 楼层)+ b,考虑到多种特征因素对房价的影响。
Snip20230822_80

损失函数

如果在房价预测案例中,房子的面积和价格的分布规律如下图所示(非线性的分布),那是否还可以使用一条直线表示特征和目标之间的趋势呢?

Snip20230822_85

可以,只要保证直线距离所有的散点距离最近,则该直线还是可以在一定程度上表示非线性分布散点之间的分布规律。但是该规律进行的预测会存在一定的误差/损失

Snip20230822_84

在多数的预测场景中,预测结果和真实结果之间都会存在一定的误差,那么误差存在,我们应该如何处理损失/误差呢?

量化损失/损失函数:真实结果y和预测结果(xw)差异平方的累加和(误差平方和/残差平方和):

Snip20230822_90

提问:损失函数公式中,误差的大小和哪个系数有直系的关联呢?

答案:和权重系数w是有直系关联。也就是说w的不同会导致误差大小的不同,那么线性回归算法迭代训练过程中最终的问题就转化成了如何去求解线性方程中的w使得误差可以最小

如何求解最小损失

最小二乘法

首先我们认为损失函数是一种凹函数(损失变化和w之间的关系),损失函数对权重系数进行求导,让其一阶导数为0 即可。导数等于0说明函数在这一点的切线斜率为0,既切线平行于x轴,而且函数在这有极值。

Snip20230823_107

求解一阶导数:

Snip20230822_93

导数为0:

Snip20230822_94
核心三点:
	- 最小二乘法的作用:求解最小损失对应的参数向量w
	- 最小二乘法的原理:它认为模型的损失和w是满足一个凹函数。然后使用损失函数对w求导,让其一阶导数为0即可找到最小损失。
	- 最小二乘法返回结果解读:最小损失对应的w,生成w的公式中出现了y标签数据,因此标签参与了模型训练。

模型详情

正轨方程(最小二乘法):from sklearn.linear_model import LinearRegression

正规方程

波士顿房屋预测

from sklearn.model_selection import train_test_split
from sklearn.datasets import load_boston
from sklearn.linear_model import LinearRegression
import warnings
warnings.filterwarnings('ignore')

data = load_boston()
feature = data.data
target = data.target

x_train,x_test,y_train,y_test = train_test_split(feature,target,test_size=0.2,random_state=2020)

linner = LinearRegression()
linner.fit(x_train,y_train)

linner.coef_ #每一个特征维度的权重系数w
linner.intercept_ #返回截距

#将系数和特征名称结合在一起查看
[*zip(data.feature_names,linner.coef_)]

linner.score(x_test,y_test)

Day18 深度学习

文件: Day18 day18深度学习通知\深度学习.md

人工智能、机器学习,神经网络以及深度学习之间的关系

人工智能(Artificial Intelligence,简称AI)

人工智能其实是一种抽象的概念,它并不是指任何实际的算法。人工智能是指可以对人的意识、思维进行模拟,但又不是人的智能。

机器学习(Machine Learning,简称ML)

机器学习是一门多领域交叉学科,涉及概率论、统计学等多门学科。机器学习包含很多具体的算法。既然人工智能是飘在天上的概念,那我们就需要一些具体的算法使得人工智能可以落地应用,而一般来说,这些具体的智能算法可以统称为机器学习算法。

神经网络(Artificial Neural Networks,简称ANN)

神经网络是众多机器学习算法中的其中一个,是模仿人类大脑神经结构构建出来的一种算法,构建出来的网络称为人工神经网络。

深度学习(Deep Learning)

深度学习则是一种特殊形式的神经网络模型,可以简单理解成是神经网络的升级、进阶版本。

Snip20230819_67

神经网络

人工神经网络(英语:Artificial Neural Network,ANN),简称 神经网络(Neural Network,NN)或 类神经网络,是一种模仿生物神经网络的结构和功能的计算模型

人脑可以看做是一个生物神经网络,由众多的神经元连接而成。每个神经元与其他神经元相连(神经元主要由三部分构成:细胞体、轴突和树突。)。各个神经元传递复杂的电信号,树突接收到输入信号,然后细胞核对信号进行处理,通过轴突输出信号。

Snipaste_2024-11-06_09-36-19
- 树突分支比较多,每个分支还可以再分支,长度一般比较短,作用是接受信号。
- 细胞体负责将树突结束到的信号,进行相关处理。
- 轴突只有一个,从细胞体的一个凸出部分伸出,长度一般比较长,作用是把细胞体处理后的信号传出到其他神经元。
- 轴突的末端分为许多小支,也就是神经末梢,其负责将该神经元连接到其他神经元的树突上。
- 大脑可视作为 1000 多亿神经元组成的神经网络。

在计算机中我们如何构建人工神经网络中的神经元呢?

人工“神经元”

1943 年,心理学家 Warren McCulloch(沃伦·麦卡洛克) 和逻辑学家 Walter Pitts(沃尔特·皮茨) 将生物神经网络的神经元抽象为一个简易数学模型,也就是一直沿用至今的 M-P 神经元模型

image-20231221035927808

受生物神经元的启发,人工神经元接收来自其他神经元或外部源的输入(xi),每个输入都有一个相关的权值(wi),它是根据该输入对当前神经元的重要性来确定的,对该输入信号加权求和后(汇总信号),经过一个激活函数 f处理后,计算得到该神经元的输出

说明:

  • x0,x1,x2,....xi为各项输入数据

  • w0,w1,w2,...wi为各项输入数据的权重值。w可以调节输入信号的值的大小,让输入信号变大(w>0),为零(w=0)或者减小(w<0)。

  • b 为偏置(bias)

  • f为激活函数,常见的激活函数有Sigmoid、tanh、ReLU、ReLU、Softmax等等

激活函数用于将非线性因素引入神经网络,使其能够学习更复杂的函数。如果没有激活函数,神经网络只能学习其输入数据的线性组合函数。

简单来讲,激活函数的作用就像是给这个神经元加上了一个“魔法开关”,让它能够根据输入的不同做出不同的反应。

神经元简单应用

示例

假如有一个人工神经元,它有 3 个输入𝑥1,𝑥2,𝑥3,同时已知 b=-0.6,𝑤1=𝑤2=𝑤3=0.5,激活函数假设为sign(x),该函数的特点是当 x>0 时,输出值为 1;当 x= 0 时,输出值为 0,;当 x<0 时,输出值为-1。我们根据神经元计算公式y = 𝑓(∑( (𝑤i𝑥i) + 𝑏 ) = sign( ∑((𝑤i𝑥i) + 𝑏 ) 就可以得到如下计算结果:

x1 x2 x3 y
0 0 0 -1
0 1 1 1
1 0 0 -1
1 1 1 1
思考

我们知道神经元输出的结果就是y,那么计算y我们得求解出w,那么w是如何被求解出来的呢?需要用到学习规则。在学习规则计算中,我们是可以先随机生成各个特征维度对应的w,然后根据学习规则对w进行迭代调整。

学习规则

假设有如下一个学习规则,此处我们不解释这个学习规则是怎么推导出来的。在这里我们可以先接受下面的学习规则公式即可:

Snipaste_2024-11-06_10-37-02
△Wi表示第i个权值的变化;
η(eta)表示学习率,用来调节权值变化的大小;
t是正确的标签;
y是神经元返回结果。
综合实现

假设现在有一个神经元,已知有三个输入 x0=1,x1=0,x2=-1,权值随机初始值为 w0=-5,w1=0,w2=0,学习率𝜂=1,正确的标签 t=1。(注意在这个例子中偏置值 b 用 𝑤0 × 𝑥0来表示,x0 的值固定为 1)

  • Step1:我们首先计算神经元对这三个输入信号的输出

    Snipaste_2024-11-06_10-48-51

    由于正确标签为1,但是神经元计算出结果为-1,所以需要对神经元中的权值进行调整:

    Snipaste_2024-11-06_10-51-37
  • Step2:重新计算神经元输出,如果依然出现和正确标签不一致的情况发生,则继续迭代也就是再次根据学习规则调整各个权值,然后再次计算神经元输出结果,直到和正确标签一致,则迭代停止。

  • 代码实现:

    import numpy as np
    # 定义输入
    x0 = 1
    x1 = 0
    x2 = -1
    # 随机定义权值
    w0 = -5
    w1 = 0
    w2 = 0
    # 定义正确的标签
    t=1
    # 定义学习率lr(learning rate) 
    lr = 1
    # 循环一个比较大的次数,比如100 (迭代次数)
    for i in range(100):
        # 打印权值
        print(w0,w1,w2)
        # 计算感知器的输出
        y = np.sign(w0 * x0 + w1 * x1 + w2*x2) 
        # 如果感知器输出不等于正确的标签
        if(y != t):
            # 更新权值
            w0 = w0 + lr * (t-y) * x0 
            w1 = w1 + lr * (t-y) * x1
            w2 = w2 + lr * (t-y) * x2
        # 如果感知器输出等于正确的标签 
        else:# 训练结束 
            print('done') # 退出循环
            break
    

人工神经网络ANN

有了对单个神经元的理解和应用后,我们就可以利用神经元来构建神经网络了,如下图所示:

image-20231221040804909

神经网络的结构可以有多层,多层的神经网络可以由“输入层”, “隐藏层”以及“输出层”组成。其中隐藏层(输入和输出层中间的都是隐层)可能有 0 到多个,所以最简单的神经网络就只有输入层和输出层。神经网络的每一层都由若干个“神经元”节点组成。

注意:

1. 神经网络中信息只向一个方向移动,即从输入层的神经元节点向前移动,通过隐藏层神经元节点,再向输出层的神经元节点移动
2. 同一层的神经元之间没有连接。
3. 每一层神经元的输出就是下一层神经元的输入。
4. 每个连接都有一个权值。

思考:神经网络为何会设计成多层结构呢?

  1. 特征学习与提取:每一层神经元都可以从上一层的输出中学习并提取更有用的特征。
  2. 提高泛化能力:当网络有足够的层数和节点时,会使得多层神经网络具有较好的泛化能力。这就像是你学会了骑自行车后,即使换了一辆不同的自行车,你也能很快适应并骑好它。因为你已经掌握了骑自行车的基本技能和原理。
  3. 层次化的信息处理:多层结构提供了层次化的信息处理机制,每层负责不同的抽象级别。这种层次化有助于理解和解决问题。比如在图像识别任务中,第一层可能会识别出图像中的线条和边缘,第二层可能会识别出形状和纹理,而更高层则可能识别出整个物体或场景。

综上所述,神经网络设计多层的作用和意义在于可以使其能够更好地处理复杂问题并适应新环境。

Pytorch介绍与安装

1.GPU简介

1.1 背景

众所周知,深度学习作为一种能够从海量数据中自主学习和提炼知识的一种技术,并且正在为各行各业赋能。深度学习这一技术不仅赋予了计算机前所未有的智能能力,更为创新注入了强劲的动力,使得看似无法落地的业务场景充满了无限可能。

凭借其卓越的数据处理能力,深度学习使得计算机能够实现多种过去仅为人类所独有的认知智能。通常而言,深度神经网络的训练过程极其复杂,通常需要进行大量的并行计算。传统的计算设备难以满足这一需求,而 GPU则可以凭借其大规模并行计算架构,完美地提供了所需的计算能力。通过 GPU 的加持,深度学习模型得以高效训练,从而使得这些复杂的智能任务得以实现。因此,GPU 不仅是深度学习技术的核心算力引擎,更是推动人工智能不断向前发展的关键力量。

Snipaste_2024-11-14_09-19-04

1.2 何为GPU

GPU是图形处理单元,它是专门设计用来加速图像和视频渲染的处理器。随着计算机图形技术的发展,实时 3D 图形的渲染需求逐渐增多,传统的CPU(中央处理器)难以高效处理这些繁重的计算任务。GPU 的出现大大缓解了这一负担,通过其独特的架构将大量的图形处理操作从 CPU 中剥离出来,从而极大地提升了系统的图形处理能力。

GPU 的架构设计非常独特,由成百上千个小型处理单元组成,每个处理单元能够独立并行执行指令。这种高度并行的处理能力使 GPU 能够同时处理海量数据,这也是其与多核 CPU 的相似之处。CPU 虽然也有多核设计,但每个核心的任务通常是串行执行的。而 GPU 的每个小型处理单元则可以相互并行执行不同的指令集,使得 GPU 能够在短时间内处理大量计算任务。

Snipaste_2024-11-14_09-18-29

1.3 GPU&深度学习

在深度学习领域,GPU的作用主要体现在以下几个方面:

  1. 并行计算能力:GPU拥有大量的处理单元,它们能够同时处理数千个线程,这使得它在执行大规模矩阵运算和向量计算时具有显著优势。
  2. 高内存带宽:GPU使用高速显存,提供比CPU内存更高的带宽,这对于深度学习中频繁的数据读写操作至关重要。
  3. 优化的架构:GPU的架构专为数据并行和任务并行设计,适合执行大量相同类型的简单计算任务,如深度学习中的矩阵乘法和卷积运算。

总的来说,GPU通过其高度并行的处理能力和优化的内存访问速度,极大地加速了深度学习模型的训练过程,提高了模型的准确性和泛化能力。

1.4 CUDA简介

CUDA是一个由NVIDIA推出的通用并行计算平台和编程模型。它主要用于利用NVIDIA GPU(图形处理器)进行大规模并行计算,使得开发者能够更加高效地利用GPU的强大计算能力来处理各种复杂的计算任务。

NVIDIA;翻译为“英伟达”,一家以设计显示芯片和主板芯片组为主的半导体公司

CUDA与GPU的关系

  • 硬件基础:GPU是CUDA运行的硬件基础。没有GPU,CUDA就无法发挥其并行计算的优势。
  • 软件平台:CUDA是NVIDIA为GPU计算提供的软件平台和编程模型。它允许开发者使用C、C++和Python等编程语言编写在支持CUDA的处理器上运行的并行程序。
  • 并行计算:CUDA的核心是其并行计算能力。通过将计算任务分解成多个小任务,并分配给GPU上的多个核心同时执行,CUDA能够显著提高计算效率。
  • 优化性能:CUDA还提供了一系列的优化工具和库,帮助开发者更好地利用GPU资源,提高程序的性能。

总的来说,CUDA和GPU之间的关系紧密而复杂。CUDA作为编程模型和软件平台,为开发者提供了利用GPU进行高性能计算的能力;而GPU则作为硬件基础,为CUDA提供了强大的计算能力和并行处理能力。两者相辅相成,共同推动了并行计算的发展和应用。

CUDA和GPU的关系可以通过一个通俗的比喻来说明:

  1. GPU:想象GPU是一个巨大的超级市场,它拥有成千上万个收银台(核心),每个收银台都可以同时处理一笔交易(计算任务)。但是,如果没有组织和协调,这些收银台可能会混乱不堪,效率低下。
  2. CUDA:CUDA就像是这个超级市场的经理,它负责制定规则、分配任务、确保所有收银台高效运作。没有CUDA,GPU就像是一个没有组织的市场,虽然有很多资源,但无法有效利用。
  3. 关联:CUDA是NVIDIA开发的并行计算平台和编程模型,它允许开发者编写能够在GPU上运行的并行程序。GPU提供了并行计算的硬件基础,而CUDA提供了软件层面的支持,使得开发者能够充分利用GPU的强大计算能力。

总结来说,GPU是硬件,提供了并行计算的能力;而CUDA是软件,提供了一种编程模型,让开发者能够高效地利用GPU进行并行计算。两者结合,就像是一个有组织的超级市场,能够高效地处理大量的计算任务。

2.Pytorch框架介绍

2.1 Pytorch框架简介

在深度学习初始阶段,每个深度学习研究者都需要写大量的重复代码,为了提高工作效率,这些研究者就将这些代码写成了一个框架放到网上让所有研究者一起使用,接着网上就出现了不同的框架。

PyTorch是一个开源的深度学习框架,由Facebook人工智能研究院(FAIR)开发。它提供了一套动态张量计算库,具有易于使用、高效性能和强大的扩展性等特点。PyTorch支持在GPU上运行,能够大大加速深度学习模型的训练和推理过程。

在深度学习和人工智能领域,PyTorch已成为最受欢迎的开源框架之一。随着技术的不断进步,PyTorch也在持续更新和优化。

2.2 其他框架介绍

随着时间的推移,最为好用的几个框架被大量的人使用从而流行了起来,首先让我们来介绍一下目前全世界最为流行的几大深度学习框架。

  1. TensorFlow
    • 灵活性与可扩展性:TensorFlow由Google开发,支持分布式训练,能够在不同硬件上高效运行,拥有庞大的社区和丰富的资源。
    • 适用场景:适用于各种应用场景,如自然语言处理、图像识别和语音合成等。
  2. PyTorch
    • 易用性与灵活性:PyTorch由Facebook开发,强调易用性和灵活性,提供动态计算图,便于模型构建和调试。
    • 适用场景:在科研领域非常受欢迎,适用于快速原型设计和实验。
  3. Keras
    • 高层次API:Keras是一个高层次的深度学习框架,专注于易用性和可扩展性,支持多种后端。
    • 适用场景:适用于快速原型设计和生产级应用开发。
  4. Caffe2
    • 高性能与灵活性:Caffe2由Facebook开源,适用于构建和部署大规模的机器学习模型。
    • 适用场景:实时分析、预测和推荐等应用。
  5. MXNet
    • 多语言支持:MXNet是一个高性能、可扩展的深度学习框架,支持多种语言和平台。
    • 适用场景:自然语言处理、计算机视觉和语音识别等。
  6. Theano
    • 性能与稳定性:Theano是一个Python编写的深度学习框架,强调性能和稳定性。
    • 适用场景:研究和原型设计,尤其是在数学和理论深度学习方面。
  7. CNTK
    • 微软开发:CNTK是微软开发的深度学习框架,具有高性能、灵活性和易用性。
    • 适用场景:自然语言处理、图像识别和语音合成等。

总的来说,在选择深度学习框架时,需要考虑项目的具体需求、团队的技能水平、硬件资源等因素。例如,对于需要快速迭代和实验的项目,PyTorch可能是一个更好的选择;而对于需要大规模部署和生产的项目,TensorFlow可能更适合。同时,也要考虑框架的社区活跃度和长期维护情况,以确保项目的可持续发展。

3.Pytorch安装

3.1 安装准备

在开始安装PyTorch之前,我们需要进行一些准备工作。首先,确保您的计算机上已安装了合适版本的Python。PyTorch需要Python 3.6或更高版本。同时,您还需要安装pip包管理器,用于安装PyTorch和其他依赖项。所以我们可以通过安装Anaconda来创建一个适合深度学习的Python环境。

Anaconda安装:访问Anaconda官网(Free Download | Anaconda),选择适合你操作系统的版本,并下载安装包

3.2 NVIDIA显卡

在安装PyTorch之前,我们需要判断您的计算机是否安装了NVIDIA显卡,因为PyTorch的GPU版本需要NVIDIA显卡来加速计算。您可以通过以下步骤来判断:

1.设备管理器:在Windows上,按下Win键和X键,然后选择“设备管理器”。在macOS上,打开“系统偏好设置”,选择“硬件”选项卡,然后点击“设备管理器”。

2.显示适配器:在设备管理器中,展开“显示适配器”或“图形处理器”部分,查看是否有NVIDIA显卡的列表。如果有NVIDIA显卡,那么您的计算机适合安装PyTorch的GPU版本。

如果没有NVIDIA显卡,您可以只安装PyTorch的CPU版本。如果您有NVIDIA显卡,您还需要进行其他步骤来安装PyTorch的GPU版本。

Windows系统,无NVIDIA显卡示例图:

Snipaste_2024-11-14_09-54-14

Windows系统,有NVIDIA显卡示例图:

Snipaste_2024-11-14_09-54-25

3.3 安装CPU版本Pytorch

在安装PyTorch之前,为了管理不同项目的Python环境,通常建议创建一个虚拟环境。虚拟环境可以帮助您隔离不同项目的依赖项,避免不同项目之间的冲突。以下是创建虚拟环境的步骤:

创建虚拟环境:运行以下命令来创建一个新的虚拟环境。您可以将<env_name>替换为您喜欢的环境名称,例如“myenv”

conda create -n <env_name> python=<version>

例如,要创建一个名为myenv的虚拟环境,其中Python版本为3.9,可以运行:

conda create -n myenv python=3.9

激活虚拟环境:运行以下命令来激活刚刚创建的虚拟环境。在Windows上,使用activate命令;在macOS和Linux上,使用source命令:

conda activate myenv
Snipaste_2024-11-14_10-00-34

安装PyTorch(CPU):在激活的虚拟环境中,使用pip安装PyTorch。根据您的需求,选择安装CPU版本。例如:(网络不佳和必要的时候可以添加国内源)

pip install torch torchvision torchaudio
Snipaste_2024-11-14_10-01-39

使用pip list查看是否安装成功:

Snipaste_2024-11-14_10-02-09

至此,CPU版本的Pytorch就安装成功了!

3.4 安装GPU版本Pytorch

3.4.1 查看CUDA显卡的驱动版本

在cmd命令行终端输入nvidia-smi,可以查看到版本为12.2。

Snipaste_2024-11-14_10-06-22
3.4.2 安装CUDA

从官网下载对应的CUDA版本,由于我的显卡版本为12.2,我只需要安装小于或者等于12.2都是可以的,因此这里我安装12.2。

官网地址:CUDA Toolkit Archive | NVIDIA Developer

Snipaste_2024-11-14_10-12-22

下一步:

Snipaste_2024-11-14_10-16-05

下载好后,双击安装包进行安装,可以安装在自定义的目录文件夹下

选择“精简”模式,接下来一直点“下一步”就行了:

Snipaste_2024-11-14_10-24-52

查看是否安装成功,在命令行输入以下指令进行检查,出现以下类似的输出就证明安装成功。

nvcc  -V
Snipaste_2024-11-14_10-35-51
3.4.4 安装Pytorch-GPU

在安装PyTorch之前,为了管理不同项目的Python环境,通常建议创建一个虚拟环境。虚拟环境可以帮助您隔离不同项目的依赖项,避免不同项目之间的冲突。以下是创建虚拟环境的步骤:

创建虚拟环境:运行以下命令来创建一个新的虚拟环境。您可以将<env_name>替换为您喜欢的环境名称,例如“myenv”

conda create -n <env_name> python=<version>

例如,要创建一个名为myenv的虚拟环境,其中Python版本为3.9,可以运行:

conda create -n myenv python=3.9

激活虚拟环境:运行以下命令来激活刚刚创建的虚拟环境。在Windows上,使用activate命令;在macOS和Linux上,使用source命令:

conda activate myenv
Snipaste_2024-11-14_10-00-34

在激活的虚拟环境中,使用pip安装PyTorch。根据您的需求,选择安装GPU版本。例如:(网络不佳和必要的时候可以添加国内源)

官网链接:PyTorch

Snipaste_2024-11-14_10-58-42
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121

注意:该安装会比较慢(1-2小时),请耐心等待!

3.4.5 验证安装

import torch
 
print(torch.__version__)
print(torch.version.cuda)
print(torch.cuda.is_available())  #输出为True,则安装成功

现在,您可以开始探索PyTorch的世界,利用它强大的计算能力和灵活性来开发出更加智能的应用程序

阿里天池

无需手动安装Pytorch和CUDA。

https://tianchi.aliyun.com/?spm=a2c22.12281897.J_3941670930.2.759023b7dWoNXw

登录注册,进行基本信息补全和实名认证后获得积分获取GPU算力资源。

Snipaste_2024-12-27_15-26-54

迁移学习

前言

在深度神经网络算法的应用过程中,如果我们面对的是数据规模较大的问题,那么在搭建好深度神经网络模型后,我们势必要花费大量的算力和时间去训练模型和优化参数,最后耗费了这么多资源得到的模型只能解决这一个问题,性价比非常低。

如果我们用这么多资源训练的模型能够解决同一类问题,那么模型的性价比会提高很多,这就促使使用迁移模型解决同一类问题的方法出现。因为该方法的出现,我们通过对一个训练好的模型进行细微调整,就能将其应用到相似的问题中,最后还能取得很好的效果;另外,对于原始数据较少的问题,我们也能够通过采用迁移模型进行有效解决,所以,如果能够选取合适的迁移学习方法,则会对解决我们所面临的问题有很大的帮助。

Snipaste_2024-12-06_15-27-28

比如上述图片描述的一个神经网络, 我花了两天训练完之后, 它已经能正确区分图片中具体描述的是男人, 女人还是眼镜. 说明这个神经网络已经具备对图片信息一定的理解能力. 这些理解能力就以参数的形式存放在每一个神经节点中. 不巧, 领导下达了一个紧急任务,要求今天之内训练出来一个预测图片里实物价值的模型. 我想这可完蛋了, 上一个图片模型都要花两天, 如果要再搭个模型重新训练, 今天肯定出不来呀.

Snipaste_2024-12-06_15-28-29

这时, 迁移学习来拯救我了. 因为这个训练好的模型中已经有了一些对图片的理解能力, 而模型最后输出层的作用是分类之前的图片, 对于现在计算实物价值的任务是用不到的, 所以我将最后一层替换掉, 变为服务于现在这个任务的输出层. 接着只训练新加的输出层, 让理解力保持始终不变. 前面的神经层庞大的参数不用再训练, 节省了我很多时间, 我也在一天时间内, 将这个任务顺利完成。

迁移学习介绍

你会发现聪明人都喜欢”偷懒”, 因为这样的偷懒能帮我们节省大量的时间, 提高效率. 还有一种偷懒是 “站在巨人的肩膀上”。不仅能看得更远, 还能看到更多。这也用来表达我们要善于学习先辈的经验, 一个人的成功往往还取决于先辈们累积的知识。这句话, 放在机器学习中, 这就是今天要说的迁移学习了。

迁移学习(Transfer Learning)是一种机器学习方法,它利用一个已经训练好的模型来解决新的问题。在深度学习中,迁移学习通常指的是将一个预训练的神经网络模型应用于新的任务或数据集上,以减少训练时间和提高模型性能。或者说迁移学习就是把一个领域(即源领域)的知识,迁移到另外一个领域(即目标领域),使得目标领域能够取得更好的学习效果。

形象化理解迁移学习

想象你在高中时学了很多数学知识,比如代数、几何和微积分。这些知识在你进入大学学习工程、物理或经济学时非常有用。即使你的专业领域不同,你仍然可以利用这些基础知识来帮助你更快地理解和掌握新的概念。这就是迁移学习的精髓:利用已有的知识来解决新问题。

这就好比,Google 和百度的关系, facebook 和人人的关系, KFC 和 麦当劳的关系, 同一类型的事业, 不用自己完全从头做, 借鉴对方的经验, 往往能节省很多时间. 有这样的思路, 我们也能偷偷懒, 不用花时间重新训练一个无比庞大的神经网络, 借鉴借鉴一个已经训练好的神经网络就行。

Snipaste_2024-12-06_15-23-31

在深度学习中,我们可以把这个过程类比为使用一个已经在大规模数据集上训练好的神经网络模型,并将其应用到一个新的、相关的任务上。这样,我们不需要从头开始训练一个全新的模型,而是利用已有模型的“知识”来加速新任务的学习过程。

为什么现在需要迁移学习?

前百度首席科学家、斯坦福的教授吴恩达(Andrew Ng)在曾经说过:「迁移学习将会是继监督学习之后的下一个机器学习商业成功的驱动力」。

Snipaste_2024-12-06_15-09-14

吴恩达在一次采访中,也提到迁移学习会是一个很有活力的领域,我们之所以对迁移学习感到兴奋,其原因在于现代深度学习的巨大价值是针对我们拥有海量数据的问题。但是,也有很多问题领域,我们没有足够数据。比如语音识别。在一些语言中,比如普通话,我们有很多数据,但是那些只有少数人说的语言,我们的数据就不够庞大。所以,为了针对数据量不那么多的中国少数人所说的方言进行语音识别,能将从学习普通话中得到的东西进行迁移吗?我们的技术确实可以做到这一点,我们也正在做,但是,这一领域的进步能让人工智能有能力解决广泛得多的问题。

迁移学习的作用和应用场景

  1. 数据稀缺:当目标数据集较小且难以收集大量标注数据时,迁移学习可以帮助利用已有的大型数据集上的预训练模型,从而减少对大量标注数据的依赖。
  2. 加快训练速度:使用预训练模型可以大大减少在新任务上的训练时间,因为大部分权重已经被优化过了,只需对特定层的权重进行微调即可。
  3. 提高模型性能:预训练模型通常在大型数据集上经过长时间训练,具有较好的特征提取能力。通过迁移学习,可以在新任务上获得更好的性能。
  4. 跨领域应用:迁移学习不仅限于计算机视觉领域,还可以应用于自然语言处理、语音识别等多个领域。
  5. 微调与冻结:在迁移学习中,常见的做法是冻结预训练模型的部分层(通常是前面的几层),只对后面的层进行微调。这样可以保持模型的基本特征提取能力,同时适应新的任务。

迁移学习理解

迁移学习允许我们将一个预训练模型应用于新的任务。通过微调(Fine-Tuning)预训练模型,我们可以利用已经学习到的特征表示,使模型更好地适应特定任务。

假设现在有两个任务一个是TASK A,一个是TASK B。其中TASK B是我们的目标任务,进行猫狗分类。TASK A是其他学者做出来的网络模型(预训练模型)。

TASK A可能是个非常大的任务,它对数据、计算资源和时间的要求都非常高,但好处是,这些是别人已经训练好的任务。

TASK B是我们的目标任务,这个任务没有那么大,因为它的样本量只有几百张图像,没有到上千万这样的级别。样本量小带来的好处是计算量小,但坏处是如果需要训练一个更加复杂模型时,样本量就不够了。

这时候,一个想法是能否把TASK A训练好的模型结构和权重直接应用到TASK B上,这就有点像果树嫁接。答案当然是可以的,但是需要注意以下两个问题:

(1)输入问题。输入相对来说比较简单,无论哪个TASK,它的输入都是图像,我们只要保证两个任务中输入图像的像素相同即可。

(2)输出问题。输出这是关键!TASK A的输出可能是为了区分1000个类别,但是我们的TASK B简单很多,只分为两类。

Snipaste_2024-12-06_15-08-26

那么如何解决输出问题呢?

  • 最简单的办法就是把TASK A整个模型中最后面的那几层输出(通常是全连接层),替换成TASK B想要的形式,例如,猫狗分类,只需要最终输出两个节点。
  • 我们要清楚,一般在图像分类的问题当中,卷积神经网络最前面的层用于识别图像最基本的特征,比如物体的轮廓,颜色,纹理等等,而后面的层才是提取图像抽象特征的关键,因此我们只需要使用TASK A前面的网络层级,对后面的全连接层进行重新训练即可(模型微调)。

迁移学习的几种方式

  1. 冻结预训练模型的全部输入层和隐层,只训练自己定制的全连接层(输出层)。

  2. 冻结预训练模型的部分网络层,训练剩下的网络层和全连接层。

  3. 获取预训练模型的参数值作为新模型参数的初始化的值,然后抛开预训练模型,只训练自己定制的网络模型,以此增加新模型的收敛速度。

预训练模型

torchvision.models:这个包中包含alexnet、densenet、inception、resnet、squeezenet、vgg等常用的网络结构,并且提供了预训练模型,可以通过简单调用来读取网络结构和预训练模型。

import torchvision.models as models

resnet18 = models.resnet18(pretrained=True,progress=True)
vgg16 = models.vgg16(pretrained=True,progress=True)
alexnet = models.alexnet(pretrained=True,progress=True)
squeezenet = models.squeezenet1_0(pretrained=True,progress=True)

# Check if GPU is available  
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  
model = alexnet.to(device)

参数介绍:

  • pretrained (bool):True表示返回在 ImageNet 数据集上预训练的模型(训练好的),False表示只返回模型的网络架构(没有训练好)
  • progress (bool) 如果为 True,则显示下载到 stderr 的进度条

下载的模型可以通过**state_dict() **来获取模型参数、缓存的字典,如下所示:

import torchvision.models as models
vgg16 = models.vgg16(pretrained=True)
# 返回包含模块所有状态的字典,包括参数和缓存
pretrained_dict = vgg16.state_dict()

如果只需要网络结构,不加载参数来初始化,可以将pretrained = False

model = torchvision.models.densenet169(pretrained=False)
# 等价于:
model = torchvision.models.densenet169()

应用VGG16模型,并进行改动,以适应CIDIAR10数据集

  • CIFAR10数据集是 10个类别
  • VGG16输出是1000个类别
  • VGG 加一层输出10个类别

1.导入模型,输出查看网络结构:

import torchvision
# 直接调用,实例化模型,pretrained代表是否下载预先训练好的参数
vgg16_false = torchvision.models.vgg16(pretrained = False)
vgg16_ture = torchvision.models.vgg16(pretrained = True)
print(vgg16_ture)

输出结果,可以看到VGG16的结构,可以看出,其最后一行 out_features = 1000.

2.模型的修改

  • 修改方式1:如在classifier 新增一层全连接层, 使用 add.module()

    vgg16_ture.classifier.add_module("add_linear",torch.nn.Linear(1000,10)) # 在vgg16的classfier里加一层
    print(vgg16_ture)
    

    输出之后,在classifier里,可以看到,最后一行:out_features = 10

  • 修改方式2:直接修改对应层,编码相对应

    print(vgg16_false)
    vgg16_false.classifier[6] = nn.Linear(4096,10) # 修改对应层,编号相对应
    print(vgg16_false)
    

    修改之前与修改之后对比,可以看到classifier中第6行的变化

网络层冻结

概述

当我们想要对一个预训练模型进行微调(fine-tuning),或者仅仅只想训练模型中的某些特定层时,就需要用到“冻结”这个概念了。冻结部分参数的意思是在模型训练过程中保持这些参数不变,这在迁移学习中非常常见。

什么是冻结?

冻结是指在模型训练过程中,阻止模型的一部分参数进行更新。在深度学习中,我们经常使用预训练模型作为初始权重,然后仅对特定层进行微调。

冻结整个模型

如果你希望冻结整个模型,你可以这样操作:

import torch.nn as nn
from torchvision import models

model = models.resnet18(pretrained=True)
for param in model.parameters(): #返回模型所有网络层的权重系数
    param.requires_grad = False
#上面的代码将会冻结整个ResNet18模型的所有参数。此时如果继续训练模型,所有参数都不会更新。

冻结模型的特定部分

如果你只需要冻结模型的某些部分网络层,可以按照下面的方式来做:

# 假设model是一个ResNet18模型
for name, param in model.named_parameters():#返回网络层名字和其对应的权重系数
    if 'layer4' not in name: # 只冻结除了layer4之外的所有层
        param.requires_grad = False
        

解冻

解冻就是将之前冻结的参数重新变为可训练状态,这可以通过将requires_grad属性设置为True来完成。

for name, param in model.named_parameters():
    if 'layer4' in name: # 只解冻layer4
        param.requires_grad = True

day19-NLP通识

文件: day19-NLP通识\NLP相关+RNN.md

自然语言NLP

NLP基本介绍

NLP(Natural Language Processing)是一种人工智能和语言学领域的交叉学科,旨在让计算机能够理解和生成人类的语言,实现人机之间的自然交流

其具体定义为:自然语言处理(Natural Language Processing, NLP)是利用计算机科学、人工智能和语言学的理论和方法,研究如何让计算机能够“听懂”人类的语言,并实现与之无障碍交流的技术。

其目标为:使计算机能够理解、处理、生成和模拟人类语言,从而执行语言翻译、情感分析、文本摘要等任务。

Snipaste_2024-12-16_10-37-10

NLP的常见应用场景

自然语言处理(NLP)技术是人工智能领域的一个重要分支,它致力于让计算机能够理解和生成人类语言。以下是一些常见的NLP应用场景及其具体描述和示例:

  1. 机器翻译:机器翻译是NLP技术中最为人所熟知的场景之一。通过将输入的源语言文本自动翻译成另一种语言的文本,NLP技术极大地促进了国际交流、商务合作和跨文化沟通。例如,百度翻译、Google翻译等在线翻译工具就是基于NLP技术开发的,它们能够实现多种语言之间的实时翻译。
  2. 情感分析:情感分析是NLP的一个子领域,它涉及识别和分类文本中的主观信息,如情感倾向(积极、消极或中性)。这一技术在市场研究、品牌监控和社交媒体分析中尤为重要。企业可以利用情感分析来了解消费者对其产品或服务的看法,从而及时调整市场策略和改进产品。
  3. 聊天机器人与虚拟助手:聊天机器人和虚拟助手是NLP技术的又一重要应用。这些系统能够理解用户的自然语言输入,并提供相应的回答或执行任务。在客户服务、在线购物和个人助理等领域,聊天机器人已经得到了广泛应用。例如,Siri、Alexa、Google Assistant等智能语音助手就是基于NLP技术开发的,它们能够与用户进行自然语言交流,提供各种便捷服务。
  4. 文本摘要与内容提取:NLP技术还可以自动生成文本的摘要,这对于快速获取大量信息的概要非常有用。文本摘要在新闻聚合、研究论文阅读和企业报告中具有极高的实用价值。通过自动提炼文档核心信息,NLP技术帮助用户快速抓住文章的主旨和要点,提高工作效率。
  5. 智能客服:智能客服是一个广泛应用NLP技术的领域。利用NLP技术,智能客服系统能够理解客户提出的问题,并提供准确的解答,从而提高了客服质量。这些系统还可以实时处理大量客户查询,降低了等待时间,增加了效率。
  6. 搜索引擎优化:在搜索引擎领域,NLP技术扮演着至关重要的角色。通过分析用户的查询意图和网页内容,NLP技术能够更准确地匹配搜索词和网页内容,从而提供更为相关和精准的搜索结果。这种技术的应用不仅提高了搜索效率,还极大地提升了用户体验。
  7. 医疗健康与法律领域:在医疗健康领域,NLP技术被用于电子健康记录的分析、临床决策支持和患者交流。通过自动提取病历文档中的关键信息,NLP技术为医生提供了更为全面和准确的诊断依据。在法律领域,NLP技术则被用来分析法律文件、合同和案例,以辅助法律专业人士进行研究和决策。

总之,随着技术的不断进步和应用领域的不断拓展,NLP将在未来的人工智能领域中发挥更加重要的作用,为人类社会带来更多的便利和创新。

NLP领域中的数据

概述

自然语言领域的核心数据是序列数据,这是一种在样本与样本之间存在特定顺序、且这种特定顺序不能被轻易修改的数据。这是什么意思呢?在机器学习和普通深度神经网络的领域中我们所使用的数据是二维表。如下所示,在普通的二维表中,样本与样本之间是相互独立的,一个样本及其特征对应了唯一的标签,因此无论我们先训练1号样本、还是先训练7号样本、还是只训练数据集中的一部分样本,都不会从本质上改变数据的含义、许多时候也不会改变算法对数据的理解和学习结果。

Snipaste_2024-12-23_08-38-39

但序列数据则不然,对序列数据来说,一旦调换样本顺序样本发生缺失,数据的含义就会发生巨大变化。最典型的序列数据有以下几种类型:

  1. 文本数据(Text Data):文本数据中的样本的“特定顺序”是语义的顺序,也就是词与词、句子与句子、段落与段落之间的顺序。在语义环境中,词语顺序的变化或词语的缺失可能会彻底改变语义,例如——

    改变顺序:事半功倍和事倍功半;曾国藩战太平天国时非常著名的典故:他将“屡战屡败”修改为“屡败屡战”,前者给人绝望,后者给人希望。

    样本缺失(对文本来说特指上下文缺失):小猫睡在毛毯上,因为它很____。当我们在横线上填上不同的词(暖/冷)时,句子的含义会发生变化。

  2. 音频数据(Audio Data):音频数据大部分时候是文本数据的声音信号,此时音频数据中的“特定顺序”也是语义的顺序;当然,音频数据中的顺序也可能是音符的顺序,试想你将一首歌的旋律全部打乱再重新播放,那整首歌的旋律和听感就会完全丧失。

  3. 视频数据(Video Data):你知道动画是由一张张原画构成的吗?视频数据本质就是由一帧帧图像构成的,因此视频数据是图像按照特定顺序排列后构成的数据。和音频数据类似,如果将动画或电影中的画面顺序打乱再重新播放,那没有任何人能够理解视频的内容。

很明显,在处理序列数据时,我们不仅要让算法理解每一个样本,还需要让算法学习到样本与样本之间的联系

序列数据的结构

序列数据的概念很容易理解,但奇妙的是,现实中的序列数据可以是二、三、四、五任意维度,只要给原始的数据加上“时间顺序”或“位置顺序”,任意数据都可以化身为序列数据。在这里,我们展现几种常见的序列数据:

二维时间序列

Snipaste_2024-12-23_08-45-08

时间序列中,样本与样本之间的顺序是时间顺序,因此每个样本是一个时间点,时间顺序也就是time_step这一维度上的顺序。这种顺序在自然语言处理领域叫做“时间步”(time_step),也被叫做“序列长度”,这正是我们要求算法必须去学习的顺序。在时间序列数据中,时间点可以是任意时间单位(分钟、小时、天),但时间点与时间点之间的间隔必须是一致的。

三维时间序列

在NLP领域中,我们常常一次性处理多个时间序列,如下图所示,我们可以一次性处理多支股票的股价波动序列。

Snipaste_2024-12-23_08-48-57

此时我们拥有的是一个三维矩阵,其中batch_size是样本量,也就是一共有多少个二维时间序列表单。因为深度学习算法会同时处理多个内在逻辑相同的时间序列。其中time_step和input_dimension决定了一个时间序列的序列长度和特征量,而batch_size决定了整个数据集中一共有多少个二维时间序列表单。这些二维表单堆叠在一起,构成深度学习算法输入所必备的三维时间序列。

二维文字序列

Snipaste_2024-12-23_08-54-33

在文字数据中,样本与样本之间的联系大部分时候是词与词、字与字之间的联系,因此在文字序列中每个样本是一个单词或一个字(对英文来说大部分时候是一个单词,偶尔也可以是一个字母),故而在中文文字数据中,一张二维表往往是一个句子或一段话。此时,不能够打乱顺序的维度是vocab_size,它代表了一个句子 /一段话中的字词总数量。一个句子或一段话越长,vocab_size也就会越大,因此这一维度的作用与时间序列中的time_step一致,vocab_size在许多时候也被称之为是序列长度(sequence_length)。同样,vocab_size这一维度上的顺序就是算法需要学习的顺序。

需要注意的是,文字序列是不能直接放入算法进行运行的,必须要要编码成数字数据才能供算法学习,因此在NLP领域中我们大概率会将文字数据进行编码。编码的方式有很多种,但无一例外的,文字编码的本质是用单一数字或一串数字的组合去代表某个字/词,在同一套规则下,同一个字会被编码为同样的序列或同样的数字,而使用一个数字还是一串数字则可以由算法工程师自行决定。下图是对句子分别进行embedding编码和独热编码后产生的二维表单:

Snipaste_2024-12-23_08-57-45

大部分时候,我们需要学习的肯定不止一个句子,当每个句子被编码成矩阵后,就会构成高维的多特征词向量。由于在实际训练时,所有句子或段落长度都一致的可能性太小(即所有句子的vocab_size都一致的可能性太小),因此我们往往为短句子进行填充、或将长句子进行裁剪,让所有的特征词向量保持在同样的维度。

Embedding编码解读: 词嵌入/词向量

继续假设我们有一句话,叫“公主很漂亮”,如果我们使用one-hot编码,可能得到的编码如下:

公 [0 0 0 0 1]
主 [0 0 0 1 0]
很 [0 1 0 0 0]
漂 [0 0 1 0 0]
亮 [1 0 0 0 0]

这样的编码,最大的好处就是,不管你是什么字,我们都能在一个一维的数组里用01给你表示出来。并且不同的字绝对不一样,以致于一点重复都没有,表达本征的能力极强。但是,因为其完全独立,其劣势就出来了。表达关联特征的能力几乎为0!!!举个例子,我们又有一句话 “王妃很漂亮” 那么在这基础上,我们可以把这句话表示为

王 [0 0 1 0 0 ]
妃 [1 0 0 0 0 ]
很 [0 0 0 1 0 ]
漂 [0 1 0 0 0 ]
亮 [0 0 0 0 1 ]

从中文表示来看,我们一下就跟感觉到,王妃跟公主其实是有很大关系的,比如:公主是皇帝的女儿,王妃是皇帝的妃子,可以从“皇帝”这个词进行关联上;公主住在宫里,王妃住在宫里,可以从“宫里”这个词关联上;公主是女的,王妃也是女的,可以从“女”这个字关联上。但是呢,我们用了one-hot编码,公主和王妃就变成了这样:

王 [0 0 1 0 0 ]
妃 [1 0 0 0 0 ]

公 [0 0 0 0 1]
主 [0 0 0 1 0]

你知道这四行向量有什么内部关系吗?看不出来,那怎么办?

既然,通过刚才的假设关联,我们关联出了“皇帝”、“宫里”和“女”三个词,那我们尝试这么去定义公主和王妃

公主一定是皇帝的女儿,我们假设她跟皇帝的关系相似度为1.0;公主从一出生就住在宫里,直到20岁才嫁到府上,活了80岁,我们假设她跟宫里的关系相似度为0.25;公主一定是女的,跟女的关系相似度为1.0;

王妃是皇帝的妃子,没有亲缘关系,但是有存在着某种关系,我们就假设她跟皇帝的关系相似度为0.6吧;妃子从20岁就住在宫里,活了80岁,我们假设她跟宫里的关系相似度为0.75;王妃一定是女的,跟女的关系相似度为1.0;于是公主王妃四个字我们可以这么表示:

​ 皇帝 宫里 女
公主 [ 1.0 0.25 1.0]
王妃 [ 0.6 0.75 1.0]

这样我们就把公主和王妃两个词,跟皇帝、宫里、女这几个字(特征)关联起来了,我们可以认为:

公主=1.0 皇帝 +0.25宫里 +1.0*女

王妃=0.6 皇帝 +0.75宫里 +1.0*女

或者我们假设每个词的每个字都是对等(注意:只是假设,为了方便解释),则公主(2个字)和王妃(2个字)的表示为:

​ 皇帝 宫里 女

公 [ 0.5 0.125 0.5]
主 [ 0.5 0.125 0.5]
王 [ 0.3 0.375 0.5]
妃 [ 0.3 0.375 0.5]

公主与皇帝的相似度为1,与皇宫的相似度为0.25,与女性的相似度为1
王妃与皇帝的相似度为0.6,与皇宫的相似度为0.75,与女性的相似度为1

这样,我们就把一些词甚至一个字,用三个特征给表征出来了。于是乎,我们把文字的one-hot编码,从稀疏态变成了密集态,并且让相互独立向量变成了有内在联系的关系向量。

所以,embedding层做了个什么呢?它把我们的稀疏矩阵,通过某种变换将其变成了一个密集矩阵,这个密集矩阵用了N(例子中N=3,皇帝、公里和女)个特征来表征所有的文字,在这个密集矩阵中,表象上代表着密集矩阵跟单个字的一一对应关系,实际上还蕴含了大量的字与字之间,词与词之间甚至句子与句子之间的内在关系。他们之间的关系,用的是嵌入层学习来的参数进行表征。从稀疏矩阵到密集矩阵的过程,叫做embedding。

三维文字序列

Snipaste_2024-12-23_08-59-37 Snipaste_2024-12-23_08-59-46

此时我们拥有的是一个三维矩阵,其中batch_size是样本量,也就是一共有多少个二维文字序列表单。

循环神经网络RNN

RNN概述

循环神经网络(Recurrent Neural Network)是自然语言处理领域的入门级深度学习算法,也是序列数据处理方法的经典代表作,它开创了“记忆”方式、让神经网络可以学习样本之间的关联、它可以处理时间、文字、音频数据,也可以执行NLP领域最为经典的情感分析、机器翻译等工作。

RNN基本架构

如果你去找寻网络上的各种资源,你会惊讶地发现循环神经网络有各种各样复杂的公式表示和图像表示方法。然而,光从网络架构来说,循环神经网络与深度神经网络是完全一致的

首先,循环神经网络由输入层、隐藏层和输出层构成,输入层的神经元个数由输入数据的特征数量决定,隐藏层数量和隐藏层上神经元的个数都可自己设置,而输出层的神经元数量则需要根据输出的任务目标进行设置。假如,现在我们将每个单词都编码成了5个特征构成的词向量,因此输入层就会需要5个神经元,我们将该文字数据输入循环神经网络执行三分类的“情感分类”任务(三分类分别是[积极,消极,中性]),那输出层就会需要三个神经元。假设有一个隐藏层,而隐藏层上有2个神经元,一个最为简单的循环网络的网络结构如下:

Snipaste_2024-12-23_10-12-24

在这个结构中,激活函数的设置、神经元的连接方式等都与深度神经网络一致,因此循环神经网络在网络构建方面没有太多可以深究的内容,循环网络真正精彩的地方在于其创造了全新的数据流

RNN数据流

当我们将数据输入到循环神经网络时,一个神经元一次性只会处理一个单词的一个数据,5个神经元会覆盖当前单词的5个特征,在一次正向传播中,循环神经网络只会接触到一个单词的全部信息。

Snipaste_2024-12-23_10-30-11

如果这样的话,岂不是要一行一行处理数据了?没错,没错,虽然非常颠覆神经网络当中对效率的根本追求,但循环神经网络是一个单词、一个单词处理文本数据,一个时间点、一个时间点处理时序数据的。具体过程如下:

Snipaste_2024-12-23_10-31-01 Snipaste_2024-12-23_10-31-09 Snipaste_2024-12-23_10-31-15

如果一次正向传播只处理一行数据,那对于结构为(vocab_size,input_dimension)的文字数据来说,就需要在同一个网络上进行vocab_size次正向传播。同样的,对于结构为(time_step,input_dimension)的时间序列数据来说,就需要在同一个网络上进行time_step次正向传播。在循环神经网络中,vocab_size和time_step这个维度可以统称为sequence_length,同时还有一个更常见的名字叫做时间步,对任意数据来说,循环神经网络都需要进行时间步次正向传播,而每个时间步上是一个单词或一个时间点的数据

基于这样的数据流设置,循环神经网络构建了自己的灵魂结构:循环数据流。在多次进行正向传播的过程中,循环神经网络会将每个单词的信息向下传递给下一个单词,从而让网络在处理下一个单词时还能够“记得”上一个单词的信息。循环网络在不同时间步的隐藏层之间建立了链接

如下图所示,在Tt-1时间步上时,循环网络处理了一个单词,此时隐藏层上输出的中间变量Ht-1会走向两条数据流,一条数据流是继续向输出层的方向正向传播,另一条则流向了下一个时间步的隐藏层。在T时间步时,隐藏层会结合当前正向传播的输入层传入的Xt和上个时间步的隐藏层传来的中间变量Ht-1共同计算当前隐藏层的输出Ht。如此,Ht当中就包含了上一个单词的信息。

假设当前时间步是t-1,当前时间步上的输入特征为𝑋𝑡−1,输入层与隐藏层之间的的权重为𝑊xh,隐藏层与输出层之间的权重为𝑊hy,当𝑋𝑡−1进入神经网络后时,权重𝑊xh将与输入信息𝑋𝑡−1共同计算,构成中间变量𝐻𝑡−1,这一中间变量被称之为是“隐藏状态”,代表在隐藏层上输出的值。

在深度神经网络中,𝐻𝑡−1将会被传导向输出层,与𝑤hy共同计算后构成输出层上的输出,但在循环神经网络中,𝐻𝑡−1除了被传导向输出层之外,还会被传导向下一个时间步,与𝑋t一起,共同构建𝐻t

Snipaste_2024-12-23_10-39-08

具体地来看:

  • 普通神经网络,其中f是激活函数

    Snipaste_2024-12-23_11-17-44
  • 循环神经网络,其中,𝑊ℎℎ是循环网络中,隐藏层与隐藏层之间链接上的权重。

    Snipaste_2024-12-23_11-18-29

使用架构图表示,则可表示如下:

Snipaste_2024-12-23_10-40-46

利用这种方式,只要进行vocal_size次向前传播,并且每次都将上一个时间步中隐藏层上诞生的中间变量传递给下一个时间步的隐藏层,整个网络就能在全部的正向传播完成后获得整个句子上的全部信息。在这个过程中,我们在同一个网络上不断运行正向传播,此过程在神经网络结构上是循环,在数学逻辑上是递归,这也是循环神经网络名称的由来

这种传递方式可以让循环神经网络“记得”历史时间步上的信息,理论上来说,在最后一个时间步上输出的变量H_T应该包含从t=0到t=T的所有时间步上的信息。

RNN的权值共享

现在已经知道循环网络的数据流和基本结构了,但我们还面临一个巨大的问题——效率。刚才我们以一张表为例讲解了循环神经网络的迭代过程,但循环网络在实际应用时可能面临batch_size张表单,如果每张表单都需要一行一行进行向前传播的话,那循环神经网络运行一次需要(batch_size * sequence_length)次向前传播,这样整个网络的运行效率必然是非常非常低的。

Snipaste_2024-12-23_08-59-37 Snipaste_2024-12-23_08-59-46

幸运的是,事实上这个问题并不存在。在现实中使用循环神经网络的时候,我们所使用的输入数据结构往往是三维时间或三维文字数据,也就是说数据中大概率会包括不止一张时序二维表、会包括不止一个句子或一个段落。之前我们提到过,循环神经网络要顺利运行的前提是所有的句子/时间序列被处理成同等的长度,因此实际上每张二维表需要循环的时间步数量是相等的,因此在实际训练的时候循环神经网络是会一次性将所有的batch_size张二维表的第一行数据都放入神经元进行处理,故而RNN并不需要对每张表单一一处理,而是对全部表单的每一行进行一一处理,所以最终循环神经网络只会进行sequence_length次向前传播,所有的batch是共享权重的。

Snipaste_2024-12-23_10-48-13

如果将三维数据看作是一个立方体,那循环神经网络就是一次性处理位于最上层的一整个平面的数据,因此循环神经网络一次性处理的数据结构与深度神经网络一样都是二维的,只不过这个二维数据不是(vocal_size,input_dimension)结构,而是(batch_size,input_dimension)结构罢了。


day20 大模型基本概述+API调用

文件: day20 大模型基本概述+API调用\大模型应用开发Functioncalling -.md

3、Deepseek-R1的API调用指南

3.1 快速开始

DeepSeek-R1正式版已于2025年1月20号正式上线,其强大的模型推理能力可以让DeepSeek-R1在数学、代码、自然语言推理等任务上取得很好的结果,性能比肩 OpenAI o1 正式版。

DeepSeek-R1的API调用也是非常简单的,但是在调用API之前需要我们完成API Key的申请:

  • 访问DeepSeek官网:https://www.deepseek.com/,进入到API开放平台中

    Snipaste_2025-02-20_18-55-11
  • 进行API Key的申请:

    • 新用户注册即赠送10元额度,约500万token额度。
    • 价格方面,DeepSeek R1价格约为OpenAI o1正式版模型的1/50:
    Snipaste_2025-02-20_18-58-23 Snipaste_2025-02-20_18-59-29
  • 注意:

    • 目前DeepSeek R1模型调用不限速

      Snipaste_2025-02-20_19-02-59
    • API调用风格和OpenAI完全一致,但是暂不支持多模态和Function calling功能。

3.2 API调用规范

在Deepseek官网中有如下内容:

Snipaste_2025-02-20_19-06-15

OpenAI安装:

pip install openai

调用实例:

from openai import OpenAI

ds_api_key = "YOUR_DS_API_KEY"

# 实例化客户端
client = OpenAI(api_key=ds_api_key, 
                base_url="https://api.deepseek.com")

# 调用 deepseek-r1 模型
response = client.chat.completions.create(
    #model="deepseek-chat" 调用Deepseek-V3模型,不存在推理过程
    #model="deepseek-coder" 调用的是DeepSeek的Coder模型,不存在推理过程。这个模型是专为代码相关任务而训练的,具有较强的代码生成、理解、修改和调试能力,适用于编程问答、代码补全、代码错误检测与修复等场景
    model="deepseek-reasoner", #调用推理模型deepseek-r1 模型标识/名称,存在推理过程
    messages=[
        {"role": "user", "content": "请问,9.8和9.11哪个更大?"}
    ]
)

# 最终回复
response.choices[0].message.content

# 思考链
response.choices[0].message.reasoning_content

注意,在上述代码中,r1模型不光可以返回模型对于用户提问的回答,还可以返回对于用户提问问题的具体推理/思考过程,该过程就是r1模型的思考链。而思考链也是r1模型和GPT的o1模型主要的一个区别。

关于返回思考链的深度思考

如果我们可以看到r1模型的思考过程的话,那么就可以根据这个思考过程的内容去创建非常多高质量的问答数据。在问答数据中,不仅会包含问题和答案,还会包含对于问题的思考过程。这也是Deepseek可以基于r1模型蒸馏了很多小尺寸模型的主要原因。

Message参数设置方法

时至今日,“多角色” 对话基本上已经成了顶尖大模型的标配。正是基于多角色对话这一基础技术架构,大模型才能非常灵活的实现各类对话需求。而实际执行多角色对话的过程中,其核心是依靠messages参数来实现的。

messages(必填)

  • messages 参数是 DeepSeek模型 API 中必填的参数之一,用于定义聊天上下文,包括用户的输入、系统的指令、助手的回复等。通过 messages 数组,模型可以理解当前对话的背景,从而生成更加连贯的响应。根据不同的使用场景,messages 包含多种类型的消息,例如 system messageuser messageassistant message。下面是对 messages 参数及其各个子类型的详细解释。

    • content (必填):系统消息的内容,可以是字符串或数组。如果是数组,可能包含多个类型的内容(如文本、图像)。
    • role (必填):此处角色为 system,表明这是系统发出的消息。
    • name (可选):提供系统消息发送者的名称,尤其适用于区分多个具有相同角色的参与者。

    示例代码:

    response = client.chat.completions.create(
        model="deepseek-reasoner",
        messages=[
            {'role':'system','content':"你是一位滑稽且幽默的小品演员。"},
            {"role": "user", "content": "请问,你如何理解人生呢?"}
        ]
    )
    display(Markdown(response.choices[0].message.content))
    
    
    
    response = client.chat.completions.create(
        model="deepseek-reasoner", 
        messages=[
            {"role": "system", "content": "你是一位大学数学系教授"},
            {"role": "user", "content": "请问,你如何理解人生呢?"}
        ]
    )
    display(Markdown(response.choices[0].message.content))
    
    • 还有一个非常常见的system message的使用方法,就是借助system消息进行聊天背景信息的设定,很多时候我们可以在system消息中输入一段长文本,这段长文本将在聊天开始之前输入到系统中,而在之后的聊天中,即可让assistant围绕这个长文本进行回答,这是一种最简单的实现大语言模型围绕本地知识库进行问答的方法。
    text = '张三,男,1990年10月25日出生于中国台湾省高雄市。\
            2013年毕业于北京工业大学的信息工程专业,由于在校表现良好,毕业后被中科院信息技术部破格录取。'
    response = client.chat.completions.create(
        model="deepseek-reasoner",  
        messages=[
            {"role": "system", "content": text},
            #请问张三是什么星座的?请问张三毕业后去哪里了?
            {"role": "user", "content": '请问张三是哪一年毕业的?'}
      ]
    )
    response.choices[0].message.content
    
    • user message 表示用户发给模型的消息,是对话的核心部分之一。它定义了用户的输入内容,模型根据这些内容生成响应。

      • content (必填):用户消息的内容,通常为文本或图像链接的数组。对于支持图像输入的模型,如 DeepSeek v2.5 ,还可以传递图像。
      • role (必填):角色为 user,表示该消息来自用户。
      • name (可选):可以为用户指定一个名称,用于区分多个具有相同角色的用户。

      示例代码:

      # 创建用户消息
      user_message = {
          "role": "user",
          "content": "你好,请介绍下你自己。"
      }
      
    • assistant message表示助手消息,是模型根据用户消息生成的响应。

      • content:类型为字符串,表示助手消息的内容,这是助手对用户提问的回答或执行任务的结果等。

      • role:类型为字符串,固定为 “assistant”,表示消息的作者角色是助手。

      • name:类型为字符串,表示对话参与者的名称,一般用于区分不同身份的助手。

3.3 多轮对话

3.3.1 基本原理

首先,任何一款大模型在原始状态下都不会存在和用户对话的长期记忆,也就是所谓的上下文或者多轮对话机制。但是正是由于message参数包含多种类型的消息,例如 system messageuser messageassistant message就可以实现“多轮对话”机制,使得模型可以具备上下文或者和用户长期对话记忆的能力。

我们只需要将模型返回的assistant message消息+用户新的提问usermessage拼接到模型的messages参数中,并再次向模型进行提问,即可非常便捷的实现多轮对话。

3.3.2 封装实现

from openai import OpenAI

ds_api_key = "sk-f010301e7xxx5214d14c30cce1e"
# 实例化客户端
client = OpenAI(api_key=ds_api_key, 
                base_url="https://api.deepseek.com")

def multi_chat_with_model(msg): #msg表示用户提出的问题
    text = '张三,男,1990年10月25日出生于中国台湾省高雄市。\
        2013年毕业于北京工业大学的信息工程专业,由于在校表现良好,毕业后被中科院信息技术部破格录取。'
    
    messages=[
        {"role": "system", "content": text},
        {"role": "user", "content": msg}
    ]
    while True:
        response = client.chat.completions.create(
            model="deepseek-reasoner",  
            messages=messages
        )
        
        # 获取模型回答
        answer = response.choices[0].message.content
        print(f"模型回答: {answer}")


        # 询问用户是否还有其他问题
        user_input = input("您还有其他问题吗?(输入退出以结束对话): ")
        if user_input == "退出":
            break
            
        # 记录用户回答
        messages.append({"role": "assistant", "content": answer})
        messages.append({"role": "user", "content": user_input})

#多轮对话测试
multi_chat_with_model('张三哪一年毕业的?')

4、Agent智能体开发

4.1 Agent简介

Agent智能体是一个由人工智能驱动的系统或程序,能够在一定的环境中自主感知、决策和执行任务。它模拟或扩展了人类或其他生物的智能行为,旨在解决复杂问题或完成特定目标。可以广泛应用与自动驾驶、智能客服、游戏NPC、金融分析、医疗诊断等多个领域。

在Agent智能体的开发过程中,有一种实现机制,可以使得开发者可以定义、管理和调用各种函数来实现复杂的任务。这些函数可以封装具体的业务逻辑、算法或外部服务调用等。这种机制就是Function Calling!因此,Function Calling是Agent智能体开发的基础

同时,Agent智能体利用Function Calling增强能力。因为,Agent智能体通过调用外部函数或服务(即Function Calling),可以访问实时数据、执行特定算法或调用其他资源来完成其任务。这种能力使得Agent智能体能够更加灵活地适应不同的应用场景和需求。

因此,随着AI技术的不断发展,Agent智能体和Function Calling都在不断地演进和完善。两者相互促进、相互影响,共同推动了AI技术的创新和应用落地。

4.2 Function Calling简介

4.2.1 背景和定义

我们都知道大语言模型的知识储量是巨大,并且它具备非常强大的原生能力,但是有时候我们在实际使用大模型时会感受到大模型能力上的某些局限,比如大模型无法回答超过大模型知识库截止日期之后发生的相关信息和知识,并且大模型只能给出文字的建议但无法直接帮我们解决某些实际操作性的问题(如自动进行邮件收发、自动预定车票等)。因此,这些问题的存在会极大程度上限制了大模型的实际应用价值。

在这一基本背景下,Function calling功能应运而生。该功能的本质是让大模型具有调用外部函数的能力。也就是说,当大模型遇到超出自身能力范围的需求时,可以通过访问相应的外部函数寻求解决方案。这样,大模型就可以不再仅仅根据自身的知识库进行回答,还可以额外挂载一个外部函数库,然后根据用户提问进行外部函数的检索,根据实际需求调用外部函数并获取函数运行结果,再基于函数运行结果进行回答。

毫无疑问,有了外部函数库的功能加持,大模型的处理和解决问题的能力也必将再上一个台阶。

4.2.2 核心原理

Snipaste_2025-02-21_09-18-50

在Agent中的应用

Snipaste_2025-02-21_09-20-27

4.3 实时气象查询Agent开发

4.3.1 项目背景

OpenWeather是一家提供全球范围内的气象数据服务的公司,该公司的服务包括实时天气信息、天气预报、历史天气数据以及各种气象相关的报告等,并且OpenWeather开放了一定使用限度内完全免费的API,即我们可以在代码环境中通过调用OpenWeather API来进行实时天气查询、天气预报等功能,这意味着开发者可以将OpenWeather的天气预报功能加入到他们自己的应用或网站中。

4.3.2 OpenWeather注册及API key获取方法

为了能够调用OpenWeather服务,和OpenAI的API使用过程类似,我们首先需要先注册OpenWeather账号,并获取OpenWeather API Key。这里需要注意的是,对于大多数在线服务的API来说,都需要通过API key来进行身份验证,尽管OpenWeather相对更加Open,有非常多的免费使用的次数,但身份验证仍然是必要的防止API被滥用的有效手段。OpenWeather API key获取流程如下:

  • Step 1.登录OpenWeather官网并点击Sign—>create account完成注册。该网站无需魔法即可直接登录,可以使用国内邮箱或者QQ邮箱均可进行注册,官网地址为:https://openweathermap.org/

    Snipaste_2025-02-21_09-26-52 Snipaste_2025-02-21_09-26-59
  • Step 2.获取API-key:注册完成后,即可在API keys页面查看当前账户的API key:

    Snipaste_2025-02-21_09-27-08 Snipaste_2025-02-21_09-27-15

    一般来说完成注册后,就会有一个已经激活的API-key。和OpenAI一样,OpenWeather的API key也可以创建多个。

  • Step 3.将其设置为环境变量:和OpenAI API key类似,为了方便后续调用,我们也可以直接将OpenWeather API key设置为环境变量,变量名为OPENWEATHER_API_KEY。具体设置环境变量的方法参考Ch.1中OpenAI APkey设置环境变量流程,此处不再赘述。

    Snipaste_2025-02-21_09-27-22

    设置完了环境变量之后,接下来即可按照如下方式创建OpenWeather API key变量:

    open_weather_key = os.getenv("OPENWEATHER_API_KEY")
    

4.3.3 获取实时天气信息API

import requests

# Step 1.构建请求
url = "https://api.openweathermap.org/data/2.5/weather"

# Step 2.设置查询参数
params = {
    "q": "Beijing",               # 查询北京实时天气
    "appid": "xxx",    # 输入API key
    "units": "metric",            # 使用摄氏度而不是华氏度
    "lang":"zh_cn"                # 输出语言为简体中文
}

# Step 3.发送GET请求
response = requests.get(url, params=params)

# Step 4.解析响应
data = response.json()
print(data)

# 即时温度最高、最低气温
data['main']['temp_min'], data['main']['temp_max']
# 天气状况
data['weather'][0]['description']

这里需要注意的是,城市名必须输入英文名,否则无法正确识别。

外部函数创建:我们尝试编写一个通过OpenWeather API实时获取天气信息的API,并作为模型可调用的外部函数之一。很明显,为了确保和大语言模型之间的顺畅通信,此时要求函数的输入和输出都是字符串格式。具体函数编写如下:

这里需要注意函数说明和参数解释的书写风格

def get_weather(loc):
    """
    查询即时天气函数
    :param loc: 必要参数,字符串类型,用于表示查询天气的具体城市名称,\
    注意,中国的城市需要用对应城市的英文名称代替,例如如果需要查询北京市天气,则loc参数需要输入'Beijing';
    :return:OpenWeather API查询即时天气的结果,具体URL请求地址为:https://api.openweathermap.org/data/2.5/weather\
    返回结果对象类型为解析之后的JSON格式对象,并用字符串形式进行表示,其中包含了全部重要的天气信息
    """
    # Step 1.构建请求
    url = "https://api.openweathermap.org/data/2.5/weather"

    # Step 2.设置查询参数
    params = {
        "q": loc,               
        "appid": 'xxx',    # 输入API key
        "units": "metric",            # 使用摄氏度而不是华氏度
        "lang":"zh_cn"                # 输出语言为简体中文
    }

    # Step 3.发送GET请求
    response = requests.get(url, params=params)
    
    # Step 4.解析响应
    data = response.json()
    return json.dumps(data)

函数测试:

#测试函数是否可用
import json
get_weather('GuangZhou')

4.3.4 tools参数解释与定义

在准备好外部函数之后,接下来非常重要的一步就是需要将外部函数的信息以某种形式传输给模型。此时就需要使用到create函数的tools参数.从参数的具体形式来看,tools参数是一个可以包含多个字典的list,每个字典都需要包含两个键值对,分别是 {type:function,function:外部函数的完整描述}。因此每个字典都代表一个外部函数的相关信息。在大语言模型实际进行问答时,会根据tools参数提供的信息对各外部函数进行检索。

tools = [
    {
        "type": "function", 
        "function":'外部函数的完整描述'
    }
]

4.3.5 外部函数完整描述

外部函数的完整描述对于Function calling功能的实现至关重要。因为在大模型进行实际问答时,会根据对外部函数的完整描述信息的语义理解进行外部函数的检索和调用。

接下来我们详细解释外部函数完整描述的指定写法。总的来说,我们会使用一个字典来对其进行完整描述,每个字典都有三个参数(三组键值对),各参数(Key)名称及解释如下:

  • name:代表函数函数名称字的符串,必选参数。
  • description:用于描述函数功能的字符串,虽然是可选参数,但该参数传递的信息实际上是Chat模型对函数功能识别的核心依据。即Chat函数实际上是通过每个函数的description来判断当前函数的实际功能的。
  • parameters:函数的参数说明,必选参数,要求遵照JSON Schema格式进行输入,JSON Schema是一种特殊的JSON对象,专门用于验证JSON数据格式是否满足要求。

例如,对于get_weather函数,我们需要创建如下字典来对其进行完整描述:

get_weather_function = {
    'name': 'get_weather',
    'description': '查询即时天气函数,根据输入的城市名称,查询对应城市的实时天气',
    'parameters': {
        'type': 'object', #json对象类型
        'properties': { #参数成员描述
            'loc': {
                'description': "城市名称,注意,中国的城市需要用对应城市的英文名称代替,例如如果需要查询北京市天气,则loc参数需要输入'Beijing'",
                'type': 'string'
            }
        },
        'required': ['loc']
    }
}

因此修改tool参数值为:

tools = [
    {
        "type": "function", 
        "function":get_weather_function
    }
]

同时还需要封装外部函数库,用于关联外部函数名称和外部函数对象

available_functions = {
            "get_weather": get_weather,
        }

4.3.6 Function calling实现

First response

在进行了一系列基础准备工作之后,接下来我们尝试在Chat模型对话执行Function calling功能。首先我们测试模型本身能否知道如何查询天气:

from openai import OpenAI
#硅基流动API
ds_api_key = "sk-atisrrfnrxsnuxxxkriejlfxlvnymvfxoesps"
client = OpenAI(api_key=ds_api_key, 
                base_url="https://api.siliconflow.cn/v1")
response = client.chat.completions.create(
    model="deepseek-ai/DeepSeek-V2.5",  
     messages=[
        {"role": "user", "content": "请帮我查询北京地区今日天气情况"}
    ], 
    )
        
response.choices[0].message.content

很明显,模型无法进行回答。接下来我们尝试将函数库相关信息输入给Chat模型

response = client.chat.completions.create(
    model="deepseek-ai/DeepSeek-V2.5",  
    messages=[
        {"role": "user", "content": "请帮我查询北京地区今日天气情况"}
    ],
    tools=tools,
)

        
response_message = response.choices[0].message
response_message

返回结果:

ChatCompletionMessage(content='', refusal=None, role='assistant', audio=None, function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='0195279d5095aab2d4ac52760de27c04', function=Function(arguments='{"loc":"Beijing"}', name='get_weather'), type='function')])

能够发现,此时返回的message中content为空,而增了一个"tool_calls"的list,该list就包含了当前调用外部函数的全部信息:

response_message.tool_calls[0]

返回结果:

ChatCompletionMessageToolCall(id='0195279d5095aab2d4ac52760de27c04', function=Function(arguments='{"loc":"Beijing"}', name='get_weather'), type='function')

对于当前CompletionMessageToolCall对象,id为外部函数调用发起请求id,function则表示调用外部函数基本信息,而type则代表了当前当前调用外部函数类型,function代表调用自定义的外部函数。

我们可以在此基础上分别提取调用外部函数名称信息和参数信息,分别保存为function_name和function_args对象:

# 完成对话需要调用的函数名称
function_name = response_message.tool_calls[0].function.name
function_name

# 基于外部函数库获取具体的函数对象
fuction_to_call = available_functions[function_name]
fuction_to_call

'''
        available_functions = {
                    "get_weather": get_weather,
                }
'''

# 执行该函数所需要的参数,将其反序列化成字典对象,便于下一步函数调用时进行传输传递
function_args = json.loads(response_message.tool_calls[0].function.arguments)
function_args

需要注意的是,外部函数的计算过程仍然是在本地执行,即Chat模型并不会将代码读取到服务器上再进行在线计算,因此接下来我们需要根据模型返回的函数和函数参数,在本地完成函数计算,然后再将计算过程和结果保存为message并追加到messages后面,并第二次调用Chat模型分析函数的计算结果,并最终根据函数计算结果输出用户问题的答案。

Second response

这里我们只需要借助**方法,直接将function_args对象传入fuction_to_call中,即可一次性传输全部参数,**方法的功能可以参考如下示例:

def function_to_call_test(a, b, c):
    return a + b + c

function_args_test = {'a': 1, 'b': 2, 'c': 3}

result = function_to_call_test(**function_args_test)

print(result)

**方法其实是一种较为特殊、但同时也非常便捷的参数传递方法吗,该方法会将字典中的每个key对应的value传输到同名参数位中。接下来我们将function_args对象传入fuction_to_call中并完成计算:

function_response  = fuction_to_call(**function_args) #get_weather(loc="Beijing")
function_response  #获取函数调用结果

能够发现,模型已经顺利完成计算。接下来我们在messages对象中追加两条消息,第一条消息是第一次模型返回的结果(即调用模型的assistant message),第二条消息则是外部函数计算结果,该条消息的role为function,且name为函数名称。这也是我们首次接触function message,和user、system、assistant message不同,function message必须要输入关键词name,且function message的内容源于外部函数执行的计算结果,并且需要手动进行输入。具体添加过程如下:

  • 追加第一条消息:模型返回的结果

    #展示目前messages内容
    print(messages)
    #将模型第一次返回的结果转换成字典类型,目的是为了将其追加到messages列表中
    response_message.model_dump()
    # 向messages追加第一次模型返回结果消息
    messages.append(response_message.model_dump())  
    #查看追加后的messages
    print(messages)
    
  • 追加第二条消息:外部函数计算结果

    # 追加function返回消息
    messages.append({
                "role": "tool",
                "content": function_response,
                "tool_call_id":response_message.tool_calls[0].id
            })
    

接下来,再次调用Chat模型来围绕messages进行回答。需要注意的是,此时我们不再需要向模型重复提问,只需要简单的将我们已经准备好的messages传入Chat模型即可:

second_response = client.chat.completions.create(
    model="deepseek-ai/DeepSeek-V2.5",
    messages=messages)

second_response.choices[0].message.content

能够发现,模型最终做出了准确回答。

完整封装
def run_conv(messages, 
             api_key,
             tools=None, 
             functions_list=None,
             model="deepseek-ai/DeepSeek-V2.5"):
    """
    能够自动执行外部函数调用的Chat对话模型
    :param messages: 必要参数,输入到Chat模型的messages参数对象
    :param api_key: 必要参数,调用模型的API-KEY
    :param tools: 可选参数,默认为None,可以设置为包含全部外部函数的列表对象
    :param model: Chat模型,可选参数,默认模型为deepseek-chat
    :return:Chat模型输出结果
    """
    user_messages = messages
    #基于硅基流动API
    client = OpenAI(api_key=api_key, 
                base_url="https://api.siliconflow.cn/v1")

    # 如果没有外部函数库,则执行普通的对话任务
    if tools == None:
        response = client.chat.completions.create(
            model=model,  
            messages=user_messages
        )
        final_response = response.choices[0].message.content

    # 若存在外部函数库,则需要灵活选取外部函数并进行回答
    else:
        # 创建外部函数库字典
        available_functions = {func.__name__: func for func in functions_list}

        # 创建包含用户问题的message
        messages = user_messages

        # first response
        response = client.chat.completions.create(
            model=model,  
            messages=user_messages,
            tools=tools,
        )
        response_message = response.choices[0].message

        # 获取函数名
        function_name = response_message.tool_calls[0].function.name
        # 获取函数对象
        fuction_to_call = available_functions[function_name]
        # 获取函数参数
        function_args = json.loads(response_message.tool_calls[0].function.arguments)

        # 将函数参数输入到函数中,获取函数计算结果
        function_response = fuction_to_call(**function_args)

        # messages中拼接first response消息
        user_messages.append(response_message.model_dump())  

        # messages中拼接外部函数输出结果
        user_messages.append(
            {
                "role": "tool",
                "content": function_response,
                "tool_call_id":response_message.tool_calls[0].id
            }
        )

        # 第二次调用模型
        second_response = client.chat.completions.create(
            model=model,
            messages=user_messages)

        # 获取最终结果
        final_response = second_response.choices[0].message.content

    return final_response

无需外部函数加持的情况:

messages = [{"role": "user", "content": "请问什么是机器学习?"}]
run_conv(messages=messages, 
         api_key = ds_api_key)

需要外部函数加持的情况:

messages = [{"role": "user", "content": "请问北京今天天气如何?"}]
run_conv(messages=messages, 
         api_key = ds_api_key,
         tools=tools, 
         functions_list=[get_weather])

day21大模型基本概述+API调用

文件: day21大模型基本概述+API调用\1.DeepSeek大模型应用开发01.md

大模型前置话题

什么时候听说大模型的?

在大家的记忆中是从什么时候开始听说或者了解大模型的?相信大部分人应该是在22年底和23年期间。但是大家知道吗,其实早在2020年6月份的时候,当时OpenAI推出了1750多亿参数的GPT-3的时候,国外就有很多创业形的科技公司就开始围绕gpt3大模型进行了商业转型和应用,到今天都已经成为了市值几十亿美元的大型科技公司。

那在2025年的今天,国内有多少企业是在真正应用大模型创业呢?大模型的技术红利不应该只被老美独享吧!因此,大模型技术在国内的推广和普及就变的至关重要。衷心的希望日后会有更多人入局大模型,更多人可以共享大模型的技术红利。

大模型不能只被老美独享

我们都知道美国的经济很糟糕,甚至是水深火热、民不聊生。虽然老美自身存在很多很严重的问题,但是不得不说老美在人工智能上的创新发展的眼光和能力还是很强的。美国的投资界把人工智能这次的大模型技术看成是1980年代的PC和1995年的互联网。所以他们就觉得大模型就是一场工业革命级别的一个机会,他们也有一个观点,那就是一旦他们美国在人工智能大模型上形成了自己的优势那对其他国家甚至对全球竞争来说,那就是降维打击。

因此我们特别希望我们国家无论是上层的政府官员还是中层的企业家还是下层的创业者、技术人员甚至是普通大众都能够特别的意识到大模型绝对是我们这一代人今天能够碰到的最大的一次机会。就像360创始人周鸿祎所说的:大模型就是一场工业革命级别的一次机会,工业革命意味着什么?意味着所有的行业都将会重新洗牌。并且这场工业革命,不仅是人类有史以来最大的一场工业革命,也可能是速度最快的一场工业革命。

AI-First意识搭建

因此,作为普通的技术人员或者其他岗位的从业人员,我们应该如何能够快速的入局大模型呢?首先,大家要具有一定的AI信仰和AI-First的意识。所谓的AI信仰指的是,你一定要believe something,你要相信大模型是可以帮你实现各种各样的需求和功能的。

其实在前几年大模型刚出现的时候,大模型本身的能力是可以被大众所接受的,但是如何让大模型技术落地、合理的应用在不同的行业中,却不是一个简单的事情。那时,有人戏称大模型就是“拿着锤子找钉子”(对各种行业进行大模型落地方案的找寻和探索)。随着大模型在近几年的急速发展,在国外大模型的落地方案也变的越来越多。此时,此时国内的各大厂商就都开始急切的找“钉子”(落地方案)了,因为谁先找到这颗“钉子”,谁就能在大模型行业落地上撕开一个口子。

因此,大家需要记住:随着大模型技术的不断发展,在各行各业大模型都会有无限的可能!

4.究竟什么是大模型

说了这么多大模型重要的因素,那么究竟什么是大模型?我们如何通俗的理解大模型这项伟大的AI技术呢?

我们可以把大模型类比生活场景中的各种“模具”:我们在生活中会使用到很多模具,比如制作雪糕的模具、蛋糕模具、爱心煎蛋的心形模具等等。我们是可以使用这些模具来更加简单便捷且快速地完成最终要制作的成品。

如果将其映射到数学上,就像是我们听到过的数学建模:比如,一位老师想计算班级100位同学的期末总成绩,那么根据学校本学期的要求,考试成绩占总成绩的70%,平成成绩占总成绩的30%,那么老师们只需要找到其中的运算规律就可以指定一个通用的模型来快速计算每一个学生的总成绩:总成绩y = (考试成绩x1 * 0.7) + (平时成绩x2 * 0.3),其中该方程式就是模型的算法,而0.7和0.3这个权重占比就是模型的参数。

大模型:LLM(Large Language Model)是指具有大量参数和复杂结构的机器学习模型。这些模型可以应用于处理大规模的数据和复杂的问题。“大模型”中的“大”是指模型的参数量非常大(百亿千亿级别)。相对来讲,参数量越大,则大模型的综合能力就越强。

大模型的两大分支

在线大模型

大模型本身是部署在云端,用户可以通过网络调用云端大模型相关的接口进行在线使用大模型相关的功能。

因此,在线大模型具备如下优势:在线模型普遍性能更强、使用的技术门槛和硬件门槛更低、配套模型生态和服务更加完善;

但是其弊端也同时存在:首先是数据安全问题,因为使用线上大模型必须通过网络进行数据传输,需要将本都数据传输给在线大模型,才可以基于大模型本身的能力对数据进行分析、处理和其他操作。但是一旦企业生产资料联网,就肯呢个会带来数据安全隐患,无法保障内容真实可信等诸多痛点的发生。还有就是,在线大模型的企业应用会产生长期的费用,在线大模型会根据调用次数或者按量进行计费,如果一旦使用频率和规避较大,则会产生较高的费用。最后一点就是在线大模型无法很好的定制化,也就是无法从训练语料进行定制化训练。

开源大模型

开源大模型是可以在本地设备上进行部署和使用的。相比与在线大模型,开源大模型可以使⽤更多微调框架进⾏模型微调,微调空间更⼤,更有可能快速完成定制化⼤模型训练;此外,数据可以直接本地训练模型,⽆需在线提交数据进⾏在线模型微调,数据安全也将更有保障;

但是,一般大模型的参数量级都会比较大,动不动就是几十亿、几百亿、几千亿的量级。因此本地部署,需要提供较高配置的硬件设备,该设备相对费用也不会太低。好在这是一次投资即可终身使用。

大模型的应用

随着人工智能技术的不断发展,AI大模型已经成为垂直行业应用的热点。

然而,AI大模型虽然功能强大,但在垂直行业应用中存在一些突出的问题,如缺乏特定行业领域的专有知识,尤其是深度知识、私有知识、保密知识等;还有就是输出内容很难精确控制;常常会出现幻觉(一本正经地胡说八道)等问题。

为解决这些问题,有三种在垂直行业中应用AI大模型的方法,包括重新训练、微调和知识库检索。

利用行业知识重新训练AI大模型

由于通用AI大模型缺乏行业专有知识,重新训练AI大模型成为一种可行的解决方案。

该方法的基本思想是,将行业相关的数据集和专业知识用于重新训练AI大模型,使其拥有行业特定的理解和知识。这样,AI大模型在垂直行业中的应用就能更好地理解和处理相关的任务和问题。

例如,在医疗领域中应用AI大模型时,可以利用医学文献、诊断报告和临床数据等专业知识进行模型的重新训练,使其具备医学背景和专业判断能力。

因此,重新训练大模型可以提高AI模型在特定领域中的准确性和专业性,并降低产生幻觉的可能性。不过,重新训练需要大量的数据,且对算力要求较高,对人力资源、算力费用和时间成本要求都非常高。

利用行业知识对AI大模型进行微调

除了重新训练,微调是另一种常用的方法来应用AI大模型于垂直行业。

微调是指在通用AI大模型的基础上,通过在特定的行业数据集上进行再次训练,调整模型的部分参数,以适应特定行业的需求。这种方法相比于重新训练,既可以保留通用AI大模型的原生能力,又能增加对行业问题的理解能力。例如,在金融领域中,可以利用金融数据和交易信息微调模型,以实现更准确和适应特定金融市场应用场景的预测和建议。

微调是在通用AI大模型的基础上,通过针对特定行业场景进行精细调整来实现更好的适应性。微调相对于重新训练而言,时间和资源消耗较少。

利用行业知识建立知识库

当AI大模型无法提供准确答案或输出时,可以利用行业知识建立知识库模型,充分融合AI大模型的通用知识和知识库的专有知识,使得大模型可以提供更有针对性的输出。

例如,在法律咨询领域,当AI大模型遇到具体法律案例时,可以将相关法律条款和判例作为知识库模型的输出,促进AI大模型产生更准确的法律意见或建议。

通过建立行业知识库和企业私有知识库模型,不仅可以提高输出内容的准确性,而且可以大大降低幻觉问题影响。但知识库模型的效果与知识库的质量和覆盖度密切相关。此外,不断更新、维护和扩充知识库也是一项重要的任务。

1、Reasoning 大模型

1.1 基本概述

Reasoning大模型特指 ”推理大模型“(Reasoning Large Language Model)是专门设计用于处理需要复杂推理任务的大型语言模型。

1.2 核心概念

推理特指的是什么呢?如何理解模型的推理能力呢?

  • 推理能力的定义:推理是指根据已知的信息和知识,通过逻辑推导得出新的结论或答案的过程。对于大模型而言,推理能力使其能够处理不仅仅是简单的事实查询,还包括解决复杂的问题、进行逻辑推断、数学计算、理解代码等需要多步思考和分析的任务。
  • 与常规大模型的区别:常规的大模型主要侧重于对大量文本数据的学习和理解,以便生成流畅、自然的文本回应,但它们通常直接输出最终的答案,而不展示中间的推理过程。而推理大模型在回答问题前,会先将问题拆解为更小的步骤,即推理步骤或思维链(Chain of Thought, CoT),然后将这些推理过程和最终答案一起输出,使人们能够看到模型的思考过程和逻辑推导路径。

1.3 技术实现

要使得模型具备强大的推理能力,可以从如下3点(架构基础、训练方法和提示工程)进行技术实现:

  • 架构基础

    • 推理大模型通常基于Transformer架构构建,这种架构能够有效地处理长序列数据,并且在自注意力机制的帮助下,可以捕捉到输入数据中的复杂依赖关系,为推理提供了良好的基础。

      自注意力机制(Self-Attention Mechanism)是Transformer架构中的一个核心部分。简单来说,自注意力机制就像是给模型装上了一双“慧眼”,让它能够同时关注输入数据中的所有部分,并自动判断哪些部分更加重要。这样,模型就能更好地理解数据的整体结构和内在联系。

      想象一下你在阅读一篇文章。当你读到文章中的一个词时,你的大脑会不自觉地回忆起与这个词相关的其他词或句子,帮助你更好地理解当前的内容。这就是一种注意力机制。
      
      在推理大模型中,自注意力机制的工作方式类似。当模型处理一个句子时,它会检查句子中的每个词,并通过比较这些词之间的关系来确定它们的重要性。例如,在处理句子“我喜欢在公园里散步”时,模型会发现“我”和“喜欢”之间有很强的关联,因为“我”是动作的执行者;同样,“散步”和“公园”之间也有紧密的联系,因为“散步”通常发生在“公园”里。通过这种方式,模型能够捕捉到句子中的复杂依赖关系,从而更准确地理解其含义。
      
      因此,自注意力机制使得推理大模型能够更有效地处理长序列数据,并在自然语言处理、语音识别等领域展现出强大的性能。
      
  • 训练方法

    推理模型的训练需要基于(预训练、微调和强化学习)来进行,以确保模型具备强大的推理能力

    • 预训练:模型首先在大规模的无标注文本数据上进行预训练,学习语言的基本结构和模式,获得广泛的语言知识和世界知识。这一阶段的数据量通常非常大,以涵盖尽可能多的语言现象和信息。

    • 微调:在预训练的基础上,使用特定领域或有标注的数据对模型进行微调,以优化其在特定任务上的性能和输出风格。例如,对于推理任务,可能会使用包含推理链的数据集进行微调,让模型学习如何生成合理的推理步骤。

    • 强化学习:部分推理大模型还会采用强化学习技术,通过奖励模型来评估模型输出的质量和准确性,并根据奖励信号不断调整模型的参数,以提高模型的推理能力。

      想象一下,一个孩子(模型)刚开始学习解决问题时,他可能会尝试不同的方法(动作),而每次尝试后,他会根据结果得到一些反馈(奖励信号)。如果某种方法得到了好结果(比如解决了问题或者得到了表扬),那么孩子就更有可能在下次遇到类似问题时再次使用这种方法。这就是强化学习的基本思想:通过“试错”和“延迟回报”来不断学习和改进。
      
  • 提示工程

    • 提示工程是提高推理大模型性能的重要手段之一。通过精心设计输入提示,引导模型按照预期的方式生成推理步骤和答案。例如,在提示中明确要求模型“一步步思考”“详细解释推理过程”等,可以帮助模型生成更符合要求的回答。

1.4 应用场景

  • 学术研究:帮助研究人员解决复杂的学术问题,如数学证明、科学理论推导等,提供新的研究思路和方法。

  • 教育领域:辅助学生学习,解答学科相关的问题,提供详细的解题步骤和解释,培养学生的逻辑思维和解决问题的能力。

  • 商业决策:为企业提供数据分析、市场预测、风险评估等方面的支持,帮助企业做出更明智的决策。

  • 智能客服:更准确地回答用户的问题,提供更详细、专业的解决方案,提高客户服务的质量和效率。

1.5 总结

总的来说,推理大模型通过独特的技术实现和广泛的应用场景,展现出了强大的能力和潜力。随着技术的不断进步和应用需求的不断提高,推理大模型将在未来发挥更加重要的作用。

2、deepseek理论篇

2.1 基本概述

深度求索(DeepSeek)是一家专注于通用人工智能(AGI)研发的中国科技公司,其推出的DeepSeek大模型是公司核心技术成果之一。目前deepseek支持智能对话、文本生成、语义理解与计算推理、代码生成补全等多种复杂任务,还支持图像、音频等多模态输入。

在技术架构上deepseek采用了混合专家(MoE)架构,通过动态路由机制,仅激活部分参数进行计算,有效降低计算能耗,提高特定任务的处理精度(英伟达市值蒸发)。并且,deepseek拥有庞大的参数量,如DeepSeek-V3的6710亿(671B)参数,能够理解和生成更复杂的语言,处理更多的上下文和细节信息。

一、混合专家(MoE)架构:智慧分工,高效协作
简单来讲,一个复杂的任务被分解成多个小任务,每个小任务都由最擅长它的“专家”来处理,这样不仅能提高效率,还能确保每个细节都得到精准的处理。DeepSeek大模型采用的正是这样一种高效的策略——混合专家(MoE)架构。

在MoE架构中,有多个“专家”模型,它们各自擅长处理不同类型的信息或特征。当输入数据进入模型时,一个名为“路由器”或“门控网络”的部分会智能地决定将哪些数据分配给哪些专家去处理。这样一来,每个专家都能专注于自己最擅长的领域,从而整体提高了处理效率和精度。

比如,在处理一个包含图像和文本的多模态任务时,MoE架构可以自动将图像部分交给擅长图像处理的专家,将文本部分交给擅长文本处理的专家,两者并行处理,最后再将结果合并,大大提升了处理速度和准确性。

二、动态路由机制:灵活调度,优化资源
在MoE架构中,动态路由机制起着至关重要的作用。它就像是一个智能调度员,根据输入数据的特性和当前专家的负载情况,动态地决定数据的去向。

这种动态调度不仅提高了模型的灵活性,还使得计算资源得到了更有效的利用。因为并非所有数据都需要经过所有专家的处理,只有最相关的专家才会被激活,从而大大降低了计算成本

因此,deepseek训练成本相对较低,且支持本地部署,对比同类型的大模型如GPT其硬件要求可降低60%以上!

与传统的大模型相比,DeepSeek大模型在训练成本上具有显著的优势。这得益于其MoE架构和一系列优化措施,使得模型能够在较低的硬件要求下进行高效的训练。

DeepSeek大模型支持本地部署,这意味着用户无需购买昂贵的云服务或高性能硬件设备,就能在普通的计算设备上运行和训练模型。这对于中小企业和个人开发者来说,无疑是一个巨大的福音。他们可以用更低的成本享受到先进的AI技术带来的便利和效益。

综上所述,DeepSeek大模型凭借其独特的混合专家(MoE)架构和动态路由机制,实现了高效的计算和精确的处理。同时,其在训练成本上的优势也使得更多用户能够享受到先进的AI技术带来的便利和效益。随着技术的不断进步和应用场景的拓展,DeepSeek大模型将在未来发挥更加重要的作用。

2.2 版本分支

DeepSeek的R系列和V系列是该模型体系中的两个不同分支,它们各自具有独特的特点和应用场景。以下是对这两个系列的详细对比:

一、技术架构与模型特性

  1. DeepSeek-R系列

    • 架构特点:R系列模型采用了先进的混合专家(MoE)架构,这种架构通过动态路由机制,仅激活部分参数进行计算,有效降低了计算能耗,提高了特定任务的处理精度。同时,R系列还引入了强化学习等先进技术,进一步提升了模型的推理能力。

      我们可以将“强化学习”类比为一个“智慧成长”的过程。
      
      想象一下,一个孩子(模型)刚开始学习解决问题时,他可能会尝试不同的方法(动作),而每次尝试后,他会根据结果得到一些反馈(奖励信号)。如果某种方法得到了好结果(比如解决了问题或者得到了表扬),那么孩子就更有可能在下次遇到类似问题时再次使用这种方法。这就是强化学习的基本思想:通过“试错”和“延迟回报”来不断学习和改进。
      
      对于DeepSeek R系列这样的大模型来说,强化学习就像是给模型赋予了一种“自我进化”的能力。模型在面对各种复杂任务时,会像那个孩子一样尝试不同的解决方案,并根据任务的完成情况(即奖励信号)来调整自己的策略。随着时间的推移,模型会逐渐学会如何更有效地解决这些问题,从而提升其推理能力。
      
      简单来说,强化学习就是让模型在不断的“尝试-反馈-调整”循环中变得更加聪明和高效。它帮助模型在复杂任务中找到最佳解决方案,就像那个孩子逐渐学会了如何更好地解决问题一样。所以,当DeepSeek R系列引入强化学习技术时,就意味着这个大模型拥有了更强的“智慧成长”能力,能够更好地应对各种复杂的推理任务。
      
    • 模型性能:R系列模型在数学、代码和自然语言推理等复杂任务上表现出色,其性能可与OpenAI的o1模型相媲美。这得益于其大规模强化学习技术,使得模型能够快速掌握新知识、新技能,并适应不同的任务和场景。

  2. DeepSeek-V系列

    • 架构特点:V系列模型则更侧重于通用性和多模态处理能力。它基于标准的Transformer架构,并通过 “优化算法和增加训练数据”,逐步提升了模型的性能和适用性。V系列模型支持中英文双语处理,并能够处理包括图像、音频在内的多种模态数据。

      优化算法和增加训练数据的理解:
      
      	优化算法可以让模型这个“超级大脑”运转得更高效。例如,在处理自然语言文本时,优化后的算法能够更快地理解文字之间的关系。就像原本模型需要花费较多时间和精力去理解一个复杂句子中每个词的联系,优化算法后,它能够更快地捕捉到关键词,更好地理解句子的含义,从而提升模型的性能。
      
      	而增加训练数据意味着让模型见识更多的场景和情况。以图像识别为例,如果模型原来只训练过少量几种动物的图片,它可能只能识别这几种动物。但是当增加了大量不同动物在各种环境下的图片作为训练数据后,模型就能识别更多种类的动物,而且在这些动物处于不同姿态、不同背景等情况下也能准确识别,这就提升了模型的适用性。
      
  • 模型性能:V系列模型在各项评测中均表现出色,尤其在知识类任务、长文本理解、编程和数学运算等领域接近甚至超越了国际顶尖闭源模型。这使得V系列模型在实际应用中具有广泛的适用性。

二、应用场景

  1. DeepSeek-R系列
    • 科学研究:R系列模型凭借其在复杂任务上的卓越性能,特别适用于前沿科学研究领域。例如,在数学推理、代码生成等方面,R系列模型能够提供强大的支持,帮助科研人员解决复杂的问题。
    • 商业决策:对于需要高精度和复杂推理的商业决策场景,如金融分析、市场预测等,R系列模型也能够提供有力的支持。其强大的逻辑分析和决策能力能够帮助企业做出更加明智的决策。
  2. DeepSeek-V系列
    • 内容创作:V系列模型在文本生成、图像识别等方面具有出色的表现,因此非常适用于内容创作领域。无论是撰写文章、设计图像还是生成音频内容,V系列模型都能够提供丰富的创意和灵感。
    • 智能客服:在智能客服领域,V系列模型能够准确理解用户的问题并提供准确的回答。其多模态处理能力还能够支持语音、图像等多种交互方式,提升用户体验。

三、总结

综上所述,DeepSeek的R系列和V系列各有千秋,分别在不同的应用场景中展现出了卓越的性能。R系列以其强大的推理能力和复杂任务处理能力著称,适用于科学研究和商业决策等高端场景;而V系列则以其通用性和多模态处理能力见长,更适用于内容创作和智能客服等广泛应用领域。

2.3 蒸馏模型

2.3.1 基本概述

在deepseek的R系列模型中还提供了6种基于Qwen和Llama的蒸馏版本,显著提升小模型性能。这六个蒸馏小模型是DeepSeek-R1-Distill-Qwen系列(包括1.5B、7B、14B、32B)和DeepSeek-R1-Distill-Llama系列(包括8B、70B)

Snipaste_2025-02-20_15-30-03

蒸馏模型是一种通过将复杂、大型模型(称为教师模型)的知识迁移到一个更小、更简单的模型(称为学生模型)中,以实现模型压缩和加速推理的技术。在DeepSeek系列中,就是将R1系列大模型作为教师模型,通过特定的蒸馏方法,把知识传递给基于Qwen和Llama系列构建的学生模型。其原理是:

通过模仿教师模型的输出,训练一个较小的学生模型,从而实现知识的传递。在训练过程中,首先利用训练数据集让教师模型生成针对输入数据的响应,这些输出结果构成了后续学生模型训练的重要参考数据。然后,学生模型以此为基础进行微调,通过优化自身的参数,使其尽可能地学习和模仿教师模型的行为模式和决策逻辑,从而实现知识从教师模型到学生模型的迁移。在此过程中,学生模型不断调整自身的内部结构和参数值,以适应从教师模型传递过来的知识和经验,逐步提升自身的性能表现

通俗理解版本:用学霸和普通学生的例子解释蒸馏模型核心内容

想象一下,在一个学校里,有一个非常聪明的学霸(我们称他为“教师模型”),他在各个学科上都有出色的表现。然后,还有一个普通的学生(我们称他为“学生模型”),他想要提高自己的成绩,但不知道从何下手。可以基于如下2步实现:

第一步:教师模型的指导
教师模型(学霸)先做了一遍模拟考试题(这相当于利用训练数据集生成响应)。
他把答案和解题思路详细地写了下来,这些答案和解题思路就像是“黄金参考资料”。
现在,普通学生(学生模型)拿到了这些“黄金参考资料”,他开始以此为基础进行学习。

第二步:学生模型的微调
- 模仿与学习:
学生模型(普通学生)开始根据教师模型(学霸)提供的“黄金参考资料”来调整自己的学习方法和策略。
他会仔细对比自己的答案和学霸的答案,找出差距,并努力缩小这个差距。

- 优化自身参数:
学生模型不断调整自己的学习计划、记忆技巧等,就像是在优化自己的内部结构和参数值一样。
他尝试不同的方法,直到找到最适合自己的学习方式。

- 逐步提升性能:
随着时间的推移,普通学生逐渐掌握了更多的知识和技巧,他的成绩也开始稳步提升。
最终,他能够独立完成考试题,并且取得了不错的成绩。

- 总结
通过这个过程,我们可以看到,普通学生(学生模型)通过模仿和学习教师模型(学霸)的行为模式和决策逻辑,实现了知识的迁移和自身性能的提升。这就像是蒸馏模型中的知识传递过程一样,学生模型不断地调整自己,以适应从教师模型那里传递过来的知识和经验,从而变得越来越优秀。

具体来说,DeepSeek的蒸馏过程是通过使用R1生成的80w个推理数据样本,对较小的基础模型(例如Qwen和Llama系列)进行微调而创建的。尽管规模变小,但这些蒸馏版本仍保留了较强的推理能力。因为它们继承了R1大模型的知识和推理模式,所以在一些推理任务上能够取得不错的成绩。

2.3.2 意义和作用

  1. 降低部署门槛:蒸馏后的轻量级模型能够在资源受限的设备上运行,如移动设备、边缘计算设备等,大大扩展了AI技术的应用范围。
  2. 提升运行效率:小型模型具有更快的推理速度和更低的能耗,这对于需要实时响应的应用场景尤为重要。
  3. 个性化定制:蒸馏技术使得模型能够针对特定场景进行优化,满足不同应用的具体需求。

2.4 DeepSeek-R1部署方案

伴随着DeepSeek R1模型使用需求不断深化,如何才能部署更高性能的满血版DeepSeek R1模型,就成了很多应用场景下的当务之急。受限于DeepSeek R1 671B(6710亿参数)的模型规模,通常情况下部署DeepSeek R1满血版模型需要1200G-1400G左右显存(考虑百人内并发情况),也就是需要60块4090或者18块A100的显卡才能够顺利运行(总成本约在260万-320万左右)。哪怕是在半精度的情况下,实际测试下来也需要占用490G的显存,也就是需要24块4090或者7块A100才能够顺利运行(总成本约在120万至240万左右)。

在此情况下,如何以更少的成本获得尽可能好的模型性能——也就是如果进行DeepSeek R1的高性能部署,就成了重中之重。基本来说,目前的解决方案有以下2种:

第一种:采用“强推理、若训练”的硬件配置

如选择国产芯片、或者采购DeepSeek一体机、甚至是选择MacMini集群等,都是不错的选择。这些硬件模型训练性能较弱,但推理能力强悍,对于一些不需要进行模型训练和微调、只需要推理(也就是对话)的场景来说,是个非常不错的选择。

例如,45万左右成本,就能购买能运行DeepSeek R1满血版模型的Mac Mini集群,相比购买英伟达显卡,能够节省很大一部分成本。但劣势在于Mac M系列芯片并不适合进行模型训练和微调。

Snipaste_2025-02-20_16-29-57

第二种:采用DeepSeek R1 Distill蒸馏模型

DeepSeek R蒸馏模型组同样推理性能不俗,且蒸馏模型尺寸在1.5B到70B之间,可以适配于任何硬件环境和各类不同的使用需求。其中各蒸馏模型、各量化版本、各不同使用场景(如模型推理、模型高效微调和全量微调)下模型所需最低配置如下:

FP16、INT8和INT4是指大模型硬件配置中三种不同的数值精度表示方式。
	- 如果对模型精度要求极高,如医疗影像分析、高精度自然语言处理任务等,应首先考虑FP16或INT8。  
	- 如果需要高推理速度,如实时语音识别、视频处理等任务,可以考虑使用INT8或INT4。
	- 如果存储资源是首要考虑因素,如在移动设备或嵌入式系统中部署模型,INT8和INT4可能是更好的选择。

GPT-4 发布于 2023 年 3 月 14 日,而 o1-mini 是在 2024 年 9 月 12 日发布的。
	- 如果你需要处理复杂的文本生成和对话任务,GPT-4 可能是一个更好的选择。
	- 而如果你需要在资源有限的环境中进行快速推理,并且对计算资源消耗有严格要求,那么 o1-mini 可能更适合你。
  • Deepseek-R1-Distill-Qwen-1.5B:

    • 性能指数:2颗星,推理能力达到GPT-4o级别(int4 32位 int8 64)
    • 适用场景:移动端应用或者个人助理
    Snipaste_2025-02-20_16-33-25
  • Deepseek-R1-Distill-Qwen-7B

    • 性能指数:3颗星,推理能力达到o1-mini的70%左右
    • 适用场景:高校实验室或者小型团队适用
    Snipaste_2025-02-20_16-34-19
  • Deepseek-R1-Distill-Qwen-14B

    • 性能指数:4颗星,推理能力达到o1-mini的80%左右
    • 适用场景:适用于一般商业场景
    Snipaste_2025-02-20_16-35-12
  • Deepseek-R1-Distill-Qwen-32B

    • 性能指数:5颗星,推理能力达到o1-mini性能级别
    • 适用场景:适用于高性能要求的商业场景
    Snipaste_2025-02-20_16-35-52
  • Deepseek-R1-Distill-Llama-8B

    • 性能指数:3颗星,推理能力达到o1-mini的70%左右
    • 适用场景:高校实验室或者小型团队适用
    Snipaste_2025-02-20_16-36-42
  • Deepseek-R1-Distill-Llama-70B

    • 性能指数:5颗星,推理能力达到o1-mini的性能级别
    • 适用场景:适用于高性能要求的商业场景
    Snipaste_2025-02-20_16-37-15

3、Deepseek-R1的API调用指南

3.1 快速开始

DeepSeek-R1正式版已于2025年1月20号正式上线,其强大的模型推理能力可以让DeepSeek-R1在数学、代码、自然语言推理等任务上取得很好的结果,性能比肩 OpenAI o1 正式版。

DeepSeek-R1的API调用也是非常简单的,但是在调用API之前需要我们完成API Key的申请:

  • 访问DeepSeek官网:https://www.deepseek.com/,进入到API开放平台中

    Snipaste_2025-02-20_18-55-11
  • 进行API Key的申请:

    • 新用户注册即赠送10元额度,约500万token额度。
    • 价格方面,DeepSeek R1价格约为OpenAI o1正式版模型的1/50:
    Snipaste_2025-02-20_18-58-23 Snipaste_2025-02-20_18-59-29
  • 注意:

    • 目前DeepSeek R1模型调用不限速

      Snipaste_2025-02-20_19-02-59
    • API调用风格和OpenAI完全一致,但是暂不支持多模态和Function calling功能。

3.2 API调用规范

在Deepseek官网中有如下内容:

Snipaste_2025-02-20_19-06-15

OpenAI安装:

pip install openai

调用实例:

from openai import OpenAI

ds_api_key = "YOUR_DS_API_KEY"

# 实例化客户端
client = OpenAI(api_key=ds_api_key, 
                base_url="https://api.deepseek.com")

# 调用 deepseek-r1 模型
response = client.chat.completions.create(
    #model="deepseek-chat" 调用Deepseek-V3模型,不存在推理过程
    #model="deepseek-coder" 调用的是DeepSeek的Coder模型,不存在推理过程。这个模型是专为代码相关任务而训练的,具有较强的代码生成、理解、修改和调试能力,适用于编程问答、代码补全、代码错误检测与修复等场景
    model="deepseek-reasoner", #调用推理模型deepseek-r1 模型标识/名称,存在推理过程
    messages=[
        {"role": "user", "content": "请问,9.8和9.11哪个更大?"}
    ]
)

# 最终回复
response.choices[0].message.content

# 思考链
response.choices[0].message.reasoning_content

注意,在上述代码中,r1模型不光可以返回模型对于用户提问的回答,还可以返回对于用户提问问题的具体推理/思考过程,该过程就是r1模型的思考链。而思考链也是r1模型和GPT的o1模型主要的一个区别。

关于返回思考链的深度思考

如果我们可以看到r1模型的思考过程的话,那么就可以根据这个思考过程的内容去创建非常多高质量的问答数据。在问答数据中,不仅会包含问题和答案,还会包含对于问题的思考过程。这也是Deepseek可以基于r1模型蒸馏了很多小尺寸模型的主要原因。

Message参数设置方法

时至今日,“多角色” 对话基本上已经成了顶尖大模型的标配。正是基于多角色对话这一基础技术架构,大模型才能非常灵活的实现各类对话需求。而实际执行多角色对话的过程中,其核心是依靠messages参数来实现的。

messages(必填)

  • messages 参数是 DeepSeek模型 API 中必填的参数之一,用于定义聊天上下文,包括用户的输入、系统的指令、助手的回复等。通过 messages 数组,模型可以理解当前对话的背景,从而生成更加连贯的响应。根据不同的使用场景,messages 包含多种类型的消息,例如 system messageuser messageassistant message。下面是对 messages 参数及其各个子类型的详细解释。

    • content (必填):系统消息的内容,可以是字符串或数组。如果是数组,可能包含多个类型的内容(如文本、图像)。
    • role (必填):此处角色为 system,表明这是系统发出的消息。
    • name (可选):提供系统消息发送者的名称,尤其适用于区分多个具有相同角色的参与者。

    示例代码:

    response = client.chat.completions.create(
        model="deepseek-reasoner",
        messages=[
            {'role':'system','content':"你是一位滑稽且幽默的小品演员。"},
            {"role": "user", "content": "请问,你如何理解人生呢?"}
        ]
    )
    display(Markdown(response.choices[0].message.content))
    
    
    
    response = client.chat.completions.create(
        model="deepseek-reasoner", 
        messages=[
            {"role": "system", "content": "你是一位大学数学系教授"},
            {"role": "user", "content": "请问,你如何理解人生呢?"}
        ]
    )
    display(Markdown(response.choices[0].message.content))
    
    • 还有一个非常常见的system message的使用方法,就是借助system消息进行聊天背景信息的设定,很多时候我们可以在system消息中输入一段长文本,这段长文本将在聊天开始之前输入到系统中,而在之后的聊天中,即可让assistant围绕这个长文本进行回答,这是一种最简单的实现大语言模型围绕本地知识库进行问答的方法。
    text = '张三,男,1990年10月25日出生于中国台湾省高雄市。\
            2013年毕业于北京工业大学的信息工程专业,由于在校表现良好,毕业后被中科院信息技术部破格录取。'
    response = client.chat.completions.create(
        model="deepseek-reasoner",  
        messages=[
            {"role": "system", "content": text},
            #请问张三是什么星座的?请问张三毕业后去哪里了?
            {"role": "user", "content": '请问张三是哪一年毕业的?'}
      ]
    )
    response.choices[0].message.content
    
    • user message 表示用户发给模型的消息,是对话的核心部分之一。它定义了用户的输入内容,模型根据这些内容生成响应。

      • content (必填):用户消息的内容,通常为文本或图像链接的数组。对于支持图像输入的模型,如 DeepSeek v2.5 ,还可以传递图像。
      • role (必填):角色为 user,表示该消息来自用户。
      • name (可选):可以为用户指定一个名称,用于区分多个具有相同角色的用户。

      示例代码:

      # 创建用户消息
      user_message = {
          "role": "user",
          "content": "你好,请介绍下你自己。"
      }
      
    • assistant message表示助手消息,是模型根据用户消息生成的响应。

      • content:类型为字符串,表示助手消息的内容,这是助手对用户提问的回答或执行任务的结果等。

      • role:类型为字符串,固定为 “assistant”,表示消息的作者角色是助手。

      • name:类型为字符串,表示对话参与者的名称,一般用于区分不同身份的助手。


day22 langchain框架基础

文件: day22 langchain框架基础\langchain-model+提示词模版.md

1. 什么是LangChain

LangChain现在归属于LangChain AI公司,LangChain作为其中的一个核心项目,开源发布在Gitub上:https://github.com/langchain-ai/langchain

LangChain给自身的定位是:用于开发由大语言模型支持的应用程序的框架。它的做法是:通过提供标准化且丰富的模块抽象,构建大语言模型的输入输入规范,利用其核心概念chains,灵活地连接整个应用开发流程。而针对每个功能模块,都源于对大模型领域的深入理解和实践经验,开发者提供出来的标准化流程和解决方案的抽象,再通过灵活的模块化组合,才有了目前这样一款在大模型应用开发领域内被普遍高度认可的通用框架。

1.1 为什么需要学习LangChain?

首先,我们需考虑当前大模型的发展态势。尽管OpenAI的GPT系列模型作为大模型领域的领军人物,在很大程度上了影响了大模型的使用规范和基于大模型进行应用开发的范式,但并不意味着所有大模型间的使用方式完全相同。因此,对于每个新模型都要花费大量时间学习其特定规范再进行应用探索,这种工作效率显然是十分低下的。

其次,必须谈论的是大模型目前面临的局限,如知识更新的滞后性、外部API调用能力、私有数据连接方式以及输出结果的不稳定性等问题。在应用开发中,如何找到这些问题的有效解决策略?

上述提到的每个限制都紧密关联于大模型本身的特性。尽管理论上可以通过重新训练、微调来增强模型的原生能力,这种方法确实有效,但实际上,大多数开发者并不具备进行这样操作所需的技术资源、时间和财力,选择这条路径一定会导致方向越来越偏离目标。我们之前讨论的Function Calling接入第三方API能够提供一些解决方案,但这每一步都需大量的研发投入,而且最终实现后的应用效果,也取决于研发人员的个人技术能力。在这种背景下,既然大家都有不同的想法和解决方案,那LangChain就来集中做这件事,提供一个统一的平台和明确的定义,来实现应用框架的快速搭建,这就是LangChain一直想要做到,且正在做的事情。

1.2 LangChain的做法

从本质上分析,LangChain还是依然采用从大模型自身出发的策略,通过开发人员在实践过程中对大模型能力的深入理解及其在不同场景下的涌现潜力,使用模块化的方式进行高级抽象,设计出统一接口以适配各种大模型。到目前为止,LangChain抽象出最重要的核心模块如下:

模型(Model I/O)

LangChain支持主流的大型语言模型哦,像DeepSeek这种,它都能轻松对接并进行接口调用。并且langchain还合理规范了大模型的输入(提示词)和输出(输出解析器)。

提示模板(Prompts)

这个提示模板功能可不得了!它可以动态地生成提示词哦。比如说,我们可以根据具体的任务需求,让系统自动生成合适的提示词来引导模型进行回答或者操作。

链(Chains)

想象一下,我们要把多个任务步骤连接起来,形成一个完整的工作流程,就像搭建一条流水线一样,这就是链(Chains)的作用啦。比如说,我们可以把“用户输入 → 检索知识库 → 模型生成 → 结果解析”这样一个流程串联起来,形成一个高效的工作流。这样一来,每个步骤都能有条不紊地进行,大大提高了工作效率。

记忆(Memory)

记忆这个组件也很关键哦。它可以帮助我们管理对话历史呢。这里面又分为短时记忆和长时记忆。短时记忆就像是我们的短期记忆,主要是会话上下文,能让我们记住当前这次对话的一些关键信息;长时记忆呢,就像是长期存储在大脑里的知识一样,它会把数据存储到数据库里,方便我们以后随时查阅和使用。

代理(Agents)

代理这个组件就像一个聪明的小助手,它可以动态地调用外部工具哦。比如说,当我们需要计算一些复杂的数学问题时,它可以调用计算器这个外部工具来帮忙;要是我们需要查找一些特定的信息,它还能调用搜索引擎为我们寻找答案呢。这样一来,就大大扩展了模型的功能,让它能做更多的事情啦。

数据检索(Indexes)

最后再给大家介绍一下数据检索这个组件哈。它能集成向量数据库,然后构建本地知识库哦。这就好比是为模型建立了一个专属的知识宝库,当模型需要回答问题的时候,就可以从这个宝库里获取更准确、更丰富的信息,从而提高回答的准确性。

2. langchain环境安装

LangChain的安装过程非常简单,可以通过常用的Python包管理工具,如pip或conda,直接进行安装。稍复杂一点的还可以通过源码进行安装。但有一点大家一定要明确:LangChain的真正价值在于它能够与多种模型提供商、数据存储解决方案等进行集成。默认情况下,使用上述两种安装方式中的任意一种来进行LangChain安装后,安装的仅仅是LangChain的默认功能,并不包括这些集成所需的额外依赖项。

也就是说,如果我们想要使用特定的集成功能,还需要额外安装这些特定的依赖。以调用OpenAI的API为例,我们首先需要通过运行命令pip install langchain-openai安装OpenAI的合作伙伴包,安装此依赖包后,LangChain才能够与OpenAI的API进行交互。后续我们在使用相关功能的时候,会提供额外的说明。

LangChain安装官方说明文档:https://python.langchain.com/docs/get_started/installation

2.1 使用包版本管理工具安装

LangChain可以使用pip 或者 conda直接安装,适用于仅使用的场景,即不需要了解其源码构建过程。这种安装方法十分简洁明了,只需执行一条命令,就可以在当前的虚拟环境中迅速完成LangChain的安装。具体操作如下:

pip install langchain=0.3.20

验证LangChain的安装情况,执行命令如下:

import langchain

print(langchain.__version__)

如果能正常输出LangChain的版本,说明在当前环境下的安装成功。

2.2 源码安装

除了通过pip安装外,还有一种通过源码安装的方法。这需要使用git拉取远程仓库,然后进入项目文件夹并执行pip install -e .命令。这种方法不仅会安装必要的依赖,同时也将程序的源代码保存在本地对于课程学习而言,我们推荐采用源码安装方式,这将非常有助于在后续的LangChain功能探索中,通过源码分析深入理解框架的构建原理和详细机制。

源码安装LangChain的详细步骤:

  • Step 1. 安装Anaconda

    按照对应的教程内容配置好Anaconda。

  • Step 2. 使用Conda创建LangChain的Python虚拟环境

    安装好Anaconda后,我们需要借助Conda包版本工具,为LangChain项目创建一个新的Python虚拟运行环境,执行代码如下:conda create --name langchain python==3.11

    创建完成后,通过如下命令进入该虚拟环境,执行后续的操作:conda activate langchain

  • Step 3. 下载LangChain的项目文件

    进入LangChain的官方Github,地址:https://github.com/langchain-ai/langchain , 在 GitHub 上将项目文件下载到有两种方式:克隆 (Clone) 和 下载 ZIP 压缩包。推荐使用克隆 (Clone)的方式。

  • Step 4. 升级pip版本

    建议在执行项目的依赖安装之前升级 pip 的版本,如果使用的是旧版本的 pip,可能无法安装一些最新的包,或者可能无法正确解析依赖关系。升级 pip 很简单,只需要运行命令如下命令:

    python -m pip install --upgrade pip

  • Step 5. 源码安装项目依赖

    不同于我们之前一直使用的pip install -r requirements.txt方式,这种方法用于批量安装多个依赖包,是在部署项目或确保开发环境与其他开发者/环境一致时的常用方式。而对于LangChain,我们需要使用pip install -e 的方式,以可编辑模式安装包。这种方式主要用于开发过程中。当以可编辑模式安装一个包时,依赖包会被直接从源代码所在位置安装,而不是复制到Python的site-packages目录,是开发模式下用于安装并实时反映对本地包更改的方法。需要执行的步骤如下:

    cd langchain-master/libs/langchain/ #进入到LangChain源码的libs下的langchain目录中
    
    pip install -e .
    

如在安装过程未发生任何报错,则说明安装成功。在安装完依赖后,我们就正式进入LangChain的Model I/O模块的实践。

3.Models I/O模块

LangChain的Model I/O模块提供了标准的、可扩展的接口实现与大语言模型的外部集成。所谓的Model I/O,包括模型输入(Prompts)、模型输出(OutPuts)和模型本身(Models),简单理解就是通过该模块,我们可以快速与某个大模型进行对话交互

任何语言模型应用的核心都是大语言模型(LLMs)。因此,在讨论和实践Model I/O模块时,首先应当关注如何集成这些大模型。因此,接下来我们首先学习:如何借助LangChain框架使用不同的大模型。

3.1 LangChain接入大模型的方法

LangChain 提供了一套与任何大语言模型进行交互的标准构建模块。所以需要明确的一点是:虽然 LLMs 是 LangChain 的核心元素,但 LangChain 本身不提供 LLMs,它仅仅是为多种不同的 LLMs 进行交互提供了一个统一的接口。简单理解:以OpenAI的GPT系列模型为例,如果我们想通过 LangChain 接入 OpenAI 的 GPT 模型,我们需要在LangChain框架下先定义相关的类和方法来规定如何与模型进行交互,包括数据的输入和输出格式以及如何连接到模型本身。然后按照 OpenAI GPT 模型的接口规范来集成这些功能。通过这种方式,LangChain 充当一个桥梁,使我们能够按照统一的标准来接入和使用多种不同的大语言模型。

需要安装OpenAI的集成依赖包langchain-openai,执行如下命令: pip install langchain-openai

LangChain作为一个应用开发框架,需要集成各种不同的大模型,通过Message数据输入规范,可以定义不同的role,即system、user和assistant来区分对话过程。LangChain目前就抽象出来的消息类型有 AIMessage 、 HumanMessage 、 SystemMessage 和FunctionMessage,但大多时候我们只需要处理 HumanMessage 、 AIMessage 和 SystemMessage,即:

  • SystemMessage :用于启动 AI 行为,作为输入消息序列中的第一个传入。
  • HumanMessage :表示来自与聊天模型交互的人的消息。
  • AIMessage :表示来自聊天模型的消息。这可以是文本,也可以是调用工具的请求。

因此我们需要导入如下模块:

from langchain_openai import OpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI

消息形式输入调用

  • 定义消息对象:
messages = [
  	SystemMessage(content="你是个取名大师,你擅长为创业公司取名字"),
		HumanMessage(content="帮我给信公司取个名字,要包含AI")
]
  • 执行推理:
API_KEY = open('deepseekAPI-Key.md').read().strip()
chat = ChatOpenAI(
        model_name="deepseek-chat",
        api_key=API_KEY,
        base_url="https://api.deepseek.com"
                 )
reponse = chat.invoke(messages) #处理单条输入
reponse.content
  • 流式调用
for chunk in chat.stream(messages):
    print(chunk.content, end="", flush=True)
  • 批量调用
#先定义三个不同的消息对象:
messages1 = [SystemMessage(content="你是一位乐于助人的智能小助手"),
HumanMessage(content="请帮我介绍一下什么是机器学习"),]

messages2 = [SystemMessage(content="你是一位乐于助人的智能小助手"),
HumanMessage(content="请帮我介绍一下什么是深度学习"),]

messages3 = [SystemMessage(content="你是一位乐于助人的智能小助手"),
HumanMessage(content="请帮我介绍一下什么是大模型技术"),]

#将上述三个消息对象放在一个列表中,使用.batch方法执行批量调用
reponse = chat.batch([messages1,
                      messages2,
                      messages3,])

contents = [msg.content for msg in reponse]
for content in contents:
    print(content, "\n---\n")

3.2 LangChain接入指定类型大模型

针对不同的模型,LangChain也提供个对应的接入方法,其相关说明文档地址(只可以接入文档中有的模型):https://python.langchain.com/docs/integrations/chat/

比如我们以DeepSeek的在线API模型为例快速接入一下:https://python.langchain.com/docs/integrations/chat/deepseek/

环境安装:pip install -qU langchain-deepseek

from langchain_deepseek import ChatDeepSeek
fp_ds = open('./key_files/deepseekAPI-Key.md','r')
ds_key = fp_ds.readline().strip()

llm = ChatDeepSeek(
    model="deepseek-chat",
    temperature=0,
    api_key=ds_key
)
messages = [
    (
        "system",
        "你是一位乐于助人的智能小助手",
    ),
    ("human", "请帮我介绍一下什么是大模型技术"),
]
ai_msg = llm.invoke(messages)
ai_msg.content
  • 思考:ChatOpenAI和ChatDeepSeek两种模型接入的区别?

基于ChatOpenAI接入DeepSeek大模型适合追求接口兼容性和快速迁移的场景,而基于ChatDeepSeek接入则更适合需要深度定制和发挥DeepSeek特有功能的场景。选择哪种方式取决于具体的应用需求、开发团队的技能背景以及对性能和定制化的要求。

3.3 LangChain接入本地大模型

LangChain使用Ollama接入本地化部署的开源大模型。环境安装:pip install langchain-ollama

from langchain_ollama import ChatOllama
from langchain_ollama import ChatOllama
from langchain_core.messages import HumanMessage
#实例化大模型
ollama_llm = ChatOllama(model="deepseek-r1:7b")
messages = [
    HumanMessage(
        content="你好,请你介绍一下你自己",
    )
]
#可以直接调用invoke方法实现模型推理
chat_model_response = ollama_llm.invoke(messages)
#获取纯净的模型推理结果,即去除掉特殊字符\n。
chat_model_response.content.replace('\n', '')

更多调用参数modelsystemtemperature等参数

from langchain_ollama import ChatOllama
from langchain_core.messages import HumanMessage
#实例化大模型
ollama_llm = ChatOllama(
    model="deepseek-r1:7b",
    # 添加temperature
    temperature=0,
    # 添加系统信息
    system="你是一位优秀且具有丰富经验的算法教授",
    # 添加format指定输出的内容形式
    format='json'
)
messages = [
    HumanMessage(
        content="你好,请你帮我详细的介绍一下什么是机器学习",
    )
]
#可以直接调用invoke方法实现模型推理
chat_model_response = ollama_llm.invoke(messages)
chat_model_response

3.4 LangChain中如何使用提示词模版

提示工程(Prompt Engineering)大家应该比较熟悉,这个概念是指在与大语言模型(LLMs),如GPT-3、DeepSeek等模型进行交互时,精心设计输入文本(即提示)的过程,以获得更精准、相关或有创造性的输出。目前,提示工程已经发展成为一个专业领域,非常多的公司设立了专门的职位,负责为特定任务编写精确且具有创造力的提示。

以使用DeepSeek等网页端对话交互应用中,大部分人常见的做法是将提示(Prompt)做硬编码,例如将一段提示文本固定在System Messages中。而在应用开发领域,开发者往往无法预知用户的具体输入内容,同时又希望大模型能够根据不同的应用任务以一种较为统一的逻辑来处理用户输入。所以,LangChain通过提供指定的提示词模版功能,优雅地解决了这个问题。提示词模版功能就是将用户输入到完整格式化提示的转换逻辑进行封装,使得模型能够更灵活、高效地处理各种输入。

LangChain 提供了创建和使用提示模板的各种工具。

3.4.1 PromptTemplate

PromptTemplate 是 LangChain 提示词组件的核心类,其构造提示词的过程本质上就是实例化这个类。在实例化 PromptTemplate 类时,需要提供两个关键参数:templateinput_variables

  • template: 这是一个字符串,表示你想要生成的提示词模板。例如,如果你想要一个用于生成故事的提示词,你的模板可能是 "Once upon a time in {location}, there was a {character}..."。
  • input_variables: 这是一个字典,包含了所有你希望在提示词中出现的变量。这些变量会在 template 字符串中被替换。例如,对于上面的模板,你可能需要提供一个包含 locationcharacter 键的字典。

准备好这两个参数后,你可以实例化一个基础的 PromptTemplate 类,生成的结果就是一个 PromptTemplate 对象,也就是一个 PromptTemplate 包装器。这个包装器可以在 LangChain 的各个链组件中被调用,从而在整个应用中复用和管理提示词模板。

以下是一个简单的示例代码,展示了如何实例化和使用 PromptTemplate 类:

from langchain.prompts import PromptTemplate
from langchain_openai import ChatOpenAI  # 假设使用 OpenAI 的聊天模型作为示例

# 定义模板和输入变量
template_str = (
    "你是一个专业的翻译助手,擅长将{input_language}文本准确翻译成{output_language}。"
    "请翻译以下内容:'{text}'"
)
input_vars = {
    "input_language": "中文",
    "output_language": "英语",
    "text": "今天天气很好,适合出去散步。"
}

# 实例化 PromptTemplate 类
prompt_template = PromptTemplate(template=template_str, input_variables=input_vars)

# 生成完整的提示词
full_prompt = prompt_template.format(**input_vars)
full_prompt

这个提示模板⽤于格式化单个字符串,通常⽤于更简单的输⼊。

结合模型使用

# 创建语言模型实例(这里以 ChatOpenAI 为例)
API_KEY = "sk-4b79f3a3ff334a15a1935366ebb425b3"
llm = ChatOpenAI(model_name="deepseek-chat",
                  api_key=API_KEY,base_url="https://api.deepseek.com")

# 向语言模型发送请求并获取响应
response = llm.invoke(full_prompt)

# 打印模型的响应
print("模型回复:", response.content)

3.4.2 ChatPromptTemplate

chatPromptTemplate包装器是 LangChain 中用于创建聊天提示词模板的组件。与PromptTemplate 包装器不同,ChatPromptTemplate包装器构造的提示词是一个消息列表,并且支持输出Message 对象。LangChain 提供了内置的聊天提示词模板(ChatPromptTemplate)和角色消息提示词模板,包括 AIMessagePromptTemplateSystemMessagePromptTemplateHumanMessagePromptTemplate` 三种类型。

主要特点

  • 消息列表: ChatPromptTemplate 生成的是消息列表,而不是单一的字符串。
  • Message 对象支持: 可以输出 Message 对象,这使得在处理复杂对话时更加灵活。
  • 多种角色模板: 提供了不同的角色消息提示词模板,如 AI、系统和人类消息提示词模板。

使用步骤

  1. 选择模板类: 根据需求选择合适的内置模板类,如 AIMessagePromptTemplateSystemMessagePromptTemplateHumanMessagePromptTemplate
  2. 实例化为包装器对象: 将选定的模板类实例化为一个包装器对象。
  3. 格式化用户输入: 使用包装器对象来格式化外部的用户输入。
  4. 调用类方法输出提示词: 通过调用包装器对象的类方法来生成最终的提示词。
from langchain.prompts.chat import ChatPromptTemplate

# 构建模版
template = """你是一只粘人的小猫,你叫{name}。我是你的主人,你每天都有和我说不完的话,下面请开启我们的聊天。要求如下:
    1.你的逾期要像一只猫
    2.你对生活的观察有独特的视角,一些想法是在人类身上很难看到的
    3.你的语气很可爱,会认真倾听我的话,又不会不断开启新的话题
    下面从你迎接我下班回家开始我们的今天的对话"""
human_template = "{user_input}"

# 生成对话形式的聊天信息格式
chat_prompt = ChatPromptTemplate.from_messages([
    ("system", template),
    ("human", human_template),
])

# 格式化变量输入
messages = chat_prompt.format_messages(name="咪咪",user_input='想我了吗')
messages

基于invoke函数可以动态更换填充内容

chat_prompt.invoke({"name":"豆豆",'user_input':'我好饿呀'})

加入到模型中使用:

API_KEY = "sk-4b79f3a3ff334a15a1935366ebb425b3"
chat = ChatOpenAI(model_name="deepseek-chat",
                  api_key=API_KEY,base_url="https://api.deepseek.com")
reponse = chat.invoke(messages) 
reponse.content

多轮对话封装

from langchain.prompts import (
    ChatPromptTemplate,
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate,
)
from langchain.schema import AIMessage, HumanMessage
from langchain_openai import ChatOpenAI
# 构建系统消息模板
system_template = """你是一只粘人的小猫,你叫{name}。我是你的主人,你每天都有和我说不完的话,下面请开启我们的聊天。要求如下:
    1. 你的语气要像一只猫
    2. 你对生活的观察有独特的视角,一些想法是在人类身上很难看到的
    3. 你的语气很可爱,会认真倾听我的话,又不会不断开启新的话题
"""

# 初始化消息列表,首先添加系统消息
messages = [
    SystemMessagePromptTemplate.from_template(system_template).format(name="咪咪")
]

API_KEY = "sk-4b79f3a3ff334a15a1935366ebb425b3"
chat = ChatOpenAI(
    model_name="deepseek-chat",
    api_key=API_KEY,
    base_url="https://api.deepseek.com"
)

while True:
    user_input = input("你: ")
    if user_input.lower() in ['退出', 'exit', 'quit']:
        print("再见!")
        break
    # 添加用户消息到消息列表
    messages.append(HumanMessage(content=user_input))
    
    # 调用模型生成回复
    response = chat.invoke(messages)
    
    # 打印AI回复
    print(f"AI: {response.content}")
    
    # 添加AI回复到消息列表
    messages.append(AIMessage(content=response.content))

3.4.3 MessagesPlaceholder

MessagesPlaceholder 是 langchain 库中的一个重要组件,它的主要作用是作为对话历史的占位符,使得聊天提示模板更加灵活和可复用。它的意义在于解耦模板与数据、支持多轮对话、提高代码的可维护性,并广泛应用于聊天机器人、对话总结、多轮任务处理等场景中。通过使用 MessagesPlaceholder,开发者可以轻松构建基于上下文的智能对话系统。

作用

MessagesPlaceholder 是一个占位符,用于在聊天提示模板中预留位置,以便后续填充具体的对话历史。它的作用类似于一个“变量”,但专门用于存储消息列表。

  • 占位符功能:在定义聊天提示模板时,MessagesPlaceholder 表示对话历史的占位符。例如:MessagesPlaceholder(variable_name="conversation") 这里,conversation 是一个变量名,表示后续会填充具体的对话内容。
  • 动态填充对话历史:在实际使用时,可以通过 format_prompt 方法将具体的对话历史(如 [human_message, ai_message])填充到占位符中。

意义

MessagesPlaceholder 的设计使得聊天提示模板更加灵活和可复用,具有以下重要意义:

  • 解耦模板与数据:
    • 通过将对话历史与提示模板分离,MessagesPlaceholder 使得提示模板可以独立于具体的对话内容。 这样,同一个提示模板可以用于不同的对话场景,只需填充不同的对话历史即可。
  • 支持多轮对话:
    • 在多轮对话中,对话历史会不断累积。MessagesPlaceholder 允许动态填充对话历史,使得提示模板能够适应多轮对话的需求。
    • 例如,在第二轮对话中,conversation 可能包含之前的对话内容,从而让 AI 能够基于完整的上下文生成回复。

实际应用场景

MessagesPlaceholder 在实际中的应用非常广泛,尤其是在需要处理多轮对话或动态生成提示的场景中。以下是一些典型的应用场景:

  • 聊天机器人:
    • 在聊天机器人中,MessagesPlaceholder 可以用于存储用户与机器人的对话历史,从而让机器人能够基于上下文生成更自然的回复。
    • 例如,用户问:“学习编程最好的方法是什么?”,机器人回答后,MessagesPlaceholder 可以记录这段对话,并在后续对话中使用。
  • 对话总结:
    • 在需要总结对话的场景中,MessagesPlaceholder 可以存储完整的对话历史,并生成简洁的总结。
  • 多轮任务处理:
    • 在需要处理多轮任务的场景中,MessagesPlaceholder 可以记录每一轮的对话内容,从而让系统能够基于完整的上下文完成任务。
    • 例如,在客服系统中,MessagesPlaceholder 可以记录用户的问题和客服的回复,从而帮助客服更好地理解用户的需求。

示例:

基于ChatPromptTemplate创建一个聊天提示词模版,使用MessagesPlaceholder存储对话记录,实现聊天内容总结。

from langchain.prompts import (
    ChatPromptTemplate,
    HumanMessagePromptTemplate,
    MessagesPlaceholder #用于在提示模板中预留位置,以便后续填充具体的消息内容。
)
#定义字符串提示词模板
human_prompt = "用 {word_count} 字总结我们迄今为止的对话。"

#将human_prompt字符串模板转换为 HumanMessagePromptTemplate 对象。
human_message_template = HumanMessagePromptTemplate.from_template(human_prompt)

#定义聊天提示模板
chat_prompt = ChatPromptTemplate.from_messages(
    [MessagesPlaceholder(variable_name="conversation"),human_message_template]
)
from langchain_core.messages import AIMessage,HumanMessage

#手动创建一轮聊天消息记录
human_message = HumanMessage(content="学习编程最好的方法是什么?")
ai_message = AIMessage(
    content = """1.选择编程语言:决定想要学习的编程语言是什么?
    2.从基础开始:熟悉变量、数据类型和流程控制等基本编程概念。
    3.练习、练习、再练习:学习编程最好的方法就是通过不断练习"""
)

#格式化提示并生成消息
chat_prompt.format_prompt(
    #conversation 被填充为 [human_message, ai_message],即对话的历史记录
    conversation=[human_message,ai_message],
    #word_count 被填充为 "10",表示需要用10个字来总结对话。
    word_count="10"
).to_messages()#to_messages() 方法将格式化后的提示转换为消息列表。
#提示词作用到模型进行聊天记录总结
API_KEY = "sk-4b79f3a3ff334a15a1935366ebb425b3"
chat = ChatOpenAI(
    model_name="deepseek-chat",
    api_key=API_KEY,
    base_url="https://api.deepseek.com"
)

chain = chat_prompt | chat
response = chain.invoke({"word_count":"10","conversation":[human_message,ai_message]})
response.content

day23langchain进阶

文件: day23langchain进阶\langchain进阶.md

缓存机制

Langchain为LLMs提供了可选的缓存层。这个很有用,原因是:

  • 如果经常向模型多次请求提问相同的内容,Caching缓存可以减少对LLM进行API调用次数来提升程序运行效率。

Caching缓存

具体实现:

  • 设置一个内存缓存(InMemoryCache)来缓存大型语言模型(LLM)的调用结果,以提高后续相同请求的处理速度。
from langchain.globals import set_llm_cache #用于设置全局的LLM缓存机制。
from langchain.cache import InMemoryCache #将缓存数据存储在内存中,而不是磁盘上
set_llm_cache(InMemoryCache()) #使用内存缓存来存储和检索LLM的调用结果。

第一次向模型进行提问:耗时较久,但是会讲提问内容加入到缓存中

from langchain_openai import OpenAI
from langchain_openai import ChatOpenAI
#提示词作用到模型进行聊天记录总结
API_KEY = "sk-4b79xxxx935366ebb425b3"
chat = ChatOpenAI(
    model_name="deepseek-chat",
    api_key=API_KEY,
    base_url="https://api.deepseek.com"
)

response = chat.invoke("3只鸭子几条腿?")
response.content

再次提问相同内容,直接基于缓存内容进行回复,响应速度快

response = chat.invoke("3只鸭子几条腿?")
response.content

SQLite缓存

具体实现:

设置一个SQLite缓存来缓存大型语言模型(LLM)的调用结果,以提高后续相同请求的处理速度。

from langchain.cache import SQLiteCache
set_llm_cache(SQLiteCache(database_path="./langchain.db"))

#加入问答到缓存中
response = chat.invoke("讲一个10个字的故事?")
response.content

基于缓存进行快速响应

response = chat.invoke("讲一个10个字的故事?")
response.content

输出解析器

输出解析器(Output Parser)在提示词工程中扮演着重要角色。让我们更详细地探讨它的两大功能以及为什么它与提示词模板有关系。

输出解析器的功能:

输出解析器的一个关键功能是向现有的提示词模板中添加输出指令。这些指令告诉语言模型应该如何格式化和结构化生成的输出内容。例如:

  • JSON格式"请以JSON格式输出以下信息:{ "name": "用户姓名", "age": "用户年龄" }"
  • HTML格式"请以HTML格式输出以下信息:<div>用户姓名: 用户名</div><div>用户年龄: 用户年龄</div>"
  • 纯文本格式"请以纯文本格式输出以下信息:姓名: 用户名, 年龄: 用户年龄"

通过添加这些输出指令,输出解析器确保模型按照指定的格式生成输出,而不是仅仅返回原始数据。

预设的 LangChain 输出解析器

LangChain 提供了一堆预设的输出解析器,这些解析器真的超实用,包括:

  1. BooleanOutputParser:这个解析器专门用于解析布尔值(即对错、真假)的输出。例如,当模型输出是 True 或 False 时,该解析器可以准确识别和处理。
  2. CommaSeparatedListOutputParser:此解析器用于解析以逗号分隔的列表输出。比如,当模型返回一个由逗号分隔的字符串时,这个解析器可以将其转换为一个列表。
  3. DatetimeOutputParser:该解析器用于处理日期和时间的输出。它能够将模型生成的日期时间字符串解析为标准的日期时间格式,方便后续处理。
  4. EnumOutputParser:枚举类型的输出可以通过这个解析器来处理。枚举类型通常是指有限个选项的类型,比如星期几、颜色等,这个解析器能够准确识别并转换这些输出。
  5. ListOutputParser:当输出是一个列表时,无论是什么类型的列表,都可以使用这个解析器进行解析。它能将模型生成的列表字符串转换为实际的列表对象。
  6. PydanticOutputParser:如果你的输出需要符合 Pydantic 的要求(Pydantic 是一个用于数据验证和转换的库),那么这个解析器就能派上用场。它可以确保输出数据符合预定义的数据模型和验证规则。
  7. StructuredOutputParser:对于具有特定结构的输出,这个解析器可以大显身手。它能够处理复杂的结构,并将模型生成的结构化数据解析为易于使用的格式。

CommaSeparatedListOutputParser列表输出解析器示例

from langchain_openai import ChatOpenAI
from langchain.output_parsers import CommaSeparatedListOutputParser
from langchain.prompts import PromptTemplate
#构造列表解析器
output_parser = CommaSeparatedListOutputParser()
#返回解析器的解析格式
output_parser.get_format_instructions()

注意:所有解析器的解析格式都是英文的,上述列表解析器解析格式的英文翻译是:您的响应应该是逗号分隔的值列表,例如:foo,bar,bazfoo,bar,baz`。也就是通过解析器的该种解析格式作为提示词的部分内容,约束模型按照指定格式进行内容的输出。

  • 解析器作用在PromptTemplate模版中

    #构造输入模版,这里的区别是:在输入的Prompt Template中,加入了OutPut Parse的内容
    template = """用户发起的提问:
    
    {question}
    
    {format_instructions}"""
    
    #实例化输出解析器(用于解析以逗号分隔的列表类型的输出)
    output_parser = CommaSeparatedListOutputParser()
    
    #创建提示词模版,将输出解析器的解析格式作为提示词模版的部分内容
    prompt = PromptTemplate.from_template(
        template,
        partial_variables={"format_instructions":
                           output_parser.get_format_instructions()},
    )
    
    
    #最后,使用LangChain中的`chain`的抽象,合并最终的提示、大模型实例及OutPut Parse共同执行。
    API_KEY = "sk-4b79f3a3xxxa1935366ebb425b3"
    
    model = ChatOpenAI(model="deepseek-chat",
                       openai_api_key=API_KEY,
                       openai_api_base="https://api.deepseek.com")
    
    chain = prompt | model | output_parser
    output = chain.invoke({"question": "列出北京的三个景点"})
    output
    
    • LCEL: LangChain Execution Language(LangChain 表达语⾔)是⼀种声明性的⽅式来链接 LangChain 组件(工作流)。
  • 解析器作用在ChatPromptTemplate模版中

    from langchain_openai import ChatOpenAI
    from langchain.output_parsers import CommaSeparatedListOutputParser
    from langchain.prompts import ChatPromptTemplate
    
    #构建提示词模版
    prompt = ChatPromptTemplate.from_messages([
        ("system", "{parser_instructions}"),
        ("human", "列出{cityName}的{viewPointNum}个著名景点。")
    ])
    
    #构建输出解析器并获取解析格式
    output_parser = CommaSeparatedListOutputParser()
    parser_instructions = output_parser.get_format_instructions()
    
    #动态补充提示词内容
    final_prompt = prompt.invoke({"cityName": "南京", "viewPointNum": 3, 
                                  "parser_instructions": parser_instructions})
    
    #最后,使用LangChain中的`chain`的抽象,合并最终的提示、大模型实例及OutPut Parse共同执行。
    API_KEY = "sk-4b79f3axxx1935366ebb425b3"
    model = ChatOpenAI(model="deepseek-chat",
                       openai_api_key=API_KEY,
                       openai_api_base="https://api.deepseek.com")
    
    
    response = model.invoke(final_prompt)
    ret = output_parser.invoke(response)
    print(ret)
    

DatetimeOutputParser时间输出解析器示例

from langchain.output_parsers import DatetimeOutputParser#日期输出解析器
from langchain.prompts import PromptTemplate

#制定输出解析器
output_parser = DatetimeOutputParser()

#制定提示词模版
template = """回答用户的问题:
{question}

{format_instructions}"""

#时间解析器的解析格式
format_instructions = output_parser.get_format_instructions()

#补充提示词模版
prompt = PromptTemplate.from_template(
    template,
    partial_variables={"format_instructions":format_instructions}
)

API_KEY = "sk-4b79f3axxx1935366ebb425b3"
model = ChatOpenAI(model="deepseek-chat",
                   openai_api_key=API_KEY,
                   openai_api_base="https://api.deepseek.com")

chain = prompt | model | output_parser
output = chain.invoke("周杰伦是什么时候出道的?")
output

EnumOutputParser枚举输出解析器示例

from langchain.output_parsers.enum import EnumOutputParser
from enum import Enum

#定义枚举类型
class Colors(Enum):
    RED = "红色"
    BROWN = "棕色"
    BLACK = "黑色"
    WHITE = "白色"
    YELLOW = "黄色"
    
#制定输出解析器
parse = EnumOutputParser(enum=Colors)

#制定提示词模版
promptTemplate = PromptTemplate.from_template(
    """{person}的皮肤主要是什么颜色?
    
    {instructions}"""
)
#解析器的解析格式:原本解析器的英文解析格式会报错
# instructions = parse.get_format_instructions() 
instructions = "响应结果请选择以下选项之一:红色、棕色、黑色、白色和黄色。"
#提示词部分补充
prompt = promptTemplate.partial(instructions=instructions)

chain = prompt | model | parse
chain.invoke({"person":"亚洲人"})

注意:直接使用输出解析器原始的英文的解析格式作用到提示词中可能由于中英文掺杂和中英文语义的区别导致模型报错,因此,可以适当将输出解析器的解析格式手动翻译成英文后再用!

Pydantic JSON 输出解析器

JSON输出解析器允许用户指定任意JSON架构并查询LLMs以获取符合该框架的输出。

from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.pydantic_v1 import BaseModel,Field
from langchain.prompts import PromptTemplate
from typing import List

#定义JSON结构
class Book(BaseModel):
    title:str = Field(description="书名")
    author:str = Field(description="作者")
    description:str = Field(description="书的简介")
    beLike:List[str] = Field(description="相关书籍的名称")
    
query = "请给我介绍下中国历史的经典书籍"

parser = JsonOutputParser(pydantic_object=Book)

format_instructions = parser.get_format_instructions()
# format_instructions = '''输出应格式化为符合以下JSON模式的JSON实例。JSON结构如下:{"title":"标题","author":"作者","description":"书的简介"}'''
prompt = PromptTemplate(
    template="{format_instructions}\n{query}\n",
    input_variables=["query"],
    partial_variables={"format_instructions":format_instructions}
)

chain = prompt | model | parser
chain.invoke({"query":query})

xml输出解析器

from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import XMLOutputParser

API_KEY = "sk-4b79f3axxx935366ebb425b3"
model = ChatOpenAI(
    model_name="deepseek-chat",
    api_key=API_KEY,
    base_url="https://api.deepseek.com"
)

# 还有⼀个⽤于提示语⾔模型填充数据结构的查询意图。
actor_query = "⽣成周星驰的简化电影作品列表,按照最新的时间降序"

# 设置解析器 + 将指令注⼊提示模板。
parser = XMLOutputParser()
prompt = PromptTemplate(
    template="回答⽤户的查询。\n{format_instructions}\n{query}\n",
    input_variables=["query"],
    partial_variables={"format_instructions": parser.get_format_instructions()},
)
# print(parser.get_format_instructions())

chain = prompt | model
response = chain.invoke({"query": actor_query})
xml_output = parser.parse(response.content)
print(response.content)

自定义输出解析器

在某些情况下,我们可以实现自定义解析器以将模型输出内容构造成自定义的格式。

from typing import Iterator
from langchain_core.messages import AIMessage,AIMessageChunk

#自定义输出解析器
def parse(ai_message:AIMessage)->str:
    #函数参数就是模型的输出。
    #swapcase表示将模型输出内容大小写进行相互转换后进行返回
    return ai_message.content.swapcase()

chain = model | parse
response = chain.invoke("are you ok?")
response

记忆模块Memory

在最开始我们就通过实验知道LLM 本身是没有记忆的,每一次LLM的API调用都是一个全新的会话。但在某些应用程序中,如:聊天机器人,让LLM记住以前的历史交互是非常重要,无论是在短期的还是长期的。langchain中的“Memory”即对话历史(message history)就是为了实现这一点。

image-20250626081534642

在与大模型进行对话和交互的过程中,一个关键步骤是能够引用交互过程中先前的信息,至少需要能够直接回溯到过去某些对话的内容。对于复杂应用而言,所需的是一个能够不断自我更新的模型,以便执行如维护相关信息、实体及其关系等任务。这种存储并回溯过去交互信息的能力,就叫做“记忆(Memory)”。

Memory作为存储记忆数据的一个是抽象模块,其作为一个独立模块使用是没有任何意义的,因为本质上它的定位就是一个存储对话数据的空间。

LangChain Memory 的作用

  • 上下文管理:通过保存历史对话,模型可以基于之前的对话内容来生成更相关的响应。
  • 状态跟踪:对于需要持续跟踪用户状态的应用程序来说,Memory 可以帮助维护会话的状态信息。
  • 个性化体验:通过记录用户的偏好或历史选择,可以提供更加个性化的用户体验。

ChatMessageHistory-对话消息历史管理

在LangChain中,ChatMessageHistory通常是一个数据结构,用于存储和检索对话消息。这些消息可以按照时间顺序排列,以便在对话过程中引用和更新。

# 初始化大模型
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder

# 本地ollama拉取过什么模型就使用什么模型
API_KEY = "sk-4b79f3axxx35366ebb425b3"
llm = ChatOpenAI(model="deepseek-chat",
                   openai_api_key=API_KEY,
                   openai_api_base="https://api.deepseek.com")

# 聊天模型提示词
template = [
    MessagesPlaceholder(variable_name="history"),
]
prompt = ChatPromptTemplate.from_messages(messages=template)
chain = prompt | llm

# 记录会话历史
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.messages import SystemMessage

history = ChatMessageHistory()
history.messages = [SystemMessage("你是由John开发的智能助手机器人,叫多啦A梦,你每次都会精简而快速的告诉用户你是一个专业的机器人以及用户问题的答案。")]
history.add_user_message("我叫John,请你记住。")
history.add_user_message("我叫什么名字,以及你叫什么名字?")
res = chain.invoke({"history": history.messages})
history.add_ai_message(res)
print(res.content)

history.add_user_message("我现在改名了,叫Johnny,请问我是谁?")
res = chain.invoke({"history": history.messages})
history.add_ai_message(res)
print(res.content)
for message in history.messages:
    print("会话记录",message.content)

多个用户多轮对话

有了对话消息历史管理对象,不仅可以管理和存储单个用户和LLM的历史对话信息以此来维持会话状态,还可以实现管理多用户与LLM的独立历史对话信息。

# 初始化大模型
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder

# 本地ollama拉取过什么模型就使用什么模型
API_KEY = "sk-4b79fxxx935366ebb425b3"
llm = ChatOpenAI(model="deepseek-chat",
                   openai_api_key=API_KEY,
                   openai_api_base="https://api.deepseek.com")

# 聊天模型提示词
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
template = [
    ("system",
     "你叫多啦A梦,今年1岁了,是John开发的智能机器人,能精准回复用户的问题"),
    MessagesPlaceholder(variable_name="history"),
]
prompt = ChatPromptTemplate.from_messages(messages=template)
chain = prompt | llm

# 记录会话历史
from langchain_community.chat_message_histories import ChatMessageHistory

#session_id设置不同的消息集
john_history = ChatMessageHistory(session_id="John")
john_history.add_user_message('我叫John,今年100岁,很高兴和你聊天')
john_res = chain.invoke({"history": john_history.messages})
john_history.add_ai_message(john_res)
print(john_res.content)
print('=======================================')

Yuki_history = ChatMessageHistory(session_id="Yuki")
Yuki_history.add_user_message('你好呀,我的名字叫Yuki,我今年200岁。你叫什么?')
Yuki_res = chain.invoke({"history": Yuki_history.messages})
Yuki_history.add_ai_message(Yuki_res)
print(Yuki_res.content)
print('=======================================')

john_history.add_user_message("你还记得我的名字和年龄吗?")
john_res = chain.invoke({"history": john_history.messages})
john_history.add_ai_message(john_res)
print(john_res.content)
print('=======================================')

Yuki_history.add_user_message("你还记得我的名字和年龄吗?")
Yuki_res = chain.invoke({"history": Yuki_history.messages})
Yuki_history.add_ai_message(Yuki_res)
print(Yuki_res.content)
print('=======================================')

RunnableWithMessageHistory-可运行的消息历史记录对象

上面虽然使用了ChatMessageHistory保存对话历史数据,但是与Chains的操作是独立的,并且每次产生新的对话消息都要手动add添加记录,所以为了方便使用,langchain还提供了RunnableWithMessageHistory可以自动为Chains添加对话历史记录。

# 初始化大模型
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser

# 本地ollama拉取过什么模型就使用什么模型
API_KEY = "sk-4b79f3xxx1935366ebb425b3"
llm = ChatOpenAI(model="deepseek-chat",
                   openai_api_key=API_KEY,
                   openai_api_base="https://api.deepseek.com")

# 聊天模型提示词
template = [
    ("system",
     "你叫多啦A梦,今年1岁了,是John开发的智能机器人,能精准回复用户的问题"),
    MessagesPlaceholder(variable_name="history"),
    ("human", "{input}"),
]
prompt = ChatPromptTemplate.from_messages(messages=template)
chain = prompt | llm | StrOutputParser()

# 记录会话历史
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
# 用于记录不同的用户(session_id)对话历史
store = {}
def get_session_history(session_id):
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]


chains = RunnableWithMessageHistory(
    chain,
    get_session_history,
    input_messages_key="input",
    history_messages_key="history",
)

res1 = chains.invoke({"input": "什么是余弦相似度?"}, config={'configurable': {'session_id': 'john'}})
print(res1)
print('====================================================')
res2 = chains.invoke({"input": "再回答一次刚才的问题"}, config={'configurable': {'session_id': 'john'}})
print(res2)

ConversationChain中的记忆

ConversationChain提供了包含AI角色和人类角色的对话摘要格式,这个对话格式和记忆机制结合得非常紧密。ConversationChain实际上是对Memory和LLMChain进行了封装,简化了初始化Memory的步骤。

该方法已经在langchain1.0版本废除,使用RunnableWithMessageHistory对其进行替代!

# 初始化大模型
from langchain_openai import ChatOpenAI

# 本地ollama拉取过什么模型就使用什么模型
API_KEY = "sk-4b79f3a3xxx935366ebb425b3"
llm = ChatOpenAI(model="deepseek-chat",
                   openai_api_key=API_KEY,
                   openai_api_base="https://api.deepseek.com")

# 导入所需的库
from langchain.chains.conversation.base import ConversationChain
# 初始化对话链
conv_chain = ConversationChain(llm=llm)

# 打印对话的模板
print(conv_chain.prompt.template)

ConversationChain中的内置提示模板中的两个参数:

  • {history}:存储会话记忆的地方,也就是人类和人工智能之间对话历史的信息。

  • {input} :新输入的地方,可以把它看成是和ChatGPT对话时,文本框中的输入。

缓冲记忆:ConversationBufferMemory

在LangChain中,ConversationBufferMemory是一种非常简单的缓冲记忆,可以实现最简单的记忆机制,它只在缓冲区中保存聊天消息列表并将其传递到提示模板中。

通过记忆机制,LLM能够理解之前的对话内容。直接将存储的所有内容给LLM,因为大量信息意味着新输入中包含更多的Token,导致响应时间变慢和成本增加。此外,当达到LLM的Token数限制时,太长的对话无法被记住。

#用于创建对话链
from langchain.chains import ConversationChain
#用于存储对话历史,以便在后续对话中参考
from langchain.memory import ConversationBufferMemory

from langchain_openai import ChatOpenAI
import warnings
warnings.filterwarnings("ignore")

# 初始化大模型(需配置OPENAI_API_KEY)
API_KEY = "sk-4b79f3axxx935366ebb425b3"
llm = ChatOpenAI(model="deepseek-chat",
                   openai_api_key=API_KEY,
                   openai_api_base="https://api.deepseek.com")

#实例化一个对话缓冲区,用于存储对话历史
memory = ConversationBufferMemory()
#创建一个对话链,将大语言模型和对话缓冲区关联起来。
conversation = ConversationChain(
    llm=llm,
    memory=memory,
)

conversation.invoke("今天早上猪八戒吃了2个人参果。")
print("记忆1: ", conversation.memory.buffer)
print()

conversation.invoke("下午猪八戒吃了1个人参果。")
print("记忆2: ", conversation.memory.buffer)
print()

conversation.invoke("晚上猪八戒吃了3个人参果。")
print("记忆3: ", conversation.memory.buffer)
print()

conversation.invoke("猪八戒今天一共吃了几个人参果?")
print("记忆4: ", conversation.memory.buffer)

功能设计:多轮对话

from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory
from langchain_openai import ChatOpenAI
import warnings
warnings.filterwarnings("ignore")

# 实例化一个对话缓冲区,用于存储对话历史
memory = ConversationBufferMemory()
# 创建一个对话链,将大语言模型和对话缓冲区关联起来。
conversation = ConversationChain(
    llm=llm,
    memory=memory,
)

print("欢迎使用对话系统!输入 '退出' 结束对话。")

while True:
    user_input = input("你: ")
    if user_input.lower() in ['退出', 'exit', 'quit']:
        print("再见!")
        break
    response = conversation.predict(input=user_input)
    print(f"AI: {response}")

# 打印出对话历史,即 memory.buffer 的内容
print("对话历史:", memory.buffer)

携带提示词模版的对轮对话(LLMChain对话链)

from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
from langchain.memory import ConversationBufferMemory
from langchain_openai import ChatOpenAI
import os
import warnings
warnings.filterwarnings("ignore")

# 初始化大模型
API_KEY = "sk-4b79f3a3fxxx1935366ebb425b3"
llm = ChatOpenAI(
    model="deepseek-chat",
    openai_api_key=API_KEY,
    openai_api_base="https://api.deepseek.com"
)

# 实例化一个对话缓冲区,用于存储对话历史
memory = ConversationBufferMemory()

# 定义提示词模板
template = """{history}
用户: {input}
AI:"""

prompt_template = PromptTemplate(
    input_variables=["history", "input"],
    template=template
)

# 创建一个包含提示词模板的对话链
conversation = LLMChain(
    llm=llm,
    prompt=prompt_template,
    verbose=True,  # 如果需要调试,可以设置为 True
    memory=memory
)

print("欢迎使用对话系统!输入 '退出' 结束对话。")

while True:
    user_input = input("你: ")
    if user_input.lower() in ['退出', 'exit', 'quit']:
        print("再见!")
        break
    try:
        # 调用对话链获取响应
        response = conversation.run(input=user_input)
        print(f"AI: {response}")
    except Exception as e:
        print(f"发生错误: {e}")

# 打印出对话历史,即 memory.buffer 的内容
print("对话历史:", memory.buffer)

如果使用聊天模型,使用结构化的聊天消息可能会有更好的性能:

from langchain_openai import ChatOpenAI
from langchain.memory import ConversationBufferMemory
from langchain.chains.llm import LLMChain
from langchain_core.messages import SystemMessage
from langchain_core.prompts import MessagesPlaceholder, HumanMessagePromptTemplate, ChatPromptTemplate
import warnings
warnings.filterwarnings("ignore")

# 初始化大模型
API_KEY = "sk-4b79f3a3xxxa1935366ebb425b3"
llm = ChatOpenAI(
    model="deepseek-chat",
    openai_api_key=API_KEY,
    openai_api_base="https://api.deepseek.com"
)

# 使用ChatPromptTemplate设置聊天提示
prompt = ChatPromptTemplate.from_messages(
    [
        SystemMessage(content="你是一个与人类对话的机器人。"),
        MessagesPlaceholder(variable_name="chat_history"),
        HumanMessagePromptTemplate.from_template("{question}"),
    ]
)

# 创建ConversationBufferMemory
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)

# 初始化链
chain = LLMChain(llm=llm,  prompt=prompt, memory=memory)

# 提问
res = chain.invoke({"question": "你是LangChain专家"})
print(str(res) + "\n")    

res = chain.invoke({"question": "你是谁?"})
print(res)

多轮对话Token限制解决

在了解了ConversationBufferMemory记忆类后,我们知道了它能够无限的将历史对话信息填充到History中,从而给大模型提供上下文的背景。但问题是:每个大模型都存在最大输入的Token限制,且过久远的对话数据往往并不能够对当前轮次的问答提供有效的信息,这种我们大家都能非常容易想到的问题,LangChain的开发人员自然也能想到,那么他们给出的解决方式是:ConversationBufferWindowMemory模块。该记忆类会保存一段时间内对话交互的列表,仅使用最后 K 个交互。所以它可以保存最近交互的滑动窗口,避免缓存区不会变得太大。

from langchain.memory import ConversationBufferWindowMemory
import warnings
warnings.filterwarnings("ignore")

#实例化一个对话缓冲区,用于存储对话历史
    #k=1,所以在读取时仅能提取到最近一轮的记忆信息
    #return_messages=True参数,将对话转化为消息列表形式
memory = ConversationBufferWindowMemory(k=1, return_messages=True)

conversation = ConversationChain(
    llm=llm,
    memory=memory,
)

# 示例对话
response1 = conversation.predict(input="你好")
response2 = conversation.predict(input="你在哪里?")
print("对话历史:", memory.buffer)

实体记忆:ConversationEntityMemory

在LangChain 中,ConversationEntityMemory是实体记忆,它可以跟踪对话中提到的实体,在对话中记住关于特定实体的给定事实。它提取关于实体的信息(使用LLM),并随着时间的推移建立对该实体的知识(使用LLM)。

使用它来存储和查询对话中引用的各种信息,比如人物、地点、事件等。

from langchain.chains.conversation.base import ConversationChain
from langchain.memory import ConversationEntityMemory
from langchain.memory.prompt import ENTITY_MEMORY_CONVERSATION_TEMPLATE
from langchain_openai import OpenAI
import warnings
warnings.filterwarnings("ignore")

# 初始化大模型
API_KEY = "sk-4b79f3a3xxx1935366ebb425b3"
llm = ChatOpenAI(
    model="deepseek-chat",
    openai_api_key=API_KEY,
    openai_api_base="https://api.deepseek.com"
)


conversation = ConversationChain(
    llm=llm,
    prompt=ENTITY_MEMORY_CONVERSATION_TEMPLATE,
    memory=ConversationEntityMemory(llm=llm)
)

# 开始对话
conversation.predict(input="你好,我是小明。我最近在学习 LangChain。")
conversation.predict(input="我最喜欢的编程语言是 Python。")
conversation.predict(input="我住在北京。")

# 查询对话中提到的实体
res = conversation.memory.entity_store.store
print(res)

Agent开发

1 AI Agent 概念与架构

  • Agent定义

咱们先来说说什么是Agent哈。简单来说,Agent就是一种能够自己做出决定、采取行动去达到某个目标的东西。它可以是一个软件程序,也可以是一个实体机器人啥的。反正就是能自己主动去干事儿的那种。

那AI Agent又是什么呢?它呀,是基于人工智能技术,特别是大模型技术造出来的智能实体。这个智能实体可不简单,它能感知周围的环境,理解各种信息,然后根据这些信息做出行动,目的就是为了完成某个特定的目标。比如说,它可以帮你自动回复邮件,或者从一堆数据里找出你想要的信息。

  • Agents 利用 LLM 作为推理引擎

接下来咱们聊聊Agents是怎么工作的。它们有个很厉害的本事,就是利用那种叫LLM(大语言模型)的东西作为推理引擎。这玩意儿可聪明了,能把你输入的自然语言,就像咱们平时说话那样的句子,转化成一系列的工具调用指令。然后呢,它还能协调这些工具一起工作,把任务给完成了。比如说,你想查点东西,你就告诉它,它就知道你要用哪个搜索引擎,怎么搜,最后把结果给你找出来。

这里面的核心思想就是让LLM自己来决定该先做哪个动作,该选哪个工具,而不是像以前那样,什么都得事先写好代码,让它按部就班地执行。这样多灵活啊,对吧?

  • Agent模块在Langchain框架中的角色

再讲讲Agent模块在Langchain框架里是干啥的。Langchain是个很有名的框架,专门用来构建基于语言的应用。在这个框架里,Agent模块可是个重要角色。它负责实现那些智能代理的功能,就是让计算机能像人一样思考和行动。它怎么做到的呢?通过预设一些规则和算法,然后自动去执行特定的任务。比如说,你可以设定一些规则,让它在收到邮件时自动回复,或者在特定时间提醒你做某件事。

  • Agent 模块的特点

最后说说Agent模块的特点吧。它有两个特别突出的地方:智能化和自动化。它能根据预设的规则和算法自己做出决策,然后去执行任务。这样一来,工作效率就高多了,而且准确性也更好。比如说,在处理大量数据时,它能快速准确地找出你需要的信息,比你自己去一个个看快多了。

  • langchain.agents模块

langchain.agents模块是LangChain框架中的核心组件之一,主要用于构建能够自主决策和执行复杂任务的智能代理(Agent)。通常情况下,我们会基于agents模块下的create_xml_agentcreate_react_agenttool进行不同智能体的构建。

2 create_xml_agent构建智能体

在LangChain框架中,create_xml_agent函数主要用于创建一个能够处理XML格式数据交互的代理。它结合了语言模型(LLM)和其他工具,使得代理可以根据输入的指令和上下文信息,以XML格式进行思考、规划和与工具的交互,最终生成符合要求的输出

```create_xml_agent`的核心目标是创建一个能够处理XML格式数据的代理。这意味着代理的输入、输出以及中间的数据交互都基于XML格式,适合与返回XML响应的工具或服务进行交互(但是并不绝对!)``

LangChain框架本身具有高度的灵活性和可扩展性,create_xml_agent也不例外。它可以与其他工具和组件进行集成,根据具体的应用场景和需求,进一步扩展智能体的功能。例如,可以结合搜索引擎工具获取外部信息,再通过XML代理对获取的信息进行整理和分析,最终生成符合要求的输出。

示例操作:让智能体自动调用工具查找数据

为了更好的理解Agent框架,让我们构建一个具有两个工具的Agent:一个用于在线查找内容,另一个用于查找指定城市的气象数据。

SerpAPI是一个搜索引擎结果页面API,它允许开发者和研究人员通过编程方式获取Google、Bing、Yahoo和其他搜索引擎的搜素结果。使用SerpAPI,用户可以避免直接与搜索引擎进行交互(无需xx上网),从而避免了可能遇到的各种问题,例如:用户代理、请求限制等问题。

环境安装:pip install google-search-results

官网进行API KEY的申请:https://serpapi.com/

  • 测试搜索效果

    from langchain_community.utilities import SerpAPIWrapper
    serpapi_api_key = "60f286e601f4xxxc65e7a9b3ceb06a3f0dc8e0fe7ce56ec93d6274ccd"
    search = SerpAPIWrapper(serpapi_api_key=serpapi_api_key)
    search.run("周杰伦演唱会最新信息")
    
  • 2个外部函数构建

    def get_search_result(question):
        """
        互联网搜索函数
        :param question: 必要参数,字符串类型,用于表示在互联网上进行搜素的关键词或者搜索内容的简短描述,\
        :return:SerpAPI API根据参数question进行互联网搜索后的结果,其中包含了全部重要的搜索结果内容。
        """
        from langchain_community.utilities import SerpAPIWrapper
        serpapi_api_key = "60f286e601f44a26600e42cxxxa3f0dc8e0fe7ce56ec93d6274ccd"
        search = SerpAPIWrapper(serpapi_api_key=serpapi_api_key)
        result = search.run(question)
        return result
    
    import requests
    import json
    def get_weather(loc):
        """
        查询即时天气函数
        :param loc: 必要参数,字符串类型,用于表示查询天气的具体城市名称,\
        :return:查询即时天气的结果\
        返回结果对象类型为解析之后的JSON格式对象,并用字符串形式进行表示,其中包含了全部重要的天气信息
        """
        api_key = "SGkvDR94bWqZfdosf"
        url = f"https://api.seniverse.com/v3/weather/now.json?key={api_key}&location={loc}&language=zh-Hans&unit=c"
        response = requests.get(url)
        data = response.json()
        return json.dumps(data)
    
    get_weather("上海")
    
  • 将外部函数封装成Agent可调用的工具对象

    from langchain.agents import Tool
    searchTool = Tool(
        name = "get_search_result",
        description = "互联网搜索函数",
        func = get_search_result, 
    )
    
    from langchain.agents import Tool
    weatherTool = Tool(
        name = "get_weather",
        description = "查询指定城市的即时天气信息",
        func = get_weather, 
    )
    
  • 定义Agent的工具列表

    tools = [weatherTool,searchTool]
    
  • 定义提示词模版

    from langchain import hub
    prompt = hub.pull("hwchase17/xml-agent-convo")
    prompt.messages
    
  • 创建大模型

    from langchain_openai import ChatOpenAI
    API_KEY = "sk-4b79f3a3fxxx35366ebb425b3"
    llm = ChatOpenAI(model_name="deepseek-reasoner",
                      api_key=API_KEY,base_url="https://api.deepseek.com")
    
  • 创建智能体

    from langchain.agents import create_xml_agent
    agent = create_xml_agent(llm,tools,prompt)
    
  • 执行智能体

    from langchain.agents import AgentExecutor
    agent_executor = AgentExecutor(
        agent = agent,
        tools = tools,
        verbose = True
    )
    agent_executor.invoke({'input':"张杰演唱会"})
    agent_executor.invoke({'input':"请帮我查询ShangHai天气"})
    

3 create_sql_agent构建智能体

在LangChain中,create_sql_agent是一个用于创建能够与SQL数据库进行交互的代理(Agent)的函数。

create_sql_agent的主要作用是创建一个基于语言模型(LLM)的代理,该代理能够:

  • 解析自然语言问题:将用户输入的自然语言问题转换为可执行的SQL查询。
  • 执行SQL查询:与SQL数据库交互,执行生成的SQL语句。
  • 返回查询结果:将查询结果以用户友好的方式返回。

通过`create_sql_agent`,用户可以使用自然语言与数据库进行交互,而无需编写复杂的SQL语句。

create_sql_agent的特点

  • 自然语言处理:利用语言模型(如OpenAI的GPT)理解用户的自然语言输入,并将其转换为SQL查询。
  • 动态SQL生成:根据用户的问题动态生成SQL语句,支持复杂的查询逻辑。
  • 错误处理:如果生成的SQL语句有误,代理会尝试修正或重新生成查询。
  • 灵活性:可以与任何SQLAlchemy支持的SQL数据库(如MySQL、PostgreSQL、SQLite等)进行交互。
  • 模块化:通过工具集扩展功能,例如添加自定义工具或集成其他API。

create_sql_agent适用于以下场景:

  • 数据分析和报告:用户可以通过自然语言查询数据库,生成分析报告或提取数据。
  • 业务决策支持:企业可以利用代理快速从数据库中提取关键信息,辅助决策。
  • 自动化任务:将自然语言查询与数据库操作结合,实现自动化流程。
  • 聊天机器人:构建能够回答数据库相关问题的智能聊天机器人。
  • 个人数据管理:个人用户可以通过自然语言查询自己的数据库(如财务数据、健康数据等)。
# 安装必要的依赖包
# pip install sqlalchemy 
# pip install pymysql
from langchain.agents import create_sql_agent, AgentExecutor, AgentType
from langchain.agents.agent_toolkits import SQLDatabaseToolkit
from langchain.llms import OpenAI
from langchain.sql_database import SQLDatabase
import os


# 配置数据库连接
db_user = "root"
db_password = "boboadmin"
db_host = "localhost"
db_name = "db001"
db = SQLDatabase.from_uri(f"mysql+pymysql://{db_user}:{db_password}@{db_host}/{db_name}")

# 初始化语言模型(LLM)
API_KEY = "sk-4b79f3a3ffxxx5366ebb425b3"
llm = ChatOpenAI(
    model="deepseek-chat",
    openai_api_key=API_KEY,
    openai_api_base="https://api.deepseek.com"
)

# 初始化工具集(Toolkit)
toolkit = SQLDatabaseToolkit(db=db, llm=llm)

# 创建 SQL 代理
agent_executor = create_sql_agent(
    llm=llm,
    toolkit=toolkit,
    agent_type=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
)

# 定义自然语言问题
# question = "当前数据库中有几张表?这些表之间有什么关联或者联系吗?"
# question = "查询LC表中男女用户数量分别是多少"
question = "不同薪资等级对应的员工数量分别是多少?"

# 运行代理并获取结果
result = agent_executor.run(question)
print("查询结果:", result)
  • 问题:create_sql_agent是如何理解mysql数据库库表的详细信息的?

    • create_sql_agent 依赖于工具集(Toolkit)来与数据库交互。工具集中包含了用于查询数据库元数据的工具,例如:
      • 获取表信息:通过 SQL 语句 SHOW TABLES; 获取数据库中的所有表名。
      • 获取列信息:通过 SQL 语句 DESCRIBE table_name;SHOW COLUMNS FROM table_name; 获取表的列名、数据类型等信息。
  • create_sql_agent 内部封装的提示词是什么内容?

    • create_sql_agent 是 LangChain 中用于创建 SQL 代理的函数,其内部封装的提示词(Prompt)通常是预定义的,用于指导语言模型(LLM)如何生成 SQL 查询。这些提示词是 LangChain 库的一部分,通常不会直接暴露给用户,但可以通过查看源码或文档来了解其内容。以下是 create_sql_agent 内部可能使用的提示词的示例,以及对其的翻译和解释:

      You are a helpful assistant that translates natural language questions into SQL queries. Here is the schema of the database:
      {schema}
      
      Given the question: "{question}", generate a valid SQL query to answer it. Make sure the query is correct and efficient.
      
    • 第一句

      • 原文You are a helpful assistant that translates natural language questions into SQL queries.
      • 翻译:你是一个将自然语言问题翻译成 SQL 查询的助手。
      • 解释:这句话明确了角色——语言模型的任务是将用户的自然语言问题转换为 SQL 查询。
    • 第二句

      • 原文Here is the schema of the database: {schema}
      • 翻译:这是数据库的架构:{schema}。
      • 解释{schema} 是数据库的表结构信息(如表名、列名、数据类型等),语言模型需要根据这些信息生成有效的 SQL 查询。
    • 第三句

      • 原文Given the question: "{question}", generate a valid SQL query to answer it.
      • 翻译:给定问题:“{question}”,生成一个有效的 SQL 查询来回答它。
      • 解释{question} 是用户输入的自然语言问题,语言模型需要根据这个问题和数据库架构生成 SQL 查询。
    • 第四句

      • 原文Make sure the query is correct and efficient.
      • 翻译:确保查询是正确的且高效的。
      • 解释:语言模型需要生成语法正确且性能良好的 SQL 查询。

4 create_react_agent构建智能体

对于一些复杂的任务,在langchain的agents模块下提供了create_react_agent可以构建用于处理复杂任务的智能体对象。大家思考下,复杂任务如何定义?

所谓的复杂任务就是需要进行多步推理和多种工具协作才可以解决的问题。

例如:

1.旅行规划与预订

  • 任务描述:用户希望规划一次旅行,包括目的地天气查询、机票/酒店比价、行程安排等。
  • 多步推理与工具协作:
    1. 天气查询工具:调用天气API获取目的地未来几天的天气数据。
    2. 航班/酒店比价工具:根据用户预算和时间,搜索并比较不同平台的机票和酒店价格。
    3. 行程生成工具:结合天气、交通、用户偏好(如景点、餐饮)生成合理行程。
    4. 预订工具:自动完成机票、酒店的预订操作。

2.电商购物决策支持

  • 任务描述:用户输入商品需求下单最合适的商品。
  • 多步推理与工具协作:
    1. 商品搜索工具:调用电商平台API,按关键词筛选商品。
    2. 评测分析工具:抓取社交媒体和专业网站的用户评测,分析优缺点。
    3. 价格对比工具:跨平台比较历史价格和促销活动。
    4. 下单工具:自动选择最优商品并完成支付流程。

核心功能与工作流程

create_react_agent生成的代理遵循“思考→行动→观察”的循环流程,具体如下:

  1. 思考(Reason):LLM基于用户输入和上下文生成推理步骤,决定是否需要调用工具、选择哪个工具,并生成工具调用的参数。
  2. 行动(Action):执行工具调用(如调用搜索引擎、数据库查询),或直接生成自然语言回复。
  3. 观察(Observe):获取工具执行结果或用户反馈,更新上下文并传递给LLM进行下一步推理。

示例操作

  • 2个外部函数构建

    def get_search_result(question):
        """
        互联网搜索函数
        :param question: 必要参数,字符串类型,用于表示在互联网上进行搜素的关键词或者搜索内容的简短描述,\
        :return:SerpAPI API根据参数question进行互联网搜索后的结果,其中包含了全部重要的搜索结果内容。
        """
        from langchain_community.utilities import SerpAPIWrapper
        serpapi_api_key = "60f286e601f44a26600e4xxxb06a3f0dc8e0fe7ce56ec93d6274ccd"
        search = SerpAPIWrapper(serpapi_api_key=serpapi_api_key)
        result = search.run(question)
        return result
    
    import requests
    import json
    def get_weather(loc):
        """
        查询即时天气函数
        :param loc: 必要参数,字符串类型,用于表示查询天气的具体城市名称,\
        :return:查询即时天气的结果\
        返回结果对象类型为解析之后的JSON格式对象,并用字符串形式进行表示,其中包含了全部重要的天气信息
        """
        api_key = "SGkvDRxxxfdosf"
        url = f"https://api.seniverse.com/v3/weather/now.json?key={api_key}&location={loc}&language=zh-Hans&unit=c"
        response = requests.get(url)
        data = response.json()
        return json.dumps(data)
    
    get_weather("上海")
    
  • 将外部函数封装成Agent可调用的工具对象

    from langchain.agents import Tool
    searchTool = Tool(
        name = "get_search_result",
        description = "互联网搜索函数",
        func = get_search_result, 
    )
    
    from langchain.agents import Tool
    weatherTool = Tool(
        name = "get_weather",
        description = "查询指定城市的即时天气信息",
        func = get_weather, 
    )
    
  • 定义Agent的工具列表

    tools = [weatherTool,searchTool]
    
  • 定义提示词模版

    from langchain import hub
    prompt_react = hub.pull("hwchase17/react")
    prompt_react
    
  • 创建大模型

    from langchain_openai import ChatOpenAI
    API_KEY = "sk-4b79f3a3fxxx1935366ebb425b3"
    llm = ChatOpenAI(model_name="deepseek-reasoner",
                      api_key=API_KEY,base_url="https://api.deepseek.com")
    
  • 创建智能体

    from langchain.agents import create_react_agent
    agent = create_react_agent(llm,tools,prompt_react)
    
  • 执行智能体

    from langchain.agents import AgentExecutor
    agent_executor = AgentExecutor(
        agent = agent,
        tools = tools,
        verbose = True
    )
    agent_executor.invoke({"input":"上海今天有雪吗?如果不下雪的话我想查询下近期上海是否有周杰伦演唱会举办?"})
    

langgraph

LangGraph 是LangChain生态系统中的一个框架,用于构建基于大型语言模型(LLM)的复杂工作流和智能体系统。它通过有向图结构定义工作流程,使开发者能够创建动态、可控且可扩展的AI应用程序。使用langchain需要pip install langgraph

  • 核心概念

    • 状态(State):是LangGraph应用的基础,包含了应用运行时的所有信息,如消息列表、当前输入、工具输出等。
    • 节点(Node):通常是Python函数,代表不同的操作或步骤,如调用LLM、处理用户输入等,用于处理状态并返回更新后的状态。
    • 边(Edge):定义了节点之间的连接关系和路由逻辑,包括标准边和条件边,标准边定义固定的执行路径,条件边可根据状态决定下一步走向。
    Snipaste_2025-05-23_09-56-41
  • 主要特性

    • 结构化工作流:能创建具有分支、循环和条件逻辑的复杂工作流,相比单一的链式调用更具灵活性。
    • 状态管理:提供强大的状态管理机制,自动保存和管理状态,支持暂停和恢复执行,便于处理长时间运行的对话。
    • 与LangChain无缝集成:可复用现有的LangChain组件,还有丰富的工具和模型支持。
    • 实现复杂逻辑:传统的智能体开发方式在处理复杂任务时存在局限,如缺乏对外部环境的感知能力、对话历史记忆有限等。LangGraph允许创建具有循环、条件分支等复杂逻辑的工作流,能更好地应对各种复杂场景和需求,例如根据不同的输入和状态动态调整执行路径,实现多步骤的推理和决策。

1 langgraph实现Agent基础操作

from typing import Literal
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END,StateGraph,MessagesState
from langgraph.prebuilt import ToolNode


#定义工具函数,用于Agent调用外部工具
@tool
def search(query:str):
    """模拟一个气象查询搜索工具"""
    if "北京" in query.lower() or "Beijing" in query.lower():
        return "阴天有雾,气温25度"
    return "天气晴朗温度较高39度"

#将工具函数存放在工具列表中
tools = [search]

创建工具集节点:ToolNode是LangGraph中的一个预构建节点,用于封装一组工具函数。这些工具函数可以通过模型调用来执行特定的任务。

tool_node = ToolNode(tools)

定义模型对象

#定义模型对象
API_KEY = "sk-4b79f3a3xxxx1935366ebb425b3"
model = ChatOpenAI(model_name="deepseek-chat",
                  api_key=API_KEY,base_url="https://api.deepseek.com")
#将工具列表绑定到模型对象上
model = model.bind_tools(tools)

定义路由函数/状态转换函数:should_continue函数用于决定当前状态之后应该转移到哪个节点。它接收一个MessagesState对象作为输入,并返回一个字符串,表示下一个节点的名称。

消息状态:MessagesState是LangGraph中的一个状态类,用于存储对话过程中的消息列表。每个状态对象都包含一个messages字段,该字段是一个消息对象的列表。

from typing import Literal 是 Python 3.8 及以上版本中引入的一种类型注解工具,用于表示某个变量或函数参数只能是特定的几个值之一。Literal 是 typing 模块中的一个特殊类型,它允许你精确地指定一个或多个字面量作为类型约束。

def should_continue(state:MessagesState)->Literal["tools",END]:
    messages = state['messages']
    #获取用户提问消息
    last_message = messages[-1] 
    
    #如果llm调用工具,则转到tools节点
    if last_message.tool_calls:
        return "tools"
    return END

定义模型调用函数

def call_model(state:MessagesState):
    #获取消息列表
    messages = state['messages']
    #调用模型返回结果
    response = model.invoke(messages)
    return {"messages":[response]}

定义一个新的状态图,使用MessagesState作为状态类型

workflow = StateGraph(MessagesState)

在状态图上添加节点

workflow.add_node("agent",call_model)
workflow.add_node("tools",tool_node)

设置入口节点为agent(入口节点指向agent节点),这意味着agent是第一个被调用的节点

workflow.set_entry_point("agent")

添加条件边:agent节点根据should_continue进行边的连接(虚线边)

workflow.add_conditional_edges('agent',should_continue)

定义普通边:tools工具节点连接agent节点的边(实线边)

workflow.add_edge("tools","agent")

初始化内存以在图运行之间持久化状态:MemorySaver是LangGraph中的一个检查点保存器,用于在内存中保存状态图的中间状态。这对于调试和监控非常有用,因为它允许你在运行时查看和恢复状态。

checkpointer = MemorySaver()

编译图:将其编译成一个langchain可运行的一个对象,在编译时传递内存

app = workflow.compile(checkpointer=checkpointer) 

执行图

final_state = app.invoke(
    {"messages":[HumanMessage(content="北京天气如何?")]},
    config={"configurable":{"thread_id":42}}
)
result = final_state['messages'][-1].content
result

配置选项(config)实现上下文共享:如果两个任务在同一个线程上执行,它们可以共享同一个上下文(例如全局变量、线程本地存储等)。这对于需要维护状态或会话信息的应用非常重要。

final_state = app.invoke(
    {"messages":[HumanMessage(content="我刚才问的是哪个城市?")]},
    config={"configurable":{"thread_id":42}}
)
result = final_state['messages'][-1].content
result

保存图文件

graph_png = app.get_graph().draw_mermaid_png()
with open('graph.png','wb') as fp:
    fp.write(graph_png)

2 langgraph实现Multi-Agent Systems

image-20250627190635391
from langchain_core.messages import (
    BaseMessage,
    HumanMessage,
    ToolMessage,
)
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

# 导⼊聊天提示模板和消息占位符
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
# 导⼊状态图相关的常量和类
from langgraph.graph import END, StateGraph, START

API_KEY = "sk-4b79f3a3xxx935366ebb425b3"
llm = ChatOpenAI(model_name="deepseek-chat",
                 api_key=API_KEY,base_url="https://api.deepseek.com")

# 定义⼀个函数,⽤于创建代理
def create_agent(llm, tools, system_message: str):
     """创建⼀个代理。"""
     # 创建⼀个聊天提示模板
     prompt = ChatPromptTemplate.from_messages(
         [
             (
             "system",
             "你是⼀个有帮助的AI助⼿,与其他助⼿合作。"
             " 使⽤提供的⼯具来推进问题的回答。"
             " 如果你不能完全回答,没关系,另⼀个拥有不同⼯具的助⼿"
             " 会接着你的位置继续帮助。执⾏你能做的以取得进展。"
             " 如果你或其他助⼿有最终答案或交付物,"
             " 在你的回答前加上FINAL ANSWER,以便团队知道停⽌。"
             " 你可以使⽤以下⼯具: {tool_names}。\n{system_message}",
             ),
             # 消息占位符
             MessagesPlaceholder(variable_name="messages"),
         ]
     )
     # 传递系统消息参数
     prompt = prompt.partial(system_message=system_message)
     # 传递⼯具名称参数
     prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
     # 绑定⼯具并返回提示模板
     return prompt | llm.bind_tools(tools)
    
#定义工具函数
@tool
def get_search_result(question):
    """
    互联网搜索函数
    :param question: 必要参数,字符串类型,用于表示在互联网上进行搜素的关键词或者搜索内容的简短描述,\
    :return:SerpAPI API根据参数question进行互联网搜索后的结果,其中包含了全部重要的搜索结果内容。
    """
    from langchain_community.utilities import SerpAPIWrapper
    serpapi_api_key = "60f286exxx42c65e7a9b3ceb06a3f0dc8e0fe7ce56ec93d6274ccd"
    search = SerpAPIWrapper(serpapi_api_key=serpapi_api_key)
    result = search.run(question)
    return result

#定义工具函数,用于Agent调用外部工具
@tool
def send_email(query:str):
    """邮件发送工具,可以接受query内容,然后进行邮件发送"""
    return "邮件已成功发送。"

#定义工具节点
# 导⼊预构建的⼯具节点
from langgraph.prebuilt import ToolNode
# 定义⼯具列表
tools = [get_search_result, send_email]
# 创建⼯具节点
tool_node = ToolNode(tools)

#定义状态:我们⾸先定义图的状态。这只是⼀个消息列表,以及⼀个⽤于跟踪最新发送者的键
# 导⼊操作符和类型注解
import operator
from typing import Annotated, Sequence, TypedDict
# 导⼊OpenAI聊天模型
from langchain_openai import ChatOpenAI
# 定义⼀个对象,⽤于在图的每个节点之间传递
# 我们将为每个代理和⼯具创建不同的节点
class AgentState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], operator.add]
    sender: str
    
#定义代理节点
import functools
from langchain_core.messages import AIMessage
# 辅助函数,⽤于为给定的Agent创建节点
def agent_node(state, agent, name):
    # 调⽤代理
    result = agent.invoke(state)
    # 将代理输出转换为适合附加到全局状态的格式
    if isinstance(result, ToolMessage):
        pass
    else:
        result = AIMessage(**result.model_dump(exclude={"type", "name"}), name=name)
        return {
            "messages": [result],
            # 由于我们有⼀个严格的⼯作流程,我们可以跟踪发送者,以便知道下⼀个传递给谁。
            "sender": name,
        }
        
#创建搜索Agent代理对象和节点对象
research_agent = create_agent(
    llm,
    [get_search_result],
    system_message="你应该提供准确的数据供MailOpt使⽤。",
)
# 创建Agent节点对象
research_node = functools.partial(agent_node, agent=research_agent, name="Researcher")


#创建发邮件Agent代理对象和节点对象
mail_agent = create_agent(
    llm,
    [send_email],
    system_message="你用于进行邮件发送业务实现",
)
# 创建Agent节点对象
mail_node = functools.partial(agent_node, agent=mail_agent, name="MailOpt")


#定义路由函数,决定是否继续执行
from typing import Literal
# 定义路由器函数,continue 表示代理应该继续处理消息队列中的下一条消息。
def router(state) -> Literal["call_tool", "__end__", "continue"]:
    # 这是路由器
    messages = state["messages"]
    last_message = messages[-1]
    if last_message.tool_calls:
        # 上⼀个代理正在调⽤⼯具
        return "call_tool"
    if "FINAL ANSWER" in last_message.content:
        # 任何代理决定⼯作完成
        return "__end__"
    return "continue"

#图、节点和边的创建
# 创建状态图实例
workflow = StateGraph(AgentState)
# 添加搜索节点
workflow.add_node("Researcher", research_node)
# 添加邮件节点
workflow.add_node("MailOpt", mail_node)
# 添加⼯具调⽤节点
workflow.add_node("call_tool", tool_node)

# 添加条件边
workflow.add_conditional_edges(
     "Researcher",
     router,
     {"continue": "MailOpt", "call_tool": "call_tool", "__end__": END},
)
workflow.add_conditional_edges(
     "MailOpt",
     router,
     {"continue": "Researcher", "call_tool": "call_tool", "__end__": END},
)
# 添加条件边
workflow.add_conditional_edges(
     "call_tool",
     # 每个代理节点更新'sender'字段
     # ⼯具调⽤节点不更新,这意味着
     # 该边将路由回调⽤⼯具的原始代理
     lambda x: x["sender"],
     {
         "Researcher": "Researcher",
         "MailOpt": "MailOpt",
     },
)
# 添加起始边
workflow.add_edge(START, "Researcher")
# 编译⼯作流图
graph = workflow.compile()

# 将⽣成的图⽚保存到⽂件
graph_png = graph.get_graph().draw_mermaid_png()
with open("collaboration.png", "wb") as f:
    f.write(graph_png)
    
#调用
events = graph.invoke(
    {
        "messages": [
            HumanMessage(
            content="获取过去5年AI软件市场规模,归纳成100字"
            " 然后进行邮件发送。"
            " ⼀旦发送完邮件表示你完成了任务。"
            )
        ],
    }
)

#获取最终结果
result = events['messages'][-1].content
result

#查看中间结果
for message in events['messages']:
    print(message.content)
    print("-----------------------------------")

day24langgraph+RAG理论

文件: day24langgraph+RAG理论\langchain高级 - 副本.md

6. langgraph(尽量进行深度了解)

LangGraph 是LangChain生态系统中的一个框架,用于构建基于大型语言模型(LLM)的复杂工作流和智能体系统。它通过有向图结构定义工作流程,使开发者能够创建动态、可控且可扩展的AI应用程序。使用LangGraph 需要pip install langgraph

  • 核心概念

    • 状态(State):是LangGraph应用的基础,包含了应用运行时的所有信息,如消息列表、当前输入、工具输出等。
    • 节点(Node):通常是Python函数,代表不同的操作或步骤,如调用LLM、处理用户输入等,用于处理状态并返回更新后的状态。
    • 边(Edge):定义了节点之间的连接关系和路由逻辑,包括标准边和条件边,标准边定义固定的执行路径,条件边可根据状态决定下一步走向。
    Snipaste_2025-05-23_09-56-41
  • 主要特性

    • 结构化工作流:能创建具有分支、循环和条件逻辑的复杂工作流,相比单一的链式调用更具灵活性。
    • 状态管理:提供强大的状态管理机制,自动保存和管理状态,支持暂停和恢复执行,便于处理长时间运行的对话。
    • 与LangChain无缝集成:可复用现有的LangChain组件,还有丰富的工具和模型支持。
    • 实现复杂逻辑:传统的智能体开发方式在处理复杂任务时存在局限,如缺乏对外部环境的感知能力、对话历史记忆有限等。LangGraph允许创建具有循环、条件分支等复杂逻辑的工作流,能更好地应对各种复杂场景和需求,例如根据不同的输入和状态动态调整执行路径,实现多步骤的推理和决策。

6.1 langgraph实现Agent基础操作

from typing import Literal
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END,StateGraph,MessagesState
from langgraph.prebuilt import ToolNode


#定义工具函数,用于Agent调用外部工具
@tool
def search(query:str):
    """模拟一个气象查询搜索工具"""
    if "北京" in query.lower() or "Beijing" in query.lower():
        return "阴天有雾,气温25度"
    return "天气晴朗温度较高39度"

#将工具函数存放在工具列表中
tools = [search]

创建工具集节点:ToolNode是LangGraph中的一个预构建节点,用于封装一组工具函数。这些工具函数可以通过模型调用来执行特定的任务。

tool_node = ToolNode(tools)

定义模型对象

#定义模型对象
API_KEY = "sk-4b79f3a3ff334a15a1935366ebb425b3"
model = ChatOpenAI(model_name="deepseek-chat",
                  api_key=API_KEY,base_url="https://api.deepseek.com")
#将工具列表绑定到模型对象上
model = model.bind_tools(tools)

定义路由函数/状态转换函数:should_continue函数用于决定当前状态之后应该转移到哪个节点。它接收一个MessagesState对象作为输入,并返回一个字符串,表示下一个节点的名称。

消息状态:MessagesState是LangGraph中的一个状态类,用于存储对话过程中的消息列表。每个状态对象都包含一个messages字段,该字段是一个消息对象的列表。

from typing import Literal 是 Python 3.8 及以上版本中引入的一种类型注解工具,用于表示某个变量或函数参数只能是特定的几个值之一。Literal 是 typing 模块中的一个特殊类型,它允许你精确地指定一个或多个字面量作为类型约束。

def should_continue(state:MessagesState)->Literal["tools",END]:
    messages = state['messages']
    #获取用户提问消息
    last_message = messages[-1] 
    
    #如果llm调用工具,则转到tools节点
    if last_message.tool_calls:
        return "tools"
    return END

定义模型调用函数

def call_model(state:MessagesState):
    #获取消息列表
    messages = state['messages']
    #调用模型返回结果
    response = model.invoke(messages)
    return {"messages":[response]}

定义一个新的状态图,使用MessagesState作为状态类型

workflow = StateGraph(MessagesState)

在状态图上添加节点

workflow.add_node("agent",call_model)
workflow.add_node("tools",tool_node)

设置入口节点为agent(入口节点指向agent节点),这意味着agent是第一个被调用的节点

workflow.set_entry_point("agent")

添加条件边:agent节点根据should_continue进行边的连接(虚线边)

workflow.add_conditional_edges('agent',should_continue)

定义普通边:tools工具节点连接agent节点的边(实线边)

workflow.add_edge("tools","agent")

初始化内存以在图运行之间持久化状态:MemorySaver是LangGraph中的一个检查点保存器,用于在内存中保存状态图的中间状态。这对于调试和监控非常有用,因为它允许你在运行时查看和恢复状态。

checkpointer = MemorySaver()

编译图:将其编译成一个langchain可运行的一个对象,在编译时传递内存

app = workflow.compile(checkpointer=checkpointer) 

执行图

final_state = app.invoke(
    {"messages":[HumanMessage(content="北京天气如何?")]},
    config={"configurable":{"thread_id":42}}
)
result = final_state['messages'][-1].content
result

配置选项(config)实现上下文共享:如果两个任务在同一个线程上执行,它们可以共享同一个上下文(例如全局变量、线程本地存储等)。这对于需要维护状态或会话信息的应用非常重要。

final_state = app.invoke(
    {"messages":[HumanMessage(content="我刚才问的是哪个城市?")]},
    config={"configurable":{"thread_id":42}}
)
result = final_state['messages'][-1].content
result

保存图文件

graph_png = app.get_graph().draw_mermaid_png()
with open('graph.png','wb') as fp:
    fp.write(graph_png)

6.2 langgraph实现Multi-Agent Systems

image-20250627190635391
from langchain_core.messages import (
BaseMessage,
HumanMessage,
ToolMessage,
)
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from typing import Literal
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END,StateGraph,MessagesState
from langgraph.prebuilt import ToolNode

# 导⼊聊天提示模板和消息占位符
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
# 导⼊状态图相关的常量和类
from langgraph.graph import END, StateGraph, START

API_KEY = "sk-4b79f3a3ff334a15a1935366ebb425b3"
llm = ChatOpenAI(model_name="deepseek-chat",
                 api_key=API_KEY,base_url="https://api.deepseek.com")

# 定义⼀个函数,⽤于创建代理
def create_agent(llm, tools, system_message: str):
     """创建⼀个代理。"""
     # 创建⼀个聊天提示模板
     prompt = ChatPromptTemplate.from_messages(
         [
             (
             "system",
             "你是⼀个有帮助的AI助⼿,与其他助⼿合作。"
             " 使⽤提供的⼯具来推进问题的回答。"
             " 如果你不能完全回答,没关系,另⼀个拥有不同⼯具的助⼿"
             " 会接着你的位置继续帮助。执⾏你能做的以取得进展。"
             " 如果你或其他助⼿有最终答案或交付物,"
             " 在你的回答前加上FINAL ANSWER,以便团队知道停⽌。"
             " 你可以使⽤以下⼯具: {tool_names}。\n{system_message}",
             ),
             # 消息占位符
             MessagesPlaceholder(variable_name="messages"),
         ]
     )
     # 传递系统消息参数
     prompt = prompt.partial(system_message=system_message)
     # 传递⼯具名称参数
     prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
     # 绑定⼯具并返回提示模板
     return prompt | llm.bind_tools(tools)
    
#定义工具函数
@tool
def get_search_result(question):
    """
    互联网搜索函数
    :param question: 必要参数,字符串类型,用于表示在互联网上进行搜素的关键词或者搜索内容的简短描述,\
    :return:SerpAPI API根据参数question进行互联网搜索后的结果,其中包含了全部重要的搜索结果内容。
    """
    from langchain_community.utilities import SerpAPIWrapper
    serpapi_api_key = "60f286e601f44a26600e42c65e7a9b3ceb06a3f0dc8e0fe7ce56ec93d6274ccd"
    search = SerpAPIWrapper(serpapi_api_key=serpapi_api_key)
    result = search.run(question)
    return result

#定义工具函数,用于Agent调用外部工具
@tool
def send_email(query:str):
    """邮件发送工具,可以接受query内容,然后进行邮件发送"""
    return "邮件已成功发送。"

#定义工具节点
# 导⼊预构建的⼯具节点
from langgraph.prebuilt import ToolNode
# 定义⼯具列表
tools = [get_search_result, send_email]
# 创建⼯具节点
tool_node = ToolNode(tools)

#定义状态:我们⾸先定义图的状态。这只是⼀个消息列表,以及⼀个⽤于跟踪最新发送者的键
# 导⼊操作符和类型注解
import operator
from typing import Annotated, Sequence, TypedDict
# 导⼊OpenAI聊天模型
from langchain_openai import ChatOpenAI
# 定义⼀个对象,⽤于在图的每个节点之间传递
# 我们将为每个代理和⼯具创建不同的节点

class AgentState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], operator.add]
    sender: str
    
#定义代理节点
import functools
from langchain_core.messages import AIMessage

# ⽤于为给定的Agent创建节点
def agent_node(state, agent, name):#name:agent代理的名字
    # 调⽤代理
    result = agent.invoke(state)
    #将 result 转换为 AIMessage 类型,并进行进一步处理
    #使用模型的 model_dump 方法将 result 转换为字典格式,同时排除 "type" 和 "name" 字段。这通常用于序列化对象以便传输或存储。
    result = AIMessage(**result.model_dump(exclude={"type", "name"}), name=name)
    return {
        "messages": [result],
        # 由于我们有⼀个严格的⼯作流程,我们可以跟踪发送者,以便知道下⼀个传递给谁。
        "sender": name,
    }
        
#创建搜索Agent代理对象和节点对象
research_agent = create_agent(
    llm,
    [get_search_result],
    system_message="你应该提供准确的数据供MailOpt使⽤。",
)
# 创建Agent节点对象:使用agent和name的值填充到agent_node函数中对应的两个参数
research_node = functools.partial(agent_node, agent=research_agent, name="Researcher")


#创建发邮件Agent代理对象和节点对象
mail_agent = create_agent(
    llm,
    [send_email],
    system_message="你用于进行邮件发送业务实现",
)
# 创建Agent节点对象
mail_node = functools.partial(agent_node, agent=mail_agent, name="MailOpt")


#定义路由函数,决定是否继续执行
from typing import Literal
# 定义路由器函数,continue 表示代理应该继续处理消息队列中的下一条消息。
def router(state) -> Literal["call_tool", "__end__", "continue"]:
    # 这是路由器
    messages = state["messages"]
    last_message = messages[-1]
    if last_message.tool_calls:
        # 上⼀个代理正在调⽤⼯具
        return "call_tool"
    if "FINAL ANSWER" in last_message.content:
        # 任何代理决定⼯作完成
        return "__end__"
    return "continue"

#图、节点和边的创建
# 创建状态图实例
workflow = StateGraph(AgentState)
# 添加搜索节点
workflow.add_node("Researcher", research_node)
# 添加邮件节点
workflow.add_node("MailOpt", mail_node)
# 添加⼯具调⽤节点
workflow.add_node("call_tool", tool_node)

# 添加条件边
workflow.add_conditional_edges(
     "Researcher",
     router,
     {"continue": "MailOpt", "call_tool": "call_tool", "__end__": END},
)
workflow.add_conditional_edges(
     "MailOpt",
     router,
     {"continue": "Researcher", "call_tool": "call_tool", "__end__": END},
)
# 添加条件边
workflow.add_conditional_edges(
     "call_tool",
     #如果 x["sender"] 的值是 "Researcher",那么边会连接到 "Researcher" 节点。
	 #如果 x["sender"] 的值是 "MailOpt",那么边会连接到 "MailOpt" 节点。
     lambda x: x["sender"],
     {
         "Researcher": "Researcher",
         "MailOpt": "MailOpt",
     },
)
# 添加起始边
workflow.add_edge(START, "Researcher")
# 编译⼯作流图
graph = workflow.compile()

# 将⽣成的图⽚保存到⽂件
graph_png = graph.get_graph().draw_mermaid_png()
with open("collaboration.png", "wb") as f:
    f.write(graph_png)
    
    
#调用
events = graph.invoke(
    {
        "messages": [
            HumanMessage(
            content="获取过去5年AI软件市场规模,归纳成100字"
            " 然后进行邮件发送。"
            " ⼀旦发送完邮件表示你完成了任务。"
            )
        ],
    }
)

#获取最终结果
result = events['messages'][-1].content
result

#查看中间结果
for message in events['messages']:
    print(message.content)
    print("-----------------------------------")

7. RAG与langchain应用

检索增强⽣成(RAG)是指对⼤型语⾔模型输出进⾏优化,使其能够在⽣成响应之前引⽤训练数据来源之外的权威知识库。⼤型语⾔模型(LLM)⽤海量数据进⾏训练,使⽤数⼗亿个参数为回答问题、翻译语⾔和完成句⼦等任务⽣成原始输出。在 LLM 本就强⼤的功能基础上,RAG 将其扩展为能访问特定领域或组织的内部知识库,所有这些都⽆需重新训练模型。这是⼀种经济⾼效地改进 LLM 输出的⽅法,让它在各种情境下都能保持相关性、准确性和实⽤性。

7.1 RAG构建流程

假设现在我们有一个偌大的知识库,当想从该知识库中去检索最相关的内容时,最简单的方法是:接收到一个查询(Query),就直接在知识库中进行搜索。这种做法其实是可行的,但存在两个关键的问题:

  1. 假设提问的Query的答案出现在一篇文章中,去知识库中找到一篇与用户输入相关的文章是很容易的,但是我们将检索到的这整篇文章直接放入Prompt中并不是最优的选择,因为其中一定会包含非常多无关的信息,而无效信息越多,对大模型后续的推理影响越大。
  2. 任何一个大模型都存在最大输入的Token限制,一个流程中可能涉及多次检索,每次检索都会产生相应的上下文,无法容纳如此多的信息。

image-20250319143643125

解决上述两个问题的方式是:把存放着原始数据的知识库(Knowledge)中的每一个raw data,切分成一个一个的小块,这些小块可以是一个段落,也可以是数据库中某个索引对应的值。这个切分过程被称为“分块”(chunking),如下述流程所示:

image-20250319143832715

以第一个原始数据为例(raw data 1),通过一些特定的方法进行切分,一个完整的内容会被分割成 chunk1 ~ chunk4。采取相同的方法,继续对raw data 2raw data 3直至raw data n进行切分。完成这一过程后,我们最终得到的是一个充满分块数据(chunks)的新的知识库(repository),其中每一项都是一个单独的chunk。例如,如果原始文档共有10个,那么经过切分,可能会产生出100个chunks。

完成这一转化后,当再次接收到一个查询(Query)时,就会在更新后的知识库(repository)中进行搜索,这时检索的范围就不再是某个完整的文档,而是其中的某一个部分,返回的是一个或多个特定的chunk,这样返回的信息量就会更小且更精确。随后,这些被检索到的chunk会被加入到Prompt中,作为上下文信息与用户原始的Query共同输入到大模型进行处理,以生成最终的回答。

在上述将原始数据(raw data)转化为chunk的过程中,就会包含构建RAG的第一部分开发工作:这包括如果做数据清洗,如去除停用词、标点符号等。此外,还涉及如何选择合适的split方法来进行数据切分的一系列技术。

接下来面临的问题是,尽管所有数据已经被切割成一个个chunk,其存储形式还是以字符串形式存在,如果想从repository中匹配到与输入的query相关的chunks,比较两句话是否相似,看一句话中相同字有几个,这显然是行不通的。我们需要获取的是句子所蕴含的深层含义,而非仅仅是表面的字面相似度。因此,大家也能想到,在NLP中去计算文本相似度的有效的方法就是Embedding,即将这些chunks转换成向量(vector)形式。所以流程会丰富如下:

image-20250319144103496

Embedding 是由向量模型⽣成的,它会根据不同的算法⽣成⾼维度的向量数据,代表着数据的不同特征,这些特征代表了数据的不同维度。例如,对于⽂本,这些特征可能包括词汇、语法、语义、情感、情绪、主题、上下⽂等。对于⾳频,这些特征可能包括⾳调、节奏、⾳⾼、⾳⾊、⾳量、语⾳、⾳乐等。

在这个流程中,会先将用户输入的 Query 转化成 Vector,然后再去与知识库中的向量进行相似度比较,检索出相似的Vector,最终返回其对应的Chunk(字符串形式的文本),再执行后续的流程。所以在这个过程中,就会产生构建RAG的第二部分的开发工作:如果将chunk转化成Vector及以何种形式进行存储。同时,我们要考虑的是:如何去计算向量之间的相似度?如果去和知识库中的向量一个一个比较,这个时间复杂度是非常高的,那么其解决办法又是什么呢?我们继续看下述流程:

image-20250319144224706

如上所示,解决搜索效率和计算相似度优化算法的答案就是:向量数据库。同时也产生了构建RAG的第三部分工作:我们要去了解和学习如何选择、使用向量数据库。

最终整体流程就如上图所示,一个基础的RAG架构会只要包含以下几方面的开发工作:

  1. 如何将原始数据转化成chunks;
  2. 如何将chunks转化成Vector;
  3. 如何选择计算向量相似度的算法;
  4. 如何利用向量数据库提升搜索效率;
  5. 如何把找到的chunks与原始query拼接在一起,产生最终的Prompt;

在以上5点开发任务中,我们确实是可以利用已经训练好的Embedding模型,开源的向量数据库等去直接解决某一类问题,所以我们前面才说一个基础架构的RAG系统搭建起来其实很简单,但搭建并不意味着直接就能用,毕竟RAG的核心是检索,检索出来的内容的准确率是衡量一个RAG系统的最基础的标准。目前没有任何一套理论、任何一套解决方案能够百分之百的指导着我们构建出一个最优的RAG系统。不同的需求,不同的数据,其构建方法也会大相径庭,需要我们在实践的过程中不断地去尝试,不断地去积累相关的经验,才能够将其真正落地。

7.2 相关核心概念和操作

7.2.1 向量数据库

向量数据库(Vector Database),也叫矢量数据库,主要用来存储和处理向量数据。

在数学中,向量是有大小和方向的量,可以使用带箭头的线段表示,箭头指向即为向量的方向,线段的长度表示向量的大小。两个向量的距离或者相似性可以通过欧式距离或者余弦距离得到。

图像、文本和音视频这种非结构化数据都可以通过某种变换或者嵌入学习转化为向量数据存储到向量数据库中,从而实现对图像、文本和音视频的相似性搜索和检索。这意味着您可以使用向量数据库根据语义或上下文含义查找最相似或相关的数据。

向量数据库的主要特点是高效存储与检索。利用索引技术和向量检索算法能实现高维大数据下的快速响应。

7.2.2 向量嵌入Vector Embeddings

对于传统数据库,搜索功能都是基于不同的索引方式加上精确匹配和排序算法等实现的。本质还是基于文本的精确匹配,这种索引和搜索算法对于关键字的搜索功能非常合适,但对于语义搜索功能就非常弱。

例如,如果你搜索 “小狗”,那么你只能得到带有“小狗” 关键字相关的结果,而无法得到 “柯基”、“金毛” 等结果,因为 “小狗” 和“金毛”是不同的词,传统数据库无法识别它们的语义关系,所以传统的应用需要人为的将 “小狗” 和“金毛”等词之间打上小狗特征标签进行关联,这样才能实现语义搜索。

同样,当你在处理非结构化数据时,你会发现非结构化数据的特征数量会迅速增加,处理过程会变得十分困难。比如我们处理图像、音频、视频等类型的数据时,这种情况尤为明显。就拿图像来说,可以标注的特征包括颜色、形状、纹理、边缘、对象、场景等多个方面。然而,这些特征数量众多,而且依靠人工进行标注的难度很大。因此,我们需要一种自动化的方式来提取这些特征,而Vector Embedding技术就能够实现这一目标。

Vector Embedding 是由专门的向量模型生成的,它会根据不同的算法生成高维度的向量数据,代表着数据的不同特征,这些特征代表了数据的不同维度。例如,对于文本,这些特征可能包括词汇、语法、语义、情感、情绪、主题、上下文等。对于音频,这些特征可能包括音调、节奏、音高、音色、音量、语音、音乐等。

7.2.3 相似性测量

如何衡量向量之间的相似性呢?有三种常见的向量相似度算法:欧几里德距离、余弦相似度和点积。

  • 点积(内积): 两个向量的点积是一种衡量它们在同一方向上投影的大小的方法。如果两个向量是单位向量(长度为1),它们的点积等于它们之间夹角的余弦值。因此,点积经常被用来计算两个向量的相似度。
  • 余弦相似度: 这是一种通过测量两个向量之间的角度来确定它们相似度的方法。余弦相似度是两个向量点积和它们各自长度乘积的商。这个值的范围从-1到1,其中1表示完全相同的方向,-1表示完全相反,0表示正交。
  • 欧氏距离: 这种方法测量的是两个向量在n维空间中的实际距离。虽然它通常用于计算不相似度(即距离越大,不相似度越高),但可以通过某些转换(如取反数或用最大距离归一化)将其用于相似度计算。

像我们最常用的余弦相似度,其代码实现也非常简单,如下所示:

import numpy as np

def cosine_similarity(A, B):
    # 使用numpy的dot函数计算两个数组的点积
    # 点积是向量A和向量B在相同维度上对应元素乘积的和
    dot_product = np.dot(A, B)
    
    # 计算向量A的欧几里得范数(长度)
    # linalg.norm默认计算2-范数,即向量的长度
    norm_A = np.linalg.norm(A)
    
    # 计算向量B的欧几里得范数(长度)
    norm_B = np.linalg.norm(B)
    
    # 计算余弦相似度
    # 余弦相似度定义为向量点积与向量范数乘积的比值
    # 这个比值表示了两个向量在n维空间中的夹角的余弦值
    return dot_product / (norm_A * norm_B)

7.2.4 相似性搜素

既然我们知道了可以通过比较向量之间的距离来判断它们的相似度,那么如何将它应用到真实的场景中呢?如果想要在一个海量的数据中找到和某个向量最相似的向量,我们需要对数据库中的每个向量进行一次比较计算,但这样的计算量是非常巨大的,所以我们需要一种高效的算法来解决这个问题。

高效的搜索算法有很多,其主要思想是通过两种方式提高搜索效率:

1)减少向量大小——通过降维或减少表示向量值的长度。

2)缩小搜索范围——可以通过聚类或将向量组织成基于树形、图形结构来实现,并限制搜索范围仅在最接近的簇中进行。

我们首先来介绍⼀下大部分算法共有的核心概念,也就是kmeans聚类。

K-Means聚类

我们可以在保存向量数据后,先对向量数据先进行聚类。例如下图在二维坐标系中,划定了 4 个聚类中心,然后将每个向量分配到最近的聚类中心,经过聚类算法不断调整聚类中心位置,这样就可以将向量数据分成 4 个簇。每次搜索时,只需要先判断搜索向量属于哪个簇,然后再在这一个簇中进行搜索,这样就从 4 个簇的搜索范围减少到了 1 个簇,大大减少了搜索的范围。

image-20250628090809249

HNSW

除了聚类以外,也可以通过构建树或者构建图的方式来实现近似最近邻搜索。这种方法的基本思想是每次将向量加到数据库中的时候,就先找到与它最相邻的向量,然后将它们连接起来,这样就构成了一个图。当需要搜索的时候,就可以从图中的某个节点开始,不断的进行最相邻搜索和最短路径计算,直到找到最相似的向量。

image-20250628091249234


day25-RAG实操

文件: day25-RAG实操\langchain高级 - 副本.md

7.2.5 Embedding models

LangChain 设计了一个 Embeddings 类。该类是一个专为与文本嵌入模型进行交互而设计的类。有许多嵌入模型提供商(如OpenAI、BaiChuan、QianFan、Hugging Face等)这个类旨在为它们提供一个标准接口。

Embeddings类会为文本创建一个向量表示。这很有用,因为这意味着我们可以在向量空间中思考文本,并做一些类似语义搜索的事情,比如在向量空间中寻找最相似的文本片段。

对于Embedding Models我们只需要学会如何去使用就可以,是因为有非常多的模型供应商,如OpenAI、Hugging Face国内的有百川、千帆都提供了标准接口并集成在LangChian框架中,这意味着:Embedding Models已经有人帮我们训练好了,我们只要按照其提供的接口规范,将自然语言文本传入进去,就能得到其对应的向量表示。这显然是非常简单的。

那么在如此多的Embedding Models都可以使用的情况下,应该如何选择呢? 首先,我们在使用形式上把 Embedding Models分为两类:

  1. 在线Embedding Models,仅提供API服务,需要按照Token付费;
  2. 开源Embedding Models可以下载到本地免费使用,但在运行过程中会消耗GPU资源。

在线Embedding Models

LangChain接入了国内的Baidu Qianfan,Baichuan Text Embeddings等向量模型,具体支持的平台可以在如下位置进行查看:https://python.langchain.com/docs/integrations/text_embedding/

image-20250320105855089

接下来我们以Baichuan Text Embeddings为例展开讲解。注意:Baichuan Text Embeddings目前仅支持中文文本嵌入。

  • 如何使用Baichuan Text Embeddings

    • 获取API Key

      • 要使用Baichuan Text Embeddings,首先需要获取API密钥。您可以通过以下步骤获取:
    • 安装必要的库pip install langchain_community

  • 代码示例

    from langchain_community.embeddings import BaichuanTextEmbeddings
    import os
    
    # 设置API密钥
    key = open('./key_files/baichuan_API-Key.md').read().strip()
    embeddings = BaichuanTextEmbeddings(api_key=key)
    
    # 示例文本
    text_1 = "今天天气不错"
    text_2 = "今天阳光很好"
    
    # 获取单个文本的嵌入
    query_result = embeddings.embed_query(text_1)
    print("单个文本嵌入结果:", query_result[:5])  # 只打印前5个元素
    
    # 获取多个文本的嵌入
    doc_result = embeddings.embed_documents([text_1, text_2])
    print("多个文本嵌入结果:", [vec[:5] for vec in doc_result])  # 每个向量只打印前5个元素
    
  • BaichuanTextEmbeddings主要参数介绍

    • api_key:这是调用Baichuan Text Embeddings服务的身份验证凭证。只有拥有有效API Key的用户才能访问和使用该模型进行文本嵌入操作。
  • BaichuanTextEmbeddings对象主要操作介绍

    • 单向量查询embed_query):法用于将单个文本嵌入为向量表示。它接受一个字符串类型 的文本作为输入,并返回该文本对应的向量表示。这个向量是一个高维向量(1024维),包含了文本的语义信息,可以用于后续的各种自然语言处理任务。
    • 多向量查询embed_documents):法用于将多个文本同时嵌入为向量表示。

这里模拟一个QA场景,我们定义一个问题,然后定义10条文本作为回答。然后分别对问题和回答各自进行词向量转换:

query = "早睡早起到底是不是保持身体健康的标准?"

sentences = ["早睡早起确实是保持身体健康的重要因素之一。它有助于同步我们的生物钟,并提高睡眠质量。", 
             "早睡早起可以帮助人们更好地适应自然光周期,从而优化褪黑激素的产生,这种激素是调节睡眠和觉醒的关键。",
             "关于提高工作效率,确保在日常饮食中包含充足的蛋白质、复合碳水化合物和健康脂肪非常关键。",
             "投资可再生能源项目和推广电动汽车可以显著减少温室气体排放,从而缓解气候变化带来的负面影响。",
             "多发性硬化症是一种影响中枢神经系统的自身免疫疾病,导致神经传导受损。虽然与阿尔茨海默症类似,多发性硬化症的主要症状包括疲劳、视觉障碍和肌肉控制问题。",
             "今天的天气太好了,可以早点起床去爬山",
             "如果下班特别晚的话,我建议你还是打车回家吧",
             "提升学术研究质量需侧重于多学科融合和国际合作。研究机构应该鼓励学者之间的交流,通过共享数据和研究方法,来推动科学发现和技术创新。",
             "如果你认为我说的没用,那你大可以不必理会。",
             "衡量一个人是否成功的标准在于他到底能不能让身边的人都变的优秀"

]

使用embed_documents方法,传入sentences列表,得到每条文本的向量表示

sentence_embeddings = embeddings_model.embed_documents(sentences)

通过embed_query方法生成问题的向量表示

embedded_query = embeddings_model.embed_query(query)

开源EMbedding Models

ollama官网进行开源模型下载:https://ollama.com/search?q=embedding

image-20250320150637127

我们以nomic-embed-text向量模型为例:

image-20250320150734846
def ollama_embedding_by_api(text):
    res = requests.post(
        url = 'http://127.0.0.1:11434/api/embeddings',
        json = {
            "model":'nomic-embed-text:latest',
            'prompt':text
        }
    )
    embedding_list = res.json()['embedding']
    return embedding_list

代码构建简易RAG

pip install chromadb

pip install requests

import uuid
import chromadb
import requests
import os
from openai import OpenAI

#创建数据库,类似创建一个文件夹
client = chromadb.PersistentClient(path="./db/chroma_demo")
#创建数据集合(库表)
collection = client.get_or_create_collection(name="collection_v2")


#数据集切分-分块处理
def file_chunk_list():
    #1.读取文件内容
    with open('中医问诊.txt','r',encoding='utf-8') as fp:
        data = fp.read()
    #2.根据换行切割:将一个病症作为一个列表元素数据
    chunk_list = data.split('\n\n')
    chunk_list = [chunk for chunk in chunk_list if chunk]
    return chunk_list
#数据集向量化封装
def ollama_embedding_by_api(text):
    #使用nomic向量模型
    # res = requests.post(
    #     url = 'http://127.0.0.1:11434/api/embeddings',
    #     json = {
    #         "model":'nomic-embed-text:latest',
    #         'prompt':text
    #     }
    # )
    # embedding_list = res.json()['embedding']
    # return embedding_list
    
    #使用阿里百炼向量模型(效果超级好)
    client = OpenAI(
        api_key="sk-52xxxd1e203c6712",  # 如果您没有配置环境变量,请在此处用您的API Key进行替换
        base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"  # 百炼服务的base_url
    )

    completion = client.embeddings.create(
        model="text-embedding-v3",
        input=text,
        dimensions=1024,
        encoding_format="float"
    )
    return completion.data[0].embedding

#deepseek模型调用
def ollama_generate_by_api(prompt):
    res = requests.post(
    url = 'http://127.0.0.1:11434/api/generate',
    json = {
            "model":'deepseek-r1:7b',
            'prompt':prompt,
            'stream':False
        }
    )
    res = res.json()['response']
    return res

#整体集成
def initial():
    #构造数据
    documents = file_chunk_list()
    #给每一个数据创建唯一的id标识
    ids = [str(uuid.uuid4()) for _ in documents]
    embeddings = [ollama_embedding_by_api(text) for text in documents]

    #插入数据
    collection.add(
        ids = ids,
        documents=documents,
        embeddings=embeddings
    )
    
def run():
    qs = '我好像是感冒了,症状是头痛、轻微发烧、肢节酸痛、打喷嚏和流鼻涕。'
    qs_embedding = ollama_embedding_by_api(qs)
    #n_results表示匹配几个最高相似度的结果
    res = collection.query(query_embeddings=[qs_embedding,],query_texts=qs,n_results=2)
    result = res['documents'][0]
    context = '\n'.join(result)
    prompt = f'''你是一个中医问答机器人,任务是根据参考信息回答用户问题,如果你参考信息不足以回答用户问题,请回复不知道,切记不要去杜撰和自由发挥任何内容和信息,请用中文回答,参考信息:{context},来回答问题:{qs},'''
    result = ollama_generate_by_api(prompt)
    print(result)

调用测试:

initial() #执行一次即可
run() #可多次测试

7.3 Source 与 data loaders

Source概念指的是RAG架构中所外挂的知识库。正如我们之前所讨论的,因为大模型的原生能力很强,所以它可以识别多种不同的类型的原始数据而不用做额外的处理,而且在实际场景中,私有数据通常也并不是单一的,可以来自多种不同的形式,可以是上百个.csv文件,可以是上千个.json文件,也可以是上万个.pdf文件,同时如果对接到具体的业务,可以是某一个业务流程外放的API,可以是某个网站的实时数据等多种情况。

所以LangChain首先做的就是:将常见的数据格式和数据来源使用LangChain的规范,抽象出一个一个的单独的集成模块,称为文档加载器(Document loaders),用于快速加载某种形式下的文本数据。如下图所示:

image-20250319151221286

这意味着,我们可以通过调用LangChain抽象好的方法直接处理私有数据,无需手动编写中间的处理流程,并且每一种文档的加载器,在LangChain官方文档中都有基本的调用示例供我们快速上手使用,具体位置如下:https://python.langchain.com/docs/integrations/document_loaders/

我们以加载txt文件为示例:

将文件作为文本读入,并将其全部放入一个文档中,这是最简单的一个文档加载程序,使用方式如下:

from langchain.document_loaders import TextLoader

docs = TextLoader('./data/reason.txt', encoding="utf-8").load()

对于TextLoader,使用.page_content.metadata去访问数据。

加载csv文件为示例:

逗号分隔值(CSV)文件是⼀种使用逗号分隔值的定界文本文件。文件的每一行是⼀个数据记录。每个记录由⼀个或多个字段组成,字段之间用逗号分隔。LangChain 实现了⼀个 CSV 加载器,可以将 CSV 文件加载为⼀系列 Document 对象。CSV 文件的每⼀行都会被翻译为⼀个文档。

from langchain_community.document_loaders.csv_loader import CSVLoader

file_path = (
		"csv_loader.py"
)

loader = CSVLoader(file_path=file_path,encoding="UTF-8")
data = loader.load()

for record in data[:2]:
		print(record)

加载pdf文件为示例:

这⾥我们使用pypdf 将PDF加载为文档数组,其中每个文档包含页面内容和带有 page 编号的元数据。

pip install pypdf

from langchain_community.document_loaders import PyPDFLoader
file_path = ("pytorch.pdf")
loader = PyPDFLoader(file_path)
#加载并分割 PDF 文件。将其按页分割成多个部分。返回的结果是一个包含每一页内容的列表 pages。
pages = loader.load_and_split()
print(pages[0])

7.4 Text Splitters 详解

7.4.1 如何将文本切分成Chunks

分块(Chunking),其实现形式上是将长文档拆分为较小的块的过程,目的是在检索时能够准确地找到最直接和最相关的段落。由于文章通常包含大量不相关信息,在进行分块之前,也常常需要进行一些预处理工作,如文本清洗、停用词处理等。

转回到核心内容来看,一个有效的分块策略,可以确保搜索结果精确地反映用户查询的实际需求。如果分块过小或过大,都可能导致搜索结果不准确或提取不到最相关的内容。理想的文本块应尽可能语义独立,即不过度依赖上下文,这样的文本是语言模型最易于理解的。因此,为文档确定最佳的块大小是确保搜索结果准确性和相关性的关键。这涉及多个决策因素,如块的大小;如果句子太短,模型可能难以理解其意义,且句子越短,包含的有效信息就越少。比较常用的有如下4种不同的方法来优化分块策略:

  1. 根据句子切分:这种方法按照自然句子边界进行切分,以保持语义完整性。
  2. 按照固定字符数来切分:这种策略根据特定的字符数量来划分文本,但可能会在不适当的位置切断句子。
  3. 按固定字符数来切分,结合重叠窗口(overlapping windows):此方法与按字符数切分相似,但通过重叠窗口技术避免切分关键内容,确保信息连贯性。
  4. 递归方法:通过递归方式动态确定切分点,这种方法可以根据文档的复杂性和内容密度来调整块的大小。

第二种方法(按照字符数切分)和第三种方法(按固定字符数切分结合重叠窗口)主要基于字符进行文本的切分,而不考虑文章的实际内容和语义。这种方式虽简单,但可能会导致主题或语义上的断裂。相对而言,递归方法更加灵活和高效,它结合了固定长度切分和语义分析。通常是首选策略,因为它能够更好地确保每个段落包含一个完整的主题。

这些方法各有优势和局限,选择适当的分块策略取决于具体的应用需求和预期的检索效果。接下来我们依次尝试用常规手段应该如何实现上述几种方法的文本切分。

接下来就具体来上上述4中切分方式的具体实现~

7.4.2 按照句子切分

按照句子切分,其实就是通过标点符号来进行文本切分(分割),这可以直接使用Python的标准库来完成这个任务。一种简单的方法是使用re模块,它提供了正则表达式的支持,可以方便地根据标点符号来分割文本。如下示例中,展示了如何使用re.split()函数来根据中文和英文的标点符号进行文本切分。代码如下:

import re

def split_text_by_punctuation(text):
    # 定义一个正则表达式,包括常见的中英文标点
    # pattern = r"[。!?。"#$%&'()*+,-/:;<=>@[\]^_`{|}~\s、]+"
    pattern = r"[。!?。]+"
    # 使用正则表达式进行分割
    segments = re.split(pattern, text)
    # 过滤掉空字符串
    return [segment for segment in segments if segment]

这个函数会根据中文和英文的标点符号来分割文本,并移除空字符串。定义好分割函数后,我们可以尝试进行功能测试:

# 文本
text = "春节的脚步越来越近,大街小巷都布满了节日的气氛。商店门口挂满了红灯笼和春联,家家户户都在忙着打扫卫生,准备迎接新的一年。\
小明回到家乡,感受到了浓浓的过年氛围。他在街上走着,看到小朋友们手持烟花棒,欢笑声此起彼伏。\
夜幕降临,整个城市亮起了五彩缤纷的灯光,映照着人们脸上的喜悦与期待。老人们聚在一起,回忆过去,展望未来。\
而年轻人则在夜市享受美食,放松心情。这是一个充满希望和喜悦的时刻,每个人都在以自己的方式庆祝这个特殊的节日。"

# 调用函数进行分割
segments = split_text_by_punctuation(text)

# 使用循环来打印每个chunk
for i, segment in enumerate(segments):
    print("Chunk {}: {}".format(i + 1, segment))

7.4.3 按照固定字符数切分

如果想按照固定字符数来切分文本,这种方法就不再依赖于标点符号,而是简单地按照给定的字符数来切分文本。我们可以编写一个函数,用来将文本分割成指定长度的片段。代码如下:

def split_text_by_fixed_length(text, length):
    # 使用列表推导式按固定长度切分文本
    return [text[i:i + length] for i in range(0, len(text), length)]

这个函数的作用是根据指定的长度(在这个例子中为100个字符)来切分文本。我们可以根据具体需要调整这个长度。

# 文本
text = "春节的脚步越来越近,大街小巷都布满了节日的气氛。商店门口挂满了红灯笼和春联,家家户户都在忙着打扫卫生,准备迎接新的一年。\
小明回到家乡,感受到了浓浓的过年氛围。他在街上走着,看到小朋友们手持烟花棒,欢笑声此起彼伏。\
夜幕降临,整个城市亮起了五彩缤纷的灯光,映照着人们脸上的喜悦与期待。老人们聚在一起,回忆过去,展望未来。\
而年轻人则在夜市享受美食,放松心情。这是一个充满希望和喜悦的时刻,每个人都在以自己的方式庆祝这个特殊的节日。"

# 定义每个片段的长度
chunk_length = 100

# 调用函数进行分割
result = split_text_by_fixed_length(text, chunk_length)

# 打印结果
for i, segment in enumerate(result):
    print(f"Chunk {i+1}: {segment}")

然而,这种方法的一个明显缺点是由于仅依据长度进行切分,切分后的片段可能无法保持完整的语义。但并不意味着它不适用于文本切分任务。例如,这种方法非常适合于处理日志文件或代码块,其中文本通常以固定长度或格式出现,或者在处理来自传感器或其他实时数据源的流数据时,固定长度切分可以确保数据被均匀地处理和分析。这些应用场景中,数据的结构和形式通常是预定和规范的,因此即便是按固定长度进行切分,反而会更有利于对数据的理解和使用。

7.4.4 结合重叠窗口的固定字符数切分

重复窗口的意义是:块之间保持一些重叠,以确保语义上下文不会在块之间丢失。在文本处理和其他数据分析领域,"重叠"(overlap)指的是连续数据块之间共享的部分。这种方法特别常见于信号处理、语音分析、自然语言处理等领域,其中数据的连续性和上下文信息非常重要。比如下述代码所示:

def split_text_by_fixed_length_with_overlap(text, length, overlap):
    # 使用列表推导式按固定长度及重叠长度切分文本
    return [text[i:i + length] for i in range(0, len(text) - overlap, length - overlap)]

# 文本
text = "春节的脚步越来越近,大街小巷都布满了节日的气氛。商店门口挂满了红灯笼和春联,家家户户都在忙着打扫卫生,准备迎接新的一年。\
小明回到家乡,感受到了浓浓的过年氛围。他在街上走着,看到小朋友们手持烟花棒,欢笑声此起彼伏。\
夜幕降临,整个城市亮起了五彩缤纷的灯光,映照着人们脸上的喜悦与期待。老人们聚在一起,回忆过去,展望未来。\
而年轻人则在夜市享受美食,放松心情。这是一个充满希望和喜悦的时刻,每个人都在以自己的方式庆祝这个特殊的节日。"

# 定义每个片段的长度和重叠长度
chunk_length = 100
overlap_length = 30

# 调用函数进行分割
result = split_text_by_fixed_length_with_overlap(text, chunk_length, overlap_length)

# 打印结果
for i, segment in enumerate(result):
    print(f"Chunk {i+1}: {segment}")

如上所示,每个文本片段长度为100个字符,并且每个片段与下一个片段有30个字符的重叠。这样,每个窗口实际上是在上一个窗口向前移动30个字符的基础上开始的。这种方法特别适用于需要数据重叠以保持上下文连续性的情况,能够较好的在某一个chunk中保存某个完整的语义信息,比如在第一个Chunk中的:'他在街上走着,看到小朋友们手持烟花棒,欢笑'被截断,但是完整的语义能够在Chunk2中被存储:'他在街上走着,看到小朋友们手持烟花棒,欢笑声此起彼伏。' 那么当这条语义信息是有关于Query的上下文,就可以在chunk2中被检索出来。

7.4.5 递归字符文本切分

在前面讲的三种切分方法,虽然简单且更容易理解,但其存在的核心问题是:完全忽视了文档的结构,只是单纯按固定字符数量进行切分。所以难免要更进一步地去做优化,那么一个更进阶的文本分割器应该具备的是:

  • 能够将文本分成小的、具有语义意义的块(通常是句子)。
  • 可以通过某些测量方法,将这些小块组合成一个更大的块,直到达到一定的大小。
  • 一旦达到该大小,请将该块设为自己的文本片段,然后创建具有一些重叠的新文本块,以保持块之间的上下文。

根据上述需求,衍生出来的就是递归字符文本切分器,在langChain中的抽象类为:RecursiveCharacterTextSplitter,同时它也是Langchain的默认文本分割器。

文档切分的可视化工具

我们可以用LangChain提供的文本切分可视化小工具进行直观的理解:https://langchain-text-splitter.streamlit.app/

如上代码所展示的就是RecursiveCharacterTextSplitter类的核心逻辑。所谓的按字符递归分割,就是使用一组分隔符以分层和迭代的方式将输入文本分成更小的块。默认使用[“\n\n” ,"\n" ," ",""] 这四个特殊符号作为分割文本的标记,如果分割文本开始的时候没有产生所需大小或结构的块,那么这个方法会使用不同的分隔符或标准对生成的块递归调用,直到获得所需的块大小或结构。这意味着虽然这些块的大小并不完全相同,但它们仍然会逼近差不多的大小。其中的关键参数:

  • separators:指定分割文本的分隔符
  • chunk_size:被切割字符的最大长度
  • chunk_overlap:如果仅仅使用chunk_size来切割时,前后两段字符串重叠的字符数量。
  • length_function:如何计算块的长度。默认情况下,只计算字符数,也可以选择按照Token。

这里我们可以使用同样的文本进行文本切分测试。示例文本如下所示:

春节的脚步越来越近,大街小巷都布满了节日的气氛。

商店门口挂满了红灯笼和春联,家家户户都在忙着打扫卫生,准备迎接新的一年。
小明回到家乡,感受到了浓浓的过年氛围。他在街上走着,看到小朋友们手持烟花棒,欢笑声此起彼伏。
夜幕降临,整个城市亮起了五彩缤纷的灯光,映照着人们脸上的喜悦与期待。老人们聚在一起,回忆过去,展望未来。而年轻人则在夜市享受美食,放松心情。
这是一个充满希望和喜悦的时刻,每个人都在以自己的方式庆祝这个特殊的节日。

同时调整Chunk Size,因为默认的是1000,很明显我们的测试文本长度低于1000,这里我们降低为100,同时将overlap设置为20:

image-20250320083323049

切分结果如下所示,会正常的切分为四个较为完整的chunks。

image-20250320083356457

这里我们需要强调的两个关键点是:

  • 切分的结果是由 length_function = len决定的,按照设置的切分规则,依次对文本进行分割;
  • 能不能进行分割,并不是由Chunk Size决定,超出Chunk Size只是触发条件,而真正会不会实际执行分割操作,取决于separator设置的切分符。

比如我们调低Chunk Size为50,再次执行。它会由原来的4个Chunk增加到8个Chunk,这里我们以chunk 4 和 chunk 5 举例说明:

image-20250320084503164

Chunk Size设置为50时,夜幕降临,整个城市亮起了五彩缤纷的灯光,映照着人们脸上的喜悦与期待。老人们聚在一起,回忆过去,展望展望未来。是超出50个字符,此时就会触发Chunk Overlap。也就说:当某一个片段溢出了Chunk Size设定的值,才会在下一个分片段中触发 Chunk Overlap,没有触发时,就不需要补充上下文,但当触发了以后,补充的上下文不能超过设定的Chunk Overlap,这是一个非常重要的点,一定要理解。

在这种情况下虽然超出了 Chunk Size,但是按照separators=["\n\n", "\n", " ", ""]的规则,没有任何一条命中,所以不能分割。因此我们才说:超出Chunk Size只是触发条件,而能不能分割,取决于separator设置的关键词。

当然,除了按照 length_function = len(即字符长度)来进行切分,也可以按照Token切分,Token和字符大概是1 :4 这样一个比例,原理是一致的,大家可以自行尝试。

image-20250320084903743

7.5 langchain中的Text Splitters设计

我们首先需要明确的是:在RAG流程中,我们不仅仅处理原始字符串,更常见的是处理文档。文档不仅包含我们关注的文本,还包括额外的元数据(文档标题、发布日期、摘要或者作者信息等),而这两点,均在LangChain的Document Loader的设计中通过 Document对象的Page_content和metadata设定中进行了定义。所以TextSplitter的核心不仅仅是为了划分数据块,而是要以一种便于日后检索和提取价值的格式来整理我们的数据。那么这里我们首先要进行探索的就是:如何去接收不同的数据形式,并能够按照预定的切分方式进行切分。

下面我们就具体来看在langchain中对于Text Splitters是如何进行设计和实现的。

CharacterTextSplitter

这是最简单的方法。其基于字符(默认为“”)进行分割,并通过字符数来测量块长度。要使用该方法,需要先进行导入:

# 如果未安装过该模块,需要先进行安装
pip install -qU langchain-text-splitters

这里先导入一个测试文本:

from langchain.text_splitter import CharacterTextSplitter
# This is a long document we can split up.
with open("./data/reason.txt", encoding="utf-8") as f:
    reason_desc = f.read()

split_text进行文本切分:

  1. chunk_size: 每个块的最大字符数为 100。
  2. chunk_overlap: 相邻两个块之间会有 20 个字符的重叠部分。这是为了确保在处理或分析时,相邻块之间有足够的上下文信息。
#定义的文本分割器实例
text_splitter = CharacterTextSplitter(separator='',
                                     chunk_size = 100, 
                                      chunk_overlap=20,)
text_res = text_splitter.split_text(reason_desc)
len(text_res) #查看切分块的个数
text_res[0],text_res[1],text_res[2] #查看每一块的内容

split_documents进行切分

要使用split_documents方法,需要的是我们使用文档加载器,将str形式的文本数据先转换为Document对象,如下代码所示:

from langchain.document_loaders import TextLoader

docs = TextLoader('./data/reason.txt', encoding="utf-8").load()
#定义的文本分割器实例
text_splitter = CharacterTextSplitter(separator='',
                                     chunk_size = 100, 
                                      chunk_overlap=20,)
text_res = text_splitter.split_documents(docs)

len(text_res) #查看切分块的个数
text_res[0],text_res[1],text_res[2] #查看每一块的内容

split_documentssplit_text定义的文本分割器实例text_splitter参数是一致的。但不同的是,split_documents其接收的是Document对象,返回的chunks也是Docement对象。

通过上述操作过程不难发现,LangChain通过巧妙的设计通过CharacterTextSplitter这一文档分割器就可以通过separatorchunk_sizechunk_overlap参数的灵活组合,实现了我们在前面。

7.6 综合应用

把向量化流程、数据加载和分块策略应用在LangChain的数据处理流中。

首先,我们通过Document Loaders读取到一个外部的.txt文件。

from langchain.document_loaders import TextLoader

docs = TextLoader('./data/Chinese.txt', encoding="utf-8").load()

这份文档中的文本内容覆盖了多个主题,用来增强测试的复杂性。接下来,使用Text Splitters中的RecursiveCharacterTextSplitter进行文本分块:

from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(chunk_size=300, chunk_overlap=0)

docs = text_splitter.split_documents(docs)

#查看每一个chunk的内容
for index, doc in enumerate(docs):
    print(f"Chunk {index + 1}: {doc.page_content}\n")

接下来,通过BaiChuan获取每个Chunk的向量表示:

def baichuan_embedding_by_api(text):
    # 设置API密钥
    key = open('./key_files/baichuan_API-Key.md').read().strip()
    embeddings = BaichuanTextEmbeddings(api_key=key)
    text = text.replace("\n", " ").strip()
    return embeddings.embed_query(text)

embeddings = [baichuan_embedding_by_api(doc.page_content) for doc in docs]

然后,通过如下代码获取到query的向量表示:

query_embedding = baichuan_embedding_by_api("现在科技创新方面有什么进展?")

在有了原始文档和query的向量表示后,我们通过余弦相似度去匹配哪一个Chunk中的内容,与输入的query是最相近的。

import numpy as np

def cosine_similarity(A, B):
    # 使用numpy的dot函数计算两个数组的点积
    # 点积是向量A和向量B在相同维度上对应元素乘积的和
    dot_product = np.dot(A, B)
    
    # 计算向量A的欧几里得范数(长度)
    # linalg.norm默认计算2-范数,即向量的长度
    norm_A = np.linalg.norm(A)
    
    # 计算向量B的欧几里得范数(长度)
    norm_B = np.linalg.norm(B)
    
    # 计算余弦相似度
    # 余弦相似度定义为向量点积与向量范数乘积的比值
    # 这个比值表示了两个向量在n维空间中的夹角的余弦值
    return dot_product / (norm_A * norm_B)
# 计算与查询最相近的文档块
similarities = [cosine_similarity(query_embedding, emb) for emb in embeddings]
max_index = np.argmax(similarities)  # 找到最高相似性的索引

# 打印最相似的文档块
print(f"The most similar chunk is Chunk {max_index + 1} with similarity {similarities[max_index]}:")
print(docs[max_index].page_content)

从输出上看,当query为现在科技创新方面有什么进展?,涉及到原始文档科技创新这一主题时,检索出来的最匹配内容就是存储着科技创新内容的这一个chunk。同样,我们可以继续进行测试,此次提问的query涉及经济问题:

query_embedding = baichuan_embedding_by_api("现在的经济趋势怎么样?")

# 计算与查询最相近的文档块
similarities = [cosine_similarity(query_embedding, emb) for emb in embeddings]
max_index = np.argmax(similarities)  # 找到最高相似性的索引

# 打印最相似的文档块
print(f"The most similar chunk is Chunk {max_index + 1} with similarity {similarities[max_index]}:")
print(docs[max_index].page_content)

对于经济问题,也能够很好的检索出原始文档中存储经济相关内容的chunk,这样的流程从本质上就是RAG检索的过程,只不过,一个应用级的RAG系统仅通过这样的简单设计肯定是不行的,首先,知识库存储的内容不可能这么少,chunks也不可能只有我们示例中的6个,那么当一个用户的query进入到这个RAG系统,query作为一个向量,要去偌大的知识库中(可能有几万、上千万个chunks)中找到与其最接近、内容最相关的问题,这就变成了一个搜索问题。

如果每个都去一一进行比较,这肯定是不现实的,它的时间复杂度会非常高,那有效的解决办法就是向量数据库,所以向量数据库,解决的核心问题是:如何以一种高效的搜索策略快速的返回检索结果。

接下来,我们就详细探讨一下向量数据库的应用方法和使用技巧。

7.7 Vector stores

向量数据库,其解决的就是一个问题:更高效的实现搜索(Search)过程。传统数据库是先存储数据表,然后用查询语句(SQL)进行数据搜索,本质还是基于文本的精确匹配,这种方法对于关键字的搜索非常合适,但对于语义的搜索就非常弱。那么把传统数据库的索引思想引用到向量数据库中,同样是做搜索,在向量数据库的应用场景中就变成了:给定一个查询向量,然后在众多向量中找到最为相似的一些向量返回。

目前市面上充斥着非常多的向量数据库,从整体上可以分为开源和闭源,当然闭源意味着我们需要付费使用,而对于开源的向量数据库来说,可以下载免费使用。通过官方的数据来看,最常用的向量数据库如下:

image-20250320160146328

其中Chroma为LangChain官方主推的向量数据库,因此我们就以Chroma 为示例,尝试一下在LangChain中如何使用集成的向量数据库。Faiss与Chrom的使用方式基本保持一致,所以我们就不再重复的说明,大家可以根据官方文档,结合我们接下来对Chroma的实操自行尝试。

7.7.1 Chroma的使用方法

Chroma 是一家构建开源项目(也称为 Chroma)的公司,其官网:https://www.trychroma.com/

它支持用于搜索、过滤等的丰富功能,并能与多种平台和工具(如LangChain,, OpenAI等)集成。Chroma的核心API包括四个命令,分别用于创建集合、添加文档、更新和删除,以及执行查询。Chroma向量数据库官方原生支持Python和JavaScript,也有其他语言的社区版本支持。所以可以直接通过Python或JS操作,具体的操作文档可查阅其官方:https://docs.trychroma.com/

在使用时,因为Chroma是作为第三方集成,所以需要安装依赖包,执行如下代码:

pip install langchain-chroma

如果安装langchain-chroma报错:

error: Microsoft Visual C++ 14.0 or greater is required. Get it with "Micro
soft C++ Build Tools": https://visualstudio.microsoft.com/visual-cpp-build-
tools/
[end of output]

解决⽅案:需要点击【下载生成工具】进行下载,再执⾏pip install langchain-chroma
下载地址:https://visualstudio.microsoft.com/zh-hans/visual-cpp-build-tools/

image-20250616155936681

具体操作:

加载一个本地的.txt文档

from langchain.document_loaders import TextLoader

raw_documents = TextLoader('./data/sora.txt', encoding="utf-8").load()

接下来,通过文档切割器RecursiveCharacterTextSplitter,将上面完整的Docement对象切分为多个chunks。

from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
    separators=["\n\n", "\n", " ", ""], # 默认
    chunk_size=500, #块长度
    chunk_overlap=20, #重叠字符串长度
    add_start_index=True
)
documents = text_splitter.split_documents(raw_documents)

准备向量模型,这里我们依然使用BaiChuan。

from langchain_community.embeddings import BaichuanTextEmbeddings
import os

# 设置API密钥
key = open('./key_files/baichuan_API-Key.md').read().strip()
embeddings_model = BaichuanTextEmbeddings(api_key=key)

创建 Chroma 数据库实例

from langchain_community.vectorstores import Chroma
#documents:文档将被转换为向量并存储在数据库中
#embeddings_model:向量的嵌入模型
#persist_directory:如果指定路径,向量存储将被持久化到此目录。如果未指定,数据将只在内存中临时存在。
db = Chroma.from_documents(documents, embeddings_model)

使用向量数据库(db)来查找与查询语句 query 相似的文档

query = "什么是Sora"
#在数据库中进行相似性搜索
#通过关键词k,可以设置返回多少个在查询过程中与Query最接近的Chunks
docs = db.similarity_search(query,k=2)
print(docs[0].page_content)

query = "Sora在训练时消耗了多少算力?"
docs = db.similarity_search(query)
print(docs[0].page_content)

在上⼀个示例的基础上,如果您想要保存到磁盘,只需初始化 Chroma 客户端并传递要保存数据的目录。

# 保存到磁盘
db2 = Chroma.from_documents(documents,embeddings_model,persist_directory="./chroma_db")
docs = db2.similarity_search(query)
                            
# 从磁盘加载
db3 = Chroma(persist_directory="./chroma_db", embeddings_model)
docs = db3.similarity_search(query)
print(docs[0].page_content)

在构建实际应用程序时,除了添加和检索,非常多的情况下还需要更新和删除数据,这就需要借助到Chroma类定义的 ids 参数,它可以传入文件名或任意的标识。我们需要先根据分成Chunks构建起唯一的对应id。

import uuid
ids = [str(uuid.uuid4()) for _ in documents]
new_db = Chroma.from_documents(documents, embeddings_model,ids=ids)

接着,执行update_document方法进行更新,如下所示:

new_db.update_document(ids[0], docs[0])

与任何其他数据库一样,在向量数据库中,也可以使用.add.get.update .delete等方法,但如果想直接访问,需要执行._collection.method()。所以我们可以通过如下的代码形式,查看更新后的内容:

print(new_db._collection.get(ids=[ids[0]]))

当然,也可以直接进行删除操作,在删除之前,先看一下有多少个Chunks,代码如下所示:

print(new_db._collection.count())

删除最后一个chunk

new_db._collection.delete(ids=[ids[-1]])

再次查看存储的总Chunks数

print(new_db._collection.count())

拓展:MMR

MMR(Maximal Marginal Relevance,最大边际相关性)是一种信息检索和文本摘要技术,用于在选择文档或文本片段时平衡相关性和多样性。其主要目的是在检索结果中既包含与查询高度相关的内容,又避免结果之间的高度冗余。因此MMR的作用就是:

  • 提高结果的多样性:通过引入多样性,MMR可以避免检索结果中出现重复信息,从而提供更全面的答案。
  • 平衡相关性和新颖性:MMR在选择结果时,既考虑与查询的相关性,也考虑新信息的引入,以确保结果的多样性和覆盖面。
  • 减少冗余:通过避免选择与已选结果高度相似的文档,MMR可以减少冗余,提高信息的利用效率。

MMR使用流程:

  • 计算相关性:首先,计算每个候选文档与查询的相似性得分。
  • 计算多样性:然后,计算每个候选文档与已选文档集合的相似性得分。
  • 选择文档:在每一步选择一个文档,使得该文档在相关性和多样性之间达到最佳平衡。
retriever = db.as_retriever(search_type="mmr")
retriever.invoke(query)[0]

7.7.2 Faiss的使用(拓展)

Faiss 是由 Facebook 团队开源的向量检索工具,专为高维空间的海量数据提供高效、可靠的相似性检索方案。Faiss 支持 Linux、macOS 和 Windows 操作系统,在处理百万级向量的相似性检索时,Faiss 可以在牺牲一定搜索准确度的情况下,实现小于 10ms 的响应时间。

集成位于 langchain-community 包中。我们还需要安装 faiss 包本身。

pip install -U faiss-cpu tiktoken

如果您想使用启用了 GPU 的版本,也可以安装 faiss-gpu 。

from langchain_community.vectorstores import FAISS
db = FAISS.from_documents(docs, embeddings_model)
query = "Pixar公司是做什么的?"
docs = db.similarity_search(query)
print(docs[0].page_content)

MMR使用:

retriever = db.as_retriever()
docs = retriever.invoke(query)
print(docs[0].page_content)

您还可以保存和加载 FAISS 索引。这样做很有用,因为您不必每次使用时都重新创建它。

#保存索引
db.save_local("faiss_index")
#读取索引
new_db = FAISS.load_local("faiss_index", embeddings_model,allow_dangerous_deseria
lization=True)
#进行检索
docs = new_db.similarity_search(query)

Faiss与Chroma使用场景的区别

  1. 数据规模和性能需求:

    • Faiss:更适合处理大规模数据,尤其是在需要利用GPU加速来提高搜索性能的场景下表现出色。例如在处理海量的图像特征向量、大规模的文本嵌入向量等场景中,Faiss能够快速地进行相似性搜索,满足对实时性和高性能的要求。
    • Chroma:适用于中小规模数据或对性能要求不是特别极致的场景。虽然Chroma也具有一定的性能优化,但在处理超大规模数据时,其性能可能受限于硬件资源,不过对于一般的小型项目或原型开发来说已经足够。
  2. 开发和集成难度:

    • Faiss:需要开发者对向量检索算法和索引结构有一定的了解,手动管理索引的创建、训练和持久化等操作,开发和集成难度相对较大。但它的灵活性也使得在一些特定场景下可以根据需求进行深度定制。
    • Chroma:提供了更简单的API和更便捷的使用方式,开箱即用,类似于一个完整的数据库,对于开发者来说更容易上手和使用,能够快速集成到各种应用中,特别适合快速原型开发和那些对数据库内部细节不太关注的应用场景。

day26大模型程序开发day01-Functioncalling技术栈

文件: day26大模型程序开发day01-Functioncalling技术栈\大模型应用开发Functioncalling.md

有言在先

  • 授课风格:
    • 没有鸡汤、没有段子。有认真负责的态度、有传道受业解惑的育人之志启智之能
  • 学习方法
    • 课上动脑听、课下用心练、笔记常翻译、消化在心中
  • 授课相关:好的授课过程就好比是谈一场恋爱,从来都不是一个人的事
    • 节奏快慢
    • 内容动态补充
    • 授课内容+顺序
    • ......
  • Finally:
    • 人无完人,每个人都有不同的短板和不足之处。互相理解、互相适应、搞定AI、成就大计!

核心内容

  • Functioncalling技术实现
  • Streamlit+大模型应用
  • RAG+Agent+Streamlit项目实战
  • MCP深度应用
  • OpenAI Agents SDK(openai在25年推出的企业级Multi-Agent开发框架)
    • Agents SDK接入MCP
  • 模型本地部署+调用(ollama+企业级)
  • 模型微调
  • 微软-GraphRAG应用开发

4、Function Calling

4.2 Function Calling简介

4.2.1 背景和定义

我们都知道大语言模型的知识储量是巨大,并且它具备非常强大的原生能力,但是有时候我们在实际使用大模型时会感受到大模型能力上的某些局限,比如大模型无法回答超过大模型知识库截止日期之后发生的相关信息和知识,并且大模型只能给出文字的建议但无法直接帮我们解决某些实际操作性的问题(如自动进行邮件收发、自动预定车票等)。因此,这些问题的存在会极大程度上限制了大模型的实际应用价值。

在这一基本背景下,Function calling功能应运而生。该功能的本质是让大模型具有调用外部函数的能力。也就是说,当大模型遇到超出自身能力范围的需求时,可以通过访问相应的外部函数寻求解决方案。这样,大模型就可以不再仅仅根据自身的知识库进行回答,还可以额外挂载一个外部函数库,然后根据用户提问进行外部函数的检索,根据实际需求调用外部函数并获取函数运行结果,再基于函数运行结果进行回答。

毫无疑问,有了外部函数库的功能加持,大模型的处理和解决问题的能力也必将再上一个台阶。

4.2.2 核心原理

Snipaste_2025-02-21_09-18-50

在Agent中的应用

Snipaste_2025-02-21_09-20-27

4.3 实时气象查询Agent开发

4.3.1 项目背景

OpenWeather是一家提供全球范围内的气象数据服务的公司,该公司的服务包括实时天气信息、天气预报、历史天气数据以及各种气象相关的报告等,并且OpenWeather开放了一定使用限度内完全免费的API,即我们可以在代码环境中通过调用OpenWeather API来进行实时天气查询、天气预报等功能,这意味着开发者可以将OpenWeather的天气预报功能加入到他们自己的应用或网站中。

4.3.2 OpenWeather注册及API key获取方法

为了能够调用OpenWeather服务,和OpenAI的API使用过程类似,我们首先需要先注册OpenWeather账号,并获取OpenWeather API Key。这里需要注意的是,对于大多数在线服务的API来说,都需要通过API key来进行身份验证,尽管OpenWeather相对更加Open,有非常多的免费使用的次数,但身份验证仍然是必要的防止API被滥用的有效手段。OpenWeather API key获取流程如下:

  • Step 1.登录OpenWeather官网并点击Sign—>create account完成注册。该网站无需魔法即可直接登录,可以使用国内邮箱或者QQ邮箱均可进行注册,官网地址为:https://openweathermap.org/

    Snipaste_2025-02-21_09-26-52 Snipaste_2025-02-21_09-26-59
  • Step 2.获取API-key:注册完成后,即可在API keys页面查看当前账户的API key:

    Snipaste_2025-02-21_09-27-08 Snipaste_2025-02-21_09-27-15

    一般来说完成注册后,就会有一个已经激活的API-key。和OpenAI一样,OpenWeather的API key也可以创建多个。

  • Step 3.将其设置为环境变量:和OpenAI API key类似,为了方便后续调用,我们也可以直接将OpenWeather API key设置为环境变量,变量名为OPENWEATHER_API_KEY。具体设置环境变量的方法参考Ch.1中OpenAI APkey设置环境变量流程,此处不再赘述。

    Snipaste_2025-02-21_09-27-22

    设置完了环境变量之后,接下来即可按照如下方式创建OpenWeather API key变量:

    open_weather_key = os.getenv("OPENWEATHER_API_KEY")
    

4.3.3 获取实时天气信息API

import requests

# Step 1.构建请求
url = "https://api.openweathermap.org/data/2.5/weather"

# Step 2.设置查询参数
params = {
    "q": "Beijing",               # 查询北京实时天气
    "appid": "xxx",    # 输入API key
    "units": "metric",            # 使用摄氏度而不是华氏度
    "lang":"zh_cn"                # 输出语言为简体中文
}

# Step 3.发送GET请求
response = requests.get(url, params=params)

# Step 4.解析响应
data = response.json()
print(data)

# 即时温度最高、最低气温
data['main']['temp_min'], data['main']['temp_max']
# 天气状况
data['weather'][0]['description']

这里需要注意的是,城市名必须输入英文名,否则无法正确识别。

外部函数创建:我们尝试编写一个通过OpenWeather API实时获取天气信息的API,并作为模型可调用的外部函数之一。很明显,为了确保和大语言模型之间的顺畅通信,此时要求函数的输入和输出都是字符串格式。具体函数编写如下:

这里需要注意函数说明和参数解释的书写风格

def get_weather(loc):
    """
    查询即时天气函数
    :param loc: 必要参数,字符串类型,用于表示查询天气的具体城市名称,\
    注意,中国的城市需要用对应城市的英文名称代替,例如如果需要查询北京市天气,则loc参数需要输入'Beijing';
    :return:OpenWeather API查询即时天气的结果,具体URL请求地址为:https://api.openweathermap.org/data/2.5/weather\
    返回结果对象类型为解析之后的JSON格式对象,并用字符串形式进行表示,其中包含了全部重要的天气信息
    """
    # Step 1.构建请求
    url = "https://api.openweathermap.org/data/2.5/weather"

    # Step 2.设置查询参数
    params = {
        "q": loc,               
        "appid": 'xxx',    # 输入API key
        "units": "metric",            # 使用摄氏度而不是华氏度
        "lang":"zh_cn"                # 输出语言为简体中文
    }

    # Step 3.发送GET请求
    response = requests.get(url, params=params)
    
    # Step 4.解析响应
    data = response.json()
    return json.dumps(data)

函数测试:

#测试函数是否可用
import json
get_weather('GuangZhou')

邮件发送外部函数:

def send_mail(msg):
    """
    该函数是用于进行指定邮件发送的。
    :param msg: 必要参数,字符串类型,该参数表示要发送的邮件内容
    :return:邮件发送成功后的状态信息
    """
    return '邮件已经成功发送,邮件内容是:'+msg

4.3.4 tools参数解释与定义

在准备好外部函数之后,接下来非常重要的一步就是需要将外部函数的信息以某种形式传输给模型。此时就需要使用到create函数的tools参数.从参数的具体形式来看,tools参数是一个可以包含多个字典的list,每个字典都需要包含两个键值对,分别是 {type:function,function:外部函数的完整描述}。因此每个字典都代表一个外部函数的相关信息。在大语言模型实际进行问答时,会根据tools参数提供的信息对各外部函数进行检索。

tools = [
    {
        "type": "function", 
        "function":'外部函数的完整描述'
    }
]

4.3.5 外部函数完整描述

外部函数的完整描述对于Function calling功能的实现至关重要。因为在大模型进行实际问答时,会根据对外部函数的完整描述信息的语义理解进行外部函数的检索和调用。

接下来我们详细解释外部函数完整描述的指定写法。总的来说,我们会使用一个字典来对其进行完整描述,每个字典都有三个参数(三组键值对),各参数(Key)名称及解释如下:

  • name:代表函数函数名称字的符串,必选参数。
  • description:用于描述函数功能的字符串,虽然是可选参数,但该参数传递的信息实际上是Chat模型对函数功能识别的核心依据。即Chat函数实际上是通过每个函数的description来判断当前函数的实际功能的。
  • parameters:函数的参数说明,必选参数,要求遵照JSON Schema格式进行输入,JSON Schema是一种特殊的JSON对象,专门用于验证JSON数据格式是否满足要求。

例如,对于get_weather函数,我们需要创建如下字典来对其进行完整描述:

get_weather_function = {
    'name': 'get_weather',
    'description': '查询即时天气函数,根据输入的城市名称,查询对应城市的实时天气',
    'parameters': {
        'type': 'object', #json对象类型
        'properties': { #参数成员描述
            'loc': {
                'description': "城市名称,注意,中国的城市需要用对应城市的英文名称代替,例如如果需要查询北京市天气,则loc参数需要输入'Beijing'",
                'type': 'string'
            }
        },
        'required': ['loc']
    }
}

send_mail_function = {
    'name': 'send_mail',
    'description': '该函数是用于进行指定邮件发送的',
    'parameters': {
        'type': 'object', #json对象类型
        'properties': { #参数成员描述
            'msg': {
                'description': "必要参数,字符串类型,该参数表示要发送的邮件内容",
                'type': 'string'
            }
        },
        'required': ['msg']
    }
}

因此修改tool参数值为:

tools = [
    {
        "type":'function',
        'function':get_weather_function
    },
    {
        "type":'function',
        'function':send_mail_function
    }
]

同时还需要封装外部函数库,用于关联外部函数名称和外部函数对象

available_functions = {
            "get_weather": get_weather,
            "send_mail":send_mail
        }

4.3.6 Function calling实现

First response

在进行了一系列基础准备工作之后,接下来我们尝试在Chat模型对话执行Function calling功能。首先我们测试模型本身能否知道如何查询天气:

from openai import OpenAI
#硅基流动API
ds_api_key = "sk-atisrxxxriejlfxlvnymvfxoesps"
client = OpenAI(api_key=ds_api_key, 
                base_url="https://api.siliconflow.cn/v1")
messages = [
        {"role": "user", "content": "请帮我查询上海地区今日天气情况"}
    ]
response = client.chat.completions.create(
    model="deepseek-ai/DeepSeek-V2.5",  
     messages=messages, 
    )
        
response.choices[0].message.content

很明显,模型无法进行回答。接下来我们尝试将函数库相关信息输入给Chat模型

response = client.chat.completions.create(
    model="deepseek-ai/DeepSeek-V2.5",  
    messages=[
        {"role": "user", "content": "请帮我查询北京地区今日天气情况"}
    ],
    tools=tools,
)

        
response_message = response.choices[0].message
response_message

返回结果:

ChatCompletionMessage(content='', refusal=None, role='assistant', audio=None, function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='0195279d5095aab2d4ac52760de27c04', function=Function(arguments='{"loc":"Beijing"}', name='get_weather'), type='function')])

能够发现,此时返回的message中content为空,而增了一个"tool_calls"的list,该list就包含了当前调用外部函数的全部信息:

response_message.tool_calls[0]

返回结果:

ChatCompletionMessageToolCall(id='0195279d5095aab2d4ac52760de27c04', function=Function(arguments='{"loc":"Beijing"}', name='get_weather'), type='function')

对于当前CompletionMessageToolCall对象,id为外部函数调用发起请求id,function则表示调用外部函数基本信息,而type则代表了当前当前调用外部函数类型,function代表调用自定义的外部函数。

我们可以在此基础上分别提取调用外部函数名称信息和参数信息,分别保存为function_name和function_args对象:

# 完成对话需要调用的函数名称
function_name = response_message.tool_calls[0].function.name
function_name

# 基于外部函数库获取具体的函数对象
fuction_to_call = available_functions[function_name]
fuction_to_call

'''
        available_functions = {
                    "get_weather": get_weather,
                }
'''

# 执行该函数所需要的参数,将其反序列化成字典对象,便于下一步函数调用时进行传输传递
function_args = json.loads(response_message.tool_calls[0].function.arguments)
function_args

需要注意的是,外部函数的计算过程仍然是在本地执行,即Chat模型并不会将代码读取到服务器上再进行在线计算,因此接下来我们需要根据模型返回的函数和函数参数,在本地完成函数计算,然后再将计算过程和结果保存为message并追加到messages后面,并第二次调用Chat模型分析函数的计算结果,并最终根据函数计算结果输出用户问题的答案。

Second response
function_response  = fuction_to_call(**function_args) #get_weather(loc="Beijing")
function_response  #获取函数调用结果

能够发现,模型已经顺利完成计算。接下来我们在messages对象中追加两条消息,第一条消息是第一次模型返回的结果(即调用模型的assistant message),第二条消息则是外部函数计算结果,该条消息的role为function,且name为函数名称。这也是我们首次接触function message,和user、system、assistant message不同,function message必须要输入关键词name,且function message的内容源于外部函数执行的计算结果,并且需要手动进行输入。具体添加过程如下:

  • 追加第一条消息:模型返回的结果

    #将模型第一次返回的结果转换成字典类型,目的是为了将其追加到messages列表中
    response_message.model_dump()
    # 向messages追加第一次模型返回结果消息
    messages.append(response_message.model_dump())  
    #查看追加后的messages
    print(messages)
    
  • 追加第二条消息:外部函数计算结果

    # 追加function返回消息
    messages.append({
                "role": "tool",
                "content": function_response,
                "tool_call_id":response_message.tool_calls[0].id
            })
    

接下来,再次调用Chat模型来围绕messages进行回答。需要注意的是,此时我们不再需要向模型重复提问,只需要简单的将我们已经准备好的messages传入Chat模型即可:

second_response = client.chat.completions.create(
    model="deepseek-ai/DeepSeek-V2.5",
    messages=messages)

second_response.choices[0].message.content

能够发现,模型最终做出了准确回答。

完整封装
def run_conv(messages, 
             api_key,
             tools=None, 
             functions_list=None,
             model="deepseek-ai/DeepSeek-V2.5"):
    """
    能够自动执行外部函数调用的Chat对话模型
    :param messages: 必要参数,输入到Chat模型的messages参数对象
    :param api_key: 必要参数,调用模型的API-KEY
    :param tools: 可选参数,默认为None,可以设置为包含全部外部函数的列表对象
    :param model: Chat模型,可选参数,默认模型为deepseek-chat
    :return:Chat模型输出结果
    """
    user_messages = messages
    #基于硅基流动API
    client = OpenAI(api_key=api_key, 
                base_url="https://api.siliconflow.cn/v1")

    # 如果没有外部函数库,则执行普通的对话任务
    if tools == None:
        response = client.chat.completions.create(
            model=model,  
            messages=user_messages
        )
        final_response = response.choices[0].message.content

    # 若存在外部函数库,则需要灵活选取外部函数并进行回答
    else:
        # 创建外部函数库字典
        available_functions = {func.__name__: func for func in functions_list}

        # 创建包含用户问题的message
        messages = user_messages

        # first response
        response = client.chat.completions.create(
            model=model,  
            messages=user_messages,
            tools=tools,
        )
        response_message = response.choices[0].message

        # 获取函数名
        function_name = response_message.tool_calls[0].function.name
        # 获取函数对象
        fuction_to_call = available_functions[function_name]
        # 获取函数参数
        function_args = json.loads(response_message.tool_calls[0].function.arguments)

        # 将函数参数输入到函数中,获取函数计算结果
        function_response = fuction_to_call(**function_args)

        # messages中拼接first response消息
        user_messages.append(response_message.model_dump())  

        # messages中拼接外部函数输出结果
        user_messages.append(
            {
                "role": "tool",
                "content": function_response,
                "tool_call_id":response_message.tool_calls[0].id
            }
        )

        # 第二次调用模型
        second_response = client.chat.completions.create(
            model=model,
            messages=user_messages)

        # 获取最终结果
        final_response = second_response.choices[0].message.content

    return final_response

无需外部函数加持的情况:

messages = [{"role": "user", "content": "请问什么是机器学习?"}]
run_conv(messages=messages, 
         api_key = ds_api_key)

需要外部函数加持的情况:

messages = [{"role": "user", "content": "请问北京今天天气如何?"}]
run_conv(messages=messages, 
         api_key = ds_api_key,
         tools=tools, 
         functions_list=[get_weather])

4.4 sql解释器Agent开发

4.4.1 项目背景

在平常的工作中,会经常对数据库中的数据进行相关的读写操作,这是一些繁杂的sql语句的编写就会尤为的麻烦也非常容易出错。尤其是在数据分析的一些业务场景中,更是需要频繁的进行数据库的相关操作且高频的编写一些对应的sql代码。

那么我们是否可以利用大模型本身的编码能力帮我们根据相关的自然语言的指令自动进行sql的编写和运行呢?

这一小节,我们就一起来学习,如何将大模型接入到本地数据库中,让大模型帮我们生成对应的sql且在本地数据库环境中进行sql的运行,将结果再次经过大模型的语义理解能力和文字生成能力进行润色后返回!

4.4.2 数据字典

对于大多数企业来说,都会围绕各关键数据集制作数据字典。所谓数据字典,指的是一份记录了每个数据集详细信息的文档,有的时候数据字典也可以以表格形式呈现。借助数据字典,开发/数据分析人员能够快速了解数据表中的各项关键信息。

那么,为了让大模型可以更好的理解数据库中的数据,我们也可以给大模型制作一个数据字典,让大模型可以更好的理解数据,返回更加具有针对性的结果。

import os
from openai import OpenAI
from IPython.display import display, Code, Markdown

#硅基流动API
ds_api_key = "sk-atisrejlfxlvnymvfxoesps"
client = OpenAI(api_key=ds_api_key, 
                base_url="https://api.siliconflow.cn/v1")
# 打开并读取Markdown文件
with open('./data/LC数据字典.md', 'r', encoding='utf-8') as f:
    md_content = f.read()
    
len(md_content)
#基于md_content作为模型背景信息,向模型进行相关提问
response = client.chat.completions.create(
    model="deepseek-ai/DeepSeek-V2.5", 
    messages=[
        {"role": "system", "content": md_content}, 
        # "content": '请帮我统计下LC数据表一共有哪些字段?共计多少个?'
        {"role": "user", "content": '请帮我介绍下LC数据表'}
    ],
)
display(Markdown(response.choices[0].message.content))

4.4.3 Function calling实现

创建生成SQL语句的外部函数
def get_sql_result(sql_query):
    """
    查询数据库相关数据的函数
    :param sql_query: 必要参数,字符串类型,用于表示查询数据的sql语句;
    :return:sql_query表示的sql语句查询到的结果;
    """
    connection = pymysql.connect(
            host='localhost',  # 数据库地址
            user='root',  # 数据库用户名
            passwd='boboadmin',  # 数据库密码
            db='testdb',  # 数据库名
            charset='utf8'  # 字符集选择utf8
        )
    
    try:
        with connection.cursor() as cursor:
            # SQL查询语句
            sql = sql_query
            cursor.execute(sql)

            # 获取查询结果
            results = cursor.fetchall()

    finally:
        connection.close()
    
    
    return json.dumps(results)
自动生成外部函数描述信息
import inspect
import json
import os
from openai import OpenAI
import pymysql
from IPython.display import display, Code, Markdown

#用于自动生成外部函数描述信息
def auto_function_desc(function): #参数为外部函数对象
    #定义一个内部函数用于生成外部函数的完整描述信息
    def inner(function):
        function_description = inspect.getdoc(function)#外部函数的函数说明
        function_name = function.__name__ #外部函数名
        
        system_prompt = '以下是某的函数说明:%s' % function_description
        
        user_prompt = '根据这个函数的函数说明,请帮我创建一个JSON格式的字典,这个字典有如下5点要求,请你仔细阅读,并且务必遵从所有要求:\
                       1.字典总共有三个键值对;\
                       2.第一个键值对的Key是字符串name,value是该函数的名字:%s,也是字符串;\
                       3.第二个键值对的Key是字符串description,value是该函数的函数的功能说明,也是字符串;\
                       4.第三个键值对的Key是字符串parameters,value是一个JSON Schema对象,用于说明该函数的参数输入规范。\
                       5.输出结果必须是一个JSON格式的字典,并且一定不要任何前后修饰语句,务必参按照如下格式进行输出:%s' % (function_name,'{key:value}')
        
        api_key = "xxx"
        client = OpenAI(api_key=ds_api_key, 
                base_url="https://api.siliconflow.cn/v1")
        response = client.chat.completions.create(
        model="deepseek-ai/DeepSeek-V2.5",  
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt}

             ]
        )

        return json.loads(response.choices[0].message.content)
    #由于模型根据提示信息生成的外部函数完整信息可能会有问题,因此,如果出现问题则loads环节会报错,则要求模型重新进行生成
    max_try_count = 5 #模型调用的最大次数
    count = 0 #当前调用模型的次数
    while count < max_try_count:
        try:
            function_desc = inner(function)
            break
        except Exception as e:
            count += 1
            print('something error:',e)
            if count == max_try_count:
                print('模型达到最大尝试次数,程序停止!')
                raise
            else:
                print('模型重新生成中......')
    tools = [
    {
        "type": "function", 
        "function":function_desc
    }
]
    return tools

测试:

auto_function_desc(get_sql_result)

输出:

[
    {
        'type': 'function',
        'function': {
            'name': 'get_sql_result',
  		   'description': '查询数据库相关数据的函数',
           'parameters': {
               'type': 'object',
               'properties': {
                   'sql_query': {
                       'type': 'string',
                       'description': '用于表示查询数据的sql语句'
                   }
               },
         'required': ['sql_query']}}}
]
sql解释器封装
#available_functions表示外部函数库
def auto_run_conversation(messages,available_functions=None):
    api_key = "sk-atisrrfnrxsnulmuvlzqnvuvcglkriejlfxlvnymvfxoesps"
    client = OpenAI(api_key=ds_api_key, 
                base_url="https://api.siliconflow.cn/v1")
    
    # 如果没有外部函数库,则执行普通的对话任务
    if available_functions == None:
        print('模型原生能力解决该提问.........')
        response = client.chat.completions.create(
            model="deepseek-ai/DeepSeek-V2.5",  
            messages=messages
        )
        final_response = response.choices[0].message.content
    else:
        #外部函数库定义
        available_functions = available_functions
        
       #step_3:外部函数完整描述定义 + #step_4:tools参数值定义
        tools = auto_function_desc(available_functions['function'])

        #step_5:第一次模型调用
        response = client.chat.completions.create(
            model="deepseek-ai/DeepSeek-V2.5",  
            messages=messages,
            tools=tools,
        )
        response_message = response.choices[0].message
        
        #判断返回结果是否存在tool_calls,即判断是否需要调用外部函数来回答问题
        if response_message.tool_calls:
            print('function_calling解决该提问.........')
            sql = response_message.tool_calls[0].function.arguments
            print('生成的sql为::',sql)
            choose = input('是否执行上述sql? y/n')
            if choose == 'n':
                print("您选择不执行sql语句,再见!")
                return
            #step_6:外部函数手动调用且获取调用结果
            fuction_to_call = available_functions['function'] #函数对象
            function_args = json.loads(response_message.tool_calls[0].function.arguments)#函数参数

            function_response = fuction_to_call(**function_args)#函数手动调用

            #step_7:向messages进行两次消息追加
            messages.append(response_message.model_dump())  
            messages.append({
                        "role": "tool",
                        "content": function_response,
                        "tool_call_id":response_message.tool_calls[0].id
                    })

            #step_8: 再次调用大模型
            second_response = client.chat.completions.create(
                model="deepseek-ai/DeepSeek-V2.5",
                messages=messages)
            final_response = second_response.choices[0].message.content
        else:
            final_response = response_message.content
    return Markdown(final_response)

测试:

messages = [
    {"role": "system", "content": md_content},
    {"role": "user", "content": "请问LC数据表有多少男性用户?"}
]
#定义外部函数库
available_functions = {
            "function": get_sql_result,
        }
auto_run_conversation(messages,available_functions)

day27大模型程序开发day02-sql解释器+streamlit基础

文件: day27大模型程序开发day02-sql解释器+streamlit基础\LC数据字典.md

数据库数据字典

本数据字典记录了testdb数据库中LC数据表的基本情况。

LC数据表

  • 基本解释

    LC数据表记录了拍拍贷互联网金融公司在2015年1月1日到2017年1月30日期间共计30余万条的贷款用户的相关信息。用户的信息维度比较广泛,大致可分为用户的基本信息、认证信息、信用信息和贷款信息等。

    希望通过该数据表中的数据来分析得出逾期用户的特征,及公司核心业务标的,达到降低业务逾期风险,增加业务收入的目的。

  • 数据来源

    LC数据集由拍拍贷平台进行的采集和记录,并且通过回访确认相关信息,数据集的准确性和可信度都非常高。

  • 各字段说明

Column Name Description Value Range Type
序号 贷款用户的唯一标识 INT
借款金额 借款成交总金额 FLOAT
初始评级 借款成交时的信用评级 A,B,C,D,E,F VARCHAR(255)
借款类型 借款的具体类型 电商,APP闪电,普通和其他 VARCHAR(255)
年龄 借款人在借款成功时的年龄 INT
性别 借款人性别 男,女 VARCHAR(255)
户口认证 该借款人户口认证是否成功 未成功认证,成功认证 VARCHAR(255)
征信认证 该借款人征信认证是否成功。成功则表示有人行征信报告 未成功认证,成功认证 VARCHAR(255)
总待还本金 借款人在借款成交之前待还本金金额 FLOAT
历史正常还款期数 借款人在借款成交之前的按期还款期数 INT
历史逾期还款期数 借款人在借款成交之前的逾期还款期数 INT

文件: day27大模型程序开发day02-sql解释器+streamlit基础\Streamlit数据看板.md

Streamlit

简介

什么是streamlit

Streamlit是一个免费的开源框架,用于快速构建和共享漂亮的数据科学Web应用程序。它是一个基于Python的库,专为机器学习工程师设计。数据分析工程师不是网络开发人员,他们对花几周时间学习使用这些框架来构建网络应用程序不感兴趣。相反,他们需要一个更容易学习和使用的工具,只要它可以显示数据并收集分析/建模所需的参数。Streamlit允许您仅用几行代码创建一个外观惊艳的应用程序。

参考文档:http://cw.hubxwiz.com/card/c/streamlit-manual/

数据科学家为何要使用Streamlit?

Streamlit最大的好处是,您甚至不需要了解Web开发的基础知识就可以开始或创建您的第一个Web应用程序。因此,如果你是一个对数据科学感兴趣的人,你想轻松、快速地部署你的模型,并且只需要几行代码,Streamlit是一个很好的选择。

优势:

  • 不需要具备前端知识即可应用streamlit。
  • 学习成本极低
    • 你不需要花费几天或几个月的时间来创建一个Web应用,你可以在几个小时甚至几分钟内创建一个非常漂亮的机器学习或数据科学应用。
  • 它兼容大多数Python库
    • 例如panda、matplotlib、seaborn、plotly、Keras、PyTorch等。

环境安装

pip install streamlit

#测试安装是否正常:cmd环境下运行
streamlit hello

程序运行

streamlit run xxx.py

具体操作

1.write()函数

可以通过该函数向看板上输出显示指定内容

import pandas as pd
import streamlit as st

st.write("1. write()函数基本操作")
st.write(pd.DataFrame({
    '第一列': [1,2,3,4,5],
    '第二列': [6,7,8,9,10]}
))

2.滑块组件slider

"slider"的中文意思是"滑块"。它是一种用户界面元素,通常用于选择一个数值范围或从给定选项中选择一个值。滑块的外观通常是一个可拖动的滑块,用户可以通过移动滑块来选择所需的值。滑块可以在许多应用程序和网页中使用,例如调整音量、选择年龄范围或设置某个参数的值。

import streamlit as st

st.write("st.slider()滑块")
#slider参数为滑块自定义名称,返回值为滑动到的数值
num = st.slider("num")
st.write(num, "squred is", num*num)

3.文本框操作text_input

import streamlit as st

st.write("文本框操作")
#文本框输入,回车结束
st.text_input("your name", key="name")
st.text_input("your age", key="age")

# 显示输入的值
st.write(st.session_state.name,st.session_state.age)

密码框:

import streamlit as st

# 创建一个文本输入框,并将其类型设置为密码
password = st.text_input("请输入密码", type="password")

# 检查密码是否正确(这里假设正确密码是"123456")
if st.button("登录"):
    if password == "123456":
        st.success("登录成功!")
    else:
        st.error("密码错误,请重试。")

4.多选框checkbox

import streamlit as st
import pandas as pd
import numpy as np

st.write("checkbox()多选框")
# 点击checkbox后返回True,未点击为False
ex1 = st.checkbox('显示/不显示 表格')
if ex1:
    df = pd.DataFrame(
        np.random.randn(20, 3),
        columns=['a', 'b', 'c']
    )
    st.write(df)

ex2 = st.checkbox('显示/不显示 滑块')
if ex2:
    x = st.slider('x')

5.下拉框selectbox

import streamlit as st

#返回值为选中的内容信息
option = st.selectbox(
    label='请选择省份信息:',
    options=['河北','山东','河南','吉林']
)

st.write("您选择的是: ", option)

6.侧边栏sidebar

import streamlit as st

#侧边栏下拉框
add_selectbox = st.sidebar.selectbox(
    label="通讯方式选项",
    options=('微信','QQ','手机','邮件')
)
#获取下拉选项
st.write("下拉选项: ", add_selectbox)

#侧边栏滑块
add_slider = st.sidebar.slider(
    label="选择一个范围的值",
    min_value=0.0, max_value=100.0, value=(25.0, 75.0)
)
#获取滑块的值
st.write("值的范围: ", add_slider)

7.单选按钮radio

import streamlit as st

#columns参数表示列数
left_column, right_column = st.columns(2)
# 左边列设置
with left_column:
    #返回值为选中的选项值
    chosen = st.radio(
        label='电脑品牌',
        options=('苹果','华为','小米')
    )
    st.write(f'你选择的品牌是: {chosen}')
    
# 右边列设置
with right_column:
    # 返回值为选中的选项值
    chosen = st.radio(
        label='手机品牌',
        options=('苹果','华为','小米')
    )
    st.write(f'你选择的品牌是: {chosen}')

8.进度条progress

import streamlit as st
import time
st.write("模拟长时间的计算...")

# 创建一个动态显示数据的容器,用于动态显示进度条的进度数值
value = st.empty()
#创建进度条,进度条初始值为0
bar = st.progress(0)
for i in range(100):
    #这是动态显示的数值
    value.text(f'Iteration {i+1}')
    # 更新进度条
    bar.progress(i+1)
    time.sleep(0.1)
st.write('运行结束!')

9.文件上传

上传penguins.csv文件,然后选择不同的两个企鹅特征,用散点图观察其分布形式。

import streamlit as st
import pandas as pd
st.write('上传文件')

#创建文件上传组件,如果上传失败则返回None
upload_file = st.file_uploader(
    label = "上传数据集CSV文件" #自定义文件上传提示信息
)

#判断上传文件是否成功
if upload_file is not None:
    df = pd.read_csv(upload_file)
    st.write('显示前5行数据:',df.head(5))
    st.success("上传文件成功!")
else:
    st.stop() # 退出

布局

1.st.sidebar - 在侧边栏增添交互元素

import streamlit as st

# 方式1:使用对象表示法添加选择框
add_selectbox = st.sidebar.selectbox(
    "您希望如何联系您?",
    ("电子邮件", "家庭电话", "移动电话")
)
# 方式2:使用“with”语法添加单选按钮
with st.sidebar:
    add_radio = st.radio(
        "选择一种运输方式",
        ("标准(5-15天)", "快递(2-5天)")
    )

2.st.columns - 并排布局多元素容器

通过调用 st.columns,您可以插入多个多元素容器,并将它们布局为并排的形式。返回的是一个容器对象的列表,每个对象都可以用来添加元素。您可以选择使用“with”语法(更推荐)或者直接在容器对象上调用方法来添加元素。

import streamlit as st

col1, col2, col3 = st.columns(3)

with col1:
   st.header("一只猫")
   st.image("https://static.streamlit.io/examples/cat.jpg")

with col2:
   st.header("一只狗")
   st.image("https://static.streamlit.io/examples/dog.jpg")

with col3:
   st.header("一只猫头鹰")
   st.image("https://static.streamlit.io/examples/owl.jpg")

或者您也可以直接在容器对象上调用方法:

import streamlit as st
import numpy as np

col1, col2 = st.columns([3, 1])
data = np.random.randn(10, 1)

col1.subheader("一个宽容器,含有图表")
col1.line_chart(data)

col2.subheader("一个窄容器,含有数据")
col2.write(data)

3.st.tabs - 以选项卡形式布局多元素容器

通过调用 st.tabs,您可以插入多个多元素容器作为选项卡。每个选项卡都代表一组相关内容。返回的是一个容器对象的列表,每个对象都可以用来添加元素。与之前一样,您可以选择使用“with”语法或者直接在容器对象上调用方法来添加元素。

需要注意的是,每个选项卡的所有内容都会被一次性发送并渲染在前端。

import streamlit as st

tab1, tab2, tab3 = st.tabs(["猫", "狗", "猫头鹰"])

with tab1:
   st.header("一只猫")
   st.image("https://static.streamlit.io/examples/cat.jpg", width=200)

with tab2:
   st.header("一只狗")
   st.image("https://static.streamlit.io/examples/dog.jpg", width=200)

with tab3:
   st.header("一只猫头鹰")
   st.image("https://static.streamlit.io/examples/owl.jpg", width=200)

或者您也可以直接在容器对象上调用方法:

import streamlit as st
import numpy as np

tab1, tab2 = st.tabs(["📈 图表", "🗃 数据"])
data = np.random.randn(10, 1)

tab1.subheader("一个带有图表的选项卡")
tab1.line_chart(data)

tab2.subheader("一个带有数据的选项卡")
tab2.write(data)

4.st.expander - 可展开/折叠的多元素容器

调用 st.expander,您可以插入一个可展开或折叠的容器,用于包含多个元素。容器的初始状态是折叠的,只显示提供的标签。用户可以点击标签来展开容器,查看其中的内容。

import streamlit as st

st.bar_chart({"data": [1, 5, 2, 6, 2, 1]})

with st.expander("查看说明"):
    st.write("""
        上面的图表展示了我为您选择的一些数字。
        这些数字是通过真实的骰子摇出来的,所以它们*保证*是随机的。
    """)
    st.image("https://static.streamlit.io/examples/dice.jpg")

或者您也可以直接在容器对象上调用方法:

import streamlit as st

st.bar_chart({"data": [1, 5, 2, 6, 2, 1]})

expander = st.expander("查看说明")
expander.write("""
    上面的图表展示了我为您选择的一些数字。
    这些数字是通过真实的骰子摇出来的,所以它们*保证*是随机的。
""")
expander.image("https://static.streamlit.io/examples/dice.jpg")

文件: day27大模型程序开发day02-sql解释器+streamlit基础\大模型应用开发Functioncalling.md

自我介绍

张晓波:bobo老师

  • 工信部认聘的数据分析和人工智能专家组专家,2024参与数字化人才《岗位能力标准》的制订工作。

  • 华为认证AI大模型专家讲师

image-20250515161541997

专业的事交给专业的人去做

有言在先

  • 授课风格:
    • 没有鸡汤、没有段子。有认真负责的态度、有传道受业解惑的育人之志启智之能
  • 学习方法
    • 课上动脑听、课下用心练、笔记常翻译、消化在心中
  • 授课相关:好的授课过程就好比是谈一场恋爱,从来都不是一个人的事
    • 节奏快慢
    • 内容动态补充
    • 授课内容+顺序
    • ......
  • Finally:
    • 人无完人,每个人都有不同的短板和不足之处。互相理解、互相适应、搞定AI、成就大计!

核心内容

  • Functioncalling技术实现
  • Streamlit+大模型应用
  • RAG+Agent+Streamlit项目实战
  • MCP深度应用
  • OpenAI Agents SDK(openai在25年推出的企业级Multi-Agent开发框架)
    • Agents SDK接入MCP
  • 模型本地部署+调用(ollama+企业级)
  • 模型微调
  • 微软-GraphRAG应用开发

4、Function Calling

4.2 Function Calling简介

4.2.1 背景和定义

我们都知道大语言模型的知识储量是巨大,并且它具备非常强大的原生能力,但是有时候我们在实际使用大模型时会感受到大模型能力上的某些局限,比如大模型无法回答超过大模型知识库截止日期之后发生的相关信息和知识,并且大模型只能给出文字的建议但无法直接帮我们解决某些实际操作性的问题(如自动进行邮件收发、自动预定车票等)。因此,这些问题的存在会极大程度上限制了大模型的实际应用价值。

在这一基本背景下,Function calling功能应运而生。该功能的本质是让大模型具有调用外部函数的能力。也就是说,当大模型遇到超出自身能力范围的需求时,可以通过访问相应的外部函数寻求解决方案。这样,大模型就可以不再仅仅根据自身的知识库进行回答,还可以额外挂载一个外部函数库,然后根据用户提问进行外部函数的检索,根据实际需求调用外部函数并获取函数运行结果,再基于函数运行结果进行回答。

毫无疑问,有了外部函数库的功能加持,大模型的处理和解决问题的能力也必将再上一个台阶。

4.2.2 核心原理

Snipaste_2025-02-21_09-18-50

在Agent中的应用

Snipaste_2025-02-21_09-20-27

4.3 实时气象查询Agent开发

4.3.1 项目背景

OpenWeather是一家提供全球范围内的气象数据服务的公司,该公司的服务包括实时天气信息、天气预报、历史天气数据以及各种气象相关的报告等,并且OpenWeather开放了一定使用限度内完全免费的API,即我们可以在代码环境中通过调用OpenWeather API来进行实时天气查询、天气预报等功能,这意味着开发者可以将OpenWeather的天气预报功能加入到他们自己的应用或网站中。

4.3.2 OpenWeather注册及API key获取方法

为了能够调用OpenWeather服务,和OpenAI的API使用过程类似,我们首先需要先注册OpenWeather账号,并获取OpenWeather API Key。这里需要注意的是,对于大多数在线服务的API来说,都需要通过API key来进行身份验证,尽管OpenWeather相对更加Open,有非常多的免费使用的次数,但身份验证仍然是必要的防止API被滥用的有效手段。OpenWeather API key获取流程如下:

  • Step 1.登录OpenWeather官网并点击Sign—>create account完成注册。该网站无需魔法即可直接登录,可以使用国内邮箱或者QQ邮箱均可进行注册,官网地址为:https://openweathermap.org/

    Snipaste_2025-02-21_09-26-52 Snipaste_2025-02-21_09-26-59
  • Step 2.获取API-key:注册完成后,即可在API keys页面查看当前账户的API key:

    Snipaste_2025-02-21_09-27-08 Snipaste_2025-02-21_09-27-15

    一般来说完成注册后,就会有一个已经激活的API-key。和OpenAI一样,OpenWeather的API key也可以创建多个。

  • Step 3.将其设置为环境变量:和OpenAI API key类似,为了方便后续调用,我们也可以直接将OpenWeather API key设置为环境变量,变量名为OPENWEATHER_API_KEY。具体设置环境变量的方法参考Ch.1中OpenAI APkey设置环境变量流程,此处不再赘述。

    Snipaste_2025-02-21_09-27-22

    设置完了环境变量之后,接下来即可按照如下方式创建OpenWeather API key变量:

    open_weather_key = os.getenv("OPENWEATHER_API_KEY")
    

4.3.3 获取实时天气信息API

import requests

# Step 1.构建请求
url = "https://api.openweathermap.org/data/2.5/weather"

# Step 2.设置查询参数
params = {
    "q": "Beijing",               # 查询北京实时天气
    "appid": "xxx",    # 输入API key
    "units": "metric",            # 使用摄氏度而不是华氏度
    "lang":"zh_cn"                # 输出语言为简体中文
}

# Step 3.发送GET请求
response = requests.get(url, params=params)

# Step 4.解析响应
data = response.json()
print(data)

# 即时温度最高、最低气温
data['main']['temp_min'], data['main']['temp_max']
# 天气状况
data['weather'][0]['description']

这里需要注意的是,城市名必须输入英文名,否则无法正确识别。

外部函数创建:我们尝试编写一个通过OpenWeather API实时获取天气信息的API,并作为模型可调用的外部函数之一。很明显,为了确保和大语言模型之间的顺畅通信,此时要求函数的输入和输出都是字符串格式。具体函数编写如下:

这里需要注意函数说明和参数解释的书写风格

def get_weather(loc):
    """
    查询即时天气函数
    :param loc: 必要参数,字符串类型,用于表示查询天气的具体城市名称,\
    注意,中国的城市需要用对应城市的英文名称代替,例如如果需要查询北京市天气,则loc参数需要输入'Beijing';
    :return:OpenWeather API查询即时天气的结果,具体URL请求地址为:https://api.openweathermap.org/data/2.5/weather\
    返回结果对象类型为解析之后的JSON格式对象,并用字符串形式进行表示,其中包含了全部重要的天气信息
    """
    # Step 1.构建请求
    url = "https://api.openweathermap.org/data/2.5/weather"

    # Step 2.设置查询参数
    params = {
        "q": loc,               
        "appid": 'xxx',    # 输入API key
        "units": "metric",            # 使用摄氏度而不是华氏度
        "lang":"zh_cn"                # 输出语言为简体中文
    }

    # Step 3.发送GET请求
    response = requests.get(url, params=params)
    
    # Step 4.解析响应
    data = response.json()
    return json.dumps(data)

函数测试:

#测试函数是否可用
import json
get_weather('GuangZhou')

邮件发送外部函数:

def send_mail(msg):
    """
    该函数是用于进行指定邮件发送的。
    :param msg: 必要参数,字符串类型,该参数表示要发送的邮件内容
    :return:邮件发送成功后的状态信息
    """
    return '邮件已经成功发送,邮件内容是:'+msg

4.3.4 tools参数解释与定义

在准备好外部函数之后,接下来非常重要的一步就是需要将外部函数的信息以某种形式传输给模型。此时就需要使用到create函数的tools参数.从参数的具体形式来看,tools参数是一个可以包含多个字典的list,每个字典都需要包含两个键值对,分别是 {type:function,function:外部函数的完整描述}。因此每个字典都代表一个外部函数的相关信息。在大语言模型实际进行问答时,会根据tools参数提供的信息对各外部函数进行检索。

tools = [
    {
        "type": "function", 
        "function":'外部函数的完整描述'
    }
]

4.3.5 外部函数完整描述

外部函数的完整描述对于Function calling功能的实现至关重要。因为在大模型进行实际问答时,会根据对外部函数的完整描述信息的语义理解进行外部函数的检索和调用。

接下来我们详细解释外部函数完整描述的指定写法。总的来说,我们会使用一个字典来对其进行完整描述,每个字典都有三个参数(三组键值对),各参数(Key)名称及解释如下:

  • name:代表函数函数名称字的符串,必选参数。
  • description:用于描述函数功能的字符串,虽然是可选参数,但该参数传递的信息实际上是Chat模型对函数功能识别的核心依据。即Chat函数实际上是通过每个函数的description来判断当前函数的实际功能的。
  • parameters:函数的参数说明,必选参数,要求遵照JSON Schema格式进行输入,JSON Schema是一种特殊的JSON对象,专门用于验证JSON数据格式是否满足要求。

例如,对于get_weather函数,我们需要创建如下字典来对其进行完整描述:

get_weather_function = {
    'name': 'get_weather',
    'description': '查询即时天气函数,根据输入的城市名称,查询对应城市的实时天气',
    'parameters': {
        'type': 'object', #json对象类型
        'properties': { #参数成员描述
            'loc': {
                'description': "城市名称,注意,中国的城市需要用对应城市的英文名称代替,例如如果需要查询北京市天气,则loc参数需要输入'Beijing'",
                'type': 'string'
            }
        },
        'required': ['loc']
    }
}

send_mail_function = {
    'name': 'send_mail',
    'description': '该函数是用于进行指定邮件发送的',
    'parameters': {
        'type': 'object', #json对象类型
        'properties': { #参数成员描述
            'msg': {
                'description': "必要参数,字符串类型,该参数表示要发送的邮件内容",
                'type': 'string'
            }
        },
        'required': ['msg']
    }
}

因此修改tool参数值为:

tools = [
    {
        "type":'function',
        'function':get_weather_function
    },
    {
        "type":'function',
        'function':send_mail_function
    }
]

同时还需要封装外部函数库,用于关联外部函数名称和外部函数对象

available_functions = {
            "get_weather": get_weather,
            "send_mail":send_mail
        }

4.3.6 Function calling实现

First response

在进行了一系列基础准备工作之后,接下来我们尝试在Chat模型对话执行Function calling功能。首先我们测试模型本身能否知道如何查询天气:

from openai import OpenAI
#硅基流动API
ds_api_key = "sk-atisrxxxriejlfxlvnymvfxoesps"
client = OpenAI(api_key=ds_api_key, 
                base_url="https://api.siliconflow.cn/v1")
messages = [
        {"role": "user", "content": "请帮我查询上海地区今日天气情况"}
    ]
response = client.chat.completions.create(
    model="deepseek-ai/DeepSeek-V2.5",  
     messages=messages, 
    )
        
response.choices[0].message.content

很明显,模型无法进行回答。接下来我们尝试将函数库相关信息输入给Chat模型

response = client.chat.completions.create(
    model="deepseek-ai/DeepSeek-V2.5",  
    messages=[
        {"role": "user", "content": "请帮我查询北京地区今日天气情况"}
    ],
    tools=tools,
)

        
response_message = response.choices[0].message
response_message

返回结果:

ChatCompletionMessage(content='', refusal=None, role='assistant', audio=None, function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='0195279d5095aab2d4ac52760de27c04', function=Function(arguments='{"loc":"Beijing"}', name='get_weather'), type='function')])

能够发现,此时返回的message中content为空,而增了一个"tool_calls"的list,该list就包含了当前调用外部函数的全部信息:

response_message.tool_calls[0]

返回结果:

ChatCompletionMessageToolCall(id='0195279d5095aab2d4ac52760de27c04', function=Function(arguments='{"loc":"Beijing"}', name='get_weather'), type='function')

对于当前CompletionMessageToolCall对象,id为外部函数调用发起请求id,function则表示调用外部函数基本信息,而type则代表了当前当前调用外部函数类型,function代表调用自定义的外部函数。

我们可以在此基础上分别提取调用外部函数名称信息和参数信息,分别保存为function_name和function_args对象:

# 完成对话需要调用的函数名称
function_name = response_message.tool_calls[0].function.name
function_name

# 基于外部函数库获取具体的函数对象
fuction_to_call = available_functions[function_name]
fuction_to_call

'''
        available_functions = {
                    "get_weather": get_weather,
                }
'''

# 执行该函数所需要的参数,将其反序列化成字典对象,便于下一步函数调用时进行传输传递
function_args = json.loads(response_message.tool_calls[0].function.arguments)
function_args

需要注意的是,外部函数的计算过程仍然是在本地执行,即Chat模型并不会将代码读取到服务器上再进行在线计算,因此接下来我们需要根据模型返回的函数和函数参数,在本地完成函数计算,然后再将计算过程和结果保存为message并追加到messages后面,并第二次调用Chat模型分析函数的计算结果,并最终根据函数计算结果输出用户问题的答案。

Second response
function_response  = fuction_to_call(**function_args) #get_weather(loc="Beijing")
function_response  #获取函数调用结果

能够发现,模型已经顺利完成计算。接下来我们在messages对象中追加两条消息,第一条消息是第一次模型返回的结果(即调用模型的assistant message),第二条消息则是外部函数计算结果,该条消息的role为function,且name为函数名称。这也是我们首次接触function message,和user、system、assistant message不同,function message必须要输入关键词name,且function message的内容源于外部函数执行的计算结果,并且需要手动进行输入。具体添加过程如下:

  • 追加第一条消息:模型返回的结果

    #将模型第一次返回的结果转换成字典类型,目的是为了将其追加到messages列表中
    response_message.model_dump()
    # 向messages追加第一次模型返回结果消息
    messages.append(response_message.model_dump())  
    #查看追加后的messages
    print(messages)
    
  • 追加第二条消息:外部函数计算结果

    # 追加function返回消息
    messages.append({
                "role": "tool",
                "content": function_response,
                "tool_call_id":response_message.tool_calls[0].id
            })
    

接下来,再次调用Chat模型来围绕messages进行回答。需要注意的是,此时我们不再需要向模型重复提问,只需要简单的将我们已经准备好的messages传入Chat模型即可:

second_response = client.chat.completions.create(
    model="deepseek-ai/DeepSeek-V2.5",
    messages=messages)

second_response.choices[0].message.content

能够发现,模型最终做出了准确回答。

完整封装
def run_conv(messages, 
             api_key,
             tools=None, 
             functions_list=None,
             model="deepseek-ai/DeepSeek-V2.5"):
    """
    能够自动执行外部函数调用的Chat对话模型
    :param messages: 必要参数,输入到Chat模型的messages参数对象
    :param api_key: 必要参数,调用模型的API-KEY
    :param tools: 可选参数,默认为None,可以设置为包含全部外部函数的列表对象
    :param model: Chat模型,可选参数,默认模型为deepseek-chat
    :return:Chat模型输出结果
    """
    user_messages = messages
    #基于硅基流动API
    client = OpenAI(api_key=api_key, 
                base_url="https://api.siliconflow.cn/v1")

    # 如果没有外部函数库,则执行普通的对话任务
    if tools == None:
        response = client.chat.completions.create(
            model=model,  
            messages=user_messages
        )
        final_response = response.choices[0].message.content

    # 若存在外部函数库,则需要灵活选取外部函数并进行回答
    else:
        # 创建外部函数库字典
        available_functions = {func.__name__: func for func in functions_list}

        # 创建包含用户问题的message
        messages = user_messages

        # first response
        response = client.chat.completions.create(
            model=model,  
            messages=user_messages,
            tools=tools,
        )
        response_message = response.choices[0].message

        # 获取函数名
        function_name = response_message.tool_calls[0].function.name
        # 获取函数对象
        fuction_to_call = available_functions[function_name]
        # 获取函数参数
        function_args = json.loads(response_message.tool_calls[0].function.arguments)

        # 将函数参数输入到函数中,获取函数计算结果
        function_response = fuction_to_call(**function_args)

        # messages中拼接first response消息
        user_messages.append(response_message.model_dump())  

        # messages中拼接外部函数输出结果
        user_messages.append(
            {
                "role": "tool",
                "content": function_response,
                "tool_call_id":response_message.tool_calls[0].id
            }
        )

        # 第二次调用模型
        second_response = client.chat.completions.create(
            model=model,
            messages=user_messages)

        # 获取最终结果
        final_response = second_response.choices[0].message.content

    return final_response

无需外部函数加持的情况:

messages = [{"role": "user", "content": "请问什么是机器学习?"}]
run_conv(messages=messages, 
         api_key = ds_api_key)

需要外部函数加持的情况:

messages = [{"role": "user", "content": "请问北京今天天气如何?"}]
run_conv(messages=messages, 
         api_key = ds_api_key,
         tools=tools, 
         functions_list=[get_weather])

4.4 sql解释器Agent开发

4.4.1 项目背景

在平常的工作中,会经常对数据库中的数据进行相关的读写操作,这是一些繁杂的sql语句的编写就会尤为的麻烦也非常容易出错。尤其是在数据分析的一些业务场景中,更是需要频繁的进行数据库的相关操作且高频的编写一些对应的sql代码。

那么我们是否可以利用大模型本身的编码能力帮我们根据相关的自然语言的指令自动进行sql的编写和运行呢?

这一小节,我们就一起来学习,如何将大模型接入到本地数据库中,让大模型帮我们生成对应的sql且在本地数据库环境中进行sql的运行,将结果再次经过大模型的语义理解能力和文字生成能力进行润色后返回!

4.4.2 数据字典

对于大多数企业来说,都会围绕各关键数据集制作数据字典。所谓数据字典,指的是一份记录了每个数据集详细信息的文档,有的时候数据字典也可以以表格形式呈现。借助数据字典,开发/数据分析人员能够快速了解数据表中的各项关键信息。

那么,为了让大模型可以更好的理解数据库中的数据,我们也可以给大模型制作一个数据字典,让大模型可以更好的理解数据,返回更加具有针对性的结果。

import os
from openai import OpenAI
from IPython.display import display, Code, Markdown

#硅基流动API
ds_api_key = "sk-atisrejlfxlvnymvfxoesps"
client = OpenAI(api_key=ds_api_key, 
                base_url="https://api.siliconflow.cn/v1")
# 打开并读取Markdown文件
with open('./data/LC数据字典.md', 'r', encoding='utf-8') as f:
    md_content = f.read()
    
len(md_content)
#基于md_content作为模型背景信息,向模型进行相关提问
response = client.chat.completions.create(
    model="deepseek-ai/DeepSeek-V2.5", 
    messages=[
        {"role": "system", "content": md_content}, 
        # "content": '请帮我统计下LC数据表一共有哪些字段?共计多少个?'
        {"role": "user", "content": '请帮我介绍下LC数据表'}
    ],
)
display(Markdown(response.choices[0].message.content))

4.4.3 Function calling实现

创建生成SQL语句的外部函数
def get_sql_result(sql_query):
    """
    查询数据库相关数据的函数
    :param sql_query: 必要参数,字符串类型,用于表示查询数据的sql语句;
    :return:sql_query表示的sql语句查询到的结果;
    """
    connection = pymysql.connect(
            host='localhost',  # 数据库地址
            user='root',  # 数据库用户名
            passwd='boboadmin',  # 数据库密码
            db='testdb',  # 数据库名
            charset='utf8'  # 字符集选择utf8
        )
    
    try:
        with connection.cursor() as cursor:
            # SQL查询语句
            sql = sql_query
            cursor.execute(sql)

            # 获取查询结果
            results = cursor.fetchall()

    finally:
        connection.close()
    
    
    return json.dumps(results)
自动生成外部函数描述信息
#用于自动生成外部函数描述信息
def auto_function_desc(function): #参数为外部函数对象
    #定义一个内部函数用于生成外部函数的完整描述信息
    def inner(function):
        function_description = inspect.getdoc(function)#外部函数的函数说明
        function_name = function.__name__ #外部函数名
        
        system_prompt = '以下是某的函数说明:%s' % function_description
        
        user_prompt = '根据这个函数的函数说明,请帮我创建一个JSON格式的字典,这个字典有如下5点要求,请你仔细阅读,并且务必遵从所有要求:\
                       1.字典总共有三个键值对;\
                       2.第一个键值对的Key是字符串name,value是该函数的名字:%s,也是字符串;\
                       3.第二个键值对的Key是字符串description,value是该函数的函数的功能说明,也是字符串;\
                       4.第三个键值对的Key是字符串parameters,value是一个JSON Schema对象,用于说明该函数的参数输入规范。\
                       5.输出结果必须是一个JSON格式的字典,并且一定不要任何前后修饰语句,务必参按照如下格式进行输出:%s' % (function_name,'{key:value}')
        
        api_key = "sk-4b79f3axxx366ebb425b3"
        client = OpenAI(api_key=ds_api_key, 
                base_url="https://api.deepseek.com")
        response = client.chat.completions.create(
            model="deepseek-reasoner",  
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt}

                 ]
        )

        return json.loads(response.choices[0].message.content)
    #由于模型根据提示信息生成的外部函数完整信息可能会有问题,因此,如果出现问题则loads环节会报错,则要求模型重新进行生成
    max_try_count = 5 #模型调用的最大次数
    count = 0 #当前调用模型的次数
    while count < max_try_count:
        try:
            function_desc = inner(function)
            break
        except Exception as e:
            count += 1
            print('something error:',e)
            if count == max_try_count:
                print('模型达到最大尝试次数,程序停止!')
                raise
            else:
                print('模型重新生成中......')
    tools = [
    {
        "type": "function", 
        "function":function_desc
    }
]
    return tools

测试:

auto_function_desc(get_sql_result)

输出:

[
    {
        'type': 'function',
        'function': {
            'name': 'get_sql_result',
  		   'description': '查询数据库相关数据的函数',
           'parameters': {
               'type': 'object',
               'properties': {
                   'sql_query': {
                       'type': 'string',
                       'description': '用于表示查询数据的sql语句'
                   }
               },
         'required': ['sql_query']}}}
]
sql解释器封装
#available_functions表示外部函数库
def auto_run_conversation(messages,available_functions=None):
    api_key = "sk-atisrrfnrxsnulmuvlzqnvuvcglkriejlfxlvnymvfxoesps"
    client = OpenAI(api_key=ds_api_key, 
                base_url="https://api.siliconflow.cn/v1")
    
    # 如果没有外部函数库,则执行普通的对话任务
    if available_functions == None:
        print('模型原生能力解决该提问.........')
        response = client.chat.completions.create(
            model="deepseek-ai/DeepSeek-V2.5",  
            messages=messages
        )
        final_response = response.choices[0].message.content
    else:
        #外部函数库定义
        available_functions = available_functions
        
       #step_3:外部函数完整描述定义 + #step_4:tools参数值定义
        tools = auto_function_desc(available_functions['function'])

        #step_5:第一次模型调用
        response = client.chat.completions.create(
            model="deepseek-ai/DeepSeek-V2.5",  
            messages=messages,
            tools=tools,
        )
        response_message = response.choices[0].message
        
        #判断返回结果是否存在tool_calls,即判断是否需要调用外部函数来回答问题
        if response_message.tool_calls:
            print('function_calling解决该提问.........')
            sql = response_message.tool_calls[0].function.arguments
            print('生成的sql为::',sql)
            choose = input('是否执行上述sql? y/n')
            if choose == 'n':
                print("您选择不执行sql语句,再见!")
                return
            #step_6:外部函数手动调用且获取调用结果
            fuction_to_call = available_functions['function'] #函数对象
            function_args = json.loads(response_message.tool_calls[0].function.arguments)#函数参数

            function_response = fuction_to_call(**function_args)#函数手动调用

            #step_7:向messages进行两次消息追加
            messages.append(response_message.model_dump())  
            messages.append({
                        "role": "tool",
                        "content": function_response,
                        "tool_call_id":response_message.tool_calls[0].id
                    })

            #step_8: 再次调用大模型
            second_response = client.chat.completions.create(
                model="deepseek-ai/DeepSeek-V2.5",
                messages=messages)
            final_response = second_response.choices[0].message.content
        else:
            final_response = response_message.content
    return Markdown(final_response)

测试:

messages = [
    {"role": "system", "content": md_content},
    {"role": "user", "content": "请问LC数据表有多少男性用户?"}
]
#定义外部函数库
available_functions = {
            "function": get_sql_result,
        }
auto_run_conversation(messages,available_functions)

day28大模型程序开发day03-项目实战

文件: day28大模型程序开发day03-项目实战\streamlit案例.md

模型对话示例:

sk-4b79f3a3ff334a15a1935366ebb425b3

import streamlit as st  
from openai import OpenAI  
  
# 设置页面标题  
st.title("💬 DeepSeek Chatbot")  
  
# 在侧边栏添加配置选项  
with st.sidebar:  
    # 提供一个文本输入框让用户可以手动输入API Key(可选)  
    openai_api_key = st.text_input("DeepSeek API Key", key="chatbot_api_key", type="password")  
    
    "[获取 DeepSeek API key](https://platform.deepseek.com/api_keys)"  
    
    if st.button("开启新对话"):  
        st.session_state.messages = [{"role": "assistant", "content": "欢迎使用对话机器人,你想知道什么?"}] 
  
#检查API Key是否已提供  
if not openai_api_key:  
    st.info("请添加新的API Key")  
else:  
    base_url = "https://api.deepseek.com"  
    client = OpenAI(api_key=openai_api_key, base_url=base_url)  
  
    # 初始化对话历史记录  
    if "messages" not in st.session_state:  
        st.session_state.messages = [{"role": "assistant", "content": "欢迎使用对话机器人,你想知道什么?"}]  
  
    # 显示对话历史  
    for msg in st.session_state.messages:  
        st.chat_message(msg["role"]).write(msg["content"])  
  
    # 获取用户输入  
    if prompt := st.chat_input():  
        st.session_state.messages.append({"role": "user", "content": prompt})  
        st.chat_message("user").write(prompt)  
  
        # 调用DeepSeek API  
        response = client.chat.completions.create(  
            model="deepseek-chat",  
            messages=st.session_state.messages,  
            stream=False  
        )  
        #追加聊天记录
        assistant_reply = response.choices[0].message.content  
        st.session_state.messages.append({"role": "assistant", "content": assistant_reply})  
        st.chat_message("assistant").write(assistant_reply)

sql解释器-1:

sk-4b79f3a3ff334a15a1935366ebb425b3

上传数据字典,围绕数据字典进行问答

import streamlit as st  
from openai import OpenAI  
# 设置页面标题  
st.title("💬 DeepSeek Chatbot")  

# 在侧边栏添加配置选项  
with st.sidebar:  
    data_dic = ""
    # 提供一个文本输入框让用户可以手动输入API Key(可选)  
    openai_api_key = st.text_input("DeepSeek API Key", key="chatbot_api_key", type="password")  
    "[获取 DeepSeek API key](https://platform.deepseek.com/api_keys)"  
    
        
    #上传数据字典文件:创建文件上传组件,如果上传失败则返回None
    upload_file = st.file_uploader(
        label = "上传数据字典文件" 
    )
    #判断上传文件是否成功
    #Streamlit 会将文件内容作为字节流 (BytesIO) 对象返回给你。
    if upload_file is not None:
        #读取上传文件数据
        data_dic = upload_file.read().decode("utf-8")
        st.success("上传文件成功!")
    else:
        st.stop() # 退出
    
    #开启对话按钮
    if st.button("开启新对话"):  
        st.session_state.messages = [{"role": "system", "content": data_dic}] 
        
#检查API Key是否已提供  
if not openai_api_key:  
    st.info("请添加新的API Key")  
else:  
    base_url = "https://api.deepseek.com"  
    client = OpenAI(api_key=openai_api_key, base_url=base_url)  
  
    # 初始化对话历史记录  
    if "messages" not in st.session_state:  
        st.session_state.messages = [{"role": "assistant", "content": "欢迎使用对话机器人,你想知道什么?"}]  
  
    # 显示对话历史  
    for msg in st.session_state.messages:  
        if msg["role"] != 'system':
            st.chat_message(msg["role"]).write(msg["content"])  
  
    # 获取用户输入  
    if prompt := st.chat_input():  
        st.session_state.messages.append({"role": "user", "content": prompt})  
        st.chat_message("user").write(prompt)  
  
        # 调用DeepSeek API  
        response = client.chat.completions.create(  
            model="deepseek-chat",  
            messages=st.session_state.messages,  
            stream=False  
        )  
        #追加聊天记录
        assistant_reply = response.choices[0].message.content  
        st.session_state.messages.append({"role": "assistant", "content": assistant_reply})  
        st.chat_message("assistant").write(assistant_reply)

RAG+Agent

import streamlit as st
import tempfile #创建临时文件和目录,并提供了自动清理这些临时文件和目录的机制,以避免占用不必要的磁盘空间
import os
from langchain.memory import ConversationBufferMemory
from langchain_community.chat_message_histories import StreamlitChatMessageHistory
from langchain_community.document_loaders import TextLoader
from langchain_openai import ChatOpenAI
from langchain_chroma import Chroma
from langchain_core.prompts import PromptTemplate
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain.agents import create_react_agent, AgentExecutor
from langchain_community.callbacks.streamlit import StreamlitCallbackHandler
from langchain_openai import ChatOpenAI
from langchain_community.embeddings import BaichuanTextEmbeddings


# 设置Streamlit应⽤的⻚⾯标题和布局
st.set_page_config(page_title="Rag Agent", layout="wide")

# 设置应⽤的标题
st.title("Rag Agent")

#上传txt⽂件,允许上传多个⽂件
uploaded_files = st.sidebar.file_uploader(
    label="上传txt⽂件", type=["txt"], accept_multiple_files=True
)
# 如果没有上传⽂件,提示⽤户上传⽂件并停⽌运⾏
if not uploaded_files:
    st.info("请先上传按TXT⽂档。")
    st.stop()
    
#实现检索器函数封装:文件读取、分块、向量转换、向量数据库、MMR信息检索
'''
@st.cache_resource(ttl="1h") 是 Streamlit 框架中的一个装饰器,用于缓存资源(如数据文件、数据库连接等)以提高性能和效率。具体来说,这个装饰器会将函数的返回值缓存一段时间(在这里是1小时),以避免在每次调用时都重新加载或计算相同的资源。
'''
@st.cache_resource(ttl="1h")
def configure_retriever(uploaded_files):
    docs = [] #存储用户上传文件的文件内容(字符串)
    #创建临时文件和目录
    temp_dir = tempfile.TemporaryDirectory(dir=r"D:\\")
    for file in uploaded_files:
        temp_filepath = os.path.join(temp_dir.name, file.name)
        with open(temp_filepath, "wb") as f:
            f.write(file.getvalue())
        # 使用TextLoader加载文本文件
        loader = TextLoader(temp_filepath, encoding="utf-8")
        docs.extend(loader.load())
    # 进行文档分割
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=300, chunk_overlap=50)
    splits = text_splitter.split_documents(docs)

    # 使用BaichuanTextEmbeddings向量模型生成文档的向量表示
    key = "xxx"
    embeddings = BaichuanTextEmbeddings(api_key=key)
    vectordb = Chroma.from_documents(splits, embeddings)

    # 创建文档检索器
    retriever = vectordb.as_retriever()
    #返回检索器对象
    return retriever

# 配置检索器:调用检索器函数,返回MMR检索器对象
retriever = configure_retriever(uploaded_files)

# 如果session_state中没有消息记录或用户点击了清空聊天记录按钮,则初始化消息记录
if "messages" not in st.session_state or st.sidebar.button("清空聊天记录"):
    st.session_state["messages"] = [{"role": "assistant", "content": "您好,我是AI智能助手,我可以查询文档"}]

# 加载历史聊天记录
for msg in st.session_state.messages:
    st.chat_message(msg["role"]).write(msg["content"])

    
# 下一步工作就是将文档检索作用在Agent对象中。创建Agent时需要让其对多轮对话具备上下文记忆能力
# 创建用于文档检索的工具
'''
    create_retriever_tool:
        功能:创建具体的检索工具实例
        用途:从文档库中检索信息
        典型场景:问答系统、文档搜索

    langchain.agents.Tool:
        功能:定义工具的通用接口
        用途:自定义工具或扩展工具功能
        典型场景:创建自定义工具或扩展现有工具 
'''
from langchain.tools.retriever import create_retriever_tool
tool = create_retriever_tool(
    retriever = retriever,
    name = "文档检索",
    description = "用于检索用户提出的问题,并基于检索到的文档内容进行回复.",
)
tools = [tool]

# 创建聊天消息历史记录
'''
StreamlitChatMessageHistory 是一个专为 Streamlit 应用设计的聊天历史管理工具,适用于需要保存和管理对话上下文的场景。
支持实时更新聊天历史,并在界面上动态显示。和之前学过的ChatMessageHistory消息集工具对象类似。
'''
msgs = StreamlitChatMessageHistory()
# 创建对话缓冲区
'''
参数介绍:
    chat_memory:指定初始的对话历史记录。 用于加载已有的对话历史,以便在后续对话中参考之前的上下文。
    return_messages:控制是否在返回结果中包含消息对象。如果设置为 True,则在调用 memory.load_memory() 时,返回的结果中会包含完整的消息对象(包括角色、内容等)。如果设置为 False,则只返回字符串形式的消息的内容。
    memory_key:指定在内存中存储对话历史的键名。当从内存中加载或保存对话历史时,使用这个键名来标识对话历史。例如,如果内存中存储了多个键值对,可以通过 memory_key 来指定要加载的对话历史。
    output_key:指定在内存中存储模型输出的键名。当将模型的输出保存到内存中时,使用这个键名来标识输出内容。例如,如果内存中存储了多个键值对,可以通过 output_key 来指定要加载的模型输出。
'''
memory = ConversationBufferMemory(
    chat_memory=msgs, return_messages=True, memory_key="chat_history", output_key="output"
)

# 指令模板
instructions = """你是一个设计用于查询文档来回答问题的代理对象。
你可以使用文档检索工具,并基于检索内容来回答问题
你可能不查询文档就知道答案,但是你仍然应该查询文档来获得答案。
如果你从文档中找不到任何信息用于回答问题,则只需返回“抱歉,这个问题我还不知道。”作为答案。
"""

# 基础提示模板-React提示词
base_prompt_template = """
{instructions}

TOOLS:
------

You have access to the following tools:

{tools}

To use a tool, please use the following format:

‍```
Thought: Do I need to use a tool? Yes
Action: the action to take, should be one of [{tool_names}]
Action Input: {input}
Observation: the result of the action
‍```

When you have a response to say to the Human, or if you do not need to use a tool, you MUST use the format:

‍```
Thought: Do I need to use a tool? No 
Final Answer: [your response here]
‍```

Begin!

Previous conversation history:
{chat_history}

New input: {input}
{agent_scratchpad}"""


# 创建基础提示模板
base_prompt = PromptTemplate.from_template(base_prompt_template)
# 创建部分填充的提示模板
prompt = base_prompt.partial(instructions=instructions)

# 创建llm
API_KEY = "xxx"
llm = ChatOpenAI(model="deepseek-reasoner",
                   openai_api_key=API_KEY,
                   openai_api_base="https://api.deepseek.com")

# 创建react Agent
agent = create_react_agent(llm, tools, prompt)

# 创建Agent执行器
'''
memory:指定用于存储和管理对话历史的内存对象。
    用途:
        - 存储对话历史,包括用户输入和机器人回复。
        - 在多轮对话中提供上下文,帮助机器人更好地理解用户意图。

handle_parsing_errors:控制是否在解析用户输入时自动处理错误。
    用途:
        - 如果设置为 True,当用户输入无法被正确解析时,系统会自动尝试修复或忽略错误,而不是直接抛出异常。
        - 如果设置为 False,系统会在遇到解析错误时抛出异常,可能导致程序中断。
'''
agent_executor = AgentExecutor(agent=agent, tools=tools, memory=memory, verbose=True, handle_parsing_errors=True)

# 创建聊天输入框
user_query = st.chat_input(placeholder="请开始提问吧!")

# 如果有用户输入的查询
if user_query:
    # 添加用户消息到session_state
    st.session_state.messages.append({"role": "user", "content": user_query})
    # 显示用户消息
    st.chat_message("user").write(user_query)
    
    #创建一个 Streamlit 的聊天消息块,用于显示助手(机器人)的回复。
    with st.chat_message("assistant"):
        # st.container(): 创建一个 Streamlit 的容器组件,用于动态更新内容。
        #StreamlitCallbackHandler 是 LangChain 的一个回调处理器,用于将模型的输出或日志信息显示在 Streamlit 界面中。通过将 st.container() 传递给 StreamlitCallbackHandler,可以将 LangChain 的输出直接渲染到 Streamlit 的容器中。
        st_cb = StreamlitCallbackHandler(st.container())
        
        # 配置 LangChain 的回调函数列表,将 StreamlitCallbackHandler 添加到回调中。通过配置回调函数,LangChain 可以在处理过程中调用StreamlitCallbackHandler,从而将输出或日志信息显示在 Streamlit 界面中。这种方式可以实现实时更新界面。
        config = {"callbacks": [st_cb]}
        
        # 执行Agent并获取响应
        response = agent_executor.invoke({"input": user_query}, config=config)
        # 添加助手消息到session_state
        st.session_state.messages.append({"role": "assistant", "content": response["output"]})
        # 显示助手响应
        st.write(response["output"])
posted @ 2025-08-10 14:57  凫弥  阅读(221)  评论(0)    收藏  举报