惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Cisco Blogs
爱范儿
爱范儿
有赞技术团队
有赞技术团队
博客园 - 【当耐特】
Jina AI
Jina AI
Project Zero
Project Zero
宝玉的分享
宝玉的分享
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
Simon Willison's Weblog
Simon Willison's Weblog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tenable Blog
F
Fortinet All Blogs
大猫的无限游戏
大猫的无限游戏
Last Week in AI
Last Week in AI
月光博客
月光博客
雷峰网
雷峰网
G
Google Developers Blog
V
V2EX
T
Tor Project blog
罗磊的独立博客
Schneier on Security
Schneier on Security
Know Your Adversary
Know Your Adversary
W
WeLiveSecurity
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
P
Privacy International News Feed
S
Securelist
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
P
Proofpoint News Feed
Blog — PlanetScale
Blog — PlanetScale
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
小众软件
小众软件
Scott Helme
Scott Helme
I
Intezer
T
Threat Research - Cisco Blogs
The GitHub Blog
The GitHub Blog
N
Netflix TechBlog - Medium
C
CERT Recently Published Vulnerability Notes
Security Archives - TechRepublic
Security Archives - TechRepublic
酷 壳 – CoolShell
酷 壳 – CoolShell
L
LINUX DO - 最新话题
N
News | PayPal Newsroom
L
Lohrmann on Cybersecurity
T
Troy Hunt's Blog
Google DeepMind News
Google DeepMind News
P
Proofpoint News Feed
人人都是产品经理
人人都是产品经理
Latest news
Latest news
AWS News Blog
AWS News Blog
Apple Machine Learning Research
Apple Machine Learning Research

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角 .NET Core 原生 DI 扩展之基于名称的注入实现 - 元视角
基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角
飞鸿踏雪 · 2024-02-29 · via 元视角

有时候,我难免不由地感慨,真实的人类世界,本就是一个巨大的娱乐圈,即使是在英雄辈出的 IT 行业。数日前,Google 正式对外发布了 Gemini 1.5 Pro,一个建立在 Transformer 和 MoE 架构上的多模态模型。可惜,这个被 Google 寄予厚望的产品并未激起多少水花,因为就在同一天 OpenAI 发布了 Sora,一个支持从文字生成视频的模型,可谓是一时风光无二。有人说,OpenAI 站在 Google 的肩膀上,用 Google 的技术疯狂刷屏。此中曲直,远非我等外人所能预也。我们唯一能确定的事情是,通用人工智能,即:AGI(Artificial General Intelligence)的实现,正在以肉眼可见的速度被缩短,以前在科幻电影中看到的种种场景,或许会比我们想象中来得更快一些。不过,等待 AGI 来临前的黑夜注定是漫长而孤寂的。在此期间,我们继续来探索 AI 应用落地的最佳实践,即:在成功部署本地 AI 大模型后,如何通过外挂知识库的方式为其 “注入” 新的知识。

从 RAG & GPTs 开始

在上一期博客中,博主曾经有一个困惑,那就是当前阶段 AI 应用的最佳实践到底是什么?站在 2023 年的时间节点上,博主曾经以为未来属于提示词工程(Prompt Engineering),而站在 2024 年的时间节点上,博主认为 RAG & GPTs 在实践方面或许要略胜一筹。在过去的一年里,我们陆陆续续看到像 Prompt HeroesPromptBaseAI Short…等等这样的提示词网站出现,甚至提示词可以像商品一样进行交易。与此同时,随着 OpenAI GPT Store 的发布,我们仿佛可以看到一种 AI 应用商店的雏形。什么是 GPTs 呢?通常是指可以让使用者量身定做 AI 助理的工具。譬如,它允许用户上传资料来丰富 ChatGPT 的知识库,允许用户使用个性化的提示词来指导 ChatGPT 的行为,允许用户整合各项技能(搜索引擎、Web API、Function Calling)…等等。我们在上一期博客中提到人工智能的 “安卓时刻”,一个重要的契机是目前产生了类似应用商店的 GPT Store,如下图所示:

