Case study
macOS Troubleshooting Agent
An agentic RAG system for macOS troubleshooting — a branching LangGraph, a domain knowledge base built from 9 scrapers, and a blind-judge evaluation harness that blocks its own deploys on quality regressions.
- Role
- Solo — design, build, deploy, evaluate
- Timeline
- Stack
- PythonLangGraphFastAPIQdrantLlama 3.3 70BPostgreSQLDockerPrometheusGrafanaOracle Cloud
Problem
macOS troubleshooting knowledge is scattered across forums, Stack Overflow, and man pages. Generic assistants guess at fixes, or lead with "have you tried restarting it" without ever asking what a user has already ruled out.
Context
A solo project — Python, LangGraph, FastAPI, Qdrant, Llama 3.3 70B, PostgreSQL, Docker, and Oracle Cloud, at roughly 17,500 lines of code, and at macos-agent-hari.duckdns.org.
Role
Solo: design, build, deploy, and evaluate.
Exploration
The write path is pulling from Apple Developer Forums, Ask Different, Stack Overflow, Reddit, WWDC, man pages, dev docs, and GitHub — cleaned, tagged, and chunked one problem per document. The open question wasn't how to retrieve; it was when to stop trusting the knowledge base and go outside it.
Decisions
-
intake → {kb_retrieve, web_search} → smart_merge → decide, wheredecidebranches three ways: answer directly, ask a clarifying question, or request a diagnostic from the user. That three-way branch isn't expressible as a straight-line RAG pipeline. - Confidence-gated web fallback. A Tavily search scoped to apple.com fires only when knowledge-base retrieval confidence is low — RAG-first, not search-first.
- Difficulty-tier triage. Every knowledge-base entry carries a difficulty tier, so the agent can infer what a user has likely already tried instead of leading with the easiest step every time.
- Stateful multi-turn memory. macOS version, chip, and already-tried
steps accumulate into a running case file across turns, persisted via
langgraph-checkpoint-postgresso it instead of living in process memory. - Cross-provider LLM failover , so a single provider outage doesn't take the agent down mid-conversation.
Implementation
Production concerns that don't usually show up in a portfolio project:
Google OAuth into session JWTs, ,
per-turn token and cost accounting with weekly and monthly spend caps, rate
limiting via slowapi, Alembic migrations run on boot, Prometheus and
Grafana dashboards with structured logs shipped through Loki, GitHub Actions
CI, and deploy/rollback/recover scripts backed by a smoke test. cover auth, credits, guardrails, and the async Postgres checkpointer.
Challenges
The hardest part wasn't the agent graph — it was trusting the agent's own opinion of itself.
Two judge models score every candidate build blind — without seeing each
other's scores — across correctness, actionability, grounding, and
depth-triage, and the result is checked against a committed baseline.json.
If a change makes the agent measurably worse, .
Results
This hasn't shipped to real users, so — the honest results are the eval's, not a usage dashboard.
| Agent | GPT-4o | |
|---|---|---|
| Multi-turn resolution (n=4) | 2 / 4 | 1 / 4 |
| Avg. turns to resolve (n=4) | 2.25 | 3.25 |
| Single-shot head-to-head (n=6) | 1 win / 5 losses | 5 wins / 1 loss |
Judged blind — judges never saw each other's scores. A second judge model scored the single-shot set 0 wins out of 3 for the agent. Both sample sizes are small: treat this as a signal, not a verdict.
What the eval told me
I built the judge panel expecting it to confirm the agent was good. It didn't.
On single-shot questions — one prompt, one answer, no follow-up — . A 70B model with retrieval does not out-recall a frontier model, and my own eval said so plainly.
On multi-turn diagnosis, which is what the product actually does, the numbers inverted: . Four cases, run interactively — treat that as a signal, not a result.
The split is the finding. The agent's advantage isn't knowing more; it's
asking for the one diagnostic it's missing instead of guessing, and
remembering what you already tried. Building the harness is what lets me
say that with a straight face — and it's why regression_gate.py blocks a
deploy when scores fall below the committed baseline. I'd rather have a red
build than a quieter, worse agent.