Skip to content

5 · From talk to graph

Everyone's first instinct is "throw it in a vector database," and it is an easy instinct to act on: chunk the transcripts, embed them, done, almost free. But a vector index is a black box. It retrieves passages that sound like your question without showing you why. A knowledge graph is more work because it builds explicit structure out of the transcripts: discrete claims, each with a subject, an object, a speaker and a source. That structure is inspectable and debuggable - you can look at exactly what was extracted and tell the claims that carry real information from the ones that are just the model noting that a conversation happened. The graph itself was the easy part. The real work was in what the extractor adds on top of the facts, trivial statements about the conversation itself, and in stopping those from turning the hosts into the busiest nodes in a graph that is supposed to be about the world.

Why a graph, not a pile of vectors

The distinction is not academic. Vector search retrieves text that resembles your query, but it cannot, on its own, do the two things a user actually asks for:

  • Cite a specific fact. "Robert Habeck is economics minister" as a discrete, addressable claim with a speaker and a timestamp, not "here are five passages that mention Habeck, read them yourself."
  • Aggregate across facts. Count how often a topic comes up, chart it over time, find where two topics intersect. Those are set operations over structured claims, and a bag of embeddings has no claims to operate on.
  • Say when there is nothing to return. A graph query that matches nothing returns nothing. A vector index always hands back its nearest neighbours, relevant or not, and never reports "no match." You can impose a similarity cutoff, but a threshold of 0.2394 is a number without a meaning.

The first two fall out for free once facts are nodes and edges, which is the entire reason the query layer can answer and cite.

One fact, one node

The schema is entities plus relations, but with a twist that does the heavy lifting: each fact is a reified Statement node, not a bare edge. Instead of Habeck -IST-> Minister as an anonymous relationship, the statement itself is a node that carries the predicate and its provenance, wired to its subject, its object, the speaker who uttered it, and the chunk it came from:

THE FACT Entity · subject · Statement predicate Entity · object · THE RECEIPT STATED_BY IN_CHUNK Speaker Chunk Chapter Episode · date ·

Left to right is the fact; the dotted edges are its receipt, who said it and where, chaining up to a date. That is what lets you both cite a fact and count across facts.

Two more decisions make the graph queryable rather than just large. Chunks are 90 seconds, aligned to the podcast's chapters (the structure the feed already handed us), so a statement's provenance is a real location in the audio. And a statement's id is the fact itself - its subject, predicate and object, plus the speaker - scoped to a single episode chapter but deliberately not keyed on the chunk's timestamp. So if the same speaker makes the same claim again a chunk or two later, it lands back on the same Statement node and just gains another link to the chunk it reappeared in, instead of duplicating. The collapse is exact, though: two different claims that merely share a predicate stay separate nodes, and relating those to each other is a query over the graph, not something the key does for you.

One entity, many episodes

An entity is one node no matter how many episodes mention it, so the graph does not just hold facts, it connects the conversations that share them. Two episodes that never reference each other line up the moment they talk about the same company:

EPISODE Die Nullnummer Folge 0 EPISODE Mikroökonomen Folge 1 mentions mentions Amazon one node, both episodes PLANT · in both · Same-Day-Delivery economic concept · shared ERSETZT DHL Folge 0 BELIEFERT München Folge 1

Two early episodes that never reference each other, joined because both mention Amazon - so both resolve to the same node. The PLANT Same-Day-Delivery fact turns up in each; ERSETZT DHL and BELIEFERT München are what each episode adds on its own.

Amazon comes up in 122 episodes; here two of the earliest both resolve to the one node, and the claim that Amazon plans same-day delivery is asserted independently in each. That is the aggregation a pile of vectors could not give you - counting a topic, or finding where two of them meet, falls out of the structure itself.

Two models, two jobs

Extraction is split across two models, because "find the entities" and "find the relations between them" are different problems that want different tools.