OpenAI 推出 GPT Store OpenAI 推出 GPT Store

如果你觉得 OpenAI 的 GPT Store 离我们还稍微有点距离的话,不妨了解一下 FastGPT 这个项目,它以更加直观的方式展示了一个 GPTs 是如何被创造出来的。如图所示,博主利用我的博客作为知识库创建了一个博客助手,而这一切只需要选模型、编写提示词、上传资料三个步骤即可。感兴趣的朋友可以从 这里 进行体验:

通过 FastGPT 创建 AI 应用 通过 FastGPT 创建 AI 应用

由此,我们就可以得出一个结论,目前 AI 应用落地主要还是围绕大模型微调(Fine Tuning)、提示词工程(Prompt Engineering) 以及知识增强展开,并且 GPTs 里依然有提示词参与,两者并不冲突。考虑到,大模型微调这条线存在一定的门槛,我们暂且将其放在一旁。此时,提示词工程和知识增强就成为了 AI 应用落地的关键。知识增强,专业术语为检索增强生成,即:Retrieval-Augmented GenerationRAG,其基本思路就是将大语言模型和知识库结合起来,通过外挂知识库的方式来增强大模型的生成能力。比如微软的 New Bing 是 GPT-4 + 搜索引擎的方案,而更一般的方案则是 LLM + 向量数据库的思路,下图展示了 RAG 运作的基本原理:

RAG 运作的基本原理 RAG 运作的基本原理

从这个角度来看,LangChain 及其衍生项目 AutoChainEmbedchain,甚至 FastGPT 等项目解决的本质都是 RAGAgent 的问题。其中,Agent 不在本文的讨论范围内,这里博主不打算详细展开。接下来的内容,博主会按照这个思路进行阐述,并且以 LangChain 为例来对其中的细节进行说明。

知识库构建

如你所见,RAG 由 LLM 和 知识库两部分组成。首先,我们来构建知识库,通常,这个过程可以划分为下面四个步骤,即:载入文档(Loader)、拆分文本(Splitter)、文本向量化(Embeddings)、向量存储(VectorStore)。

构建知识库的步骤 构建知识库的步骤

Loader

你会注意到,博主在文章中加粗显示了这四个步骤的英文描述,事实上,这代表了 LangChain 中的一部分概念,以 Loader 为例,它负责从各种文档中载入内容,下面展示了从文本文件、PDF 文件以及网页中载入内容:

from langchain_community.document_loaders import DirectoryLoader, TextLoader, PyPDFLoader, WebBaseLoader

# TextLoader
# 指定编码
loader = TextLoader("./input/金庸武侠小说全集/射雕英雄传.txt", encoding="utf-8")
loader.load()
# 自动推断
# python -m pip install chardet
loader = TextLoader("./input/金庸武侠小说全集/射雕英雄传.txt", autodetect_encoding=True)
loader.load()

# PyPDFLoader
# python -m pip install pypdf
loader = PyPDFLoader("./input/文学作品/追风筝的人.pdf")
loader.load()

# WebBaseLoader
# python -m pip install beautifulsoup4
loader = WebBaseLoader(web_paths=('https://blog.yuanpei.me',), bs_kwargs={})
loader.load()

当然,现实中通常会有很多文档,此时,我们可以使用 DirectoryLoader 来一次性载入多个文档:

from langchain_community.document_loaders import DirectoryLoader

loader = DirectoryLoader("./posts/", glob="*.md", loader_kwargs={}, show_progress=True, silent_errors=True)

默认情况下,DirectoryLoader 使用 UnstructuredFileLoader 这个通用的 Loader 来兼容各种格式的文件,不过,你依然可以使用 loader_cls 参数来指定 Loader 类型:

from langchain_community.document_loaders import DirectoryLoader, TextLoader

loader = DirectoryLoader("./posts/", glob="*.md", loader_cls=TextLoader, loader_kwargs={}, show_progress=True, silent_errors=True)

Splitter

