Can a locally-run general LLM (Gemma 4 12B) replace a purpose-built 200M specialist (GLiNER-Relex) for knowledge-graph extraction? I ran the test, and the answer turned out to depend on a single default setting far more than on the model itself.
If you build with AI agents, you hit the context problem: agents lose the thread every time they hand work to each other. One way to give them shared context is a knowledge graph. But a knowledge graph has to be built, and that means pulling entities and the relationships between them out of plain text first.
I run everything locally, and I did not want to load a second model just for that extraction step. So the question was simple: can the LLM I already run (Gemma 4 12B) do the extraction itself, or do I still need a dedicated specialist?
To answer it, I benchmarked Gemma 4 12B against GLiNER-Relex, a 200M model built only for this, on CoNLL04 (joint named-entity and relation extraction).
CoNLL04 test set, 288 sentences, strict micro-F1.
| Model | Entity F1 | Relation F1 | Parse failures | Hallucinated entities | Time / sentence |
|---|---|---|---|---|---|
| GLiNER-Relex (200M specialist) | 0.543 | 0.322 | 0% | 0 | 0.6s |
| Gemma 4 12B (thinking on, default) | 0.648 | 0.328 | 11.1% | 18 | 139s |
| Gemma 4 12B (thinking off) | 0.725 | 0.506 | 0% | 4 | 10s |
Notes:
outputs/per_type_f1.csv.analysis.ipynb.Requirements:
gemma4:12b pulled (the 4-bit build runs in about 16GB of RAM)python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
ollama pull gemma4:12b
cd src
# run both models over the test set (thinking on by default)
python run_experiment.py
# run Gemma with thinking off
python run_experiment.py --thinking=False
# quick smoke test on a subset
python run_experiment.py --thinking=False --limit=15
# build the result tables from saved predictions
python make_report.py
The runner writes to outputs/predictions.csv incrementally and is resumable. Stop it and rerun, and it continues where it left off. Thinking-on and thinking-off results are stored under separate model labels, so a second run appends rather than overwrites. make_report.py reads those predictions and writes results.csv, per_type_f1.csv, and summary_table.md.
The most interesting part was Gemma's 11% failure rate, and the fact that the cause was not what it first looked like.
Reading the raw outputs and Ollama's token-level timing, I traced the failures to three modes:
done_reason=length and an empty answer. Disabling thinking fixed it (8 to 10 seconds, clean answer), and it was also most of the 138s average latency.100000...) until it was cut off.<|tool_response>) glued onto the end, which broke strict JSON parsing. A defensive parser that extracts the outermost {...} recovers these.The diagnosis was not obvious, and ruling out two plausible causes with data was part of the work:
num_predict) truncating answers. Ruled out, because the failures were the longest runs producing empty or looping output, not short truncations of good answers.eval_count does not report.src/
schema.py canonical data model (Entity, Relation, Example, Prediction) and shared label maps
load_data.py CoNLL04Loader: loads and reconstructs gold entities and relations
run_gliner.py GLiNERExtractor: the 200M specialist
run_gemma.py GemmaExtractor: local Gemma via Ollama, schema-constrained output, thinking toggle, defensive parsing
run_experiment.py resumable runner, writes predictions.csv, --thinking and --limit flags
evaluate.py Evaluator: strict micro-F1, per-type F1, and the diagnostics
make_report.py writes results.csv, per_type_f1.csv, summary_table.md
outputs/ generated results
analysis.ipynb the full visual analysis (a chart for every finding)
I report strict micro-F1: pool every entity and relation type into one score, with exact matching. It is the standard for this benchmark and it keeps the comparison honest, since there is no tunable leniency to accidentally inflate a score. It also makes the numbers conservative ("the United States" vs "United States" counts as a miss), but the same strict rule is applied to both models, so the comparison stays fair.
The model was capable the whole time. What decided the outcome was the harness around it: the prompt, the output format, the decoding settings, and the parsing. A single default flag flipped Gemma from a slow, unreliable tie into the most accurate model in the test. For a local setup, how you run a model can matter as much as which model you pick.
3 commits
Jupyter Notebook
96.9%
Python
3.1%
Can a locally-run general LLM (Gemma 4 12B) replace a purpose-built 200M specialist (GLiNER-Relex) for knowledge-graph extraction? I ran the test, and the answer turned out to depend on a single default setting far more than on the model itself.
If you build with AI agents, you hit the context problem: agents lose the thread every time they hand work to each other. One way to give them shared context is a knowledge graph. But a knowledge graph has to be built, and that means pulling entities and the relationships between them out of plain text first.
I run everything locally, and I did not want to load a second model just for that extraction step. So the question was simple: can the LLM I already run (Gemma 4 12B) do the extraction itself, or do I still need a dedicated specialist?
To answer it, I benchmarked Gemma 4 12B against GLiNER-Relex, a 200M model built only for this, on CoNLL04 (joint named-entity and relation extraction).
CoNLL04 test set, 288 sentences, strict micro-F1.
| Model | Entity F1 | Relation F1 | Parse failures | Hallucinated entities | Time / sentence |
|---|---|---|---|---|---|
| GLiNER-Relex (200M specialist) | 0.543 | 0.322 | 0% | 0 | 0.6s |
| Gemma 4 12B (thinking on, default) | 0.648 | 0.328 | 11.1% | 18 | 139s |
| Gemma 4 12B (thinking off) | 0.725 | 0.506 | 0% | 4 | 10s |
Notes:
outputs/per_type_f1.csv.analysis.ipynb.Requirements:
gemma4:12b pulled (the 4-bit build runs in about 16GB of RAM)python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
ollama pull gemma4:12b
cd src
# run both models over the test set (thinking on by default)
python run_experiment.py
# run Gemma with thinking off
python run_experiment.py --thinking=False
# quick smoke test on a subset
python run_experiment.py --thinking=False --limit=15
# build the result tables from saved predictions
python make_report.py
The runner writes to outputs/predictions.csv incrementally and is resumable. Stop it and rerun, and it continues where it left off. Thinking-on and thinking-off results are stored under separate model labels, so a second run appends rather than overwrites. make_report.py reads those predictions and writes results.csv, per_type_f1.csv, and summary_table.md.
The most interesting part was Gemma's 11% failure rate, and the fact that the cause was not what it first looked like.
Reading the raw outputs and Ollama's token-level timing, I traced the failures to three modes:
done_reason=length and an empty answer. Disabling thinking fixed it (8 to 10 seconds, clean answer), and it was also most of the 138s average latency.100000...) until it was cut off.<|tool_response>) glued onto the end, which broke strict JSON parsing. A defensive parser that extracts the outermost {...} recovers these.The diagnosis was not obvious, and ruling out two plausible causes with data was part of the work:
num_predict) truncating answers. Ruled out, because the failures were the longest runs producing empty or looping output, not short truncations of good answers.eval_count does not report.src/
schema.py canonical data model (Entity, Relation, Example, Prediction) and shared label maps
load_data.py CoNLL04Loader: loads and reconstructs gold entities and relations
run_gliner.py GLiNERExtractor: the 200M specialist
run_gemma.py GemmaExtractor: local Gemma via Ollama, schema-constrained output, thinking toggle, defensive parsing
run_experiment.py resumable runner, writes predictions.csv, --thinking and --limit flags
evaluate.py Evaluator: strict micro-F1, per-type F1, and the diagnostics
make_report.py writes results.csv, per_type_f1.csv, summary_table.md
outputs/ generated results
analysis.ipynb the full visual analysis (a chart for every finding)
I report strict micro-F1: pool every entity and relation type into one score, with exact matching. It is the standard for this benchmark and it keeps the comparison honest, since there is no tunable leniency to accidentally inflate a score. It also makes the numbers conservative ("the United States" vs "United States" counts as a miss), but the same strict rule is applied to both models, so the comparison stays fair.
The model was capable the whole time. What decided the outcome was the harness around it: the prompt, the output format, the decoding settings, and the parsing. A single default flag flipped Gemma from a slow, unreliable tie into the most accurate model in the test. For a local setup, how you run a model can matter as much as which model you pick.
3 commits
Jupyter Notebook
96.9%
Python
3.1%