### 前置 #### 课程大纲 * **课程介绍** - AI智能化云盘大课:后端分布式大项目+结合AI大模型智能体开发+业务应用 - 众多新技术+后端业务领域超多解决方案+AI大模型多案例应用场景落地 - 多语言开发:云盘板块采用Java开发后端项目,AI智能化板块采用Python+LangChain框架+大模型开发; - AI大模型Agent开发和主流解决方案和三方类库等多案例实战 - **项目核心技术体系** - 后端业务全新技术:SpringBoot3.X全家桶+JDK21+超多实用java生态类库+大文件传输处理 - AI大模型全新技术:Python3.1X+FastAPI框架+全新LangChain框架+向量数据库Milvus+多个大模型 - 智能化云盘 多数一线大厂正在研发的业务领域,全网首个后端业务+AI大模型一起的项目教程 - LLM大模型支持在线大模型调用和本地私有化部署,包括不限于ChatGLM、GPT-4、通义千问、LLaMa等 - 后端业务开发板块:打造私有化云盘,大文件上传、秒传、下载、在线分享等核心模块 - AI大模型Agent板块 - LLM大模型Prompt工程、RAG知识库构建、Agent智能体开发 - Memory长短期记忆、LCEL、Tools自定义工具、MaaS模型服务搭建等 * **核心业务模块应用场景** * 业务应用类似阿里/百度云盘、NAS等,支持多类型文件存储和处理,支持多类型存储架构 - 基于云盘存储文件,结合LLM大模型,开发多个Agent智能体,应用多个业务场景,包括不限于 - 从0到1讲解AI大模型基础+项目实战,拓展前端/后端工程师必备的人工智能知识和应用实战 - 智能机器人Chat助理:长短期记忆的个人助理、智能客服、智能销售顾问等 - 企业问答知识库: 知识库检索内容问答、自定义上传到知识库、在线解析URL地址、实时联网搜索等 - 文档AI助手:文档概要总结、内容进行分段总结、AIGC营销内容生产等 image-20241217115445442 - **AI大模型的行业解决方案和案例库参考** - https://page.dingtalk.com/wow/dingtalk/default/dingtalk/I0HfYX4QStBIpLgxnZQe - https://wolai.dingtalk.com/jVUBREtv4JHXRnBSWRaWj6 - https://bigmodel.cn/ #### 高频问题解答 * **问题:大模型是直接调用API吗,就是调用通义千问或者文心一言接口吗?** - ,LLM大模型只是简单调用API?那如何和后端业务+数据库数据联动? - 架构也和微服务类似,LLM重试机制、兜底降级机制等等怎么做?敏感数据敢上传外部? - 比如 - 某一次失败之后应该怎么处理,还有日志生成、管理资源、性能优化、准确性等等 - 这一些都是很关键的,靠普通的API是解决不了的,这个就是大课的部分解决方案,还有更多!!!! - 通义千问或者文心一言都是一个基层模型底座,这些大模型类似我们的操作系统,不是商业应用程序 - 类似我们会基于操作系统上开发App软件;那AI应用就是基于这些大模型作为底座,开发上层的商业智能化应用 - 比如 - 公司需要做智能知识库、行业智能客服、智慧政务、AI律师、AI客服等,那就没法用这些平台 - 因为你公司不可能把敏感数据上传上去,而且也没法做到; - 比如律师行业,医疗行业,财税行业等专业领域知识都是。 - 所以直接调用外部的API完全不一样,像通义千问等只是通用大模型,适合个人提升效率啥的,这个很容易。 - 但是达不到商用级别,也难和公司的业务结合一起; - 像很多公司都是有沉淀很多历史的资料,文档记录,案例等,而且又敏感,不能上传外部的LLM平台 - 所以都需要私有化部署,针对公司本身所处的行业进行深度定制和优化,结合常规的后端和前端项目整合一起 * **问题:学完这个大课,可以开发怎么样的项目和应用呢?** - AI文档助手 - 你可以给一堆专业文档,包括word文档、PDF等,让AI工具帮你生成 文档总结做周报、季度汇报等 - 给公司培训的的时候,可以从网上寻找很多资料,但是杂乱分散,可以让AI帮你整理和汇总,排版清晰 - 让AI帮你写多类型跳槽简历、毕业论文 - 企业知识库 - 将企业的各类知识资源进行智能化归类、整合,形成一套问题与答案的集合 - 【企业内部知识共享】作为企业内部的知识共享平台,帮助员工快速获取所需知识,提高团队协作效率 - 【客户服务】AI企业问答知识库可以为客户提供快速准确的解答服务 理解客户的问题并给出相应的答案 - 【员工培训】AI企业问答知识库还可以作为员工培训的平台,根据员工的个人需求进行定制化培训 - 私人AI助理 - 聊天与陪伴:私人AI助理可以陪伴用户聊天、讲笑话、玩小游戏等,提供轻松愉快的休闲娱乐体验。 - 个性化推荐:根据用户的喜好和行为习惯,推荐音乐、电影、书籍等娱乐内容。 - 健康管理:监测家庭成员的健康状况,提供运动、饮食建议,甚至可以协助医生进行诊断 - 特定领域智能聊天机器人 - 通过给AI一系列资料,单独训练特定领域,然后让帮我们做出决策 - 比如 - 各个大公司财报和历史股票行情信息,让AI汇总和给出指导建议 - 给出医院检查报告等,AI训练可以给出诊断和建议 - 给出特定领域销售部门的日常话术和专业知识, 充当智能客服 * **问题:什么是AI大模型应用,什么是AI大模型底层原理?课程是重点讲解哪块?** - AI大模型应用开发 - 就是我们用的很多人工智能工具,比如【智能美颜相机】【智能客服机器人】 - LLM应用层面很多很多:企业问答、智能律师、智慧政务、税务等 - 多数公司都是开发这类应用产品,包括App,网站等,使用人员和市场需求最多,90%占比 - AI大模型底层原理 - 就是为啥他的更加智能,采用什么数学算法,为啥更加智能,刨根问底 - 需要高学历,需要看很多行业英文论文、高等数学知识、算法原理等,岗位和市场需求少 10%占比 - 如果个人喜欢编写代码,实现具体的功能,且想要快速看到应用效果,则AI大模型应用开发 - 如果你对算数学模型、机器学习等有很好的基础,有精力进行深入研究则可以学习AI大模型底层原理 - 我们这个课程对于侧重AI大模型应用开发,如果你是0基础,之前是前端/后端/测试/大数据等背景则推荐 - 建议优先学习AI大模型应用开发,然后在进一步学习LLM算法方面知识提升 * **问题:后端业务+LLM大模型课程对电脑配置有什么要求,常规几千块的电脑能学不?** - 常规电脑即可学习,虽然后端项目涉及多个中间件,LLM大模型训练和私有化部署等需要用到大量硬件资源 - 课程会教采用云服务器和三方算力平台解决这类问题,几十块就可以搞定,所以不用担心。 - 课程会讲多个LLM大模型,封装成底层,容易切换不同的大模型,包括在线和离线私有化部署的大模型 - LLM大模型参数有几亿和几十亿、几百亿等参数规模,这个是需要比较大的算力资源 - 学习的时候可以使用少点参数进行练习,生产的时候可以根据公司需要选择不同级别的参数规模,结合硬件 - 这些都需要掌握,不同的级别的项目才好根据情况进行选择 #### 技术栈要求和内容安排 * **大课技术栈概览** * 基础工具环境:AI大模型编码插件+JDK21+IDEA旗舰版+VSCode+Python3.1X+Linux服务器 * 后端高并发技术:新版SpringBoot3.X+MybatisPlus+Lombok+Hutool+Mysql8.X+多个开源工具包 * 中间件+存储技术:Redis7.X+Kafak3.X-Kraft架构|RabbitMQ+分布式文件存储MinIO或OSS存储引擎 * 前后端分离架构下的 Vue3+ AntDesign+ Nginx网关+多个前端开源组件 (提供完整代码) * 超多AI大模型+模型库应用:新版GPT/ChatGLM/通义千问等+Huggingface/ModelScope等 * AI大模型技术:FastAPI框架+全新LangChain框架+向量数据库Milvus+多个大模型高频类库等 * LLM框架组件:Model+Prompt+Agent+Chains+Memory+Indexes+RAG+ReAct等 * DevOps上线部署:Jenkins CICD + 阿里云Git仓库+ 阿里云ECS 服务器+ Docker容器编排调度 * ....更多精彩 * **内容安排说明** * 前置必备技术栈:SpringBoot + Mysql +Redis + Kafka|RabbitMQ + Docker +Linux * 其他新技术栈:Python + LangChain + FastAPI + Milvus +MinIO 等大课里面会讲 ### 需求文档和架构图 #### 为什么技术Leader需要掌握产品需求文档 * 核心:有些不懂技术的产品经理没法编写特定领域的项目需求文档 * **技术知识缺乏**: * 特定领域的项目可能需要特定的技术知识。 * 如果产品经理缺乏相关技术背景,难以理解技术实现的复杂性和可行性,从而难以准确描述技术需求。 * **沟通障碍** * 产品经理需要与技术团队紧密合作,以确保需求的可实现性。 * 如果产品经理不懂技术,他们可能难以与技术团队有效沟通,导致需求文档中的技术细节不准确或不完整。 * **风险评估不足**: * 不懂技术的产品经理可能无法准确评估技术实现的风险,这可能导致项目在实施过程中遇到预料之外的问题。 * **需求优先级判断失误**: * 技术背景可以帮助产品经理判断哪些需求对项目成功最为关键。 * 缺乏技术背景的产品经理可能难以做出正确的优先级排序。 * **一份合格的产品需求文档(多数内容有即可,不同团队要求大体类似)** ``` ## 1. 标题页 - **产品名称**:[产品名称] - **版本/修订号**:[版本号] - **编制日期**:[编制日期] - **编制人**:[编制人姓名] - **审核人**:[审核人姓名] ## 2. 目录 - 根据文档内容创建目录,方便快速跳转到各个部分。 ## 3. 引言 ### 3.1 目的 - 简要说明编写此文档的目的。 ### 3.2 背景 - 描述产品的背景信息,包括市场机会、业务需求等。 ### 3.3 定义 - 对文档中使用的专业术语或缩写词进行定义。 ## 4. 产品概述 ### 4.1 产品愿景 - 描述产品的长远目标和愿景。 ### 4.2 产品目标 - 明确产品的短期和长期目标。 ### 4.3 用户和市场 - 描述目标用户群体和市场定位。 ## 5. 功能需求 ### 5.1 功能列表 - 列出产品需要实现的所有功能。 ### 5.2 功能描述 - 对每个功能进行详细描述,包括用户故事或用例。 ## 6. 非功能需求 ### 6.1 性能要求 - 描述产品的性能标准,如响应时间、并发用户数等。 ### 6.2 安全要求 - 列出产品必须满足的安全标准。 ### 6.3 可用性要求 - 描述产品的易用性和可访问性要求。 ### 6.4 法律和标准 - 指出产品需要遵守的法律、法规和行业标准。 ## 7. 技术和开发约束 - 列出技术栈、开发平台、第三方服务等技术约束。 ## 8. 项目计划 - 提供产品开发的时间线和里程碑。 ## 9. 预算和资源 - 概述项目的预算和所需资源。 ## 10. 风险评估 - 识别项目可能面临的风险,并提出相应的缓解措施。 ## 11. 附件 - 包括市场调研报告、竞品分析、用户访谈记录等支持文档。 ``` #### AI智能化云盘需求文档说明 image-20241217151318969 #### 架构图的作用和绘制技巧 - 什么是架构图 - 架构图 = 架构 + 图 - 用图的形式把系统架构展示出来,配上简单的文案 - 一图胜千言,解决沟通障碍,给不同的【业务方】看懂 - 业务方很多,不同人看到角度不一样,你让【产品经理】看 【物理部署视图】他看得懂? * 架构图是给人看的,这些人我们习惯称为【业务方、客户】,有哪些人? - 人员 - 上级:你的公司Leader(晋升汇报)、老板、外部投资人 - 团队内:产品、运营、测试、技术、运维同学 - 外部:最终系统使用的用户 - 好比阿里这边评定绩效,有一项就是业务方评分 - 你做的外部用户的活动系统,测试同学会进行测试,太多bug肯定就不行 - 你做的给运营同学使用的系统,不能提升她运营的效率,业务方是否满意? * 为什么要搞出这么多个架构图?用一个图不行吗? - 一开始确实是一个图表示系统架构设计 - 但是业务方很多,不同人看到角度不一样,你让软件用户看物理部署视图?他看得懂? - 要明确沟通交流面向的客户 - 开发人员、运维人员、项目经理、软件最终用户、客户 - 避免在一张图中展示所有细节,根据受众的需要简化信息,突出关键组件和关系。 - 不同架构视图承载不同的架构设计决策,支持不同的目标和用途 - 架构图也不能太多(过度文档化)维护更新起来成本大 - 不同架构图应该使用哪种方法来画? - 可以用的表示法和工具很多,没有太多的限制,把握对应的视图关注点才是关键 - Xmind、EdrawMax、PPT、PowerDesigner - OmniGraffle、Visio、Process On - 开始阶段不要陷入过度设计中,没那么多需求不一定要那么多图(你是否有那么多客户) * 常见架构图作用对比 * 产品/应用/产品业务架构 - 表达业务是如何开展的,服务于业务目标,通过描绘业务上下层关系,简单的业务视图降低业务系统的复杂 - 是对整个系统实现的总体架构 , 应用架构和**系统架构**很大类似 - 一方面承接业务架构的落地,一方面影响技术选型 - 注意:一般应用架构图【不加入太多技术框架和实现】 - 下面这个是什么架构图(产品架构图-方便技术和产品沟通,图片阿里云官方网站VOD视频点播) ![img](./img/p411278-4421764.png) * 技术架构 - 应用架构本身只关心需要哪些应用系统,不关心在整个项目中你需要使用哪些技术 - 技术架构则是实现应用架构的承接方,识别技术需求,进行技术选型,描述技术之间的关系 - 解决的问题包括 - 技术层面的分层、开发语言、框架的选择 - 通信技术、存储技术的选择、非功能性需求的技术选择等 #### 教你画架构图 - 在画架构图之前,想清楚3个问题,架构图想表达什么?有什么用?给谁看? - 表达是业务系统之间的关系,梳理业务结构 - 将复杂的业务逻辑简单化,降低理解难度,更方便业务方理解 - 给业务方查看,业务相关干系人 - 业务架构图 - 表达业务是如何开展的,服务于业务目标,通过描绘业务上下层关系,简单的业务视图降低业务系统的复杂度,提高客户理解度 - 图中【尽量不出现技术】的字眼,不同架构图的读者是不同的,确保能看懂。 - 架构图中模块的划分粒度,一定要合适,既不能太宽泛,也不能太细粒度 - 无技术背景人员可参与实现的讨论,向技术人员描述解决方案核心要做什么,必须实现的关键是什么 - 明白一个点 - 先有业务,再有系统,微服务/系统/中心 是类似概念 - 系统是来实现业务的,比如电商业务里面A系统、B系统 * 业务架构类型 - 上中下结构:用户展现层-业务平台层-公共能力层-数据存储层-基础资源层 - 案例一(图片来源-阿里云数字政府) ![image-20241217155520041](./img/image-20241217155520041.png) - 左中右结构:上游产业 - 业务平台- 下游产业 - 相对较少用,就是倒置过去 * 画图三步走(**不同架构图通用法则**) - 分层 - 业务按照层级进行划分,各个层级属于独立的版块 - 下层为上层提供服务能力支撑,比如:laaS / PaaS / SaaS - 分模块 - 同层级中进行小归类;属于平行关系,可以独立存在 - 理清架构图类型、业务要全面、专业术语一致、图形清晰美观、颜色类型划分合理 - 不同颜色可以表示当下要做的,未来要做的 - 分功能 - 独立功能划分出来,即业务入口 - 业务方重点关注的功能点,可以认为是微服务划分 ![image-20241217155622979](./img/image-20241217155622979.png) * 如何判断架构图的好和坏? - 业务抽象设计的合理性,是否满足高内聚、低耦合的要求,不能太宽泛,也不能太细粒度 - 层级划分目标系统边界,自下而上 或 由上而下,一般包括 基础设施、数据层、应用层、用户层四个层次 - 使用清晰的布局,确保组件之间的连接线不交叉,易于跟踪。 - 使用颜色和样式来区分不同类型的组件,但不要过度使用,以免分散注意力。 - 纵向分层 上层依赖于下层越底层,越是基础服务;横向并列关系,级别相同 - 理清架构图类型、业务要全面、专业术语一致、图形清晰美观、颜色类型划分合理 - 最重要是:**你的业务方能 满意+看懂!!!** #### AI智能化云盘应用架构图讲解 * 什么是应用架构图 - 是对整个系统实现的总体架构 , 应用架构和**系统架构**很大类似 - 一方面承接业务架构的落地,一方面影响技术选型 * 注意:一般应用架构图【不加入太多技术框架和实现】 - 作用 - 根据业务场景 对系统进分层,指出开发的原则、系统各个层次的应用服务 - 业务方 - **研发人员,各层级架构师,各层级技术管理者** - 分类 - 多系统应用架构,用来分层次说明不同系统间的业务逻辑关系、系统边界等,比如 分布式、微服务 - 单系统应用架构,用来分层次说明系统的组成模块和功能点之间的业务逻辑关系,比如单体应用 - 常规分层 - 表示-展现层:负责用户体验 - 业务-服务层:负责业务逻辑 - 数据-访问层:负责数据库存取 * 画图三步走 - 分层 - 业务按照层级进行划分,各个层级属于独立的版块 - 下层为上层提供服务能力支撑,比如:laaS / PaaS / SaaS - 分模块 - 同层级中进行小归类;属于平行关系,可以独立存在 - 理清架构图类型、业务要全面、专业术语一致、图形清晰美观、颜色类型划分合理 - 不同颜色可以表示当下要做的,未来要做的 - 分功能 - 独立功能划分出来,即业务入口 - 业务方重点关注的功能点,可以认为是微服务划分 * 新一代AI智能化云盘应用架构图(找bug) image-20241217180704525 #### AI智能化云盘技术架构图和作业提交 * 什么是技术架构 - 应用架构本身只关心需要哪些应用系统,不关心在整个项目中你需要使用哪些技术 - 技术架构则是实现应用架构的承接方,识别技术需求,进行技术选型,描述技术之间的关系 - 解决的问题包括 - 技术层面的分层、开发语言、框架的选择 - 通信技术、存储技术的选择、非功能性需求的技术选择等 - 案例 image-20241217160721765 * 新一代AI智能化云盘技术选型(**下面只是部分技术栈**) * 基础工具环境:AI大模型编码插件+JDK21+IDEA旗舰版+VSCode+Python3.1X+Linux服务器 * 后端高并发技术:新版SpringBoot3.X+MybatisPlus+Lombok+Hutool+Mysql8.X+多个开源工具包 * 中间件+存储技术:Redis7.X+Kafak3.X-Kraft架构+分布式文件存储MinIO或OSS存储引擎 * 前后端分离架构下的 Vue3+ AntDesign+ Nginx网关+多个前端开源组件 (提供完整代码) * 超多AI大模型+模型库应用:新版GPT/ChatGLM/通义千问等+Huggingface/ModelScope等 * AI大模型技术:FastAPI框架+全新LangChain框架+向量数据库Milvus+多个大模型高频类库等 * LLM框架组件:Model+Prompt+Agent+Chains+Memory+Indexes+RAG+ReAct等 * DevOps上线部署:Jenkins CICD + 阿里云Git仓库+ 阿里云ECS 服务器+ Docker容器编排调度 ### 开发环境搭建 #### AI编码插件 AI会淘汰程序员? * AI技术的发展一定程度上改变我们程序员的工作方式,例如自动化一些重复性任务,辅助程序员进行代码审查和优化等 * 也可以编写包括中等程度的CURD、算法等;但AI很难完全替代程序员,可以很大程度辅助我们工程师 * 程序员在创造力、人际沟通、适应新技术、解决复杂问题以及法律责任等方面具有不可替代的优势,AI背锅? * 如果程序员不懂技术,你能判断AI写的代码上生产环境?出问题你可以排查? AI编码插件对比 * CodeGeeX(清华大学+智谱AI) * 地址:https://codegeex.cn/ * 优点: * 多语言代码生成模型,支持代码生成与补全、自动添加注释、代码翻译以及智能问答等功能 * 支持多种主流编程语言,并适配多种主流IDE * 对于个人开发者完全免费,国内开发,无需额外连接VPN * 缺点 * 对于复杂的场景,AI工具可能提供错误的答案 * 通义灵码(阿里) * 地址:https://tongyi.aliyun.com/lingma * 优点: * 基于通义大模型,提供代码智能生成、研发智能问答能力 * 支持行级/函数级实时续写,自然语言生成代码 * 生成单元测试,支持多种测试框架。 * 支持多种主流编程语言 * 缺点 * 单元测试生成功能表现一般 * 高级功能需要付费 * GitHub Copilot * 地址:https://github.com/features/copilot/ * 优点: * 根据提示自动生成代码,提高开发效率 * 学习项目中的代码风格,获取足够多的上下文,并根据其生成代码 * 支持多种编程语言,适用范围广 * 缺点: * 可能存在隐私问题 * 功能收费,对于个人开发者成本较高 * **其他比较牛的(都需要科学上网):** * **Cursor、Claude** * 能够完成复杂的任务,并且可以与其他系统集成,支持多种应用场景,包括独立开发程序 * **一个是目前适配AI最好的代码编辑器,一个是目前AI编程能力最强的大模型。** #### SpringBoot3.X本地开发环境创建 技术版本 * Maven-3.9以上: `mvn -version` * JDK-21版本(LTS版本 主流应该是26到28年) * 新版IDEA-旗舰版 * 框架版本-SpringBoot3.X 项目创建 ycloud-aipan * 快速创建地址:https://start.spring.io/ ```xml org.springframework.boot spring-boot-starter-parent 3.2.4 ``` #### 依赖初始化 * 项目依赖配置添加 ```xml 21 1.12.730 3.5.6 5.8.27 2.8.0 2.0.42 8.0.27 ``` * 工程依赖配置 **最佳建议:把这章这集的代码导入到你们IDEA里面,进行构建** ```xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop mysql mysql-connector-java ${mysql.version} org.projectlombok lombok 1.18.30 org.springframework.boot spring-boot-starter-test test com.amazonaws aws-java-sdk-s3 ${aws-java-sdk-s3.version} io.jsonwebtoken jjwt 0.12.3 com.baomidou mybatis-plus-spring-boot3-starter ${mybatisplus.version} com.baomidou mybatis-plus-generator ${mybatisplus.version} org.apache.velocity velocity-engine-core 2.0 cn.hutool hutool-all ${hutool-all.version} com.alibaba fastjson ${fastjson.version} com.github.xiaoymin knife4j-openapi3-jakarta-spring-boot-starter 4.4.0 org.springframework.boot spring-boot-maven-plugin org.apache.maven.plugins maven-compiler-plugin 3.1 ${java.version} ${java.version} org.apache.maven.plugins maven-surefire-plugin 2.19.1 true ``` #### Linux操作系统EOL解决方案 * 操作系统停止维护EOL(End of Life) * 大家也知道很多生产环境操作系统都是使用CentOS,尤其是互联网公司 * 但是CentOS官方在24年尾的时候停止了支持,这个就涉及到切换系统 * 建议 * Linux大体是类似的,迁移需要周期,常规25到28年还会是多数公司的首选CentOS * 所以大家还是需要掌握这个主流的系统;如果新项目则可以选择其他操作系统 * 常见的 CentOS 替代方案,包括 AlmaLinux、Rocky Linux、Oracle Linux、Ubuntu 和 Debian * Rocky Linux9.X以上 ,推荐2核4G或4核8G #### Docker镜像加速+软件安装 * 软件安装 * Docker-ce社区版本 * Mysql8.X * 可视化工具自己选择 * Redis7.X * 可视化工具下载地址 * https://gitee.com/qishibo/AnotherRedisDesktopManager * https://github.com/qishibo/AnotherRedisDesktopManager 安装脚本 ```shell ————————Docker-ce社区版本———————— #运行以下命令,下载docker-ce的yum源。 sudo wget -O /etc/yum.repos.d/docker-ce.repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo #运行以下命令,安装Docker。 sudo yum -y install docker-ce #执行以下命令,检查Docker是否安装成功。 sudo docker -v #执行以下命令,启动Docker服务,并设置开机自启动。 sudo systemctl start docker sudo systemctl enable docker #执行以下命令,查看Docker是否启动。 sudo systemctl status docker #配置Docker镜像加速 ([ -f /etc/docker/daemon.json ] || mkdir -p /etc/docker) && echo '{ "registry-mirrors" : [ "https://docker.m.daocloud.io", "https://noohub.ru", "https://huecker.io", "https://dockerhub.timeweb.cloud" ] }' > /etc/docker/daemon.json && sudo systemctl restart docker && sleep 1 && docker info | grep -A 4 "Registry Mirrors" # ————————Mysql8.X安装———————— #创建目录 mkdir -p /home/data/mysql/ #创建配置文件 touch /home/data/mysql/my.cnf #部署 docker run \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=xdclass.net168 \ -v /home/data/mysql/conf:/etc/mysql/conf.d \ -v /home/data/mysql/data:/var/lib/mysql:rw \ -v /home/data/mysql/my.cnf:/etc/mysql/my.cnf \ --name xdclass_mysql \ --restart=always \ -d mysql:8.0 # ————————Redis7.X———————— docker run -itd --name xdclass-redis -p 6379:6379 -v /mydata/redis/data:/data redis:7.0.8 --requirepass abc123456 ``` #### 纳入阿里云Git版本控制 基于git协议的代码仓库 - github 全球最大同性交友社区 - gitee 开源中国 - gitlab 开源的git仓库平台,阿里等大厂就是基于这个搭建 - codeup 阿里云上的免费git仓库 * 地址:https://codeup.aliyun.com/ * 配置ssh * 纳入管理 #### 项目规范说明和工具类封装 响应工具、通用工具、Json工具、对象拷贝工具、枚举状态码、全局异常处理 - 响应工具 ```java /** * 响应类 */ @Data @AllArgsConstructor @NoArgsConstructor public class JsonData { /** * 状态码 0 表示成功 */ private Integer code; /** * 数据 */ private Object data; /** * 描述 */ private String msg; /** * 获取远程调用数据 * * @param typeReference 数据类型的引用 * @param 泛型类型 * @return 返回解析后的对象 */ public T getData(Class typeReference) { return JSON.parseObject(JSON.toJSONString(data), typeReference); } /** * 成功,不传入数据 * * @return 返回一个状态码为0的JsonData对象 */ public static JsonData buildSuccess() { return new JsonData(0, null, null); } /** * 成功,传入数据 * * @param data 成功时返回的数据 * @return 返回一个JsonData对象,其中包含状态码0和传入的数据 */ public static JsonData buildSuccess(Object data) { return new JsonData(0, data, null); } /** * 失败,传入描述信息 * * @param msg 失败时的描述信息 * @return 返回一个JsonData对象,其中包含状态码-1和传入的描述信息 */ public static JsonData buildError(String msg) { return new JsonData(-1, null, msg); } /** * 自定义状态码和错误信息 * * @param code 自定义的状态码 * @param msg 自定义的错误信息 * @return 返回一个JsonData对象,其中包含传入的状态码和错误信息 */ public static JsonData buildCodeAndMsg(int code, String msg) { return new JsonData(code, null, msg); } /** * 自定义状态码和错误信息 * * @param codeEnum 自定义的状态码枚举 * @return 返回一个JsonData对象,其中包含传入的状态码枚举对应的状态码和错误信息 */ public static JsonData buildResult(BizCodeEnum codeEnum) { return JsonData.buildCodeAndMsg(codeEnum.getCode(), codeEnum.getMessage()); } /** * 判断当前JsonData对象是否表示成功 * * @return 如果状态码为0,则返回true,表示成功;否则返回false,表示失败 */ public boolean isSuccess() { return code == 0; } } ``` - 通用工具 ```java package org.ycloud.aipan.util; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.io.PrintWriter; @Slf4j public class CommonUtil { /** * 响应json数据给前端 * * @param response HttpServletResponse对象,用于向客户端发送响应 * @param obj 需要转换为json格式的对象 */ public static void sendJsonMessage(HttpServletResponse response, Object obj) { // 设置响应内容类型为json,并指定字符编码为utf-8 response.setContentType("application/json; charset=utf-8"); try (PrintWriter writer = response.getWriter()) { // 将对象转换为json字符串并写入响应输出流 writer.print(JsonUtil.obj2Json(obj)); // 刷新缓冲区,确保数据被发送到客户端 response.flushBuffer(); } catch (IOException e) { // 捕获并记录异常信息 log.warn("响应json数据给前端异常:{}", e.getMessage()); } } /** * 根据文件名称获取文件后缀 * * @param fileName 文件名 * @return 文件后缀名 */ public static String getFileSuffix(String fileName) { // 从文件名中提取后缀名 return fileName.substring(fileName.lastIndexOf(".") + 1); } /** * 根据文件后缀,生成文件存储路径:年/月/日/uuid.suffix 格式 * * @param fileName 文件名 * @return 生成的文件存储路径 */ public static String getFilePath(String fileName) { // 获取文件后缀名 String suffix = getFileSuffix(fileName); // 生成文件在存储桶中的唯一键 return StrUtil.format("{}/{}/{}/{}.{}", DateUtil.thisYear(), DateUtil.thisMonth() + 1, DateUtil.thisDayOfMonth(), IdUtil.randomUUID(), suffix); } } ``` - Json工具 ```java @Slf4j public class JsonUtil { // 创建一个ObjectMapper对象,用于处理JSON数据的序列化和反序列化 private static final ObjectMapper MAPPER = new ObjectMapper(); // 静态代码块,用于初始化ObjectMapper对象的配置 static { //设置可用单引号 MAPPER.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); //序列化的时候序列对象的所有属性 MAPPER.setSerializationInclusion(JsonInclude.Include.ALWAYS); //反序列化的时候如果多了其他属性,不抛出异常 MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); //下划线和驼峰互转 //mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); //如果是空对象的时候,不抛异常 MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); //取消时间的转化格式,默认是时间戳,可以取消,同时需要设置要表现的时间格式 MAPPER.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); MAPPER.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); } /** * 获取ObjectMapper对象 * * @return ObjectMapper对象 */ public static ObjectMapper get() { return MAPPER; } /** * 将对象转换为JSON字符串 * * @param obj 要转换的对象 * @return JSON字符串 */ public static String obj2Json(Object obj) { String jsonStr = null; try { jsonStr = MAPPER.writeValueAsString(obj); } catch (JsonProcessingException e) { log.error("json格式化异常", e); } return jsonStr; } /** * 将JSON字符串转换为对象 * * @param jsonStr 要转换的JSON字符串 * @param beanType 目标对象的类型 * @return 转换后的对象 */ public static T json2Obj(String jsonStr, Class beanType) { T obj = null; try { obj = MAPPER.readValue(jsonStr, beanType); } catch (Exception e) { log.error("json格式化异常", e); } return obj; } /** * 将JSON数据转换为对象列表 * * @param jsonData 要转换的JSON数据 * @param beanType 目标对象的类型 * @return 转换后的对象列表 */ public static List json2List(String jsonData, Class beanType) { JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType); try { // 使用ObjectMapper将JSON数据转换为对象列表 return MAPPER.readValue(jsonData, javaType); } catch (Exception e) { log.error("json格式化异常", e); } // 返回空列表 return new ArrayList<>(0); } } ``` - 对象拷贝工具 ```java /** * SpringBeanUtil 工具类,提供了对象属性复制的功能。 */ public class SpringBeanUtil { /** * 复制属性 * * @param 目标对象类型 * @param source 源对象 * @param target 目标对象类型 * @return 复制后的目标对象 */ public static T copyProperties(Object source, Class target) { try { T t = target.getConstructor().newInstance(); BeanUtils.copyProperties(source, t); return t; } catch (Exception e) { throw new RuntimeException(e); } } /** * 复制一份具有相同属性的列表 * * @param sourceList 源列表 * @param target 目标对象的类型 * @param 目标对象的类型 * @return 复制后的目标列表 */ public static List copyProperties(List sourceList, Class target) { ArrayList targetList = new ArrayList<>(); sourceList.forEach(source -> { T t = copyProperties(source, target); targetList.add(t); }); return targetList; } /** * 复制属性 * * @param source 源对象 * @param target 目标对象 */ public static void copyProperties(Object source, Object target){ BeanUtils.copyProperties(source,target); } } ``` - 枚举状态码 ```java @Getter @AllArgsConstructor public enum BizCodeEnum { /** * 账号 */ ACCOUNT_REPEAT(250001, "账号已经存在"), ACCOUNT_UNREGISTER(250002, "账号不存在"), ACCOUNT_PWD_ERROR(250003, "账号或者密码错误"), ACCOUNT_UNLOGIN(250004, "账号未登录"), /** * 文件操作相关 */ FILE_NOT_EXISTS(220404, "文件不存在"), FILE_RENAME_REPEAT(220405, "文件名重复"), FILE_DEL_BATCH_ILLEGAL(220406, "文件删除参数错误"), FILE_TYPE_ERROR(220407, "文件类型错误"), FILE_CHUNK_TASK_NOT_EXISTS(230408, "分片任务不存在"), FILE_CHUNK_NOT_ENOUGH(230409, "分片数量不匹配,合并不够"), FILE_STORAGE_NOT_ENOUGH(240403, "存储空间不足"), FILE_TARGET_PARENT_ILLEGAL(250403, "目标父级目录不合法"), SHARE_CANCEL_ILLEGAL(260403, "取消分享失败,参数不合法"), SHARE_CODE_ILLEGAL(260404, "分享码不合法"), SHARE_NOT_EXIST(260405, "分享不存在"), SHARE_CANCEL(260406, "分享已取消"), SHARE_EXPIRED(260407, "分享已过期"), SHARE_FILE_ILLEGAL(260408, "分享的文件不合规"); private final int code; private final String message; } ``` - 全局异常处理 ```java /** * 业务异常类,继承自 RuntimeException * 用于封装业务逻辑中的异常信息 */ @Data public class BizException extends RuntimeException { /** * 异常代码 */ private int code; /** * 异常消息 */ private String msg; /** * 异常详细信息 */ private String detail; /** * 构造函数,使用自定义的异常代码和消息 * * @param code 异常代码 * @param message 异常消息 */ public BizException(Integer code, String message) { // 调用父类构造函数,设置异常消息 super(message); // 设置异常代码 this.code = code; // 设置异常消息 this.msg = message; } /** * 构造函数,使用 BizCodeEnum 枚举中的异常代码和消息 * * @param bizCodeEnum 业务代码枚举 */ public BizException(BizCodeEnum bizCodeEnum) { // 调用父类构造函数,设置异常消息 super(bizCodeEnum.getMessage()); // 设置异常代码 this.code = bizCodeEnum.getCode(); // 设置异常消息 this.msg = bizCodeEnum.getMessage(); } /** * 构造函数,使用 BizCodeEnum 枚举中的异常代码和消息,并包含原始异常的详细信息 * * @param bizCodeEnum 业务代码枚举 * @param e 原始异常 */ public BizException(BizCodeEnum bizCodeEnum, Exception e) { // 调用父类构造函数,设置异常消息 super(bizCodeEnum.getMessage()); // 设置异常代码 this.code = bizCodeEnum.getCode(); // 设置异常消息 this.msg = bizCodeEnum.getMessage(); // 设置异常详细信息 this.detail = e.toString(); } } ``` ```java /** * 自定义异常处理器 * 用于捕获并处理全局异常,返回统一的JSON格式响应 */ @ControllerAdvice @Slf4j public class CustomExceptionHandler { /** * 处理所有异常的方法 * * @param e 捕获到的异常对象 * @return JsonData对象,包含错误码和错误信息 */ @ExceptionHandler(value = Exception.class) @ResponseBody public JsonData handler(Exception e){ // 判断异常是否为业务异常 if(e instanceof BizException bizException){ // 记录业务异常日志 log.error("[业务异常]",e); // 返回业务异常的错误码和错误信息 return JsonData.buildCodeAndMsg(bizException.getCode(),bizException.getMsg()); }else { // 记录系统异常日志 log.error("[系统异常]",e); // 返回系统异常的错误信息 return JsonData.buildError("系统异常"); } } } ``` ### 存储引擎MinIO和AWS-S3常规API实战 #### 分布式文件存储行业解决方案和技术选型分析 * 背景说明 * 数据爆炸的时代,产生的数据量不断地在攀升,基本都离不开文件存储 - 存储单位从KB、MB、GB、TB、PB到ZB级别的数据,图片、文档、素材、静态化页面、长短视频、安装包等一系列文件 * 业务应用内存储 - 传统的javaweb项目, 文件数量达到一定后占据大量的内存、磁盘和带宽, 无法满足海量请求的业务 - 开发容易-扩容难 * 分布式文件系统(Distributed File System) - 海量数据对存储提出了新的要求,从而诞生了分布式文件存储 - 文件系统管理的物理存储资源不一定直接连接在本地节点上,而是通过计算机网络与节点相连 - 扩容容易-开发难 * 目前业界比较多的解决方案 * 免费:MinIO * 官网:https://minio.org.cn/ * 是一个高性能、分布式的对象存储系统,完全兼容Amazon S3协议 * 学习成本低,安装运维简单,主流语言的客户端整合都有, 号称最强的对象存储文件服务器 * 提供简单的Web界面和广泛的API支持,方便集成和开发 * 适用于各种规模的部署,从个人小型项目到大型企业级应用 * 提供数据加密功能、访问控制、身份验证功能 * 具有高可用性,可以在分布式环境中运行,并自动处理数据的冗余和复制 * 高度可扩展性,可以根据需求增加更多的存储节点或容量来扩展存储规模 * 花钱:云厂商 - 阿里云OSS、七牛云、亚马逊云 * 面试官:智能化云盘如何选型哪类存储呢,自建或者云厂商如何思考,为啥选择这个? * 选云厂商理由 * 优点:开发简单,功能强大,容易维护(不同网络下图片质量、水印、加密策略、扩容、加速) * 缺点:要钱, 个性化处理,未来转移比较复杂,不排除有些厂商会提供一键迁移工具 - 选开源MinIO的理由 - 优点:功能强大、可以根据业务做二次的定制,新一代分布式文件存储系统,容器化结合强大,更重要的是免费 - 缺点:自己需要有专门的团队进行维护、扩容等 * 推荐答案 * 参考一:由于平台业务特殊性,多数企业会考虑【私有化】部署,因此如果绑定外部对象存储,则迁移麻烦 * 参考二:公司现有的分布式文件存储基建平台采用的是MinIO,技术团队也比较熟悉,也满足业务需求 #### Docker容器化部署分布式文件存储MinIO实战 * 部署MinIO实战 ```shell mkdir -p /minio/data chmod 777 /minio/data docker run \ -d --restart=always \ --name minio \ --hostname minio-server \ -p 9000:9000 \ -p 9001:9001 \ -v /app/docker/minio/data:/bitnami/minio/data \ -e MINIO_ROOT_USER="minio_root" \ -e MINIO_ROOT_PASSWORD="minio_123456" \ -e MINIO_DEFAULT_BUCKETS="bucket" \ -e "MINIO_SERVER_URL=http://39.108.115.28:9000" \ bitnami/minio:2023.12.7 ``` * 端口说明 * 9000端口是用于内部访问,比如通过SpringBoot接口间接访问MinIO * 9001端口是用于外部访问,即通过浏览器访问 * 安装实战 * 网络安全组开放端口 9000, 9001 * 访问:ip+9001端口 * 操作 * 界面登录 * 文件上传下载 * **疑惑点:那么多存储引擎,是否有行业标准接口协议呢?类似JDBC一样,可以对接多个数据库** #### SpringBoot3.X整合MinIO存储原生方案 * 需求 * SpringBoot3.X整合MinIO文件上传开发实战,采用原生方案 * 编码实战 * 项目增加依赖 ```xml io.minio minio 8.3.7 ``` * 配置文件 ```yaml # minio配置 minio: endpoint: http://39.108.115.28:9000 access-key: minio_root access-secret: minio_123456 bucket-name: ai-pan ``` * 配置类 ```java @Data @Component @ConfigurationProperties(prefix = "minio") public class MinioConfig { @Value("endpoint") private String endpoint; @Value("access-key") private String accessKey; @Value("access-secret") private String accessSecret; @Value("bucket-name") private String bucketName; // 预签名url过期时间(ms) private Long PRE_SIGN_URL_EXPIRE = 60 * 10 * 1000L; } ``` * 测试文件上传 ```java @PostMapping("/upload") public JsonData upload(@RequestParam("file") MultipartFile file) { return JsonData.buildSuccess(minioService.upload(file)); } @Override public String upload(MultipartFile file) { // 获取上传文件名 String filename = CommonUtil.getFilePath(file.getOriginalFilename()); try { InputStream inputStream = file.getInputStream(); minioClient.putObject(PutObjectArgs.builder() .bucket(minioConfig.getBucketName()) .object(filename) .stream(inputStream, file.getSize(), -1) .contentType(file.getContentType()) .build()); } catch (Exception e) { throw new BizException(BizCodeEnum.FILE_REMOTE_UPLOAD_FAILED,e); } return minioConfig.getEndpoint() + "/" + minioConfig.getBucketName() + "/" + filename; } ``` #### AWS-S3通用存储协议介绍和项目依赖配置 * 什么是Amazon S3 * Amazon S3(Amazon Simple Storage Service)是亚马逊提供的一种对象存储服务,行业领先的可扩展性、数据可用性和性能 * 就类似阿里云OSS、七牛云OSS、MinIO等多个存储服务一样 * Amazon S3协议 * 是Amazon Simple Storage Service(简称Amazon S3)的接口规范 * 它是一种基于HTTP协议的RESTful API,用于访问Amazon Web Services(AWS)提供的对象存储服务 * S3-API: https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/API/API_Operations_Amazon_Simple_Storage_Service.html * 支持阿里云OSS、七牛云OSS(对象存储服务) * 在一定程度上与Amazon S3协议兼容,可以使用S3 API来操作OSS多数操作 * 存在一些差异,如ACL权限定义、存储类型处理,需要单独处理 * 支持MinIO * 兼容Amazon S3协议的对象存储服务器,它提供了与Amazon S3完全相同的S3 API兼容性 * 在公共云、私有云中 ,MinIO支持广泛的S3 API,包括S3 Select和AWS Signature V4,复杂的查询和身份验证 。 * Amazon S3构建的应用程序可以无缝迁移到MinIO,无需任何代码更改 * 如何用? * 项目添加依赖,配置相关底层存储即可 * 是亚马逊提供的官方软件开发工具包,用在Java程序与Amazon Simple Storage Service(S3)进行交互 * AWS Java SDK for S3提供了创建S3客户端、上传、下载、列出、复制、删除S3存储桶中的对象等功能 ```xml com.amazonaws aws-java-sdk-s3 ${aws-java-sdk-s3.version} ``` * 代码配置 ```java /** * 配置类,用于定义Bean并配置Amazon S3客户端 */ @Configuration public class AmazonS3Config { // 注入Minio配置类,用于获取访问密钥和Endpoint等信息 @Resource private MinioConfig minioConfig; /** * 创建并配置Amazon S3客户端 * * @return AmazonS3 实例,用于与Amazon S3服务进行交互 */ @Bean(name = "amazonS3Client") public AmazonS3 amazonS3Client() { // 设置连接时的参数 ClientConfiguration config = new ClientConfiguration(); // 设置连接方式为HTTP,可选参数为HTTP和HTTPS config.setProtocol(Protocol.HTTP); // 设置网络访问超时时间 config.setConnectionTimeout(5000); config.setUseExpectContinue(true); // 使用Minio配置中的访问密钥和秘密密钥创建AWS凭证 AWSCredentials credentials = new BasicAWSCredentials(minioConfig.getAccessKey(), minioConfig.getAccessSecret()); // 设置Endpoint AwsClientBuilder.EndpointConfiguration endpointConfiguration = new AwsClientBuilder .EndpointConfiguration(minioConfig.getEndpoint(), Regions.US_EAST_1.name()); // 使用以上配置创建并返回Amazon S3客户端实例 return AmazonS3ClientBuilder.standard() .withClientConfiguration(config) .withCredentials(new AWSStaticCredentialsProvider(credentials)) .withEndpointConfiguration(endpointConfiguration) .withPathStyleAccessEnabled(true).build(); } } ``` #### AWS-S3通用存储案例接口测试和封装实战 - 案例代码测试-Bucket相关操作 ```java @SpringBootTest @Slf4j class AmazonS3ClientTests { @Autowired private AmazonS3Client amazonS3Client; /** * 判断bucket是否存在 */ @Test public void testBucketExists() { boolean bucketExist = amazonS3Client.doesBucketExist("ai-pan1"); log.info("bucket是否存在:{}", bucketExist); } /** * 创建bucket */ @Test public void testCreateBucket() { String bucketName = "ai-pan1"; Bucket bucket = amazonS3Client.createBucket(bucketName); log.info("bucket:{}", bucket); } /** * 删除bucket */ @Test public void testDeleteBucket() { String bucketName = "ai-pan1"; amazonS3Client.deleteBucket(bucketName); } /** * 获取全部bucket */ @Test public void testListBuckets() { for (Bucket bucket : amazonS3Client.listBuckets()) { log.info("bucket:{}", bucket.getName()); } } /** * 根据bucket名称获取bucket详情 */ @Test public void testGetBucket() { String bucketName = "ai-pan1"; Optional optionalBucket = amazonS3Client.listBuckets().stream().filter(bucket -> bucketName.equals(bucket.getName())).findFirst(); if (optionalBucket.isPresent()) { log.info("bucket:{}", optionalBucket.get()); } else { log.info("bucket不存在"); } } } ``` - 案例代码测试-文件相关操作 ```java /** * 上传单个文件,直接写入文本 */ @Test public void testUploadFile() { PutObjectResult putObject = amazonS3Client.putObject("ai-pan", "test1.txt", "hello world11"); log.info("putObject:{}", putObject); } /** * 上传单个文件,直接写入文本 */ @Test public void testUploadFile2() { amazonS3Client.putObject("ai-pan", "test2.txt", new File("/Users/xdclass/Desktop/dpan.sql")); } /** * 上传文件 包括文件夹路径 不带斜杠 都一样 */ @Test public void testUploadFileWithDir1() { amazonS3Client.putObject("ai-pan", "aa/bb/test3.txt", new File("/Users/xdclass/Desktop/dpan.sql")); } /** * 上传文件 包括文件夹路径 带斜杠 都一样 */ @Test public void testUploadFileWithDir2() { amazonS3Client.putObject("ai-pan", "/a/b/test4.txt", new File("/Users/xdclass/Desktop/dpan.sql")); } /** * 上传文件,输入流的方式 带上文件元数据 */ @Test @SneakyThrows public void testUploadFileWithMetadata() { try (FileInputStream fileInputStream = new FileInputStream("/Users/xdclass/Desktop/dpan.sql");) { ObjectMetadata objectMetadata = new ObjectMetadata(); objectMetadata.setContentType("text/plain"); amazonS3Client.putObject("ai-pan", "/meta/test5.txt", fileInputStream, objectMetadata); } } /** * 上传文件,输入流的方式 带上文件元数据 */ @Test @SneakyThrows public void testUploadFileWithMetadata2() { try (FileInputStream stream = new FileInputStream("/Users/xdclass/Desktop/dpan.sql");) { byte[] bytes = IOUtils.toByteArray(stream); ObjectMetadata objectMetadata = new ObjectMetadata(); objectMetadata.setContentType("text/plain"); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); // 上传 amazonS3Client.putObject("ai-pan", "/meta/testIO.txt", byteArrayInputStream, objectMetadata); } } /** * 获取文件 */ @Test @SneakyThrows public void testGetObject() { try (FileOutputStream fileOutputStream = new FileOutputStream(new File("/Users/xdclass/Desktop/test5.txt"));) { S3Object s3Object = amazonS3Client.getObject("ai-pan", "/meta/test5.txt"); s3Object.getObjectContent().transferTo(fileOutputStream); } } /** * 删除文件 */ @Test public void testDeleteObject() { amazonS3Client.deleteObject("ai-pan", "/meta/test5.txt"); } /** * 生成文件访问地址 */ @Test public void testGeneratePresignedUrl() { // 预签名url过期时间(ms) long PRE_SIGN_URL_EXPIRE = 60 * 10 * 1000L; // 计算预签名url的过期日期 Date expireDate = DateUtil.offsetMillisecond(new Date(), (int) PRE_SIGN_URL_EXPIRE); // 创建生成预签名url的请求,并设置过期时间和HTTP方法, withMethod是生成的URL访问方式 GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest("ai-pan", "/meta/test5.txt") .withExpiration(expireDate).withMethod(HttpMethod.GET); // 生成预签名url URL preSignedUrl = amazonS3Client.generatePresignedUrl(request); // 输出预签名url System.out.println(preSignedUrl.toString()); } ``` ### 存储引擎-设计模式案例实战和AI代码一键优化 #### 策略模式设计模式应用-文件存储引擎抽取方案 * 策略模式(Strategy Pattern) - 定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换 - **定义共享接口**:首先定义共享接口,接口规定了所有支持的算法必须遵循的规则。 - **实现具体策略**:为这个接口提供多种不同的实现,每个实现代表一个具体的算法或行为。 - 比如 - 淘宝天猫双十一,正在搞活动有打折的、有满减的、有返利的等等,这些算法只是一种策略,并且是随时都可能互相替换的, - 我们就可以定义一组算法,将每个算法都封装起来,并且使它们之间可以互换 - **优点**: - **算法的封装**:策略模式将算法封装在独立的策略类中,使得算法可以独立于使用它们的客户端变化。 - **易于扩展**:新增算法时,只需新增一个实现了共享接口的策略类,无需修改原有代码。 - **简化单元测试**:可以单独对每个策略进行单元测试。 **缺点**: - **客户端需要知道所有策略类**:客户端需要了解所有策略类的存在,以便能够选择合适的策略。 - **增加系统复杂性**:如果策略类数量过多,可能会增加系统的复杂性。 - **角色** - Context上下文:屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化【不复杂可以去除】 - Strategy策略角色:抽象策略角色,是对策略、算法家族的抽象,定义每个策略或算法必须具有的方法和属性 - ConcreteStrategy具体策略角色:用于实现抽象策略中的操作,即实现具体的算法 image-20241220112247685 * 应用场景 - 外出旅游,选择骑自行车、坐汽车、飞机等,每一种旅行方式都是一个策略 - 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么可以使用策略模式 - 不希望暴露复杂的、与算法有关的数据结构,那么可以使用策略模式来封装算法 * 为什么要抽象存储引擎接口 * 将文件存储引擎的接口抽象出来,具体实现可以多种,提高系统的灵活性和可维护性。 * 允许我们根据不同的需求和环境(如开发、测试、生产)灵活切换不同的存储解决方案 * 优点 - **灵活性和可扩展性**:通过定义一个统一的存储接口,我们可以在不修改客户端代码的情况下引入新的存储解决方案。 - **解耦**:将存储逻辑从业务逻辑中解耦,使得存储引擎的变化不影响业务逻辑。 - **易于测试**:可以针对接口编写单元测试,而不必依赖具体的存储实现。 - **代码复用**:多个项目可以共享相同的存储接口,提高代码复用率。 - **简化维护**:统一的接口使得维护和更新存储逻辑变得更加简单。 * 缺点 - **复杂性增加**:需要额外定义接口和可能的抽象类,增加了系统的复杂性。 - **性能考虑**:接口调用可能引入额外的性能开销,尤其是在接口频繁调用的情况下。 - **实现一致性**:确保所有存储策略实现都遵循相同的接口规范,需要严格的代码审查和测试。 * 注意 * **其实aws-java-sdk-s3本身就是封装好了,支持多个存储的,为啥我们又要加一层呢???** * 假想下 * 万一我以后不用aws-java-sdk-s3,那岂不是四处要修改aws-java-sdk-s3的API方法 * 但如果我加了一层,其他地方使用的话,后续修改换别的SDK,我只需要修改我自己封装的那层即可 #### SpringBoot3.X整合MinIO存储AWS-S3封装 封装存储引擎接口设计【常规版】 * * 定义一个名为StorageEngine的接口,包含多个方法 * 可以跟进需求,实现`StorageEngine`接口的不同存储策略 - **LocalFileStorageEngine**:使用本地文件系统作为存储。 - **S3StorageEngine**:使用Amazon S3作为存储。 - **DatabaseStorageEngine**:使用数据库存储文件元数据和内容。 - **MinIOStorageEngine**:使用MinIO存储文件内容。 - ... * 使用策略模式的优势 - **客户端代码与存储实现解耦**:客户端代码只需与`StorageEngine`接口交互,不需要关心具体的存储细节。 - **易于切换存储策略**:根据不同的业务需求或环境(开发、测试、生产)灵活切换不同的存储策略。 - **支持A/B测试**:可以同时运行多个存储策略,进行性能和效果比较。 * 抽取文件操作相关接口 StoreEngine ```java public interface StoreEngine { /*=====================Bucket相关===========================*/ /** * 检查指定的存储桶是否存在于当前的存储系统中 * * @param bucketName 存储桶的名称 * @return 如果存储桶存在,则返回true;否则返回false */ boolean bucketExists(String bucketName); /** * 删除指定名称的存储桶 * * @param bucketName 存储桶的名称 * @return 如果存储桶删除成功,则返回true;否则返回false */ boolean removeBucket(String bucketName); /** * 创建一个新的存储桶 * * @param bucketName 新存储桶的名称 */ void createBucket(String bucketName); /** * 获取当前存储系统中的所有存储桶列表 * * @return 包含所有存储桶的列表 */ List getAllBucket(); /*===================文件处理相关=============================*/ /** * 列出指定桶中的所有对象 * * @param bucketName 桶名称 * @return 包含桶中所有对象摘要的列表 */ List listObjects(String bucketName); /** * 判断文件是否存在 */ boolean doesObjectExist(String bucketName, String objectKey); /** * 将本地文件上传到指定桶 * * @param bucketName 桶名称 * @param objectKey 上传后对象的名称 * @param localFileName 本地文件的路径 * @return 上传是否成功 */ boolean upload(String bucketName, String objectKey, String localFileName); /** * 将multipart文件上传到指定桶 * * @param bucketName 桶名称 * @param objectKey 上传后对象的名称 * @param file 要上传的multipart文件 * @return 上传是否成功 */ boolean upload(String bucketName, String objectKey, MultipartFile file); /** * 从指定桶中删除对象 * * @param bucketName 桶名称 * @param objectKey 要删除的对象的名称 * @return 删除是否成功 */ boolean delete(String bucketName, String objectKey); /*===================下载相关=============================*/ /** * 获取指定对象的下载URL * * @param bucketName 桶名称 * @param remoteFileName 对象的名称 * @param timeout URL的有效时长 * @param unit URL有效时长的时间单位 * @return 对象的下载URL */ String getDownloadUrl(String bucketName, String remoteFileName, long timeout, TimeUnit unit); /** * 将指定对象下载到HTTP响应中 * * @param bucketName 桶名称 * @param objectKey 对象的名称 * @param response HTTP响应对象,用于输出下载的对象 */ void download2Response(String bucketName, String objectKey, HttpServletResponse response); } ``` * 实现文件存储引擎操作相关接口StoreEngine ```java @Component @Slf4j public class MinioFileStoreEngine implements StoreEngine { @Resource private AmazonS3Client amazonS3Client; @Override public boolean bucketExists(String bucketName) { return amazonS3Client.doesBucketExistV2(bucketName); } @Override public boolean removeBucket(String bucketName) { try { if (bucketExists(bucketName)) { List objects = listObjects(bucketName); if (!objects.isEmpty()) { return false; } amazonS3Client.deleteBucket(bucketName); return !bucketExists(bucketName); } } catch (Exception e) { log.error("errorMsg={}", e.getMessage()); return false; } return false; } @Override public void createBucket(String bucketName) { if (bucketExists(bucketName)) { log.info("Bucket {} already exists.", bucketName); return; } try { Bucket bucket = amazonS3Client.createBucket(bucketName); log.info("Bucket {} created.", bucketName); } catch (Exception e) { log.error("errorMsg={}", e.getMessage()); } } @Override public List getAllBucket() { return amazonS3Client.listBuckets(); } @Override public List listObjects(String bucketName) { if (bucketExists(bucketName)) { ListObjectsV2Result result = amazonS3Client.listObjectsV2(bucketName); return result.getObjectSummaries(); } return List.of(); } @Override public boolean doesObjectExist(String bucketName, String objectKey) { return amazonS3Client.doesObjectExist(bucketName, objectKey); } @Override public boolean upload(String bucketName, String objectName, String localFileName) { try { File file = new File(localFileName); amazonS3Client.putObject(bucketName, objectName, file); return true; } catch (Exception e) { log.error("errorMsg={}", e.getMessage()); return false; } } @Override public boolean upload(String bucketName, String objectKey, MultipartFile file) { try { ObjectMetadata objectMetadata = new ObjectMetadata(); objectMetadata.setContentLength(file.getSize()); objectMetadata.setContentType(file.getContentType()); amazonS3Client.putObject(bucketName, objectKey, file.getInputStream(), objectMetadata); return true; } catch (Exception e) { log.error("errorMsg={}", e.getMessage()); return false; } } @Override public boolean delete(String bucketName, String objectKey) { try { amazonS3Client.deleteObject(bucketName, objectKey); return true; } catch (Exception e) { log.error("errorMsg={}", e); return false; } } @Override public String getDownloadUrl(String bucketName, String remoteFileName, long timeout, TimeUnit unit) { try { Date expiration = new Date(System.currentTimeMillis() + unit.toMillis(timeout)); return amazonS3Client.generatePresignedUrl(bucketName, remoteFileName, expiration).toString(); } catch (Exception e) { log.error("errorMsg {}", e); return null; } } @Override @SneakyThrows public void download2Response(String bucketName, String objectKey, HttpServletResponse response) { S3Object s3Object = amazonS3Client.getObject(bucketName, objectKey); response.setHeader("Content-Disposition", "attachment;filename=" + objectKey.substring(objectKey.lastIndexOf("/") + 1)); response.setContentType("application/force-download"); response.setCharacterEncoding("UTF-8"); IOUtils.copy(s3Object.getObjectContent(), response.getOutputStream()); } } ``` * **思考:上面代码有什么问题?哪里可以优化的** #### AI大模型编码效能提升-一键优化代码案例实战 * 上述潜在问题与风险 * 异常处理不一致: * 多个方法中使用了不同的异常处理逻辑,部分方法直接捕获 Exception,而没有具体处理特定的异常类型。这可能导致隐藏 * 潜在的错误信息。 * 异常日志记录不完整,只记录了 e.getMessage(),而没有记录完整的堆栈信息,不利于调试。 * 资源未关闭: * 在 download2Response 方法中,s3Object.getObjectContent() 返回的输入流没有关闭,可能会导致资源泄漏。 * 硬编码的响应头: * download2Response 方法中的响应头设置是硬编码的,缺乏灵活性和可配置性。 * 空返回值: * 多个方法在异常情况下返回 null 或 false,这可能会导致调用方需要额外的空值检查,增加了复杂性。 * 缺少边界条件检查: * upload 方法中没有对 localFileName 和 file 进行有效性检查,可能会导致 NullPointerException。 * S3 客户端实例化: * amazonS3Client 的实例化方式未明确,如果每次调用都创建新实例,可能会导致性能问题。 * 更多.... * **AI一键优化代码案例实战** * **注意** * **并非AI优化的代码可以直接使用,关系到Prompt编写、上下文等,务必要结合实际情况和代码审查再使用** * **可以辅助工程师更好的优化代码和发现问题,提高程序的健壮性** > /optimize 补充接口文档和参数注释,优化代码,统一异常和日志打印,不要使用自定义异常,出错的话log记录即可 ### AI智能化云盘数据库设计和逆向工程 #### AI智能化云盘文件存储设计和核心关系 * **思考:文件存储,如果老板让去负责,你会如何设计?假如你没接触过这个领域,看同行竞品** * 百度网盘 image-20241224104021085 * 智能云盘 image-20241224104920314 * 云盘存储相关设计说明 * 任何文件都有一个唯一标识,我们统一命名为 **identifier**,同个文件产生的标识是不变的 * 唯一标识(identifier)可以采用多个方案,也有对应的类库 * 哈希函数(如MD5、SHA-256) * 优点: * 唯一性:理论上 不同的文件内容会产生不同的哈希值,保证了标识的唯一性。 * 快速计算:哈希函数可以快速计算出文件的哈希值。 * 安全性:对于SHA-256等哈希算法,抗碰撞性较强,不易被篡改。 * 缺点: * 安全性问题:对于MD5,由于其抗碰撞性较弱,已经不推荐用于安全敏感的应用。 * 存储和比较:哈希值需要存储和比较,对于非常大的文件系统,这可能会增加存储和计算开销。 * 基于内容的指纹(如SimHash、Locality-Sensitive Hashing) * 优点: * 相似性检测:适用于检测相似或重复的文件,可以容忍文件内容的微小变化。 * 减少存储:通过减少哈希值的位数来减少存储需求。 * 缺点: * 计算复杂性:相比于简单的哈希函数,这些算法可能需要更复杂的计算。 * 误判率:在某些情况下可能会有误判,即不同的文件产生相同的指纹。 * 文件元数据组合 * 优点: * 简单易实现:通过文件的大小、创建时间、修改时间等元数据生成标识。 * 快速检索:基于元数据的检索通常很快。 * 缺点: * 非唯一性:不同的文件可能具有相同的元数据,特别是在文件被复制或修改的情况下。 * 不稳定性:文件的元数据(如修改时间)可能会改变,导致标识失效 * 方案:采用MD5, 相关标识可以前端和后端保持一定规则,前端上传的时候生成标识传递给后端 #### 账号表-文件表和关联关系表设计说明 * 三个关键表说明 * **account表**:存储用户的基本信息,如用户名、密码、头像等。这是用户身份验证和个性化设置的基础。 * **file表**:存储文件的元数据,包括文件名、大小、后缀、唯一标识符(MD5)等。主要用于跟踪文件的属性和文件的唯一性 * **account_file表**: * 存储用户与文件之间的关系,包括文件的层级结构(文件夹和子文件),以及文件的类型和大小等信息。 * 这个表允许一个用户有多个子文件和文件夹,并且可以表示文件的层级关系 * 如果没有`account_file`表, * 每个用户都重复上传,随着文件数量的增加,没有`account_file`表来组织文件结构,`file`表会变得非常大,性能问题 * 无法有效地表示文件和文件夹的层级结构 * 实现文件的移动、复制、删除等操作会变得复杂,因为没有一个明确的结构来跟踪文件的层级和用户关系 * 权限管理也会变得更加复杂,因为没有一个清晰的结构来定义哪些文件可以被哪些用户访问。 ![image-20241224112405905](./img/image-20241224112405905.png) * 智能化云盘设计的3个表理解清楚 * 账号表 * 记录账号相关基础信息 * 关键字段 ``` id 即后续用的 account_id username password role 用户角色 COMMON, ADMIN ``` * 账号文件关系表 * 记录对应账号下的文件和文件夹、关系等 * 关键字段 ``` id account_id 账号ID is_dir 是否是目录,0不是文件夹,1是文件夹 parent_id 上层文件夹ID,顶层文件夹为0 file_id 文件ID,真正存储的文件 file_name 文件名和实际存储的文件名区分开来,可能重命名 ``` * 文件表 * 记录文件相关的物理存储信息 ``` id 即file_id account_id 哪个账号上传的 file_name 文件名 object_key 文件的key, 格式 日期/md5.拓展名,比如 2024/11/13/921674fd-cdaf-459a-be7b-109469e7050d.png identifier 唯一标识,文件MD5 ``` #### AI智能化云盘数据库设计和字段说明 * 数据库ER图设计(**后续还有调整相关表结构**) ![image-20241224113826448](./img/image-20241224113826448.png) * 导入建表语句 #### 智能化云盘数据库逆向工程配置生成 * 配置数据库 ```java public class MyBatisPlusGenerator { public static void main(String[] args) { String userName = "root"; String password = "xx"; String serverInfo = "127.0.0.1:3306"; String targetModuleNamePath = "/"; String dbName = "ycloud-aipan"; String[] tables = { "account", "file","account_file","file_chunk", "file_suffix","file_type", "share", "share_file", "storage" }; // 使用 FastAutoGenerator 快速配置代码生成器 FastAutoGenerator.create("jdbc:mysql://"+serverInfo+"/"+dbName+"?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&tinyInt1isBit=true", userName, password) .globalConfig(builder -> { builder.author("everyone") // 设置作者 .commentDate("yyyy-MM-dd") .enableSpringdoc() .disableOpenDir() //禁止打开输出目录 .dateType(DateType.ONLY_DATE) //定义生成的实体类中日期类型 DateType.ONLY_DATE 默认值: DateType.TIME_PACK .outputDir(System.getProperty("user.dir") + targetModuleNamePath + "/src/main/java"); // 指定输出目录 }) .packageConfig(builder -> { builder.parent("org.ycloud.aipan") // 父包模块名 .entity("model") //Entity 包名 默认值:entity .mapper("mapper") //Mapper 包名 默认值:mapper .pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + targetModuleNamePath + "/src/main/resources/mapper")); // 设置mapperXml生成路,默认存放在mapper的xml下 }) .dataSourceConfig(builder -> {//Mysql下tinyint字段转换 builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> { if (JdbcType.TINYINT == metaInfo.getJdbcType()) { return DbColumnType.BOOLEAN; } return typeRegistry.getColumnType(metaInfo); }); }) .strategyConfig(builder -> { builder.addInclude(tables) // 设置需要生成的表名 可变参数 .entityBuilder()// Entity策略配置 .enableFileOverride() // 开启生成Entity层文件覆盖 .idType(IdType.ASSIGN_ID)//主键策略 雪花算法自动生成的id .enableLombok() //开启lombok .logicDeleteColumnName("del")// 说明逻辑删除是哪个字段 .enableTableFieldAnnotation()// 属性加上注解说明 .formatFileName("%sDO") //格式化生成的文件名称 .controllerBuilder().disable()// Controller策略配置,这里不生成Controller层 .serviceBuilder().disable()// Service策略配置,这里不生成Service层 .mapperBuilder()// Mapper策略配置 .enableFileOverride() // 开启生成Mapper层文件覆盖 .formatMapperFileName("%sMapper")// 格式化Mapper文件名称 .superClass(BaseMapper.class) //继承的父类 .enableBaseResultMap() // 开启生成resultMap, .enableBaseColumnList() // 开启生成Sql片段 .formatXmlFileName("%sMapper"); // 格式化xml文件名称 }) .templateConfig(builder -> { // 不生成Controller builder.disable(TemplateType.CONTROLLER,TemplateType.SERVICE,TemplateType.SERVICE_IMPL); }) .execute(); // 执行生成 } } ``` ### 账号模块开发和Knife4j接口文档配置 #### Knife4j接口文档工具 * 什么是Knife4j * 一个为Java MVC框架集成Swagger生成Api文档的增强解决方案,前身是swagger-bootstrap-ui。 * 提供了新的Web页面,更符合使用习惯和审美;补充了一些注解,扩展了原生Swagger的功能; * 是一个更小巧、轻量且功能强悍的接口文档管理工具 * 核心功能 * **文档说明**:详细列出接口文档的说明,包括接口地址、类型、请求示例、请求参数、响应示例、响应参数、响应码等信息。 * **在线调试**:提供在线接口联调功能,自动解析当前接口参数,返回接口响应内容、headers、响应时间、响应状态码等信息。 * **接口搜索**:提供强大的接口搜索功能,支持按接口地址、请求方法、接口描述等关键字进行搜索。 * **接口过滤**:提供接口过滤功能,可以根据接口分组、接口标签、接口地址等条件进行过滤。 * **自定义主题**:支持自定义主题,定制个性化的API文档界面。 * **丰富的扩展功能**:如接口排序、接口分组、接口标签等,进一步丰富了API文档管理的功能。 * 配置实战 * 添加依赖 ```xml com.github.xiaoymin knife4j-openapi3-jakarta-spring-boot-starter 4.4.0 ``` * 创建配置类 ```java /** * Knife4j配置 ,默认是下面 *

* knife4j 访问地址:http://localhost:8080/doc.html * Swagger2.0访问地址:http://localhost:8080/swagger-ui.html * Swagger3.0访问地址:http://localhost:8080/swagger-ui/index.html */ @Slf4j @Configuration public class Knife4jConfig { @Bean public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info() .title("AI智能云盘系统 API") .version("1.0-SNAPSHOT") .description("AI智能云盘系统") .termsOfService("https://www.xxx.net") .license(new License().name("Apache 2.0").url("https://www.xxx.net")) // 添加作者信息 .contact(new Contact() .name("anonymity") // 替换为作者的名字 .email("anonymity@qq.com") // 替换为作者的电子邮件 .url("https://www.xxx.net") // 替换为作者的网站或个人资料链接 ) ); } } ``` * 配置Spring Boot控制台打印 ```java @Slf4j @SpringBootApplication public class CloudApplication { public static void main(String[] args) throws Exception { ConfigurableApplicationContext application = SpringApplication.run(CloudApplication.class, args); Environment env = application.getEnvironment(); log.info("\n----------------------------------------------------------\n\t" + "Application '{}' is running! Access URLs:\n\t" + "Local: \t\thttp://localhost:{}\n\t" + "External: \thttp://{}:{}\n\t" + "API文档: \thttp://{}:{}/doc.html\n" + "----------------------------------------------------------", env.getProperty("spring.application.name"), env.getProperty("server.port"), InetAddress.getLocalHost().getHostAddress(), env.getProperty("server.port"), InetAddress.getLocalHost().getHostAddress(), env.getProperty("server.port")); } } ``` #### 账号注册相关模块接口开发实战 * 需求 * 开发用户注册相关接口,手机号注册 * 内部使用, 不加验证码,如果需要对外则可以加入验证码逻辑 * 用户板块不做复杂权限或者多重校验处理等 * 逻辑说明 * 根据手机号查询是否重复(或者唯一索引) * 密码加密处理 * 保存用户注册逻辑 * 其他逻辑(创建默认的存储空间,初始化根目录) * 编码实战: > 编写`AccountController,AccountRegisterReq,AccountService,AccountConfig`... ```sql CREATE TABLE `account` ( `id` bigint NOT NULL COMMENT 'ID', `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名', `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码', `avatar_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户头像', `phone` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '手机号', `role` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT 'COMMON' COMMENT '用户角色 COMMON, ADMIN', `del` tinyint DEFAULT '0' COMMENT '逻辑删除(1删除 0未删除)', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `idx_phone_uni` (`phone`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='用户信息表'; ``` #### 头像上传接口开发和MinIO权限配置 * 需求 * 开发头像上传接口,用户注册时候需要把头像url进行上传 * **存储到minio需要可以公开访问,和文件存储分开bucket** * 逻辑说明 * 文件上传接口 * 返回文件访问路径 * **配置minio的头像存储bucket存储权限为public** #### 网盘存储容量设计和根目录初始化配置 * 需求 * **问题一:新用户注册,有默认网盘存储容量,什么时候进行初始化?** * 答案 * 用户注册的时候一并配置相关的初始化内容 * 如果是简单场景:直接调用; 复杂场景:结合消息队列 * 类似场景大家可以思考下还有哪些,各大公司拉新活动折扣 image-20241225144548702 * **问题二:网盘文件存储有个根目录,这个如何进行设计?** * 上传文件的到根目录,这个相关的parent_id是怎么填写? * 答案:参考Linux操作系统,根目录也是一个目录 ![image-20241225143220117](./img/image-20241225143220117.png) * 开发编码实战:创建文件夹 ```java //3.创建默认的存储空间 StorageDO storageDO = new StorageDO(); storageDO.setAccountId(accountDO.getId()); storageDO.setUsedSize(0L); storageDO.setTotalSize(AccountConfig.DEFAULT_STORAGE_SIZE); storageMapper.insert(storageDO); //4.初始化根目录 FolderCreateReq createRootFolderReq = FolderCreateReq.builder() .accountId(accountDO.getId()) .parentId(AccountConfig.ROOT_PARENT_ID) .folderName(AccountConfig.ROOT_FOLDER_NAME) .build(); accountFileService.createFolder(createRootFolderReq); ``` #### 账号登录相关模块设计和开发实战 * 需求 * 开发用户登录模块 * 配置生成JWT * 编码实战 ```java //业务逻辑 public AccountDTO login(AccountLoginReq req) { String encryptPassword = DigestUtils.md5DigestAsHex(( AccountConfig.ACCOUNT_SALT+ req.getPassword()).getBytes()); QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("phone", req.getPhone()).eq("password", encryptPassword); AccountDO accountDO = accountMapper.selectOne(queryWrapper); return SpringBeanUtil.copyProperties(accountDO, AccountDTO.class); } //JWT工具 @Slf4j public class JwtUtil { // JWT的主题 private static final String LOGIN_SUBJECT = "XDCLASS"; /** * token有效期1小时 */ private static final Long SHARE_TOKEN_EXPIRE = 1000L * 60 * 60L; //注意这个密钥长度需要足够长, 推荐:JWT的密钥,从环境变量中获取 private final static String SECRET_KEY = "xdclass.net168xdclass.net168xdclass.net168xdclass.net168"; // 签名算法 private final static SecureDigestAlgorithm ALGORITHM = Jwts.SIG.HS256; // 使用密钥 private final static SecretKey KEY = Keys.hmacShaKeyFor(SECRET_KEY.getBytes()); // token过期时间,30天 private static final long EXPIRED = 1000 * 60 * 60 * 24 * 7; /** * 生成JWT * @param accountDTO 登录账户信息 * @return 生成的JWT字符串 * @throws NullPointerException 如果传入的accountDTO为空 */ public static String geneLoginJWT(AccountDTO accountDTO) { if (accountDTO == null) { throw new NullPointerException("对象为空"); } // 创建 JWT token String token = Jwts.builder() .subject(LOGIN_SUBJECT) .claim("accountId", accountDTO.getId()) .claim("username", accountDTO.getUsername()) .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() + EXPIRED)) .signWith(KEY, ALGORITHM) // 直接使用KEY即可 .compact(); // 添加自定义前缀 return addPrefix(token); } /** * 校验JWT * @param token JWT字符串 * @return JWT的Claims部分 * @throws IllegalArgumentException 如果传入的token为空或只包含空白字符 * @throws RuntimeException 如果JWT签名验证失败、JWT已过期或JWT解密失败 */ public static Claims checkLoginJWT(String token) { try { log.debug("开始校验 JWT: {}", token); // 校验 Token 是否为空 if (token == null || token.trim().isEmpty()) { log.error("Token 不能为空"); throw new IllegalArgumentException("Token 不能为空"); } token = token.trim(); // 移除前缀 token = removePrefix(token); log.debug("移除前缀后的 Token: {}", token); // 解析 JWT Claims payload = Jwts.parser() .verifyWith(KEY) //设置签名的密钥, 使用相同的 KEY .build() .parseSignedClaims(token).getPayload(); log.info("JWT 解密成功,Claims: {}", payload); return payload; } catch (IllegalArgumentException e) { log.error("JWT 校验失败: {}", e.getMessage(), e); throw e; } catch (io.jsonwebtoken.security.SignatureException e) { log.error("JWT 签名验证失败: {}", e.getMessage(), e); throw new RuntimeException("JWT 签名验证失败", e); } catch (io.jsonwebtoken.ExpiredJwtException e) { log.error("JWT 已过期: {}", e.getMessage(), e); throw new RuntimeException("JWT 已过期", e); } catch (Exception e) { log.error("JWT 解密失败: {}", e.getMessage(), e); throw new RuntimeException("JWT 解密失败", e); } } /** * 给token添加前缀 * @param token 原始token字符串 * @return 添加前缀后的token字符串 */ private static String addPrefix(String token) { return LOGIN_SUBJECT + token; } /** * 移除token的前缀 * @param token 带前缀的token字符串 * @return 移除前缀后的token字符串 */ private static String removePrefix(String token) { if (token.startsWith(LOGIN_SUBJECT)) { return token.replace(LOGIN_SUBJECT, "").trim(); } return token; } } ``` #### 拦截器开发和ThreadLocal传递用户信息 * 需求 * 开发登录拦截器 解密JWT * 传递登录用户信息 * request的attribute传递 * threadLocal传递 * 配置拦截器放行路径开发配置 * ThreadLocal知识点说明 * 全称thread local variable(线程局部变量)功用非常简单,使用场合主要解决多线程中数据因并发产生不一致问题。 * ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某时间访问到的并不是同一个对象 * 注意:ThreadLocal不能使用原子类型,只能使用Object类型 ![image-20241225154054266](./img/image-20241225154054266.png) * 应用场景 * ThreadLocal 用作每个线程内需要独立保存信息,方便同个线程的其他方法获取该信息的场景。 * 每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过 ThreadLocal 直接获取到 * 类似于全局变量的概念 比如用户登录令牌解密后的信息传递(用户权限信息、从用户系统获取到的用户名、用户ID) * 编码实战 * 开发登录拦截器 解密JWT ```java @Component public class LoginInterceptor implements HandlerInterceptor { public static ThreadLocal threadLocal = new ThreadLocal<>(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 处理OPTIONS请求 if (HttpMethod.OPTIONS.toString().equalsIgnoreCase(request.getMethod())) { response.setStatus(HttpStatus.NO_CONTENT.value()); return true; } // 从请求头或参数中获取token String token = request.getHeader("token"); if (StringUtils.isBlank(token)) { token = request.getParameter("token"); } // 如果token存在,解析JWT if (StringUtils.isNotBlank(token)) { Claims claims = JwtUtil.checkLoginJWT(token); if (claims == null) { // 如果token无效,返回未登录的错误信息 CommonUtil.sendJsonMessage(response, JsonData.buildResult(BizCodeEnum.ACCOUNT_UNLOGIN)); return false; } // 从JWT中提取用户信息 Long accountId = Long.valueOf( claims.get("accountId")+""); String userName = (String) claims.get("username"); // 创建 AccountDTO 对象 AccountDTO accountDTO = AccountDTO.builder() .id(accountId) .username(userName) .build(); // 将用户信息存入 ThreadLocal threadLocal.set(accountDTO); return true; } // 如果没有token,返回未登录的错误信息 CommonUtil.sendJsonMessage(response, JsonData.buildResult(BizCodeEnum.ACCOUNT_UNLOGIN)); return false; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 清理 ThreadLocal 中的用户信息 threadLocal.remove(); } } ``` * 配置拦截器放行路径开发配置 ```java @Configuration @Slf4j public class InterceptorConfig implements WebMvcConfigurer { @Resource private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) //添加拦截的路径 .addPathPatterns("/api/account/*/**","/api/file/*/**","/api/share/*/**") //排除不拦截 .excludePathPatterns("/api/account/*/register","/api/account/*/login","/api/account/*/upload_avatar", "/api/share/*/check_share_code","/api/share/*/visit","/api/share/*/detail_no_code","/api/share/*/detail_with_code"); } } ``` #### 首页前后端交互逻辑和账号详情接口开发 * 需求 * 网盘存储首页进入,会触发哪些请求? image-20241224104920314 * 逻辑说明 * 步骤一 * 进入首页需要先获取用户的根目录文件夹ID * 通过根目录文件夹ID去获取对应的文件列表 * 步骤二 * 首页需要显示用户的存储空间 * 编码实战 ```java public AccountDTO queryDetail(Long accountId) { //账号详情 AccountDO accountDO = accountMapper.selectById(accountId); AccountDTO accountDTO = SpringBeanUtil.copyProperties(accountDO, AccountDTO.class); //存储信息 StorageDO storageDO = storageMapper.selectOne(new QueryWrapper().eq("account_id", accountId)); StorageDTO storageDTO = SpringBeanUtil.copyProperties(storageDO, StorageDTO.class); accountDTO.setStorageDTO(storageDTO); //根文件相关信息 AccountFileDO accountFileDO = accountFileMapper.selectOne(new QueryWrapper() .eq("account_id", accountId) .eq("parent_id", AccountConfig.ROOT_PARENT_ID)); // bug处理 if (accountFileDO != null) { accountDTO.setRootFileId(accountFileDO.getId()); accountDTO.setRootFileName(accountFileDO.getFileName()); } return accountDTO; } ``` #### AI编码-账号注册和登录单元测试生成 * 需求 * 利用AI编写账号注册和登录 * 验证相关接口逻辑 * 单元测试实战 > 操作:复制controller对应的接口,右键,选择生成测试 ### 技术架构图答案+AI接口文档快速生成 * 技术架构图 image-20241230155838690 AI补充接口文档和注释字段操作 * AI补充API接口文档 * `补充knife4j的接口文档配置内容,@Tag @Operation等注解,使用v3` * AI补充字段解释说明 * `补充knife4j接口文档信息,使用@Schema,使用v3,添加参数举例` ### 网盘文件模块基础设计和开发 #### 资源访问安全之web常见越权攻击和防范 * **越权攻击介绍** * 是Web应用程序中一种常见的漏洞,由于其存在范围广、危害 大, 列为Web应用十大安全隐患的第二名 * 指应用在检查授权时存在纰漏,使得攻击者在获得低权限用户账户后,利用一些方式绕过权限检查,访问或者操作其他用户 * 产生原因:主要是因为开发人员在对数据进行增、删、改、查询时对客户端请求的数据过分相信,而遗漏了权限的判定 * 比如网盘里面:分享、转存、查看文件的时候都容易触发 * **水平越权攻击** - 指的是攻击者通过某种手段获取了与自己权限相同的其他账户的访问权限。 - 用户A能够访问用户B的账户信息,尽管他们都是普通用户,但A不应该能够访问B的数据。 - 技术实现方式 - **参数篡改**: - 攻击者通过修改请求中的用户ID参数,尝试访问其他同级别用户的资源。 - 在电商系统中,用户A通过修改订单ID参数,尝试查看或修改用户B的订单信息。 - **会话劫持**: - 攻击者通过某种方式获取了其他用户的会话信息,从而冒充该用户进行操作,这可能导致水平越权问题。 - **利用前端安全漏洞**: - 如果前端安全措施不当,攻击者可能会通过修改前端显示的界面元素,如隐藏的URL或参数,来访问其他用户的数据。 * **水平越权攻击的防范**: - **权限验证**:确保每次数据访问都进行严格的权限验证。 - **数据隔离**:不同用户的数据应该在数据库层面进行隔离。 - **会话管理**:使用安全的会话管理机制,如HTTPS、Token等。 * **垂直越权攻击** - 指的是攻击者通过某种手段获取了更高权限的账户的访问权限。 - 普通用户获取了管理员账户或者更高的权限。 - 技术实现方式 - **权限配置错误**: - 由于系统配置不当,普通用户能够执行管理员级别的操作,例如通过修改请求中的权限参数来提升权限。 - **利用系统漏洞**: - 攻击者利用系统或应用程序的漏洞提升权限,例如通过SQL注入攻击来执行管理员级别的数据库操作。 - **多阶段功能滥用**: - 在多阶段功能实现中,如果后续阶段不再验证用户身份,攻击者可能通过抓包修改参数值,实现越权操作,如修改任意用户密码 - **垂直越权攻击的防范**: - **最小权限原则**:用户和系统组件应该只拥有完成其任务所必需的最小权限。 - **权限审查**:定期审查权限设置,确保没有不必要的权限提升。 - **安全编码**:遵循安全编码实践,避免常见的安全漏洞,如SQL注入、跨站脚本(XSS)等。 - **安全审计**:实施安全审计,监控和记录关键操作,以便在发生安全事件时进行追踪。 * 智能化网盘项目里面的避免越权处理方案 * 相关文件数据处理,加入account_id确认 * 角色权限通过role进行确认操作 #### 文件模块开发之查询文件列表接口开发 * 需求 * 网盘存储首页进入,会触发哪些请求?**获取当前用户根目录文件夹** * 根据根目录文件夹查询对应的文件列表 * 进入相关的指定文件夹,查询对应的子文件 image-20241224104920314 * 注意事项 * 查询的时候都需要加入账号相关进行确认 **前面代码相对会简单点,逐步代码封装和抽取就会上升难度,** * 编码实战 ```java @GetMapping("list") public JsonData list(@RequestParam(value = "parent_id")Long parentId){ Long accountId = LoginInterceptor.threadLocal.get().getId(); List list = fileService.listFile(accountId,parentId); return JsonData.buildSuccess(list); } public List listFile(Long accountId, Long parentId) { List accountFileDOList = accountFileMapper.selectList(new QueryWrapper() .eq("account_id", accountId).eq("parent_id", parentId) .orderByDesc("is_dir") .orderByDesc("gmt_create") ); return SpringBeanUtil.copyProperties(accountFileDOList, AccountFileDTO.class); } ``` #### 创建文件夹相关接口设计和开发 * 需求 * 开发网盘里面可以创建文件夹 ![image-20241226151209235](img/image-20241226151209235.png) * 业务逻辑方法梳理(**哪些方法会其他地方复用**) * 检查父文件ID是否存在(抽) * 生成账号文件信息 * 检查文件名是否重复(抽) * 保存相关账号文件夹信息 * 编码实战 ```java @PostMapping("/create_folder") public JsonData createFolder(@RequestBody FolderCreateReq req){ req.setAccountId(LoginInterceptor.threadLocal.get().getId()); fileService.createFolder(req); return JsonData.buildSuccess(); } AccountFileDTO accountFileDTO = AccountFileDTO.builder().accountId(req.getAccountId()) .parentId(req.getParentId()) .fileName(req.getFolderName()) .isDir(FolderFlagEnum.YES.getCode()).build(); return saveAccountFile(accountFileDTO); ``` * 需求 * 处理用户和文件的映射存储,存储文件和文件夹都可以 * 编码实战 ```java /** * 处理用户和文件的映射存储,存储文件和文件夹都可以 *

* 1、检查父文件ID是否存在,避免越权 * 2、检查文件名是否重复 * 3、保存文件信息 * * @return */ private Long saveAccountFile(AccountFileDTO accountFileDTO) { //检查父文件ID是否存在 checkParentFileId(accountFileDTO); //存储文件信息 AccountFileDO accountFileDO = SpringBeanUtil.copyProperties(accountFileDTO, AccountFileDO.class); //检查文件名是否重复 processFileNameDuplicate(accountFileDO); accountFileMapper.insert(accountFileDO); return accountFileDO.getId(); } ``` #### 网盘文件重命名相关接口 * 需求 * 开发网盘文件重命名接口,包括文件夹和文件一样适用 image-20241226170055385 * 业务逻辑方法梳理 * 文件ID是否存在,避免越权 * 新旧文件名称不能一样 * 也不能用同层文件夹的名称,通过parent_id进行查询 * 编码实战 ```java @Override public void renameFile(FileUpdateReq req) { //文件ID是否存在,避免越权 AccountFileDO accountFileDO = accountFileMapper.selectOne(new QueryWrapper() .eq("id", req.getFileId()) .eq("account_id", req.getAccountId())); if (accountFileDO == null) { log.error("文件ID不存在,请检查:{}", req); throw new BizException(BizCodeEnum.FILE_NOT_EXISTS); } else { //新旧文件名称不能一样 if (Objects.equals(accountFileDO.getFileName(), req.getNewFilename())) { log.error("新旧文件名称不能一样,{}", req); throw new BizException(BizCodeEnum.FILE_RENAME_REPEAT); } else { //同层的文件或者文件夹也不能一样 Long selectCount = accountFileMapper.selectCount(new QueryWrapper() .eq("account_id", req.getAccountId()) .eq("parent_id", accountFileDO.getParentId()) .eq("file_name", req.getNewFilename())); if (selectCount > 0) { log.error("同层的文件或者文件夹也不能一样,{}", req); throw new BizException(BizCodeEnum.FILE_RENAME_REPEAT); } else { accountFileDO.setFileName(req.getNewFilename()); accountFileMapper.updateById(accountFileDO); } } } } ``` #### 接口测试工具-文件夹创建-查询-重命名接口测试 * 接口测试工具 * Apifox和Postman都是流行的API接口管理工具 * 选择哪个工具取决于具体的使用场景和需求 * 接口工具核心功能 * 支持多种HTTP请求方法(如GET、POST、PUT、DELETE等),允许用户设置请求头、请求体、查询参数等 * 环境变量允许用户存储和管理多个环境(如开发、测试、生产环境)的配置信息,便于在不同环境间切换 * 我们采用ApiFox录入相关接口进行测试 * 配置全局环境变量 * 录入相关接口模块 * bug修复 ```JAVA //bug1 /** * 检查父文件是否存在 * @param accountFileDTO */ private void checkParentFileId(AccountFileDTO accountFileDTO) { if(accountFileDTO.getParentId()!=0){ AccountFileDO accountFileDO = accountFileMapper.selectOne( new QueryWrapper() .eq("id", accountFileDTO.getParentId()) .eq("account_id", accountFileDTO.getAccountId())); if(accountFileDO == null){ throw new BizException(BizCodeEnum.FILE_NOT_EXISTS); } } } //bug2 @AllArgsConstructor @NoArgsConstructor public class AccountFileDTO //bug3 private void processFileNameDuplicate(AccountFileDO accountFileDO) { Long selectCount = accountFileMapper.selectCount(new QueryWrapper() .eq("account_id", accountFileDO.getAccountId()) .eq("parent_id", accountFileDO.getParentId()) .eq("is_dir", accountFileDO.getIsDir()) .eq("file_name", accountFileDO.getFileName())); if(selectCount>0){ //处理重复文件夹 if(Objects.equals(accountFileDO.getIsDir(), FolderFlagEnum.YES.getCode())){ accountFileDO.setFileName(accountFileDO.getFileName()+"_"+System.currentTimeMillis()); }else { //处理重复文件名,提取文件拓展名 String[] split = accountFileDO.getFileName().split("\\."); accountFileDO.setFileName(split[0]+"_"+System.currentTimeMillis()+"."+split[1]); } } } ``` #### Swagger+Apifox 1. 使用AI将源码中的Controller接口和Req对象生成knife4j注释 :参考: AI补充接口文档和注释字段操作 2. 注册Apifox账号,配置 API 访问令牌 3. 在idea中安装Apifox插件,通过插件将对应的接口同步到Apifox。[快速上手 - Apifox 帮助文档](https://docs.apifox.com/doc-5743620) ### 查询文件树接口设计和文件操作进阶 #### 【难点】查询文件树接口应用场景和流程设计讲解 * 什么是文件树和应用场景 * 多层级展示文件夹列表和子文件夹 * 用途包括移动、复制、转存文件 * 开发这个接口有多种方式 * 递归和非递归,我们采用非递归,内存里面操作的方式 * 内存里面操作也有多种实现方式,比如分组或者遍历处理 ![image-20241226170536073](/img/image-20241226170536073.png) * 后端接口协议分析,倒推代码处理逻辑 ```json { "code": 0, "data": [ { "id": 1871837581885325314, "parentId": 0, "label": "全部文件夹", "children": [ { "id": 1871838400252755969, "parentId": 1871837581885325314, "label": "a2", "children": [ { "id": 1872208466167484418, "parentId": 1871838400252755969, "label": "b2", "children": [] }, { "id": 1872208451487420418, "parentId": 1871838400252755969, "label": "b1", "children": [ { "id": 1872208603140870145, "parentId": 1872208451487420418, "label": "c2(1)", "children": [] } }, { "id": 1872208573759770626, "parentId": 1872208451487420418, "label": "c1", "children": [] } ] }, { "id": 1872208480121933825, "parentId": 1871838400252755969, "label": "b3", "children": [] } ] }, { "id": 1871838384587030529, "parentId": 1871837581885325314, "label": "a1", "children": [] } ] } ], "msg": null, "success": true } ``` * 代码逻辑思路 * 查询用户的全部文件夹列表 ![image-20241226173942236](/img/image-20241226173942236.png) * 构建一个Map,key为文件夹ID,value为FolderTreeNodeDTO对象 ![image-20241226174734539](/img/image-20241226174734539.png) * 构建文件夹树,遍历文件夹映射,为每个文件夹找到其子文件夹 ![image-20241226181052870](/img/image-20241226181052870.png) * 返回根节点(parentId为0的节点)过滤出根文件夹即可 ![image-20241226181539405](/img/image-20241226181539405.png) #### 【难点】查询文件树接口编码案例实战 * 编码实战 ```json /** * 获取文件树接口,非递归方式 * 1、查询当前用户的所有文件夹 * 2、拼装文件夹树 * @param accountId * @return */ @Override public List fileTree(Long accountId) { // 查询当前用户的所有文件夹 List folderList = accountFileMapper.selectList(new QueryWrapper() .eq("account_id", accountId) .eq("is_dir", FolderFlagEnum.YES.getCode())); // 拼装文件夹树列表 if (CollectionUtils.isEmpty(folderList)) { return List.of(); } // 构建一个Map,key为文件夹ID,value为FolderTreeNodeDTO对象 Map folderMap = folderList.stream().collect(Collectors.toMap( AccountFileDO::getId, file -> FolderTreeNodeDTO.builder() .id(file.getId()) .label(file.getFileName()) .parentId(file.getParentId()) .children(new ArrayList<>()) .build() )); // 构建文件夹树,遍历文件夹映射,为每个文件夹找到其子文件夹 for (FolderTreeNodeDTO node : folderMap.values()) { // 获取当前文件夹的父ID Long parentId = node.getParentId(); // 如果父ID不为空且父ID在文件夹映射中存在,则将当前文件夹添加到其父文件夹的子文件夹列表中 if (parentId != null && folderMap.containsKey(parentId)) { // 获取父文件夹 FolderTreeNodeDTO folderTreeNodeDTO = folderMap.get(parentId); // 获取父文件夹的子文件夹列表 List children = folderTreeNodeDTO.getChildren(); // 将当前文件夹添加到子文件夹列表中 children.add(node); } } // 返回根节点(parentId为0的节点)过滤出根文件夹即可,里面包括多个 List folderTreeNodeDTOS = folderMap.values().stream() .filter(node -> Objects.equals(node.getParentId(), 0L)) .collect(Collectors.toList()); return folderTreeNodeDTOS; } ``` #### 查询文件树接断点调试和另外一种实现方式 **简介: 查询文件树接断点调试和另外一种实现方式** * 需求 * 断点调试查询文件树接口逻辑 * 编写另外一种文件树实现代码(思考哪种方式好) * 对比不同方式,多数据和少数据的优缺点 * 另一种文件树实现代码 ```json //查询当前用户的所有文件夹 List folderList = accountFileMapper.selectList(new QueryWrapper() .eq("account_id", accountId) .eq("is_dir", FolderFlagEnum.YES.getCode())); //拼装文件夹树列表 if (CollectionUtils.isEmpty(folderList)) { return List.of(); } List folderTreeNodeDTOS = folderList.stream().map(file->{ return FolderTreeNodeDTO.builder() .id(file.getId()) .label(file.getFileName()) .parentId(file.getParentId()) .children(new ArrayList<>()) .build(); }).toList(); //根据父文件夹进行分组 key是当前文件夹ID,value是当前文件夹下的所有子文件夹 Map> folderTreeNodeVOMap = folderTreeNodeDTOS.stream() .collect(Collectors.groupingBy(FolderTreeNodeDTO::getParentId)); for (FolderTreeNodeDTO node : folderTreeNodeDTOS) { List children = folderTreeNodeVOMap.get(node.getId()); //判断列表是否为空 if (!CollectionUtils.isEmpty(children)) { node.getChildren().addAll(children); } } return folderTreeNodeDTOS.stream().filter(node -> Objects.equals(node.getParentId(), 0L)).collect(Collectors.toList()); ``` #### 网盘小文件上传接口设计和开发 * 需求 * 文件上传分三部分接口:小文件上传、大文件上传、文件秒传 * 先开发:小文件上传接口 * 上传到存储引擎 * 保存文件信息 * 保存文件映射关系 * 编码实战 * 上传文件到存储引擎,返回存储的文件路径 ```json private String storeFile(FileUploadReq req) { String objectKey = CommonUtil.getFilePath(req.getFilename()); fileStoreEngine.upload(minioConfig.getBucketName(), objectKey, req.getFile()); return objectKey; } ``` * 保存文件信息 ```json private FileDO saveFile(FileUploadReq req, String storeFileObjectKey) { FileDO fileDO = new FileDO(); fileDO.setAccountId(req.getAccountId()); fileDO.setFileName(req.getFilename()); fileDO.setFileSize(req.getFile() != null ? req.getFile().getSize() : req.getFileSize()); fileDO.setFileSuffix(CommonUtil.getFileSuffix(req.getFilename())); fileDO.setIdentifier(req.getIdentifier()); fileDO.setObjectKey(storeFileObjectKey); fileMapper.insert(fileDO); return fileDO; } ``` * 保存文件映射关系 ```json AccountFileDTO accountFileDTO = AccountFileDTO.builder().fileName(req.getFilename()) .accountId(req.getAccountId()) .fileId(fileDO.getId()) .fileSize(fileDO.getFileSize()) .fileSuffix(fileDO.getFileSuffix()) .parentId(req.getParentId()) .isDir(FolderFlagEnum.NO.getCode()) .fileType(FileTypeEnum.fromExtension(fileDO.getFileSuffix()).name()) .build(); saveAccountFile(accountFileDTO); ``` * 文件枚举 ```json @Getter public enum FileTypeEnum { COMMON("common"), COMPRESS("compress"), EXCEL("excel"), WORD("word"), PDF("pdf"), TXT("txt"), IMG("img"), AUDIO("audio"), VIDEO("video"), PPT("ppt"), CODE("code"), CSV("csv"); private final String type; private static final Map EXTENSION_MAP = new HashMap<>(); static { for (FileTypeEnum fileType : values()) { switch (fileType) { case COMPRESS: EXTENSION_MAP.put("zip", fileType); EXTENSION_MAP.put("rar", fileType); EXTENSION_MAP.put("7z", fileType); break; case EXCEL: EXTENSION_MAP.put("xls", fileType); EXTENSION_MAP.put("xlsx", fileType); break; case WORD: EXTENSION_MAP.put("doc", fileType); EXTENSION_MAP.put("docx", fileType); break; case PDF: EXTENSION_MAP.put("pdf", fileType); break; case TXT: EXTENSION_MAP.put("txt", fileType); break; case IMG: EXTENSION_MAP.put("jpg", fileType); EXTENSION_MAP.put("jpeg", fileType); EXTENSION_MAP.put("png", fileType); EXTENSION_MAP.put("gif", fileType); EXTENSION_MAP.put("bmp", fileType); break; case AUDIO: EXTENSION_MAP.put("mp3", fileType); EXTENSION_MAP.put("wav", fileType); EXTENSION_MAP.put("aac", fileType); break; case VIDEO: EXTENSION_MAP.put("mp4", fileType); EXTENSION_MAP.put("avi", fileType); EXTENSION_MAP.put("mkv", fileType); break; case PPT: EXTENSION_MAP.put("ppt", fileType); EXTENSION_MAP.put("pptx", fileType); break; case CODE: EXTENSION_MAP.put("java", fileType); EXTENSION_MAP.put("c", fileType); EXTENSION_MAP.put("cpp", fileType); EXTENSION_MAP.put("py", fileType); EXTENSION_MAP.put("js", fileType); EXTENSION_MAP.put("html", fileType); EXTENSION_MAP.put("css", fileType); break; case CSV: EXTENSION_MAP.put("csv", fileType); break; default: break; } } } FileTypeEnum(String type) { this.type = type; } public static FileTypeEnum fromExtension(String extension) { if (extension == null || extension.isEmpty() || !isValidExtension(extension)) { return COMMON; } try { return EXTENSION_MAP.getOrDefault(extension.toLowerCase(), COMMON); } catch (NullPointerException e) { // 记录日志 System.err.println("Unexpected null pointer exception: " + e.getMessage()); return COMMON; } } private static boolean isValidExtension(String extension) { // 确保扩展名只包含字母和数字 return extension.matches("[a-zA-Z0-9]+"); } } ``` #### 网盘小文件上传接口测试验证 * 需求 * ApiFox测试文件上传接口 * 测试创建多个文件夹 * 对应的文件上传多个类型的文件 * 测试实战 #### 文件批量移动接口设计和开发 * 需求 * 批量操作对应的文件列表,移动到对应的目录下面 * 需要考虑什么?如何实现相关功能? image-20250102105611605 * 业务逻辑设计(哪些方法会复用) * 检查被转移的文件ID是否合法(复用) * 检查目标文件夹ID是否合法(复用) * 目标文件夹ID必须是当前用户的文件夹,不能是文件 * 要操作(移动、复制)的文件列表不能包含是目标文件夹的子文件夹,递归处理 * 批量转移文件到目标文件夹 * 处理重复文件名 * 更新文件或文件夹的parentId为目标文件夹ID * 编码实战 image-20250102112917137 ```java @Transactional(rollbackFor = Exception.class) public void moveBatch(FileBatchReq req) { //检查被转移的文件ID是否合法 List accountFileDOList = checkFileIdLegal(req.getFileIds(), req.getAccountId()); //检查目标文件夹ID是否合法,需要包括子文件夹 checkTargetParentIdLegal(req); //批量转移文件到目标文件夹 //处理重复文件名 accountFileDOList.forEach(this::processFileNameDuplicate); // 更新文件或文件夹的parentId为目标文件夹ID UpdateWrapper updateWrapper = new UpdateWrapper<>(); updateWrapper.in("id", req.getFileIds()) .set("parent_id", req.getTargetParentId()); int updatedCount = accountFileMapper.update(null, updateWrapper); if (updatedCount != req.getFileIds().size()) { throw new RuntimeException("部分文件或文件夹移动失败"); } } public List checkFileIdLegal(List fileIdList, Long accountId) { List accountFileDOList = accountFileMapper.selectList(new QueryWrapper() .in("id", fileIdList) .eq("account_id", accountId)); if (accountFileDOList.size() != fileIdList.size()) { log.error("文件ID数量不合法,请检查:accountId={},fileIdList={}", accountId, fileIdList); throw new BizException(BizCodeEnum.FILE_DEL_BATCH_ILLEGAL); } return accountFileDOList; } ``` * 需求 * 批量整理对应的文件列表,移动到对应的目录下面 * 需要考虑什么?如何实现相关功能? image-20250102105611605 * 编码实现 image-20250102112809176 * 检查父ID是否合法 ```java private void checkTargetParentIdLegal(FileBatchReq req) { //1、目标文件夹ID 必须是当前用户的文件夹,不能是文件 AccountFileDO targetParentFolder = accountFileMapper.selectOne(new QueryWrapper() .eq("id", req.getTargetParentId()) .eq("account_id", req.getAccountId()) .eq("is_dir", FolderFlagEnum.YES.getCode())); if (targetParentFolder == null) { log.error("目标文件夹不存在,目标文件夹ID:{}", req.getTargetParentId()); throw new BizException(BizCodeEnum.FILE_TARGET_PARENT_ILLEGAL); } /** * 2、要操作(移动、复制)的文件列表不能包含是目标文件夹的子文件夹 * 思路: * 1、查询批量操作中的文件夹和子文件夹,递归处理 * 2、判断是否在里面 */ //查询待批量操作中的文件夹和子文件夹 List prepareAccountFileDOList = accountFileMapper.selectList(new QueryWrapper() .in("id", req.getFileIds()) .eq("account_id", req.getAccountId())); List allAccountFileDOList = new ArrayList<>(); findAllAccountFileDOWithRecur(allAccountFileDOList, prepareAccountFileDOList, false); // 判断allAccountFileDOList是否包含目标夹的id if (allAccountFileDOList.stream().anyMatch(file -> file.getId().equals(req.getTargetParentId()))) { log.error("目标文件夹不能是源文件列表中的文件夹,目标文件夹ID:{},文件列表:{}", req.getTargetParentId(), req.getFileIds()); throw new BizException(BizCodeEnum.FILE_TARGET_PARENT_ILLEGAL); } } ``` #### 文件批量操作-递归接口设计和开发实战 * 什么是递归 * 允许函数调用自身来解决问题。 * 递归的基本思想是将一个复杂的问题分解成更小的、相似的子问题,直到这些子问题足够简单,可以直接解决 * **优点:包括代码简洁和优雅,比如对于某些问题(如树结构遍历、分治算法等)的处理逻辑更加简单** * 缺点:比如可能导致较大的内存消耗(因为每次函数调用都需要在调用栈上保存信息),在某些情况下可能不如迭代方法高效 * 递归通常包含两个主要部分: * **基本情况(Base Case)**:停止的条件,在每个递归调用中,都会检查是否达到了基本情况,如果是则停止递归返回结果。 * **递归步骤(Recursive Step)**:函数调用自身的过程。在这一步中问题被分解成更小的子问题,递归地解决这些子问题。 * 编码设计和逻辑说明 * 遍历文件列表:对传入的 accountFileDOList 进行遍历。 * 判断是否为文件夹:如果当前项是文件夹,则递归获取其子文件,并继续递归处理。 * 添加到结果列表:根据 onlyFolder 参数决定是否只添加文件夹,或者同时添加文件和文件夹 image-20250102113509986 * `findAllAccountFileDOWithRecur` 递归逻辑处理(多个地方会使用,封装方法) ```java @Override public void findAllAccountFileDOWithRecur(List allAccountFileDOList, List accountFileDOList, boolean onlyFolder) { for (AccountFileDO accountFileDO : accountFileDOList) { if (Objects.equals(accountFileDO.getIsDir(), FolderFlagEnum.YES.getCode())) { //文件夹,递归获取子文件ID List childFileList = accountFileMapper.selectList(new QueryWrapper() .eq("parent_id", accountFileDO.getId())); findAllAccountFileDOWithRecur(allAccountFileDOList, childFileList, onlyFolder); } //如果通过onlyFolder是true只存储文件夹到allAccountFileDOList,否则都存储到allAccountFileDOList if (!onlyFolder || Objects.equals(accountFileDO.getIsDir(), FolderFlagEnum.YES.getCode())) { allAccountFileDOList.add(accountFileDO); } } //return allAccountFileDOList; } ``` #### 文件批量移动接口测试验证实战 * 需求 * 测试文件批量移动 * bug修复 ![image-20250106111000289](/img/image-20250106111000289.png) * 数据准备 * 创建m1和m2文件夹, * m1文件夹上传多个文件,然后里面创建子文件夹 * 查看m1和m2文件夹相关数据 * 移动m1到m2文件夹 * 查看m1和m2文件夹相关数据 ## 大模型 ### LLM大模型开发核心-LangChain框架实战 #### LLM开发框架LangChain介绍和技术生态 * 背景需求 * 大模型(如ChatGPT、DeepSeek)的局限性: - 无法获取训练数据外的实时信息(如今天的天气) - 不能直接执行具体操作(发邮件/查数据库) - 处理复杂任务时缺乏步骤规划能力 * 开发者的痛点 ```java // 传统Java开发模式 vs AI应用开发 String result = service.doSomething(input); // 确定性结果 // VS String aiResponse = llm.generate(prompt); // 非确定性输出 ``` * 什么是LangChain框架(类似SpringCloud) * 是一个基于大型语言模型(LLM)开发应用程序的框架,专为构建与大语言模型(LLMs)相关的应用而设计。 * 通过将多个 API、数据源和外部工具无缝集成,LangChain 能帮助开发者更高效地构建智能应用。 * 从与 OpenAI 、DeepSeek等顶级大模型供应商的集成,到复杂的对话系统、智能搜索、推荐系统等 * LangChain 提供了丰富的功能和灵活的接口,极大地方便了开发者的工作。 * **通俗点:LangChain 就是对各类大模型提供的 API 的套壳,方便开发者使用这些 API和协议,搭建起来的模块和接口组合** * 官网:https://www.langchain.com * GIthub地址:https://github.com/langchain-ai/langchain * LangChain生态产品介绍 * LangChain * 提供模块化开发能力,支持LLM(如GPT、Claude等)与外部数据源(数据库、API)的集成 * 包含链(Chain)、代理(Agent)、记忆(Memory)等核心组件,用于构建复杂AI应用 image-20250301141529448 * LangServer * 部署工具,可将LangChain应用快速转换为REST API,支持并行处理、流式传输和异步调用 * 自动生成OpenAPI文档, 滚动更新支持, 内置Prometheus指标, 适用于企业级生产环境 image-20250301140529990 * LangSmith * 开发者调试与监控平台,支持对LLM应用的性能分析、测试和部署优化 * 提供可视化调试界面和自动化评估工具,提升开发效率与可靠性 ![image-20250301140503525](/img/image-20250301140503525.png) * LangGraph * 状态管理工具,用于构建多代理系统,支持流式处理和复杂任务分解 * 可视化流程设计器, 循环/条件分支支持,分布式状态持久化, 自动断点续跑 ![image-20250301140650448](/img/image-20250301140650448.png) * 产品矩阵对比 | **产品** | **核心价值** | **Java生态对标** | **适用场景** | | :------------: | :----------------: | :------------------: | :----------------------: | | LangSmith | 全生命周期可观测性 | Prometheus + Grafana | 生产环境监控、效果评估 | | LangServe | 快速服务化 | Spring Boot | 模型API部署、快速原型 | | LangGraph | 复杂流程编排 | Activiti BPMN | 业务工作流设计、状态管理 | | LangChain Core | 基础组件库 | Spring AI | 基础AI功能开发 | ![export_a9yct](/img/export_a9yct.png) #### Python虚拟环境evn应用讲解和实战 * 什么是Python的虚拟环境 * 类似虚拟机、沙箱机制一样,隔离不同的项目依赖的环境 * 核心作用 * **隔离项目依赖**:不同项目可能依赖同一库的不同版本。 * **避免全局污染**:防止安装过多全局包导致冲突。 * **便于协作**:通过依赖清单(如`requirements.txt`)复现环境 * 虚拟环境 vs 全局环境 | **特性** | **虚拟环境** | **全局环境** | | :----------: | :--------------------------: | :------------------------: | | **依赖隔离** | 项目独立,互不影响 | 所有项目共享 | | **安全性** | 避免权限问题(无需sudo安装) | 需谨慎操作(可能影响系统) | | **适用场景** | 开发、测试、多版本项目 | 系统级工具或少量通用库 | * 镜像源配置 * 查看系统配置的镜像源操作 ```shell pip config list pip config get global.index-url ``` * 配置国内镜像源 ```shell pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple ``` * 虚拟环境基础操作 * 创建虚拟环境 ```shell # 语法:python -m venv <环境目录名> python -m venv myenv # 创建名为myenv的虚拟环境 ``` * 激活虚拟环境 * Windows(CMD/PowerShell) ```shell myenv\Scripts\activate.bat # CMD myenv\Scripts\Activate.ps1 # PowerShell(需管理员权限解除限制) ``` * Linux/macOS: ```shell source myenv/bin/activate ``` * 激活后提示符变化 ```shell source myenv/bin/activate ``` * 退出虚拟环境 ```shell deactivate ``` * 依赖管理 * 安装库到虚拟环境 ```shell # 激活环境后操作 (myenv) pip install requests # 安装最新版本 (myenv) pip install django==3.2 # 安装指定版本 ``` * 导出依赖清单 ```shell (myenv) pip freeze > requirements.txt ``` * 从清单恢复依赖 ```shell # 在新环境中执行 (myenv) pip install -r requirements.txt ``` * 最佳实践与案例 * 典型项目流程(区分Linux、Mac和Window) ```shell # 创建项目目录并进入 mkdir myproject && cd myproject # 创建虚拟环境 python -m venv .venv # 激活环境(Windows: .venv\Scripts\activate) source .venv/bin/activate # 安装依赖 pip install django pandas # 导出依赖 pip freeze > requirements.txt # 开发完成后退出 deactivate ``` * 协作复现环境 ```shell # 克隆项目后操作 git clone https://github.com/user/project.git cd project # 创建并激活虚拟环境 python -m venv .venv source .venv/bin/activate # 安装依赖 pip install -r requirements.txt ``` * 常见问题与解决 * 虚拟环境激活失败 * 现象:source: command not found * 原因:在Windows使用Linux命令或在Linux未使用source。 * 解决:根据操作系统选择正确激活命令。 * 跨平台路径问题 * 问题:Windows与Linux路径格式差异导致脚本无法运行。 * 方案:使用/统一路径分隔符,或在代码中处理路径 * 依赖版本冲突 * 场景:项目A需要`numpy==1.18`,项目B需要`numpy==1.20`。 * 解决:为每个项目创建独立虚拟环境。 * 案例实战: * LangChain框架环境搭建 #### VSCode编辑器LangChain环境安装和验证 * Python虚拟环境和项目创建 * 创建虚拟环境(Windows/macOS/Linux通用) ```python # 创建环境目录 python -m venv langchain_env ``` * 激活虚拟环境 * **Windows** ```python .\langchain_env\Scripts\activate ``` * **macOS/Linux** ```python source langchain_env/bin/activate ``` * 验证环境 ```python # 查看Python路径(应显示虚拟环境路径) which python # macOS/Linux where python # Windows ``` * LangChain环境安装 * 安装核心依赖包 (**版本和课程保持一致,不然很多不兼容!!!**) * 下载相关资料 ,使用**【wget】或者【浏览器】远程下载相关依赖包(需要替换群里最新的)** ```python 原生资料下载方式(账号 - 密码 - ip地址 - 端口 需要替换群里最新的,【其他路径不变】) wget --http-user=用户名 --http-password=密码 http://ip:端口/dcloud_pan/aipan_install_1.zip #比如 命令行下 wget --http-user=admin --http-password=xdclass.net888 http://47.115.31.28:9088/dcloud_pan/aipan_install_1.zip # 比如 浏览器直接访问 http://47.115.31.28:9088/dcloud_pan/aipan_install_1.zip ``` * 解压后执行【**依赖很多,版本差异大,务必按照下面执行,否则课程无法进行下去,加我微信 xdclass6**】 ```python # 安装依赖 pip install -r requirements.txt ``` * 验证安装【很多模块后续使用会验证】 ```python # 执行简单测试 from langchain_core.prompts import ChatPromptTemplate print(ChatPromptTemplate.from_template("Hello 欢迎来到小滴课堂-AI大模型开发课程 {title}!").format(title=",干就完了")) # 应输出: Human: Hello 欢迎来到小滴课堂-AI大模型开发课程 ,干就完了! ``` #### LangChain框架模块和大模型IO交互链路讲解 **简介: LangChain框架模块和大模型IO交互链路讲解** * 大模型IO交互链路概览 ![image-20250301152117731](/img/image-20250301152117731.png) * LangChain模块对比大家熟知的Java Spring生态 | LangChain模块 | Spring对应技术 | 交互方式差异 | | :-----------: | :-------------: | :----------------: | | Models | Spring AI | 多模型热切换支持 | | Memory | Redis/Hazelcast | 内置对话上下文管理 | | Chains | Activity工作流 | 动态流程重组能力 | | Agents | Drools规则引擎 | 基于LLM的决策机制 | * LangChain架构六大模块(后续围绕模块逐步深入) * **Models(模型层)** - 相当于`interface LLM`,支持多种大模型(OpenAI/Gemini等) - 示例:就像Java中的JDBC接口,可以对接不同数据库 * **Prompts(提示工程)** - 相当于模板引擎(类似Thymeleaf) ```python from langchain.prompts import PromptTemplate template = """ 你是一个Java专家,请用比喻解释{concept}: 要求: 1. 用{framework}框架做类比 2. 不超过2句话 """ prompt = PromptTemplate.from_template(template) print(prompt.format(concept="机器学习", framework="Spring")) ``` * **Chains(任务链)** - 类似Java的工作流引擎,将多个组件组合在一起,创建一个单一、连贯的任务 - 包括不同的链之间组合 ```python from langchain.chains import LLMChain # 创建任务链(类似Java的链式调用) chain = LLMChain(llm=model, prompt=prompt) result = chain.run(concept="多线程", framework="Spring Batch") ``` * **Memory(记忆)** - 类似HTTP Session的会话管理 ```python from langchain.memory import ConversationBufferMemory memory = ConversationBufferMemory() memory.save_context({"input": "你好"}, {"output": "您好!"}) ``` * **Indexes(索引)** - 类似数据库索引+JDBC连接 - 对不通的文档进行结构化的方法,包括提取、切割、向量存储等,方便 LLM 能够更好的与之交互 ```python from langchain.document_loaders import WebBaseLoader # 加载外部数据(类似JDBC读取数据库) loader = WebBaseLoader("https://docs.spring.io/spring-boot/docs/current/reference/html/") docs = loader.load() ``` * **Agents(智能体)** - 类似策略模式+工厂模式,比chain更高级,可以自己选择调用链路 - 比如下一步有多个选择, 包括不同工具、流程链路等,由程序自己选择 ```python from langchain.agents import Tool, initialize_agent tools = [ Tool(name="Calculator", func=lambda x: eval(x), description="计算数学表达式") ] agent = initialize_agent(tools, llm, agent="zero-shot-react-description") ``` * 常见分层设计和交互如下 ```python +----------------+ | 应用层 (Agents) | +----------------+ | 编排层 (Chains) | +----------------+ | 能力层 (Tools) | +----------------+ | 模型层 (Models) | +----------------+ | 数据层 (Memory) | +----------------+ ``` #### 大模型Model-IO链路抽象和Chat模型开发 **简介: 大模型Model-IO链路抽象和Chat模型实战** * 大模型使用开发的Model IO链路核心三要素 ![1](/img/1.png) | 组件 | 作用 | 典型类/方法 | | :---------: | :------------------------: | :-------------------------------------------: | | **Prompts** | 构建模型输入的结构化模板 | `ChatPromptTemplate`, `FewShotPromptTemplate` | | **Models** | 对接不同LLM服务的统一接口 | `ChatOpenAI` | | **Parsers** | 将模型输出转换为结构化数据 | `StrOutputParser`, `JsonOutputParser` | ![image-20250301152117731](/img/image-20250301152117731.png) * LangChain支持的模型类型说明 * 文本生成模型(Text Generation Models-逐渐少用了,Chat更强大) * 功能:生成连贯文本,主要用于处理文本相关的任务,如自然语言理解、文本生成、情感分析、翻译 * 典型模型:GPT-3、Claude、PaLM * 对话模型(Chat Models,多数采用这个) * 功能:处理多轮对话,人机交互中的对话能力,能够进行自然流畅的对话交流,适用于客户服务、智能助手 * 典型模型:GPT-4、Claude-2 * 嵌入模型(Embedding Models) * 功能:生成文本向量表示,将文本转换为固定长度的向量表示,向量保留了数据的语义信息,便于后续的相似度计算、分类等任务。 * 典型模型:text-embedding-ada-002 * 多模态模型(Multimodal Models) * 功能:处理文本+图像,例如文本、图像、音频等,更全面的信息理解和处理能力。 * 典型模型:GPT-4V、Qwen-omni-turbo * 其他更多.... * LangChain开发LLM聊天模型快速编码实战 ```python from langchain_openai import ChatOpenAI # 调用Chat Completion API llm = ChatOpenAI( model_name='qwen-plus', base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-0903038424424850a88ed161845d7d4c") response = llm.invoke('你是谁?') print(response) ``` #### 类型增强模块Typing应用和案例》 * Python的动态类型痛点 ```python # 传统动态类型代码示例 def calculate(a, b): return a + b # 无法直观看出参数类型和返回值类型 result1 = calculate(3, 5) # ✅ 正确用法 result2 = calculate("3", 5) # ❌ 运行时才报错 ``` * 什么是Typing模块 * 自python3.5开始,PEP484为python引入了类型注解(type hints), 为Python带来了类型提示和类型检查的能力。 * 允许开发者在代码中添加类型注解,提高代码的可读性和可维护性。 * 尽管Python是一种动态类型语言,但类型注解能让开发者更清晰地了解函数和变量的预期类型 * 核心 * **提升代码可读性**:明确参数和返回类型 * **增强IDE支持**:智能提示与自动补全 * **静态类型检查**:开发阶段发现潜在错误 * **完善文档生成**:自动生成类型化API文档 * 核心语法快速入门 * 简单类型(Primitive Types) * Python内置的基本数据类型注解 * 适用场景:变量、函数参数、返回值的简单类型声明 * 类型注解不影响运行时行为 * 兼容子类型(如int注解可接受bool值) ``` age: int = 25 # 整数类型 name: str = "Alice" # 字符串类型 price: float = 9.99 # 浮点数类型 is_valid: bool = True # 布尔类型 data: bytes = b"binary" # 字节类型 ``` * 容器类型 * 有多种内置的类型别名,比如`List`、`Tuple`、`Dict`等,可用于注解变量和函数的预期类型 * 例如 * `Dict[str, int]`表示键是字符串类型,值是整数类型的字典 * `Set[int]`表示整数类型的集合 * `List`同质元素的序列容器 * 适用场景:列表类型数据,元素类型相同 ```python from typing import List scores: List[int] = [90, 85, 95] # 整型列表 matrix: List[List[float]] = [[1.1, 2.2], [3.3]] # 嵌套列表 ``` * `Dict` 键值对映射容器 * 适用场景:字典类型数据,需指定键值类型 ```python from typing import Dict person: Dict[str, str] = {"name": "Bob", "job": "dev"} # 字符串字典 config: Dict[str, Union[int, str]] = {"timeout": 30} # 混合值类型 ``` * `Tuple`固定长度、类型的不可变序列 - 适用场景:坐标、数据库记录等固定结构 - 变长声明:`Tuple[T, ...]`:元素类型相同但长度不限 , ():空元组 ```python from typing import Tuple point: Tuple[float, float] = (3.14, 2.71) # 二元坐标 rgb: Tuple[int, int, int] = (255, 0, 128) # 颜色值 flexible: Tuple[str, ...] = ("a", "b", "c") # 任意长度元组 ``` * `Set` 无序不重复元素的集合 - 适用场景:去重数据、集合运算 ```python from typing import Set unique_ids: Set[int] = {1, 2, 3} # 整型集合 tags: Set[Union[str, int]] = {"urgent", 1001} # 混合类型集合 ``` * 任意类型 `Any` - 动态类型占位符,放弃类型检查, 应尽量避免过度使用 - 适用场景:兼容无类型代码或动态行为 ```python from typing import Any def debug_log(obj: Any) -> None: print(repr(obj)) ``` * 函数类型注解 * 为函数添加typing模块的注解后,函数使用者就能清晰的了解函数的参数以及返回值类型 ```python def greet(name: str) -> str: # 参数类型 -> 返回值类型 return f"Hello, {name}" def calculate(a: int, b: int) -> int: return a * b # 无返回值使用None def show_info(info: str) -> None: print(info) ``` * `Literal` 字面量类型 * 精确值类型约束, 替代简单字符串枚举 * 适用场景:枚举值的类型安全 ```python from typing import Literal # 限定特定值 HttpMethod = Literal["GET", "POST", "PUT", "DELETE"] def send_request(method: HttpMethod, url: str) -> None: print(f"Sending {method} request to {url}") send_request("POST", "/api") # ✅ send_request("PATCH", "/api") # ❌ mypy报错 ``` * `Union`联合类型 * Union允许参数接受多种不同类型的数据。 * 例如 `Union[int, float]`表示变量可以是int类型或float类型 ```python from typing import Union def process_input(value: Union[int, str]) -> None: if isinstance(value, int): print(f"Number: {value}") else: print(f"String: {value}") process_input(42) # Number: 42 process_input("test") # String: test ``` * `Optional`可选类型 * `Optional`表示参数可以是指定类型或者`None` * 让编译器识别到该参数有一个类型提示,可以使指定类型,也可以是None,且参数是可选非必传的。 * `Optional[int]` 等价于 `Union[int, None]`,表示:既可以传指定的类型 int,也可以传 None, `Optional[ ]` 里面只能写一个数据类型 * 适用场景:可能返回空值的操作 * 在下面函数定义中,Optional[str] = None表示参数name的类型可以是str或None。 * 注意 * `= None`可省略,它表示默认参数。 * 从 Python 3.10 开始,Optional[Type] 可以直接用 `Type | None` 替代,写法更清晰 ```python from typing import Optional def greet1(name: Optional[str] = None) -> str: if name: return f"Hello, {name}!" else: return "Hello, world!" def greet2(name: Optional[str]) -> str: if name: return f"Hello, {name}!" else: return "Hello, world!" print(greet1()) # print(greet2()) # 报错,必须要有参数 print(greet1("老王")) print(greet2("冰冰")) ``` * 类型别名 * 自定义类型别名提高代码可读性。 ```python from typing import Tuple # 基本别名 UserId = int Point = Tuple[float, float] def get_user(id: UserId) -> str: return f"User{id}" def plot(points: List[Point]) -> None: for x, y in points: print(f"({x}, {y})") ``` * `NewType`新类型创建 * 创建具有类型检查的语义化新类型 * 适合 区分相同基础类型的不同用途 ```python from typing import NewType # 创建强类型 UserId = NewType('UserId', int) admin_id = UserId(1001) def print_id(user_id: UserId) -> None: print(user_id) # 正确调用 print_id(admin_id) # ✅ print_id(1001) # ❌ mypy报错 ``` * `TypeVar`(类型变量) * 创建通用类型参数 * 适用场景:泛型函数/类的类型参数化;比如创建一个函数,无论是处理整数、字符串还是自定义对象 ```python from typing import TypeVar, Sequence T = TypeVar('T') # 无约束类型 Num = TypeVar('Num', int, float) # 受限类型 def first(items: Sequence[T]) -> T: return items[0] def sum(values: Sequence[Num]) -> Num: return sum(values) ``` ```python from typing import TypeVar # 定义一个泛型变量T T = TypeVar('T') # 创建一个泛型函数 def get_first_item(items: list[T]) -> T: """获取列表的第一个元素""" if items: return items[0] raise ValueError("列表为空") # 使用示例 numbers = [1, 2, 3, 4, 5] words = ['apple', 'banana', 'cherry', 'fruit'] print(get_first_item(numbers)) # 输出: 1 print(get_first_item(words)) # 输出: apple ``` ### Prompt提示词工程和案例最佳实践 #### 大模型必备Prompt提示词工程 * 什么是Prompt Engineering提示词工程 * 通过特定格式的文本输入引导AI模型生成期望输出的技术,明确地告诉模型你想要解决的问题或完成的任务 * 也是大语言模型理解用户需求并生成相关、准确回答或内容的基础 * **类比:给Java程序员的任务需求文档(越清晰明确,结果越符合预期)** * 为什么需要学习? * 大模型就是你的员工,你可以有多个助手,OpenAI、DeepSeek、千问等 * 作为老板的你,需要正确的下达任务,描述合理和交付目标等 ```python 传统编程:写代码→计算机执行 Prompt工程:写自然语言指令→大模型生成结果 ``` ![image-20250304142514844](/img/image-20250304142514844.png) * Prompt设计四要素 * 角色设定(Role Prompting) * 作用:限定模型回答视角 ```python [差] 写一首关于春天的诗 [优] 你是一位擅长写现代诗的诗人,请用比喻手法创作一首8行的春天主题短诗 ``` * 任务描述 * STAR原则:Situation 场景、Task 任务、Action 行动、Result 结果 ```python (场景)用户提交了一个技术问题 (任务)需要给出准确且易懂的解答 (行动)分步骤说明解决方案 (结果)最后用一句话总结要点 ``` * 格式规范 * 常用格式指令:分点列表、指定段落数、表格呈现、代码格式 ```python 用JSON格式输出包含以下字段: { "summary": "不超过50字的摘要", "keywords": ["关键词1", "关键词2", "关键词3"] } ``` * 约束条件 * 常见约束类型: | 类型 | 示例 | | :--: | :--------------------: | | 长度 | "答案控制在200字内" | | 风格 | "用初中生能理解的语言" | | 内容 | "不包含专业术语" | | 逻辑 | "先解释概念再举例说明" | * 汇总 | 要素 | 说明 | 反面案例 | 优化案例 | | :----------: | :------------: | :----------: | :-------------------------: | | **角色设定** | 明确模型身份 | "帮我写代码" | "你是一个资深Java架构师..." | | **任务说明** | 具体执行要求 | "分析数据" | "使用Markdown表格对比..." | | **输出格式** | 结构化结果定义 | 自由文本 | JSON/XML/YAML格式 | | **约束条件** | 限制输出范围 | 无限制 | "不超过200字,不用专业术语" | * 模板结构设计(黄金公式) ```python # 标准三段式结构 prompt_template = """ [角色设定] 你是一个具有10年经验的{领域}专家,擅长{特定技能} [任务说明] 需要完成以下任务: 1. {步骤1} 2. {步骤2} 3. {步骤3} [输出要求] 请按照以下格式响应: {示例格式} """ ``` * 常见问题和排查原因 | 现象 | 可能原因 | 解决方案 | | :--------------: | :----------------: | :----------------------: | | 输出内容偏离主题 | 角色设定不明确 | 添加"忽略无关信息"约束 | | 生成结果过于笼统 | 缺少具体步骤要求 | 添加"分步骤详细说明"指令 | | 格式不符合要求 | 未提供明确格式示例 | 添加XML/JSON标记示例 | #### Prompt提示词工程多案例最佳实践 * 需求 * 利用在线大模型或者本地大模型 * 测试不同的提示词效果,分析优化前、后的Prompt工程 * **案例实战:通用回答助手、代码生成助手、技术问答、AI数据分析 等案例实战** * 案例实战一:通用回答 * 差Prompt: ``` 告诉我关于人工智能的信息 ``` * 问题分析:过于宽泛,缺乏焦点、没有指定回答的深度和范围、未明确期望的格式 * 输出结果:可能得到从历史发展到技术原理的冗长概述,缺乏针对性 * 好prompt ```python 你是一位科技专栏作家,请用通俗易懂的方式向高中生解释: 1. 什么是人工智能(用1个生活化比喻说明) 2. 列举3个当前主流应用场景 3. 字数控制在300字以内 要求使用「首先」、「其次」、「最后」的结构 ``` * 优化后 * 设定回答视角(科技专栏作家) * 明确目标受众(高中生) * 结构化输出要求 * 添加格式约束 * 案例实战二:代码生成 * 差Prompt: ```python 写个Python程序 ``` * 问题分析:没有具体功能描述、未指定输入输出格式、忽略异常处理需求 * 输出结果:可能生成简单的"Hello World"程序,与真实需求不符 * 好Prompt: ```python 编写一个Python函数,实现以下功能: - 输入:字符串形式的日期(格式:YYYY-MM-DD) - 输出:该日期对应的季度(1-4) - 要求: - 包含参数校验(不符合格式时抛出ValueError) - 使用datetime模块 - 编写对应的单元测试用例 示例: 输入 "2024-03-15" → 返回 1 ``` * 优化后 * 明确定义输入输出 * 指定实现方式 * 包含测试要求 * 提供示例验证 * 案例实战三:技术问答 * 差Prompt ```python 如何优化网站性能? ``` * 问题分析:问题范围过大、未说明技术栈、缺少评估标准 * 输出结果:可能得到泛泛而谈的通用建议 * 好Prompt ```python 针对使用SpringBoot+Vue3的技术栈,请给出5项可量化的性能优化方案: 要求: 1. 每项方案包含: - 实施步骤 - 预期性能提升指标(如LCP减少20%) - 复杂度评估(低/中/高) 2. 优先前端和后端优化方案 3. 引用Web Vitals评估标准 限制条件: - 不涉及服务器扩容等硬件方案 - 排除已广泛采用的方案(如代码压缩) ``` * 优化点 * 限定技术范围 * 结构化响应要求 * 设定评估标准 * 排除已知方案 * 案例实战四:数据分析 * 差Prompt ```python 分析这份销售数据 ``` * 问题分析:未说明数据特征、没有指定分析方法、缺少可视化要求 * 输出结果:可能得到无重点的描述性统计,缺乏洞察 * 好Prompt ```python 你是一位资深数据分析师,请完成以下任务: 数据集特征: - 时间范围:2027年1-12月 - 字段:日期/产品类别/销售额/利润率 要求: 1. 找出销售额top3的月份,分析增长原因 2. 识别利润率低于5%的产品类别 3. 生成包含趋势图的Markdown报告 输出格式: ## 分析报告 ### 关键发现 - 要点1(数据支撑) - 要点2(对比分析) ### 可视化 趋势图描述,生成base64编码的折线图 ``` * 优化点: * 明确分析者角色 * 描述数据集特征 * 指定分析方法论 * 规范输出格式 #### LangChain 提示模板PromptTemplate介绍 * 需求 * 掌握LangChain 提示模板PromptTemplate常见用法 * 掌握提示词里面的占位符使用和预置变量 * PromptTemplate介绍 * 是LangChain中用于构建结构化提示词的组件,负责将用户输入/动态数据转换为LLM可理解的格式 * 它是一种单纯的字符模板,后续还有进阶的ChatPromptTemplate * 主要解决 * 动态内容组装 * 避免Prompt硬编码 ![1](/img/1-1073680.png) * PromptTemplate核心变量和方法 * template 定义具体的模板格式,其中 `{变量名}` 是占位符 * input_variables 定义模板中可以使用的变量。 * partial_variables:前置变量,可以提前定义部分变量的值,预填充进去 * format 使用 `format` 方法填充模板中的占位符,形成最终的文本 * 案例实战 * 创建PromptTemplate对象简单模版 ```python from langchain.prompts import PromptTemplate # 定义模板 template = """ 你是一位专业的{domain}顾问,请用{language}回答: 问题:{question} 回答: """ # 创建实例 prompt = PromptTemplate( input_variables=["domain", "language", "question"], template=template ) print(prompt) # 格式化输出 print(prompt.format( domain="网络安全", language="中文", question="如何防范钓鱼攻击?" )) ``` * 自动推断变量 ```python from langchain.prompts import PromptTemplate # 当不显式声明 input_variables 时 template = "请将以下文本翻译成{target_language}:{text}" prompt = PromptTemplate.from_template(template) print(prompt.input_variables) # 输出: ['target_language', 'text'] ``` * 默认值设置 ```python from langchain.prompts import PromptTemplate template = """分析用户情绪(默认分析类型:{analysis_type}): 用户输入:{user_input} 分析结果:""" prompt_template = PromptTemplate( input_variables=["user_input"], template=template, template_format="f-string", # 新增参数 partial_variables={"analysis_type": "情感极性分析"} # 固定值 ) print(prompt_template.format(user_input="这个产品太难用了")) #====打印内部的变量====== print(prompt_template.template) print(prompt_template.input_variables) print(prompt_template.template_format) print(prompt_template.input_types) print(prompt_template.partial_variables) ``` #### PromptTemplate结合LLM案例实战 * 案例实战 ```python from langchain_openai import ChatOpenAI from langchain_core.prompts import PromptTemplate from langchain_core.output_parsers import StrOutputParser #创建prompt AIGC prompt_template = PromptTemplate( input_variables=["product"], template="为{product}写3个吸引人的广告语,需要面向年轻人", ) prompt = prompt_template.invoke({"product":"小滴课堂"}) #创建模型 model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) #调用大模型 response = model.invoke(prompt) #print(response.content) #创建输出解析器 out_parser = StrOutputParser() answer = out_parser.invoke(response) print(answer) ``` #### 大模型ChatModel聊天模型和Token计算 * 什么是**ChatModel** * 是专为**多轮对话场景**设计的大语言模型(LLM),通过理解上下文和对话逻辑,生成连贯、符合人类交互习惯的回复。 * 不仅是简单的文本生成工具,更是能处理复杂对话流程的智能系统 * 核心特点 | 特性 | 说明 | 示例场景 | | :--------------: | :----------------------------------------------: | :----------------------------------------------------------: | | **上下文感知** | 追踪多轮对话历史,理解指代关系(如“它”、“这个”) | 用户:“什么是量子计算?” → AI 解释 → 用户:“它有什么应用?” → AI 能正确关联“它”指量子计算 | | **角色扮演能力** | 可设定特定角色(如客服、教师)并保持一致性 | 设定AI为“医疗助手”时,拒绝提供诊断建议,仅提供健康信息 | | **意图识别** | 解析用户深层需求(如咨询、投诉、闲聊) | 用户:“我的订单没收到!” → AI 识别为物流投诉,优先转接人工客服 | | **情感分析** | 识别用户情绪(积极/消极),调整回复语气 | 用户表达不满时,AI 回复:“非常抱歉给您带来不便,我们会立刻处理...” | | **安全过滤** | 避免生成有害、偏见或敏感内容 | 用户请求生成暴力内容时,AI 拒绝并提示:“我无法协助这个请求” | * ChatModel vs. 传统 Text Model | **对比维度** | **ChatModel** | **传统 Text Model(如 text-davinci-003)** | | :------------: | :------------------------------------: | :----------------------------------------: | | **核心目标** | 多轮交互式对话 | 单次文本生成(文章、代码等) | | **输入格式** | 结构化消息序列(System/Human/AI 角色) | 纯文本提示 | | **上下文处理** | 自动管理对话历史 | 需手动拼接历史文本 | | **输出控制** | 内置安全审查和格式约束 | 依赖提示词工程控制 | | **典型应用** | 客服机器人、虚拟助手 | 内容创作、数据清洗 | * 聊天模型(如 GPT-3.5-turbo、GPT-4)通过 **角色化消息** 实现对话控制,核心角色包括: | 角色类型 | 标识符 | 功能定位 | 使用场景示例 | | :-----------: | :---------: | :------------------------: | :----------------------------------------------------------: | | **System** | `system` | 定义AI的行为准则和角色设定 | 设定AI身份、回答规则、知识范围
`("system", "你是一位医疗助手...")` | | **User** | `user` | 代表用户的输入信息 | 用户提问、指令、反馈
`("human", "如何缓解头痛?")` | | **Assistant** | `assistant` | 存储AI的历史回复 | 维护对话上下文、保持回答连贯性
`("ai", "建议服用布洛芬...")` | * 参考案例OpenAI代码 ```python # 多轮对话示例 messages = [ {"role": "system", "content": "你是一个电影推荐助手"}, {"role": "user", "content": "我喜欢科幻片,推荐三部经典"}, {"role": "assistant", "content": "1.《银翼杀手2049》... 2.《星际穿越》... 3.《黑客帝国》"}, {"role": "user", "content": "第二部的主演是谁?"} # 基于上下文追问 ] response = client.chat.completions.create( model="gpt-3.5-turbo", messages=messages ) print(response.choices[0].message.content) # 输出:《星际穿越》的主演是马修·麦康纳和安妮·海瑟薇... ``` * Chat聊天多轮对话中的Token如何计算 * 在多轮对话场景下,上下文通常涵盖以下几部分: * **用户的历史输入** :之前用户说过的话,这些内容会作为上下文的一部分,帮助模型理解当前对话的背景和意图。 * **模型的历史回复** :模型之前给出的回应,这样能让模型保持对话的连贯性和一致性。 * **系统提示** :用于设定聊天机器人的角色、目标等,引导对话的方向和风格。 * 随着上下文内容的增加,Token 数量也会相应增多。 * 多轮对话的上下文 Token 累积 * 假设每轮对话中,用户的输入和模型的输出分别对应一定数量的 Token * 我们以每轮对话输入 50 Token、输出 100 Token 为例来计算: * **第 1 轮** :用户的输入为 50 Token,模型的输出为 100 Token,此时上下文中的 Token 总数为 50 + 100 = 150 Token。 * **第 2 轮** :新的用户输入为 50 Token,模型新的输出为 100 Token,加上之前的历史上下文 150 Token,此时上下文中的 Token 总数为 50 + 100 + 150 = 300 Token。 * **第 3 轮** :再次新增用户输入 50 Token 和模型输出 100 Token,加上之前的历史上下文 300 Token,上下文中的 Token 总数变为 50 + 100 + 300 = 450 Token * 上下文窗口的限制 * 每个大语言模型都有一个 “上下文窗口大小”(Context Window)的限制, * 它决定了模型能够处理的上下文的最大 Token 数量。 * 常见的上下文窗口大小有: * 4k Tokens :例如 OpenAI GPT - 3.5,其上下文窗口大小为 4096 个 Token。 * 8k、32k Tokens :支持长上下文的模型,如 GPT-4 等,有更大的上下文窗口,分别为 8192 个和 32768 个 Token。 #### 聊天模型ChatPromptTemplate讲解 * ChatPromptTemplate 核心概念 * 核心差异 * 支持**消息角色**(system/user/assistant) * 天然适配聊天模型(如GPT-3.5/4) * 可维护对话上下文 ![2](/img/2.png) * 消息类型体系 | 消息模板类 | 对应角色 | 典型用途 | | :-------------------------: | :--------: | :--------------: | | SystemMessagePromptTemplate | 系统消息 | 设定AI行为规则 | | HumanMessagePromptTemplate | 用户消息 | 接收用户输入 | | AIMessagePromptTemplate | AI回复消息 | 记录历史响应 | | ChatPromptTemplate | 容器模板 | 组合多个消息模板 | * 掌握两个常见类方法 * `ChatPromptTemplate.from_template()` * 用于创建单条消息模板,通常结合其他方法构建更复杂的对话流程。 * 适用于单一角色的消息定义(如仅系统指令或用户输入)。 * 需与其他模板组合使用,例如通过`from_messages`整合多个单模板 * `ChatPromptTemplate.from_messages()` * 用于构建多轮对话模板,支持定义不同角色(如系统、用户、AI)的消息,并允许动态插入变量。 * 支持消息列表,每个消息可以是元组或`MessagePromptTemplate`对象。 * 角色类型包括:`system(系统指令)、human(用户输入)、ai(模型回复)` * 可通过占位符(如`{variable}`)动态替换内容。 * 汇总对比 | 方法 | 适用场景 | 灵活性 | 代码复杂度 | | :-------------: | :------------------------------: | :----: | :----------------: | | `from_messages` | 多角色、多轮对话(如聊天机器人) | 高 | 较高(需定义列表) | | `from_template` | 单角色消息模板(需组合使用) | 低 | 简单 | * 案例实战 * 使用from_messages构建多轮对话模板 ```python from langchain_core.prompts import ChatPromptTemplate # 定义消息列表,包含系统指令、用户输入和AI回复模板, 通过元组列表定义角色和模板,动态插入name和user_input变量 chat_template = ChatPromptTemplate.from_messages([ ("system", "你是一个助手AI,名字是{name}。"), ("human", "你好,最近怎么样?"), ("ai", "我很好,谢谢!"), ("human", "{user_input}") ]) # 格式化模板并传入变量 messages = chat_template.format_messages( name="Bob", user_input="你最喜欢的编程语言是什么?" ) print(messages) # 输出结果示例: # SystemMessage(content='你是一个助手AI,名字是Bob。') # HumanMessage(content='你好,最近怎么样?') # AIMessage(content='我很好,谢谢!') # HumanMessage(content='你最喜欢的编程语言是什么?') ``` * 结合`from_template`与`from_messages` ```python from langchain_core.prompts import ( ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate ) # 创建单条消息模板,通过细分模板类(如SystemMessagePromptTemplate)定义单条消息,再通过from_messages组合 system_template = SystemMessagePromptTemplate.from_template( "你是一个{role},请用{language}回答。" ) user_template = HumanMessagePromptTemplate.from_template("{question}") # 组合成多轮对话模板 chat_template = ChatPromptTemplate.from_messages([ system_template, user_template ]) # 使用示例 messages = chat_template.format_messages( role="翻译助手", language="中文", question="将'I love Python'翻译成中文。" ) print(messages) # 输出结果示例: # SystemMessage(content='你是一个翻译助手,请用中文回答。') # HumanMessage(content='将'I love Python'翻译成中文。' ``` #### LangChain聊天模型多案例实战 * 需求 * 聊天模型案例实战,需要结合LLM大模型进行调用 * 简单记执行顺序 ` from_template->from_messages->format_messages` * 最终传递给大模型是完整构建消息列表对象 * 领域专家案例实战 ```python from langchain_openai import ChatOpenAI from langchain_core.messages import SystemMessage, HumanMessage model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) # 构建消息列表(类似Java的ListMessage>) messages = [ SystemMessage(content="你是一个Java专家,用中文回答"), HumanMessage(content="解释volatile关键字的作用") ] # 同步调用(类似Java的execute()) response = model.invoke(messages) print(response.content) """ volatile关键字主要用于: 1. 保证变量可见性... 2. 禁止指令重排序... """ ``` * 带参数的领域专家 ```python from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain.prompts import ( ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate ) # 定义系统消息模板 system_template = SystemMessagePromptTemplate.from_template( "你是一位专业的{domain}专家,回答需满足:{style_guide}" ) # 定义用户消息模板 human_template = HumanMessagePromptTemplate.from_template( "请解释:{concept}" ) # 组合聊天提示 chat_prompt = ChatPromptTemplate.from_messages([ system_template, human_template ]) # 格式化输出 messages = chat_prompt.format_messages( domain="机器学习", style_guide="使用比喻和示例说明", concept="过拟合" ) model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) response = model.invoke(messages) print(response) ``` * 合规客服系统 ```python from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) compliance_template = ChatPromptTemplate.from_messages([ ("system", """您是{company}客服助手,遵守: 1. 不透露内部系统名称 2. 不提供医疗/金融建议 3. 遇到{transfer_cond}转人工"""), ("human", "[{user_level}用户]:{query}") ]) messages = compliance_template.format_messages(company="小滴课堂老王医生", transfer_cond="病情咨询、支付问题", user_level="VIP", query="感冒应该吃什么药?") response = model.invoke(messages) print(response) ``` ### LCEL表达式和输出解析器多案例实战 #### LangChain链和LLMChain链案例实战 * 什么是Chain链 * 是构建语言模型应用的核心组件,用于将多个模块(如模型调用、提示模板、记忆系统等)组合成可复用的工作流程。 * 本质:将多个处理单元(模型/工具/逻辑)按特定顺序连接起来,形成完整的任务处理流程 * 想象Java中的责任链模式或工作流引擎中的步骤串联。 ```java // 传统Java责任链模式(对比理解) public interface Handler { void handle(Request request); } class ValidationHandler implements Handler { /* 验证逻辑 */ } class LLMProcessingHandler implements Handler { /* 大模型处理 */ } class DatabaseSaveHandler implements Handler { /* 存储结果 */ } // 构建处理链 List chain = Arrays.asList( new ValidationHandler(), new LLMProcessingHandler(), new DatabaseSaveHandler() ); // 执行链式处理 for (Handler handler : chain) { handler.handle(request); } ``` * 常见类说明 * LLMChain类详解(最基础的链,少用仍保留,推荐用 LCEL) * 是最基础的Chain,负责结合**提示模板**和**LLM模型**,生成并执行模型的调用流程 * 专门用于与大语言模型(如ChatGPT)交互的标准化处理单元 * 核心功能: * 提示模板:将用户输入动态填充到预设的模板中,生成模型的输入文本。 * 模型调用:将模板生成的文本传递给LLM,返回生成结果。 * 输出解析(可选):对模型输出进行后处理(如JSON解析) * 核心参数 | 参数 | 类比Java场景 | 示例值 | | :-----------: | :-------------------------------: | :-------------------------------------: | | llm | 依赖注入的模型对象 | new OpenAI() | | prompt | 预定义的提示词模板 | PromptTemplate("回答用户问题: {input}") | | output_parser | 结果解析器(类似Jackson处理JSON) | new CommaSeparatedListOutputParser() | * 代码示例 ```python from langchain_openai import ChatOpenAI from langchain.chains import LLMChain from langchain_core.prompts import PromptTemplate # 创建提示词模板(类似Java的String.format()) prompt_template = PromptTemplate( input_variables=["product"], template="你是文案高手,列举三个{product}的卖点:" ) # #模型 model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) # 创建LLMChain(类似构建一个Service类) llm_chain = LLMChain( llm=model, prompt=prompt_template ) # LLMChain 类在 LangChain 0.1.17 中已被弃用,建议使用 RunnableSequence(如 prompt | llm) llm_chain = prompt_template | model # 执行调用(类似service.execute(input)),run方法被淘汰了,统一invoke方法调用 result = llm_chain.invoke("智能手机") print(result) # 输出:"1. 高清OLED屏幕 2. 5000mAh大电池 3. 旗舰级处理器" ``` * 其他常见的Chain,比如 `SequentialChain、TransformChain、RouterChain、RetrievalChain` 部分已经淘汰 #### 新版LCEL表达式讲解和案例实战 * **什么是LCEL(LangChain Expression Language**) * 是 LangChain 0.3+ 推出的**声明式编程语言**,用于简化 AI 流程编排,核心思想是**用管道符 `|` 连接组件**。 * 优势: * 代码简洁,支持流式响应(如 ChatGPT 逐字输出) * 标准化接口, 所有组件输入为 `Dict`,输出为 `Dict`,支持无缝连接 * 所有组件实现 `Runnable` 协议, 兼容同步/异步调用 * 内置调试、重试、并行等高级功能 * 版本对比 | 特性 | 0.2 版本 | 0.3+ 版本 | | :----------: | :-------------------: | :-------------------------------: | | **构建方式** | 类继承 (`Chain` 子类) | LCEL 表达式语法 (`Runnable` 接口) | | **组合方法** | `SequentialChain` 类 | 管道操作符 ` | | **执行模式** | 同步为主 | 原生支持 Async / Stream | | **核心模块** | `langchain.chains` | `langchain_core.runnables` | * **语法与结构** - **LLMChain(旧版)** - 基于类的封装,通过组合`PromptTemplate`、`LLM`和`OutputParser`等组件构建链式调用。 - 需要显式定义每个组件并手动传递输入 - **适用场景** - 简单问答、单步生成任务(如生成公司名称、翻译句子)。 - 快速原型开发,无需复杂配置 ```python chain = LLMChain(llm=model, prompt=prompt) result = chain.invoke({"text": "Hello"}) ``` - **LCEL** - 采用声明式管道语法(`|`操作符),允许更直观的链式组合 - 通过管道连接组件,代码更简洁且逻辑清晰 - **适用场景** - 复杂工作流(如多模型协作、动态路由、实时流式交互)。 - 生产级应用开发,需高并发、稳定性和可观察性(如集成LangSmith跟踪) ```python chain = prompt | model | output_parser result = chain.invoke({"text": "Hello"}) ``` * LCEL 核心语法 * 作用:将左侧组件的输出传递给右侧组件作为输入,构建了类似Unix管道运算符的设计 * 语法规则:`chain = component_a | component_b | component_c ` * 案例实战:构建一个简单的问答链 ```python from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI from langchain_core.output_parsers import StrOutputParser # 定义组件 prompt = ChatPromptTemplate.from_template("回答这个问题:{question}") # #模型 model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) parser = StrOutputParser() # 构建 LCEL 链 chain = prompt | model | parser # 调用 result = chain.invoke({"question": "如何学习AI大模型?"}) print(result) ``` ```python # 旧版本的写法, 创建LLMChain(类似构建一个Service类) chain = LLMChain( llm=model, prompt=prompt_template ) # 执行调用(类似service.execute(input)),run方法被淘汰了,统一invoke方法调用 result = chain.invoke("智能手机") print(result) # 输出:"1. 高清OLED屏幕 2. 5000mAh大电池 3. 旗舰级处理器" ``` #### LLM大模型Stream流式输出实战 * Stream流式响应实战 * 前面我们的大模型输出都是一次性响应,这个容易造成体验不好 * 支持流式响应(Streaming Response),能够显著提升应用的响应速度和用户体验。 * 流式响应的核心在于逐步生成和返回数据,而不是等待整个结果生成后再一次性返回 * 案例实战:故事小助手 ```python from langchain_openai import ChatOpenAI # #模型 model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) for chunk in model.stream("讲一个翠花的故事。"): print(chunk.content, end="", flush=True) ``` * 案例实战:科普助手(采用LCEL表达式进行) ```python from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI from langchain_core.output_parsers import StrOutputParser # 1. 定义提示词模版 prompt = ChatPromptTemplate.from_template("用100字解释以下知识点:{concept}") # 2. 创建模型 model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7, streaming=True # 启用流式 ) # 使用 LCEL 语法 streaming_chain = prompt | model | StrOutputParser() # 2. 执行流式调用 for chunk in streaming_chain.stream({"concept": "小滴课堂"}): print(chunk, end="", flush=True) # 逐词输出 ``` * 流式响应的优势与限制 * 优势 * 提升用户体验:用户可以看到实时的生成过程,避免长时间等待。 * 节省内存:逐步生成和返回数据,避免一次性加载大量数据。 * 灵活性高:支持同步、异步和事件驱动的流式响应,适用于多种场景29。 * 限制 * 等待时间较长:涉及大语言模型的节点可能需要较长时间才能返回完整结果。 * 复杂性较高:事件驱动的流式响应需要精细的控制和管理 #### 输出解析器OutputParse实战和原理讲解 * 为什么需要输出解析OutputParse? * 大模型原始输出通常是**非结构化文本** * 实际需要: * 将大模型的自由文本转为结构化数据(类似Java的JSON/XML解析) * 自动处理模型输出的格式错误 ![image-20250301152117731](/img/image-20250301152117731.png) * 解析器工作原理 * `输入文本 → LLM 生成 → 解析器 → 结构化数据` * 工作原理: - 在提示模板中预留一个占位符变量,由输出解析器负责填充。 - 当 LLM 按照输出要求返回文本答案后,该答案会被传递给输出解析器,解析为预期的数据结构。 - **通俗来说:就是在prompt结尾追加,告诉大模型应该返回怎样的格式** * 解析器核心接口 * `parse()`:解析原始文本为结构化数据。 * `parse_with_prompt()`:结合上下文提示词解析(处理多轮对话场景)。 * `get_format_instructions()`:生成提示词模板,指导 LLM 输出格式。 * 文档地址:https://python.langchain.com/docs/concepts/output_parsers/ * 输出解析器基础结构三要素 ```python from langchain.output_parsers import XxxParser # 要素1:创建解析器(类似Java的Gson实例) parser = XxxParser() # 要素2:构建提示词模板(注意{format_instructions}占位符) prompt = PromptTemplate( template="请生成用户信息,按格式:{format_instructions}\n输入:{input}", input_variables=["input"], partial_variables={"format_instructions": parser.get_format_instructions()} ) # 要素3:组合成链(类似Java的责任链模式) chain = prompt | model | parser ``` * 测试查看 多个解析器prompt提示词 ```python from langchain_core.output_parsers import JsonOutputParser,CommaSeparatedListOutputParser from langchain_core.prompts import ChatPromptTemplate #parser = JsonOutputParser() parser = CommaSeparatedListOutputParser() # 获取格式指令 format_instructions = parser.get_format_instructions() # 定义提示词 prompt = ChatPromptTemplate.from_template(""" 分析以下商品评论,按指定格式返回结果: 评论内容:{review} 格式要求: {format_instructions} """) # 注入格式指令 final_prompt = prompt.partial(format_instructions=format_instructions) print(final_prompt.format_prompt(review="这个手机超级好用,超级流畅")) ``` * 案例实战 ```python from langchain_core.output_parsers import CommaSeparatedListOutputParser from langchain.prompts import PromptTemplate from langchain_openai import ChatOpenAI # 1. 实例化一个 CommaSeparatedListOutputParser对象,将逗号分隔文本转为列表 output_parser = CommaSeparatedListOutputParser() format_instructions = output_parser.get_format_instructions() # 2. 创建一个 prompt template,将 output_parser.get_format_instructions()追加到的prompt里面 prompt = PromptTemplate( template="列举多个常见的 {subject}.{format_instructions}", input_variables=["subject"], partial_variables={"format_instructions": format_instructions}, ) #3. 创建模型 model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) # 4. 构建链 chain = prompt | model | output_parser print(chain.invoke({"subject": "水果"})) ``` * !!!注意!!!! * 虽然有解析器,但是存在大模型返回内容不符合格式要求,则一样会报错 * 本质解析器还是Prompt提示词内容,如果有报错则可以让大模型重试 #### LangChain字符串和列表输出解析器实战 * 需求 * 课程讲常见的解析器,其他的可以自行拓展 * 掌握字符串和列表输出解析器 * 掌握解析器源码流程 * `StrOutputParser` * **功能**:将模型输出直接解析为字符串,保留原始文本输出,不做处理(默认行为) * **适用场景**:无需结构化处理,直接返回原始文本 ```python from langchain_core.output_parsers import StrOutputParser from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate # 定义链 prompt = ChatPromptTemplate.from_template("写一首关于{topic}的诗") #创建模型 model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) # 创建解析器 parser = StrOutputParser() chain = prompt | model | parser # 调用 result = chain.invoke({"topic": "秋天"}) print(result) # 输出:秋天的落叶轻轻飘落... ``` * `CommaSeparatedListOutputParser` * **功能**:将逗号分隔的文本解析为列表。 * **适用场景**:模型生成多个选项或标签。 ```python from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import CommaSeparatedListOutputParser from langchain_openai import ChatOpenAI parser = CommaSeparatedListOutputParser() prompt = ChatPromptTemplate.from_template("列出3个与{topic}相关的关键词:") #创建模型 model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) chain = prompt | model | parser # 调用 result = chain.invoke({"topic": "Java"}) print(result) # 输出:["机器学习", "深度学习", "神经网络"] ``` #### Json输出解析器和问答系统答案提取实战 * `JsonOutputParser` * **功能**:将模型输出解析为 JSON 对象。 * **适用场景**:需要模型返回结构化数据(如 API 响应),**多数要结合Pydantic进行使用** ```python from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI from langchain_core.output_parsers import JsonOutputParser prompt = ChatPromptTemplate.from_template( "返回JSON:{{'name': '姓名', 'age': 年龄}},输入:{input}" ) #创建模型 model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) chain = prompt | model | JsonOutputParser() result = chain.invoke({"input": "张三今年30岁,准备结婚后学AI大模型课程"}) print(result) # 输出:{"name": "张三", "age": 30} ``` * 案例实战:问答系统答案提取 * 目标:从模型回答中提取答案和置信度。 ```python from langchain_core.output_parsers import JsonOutputParser from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI # 定义 JSON 格式要求 prompt = ChatPromptTemplate.from_template(""" 回答以下问题,返回 JSON 格式: {{ "answer": "答案文本", "confidence": 置信度(0-1) }} 问题:{question} """) #创建模型 model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) parser = JsonOutputParser() chain = prompt | model | parser # 调用 result = chain.invoke({"question": "地球的半径是多少?"}) print(result) #{'answer': '地球的平均半径约为6,371公里。', 'confidence': 0.95} print(f"答案:{result['answer']},置信度:{result['confidence']}") #答案:地球的平均半径约为6,371公里。,置信度:0.95 ``` ### Pydantic模型和LLM高级解析器实战 #### Python模型管理Pydantic介绍和安装 * 什么是Pydatic * Pydantic 是一个在 Python 中用于数据验证和解析的第三方库,是 Python 使用最广泛的数据验证库 * 声明式的方式定义数据模型和,结合Python 类型提示的强大功能来执行数据验证和序列化 * Pydantic 提供了从各种数据格式(例如 JSON、字典)到模型实例的转换功能 * 官方文档:https://pydantic.com.cn/ * 为什么要用Pydantic * 处理来自系统外部的数据,如API、用户输入或其他来源时,必须牢记开发中的原则:“永远不要相信用户的输入” * AI智能体需要处理结构化数据(API请求/响应,配置文件等) * 必须对这些数据进行严格的检查和验证,确保它们被适当地格式化和标准化 * **解决的问题** - **数据验证**:自动验证输入数据的类型和格式 - **类型提示**:结合Python类型提示系统 - **序列化**:轻松转换数据为字典/JSON - **配置管理**:支持复杂配置项的验证 image-20250228151848080 * 对比Java开发的模型验证 * 典型Java方式(需手写校验逻辑) ```java public class User { private String name; private int age; // 需要手写校验方法 public void validate() throws IllegalArgumentException { if (name == null || name.isEmpty()) { throw new IllegalArgumentException("姓名不能为空"); } if (age > 150) { throw new IllegalArgumentException("年龄不合法"); } } } ``` * 传统Python方式(样板代码) ```python class User: def __init__(self, name: str, age: int): if not isinstance(name, str): raise TypeError("name必须是字符串") if not isinstance(age, int): raise TypeError("age必须是整数") if age > 150: raise ValueError("年龄必须在0-150之间") self.name = name self.age = age ``` * pydantic方式(声明式验证) ```python # 只需3行代码即可实现完整验证! from pydantic import BaseModel, Field class User(BaseModel): name: str = Field(min_length=1, max_length=50) # 内置字符串长度验证 age: int = Field(ge=0, le=150) # 数值范围验证(类似Java的@Min/@Max) ``` * 案例实战 * 模块安装 Pydantic V2(需要Python 3.10+) ```shell pip install pydantic==2.7.4 ``` * 使用 * Pydantic 的主要方法是创建继承自 BaseModel 的自定义类 ```python from pydantic import BaseModel # 类似Java中的POJO,但更强大 class UserProfile(BaseModel): username: str # 必须字段 age: int = 18 # 默认值 email: str | None = None # 可选字段 # 实例化验证 user1 = UserProfile(username="Alice") print(user1) # username='Alice' age=18 email=None user2 = UserProfile(username="Bob", age="20") # 自动类型转换 print(user2.age) # 20(int类型) ``` * 创建实例与校验 ```python try: UserProfile(username=123) # 触发验证错误 except ValueError as e: print(e.errors()) # [{ # 'type': 'string_type', # 'loc': ('username',), # 'msg': 'Input should be a valid string', # 'input': 123 # }] ``` * 字段类型验证 ```python from pydantic import BaseModel,HttpUrl,ValidationError class WebSite(BaseModel): url: HttpUrl #URL格式验证 visits: int = 0 #默认值 tags: list[str] = [] #字符串列表 valid_data = { "url": "https://www.baidu.com", "visits": 100, "tags": ["python", "fastapi"] } # try: # website = WebSite(**valid_data) # print(website) # except ValidationError as e: # print(e.errors()) try: website = WebSite(url="xdclass.net",visits=100) print(website) except ValidationError as e: print(e.errors()) ``` * 数据解析/序列化 ```python from pydantic import BaseModel class Item(BaseModel): name: str price: float # 从JSON自动解析(类似Jackson) data = '{"name": "Widget", "price": "9.99"}' # 字符串数字自动转换 item = Item.model_validate_json(data) # 导出为字典(类似Java的POJO转Map) print(item.model_dump()) # {'name': 'Widget', 'price': 9.99} ``` * 调试技巧:打印模型结构 ```python print(Website.model_json_schema()) # 输出完整的JSON Schem ``` #### Pydantic字段校验Field函数多案例实战 * Filed函数讲解 * Field函数通常用于给模型字段添加额外的元数据或者验证条件。 * 例如,title参数用来设置字段的标题,min_length用来限制最小长度。 * 核心 * **`...` 的本质**:表示“无默认值”,强制字段必填。 * **`Field` 的常用参数**: - `title`:字段标题(用于文档) - `description`:详细描述 - `min_length`/`max_length`(字符串、列表) - `gt`/`ge`/`lt`/`le`(数值范围) - `regex`(正则表达式) - `example`(示例值,常用于 API 文档) * 案例实战 * 必填字段(无默认值) * 必填字段: * ... 表示该字段必须显式提供值。若创建模型实例时未传入此字段,Pydantic 会抛出验证错误(ValidationError)。 * 默认值占位 * Field 的第一个参数是 default,而 ... 在此处的语义等价于“无默认值”。 * 若省略 default 参数(如 Field(title="用户名")),Pydantic 会隐式使用 ...,但显式写出更明确 ```python from pydantic import BaseModel, Field class User(BaseModel): name: str = Field(..., title="用户名", min_length=2) # 正确用法 user = User(name="Alice") # 错误用法:缺少 name 字段 user = User() # 触发 ValidationError ``` * 可选字段(有默认值) ```python from pydantic import BaseModel, Field class UserOptional(BaseModel): name: str = Field("Guest", title="用户名") # 默认值为 "Guest" # 可不传 name,自动使用默认值 user = UserOptional() print(user.name) # 输出 "Guest" ``` * 以下两种写法完全等价, 使用 `Field` 的优势在于可以附加额外参数(如 `title`、`min_length`、`description` 等)。 ```python # 写法 1:省略 Field,直接类型注解 name: str # 写法 2:显式使用 Field(...) name: str = Field(...) ``` * 数值类型必填 ```python from pydantic import BaseModel, Field, ValidationError class Product(BaseModel): price: float = Field(..., title="价格", gt=0) # 必须 > 0 stock: int = Field(..., ge=0) # 必须 >= 0 # 正确 product = Product(price=99.9, stock=10) # 错误:price <= 0 try: Product(price=-5, stock=10) except ValidationError as e: print(e.json()) # 提示 "price" 必须大于 0 ``` * 嵌套模型必填 ```python from pydantic import BaseModel, Field class Address(BaseModel): city: str = Field(..., min_length=1) street: str class User(BaseModel): name: str = Field(...) address: Address # 等效于 address: Address = Field(...) # 正确 user = User(name="Alice", address={"city": "Shanghai", "street": "Main St"}) # 错误:缺少 address user = User(name="Bob") # 触发 ValidationError ``` * 明确可选字段 ```python from pydantic import BaseModel, Field from typing import Optional class User(BaseModel): name: str = Field(...) email: Optional[str] = Field(None, title="邮箱") # 可选,默认值为 None # 正确:不传 email user = User(name="Alice") ``` * 混合使用默认值和必填 ```python from pydantic import BaseModel, Field class Config(BaseModel): api_key: str = Field(...) # 必填 timeout: int = Field(10, ge=1) # 可选,默认 10,但必须 >=1 # 正确 config = Config(api_key="secret") assert config.timeout == 10 # 错误:未传 api_key Config(timeout=5) # 触发 ValidationError ``` #### Pydantic自定义验证器多案例实战 * `@field_validator`介绍 * 是 Pydantic 中用于为**单个字段**添加自定义验证逻辑的装饰器 * **适用场景**:当默认验证规则(如 `min_length`、`gt`)无法满足需求时,通过编写代码实现复杂校验。 * **触发时机**:默认在字段通过基础类型和规则校验后执行(可通过 `mode` 参数调整) * 基础语法 ```python from pydantic import BaseModel, ValidationError, field_validator class User(BaseModel): username: str # 带默认值的可选字段 # int | None表示 age 变量的类型可以是整数 (int) 或 None,旧版本的写法:age: Union[int, None] # Python 3.10 开始引入,替代了早期通过 Union[int, None] 的形式(仍兼容) age: int | None = Field( default=None, ge=18, description="用户年龄必须≥18岁" ) @field_validator("username") def validate_username(cls, value: str) -> str: # cls: 模型类(可访问其他字段) # value: 当前字段的值 if len(value) < 3: raise ValueError("用户名至少 3 个字符") return value # 可修改返回值(如格式化) ``` * 案例实战 * 字符串格式校验 ```python from pydantic import BaseModel, Field,field_validator class User(BaseModel): email: str @field_validator("email") def validate_email(cls, v): if "@" not in v: raise ValueError("邮箱格式无效") return v.lower() # 返回格式化后的值 # 正确 User(email="ALICE@example.com") # 自动转为小写:alice@example.com # 错误 #User(email="invalid") # 触发 ValueError ``` * 验证用户名和长度 ```python from pydantic import BaseModel, Field,field_validator class User(BaseModel): username: str = Field(..., min_length=3) @field_validator("username") def validate_username(cls, v): if "admin" in v: raise ValueError("用户名不能包含 'admin'") return v # 正确 User(username="alice123") # 错误 #User(username="admin") # 触发自定义验证错误 ``` * 密码复杂性验证 ```python from pydantic import BaseModel, Field,field_validator class Account(BaseModel): password: str @field_validator("password") def validate_password(cls, v): errors = [] if len(v) < 8: errors.append("至少 8 个字符") if not any(c.isupper() for c in v): errors.append("至少一个大写字母") if errors: raise ValueError("; ".join(errors)) return v # 错误:密码不符合规则 Account(password="weak") # 提示:至少 8 个字符; 至少一个大写字母 ``` * 多个字段共享验证器 ```python from pydantic import BaseModel, Field,field_validator class Product(BaseModel): price: float cost: float @field_validator("price", "cost") def check_positive(cls, v): if v <= 0: raise ValueError("必须大于 0") return v # 同时验证 price 和 cost 是否为正数 Product(price=1, cost=-2) ``` * 注意事项 * 忘记返回值:验证器必须返回字段的值(除非明确要修改)。 ```python @field_validator("email") def validate_email(cls, v): if "@" not in v: raise ValueError("Invalid email") # ❌ 错误:未返回 v ``` * **Pydantic V2 现为 Pydantic 的当前生产发布版本** * 网上不少是V1版本的教程,需要注意 * Pydantic V1和Pydantic V2的API差异 | Pydantic V1 | Pydantic V2 | | :----------------------- | :----------------------- | | `__fields__` | `model_fields` | | `__private_attributes__` | `__pydantic_private__` | | `__validators__` | `__pydantic_validator__` | | `construct()` | `model_construct()` | | `copy()` | `model_copy()` | | `dict()` | `model_dump()` | | `json_schema()` | `model_json_schema()` | | `json()` | `model_dump_json()` | | `parse_obj()` | `model_validate()` | | `update_forward_refs()` | `model_rebuild()` | #### 重点-解析器PydanticOutputParser实战 * 为啥要用为什么需要Pydantic解析? * 结构化输出:将非结构化文本转为可编程对象 * 数据验证:自动验证字段类型和约束条件,单纯json解析器则不会校验 * 开发效率:减少手动解析代码 * 错误处理:内置异常捕获与修复机制 * **案例实战一:大模型信息输出提取( `PydanticOutputParser` 结合Pydantic模型验证输出)** ```python from langchain_openai import ChatOpenAI from pydantic import BaseModel, Field from langchain_core.output_parsers import PydanticOutputParser #定义模型 model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) # Step1: 定义Pydantic模型 class UserInfo(BaseModel): name: str = Field(description="用户姓名") age: int = Field(description="用户年龄", gt=0) hobbies: list[str] = Field(description="兴趣爱好列表") # Step2: 创建解析器 parser = PydanticOutputParser(pydantic_object=UserInfo) # Step3: 构建提示模板 from langchain_core.prompts import ChatPromptTemplate prompt = ChatPromptTemplate.from_template(""" 提取用户信息,严格按格式输出: {format_instructions} 输入内容: {input} """) # 注入格式指令 prompt = prompt.partial( format_instructions=parser.get_format_instructions() ) # Step4: 组合处理链 chain = prompt | model | parser # 执行解析 result = chain.invoke({ "input": """ 我的名称是张三,年龄是18岁,兴趣爱好有打篮球、看电影。 """ }) print(type(result)) print(result) ``` * **案例实战二:电商评论情感分析系统(JsonOutputParser和pydantic结合)** * `JsonOutputParser`与`PydanticOutputParser`类似 * 新版才支持从pydantic获取约束模型,该参数并非强制要求,而是可选的增强功能 * `JsonOutputParser`可以处理流式返回的部分JSON对象。 ```python from langchain_openai import ChatOpenAI from pydantic import BaseModel, Field from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import JsonOutputParser #定义模型 model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) # 定义JSON结构 class SentimentResult(BaseModel): sentiment: str confidence: float keywords: list[str] # 构建处理链 parser = JsonOutputParser(pydantic_object=SentimentResult) prompt = ChatPromptTemplate.from_template(""" 分析评论情感: {input} 按以下JSON格式返回: {format_instructions} """).partial(format_instructions=parser.get_format_instructions()) chain = prompt | model | parser # 执行分析 result = chain.invoke({"input": "物流很慢,包装破损严重"}) print(result) # 输出: # { # "sentiment": "negative", # "confidence": 0.85, # "keywords": ["物流快", "包装破损"] # } # 2. 执行流式调用 #for chunk in chain.stream({"input": "物流很慢,包装破损严重"}): # print(chunk) # 逐词输出 ``` #### 重点-大模型修复机制OutputFixingParser * `OutputFixingParser` * 是LangChain中用于修复语言模型(LLM)输出格式错误的工具,通常与`PydanticOutputParser`配合使用。 * 当原始解析器因格式问题(如JSON语法错误、字段缺失等)失败时,它能自动调用LLM修正输出,提升解析的鲁棒性。 * 核心功能: * 自动纠错:修复不规范的输出格式(如单引号JSON、字段顺序错误等)。 * 兼容性:与Pydantic数据模型无缝集成,支持结构化输出验证。 * 容错机制:避免因模型输出不稳定导致程序中断 export_4232z * 核心语法与使用步骤 * 基础语法 ```python from langchain.output_parsers import OutputFixingParser, PydanticOutputParser from langchain_openai import ChatOpenAI # 步骤1:定义Pydantic数据模型 class MyModel(BaseModel): field1: str = Field(description="字段描述") field2: int # 步骤2:创建原始解析器 parser = PydanticOutputParser(pydantic_object=MyModel) #定义模型 model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) # 步骤3:包装为OutputFixingParser fixing_parser = OutputFixingParser.from_llm(parser=parser, llm=model) ``` * 参数说明: * parser: 原始解析器对象(如PydanticOutputParser)。 * llm: 用于修复错误的语言模型实例。 * max_retries(可选): 最大重试次数(默认1) * 案例实战 * 修复机制 * 检测到错误后,将错误信息与原始输入传递给LLM。 * LLM根据提示生成符合Pydantic模型的修正结果。 ```python from langchain.output_parsers import OutputFixingParser from langchain_core.output_parsers import PydanticOutputParser from langchain_openai import ChatOpenAI from pydantic import BaseModel, Field from typing import List class Actor(BaseModel): name: str = Field(description="演员姓名") film_names: List[str] = Field(description="参演电影列表") parser = PydanticOutputParser(pydantic_object=Actor) #定义模型 model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) # 包装原始解析器 fixing_parser = OutputFixingParser.from_llm(parser=parser, llm=model) # 模拟模型输出的错误格式(使用单引号) misformatted_output = "{'name': '小滴课堂老王', 'film_names': ['A计划','架构大课','一路向西']}" #在LLM 链中,chain.invoke会将 LLM 返回的文本字符串传入output_parser.invoke #而`output_parser.invoke`最终会调用到`output_parser.parse`。 # try: # parsed_data = parser.parse(misformatted_output) # 直接解析会失败 # except Exception as e: # print(f"解析失败:{e}") # 抛出JSONDecodeError # 使用OutputFixingParser修复并解析 fixed_data = fixing_parser.parse(misformatted_output) print(type(fixed_data)) print(fixed_data.model_dump()) # 输出:{'name': '小滴课堂老王', 'film_names': ['A计划','架构大课','一路向西']} ``` * 完整正常案例 ```python from langchain.output_parsers import OutputFixingParser from langchain_core.output_parsers import PydanticOutputParser from langchain_openai import ChatOpenAI from pydantic import BaseModel, Field from langchain_core.prompts import PromptTemplate from typing import List class Actor(BaseModel): name: str = Field(description="演员姓名") film_names: List[str] = Field(description="参演电影列表") parser = PydanticOutputParser(pydantic_object=Actor) #定义模型 model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) prompt = PromptTemplate( template="{format_instructions}\n{query}", input_variables=["query"], partial_variables={"format_instructions": parser.get_format_instructions()}, ) # 包装原始解析器 fixing_parser = OutputFixingParser.from_llm(parser=parser, llm=model) chain = prompt | model | fixing_parser response = chain.invoke({"query": "说下成龙出演过的5部动作电影? "}) print(response) print(type(response)) print(response.model_dump()) ``` * 常见问题与解决方案 * 修复失败的可能原因 * 模型能力不足:升级LLM版本(如使用更高级的模型和参数量)。 * 提示词不清晰:在提示模板中明确格式要求。 * 网络问题:通过代理服务优化API访问 ### AI大模型必备之RAG和智能医生实战 #### 什么是大模型的幻觉输出 * “幻觉输出”(Hallucination) * 是大语言模型(如GPT、Llama、DeepSeek等)生成内容时的一种常见问题 * 指模型输出看似合理但实际错误、虚构或脱离事实的信息。 * 这种现象类似于人类的“臆想”——模型基于不完整或错误的知识,生成逻辑通顺但内容失实的回答 * 表现形式 * 虚构事实 * 例1:生成不存在的书籍(如称《时间简史》是鲁迅所写)。 * 例2:编造错误的历史事件(如“秦始皇于公元前200年统一六国”)。 * 错误推理 * 例:回答数学问题时,步骤正确但结果错误(如“2+3=6”)。 * 过度泛化 * 例:将特定领域的知识错误迁移到其他领域(如用医学术语解释物理现象)。 * 矛盾内容 * 例:同一段回答中前后逻辑冲突(如先说“地球是平的”,后又说“地球绕太阳公转”) * 幻觉产生的根本原因 * **训练数据的局限性** - **数据噪声**:模型训练数据可能包含错误、过时或偏见信息(如互联网上的谣言)。 - **知识截止**:模型训练后无法获取新知识(如GPT-3的数据截止到2021年)。 * **模型生成机制** - **概率驱动**:模型通过预测“最可能的下一词”生成文本,而非验证事实。 - **缺乏常识判断**:无法区分“合理表达”与“真实事实”。 * **模型策略的副作用** - **创造性模式**:模型在开放生成任务中更易“放飞自我”(如写小说时虚构细节) * 典型案例分析 - **案例1:医疗问答** - **问题**:用户问“新冠疫苗会导致自闭症吗?” - **幻觉输出**:模型可能生成“有研究表明两者存在相关性”(错误)。 - **解决方案**:RAG检索WHO官方声明,生成“无科学证据支持此说法”。 - **案例2:金融咨询** - **问题**:“2024年比特币会涨到10万美元吗?” - **幻觉输出**:模型虚构专家预测或历史数据。 - **解决方案**:限定回答范围(如“截至2023年,比特币最高价格为…”) * 产生的影响 * **误导用户**:在医疗、法律等专业领域可能引发严重后果。 * **信任危机**:用户对模型输出的可靠性产生质疑。 * **技术瓶颈**:暴露大模型在事实性、可解释性上的不足。 * 如何缓解幻觉输出(注意:不是解决) * **技术改进方案** - **检索增强生成(RAG)**:通过实时检索外部知识库(如维基百科、专业数据库),为生成提供事实依据。 - **微调对齐**:用高质量数据(如标注正确的问答对)调整模型输出偏好。 - **强化学习(RLHF)**:通过人类反馈惩罚错误生成,奖励准确回答。 * **生成策略优化** - **温度参数调整**:降低随机性(`temperature=0`),减少“胡编乱造”。 - **后处理校验**:添加事实核查模块(如调用知识图谱API验证答案)。 * **用户侧应对** - **提示词设计**:明确要求模型标注不确定性(如“回答需基于2023年数据”)。 - **多源验证**:对关键信息人工交叉核对(如学术论文、权威网站)。 #### 带你走进RAG检索增强生成和应用场景 **简介: 带你走进RAG检索增强生成和应用场景** * 什么是RAG技术 * RAG(Retrieval-Augmented Generation)检索增强生成,是结合信息检索与文本生成的AI技术架构。 * 核心思想: * 先通过检索系统找到与问题相关的知识片段 * 再将检索结果与问题共同输入生成模型得到最终答案 * 类比人类解答问题的过程:遇到问题时先查资料(检索),再结合资料组织回答(生成) ![2](/img/2-1580890.png) * 用Java伪代码描述RAG工作流程: ```java public class JavaRAG { public static void main(String[] args) { // 1. 加载文档 List docs = FileLoader.load("data/"); // 2. 创建向量库(类似建立索引) VectorDB vectorDB = new FAISSIndex(docs); // 3. 处理用户问题 String question = "如何申请报销?"; List results = vectorDB.search(question, 3); // 4. 生成回答 String context = String.join("\n", results.stream() .map(Document::content) .collect(Collectors.toList())); String answer = LLM.generate(context + "\n问题:" + question); System.out.println(answer); } } ``` * 需求背景:为什么需要RAG? * 传统生成模型的局限性 * 知识过时:大模型(如GPT-3)训练数据截止于特定时间,无法覆盖实时信息。 * 幻觉问题:生成内容可能包含不准确或虚构的信息。 * 领域局限性:通用模型缺乏垂直领域的专业知识(如法律、医疗),企业私有数据无法写入公开模型 | 题类型 | 示例 | 传统模型表现 | | :----------------: | :------------------------: | :----------------------------------: | | 时效性问题 | "2027年诺贝尔奖得主是谁?" | 无法回答(训练数据截止到当时的数据) | | 领域专业问题 | "如何配置Hadoop集群参数?" | 回答模糊或错误 | | 需要引用来源的问题 | "不睡觉有哪些副作用?" | 无法提供可信出处 | * 检索与生成的互补性 * 检索系统:擅长从海量数据中快速找到相关文档(实时、精准),实时从外部知识库检索信息 * 生成模型:擅长语言理解和流畅输出。 * 结合优势:RAG通过检索外部知识库增强生成结果,提高准确性和可信度 * 降低训练成本:无需重新训练模型即可更新知识 * 生成可验证性:每句回答都可追溯来源文档 * 技术架构 1 * **涉及的技术链路环节: 文档加载器->文档转换器->文本嵌入模型->向量存储->检索器** * 关键技术组件 | 组件 | 常用工具 | Java类比 | | :--------: | :-----------------------: | :------------------: | | 文档加载器 | PyPDFLoader, Unstructured | FileInputStream | | 文本分块器 | RecursiveTextSplitter | String.split()增强版 | | 元数据处理 | LangChain Document类 | DTO对象封装 | | 向量存储 | FAISS, Pinecone | 数据库索引 | * 典型应用场景 * 案例1:智能客服系统 * 传统问题:客服知识库更新频繁,模型无法实时同步。 * RAG方案: * 实时检索最新的产品文档、FAQ。 * 生成个性化回答(如退货政策、故障排查)。 ```python 用户问:"这个小滴手机支持老人家使用不?" 系统检索:产品适合人群相关词条 生成回答:"我们这个产品适合18岁以上的成人使用,包括中老年人等" ``` * 效果:减少人工干预,回答准确率提升30%+。 * 案例2:医疗问答助手 * 传统问题:通用模型缺乏专业医学知识,可能给出危险建议。 * RAG方案: * 检索权威医学数据库(如PubMed、临床指南)。 * 生成基于循证医学的答案,标注参考文献来源。 ```python 用户问:"二甲双胍的禁忌症有哪些?" 系统检索:最新《临床用药指南》第5.3节 生成回答:"根据2023版用药指南,二甲双胍禁用于以下情况:1)严重肾功能不全..." ``` * 效果:合规性提升,避免法律风险。 * 案例3:金融研究报告生成 * 传统问题:市场数据动态变化,模型无法实时分析。 * RAG方案: * 检索实时财报、新闻、行业数据 → 输入生成模型。 * 自动生成带有数据支撑的投资建议。 ```python 用户问:"XXX公司财报如何" 系统检索:某某公司财报 生成回答:"根据公司的财报和解读,利润和负债..." ``` * 效果:析师效率提升,报告更新频率加快 #### LLM智能AI医生+RAG系统案例实战 * 需求 * 快速搭建智能医生客服案例,基于LLM大模型+RAG技术 * 方便大家可以直观的看相关效果,并方便后续拆解每个步骤 * 效果:可以根据用户的提问,检索相关私有数据,整合大模型,最终生成返回给用户 * 案例实战【本集不提供课程代码,请从下面地址下载】 * 创建项目和安装相关环境 ```python # 创建并激活虚拟环境 python -m venv .venv source .venv/bin/activate # 安装依赖 pip install -r requirements.txt ``` * 查看训练的文档数据 * 项目部署运行 (**版本和课程保持一致,不然很多不兼容!!!**) * 下载相关资料 ,使用**【wget】或者【浏览器】远程下载相关依赖包(需要替换群里最新的)** ```python 原生资料下载方式(账号 - 密码 - ip地址 - 端口 需要替换群里最新的,【其他路径不变】) wget --http-user=用户名 --http-password=密码 http://ip:端口/dcloud_pan/aipan_xd-rag.zip #比如 命令行下 wget --http-user=admin --http-password=xdclass.net888 http://47.115.31.28:9088/dcloud_pan/aipan_xd-rag.zip # 比如 浏览器直接访问 http://47.115.31.28:9088/dcloud_pan/aipan_xd-rag.zip ``` * 解压后执行【**依赖很多,版本差异大,务必按照下面执行,否则课程无法进行下去,加我微信 xdclass6**】 ``` # 安装依赖 pip install -r requirements.txt ``` * 效果测试 ![image-20250314122237372](/img/image-20250314122237372.png) * 多数同学的问题 * **为啥可以根据我们的提问,进行检索到对应的词条?而且还可以正确检索?** * **为啥要加载文件?然后切割?什么是向量数据库?** * **为啥检索到词条后,还可以用调整输出内容,更加友好?** * **什么是嵌入大模型?和前面学的LLM大模型有啥区别?** ### RAG 检索增强生成之Loader实战 #### RAG系统链路和数据加载Loaders技术 * RAG系统与LLM交互架构图 * 注意 * 万丈高楼平地起,基础需要打牢固,一步步进行,然后学会举一反三使用 * 如果直接讲Agent智能体项目,那项目涉及到的很多技术就懵逼了,要学会思路 ![0](/img/0.png) * **涉及的技术链路环节: 文档加载器->文档转换器->文本嵌入模型->向量存储->检索器** * RAG数据流水线示意图 ``` 原始数据 → 数据加载 → 预处理 → 向量化 → 存储 → 检索增强生成 ↗ ↗ ↗ PDF 文本清洗 嵌入模型 数据库 分块 网页 ``` ![data_connection_diagram](/img/data_connection-95ff2033a8faa5f3ba41376c0f6dd32a.jpg) * 文档加载器 (Document Loaders) * 外部数据多样性,包括在线,本地 或者数据库等来源 * 将不同来源的原始数据(如PDF、网页、JSON、、HTML、数据库等)转换为统一格式的文档对象,便于后续处理。 * **核心任务**:数据源适配与初步结构化 * LangChain里面的Loader * 接口文档地址【如果失效就忽略):https://python.langchain.com/docs/integrations/document_loaders/ ``` from langchain_community.document_loaders import ( TextLoader, #文本加载 PyPDFLoader, # PDF Docx2txtLoader, # Word UnstructuredHTMLLoader, # HTML CSVLoader, # CSV JSONLoader, # JSON SeleniumURLLoader, # 动态网页 WebBaseLoader #网页加载 ) ``` * LangChain 设计了一个统一的接口`BaseLoader`来加载和解析文档, ```python class BaseLoader(ABC): # noqa: B024 """Interface for Document Loader. Implementations should implement the lazy-loading method using generators to avoid loading all Documents into memory at once. `load` is provided just for user convenience and should not be overridden. """ # Sub-classes should not implement this method directly. Instead, they # should implement the lazy load method. def load(self) -> list[Document]: """Load data into Document objects.""" return list(self.lazy_load()) ``` * 将原始数据(如文件、API 响应、文本文件、网页、数据库等)转换为 LangChain 的 `Document` 对象 * `load`方法返回一个`Document`数组, 每个 `Document` 包含 * **`page_content`**: 文本内容 * **`metadata`**: 元数据(如来源、创建时间、作者等) ```python class Document(BaseMedia): """Class for storing a piece of text and associated metadata. Example: .. code-block:: python from langchain_core.documents import Document document = Document( page_content="Hello, world!", metadata={"source": "https://example.com"} ) """ page_content: str """String text.""" type: Literal["Document"] = "Document" ``` * Loader 的分类与常见类型 * 文件加载器(File Loaders) | Loader 类型 | 功能描述 | | :----------------------: | :----------------------------------: | | `TextLoader` | 加载纯文本文件(.txt) | | `CSVLoader` | 解析 CSV 文件,按行生成 Document | | `PyPDFLoader` | 提取 PDF 文本及元数据(基于 PyPDF2) | | `Docx2txtLoader` | 读取 Word 文档(.docx) | | `UnstructuredFileLoader` | 通用文件解析(支持多种格式) | * 网页加载器(Web Loaders) | Loader 类型 | 功能描述 | | :--------------: | :----------------------------: | | `WebBaseLoader` | 抓取网页文本内容 | | `SeleniumLoader` | 处理需要 JavaScript 渲染的页面 | * 数据库加载器(Database Loaders) | Loader 类型 | 功能描述 | | :-----------------: | :---------------------: | | `SQLDatabaseLoader` | 执行 SQL 查询并加载结果 | | `MongoDBLoader` | 从 MongoDB 中读取数据 | * 其他加载器 (自定义) ... #### 文档加载器Loaders技术多案例实战 * TextLoader - 加载纯文本文件 * ##### **通用参数** - **`encoding`**: 文件编码(默认 `utf-8`) - **`autodetect_encoding`**: 自动检测编码(如处理中文乱码) ```python from langchain_community.document_loaders import TextLoader # 文本加载 loader = TextLoader("data/test.txt") documents = loader.load() print(documents) print(len(documents)) #长度 print(documents[0].page_content[:100]) # 打印前100个字符 print(documents[0].metadata) # 输出: {'source': 'data/test.txt'} ``` * CSVLoader - 加载 CSV 文件 * 基础案例代码 ```python from langchain_community.document_loaders import CSVLoader loader = CSVLoader("data/test.csv", csv_args={"delimiter": ","}) documents = loader.load() # 每行转换为一个 Document,metadata 包含行号 print(len(documents)) print(documents[0].metadata) # 输出: {'source': 'data.csv', 'row': 0} print(documents[0].page_content) ``` * 可指定列名,按行生成文档 ```python from langchain_community.document_loaders import CSVLoader #loader = CSVLoader("data/test.csv", csv_args={"delimiter": ","}) loader = CSVLoader("data/test.csv", csv_args={"fieldnames": ["产品名称", "销售数量", "客户名称"]}) documents = loader.load() # 每行转换为一个 Document,metadata 包含行号 print(len(documents)) print(documents[1].metadata) # 输出: {'source': 'data.csv', 'row': 0} print(documents[1].page_content) ``` * JSONLoader - 加载 JSON 文件 * 核心参数详解 | 参数 | 类型 | 必填 | 说明 | | :-------------- | :------- | :--- | :----------------------------------------------- | | `file_path` | str | ✅ | JSON 文件路径 | | `jq_schema` | str | ✅ | jq 查询语法,定义数据提取逻辑 | | `content_key` | str | ❌ | 指定作为文本内容的字段(默认直接使用提取到的值) | | `metadata_func` | Callable | ❌ | 自定义元数据处理函数 | | `text_content` | bool | ❌ | 是否将提取内容强制转为字符串(默认 True) | * 必选参数 `jq_schema` * 必须使用 `jq_schema` 语法指定数据提取路径 * 支持更复杂的 JSON 结构解析 * jq 语法常用模式 | 场景 | jq_schema 示例 | 说明 | | :----------- | :----------------------------------- | :--------------------------- | | 提取根级数组 | `.[]` | 适用于 JSON 文件本身是数组 | | 嵌套对象提取 | `.data.posts[].content` | 提取 data.posts 下的 content | | 条件过滤 | `.users[] | select(.age > 18)` | 筛选年龄大于18的用户 | | 多字段合并 | `{name: .username, email: .contact}` | 组合多个字段为对象 | * 案例实战 * 安装依赖包 ``` pip install jq ``` * 编码实战 ```python from langchain_community.document_loaders import JSONLoader loader = JSONLoader( file_path="data/test.json", jq_schema=".articles[]", # 提取 articles 数组中的每个元素 content_key="content" # 指定 content 字段作为文本内容 ) docs = loader.load() print(len(docs)) print(docs[0] ``` #### PDF文档加载器实战和常见问题处理 * `PyPDFLoader` 加载PDF文件 * `PyPDFLoader` 是 LangChain 中专门用于加载和解析 **PDF 文件** 的文档加载器。 * 它能将 PDF 按页拆分为多个 `Document` 对象,每个对象包含页面文本和元数据(如页码、来源路径等)。 * 适用于处理多页PDF文档的文本提取任务。 * 使用步骤 * 安装依赖库 ```python pip install pypdf ``` * 案例代码实战 ```python from langchain_community.document_loaders import PyPDFLoader # PDF加载 loader = PyPDFLoader("data/test.pdf") # 加载文档并按页分割 pages = loader.load() # 返回 Document 对象列表 # 查看页数 print(f"总页数: {len(pages)}") # 访问第一页内容 page_content = pages[0].page_content metadata = pages[0].metadata print(f"第一页内容:\n{page_content[:200]}...") # 预览前200字符 print(f"元数据: {metadata}") ``` * 按需加载, 通过 `load()` 方法的参数控制加载范围: ```python # 加载指定页码范围(例如第2页到第4页) pages = loader.load([1, 2, 3]) # 注意页码从0开始(第1页对应索引0) ``` * 提取所有文本合并为单个文档, 若需将全部页面内容合并为一个字符串: ```python full_text = "\n\n".join([page.page_content for page in pages]) print(f"合并后的全文长度: {len(full_text)} 字符") ``` * 常见问题与解决方案 * PDF无法加载或内容为空 * 原因:PDF为扫描版图片或加密。 * 解决: * 使用OCR工具(如pytesseract+pdf2image)提取图片文本。 * 解密PDF后加载(需密码时,PyPDFLoader暂不支持直接解密) * 文本分块不理想 * 调整分块策略:选择合适的分隔符或分块大小 ```python text_splitter = RecursiveCharacterTextSplitter( separators=["\n\n", "\n", "."], # 按段落、句子分割 chunk_size=500, chunk_overlap=100 ) ``` * 高级技巧 * **批量处理PDF**:遍历文件夹内所有PDF文件。 ```python import os pdf_folder = "docs/" all_pages = [] for filename in os.listdir(pdf_folder): if filename.endswith(".pdf"): loader = PyPDFLoader(os.path.join(pdf_folder, filename)) all_pages.extend(loader.load()) ``` #### Loader进阶-PDF文档里面的图片提取解析 * 如何提取PDF里面的图片文案? * `PyPDFLoader` 仅提取文本,如果没配置第三方类库则会提取不了对应的图片文案 * 需结合其他库(如`camelot`、`pdfplumber`、`rapidocr-onnxruntime`)提取表格或图像。 * 如果需要提取,安装好依赖库后,设置`extract_images`参数为`True`。 * `RapidOCR-ONNXRuntime `介绍 * 是一个基于 ONNX Runtime 推理引擎的轻量级 OCR(光学字符识别)工具库,专注于高效、跨平台部署。 * 它是 [RapidOCR](https://github.com/RapidAI/RapidOCR) 项目的一个分支,实现了更高的推理速度和更低的资源占用 * 特点: * 跨平台支持:支持 Windows、Linux、macOS,以及移动端(Android/iOS)和嵌入式设备。 * 多语言识别:支持中文、英文、日文、韩文等多种语言,尤其擅长中英混合文本。 * 轻量级:模型体积小(约几 MB),适合资源受限的环境。 * 预处理与后处理集成:内置图像预处理(如二值化、方向校正)和文本后处理(如去除冗余字符)。 * RapidOCR-ONNXRuntime 与其他主流 OCR 工具的对比: | 工具 | 引擎 | 速度 | 准确率 | 语言支持 | 依赖项 | 适用场景 | | :----------------------- | :----------- | :--- | :----- | :------- | :----- | :--------------------- | | **RapidOCR-ONNXRuntime** | ONNX Runtime | ⭐⭐⭐⭐ | ⭐⭐⭐ | 多语言 | 少 | 跨平台、轻量级部署 | | **Tesseract** | 自研引擎 | ⭐⭐ | ⭐⭐ | 多语言 | 多 | 历史项目、简单场景 | | **EasyOCR** | PyTorch | ⭐⭐ | ⭐⭐⭐ | 多语言 | 多 | 快速原型开发 | | **Microsoft Read API** | 云端服务 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 多语言 | 无 | 企业级、高并发云端需求 | * 案例实战 * 安装依赖包 (**耗时会有点久**) ``` pip install rapidocr-onnxruntime ``` * 代码实战 ```python from langchain_community.document_loaders import PyPDFLoader loader = PyPDFLoader("data/pdf-img.pdf", extract_images=True) pages = loader.load() print(pages[0].page_content) ``` #### 网页加载器WebBaseLoader案例实战 **简介: Web网页加载器WebBaseLoader案例实战** * 什么是WebBaseLoader * `WebBaseLoader` 是 LangChain 中用于抓取 **静态网页内容** 的文档加载器。 * 通过 HTTP 请求直接获取网页 HTML,并提取其中的文本内容(自动清理标签、脚本等非文本元素) * 生成包含网页文本和元数据的 `Document` 对象 * 适用于新闻文章、博客、文档页面等静态内容的快速提取。 * 场景 * 知识库构建(知识问答、企业知识库)、舆情监控(新闻/社交媒体分析) * 竞品分析(产品功能/价格监控)、SEO 内容聚合 * 使用步骤 * 安装依赖库 ```python pip install beautifulsoup4 # HTML 解析依赖(默认已包含) pip install requests # 网络请求依赖(默认已包含) ``` * 目标网页要求 * 无需 JavaScript 渲染(动态内容需改用 `SeleniumURLLoader` ,但是很鸡肋,少用) * 未被反爬虫机制拦截(如需要,需配置代理或请求头) * 如果动态网页,且内容提取好,还是需要单独针对不同的网站写代码进行提取内容 * 案例实战 * 基础用法:加载单个网页 ```python import os #代码中设置USER_AGENT, 设置USER_AGENT的代码一定要放在WebBaseLoader 这个包前面,不然还是会报错 os.environ['USER_AGENT'] = 'Mozilla/5.0 (Windows NT 14.0; Win64; x64) AppleWebKit/567.36 (KHTML, like Gecko) Chrome/58.0.444.11 Safari/337.3' from langchain_community.document_loaders import WebBaseLoader #警告日志信息:USER_AGENT environment variable not set, consider setting it to identify your requests. # 初始化加载器,传入目标URL列表(可多个) urls = ["https://www.cnblogs.com"] loader = WebBaseLoader(urls) # 加载文档(返回Document对象列表) docs = loader.load() # 查看结果 print(f"提取的文本长度: {len(docs[0].page_content)} 字符") print(f"前200字符预览:\n{docs[0].page_content[:200]}...") print(f"元数据: {docs[0].metadata}") ``` * 批量加载多个网页 ```python import os #代码中设置USER_AGENT, 注意设置USER_AGENT的代码一定要放在WebBaseLoader 这个包前面,不然还是会报错 os.environ['USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3' from langchain_community.document_loaders import WebBaseLoader #警告日志信息:USER_AGENT environment variable not set, consider setting it to identify your requests. # 初始化加载器,传入目标URL列表(可多个) urls = [ "https://news.baidu.com/", # 新闻 "https://tieba.baidu.com/index.html" # 贴吧 ] loader = WebBaseLoader(urls) docs = loader.load() print(f"共加载 {len(docs)} 个文档") print("各文档来源:") for doc in docs: print(f"- {doc.metadata['source']}") ``` * 更多详细API和参数 https://python.langchain.com/docs/integrations/document_loaders/web_base/ #### Word文档加载器实战和常见问题处理 * `Docx2txtLoader`介绍 * 是 LangChain 中专门用于加载 **Microsoft Word 文档(.docx)** 的文档加载器。 * 提取文档中的纯文本内容(包括段落、列表、表格文字等),忽略复杂格式(如字体、颜色),生成统一的 `Document` 对象。 * 适用于从 Word 报告中快速提取结构化文本 * 使用步骤 * 安装依赖库 ```python pip install docx2txt # 核心文本提取库 ``` * 准备.docx文件:确保目标文件为 .docx 格式(旧版 .doc 需转换),且未被加密 * 案例代码实战 * 基础用法:加载单个Word文档 ```python from langchain_community.document_loaders import Docx2txtLoader # 初始化加载器,传入文件路径 loader = Docx2txtLoader("data/test1.docx") # 加载文档(返回单个Document对象) documents = loader.load() # 查看内容 print(f"文本长度: {len(documents[0].page_content)} 字符") print(f"前200字符预览:\n{documents[0].page_content[:200]}...") print(f"元数据: {documents[0].metadata}") ``` * 批量加载文档 ```python from langchain_community.document_loaders import Docx2txtLoader import os folder_path = "data/" all_docs = [] # 遍历文件夹内所有.docx文件 for filename in os.listdir(folder_path): if filename.endswith(".docx"): file_path = os.path.join(folder_path, filename) loader = Docx2txtLoader(file_path) all_docs.extend(loader.load()) # 合并所有文档 print(f"加载文件: {filename}") print(all_docs) ``` * 常见问题与解决方案 * 加载 .doc 文件报错 * 原因:docx2txt 仅支持 .docx 格式。 * 解决:使用 Word 将 .doc 另存为 .docx。 * 文档中的图片/图表未被提取 * 原因:Docx2txtLoader 仅提取文本,忽略图片。 * 解决:使用 python-docx 单独提取图片,也可以使用其他组件,类似OCR ### RAG检索增强生成之文档切割 #### RAG系统链路构建之文档切割转换 * 构建RAG系统:**涉及的技术链路环节: 文档加载器->文档转换器->文本嵌入模型->向量存储->检索器** * RAG数据流水线示意图 ```python 原始数据 → 数据加载 → 预处理 → 向量化 → 存储 → 检索增强生成 ↗ ↗ ↗ PDF 文本清洗 嵌入模型 数据库 分块 网页 ``` ![data_connection_diagram](/img/data_connection-95ff2033a8faa5f3ba41376c0f6dd32a.jpg) * 需求背景,为啥要用? * 模型输入限制:GPT-4最大上下文32k tokens,Claude 3最高200k * 信息密度不均:关键信息可能分布在长文本的不同位置 * 格式兼容性问题:PDF/HTML/代码等不同格式的结构差异 * 文档转换器(Document Transformers) * 文档转换器是 LangChain 处理文档流水线的核心组件,负责对原始文档进行结构化和语义化处理, * 为后续的向量化存储、检索增强生成(RAG)等场景提供标准化输入。 * 核心任务:文本清洗、分块、元数据增强 * 关键操作 * **文本分块**:按固定长度或语义分割(防止截断完整句子) * **去噪处理**:移除特殊字符、乱码、广告内容 * **元数据注入**:添加来源、时间戳等上下文信息 * 效果 * **保留语义完整性**:避免因分割导致上下文断裂或信息丢失 * **适配模型输入限制**:确保分割后的文本块长度符合大语言模型(LLM)的token限制 * **优化向量化效果**:通过合理分块提升向量表示的语义精度,从而提高检索匹配率 | 问题类型 | 原始文档示例 | 转换前问题 | 转换后效果 | | :--------: | :-----------: | :-----------------------: | :--------------------: | | 长文本溢出 | 500页法律合同 | 直接输入导致API报错 | 分割为上下文合规的段落 | | 信息碎片化 | 产品手册PDF | 技术参数分散在不同页面 | 按功能模块重组内容 | | 噪音污染 | 网页抓取内容 | 包含广告/导航栏等干扰信息 | 提取纯净正文内容 | | 格式混乱 | 代码仓库文档 | Markdown/代码片段混合 | 分离代码与说明文本 | * 基础类和核心参数说明 ```python from langchain_text_splitters import TextSplitter #源码 class TextSplitter(BaseDocumentTransformer, ABC): """Interface for splitting text into chunks.""" def __init__( self, chunk_size: int = 4000, chunk_overlap: int = 200, length_function: Callable[[str], int] = len, keep_separator: Union[bool, Literal["start", "end"]] = False, add_start_index: bool = False, strip_whitespace: bool = True, ) -> None: ``` * 方法说明 * `TextSplitter`本身没有实现`split_text`,要文档分割器按自己的分割策略实现分割 * 关键方法调用 `split_documents()->create_documents->()->split_text()` * `split_text()`是基础文本分割方法 * `create_documents()`在`split_text()`基础上封装了元数据绑定逻辑 * `split_documents()`内部调用`create_documents()`并自动处理元数据传递 | 方法 | 输入类型 | 输出类型 | 元数据处理 | 典型使用场景 | | :----------------------- | :------------------- | :--------------- | :------------------------- | :------------------------------------ | | **`split_text()`** | **单个字符串** | `List[str]` | ❌ 不保留元数据 | 仅需分割纯文本内容时使用 | | **`create_documents()`** | **字符串列表** | `List[Document]` | ✅ 需手动传递元数据 | 从原始文本构建带元数据的文档对象 | | **`split_documents()`** | **Document对象列表** | `List[Document]` | ✅ 自动继承输入文档的元数据 | 分割已加载的文档对象(如PDF解析结果) | * `chunk_size` * 定义:每个文本块的最大长度(字符数或token数),用于控制分割后的文本块大小。 * 作用 * 防止文本过长超出模型处理限制,影响检索精度。 * 较小的chunk_size能提高检索细粒度,会导致上下文缺失。 * 例子 ```python # 设置chunk_size=100,分割文本为不超过100字符的块 text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=20) #输入文本:"Python是一种解释型语言,适合快速开发。它支持面向对象编程,语法简洁。" #分割结果:["Python是一种解释型语言,适合快速开发。", "开发。它支持面向对象编程,语法简洁。"](假设每个块接近100字符) ``` * `chunk_overlap` * 定义:相邻文本块之间的重叠字符数,用于保留上下文连贯性。 * 作用:避免因分割导致关键信息被切断(如句子中间被截断) * 案例一 ```python 如果chunk_size设为1024,chunk_overlap设为128, 对一个长度为2560的文本序列,会切分成3个chunk: chunk 1: 第1-1024个token chunk 2: 第897-1920个token (与chunk 1重叠128个) chunk 3: 第1793-2560个token (与chunk 2重叠128个) ``` * 案例二 ```python # 设置chunk_size=50,chunk_overlap=10 text = "深度学习需要大量数据和计算资源。卷积神经网络(CNN)在图像处理中表现优异。" text_splitter = CharacterTextSplitter(chunk_size=50, chunk_overlap=10) #分割结果:["深度学习需要大量数据和计算资源。卷积神经", "计算资源。卷积神经网络(CNN)在图像处理中表现优异。"] # 重叠部分"计算资源。"确保第二块包含前一块的结尾 ``` * `separators` * 定义:分隔符优先级列表,用于递归分割文本。 * 作用:优先按自然语义边界(如段落、句子)分割,减少语义断裂。 * 例子 ```python # 默认分隔符:["\n\n", "\n", " ", ""] text_splitter = RecursiveCharacterTextSplitter( separators=["\n\n", "。", ",", " "] ) #输入文本:"第一段\n\n第二段。第三段,第四段" #分割流程:先按\n\n分割为两段,若仍超长则按。继续分割 ``` #### 字符文档转换器TextSplitter案例实战 * `CharacterTextSplitter` 字符分割器 * 核心特点 * 是 LangChain 中最基础的文本分割器,采用**固定长度字符切割**策略。 * 适用于结构规整、格式统一的文本处理场景,强调**精确控制块长度** * 适用于结构清晰的文本(如段落分隔明确的文档)。 * 核心参数详解 | 参数 | 类型 | 默认值 | 说明 | | :------------------: | :--: | :------: | :----------------------: | | `separator` | str | `"\n\n"` | 切割文本的分隔符 | | `chunk_size` | int | `4000` | 每个块的最大字符数 | | `chunk_overlap` | int | `200` | 相邻块的重叠字符数 | | `strip_whitespace` | bool | `True` | 是否清除块首尾空格 | | `is_separator_regex` | bool | `False` | 是否启用正则表达式分隔符 | * 案例代码 * 长文本处理 ```python from langchain.text_splitter import CharacterTextSplitter text = "是一段 需要被分割的 长文本示例....,每个文本块的最大长度(字符数或token数)Document loaders are designed to load document objects. LangChain has hundreds of integrations with various data sources to load data from: Slack, Notion, Google Drive" splitter = CharacterTextSplitter( separator=" ", chunk_size=50, chunk_overlap=10 ) chunks = splitter.split_text(text) print(len(chunks)) for chunk in chunks: print(chunk) ``` * 日志文件处理 ```python from langchain.text_splitter import CharacterTextSplitter log_data = """ [ERROR] 2026-03-15 14:22:35 - Database connection failed [INFO] 2026-03-15 14:23:10 - Retrying connection... [WARNING] 2026-03-15 14:23:45 - High memory usage detected """ splitter = CharacterTextSplitter( separator="\n", chunk_size=60, chunk_overlap=20 ) log_chunks = splitter.split_text(log_data) for chunk in log_chunks: print(chunk) ``` * 优缺点说明 | 特性 | 优势 | 局限性 | | :--------: | :----------------------: | :----------------------------------------: | | 分割速度 | ⚡️ 极快(O(n)复杂度) | 不考虑语义结构 | | 内存消耗 | 🟢 极低 | 可能切断完整语义单元, 对语义关联性保持较弱 | | 配置灵活性 | 🛠️ 支持自定义分隔符和重叠 | 需要预定义有效分隔符 | | 多语言支持 | 🌍 支持任意字符集文本 | 对表意文字计算可能不准确 | * 适合场景 * 推荐使用: - 结构化日志处理 - 代码文件解析 - 已知明确分隔符的文本(如Markdown) - 需要精确控制块大小的场景 * 不推荐使用: - 自然语言段落(建议用RecursiveCharacterSplitter) - 需要保持语义完整性的场景 - 包含复杂嵌套结构的文本 #### 递归字符文档转换器TextSplitter案例实战 * `RecursiveCharacterTextSplitter` 递归字符分割器 * 核心特点 * **递归字符分割器**采用**多级分隔符优先级切割**机制,是 LangChain 中使用最广泛的通用分割器。 * 递归尝试多种分隔符(默认顺序:`["\n\n", "\n", " ", ""]`),优先按大粒度分割 * 若块过大则继续尝试更细粒度分隔符,适合处理结构复杂或嵌套的文本。 * 核心参数说明 ```python from langchain.text_splitter import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=1000, # 目标块大小(字符)每个块最多包含 1000 个字符 chunk_overlap=200, # 块间重叠量,最多有200个字符重叠 separators=["\n\n", "\n", "。", "?", "!", " ", ""], # 优先级递减的分割符 length_function=len, # 长度计算函数 keep_separator=True, # 是否保留分隔符 ) ``` * 案例实战 * 基础案例测试, 处理后chunk之间也有overlap ```python from langchain_text_splitters import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( #separators=["\n\n", "\n", " ", ""], #首先按段落分割,然后按行分割,最后按空格分割,如果都不行则按字符分割 chunk_size=20, #每个块的最大大小,不超过即可,如果超过则继续调用_split_text分隔(以字符数为单位) chunk_overlap=4 #块与块之间的重叠部分的大小 ) text = "I Love English hello world, how about you? If you're looking to get started with chat models, vector stores, or other LangChain components from a specific provider, check out our supported integrations" chunks = splitter.split_text(text) print(len(chunks)) for chunk in chunks: print(chunk) ``` * 学术论文处理 ```python from langchain.text_splitter import RecursiveCharacterTextSplitter paper_text = """ 引言机器学习近年来取得突破性进展...(长文本)若块过大则继续尝试更细粒度分隔符,适合处理结构复杂或嵌套的文本 方法我们提出新型网络架构...(技术细节)按优先级(如段落、句子、单词)递归分割文本,优先保留自然边界,如换行符、句号 实验在ImageNet数据集上...处理技术文档时,使用chunk_size=800和chunk_overlap=100,数据表格 """ splitter = RecursiveCharacterTextSplitter( chunk_size=20, chunk_overlap=4 ) paper_chunks = splitter.split_text(paper_text) print(len(paper_chunks)) for chunk in paper_chunks: print(chunk) ``` * 避坑指南 ```python # 错误示范:不合理的separators顺序 bad_splitter = RecursiveCharacterTextSplitter( separators=[" ", "\n"], # 空格优先会导致过早分割 chunk_size=500 ) # 正确写法:从大结构到小结构 good_splitter = RecursiveCharacterTextSplitter( separators=["\n\n", "\n", "。", " ", ""] ) ``` * 核心优势对比 | 特性 | CharacterTextSplitter | RecursiveCharacterTextSplitter | | :------------: | :-------------------: | :----------------------------: | | 分隔符策略 | 单级固定分隔符 | 多级优先级递归分隔符 | | 语义保持能力 | ★★☆☆☆ | ★★★★☆ | | 复杂文本适应性 | 简单结构化文本 | 混合格式/长文本/多语言 | | 典型应用场景 | 日志/CSV | 论文/邮件/网页/混合代码文 | * 推荐场景 * 学术论文/技术文档解析 * 多语言混合内容处理 * 包含嵌套结构的文本(如Markdown) * 需要保持段落完整性的问答系统 #### 分割器常见问题和优化最佳实践 * 其他常见分割器 * 递归字符分割(RecursiveCharacterTextSplitter) * 原理:按优先级(如段落、句子、单词)递归分割文本,优先保留自然边界(如换行符、句号) * 适用场景:通用文本处理,尤其适合逻辑紧密的长文档(如论文、技术手册) * 固定大小分割(CharacterTextSplitter) * 原理:按固定字符数分割,简单但可能打断句子结构。 * 优化:通过重叠(chunk_overlap)和智能截断(如优先在标点处分隔)减少语义断裂 * 适用场景:结构松散或句子独立性强的文本(如产品说明书) * Token分割(TokenTextSplitter) * 原理:基于LLM的token限制分割,避免超出模型输入长度。 * 优势:更贴近模型处理逻辑(如GPT系列) * 结构化文档分割 * HTML/Markdown分割器:按标题层级分割,保留元数据(如`HTMLHeaderTextSplitter`) * 适用场景:网页、技术文档等结构化内容 * 还有很多,可以查看官方文档拓展(如果不可访问,百度搜索) * https://python.langchain.com/docs/how_to/ * 关键参数`chunk_size、chunk_overlap` | 参数 | 作用 | 默认值 | 关键限制条件 | | :------------------ | :----------------------------------------------------------- | :----- | :------------------------ | | **`chunk_size`** | 定义每个文本块的最大长度(根据`length_function`计算,默认按字符数) | 1000 | 必须为正整数 | | **`chunk_overlap`** | 定义相邻块之间的重叠长度 | 20 | 必须小于`chunk_size`的50% | * 重叠内容未出现的常见原因 * 文本总长度不足:当输入文本长度 ≤ chunk_size时,不会触发分割。 ```python #解释:文本长度远小于chunk_size,不触发分割,无重叠。 from langchain_text_splitters import CharacterTextSplitter text = "这是一个非常短的测试文本。" text_splitter = CharacterTextSplitter( chunk_size=100, chunk_overlap=20, separator="。", # 按句号分割 length_function=len ) chunks = text_splitter.split_text(text) print(chunks) # 输出:['这是一个非常短的测试文本。'] ``` * 递归分割策略:RecursiveCharacterTextSplitter优先保证块大小,可能牺牲重叠。 ```python # 解释:当无法找到分隔符时,按字符数硬分割,强制保留重叠。 from langchain_text_splitters import RecursiveCharacterTextSplitter text = "这是一段没有标点的超长文本需要被分割成多个块但是因为没有分隔符所以分割器会尝试按字符递归分割直到满足块大小要求" text_splitter = RecursiveCharacterTextSplitter( chunk_size=30, chunk_overlap=10, separators=["", " "], # 无有效分隔符时按字符分割 length_function=len ) chunks = text_splitter.split_text(text) for i, chunk in enumerate(chunks): print(f"块{i+1}(长度{len(chunk)}): {chunk}") # 输出示例: # 块1(长度30): 这是一段没有标点的超长文本需要被分割成多个块但是因为没有分隔 # 块2(长度30): 个块但是因为没有分隔符所以分割器会尝试按字符递归分割直到满足 # 块3(长度15): 字符递归分割直到满足块大小要求 # 重叠部分:"个块但是因为没有分隔"(10字符) ``` * `enumerate()` * 函数是一个内置函数,用于在迭代过程中同时获取元素的索引和值。 * 它返回一个枚举对象,包含了索引和对应的元素 ```python # enumerate(iterable, start=0) #参数:iterable:必需,一个可迭代对象,如列表、元组、字符串等。 #参数:start:可选,指定索引的起始值,默认为 0。 fruits = ['apple', 'banana', 'orange'] for index, fruit in enumerate(fruits): print(index, fruit) ``` * 分隔符强制分割:在分隔符处切割时,剩余文本不足以形成重叠。 ```python #解释:分隔符优先切割,每个块正好为7个字符,无法形成重叠。 from langchain_text_splitters import CharacterTextSplitter text = "abcdefg.hijkllm.nopqrst.uvwxyz" text_splitter = CharacterTextSplitter( chunk_size=7, chunk_overlap=3, separator=".", # 按句号分割 is_separator_regex=False ) chunks = text_splitter.split_text(text) print("分割块数:", len(chunks)) for i, chunk in enumerate(chunks): print(f"块{i+1}: {chunk}") ``` * 参数调优最佳实践 * 通用文本处理 * 参数建议:`chunk_size=500-1000字符,chunk_overlap=10-20%` * 案例: * 处理技术文档时,使用`chunk_size=800和chunk_overlap=100` * 确保每个块包含完整段落,同时通过重叠保留跨段落的关键术语 * 代码分割 * 参数建议:根据编程语言特性调整分隔符。 * 案例: * 分割Python代码时,`RecursiveCharacterTextSplitter.from_language(Language.PYTHON)` * 会自动识别函数、类等结构,避免打断代码逻辑 ```python from langchain_text_splitters import Language, RecursiveCharacterTextSplitter python_splitter = RecursiveCharacterTextSplitter.from_language( language=Language.PYTHON, chunk_size=200, chunk_overlap=50 ) ``` * 结构化文档(如Markdown) * 参数建议:结合标题层级分割。 * 案例: * 使用MarkdownHeaderTextSplitter按标题分割,保留元数据 * 输入Markdown内容将按标题层级生成带元数据的块 ```python headers_to_split_on = [("#", "Header 1"), ("##", "Header 2")] markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on) ``` * 常见问题与解决方案 * 过长导致语义模糊 * 表现:检索时匹配不精准。 * 解决:缩小chunk_size,增加chunk_overlap * 块过短丢失上下文 * 表现:回答缺乏连贯性。 * 解决:合并相邻块或使用ParentDocumentRetriever,将细粒度块与父文档关联 * 参数选择原则: * 密集文本(如论文):chunk_size较大(如1000),chunk_overlap约15%。 * 松散文本(如对话记录):chunk_size较小(如200),chunk_overlap约20%。 * **实验验证:通过AB测试对比不同参数的检索准确率与生成质量** ### 人工智能和高等数学核心基础扫盲 #### 扫盲-AI大模型必备之向量-张量和应用场景 * 什么是向量 * 向量就是一串有序的数字,像一条带方向的“箭头”, 在机器学习里面尤其重要 image-20250313142026018 * 世间万物使用计算机表示,用的数字化语言,让计算机能理解复杂事物, 每个数据都包含多种属性 * 比如气象数据(包含温度,湿度,风向等等) * 金融数据(开盘价,收盘价,交易量等等) * 销售数据(价格,库存量,卖出数量等等) * 为了表示多属性的数据,或者称为多维度的数据,向量最为合适。 * 向量就是有几个数字横向或者纵向排列而成,每个数字代表一个属性。 ```python // 传统Java数据存储 String[] names = {"小明", "身高", "体重"}; Object[] person = {"张三", 175, 68.5}; // 向量化表示(特征向量) float[] vector = {0.83f, 175.0f, 68.5f}; // [性别编码, 身高, 体重] # Python列表表示 vector = [1.2, 3.5, 4.0, 0.8] ``` * 向量能做什么? * 计算相似度:比如比较两个人的喜好是否接近、物品是否类似 * 用户画像:`[年龄=25, 身高=175, 消费=5000]` → 用数字描述一个人 * 案例:有两个水果的向量(也可以抽取更多属性) - 苹果:[红色: 0.91, 甜度: 0.83, 圆形: 0.79] - 草莓:[红色: 0.85, 甜度: 0.75, 圆形: 0.69] * 多维向量 * 就是维度更多的向量(比如100个数字组成的列表)。 * 例子: - 词向量:`“猫” = [0.2, -0.3, 0.7, ..., 0.1]`(300个数字表示词义,抽取多点属性,形状、毛发、行为、食物...)。 - 图片特征:一张猫的图片转换为 `[0.8, 0.1, 0.05, ...]`(1000个数字描述图片内容) - 高维向量表示用户画像 ```python public class UserVector { // 每个维度代表一个特征 float[] features = new float[256]; // 可能包含: // [0-49]: 兴趣标签权重 // [50-99]: 行为频率 // [100-255]: 深度模型提取特征 } ``` * 为什么维度越多越好? - 细节更丰富 , 就像简历写得越详细,越能区分不同的人。 - 低维:`[年龄=25, 性别=1]` → 只能简单分类。 - 高维:`[年龄、性别、职业、兴趣1、兴趣2...]` → 精准推荐商品。 * 多维向量应用场景 - 推荐系统:用用户向量和商品向量计算匹配度。 - 人脸识别:把照片变成向量,对比找到最相似的人。 - 数据表示:用户画像(年龄、性别、兴趣等N个特征) - 几何计算:三维游戏中物体的位置和移动 * 通俗理解高维空间 - 假设你在三维空间找不到两片不同的树叶,但在100维空间里,每片树叶的位置都会独一无二 * 张量 * 张量是多维数组的统称,涵盖标量、向量、矩阵及更高维结构 , * 阶(Rank)表示维度数: * **标量(0阶张量)**:单个数字(如温度25℃)。 * **向量(1阶张量)**:一维数组(如 `[1, 2, 3]`)。 * **矩阵(2阶张量)**:二维表格(如Excel表格)。 * **高阶张量**:三维及以上(如视频数据:时间×宽×高×颜色通道) | 阶数 | 数学名称 | 典型应用 | Java存储结构 | | :--: | :------: | :-------: | :----------: | | 0 | 标量 | 温度值 | float | | 1 | 向量 | 用户画像 | float[] | | 2 | 矩阵 | Excel表格 | float[][] | | 3 | 张量 | 彩色图片 | float[][][] | | **数据** | **张量形状** | **解释** | | :----------: | :------------------: | :-------------------------: | | 一张黑白图片 | `(28, 28)` | 28行×28列的像素矩阵 | | 一个视频片段 | `(100, 128, 128, 3)` | 100帧,每帧128x128的RGB图片 | | 一批用户数据 | `(500, 10)` | 500个用户,每人10个特征 | * 常见问题 * 为什么要用张量而不是数组? - 张量是AI框架(如PyTorch)的“通用语言”,能自动支持GPU加速和梯度计算。 * 维度太多会不会算不过来? - 会!所以需要GPU和优化算法(比如深度学习)。 * 我该用几维向量 * 看任务需求:简单任务用几十维,复杂任务(如ChatGPT)可能用上千维! * 总结:一句话记住它们! * 向量 → 一串数字(像GPS坐标)。 * 多维向量 → 更长的数字串(像详细简历)。 * 张量 → 数字的“集装箱”(像Excel表格、图片集、视频流)。 #### 扫盲-高等数学里面的求和-点积公式讲解 * **什么是求和符号?** * 求和符号Σ(希腊字母sigma)表示**连续加法运算**,用于简化多个数的累加书写。 * 英文意思为Sum,Summation,汉语意思为“和”“总和”。 * 基本形式 image-20250313151603856 ![image-20250313151918579](/img/image-20250313151918579.png) * **单变量求和示例** * 计算1到5的整数和: image-20250313151657094 * 计算前3个偶数的平方和: image-20250313151724171 * 点积(内积)的定义与计算 * 点积是什么 * 点积是**两个向量对应分量相乘后求和**的运算,结果是一个标量(数值)。 image-20250313161943645 * 几何意义 * 点积反映两个向量的夹角关系 * 通过在空间中引入笛卡尔坐标系,向量之间的点积既可以由向量坐标的代数运算得出 image-20250313162742811 * 点积计算示例 image-20250313162827278 #### 扫盲-向量的相似度计算之余弦相似度 * 补充:三角函数 * **正弦(sinθ)**:对边长度与斜边长度的比值。 * **余弦(cosθ)**:邻边长度与斜边长度的比值(即夹角的两边) image-20250313164754696 * 为啥要学余弦相似度? * 生活中的相似度问题,假设你需要完成以下任务: * 网购时:找出和你刚买的衣服最搭配的裤子 * 听歌时:发现与当前播放歌曲风格相似的曲目 * 点外卖时:找到和你常点菜品口味接近的新店铺 * **LLM大模型的RAG原理:用户输入自然语言query,找到最相关的文档** * 这些场景的共同点:需要量化两个事物的相似程度,向量空间中的"方向感" * 什么是余弦相似度? * 基础定义:余弦相似度(Cosine Similarity)用于衡量两个向量在方向上的相似程度,忽略其绝对长度 * **值的范围为[-1,1],-1为完全不相似,1为完全相似。** * 公式(向量的模长等于其各分量平方和的平方根) ![image-20250315152627015](/img/image-20250315152627015.png) image-20250313170520819 * **直观理解** - 几何意义:两个向量的夹角越小,方向越一致,余弦值越接近1。 - 1 → 完全相同方向 - 0 → 完全无关(正交) - -1 → 完全相反方向 - 两条线段之间形成一个夹角, - 如果夹角为0度,意味着方向相同、线段重合,这是表示两个向量代表的文本完全相等; - 如果夹角为90度,意味着形成直角,方向完全不相似; - 如果夹角为180度,意味着方向正好相反。 - **因此可以通过夹角的大小,来判断向量的相似程度,夹角越小,就代表越相似。** * **为何选择余弦相似度?** - **高维数据友好**:适用于文本、图像等高维稀疏数据。 - **长度不变性**:只关注方向,忽略向量长度差异(如文档长短)。 - **计算高效**:适合大规模向量检索 * 案例 * 以二维向量为例:向量A = [3, 4],向量B = [1, 2] * 结论:两个向量方向高度相似! image-20250313170904153 * 大模型中的核心应用场景 * 语义搜索(Semantic Search) * 问题:用户输入自然语言query,找到最相关的文档。 * 实现步骤: * 将query和所有文档编码为向量。 * 计算query向量与文档向量的余弦相似度。 * 返回Top-K相似度最高的文档。 * 推荐系统(User-Item Matching) * 原理:用户向量和物品向量的余弦相似度 → 推荐得分。 * 示例: * 用户向量:[0.3, 0.5, -0.2](表示对科技、体育、艺术的兴趣) * 物品向量:[0.4, 0.1, 0.0](科技类文章) * 相似度 ≈ 0.3×0.4 + 0.5×0.1 = 0.17 → 推荐该文章。 * 为什么重要:掌握余弦相似度,就是掌握了**连接数据与智能的钥匙**! ![1](/img/1-1859494.png) #### 科学计算核心库NumPy和推荐系统案例 * 什么是 NumPy? * NumPy(Numerical Python)是 Python 的科学计算核心库,专门处理多维数组(矩阵)的高效运算。 * 核心功能:提供高性能的数组对象 ndarray,支持向量化操作(无需循环直接计算)。 * 江湖地位:几乎所有 Python 科学计算库(如 Pandas、SciPy、TensorFlow)都依赖 NumPy。 * 设计目标:用简洁的语法替代传统循环,提升大规模数据计算效率。 * 为什么需要 NumPy?对比原生 Python * 原生 Python 列表的痛点 ```python # 计算两个列表元素相加 a = [1, 2, 3] b = [4, 5, 6] result = [a[i] + b[i] for i in range(len(a))] # 需要循环逐个计算 ``` * NumPy 的解决方案 ```python import numpy as np a = np.array([1, 2, 3]) b = np.array([4, 5, 6]) result = a + b # 直接向量化运算 → 输出 [5,7,9] ``` * 性能对比(关键优势) | 操作类型 | Python 列表耗时 | NumPy 耗时 | 速度提升倍数 | | :------------ | :--------------- | :--------- | :----------- | | 100万元素求和 | 15 ms | 0.5 ms | **30倍** | | 矩阵乘法 | 手动循环实现复杂 | 单行代码 | **100倍+** | * 基础安装 ```python # 常规安装 pip install numpy # 验证安装, 应输出版本号 python -c "import numpy as np; print(np.__version__)" # 升级版本 pip install numpy --upgrade ``` * 余弦相似度案例实战 * 案例1:基础向量计算 ```python import numpy as np def cos_sim(v1, v2): """ 计算两个向量的余弦相似度 余弦相似度用于衡量两个向量方向的相似性,结果范围从-1到1 -1表示完全相反,1表示完全相同,0表示两者正交(即无相似性) 参数: v1: numpy数组,第一个向量 v2: numpy数组,第二个向量 返回: 两个向量的余弦相似度 """ # 计算向量点积 dot = np.dot(v1, v2) # 计算向量的模的乘积 norm = np.linalg.norm(v1) * np.linalg.norm(v2) # 返回余弦相似度 return dot / norm # 测试向量(支持任意维度) vec_a = np.array([0.2, 0.5, 0.8]) # 文本A的嵌入向量 vec_b = np.array([0.3, 0.6, 0.7]) # 文本B的嵌入向量 # 输出两向量的余弦相似度 print(f"相似度:{cos_sim(vec_a, vec_b):.4f}") # 输出:0.9943 ``` * 案例2:推荐系统 ```python import numpy as np def cosine_similarity(a, b): # 将列表转换为NumPy数组 a = np.array(a) b = np.array(b) # 计算点积 dot_product = np.dot(a, b) # 计算模长 norm_a = np.linalg.norm(a) norm_b = np.linalg.norm(b) # 计算余弦相似度 return dot_product / (norm_a * norm_b) if norm_a * norm_b != 0 else 0 # 用户嵌入向量(根据浏览行为生成) user_embedding = [0.7, -0.2, 0.5, 0.1] # 商品嵌入库 products = { "item1": [0.6, -0.3, 0.5, 0.2], "item2": [0.8, 0.1, 0.4, -0.1], "item3": [-0.5, 0.7, 0.2, 0.3] } # 计算相似度并推荐 recommendations = [] for item_id, vec in products.items(): sim = cosine_similarity(user_embedding, vec) recommendations.append((item_id, round(sim, 3))) # 保留3位小数 # 按相似度降序排序 recommendations.sort(key=lambda x: x[1], reverse=True) print("推荐排序:", recommendations) ``` ### RAG系统必备之嵌入大模型Embedding #### 嵌入大模型Embedding和LLM大模型对比 * 什么是文本嵌入Text Embedding * 文本嵌入(Text Embedding)是将文本(单词、短语、句子或文档)映射到高维向量空间的技术。 * 类比 * 假设你是一个Java工程师,现在需要将一段文字(比如用户评论)存入数据库。 * 传统方式可能是存字符串,但计算机无法直接“理解”语义。 * Embedding的作用 * 把文字转换成一个固定长度的数字数组(向量),比如 `[0.2, -0.5, 0.8, ...]`,这个数组能“编码”文字的含义。 * 想象每个词或句子是一个点,而Embedding就是给这些点在地图上标坐标。 * 语义相近的词(如“猫”和“狗”)坐标距离近,无关的词(如“猫”和“汽车”)坐标距离远。 * **通俗来说** * 将不可计算、非结构化 的词 转化为 可计算,结构化的 向量,把文字变成一串有含义的数字密码 * 就像给每个句子/词语颁发"数字身份证"。这个身份证号码能表达文字的**核心含义** * 例:"猫" → [0.2, -0.5, 0.8,...](300个数字组成的向量) * 核心特点 * **语义感知**:相似的文字数字也相似 ```python # "狗"和"犬"的嵌入向量距离近 # "苹果"(水果)和"苹果"(手机)的嵌入距离远 ``` * 降维表示:将离散的文本转化为连续的向量 * 维度固定:无论输入多长,同个嵌入大模型 输出数字个数相同(如384/768维) * 案例【采用二维方式,正常的向量化后都是上千个维度】 * 句子 * 句子1:老王喜欢吃香蕉 * 句子2:冰冰喜欢吃苹果 * 句子3:老帆在打篮球 * 向量化后续图形表示【二维】 * 句子1和2相近,因为维度大体相同 image-20250313210027704 * 应用场景 * 语义搜索: 找含义相近的内容,不依赖关键词匹配 ``` # 搜索"如何养小猫咪" → 匹配到"幼猫护理指南" ``` * 智能分类:自动识别用户评论的情绪/类型 * 问答系统:快速找到与问题最相关的答案段落 * 什么是嵌入Embedding大模型 * Embedding 模型的主要任务是将文本转换为数值向量表示 * 这些向量可以用于计算文本之间的相似度、进行信息检索和聚类分析 * 文本嵌入的整体链路 `原始文本 → Embedding模型 → 数值向量 → 存储/比较` ![image-20250313212722723](/img/image-20250313212722723.png) * LLM 大模型 vs Embedding 大模型 | 维度 | LLM (如GPT-4、Claude) | Embedding 模型 (如BERT、text-embedding-3) | | :----------: | :------------------------: | :---------------------------------------: | | **核心能力** | 理解并生成人类语言 | 将文本转化为数学向量 | | **输出形式** | 自然文本(对话/文章/代码) | 数值向量(如1536维浮点数) | | **典型交互** | 多轮对话、持续创作 | 单次转换、批量处理 | * 关键联系 * 数据基础相同: * 都通过海量文本训练,理解语言规律 * 就像作家和图书管理员都读过很多书 * 协作关系 * 常组合使用,Embedding快速筛选相关信息,LLM精细加工生成最终结果 * 就像先让图书管理员找到资料,再让作家整理成报告 * 常见误区 * ❌ 误区1:"Embedding是简化版LLM"→ 其实它们是不同工种,就像厨师和营养师的关系 * ❌ 误区2:"LLM可以直接做Embedding的事"→ 虽然理论上可以,但就像用跑车送外卖——又贵又慢 * ❌ 误区3:"Embedding模型不需要训练"→ 好的Embedding也需要大量训练,就像图书管理员需要学习分类方法 * 一句话总结 * LLM是内容生产者,Embedding是内容组织者 * 它们就像餐厅里的厨师和配菜员,一个负责烹饪(生成内容),一个负责准备食材(组织信息) * 组合应用场景 * 场景1:智能客服系统 * Embedding:把用户问题"我的订单怎么还没到?"转换成向量,快速匹配知识库中相似问题 * LLM:根据匹配到的问题模板,生成具体回答:"您的订单已发货,预计明天送达" * 场景2:论文查重 * Embedding:把论文段落转为向量,计算与数据库的相似度 * LLM:若发现高相似度,自动改写重复段落并给出修改建议 #### LangChain框架文本嵌入Embedding实战 * LangChain框架中的Embedding * 通过标准化接口集成了多种嵌入模型,支持开发者灵活调用 * **功能**:对接各类文本嵌入模型的标准化接口 * **作用**:将文本转换为向量,供后续检索/比较使用 * **类比**:不同品牌手机充电器 → LangChain是万能充电头 * 源码查看 ```python from langchain.embeddings import OpenAIEmbeddings from abc import ABC, abstractmethod from langchain_core.runnables.config import run_in_executor class Embeddings(ABC): @abstractmethod def embed_documents(self, texts: list[str]) -> list[list[float]]: """Embed search docs. Args: texts: List of text to embed. Returns: List of embeddings. """ @abstractmethod def embed_query(self, text: str) -> list[float]: """Embed query text. Args: text: Text to embed. Returns: Embedding. """ ``` * 支持的嵌入模型类型【不同嵌入模型维度和精度不一样】 | 类型 | 代表模型 | 特点 | | :------------: | :-----------------------------: | :--------------------: | | 云端API | OpenAI, Cohere, HuggingFace Hub | 无需本地资源,按量付费 | | 本地开源模型 | Sentence-Transformers, FastText | 数据隐私高,可离线运行 | | 自定义微调模型 | 用户自行训练的模型 | 领域适配性强 | * 核心API与属性 | 方法 | 作用 | 示例场景 | | :----------------------: | :----------------------------: | :----------------: | | `embed_query(text)` | 对单个文本生成向量 | 用户提问的实时嵌入 | | `embed_documents(texts)` | 批量处理多个文本,返回向量列表 | 处理数据库文档 | | `max_retries` | 失败请求重试次数 | 应对API不稳定 | | `request_timeout` | 单次请求超时时间(秒) | 控制长文本处理时间 | * 案例实战 * 在线嵌入模型使用,也可以使用其他的厂商 * 地址:https://bailian.console.aliyun.com/ ![image-20250313215608473](/img/image-20250313215608473.png) ```python from langchain_community.embeddings import DashScopeEmbeddings # 初始化模型 ali_embeddings = DashScopeEmbeddings( model="text-embedding-v2", # 第二代通用模型 max_retries=3, dashscope_api_key="sk-005c3c25f6d042848b29d75f2f020f08" ) # 分析商品评论情感倾向 comments = [ "衣服质量很好,但是物流太慢了", "性价比超高,会回购!", "尺寸偏小,建议买大一号" ] # 生成嵌入向量 embeddings = ali_embeddings.embed_documents(comments) print(embeddings) print(len(embeddings)) # 5 print(len(embeddings[0])) # 1536 ``` #### 本地私有化部署嵌入大模型Embedding实战 * 为什么要本地部署嵌入大模型 * **对比云端风险**:第三方API可能造成数据泄露(如某云服务商API密钥泄露事件) | 需求类型 | 典型场景 | 案例说明 | | :------------: | :----------------: | :------------------------------: | | **数据安全** | 政府/金融/医疗行业 | 医院病历分析需符合《数据安全法》 | | **定制化需求** | 垂直领域术语适配 | 法律文书嵌入需理解专业法条词汇 | | **成本控制** | 长期高频使用场景 | 电商评论分析每日百万次调用 | | **网络限制** | 内网隔离环境 | 军工企业研发内部知识库 | * 本地部署嵌入大模型数据闭环 ``` 用户数据 → 企业内网服务器 → 本地模型处理 → 结果存于本地数据库 ``` ![data_connection_diagram](/img/data_connection-95ff2033a8faa5f3ba41376c0f6dd32a.jpg) * 部署实战 * 使用ollama下载嵌入大模型 * 地址:https://ollama.com/search?c=embed ```python #下载嵌入模型 ollama run mofanke/acge_text_embedding # 后台启动服务(默认端口11434) ollama serve & #查看运行的模型 ollama ps ``` * 嵌入模型请求测试 ``` curl http://localhost:11434/api/embeddings -d '{"model": "mofanke/acge_text_embedding", "prompt": "小滴课堂AI大模型课程"}' ``` * 编码实战 * 由于 LangChain 0.3.x 尚未原生支持 Ollama 嵌入模型,需自定义接口类 * `pip install requests` ```python from typing import List, Optional from langchain.embeddings.base import Embeddings import requests class OllamaEmbeddings(Embeddings): def __init__(self, model: str = "llama2", base_url: str = "http://localhost:11434"): self.model = model self.base_url = base_url def _embed(self, text: str) -> List[float]: try: response = requests.post( f"{self.base_url}/api/embeddings", json={ "model": self.model, "prompt": text # 注意:某些模型可能需要调整参数名(如"prompt"或"text") } ) response.raise_for_status() return response.json().get("embedding", []) except Exception as e: raise ValueError(f"Ollama embedding error: {str(e)}") def embed_query(self, text: str) -> List[float]: return self._embed(text) def embed_documents(self, texts: List[str]) -> List[List[float]]: return [self._embed(text) for text in texts] ``` * 使用自定义嵌入模型处理文档 ```python embeddings = OllamaEmbeddings( model="mofanke/acge_text_embedding", base_url="http://localhost:11434" ) # 分析商品评论情感倾向 comments = [ "衣服质量很好,但是物流太慢了", "性价比超高,会回购!", "尺寸偏小,建议买大一号" ] # 生成嵌入向量 embeddings = embeddings.embed_documents(comments) print(embeddings) print(len(embeddings)) # 3 print(len(embeddings[0])) # 1024 ``` #### 【面试题】RAG系统构建之嵌入模型性能优化 * 需求背景(面试高频题目) * 嵌入计算的痛点 * 嵌入生成成本高:每次调用模型API都需要计算资源 * 重复计算浪费:相同文本多次生成嵌入浪费资源 * API调用限制:商业API有调用次数限制和成本 * 响应速度瓶颈:实时场景需要快速响应 * 解决方案:缓存 * 降低计算成本:相同文本只需计算一次 * 提升响应速度:缓存读取比模型计算快10-100倍 * 突破API限制:本地缓存不受远程API配额限制 * 支持离线场景:网络不可用时仍能获取历史嵌入 * `CacheBackedEmbeddings` 介绍 * 技术架构图 ```python [应用程序] → 检查缓存 → 命中 → 返回缓存嵌入 ↓ 未命中 → 调用模型 → 存储结果 → 返回新嵌入 ``` * 是LangChain提供的缓存装饰器,支持 本地文件系统、Redis、数据库等存储方式,自动哈希文本生成唯一缓存键 ``` from langchain.embeddings import CacheBackedEmbeddings ``` * 核心语法与参数 ```python from langchain.storage import LocalFileStore from langchain.embeddings import CacheBackedEmbeddings # 初始化 cache = CacheBackedEmbeddings( underlying_embeddings=embedding_model, # 原始嵌入模型 document_embedding_store=storage, # 缓存存储对象 namespace="custom_namespace" # 可选命名空间(隔离不同项目) ) ``` | 参数 | 类型 | 作用 | | :------------------------: | :--------: | :--------------------------------: | | `underlying_embeddings` | Embeddings | 原始嵌入模型(如OpenAIEmbeddings) | | `document_embedding_store` | BaseStore | 缓存存储实现类(如LocalFileStore) | | `namespace` | str | 缓存命名空间(避免键冲突) | * 存储支持多类型 ```python #这个包里面 from langchain.storage __all__ = [ "create_kv_docstore", "create_lc_store", "EncoderBackedStore", "InMemoryByteStore", "InMemoryStore", "InvalidKeyException", "LocalFileStore", "RedisStore", "UpstashRedisByteStore", "UpstashRedisStore", ] ``` * 应用案例:智能客服知识库加速 * 场景需求 * 知识库包含10万条QA对 * 每次用户提问需要检索最相关答案 * 传统方式每次请求都要计算所有问题嵌入 * 缓存方案 * 首次加载时全量预计算并缓存 * 后续请求直接读取缓存 * 新增问题时动态更新缓存 * 代码案例 * 基础版本(无缓存) ```python from langchain.embeddings import OpenAIEmbeddings # 初始化模型 embedder = OpenAIEmbeddings(openai_api_key="sk-xxx") # 生成嵌入 vector = embedder.embed_documents("如何重置密码?") print(f"向量维度:{len(vector)}") ``` * 带缓存版本 ```python from langchain.storage import LocalFileStore from langchain.embeddings import CacheBackedEmbeddings # 创建文件缓存 fs = LocalFileStore("./embedding_cache/") # 组合缓存与模型(Java的装饰器模式模式类似) cached_embedder = CacheBackedEmbeddings.from_bytes_store( underlying_embeddings=embedder, document_embedding_store=fs, namespace="openai-embeddings" # 区分不同模型版本 ) # 首次调用(写入缓存) vector1 = cached_embedder.embed_documents("如何重置密码?") # 二次调用(读取缓存) vector2 = cached_embedder.embed_documents("如何重置密码?") print(f"结果一致性:{vector1 == vector2}") # 输出True ``` * 高级配置示例(分布式案例-存储Redis) ```python # 带TTL的Redis缓存 from redis import Redis from langchain.storage import RedisStore redis_client = Redis(host="localhost", port=6379) redis_store = RedisStore(redis_client, ttl=86400) # 24小时过期 cached_embedder = CacheBackedEmbeddings.from_bytes_store( underlying_embeddings=embedder, document_embedding_store=redis_store, namespace="openai-v3" ) ``` #### 嵌入大模型CacheBackedEmbeddings案例实战 * **案例实战:对比嵌入大模型使用缓存前后性能区别** * 注意:API设计区别 * `embed_documents`:面向批量文档预处理(适合缓存) * `embed_query`:面向实时查询处理(默认不缓存) * 设计考量因素 | 维度 | `embed_documents` | `embed_query` | | :------: | :--------------------------------: | :------------------------------: | | 使用场景 | 预处理大量重复文档(如知识库构建) | 实时响应用户查询 | | 数据特征 | 高重复率(法律条款、产品描述) | 低重复率(用户提问多样化) | | 性能损耗 | 批量处理可分摊缓存读写开销 | 单次查询增加延迟(缓存命中率低) | | 存储效率 | 缓存大量重复文本收益显著 | 缓存大量唯一查询浪费资源 | * 编码实战 ```python from langchain.embeddings import CacheBackedEmbeddings # 已被弃用,需改为从 langchain_community.embeddings 导入 # from langchain.embeddings import DashScopeEmbeddings from langchain_community.embeddings import DashScopeEmbeddings import time # 初始化模型和缓存 from langchain.storage import LocalFileStore # 初始化模型 embedding_model = DashScopeEmbeddings( model="text-embedding-v2", # 第二代通用模型 max_retries=3, dashscope_api_key="sk-005c3c25f6d042848b29d75f2f020f08" ) storage = LocalFileStore("./embedding_cache/") # 本地缓存目录 cached_embeddings = CacheBackedEmbeddings.from_bytes_store( embedding_model, storage, namespace="openai_emb" # 命名空间隔离不同模型 ) texts = ["小滴课堂AI大模型开发实战", "小滴课堂AI大模型开发实战"] # 故意重复 start_time = time.time() # 第一次调用(写入缓存) emb1 = cached_embeddings.embed_documents(texts) print(f"首次调用嵌入维度: {len(emb1[0])}") embedded1_end_time = time.time() print(f"首次调用耗时: {embedded1_end_time - start_time}") # 第二次调用(读取缓存) emb2 = cached_embeddings.embed_documents(texts) print(f"二次调用结果相等: {emb1 == emb2}") embedded2_end_time = time.time() print(f"二次调用耗时: {embedded2_end_time - embedded1_end_time}") ``` * 最佳实践建议 * **适用场景** - 处理大量重复文本(如商品描述、法律条款) - 需要控制API调用成本的商业应用 - 本地模型加速重复计算 * **存储选择策略** | 存储类型 | 优点 | 缺点 | 适用场景 | | :------------: | :----------------: | :-----------: | :-----------: | | LocalFileStore | 零配置、易调试 | 不适合分布式 | 本地开发/测试 | | RedisStore | 高性能、支持分布式 | 需要运维Redis | 生产环境集群 | | InMemoryStore | 最快速度 | 重启丢失数据 | 临时测试 | ### 大模型必备技术Milvus向量数据库 #### 向量数据库介绍和技术选型思考 * 为什么要用向量数据库,不能用MySQL存储? * 文档块通过嵌入模型处理后得到对应向量,下一步就是将向量存储到数据库中,方便后续进行检索使用 ![data_connection_diagram](/img/data_connection-95ff2033a8faa5f3ba41376c0f6dd32a.jpg) * 传统数据库的局限性 * **维度灾难**:传统索引(B-Tree/Hash)在100+维度时效率断崖式下降,无法高效处理高维向量(常达768-1536维) * **相似度计算**:无法高效处理余弦相似度/Euclidean距离等复杂运算 * **实时性要求**:亿级向量场景下传统方案响应延迟高达秒级,暴力搜索时间复杂度O(N) ```python // 传统关系型数据库查询(精确匹配) SELECT * FROM products WHERE category = 'electronics'; // 向量数据库查询(相似度匹配) Find top5 similar_products where description ≈ '高性能游戏本' ``` * 向量数据库的核心能力 * **相似性搜索**:余弦相似度/欧式距离 * **混合查询**:向量搜索 + 传统条件过滤 * **动态扩展**:支持实时数据更新 * **高效存储**:压缩向量存储技术 * 向量数据库典型应用场景 | 场景 | 案例 | 核心需求 | | :--------: | :-------------: | :------------: | | 推荐系统 | 电商商品推荐 | 高并发低延迟 | | 语义搜索 | 法律条文检索 | 高精度召回 | | AI代理记忆 | GPT长期记忆存储 | 快速上下文检索 | | 图像检索 | 以图搜图系统 | 多模态支持 | * 主流的向量数据库介绍 * 开源向量数据库 * **Milvus** - **核心优势**:分布式架构支持千亿级向量规模,QPS超百万级,提供HNSW、IVF-PQ等多样化索引算法,支持高并发场景如金融风控、生物医药分子库检索。 - **优点**:高扩展性、多租户支持、完整的API生态(Python/Java/Go等)。 - **缺点**:部署复杂度高,运维成本较大,适合有专业团队的企业。 * **Qdrant** - **核心优势**:基于Rust开发,支持稀疏向量检索(速度提升16倍),内置标量量化和产品量化技术优化存储效率,适合电商推荐、广告投放等高并发场景。 - **优点**:高性能过滤、云原生设计,支持地理空间和混合数据类型。 - **缺点**:社区生态较新,文档和案例相对较少。 * 云原生服务 * **Pinecone** - **核心优势**:全托管服务,实时数据更新延迟低于100ms,支持Serverless计费(按查询付费),适合SaaS快速集成和中小型企业。 - **优点**:零运维、低延迟、无缝集成LangChain生态。 - **缺点**:成本较高,大规模数据场景费用显著。 * **腾讯云VectorDB** - **核心优势**:国产化方案,单索引支持千亿向量,集成AI套件实现文档自动向量化,适合政务、金融等数据主权敏感场景。 - **优点**:端到端RAG解决方案,与腾讯云生态深度整合。 - **缺点**:依赖腾讯云生态,跨云部署受限。 * 轻量级工具 * **Chroma** - **核心优势**:没有深厚数据库背景的开发者也能快速上手,5分钟完成单机部署,适合学术研究、初创团队验证。 - **优点**:开发友好、快速原型验证,支持LangChain集成。 - **缺点**:不适合生产级大规模应用,功能有限。 * **Faiss** - **核心优势**:Meta开源的GPU加速检索库,百万级向量查询延迟低于10ms,常作为其他数据库的底层引擎。 - **优点**:性能标杆、算法丰富,支持混合检索架构。 - **缺点**:无托管服务,需自行处理分布式与高可用性。 * 传统数据库扩展 * **MongoDB Atlas** - **核心优势**:文档数据库嵌入向量索引,支持每个文档存储16MB向量数据,适合已有MongoDB基础设施的企业智能化升级。 - **优点**:事务处理与向量检索一体化,兼容现有业务逻辑。 - **缺点**:向量检索性能弱于专用数据库,扩展性受限。 * 其他:PostgreSQL、ElasticSearch等 * 综合选型对比 | 维度\产品 | Pinecone | Milvus | Qdrant | Chroma | | :----------: | :--------: | :-----------: | :-----------: | :------: | | **部署模式** | 全托管 | 自建/云 | 自建/云 | 嵌入式 | | **学习曲线** | 简单 | 复杂 | 中等 | 极简 | | **扩展能力** | 自动扩展 | 手动分片 | 自动分片 | 单机 | | **典型场景** | 生产级SaaS | 企业私有云 | 高性能需求 | 本地开发 | | **成本模型** | 按用量付费 | 基础设施+运维 | 基础设施+运维 | 免费 | * 技术选型建议 * **数据规模** - **十亿级以上**:选择Milvus、腾讯云VectorDB等分布式方案。 - **百万级以下**:轻量级工具如Chroma或Faiss。 * **部署复杂度** - **云服务优先**:中小企业选Pinecone或腾讯云VectorDB,省去运维成本。 - **私有化部署**:大型企业选Milvus、Qdrant,需专业团队支持。 * **成本考量** - **开源方案**:Milvus、Qdrant适合长期可控成本。 - **按需付费**:小规模试用选Pinecone Serverless。 * **生态兼容性** - **国产化需求**:腾讯云VectorDB或华为云等国产方案。 - **现有技术栈**:PostgreSQL/MongoDB扩展适合渐进式改造。 * **总结(花钱的多数更轻松方便)** * 向量数据库的选择需结合数据规模、业务场景、团队能力和成本预算综合评估。 * 对于AI驱动的应用(如RAG、多模态搜索),建议优先考虑云原生或分布式开源方案(如Milvus、Pinecone); * 传统业务系统可尝试数据库扩展插件以降低迁移成本,具体案例可参考各数据库的官方文档 #### 向量数据库Milvus介绍和架构讲解 * 什么是Milvus向量数据库 * 地址:https://milvus.io/ * 一种高性能、高扩展性的向量数据库,可在从笔记本电脑到大规模分布式系统等各种环境中高效运行。 * 可以开源软件的形式提供,也可以云服务的形式提供 * 核心能力:高性能、可扩展、低延迟,支持多种相似度计算方式(如欧氏距离、余弦相似度)。 | 维度 | 指标/能力 | | :--------: | :-----------------------------------------: | | 数据规模 | 支持**千亿级向量**,PB级存储 | | 查询性能 | 亿级向量**亚秒级响应**(GPU加速) | | 扩展性 | 水平扩展,支持**动态增删节点** | | 查询类型 | 相似度搜索、混合查询、多向量联合查询 | | 生态兼容性 | 支持Python/Java/Go/REST API,整合主流AI框架 | * 适用场景:推荐系统、图像检索、自然语言处理(NLP)等 * 全球大厂使用者 ![Milvus Adopters](/img/milvus-adopters.png) * 支持部署的架构 * Milvus提供多种部署选项,包括本地部署、Docker、Kubernetes on-premises、云SaaS和面向企业的自带云(BYOC) * Milvus Lite 是一个 Python 库,轻量级版本,适合在 Jupyter Notebooks 中进行快速原型开发,或在资源有限的边缘设备上运行 * Milvus Standalone 是单机服务器部署,所有组件都捆绑在一个 Docker 镜像中,方便部署 * Milvus Distributed 可部署在 K8S 集群上,采用云原生架构,适合十亿规模甚至更大的场景,该架构可确保关键组件的冗余。 image-20250318151846918 image-20250318155633751 * Milvus 架构解析 * 数据处理流程 :插入数据 → 生成日志 → 持久化到存储层 → 构建索引 → 支持查询。 export_qtyb85 | 组件名称 | 核心职责 | 关键特性 | | :-------------: | :----------------------------------------------------------: | :---------------------------: | | **Proxy** | 客户端请求入口,路由转发,负载均衡与协议转换(gRPC/RESTful) | 支持负载均衡、连接池管理 | | **Query Node** | 执行向量搜索,标量过滤,向量相似度计算与混合过滤 | 内存索引加载,GPU加速支持 | | **Data Node** | 处理数据插入、日志流处理与数据持久化存储 | 写入日志(WAL)保障数据一致性 | | **Index Node** | 负责索引构建与优化 | 支持后台异步构建索引 | | **Coordinator** | 集群元数据管理、任务调度 | 高可用部署(etcd存储元数据) | * 极速认知存储内容 * Collection 是一个二维表,具有固定的列和变化的行。 * 每列代表一个字段,每行代表一个实体。 * 下图显示了一个有 8 列和 6 个实体的 Collection ![Collection explained](/img/collection-explained.png) #### Milvus核心概念和数据结构讲解 * **向量数据库对比关系型数据库** | Milvus 向量数据库 | 关系型数据库 | | :---------------- | :----------- | | Collection | 表 | | Entity | 行 | | Field | 表字段 | * **基础数据结构** * **Collection(集合)** - 类比关系型数据库的“表”,用于存储和管理一组具有相同结构的实体(Entity) - Schema 定义字段结构(主键、向量、标量字段),支持动态字段(Milvus 2.3+),自动生成唯一ID(可选) - 例子 ```python pip install pymilvus==2.5.5 from pymilvus import FieldSchema, CollectionSchema, DataType # 定义字段 fields = [ FieldSchema(name="id", dtype=DataType.INT64, is_primary=True), FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=768), FieldSchema(name="category", dtype=DataType.VARCHAR, max_length=50) ] # 创建集合 schema = CollectionSchema(fields, description="商品向量库") collection = Collection(name="products", schema=schema) ``` * **Entity(实体)** - 数据的基本单位,包含多个字段(Field),如主键、标量字段(结构化数据)和向量字段。 - 主键(Primary Key):唯一标识实体,支持整数或字符串类型。 - 注意:Milvus 目前不支持主键去重,允许重复主键存在 - 组成要素 - **主键**:唯一标识(支持整数/字符串) - **向量**:浮点数组(维度需固定) - **标量字段**:元数据(文本/数值/布尔) - 数据存储示意图 | ID (INT64) | Vector (FLOAT_VECTOR[768]) | Category (VARCHAR) | Price (FLOAT) | | ---------- | -------------------------- | ------------------ | ------------- | | 1001 | [0.12, 0.34, ..., 0.98] | "Electronics" | 299.99 | | 1002 | [0.55, 0.21, ..., 0.11] | "Clothing" | 89.99 | * **字段(Field)** * 标量字段:存储数值、字符串等结构化数据,支持过滤查询(如 price < 100)。 * 向量字段:存储高维向量(如512维浮点数组),支持相似性搜索 * **查询方式(Query)** * 向量搜索:输入一个向量,返回最相似的 Top-K 结果。 * 混合查询:结合向量相似度和标量过滤条件(如“价格 < 100”) * **数据组织与扩展** * **分区(Partition)** - 逻辑划分集合数据,用于优化查询性能(如按时间或地域分区)。 - 每个分区可包含多个分片(Sharding)和段(Segment),查询时按分区减少扫描范围 * **分片(Sharding)** - 数据写入时分散到不同节点,实现并行写入。 - 默认每个集合分2个分片,基于主键哈希分配 * **段(Segment)** - 物理存储单元,自动合并插入数据形成数据文件。 - 段分为“增长段”(持续写入)和“密封段”(持久化存储),查询时合并所有段结果 * **索引**(类似MySQL有多个不同索引类型) * 一种特殊的数据结构,用于快速查找和访问数据,存储在内存中. 本身不存储数据,是存储指向数据存储位置的指针或键值对。 * Milvus索引基于原始数据构建,提高对 collection 数据搜索的速度,一个向量字段仅支持一种索引类型。 * 为提高查询性能,Milvus 支持多种索引类型,可以为每个向量字段指定一种索引类型。 * 索引类型(先大体知道即可) | 索引类型 | 适用场景 | 内存消耗 | 精度 | 构建速度 | | :------: | :--------------: | :------: | :--: | :------: | | FLAT | 小数据集精准搜索 | 高 | 100% | 快 | | IVF_FLAT | 平衡型场景 | 中 | 98% | 较快 | | HNSW | 高召回率需求 | 高 | 99% | 慢 | | IVF_PQ | 超大规模数据 | 低 | 95% | 快 | * **相似度计算** * **欧氏距离(Euclidean Distance L2)** * 数值越小越相似,理解为两个向量为两个点,欧式距离就是这两个点的直线距离 * 两点之间的距离,最小值为0,最大值不确定 * 两个点的距离,距离越近,则相似度越高,距离越大,则差异性越大 * **内积(Inner Product 简写 IP)** * 内积又称之为点积,数值越大越相似 `a·b=|a||b|cosθ ` ``` # 向量A = [a1, a2,..., an] # 向量B = [b1, b2,..., bn] IP = a1*b1 + a2*b2 + ... + an*bn ``` * **余弦相似度(Cosine)** * 基于向量夹角的相似度。 image-20250313170520819 #### Milvus的分区-分片-段结构和最佳实践 * 分区-分片-段 很多同学懵逼,用图书馆比喻理解三者关系 * 想象管理一个超大型图书馆(类比 **Collection** 集合),里面存放了上亿本书。 * 为了更好地管理图书,用了三种组织方式 * **分区(Partition)**:按书籍**主题**划分区域 * 比如:1楼科技区、2楼文学区、3楼艺术区 * **作用**:快速定位某一类书籍,避免全馆搜索 * **类比**:电商平台按商品类别(电器/服装/食品)分区存储 * **分片(Shard)**:每个主题区内设置**多个平行书架** * 比如:科技区分成10个相同结构的书架,每个书架存100万本 * **作用**:多人同时查找时,不同书架可并行工作 * **类比**:分布式系统中用分片实现水平扩展 * **段(Segment)**:每个书架上的**可拆卸书盒** * 比如:每个书架由多个书盒组成,新书先放临时盒,写满后密封成固定盒 * **作用**:优化存储空间,旧书盒可压缩归档 * **类比**:数据库将数据分块存储,便于后台合并优化 * 三者协作关系 0 | 维度 | 分区(Partition) | 分片(Shard) | 段(Segment) | | :--------: | :---------------: | :--------------------: | :----------------: | | **层级** | 逻辑划分 | 物理分布 | 物理存储单元 | | **可见性** | 用户主动创建管理 | 系统自动分配 | 完全由系统管理 | | **目的** | 业务数据隔离 | 负载均衡与扩展 | 存储优化与查询加速 | | **类比** | 图书馆的不同楼层 | 楼层内的多个相同书架 | 书架上的可替换书盒 | | **操作** | 手动指定查询分区 | 自动路由请求到不同节点 | 自动合并/压缩 | * 实际工作流程例子 * **场景**:用户上传10万条商品数据到电商平台 * **分区阶段** * 按业务维度划分(如用户ID、时间范围), 示例:`partition_2024Q1`, `vip_users` ```python # 按商品类别创建分区 # 电子产品存入electronics分区 collection.create_partition("electronics") # 服装类存入clothing分区 collection.create_partition("clothing") ``` * **分片阶段(自动完成)** * 系统自动将数据均分到3个分片(假设集群有3个节点) 2 * **段阶段(自动完成)** * 分片内数据按512MB大小自动切割成多个段 4 * 三者协作原理 * **写入过程** ``` 新数据 → 选择分区 → 分配到分片 → 写入活跃段 → 段写满后冻结 → 生成新段 ``` * **查询过程** ``` 用户请求 → 定位分区 → 并行查询所有相关分片 → 各分片扫描所有段 → 合并结果排序 ``` * **合并优化** ```python # 自动将小段合并成大段(类似HBase Compaction) [Segment1(100MB)] + [Segment2(100MB)] → [SegmentMerged(200MB)] ``` * 注意: 分区的意义是通过划定分区减少数据读取,而分片的意义在于多台机器上并行写入操作。 * **开发注意事项** * **分区使用技巧** - 按时间分区:`2023Q1`, `2023Q2` - 按业务线分区:`user_profiles`, `product_info` - **错误示范**:创建超过1000个分区(影响元数据性能) ```python # 好的实践:按时间分区 client.create_partition( collection_name="logs", partition_name="2024-01" ) # 坏的实践:每个用户一个分区(容易超过限制) ``` * **分片配置建议** * ❌ 8核机器设置128分片 → 线程频繁切换导致性能下降 * ✅ 使用公式:`分片数 = 节点数 × CPU核心数` | 分片数少 | 分片数多 | | :------------: | :------------: | | 单分片数据量大 | 单分片数据量小 | | 写入吞吐低 | 写入吞吐高 | | 易成性能瓶颈 | 资源消耗大 | ```python # 创建集合时指定 collection = Collection( name="product_images", shards_num=64, # 分片数 = 8台 × 8核 = 64 partitions=[ "electronics", "clothing", "home_appliances" ] ) # 调整段配置 client.set_property("dataCoord.segment.maxSize", "1024") # 1GB client.set_property("dataCoord.segment.sealProportion", "0.7") ``` * 段优化策略 * 监控段大小:`collection.get_segment_info()` * 手动触发合并:`collection.compact()` * 设置段容量阈值:`storage.segmentSize=1024` (单位MB) * 根据数据特性调整 ```python if 向量维度 > 1024: maxSize = 512 # 降段大小缓解内存压力 else: maxSize = 1024 ``` #### Milvus部署架构选择和Docker部署实战 * 部署架构选择 * 选择取决于项目的阶段和规模,Milvus 为从快速原型开发到大规模企业部署的各种需求提供了灵活而强大的解决方案。 - **Milvus Lite**建议用于较小的数据集,多达几百万个向量, 不支持WINDOWS系统。 - **Milvus Standalone**适用于中型数据集,可扩展至 1 亿向量。 - **Milvus Distributed 专为**大规模部署而设计,能够处理从一亿到数百亿向量的数据集。 ![Select deployment option for your use case](/img/select-deployment-option.jpeg) * Milvus分层架构(Docker部署都包括了) ```python ┌───────────────────────────────┐ │ Coordinator │ ← 管理元数据、负载均衡 ├───────────────┬───────────────┤ │ Query Node │ Data Node │ ← 处理查询与数据存储 ├───────────────┴───────────────┤ │ Object Storage (S3) │ ← 持久化存储(可选MinIO、AWS S3) └───────────────────────────────┘ ``` * Milvus Standalone 是单机服务器部署,所有组件都打包到一个Docker 镜像中,部署方便。 * 此外,Milvus Standalone 通过主从复制支持高可用性。 * 有钱的当我没说,直接购买云厂商的服务:https://help.aliyun.com/zh/milvus * LInux服务器部署Milvus实战 * 阿里云网络安全组记得开放端口 `2379`、`9091`, `19530` * 注意: 默认没加权限校验,生产环境使用一般都是内网,结合配置IP白名单 * 版本和课程保持一致,不然很多不兼容!!! * 下载相关资料 ,使用**【wget】或者【浏览器】远程下载相关依赖包(需要替换群里最新的)** ```python 原生资料下载方式(账号 - 密码 - ip地址 - 端口 需要替换群里最新的,【其他路径不变】) wget --http-user=用户名 --http-password=密码 http://ip:端口/dcloud_pan/standalone_embed.sh #比如 命令行下 wget --http-user=admin --http-password=xdclass.net888 http://47.115.31.28:9088/dcloud_pan/standalone_embed.sh # 比如 浏览器直接访问 http://47.115.31.28:9088/dcloud_pan/standalone_embed.sh ``` * 解压后执行【**依赖很多,版本差异大,务必按照下面执行,否则课程无法进行下去,加我微信 xdclass6**】 ```python #启动 bash standalone_embed.sh start #停止 bash standalone_embed.sh stop #删除 bash standalone_embed.sh delete #升级 bash standalone_embed.sh upgrade ``` * 运行安装脚本后 - 一个名为 Milvus 的 docker 容器已在**19530** 端口启动。 - 嵌入式 etcd 与 Milvus 安装在同一个容器中,服务端口为**2379**。 - Milvus 数据卷被映射到当前文件夹中的**volumes/milvus** - 访问 `http://${MILVUS_PROXY_IP}:9091/webui` 例子: http://47.119.128.20:9091/webui/ * 注意 * Milvus Web UI 与 Attu等可视化工具 不同,它是一个内置工具,只是提供简单直观的界面,查看系统的基本信息 * 主要功能:运行环境、数据库/ Collections 详情、任务和慢查询请求,不支持数据库管理和操作任务 * 参考:https://milvus.io/docs/zh/milvus-webui.md #### Milvus可视化客户端安装实战 * Attu 可视化客户端介绍 * 是一款专为 **Milvus 向量数据库**设计的开源图形化管理工具,通过直观的界面简化了数据库的日常操作与维护流程 * **跨平台支持**:提供 Docker 镜像,适配 Windows、Linux 和 macOS * **开箱即用**:无需编写代码即可完成 Milvus 的日常管理,降低学习成本 * **社区与生态**:由 Zilliz 团队维护,与 Milvus 深度集成,持续更新功能 * **版本兼容性**:注意 Attu 与 Milvus 版本的匹配,避免接口不兼容问题【**当前安装的Milvus版本 V2.5X**】 * GitHub地址:https://github.com/zilliztech/attu ![image-20250318190605311](/img/image-20250318190605311.png) * 核心功能 * 数据库与集合管理 * 数据库管理:支持创建、删除数据库,默认提供 default 数据库且不可删除。 * 集合(Collection)操作:可创建集合、定义字段(主键、标量字段、向量字段)、构建索引,并支持数据导入/导出。 * 分区与分片:支持按业务需求划分分区(如按时间或用户组),优化查询效率;默认分片数为 2,支持水平扩展。 * 向量检索与混合查询 * 相似性搜索:输入向量即可快速检索 Top-K 相似结果,支持欧氏距离(L2)、余弦相似度等度量方式。 * 标量过滤:通过 Advanced Filter 功能结合标量字段(如价格、标签)进行条件筛选,提升搜索精准度。 * 数据加载与释放:可将数据加载至内存加速检索,或释放内存以优化资源占用。 * 用户与权限管理 * 多角色权限控制:支持创建用户与角色,并分配细粒度权限(如全局权限、集合操作权限)。 * 权限类型:涵盖数据插入、删除、查询等操作, * 例如: * 全局权限:创建/删除数据库、管理资源组。 * 集合权限:加载/释放数据、构建索引、执行搜索。 * 用户权限:更新用户凭证、查询用户信息 * 安装实战(根据系统选择对应的客户端下载,输入ip+端口) ![image-20250318201255473](/img/image-20250318201255473.png) ### Milvus向量数据库多案例实战 #### Python整合向量数据库Milvus案例实战 * Python操作Milvus实战 * 安装 Milvus Python SDK, 支持 Python、Node.js、GO 和 Java SDK。 * 建议安装与所安装 Milvus 服务器版本相匹配的 PyMilvus 版本 * 安装 ``` pip install pymilvus==2.5.5 ``` * 验证安装 如果 PyMilvus 安装正确,运行以下命令时不会出现异常 ``` python -c "from pymilvus import Collection" ``` * 接口可分为以下几类: * **DDL / DCL:**createCollection / createPartition / dropCollection / dropPartition / hasCollection / hasPartition * **DML / Produce:**插入 / 删除 / 上移 * **DQL:**搜索/查询 * 操作Milvus数据库 * 使用connect()连接 Milvus 服务器,进行相关操作 ```python #也可以使用MilvusClient #from pymilvus import MilvusClient #client = MilvusClient("http://47.119.128.20:19530") from pymilvus import connections, db conn = connections.connect(host="47.119.128.20", port=19530) # 创建数据库 #db.create_database("my_database") # 使用数据库 db.using_database("my_database") # 列出数据库 dbs = db.list_database() print(dbs) #['default', 'my_database'] # 删除数据库 db.drop_database("my_database") ``` * Collection与Schema的创建和管理 * Collection 是一个二维表,具有固定的列和变化的行,每列代表一个字段,每行代表一个实体。 * 要实现这样的结构化数据管理,需要一个 Schema定义 Collections 的表结构 * 每个Schema由多个`FieldSchema`组成: ```python from pymilvus import FieldSchema, DataType # 字段定义示例 fields = [ FieldSchema(name="id", dtype=DataType.INT64, is_primary=True), FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=128), FieldSchema(name="category", dtype=DataType.VARCHAR, max_length=50) ] ``` * 字段类型详解 | 数据类型 | 说明 | 示例 | | :-------------: | :----------: | :---------------: | | `INT8/16/32/64` | 整型 | `DataType.INT64` | | `FLOAT` | 单精度浮点数 | `DataType.FLOAT` | | `DOUBLE` | 双精度浮点数 | `DataType.DOUBLE` | | `VARCHAR` | 变长字符串 | `max_length=255` | | `FLOAT_VECTOR` | 浮点向量 | `dim=768` | * 创建collection实战 ```python from pymilvus import connections from pymilvus import FieldSchema, DataType from pymilvus import CollectionSchema, Collection conn = connections.connect(host="47.119.128.20", port=19530) # 步骤1:定义字段 fields = [ FieldSchema("id", DataType.INT64, is_primary=True), FieldSchema("vector", DataType.FLOAT_VECTOR, dim=128), FieldSchema("tag", DataType.VARCHAR, max_length=50) ] # 步骤2:创建Schema schema = CollectionSchema(fields, description="示例集合") # 步骤3:实例化Collection collection = Collection( name="demo_collection", schema=schema, shards_num=2 # 分片数(分布式扩展关键) ) ``` * 关键参数解析 | 参数 | 说明 | 推荐值 | | :-----------: | :------------------------: | :--------------: | | `shards_num` | 分片数量(创建后不可修改) | 集群节点数×2 | | `description` | 集合描述信息 | 建议填写业务用途 | * 动态字段Schema * 在集合中启用动态字段后,所有未在 Schema 中定义的字段及其值都将作为键值对存储在动态字段中 ```python # 启用动态字段(Milvus 2.3+) schema = CollectionSchema( fields, enable_dynamic_field=True ) ``` * 案例讲解 * 假设 Collections Schema 只定义两个字段,名为`id` 和`vector` ,启用了动态字段,在 Collections 中插入以下数据集 * 数据集包含 多个实体,每个实体都包括字段`id`,`vector`, 和`color` ,Schema 中没有定义`color` 字段。 * 由于 Collections 启用了动态字段,因此字段`color` 将作为键值对存储在动态字段中。 ```python [ {id: 0, vector: [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592], color: "pink_8682"}, {id: 7, vector: [-0.33445148015177995, -0.2567135004164067, 0.8987539745369246, 0.9402995886420709, 0.5378064918413052], color: "grey_8510"}, {id: 8, vector: [0.39524717779832685, 0.4000257286739164, -0.5890507376891594, -0.8650502298996872, -0.6140360785406336], color: "white_9381"}, {id: 9, vector: [0.5718280481994695, 0.24070317428066512, -0.3737913482606834, -0.06726932177492717, -0.6980531615588608], color: "purple_4976"} ] ``` | 类型 | 特点 | 适用场景 | | :------------: | :---------------------------: | :----------------------------: | | **静态Schema** | 严格字段定义 | 数据结构固定的业务(用户画像) | | **动态Schema** | 允许灵活字段(需Milvus 2.3+) | 日志类多变数据 | #### Milvus索引操作和最佳实践避坑指南 * 为什么需要索引? * 加速查询:避免暴力比对,快速定位相似向量, 平衡召回率与查询速度 * 节省资源:减少内存占用和计算开销, 建议为经常访问的向量和标量创建索引 * 常见的索引类型 | 索引类型 | 适用场景 | 内存占用 | 精度 | 构建速度 | | :------: | :----------------------: | :------: | :-----: | :------: | | FLAT | 小数据精确搜索(<100万) | 高 | 100% | 快 | | IVF_FLAT | 大数据平衡场景(千万级) | 中 | 95%-98% | 较快 | | HNSW | 高召回率需求 | 高 | 98%-99% | 慢 | | DISKANN | 超大规模(10亿+) | 低 | 90%-95% | 最慢 | * Milvus索引操作 * 创建索引 ```python # 导入MilvusClient和DataType模块,用于连接Milvus服务器并操作数据类型 from pymilvus import MilvusClient, DataType # 实例化MilvusClient以连接到指定的Milvus服务器 client = MilvusClient( uri="http://47.119.128.20:19530" ) # 创建schema对象,设置自动ID生成和动态字段特性 schema = MilvusClient.create_schema( auto_id=False, enable_dynamic_field=True, ) # 向schema中添加字段"id",数据类型为INT64,作为主键 schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True) # 向schema中添加字段"vector",数据类型为FLOAT_VECTOR,维度为5 schema.add_field(field_name="vector", datatype=DataType.FLOAT_VECTOR, dim=5) # 使用create_collection方法根据schema创建集合"customized_setup" client.create_collection( collection_name="customized_setup", schema=schema, ) # 准备索引参数,为"vector"字段创建索引 index_params = MilvusClient.prepare_index_params() # 添加索引配置,指定字段名、度量类型、索引类型、索引名和参数 index_params.add_index( field_name="vector", metric_type="COSINE", # 距离计算方式 (L2/IP/COSINE) index_type="IVF_FLAT", index_name="vector_index", params={ "nlist": 128 } #聚类中心数 (建议值:sqrt(数据量)) ) # 创建索引,不等待索引创建完成即返回 client.create_index( collection_name="customized_setup", index_params=index_params, sync=False # 是否等待索引创建完成后再返回。默认为True。 ) ``` * 参数说明 | 参数 | 参数 | | ----------------- | ------------------------------------------------------------ | | `field_name` | 指定字段名称 | | `metric_type` | 用于衡量向量间相似性的算法。值有**IP**、**L2**、**COSINE**、**JACCARD**、**HAMMING**。只有当指定字段是向量字段时才可用。 | | `index_type` | 索引类型 | | `index_name` | 索引名称 | | `params` | 指定索引类型的微调参数 | | `collection_name` | Collections 的名称。 | | `sync` | 控制与客户端请求相关的索引构建方式。有效值: `True` (默认):客户端等待索引完全建立后才返回。在该过程完成之前不会收到响应。`False`:客户端收到请求后立即返回,索引在后台建立 | * 查看索引信息 ```python #列出索引名称 res = client.list_indexes( collection_name="customized_setup" ) print(res) #获取索引详细信息 res = client.describe_index( collection_name="customized_setup", index_name="vector_index" ) print(res) ``` * 删除索引 * 删除前需确保无查询正在使用该索引 * 删除后需重新创建索引才能进行有效查询 ```python #如果不再需要索引,可以直接将其删除。 client.drop_index( collection_name="customized_setup", index_name="vector_index" ) print("索引已删除") ``` * 最佳实践与避坑指南 * **Schema设计原则** - 主键选择 - 推荐自增ID避免冲突 - 禁止使用向量字段作为主键 - **字段数量**:单个集合不超过32个字段 - **向量维度**:创建后不可修改,需提前规划 * **索引选择策略**: - 百万级以下 → FLAT - 百万到亿级 → IVF/HNSW - 十亿级以上 → DISKANN * **操作规范**: - 数据插入完成后再建索引 - 定期重建索引(数据变更超过30%) - 为高频查询字段建立独立索引 * **常见错误处理** | 错误场景 | 解决方案 | | :--------------: | :----------------------------------: | | "字段类型不匹配" | 检查插入数据与Schema定义的一致性 | | "主键冲突" | 插入前检查ID唯一性,或使用自动生成ID | | "向量维度错误" | 校验dim参数与数据实际维度 | #### Milvus向量数据库的DML操作实战 * 核心DML操作实战 * 创建集合(Collection),集合是Milvus中数据存储的基本单位,需定义字段和索引 * `auto_id=True`时无需手动指定主键 * 动态字段(`enable_dynamic_field=True`)允许灵活扩展非预定义字段 ```python # 导入MilvusClient和DataType模块,用于连接Milvus服务器并操作数据类型 from pymilvus import MilvusClient, DataType # 实例化MilvusClient以连接到指定的Milvus服务器 client = MilvusClient( uri="http://47.119.128.20:19530" ) # 定义Schema schema = client.create_schema(auto_id=False, enable_dynamic_field=True) schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True) schema.add_field(field_name="vector", datatype=DataType.FLOAT_VECTOR, dim=128) schema.verify() # 验证Schema # 定义索引参数 index_params = client.prepare_index_params() index_params.add_index( field_name="vector", index_type="IVF_FLAT", # 量化索引,平衡速度与精度 metric_type="L2", # 相似性度量标准(欧式距离) params={"nlist": 1024} # 聚类中心数 ) # 创建集合 client.create_collection( collection_name="my_collection", schema=schema, index_params=index_params ) ``` * 插入数据(Insert)支持单条或批量插入【可视化工具那边需要加载,包括查询等都是需要加载状态才可以操作】 ```python data = [ {"id": 1, "vector": [0.1]*128, "text": "Sample text 1"}, {"id": 2, "vector": [0.2]*128, "text": "Sample text 2"} ] # 插入数据 insert_result = client.insert( collection_name="my_collection", data=data ) print("插入ID列表:", insert_result["ids"]) # 返回主键ID ``` * 删除数据(Delete)通过主键或条件表达式删除 ```python # 按主键删除 client.delete( collection_name="my_collection", ids=[1, 2] # 主键列表 ) # 按条件删除(如删除text字段为空的记录) client.delete( collection_name="my_collection", filter="text == ''" ) ``` * 更新数据(Update)Milvus不支持直接更新,需通过“删除+插入”实现: ```python # 删除旧数据 client.delete(collection_name="my_collection", ids=[3]) # 插入新数据 client.insert( collection_name="my_collection", data=[{"id": 3, "vector": [0.3]*128, "text": "Updated text"}] ) ``` #### Milvus向量Search查询综合案例实战 * 需求说明 * 创建包含混合数据类型(标量+向量)的集合 * 批量插入结构化和非结构化数据 * 实现带过滤条件的混合查询 * 验证端到端的向量搜索流程 * Search语法深度解析 * 核心参数说明 ```python results = client 或 collection.search( data=[[0.12, 0.23, ..., 0.88]], # 查询向量(必须) anns_field="vector", # 要搜索的向量字段名(必须) param={"metric_type": "L2", "params": {"nprobe": 10}}, # 搜索参数 limit=10, # 返回结果数量 expr="price > 50", # 过滤表达式(可选) output_fields=["product_id", "price"], # 返回的字段 ) ``` | 参数 | 类型 | 说明 | 常用值 | | :-----------: | :--: | :----------: | :--------------------------------------: | | data | list | 查询向量列表 | 二维数组 | | anns_field | str | 向量字段名 | 创建时定义的字段 | | param | dict | 搜索参数 | 包含metric_type和params | | limit | int | 返回结果数 | 5-100 | | expr | str | 过滤条件 | price > 50 AND category == 'electronics' | | output_fields | list | 返回字段 | ["field1", "field2"] | * 搜索案例实战(MilvusClient方式) * 准备数据 ```python from pymilvus import ( connections,MilvusClient, FieldSchema, CollectionSchema, DataType, Collection, utility ) import random # # 创建Milvus客户端 client = MilvusClient( uri="http://47.119.128.20:19530", ) #删除已存在的同名集合 if client.has_collection("book"): client.drop_collection("book") # 定义字段 fields = [ FieldSchema(name="book_id", dtype=DataType.INT64, is_primary=True, auto_id=True), FieldSchema(name="title", dtype=DataType.VARCHAR, max_length=200), FieldSchema(name="category", dtype=DataType.VARCHAR, max_length=50), FieldSchema(name="price", dtype=DataType.DOUBLE), FieldSchema(name="book_intro", dtype=DataType.FLOAT_VECTOR, dim=4) ] # 创建集合Schema schema = CollectionSchema( fields=fields, description="Book search collection" ) #创建集合 client.create_collection(collection_name="book", schema=schema) # 生成测试数据 num_books = 1000 categories = ["科幻", "科技", "文学", "历史"] titles = ["量子世界", "AI简史", "时光之轮", "文明起源", "未来简史", "数据科学"] data = [] for i in range(num_books): data.append({ "title": f"{random.choice(titles)}_{i}", "category": random.choice(categories), "price": round(random.uniform(10, 100), 2), "book_intro": [random.random() for _ in range(4)] # 4维向量 }) # 批量插入 insert_result = client.insert( collection_name="book", data=data ) print(f"插入数据量:{len(insert_result['ids'])}") ``` * 创建索引 ```python # 准备索引参数,为"vector"字段创建索引 index_params = MilvusClient.prepare_index_params() # 添加索引配置,指定字段名、度量类型、索引类型、索引名和参数 index_params.add_index( field_name="book_intro", metric_type="L2", # 距离计算方式 (L2/IP/COSINE) index_type="IVF_FLAT", index_name="vector_index", params={ "nlist": 128 } #聚类中心数 (建议值:sqrt(数据量)) ) # 创建索引,不等待索引创建完成即返回 client.create_index( collection_name="book", index_params=index_params ) print("索引创建完成") ``` * 执行查询【执行查询前需要加载才可以使用】 ```python client.load_collection(collection_name="book") # 加载集合到内存 # 生成查询向量 query_vector = [random.random() for _ in range(4)] # 执行带过滤条件的向量搜索 results = client.search( collection_name="book", data=[query_vector], # 支持批量查询 filter="category == '科幻' and price < 50", output_fields=["title", "category", "price"], limit=3, search_params={"nprobe": 10} ) # 解析结果 print("\n科幻类且价格<50的搜索结果:") for result in results[0]: # 第一个查询结果集 print(f"ID: {result['book_id']}") print(f"距离: {result['distance']:.4f}") print(f"标题: {result['entity']['title']}") print(f"价格: {result['entity']['price']:.2f}") print("-" * 30) ``` * 向量数据库完整工作流程示意图 ``` 1. 创建集合Schema ↓ 2. 插入测试数据 ↓ 3. 创建向量索引 ↓ 4. 加载集合到内存 ↓ 5. 执行混合查询(向量+标量过滤) ``` * 全量查询案例演示 * 测试是否有 output_fields 字段,返回结果的差异 ```python # 案例1:基础向量查询 basic_res = client.search( collection_name="book", data=[query_vector], limit=5 ) # 案例2:分页查询 page_res = client.search( collection_name="book", data=[query_vector], offset=2, limit=3 ) # 案例3:批量查询 batch_res = client.search( collection_name="book", data=[query_vector, [0.5]*4], # 同时查询两个向量,每个向量都会返回2条 limit=2 ) ``` * 集合状态 ```python # 验证集合状态 print(client.describe_collection("book")) # 索引状态检查 print(client.list_indexes("book")) ``` * 新旧版本对比表 | 功能 | PyMilvus旧版 | MilvusClient新版 | | :----------- | :---------------------- | :---------------------------- | | 连接管理 | 需要手动管理connections | 客户端自动管理 | | 数据插入格式 | 多列表结构 | 字典列表 | | 字段定义 | 使用FieldSchema | 在create_collection中直接定义 | | 返回结果格式 | 对象属性访问 | 标准化字典格式 | | 错误处理 | 异常类捕获 | 统一错误码系统 | | 动态字段支持 | 需要额外配置 | 参数开启即可 | ### MMR搜索和LangChain整合Milvus实战 #### 相似度Similarity和MMR最大边界相关搜索 * 搜索的行业应用案例:电商推荐系统(明白两个差异) * **相似度搜索**: ``` "用户点击商品A → 推荐相似商品B、C" ``` * **MMR搜索**: ``` "用户浏览历史多样 → 推荐跨品类商品" ``` * 基础相似度搜索(Similarity Search) * **原理**:通过向量空间中的距离计算(余弦相似度/L2距离等)找出最接近目标向量的结果 1 * 核心特点 * **纯向量驱动**:仅依赖向量空间中的几何距离,余弦相似度、L2距离 * **结果同质化**:返回最相似的连续区域数据 * **高性能**:时间复杂度 O(n + klogk) * 参数配置模板,方法 `vector_store.similarity_search( )` ```python vector_store.as_retriever( search_type="similarity", search_kwargs={ "k": 5, # 返回数量 "score_threshold": 0.65, # 相似度得分的最低要求,相似度≥65%的才考虑 "filter": "category == 'AI'", # 元数据过滤 "param": { # Milvus专属参数 "nprobe": 32, #nprobe是Milvus中用于控制搜索时访问的聚类数量的参数,nprobe越大,搜索越精确但耗时更长。 "radius": 0.8 #是在范围搜索中使用的参数,指定搜索的半径范围,结合score_threshold可用于限定结果的范围 } } ) ``` * 典型应用场景 * 精确语义匹配(专利检索、论文查重) * 基于内容的推荐("更多类似内容") * 敏感信息过滤(高阈值精准匹配) * 最大边界相关搜索(MMR Search) * Maximal Marginal Relevance,最大边际相关性, 是一种信息检索和推荐系统中常用的算法 * 核心目标是 在返回的结果中平衡相关性与多样性,避免返回大量高度相似的内容。 * 设计初衷是解决传统相似性搜索(如余弦相似度)可能导致的“信息冗余”问题,在需要覆盖多角度信息或推荐多样化内容的场景中效果显著 * **原理**:在相似度和多样性之间进行权衡,避免结果冗余 2 * 算法原理图解 ``` 初始候选集(fetch_k=20) │ ├── 相似度排序 │ [1, 2, 3, ..., 20] │ └── 多样性选择(λ=0.5) ↓ 最终结果(k=5) [1, 5, 12, 3, 18] # 兼顾相似与差异 ``` * 参数配置模板, 方法 `vector_store.max_marginal_relevance_search( )` ```python mmr_retriever = vector_store.as_retriever( search_type="mmr", search_kwargs={ "k": 3, #最终返回的结果数量,MMR 会从更大的候选集中筛选出 3 个最相关且多样化的结果 "fetch_k": 20, #指定 MMR 算法的候选集大小,fetch_k 越大,候选集越广,结果可能越多样,但计算成本更高 "lambda_mult": 0.6, #控制相关性与多样性之间的权衡系数。范围为 [0, 1]:接近1 结果更相似。接近0:结果差异更大 "param": { "nprobe": 64, # 针对Milvus的IVF 类索引,控制搜索的聚类数量,搜索的聚类范围越广,召回率越高,但速度越慢 "ef": 128 # Milvus HNSW索引,控制搜索的深度,值越大搜索越精确,耗时增加;值越小速度更快,可能漏掉相关结果 } } ) ``` * 关键参数解析 | 参数 | 相似度搜索 | MMR搜索 | 影响效果 | | :-------------: | :------------: | :--------------: | :------------------------------------: | | k | 控制结果数量 | 控制最终结果数量 | 值越大返回越多但可能降低精度 | | lambda_mult | 无 | 0-1之间 | 值越大越偏向相关性,值越小越强调多样性 | | score_threshold | 过滤低质量结果 | 通常不使用 | 阈值设置需根据embedding模型调整 | | filter | 元数据过滤 | 支持同左 | 可结合业务维度进行筛选 | * 典型应用场景 * 多样化推荐:电商跨品类推荐 * 知识发现:科研文献探索 * 内容生成:生成多样化文案 * 对比决策矩阵 | 维度 | Similarity Search | MMR Search | | :----------: | :----------------: | :-----------------: | | **结果质量** | 高相似度但可能重复 | 多样性更佳 | | **响应速度** | 平均 120ms | 平均 200-300ms | | **内存消耗** | 低(仅存储topK) | 高(需缓存fetch_k) | | **适用场景** | 精确匹配、去重 | 推荐系统、知识发现 | | **可解释性** | 直观的相似度排序 | 综合评分需二次解释 | * 企业推荐系统架构示例 3 #### 新版LangChain向量数据库VectorStore设计 * LangChain 向量存储体系架构 * RAG系统核心设计模式 ``` Document │ ▼ Text Splitter │ ▼ Embedding Model │ ▼ [Milvus|Chroma|Pinecone...] ←→ VectorStore ``` * LangChain设计抽象类`VectorStore`,统一接口,具体的实现由各自数据库负责 * 文档(如果过期就忽略) https://python.langchain.com/docs/integrations/vectorstores/ * 安装依赖 `pip install langchain-milvus` ``` from langchain_core.vectorstores import VectorStore ``` 1 * VectorStore 核心方法详解 * 通用方法列表 | 方法名 | 作用描述 | 常用参数示例 | | :------------------------------: | :--------------------: | :----------------------------: | | from_documents() | 从文档创建向量库 | documents, embedding, **kwargs | | add_documents() | 追加文档到已有库 | documents | | similarity_search() | 相似度查询 | query, k=4 | | similarity_search_with_score() | 带相似度得分的查询 | query, k=4 | | max_marginal_relevance_search( ) | MMR最大边界搜索 | query, k=4 | | as_retriever() | 转换为检索器供链式调用 | search_kwargs={} | * 初始化方法 ```python @classmethod def from_documents( cls, documents: List[Document], embedding: Embeddings, **kwargs ) -> VectorStore: """ 文档自动转换存储 :param documents: LangChain Document对象列表 :param embedding: 文本向量化模型 :param kwargs: 向量库特有参数 :return: 初始化的VectorStore实例 """ ``` * 和`add_documents()`区别 | 特性 | `from_documents()` | `add_documents()` | | :----------: | :----------------------: | :--------------------: | | **方法类型** | 类方法(静态方法) | 实例方法 | | **主要用途** | 初始化集合并批量插入文档 | 向已存在的集合追加文档 | | **集合创建** | 自动创建新集合 | 要求集合已存在 | | **性能消耗** | 高(需建索引+数据迁移) | 低(仅数据插入) | | **典型场景** | 首次数据入库 | 增量数据更新 | | **连接参数** | 需要完整连接配置 | 复用已有实例的配置 | * 数据插入方法 ``` def add_texts( self, texts: Iterable[str], metadatas: Optional[List[dict]] = None, **kwargs ) -> List[str]: """ 插入文本数据到向量库 :param texts: 文本内容列表 :param metadatas: 对应的元数据列表 :return: 插入文档的ID列表 """ ``` * 相似性搜索方法 ```python def similarity_search( self, query: str, k: int = 4, filter: Optional[dict] = None, **kwargs ) -> List[Document]: """ 执行相似性搜索 :param query: 查询文本 :param k: 返回结果数量 :param filter: 元数据过滤条件 :return: 匹配的Document列表 """ ``` * 最大边界相关算法(MMR) ```python def max_marginal_relevance_search( self, query: str, k: int = 4, fetch_k: int = 20, lambda_mult: float = 0.5 ) -> List[Document]: """ 多样性增强搜索 :param k: 返回结果数量 :param fetch_k: 初始获取数量 :param lambda_mult: 多样性权重(0-1) """ ``` * 不同向量数据库支持特性不一样 | 特性 | Milvus | FAISS | Pinecone | Chroma | | :----------: | :----: | :---: | :------: | :----: | | 分布式支持 | ✓ | ✗ | ✓ | ✗ | | 元数据过滤 | ✓ | ✗ | ✓ | ✓ | | 自动索引管理 | ✓ | ✗ | ✓ | ✓ | | 本地运行 | ✓ | ✓ | ✗ | ✓ | | 相似度算法 | 8种 | 4种 | 3种 | 2种 | * 场景:知识库冷启动 export_j1u9v #### LangChain整合Milvus新增和删除实战 * 需求 * 使用LangChain整合向量数据库Milvus * 实现新增和删除向量数据库的数据实战 * 文档(如果过期就忽略) * https://python.langchain.com/docs/integrations/vectorstores/milvus/ * 案例实战 * 准备数据 ```python from langchain_community.embeddings import DashScopeEmbeddings #from langchain.vectorstores import Milvus from langchain_milvus import Milvus from langchain_core.documents import Document from uuid import uuid4 # 初始化模型 embeddings = DashScopeEmbeddings( model="text-embedding-v2", # 第二代通用模型 max_retries=3, dashscope_api_key="sk-005c3c25f6d042848b29d75f2f020f08" ) vector_store = Milvus( embeddings, connection_args={"uri": "http://47.119.128.20:19530"}, collection_name="langchain_example", ) document_1 = Document( page_content="I had chocolate chip pancakes and scrambled eggs for breakfast this morning.", metadata={"source": "tweet"}, ) document_2 = Document( page_content="The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees.", metadata={"source": "news"}, ) document_3 = Document( page_content="Building an exciting new project with LangChain - come check it out!", metadata={"source": "tweet"}, ) document_4 = Document( page_content="Robbers broke into the city bank and stole $1 million in cash.", metadata={"source": "news"}, ) document_5 = Document( page_content="Wow! That was an amazing movie. I can't wait to see it again.", metadata={"source": "tweet"}, ) document_6 = Document( page_content="Is the new iPhone worth the price? Read this review to find out.", metadata={"source": "website"}, ) document_7 = Document( page_content="The top 10 soccer players in the world right now.", metadata={"source": "website"}, ) document_8 = Document( page_content="LangGraph is the best framework for building stateful, agentic applications!", metadata={"source": "tweet"}, ) document_9 = Document( page_content="The stock market is down 500 points today due to fears of a recession.", metadata={"source": "news"}, ) document_10 = Document( page_content="I have a bad feeling I am going to get deleted :(", metadata={"source": "tweet"}, ) documents = [ document_1,document_2,document_3,document_4,document_5,document_6, document_7,document_8,document_9,document_10, ] ``` * 插入 ```python ids = [ str(i+1) for i in range(len(documents))] print(ids) result = vector_store.add_documents(documents=documents, ids=ids) print(result) ``` * 删除 ```python result = vector_store.delete(ids=["1"]) print(result) #(insert count: 0, delete count: 1, upsert count: 0, timestamp: 456798840753225732, success count: 0, err count: 0 ``` #### LangChain实战MMR和相似性搜索实战 * 需求 * 使用LangChain整合向量数据库Milvus * 测试相关搜索:相似度搜索和MMR搜索 * 案例实战 * 准备数据【**执行多次有多条重复记录,向量数据库不会去重,方便测试MMR**】 ```python from langchain_community.embeddings import DashScopeEmbeddings #from langchain.vectorstores import Milvus from langchain_milvus import Milvus from langchain_core.documents import Document # 初始化模型 embeddings = DashScopeEmbeddings( model="text-embedding-v2", # 第二代通用模型 max_retries=3, dashscope_api_key="sk-005c3c25f6d042848b29d75f2f020f08" ) document_1 = Document( page_content="LangChain支持多种数据库集成,小滴课堂的AI大课", metadata={"source": "xdclass.net/doc1"}, ) document_2 = Document( page_content="Milvus擅长处理向量搜索,老王的课程不错", metadata={"source": "xdclass.net/doc2"}, ) document_3 = Document( page_content="我要去学小滴课堂的架构大课", metadata={"source": "xdclass.net/doc3"}, ) document_4 = Document( page_content="今天天气不错,老王和老帆去按摩了", metadata={"source": "xdclass.net/doc4"}, ) documents = [document_1,document_2,document_3,document_4] vector_store = Milvus.from_documents( documents=documents, embedding=embeddings, collection_name="mmr_test", connection_args={"uri": "http://47.119.128.20:19530"} ) ``` * 相似性搜索(向量数据库插入多个重复数据,看是否会返回一样的) ```python # 相似性搜索 query = "如何进行数据库集成?" results = vector_store.similarity_search(query, k=2) for doc in results: print(f"内容:{doc.page_content}\n元数据:{doc.metadata}\n") # 混合搜索(结合元数据过滤) results = vector_store.similarity_search( query, k=2, expr='source == "xdclass.net/doc1"' ) print(results) ``` * MMR搜索(跨类搭配,向量数据库插入多个重复数据,看是否会返回一样的) ```python # MMR推荐(跨类搭配) diverse_results = vector_store.max_marginal_relevance_search( query="如何进行数据库集成", k=2, fetch_k=10, lambda_mult=0.4, # expr="category in ['shoes', 'clothes', 'accessories']", search_params={ "metric_type": "IP", "params": {"nprobe": 32} } ) print(diverse_results) ``` ### Retrievers检索器+RAG文档助手项目实战 #### LangChain检索器Retrievers案例实战 * 什么是`Retriever` * **统一接口**:标准化检索流程,无论数据来源如何,最终输出`Document`对象列表。 * **多源混合检索**:支持同时查询向量库、传统数据库和搜索引擎【提高召回率】 * **与VectorStore的关系**:Retriever不直接管理存储,依赖VectorStore(如FAISS、Chroma)实现向量化与检索。 * **RAG中的角色**:作为检索增强生成(RAG)流程的“数据入口”,为生成模型提供精准上下文 data_connection_diagram * 有多个实现:VectorStoreRetriever、MultiQueryRetriever、SelfQueryRetriever等 * 特点 * **模块化设计**:支持插件式扩展,可自定义检索算法(如混合搜索、重排序)。 * **异步支持**:通过`async_get_relevant_documents`实现高并发场景下的高效检索。 * **链式调用**:可与LangChain的其他组件(如Text Splitters、Memory)无缝集成。 ``` # from langchain_core.retrievers import BaseRetriever ``` * 补充知识点:召回率(Recall)信息检索和机器学习中衡量模型找全相关结果能力的核心指标 * 比如 * 在文档检索中,如果有100篇相关文档,系统找出了80篇,那么召回率就是80%。 * 召回率高意味着系统漏掉的少,但可能夹杂了不相关的结果,这时候准确率可能低。 * Retriever常见类型之基础检索器 `VectorStoreRetriever` * 基础使用参考案例 ```python #将文档嵌入为向量,通过相似度计算(如余弦相似度)检索 from langchain_community.vectorstores import FAISS retriever = FAISS.from_documents(docs, embeddings).as_retriever( search_type="mmr", # 最大边际相关性 search_kwargs={"k": 5, "filter": {"category": "news"}} ) ``` * `as_retriever()` 方法介绍 * 将向量库实例转换为检索器对象,实现与 LangChain 链式调用(如 `RetrievalQA`)的无缝对接。 * 源码 ```python def as_retriever(self, **kwargs: Any) -> VectorStoreRetriever: tags = kwargs.pop("tags", None) or [] + self._get_retriever_tags() return VectorStoreRetriever(vectorstore=self, tags=tags, **kwargs) """ 向量库实例转换为检索器对象,实现与 LangChain 链式调用 """ ``` * 关键参数详解 * search_type 搜索类型 | 类型 | 适用场景 | Milvus 对应操作 | | :----------------------------: | :------------: | :-------------------------------: | | `"similarity"` | 基础相似度检索 | `search()` | | `"mmr"` | 多样性结果优化 | `max_marginal_relevance_search()` | | `"similarity_score_threshold"` | 阈值过滤检索 | `search()` + `score_threshold` | ```python # MMR 检索配置示例 mmr_retriever = vector_store.as_retriever( search_type="mmr", search_kwargs={ "k": 3, "fetch_k": 20, "lambda_mult": 0.5 } ) ``` * search_kwargs 参数详解 | 参数 | 类型 | 默认值 | 说明 | | :-------------: | :------: | :----: | :-----------------: | | `k` | int | 4 | 返回结果数量 | | `filter`/`expr` | str/dict | None | 元数据过滤条件 | | `param` | dict | None | Milvus 搜索参数配置 | * 综合案例实战 * 默认是similarity search ````python from langchain_community.embeddings import DashScopeEmbeddings from langchain_milvus import Milvus from langchain_core.documents import Document # 初始化模型 embeddings = DashScopeEmbeddings( model="text-embedding-v2", # 第二代通用模型 max_retries=3, dashscope_api_key="sk-005c3c25f6d042848b29d75f2f020f08" ) document_1 = Document( page_content="LangChain支持多种数据库集成,小滴课堂的AI大课", metadata={"source": "xdclass.net/doc1"}, ) document_2 = Document( page_content="Milvus擅长处理向量搜索,老王的课程不错", metadata={"source": "xdclass.net/doc2"}, ) document_3 = Document( page_content="我要去学小滴课堂的架构大课", metadata={"source": "xdclass.net/doc3"}, ) document_4 = Document( page_content="今天天气不错,老王和老帆去按摩了", metadata={"source": "xdclass.net/doc4"}, ) documents = [document_1,document_2,document_3,document_4] vector_store = Milvus.from_documents( documents=documents, embedding=embeddings, collection_name="retriever_test1", connection_args={"uri": "http://47.119.128.20:19530"} ) #默认是 similarity search retriever = vector_store.as_retriever(search_kwargs={"k": 2}) results = retriever.invoke("如何进行数据库操作") for result in results: print(f"内容 {result.page_content} 元数据 {result.metadata}") ```` * 可以调整为MMR检索 ``` retriever = vector_store.as_retriever(search_type="mmr",search_kwargs={"k": 2}) ``` #### 大厂面试题-如何提升大模型召回率和实战 * **LLM大模型开发高频面试题:如何提升大模型召回率和准确率?** * 需求背景 * 当原始查询不够明确时,或者当文档库中的内容使用不同的术语表达同一概念时 * 单一查询可能无法有效检案到所有相关内容; * 或者用户的问题可能有不同的表达方式,导致的检索结果不理想, * 需要从多个角度切入才能找到最相关的文档片段。这种情况下,生成多个变体查询可以提高召回率,确保覆盖更多相关文档。 * `MultiQueryRetriever` * 通过生成多个相关查询来增强检索效果,解决单一查询可能不够全面或存在歧义的问题。 * 原理: * **查询扩展技术**:通过LLM生成N个相关查询(如改写、扩展、翻译),合并结果去重,生成多个变体查询 * **双重增强效果**:提升召回率(+25%↑)和准确率(+18%↑)的平衡 ![1](./img/1-2885071.png) * 用法 ```python retriever = MultiQueryRetriever.from_llm( retriever=base_retriever, llm=ChatOpenAI() ) ``` * **典型问题场景** - **术语差异问题**:用户提问使用"SSL证书" vs 文档中使用"TLS证书" - **表述模糊问题**:"怎么备份数据库" vs "数据库容灾方案实施步骤" - **多语言混合**:中英文混杂查询(常见于技术文档检索) - **专业领域知识**:医学问诊中的症状不同描述方式 * 案例实战 ```python from langchain_community.embeddings import DashScopeEmbeddings #from langchain.vectorstores import Milvus from langchain_milvus import Milvus from langchain_openai import ChatOpenAI from langchain_community.document_loaders import TextLoader from langchain.retrievers.multi_query import MultiQueryRetriever from langchain_text_splitters import RecursiveCharacterTextSplitter import logging # 设置日志记录的基本配置 logging.basicConfig() # 设置多查询检索器的日志记录级别为INFO logging.getLogger("langchain.retrievers.multi_query").setLevel(logging.INFO) # 使用TextLoader加载文本数据 loader = TextLoader("data/qa.txt") # 加载数据到变量中 data = loader.load() # 初始化文本分割器,将文本分割成小块 text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=10) # 执行文本分割 splits = text_splitter.split_documents(data) # 初始化模型 embedding = DashScopeEmbeddings( model="text-embedding-v2", # 第二代通用模型 max_retries=3, dashscope_api_key="sk-005c3c25f6d042848b29d75f2f020f08" ) # 初始化向量数据库 vector_store = Milvus.from_documents( documents=splits, embedding=embedding, collection_name="mulit_retriever2", connection_args={"uri": "http://47.119.128.20:19530"} ) # 定义问题 question = "老王不知道为啥抽筋了" # 初始化语言模型 llm = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) # 从语言模型中创建多查询检索器 retriever_from_llm = MultiQueryRetriever.from_llm( retriever=vector_store.as_retriever(), llm=llm ) # 使用检索器执行问题检索 results = retriever_from_llm.invoke(question) # 打印检索到的结果数量 len(results) # 遍历并打印每个检索结果的内容和元数据 for result in results: print(f"内容 {result.page_content} 元数据 {result.metadata}") # 以下是未使用的代码片段,已将其注释掉 # vector_store = Milvus( # embeddings, # connection_args={"uri": "http://47.119.128.20:19530"}, # collection_name="mmr_test", # ) # print(vector_store) ``` #### RAG综合项目实战-AI文档问答助手 * **需求:在线文档的问答助手,方便查找相关手册和接口API** * 主要功能包括 * 文档加载与切分、向量嵌入生成、向量存储与检索。 * 基于检索增强生成(Retrieval-Augmented Generation, RAG)的问答。 * 技术选型:LangChain框架+Milvus向量数据库 * **实现的功能** * 文档加载与切分 * 使用`WebBaseLoader`从指定URL加载文档。 * 使用`RecursiveCharacterTextSplitter`将加载的文档按照指定的块大小和重叠大小进行切分。 * 向量嵌入生成 - 使用`DashScopeEmbeddings`生成文档切片的向量嵌入,模型为`text-embedding-v2`,支持最大重试次数为3次。 * 向量存储与检索 - 使用`Milvus`作为向量数据库,创建名为`doc_qa_db`的Collection。 - 将生成的向量嵌入存储到Milvus中,并支持相似性检索。 * 基于RAG的问答 - 初始化`ChatOpenAI`模型,使用`qwen-plus`作为LLM模型。 - 定义`PromptTemplate`,用于构建输入给LLM的提示信息。 - 构建RAG链,结合相似性检索结果和LLM生成回答。 * 编码实战 ```python from langchain_community.document_loaders import WebBaseLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_milvus import Milvus from langchain.schema.runnable import RunnablePassthrough from langchain.prompts import PromptTemplate from langchain_community.embeddings import DashScopeEmbeddings from langchain_openai import ChatOpenAI # 设置Milvus Collection名称。 COLLECTION_NAME = 'doc_qa_db' # 初始化WebBaseLoader加载指定URL的文档。 loader = WebBaseLoader([ 'https://milvus.io/docs/overview.md', 'https://milvus.io/docs/release_notes.md' ]) # 加载文档。 docs = loader.load() # 初始化RecursiveCharacterTextSplitter,用于切分文档。 text_splitter = RecursiveCharacterTextSplitter(chunk_size=1024, chunk_overlap=0) # 使用LangChain将输入文档按照chunk_size切分。 all_splits = text_splitter.split_documents(docs) # 初始化DashScopeEmbeddings,设置embedding模型为DashScope的text-embedding-v2。 embeddings = DashScopeEmbeddings( model="text-embedding-v2", # 第二代通用模型 max_retries=3, dashscope_api_key="sk-005c3c25f6d042848b29d75f2f020f08" ) # 创建connection,为阿里云Milvus的访问域名。 connection_args = {"uri": "http://47.119.128.20:19530"} # 创建Collection。 vector_store = Milvus( embedding_function=embeddings, connection_args=connection_args, collection_name=COLLECTION_NAME, drop_old=True, ).from_documents( all_splits, embedding=embeddings, collection_name=COLLECTION_NAME, connection_args=connection_args, ) # vector_store = Milvus( # embeddings, # connection_args={"uri": "http://47.119.128.20:19530"}, # collection_name=COLLECTION_NAME, # ) # 利用Milvus向量数据库进行相似性检索。 query = "What are the main components of Milvus?" docs = vector_store.similarity_search(query) print(len(docs)) # 初始化ChatOpenAI模型。 llm = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) # 将上述相似性检索的结果作为retriever,提出问题输入到LLM之后,获取检索增强之后的回答。 retriever = vector_store.as_retriever() ``` * 编码测试实战 ```python # 定义PromptTemplate,用于构建输入给LLM的prompt。 template = """你是AI文档助手,使用以下上下文来回答最后的问题。 如果你不知道答案,就说你不知道,不要试图编造答案。 最多使用10句话,并尽可能简洁地回答。总是在答案的末尾说“谢谢你的提问!”. {context} 问题: {question} """ rag_prompt = PromptTemplate.from_template(template) # 构建Retrieval-Augmented Generation链。 rag_chain = ( {"context": retriever, "question": RunnablePassthrough()} | rag_prompt | llm ) # 调用rag_chain回答问题。 print(rag_chain.invoke("什么是Milvus.")) ``` * 应用场景 * 教育领域:可用于备课笔记、课程内容总结等场景。 * 企业知识库:帮助企业快速构建基于内部文档的知识问答系统。 * 技术支持:提供技术文档的智能检索与问答服务。 * 扩展方向 * 支持更多类型的文档加载器(如PDF、Word等)。 * 增加多语言支持。 * 优化向量嵌入生成与检索效率 * 大家的疑惑点(下一章讲解) ```python # 构建Retrieval-Augmented Generation链。 rag_chain = ( {"context": retriever, "question": RunnablePassthrough()} | rag_prompt | llm ) ``` * 在 `rag_chain` 的定义中,`{"context": retriever, "question": RunnablePassthrough()}` 创建了一个输入字典。 * `context` 的值来自 `retriever`,它将使用向量存储检索与问题相关的文档片段。 * `question`键的值通过 `RunnablePassthrough()` 直接传递,用户输入的问题会被透传到后续的处理步骤。 * 输入字典随后会被传递给 `rag_prompt`,构建最终的提示(prompt)被传递给 `llm`(语言模型),生成最终的回答 * 总结: * 用户输入的问题会同时传给`retriever`和`RunnablePassthrough()` * `retriever`完成检索后,会自动把结果赋值给`context`。 * 检索结果`context`和用户输入`question`一并传给提示模板`prompt_template`。 ### 解析和多实现类案例实战 #### LangChain核心之Runnable接口底层实现 * 什么是`Runnable`接口 * 是LangChain框架中所有组件的核心抽象接口,用于封装可执行的逻辑单元(如模型调用、数据处理、API集成等)。 * 通过实现统一的`invoke`、`batch`、`stream`等方法,支持模块化构建链式任务,允许开发者以声明式编程LCEL串联不同组件 ``` from langchain_core.runnables ``` * 为什么要使用`Runnable` - **统一接口**:所有组件(如Prompt模板、模型、解析器)均实现Runnable接口,确保类型兼容和链式调用的无缝衔接 - **灵活组合**:通过管道符`|`将多个Runnable串联成链,简化复杂逻辑的编排,类似数据流处理 - **动态配置**:支持运行时参数绑定、组件替换和错误恢复机制(如`with_retry()`),提升系统灵活性和鲁棒性 - **异步与性能优化**:内置异步方法(如`ainvoke`)和并行处理(如`RunnableParallel`),适用于高并发场景 * 什么是`RunnableSequence` * 是LangChain中用于构建**顺序执行链**的核心组件,通过管道符`|`将多个Runnable串联,形成线性执行流程,是`Runnable`子类 ``` from langchain_core.runnables import RunnableSequence ``` * 执行 LCEL链调用的方法(invoke/stream/batch),链中的每个组件也调用对应的方法,将输出作为下一个组件的输入 ```python #RunnableSequence.invoke 的源码解读 def invoke( self, input: Input, config: Optional[RunnableConfig] = None, **kwargs: Any ) -> Output: # invoke all steps in sequence try: for i, step in enumerate(self.steps): # mark each step as a child run config = patch_config( config, callbacks=run_manager.get_child(f"seq:step:{i + 1}") ) with set_config_context(config) as context: if i == 0: input = context.run(step.invoke, input, config, **kwargs) else: input = context.run(step.invoke, input, config) ``` * 从LECL表达式开始理解 ``` chain = prompt | model | output_parser # 通过|直接连接 ``` * **数据流传递** * 每个Runnable的输出作为下一个Runnable的输入,形成单向数据流。 * 例如,若链为`A | B | C`,则执行流程为`A的输出 → B的输入 → B的输出 → C的输入` * **统一接口**: * 所有组件(如Prompt模板、模型、输出解析器)均实现`Runnable`接口,确保类型兼容性和链式调用的无缝衔接 * **延迟执行**: * 链的构建仅定义逻辑关系,实际执行在调用`invoke`或`stream`时触发,支持动态参数绑定和运行时配置 * **底层实现**: * 管道符`|`在Python中被重写为`__or__`方法,实际调用`RunnableSequence`构造函数, * 将多个Runnable存入内部列表`steps`中, 执行时按顺序遍历列表并调用每个Runnable的`invoke`方法 * Runnable接口定义了以下核心方法,支持多种执行模式 ```python class Runnable(Generic[Input, Output]): #处理单个输入,返回输出。 def invoke(self, input: Input) -> Output: ... #异步处理单个输入。 async def ainvoke(self, input: Input) -> Output: ... #逐块生成输出,适用于实时响应。 def stream(self, input: Input) -> Iterator[Output]: ... #批量处理输入列表,提升吞吐量。 def batch(self, inputs: List[Input]) -> List[Output]: ... ``` | 方法 | 说明 | 使用场景 | | :---------: | :----------: | :----------: | | `invoke()` | 同步执行 | 单次调用 | | `batch()` | 批量同步执行 | 处理数据集 | | `stream()` | 流式输出 | 实时生成文本 | | `ainvoke()` | 异步执行 | Web服务集成 | * 具有多个子类实现 | 组件 | 特点 | 适用场景 | | :-------------------: | :------: | :------------: | | `RunnableSequence` | 顺序执行 | 线性处理流水线 | | `RunnableBranch` | 条件路由 | 分支选择逻辑 | | `RunnableParallel` | 并行执行 | 多任务独立处理 | | `RunnablePassthrough` | 数据透传 | 保留原始输入 | #### RunnablePassthrough介绍和透传参数实战 * `RunnablePassthrough` * 核心功能:用于在链中直接传递输入数据,不进行任何修改,或通过 `.assign()` 扩展上下文字段 ![2](./img/2-2902256.png) * 应用场景: - 保留原始输入数据供后续步骤使用。 - 动态添加新字段到上下文中(如结合检索结果与用户问题) * 基础用法 ```python from langchain_core.runnables import RunnablePassthrough # 直接传递输入 chain = RunnablePassthrough() | model output = chain.invoke("Hello") ``` * 扩展字段案例 * 案例一 ```python # 使用 assign() 添加新字段 from langchain_core.runnables import RunnablePassthrough # 使用 assign() 方法添加新字段,该方法接收一个关键字参数,其值是一个函数 # 这个函数定义了如何处理输入数据以生成新字段 # 在这个例子中,lambda 函数接收一个输入 x,并返回 x["num"] * 2 的结果 # 这将创建一个新的字段 'processed',其值是输入字段 'num' 的两倍 chain = RunnablePassthrough.assign(processed=lambda x: x["num"] * 2) # 调用 chain 对象的 invoke 方法,传入一个包含 'num' 字段的字典 # 这将执行之前定义的 lambda 函数,并在输入字典的基础上添加 'processed' 字段 # 最后输出处理后的字典 output = chain.invoke({"num": 3}) # 输出 {'num':3, 'processed':6} print(output) ``` * 案例二(伪代码) ```python # 构建包含原始问题和处理上下文的链 chain = ( RunnablePassthrough.assign( context=lambda x: retrieve_documents(x["question"]) ) | prompt | llm ) # 输入结构 input_data = {"question": "LangChain是什么?"} response = chain.invoke(input_data) ``` * 透传参数LLM案例实战 * 用户输入的问题会同时传给`retriever`和`RunnablePassthrough()` * `retriever`完成检索后,会自动把结果赋值给`context`。 * 检索结果`context`和用户输入`question`一并传给提示模板`prompt_template`。 * **输出**:模型根据检索到的上下文生成答案 ```python from langchain_community.embeddings import DashScopeEmbeddings from langchain_milvus import Milvus from langchain_core.documents import Document from langchain_core.runnables import RunnablePassthrough from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI # 初始化模型 embeddings = DashScopeEmbeddings( model="text-embedding-v2", # 第二代通用模型 max_retries=3, dashscope_api_key="sk-005c3c25f6d042848b29d75f2f020f08" ) document_1 = Document( page_content="LangChain支持多种数据库集成,小滴课堂的AI大课", metadata={"source": "xdclass.net/doc1"}, ) document_2 = Document( page_content="Milvus擅长处理向量搜索,老王的课程不错", metadata={"source": "xdclass.net/doc2"}, ) documents = [document_1,document_2] vector_store = Milvus.from_documents( documents=documents, embedding=embeddings, collection_name="runnable_test", connection_args={"uri": "http://47.119.128.20:19530"} ) #默认是 similarity search retriever = vector_store.as_retriever(search_kwargs={"k": 2}) prompt = ChatPromptTemplate.from_template("基于上下文回答:{context}\n问题:{question}") #定义模型 model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) chain = { "context": retriever, "question": RunnablePassthrough() # 直接传递用户问题 } | prompt | model result = chain.invoke("LangChain支持数据库吗") print(result) ``` #### AI智能推荐实战之RunnableParallel并行链 * `RunnableParallel` 介绍 * 并行执行多个 Runnable,合并结果为一个字典,键为子链名称,值为对应输出 ``` class RunnableParallel(Runnable[Input, Dict[str, Any]]): """ 并行执行多个Runnable的容器类 输出结果为字典结构:{key1: result1, key2: result2...} """ ``` * 在 LCEL 链上,会将字典隐形转换为`RunnableParallel` ```python multi_retrieval_chain = ( RunnableParallel({ "context1": retriever1, #数据源一 "context2": retriever2, #数据源二 "question": RunnablePassthrough() }) | prompt_template | llm | outputParser ) ======= 自动化转换为下面,写法一样 ======== multi_retrieval_chain = ( { "context1": retriever1, #数据源一 "context2": retriever2, #数据源二 "question": RunnablePassthrough() } | prompt_template | llm | outputParser ) ``` * 特点 | 特性 | 说明 | 示例 | | :----------: | :--------------------: | :------------------------: | | **并行执行** | 所有子Runnable同时运行 | 3个任务耗时2秒(而非累加) | | **类型安全** | 强制校验输入输出类型 | 自动检测字典字段类型 | 1 * API 与用法, 构造函数所有子链接收相同的输入 ```python from langchain_core.runnables import RunnableParallel runnable = RunnableParallel( key1=chain1, key2=chain2 ) ``` * 应用场景: * **数据并行处理器**:同时处理多个数据流 * **结构化数据装配器**:构建标准化的输出格式 * **流水线分叉合并器**:实现Map-Reduce模式中的Map阶段 * 举例 * 多维度数据分析 ``` analysis_chain = RunnableParallel({ "sentiment": sentiment_analyzer, "keywords": keyword_extractor, "entities": ner_recognizer }) ``` * 多模型对比系统 ```python model_comparison = RunnableParallel({ "gpt4": gpt4_chain, "claude": claude_chain, "gemini": gemini_chain }) ``` * 智能文档处理系统 ```python document_analyzer = RunnableParallel({ "summary": summary_chain, # 摘要生成 "toc": toc_generator, # 目录提取 "stats": RunnableLambda(lambda doc: { "char_count": len(doc), "page_count": doc.count("PAGE_BREAK") + 1 }) }) # 处理200页PDF文本 analysis_result = document_analyzer.invoke(pdf_text) ``` * 案例实战 * 场景:并行生成景点与书籍推荐 ```python from langchain_core.runnables import RunnableParallel from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI from langchain_core.output_parsers import JsonOutputParser #定义模型 model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) #构建解析器 parser = JsonOutputParser() prompt_attractions = ChatPromptTemplate.from_template("""列出{city}的{num}个景点。返回 JSON 格式: {{ "num": "编号", "city": "城市", "introduce": "景点介绍", }} """) prompt_books = ChatPromptTemplate.from_template("""列出{city}相关的{num}本书返回 JSON 格式: {{ "num": "编号", "city": "城市", "introduce": "书籍介绍", }} """) chain1 = prompt_attractions | model | parser chain2 = prompt_books | model | parser chain = RunnableParallel( attractions = chain1 , books = chain2 ) output = chain.invoke({"city": "南京", "num": 3}) print(output) ``` #### RunnableLambda介绍和包装链式函数实战 * `RunnableLambda` * 核心功能 * 将任意 Python 函数转换为 Runnable,将普通的 Python 函数或可调用对象转换为 `Runnable`对象,无缝集成到链中 * 把自己需要的功能通过自定义函数 + RunnableLambda的方式包装,可以跟任何外部系统打通,集成到 LCEL 链 ``` class RunnableLambda(Runnable[Input, Output]): """ 将任意Python函数转换为符合Runnable协议的对象 实现自定义逻辑与LangChain生态的无缝集成 """ ``` * 与普通函数的区别 | 特性 | 普通函数 | RunnableLambda | | :------: | :-----------------: | :-------------------: | | 可组合性 | ❌ 无法直接接入Chain | ✅ 支持` | | 类型校验 | ❌ 动态类型 | ✅ 静态类型检查 | | 异步支持 | ❌ 需手动实现 | ✅ 原生支持async/await | | 批量处理 | ❌ 需循环调用 | ✅ 自动批量优化 | * 适合场景: * 插入自定义逻辑(如日志记录、数据清洗) * 转换数据格式(如 JSON 解析)。 * API 与用法 ```python from langchain_core.runnables import RunnableLambda def log_input(x): print(f"Input: {x}") return x chain = prompt | RunnableLambda(log_input) | model ``` * 案例实战 * 基础文本处理链 ```python from langchain_core.runnables import RunnableLambda text_clean_chain = ( RunnableLambda(lambda x: x.strip()) | RunnableLambda(str.lower) ) result = text_clean_chain.invoke(" Hello123World ") print(result) # 输出 "helloworld" ``` * 打印中间结果并过滤敏感词(在链中插入自定义处理逻辑) ```python from langchain_core.runnables import RunnableLambda from langchain_openai import ChatOpenAI def filter_content(text: str) -> str: return text.replace("暴力", "***") #定义模型 model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) chain = ( RunnableLambda(lambda x: x["user_input"]) | RunnableLambda(filter_content) | model ) result = chain.invoke({"user_input": "暴力内容"}) print(result) # 输出过滤后的结果 ``` #### 智能客服路由实战之RunnableBranch条件分支 * `RunnableBranch` * 核心功能:根据条件选择执行不同的子链,类似 if-else 路由 * API 与用法 ```python from langchain_core.runnables import RunnableBranch #条件函数:接收输入,返回布尔值。 branch = RunnableBranch( (condition1, chain1), (condition2, chain2), default_chain ) """ 参数说明: - Condition: 返回bool的可调用对象 - Runnable: 条件满足时执行的分支 - default: 所有条件不满足时执行的默认分支 技术细节: 1. 条件按声明顺序 2. 第一个满足条件的分支会被执行 3. 无默认分支且所有条件不满足时抛出异常 """ ``` 3 * 适合场景: * 多任务分类(如区分数学问题与物理问题) * 错误处理分支(如主链失败时调用备用链) * 多轮对话路由(根据对话历史选择回复策略) ``` # 根据对话历史选择回复策略 branch = RunnableBranch( (lambda x: "投诉" in x["history"], complaint_handler), (lambda x: "咨询" in x["history"], inquiry_handler), default_responder ) ``` * 智能路由系统(根据输入类型路由处理方式) ```python # 定义分类函数 def detect_topic(input_text): if "天气" in input_text: return "weather" elif "新闻" in input_text: return "news" else: return "general" # 构建分支链 branch_chain = RunnableBranch( (lambda x: detect_topic(x["input"]) == "weather", weather_chain), (lambda x: detect_topic(x["input"]) == "news", news_chain), general_chain ) # 执行示例 branch_chain.invoke({"input": "北京今天天气怎么样?"}) ``` * 案例实战:需要构建一个 **智能客服系统**,根据用户输入的请求类型自动路由到不同的处理流程: * **技术问题**:路由到技术支持链。 * **账单问题**:路由到财务链。 * **默认问题**:路由到通用问答链。 * 步骤 * 导入依赖 ```python from langchain_core.runnables import RunnableBranch, RunnableLambda from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI from langchain_core.output_parsers import StrOutputParser ``` * 定义模型 ```python #定义模型 model = ChatOpenAI( model_name = "qwen-plus", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key="sk-005c3c25f6d042848b29d75f2f020f08", temperature=0.7 ) ``` * 定义子链 ```python # 技术支持链 tech_prompt = ChatPromptTemplate.from_template( "你是一名技术支持专家,请回答以下技术问题:{input}" ) tech_chain = tech_prompt | model | StrOutputParser() # 财务链 billing_prompt = ChatPromptTemplate.from_template( "你是一名财务专员,请处理以下账单问题:{input}" ) billing_chain = billing_prompt | model | StrOutputParser() # 默认通用链 default_prompt = ChatPromptTemplate.from_template( "你是一名客服,请回答以下问题:{input}" ) default_chain = default_prompt | model | StrOutputParser() ``` * 定义路由条件函数 ```python def is_tech_question(input: dict) -> bool: # 获取 "input" 键对应的值 input_value = input.get("input", "") # 检查是否包含关键词 return "技术" in input_value or "故障" in input_value def is_billing_question(input: dict) -> bool: # 获取 "input" 键对应的值 input_value = input.get("input", "") # 检查是否包含关键词 return "账单" in input_value or "支付" in input_value ``` * 构建 RunnableBranch ```python branch = RunnableBranch( (is_tech_question, tech_chain), # 技术问题 → tech_chain (is_billing_question, billing_chain), # 账单问题 → billing_chain default_chain # 默认问题 → default_chain ) full_chain = RunnableLambda(lambda x: {"input": x}) | branch ``` * 测试案例 ```python # 测试技术问题 tech_response = full_chain.invoke("我的账号登录失败,提示技术故障") print("技术问题响应:", tech_response) # 测试账单问题 billing_response = full_chain.invoke("我的账单金额有误,请核对") print("账单问题响应:", billing_response) # 测试默认问题 default_response = full_chain.invoke("你们公司的地址在哪里?") print("默认问题响应:", default_response) #输出示例 #技术问题响应: 建议您尝试清除浏览器缓存或重置密码。若问题持续,请联系我们的技术支持团队。 #账单问题响应: 已记录您的账单问题,财务部门将在24小时内与您联系核实。 #默认问题响应: 我们的公司地址是北京市海淀区中关村大街1号。 ``` * 关键原理解析 * **条件路由逻辑** * `RunnableBranch` 接收一个由 `(条件函数, Runnable)` 组成的列表。 * 按顺序检查条件,第一个满足条件的分支会被执行,若均不满足则执行默认分支 * **输入处理**: * 输入需为字典格式(如 `{"input": "问题内容"}`),通过 `RunnableLambda` 包装原始输入为字典 * **链式组合**: * 每个分支链(如 `tech_chain`)独立处理输入,输出结果直接返回给调用方 * **调试技巧**: - 添加日志中间件(通过 `RunnableLambda`)记录路由决策过程 ```python def log_decision(input_data): print(f"路由检查输入:{input_data}") return input_data log_chain_branch = RunnableLambda(log_decision) | branch full_chain = RunnableLambda(lambda x: {"input": x}) | log_chain_branch ``` * 总结与最佳实践 * **组合使用**:通过 `|` 串联或嵌套 `Runnable` 类,构建复杂逻辑。 * **性能优化**:利用 `RunnableParallel` 减少 IO 等待时间。 * **调试技巧**:使用 `RunnableLambda` 插入日志或数据检查点。 * **容错设计**:结合 `RunnableBranch` 和 提升健壮性