原文链接:https://rust-dd.com/post/our-first-production-ready-rag-dev-journey-in-pure-rust

我们涉足AI开发已有一段时日——不仅仅是那种“调用现有API”的高层次开发,而是那种必须构建无法依赖OpenAI或类似在线系统的解决方案。最近,我们面临了一项挑战:在Rust中创建我们首个生产级RAG(检索增强生成)解决方案。在这篇文章中,我将详细阐述我们所采取的步骤、遇到的困难,以及Rust社区在这一领域的现状。

我们的Rust RAG诞生记

一切始于我们在rust-dd的探索,当时我们想要构建一个抽象的RAG解决方案。这个想法很简单:我们将向量嵌入存储在某个向量数据库中,并使用标准的向量相似性搜索来为我们的本地LLM检索相关上下文。

初始技术栈

  • 编程语言:Rust(当然!)
  • 向量数据库:Qdrant —— 选择它是因为它也是用Rust编写的,并且对Rust有很好的支持。
  • LLM推理:Mistral.rs —— llama.cpp的纯Rust竞争对手。

大约一个月的时间里,这个项目完全以开源方式运行。我们构建了功能性RAG系统所需的一切:

  • 一个类似ChatGPT的前端
  • 文件上传功能,这些文件随后被嵌入到我们的向量数据库中
  • 一个本地LLM,能够利用检索到的上下文生成最佳答案

在这个阶段,我们通过官方的Qdrant fastembed Rust库使用纯文本来生成嵌入。初步测试看起来非常有希望,然后……真正的挑战开始了。

试点请求:压力骤增

我们收到了一个工业解决方案的试点请求,期限为1.5个月。这个RAG必须处理未知的数据源,因此我们不能依赖ChatGPT或其他大型成熟模型。我们的资源有限,这意味着我们实际上只能使用14–32B参数的模型——大多数是4位或8位量化形式。

PDF的烦恼与多语言混乱

我们无法分享所有细节,但可以说,我们必须回答来自用多种语言编写的PDF文件的查询,包括一种特别奇特的语言。任何尝试处理过PDF的人都知道,这是一个由来已久的难题:

  • 格式是非结构化的(几十年的难题)。
  • 将其转换为文本会变得更加诡异。

我们测试了几种策略,最终选择了:

  1. 将PDF转换为Markdown(我们尝试了基于AI的PDF转换器,如Marker、Docling等,但最终选择了PyMuPDF以确保可靠性)。
  2. 使用Rust文本分割库(例如text-splitter或类似工具)进行语义分块。

由于文档是专业化的,较小的LLM没有足够的内置领域知识。在许多方面,将数据上传到Qdrant比从LLM中获得正确答案更具挑战性。许多博客文章警告说,在RAG解决方案中使用PDF文件有点像是噩梦——我们完全同意!如果你对自己的数据很了解,并且不需要通用解决方案,你可以通过蛮力找到一种不错的方法。但要小心:

  • 页眉和页脚:如果它们被嵌入,可能会严重误导你的向量搜索。
  • 分块大小:如果分块太大,而嵌入模型不支持长序列长度,某些数据可能永远无法正确嵌入。
  • 嵌入向量维度:如果你的分块对于较小的嵌入维度来说太大,你会失去保真度。

突然之间,我们面临着一个关于分块大小、嵌入限制以及寻找完美平衡点的无尽难题。

多模型Embeddings

由于我们需要处理多种语言和专业化文本,我们最终使用了不同的模型,所有这些模型都来自Hugging Face。我们还引入了一个基于BERT的模型,甚至是一个生成稀疏向量的模型。结果是,我们构建了一个比最初预期更复杂的嵌入管道,但这是捕捉这些专业化文本细微差别的必要之举。

遇到障碍:Mistral.rs与CUDA

随着截止日期的临近,更多问题出现了:

  • 我们在Mistral.rs中遇到了设备映射问题(这意味着模型数据映射到GPU内存的方式出现了问题)。
  • 我们也遇到了一些与CUDA相关的错误。

