返回顶部
热门问答 更多热门问答
技术文章 更多技术文章

基于langchaingo实现知识库对接本地模型ollama的分步探索

[复制链接]
链载Ai 显示全部楼层 发表于 14 小时前 |阅读模式 打印 上一主题 下一主题

前言

在前边的两篇文章中,首先介绍了当下最火热的本地大语言模型管理框架 ollama[1] 的入门,之后又单独开了一篇介绍 rag[2] 的核心概念及问题,谈到 rag 的问题之后,最后我得出的结论是,劝退,劝退你,也劝退我自己。

但,且慢,上篇文章已经把理论,以及流程都介绍完了,那,不亲自上手玩一玩,岂不是显得太过纸上谈兵了。

因此,这篇就是通过一个简单的示例,结合 langchaingo 来实现一下自己开发 rag 应用的整个流程。

本文项目代码地址:langchaingo-ollama-rag[3]

正文

前文说到,rag 的核心流程大概有如下几步:

  • 先将知识库内容切分
  • 再把切分后的内容向量化存入向量数据库
  • 用户提问之后,先将问题在向量库中进行相似性检索,找出匹配度高的答案。
  • 然后把查询出来的结果,包装好 Prompt。
  • 最后调用大语言模型,让大语言模型基于上一步的结果进行分析并形成最终的答案,返回给用户。

接下来我就通过代码,来按照上边的流程,做下实践。

前置准备

关于如何配置 ollama 的模型,这里就不再赘述了,ollama 的用法详见我第一篇文章。

这里向量数据库使用的是 qdrant,可通过如下方式快速拉起测试环境。

$dockerpullqdrant/qdrant
$dockerrun-itd--nameqdrant-p6333:6333qdrant/qdrant

使用如下命令可创建一个集合:

$curl-XPUThttp://localhost:6333/collections/langchaingo-ollama-rag\
-H'Content-Type:application/json'\
--data-raw'{
"vectors":{
"size":768,
"distance":"Dot"
}
}'

使用如下命令可删除该集合:

$curl--location--requestDELETE'http://localhost:6333/collections/langchaingo-ollama-rag'

先切分文档

文档我准备的内容取自个人之前发布的一篇小作:我这一生--一个显示器的故事[4]

主要代码如下:

//TextToChunks函数将文本文件转换为文档块
funcTextToChunks(dirFilestring,chunkSize,chunkOverlapint)([]schema.Document,error){
file,err:=os.Open(dirFile)
iferr!=nil{
returnnil,err
}
//创建一个新的文本文档加载器
docLoaded:=documentloaders.NewText(file)
//创建一个新的递归字符文本分割器
split:=textsplitter.NewRecursiveCharacter()
//设置块大小
split.ChunkSize=chunkSize
//设置块重叠大小
split.ChunkOverlap=chunkOverlap
//加载并分割文档
docs,err:=docLoaded.LoadAndSplit(context.Background(),split)
iferr!=nil{
returnnil,err
}
returndocs,nil
}

这里将 chunkSizechunkOverlap 两个变量参数化,也是为了能够更加清晰地看到参数所代表的含义,以及对于整个流程的影响。

比如我默认情况下,执行效果如下:

$gorunmain.gofiletochunks
2024/04/1723:03:32INFO[转换文件为块儿成功,块儿数量:40]

?块儿内容==>工程师对我虽然恩为再造,但我很长一段时间里并不感谢他,因为他既然塑我成异类,就应该把我留在他身边,那样我也会好过一些,然而或许是他马虎,把我丢在了旁的显示器中间,我的一生坎坷也正是自此开始。

几天后,我与其它四十九个同胞一起被货车拉到一个毫不起眼的地方,几个男人搬运着我们。

我转身问在路上认识的小杰:“小杰,他们这是要干什么?”

小杰转过脸:“你不知道吗?把我们装备到网吧里呀。”
?块儿内容==>小杰转过脸:“你不知道吗?把我们装备到网吧里呀。”

“网吧是干什么的?”

“网吧是让我们的爸爸赚钱的呀。怎么了?李尤?”

我不仅愤然:“他拿我们赚钱使,你还叫他爸爸,你是不是脑子进水了?”

......

通过上边切分之后,可以看出,单个 chunkSize 将决定单个块儿的内容大小,chunkOverlap 将决定有多少向前重复的内容。同理,当我调试时把块儿调大,那么最终块儿的数量就会减少:

$gorunmain.gofiletochunks-c500
2024/04/1723:06:30INFO[转换文件为块儿成功,块儿数量:13]

块儿文本向量化

主逻辑代码如下:

//storeDocs将文档存储到向量数据库
funcstoreDocs(docs[]schema.Document,store*qdrant.Store)error{
//如果文档数组长度大于0
iflen(docs)>0{
//添加文档到存储
_,err:=store.AddDocuments(context.Background(),docs)
iferr!=nil{
returnerr
}
}
returnnil
}

执行如下命令可将切分后的文本块儿存入向量数据库:

$gorunmain.goembedding
转换块儿为向量成功

获取用户输入并查询向量数据库

主逻辑代码如下:

//useRetriaver函数使用检索器
funcuseRetriaver(store*qdrant.Store,promptstring,topkint)([]schema.Document,error){
//设置选项向量
optionsVector:=[]vectorstores.Option{
vectorstores.WithScoreThreshold(0.80),//设置分数阈值
}

//创建检索器
retriever:=vectorstores.ToRetriever(store,topk,optionsVector...)
//搜索
docRetrieved,err:=retriever.GetRelevantDocuments(context.Background(),prompt)

iferr!=nil{
returnnil,fmt.Errorf("检索文档失败:%v",err)
}

//返回检索到的文档
returndocRetrieved,nil
}