调用 Loader 的 load() 方法,返回的是一组 Document 的集合。此时,我们可以将这些 Document 交给 TextSplitter 来对分本内容的分割,因为我们最终需要对文本块做向量化处理:

from langchain.text_splitter import CharacterTextSplitter, RecursiveCharacterTextSplitter

text_splitter = CharacterTextSplitter(separator = "\n\n", chunk_size = 600, chunk_overlap = 100, length_function = len)
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)

这里,博主先后使用了 CharacterTextSplitter 和 RecursiveCharacterTextSplitter 两种 Splitter,两者的区别是:CharacterTextSplitter 会将文本按照单个字符进行分割,而 RecursiveCharacterTextSplitter 则会将文本按照连续的多个字符进行分割。不过,经过博主的测试,两种分割方式最终对 LLM 的影响微乎其微,大家可以参照官方文档中的说明,选择符合个人预期的分割方法:

LangChain 中不同的 TextSplitter LangChain 中不同的 TextSplitter

按照一般的流程,我们只需要按下面的方式,即可完成文档的分割:

documents = loader.load()
text_splitter.split_documents(documents)

不过,实际操作中更推荐使用 load_and_Split() 方法,两步合并为一步,更简洁一点:

documents = loader.load_and_split(text_splitter)

Embeddings

经过 Splitter 处理以后,我们将得到一系列文本块,它依然是一组 Document 集合。此时,便轮到 Embeddings 出场,它将负责将文本块向量化。这里,博主使用的是 HuggingFaceEmbeddings,首次运行它会自动下载模型文件。当然,我天朝上国自有国情在此,从 Hugging Face 下载模型的问题在 AI 的道路上可谓是阴魂不散。因此,下面演示的是,通过镜像站来下载模型的方法。考虑到,默认的 sentence-transformers/all-mpnet-base-v2 模型是英文模型,我们引入一个对中文更友好的 GanymedeNil/text2vec-large-chinese 模型:

import os
from langchain_community.embeddings import HuggingFaceEmbeddings

os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
embeddings = HuggingFaceEmbeddings(model_name="GanymedeNil/text2vec-large-chinese")

# 单个句子
print(embeddings.embed_query("人生若只如初见,何事秋风悲画扇"))
# 多个句子
print(embeddings.embed_documents(["人生若只如初见,何事秋风悲画扇","等闲变却故人心,却道故人心易变"]))

当然,从最终向量化以后的结果来看,前者的向量维数只有 768,而后者则可以达到 1024。至此,我们就完成文本内容的向量化。一旦所有的文本信息都被转换为向量数据,此时,信息检索就完完全全地变成了一道数学题,由向量的余弦公式,我们可以非常容易地计算出两个向量间的夹角,这个夹角越小,则表明两个向量越相近。这就是向量相似性检索的基本原理,在此前的文章《视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索》一文中,博主曾经尝试利用这种思路来优化人脸识别效率,不知道大家是否还留下印象🙃…

link-img

视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索

VectorStore

OK,当我们将文本信息转化为向量以后,就需要考虑如何储存这些向量信息,如上文中给出的图片所示,LangChain 中支持诸如 FAISSMilvusPineconeChroma…等多种向量数据库,以及像 ElasticsearchPostgreSQL 这样的具备向量存储能力的传统数据库,我们可以根据自己的需求选择合适的存储方案。为了方便演示,这里我们使用 Facebook 出品的 FAISS,它不需要像 Milvus 那样准备额外的环境,使用体验上更接近 SQLite 这种嵌入式的数据库。当然,如果你比较中意于 Chroma,一切行止由心,LangChain 在自由度上拉满:

from langchain_community.vectorstores.faiss import FAISS
import pickle

# python -m pip install faiss-cpu
vector_store = FAISS.from_documents(documents, embeddings)
with open('./output/<Your-Persistence-Path>', "wb") as f:
    pickle.dump(vector_store, f)

