Skip to main content

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 of IterationRecord(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_answer as grounding_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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

relapse-0.1.0.tar.gz (12.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

relapse-0.1.0-py3-none-any.whl (9.4 kB view details)

Uploaded Python 3

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

Hashes for relapse-0.1.0.tar.gz
Algorithm Hash digest
SHA256 6a79f61a0fdc18f462a38663d6a7012c4548e2d09b13478b8ff8973d848d71c1
MD5 e0724fa8dd6224b89322ab9a10526c94
BLAKE2b-256 b6b56fc470f1f12fae73eddaae86b0d7ce0c93947a40bc034aae32f9a19201b3

See more details on using hashes here.

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

Hashes for relapse-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 12d41d3a99f2adea0cdac172e2106d8e225e472b803ec428ec5704e8805305f6
MD5 c2eb3a541d8045ccbd37933d0d4b5f9f
BLAKE2b-256 e93d96f4f55bd9f7ff73524c04e2d6068426103bb753e73437095e50f390a5be

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page