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:
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:
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.
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.
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:
- Drop speech-act predicates at the write boundary (
said,asks,mentions, …). - Reinforce the prompt to extract world-facts, not commentary about the discussion.
- 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:
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.