cluster-only silently labels 3 of 16 communities and exits 0: _call_llm discards finish_reason='length', so the label split-retry never fires
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 58/100
Hướng nghiên cứu
Start with the deterministic offline reproducer and inspect llm.py, especially _call_llm, _parse_label_response, and _label_batch_with_retry, then trace the generated_labels merge in cli.py. Confirm that a truncated reply produces only 3 of 16 labels without an error. Done means truncation or incomplete labeling is surfaced or retried, and cluster-only no longer reports a partial result as success, with focused tests covering the path.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Summary
On the gemini backend's current default model, cluster-only --backend gemini regularly names only the first 3 of 16 communities, silently keeps label_communities_by_hub filenames for the other 13, prints Done - 16 communities, and exits 0. Measured 6 of 12 runs on the same graph.
The API reports the problem correctly — finish_reason='length' — but _call_llm returns resp.choices[0].message.content and discards it. Every downstream recovery mechanism is then defeated in turn, and the run is reported as a success.
Verified on 0.9.63, and llm.py + cluster.py are byte-identical at v0.9.64; the cli.py merge block at v0.9.64:2265-2272 is unchanged.
Why it is hard to notice
This is the failure mode #2534 was about — a silent downgrade with a success exit code — but the hub labeler makes this instance much quieter than the ones fixed there. The un-labeled state is no longer a conspicuous Community 7; it is a plausible-looking filename. A half-labeled report reads as a slightly disappointing report, not a broken one.
GRAPH_REPORT.md from a bad run (13 of 16 are hub fallbacks, but nothing says so):
## Community Hubs (Navigation)
- Test Context and Fixtures <- LLM
- Request Body Parsing <- LLM
- Object Storage Operations <- LLM
- 02-server.spec.ts <- hub fallback
- projection.ts <- hub fallback
- index.ts <- hub fallback
- <migration>.sql <- hub fallback
- <component>.tsx <- hub fallback
... 8 more
A good run on the identical graph names all 16: Testing Infrastructure, Snapshot Serialization, Hono Web Framework, Ledger Migration, ...
Root cause
_label_batch_with_retry budgets min(256 + 48 * n, 8192) output tokens — here 1024 for 16 communities — sized by the comment above it for "a 2-5 word name is ~10 tokens" plus preamble headroom. That predates the backend default moving to a reasoning model. BACKENDS["gemini"]["default_model"] is now gemini-3-flash-preview with reasoning_effort: "low", and reasoning tokens draw down the same completion budget.
Measured directly against the endpoint, with the exact prompt _community_label_lines builds and max_completion_tokens=1024:
call 0: finish_reason='length' completion_tokens=40 chars=136 -> TRUNCATED/INVALID
call 1: finish_reason='length' completion_tokens=38 chars=121 -> TRUNCATED/INVALID
call 2: finish_reason='length' completion_tokens=40 chars=136 -> TRUNCATED/INVALID
call 3: finish_reason='length' completion_tokens=38 chars=121 -> TRUNCATED/INVALID
call 4: finish_reason='length' completion_tokens=40 chars=136 -> TRUNCATED/INVALID
call 5: finish_reason='length' completion_tokens=40 chars=136 -> TRUNCATED/INVALID
Worth noting on its own: this same path already declares a much larger budget for this backend. BACKENDS["gemini"]["max_completion_tokens"] is 16384, and the extraction path honours it via _resolve_max_tokens(cfg.get("max_completion_tokens") or ...). _call_llm reads cfg for base_url, temperature, reasoning_effort and extra_body, but takes the completion cap from its own max_tokens argument and never looks at cfg["max_completion_tokens"] — so the labeling path sends 1024 to a model whose own entry says 16384.
Then five layers each behave reasonably in isolation and compound:
_call_llmdrops the signal. It returnsresp.choices[0].message.contentonly.finish_reasonis right there onresp.choices[0]and is discarded — while_call_claude,_call_claude_cliand_call_bedrockall setresult["finish_reason"] = "length" if ... else "stop"for the extraction path. The labeling path is the one that throws it away.- The salvage regex converts truncation into a parse success.
_parse_label_responsefailsjson.loadson the cut-off object, then recovers the complete"cid": "name"pairs (#1690). It returns 3 pairs and does not raise. - The split-retry therefore never fires.
_label_batch_with_retryonly splits and retriesexcept (json.JSONDecodeError, ValueError). Because step 2 returned cleanly, the mechanism built precisely to recover truncation is skipped. The two fixes defeat each other: #1690 (salvage partial) pre-empts #1278 (split and retry), and salvaging 3 of 16 is much worse here than re-asking for 8 and 8. generate_community_labelsreports success. It returnssource="llm", and warns only on no-backend or a raised exception. A partial dict is indistinguishable from a complete one to its caller.cli.pymerges without counting.labels.update({cid: v for cid, v in generated_labels.items() if ...})correctly declines to clobber hub labels with placeholders, but nothing compareslen(generated_labels)tolen(communities).generated_labelsappears exactly twice incli.pyand neither use is a count.
Related: the docstring on _thinking_disabled_via_env justifies leaving thinking on by arguing that a reasoning model's truncation "is caught and re-tried by the adaptive extraction/labeling retry, so it is a rare, recoverable failure" (#1621). For the labeling path that assumption does not hold — step 2 above means the retry does not see it. The reasoning that keeps the default in place depends on a recovery that is bypassed.
Reproducer (deterministic, offline, no API key)
The field failure is intermittent because it depends on how many reasoning tokens the model spends. The defect itself is not — stub the backend with a truncated reply and it is fully deterministic:
import networkx as nx
from unittest.mock import patch
import graphify.llm as llm
G = nx.Graph(); communities = {}
for cid in range(16):
members = [f"c{cid}_n{i}" for i in range(4)]
for m in members: G.add_node(m, label=m)
G.add_edges_from([(members[0], members[i]) for i in (1, 2, 3)])
communities[cid] = members
# A real reply, cut off mid-object at the completion cap.
TRUNCATED = ('{"0": "Testing Infrastructure", "1": "Request Body Parsing", '
'"2": "Object Store Operations", "3": "End-to-')
with patch.object(llm, "_call_llm", return_value=TRUNCATED):
labels, source = llm.generate_community_labels(G, communities, backend="gemini")
named = {c: v for c, v in labels.items() if not v.startswith("Community ")}
print(len(named), "of", len(communities), "labeled; source =", source)
3 of 16 labeled; source = 'llm'
No exception, no warning on stderr, source="llm".
Measurements
Same graph (537 nodes / 1628 edges / 16 communities, from a 76-file TypeScript+SQL repo), repeated cluster-only --backend gemini, labels file inspected each run:
| config | runs | fully labeled | 3-of-16 | output tokens |
|---|---|---|---|---|
| default | 12 | 6 | 6 | 133-167 good / 39 bad |
GRAPHIFY_MAX_OUTPUT_TOKENS=16384 |
5 | 5 | 0 | 133-167 |
The bimodality is the tell: a bad run is always exactly 39 output tokens and exactly 3 labels, because the reply dies at the same point every time.
Workaround
GRAPHIFY_MAX_OUTPUT_TOKENS=16384 — 5 of 5 clean above. It is a blunt global override, but it is the only lever a user has here, since the label budget is not otherwise configurable.
Suggested fixes
In rough order of value:
- Count and report. In
cli.py, comparelen(generated_labels)againstlen(label_communities_input)and warn when short —warning: labeled 3 of 16 communities; 13 kept structural fallback names. Re-run cluster-only, or raise GRAPHIFY_MAX_OUTPUT_TOKENS.This alone converts a silent wrong answer into a visible one, and matches what #2534 established for this class. - Stop discarding
finish_reasonin_call_llm. It is already modelled on the other three backends. With it,_label_batch_with_retrycan treatlengthas a truncation and split-retry even when salvage recovered some pairs — which is what #1278 was for. - Let the label budget see the backend config, e.g. floor it at
cfg.get("max_completion_tokens"), or scale the per-community allowance when the resolved model is a reasoning model. Sending 1024 to a model whose own entry says 16384 looks unintended. - Re-ask for the missing cids. Even without (2),
generated_labelsmissing cids is a sufficient trigger for one narrowed retry before falling back.
Happy to open a PR for (1) and (2) if that is useful — (1) is a few lines and self-contained.
Environment
- graphify 0.9.63 (
uv tool install "graphifyy[gemini,sql]"), labeling code verified unchanged at v0.9.64 - macOS 26 (Darwin 25.6.0), Python 3.13
- backend
gemini, default modelgemini-3-flash-preview, via the OpenAI-compatible endpoint --code-onlyextract, so the only LLM call in the run is community labeling
- Ngôn ngữ chính
- Python
- Star
- 124k
- Fork
- 11.9k
- Merge trung bình
- 2 giờ 35 phút
- Pull request đã merge (30 ngày)
- 6
Chuẩn bị môi trường
Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của Graphify-Labs/graphify
-
`file_hash` opens the lowercased path on Windows, so files with uppercase names fail on case-sensitive dirs (`\wsl.localhost`)Có thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
Graphify-Labs/graphify#4925 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
watch liveness check resolves relative source identities against process CWD instead of watch rootCó thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Graphify-Labs/graphify#4896 · 5 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
extract --out <dir>: semantic cache never saved, docs missing from manifest (relative source_file resolved against --out)Có thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
Graphify-Labs/graphify#4831 · 3 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
hooks: last-resort `python3` probe launches the Microsoft Store redirector on Windows (visible console flash, exit 9009)Có thể đã có người làm @Deepjyoti-Sarmah đã nhận 14 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
Graphify-Labs/graphify#4563 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
GRAPHIFY_DISABLE_THINKING is ignored by the claude backend, so thinking eats max_tokens and extraction truncatesCó thể đã có người làm @bercedev đã nhận 14 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 69/100
Graphify-Labs/graphify#4546 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của Graphify-Labs/graphify
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
Nixtla/mlforecast#823 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
microsoft/ML-For-Beginners#1061 ·
-
bot-found documentation priority: P3
Độ khó 2/5 Dưới một giờ Mức phù hợp với người mới 82/100
madenvel/KalinkaPlayer#503 ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 63/100
EleutherAI/lm-evaluation-harness#4508 ·
Maintainer thường phản hồi trong vòng 1 ngày