我们不是CUDA开发专家,也没有时间自己修复这些问题,但非常感谢Mistral.rs的维护者Eric Blueher,他已经解决了这些问题。不幸的是,由于时间紧迫,我们转向了Ollama-rs以获取更稳定的框架,而Ollama运行顺利,没有进一步的GPU问题。我们仍然打算最终回归Mistral.rs,因为我们希望完全控制每个组件,而Mistral.rs提供了许多低层配置的可能性。

Python的渗透:Rust-Bert与PyO3

我们还不得不在Rust中使用Rust-Bert处理基于BERT的模型,但它底层使用了LibTorch。这带来了一些依赖关系上的摩擦。这是我们第一次感觉到,无论好坏,我们现在根本无法避免使用Python。

因此,我们采取了务实的方法:

  • 编写了一些快速的Python脚本
  • 使用PyO3从Rust调用它们

这为我们提供了所需的嵌入,并且我们可以继续冲刺到终点线。

最后的冲刺

我们只剩下不到一个月的时间。最大的挑战是弄清楚如何将最佳的向量数据库结果返回给LLM。如果你有巨大的文档和小的分块大小,你可以保持文档的连贯性,但可能会错过大局上下文。如果你有巨大的分块大小,你可能会失去细节或使搜索变得不那么相关。我们尝试了许多“常识性”的解决方案,但结果并不如我们希望的那么好。

最终,我们不得不退一步,真正思考底层数学如何塑造我们的数据。通常,你不仅仅需要遵循最佳实践;你需要深入了解模型的结构以及向量相似性是如何计算的。但时间紧迫,所以我们决定为演示选择我们拥有的最佳解决方案。

演示与成功

终于到了展示我们解决方案的日子。当一切顺利运行时,我们感到无比轻松——甚至有点惊讶:
系统在85-90%的时间内给出了正确的英文答案,几乎没有出现幻觉。

由于整个管道是英文的,其他语言的准确性稍差,但也不落后太多。
总的来说,我们自信地认为,我们已经在紧迫的期限内实现了交付基于Rust的生产级RAG的目标。

下一步是什么?

我们已经期待提高解决方案的准确性并扩展到更多语言。那些许多个月——以及熬夜的日子——绝对值得。我们的计划是:

  • 重新审视Mistral.rs以获得更多控制权。
  • 调整我们的分块策略,以更优雅地处理巨大文档。
  • 更多地尝试不同的嵌入模型,以支持真正的多语言和特定领域的用例。

这就是我们在Rust中成功构建RAG的故事——我们希望这只是众多成功故事中的第一个!

最后感想

在Rust中为RAG解决方案工作非常有成就感,但也充满了陷阱。无论是嵌入管道的复杂性、GPU的问题,还是不得不集成Python,构建一个稳健的RAG都是一个多层次的难题。但如果你对Rust和AI充满热情,毫无疑问,社区正在突破界限。我们很高兴能成为这段旅程的一部分——并迫不及待地想看看它接下来会走向何方。

如果你有任何问题或见解(特别是如果你正在处理Rust、GPU推理和复杂嵌入),请随时联系或留言。让我们继续用Rust构建AI的未来!

最近在研究ai,通过ollama部署了deepseek之后,对ollama调用方式感到很有兴趣,所有研究了一下,发现了一些以前没见过的技术,下面来说一下吧。

Ollama流式响应

一开始我是使用了Page Assist这个浏览器插件对ollama进行调用,这里展示一下ollama的接口是怎样响应的:

image.png

image.png

image.png

很可惜edge并不支持直接预览这个响应,只能看十六进制值以及对应的字符,而且和标准的event-source也不一样,不过好在我在apifox里面调试发现可以正常显示json

image.png

ndjson

从响应可以看到,响应的类型是 application/x-ndjson ,这是ds对他的介绍:

application/x-ndjson 是一种 MIME 类型,表示数据格式为 Newline Delimited JSON(简称 NDJSON)。它是一种基于文本的数据格式,用于存储或传输多个独立的 JSON 对象,每个对象占一行,并通过换行符(\n)分隔。NDJSON 在需要高效处理大规模或流式 JSON 数据的场景中非常实用。