执行如下命令可得到如下结果:

$gorunmain.goretriever-t3
请输入你的问题:规定
?根据输入的内容检索出的块儿内容==>果,转身笑脸同那小孩儿讲:“你换一台机器吧。”接着告诉伙计:“如果有人来玩这一台,你就说坏了。我明天找那个工程师来,他一定知道怎么回事儿!”
?根据输入的内容检索出的块儿内容==>利、物质的享受,却不去思考金钱的短暂,物质的虚无;旁的显示器似也承认并接受那规划,并坦率的说:“显示器嘛,不就是用来显示的。”至于显示什么,它们也全不管。身处浑浊之中,独我欲清何其难哉!我于是沉默了。
?根据输入的内容检索出的块儿内容==>我被成功改造,在后来的岁月里,我也渐渐变得健忘,有时候有人朝我讥吼“不自主,毋宁死啊”,“去他妈的规定”说完他们便笑作一团。而我也只是静静地看着他们发狂地吼,发疯地笑,不做一言。后来偶然听说网吧里的电脑两年换一次新,我于是又像牢犯盼出狱那样地期待着更新换代。

将检索到的内容,交给大语言模型处理

主逻辑代码如下:

//GetAnswer获取答案
funcGetAnswer(ctxcontext.Context,llmllms.Model,docRetrieved[]schema.Document,promptstring)(string,error){
//创建一个新的聊天消息历史记录
history:=memory.NewChatMessageHistory()
//将检索到的文档添加到历史记录中
for_,doc:=rangedocRetrieved{
history.AddAIMessage(ctx,doc.PageContent)
}
//使用历史记录创建一个新的对话缓冲区
conversation:=memory.NewConversationBuffer(memory.WithChatHistory(history))

executor:=agents.NewExecutor(
agents.NewConversationalAgent(llm,nil),
nil,
agents.WithMemory(conversation),
)
//设置链调用选项
options:=[]chains.ChainCallOption{
chains.WithTemperature(0.8),
}
//运行链
res,err:=chains.Run(ctx,executor,prompt,options...)
iferr!=nil{
return"",err
}

returnres,nil
}

通过该方法拿到的结果,我这边调试下来,拿到的总是英文结果,各种调试 prompt,都没有成功,这里猜测可能是跟选用的模型有关系,暂时通过将得到的结果再次丢给大模型,做一次翻译来解决。

//Translate将文本翻译为中文
funcTranslate(llmllms.Model,textstring)(string,error){
completion,err:=llms.GenerateFromSinglePrompt(
context.TODO(),
llm,
"将如下这句话翻译为中文,只需要回复翻译后的内容,而不需要回复其他任何内容。需要翻译的英文内容是:\n"+text,
llms.WithTemperature(0.8))
iferr!=nil{
return"",err
}
returncompletion,nil
}

我之所以怀疑可能是模型的因素,是因为我在调试这段翻译功能时发现,使用 mistral 模型总是很难直接把英文转换为中文,虽然功能上他给转换了,但是仍旧还会输出一些英文,所以应该是模型的原因。

经过两个方法的加持,接下来就是见证奇迹的时刻了:

$gorunmain.gogetanswer
请输入你的问题:这篇文章讲了什么
?原始回答==>Thisarticletalksaboutthespeaker'sinnerstrugglebetweenadheringtotruthandenjoyingmaterialpleasures.Thespeakerexpressesfrustrationwiththefactthattheymustconformtosocietalexpectationsoflivingaccordingtoparallelrules,andquestionsthemeaningoftheirownexistenceinthisworld.Theyalsoreflectonthefutilityofrealityandcontemplatesuicideasameansofescapefromtheirsuffering.However,theyultimatelyremainsilentandcontinuetoendurethepain.

?翻译后的回答==>这篇文章讲述了讲话者内心的斗争,他在追求真相和愉快的物质待遇之间犹豫不决。他表达了对社会对我们所定义生活中必须遵守平行原则的不满,并怀疑自己在这个世界中的意义。他还思考现实的无用和死亡作为逃离他的痛苦的方式。然而,最终他保持沉默并继续忍受痛苦。

如上得到的结果虽然不算很贴切,但感觉还算是相对沾边的,这就是我上篇文章提到的,当你掌握了整个概念,也学会了整个流程的玩法,最终得到的结果,可能只有实际预期的 50%不到。

那么如何通过优化来提高这个结果所达到的预期值呢,这就要从如上步骤的每一个细节,每一个参数开始调优,且这种调优并不是一劳永逸的,还需要结合原始文档的格式,内容等情况进行不同的调整。

这也是我上篇为什么得出劝退的结论的原因,而为了印证劝退的合理性,本文应运而生。也算是给我自己一个交代,关于 rag,关于大语言模型,可先到此告一段落。


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

链载AI是专业的生成式人工智能教程平台。提供Stable Diffusion、Midjourney AI绘画教程,Suno AI音乐生成指南,以及Runway、Pika等AI视频制作与动画生成实战案例。从提示词编写到参数调整,手把手助您从入门到精通。
  • 官方手机版

  • 微信公众号

  • 商务合作

  • Powered by Discuz! X3.5 | Copyright © 2025-2025. | 链载Ai
  • 桂ICP备2024021734号 | 营业执照 | |广西笔趣文化传媒有限公司|| QQ