Pinned Post

Who picks the tool

Bild
The previous post ended with an agent that writes a missing tool into a running node. Generate, sanitize, persist, compile, load, execute, about 25 milliseconds after the model answers. I was happy with that for roughly a week. Then I looked at the demo page again and counted the text fields. Tool name. What it should do. Parameters as JSON. I had filled in all three. The gap-finding was real. The deciding was mine. MetaPlannerAgent.run("Publish the article", [ %{tool: "classic_plan", params: %{goal: "Write draft, review, publish"}}, %{tool: "slugify_text", description: "Turns a title into a URL slug", params: %{text: "Hello World"}} ]) Everything interesting in that call sits in the second argument, and I typed it. The agent resolved slugify_text , noticed it wasn't in the catalog and had it written. It never asked whether the task needed a slug at all. So this post is about the agent in front of that, the on...

ML-Last verschieben mit einer Zeile Code

Mein Webserver läuft auf einer günstigen Linux-Box ohne GPU, das Embedding-Modell aber will Rechenpower. Die Lösung: Das Modell läuft auf meinem MacBook, die App auf dem Server. Und der Code, der die Inferenz aufruft, sieht exakt gleich aus, als liefe alles auf einer Maschine.

Das ist der Teil des Projekts, bei dem die meisten Leute, denen ich es zeige, kurz innehalten. Nicht weil es kompliziert wäre, sondern weil es so wenig Code ist für etwas, das in anderen Stacks ein eigenes Infrastruktur-Projekt wäre.

Das Problem: Die App läuft produktiv auf einer kleinen Linux-Maschine, die für Phoenix und Postgres reicht. Ein 278-MB-Transformer-Modell mit EXLA-Inferenz wird darauf aber zäh. Die nötige Rechenleistung sitzt woanders, nämlich in meinem MacBook Pro mit ordentlich CPU und GPU.

In der typischen Microservice-Welt heißt das jetzt: das Modell in einen eigenen Service packen, eine API drumherum bauen (gRPC oder REST), Serialisierung definieren, dazu Service-Discovery, Timeouts, Retries, ein zweites Deployment-Artefakt und Monitoring für die Verbindung dazwischen. Das ist tagelange Arbeit, und am Ende hast du einen neuen verteilten Fehlerfall geschaffen.

Auf der BEAM ist „eine andere Maschine" dagegen eingebaut. Die Erlang-VM ist seit den 80ern für verteilte Systeme gemacht. Mehrere Knoten verbinden sich zu einem Cluster und rufen sich gegenseitig auf, als wären sie ein einziger Prozess. Das Werkzeug für ML-Inferenz, Nx.Serving, ist von Grund auf verteilungs-transparent: Es kann lokal laufen oder den Aufruf an einen anderen Knoten weiterreichen. Der aufrufende Code ist in beiden Fällen identisch:

# Das ist der gesamte Inferenz-Aufruf. Ob die Serving lokal läuft
# oder auf dem MacBook im selben Cluster, diese Zeile ändert sich nicht.
Nx.Serving.batched_run(@serving_name, "query: " <> text)

Die ganze Architektur fällt auf eine einzige Rollen-Unterscheidung beim Start zusammen. Es ist dieselbe Codebasis und dieselbe Binary-Logik. Eine Umgebungsvariable entscheidet, was ein Knoten hochfährt:

defp serving_children(:ml), do: [Finance.Classifier.Bumblebee] # nur das Modell
defp serving_children(:web), do: [] # kein Modell

defp web_children(:ml), do: [] # keine DB, kein Phoenix
defp web_children(_), do: [Finance.Repo, ...] # die volle Web-App
  • Der Web-Knoten (Linux) fährt Phoenix, Postgres und die Klassifikations-Logik hoch, aber nicht das Modell.
  • Der ML-Knoten (MacBook) fährt nur das Modell hoch. Keine Datenbank, kein Webserver.

Verbunden werden sie über libcluster, und die schwere Nx.Serving.batched_run-Berechnung wird automatisch zum ML-Knoten geroutet. Mein Klassifikations-Code weiß nicht und muss auch nicht wissen, auf welcher Maschine die Matrix-Multiplikationen passieren.

Der wichtigste Teil ist die Frage: Was, wenn das MacBook aus ist? Eine naive verteilte App würde abstürzen oder hängen bleiben. Hier fange ich den Fall, dass der ML-Knoten nicht erreichbar ist, ab und behandle ihn als eigenständigen Zustand statt als Fehler:

defp embed(text, prefix) do
Nx.Serving.batched_run(@serving_name, prefix <> text)
catch
# ML-Knoten nicht erreichbar -> kein Crash, kein Fehlklassifizieren.
:exit, reason -> throw({:classifier_unavailable, reason})
end

Die Folge: Ist das Modell offline, bleibt die Transaktion einfach unklassifiziert liegen, das UI zeigt einen „später erneut versuchen"-Hinweis, und nichts wird falsch einsortiert. „Ehrlich unsicher" statt „selbstbewusst falsch", dasselbe Prinzip wie beim Schwellwert aus Artikel 2, nur jetzt auf der Infrastruktur-Ebene.

Die Lektion daraus: Verteilte ML-Inferenz gilt oft als schweres Infrastruktur-Thema, und in vielen Stacks ist sie das auch. Auf der BEAM ist „lass das woanders rechnen" kein Architektur-Projekt, sondern ein Baustein der Plattform. Du bezahlst nicht mit einem Service-Mesh, sondern mit ein paar Zeilen Rollen-Logik und einem catch.

Das ist es, was ich mit „guter ML-Unterstützung" bei Elixir meine. Es geht nicht nur darum, dass man Modelle laden kann, sondern darum, dass die Architektur drumherum schon da ist, bevor du anfängst: Fehlertoleranz, Verteilung, Nebenläufigkeit.

Im nächsten Teil, zum Abschluss: der Teil des ML-Features, der bewusst kein ML ist.

→ Weiter zu Teil 5: Wann du KEIN ML nehmen solltest

→ Ein ML-Modell und eine Web-App auf zwei BEAM-Knoten trennen — der technische Bauplan

Kommentare

Beliebte Posts aus diesem Blog

Splitting an ML model and a web app across two BEAM nodes — the technical blueprint

Jido in Practice: Agents in Elixir as Composable Actions

A Foundation Model in the BEAM: On-Chain Anomalies with Google TimesFM in Elixir