这个内容说明了,这是一个二进制(application代表这是二进制数据)的多个json,而且每个json之间用换行符隔开,可以看apifox的响应预览,切到raw:

image.png

可以看到确实是每一行都是一个json字符串。

Transfer-Encoding: chunked

除了内容类型,还有个传输编码方式值得注意,就是响应头的:Transfer-Encoding: chunked。这是ds对这个头的说明:

Transfer-Encoding: chunked 是 HTTP 协议中的一种传输编码方式,允许服务器将响应数据分成多个“块”(chunk)逐步发送给客户端,无需预先知道数据的总长度。适用于动态生成内容或大文件传输的场景。

核心特点

无需预知总大小
不需要提前计算并设置 Content-Length 头,适合实时生成内容(如流媒体、动态 API 响应)。

分块传输
数据被拆分为多个独立块,每块包含:

-   **块大小**(十六进制数值,如 `1A` 表示 26 字节)。
-   **块内容**(实际数据)。
-   换行符(`\r\n`)。

结束标志
最后发送一个 0 长度的块(0\r\n\r\n),表示传输完成。

可以看到对比于普通的请求,这样的传输方式是一块一块的,不用提前知道整个请求的最终文本长度,比较咱们也没办法预算ai的回答有多长对吧,所有这种传输方法就非常合适。

在浏览器请求

讲解完这两个核心知识点之后,我们就已经大致掌握了流式请求的要义了,剩下的就是怎么发起和处理这些数据了,咱们先来看看JS是怎么请求的,咱们可以参考Page Assist,通过对源码的预览,我找到发起请求的源码:

https://github.com/n4ze3m/page-assist/blob/08b84e3918195b7cb8470fd67f60a80f94522005/src/models/utils/ollama.ts#L125-L192

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
29
30
async function* createOllamaStream(
url: string,
params: OllamaRequestParams,
options: OllamaCallOptions
) {
const response = await fetch(formattedUrl, {
method: "POST",
body: JSON.stringify(params),
})
if (!response.ok) {
// ....
}

const stream = IterableReadableStream.fromReadableStream(response.body)

const decoder = new TextDecoder()
let extra = ""
for await (const chunk of stream) {
const decoded = extra + decoder.decode(chunk)
const lines = decoded.split("\n")
extra = lines.pop() || ""
for (const line of lines) {
try {
yield JSON.parse(line)
} catch (e) {
console.warn(`Received a non-JSON parseable chunk: ${line}`)
}
}
}
}

其他那些有的没的代码就先删掉了,之间看核心部分代码。首先是使用fetch这个api发起请求,对于fetch这个api就不多说了,详情看MDN文档

为了方便迭代处理,这里将普通的可读流转成可迭代的可读的流,方便下面for await处理,这部分语法详情可以看mdn文档

因为前面说了,我们拿到的是二进制(也是十六进制)码,我们需要解码成文本,才能进行json反序列化,所有这里创建了一个文本解码类:TextDecoder,剩下就是调用decode方法对二进制进行解码。

解码出每一行json之后,就是大家都懂的json反序列化了。

最后我们修改一下代码,在控制台看看数据:

image.png

image.png

最终我们这样就能从流式请求接口拿到咱们的数据啦!

rust请求

因为最近还在学习rust,然后我也突发奇想使用rust调用试试,一开始问ai也写不出rust的代码,经过对各种rust群进行发问,有个群友发了个ollama的rust版客户端

详情看发起请求的代码:

https://github.com/pepperoni21/ollama-rs/blob/2409a5b584b50b83b361b229455e59fe0f156dc8/ollama-rs/src/generation/chat/mod.rs#L26-L70

然后是调用这个方法的example:

https://github.com/pepperoni21/ollama-rs/blob/2409a5b584b50b83b361b229455e59fe0f156dc8/ollama-rs/examples/chat_api_chatbot.rs

然后我们抄袭借鉴一下代码:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
use ollama_rs::generation::chat::ChatMessageResponse;
use reqwest::Client;
use serde::Serialize;
use std::io::{stdout, Write};

