Skip to content

Non-deterministic graphs from identical inputs: extraction lacks seed/prompt-override controls and extract_corpus_parallel ignores extraction-spec #1695

Description

@reeve-c

Summary

Running graphify on an identical set of input files produces a materially different graph on every run — different node counts, different node sets, different communities.

Important constraint: the semantic cache is not an acceptable workaround for us. Our runs are independent (fresh/cold cache each time — no shared cache state), so "identical cache → identical output" doesn't apply. We need extraction itself to be reproducible from a cold start, without relying on cache reuse. This report is about the controls needed to make generation deterministic.

Environment

  • graphify 0.9.5 (graphifyy==0.9.5)
  • Driven headlessly via the Claude Agent SDK running the /graphify <dir> --mode deep skill
  • Corpus: 12 Markdown documents (~104k words total; ~8.7k words each), docs-only (no code)
  • Backends tested: host Claude subagents; and extract_corpus_parallel with OpenAI gpt-4.1 / gpt-4.1-mini and Gemini gemini-2.5-pro / gemini-2.5-flash, via an OpenAI-compatible endpoint

What I observed

Repeated runs over the exact same files (cold cache each time):

Backend / mode Nodes across runs Node-ID overlap (Jaccard), run-vs-run
Host Claude subagents (default temp) 22 / 72 / 100 very low
OpenAI gpt-4.1 (temperature 0) 97 vs 80 ~43%
OpenAI gpt-4.1-mini (temperature 0) 84 vs 83 ~30%

Everything downstream of extraction looks deterministic (sorted file collection, label-derived IDs, dedup, Leiden with random_seed=42). The variance is entirely in semantic extraction (Pass 3).

Experiments and findings

  1. Default path uses non-deterministic subagents. With no GEMINI_API_KEY/GOOGLE_API_KEY set, the skill routes doc extraction to the host agent's subagents, which run at the model's default sampling temperature with no seed — runs diverge wildly (22 → 100 nodes).

  2. extract_corpus_parallel (temperature 0) helps but is far from reproducible — ~43% node overlap for gpt-4.1. Two structural causes:

    • Open-ended entity selection — the built-in prompt asks to "extract named concepts and entities"; which spans become nodes varies per run.
    • Free-form labels → derived IDs — "FSD" vs "FSD (Supervised)" vs "Full Self-Driving" yield different IDs for one entity; dedup catches some, not all.
  3. extraction-spec.md does not affect the extract_corpus_parallel path. Editing it to make extraction stricter had no effect — that file is only the subagent prompt. extract_corpus_parallel uses the hardcoded _EXTRACTION_SYSTEM in graphify/llm.py, with no env var / param to override it.

  4. No seed is sent on the OpenAI-compatible call. _call_openai_compat sets temperature but never seed, so seeded/reproducible sampling can't be pinned through graphify.

  5. Reasoning ("thinking") models can't be made deterministic heregemini-2.5-pro run-to-run (fresh reasoning trace each call).

  6. gemini-2.5-flash frequently produced invalid JSON. With no JSON/structured-outpue and were dropped, changing the graph.

  7. Subagent chunking is by file count, not tokens — and overflows. skill.md Step B1cked 4 large docs into one subagent, which blew the model's 200k-token session limitmid-write; the chunk was lost and the run failed with no graph.json. The extract_corpus_parallel path handles this (_pack_chunks_by_tokens, expand_oversized_files, adaptive bisect_slice), but
    the subagent skill path has none of it.

Requests (to make cold-start extraction reproducible)

  1. Overridable extraction system prompt for extract_corpus_parallel — an env var/param to supply a strict, closed-schema prompt (mechanical entity categories, canonical verbatim labels, closed relation
    vocabulary). Currently impossible without patching the package; this is the single bigges
  2. Forward a configurable seed to OpenAI-compatible / Anthropic calls, so backends ing can be pinned.
  3. Optional JSON / structured-output mode (response_format) for backends that suppolure-driven variance.
  4. Token-aware chunking in the subagent skill path (skill.md Step B1) — chunk by wo_corpus_parallel` already does, so large-document corpora don't overflow and silently
    fail.
  5. Docs: note that (a) reasoning/thinking models are unsuitable for reproducible extrpec.mdgoverns only the subagent path, notextract_corpus_parallel`.

Minimal repro

  1. `graphify extract <dir-of-large-.md-docs> --backend openai --model gpt-4.1 --mode deep
  2. Run twice into separate output dirs (cold cache)
  3. Compare graph.json node ID sets → ~40–45% Jaccard, different node counts

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions