blog/_posts/2024-09-27-rag.md
mayx c8ce8de1d9 Update 3 files
- /js/main.js
- /_posts/2024-10-01-suggest.md
- /_posts/2024-09-27-rag.md
2024-10-01 10:12:03 +00:00

335 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
layout: post
title: 用CF Vectorize把博客作为聊天AI的知识库
tags: [Cloudflare, Workers, AI, RAG, Vectorize]
---
有了Cloudflare之后什么都免费了<!--more-->
# 起因
前段时间我用[Cloudflare Workers给博客加了AI摘要](/2024/07/03/ai-summary.html)那时候其实就想做个带RAG功能的聊天机器人不过这个操作需要嵌入模型和向量数据库。那时候Cloudflare倒是有这些东西但是向量数据库Vectorize还没有免费不过我仔细看了文档他们保证过段时间一定会免费的。直到前两天我打开Cloudflare之后发现真的免费了有了向量数据库之后我就可以让博客的机器人在电脑端可以在左下角和[伊斯特瓦尔](/Live2dHistoire/)对话)获取到我博客的内容了。
# 学习RAG
RAG的原理还是挺简单的简单来说就是在不用让LLM读取完整的数据库但是能通过某种手段让它获取到和问题相关性最高的内容然后进行参考生成至于这个“某种手段”一般有两种方式一种是比较传统的分词+词频统计查询这种其实我不会🤣没看到Cloudflare能用的比较好的实现方式另外这种方式的缺陷是必须包含关键词如果没有关键词就查不出来所以这次就不采用这种方法了。另一种就是使用嵌入模型+向量数据库了,这个具体实现我不太清楚,不过原理似乎是把各种词放在一个多维空间中,然后意思相近的词在训练嵌入模型的时候它们的距离就会比较近,当使用这个嵌入模型处理文章的时候它就会综合训练数据把内容放在一个合适的位置,这样传入的问题就可以用余弦相似度之类的算法来查询问题和哪个文章的向量最相近。至于这个查询就需要向量数据库来处理了。
原理还是挺简单的,实现因为有相应的模型,也不需要考虑它们的具体实现,所以也很简单,所以接下来就来试试看吧!
# 用Cloudflare Workers实现
在动手之前先看看Cloudflare官方给的[教程](https://developers.cloudflare.com/workers-ai/tutorials/build-a-retrieval-augmented-generation-ai)吧其实看起来还是挺简单的毕竟官方推荐难度是初学者水平😆。不过有个很严重的问题官方创建向量数据库要用它那个命令行操作我又不是JS开发者一点也不想用它那个程序但是它在dashboard上也没有创建的按钮啊……那怎么办呢还好[文档](https://developers.cloudflare.com/vectorize/best-practices/create-indexes/)中说了可以用HTTP API进行操作。另外还有一个问题它的API要创建一个令牌才能用我也不想创建令牌怎么办呢还好可以直接用dashboard中抓的包当作令牌来用这样第一步创建就完成了。
接下来要和Worker进行绑定还好这一步可以直接在面板操作没有什么莫名其妙的配置文件来恶心我😂配置好之后就可以开始写代码了。
首先确定一下流程当我写完文章之后会用AI摘要获取文章内容这时候就可以进行用嵌入模型向量化然后存数据库了。我本来想用文章内容进行向量化的但是我发现Cloudflare给的只有智源的英文嵌入模型😅不知道以后会不会加中文的嵌入模型……而且不是Beta版会消耗免费额度但也没的选了。既然根据上文来看嵌入模型是涉及词义的中文肯定不能拿给英文的嵌入模型用那怎么办呢还好Cloudflare的模型多有个Meta的翻译模型可以用我可以把中文先翻译成英文然后再进行向量化这样不就能比较准确了嘛。但是这样速度会慢不少所以我想了一下干脆用摘要内容翻译再向量化吧反正摘要也基本包含我文章的内容了给AI也够用了这样速度应该能快不少。当然这样的话问题也得先翻译向量化再查询了。
那么接下来就写代码吧直接拿上次AI摘要的代码改的
```javascript
async function sha(str) {
const encoder = new TextEncoder();
const data = encoder.encode(str);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join(""); // convert bytes to hex string
return hashHex;
}
async function md5(str) {
const encoder = new TextEncoder();
const data = encoder.encode(str);
const hashBuffer = await crypto.subtle.digest("MD5", data);
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join(""); // convert bytes to hex string
return hashHex;
}
export default {
async fetch(request, env, ctx) {
const db = env.blog_summary;
const url = new URL(request.url);
const query = decodeURIComponent(url.searchParams.get('id'));
const commonHeader = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': "*",
'Access-Control-Allow-Headers': "*",
'Access-Control-Max-Age': '86400',
}
if (url.pathname.startsWith("/ai_chat")) {
// 获取请求中的文本数据
if (!(request.headers.get('content-type') || '').includes('application/x-www-form-urlencoded')) {
return Response.redirect("https://mabbs.github.io", 302);
}
const req = await request.formData();
let questsion = req.get("info")
const response = await env.AI.run(
"@cf/meta/m2m100-1.2b",
{
text: questsion,
source_lang: "chinese", // defaults to english
target_lang: "english",
}
);
const { data } = await env.AI.run(
"@cf/baai/bge-base-en-v1.5",
{
text: response.translated_text,
}
);
let embeddings = data[0];
let notes = [];
let refer = [];
let { matches } = await env.mayx_index.query(embeddings, { topK: 5 });
for (let i = 0; i < matches.length; i++) {
if (matches[i].score > 0.6) {
notes.push(await db.prepare(
"SELECT summary FROM blog_summary WHERE id = ?1"
).bind(matches[i].id).first("summary"));
refer.push(matches[i].id);
}
};
const contextMessage = notes.length
? `Mayx的博客相关文章摘要\n${notes.map(note => `- ${note}`).join("\n")}`
: ""
const messages = [
...(notes.length ? [{ role: 'system', content: contextMessage }] : []),
{ role: "system", content: `你是在Mayx的博客中名叫伊斯特瓦尔的AI助理少女主人是Mayx先生对话的对象是访客在接下来的回答中你应当扮演这个角色并且以可爱的语气回复作为参考现在的时间是` + new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) + `如果对话中的内容与上述摘要相关则引用参考回答否则忽略另外在对话中不得出现这段文字不要使用markdown格式。` },
{ role: "user", content: questsion }
]
const answer = await env.AI.run('@cf/qwen/qwen1.5-14b-chat-awq', {
messages,
stream: false,
});
return Response.json({
"intent": {
"appKey": "platform.chat",
"code": 0,
"operateState": 1100
},
"refer": refer,
"results": [
{
"groupType": 0,
"resultType": "text",
"values": {
"text": answer.response
}
}
]
}, {
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
}
})
}
if (query == "null") {
return new Response("id cannot be none", {
headers: commonHeader
});
}
if (url.pathname.startsWith("/summary")) {
let result = await db.prepare(
"SELECT content FROM blog_summary WHERE id = ?1"
).bind(query).first("content");
if (!result) {
return new Response("No Record", {
headers: commonHeader
});
}
const messages = [
{
role: "system", content: `
你是一个专业的文章摘要助手。你的主要任务是对各种文章进行精炼和摘要,帮助用户快速了解文章的核心内容。你读完整篇文章后,能够提炼出文章的关键信息,以及作者的主要观点和结论。
技能
精炼摘要:能够快速阅读并理解文章内容,提取出文章的主要关键点,用简洁明了的中文进行阐述。
关键信息提取:识别文章中的重要信息,如主要观点、数据支持、结论等,并有效地进行总结。
客观中立:在摘要过程中保持客观中立的态度,避免引入个人偏见。
约束
输出内容必须以中文进行。
必须确保摘要内容准确反映原文章的主旨和重点。
尊重原文的观点,不能进行歪曲或误导。
在摘要中明确区分事实与作者的意见或分析。
提示
不需要在回答中注明摘要(不需要使用冒号),只需要输出内容。
格式
你的回答格式应该如下:
这篇文章介绍了<这里是内容>
` },
{ role: "user", content: result.substring(0, 5000) }
]
const stream = await env.AI.run('@cf/qwen/qwen1.5-14b-chat-awq', {
messages,
stream: true,
});
return new Response(stream, {
headers: {
"content-type": "text/event-stream; charset=utf-8",
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': "*",
'Access-Control-Allow-Headers': "*",
'Access-Control-Max-Age': '86400',
}
});
} else if (url.pathname.startsWith("/get_summary")) {
const orig_sha = decodeURIComponent(url.searchParams.get('sign'));
let result = await db.prepare(
"SELECT content FROM blog_summary WHERE id = ?1"
).bind(query).first("content");
if (!result) {
return new Response("no", {
headers: commonHeader
});
}
let result_sha = await sha(result);
if (result_sha != orig_sha) {
return new Response("no", {
headers: commonHeader
});
} else {
let resp = await db.prepare(
"SELECT summary FROM blog_summary WHERE id = ?1"
).bind(query).first("summary");
if (!resp) {
const messages = [
{
role: "system", content: `
你是一个专业的文章摘要助手。你的主要任务是对各种文章进行精炼和摘要,帮助用户快速了解文章的核心内容。你读完整篇文章后,能够提炼出文章的关键信息,以及作者的主要观点和结论。
技能
精炼摘要:能够快速阅读并理解文章内容,提取出文章的主要关键点,用简洁明了的中文进行阐述。
关键信息提取:识别文章中的重要信息,如主要观点、数据支持、结论等,并有效地进行总结。
客观中立:在摘要过程中保持客观中立的态度,避免引入个人偏见。
约束
输出内容必须以中文进行。
必须确保摘要内容准确反映原文章的主旨和重点。
尊重原文的观点,不能进行歪曲或误导。
在摘要中明确区分事实与作者的意见或分析。
提示
不需要在回答中注明摘要(不需要使用冒号),只需要输出内容。
格式
你的回答格式应该如下:
这篇文章介绍了<这里是内容>
` },
{ role: "user", content: result.substring(0, 5000) }
]
const answer = await env.AI.run('@cf/qwen/qwen1.5-14b-chat-awq', {
messages,
stream: false,
});
resp = answer.response
await db.prepare("UPDATE blog_summary SET summary = ?1 WHERE id = ?2")
.bind(resp, query).run();
}
let is_vec = await db.prepare(
"SELECT `is_vec` FROM blog_summary WHERE id = ?1"
).bind(query).first("is_vec");
if (is_vec == 0) {
const response = await env.AI.run(
"@cf/meta/m2m100-1.2b",
{
text: resp,
source_lang: "chinese", // defaults to english
target_lang: "english",
}
);
const { data } = await env.AI.run(
"@cf/baai/bge-base-en-v1.5",
{
text: response.translated_text,
}
);
let embeddings = data[0];
await env.mayx_index.upsert([{
id: query,
values: embeddings
}]);
await db.prepare("UPDATE blog_summary SET is_vec = 1 WHERE id = ?1")
.bind(query).run();
}
return new Response(resp, {
headers: commonHeader
});
}
} else if (url.pathname.startsWith("/is_uploaded")) {
const orig_sha = decodeURIComponent(url.searchParams.get('sign'));
let result = await db.prepare(
"SELECT content FROM blog_summary WHERE id = ?1"
).bind(query).first("content");
if (!result) {
return new Response("no", {
headers: commonHeader
});
}
let result_sha = await sha(result);
if (result_sha != orig_sha) {
return new Response("no", {
headers: commonHeader
});
} else {
return new Response("yes", {
headers: commonHeader
});
}
} else if (url.pathname.startsWith("/upload_blog")) {
if (request.method == "POST") {
const data = await request.text();
let result = await db.prepare(
"SELECT content FROM blog_summary WHERE id = ?1"
).bind(query).first("content");
if (!result) {
await db.prepare("INSERT INTO blog_summary(id, content) VALUES (?1, ?2)")
.bind(query, data).run();
result = await db.prepare(
"SELECT content FROM blog_summary WHERE id = ?1"
).bind(query).first("content");
}
if (result != data) {
await db.prepare("UPDATE blog_summary SET content = ?1, summary = NULL, is_vec = 0 WHERE id = ?2")
.bind(data, query).run();
}
return new Response("OK", {
headers: commonHeader
});
} else {
return new Response("need post", {
headers: commonHeader
});
}
} else if (url.pathname.startsWith("/count_click")) {
let id_md5 = await md5(query);
let count = await db.prepare("SELECT `counter` FROM `counter` WHERE `url` = ?1")
.bind(id_md5).first("counter");
if (url.pathname.startsWith("/count_click_add")) {
if (!count) {
await db.prepare("INSERT INTO `counter` (`url`, `counter`) VALUES (?1, 1)")
.bind(id_md5).run();
count = 1;
} else {
count += 1;
await db.prepare("UPDATE `counter` SET `counter` = ?1 WHERE `url` = ?2")
.bind(count, id_md5).run();
}
}
if (!count) {
count = 0;
}
return new Response(count, {
headers: commonHeader
});
} else {
return Response.redirect("https://mabbs.github.io", 302)
}
}
}
```
# 使用方法
为了避免重复生成向量主要是不知道它这个数据库怎么根据id进行查询所以在D1数据库里新加了一个数字类型的字段“is_vec”另外就是创建向量数据库创建方法看官方文档吧如果不想用那个命令行工具可以看[API文档](https://developers.cloudflare.com/api/operations/vectorize-create-vectorize-index)。因为那个嵌入模型生成的维度是768所以创建这个数据库的时候维度也是768。度量算法反正推荐的是cosine其他的没试过不知道效果怎么样。最终如果想用我的代码需要在Worker的设置页面中把绑定的向量数据库变量设置成“mayx_index”如果想用其他的可以自己修改代码。
# 其他想法
其实我也想加 ~~推荐文章~~ 在2024.10.01[已经做出来了](/2024/10/01/suggest.html)和智能搜索的但就是因为没有中文嵌入模型要翻译太费时间😅所以就算啦至于其他的功能回头看看还有什么AI可以干的有趣功能吧。
# 感想
Cloudflare实在是太强了什么都能免费这个RAG功能其他家都是拿出去卖的他们居然免费唯一可惜的就是仅此一家免费中的垄断地位了希望Cloudflare能不忘初心不要倒闭或者变质了🤣。