原文:Agent architectures

许多LLM(大型语言模型)应用程序在LLM调用之前和/或之后执行特定的步骤控制流。例如,RAG(检索增强生成)会检索与问题相关的文档,并将这些文档传递给LLM,以便为模型的回答提供依据。

阅读全文 »

代数数据类型 (Algebraic Data Type) 、 广义代数数据类型 (Generalized Algebriac Data Type) 、余代数数据类型 (Coalgebraic Data Type)

https://github.com/asukaminato0721/magic-in-ten-mins-ts/blob/master/doc/ADT.md

https://github.com/asukaminato0721/magic-in-ten-mins-ts/blob/master/doc/GADT.md

https://github.com/asukaminato0721/magic-in-ten-mins-ts/blob/master/doc/CoData.md

阅读全文 »

原文:Modern Node.js Patterns for 2025,翻译来自DeepSeek-R1

Node.js 自诞生以来经历了显著的变革。如果您使用 Node.js 已有数年,您很可能亲身见证了它的演进——从重度依赖回调、CommonJS 主导的环境,发展到如今基于标准的、简洁的开发体验。这些变化不仅仅是表面的;它们代表了我们在服务器端 JavaScript 开发方法上的根本性转变。现代 Node.js 拥抱 Web 标准,减少外部依赖,并提供更直观的开发体验。让我们探索这些转变,并理解它们为何对您 2025 年的应用程序至关重要。

阅读全文 »

前面学了如何构建提示词、加载文档/网页内容、检索、嵌入等等,实现简单的RAG,下面再来继续学一下函数调用,让ai调用代码能力实现对ai能力的增强

阅读全文 »

上一篇讲了基本的调用LLM,还有简单或者灵活的去使用模板生成提示词,以及使用提示词+解析器实现结构化的输出,这一节介绍一下Loader、Embedding和搭建向量数据库,最后实现一个简单的RAG

阅读全文 »

前言

最近在学RAG,学到Embedding部分,想搭建一个向量库去存数据,看了一圈网上以及问大佬,基本都是推荐milvus这个数据库,所以就开始想搭建一下玩玩。

部署milvus

看官方的部署文档有三种部署方式,轻量、单机、集群,其中轻量的限制linux和mac,我电脑是windows,只有虚拟机linux,而且集群的话又挺麻烦的,就选了单机版。单机版又分了单镜像和compose,单镜像还得用一个脚本来启动,我个人不怎么喜欢脚本(可能是因为不会吧),就选了compose了。

1
2
3
4
5
# Download the configuration file 
$ wget https://github.com/milvus-io/milvus/releases/download/v2.5.6/milvus-standalone-docker-compose.yml -O docker-compose.yml

# Start Milvus
$ sudo docker compose up -d

非常熟悉的docker compose,不过我启动完成后在docker desktop一看竟然没发现刚刚启动的容器,想了一下估计是因为我用sudo启动的,而docker desktop是非root启动,然后他们就被隔离开了导致看不到。

那之后删掉之前的容器,重新在当前用户启动

1
2
3
sudo docker compose down -v

docker compose up -d

然后发现怎么还得重新下镜像,原来不同用户连镜像都隔离开了,那好吧,我知道docker有个导出镜像的功能,就将那这几个镜像导出然后再在非root用户下导入(不知道还有没有更方便的办法,有懂的docker的大佬可以评论区告诉我拜托了)。

使用milvus

前面说到我是为了学ai才选了这个数据库,那么部署完成之后我就开始使用,我跑的是Langchain.js的一个demo代码:

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
import { Milvus } from "@langchain/community/vectorstores/milvus";
import { OllamaEmbeddings } from "@langchain/ollama";
import "dotenv/config";

const vectorStore = await Milvus.fromTexts(
[
"Tortoise: Labyrinth? Labyrinth? Could it Are we in the notorious Little\
Harmonic Labyrinth of the dreaded Majotaur?",
"Achilles: Yiikes! What is that?",
"Tortoise: They say-although I person never believed it myself-that an I\
Majotaur has created a tiny labyrinth sits in a pit in the middle of\
it, waiting innocent victims to get lost in its fears complexity.\
Then, when they wander and dazed into the center, he laughs and\
laughs at them-so hard, that he laughs them to death!",
"Achilles: Oh, no!",
"Tortoise: But it's only a myth. Courage, Achilles.",
],
[{ id: 2 }, { id: 1 }, { id: 3 }, { id: 4 }, { id: 5 }],
new OllamaEmbeddings({
model: "bge-m3:latest",
}),
{
collectionName: "goldel_escher_bach",
}
);

