Self-correcting LLM call wrapper: compress input, call the model, ground-check the answer, and on hallucination restructure the prompt and retry. The runtime guard rails for RAG.
Project description
relapse
Self-correcting LLM call wrapper for RAG. Wraps your LLM call in a feedback loop: call → ground-check the answer → restructure the prompt with explicit findings → retry. The first call uses your prompt verbatim; only on hallucination does relapse rewrite and re-ask. The user sees the final grounded answer, plus a transcript of what was rejected and why.
pip install relapse # core (Python stdlib only)
pip install relapse[ground] # optional: pulls corroborate for richer grounding
import relapse
def my_llm(prompt: str) -> str:
# your wrapper around Anthropic / OpenAI / Bedrock / local model
return client.messages.create(...).content[0].text
result = relapse.run(
query="When did WW2 end?",
retrieved_chunks=["World War II ended in 1945 with the surrender of Japan in September."],
call_llm=my_llm,
max_iterations=3,
)
print(result.answer) # the final grounded answer
print(result.iterations) # how many tries it took
print(result.grounded) # True if the final answer passed the check
print(result.transcript) # list of (prompt, answer, findings) per iteration
That's the whole API. call_llm is provider-agnostic — pass a function that takes a prompt and returns text. relapse doesn't bake in Anthropic, OpenAI, or anything else.
What relapse is for
LLM hallucination at the answer layer. You can't fix it with better retrieval — the model has the right context but invents a fact anyway. The standard fix is "retry with feedback," but most teams hand-roll it badly: they retry on a vague "are you sure?" prompt, which often produces the same hallucination with more confidence.
relapse runs a deterministic check after each call (numbers, years, quoted spans must appear in the retrieved chunks), and feeds the specific failed spans back into the next prompt. That's what makes the retry productive.
How it works
user query + retrieved chunks
│
▼
┌─────────────────────────┐
│ iteration 1 │
│ build prompt with ctx │
│ call_llm(prompt) │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ grounding_check │ <-- deterministic, no extra LLM call
│ - numbers in chunks? │
│ - years in chunks? │
│ - quotes in chunks? │
└─────────────────────────┘
│
▼
grounded? ──yes──► return answer
│
no
▼
┌─────────────────────────┐
│ iteration N+1 │
│ rewrite prompt: │
│ - quote prev answer │
│ - cite each failed │
│ span by code │
│ - tell model to use │
│ ONLY the context │
│ call_llm(prompt) │
└─────────────────────────┘
│
└─► loop until grounded OR max_iterations
Iteration 1 = the user's original prompt. Iterations 2+ explicitly cite what was wrong. relapse never silently rewrites a fine answer.
Pluggable grounding check
The default check is a vendored copy of corroborate's FG001/002/003 (numbers / years / quoted spans). It's deterministic, sub-millisecond, and never produces a false negative on those classes.
For richer checks, plug your own:
import corroborate
def my_check(query, chunks, answer):
rep = corroborate.check_answer(query, chunks, answer)
return relapse.GroundingResult(
ok=rep.ok(),
findings=tuple(
relapse.GroundingFinding(code=f.code, message=f.message, spans=f.spans)
for f in rep.findings
),
)
result = relapse.run(query, chunks, call_llm=my_llm, grounding_check=my_check)
Or write a custom check that uses any signal you want — entity matching, NLI scoring, an LLM-as-judge if you really want one. relapse just expects (query, chunks, answer) -> GroundingResult(ok, findings).
Optional compression
result = relapse.run(query, chunks, call_llm=my_llm, compress=True)
compress=True runs a light deterministic pass before the first LLM call: dedup, whitespace-normalize, cap total characters to 8000. Real semantic compression should use an LLM — that's not what relapse is for. Compression is opt-in because it changes token cost and the first-call behavior, so you have to ask for it.
API reference
relapse.run(
query, # str
retrieved_chunks, # list[str] or list[{"text": str, ...}]
*,
call_llm, # callable(prompt: str) -> str
grounding_check=None, # callable(query, chunks, answer) -> GroundingResult
compress=False, # opt-in light compression
max_iterations=3, # cap LLM calls
) -> RelapseResult
RelapseResult:
answer— the final answer string.grounded— True iff the final answer passed the grounding check.iterations— number of LLM calls made (1 on success, ≤ max_iterations on failure).transcript— list ofIterationRecord(iteration, prompt, answer, grounded, findings).
GroundingFinding(code, message, spans). GroundingResult(ok, findings).
Cost considerations
relapse calls your LLM up to max_iterations times. The default of 3 means worst case 3× the token cost of a naive single call. In practice:
- Most non-hallucinated answers terminate at iteration 1 (no extra cost).
- The retry prompt is longer (it includes the previous answer + findings), so a retry is ~1.3-1.5× a fresh call.
- Most hallucinations terminate at iteration 2 — the explicit "you said X but X isn't in the chunks" rephrasing is highly corrective.
If cost matters more than catch rate, lower max_iterations to 2. If you need maximum safety, raise it to 5.
What relapse is NOT
- Not a runtime moderation layer — for that, see LLM Guard or NeMo Guardrails.
- Not a retrieval improver — broken retrieval is upstream of relapse. See chaffer for that.
- Not an answer reranker — relapse retries the same model with a tighter prompt, it doesn't pick between candidates.
- Not a free lunch on cost — see "Cost considerations" above.
See also
- corroborate — the standalone deterministic answer-grounding check. relapse vendors its core; for rich grounding, install corroborate and pass
corroborate.check_answerasgrounding_check. - chaffer — corpus linter (retrieval-quality bugs).
- redoubt — corpus linter (prompt-injection scanner).
- staleness — corpus linter (freshness / drift).
- dash-mlguard — same author, same form factor, but for ML training pipelines.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file relapse-0.1.0.tar.gz.
File metadata
- Download URL: relapse-0.1.0.tar.gz
- Upload date:
- Size: 12.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6a79f61a0fdc18f462a38663d6a7012c4548e2d09b13478b8ff8973d848d71c1
|
|
| MD5 |
e0724fa8dd6224b89322ab9a10526c94
|
|
| BLAKE2b-256 |
b6b56fc470f1f12fae73eddaae86b0d7ce0c93947a40bc034aae32f9a19201b3
|
File details
Details for the file relapse-0.1.0-py3-none-any.whl.
File metadata
- Download URL: relapse-0.1.0-py3-none-any.whl
- Upload date:
- Size: 9.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
12d41d3a99f2adea0cdac172e2106d8e225e472b803ec428ec5704e8805305f6
|
|
| MD5 |
c2eb3a541d8045ccbd37933d0d4b5f9f
|
|
| BLAKE2b-256 |
e93d96f4f55bd9f7ff73524c04e2d6068426103bb753e73437095e50f390a5be
|