Repository navigation
[Feat]: Add retry logic for LLM judge and semantic memory consolidation on structured output failures #1332
Description
Is your feature request related to a problem?
Some LLM call sites using structured output (JSON format) lack retry logic when response parsing fails, causing transient errors to be treated as permanent failures.
- Evaluation w/ LLM judge: Benchmark results are skipped or corrupted due to malformed JSON responses.
- Semantic memory feature consolidation: Feature deduplication appears to be silently skipped when LLM parsing fails.
- Related issues and PR:
- [Bug]: Search memory failed with Qwen3.5-xxx thinking mode (default enabled) #1265
- [Bug]: Dead loop to trigger memmachine_server.semantic_memory.semantic_ingestion when LLM reponse is invalid #1270
- fix(semantic-memory): break ingestion retry loop on <think>-prefixed LLM responses #1327
- This PR addressed the problem for semantic memory profile extraction, but other LLM call sites using structured output seem to remain unprotected.
Details:
Evaluation w/ LLM judge:
At evaluation/retrieval_agent/llm_judge.py::evaluate_llm_judge(),
raw = call_fn(prompt)
label = json_repair.loads(raw)["label"]
return 1 if label == "CORRECT" else 0- LLM returns None or JSON missing the "label" key.
- Raw SDK calls have no retry handling.
In LoCoMo Benchmark with GLM-4.7-Flash model, ~6-8 failures out of 1540 judgments (~0.5%).
Semantic memory feature consolidation:
At packages/server/src/memmachine_server/semantic_memory/semantic_ingestion.py::_deduplicate_features(),
- On ValueError/TypeError from
llm_consolidate_features(), the function returns immediately. Retry logic seems to be absent. - Unlike Profile Extraction, may also lack _is_context_length_exceeded_error handling.
Describe the solution you'd like
LLM judge retry logic:
Add retry handling in evaluate_llm_judge() or within the create_judge_fn() call functions to catch JSON parsing failures (json_repair.loads(raw)["label"]) and retry the LLM call 1-2 times before marking the judgment as failed.
Semantic memory consolidation retry logic
Mirror the approach from PR #1327 for Profile Extraction. For example, add retry logic in _deduplicate_features() when llm_consolidate_features() raises ValueError/TypeError, with appropriate handling for non-retryable errors (e.g., context length exceeded).
Describe alternatives you've considered
| Alternative | Pros | Cons |
|---|---|---|
| No retry, fail silently (current) | Simple implementation | Loses eval data, accumulates redundant memories |
| Fail loudly (raise exception) | Makes failures visible | Breaks benchmark runs, requires manual intervention |
| Retry with exponential backoff | Better handling of rate limits | More complex, may delay execution |
| Retry once (proposed) | Simple, handles most transient errors | May not resolve persistent issues |
We tested single retry locally for LLM judge in evaluation/retrieval_agent with GLM-4.7-Flash and confirmed it resolves the ~0.5% failure rate experimentally.
Additional context
Invalid JSON patterns in semantic memory consolidation are categorized as below.
- Truncated responses – JSON cuts off before completion (EOF mid-string)
- Instruction ignoring – LLM adds preamble text or wraps output in markdown code blocks (```json)
- Control characters – Unicode control chars (\u0000–\u001F) embedded in strings
- Structural hallucination – Unclosed braces, random text inserted mid-JSON
Activity
sscargal commentedon Apr 17, 2026
Thanks for the feature submission. We'll review and add it to a future milestone.
sscargal commentedon Apr 21, 2026
Thanks for the detailed and well-researched issue report. After reviewing the current code at both call sites, I can confirm both gaps are real and actionable. Here's a full technical review.
Finding 1 — LLM Judge (
evaluate_llm_judge)The current code at
evaluation/retrieval_agent/llm_judge.py#L156-L158:There are actually two distinct failure modes here, not one:
call_fnreturnsNoneor""—json_repair.loads("")returns"", which is not subscriptable, raisingTypeError. The_call_responsesvariant already guards withor ""but_call_chatdoes not (resp.choices[0].message.contentcan beNone)."label"key —json_repair.loads(raw)["label"]raisesKeyError. This is the primary failure mode you observed (~0.5% with GLM-4.7-Flash).Additionally, the
main()loop atllm_judge.py#L200has no exception handling aroundevaluate_llm_judge(...), meaning a single failure aborts the entire benchmark run rather than just skipping that judgment.Recommended fix for
evaluate_llm_judge:And in
main(), update the call site to handleNone:Finding 2 — Semantic Memory Consolidation (
_deduplicate_features)The current code at
semantic_ingestion.py#L381-L391:Two confirmed gaps versus the
llm_feature_updatepath inprocess_semantic_type:process_semantic_type(llm_feature_update)_deduplicate_features(llm_consolidate_features)_is_context_length_exceeded_errorcheckpydantic.ValidationErrorcatchexcept Exception(ValueError, TypeError)Recommended fix for
_deduplicate_features:Design Considerations
A few points worth discussing before implementation:
Retry delay: A simple immediate retry is fine for JSON parse failures (these are not rate-limit errors). However, if
call_fn/generate_parsed_responsecan itself raise rate-limit errors (HTTP 429), a short backoff (e.g.,asyncio.sleep(1.0 * attempt)) would be safer. This may be worth checking ingenerate_parsed_response's implementation._call_chatNoneguard: The_call_responsespath already guards withor "", but_call_chatreturnsresp.choices[0].message.contentdirectly, which can beNoneper the OpenAI SDK types. This should be patched toreturn resp.choices[0].message.content or ""for consistency.Catch width in
_deduplicate_features: Per the PR fix(semantic-memory): break ingestion retry loop on <think>-prefixed LLM responses #1327 architect note, consider whether the retry loop should catchpydantic.ValidationErrorspecifically (for transient structured-output failures) and keepexcept Exceptionas a separate non-retried fallback — this prevents inadvertently retrying errors from the embedder or storage layers that happen to raiseValueError.Metrics/observability: Given the known ~0.5% failure rate on GLM-4.7-Flash, it may be worth adding a counter or log line that is easy to grep for in production to track retry effectiveness over time.
Testing: Unit tests should cover: (a) success on retry-after-failure, (b)
Nonereturn after exhausting retries for the judge, (c) context-length-exceeded skips without retry in consolidation, and (d)debug_fail_loudly=Truestill raises even after retries.Summary
The feature request is well-scoped and confirmed valid by direct code inspection. Both call sites lack the protections that exist elsewhere in the codebase. The proposed single-retry approach is the right default — simple, effective for transient parse failures, and consistent with what was experimentally validated. The main additions beyond what the issue describes are: guarding the
_call_chatNonecase, adding_is_context_length_exceeded_errorto_deduplicate_features, and aligning the exception catch width with the guidance from PR #1327.add retry logic for LLM judge and semantic memory consolidation (#1361)