#[derive(Debug, Clone, Serialize)]
pub struct ChatMessageRequest {
model: String,
messages: Vec<ollama_rs::generation::chat::ChatMessage>,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
use tokio_stream::StreamExt;
let mut stdout = stdout();
let client = Client::new();
let builder = client.post("http://127.0.0.1:11434/api/chat");
let serialized = serde_json::to_string(&ChatMessageRequest {
model: "deepseek-r1:1.5b".to_string(),
messages: vec![ollama_rs::generation::chat::ChatMessage {
role: ollama_rs::generation::chat::MessageRole::User,
content: "你是谁".to_string(),
tool_calls: vec![],
images: None,
}],
})
.map_err(|e| e.to_string())?;
let res = builder.body(serialized).send().await?;
if !res.status().is_success() {
println!("请求不成功:{}", res.status());
return Ok(());
}
let mut stream = Box::new(res.bytes_stream().map(|res| match res {
Ok(bytes) => {
let res = serde_json::from_slice::<ChatMessageResponse>(&bytes);
match res {
Ok(res) => Ok(res),
Err(e) => {
eprintln!("Failed to deserialize response: {}", e);
Err(())
}
}
}
Err(e) => {
eprintln!("Failed to read response: {}", e);
Err(())
}
}));
while let Some(Ok(res)) = stream.next().await {
stdout.write_all(res.message.content.as_bytes())?;
stdout.flush()?;
}
Ok(())
}

代码和js类似,看代码是少了一步转字符串的,可以直接将字符串字节反序列化。

a7ddz-njehu.gif

总所周知,一般前后端项目都会使用nginx来部署前端项目,有些小伙伴可能会想到go的打包部署非常方便,那有没有一种可能直接把前端内容也打包进产物里面呢?(类似gitea)

有的兄弟,有的,这么简单的需求,只需要用goframe的cli将前端产物文件转译成类似base64编码的的方式,将文件转成一个go代码,然后通过fs读取即可,话不多说咱们直接开始。

gf-cli

首先我们会需要用到gf-cli的pack方法将整个前端产物转译,直接去github下载最新的gf-cli,https://github.com/gogf/gf/releases ,比如我的是windows系统就是下载gf_windows_amd64.exe

下载完直接改名gf.exe然后丢到C:\Windows\System32,或者其他在PATH里的目录即可。

image.png

在终端输入gf能正常打印说明安装成功。

创建项目然后打包

我们可以通过gf的init创建一些标准的后端目录结构,但是我这里因为演示的是只是怎么使用gf打包前端项目,这里我们使用最简单的目录结构,一个main.go和那种,我们直接使用goland来创建一个简单项目:

image.png

然后我们再找一个前端项目,我这里就选SoybeanAdmin吧,使用我自己的一个脚手架创建项目:

image.png

修改一下.env.prod加入:

VITE_BASE_URL=/web

然后build出来之后,将dist里的产物放到我们的go项目,然后使用我们的pack命令来打包一下

1
gf pack ./web packed/data.go -n=packed

image.png

最后修改一下我们的 main.go

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
29
30
31
32
33
34
35
36
37
38
package main

import (
"io/fs"
"net/http"

"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gres"

_ "awesomeProject1/packed"
)

type GFPackFS struct {
Fallback string
}

// Open GFPackFS实现FS接口
func (f *GFPackFS) Open(name string) (fs.File, error) {
file := gres.Get(name)
// file == nil的逻辑用来模仿nginx的try_files配置,如果没有对应的文件,则返回index.html
// 如果是访问首页,既/web路径的时候,也同样返回我们index.html
if file == nil || name == "web" {
fullback := gres.Get(f.Fallback)
return fullback, nil
}
return file, nil
}

func main() {
s := g.Server()
// 创建一个路由组,前缀是/web即我们的前端页面
s.Group("/web", func(group *ghttp.RouterGroup) {
group.ALL("/*", ghttp.WrapH(http.FileServer(http.FS(&GFPackFS{"web/index.html"}))))
})
s.SetPort(8199)
s.Run()
}

访问一下:http://localhost:8199/web/

