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

推荐订阅源

NISL@THU
NISL@THU
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
D
Darknet – Hacking Tools, Hacker News & Cyber Security
阮一峰的网络日志
阮一峰的网络日志
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
WordPress大学
WordPress大学
IT之家
IT之家
Cyberwarzone
Cyberwarzone
博客园_首页
博客园 - 聂微东
V
Visual Studio Blog
Cisco Talos Blog
Cisco Talos Blog
V
Vulnerabilities – Threatpost
Google DeepMind News
Google DeepMind News
Schneier on Security
Schneier on Security
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
The Hacker News
The Hacker News
雷峰网
雷峰网
Last Week in AI
Last Week in AI
Spread Privacy
Spread Privacy
L
Lohrmann on Cybersecurity
O
OpenAI News
人人都是产品经理
人人都是产品经理
AWS News Blog
AWS News Blog
小众软件
小众软件
T
Tailwind CSS Blog
The Cloudflare Blog
L
LINUX DO - 最新话题
有赞技术团队
有赞技术团队
Know Your Adversary
Know Your Adversary
The GitHub Blog
The GitHub Blog
L
LINUX DO - 热门话题
Y
Y Combinator Blog
Stack Overflow Blog
Stack Overflow Blog
B
Blog
MyScale Blog
MyScale Blog
S
SegmentFault 最新的问题
S
Schneier on Security
The Last Watchdog
The Last Watchdog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
大猫的无限游戏
大猫的无限游戏
罗磊的独立博客
Blog — PlanetScale
Blog — PlanetScale
博客园 - Franky
I
InfoQ
P
Proofpoint News Feed
量子位
S
Security @ Cisco Blogs

二丫讲梵

学习周刊-总第258期-2026年第15周 学习周刊-总第257期-2026年第14周 学习周刊-总第256期-2026年第13周 学习周刊-总第255期-2026年第12周 学习周刊-总第254期-2026年第11周 学习周刊-总第253期-2026年第10周 临时插播一条羊毛,免费领取450元大模型API代金券 学习周刊-总第252期-2026年第09周 学习周刊-总第251期-2026年第08周 学习周刊-总第250期-2026年第07周 学习周刊-总第249期-2026年第06周 诚邀评论,聊聊你所知欲知的我 学习周刊-总第248期-2026年第05周 我的QQ动态之2015年 我的QQ动态之2014年 我的QQ动态之2013年 我的QQ动态之2012年 我的QQ动态-2010-2011年 我的QQ动态-创栏小叙 学习周刊-总第247期-2026年第04周 Nexus社区版权益阉割--一文告诉你有哪些版本可以选择 学习周刊-总第246期-2026年第03周 学习周刊-总第245期-2026年第02周 学习周刊-总第244期-2026年第01周 学习周刊-总第243期-2025年第52周 学习周刊-总第242期-2025年第51周 开源项目ZenOps:带你领略禅意运维 学习周刊-总第241期-2025年第50周 用京东金融,享负债人生 学习周刊-总第240期-2025年第49周 学习周刊-总第239期-2025年第48周 整理我在静态服务透明代理上的极致求索之路,最后一个你绝想不到 学习周刊-总第238期-2025年第47周 我们输了,但收获颇多 太多了,太多了 学习周刊-总第237期-2025年第46周 学习周刊-总第236期-2025年第45周 学习周刊-总第235期-2025年第44周 二五年国庆二三事 学习周刊-总第234期-2025年第43周 学习周刊-总第233期-2025年第42周 学习周刊-总第232期-2025年第41周 学习周刊-总第231期-2025年第40周 学习周刊-总第230期-2025年第39周 西湖毅行 2025年开源世界逸闻三则 学习周刊-总第229期-2025年第38周 手抄《与妻书》 CNB开发与构建基于docker-cache缓存复用的配置实践心得 学习周刊-总第228期-2025年第37周 父亲善行录 学习周刊-总第227期-2025年第36周 带你认识我之看看我的高中同学录(其二) 学习周刊-总第226期-2025年第35周 带你认识我之看看我的高中同学录(其一) 认识神级MCP工具系列--用anyquery和数据库交互 学习周刊-总第225期-2025年第34周 学习周刊-总第224期-2025年第33周 学习周刊-总第223期-2025年第32周 学习周刊-总第222期-2025年第31周 学习周刊-总第221期-2025年第30周 CNB云原生开发环境届的瑞士军刀,详解qifei项目 vuepress-vdoing主题配置自建不蒜子统计 学习周刊-总第220期-2025年第29周 写在博客发表文章1000篇的节点 从claude cli的体验聊聊最大的敌人是我们自己的成见 学习周刊-总第219期-2025年第28周 学习周刊-总第218期-2025年第27周 学习周刊-总第217期-2025年第26周 理论正确,事实错误 学习周刊-总第216期-2025年第25周 学习周刊-总第215期-2025年第24周 学习周刊-总第214期-2025年第23周 学习周刊-总第213期-2025年第22周 学习周刊-总第212期-2025年第21周 从赵心童世锦赛夺冠聊聊我的斯诺克情缘 学习周刊-总第211期-2025年第20周 记录二五年五一之短暂回归家庭 学习周刊-总第210期-2025年第19周 学习周刊-总第209期-2025年第18周 学习周刊-总第208期-2025年第17周 Go开发实践之Gin框架将前端的dist目录embed到二进制 学习周刊-总第207期-2025年第16周 近期关于cobra库的一些实践心得总结 学习周刊-总第206期-2025年第15周 我的嗜好--嗑瓜子 记大宝参加幼儿园组织的清明烈士陵园扫墓活动 学习周刊-总第205期-2025年第14周 学习周刊-总第204期-2025年第13周 前端开发小笔记--在数组中给字段添加值与删除字段的操作 学习周刊-总第203期-2025年第12周 人生实苦,何以自渡 学习周刊-总第202期-2025年第11周 近期发现的一些优秀的工具提名(一) 学习周刊-总第201期-2025年第10周 学习周刊-总第200期-2025年第09周 记一次女友喝醉使我忍术破功 爱情风波--一次分手又复合的经历 学习周刊-总第199期-2025年第08周 整理我的信息源
基于langchaingo实现知识库对接本地模型ollama的分步探索
二丫讲梵 · 2024-04-19 · via 二丫讲梵

