Skip to content

query retrieval quality degrades as the graph grows, because the output budget is a fixed 2000 tokens #2530

Open

Description

@arnasmmm

As a graph grows, query finds proportionally more nodes but prints the same number, so the
share of the answer you actually see falls steadily. Nothing about the invocation changes — the
same question, on a better graph, returns a worse answer. Measured on 0.9.34 (PyPI package
graphifyy), Windows, Python 3.13; the relevant code is unchanged in 0.9.35 (version note at
the end).

The mechanism

budget = 2000 is a constant in cli.py, and _query_graph_text / _subgraph_to_text default
to token_budget = 2000 with char_budget = token_budget * 3. It does not vary with the size
of the graph or of the found set.

Self-contained repro

Generate graphs of increasing size with an identical shape — one seed, K mid nodes, 45 leaves
each. Every non-seed node keeps degree 46, deliberately below the hub threshold (see below), so
nothing is pruned as a hub and the growth is purely "a larger relevant neighbourhood":

import json, os
LEAVES = 45
def build(k, path):
    os.makedirs(os.path.join(path, "graphify-out"), exist_ok=True)
    nodes = [{"id": "core", "label": "CoreService", "source_file": "core.py",
              "source_location": "L1", "community": 0}]
    links = []
    for i in range(k):
        mid = "mid%03d" % i
        nodes.append({"id": mid, "label": "MidModule%03d" % i,
                      "source_file": "mid%03d.py" % i, "source_location": "L1", "community": 0})
        links.append({"source": "core", "target": mid, "relation": "uses"})
        for j in range(LEAVES):
            leaf = "leaf%03d_%02d" % (i, j)
            nodes.append({"id": leaf, "label": "Leaf%03d_%02d" % (i, j),
                          "source_file": "leaf%03d_%02d.py" % (i, j),
                          "source_location": "L1", "community": 0})
            links.append({"source": mid, "target": leaf, "relation": "uses"})
    json.dump({"directed": False, "multigraph": False, "graph": {},
               "nodes": nodes, "links": links},
              open(os.path.join(path, "graphify-out", "graph.json"), "w"))

Then run graphify query "CoreService" in each:

graph nodes nodes found printed at default share printed printed at --budget 8000
93 93 93 100.0 % 93
231 231 109 47.2 % 231
461 461 109 23.6 % 436
921 921 109 11.8 % 437
1841 1841 110 6.0 % 437

The found set grows 20x; the printed set stays flat at ~109.

The same thing on a real graph

Our prose graph over two weeks, taken from git history of graphify-out/graph.json:

date nodes edges
2026-07-24 585 1167
2026-07-31 1478 3324
2026-08-04 2588 5701
2026-08-06 4139 10043

7x node growth in 13 days, while the default budget stayed 2000 tokens. Replaying 121 real
questions
from our own query log against today's graph, at the default budget:

  • 110 of 121 are truncated
  • median share of found nodes actually printed: 28.8 %
  • for the 11 questions that find 300+ nodes, median share printed: 11.6 % (worst 5.2 %)
  • median printed count: 44 at the default budget, 152 at --budget 8000

The printed count is essentially constant at ~40-50 regardless of whether the question found
60 nodes or 763.

Hub blocking is already there and is not the missing piece

I want to be explicit about this, because it is the obvious first hypothesis and it is wrong.
_bfs already declines to expand through high-degree nodes:

degrees_sorted = sorted(degrees)
p99_idx = int(len(degrees_sorted) * 0.99)
hub_threshold = max(50, degrees_sorted[p99_idx])
...
if n not in seed_set and G.degree(n) >= hub_threshold:
    continue

On our graph p99 degree is 35, so the threshold floors to 50, and 29 nodes (0.7 %) are excluded
as transit. I tested the hypothesis directly by ablating the single highest-degree node — degree
966, about 10 % of all edges — and re-asking 25 questions that each find 100+ nodes:

  • mean share printed, full graph: 27.5 %
  • mean share printed, top hub removed: 27.9 %

Effectively no change. The flood is not coming from hubs; it is the ordinary depth-2
neighbourhood in a graph that has simply got denser. So this is not a traversal-pruning bug —
it is that the output budget does not scale with what the traversal legitimately found.

What the banner already does, and why it is not enough

Credit where due, the truncation banner is informative:

[!] TRUNCATED: showing 46 of 524 nodes (~2000-token budget). The answer may be among the
478 cut nodes - raise the token budget (CLI: --budget) or narrow the query
(e.g. context_filter=['call'], or get_node for a specific symbol).

It already says the answer may be in the cut portion. Three reasons that does not close the gap:

  1. It is advice, not a default. The default stays wrong, and the overwhelmingly common case is
    a caller who never passes --budget. On our corpus the default is wrong for 110 of 121 real
    questions.
  2. The recommended narrowing does not work on LLM-extracted graphs. Following
    context_filter=['call'] on our graph takes 524 found nodes down to 8, because almost no
    edge carries a context attribute. I have filed that separately.
  3. The banner also prints when nothing was cut — showing 30 of 30 nodes ... The answer may be among the 0 cut nodes — which trains the reader to ignore it.

Request

  1. Scale the default budget with the size of the found set or the graph — for example a
    floor of 2000 with growth in the number of nodes found, capped at something sane. The
    information needed is already available at the point the banner is printed.
  2. Failing that, at minimum suppress the banner when nothing was cut, and drop the
    context_filter suggestion unless the graph actually has context attributes on its edges.

Version note

Measured on 0.9.34. The 0.9.35 diff does not touch this path: cli.py is byte-identical
between the two tags, so the budget = 2000 default is unchanged, and serve.py's only changes
are to context hints and seed selection, leaving _subgraph_to_text, _cut_lines_to_budget
and the BFS/hub logic untouched. The MCP tool schema still declares "token_budget": {"default": 2000}.

Happy to test a branch. We have a 4139-node LLM-extracted graph and a log of 336 real queries I
can replay to give you a before/after distribution of "share of found nodes printed".

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions