书接上文,本文完了RAG的后半部分,在浏览器运行qwen1.5-0.5B实现了增强搜索全流程。但受限于浏览器和模型性能,仅适合于研究、离线和高隐私场景,但对前端小伙伴来说大模型也不是那么遥不可及了,附带全部代码,动手试试吧! 纯前端,不适用第三方接口
1 准备工作
1.1 前置知识
- 读完前端大模型入门:使用Transformers.js实现纯网页版RAG(一)
- 了解WebML 前端大模型入门:Transformer.js 和 Xenova
- 基本的前端开发知识,esm和async/await
1.2页面代码框架
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>网页端侧增强搜索</title>
</head>
<body>
<div id="app">
<div>
<input type="text" id="question" />
<button id="search">提问</button>
</div>
<div id="info"></div>
</div>
<script type="module">
import {
pipeline,
env,
cos_sim,
} from "https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2/dist/transformers.min.js";
env.remoteHost = "https://hf-mirror.com";
// 后续代码位置
</script>
</body>
</html>
1.3 chrom/edge浏览器
目前测试firefox模型缓存有问题,建议用这两个,首次加载模型需要点时间,后续就不需要了,记住刷新时按F5不要清空缓存了。
2 搜索代码实现 - R
2.1 准备好知识库和初始化向量库
前一篇文章已经介绍了相关内容,本文知识库有些不一样,因为是需要给大模型去生成回答,而不是直接给出答案,所以合并在了一起。
const knowledges = [
"问:洛基在征服地球的尝试中使用了什么神秘的物体?\n答:六角宝",
"问:复仇者联盟的哪两名成员创造了奥创?\n答:托尼·斯塔克(钢铁侠)和布鲁斯·班纳(绿巨人浩克)。",
"问:灭霸如何实现了他在宇宙中消灭一半生命的计划?\n答:通过使用六颗无限宝石",
"问:复仇者联盟用什么方法扭转了灭霸的行动?\n通过时间旅行收集宝石。",
"问:复仇者联盟的哪位成员牺牲了自己来打败灭霸?\n答:托尼·斯塔克(钢铁侠)",
];
const verctorStore = [];
2.2 定义打印输出和参数
topK控制送给大模型处理的最匹配的知识数量上下,越多的知识条数prompt越大会导致处理用时越长,一般三个最匹配的知识就差不多够用了,尤其是在网页中运行时
const infoEl = document.getElementById("info");
const print = text => infoEl.innerHTML = text;
const knowEl = document.getElementById("knowEl");
const topK = 3;
2.3 准备好嵌入和生成模型
嵌入使用 bge-base ,回答生成使用qwen1.5-0.5B
const embedPipe = pipeline("feature-extraction", "Xenova/bge-base-zh-v1.5", {
progress_callback: (d) => {
infoEl.innerHTML = `embed:${JSON.stringify(d)}`;
},
});
const chatPipe = pipeline('text-generation', 'Xenova/Qwen1.5-0.5B-Chat', {
progress_callback: (d) => {
infoEl.innerHTML = `chat:${JSON.stringify(d)}`;
},
});
2.4 定义向量库数据初始方法
这个不多赘述,和前一篇的类似
const buildVector = async () => {
if (!verctorStore.size) {
const embedding = await embedPipe;
print(`构建向量库`)
const output = await embedding(knowledges, {
pooling: "mean",
normalize: true,
});
knowledges.forEach((q, i) => {
verctorStore[i] = output[i];
});
}
};
2.5 定义问答主方法
这里也不赘述过多,和上一篇不同之处在于:根据score从大到小排序,选出topK传入生成方法
const search = async () => {
const start = Date.now()
const embedding = await embedPipe;
const question = document.getElementById("question").value;
const [qVector] = await embedding([question], {
pooling: "mean",
normalize: true,
});
await buildVector();
const scores = verctorStore.map((q, i) => {
return {
score: cos_sim(qVector.data, verctorStore[i].data),
knowledge: knowledges[i],
index: i,
};
});
scores.sort((a, b) => b.score - a.score);
const picks = scores.slice(0, topK)
const docs = picks.map(e => e.knowledge)
const answer = await generateAnswer(question, docs.join('\n'))
print(answer + `(用时:${Date.now()- start}ms)`)
};
document.querySelector("#search").onclick = search;
3 生成代码实现 - G
这一部分主要介绍generateAnser的实现
3.1 定义prompt
这部分自己测试时可多调整下,prompt定义的越好效果越好
const prompt =
`请根据【上下文】回答【问题】,当得不到较为准确的答案时,必须回答我不知道。
【上下文】
${context}
【问题】
${question}
请给出你的答案:
`
3.2 构建消息和输入
const messages = [
{ role: 'system', content: '你是一个分析助手,根据上下文回答问题。必须生成更人性化的答案。' },
{ role: 'user', content: prompt }
]
console.log(messages)
// 生成cha
const text = generator.tokenizer.apply_chat_template(messages, {
tokenize: false,
add_generation_prompt: true,
});
console.log(text)
3.3 等待回答返回首个答案
print(`思考中...`)
const output = await generator(text, {
max_new_tokens: 128,
do_sample: false,
return_full_text: false,
});
console.log(output)
return output[0].generated_text;
4 运行测试
4.1 等待模型加载就绪
嵌入和千问整体有接近1G的数据下载,需要稍微等待下,直到看到下图所示结果
4.2 输入提问
我的问题是“他是怎么实现计划的”,点击提问
4.3 检查控制台输出的prompt
可以看到匹配到的三个答案和问题
<|im_start|>system
你是一个分析助手,根据上下文回答问题。必须生成更人性化的答案。<|im_end|>
<|im_start|>user
请根据【上下文】回答【问题】,当得不到较为准确的答案时,必须回答我不知道。
【上下文】
问:灭霸如何实现了他在宇宙中消灭一半生命的计划?
答:通过使用六颗无限宝石
问:复仇者联盟用什么方法扭转了灭霸的行动?
通过时间旅行收集宝石。
问:洛基在征服地球的尝试中使用了什么神秘的物体?
答:六角宝
【问题】
他是怎么实现计划的
请给出你的答案:
<|im_end|>
<|im_start|>assistant
4.4 等待回复
耗时25s,有点长,但考虑到这是可以离线在端侧运行的非gpu版本,用于做一些后台任务还是可以的,结果如下
5 总结
5.1 qwen1.5-0.5B比预期效果好
结果比续期要好一些,因为比较新的web版本大模型就找到qwen1.5-0.5B的,后续有时间我会出一期试试llama3.2-1B,但整个过程会比较长 - 因为还涉及到模型迁移
5.2 除非离线和高隐私环境网页大模型暂不适用
受限于网页性能和WebGPU的支持在transformer.js处于实验性阶段,生成用时比较久,除非是离线环境,以及对隐私要求比较高的情况下,目前的响应速度还是比较慢的
最近眼睛肿了,今天就一篇吧,剩下时间休息了,明天又得上班 ~ 啊啊啊