# 前言

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

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

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

本文项目代码地址:langchaingo-ollama-rag (opens new window)

# 正文

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

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

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

# 前置准备

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

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

$ docker pull qdrant/qdrant
$ docker run -itd --name qdrant -p 6333:6333 qdrant/qdrant

1
2

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

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

1
2
3
4
5
6
7
8

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

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

1

# 先切分文档

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

主要代码如下:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

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

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

$ go run main.go filetochunks
2024/04/17 23:03:32 INFO [转换文件为块儿成功,块儿数量:  40]

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

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

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

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

“网吧是干什么的?”

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

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

......

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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

$ go run main.go filetochunks -c 500
2024/04/17 23:06:30 INFO [转换文件为块儿成功,块儿数量:  13]

1
2

申明

原创文章eryajf,未经授权,严禁转载,侵权必究!此乃文中随机水印,敬请读者谅解。

# 块儿文本向量化

主逻辑代码如下:

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

1
2
3
4
5
6
7
8
9
10
11
12

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

$ go run main.go embedding
转换块儿为向量成功

1
2

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

主逻辑代码如下:

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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

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

1
2
3
4
5

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

主逻辑代码如下:

// GetAnswer 获取答案
func GetAnswer(ctx context.Context, llm llms.Model, docRetrieved []schema.Document, prompt string) (string, error) {
	// 创建一个新的聊天消息历史记录
	history := memory.NewChatMessageHistory()
	// 将检索到的文档添加到历史记录中
	for _, doc := range docRetrieved {
		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...)
	if err != nil {
		return "", err
	}

	return res, nil
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

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

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

1
2
3
4
5
6
7
8
9
10
11
12

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

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

$ go run main.go getanswer
请输入你的问题: 这篇文章讲了什么
🗂 原始回答==>  This article talks about the speaker's inner struggle between adhering to truth and enjoying material pleasures. The speaker expresses frustration with the fact that they must conform to societal expectations of living according to parallel rules, and questions the meaning of their own existence in this world. They also reflect on the futility of reality and contemplate suicide as a means of escape from their suffering. However, they ultimately remain silent and continue to endure the pain.

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

1
2
3
4
5

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

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

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