FastGPT 中创建知识库的过程,原理完全相同,它支持从文档或者网址构建知识库,最终将向量化数据存储在 MonggoDB 中,目前,这个项目使用的 FastAI 4K 模型来源不明,而 Embedding-2 模型则是来自于 OpenAI。所以,FastGPT 这个网站最大的资金消耗主要来自 FastAI,目测是一个独立训练出来的私有模型。下图展示了博主在本地构建知识库的过程,如你所见,博主使用了 263 篇博客的内容来构建这个知识库:

本地知识库构建过程 本地知识库构建过程

LLaMA 的再度整合

坦白讲,纯 CPU 环境下的知识库非常花时间,譬如,博主使用 1024 个维度的向量来储存金庸先生的 15 部小说,经序列化后的文件体积高达 1.2G。果然,大模型相关的一切事物都非常庞大,这或许能帮大家理解类似 7B、13B、60B…等等的大模型的本质,因为它们同样是由大量的高精度的、浮点型的向量数据构成。可偏偏就是这些对人类来说毫无意义、宛如天书一般的数字,能够从你的只言片语中“理解”你的意图,回应你的各种输入,难道你还能笃定,这一切不是某种魔法,而提示词不过是施展魔法时吟唱的咒语?从这个角度来看,RAG 是什么呢?RAG 更像是你在吟唱咒语时随手拿一本魔法书在手里,你随时可以从这本魔法书里查询资料完善咒语。

RAG 如何工作 RAG 如何工作

RAG 与 Prompt

现在,我们顺着这个思路,将 LLaMA 和 LangChain 结合起来,这里的 LLaMA 指的是 llama.cpp + Qwen-1_8B-Chat,这个方案我们在上一期博客已经介绍过了,这里不再赘述。我们先来看看 Retrieve 的过程,即:如何从向量数据库中找到问题相关的内容,显然,这是 RAG 里的第一步:

with open("./output/个人博客.pkl", "rb") as f:
    vectorstore = pickle.load(f)

    query = 'Envoy 在微服务中的应用场景有哪些'
    topK = 3

    # 普通的相似性检索
    result = vectorstore.similarity_search(query=query, k=topK)
    # 带有分数的相似性检索
    result = vectorstore.similarity_search_with_relevance_scores(query=query, k=topK)
    # 最大边界相关性检索
    result = vectorstore.max_marginal_relevance_search(query=query, k=topK)

这里,博主例举出了常用的向量相似性检索的方法,大家可以参考 官方文档 选择合适的方法,通常情况下,使用 similarity_search() 方法即可;可有时候向量数据库会检索出与问题相关性不强的内容,此时,就需要使用 similarity_search_with_relevance_scores() 配合某个阈值进行处理;第三种方法,我个人感觉,其返回结果的相关性不如前两种方法。还是那句话,目前博主的 AI 道路还在摸索阶段,大家酌情使用即可。

from langchain.prompts.prompt import PromptTemplate

# 定义提示词模板
PROMPT_TEMPLATE = """
You are a helpful AI bot. Your name is {name}.
Please answer the question only based on the following context:

{context}

If the question is about your identity or role or name, answer '{name}' directly, no need to refer to the context.
If the context is not enough to support the generation of an answer, Please return "I'm sorry, I can't answer your question." immediately.
You have an opportunity to refine the existing answer (only if needed) with current context.
You must always answer the question in Chinese.

Queation: {question}
"""

# 根据问题做相似性检索
question = "Envoy在微服务中都有哪些应用场景"
documents = vector_store.similarity_search(question, k=3)
context = '\n\n'.join([document.page_content for document in documents])

# 从模板创建提示词并填充模板
prompt = PromptTemplate.from_template(PROMPT_TEMPLATE)
query = prompt.format(question=question, name="ChatGPT", context=context)

RAG 中的 A 是指 Augmented,表示增强的,这个增强体现在哪里呢?答案是提示词。如图所示,在这段提示词里面,博主试图让大语言模型假扮 ChatGPT,同时告诉它按照给定的上下文来回答问题,并且要求它必须使用中文进行回答。通过这段代码,我们就可以产生符合我们预期的输入,而接下来,我们只需要将其传递给大语言模型即可。除了这种方式以外,我们还可以按 OpenAI 的规范,使用 ChatPromptTemplate 来组织提示词:

