Pharos
Turn PDFs, scans, docx, pptx and xlsx into a local knowledge base you can ask questions of — with enterprise access control and citations you can trace back to the page. Each RAG component was sharpened on its own, then folded into one repository: multi-identity auth, observability and systemd supervision come with it.
A note on language. The code, CLI and commit history are in English; the documentation — including the 12-part learning series — is written in Chinese. This README is the English entry point and covers what the system is, how it is put together and how to run it. For anything deeper, you will be reading Chinese (or machine-translating it).
The Lighthouse of Alexandria stood beside the Library, guiding ships to shore. Pharos does the same job, except what it lights up is your team's document library.
This is not another chunking toy. Every significant decision here is backed by a measurement, argued through several rounds of adversarial review, and is running today over 77 real documents and 7,652 chunks. Installing it is one line: pip install -e '.[dev]' (src-layout, editable).
Two exits, one set of semantics
One knowledge base, one door for each kind of consumer:
Both doors share one set of semantics, held in place by two rules: the tool contract has exactly one source (toolcore, so the stdio and HTTP sides cannot drift), and identity is decided server-side (an agent cannot edit its own permissions).
Architecture
Why a resident daemon? Embedded Qdrant admits a single client and holds an exclusive lock, and an 8B model takes a minute or two just to load. Under the earlier stdio-direct design, every agent session spawned its own process — fighting over the lock and reloading the model each time. Pharos inverts that: the daemon owns the heavy resources, MCP shrinks to a thin HTTP adapter that starts in milliseconds, and every session shares one already-warm backend. The trade-offs behind this are in docs/DESIGN.md (Chinese).
The single-node problem is that the GPU model, embedded Qdrant and application logic all live in one process, so horizontal scaling hits a ceiling immediately. Split apart, there are three tiers that scale independently:
inference(FastAPI on:8900) — GPU forward passes lifted out on their own, returning full-width vectors that the client then truncates, with both sides kept equivalent;pharos— the application layer sheds torch entirely (setinference_urland it never loads a model), which is what makes--scale pharos=Npossible;- Qdrant in server mode — replicas share one source of truth, with
nginxin front for load balancing;docker killon any replica goes unnoticed.
docker compose --env-file .env.compose up -d --scale pharos=3 # entry point: http://127.0.0.1:8080
One thing worth stating plainly: the throughput ceiling is the forward speed of a single inference card (the GPU serialises), and adding replicas does not raise it. What replicas actually buy you is concurrency in the non-GPU parts, plus crash isolation and rolling upgrades. The full write-up is in docs/SCALE_OUT.md (Chinese).
Getting started
The service runs under systemd — starts at boot, restarts on failure. Day to day, a team member needs their own API key and then:
conda activate pharos
pip install -e '.[dev]' # src-layout, editable install
python -m pharos ask "What do we have on X?" # closed-pipeline QA, answers carry citations
python -m pharos health
For agentic mode in Claude Code: copy .mcp.json.example to .mcp.json (gitignored), fill in your PHAROS_API_KEY and local paths, and you get the rag tool — provided the daemon is running. With no daemon around, pharos mcp --direct also works: it talks stdio, loads the GPU model itself, and depends on nothing resident.
Building the index, as an administrator (the corpus is a directory of MinerU parse output):
sudo systemctl stop pharos # single-client lock, so stop first
python -m pharos index --corpus <parsed_dir> --dest ~/rag_real
sudo systemctl start pharos
Want to understand RAG, not just use it?
The repository also carries a RAG learning and interview-prep set: this system — repeatedly contradicted by its own data and repeatedly fixed — taken apart into 12 pieces and 4,122 lines of tutorial. It runs from chunking, hybrid retrieval, the permission model and generation grounding all the way through agentic use, evaluation methodology and multi-replica scale-out. Every piece carries code anchors, measured numbers, interview framings, and experiments you can run yourself. Written in Chinese.
Security model
- Identity answers "who is asking", ACL answers "what may they see". An X-API-Key resolves to an identity first; that identity's tenant and principals are then handed to the retrieval layer per request, where ACL does hard filtering to enforce visibility. The whole chain is verified by the five-user matrix in
acl_regression. - Fail-closed throughout. An unrecognised or absent key is a 401. A malformed keys file refuses to start. Binding to a non-loopback address forces keys mode — the library does not go out on the LAN unguarded. A document with no permissions attached is visible to nobody by default.
- Sessions are isolated from each other. Cross-call deduplication is bucketed by identity and session, so one person never sees another's state. It also does not leak detail: logs record identity names and never keys, errors do not spill internals, and every retrieved passage is tagged
trust: untrustedagainst prompt injection. - Binds
127.0.0.1only by default. HTTPS and public exposure are explicit non-goals — tunnel in if you need remote access.
python -m pharos keys new alice --tenant demo --admin # issue an identity; the key prints exactly once
python -m pharos keys new bob --tenant demo --principals g_eng
# set PHAROS_KEYS_FILE=~/pharos.keys.json in .env; add PHAROS_HOST=0.0.0.0 for LAN access
sudo systemctl restart pharos
Configuration lives in the single .env at the repository root (see .env.example; everything is prefixed PHAROS_*): PHAROS_KEYS_FILE, PHAROS_CORPUS_DIR, PHAROS_INDEX_DIR (defaults to ~/rag_real), PHAROS_COLLECTION, PHAROS_PORT (8787), PHAROS_HOST (a non-loopback bind forces keys mode), PHAROS_LOG_DIR, and DEEPSEEK_API_KEY (needed by /v1/ask). The full deployment, key and operations procedures are in docs/OPERATIONS.md (Chinese).
Tests
python -m pytest tests -q # full CPU suite: product layer + engine layer, ACL predicates included
python -m pytest tests -q --ignore=tests/engine # product layer only (fake retriever + MockLLM; no GPU, no network)
Neither touches a GPU, a Qdrant server or the network, and CI runs them on every push. The suite size, its product/engine split and the measurement provenance live in docs/TESTING.md §1 — the single authority for that number. The GPU end-to-end zero-leak regression (eval/acl_regression.py, on WSL + a 4090), concurrency benchmarks, backup drills and security review records are in the same document.
Documentation index
All documentation below is in Chinese.
Component-level design and evaluation live in docs/components/ (chunker / embedder / generator / mcp-server); methodology in docs/methodology/.
License
MIT.