Milvus + RustFS+ Vibe Coding: Bauen Sie einen leichtgewichtigen RAG Chatbot von Grund auf neu
Dieser Blog wurde von Jinghe Ma, einem Mitarbeiter der Milvus-Community,verfasst und wird hier mit Genehmigung veröffentlicht.
Ich wollte einen Chatbot, der Fragen aus meiner eigenen Dokumentation beantworten kann, und ich wollte die volle Kontrolle über den Stack dahinter - von der Objektspeicherung bis zur Chat-Schnittstelle. Das brachte mich dazu, einen leichtgewichtigen RAG-Chatbot mit Milvus und RustFS als Kernstück zu bauen.
Milvus ist die am weitesten verbreitete Open-Source-Vektordatenbank für die Entwicklung von RAG-Anwendungen. Es trennt die Berechnung von der Speicherung und hält heiße Daten im Speicher oder auf SSD für eine schnelle Suche, während es sich auf Objektspeicher für ein skalierbares, kosteneffizientes Datenmanagement verlässt. Da es mit S3-kompatiblem Speicher arbeitet, war es die ideale Lösung für dieses Projekt.
Für die Speicherebene wählte ich RustFS, ein in Rust geschriebenes Open-Source-Objektspeichersystem, das mit S3 kompatibel ist. Es kann über Binary, Docker oder Helm Chart bereitgestellt werden. Obwohl es sich noch in der Alpha-Phase befindet und nicht für die Produktion empfohlen wird, war es stabil genug für diesen Build.
Sobald die Infrastruktur eingerichtet war, brauchte ich eine Wissensdatenbank zum Abfragen. Die RustFS-Dokumentation - ca. 80 Markdown-Dateien - war ein geeigneter Ausgangspunkt. Ich zerlegte die Dokumentation, generierte Einbettungen, speicherte sie in Milvus und programmierte den Rest mit Vibe: FastAPI für das Backend und Next.js für die Chat-Oberfläche.
In diesem Beitrag werde ich das gesamte System von Anfang bis Ende vorstellen. Der Code ist unter https://github.com/majinghe/chatbot verfügbar. Es handelt sich um einen funktionierenden Prototyp und nicht um ein produktionsreifes System, aber das Ziel ist es, ein klares, erweiterbares Build bereitzustellen, das Sie für Ihren eigenen Gebrauch anpassen können. Jeder der folgenden Abschnitte geht durch eine Schicht, von der Infrastruktur bis zum Frontend.
Installation von Milvus und RustFS mit Docker Compose
Beginnen wir mit der Installation von Milvus und RustFS.
Milvus kann mit jedem S3-kompatiblen Objektspeicher arbeiten, obwohl MinIO das Standard-Backend im Standard-Setup ist. Da MinIO keine Community-Beiträge mehr annimmt, werden wir es in diesem Beispiel durch RustFS ersetzen.
Um diese Änderung vorzunehmen, aktualisieren Sie die Objektspeicherkonfiguration in configs/milvus.yaml innerhalb des Milvus-Repositorys. Der entsprechende Abschnitt sieht wie folgt aus:
minio:
address: localhost:9000
port: 9000
accessKeyID: rustfsadmin
secretAccessKey: rustfsadmin
useSSL: false
bucketName: "rustfs-bucket"
Es gibt zwei Möglichkeiten, diese Änderung durchzuführen:
- Einhängen einer lokalen Konfigurationsdatei. Kopieren Sie configs/milvus.yaml lokal, aktualisieren Sie die MinIO-Felder, so dass sie auf RustFS verweisen, und binden Sie sie dann über ein Docker-Volume in den Container ein.
- Patch beim Start mit yq****. Ändern Sie den Befehl des Containers, um yq gegen /milvus/configs/milvus.yaml auszuführen, bevor der Milvus-Prozess startet.
Dieser Build verwendet den ersten Ansatz. Der Milvus-Dienst in docker-compose.yml erhält einen zusätzlichen Volume-Eintrag:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus/milvus.yaml:/milvus/configs/milvus.yaml:ro
Das Docker Compose Setup
Die vollständige docker-compose.yml führt vier Dienste aus.
etcd - Milvus ist für die Speicherung von Metadaten auf etcd angewiesen:
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://etcd: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
Attu - eine visuelle Benutzeroberfläche für Milvus, entwickelt und freigegeben von Zilliz (Hinweis: Versionen nach 2.6 sind Closed-Source):
attu:
container_name: milvus-attu
image: zilliz/attu:v2.6
environment:
- MILVUS_URL=milvus-standalone:19530
ports:
- "8000:3000"
restart: unless-stopped
RustFS - das Objektspeicher-Backend:
rustfs:
container_name: milvus-rustfs
image: rustfs/rustfs:1.0.0-alpha.58
environment:
- RUSTFS_VOLUMES=/data/rustfs0,/data/rustfs1,/data/rustfs2,/data/rustfs3
- RUSTFS_ADDRESS=0.0.0.0:9000
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_EXTERNAL_ADDRESS=:9000 # Same as internal since no port mapping
- RUSTFS_CORS_ALLOWED_ORIGINS=*
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
- RUSTFS_ACCESS_KEY=rustfsadmin
- RUSTFS_SECRET_KEY=rustfsadmin
ports:
- "9000:9000"# S3 API port
- "9001:9001"# Console port
volumes:
- rustfs_data_0:/data/rustfs0
- rustfs_data_1:/data/rustfs1
- rustfs_data_2:/data/rustfs2
- rustfs_data_3:/data/rustfs3
- logs_data:/app/logs
restart: unless-stopped
healthcheck:
test:
[
"CMD",
"sh", "-c",
"curl -f http://localhost:9000/health && curl -f http://localhost:9001/health"
]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Milvus - läuft im Standalone-Modus:
standalone:
container_name: milvus-standalone
image: milvusdb/milvus:v2.6.0
command: ["milvus", "run", "standalone"]
security_opt:
- seccomp:unconfined
environment:
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: rustfs:9000
MQ_TYPE: woodpecker
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus/milvus.yaml:/milvus/configs/milvus.yaml:ro
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"
- "rustfs"
Alles starten
Sobald die Konfiguration eingerichtet ist, starten Sie alle vier Dienste:
docker compose -f docker-compose.yml up -d
Mit können Sie überprüfen, ob alles läuft:
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4404b5cc6f7e milvusdb/milvus:v2.6.0 "/tini -- milvus run…" 53 minutes ago Up 53 minutes (healthy) 0.0.0.0:9091->9091/tcp, :::9091->9091/tcp, 0.0.0.0:19530->19530/tcp, :::19530->19530/tcp milvus-standalone
40ddc8ed08bb zilliz/attu:v2.6 "docker-entrypoint.s…" 53 minutes ago Up 53 minutes 0.0.0.0:8000->3000/tcp, :::8000->3000/tcp milvus-attu
3d2c8d80a8ce quay.io/coreos/etcd:v3.5.18 "etcd -advertise-cli…" 53 minutes ago Up 53 minutes (healthy) 2379-2380/tcp milvus-etcd
d760f6690ea7 rustfs/rustfs:1.0.0-alpha.58 "/entrypoint.sh rust…" 53 minutes ago Up 53 minutes (unhealthy) 0.0.0.0:9000-9001->9000-9001/tcp, :::9000-9001->9000-9001/tcp milvus-rustfs
Wenn alle vier Container hochgefahren sind, sind Ihre Dienste verfügbar unter:
- Milvus:
:19530 - RustFS:
:9000 - Attu:
:8000
Vektorisierung der RustFS-Dokumente und Speicherung der Einbettungen in Milvus
Wenn Milvus und RustFS laufen, besteht der nächste Schritt darin, die Wissensbasis aufzubauen. Das Ausgangsmaterial ist die chinesische RustFS-Dokumentation: 80 Markdown-Dateien, die Sie zerhacken, einbetten und in Milvus speichern werden.
Lesen und Chunking der Docs
Das Skript liest rekursiv jede .md-Datei im docs-Ordner und teilt dann den Inhalt jeder Datei nach Zeilenumbruch in Chunks auf:
# 3. Read Markdown files
def load_markdown_files(folder):
files = glob.glob(os.path.join(folder, "**", "*.md"), recursive=True)
docs = []
for f in files:
with open(f, "r", encoding="utf-8") as fp:
docs.append(fp.read())
return docs
# 4. Split documents (simple paragraph-based splitting)
def split_into_chunks(text, max_len=500):
chunks, current = [], []
for line in text.split(“\n”):
if len(" ".join(current)) + len(line) < max_len:
current.append(line)
else:
chunks.append(" ".join(current))
current = [line]
if current:
chunks.append(" ".join(current))
return chunks
Diese Chunking-Strategie ist absichtlich einfach. Wenn Sie eine genauere Kontrolle wünschen - Aufteilung nach Kopfzeilen, Beibehaltung von Codeblöcken oder Überlappung von Chunks zum besseren Auffinden - ist dies der richtige Ort, um zu iterieren.
Einbetten der Chunks
Wenn die Chunks fertig sind, betten Sie sie mit OpenAIs text-embedding-3-large-Modell ein, das 3072-dimensionale Vektoren ausgibt:
def embed_texts(texts):
response = client.embeddings.create(
model="text-embedding-3-large",
input=texts
)
return [d.embedding for d in response.data]
Speichern von Einbettungen in Milvus
Milvus organisiert die Daten in Sammlungen, die jeweils durch ein Schema definiert sind. Hier speichert jeder Datensatz den Rohtextchunk zusammen mit seinem Einbettungsvektor:
# Connect to Milvus
connections.connect("default", host="ip", port="19530")
# Define the schema
fields = [
FieldSchema(name=“id”, dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name=“content”, dtype=DataType.VARCHAR, max_length=2000),
FieldSchema(name=“embedding”, dtype=DataType.FLOAT_VECTOR, dim=3072),
]
schema = CollectionSchema(fields, description=“Markdown docs collection”)
# Create the collection
if utility.has_collection(“docs_collection”):
utility.drop_collection(“docs_collection”)
collection = Collection(name=“docs_collection”, schema=schema)
# Insert data
collection.insert([all_chunks, embeddings])
collection.flush()
Sobald das Einfügen abgeschlossen ist, können Sie die Sammlung in Attu unter
Sie können auch in RustFS unter
Aufbau einer RAG-Pipeline mit Milvus und OpenAIs GPT-5
Mit den in Milvus gespeicherten Einbettungen haben Sie alles, was Sie zum Aufbau der RAG-Pipeline benötigen. Der Ablauf ist: Einbetten der Benutzeranfrage, Abrufen der semantisch ähnlichsten Chunks aus Milvus, Zusammenstellen eines Prompts und Aufrufen des GPT-5. Der Aufbau hier verwendet OpenAIs GPT-5, aber jedes Chat-fähige Modell funktioniert hier - die Abrufschicht ist das, worauf es ankommt, und Milvus handhabt das unabhängig davon, welcher LLM die endgültige Antwort erzeugt.
# 1. Embed the query
query_embedding = embed_texts(query)
# 2. Retrieve similar documents from Milvus
search_params = {“metric_type”: “COSINE”, “params”: {“nprobe”: 10}}
results = collection.search(
data=[query_embedding],
anns_field=“embedding”,
param=search_params,
limit=3,
output_fields=[“content”]
)
docs = [hit.entity.get(“text”) for hit in results[0]]
# 3. Assemble the RAG prompt
prompt = f"You are a RustFS expert. Answer the question based on the following documents:\n\n{docs}\n\nUser question: {query}"
# 4. Call the LLM
response = client.chat.completions.create(
model=“gpt-5”, # swap to any OpenAI model, or replace this call with another LLM provider
messages=[{“role”: “user”, “content”: prompt}],
# max_tokens=16384,
# temperature=1.0,
# top_p=1.0,
)
answer = response.choices[<span class="hljs-number">0</span>].message.content
<span class="hljs-keyword">return</span> {<span class="hljs-string">"answer"</span>: answer, <span class="hljs-string">"sources"</span>: docs}
Um es zu testen, führen Sie eine Abfrage durch:
How do I install RustFS in Docker?
Abfrageergebnisse:
Alles in einen FastAPI + Next.js Chatbot verpacken
Die RAG-Pipeline funktioniert, aber jedes Mal, wenn man eine Frage stellen möchte, ein Python-Skript auszuführen, verfehlt den Zweck. Also habe ich AI um einen Stack-Vorschlag gebeten. Die Antwort: FastAPI für das Backend - der RAG-Code ist bereits Python, also ist die Einbettung in einen FastAPI-Endpunkt die natürliche Lösung - und Next.js für das Frontend. FastAPI stellt die RAG-Logik als HTTP-Endpunkt zur Verfügung; Next.js ruft sie auf und rendert die Antwort in einem Chatfenster.
FastAPI Backend
FastAPI verpackt die RAG-Logik in einen einzigen POST-Endpunkt. Jeder Client kann nun Ihre Wissensdatenbank mit einer JSON-Anfrage abfragen:
app = FastAPI()
@app.post(“/chat”)
def chat(req: ChatRequest):
query = req.query
…
Starten Sie den Server mit:
uvicorn main:app --reload --host 0.0.0.0 --port 9999
INFO: Will watch for changes in these directories: ['/home/xiaomage/milvus/chatbot/.venv']
INFO: Uvicorn running on http://0.0.0.0:9999 (Press CTRL+C to quit)
INFO: Started reloader process [2071374] using WatchFiles
INFO: Started server process [2071376]
INFO: Waiting for application startup.
INFO: Application startup complete.
Next.js Frontend
Das Frontend sendet die Anfrage des Benutzers an den FastAPI-Endpunkt und rendert die Antwort. Die zentrale Abfragelogik:
javascript
try {
const res = await fetch('http://localhost:9999/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: input }),
});
<span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> res.<span class="hljs-title function_">json</span>();
<span class="hljs-keyword">const</span> <span class="hljs-attr">botMessage</span>: <span class="hljs-title class_">Message</span> = { <span class="hljs-attr">role</span>: <span class="hljs-string">'bot'</span>, <span class="hljs-attr">content</span>: data.<span class="hljs-property">answer</span> || <span class="hljs-string">'No response'</span> };
<span class="hljs-title function_">setMessages</span>(<span class="hljs-function"><span class="hljs-params">prev</span> =></span> [...prev, userMessage, botMessage]);
} <span class="hljs-keyword">catch</span> (error) {
<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(error);
<span class="hljs-keyword">const</span> <span class="hljs-attr">botMessage</span>: <span class="hljs-title class_">Message</span> = { <span class="hljs-attr">role</span>: <span class="hljs-string">'bot'</span>, <span class="hljs-attr">content</span>: <span class="hljs-string">'Error connecting to server.'</span> };
<span class="hljs-title function_">setMessages</span>(<span class="hljs-function"><span class="hljs-params">prev</span> =></span> [...prev, botMessage]);
} <span class="hljs-keyword">finally</span> {
<span class="hljs-title function_">setLoading</span>(<span class="hljs-literal">false</span>);
}
Starten Sie das Frontend mit:
pnpm run dev -H 0.0.0.0 -p 3000
> rag-chatbot@0.1.0 dev /home/xiaomage/milvus/chatbot-web/rag-chatbot
> next dev --turbopack -H 0.0.0.0 -p 3000
▲ Next.js 15.5.3 (Turbopack)
- Local: http://localhost:3000
- Network: http://0.0.0.0:3000
✓ Starting…
✓ Ready in 1288ms
Öffnen Sie http://<ip>:3000/chat in Ihrem Browser.
Geben Sie eine Frage ein:
How do I install RustFS in Docker?
Antwort der Chat-Schnittstelle::
Und schon ist der Chatbot fertig.
Fazit
Was als Neugier auf das Speicher-Backend von Milvus begann, wurde zu einem voll funktionsfähigen RAG-Chatbot - und der Weg vom einen zum anderen war kürzer als erwartet. Hier ist, was der Build umfasst, von Anfang bis Ende:
- Milvus + RustFS mit Docker Compose. Milvus läuft im Standalone-Modus mit RustFS als Objektspeicher-Backend und ersetzt damit das standardmäßige MinIO. Insgesamt vier Dienste: etcd, Milvus, RustFS und Attu.
- Vektorisierung der Wissensbasis. Die RustFS-Dokumentation - 80 Markdown-Dateien - wird gechunked, mit text-embedding-3-large eingebettet und in Milvus als 466 Vektoren gespeichert.
- Die RAG-Pipeline. Zur Abfragezeit wird die Frage des Benutzers auf die gleiche Weise eingebettet, Milvus ruft die drei semantisch ähnlichsten Chunks ab, und GPT-5 generiert eine Antwort, die auf diesen Dokumenten basiert.
- Die Chatbot-Benutzeroberfläche. FastAPI verpackt die Pipeline in einen einzigen POST-Endpunkt; Next.js setzt ein Chatfenster davor. Man muss nicht mehr in ein Terminal springen, um eine Frage zu stellen.
Ein paar meiner Erkenntnisse aus diesem Prozess:
- Die Dokumentation von Milvus ist großartig. Vor allem die Deployment-Abschnitte sind klar, vollständig und einfach zu verstehen.
- Die Arbeit mitRustFS als Objektspeicher-Backend ist ein Vergnügen. Es für MinIO einzusetzen, war weniger aufwändig als erwartet.
- Vibe Coding ist schnell, bis der Scope die Kontrolle übernimmt. Eines führte zum anderen - Milvus zu RAG zu Chatbot zu "vielleicht sollte ich das Ganze dockerisieren". Die Anforderungen konvergieren nicht von selbst.
- Beim Debuggen lernt man mehr als beim Lesen. Jeder Fehler in diesem Build ließ den nächsten Abschnitt schneller klicken, als es jede Dokumentation getan hätte.
Der gesamte Code dieses Builds ist unter github.com/majinghe/chatbot zu finden. Wenn Sie Milvus selbst ausprobieren wollen, ist der Schnellstart ein guter Anfang. Wenn du über deine Arbeit sprechen möchtest oder auf etwas Unerwartetes stößt, kannst du uns im Milvus Slack finden. Wenn Sie lieber ein persönliches Gespräch führen möchten, können Sie auch einen Termin während der Bürozeiten vereinbaren.
Try Managed Milvus for Free
Zilliz Cloud is hassle-free, powered by Milvus and 10x faster.
Get StartedLike the article? Spread the word