const response = await vectorStore.similaritySearch("scared", 2);
console.log(response);

简单说一下这段代码,就是通过Embedding模型将文本转成向量存到milvus数据库,

但是还没跑完发现代码报错了:Error: 14 UNAVAILABLE: No connection established. Last error: Failed to connect (2025-04-07T02:15:51.637Z)

怎么突然就连不上了,然后去docker desktop看了一下发现容器停掉了,点进去一看全是go的panic报错,看了一下panic前面的日志,提到:Resource requested is unreadable, please reduce your request rate

想了一会之后我记得milvus使用minio作为存储,然后去看了一下minio的日志,发现了确实是minio报错了:

1
2
3
4
5
6
7
8
9
10
11
2025-04-03 15:39:46 API: SYSTEM()
2025-04-03 15:39:46 Time: 07:39:46 UTC 04/03/2025
2025-04-03 15:39:46 DeploymentID: 755ca3ec-ee78-4b0d-b5bd-4796144ff205
2025-04-03 15:39:46 Error: write /minio_data/.minio.sys/tmp/d5510ac8-cbca-48af-9e21-98bd3dea7b66/xl.meta: invalid argument (*fs.PathError)
2025-04-03 15:39:46 6: internal/logger/logger.go:258:logger.LogIf()
2025-04-03 15:39:46 5: cmd/storage-errors.go:165:cmd.osErrToFileErr()
2025-04-03 15:39:46 4: cmd/xl-storage.go:2402:cmd.(*xlStorage).RenameData()
2025-04-03 15:39:46 3: cmd/xl-storage-disk-id-check.go:378:cmd.(*xlStorageDiskIDCheck).RenameData()
2025-04-03 15:39:46 2: cmd/erasure-object.go:774:cmd.renameData.func1()
2025-04-03 15:39:46 1: internal/sync/errgroup/errgroup.go:123:errgroup.(*Group).Go.func1()
2025-04-03 15:39:46 Waiting for all MinIO sub-systems to be initialized.. possible cause (Unable to initialize config system: migrateConfigToMinioSys: Storage resources are insufficient for the write operation .minio.sys/config/config.json)

然后去minio的github搜了一遍也没找到什么解决方法。我突然想到milvus的教程里面是用root用户启动的,我就想了一下用root试了一下。最后发现root启动是没有问题,上面代码也能跑通。那应该可能是权限之类的问题了。

解决问题

说起来我的docker使用经验也不多,我就去问了一些大佬,大佬看了一下docker-compose.yml之后给了两个方法,一个是特权模式,另一个是将volume的绑定目录改成命名卷。特权模式我试了一下没用,改成命名卷的方式通过问ai,完成修改:

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
version: '3.5'

services:
etcd:
container_name: milvus-etcd
image: quay.io/coreos/etcd:v3.5.18
environment:
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1000
- ETCD_QUOTA_BACKEND_BYTES=4294967296
- ETCD_SNAPSHOT_COUNT=50000
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd
command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
healthcheck:
test: ["CMD", "etcdctl", "endpoint", "health"]
interval: 30s
timeout: 20s
retries: 3

minio:
container_name: milvus-minio
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
ports:
- "9001:9001"
- "9000:9000"
volumes:
- milvus-minio:/minio_data
command: minio server /minio_data --console-address ":9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3

standalone:
container_name: milvus-standalone
image: milvusdb/milvus:v2.5.6
command: ["milvus", "run", "standalone"]
security_opt:
- seccomp:unconfined
environment:
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: minio:9000
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
interval: 30s
start_period: 90s
timeout: 20s
retries: 3
ports:
- "19530:19530"
- "9091:9091"
depends_on:
- "etcd"
- "minio"

networks:
default:
name: milvus

# 定义命名卷
volumes:
milvus-minio:

然后测了一下上面的代码,也能正常跑通了,minio和milvus也没有报错了。

这个问题根据github的issue所提,应该是只有在linux6.0之后的版本才会出现。

前文:手把手教你在浏览器和RUST中处理流式传输 提到如何简单的处理流式输出,但是后来发现这个写法有bug,下面讲解一下更好的写法

顺便补充一下,上一篇文章提到的IterableReadableStream来自@langchain/core,你可以这样导入使用:

1
import { IterableReadableStream } from '@langchain/core/utils/stream'

处理Event Stream

除了上一章的ndjson以外,最常用就是Event Stream了,包括OpenAi等一众ai服务提供商都会提供sse接口,并且以Event Stream的格式进行输出,先来看看ai是怎么理解Event StreamSSE的:

