欢迎大家来到企业级AI售前机器人实战系列文章: 从0到1完成一个企业级AI售前机器人的实战指南。
本篇是该系列的第三篇,核心内容是:售前AI系统中RAG工程的搭建细节和优化
上一篇我们讲了RAG的原理和RAG的数据要如何整理,本篇我们会附上代码同时讲一下RAG工程的搭建和优化原理。
同时,本文还会解决上一篇留下的四个问题:
- 只能进行文本回复,无法提供相关的图片、视频等能力。
- 如果用户query不标准,问题不全,我们的知识库可能匹配不到内容。
- 知识库中的内容仍然存在匹配错误的情况。例如:用户问A产品的价钱,我们知识库筛选出了B产品的价钱,然后回复给了用户。
- 经典的中文二义性问题。用户的问题可以用A来回答,也可以用B来回答,怎么办?
关于我
我是一个十年老程序员、React Contributor,三年前转型至AI在应用层的设计与落地。
目前转型成功,并担任多个AI项目负责人,已经完成了多款智能问答产品的上线、以及TOB产品的功能AI化升级。
本专栏将会基于我过去几年的经验,对各类AI应用落地的方案和思路积累了很多踩坑和迭代经验,进行全方位的AI产品的核心流程拆解!
我相信AI在未来将会是基础设施,而对于普通人而言,基础设施的机会不在基础设施本身,在应用层谋求发展可能是一个不错的出路。
加油!共勉!
回归正题:
RAG工程搭建
RAG的工程搭建有两种方案,调用云端知识库服务和自己实现知识库服务。
这两种方式我这边不同的项目都有应用,根据实际的表现来看,效果基本上是差不多的,毕竟做出来都是一套流程。
没有特殊要求的话,直接选云端服务就行,自己实现知识库服务的场景通常是以下三种:
- 有本地部署的需求。
- 对流程的细节有自己业务上的处理。
- 私有数据不能外泄。
我们分别来看下云端服务和自己实现都怎么用:
一. 调用云端服务
云端知识库服务我们使用火山云的知识库服务来做示例:
调用火山云端的知识库服务,只需要三个步骤:创建知识库
,上传内容
,接口调用
。
首先我们打开知识库控制台,这里可以看到向量数据和知识库两个选项,这里我们选择知识库,向量数据库是接下来自己实现知识库时用来存储向量的数据库。
创建知识库
创建知识库的时候,我们要确定使用的数据类型、向量化模型、向量维度、数据处理方式、使用的索引算法等。
数据类型分为结构化的数据
和非结构化的数据
,如果我们是用excel承载的数据,例如QA问答对、商品数据等,那么我们的数据就是结构化数据,如果我们的数据是以文档为主的,例如文章、报告等,我们的数据就是非结构化数据
专栏上一篇【RAG工程的数据处理】我们讲过了,目前我们基本都是选择混合检索来满足我们在生产环境中同时使用语义理解和关键词理解的需求,所以向量化模型的选择我们会选择embedding模型
+关键词模型
使用。同时我们可以尽可能选择更新的模型以及向量维度更高的模型。
简单介绍一下向量维度,可以这么理解,更高维度的向量会比低维度的向量所包含的信息更多。通俗理解可以是:三维包含长宽高,而二维只能包含长宽。三维向量会比二维向量包含了更多一层的含义。 所以我们选择更高维度的向量就会保留数据中的更多的信息。
当然理论上也并不是维度越高越好的,在考虑冗余或者噪声的情况下,高纬度可能会存在效果不好的情况,不过这种情况在实战中基本很少见,如果真的遇到了,切换到低纬度即可。
数据处理方式是根据我们的数据类型来决定的,非结构化数据就需要做切片处理,结构化的数据需要选择字段名称。
对于我们来说是使用了结构化的数据,所以我们只需要按照我们excel中的字段进行设置即可。
索引算法的选择就默认HNSW-Hybrid
即可,这个算法就是适合混合检索的场景。其他还有几种:
- HNSW:全称是 Hierarchical Navigable Small World,一种用于在高维空间中采用 ANN 搜索的数据结构和算法,是基于图的索引。
- HNSW_HYBRID:支持混合索引的 hnsw 算法。
- FLAT:暴力索引,搜索时遍历整个向量数据库的所有向量与目标向量进行距离计算和比较,查询速度较慢,但是能提供100%的检索召回率,适用于向量候选集较少,且需要100%检索召回率的场景。
- IVF:倒排索引,利用倒排的思想保存每个聚类中心下的向量,每次查询向量的时候找到最近的几个中心,分别搜索这几个中心下的向量,速度较快,但是精度略低。
- DISKANN:基于 Vamana 图的磁盘索引算法,将 Vamana 图与 PQ 量化压缩方案结合,构建DiskANN索引。
最终我们创建的知识库大致如下:
另外需要注意的是,量化方式我们是要保证精度的,一律不选择压缩,选择float
即可。
CU的选择上1CU
就能够支持100QPS
,够用的话就不要再加了,毕竟收费的。
云端知识库的费用大约一天十块钱,收费逻辑是向量化模型调用(1元/百万tokens) + 计算资源(0.45 元/CU/小时) + 存储资源(0.0015元/GB/小时)。
完成知识库创建之后,就可以进行内容上传了,把我们上一篇处理完的数据上传知识库,然后我们就可以通过API进行知识库检索了
调用代码如下:
封装的知识库文件,knowledge/index.js
:
import axios from 'axios';
import dotenv from "dotenv";
import { Signer } from '@volcengine/openapi';
dotenv.config() // 加载环境变量
// 配置参数
const collectionName = "sq_data";
const projectName = "default";
const ak = process.env.DB_AK; // 缓存自己账号的AK
const sk = process.env.DB_SK; // 缓存自己账号的SK
const knowledgeBaseDomain = "api-knowledgebase.ml_platform.cn-beijing.volces.com";
// 签名
function signer({ pathname, method, body, region, params }) {
const requestObj = {
region,
headers: {
Accept: 'application/json',
'Content-type': 'application/json'
},
method,
body,
pathname,
params,
}
const signer = new Signer(requestObj, 'air')
signer.addAuthorization({
accessKeyId: ak,
secretKey: sk,
})
return requestObj
}
// 调用知识库检索,返回符合分数要求的数据
// query 需要进行检索的问题
// limit = 10 知识库检索条数
// dense_weight = 0.7 用于控制检索时语义相似程度的重要性权重,越偏向 1 表示越倾向于语义检索,越偏向 0 则表示越偏向于关键词匹配检索。
// score = 0.65 知识库有效数据的分数限制
export default async function searchKnowledge({ query, limit = 10, dense_weight = 0.7, score = 0.65 }) {
const requestBody = {
project: projectName,
name: collectionName,
query: query,
limit: limit,
pre_processing: {
need_instruction: true,
return_token_usage: true,
messages: [
{ role: "user", content: query }
]
},
dense_weight: dense_weight,
post_processing: {
rerank_only_chunk: true
}
};
const body = JSON.stringify(requestBody);
let requestObj = signer({
pathname: '/api/knowledge/collection/search_knowledge',
method: 'POST',
region: 'cn-beijing',
body
})
const response = await axios({
method: requestObj.method,
url: `http://${knowledgeBaseDomain}${requestObj.pathname}`,
headers: requestObj.headers,
data: requestObj.body
});
let result = []
response.data.data.result_list.forEach(item => {
if (item.score >= score) {
result.push({ ...item })
}
})
return result
}
主文件,src/index.js
:
export default async function main({ query }) {
// 其他逻辑
// 调用知识库
let knowledgeData = await searchKnowledge({ query })
console.log("知识库:" + JSON.stringify(knowledgeData))
// 其他逻辑
}
我们可以在调用知识库时,就定义好,知识库检索条数(limit)、语义相似度的权重(dense_weight)、限制的最低分数(score)。
limit
限制的数量取决于我们知识库中的数据是否具有很强的关联性,比如切片类的数据,一个文档切成了十片、二十片,用户query的答案可能出现在多个切片中,那么就需要把limit的条数设置的多一些。
dense_weight
越偏向 1 表示越倾向于语义检索,越偏向 0 则表示越偏向于关键词匹配检索。根据我们的数据中是否具备大量的关键词、名词类数据来决定是否更偏向于关键词匹配检索,通常默认在0.6左右。
score
是我们对于知识库检索结果语义相似度达到的限制,当相似度达到我们设置的默认分数0.65时,我们才把内容返回给大模型。
我们输入query:天穹科技有哪些行业认证
,测试一下,可以看到src/index,js
打印的知识库日志:
{
id: '_sys_auto_gen_doc_id-7216196016653048918-0',
content: 'tag:\n' +
'id:1\n' +
'question:天穹科技有哪些行业认证?\n' +
'answer:ISO 9001质量管理体系认证、国家高新技术企业、AI机器人行业安全标准认证\n' +
'meta:',
score: 0.5584835410118103,
point_id: '_sys_auto_gen_doc_id-7216196016653048918-0',
chunk_id: 0,
process_time: 1746860682,
doc_info: {
doc_id: '_sys_auto_gen_doc_id-7216196016653048918',
doc_name: 'qa.xlsx',
create_time: 1746860674,
doc_type: 'xlsx',
doc_meta: '[{"field_name":"doc_id","field_type":"string","field_value":"_sys_auto_gen_doc_id-7216196016653048918"}]',
source: 'tos_fe'
},
recall_position: 2,
chunk_type: 'structured',
chunk_source: 'document',
update_time: 1746860682,
table_chunk_fields: [ [Object], [Object], [Object], [Object], [Object] ],
FromRecallQueue: 'master_recall'
},
其中我们需要关注的内容是:content
,这里面就是我们知识库中存储的完整信息。下面我们解决遗留问题时,会再次用到。
接下来我们先简单说一下自己实现知识库:
二. 自己实现知识库服务
当出现以下场景时,我们就不得不考虑自己实现知识库了,
- 有本地部署的需求。
- 对流程的细节有自己业务上的处理。
- 私有数据不能外泄。
自己实现知识库,我们需要自己选择embedding模型,选择向量数据库来完成知识库的流程。
实现的流程还是这张流程图:
这里不贴怎么实现知识库的功能的代码了,贴出来太多了,而且每个团队实现的细节都会根据自己的业务有所调整,不过流程都是上面提到的这个流程,这里简单给大家过一下逻辑,一看就懂:
- 我们需要实现不同类型的数据处理,非结构化数据的切片,结构化数据的拆解
- 我们需要实现对处理后数据进行embedding然后进入向量数据库的流程
- 我们需要实现用户的query进行embedding然后在向量数据库中进行检索
- 我们需要实现不同的检索方式,然后结合分析来返回最终的检索结果
这里需要注意的一点是,我们对本地知识做向量化操作和对用户的query做向量化操作时所使用的embedding模型,必须是同一个模型,否则在向量数据库中计算向量距离时就会出现非常大的偏差。
比如要实现火山云这套知识库服务的话,我们选择embedding模型是:火山向量模型
, 选择的向量数据库是:vkingdb
。
遗留问题优化方案
接下来我们解决一下上一篇中的遗留问题:
只能进行文本回复,无法提供相关的图片、视频等能力。
RAG系统想要加入图片、视频等能力,有两种方案可以解决:元数据
和描述
。
第一种方案:元数据
是应用在结构化数据中,在结构化数据中添加有meta
字段,meta
字段代表着与我们的这条数据有关的其他数据。
当用户的query在知识库检索时检索到了我们这条数据,我们就可以根据meta
字段进行相应的图片\视频等内容的输出。
例如,在售前场景中。我们有一批专利图片,我们要利用元数据的方案就要在我们的excel中加入图片地址信息:
当用户询问专利相关问题时,我们的程序会获取到meta
字段,还记得方才我们说的那个知识库响应内容content
字段么?
meta
字段就在里面,我们的程序拿到后就可以进行处理,返回给前端显示对应的专利图片。
第二种方案:描述
是应用在非结构化的数据中,通过自然语言对图片\视频进行描述,然后把描述文案向量化之后添加到向量数据库中。
这样当用户的query和我们向量数据库中的内容进行匹配时,就可以匹配到一个图片信息,然后我们的大模型可以根据描述介绍图片,我们的程序可以根据信息返回图片。
还是刚才那个例子我们有一批专利图片,如果是使用描述
的方案:
我们可以进行专利的描述,然后把描述文案加入到向量数据库中:
- 企业申请的XXX专利。
- 企业获取的XXX授权。
- 企业的国际PCT专利。
当用户询问专利相关问题时,知识库就会检索到相关的内容,我们就可以进行图片化的回复了。
如果用户query不标准,问题不全,我们的知识库可能匹配不到内容。
这个问题的出现是因为:RAG系统本身是不支持上下文的:
例如,我们先问:你们企业有哪些授权?
AI回复之后,我们又问:专利呢?
这时需要进入RAG系统参与匹配的应该是你们企业有哪些专业?
而不是专利呢?
所以这时候就需要上下文分析的能力来分析上下文得到用户的真实问题你们企业有哪些专业?
我们的解决方案是使用提示词来进行上下文分析
+ query改写
。
上下文分析提示词如下:
## 业务知识
【这里根据自己的业务场景,添加必须让大模型知道的业务知识,例如对某些名词的解释】
## 要求
- 根据user的上下文对话,分析出user本次对话的真实意图。
- 必要的知识放在【业务知识】中,查询业务知识的信息与user对齐概念。
- 把user最终的真实意图转化成与上下文文风一致的问题后直接输出,不要输出分析过程
- 输出格式为{user:真实意图}
## 上下文
question:【上一次分析的结果query】
answer:【回复内容】
question:【用户本次query】
## 输出
上下文分析
提示词用在解决使用RAG系统时,问题需要有上下文关联性的场景,可以帮助我们获得完整的用户意图。
query改写提示词如下:
## 业务知识
【这里根据自己的业务场景,添加必须让大模型知道的业务知识,例如对某些名词的解释】
## 要求
- 对问题进行一次改写,改写为一条新的问题。
- 需要保持语义一致性,核心意图不变,允许根据上下文扩展关联信息。
- 如果只有一个名词,用户的默认意图是需要解释。
- 用词保持简单,保证新问题是一句话,没有过多的冗余内容。
- 改写标准为主语 + 谓语 + 宾语的标准语法,不要使用倒装句等其他语法。
- 输出格式为{user:新问题}
## 上下文
question:【上一次分析的结果query】
answer:【回复内容】
## 问题
【用户本次query】
## 输出
query改写
提示词用在我们为了提高准确性,会通过改写query,对RAG系统进行多次匹配的场景。可以帮助我们把用户的非标准问题改写成更容易匹配我们的知识库的问题。
比如这个提示词,我们的知识库中全都是主谓宾组成的标准语法,那么我们就更希望把用户的问题也全部改成主谓宾的标准语法。
知识库中的内容仍然存在匹配错误的情况。例如:用户问A产品的价钱,我们知识库筛选出了B产品的价钱,然后回复给了用户。
这个问题的出现是因为:RAG系统是根据语义进行匹配的,虽然现在大家都是用混合检索了,但是在匹配的过程中还是会出现匹配错误的情况
我们当然是不允许把错误的信息返回给用户的。
所以在匹配到答案之后,需要再验证一次这个答案是否解决了用户的问题。
例如:用户问A会议的开始时间,我们拿到了B会议的信息,其中也有开始时间。如果不做相关性验证,就对用户造成了误导。
提示词如下:
## 业务知识
【这里根据自己的业务场景,添加必须让大模型知道的业务知识,例如对某些名词的解释】
## 资料
【知识库匹配出来的答案】
## 问题
【用户的query】
## 要求
- 必要的知识放在【业务知识】中,查询业务知识的信息与user对齐概念。
- 判断资料是否能够有效的回复user的问题。
- 如果资料是有效的,返回'''Y''',否则返回'''N''',不要输出任何其他内容。
## 输出
当相关性验证没有通过时,我们还可以调用query改写,再进行重试。或者把知识库匹配到的问题直接以相似问
的形式返回给用户。
- query改写的逻辑是把query的语法、用词等修改成和我们的知识库中存的数据相似性高一些的新query,以此来增加匹配度。
- 相似问的逻辑是我们把问题抛回给用户,让用户自己进行选择他想问的问题或者重新提问。
经典的中文二义性问题。用户的问题可以用A来回答,也可以用B来回答,怎么办?
中文是具有的二义性问题的:
能吃多少吃多少?
,是多吃点还是少吃点?咬死了猎人的狗。
,是要死了猎人的狗,还是猎人的狗被咬死了?
这些二义性问题还可以根据上下文来大概进行判断,但是还有一些二义性问题,就无法利用上下文了,例如:
Q1:清华大学怎么样? Q2:计算机专业怎么样?
用户是问计算机专业怎么样?
还是清华大学的计算机专业怎么样?
,这种问题结合上下文和不结合上下文是完全两个问题。
对于这种场景的问题,给大家分享一个我的解决方案,两个问题答案我们都回复给用户,利用结构化的表达方式,无论用户的真实意图是哪一个,都可以感受到被回答了。
回复示例:
【内容一:计算机专业介绍,回复计算机专业怎么样】
上下文衔接内容
【内容二:计算机专业的前景、发展等内容】
上下文衔接内容
【内容三:清华大学的计算机专业优势、介绍。】
既然回答哪个都有可能是错的,那么我们就全都要,全部回复即可。
结语
传统的RAG目前比较主流存在的问题有以下几个:
- 知识库内容缺失:现有的文档其实回答不了用户的问题,系统有时被误导,给出的回应其实是“胡说八道”,理想情况系统应该回应类似“抱歉,我不知道”。
- TopK截断有用文档:和用户查询相关的文档因为相似度不足被TopK截断,本质上是相似度不能精确度量文档相关性。
- 上下文整合丢失:从数据库中检索到包含答案的文档,因为重排序/过滤规则等策略,导致有用的文档没有被整合到上下文中。
- 有用信息未识别:受到LLM能力限制,有价值的文档内容没有被正确识别,这通常发生在上下文中存在过多的噪音或矛盾信息时。
- 提示词格式问题:提示词给定的指令格式出现问题,导致大模型/微调模型不能识别用户的真正意图。
- 准确性不足:LLM没能充分利用或者过度利用了上下文的信息,比如给学生找老师首要考虑的是教育资源的信息,而不是具体确定是哪个老师。另外,当用户的提问过于笼统时,也会出现准确性不足的问题。
- 答案不完整:仅基于上下文提供的内容生成答案,会导致回答的内容不够完整。比如问“文档 A、B和C的主流观点是什么?”,更好的方法是分别提问并总结。
总的来看:
- 问题1-3:属于知识库工程层面的问题,可以通过完善知识库、增强知识确定性、优化上下文整合策略解决。
- 问题4-6:属于大模型自身能力的问题,依赖大模型的训练和迭代。
- 问题7:属于RAG架构问题,更有前景的思路是使用Agent引入规划能力。
我们今天讲解的主要是知识库工程层面的问题的解决方案,大模型自身能力的问题的解决方案可以交给微调来解决。而RAG架构问题目前大家也在不断提供新的解决方案例如Graph RAG
之类的。
后续我会不断把新的内容搬到这个专栏,希望这个系列能够打造成帮助大家落地AI产品时的实战手册!
大家多多点赞 + 关注,给点动力,更新的快快的!
提前订阅不迷路:售前AI机器人掘金专栏地址。
☺️你好,我是华洛,如果你对程序员转型AI产品负责人感兴趣,请给我点个赞。
你可以在这里联系我👉www.yuque.com/hualuo-fztn…
已入驻公众号【华洛AI转型纪实】,欢迎大家围观,后续会分享大量最近三年来的经验和踩过的坑。
专栏文章
# 从0到1打造企业级AI售前机器人——实战指南二:RAG工程落地之数据处理篇🧐