writing / 2026-09-19
LLM jobs on an agent fleet
The homelab's fire-and-forget LLM job service, first built as a queue in front of a LiteLLM gateway, then rebuilt so every job is a one-shot task for a NanoClaw agent group, with local, Claude and Blender render tiers.
Some LLM work does not belong in the session that thinks of it: a hundred summaries, a batch of classifications, a long low-stakes draft, anything private that should stay on the LAN. The homelab has a small service for that. A caller submits a prompt and a tier, gets a job id back at once, and collects the result later. It has now been built twice.
Version one: a queue and a gateway
The June 2026 version ran on nemesis as two stacks. llm-queue was a FastAPI app with a web dashboard, a REST API and an MCP endpoint, all over one SQLite database in WAL mode. Callers picked a capability alias (fast on the B70’s vLLM, background and reasoning on CPU Ollama), and each alias had its own worker so a slow CPU job never blocked a GPU job.
The second phase put a LiteLLM gateway in front of the models. The gateway issued each user a virtual key, and that key was the login everywhere: web UI, REST and MCP. The queue scoped jobs to a hash of the key, and ran every job through the gateway as that user, so quotas lived in one place. That meant the worker needed the caller’s key after the request had returned, so each job row carried the key Fernet-encrypted, and the worker decrypted it, ran the job and scrubbed it once the job was finished or cancelled. LiteLLM would not keep its keys in SQLite, so the gateway also needed a Postgres sidecar.
It worked, and it was a lot of machinery.
The fleet it moved onto
In September the homelab got a NanoClaw fleet on devastator. NanoClaw runs Claude Code agents in containers, organized as agent groups: each group has its own instructions, its own skills, its own mounts and environment, and wakes for messages from a channel or for scheduled and one-shot tasks. Some groups talk to Anthropic directly. Others run on qwen3-14b, served by vLLM on the local B70 and reached through a small normalizing proxy that makes the local model’s API behave enough like Anthropic’s for the Claude Code harness (it folds system messages, clamps output tokens and turns off qwen3’s thinking mode).
That fleet already did the routing, access control and audit the queue had built for itself. And nothing had used the queue in 30 days.
Version two: jobs as agent tasks
On 2026-09-19 the queue and the gateway were composed down and removed from the repo. The replacement, llm-jobs, is a Python standard-library service running as a systemd user unit on devastator. It owns two things, keys and jobs, both in one SQLite file. Everything else is NanoClaw.
A job goes like this:
- A caller submits a prompt and a tier.
- The service creates a one-shot NanoClaw task on that tier’s agent group.
- The group’s container wakes with a short worker contract prepended: do exactly the job, deliver once, no channel posts, no questions.
- The agent calls the service’s MCP tool
deliver_resultwith the job id and its answer, or with an error sentence if the job cannot be done. - The caller polls and fetches.
Claude Code sessions reach it as an MCP server with three tools that matter: submit_job, get_status and get_result (plus list_tiers, list_jobs and cancel_job). A typical call looks like:
submit_job(
tier="local",
prompt="Summarize the release notes below in five bullets for a homelab operator. <notes...>",
params={"max_words": 150}
)
The prompt must be self-contained; the worker sees nothing else. The params hints become lines in the worker contract. Keys are bearer keys, scoped per owner: a caller sees only its own jobs, and results are kept 30 days. One job runs per tier at a time, and a job with no delivery inside its timeout is failed and its task cancelled.
Three tiers
- local runs on the
jobs-localgroup: qwen3-14b through the proxy, with Anthropic blocked from the container. Free and private. A trivial job came back in 16 seconds. - claude runs on
jobs-claude: Sonnet, metered. A trivial job took 50 seconds. NanoClaw has no per-group usage cap, so the service enforces one: per-key daily caps, plus a tier-wide daily ceiling on jobs and output tokens that every key shares. However many keys are out, the metered tier has one ceiling. - render, added on 2026-09-26, runs on
jobs-render. The prompt is a JSON recipe (a model URL, views, samples, size), the group has ablender-renderskill and a read-only Blender install, and it renders stills headless with Cycles. A tier can ask for artifacts, and this one does: each job gets its own folder mounted into the container,get_resultlists the files, and the service serves them to the job’s owner. With"photo": truein the recipe, the worker also callsrequest_photofor each view and pollsphoto_status. That queues a pass through the GPU arbiter on devastator, which lends the B70 from vLLM to a FLUX Kontext image model when the card is idle, and writes a photo next to each render.
The worker tools (deliver_result, request_photo, photo_status) are visible only to workers.
What did not work
- qwen3 cannot drive tool calls through the proxy. Plain text jobs work on the local tier. Behind the proxy, qwen3-14b fails on tool-call turns with “Content block is not a text block”, and a render job is nothing but tool calls. The render tier runs on Sonnet instead, under its own small daily cap; a render job needs only a few of those calls.
- No GPU in agent containers. NanoClaw has no device passthrough, so Blender renders on the CPU, about 35 seconds a view at 128 samples. The photo step gets the GPU only because the arbiter runs on the host, outside NanoClaw, and the worker asks it for photos over MCP.
What carries forward
The second version is smaller because it stopped owning things another system already owned. The job service keeps keys, jobs, caps and artifacts; NanoClaw keeps models, containers, skills and credentials. A new tier is a new agent group with its own instructions and skills, plus one entry in the tier table, which is how a Blender renderer arrived a week later without any change to how callers submit. And the delivery contract is the same for every tier: the worker calls one tool, exactly once, and stops.
LLMジョブをエージェント艦隊で実行する
ホームラボのfire-and-forget LLMジョブサービス。最初はLiteLLMゲートウェイの前にキューとして構築され、その後はすべてのジョブがNanoClawエージェントグループ向けのワンショットタスクになるよう再構築され、ローカル、Claude、Blenderのレンダリング階層を備えています。
LLMの一部の作業は、セッションとして扱うべきではない。100の要約、分類のバッチ、長くても低リスクの下書き、LAN上に残るべきプライベートな内容などである。ホームラボには、このような作業を処理するための小さなサービスがある。呼び出し元はプロンプトと階層を送信し、すぐにジョブIDを受け取り、後で結果を収集する。このサービスはすでに2回構築された。
バージョン1: キューとゲートウェイ
2026年6月のバージョンは、nemesis上に2つのスタックとして実行されていた。llm-queueは、FastAPIアプリで、Webダッシュボード、REST API、MCPエンドポイントを1つのWALモードのSQLiteデータベース上で提供していた。呼び出し元は、能力のエイリアス(B70のvLLM上のfast、CPU Ollama上のbackgroundとreasoning)を選択し、それぞれのエイリアスには独自のワーカーが存在したため、遅いCPUジョブがGPUジョブを妨げること never なかった。
2段階目では、モデルの前にLiteLLMゲートウェイを配置した。ゲートウェイは、各ユーザーに仮想キーを発行し、そのキーがすべての場所でログインに使用された。キューは、キーのハッシュに基づいてジョブをスコープ化し、ゲートウェイを通じてそのユーザーとしてすべてのジョブを実行したため、クォータは1箇所に管理された。つまり、ワーカーは、リクエストが返却された後に呼び出し元のキーが必要となったため、各ジョブ行はFernetで暗号化されたキーを保持し、ワーカーはそれを復号化し、ジョブを実行し、ジョブが完了またはキャンセルされた後に情報を削除した。LiteLLMはSQLiteにキーを保持しないため、ゲートウェイにはPostgreSQLのサイドカーも必要だった。
これは動作し、多くの機械仕掛けを必要とした。
それが動かした艦隊
9月、homelabはdevastatorにNanoClawの艦隊を導入しました。NanoClawは、コンテナ内でClaude Codeエージェントを実行し、エージェントグループとして組織されています。各グループは独自の指示、スキル、マウント、環境を持ち、特定のチャンネルからのメッセージ、またはスケジュールされたタスクや一時的なタスクのためだけに起動します。一部のグループはAnthropicと直接通信します。他のグループは、ローカルのB70でvLLMによって提供されるqwen3-14b上で実行され、AnthropicのAPIとある程度似た挙動を示す小さな正規化プロキシを通じてアクセスされます(このプロキシは、システムメッセージを折り畳み、出力トークンを制限し、qwen3の思考モードを無効にします)。
この艦隊は、すでにキューが自ら構築したルーティング、アクセス制御、および監査を担当していました。そして、このキューは30日間使用されていませんでした。
バージョン2: ジョブをエージェントタスクとして
2026年9月19日、キューとゲートウェイは削除され、リポジトリから取り除かれました。代替として、llm-jobsはdevastator上でsystemdユーザー単位として実行されるPython標準ライブラリサービスです。このサービスは、鍵とジョブの2つの要素を1つのSQLiteファイルに保存しています。それ以外のすべてはNanoClawです。
ジョブの流れは以下の通りです:
- 呼び出し元がプロンプトと階層を送信します。
- サービスは、その階層のエージェントグループで一時的なNanoClawタスクを作成します。
- グループのコンテナは、短いワーカー契約を先頭に付けて起動します: ジョブを正確に実行し、1回だけ結果を返し、チャンネル投稿や質問は行いません。
- エージェントは、ジョブIDとその回答、またはジョブが実行できない場合のエラーメッセージを含むサービスのMCPツール
deliver_resultを呼び出します。 - 呼び出し元はポーリングを行い、結果を取得します。
Claude Codeセッションは、3つの重要なツールを持つMCPサーバーとしてこのサービスにアクセスします: submit_job、get_status、get_result(さらにlist_tiers、list_jobs、cancel_jobもあります)。典型的な呼び出しは以下のようになります:
submit_job(
tier="local",
prompt="Summarize the release notes below in five bullets for a homelab operator. <notes...>",
params={"max_words": 150}
)
プロンプトは自己完結型でなければなりません。ワーカーはそれ以外の情報を何も見ません。paramsのヒントは、ワーカー契約の行になります。キーはベアラー・キーであり、所有者ごとにスコープが設定されています。呼び出し元は自分のみのジョブを確認でき、結果は30日間保持されます。1つのティアに1つのジョブが同時に実行され、タイムアウト内に配信がない場合は、そのジョブは失敗とされ、タスクはキャンセルされます。
3つのティア
- local は
jobs-localグループ上で実行されます: qwen3-14b はプロキシを通じて実行され、Anthropic はコンテナからブロックされています。無料でプライベートです。簡単なジョブは16秒で戻ってきました。 - claude は
jobs-claude上で実行されます: Sonnet、メータリング済み。簡単なジョブは50秒かかりました。NanoClaw にはグループごとの使用量上限はありませんので、サービスは1つ設定しています: キーごとの日次上限、およびすべてのキーが共有するティア全体の日次上限(ジョブ数と出力トークン数)。どのくらいのキーが存在しても、メータリングされたティアには1つの上限があります。 - render は 2026-09-26 に追加され、
jobs-render上で実行されます。プロンプトは JSON レシピ(モデルURL、ビュー、サンプル、サイズ)です。グループにはblender-renderスキルと読み取り専用の Blender インストールがあり、Cycles を使用してヘッドレスで静止画をレンダリングします。ティアはアーティファクトを要求できます。このティアはそうしています: 各ジョブはコンテナにマウントされた独自のフォルダを取得し、get_resultはファイルを一覧表示し、サービスはそれらをジョブの所有者に提供します。レシピに"photo": trueが含まれている場合、ワーカーはrequest_photoを各ビューに対して呼び出し、photo_statusをポーリングします。それにより、devastator 上の GPU アービタにパスがキューイングされ、カードが空いているときに vLLM から B70 を FLUX Kontext 画像モデルに貸し出し、各レンダリングの隣に写真を保存します。
ワーカー・ツール(deliver_result、request_photo、photo_status)はワーカーのみに表示されます。
うまくいかなかったこと
- qwen3はプロキシを通じてツールコールを実行できません。 プレーンテキストのジョブはローカル層で動作します。プロキシの後ろでは、qwen3-14bはツールコールのターンで「Content block is not a text block」というエラーになり、レンダリングジョブはツールコール以外のものではありません。レンダリング層はSonnetで実行され、独自の小さな日次上限があります。レンダリングジョブには、それらのコールを数回行うだけで済みます。
- エージェントコンテナにGPUがありません。 NanoClawにはデバイスパススルーがありませんので、BlenderはCPUでレンダリングを行い、128サンプルあたり約35秒かかります。写真のステップではGPUが使用できるのは、アービターがNanoClawの外側、ホスト上に実行されているため、ワーカーはMCPを通じてアービターに写真を依頼するからです。
進化した点
2バージョン目は、もう1つのシステムがすでに所有していたものを所有しなくなったため、サイズが小さくなっています。ジョブサービスは鍵、ジョブ、上限、アーテファクトを保持し、NanoClawはモデル、コンテナ、スキル、資格情報を保持します。新しい層は、独自の指示とスキルを持つ新しいエージェントグループ、および層テーブルに1つのエントリを追加することによって実現されます。このように、呼び出し元がどのように送信するかを変更することなく、1週間後にBlenderレンダラーが到着しました。そして、すべての層で配信契約は同じです:ワーカーは1つのツールを1回だけ呼び出し、その後終了します。
에이전트 플릿에서의 LLM 작업
하이메이트의 fire-and-forget LLM 작업 서비스, 처음에는 LiteLLM 게이트웨이 앞에 있는 대기열로 구축되었고, 이후에는 모든 작업이 NanoClaw 에이전트 그룹에 대한 단일 작업으로 재구성되어, 로컬, Claude 및 Blender 렌더링 계층을 갖게 되었다.
일부 LLM 작업은 세션에서 처리되어서는 안 된다: 수십 개의 요약, 일괄 분류, 긴 낮은 위험도 초안, LAN에 남아야 할 개인 정보 등이다. 홈 랩에는 그런 작업을 처리할 수 있는 작은 서비스가 있다. 호출자는 프롬프트와 계층을 제출하면, 즉시 작업 ID를 받고 나중에 결과를 수집한다. 이 서비스는 두 번 만들어졌다.
첫 번째 버전: 대기열과 게이트웨이
2026년 6월 버전은 nemesis에서 두 개의 스택으로 실행되었다. llm-queue은 WAL 모드의 하나의 SQLite 데이터베이스 위에 웹 대시보드, REST API 및 MCP 엔드포인트를 갖춘 FastAPI 앱이었다. 호출자는 B70의 vLLM에서 fast, CPU Ollama에서 background 및 reasoning과 같은 능력 별칭을 선택했으며, 각 별칭은 자체 워커를 가졌기 때문에 느린 CPU 작업이 GPU 작업을 차단하지 않았다.
두 번째 단계에서는 모델 앞에 LiteLLM 게이트웨이를 배치했다. 게이트웨이는 각 사용자에게 가상 키를 발급했으며, 이 키는 모든 곳에서 로그인에 사용되었다: 웹 UI, REST 및 MCP. 대기열은 키의 해시에 따라 작업을 범위 지정했으며, 모든 작업은 해당 사용자로 게이트웨이를 통해 실행되었기 때문에 할당량은 한 곳에서 관리되었다. 이는 워커가 요청이 반환된 후 호출자의 키가 필요하게 되었음을 의미했으며, 따라서 각 작업 행은 키를 Fernet 암호화하여 저장했고, 워커는 이를 해독하여 작업을 실행하고 작업이 완료되거나 취소되면 이를 정리했다. LiteLLM은 SQLite에 키를 보관하지 않기 때문에 게이트웨이에는 Postgres 사이드카도 필요했다.
이것은 작동했으며, 많은 장비가 필요했다.
이 서비스가 이전한 플랫폼
9월에 homelab은 devastator에 NanoClaw 대규모 편대를 추가했습니다. NanoClaw는 컨테이너에서 Claude Code 에이전트를 실행하며, 에이전트 그룹으로 구성됩니다. 각 그룹은 자체 지시사항, 자체 기술, 자체 마운트 및 환경을 가지며, 특정 채널에서 메시지를 받거나 예약 및 단일 작업을 위해 활성화됩니다. 일부 그룹은 Anthropic과 직접 통신합니다. 다른 그룹은 로컬 B70에서 vLLM을 통해 제공되는 qwen3-14b 모델에서 실행되며, 이는 작은 정규화 프록시를 통해 로컬 모델의 API가 Anthropic의 API와 충분히 유사하게 작동하도록 합니다(시스템 메시지를 접합하고 출력 토큰 수를 제한하며 qwen3의 사고 모드를 비활성화합니다).
이 편대는 이미 큐가 자체적으로 구축한 라우팅, 접근 제어 및 감사를 수행해 왔습니다. 그리고 30일 동안 아무것도 큐를 사용하지 않았습니다.
두 번째 버전: 작업을 에이전트 작업으로
2026-09-19에 큐와 게이트웨이는 구성되어 제거되고 저장소에서 제거되었습니다. 대체품인 llm-jobs은 Python 표준 라이브러리 서비스로, devastator에서 systemd 사용자 유닛으로 실행됩니다. 이 서비스는 키와 작업 두 가지를 소유하며, 모두 하나의 SQLite 파일에 저장됩니다. 나머지는 모두 NanoClaw입니다.
작업의 흐름은 다음과 같습니다:
- 호출자는 프롬프트와 계층을 제출합니다.
- 서비스는 해당 계층의 에이전트 그룹에서 단일 작업용 NanoClaw 작업을 생성합니다.
- 그룹의 컨테이너는 짧은 작업자 계약을 앞에 추가하여 활성화됩니다: 작업을 정확히 수행하고, 한 번만 결과를 전달하며, 채널 게시물이나 질문은 하지 않습니다.
- 에이전트는 작업 ID와 답변을 서비스의 MCP 도구
deliver_result에 전달하거나, 작업을 수행할 수 없는 경우 오류 문장을 전달합니다. - 호출자는 결과를 확인하고 가져옵니다.
Claude Code 세션은 submit_job, get_status 및 get_result(또한 list_tiers, list_jobs 및 cancel_job)의 세 가지 도구가 있는 MCP 서버로 접근합니다. 일반적인 호출 예시는 다음과 같습니다:
submit_job(
tier="local",
prompt="Summarize the release notes below in five bullets for a homelab operator. <notes...>",
params={"max_words": 150}
)
프롬프트는 자체적으로 완전해야 합니다; 워커는 다른 것을 보지 못합니다. params의 힌트는 워커 계약의 줄이 됩니다. 키는 베어어 키이며, 소유자별로 범위가 지정됩니다: 호출자는 자신의 작업만 볼 수 있으며, 결과는 30일 동안 보관됩니다. 한 계층당 한 번에 하나의 작업이 실행되며, 시간 초과 내에 전달이 없는 작업은 실패하고 작업이 취소됩니다.
세 계층
- local은
jobs-local그룹에서 실행됩니다: qwen3-14b는 프록시를 통해 실행되며, Anthropic는 컨테이너에서 차단됩니다. 무료 및 개인적입니다. 간단한 작업은 16초 만에 돌아왔습니다. - claude는
jobs-claude에서 실행됩니다: Sonnet, 체크된 사용량. 간단한 작업은 50초 소요되었습니다. NanoClaw는 그룹별 사용량 제한이 없기 때문에 서비스는 하나를 강제로 적용합니다: 키별 일일 사용량 한도, 그리고 모든 키가 공유하는 계층 전체의 일일 작업 및 출력 토큰 한도. 키가 몇 개든, 체크된 계층은 하나의 한도만 가지고 있습니다. - render, 2026-09-26에 추가되었습니다,
jobs-render에서 실행됩니다. 프롬프트는 JSON 레시피(모델 URL, 뷰, 샘플, 크기)입니다. 그룹에는blender-render기술과 읽기 전용 Blender 설치가 있으며, Cycles를 사용하여 헤드리스로 정지 이미지를 렌더링합니다. 계층은 아티팩트를 요청할 수 있으며, 이 계층은 그렇게 합니다: 각 작업은 컨테이너에 마운트된 고유한 폴더를 가지며,get_result는 파일을 나열하고, 서비스는 해당 작업의 소유자에게 이를 제공합니다. 레시피에"photo": true이 포함되어 있다면, 워커는 각 뷰에 대해request_photo을 호출하고photo_status을 확인합니다. 이는 devastator의 GPU 관리자에서 한 번의 요청을 줄이며, 카드가 유휴 상태일 때 vLLM의 B70을 FLUX Kontext 이미지 모델에 빌려주고, 각 렌더링 옆에 사진을 작성합니다.
워커 도구(deliver_result, request_photo, photo_status)는 워커에게만 보입니다.
작동하지 않은 사항
- qwen3는 프록시를 통해 툴 호출을 실행할 수 없습니다. 일반 텍스트 작업은 로컬 계층에서 실행됩니다. 프록시 뒤에서 qwen3-14b는 툴 호출 단계에서 “Content block is not a text block” 오류가 발생하며, 렌더 작업은 툴 호출뿐입니다. 렌더 계층은 Sonnet에서 실행되며, 자체의 작은 일일 제한 내에서 작동합니다. 렌더 작업은 그 호출 중 몇 개만 필요합니다.
- 에이전트 컨테이너에 GPU가 없습니다. NanoClaw에는 장치 전달이 없기 때문에 Blender는 CPU에서 렌더링되며, 128 샘플 기준으로 한 뷰당 약 35초가 소요됩니다. 사진 단계에서만 GPU가 사용되는 이유는 중재자(arbiter)가 호스트에서 NanoClaw 외부에 실행되고, 워커가 MCP를 통해 사진을 요청하기 때문입니다.
이어나가는 요소
두 번째 버전은 다른 시스템이 이미 소유하고 있던 것을 더 이상 소유하지 않기 때문에 더 작아졌습니다. 작업 서비스는 키, 작업, 제한 및 아티팩트를 유지하고, NanoClaw는 모델, 컨테이너, 기술 및 자격 증명을 유지합니다. 새로운 계층은 각각의 지시 및 기술을 가진 새로운 에이전트 그룹과, 계층 테이블에 하나의 항목을 추가하는 것입니다. 이로 인해 한 주 후 Blender 렌더러가 호출자들이 제출하는 방식에 어떤 변경도 없이 도착할 수 있었습니다. 그리고 모든 계층에 대한 배송 계약은 동일합니다: 워커는 하나의 툴을 정확히 한 번 호출하고 멈춥니다.
在代理舰队上运行 LLM 任务
家庭实验室的无需值守的LLM任务服务,最初构建为LiteLLM网关前的队列,随后重建为每个任务都是一次性任务,供NanoClaw代理组处理,具有本地、Claude和Blender渲染层级。
某些 LLM 的工作不应该在会话中进行:一百个摘要,一批分类,一个长期低风险的草稿,任何应保留在局域网中的私密内容。家庭实验室为此提供了一个小型服务。调用者提交一个提示和一个层级,会立即得到一个任务 ID,之后可以收集结果。这个服务现在已经构建了两次。
第一版:一个队列和一个网关
2026 年 6 月的版本在 nemesis 上运行,作为两个堆栈。 llm-queue 是一个 FastAPI 应用,包含一个 Web 仪表板、一个 REST API 和一个 MCP 端点,所有这些都使用一个 WAL 模式的 SQLite 数据库。调用者选择一个能力别名(在 B70 的 vLLM 上是 fast,在 CPU Ollama 上是 background 和 reasoning),每个别名都有自己的工作器,这样慢速的 CPU 任务不会阻塞 GPU 任务。
第二阶段在模型前面放置了一个 LiteLLM 网关。网关为每个用户颁发一个虚拟密钥,该密钥是所有地方的登录凭证:Web 界面、REST 和 MCP。队列将任务限定在密钥的哈希值上,并以该用户身份通过网关运行每个任务,因此配额只在一个地方管理。这意味着工作器在请求返回后需要调用者的密钥,因此每个任务行都携带了 Fernet 加密的密钥,工作器解密它,运行任务并在任务完成或取消后擦除它。LiteLLM 不会在 SQLite 中保存密钥,因此网关还需要一个 Postgres 侧车。
它有效,而且有很多复杂的机制。
它迁移到的舰队
九月,家庭实验室在 devastator 上部署了 NanoClaw 队列。NanoClaw 在容器中运行 Claude Code 代理,组织为代理组:每个组都有自己的指令、技能、挂载和环境,并且会为来自某个频道的消息或计划任务和一次性任务而唤醒。一些组直接与 Anthropic 通信。其他组则运行在 qwen3-14b 上,由本地 B70 上的 vLLM 提供服务,并通过一个小型的标准化代理进行访问,该代理使本地模型的 API 足够接近 Anthropic 的 API,以满足 Claude Code 的需求(它会折叠系统消息、限制输出令牌并关闭 qwen3 的思考模式)。
该队列已经完成了路由、访问控制和审计,这些是队列自己构建的功能。而且在 30 天内,没有任何东西使用过这个队列。
第二版:将任务作为代理任务
在 2026-09-19,队列和网关被从仓库中移除并停用。替代方案 llm-jobs 是一个基于 Python 标准库的服务,作为 systemd 用户单元在 devastator 上运行。它拥有两个东西:密钥和任务,都存储在一个 SQLite 文件中。其余一切均由 NanoClaw 处理。
任务的执行流程如下:
- 调用者提交一个提示和一个层级。
- 服务在该层级的代理组上创建一个一次性 NanoClaw 任务。
- 该组的容器会唤醒,并在前面附加一个简短的工作者合同:只执行该任务,仅交付一次,不发布频道消息,不提问。
- 代理通过 MCP 工具
deliver_result向服务提交任务 ID 和其答案,或者如果任务无法执行,则提交一个错误信息。 - 调用者轮询并获取结果。
Claude Code 会通过三个重要的 MCP 工具将其会话连接到该服务:submit_job、get_status 和 get_result(还有 list_tiers、list_jobs 和 cancel_job)。一个典型的调用看起来像这样:
submit_job(
tier="local",
prompt="Summarize the release notes below in five bullets for a homelab operator. <notes...>",
params={"max_words": 150}
)
提示必须自包含;工作者看不到其他内容。params 的提示会变成工作者合同中的行。密钥是持有者密钥,按所有者范围限定:调用者只能看到自己的任务,结果保留 30 天。每个层级一次运行一个任务,如果任务在超时时间内没有交付,则任务失败并取消。
三个层级
- local 在
jobs-local组上运行:通过代理运行 qwen3-14b,Anthropic 被阻止在容器中。免费且私有。一个简单的任务在 16 秒内返回。 - claude 在
jobs-claude上运行:Sonnet,计费。一个简单的任务用了 50 秒。NanoClaw 没有每组使用上限,因此服务强制实施一个:每个密钥的每日上限,加上每个层级的每日任务和输出令牌上限,所有密钥共享。无论有多少密钥被使用,计费层级只有一个上限。 - render,于 2026-09-26 添加,在
jobs-render上运行。提示是一个 JSON 配方(模型 URL、视图、样本、大小),该组具有blender-render技能和一个只读的 Blender 安装,并使用 Cycles 进行无头渲染静态图像。一个层级可以请求工件,这个层级会这样做:每个任务都会在容器中挂载自己的文件夹,get_result列出文件,服务将它们提供给任务的所有者。如果配方中包含"photo": true,工作者还会为每个视图调用request_photo并轮询photo_status。这会在 devastator 上通过 GPU 调度器排队,当显卡空闲时,将从 vLLM 借出 B70 用于 FLUX Kontext 图像模型,并在每个渲染旁边生成一张照片。
工作者工具(deliver_result、request_photo、photo_status)仅对工作者可见。
未成功的内容
- qwen3 无法通过代理驱动工具调用。 普通文本任务在本地层级上运行。在代理后面,qwen3-14b 在工具调用回合中会失败,提示“内容块不是文本块”,而一个渲染任务仅仅是一些工具调用。渲染层级在 Sonnet 上运行,其有自己的小规模每日配额;一个渲染任务只需要其中几次调用。
- 代理容器中没有 GPU。 NanoClaw 没有设备直通功能,因此 Blender 在 CPU 上进行渲染,每个视图在 128 次采样下大约需要 35 秒。照片步骤只能获得 GPU,因为仲裁器在主机上运行,位于 NanoClaw 之外,而工作者通过 MCP 向其请求照片。
继续推进的内容
第二个版本更小,因为它不再拥有其他系统已经拥有的东西。作业服务保留密钥、作业、配额和工件;NanoClaw 保留模型、容器、技能和凭证。一个新的层级就是一个新的代理组,拥有自己的指令和技能,再加上层级表中的一条记录,这就是为什么一周后 Blender 渲染器可以到达,而调用者提交方式没有任何变化的原因。而且,每个层级的交付合同都是一样的:工作者调用一个工具,且仅调用一次,然后停止。
Trabajo con LLM en una flota de agentes
El servicio de trabajos de LLM de "fire-and-forget" del homelab, primero construido como una cola frente a un gateway LiteLLM, luego reconstruido para que cada trabajo sea una tarea de un solo disparo para un grupo de agentes NanoClaw, con capas de renderizado local, Claude y Blender.
Algunos trabajos con LLM no deben realizarse en la sesión que los considera: cien resúmenes, un lote de clasificaciones, un borrador largo de baja importancia, cualquier cosa privada que deba permanecer en la LAN. El homelab tiene un pequeño servicio para eso. Un solicitante envía un prompt y un nivel, recibe un ID de trabajo de inmediato y recoge el resultado más tarde. Ahora ha sido construido dos veces.
Versión uno: una cola y un gateway
La versión de junio de 2026 se ejecutó en nemesis como dos stacks. llm-queue fue una aplicación FastAPI con un panel web, una API REST y un endpoint MCP, todo sobre una sola base de datos SQLite en modo WAL. Los solicitantes eligieron un alias de capacidad (fast en el vLLM de la B70, background y reasoning en Ollama en CPU), y cada alias tenía su propio trabajador, por lo que un trabajo lento en CPU nunca bloqueaba un trabajo en GPU.
La segunda fase colocó un gateway LiteLLM delante de los modelos. El gateway emitía a cada usuario una clave virtual, y esa clave era el login en todas partes: interfaz web, REST y MCP. La cola limitaba los trabajos a un hash de la clave, y ejecutaba cada trabajo a través del gateway como ese usuario, por lo que las cuotas estaban en un solo lugar. Eso significó que el trabajador necesitaba la clave del solicitante después de que la solicitud hubiera regresado, por lo que cada fila de trabajo llevaba la clave cifrada con Fernet, y el trabajador la descifraba, ejecutaba el trabajo y lo limpiaba una vez que el trabajo terminaba o se cancelaba. LiteLLM no guardaría sus claves en SQLite, por lo que el gateway también necesitaba un sidecar Postgres.
Funcionó, y fue mucha maquinaria.
La flota a la que se trasladó
En septiembre, el homelab recibió una flota de NanoClaw en devastator. NanoClaw ejecuta agentes de Claude Code en contenedores, organizados como grupos de agentes: cada grupo tiene sus propias instrucciones, habilidades, montajes y entorno, y se despierta para mensajes de un canal o para tareas programadas y de una sola vez. Algunos grupos se comunican directamente con Anthropic. Otros se ejecutan en qwen3-14b, servido por vLLM en el local B70 y alcanzado a través de un pequeño proxy normalizador que hace que la API del modelo local se comporte lo suficiente como la de Anthropic para el soporte de Claude Code (se pliegan los mensajes del sistema, se limitan los tokens de salida y se desactiva el modo de pensamiento de qwen3).
Esa flota ya había realizado la ruta, el control de acceso y la auditoría que la cola había construido para sí misma. Y nada había utilizado la cola en 30 días.
Versión dos: trabajos como tareas de agentes
El 2026-09-19 la cola y la puerta de enlace se descompusieron y se eliminaron del repositorio. El reemplazo, llm-jobs, es un servicio de la biblioteca estándar de Python que se ejecuta como una unidad de usuario systemd en devastator. Tiene propiedad de dos cosas, claves y trabajos, ambas en un solo archivo SQLite. Todo lo demás es NanoClaw.
Un trabajo funciona así:
- Un llamante envía un prompt y un nivel.
- El servicio crea una tarea de NanoClaw de una sola vez en el grupo de agentes del nivel.
- El contenedor del grupo se despierta con un contrato de trabajador corto precedido: haz exactamente el trabajo, entrega una vez, no publiques en canales, no hagas preguntas.
- El agente llama a la herramienta MCP del servicio
deliver_resultcon el identificador del trabajo y su respuesta, o con una oración de error si el trabajo no puede realizarse. - El llamante consulta y recibe.
Las sesiones de Claude Code lo alcanzan como un servidor MCP con tres herramientas importantes: submit_job, get_status y get_result (más list_tiers, list_jobs y cancel_job). Una llamada típica se ve así:
submit_job(
tier="local",
prompt="Summarize the release notes below in five bullets for a homelab operator. <notes...>",
params={"max_words": 150}
)
El prompt debe ser autónomo; el trabajador no ve nada más. Las params sugerencias se convierten en líneas en el contrato del trabajador. Las claves son claves de portador, con alcance por propietario: un llamante solo ve sus propios trabajos, y los resultados se mantienen 30 días. Se ejecuta un trabajo por nivel a la vez, y un trabajo sin entrega dentro de su tiempo de espera se considera fallido y su tarea cancelada.
Tres niveles
- local se ejecuta en el grupo
jobs-local: qwen3-14b a través del proxy, con Anthropic bloqueado del contenedor. Gratis y privado. Un trabajo trivial regresó en 16 segundos. - claude se ejecuta en
jobs-claude: Sonnet, con medición. Un trabajo trivial tomó 50 segundos. NanoClaw no tiene límite de uso por grupo, por lo tanto el servicio impone uno: límites diarios por clave, más un techo diario para trabajos y tokens de salida que comparten todas las claves. Sin importar cuántas claves estén activas, el nivel medido tiene un solo techo. - render, añadido el 2026-09-26, se ejecuta en
jobs-render. El prompt es una receta JSON (una URL del modelo, vistas, muestras, tamaño), el grupo tiene una habilidadblender-rendery una instalación de Blender de solo lectura, y renderiza imágenes estáticas sin interfaz gráfica con Cycles. Un nivel puede solicitar artefactos, y este lo hace: cada trabajo obtiene su propia carpeta montada en el contenedor,get_resultlista los archivos, y el servicio los entrega al propietario del trabajo. Con"photo": trueen la receta, el trabajador también llama arequest_photopara cada vista y consulta aphoto_status. Eso encola un paso a través del arbitro de GPU en devastator, que presta el B70 de vLLM a un modelo de imagen de FLUX Kontext cuando la tarjeta esté ociosa, y escribe una foto junto a cada renderización.
Las herramientas del trabajador (deliver_result, request_photo, photo_status) son visibles solo para los trabajadores.
Lo que no funcionó
- qwen3 no puede realizar llamadas a herramientas a través del proxy. Los trabajos de texto plano funcionan en el nivel local. Detrás del proxy, qwen3-14b falla en los pasos de llamada a herramientas con el mensaje “Content block is not a text block”, y un trabajo de renderizado no es más que llamadas a herramientas. El nivel de renderizado se ejecuta en Sonnet, bajo su propio pequeño límite diario; un trabajo de renderizado solo necesita unas pocas de esas llamadas.
- No hay GPU en los contenedores del agente. NanoClaw no tiene passthrough de dispositivos, por lo que Blender renderiza en la CPU, aproximadamente 35 segundos por vista a 128 muestras. El paso de foto solo obtiene la GPU porque el árbitro se ejecuta en el host, fuera de NanoClaw, y el trabajador le pide las fotos a través de MCP.
¿Qué se lleva adelante?
La segunda versión es más pequeña porque dejó de ser dueña de cosas que ya eran propiedad de otro sistema. El servicio de trabajos mantiene las claves, los trabajos, los límites y los artefactos; NanoClaw mantiene los modelos, los contenedores, las habilidades y las credenciales. Un nuevo nivel es un nuevo grupo de agentes con sus propias instrucciones y habilidades, más una entrada en la tabla de niveles, lo que es cómo un renderizador de Blender llegó una semana más tarde sin ningún cambio en cómo los llamadores envían las solicitudes. Y el contrato de entrega es el mismo para cada nivel: el trabajador llama a una herramienta, exactamente una vez, y se detiene.
LLM-Aufgaben auf einer Agentenflotte
Der fire-and-forget-Arbeitsauftragsdienst für LLM im Homelab, zunächst als Warteschlange vor dem LiteLLM-Gateway erstellt, dann neu aufgebaut, sodass jeder Auftrag eine Einmal-Aufgabe für eine NanoClaw-Agentengruppe ist, mit lokalen, Claude- und Blender-Renderebenen.
Einige LLM-Arbeiten passen nicht in die Sitzung, die sie sich vorstellen: Hunderte von Zusammenfassungen, eine Charge an Klassifizierungen, ein langer Entwurf mit geringem Risiko, alles Private, das auf dem LAN bleiben sollte. Der Homelab hat einen kleinen Dienst dafür. Ein Aufrufer sendet einen Prompt und eine Ebene, erhält sofort eine Job-ID und sammelt das Ergebnis später ein. Dieser Dienst wurde bereits zweimal gebaut.
Version 1: eine Warteschlange und ein Gateway
Die Version im Juni 2026 lief auf nemesis als zwei Stacks. llm-queue war eine FastAPI-App mit einem Web-Dashboard, einer REST-API und einem MCP-Endpunkt, alles über eine SQLite-Datenbank im WAL-Modus. Aufrufer wählten einen Fähigkeitsalias (fast auf dem B70’s vLLM, background und reasoning auf CPU Ollama), und jeder Alias hatte seinen eigenen Worker, sodass ein langsamer CPU-Auftrag niemals einen GPU-Auftrag blockierte.
Die zweite Phase platzierte ein LiteLLM-Gateway vor den Modellen. Das Gateway erteilte jedem Benutzer einen virtuellen Schlüssel, und dieser Schlüssel war das Login überall: Web-Oberfläche, REST und MCP. Die Warteschlange begrenzte Jobs auf eine Hash des Schlüssels und ließ jeden Job über das Gateway als dieser Benutzer laufen, sodass die Quoten an einem Ort lagen. Das bedeutete, dass der Worker den Schlüssel des Aufrufers nach dem Rückgabevereinbarung benötigte, also trug jede Job-Zeile den Schlüssel Fernet-verschlüsselt, und der Worker entschlüsselte ihn, führte den Job aus und entfernte ihn nach Abschluss oder Abbruch des Jobs. LiteLLM würde seine Schlüssel nicht in SQLite speichern, also benötigte das Gateway auch einen Postgres-Sidecar.
Es funktionierte, und es war eine Menge Maschinerie.
Die Flotte, auf die es migrierte
Im September bekam das homelab eine NanoClaw-Flotte auf devastator. NanoClaw läuft Claude Code-Agenten in Containern, organisiert als Agentengruppen: jede Gruppe hat ihre eigenen Anweisungen, ihre eigenen Fähigkeiten, ihre eigenen Mounts und Umgebungen und wird für Nachrichten aus einem Kanal oder für geplante und einmalige Aufgaben geweckt. Einige Gruppen kommunizieren direkt mit Anthropic. Andere laufen auf qwen3-14b, die von vLLM auf dem lokalen B70 bereitgestellt und über einen kleinen Normalisierungsproxy erreicht werden, der die API des lokalen Modells genug wie Anthropics verhält, damit der Claude Code-Harness funktioniert (es faltet Systemnachrichten, begrenzt Ausgabetoken und schaltet den Denkmodus von qwen3 aus).
Diese Flotte hatte bereits die Routing, Zugriffssteuerung und Audit durchgeführt, die die Warteschlange für sich selbst gebaut hatte. Und nichts hatte die Warteschlange in 30 Tagen genutzt.
Version zwei: Jobs als Agentenaufgaben
Am 2026-09-19 wurden die Warteschlange und der Gateway abgebaut und aus dem Repository entfernt. Der Ersatz, llm-jobs, ist ein Python-Standardbibliotheksdienst, der als systemd-Benutzerunit auf devastator läuft. Er besitzt zwei Dinge, Schlüssel und Jobs, beide in einer SQLite-Datei. Alles andere ist NanoClaw.
Ein Job verläuft wie folgt:
- Ein Aufrufer sendet einen Prompt und eine Ebene.
- Der Dienst erstellt eine einmalige NanoClaw-Aufgabe in der Agentengruppe dieser Ebene.
- Der Container der Gruppe wird mit einem kurzen Worker-Vertrag vorgestellt: führe genau den Job aus, liefere einmalig, keine Kanalbeiträge, keine Fragen.
- Der Agent ruft das MCP-Tool des Dienstes
deliver_resultmit der Job-ID und seiner Antwort auf, oder mit einem Fehlermeldung, wenn der Job nicht durchgeführt werden kann. - Der Aufrufer fragt ab und ruft die Daten ab.
Claude Code-Sitzungen erreichen es als MCP-Server mit drei Tools, die wichtig sind: submit_job, get_status und get_result (plus list_tiers, list_jobs und cancel_job). Ein typischer Aufruf sieht wie folgt aus:
submit_job(
tier="local",
prompt="Summarize the release notes below in five bullets for a homelab operator. <notes...>",
params={"max_words": 150}
)
Der Prompt muss selbstständig sein; der Worker sieht nichts anderes. Die params Hinweise werden zu Zeilen im Worker-Vertrag. Schlüssel sind Bearer-Schlüssel, die pro Besitzer begrenzt sind: ein Aufrufer sieht nur seine eigenen Aufgaben, und die Ergebnisse werden 30 Tage lang gespeichert. Eine Aufgabe läuft pro Tier gleichzeitig ab, und eine Aufgabe, die innerhalb ihres Timeout keine Lieferung hat, wird als fehlgeschlagen markiert und ihre Aufgabe wird abgebrochen.
Drei Tiers
- local läuft auf der
jobs-localGruppe: qwen3-14b über den Proxy, wobei Anthropic im Container blockiert ist. Kostenlos und privat. Eine triviale Aufgabe kam nach 16 Sekunden zurück. - claude läuft auf
jobs-claude: Sonnet, abgerechnet. Eine triviale Aufgabe benötigte 50 Sekunden. NanoClaw hat keine pro-Gruppe Nutzungsgrenze, daher erzwingt der Dienst eine: tägliche Schlüsselobergrenzen pro Schlüssel, plus eine tägliche Obergrenze für Aufgaben und Ausgabetoken, die jeder Schlüssel teilt. Unabhängig davon, wie viele Schlüssel vorhanden sind, hat das abgerechnete Tier eine Obergrenze. - render, hinzugefügt am 2026-09-26, läuft auf
jobs-render. Der Prompt ist ein JSON-Rezept (ein Modell-URL, Ansichten, Proben, Größe), die Gruppe hat eineblender-renderFähigkeit und eine nur-lese Blender-Installation, und es rendert Stillleben headless mit Cycles. Ein Tier kann nach Artefakten fragen, und dieses tut es: jede Aufgabe erhält ihr eigenes Verzeichnis, das in den Container gemountet wird,get_resultlistet die Dateien, und der Dienst liefert sie dem Besitzer der Aufgabe. Mit"photo": trueim Rezept ruft der Worker auchrequest_photofür jede Ansicht ab und fragtphoto_status. Das schaltet einen Durchlauf über den GPU-Verwalter auf devastator, der die B70 von vLLM an ein FLUX Kontext-Bildmodell verleiht, wenn die Karte leer ist, und ein Foto neben jedem Render schreibt.
Die Worker-Tools (deliver_result, request_photo, photo_status) sind nur für Worker sichtbar.
Was nicht funktioniert hat
- qwen3 kann keine Toolaufrufe über den Proxy durchführen. Plain-Text-Aufgaben funktionieren auf der lokalen Ebene. Hinter dem Proxy schlägt qwen3-14b bei Tool-Aufrufen mit der Fehlermeldung “Content block is not a text block” fehl, und eine Renderaufgabe besteht aus nichts anderem als Toolaufrufen. Die Renderebene läuft stattdessen auf Sonnet, unter einem eigenen kleinen täglichen Limit; eine Renderaufgabe benötigt nur wenige dieser Aufrufe.
- Keine GPU in Agent-Containern. NanoClaw hat keine Geräte-Übergabe, daher rendert Blender auf der CPU, etwa 35 Sekunden pro Ansicht bei 128 Proben. Der Schritt für Fotos erhält die GPU, weil der Arbiter auf dem Host läuft, außerhalb von NanoClaw, und der Worker ihn über MCP um Fotos bittet.
Was weitergegeben wird
Die zweite Version ist kleiner, weil sie Dinge nicht mehr besitzt, die bereits von einem anderen System besessen werden. Der Job-Service behält Schlüssel, Aufgaben, Limits und Artefakte; NanoClaw behält Modelle, Container, Fähigkeiten und Anmeldeinformationen. Eine neue Ebene ist eine neue Agentengruppe mit eigenen Anweisungen und Fähigkeiten, plus einem Eintrag in der Ebentabelle, und so kam ein Blender-Rendern eine Woche später ohne Änderung an der Art, wie Aufrufer ihre Anfragen stellen. Und das Liefervertrag ist für jede Ebene gleich: der Worker ruft ein Tool genau einmal auf und hört dann auf.