from langchain_core.prompts import ChatPromptTemplate

# 定义提示词模板
PROMPT_TEMPLATE = """
You are a helpful AI bot. Your name is {name}.
Please answer the question only based on the following context:

{context}

If the question is about your identity or role or name, answer '{name}' directly, no need to refer to the context.
If the context is not enough to support the generation of an answer, Please return "I'm sorry, I can't answer your question." immediately.
You have an opportunity to refine the existing answer (only if needed) with current context.
You must always answer the question in Chinese. 
"""

# 根据问题做相似性检索
question = "Envoy在微服务中都有哪些应用场景"
documents = vector_store.similarity_search(question, k=3)
context = '\n\n'.join([document.page_content for document in documents])

# 从模板创建提示词并填充模板
prompt = ChatPromptTemplate.from_messages([
    ("system", PROMPT_TEMPLATE),
    ("user", "{question}"),
])
query = prompt.format(question=question, name="ChatGPT", context=context)

显而易见,第二种更好一点,因为它可以将问题和上下文显著地区分开来。当然,为了让 LLM 确信它就是 ChatGPT,博主在提示词工程上可没少下功夫。这其实佐证了博主一开始的观点:2023 年的提示词工程,2024 年的 GPTs,这两者并不冲突,而是一种相辅相成的关系。RAG 中的 Retrieve 和 Augmented 现在都已先后登场, 最后这个 G 自然是指 Generation,理论上剩下的工作交给 LLM 即可。可是如何你仔细想想,就会发现这里隐含着两个问题,其一是从向量数据库中检索到的信息,有一定的可能性超过 LLM 的上下文长度;其二是对上下文的约束越严格,LLM 就越显得 “弱智”,你必须要在发散和收敛中做出选择。以一言蔽之,LLM 有幻觉固然不好,可如果缺乏想象力,一切只怕是会变得更糟糕!不知大家是如何考虑这个问题的呢?

LangChain 中的 Chain

在 LangChain 的诸多概念中,Chain 或许是最抽象、最重要的哪一个,因为它就像一个管道一样,可以讲我们这篇文章中提到的各种组件串联起来。譬如, LLMChain 可以将一个LLM 和 Prompt 串联起来,RetrievalQA 可以配合 LLM 和知识库实现简单的 Q&A,ConversationalRetrievalChain 可以配合 LLM、知识库和聊天历史实现对话式检索。起初,我对于 RetrievalQA 这个类是极其排斥和反感的,因为它不论是看起来还是用起来,都像极了一个 Chain,可它就偏偏不是一个 Chain,真是奇哉怪也!

from langchain.chains import LLMChain
from langchain_community.llms import OpenAI
from langchain_core.prompts import PromptTemplate

prompt_template = "Tell me a {adjective} joke"
prompt = PromptTemplate(
    input_variables=["adjective"], 
    template=prompt_template
)
llm = LLMChain(llm=OpenAI(), prompt=prompt)

刚刚提到,我们大体上有两种方式来将 LLM 和 LangChain 结合起来,一种是基本的 Q&A,一种是对话式检索。其中,streaming 参数用于支持流式返回,return_source_documents 参与用于控制是否返回引用的文档信息,ConversationBufferMemory 组件用于处理对话历史。下面给出具体的实现代码:

# 基本的 Q&A
def get_basic_qa_chain(baseUrl='', apiKey='', storeFilePath=''):
    llm = ChatOpenAI(
        model_name="gpt-3.5-turbo", 
        temperature=0.75, 
        openai_api_base=baseUrl, 
        openai_api_key=apiKey,
        streaming=True
    )
    retriever = load_retriever(storeFilePath)
    chain = RetrievalQA.from_chain_type(
        llm=llm, 
        chain_type="stuff",
        retriever=retriever, 
        return_source_documents=True,
    )
    return chain