Entities come from a NER model, not the LLM. A purpose-built extractor - GLiNER2, a small DeBERTa encoder that runs on the laptop GPU - is handed a fixed schema of entity types (people, companies, institutions, policies, economic concepts) and returns the typed spans it finds above a confidence threshold. It generates nothing; it labels. That makes it fast, effectively free, and high-precision on the one job it has, and it settles the graph's vocabulary — the set of nodes — before a single relation is drawn.

Amazon hat extrem viel vor, was Same-Day-Delivery und demnächst Lebensmittel angeht. company economic concept economic concept GLiNER2 · FIXED TYPE SCHEMA person company institution policy economic concept

Entities come from a NER pass, not the LLM: GLiNER2 tags spans against a fixed schema and generates nothing. It fixes the graph’s vocabulary before a single relation is drawn.

Relations come from a local instruction LLM. The harder reading - which entity did what to which, and who said so - goes to Gemma 3, Google's open 12B instruction model, run locally in 4-bit through MLX, one diarized chunk at a time, returning subject-predicate-object triples as JSON. This is the part that genuinely needs comprehension, so it gets the generative model. Everything cheaper is spent first.

Amazon hat extrem viel vor, was Same-Day-Delivery und demnächst Lebensmittel angeht. GEMMA 3 · RELATION EXTRACTION AmazonPLANTSame-Day-Delivery AmazonPLANTLebensmittel The predicate isn’t a word in the text: the model reads hat viel vor and normalises it to one relationship type, PLANT.

Relations come from the local instruction model, one diarized chunk at a time. It reads for meaning - hat viel vor becomes PLANT - and returns subject-predicate-object triples, the edges the NER pass never draws.

Splitting the work this way is mostly a win, but it leaves one seam. The LLM, reading freely, names an endpoint slightly differently from the NER span it should attach to — a declension, an extra qualifier, a casing difference — and a naive exact-match join drops the fact on the floor. Reconnecting those endpoints to the vocabulary GLiNER2 already fixed is its own piece of work, in chapter 7.

The hard part: moving beyond low-information statements

Point a relation extractor at a transcript and it dutifully captures the conversation rather than its content: "Hannah asks about SAP," "Frederik mentions Habeck," "the host says the economy is slowing." These are low-information statements - true about the talk, empty as statements about the world. They are not the hosts making small talk; they are what the model produces when it records that a sentence was spoken instead of what the sentence claimed. Left in, they do not so much bury the real facts as distort the graph's shape: each one wires a host into another node, and the host quietly becomes the most connected "entity" in a graph that is meant to be about the world, while carrying no factual weight of its own.

The fix was three parts, a prompt to steer the model plus a deterministic filter to catch what the prompt misses:

  1. Drop speech-act predicates at the write boundary (said, asks, mentions, …).
  2. Reinforce the prompt to extract world-facts, not commentary about the discussion.
  3. Dedup restated facts into one node via the statement-id key above.

Across the proof-of-concept this removed 13% of the raw LLM relations as conversational bleed, with zero speech-acts leaking into the final graph. Nearly all of that 13% is one pattern, a host sitting in the subject or object slot, with a thin remainder of speech-act predicates where neither side is a host:

Raw LLM-extracted relations, by outcome 87% kept as world-facts host as subject or object · 11.5% speech-act predicate · 1.5%

Almost all the dropped 13% is one pattern: a host sitting in the subject or object slot. Speech-act-only is a thin 1.5%.

And the prompt change alone was not enough: even told to extract world-facts, the model still emitted speech-acts that the filter had to catch. Belt and suspenders, the same pattern as the masked-context speaker fix. Getting extraction right, and cleaning up what slips through anyway, is the work of the next two chapters.

How this was made. The pipeline and the articles you're about to read were both created with AI assistance. Responsibility for both, and especially for the statements made in the articles, lies with me. I acknowledge and respect that many people, for ethical or other reasons, will not want to consume content that was created in part or wholly using LLMs. This is your notice. My intent is to inform, not deceive.

Close