Server-Sent Events (SSE) ,一种基于 HTTP 的轻量协议,允许服务器向客户端推送实时数据流。

SSE 格式规范

  • 数据通过 HTTP 流式传输,内容类型为 text/event-stream

  • 每条事件由字段组成,用换行符分隔。字段包括:

    • data: 事件的具体内容(必填)。
    • event: 自定义事件类型(可选)。
    • id: 事件唯一标识符(可选)。
    • retry: 重连时间(毫秒,可选)。

示例

1
2
3
4
5
6
7
event: status_update
data: {"user": "Alice", "status": "online"}

id: 12345
data: This is a message.

retry: 3000

那再来看看ai输出的结果:

image.png

很标准的text/event-stream格式

使用langchainjs处理

你以为我要像上一篇一样开始手搓处理代码了吗,no no no,我们还是使用langchainjs进行处理,原因后面会提到。

这里推荐一个fetch封装工具:ofetch,一个类似axios的库,作用大家应该都懂了吧,这里我拿火山的接口来演示:

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
// vite.config.js
export default defineConfig({
base: "/",
server: {
proxy: {
"/huoshan": {
changeOrigin: true,
ws: true,
secure: false,
target: "https://ark.cn-beijing.volces.com",
rewrite: (path) => path.replace(/^\/huoshan/, ""),
},
},
},
});

// vue.config.js
module.export = {
devServer: {
compress: false, // 重点!!!不关闭则有可能导致无法正常流式返回
proxy: {
'/huoshan': {
target: 'https://ark.cn-beijing.volces.com', // 代理
changeOrigin: true,
ws: true,
secure: false,
pathRewrite: {
'^/huoshan': '',
},
},
}
}
}

如果是webpack的话,一定要关闭devServercompress,不然会导致整个请求结束才返回,这样就不是流式输出了。

1
2
3
4
5
6
7
8
9
10
11
// request.js

import { ofetch } from "ofetch";

export const fetchRequest = ofetch.create({
baseURL: '/huoshan',
timeout: 60000,
onRequest({ options }) {
options.headers.set('Authorization', 'Bearer xxxxx') // 替换火山api的key
},
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { fetchRequest } from "./request";
import { convertEventStreamToIterableReadableDataStream } from "@langchain/core/utils/event_source_parse";

async function test() {
const res = await fetchRequest("/api/v3/chat/completions", {
responseType: "stream",
method: "post",
body: {
model: "deepseek-v3-250324",
messages: [
{
role: "user",
content: "你是谁?",
},
],
stream: true,
},
});
const stream = convertEventStreamToIterableReadableDataStream(res);
for await (const chunk of stream) {
console.log(chunk);
}
}
test()

image.png

返回正常,不过要注意,结尾有个[DONE],所以不能无脑反序列化,

1
2
3
4
5
for await (const chunk of stream) {
if (chunk !== '[DONE]') {
console.log(JSON.parse(chunk))
}
}

这样就拿到每个chunk了,当然你可以将test方法改成生成器,然后for里面yield JSON.parse(chunk)

为什么要用langchainjs封装好的方法处理

既然大家都知道流式输出是一个一个chunk的方式返回,那么是不是有可能一行的文本,拆分成两个chunk(在js看来是ArrayBuffer)?而一个utf8字符是定长的,可能是1-3字节,那是不是有可能在某个字符的时候,其中一部分字节拆分到一个chunk,然后剩下部分字节拆分到下一个chunk?

这样就会导致你在decode的时候发生报错,无法正常decode成文字,所以langchainjs的方法考虑到这个情况:

image.png

代码在:https://github.com/langchain-ai/langchainjs/blob/5100a9d0a1eda7b7998dd40624abdd3ff3002b36/langchain-core/src/utils/event_source_parse.ts#L88-L165

其他关注点

使用代理时需要注意

上面的webpack配置已经讲解了一下devServer应该怎么配置才能流式输出。还有就是使用nginx代理的时候也需要修改一下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server {
listen 80;
location /huoshan/ {
# http1.1才支持长连接
proxy_http_version 1.1;
# 关闭代理缓冲
proxy_buffering off;
# 设置代理缓冲区大小
proxy_buffer_size 10k;
# 设置代理缓冲区数量和大小
proxy_buffers 4 10k;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass https://ark.cn-beijing.volces.com/;
}


}

其实就是关闭一些代理缓冲,以及设置一下缓冲区,为什么要这样设置,这里有请懂nginx配置的大佬细说一下😜

原文链接: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的未来!

0%