image.png

配置下打包参数

众所周知go交叉编译非常方便,而使用gf之后更加方便,只需要在配置文件配置一下目标架构和系统,直接一个build命令即可打包出所有的标架构和系统可执行文件,这里直接创建个配置文件 config.yaml

1
2
3
4
5
6
gfcli:
build:
name: "gf"
arch: "amd64"
system: "linux,windows"
path: "./dist/"

然后直接一个 gf build

image.png

就这么简单,部署当然也就一个可执行文件丢上去就完事了!

当然这样只是为了搞个小程序玩玩,正经部署最好还是用nginx哦

相信很多小伙伴在vue转uniapp或者微信小程序的时候都会想过想简单的一个函数调用一些信息提示的功能,比如:

image.png

image.png

熟悉uniapp的小伙伴肯定知道有showToast这个方法,但是这个方法的限制很大,比如限制图标、限制文本字数等等。

当然,uniapp的一些组件库也提供了类似的功能,比如uview的Toast组件,提供了更丰富的样式和功能。但是像这种组件都必须在每个用到的页面都写上这么一个组件,那有没有什么办法像上面的element一样直接一个方法搞定呢?

众所周知小程序的模板不支持动态修改dom,也就不能像web一样往body里面append一个dom,那么怎么实现呢?

我的思路是像上面的naive-ui一样在页面内容外统一使用这个组件,至于怎么实现呢,我相信聪明的你肯定能想到布局,只需要每个页面都使用这个布局不就好了,这时候我们就需要使用我们的布局插件!

@uni-helper/vite-plugin-uni-layouts

这个插件为我们提供了页面布局的功能,详情使用方法可进去查看readme,安装好这个插件那么就能在默认模板里面加入我们想要的任何组件了。

但是我们如何在任意地方使用这个toast呢?

用过微信小程序的小伙伴都知道有个globalData的东西,能够整个app共用一个对象用于存储数据,而uniapp也为我们提供了这个功能,写h5、app的小伙伴也不用担心,uniapp为我们兼容了所有端。

那么我们只需要在进入每个页面的时候,把当前页面的Toast组件的ref存到globalData不就可以了吗?

image.png

需要留意的是,因为我们有很多个页面,所有在页面切换的时候,需要把当前页面布局的Toast组件的ref存下来,所以需要onShow这个生命周期钩子。

又因为onShow应该是要比组件mount的时机要早的,所以第一次执行onShow回调的时候,这里的uToast.value是undefined的,所以我们需要补一个onMounted的生命周期钩子,保证第一次加载完组件能拿到ref。

在其他地方使用

首先第一个是封装成一个方法并且标明入参类型提供其他页面去使用:

image.png

image.png

这样我们就能在任意地方使用我们在布局里的东西了,包括咱们的网络请求拦截器:

image.png

只能说,这个插件真的是,太裤辣

最后这是我的模板,详情代码可以看这个仓库: https://github.com/MatrixCross/Vue3-Uniapp-Starter

unjs是github上面一个挺多人关注的组织,组织下有很多很好用的工具包,搜了一下掘金好像并没有相关的详细介绍,然后就想着为他们介绍一下(水一篇文章)。

这里按照github上面的高星进行排名,下面开始咯。

阅读全文 »

虽然说很多人喜欢MacBookPro来做开发,但是对于很多人来说MBP的价格可不便宜,而MBA我觉得又是个电子垃圾,那么Windows的选择也很不错。我现在用的鸡哥的14X,机子本身3200,64G内存条1200,机子本身自带24G的两根内存条,如果需要加内存可以咸鱼300-400左右出掉,然后整机基本4000出头(上一个本子是ThinkBook14p 13500h 32g因为内存不够用所以换了)

image.png

基础美化

首先是Win11的任务栏默认居中,我不太习惯,可以右键任务栏-任务栏设置-任务栏对齐,选择靠左

image.png

然后是终端,终端推荐使用Terminal,然后安装oh-my-posh,主题我选择的是owl-night,字体是JetBrain Mono NF,记得字体需要下载NF字体,否则图标不能正常显示,当然Terminal还可以设置一些背景图片啥的,把二次元老婆/女神放上去也不错