# 对话式检索
def get_conversational_retrieval_chain(baseUrl='', apiKey='', storeFilePath=''):
    llm = ChatOpenAI(
        model_name="gpt-3.5-turbo", 
        temperature=0.75, 
        openai_api_base=baseUrl, 
        openai_api_key=apiKey,
        streaming=True
    )
    retriever = load_retriever(storeFilePath)
    memory = ConversationBufferMemory(
        memory_key="chat_history", 
        return_messages=True, 
        input_key="question", 
        output_key="source_documents"
    )
    chain = ConversationalRetrievalChain.from_llm(
        llm=llm, 
        retriever=retriever, 
        memory=memory, 
        return_source_documents=True, 
    )
    return chain

现在,我们就可以调用 Chain 来和大语言模型及知识库进行交互:

# 构造 Chain,使用本地模型
chain = get_conversational_retrieval_chain_("http://localhost:8080/v1/", 'sk-1234567890',  './output/个人博客.pkl')

# 根据问题做相似性检索
question = "Envoy在微服务中都有哪些应用场景"
documents = vector_store.similarity_search(question, k=3)
context = '\n\n'.join([document.page_content for document in documents])

# 从模板创建提示词并填充模板
prompt = ChatPromptTemplate.from_messages([
    ("system", PROMPT_TEMPLATE),
    ("user", "{question}"),
])
query = prompt.format(question=question, name="ChatGPT", context=context)

# 方式一: 同步调用
result = chain.invoke(query)

# 方式二: 流式调用
result = chain.stream(query)

如下图所示,在经过反复地调试和优化以后,我们可以针对博客中的内容进行提问,并且程序会返回与问题相关的文档信息,这个结果整体而言还是挺不错的。当然,距离 FastGPT 这种相对完善的产品还是挺遥远的,使用 CPU 进行推理的 llama.cpp 除了生成速度慢以外,还会出现缓存 kv cache 不足的问题。庆幸的是,在折腾 LangChain 的过程中,逐渐理解了 RAG 以及 LangChain 的整体思路。在 LangChain 的 官方文档 中,官方提供了使用 LangChain 落地 AI 应用的示例,这篇文章主要参考了 RAG 这一篇,大家可以特别关注一下。

本地 AI 知识库运行结果展示 本地 AI 知识库运行结果展示

LangChain 除了概念多以外,API 变动非常频繁,经常出现破坏性的变更,而且官方现在主要在推 LangChain Expression Language(LCEL),所以,网络上的资料经常都是过期的,建议大家有时间还是去看纯英文的官方文档。当然,大家有兴趣的话,可以考虑下 LangChain 的替代品,譬如 AutoChainEmbedchain,它们的 API 毫无疑问都比 LangChain 简单,实际使用情况,只有靠各位自己去体验啦,哈哈!

本文小结

在探索 AI 的过程中,一个令人难过的事实是:为了创作这篇大约1.5万字的文章,博主先后花了一周左右的时间来做实验,然后花了两天的时间来形成文字,而在将这些博客内容 “投喂” 给 AI 以后,它可以迅速 “描摹” 出我的轮廓。你不得不承认,这个世界的信息实在多到爆炸,以致于我们绝无可能构建出一个无穷大的知识库。AI 更像是这个复杂时代地观察者,可以帮助我们更快、更好地去理解这个世界,虽然这个代价是 AI 可能会产生更多噪音,这有一点像什么呢?就像你在看视频时开启倍速播放,或许你节省了时间、提升了节奏,可终究失去了某种身体力行的体验感,正如在这篇文章里,我使用 LLaMA 和 LangChain 实现 RAG 的过程,于屏幕前的你而言,不过是互联网海洋里鼠标轻轻掠起的浪花。对我而言,写作的整个过程,与 RAG 更是何其的相似,我在输出这篇文章的同时,查阅大量的资料,最终,它们成为了我认知、感悟以及价值观的一部分,而这,便是自我的增强。