Home

Thoughts

July 9, 2026

Building Restaurant Decision Assistant

Building Restaurant Decision Assistant started as a simple question: how do I make restaurant recommendations that are not just fluent, but actually grounded in evidence? A normal LLM can generate a convincing answer, but that answer may not be traceable to the data. I wanted the system to recommend restaurants using structured business information, review-level evidence, and graph relationships instead of relying on generic model knowledge.

One of the first problems I ran into was that small local LLMs are useful, but not automatically reliable. For example, the User Intent Agent could parse a query into JSON, but sometimes placed concepts in the wrong fields. Later, the Review Extraction Agent initially copied example evidence from the prompt instead of quoting the actual review. That was an important lesson: valid JSON is not the same as trustworthy output. I approached this by combining LLM interpretation with deterministic guardrails. Every agent output is validated with schemas, enum-limited fields, retry logic, structured logs, and evidence provenance checks. If an extracted evidence quote does not appear in the original review text, the system drops it.

The system design evolved around a local-first multi-stage GraphRAG pipeline. A user query flows through intent parsing, retrieval planning, Cypher retrieval, vector retrieval, evidence synthesis, and guardrail validation. The graph itself is built in Neo4j with nodes like Business, Review, City, Category, Aspect, Sentiment, and EvidenceChunk. I chose to make EvidenceChunk a first-class node because evidence needs to be traced, ranked, connected to aspects, connected back to reviews, and connected to businesses. That design makes the graph auditable: a recommendation can eventually point to a real path like Business <- EvidenceChunk -> Review -> Aspect.

A key architectural decision was to avoid arbitrary LLM-generated Cypher. The LLM can interpret language, but deterministic tools own database writes and retrieval templates. This keeps the graph safer and easier to debug. For example, structured graph loading uses fixed MERGE templates, uniqueness constraints, and count validation. Retrieval uses allowlisted Cypher templates and typed retrieval plans. This matters because one bad generated query could corrupt the graph or return unsupported evidence.

The project also surfaced an important retrieval tradeoff. Cypher retrieval is precise and grounded in graph structure, but it depends on fixed labels like service, food_quality, or ambience. Vector retrieval is more flexible for fuzzy language like "friendly service" or "good vibes," but vector-only retrieval can ignore graph context. The solution was hybrid retrieval: first constrain results to graph-valid businesses, then rank retained evidence chunks by vector similarity. In one sample query, hybrid retrieval returned three Philadelphia restaurant candidates with evidence ranked by semantic similarity while still respecting the graph context.

I tracked metrics throughout the project because I wanted this to feel like an engineering system, not just a demo. The tiny development graph started with 5 businesses, 15 reviews, 15 categories, and 39 structured relationships. Review extraction ran on 10 reviews and succeeded on 10/10 after guardrail fixes, producing 24 evidence chunks. The evidence graph added 5 aspect nodes, 3 sentiment nodes, 24 evidence chunks, and provenance relationships back to reviews and businesses. Retrieval tests showed Cypher, vector, and hybrid retrieval all working, with the current test suite at 32 passing tests.

Scalability is still intentionally staged. Right now, the project uses a tiny Yelp sample, row-by-row graph writes, and a local JSONL vector index. That is the right choice for learning and debugging, but not the final architecture. To scale it, I would batch Neo4j writes, normalize categories more aggressively, move vector search into a real vector database or Neo4j vector index, add evaluation datasets, and introduce stronger monitoring around extraction quality, retrieval precision, latency, and answer faithfulness.

The biggest thing I learned is that GraphRAG is not just "graph plus LLM." It is a reliability problem. The graph gives structure, the LLM gives interpretation, vector search gives semantic flexibility, and guardrails hold the system accountable. The hard part is not making the model answer; the hard part is making sure every answer can be traced back to evidence the system actually retrieved.