image.png

安装方法如没有魔法的话可以从Windows的应用商店下载安装:

image.png

对于大部分开发者来说终端美化基本就够了,如果大家有什么好的建议可以评论区说一下。

基础开发环境

前端

因为我主要还是前端开发,所有这里重点说说前端我用到的一些软件,首先重中之重就是nvm-windows,下载地址是:https://github.com/coreybutler/nvm-windows/releases

然后我安装的时候会选择node下载到D盘,然后node的链接也是D盘:

image.png

image.png

安装完成之后可以设置下淘宝源下载node和npm:

nvm node_mirror https://npmmirror.com/mirrors/node/
nvm npm_mirror https://npmmirror.com/mirrors/npm/

装完node之后可以安装一些常用工具:npkill、tsx、taze、nrm

然后开发软件我喜欢WebStorm,WS已经在10月份公布免费非商用了(可以不用破解了),因为我喜欢JB全家桶,如果你也喜欢的话推荐使用JetBrainsToolbox

image.png

然后如果你的C盘比较少的话,推荐把全家桶安装在其他盘:

image.png

Java、Go等开发

对于这些我的推荐都是使用JB全家桶,因为他们都自带了对应的开发环境配置功能:

image.png

image.png

对应的java、go版本都可以直接在IDE里面去下载使用,不用自己跑到网上下载安装,而且这样安装我感觉是非常绿色的,洁癖狂喜!

对于rust开发的话可以用rustup安装,编译后端推荐使用msvc生成工具而不是VS:

https://visualstudio.microsoft.com/zh-hans/downloads/

image.png

其他的一些常用开发软件

ssh软件可以选择Xshell,自从6以后Xshell已经可以免费使用了,只需要邮箱校验一下就能一直用了。

全盘搜索我推荐Everything,如果你用Everything比较多的话可以安装个Everything Toolbar,将Toolbar加到任务栏会更加方便。

如果本地需要跑nginx、mysql、redis这些我常用的是小皮面板,安装方便只需要一个软件就给你把所有基础中间件跑起来,如果是新手的话强烈推荐!

image.png

虚拟机我更喜欢的是Vmware Workstation Pro而不是WSL/Hyper-V,这个软件也是从17开始就可以免费用了,不需要输入注册码了,然后Linux我可能比较喜欢Ubuntu、Deepin、Fedora这些开箱即用的。

Api调试、Api文档管理、Api自动化测试/压测:Apifox

其他的一些日常软件

办公三件套软件我推荐MS office,因为我对WPS的印象是很多东西需要收费而且有广告而且界面不好看所有推荐MS office,可以使用Office Plus Tools安装:https://otp.landian.vip/zh-cn/

解压缩软件推荐:7-Zip、Bindizip6.25版本(新版本有广告)

视频播放器:PotPlayer,官网:https://potplayer.daum.net/?lang=zh_CN

图片预览:HoneyView,官网:https://www.bandisoft.com/honeyview/

各种各样的工具集合:uTools

原文:How to Prerelease an npm Package

最近,我们彻底修改了共享的 ESLint 配置,在测试过程中,我需要发布一个 alpha 版本。我知道这是可能的,但我完全不知道该怎么做。幸运的是,一旦你知道怎么做,这其实很简单。

阅读全文 »

关注了这么久的装饰器提案终于进入到Stage3阶段了,掘金站内也有简单用法介绍版本:JS装饰器(Decorators)用法-Stage3(新),这篇就根据提案详情翻译一下。

装饰器(Decorators)

Stage: 3

装饰器是一项用于扩展 JavaScript 类的提案,在转译器环境中得到了开发者的广泛采用,并且在标准化方面引起了广泛兴趣。TC39 已经在装饰器提案上迭代了五年多。本文档描述了一个基于所有过去提案元素的新装饰器提案。

本 README 文件描述了当前的装饰器提案,该提案仍在进行中。有关该提案之前的迭代版本,请参阅本仓库的提交历史

阅读全文 »
0%