01 - System Architecture
7/30/2026, 8:26:18 PM · updated 7/30/2026, 10:41:06 PM
An end-to-end reference of a local Mac-based AI infrastructure—Ollama model serving, dual gateway APIs, three-tier knowledge banks with RAG indexing, a hand-built agent harness pipeline, Tailscale networking, backup strategy, and the architectural principles that tie it all together.
This document provides a complete mapping of the llmpowerhouse system architecture — every component, how they interconnect, and the design philosophies driving structural choices. It covers seven architectural layers from hardware through backup, plus networking, supporting apps, and the rationale behind major decisions.
Overview
The entire system runs on a single Mac Studio with model weights, documents, code, logs, and backups all stored on one external volume (/Volumes/AI_DATA). This portability lets the whole setup be rebuilt from scratch if the Mac itself is ever replaced. The architecture deliberately separates two front doors — one for human chat use, one for programmatic API access — while sharing knowledge banks between them for retrieval-augmented generation (RAG).
Layer 1 — Hardware and the Model Engine
Mac Studio (Apple M3 Ultra, 28 cores, 96 GB unified memory)
└── /Volumes/AI_DATA (1.8 TB dedicated APFS volume)
└── models/ollama/ (102 GB of model weights)
└── Ollama (localhost:11434) — the engine, OpenAI-compatible API
Three chat models and one embedding model are pulled into Ollama. Here is how they break down by size and role:
| Model | Size | Role |
|---|---|---|
qwen3.6:35b-a3b | 22.3 GiB | Default / workhorse. Mixture-of-experts, ~3B active params per token, fastest of the three (~72 tok/s measured). |
qwen3-vl:30b | 18.2 GiB | Vision-language. The only model that reads images. |
gpt-oss:120b | 60.9 GiB | Largest, deepest reasoning. Cannot share VRAM with anything else — see the constraint below. |
nomic-embed-text | 0.3 GiB | Not a chat model — produces the vectors that power retrieval search. |
The VRAM Constraint
The Mac has 77.8 GiB of usable VRAM, configured for at most 2 resident models at once (OLLAMA_MAX_LOADED_MODELS=2). qwen3.6 + qwen3-vl together fit (40.5 GiB) and stay warm side by side. Either one paired with gpt-oss:120b exceeds the budget — selecting the big model evicts everything else, and switching back evicts it again, paying a multi-minute cold-load penalty each way.
In practice this means:
qwen3.6handles nearly all traffic.qwen3-vlonly activates for image input.gpt-oss:120bis reserved for occasional hard problems where the load-time cost is acceptable.
Layer 2 — Two Front Doors, on Purpose
┌─────────────────────────────┐
│ Ollama :11434 (private) │
│ never exposed to internet │
└───────────▲─────────────────┘
│
┌──────────────────┴───────────────────┐
│ │
┌──────────┴──────────┐ ┌───────────┴────────────┐
│ Open WebUI :8080 │ │ agent-server :8788 │
│ (human chat UI) │ │ (API for apps/tools) │
│ + built-in RAG │ │ + ChromaDB RAG │
│ no memory, no tools│ │ + per-user memory │
└─────────────────────┘ │ + MCP tool calling │
└───────────┬────────────┘
│
┌────────────┴─────────────┐
│ gateway.py :8787 (legacy)│
│ dumb proxy, ~20 old apps │
└───────────────────────────┘
This is a deliberate design choice, not an unfinished migration. The two front doors serve different audiences with different capabilities.
gateway.py (port 8787) is the original, simple bridge — it forwards a request to Ollama and does nothing else. It still serves roughly 20 existing integrations and is explicitly not being retired.
agent-server (port 8788) is the actively developed system. It adds:
- Knowledge-base search
- Per-user memory
- MCP tool calling
- Real-time streaming
- Per-app API keys with scopes and rate limits
A key issued for one server does not work on the other — they have separate key stores. The :8443 vs :443 suffix (see networking) is the only visible difference from outside.
Open WebUI talks directly to Ollama, not through agent-server — so it has its own (simpler) retrieval but none of agent-server's memory or tool-calling.
Layer 3 — Knowledge, in Three Tiers
knowledge-bank/
├── DOD-FM-Knowledge-Bank/ (financial-management documents, 288 MB)
├── K12-Knowledge-Bank/ (education standards/curricula, 1.8 GB)
├── Wiki/ (curated one-page-per-concept notes, 257 pages)
├── _inbox/ (drop zone for new files)
└── _index/
└── chromadb/ (722 MB — the vector search index)
Tier 1 — Vector RAG
The foundation layer: documents are chunked (800 characters, 100-character overlap), embedded with nomic-embed-text, and stored in ChromaDB. A query is embedded the same way and matched by cosine similarity. This is described in the project's own documentation as "90% librarianship, 10% technology" — folder taxonomy, consistent filenames, and pruning stale duplicates matter more than any algorithm.
Tier 2 — LLM Wiki
apps/llm-wiki/llm_wiki.py watches the knowledge banks and auto-drafts a distilled Markdown page per concept, which a human reviews before it's promoted. Wiki pages are themselves indexed by Tier 1, so a well-curated wiki page tends to out-compete raw source text in retrieval.
Tier 3 — Graph RAG
Designed but not yet deployed — reserved for if Tiers 1–2 demonstrably fail on multi-hop "what depends on what" questions, using LightRAG rather than a hand-rolled graph database.
Source-Authority Re-Ranking
A knowledge-bank/source_authority.json file nudges search results toward higher-trust documents (e.g., the actual regulation rather than a summary of it) without overriding relevance entirely.
Layer 4 — The agent-server Request Pipeline
This is the most architecturally interesting part of the system — a hand-built agent harness, not a framework. Full detail lives in 03_Agent_Harness_Deep_Dive.md; here's the shape:
Caller (app, curl, VS Code)
│ POST /v1/chat/completions + Bearer API key
▼
auth.py — validate key hash, scope, rate limit, quota
▼
agent_loop.py — compose system prompt (AGENT.md + matched skill + memory)
│ check: is this a self-referential question? (skip tools if so)
▼
graph.py — wave-based executor: run planner, fan out any tool calls
│ concurrently, fan back in, repeat until done or budget hit
├──► tools/registry.py (hand-written tools: search_knowledge_base, get_study_plan)
├──► mcp_client.py (external tools: filesystem, web search, PDF reader, planning)
└──► hooks/ (pre-tool: block disallowed writes; post-tool: trigger re-sync)
▼
Ollama :11434 — the actual model call
▼
memory_pipeline.py — record what was asked/found (fire-and-forget, doesn't block response)
▼
Response to caller (streamed or complete)
Every request, tool call, skill match, and feedback signal is logged to logs/agents/*.jsonl — this audit trail is what enabled the July 30 latency bug (see 05_Gap_Analysis.md) to be diagnosed from evidence instead of guesswork.
Layer 5 — Networking and Exposure
Internet
│
▼
Tailscale Funnel (encrypted tunnel, no router port-forwarding)
├── :443 → gateway.py (legacy, ~20 apps)
└── :8443 → agent-server (current)
Ollama itself is never exposed beyond localhost and the private Tailscale network — it has no authentication of its own, so this is treated as an absolute rule, not a preference. The only public doors are the two proxies above, both of which enforce their own API keys before anything reaches Ollama.
Admin access to the Mac itself is tiered: browser-based admin panels first, SSH only when needed, screen sharing (VNC) as a last resort.
Layer 6 — Supporting Apps
| App / Directory | Purpose |
|---|---|
apps/llmpowerhouse-site/ | Public Next.js showcase site (Vercel-hosted), including a live demo bridged to the home server and a /server-status health page. |
apps/llm-wiki/ | The wiki automation watcher. |
apps/open-webui-filters/ | Custom filter (knowledge_scope_filter.py) that prevents cross-bank knowledge leakage in Open WebUI chats. |
apps/mcp-servers/fetch/ | Isolated Python environment for one MCP tool that needed a different dependency version than agent-server's own. |
Layer 7 — Backup
Backup-AI.command → backups/<timestamp>/
(keys.db, memory.db, AGENT.md, skills/, *.command scripts — small, irreplaceable)
Backup-AI.command full → also copies the 722 MB ChromaDB index
Model weights (104 GB) and source PDFs are deliberately not backed up in the same way — they're reproducible (ollama pull, re-download) rather than irreplaceable.
Open risk: Backups currently live on the same volume they protect, which covers accidental deletion but not drive failure. See
05_Gap_Analysis.md.
Why This Shape
Every major structural choice traces back to one of two constraints:
- The 96 GB memory ceiling — which forced the "workhorse default, big model for hard cases" model policy and the two-server-not-one exposure design.
- A philosophy of layering cheap deterministic mechanisms in front of expensive flexible ones:
- Regex before embeddings before a full model call
- Vector RAG before wiki before graph
- Hand-written tools and MCP tools sharing one registry so the routing logic never needs to know which is which
That pattern is worth keeping in mind when extending the system — see 08_Extending_The_Harness_Agent.md.
Learning map
Learning Map: Building from Hardware to Agent Harness
Stage 1 — Foundations (Hardware & Model Serving)
- Understand GPU/VRAM constraints for multi-model concurrency on consumer silicon
- Run Ollama as a local, OpenAI-compatible inference server (port 11434)
- Load chat (
qwen3.6:35b-a3b), vision (qwen3-vl:30b), and large-scale reasoning models (gpt-oss:120b)
Stage 2 — Knowledge Layer (RAG & Indexing)
- Chunk documents (800-char / 100-char overlap) and embed them with an embedding model (
nomic-embed-text) - Store vectors in ChromaDB and query by cosine similarity for retrieval
- Add authority-weighted re-ranking via
source_authority.json - Introduce Tier 2 (auto-drafted wiki pages through human curation) and learn when Tier 3 (Graph RAG / LightRAG) is warranted
Stage 3 — Dual API Gateways
- Deploy Open WebUI (:8080) for human-facing chat with its own lightweight RAG
- Stand up agent-server (:8788) for programmatic API access—memory, MCP tool-calling, per-app keys & rate limits
- Keep the legacy gateway.py proxy on :8443 as a stable surface for existing integrations
Stage 4 — Agent Harness Pipeline
- Wire authentication (
auth.py), system prompt composition (agent_loop.py), and model calls to Ollama - Use the wave-based executor (
graph.py) to fan out tool calls concurrently and converge results - Implement a single tool registry mixing hand-written tools (
search_knowledge_base,get_study_plan) with MCP servers (filesystem, web search, PDF reader) - Add audit logging (
logs/agents/*.jsonl), memory pipeline, and pre/post hooks
Stage 5 — Networking & Operations
- Tunnel public access through Tailscale Funnel without router port-forwarding
- Implement backup strategy (
Backup-AI.command) covering configs vs. reproducible model weights - Set up monitoring via a Next.js dashboard (
/server-status)
Prerequisite: Comfort with Docker / Python virtual environments and basic REST APIs.
Get hands-on — step by step
-
Provision a macOS machine with at least 32 GB RAM (96 GB recommended) and format an external volume as APFS.
-
Install Ollama (
brew install ollamaor download the installer), confirm it's running on localhost:11434, setOLLAMA_MAX_LOADED_MODELS=2. Pull three models:ollama pull qwen3:35bollama pull qwen3-vl:30bollama pull gpt-oss:120bPlus the embedding model:
ollama pull nomic-embed-text. Verify each viaollama listand a quick query likecurl http://localhost:11434/api/generate. -
Create a knowledge-bank directory with a Tier 1 structure:
mkdir -p knowledge-bank/{_inbox,_index/chromadb}Place PDFs and text docs in
_inbox/. Use python-pymupdf or similar library, chunk them at 800 chars / 100 overlap. -
Build the vector index with ChromaDB:
- Install
chromadband usenomic-embed-textvia Ollama's/api/embeddingsendpoint or a compatible HuggingFace pipeline. - Store each chunk's embedding + metadata (source, page, section) in a Chroma collection named
kb_tier1.
- Install
-
Write a simple retrieval function:
import chromadb client = chromadb.Client() coll = client.get_or_create_collection("kb_tier1") query_emb = embed("your question here") # same encoder as step 4 results = coll.query(query_embeddings=query_emb, n_results=6) -
Add authority re-ranking: create
knowledge-bank/source_authority.jsonmapping document filenames to numeric priority (e.g.,{"DOD-Fmr-12-1.pdf": 10}). Post-ChromaDB retrieval multiply each score by(1 + authority_weight)before sorting. -
Deploy Open WebUI:
docker run -d -p 8080:8080 --name webui ghcr.io/open-webui/open-webuiSet the base URL to
http://host.docker.internal:11434(Ollama). Add a filter (apps/open-webui-filters/knowledge_scope_filter.py) to scope queries to specific Chroma collections so knowledge banks never leak. -
Build agent-server (:8788):
- Create
auth.py— validate Bearer API keys (hashed), enforce scopes and per-key quotas. - Create
agent_loop.py— compose the system prompt from core rules (AGENT.md) + matched skills from stored skill markdowns + per-user memory snippets fetched from a local SQLite / Postgres table. - Wire
graph.py(wave-based loop): after the LLM responds, parse tool calls; route them throughtools/registry.pyfor built-in tools andmcp_client.pyfor MCP tool servers (use MCP SDK to discover endpoints). - Log every API call & tool result to
logs/agents/*.jsonl. Use python-fire-and-forgetasyncio.create_task()or a background queue so memory recording doesn't block the response.
- Create
-
Network public exposure:
- Install and configure Tailscale on your Mac. Enable "Funnel" for two listeners: map :443 → gateway.py (legacy HTTP proxy forwarding to port 11434) and :8443 → agent-server.
- Never expose Ollama directly. Confirm with
ss -tlnp | grep 11434that the port is bound only to 127.0.0.1.
-
Validate everything end-to-end:
# Via Open WebUI — ask a question, check source documents are surfaced. # Via agent-server — send a Bearer key with MCP tool scope and verify a filesystem write tool is blocked by hooks/. -
Set up backups:
- Short backup (
Backup-AI.command): rsynckeys.db,memory.db,AGENT.md,skills/to a timestampedbackups/<date>/directory on the same volume. - Long backup (
Backup-AI.command full): additionally rsync the ChromaDB index (~722 MB).
- Short backup (
-
Extend with Tier 3 (optional): when answer quality degrades on multi-hop queries, integrate LightRAG as a post-processing layer over ChromaDB results instead of replacing it.
Top 3 sources
- 1Ollama Documentation
The official reference for installing, pulling models (Qwen, GPT-oss), configuring OLLAMA_MAX_LOADED_MODELS, and leveraging OpenAI-compatible endpoints.
https://ollama.com/blog/openai-compatibility
- 2ChromaDB Docs — Embeddings & Collections
Step-by-step guide to creating collections, embedding chunks with custom encoders (like nomic-embed-text), and querying by cosine similarity.
https://docs.trychroma.com/docs/overview/introduction
- 3Model Context Protocol (MCP) SDK — Quickstart
The canonical MCP specification and SDK docs for building unified tool-calling agents that bridge locally-hosted tools and external MCP servers.
https://modelcontextprotocol.io/introduction
Links are AI-suggested — worth a quick sanity check before diving in.