FinLaw-UK
Graph-augmented RAG for UK financial regulation
- Timeline
- Sep 2024 - Sep 2025
- Role
- Solo MSc dissertation, University of Bradford
- Status
- Research
- Primary stack
- Mistral 7B · Neo4j · Sentence Transformers · RAGAS
Headline metrics
- What it tackles
- Compliance teams thread citations by hand across the FCA Handbook, the PRA Rulebook and MiFID II, and a plain LLM answers confidently without showing which rule it relied on.
- What it delivers
- A retrieval pipeline that resolves every citation against a Neo4j graph and refuses rather than answer when verification fails, released with a measurement-integrity report on which figures reproduce and which do not.
The problem in full
UK financial regulation is a moving target. The FCA Handbook alone runs to thousands of pages, cross-referenced with MiFID II, the PRA Rulebook, and binding technical standards. Compliance teams burn hours threading citations across documents, and naive LLM lookups hallucinate confidently in exactly the places that matter most.
Off-the-shelf RAG fails here for two reasons. Dense retrieval surfaces semantically similar passages but misses the regulatory entity graph - a single rule is meaningful only in the context of its parent chapter, the obligated entities, and the cross-references it triggers. And without faithfulness evaluation, you can't tell a polished answer from a hallucinated one.
FinLaw-UK was my MSc dissertation: a graph-augmented RAG pipeline that retrieves both semantically and structurally, generates with a small open-weight model, and audits every answer against retrieved context using RAGAS.
System design
The pipeline begins with bulk ingestion of FCA Handbook chapters and adjacent regulatory documents. Each section is chunked at the smallest semantic unit - typically a single rule or sub-rule - then run through a parallel two-stream extraction: Sentence Transformer embeddings into a vector index, and entity/relationship extraction into a Neo4j knowledge graph that captures Rule → Chapter, Rule → Entity, and Rule → Cross-reference relationships.
At query time retrieval is hybrid: BM25 lexical search and BGE-small dense search run in parallel and are fused by reciprocal rank fusion, so an exact rule reference and a paraphrased description both land. The fused candidates' graph nodes are expanded two hops in Neo4j to add adjacent rules, parent chapter context, and cross-referenced sections - the structural context pure vector search loses. A cross-encoder re-ranks the expanded set before it is trimmed to fit Mistral 7B-Instruct's context window.
Generation runs locally on Mistral 7B-Instruct with strict citation-required prompting: every claim must reference a chunk ID, and every chunk ID must exist in the retrieved set. Post-generation, every response goes through a RAGAS evaluator that scores faithfulness (does the answer stay grounded in retrieved context?) and answer relevance (does it actually address the query?).
Evaluation runs against a 110-item benchmark I built and released: 80 factual questions, 20 document tasks, 10 case scenarios. The pipeline reaches 0.76 RAGAS faithfulness and 0.74 answer relevance, with a custom legal-completeness metric at 0.68.
Explore the benchmark
These are 10 real items from the 110-item benchmark the system was scored against - the question, the gold answer, and the citations a correct answer must carry. The Neo4j graph validates those citations at generation time: an answer citing a rule that is absent from the graph is flagged as a potential hallucination.
Data verbatim from the public eval set (curated basic tier) - the aggregate scores above come from the full 110 items. View the eval set on GitHub
Key technical decisions
Graph expansion, then re-rank
Re-ranking alone can't fix the real failure mode: missing context. Graph expansion first captures the 'Rule X is meaningless without Rule Y next to it' pattern no reranker can recover, and cross-encoder re-ranking then orders the expanded set before it hits the context window.
Mistral 7B over a frontier model
A 70B+ model would lift answer quality, but a frontier API on financial text creates a vendor dependency and a data egress problem the project couldn't accept. 7B-Instruct ran locally on a single GPU and proved that the structural retrieval improvements transferred regardless of generator size.
Neo4j over a vector-only store
Pinecone or Qdrant alone would have been faster to ship, but the regulatory cross-reference graph is the actual moat. Storing it in a graph DB lets retrieval expand structurally - a query for one rule pulls in the chapter, the entities, and the cross-references in a single Cypher hop.
RAGAS over BLEU/ROUGE
Surface metrics reward fluent paraphrasing. Faithfulness and answer relevance both require an LLM judge against the retrieved context, which is what actually matters for regulatory text where one wrong citation can be liability.
Refuse rather than answer
Every citation is resolved against the graph before the answer ships, and an answer that cannot be verified is refused instead of returned. It scores worse: RAGAS marks a refusal zero, so 30 refusals pull the headline relevancy down 23 points. On regulatory text a confident wrong citation is the expensive failure, not a blank.
The results in full
The result I would lead with is a correction to my own submitted work. Re-measuring the evaluation after submission showed that the reported source-accuracy and citation-quality figures were regex shape-checks, not correctness measures: the scorer awarded a flat 0.85 to any citation-shaped string, and 103 of the 110 rows carried that constant. The true graph-verified citation rate was 3 in 110. Both figures are withdrawn, and the repository ships a measurement-integrity report stating which numbers reproduce and which do not.
The figures that do reproduce: 0.76 RAGAS faithfulness, 0.74 answer relevance, and a custom legal-completeness metric that holds at 0.68 across both evaluation tracks. That correction is what motivated the citation normaliser, the cross-encoder re-ranker and the refusal gate.
Refusing costs something measurable, and it is worth paying. Headline answer relevancy drops 23 points because RAGAS scores a refusal as zero by construction. Exclude the 30 refusals and the mean is 0.658, against 0.641 for the non-refusing baseline - which scored higher only by confabulating answers to questions the corpus cannot support.
What I'd do next
The measurement bug is the part I would want a supervisor to press me on. A scorer returning the same constant for 103 of 110 rows is visible in the distribution and invisible in the mean, and I only caught it because I went back to reproduce my own numbers after the dissertation was already submitted. Publishing the correction next to the code was the only defensible option, and it changed what the system does: the refusal gate exists because the citation numbers did not survive scrutiny.
If I were continuing this past the dissertation, the next move is two-pronged. First, extend the evaluation harness into a structured legal-reasoning benchmark - RAGAS catches faithfulness drift but not legal-specific failure modes like jurisdictional misapplication. Second, ship a confidence-aware UI that surfaces uncertainty when the graph expansion returns sparse adjacency, so users know when the system is reasoning from rich vs thin context.
Continue reading
Autonomous Voice Agent
Cutting voice-agent latency 54%, from 2.4s to 1.1s
DiabetesSense
93% accurate diabetes risk screening from an 11-model benchmark