Skip to main content

Attribution tooling for LLM responses.

Project description

larpie

test

The problem

LLM traces show what happened: the prompt that went in, the response that came out. They don't show which specific context segments, retrieved chunks, tool outputs, memory, earlier turns in the conversation, actually caused that response. When a RAG or agent pipeline gives a wrong or hallucinated answer, debugging it today usually means reading the whole trace by hand and guessing.

What larpie is

larpie is a developer tooling layer built around existing context-attribution research. It implements two attribution algorithms, at different points on that spectrum:

  • SimilarityAlgorithm is a cheap proxy: it regenerates a response for each ablated context and compares embeddings, so it works against any hosted chat API (OpenAI, Anthropic, Ollama, via litellm) with no logprob access needed.
  • LikelihoodAlgorithm implements the actual mechanism ContextCite: Attributing Model Generation to Context (Cohen-Wang, Shah, Georgiev, Madry; NeurIPS 2024) describes: teacher-forced scoring of the original response's log-likelihood under each ablated context, fit with the same LASSO-plus-bootstrap approach. This needs an engine with direct model access - currently a local Hugging Face model via HFEngine - since hosted chat APIs don't expose that.

Both use the same ablate-and-regress idea as AttriBoT: A Bag of Tricks for Efficiently Approximating Leave-One-Out Context Attribution (Liu, Kandpal, Raffel; ICLR 2025) too, though larpie doesn't implement AttriBoT's efficiency tricks (proxy models, hierarchical attribution, cached-activation reuse) - ablation here is the straightforward exhaustive-or-sampled approach, not the faster approximation.

What larpie adds beyond either paper is the developer-tool layer around them: real integration with LLM providers (via litellm, across OpenAI, Anthropic, and Ollama, plus a local Hugging Face model for likelihood attribution), segmentation that understands RAG metadata (which parts of a conversation are retrieved chunks, tool outputs, or plain turns), a capability-based engine architecture that fails loudly instead of silently producing a wrong result when an engine can't support an algorithm, a CLI workflow for inspecting logged calls, and SQLite-backed caching so re-inspecting the same call is free.

Quickstart

pip install larpie

This requires a local Ollama server with a model pulled (ollama pull llama3), or any OpenAI/Anthropic model string if you'd rather pay for API calls.

Wrap a real litellm.completion call so it gets logged, then inspect it:

from litellm import completion
from larpie.middleware import with_logging

logged_completion = with_logging(completion)

response = logged_completion(
    model="ollama/llama3",
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "The Eiffel Tower was completed in 1889. When was it finished?"},
    ],
)

# with_logging logs the call and stamps the id of the row it just wrote
# onto the response:
call_id = response.larpie_call_id
print(call_id)
larpie inspect <call_id>

inspect looks up the logged call, segments its messages, re-runs the response with pieces of the context ablated, and prints a ranked table of which part of the context actually drove the answer. --model defaults to the model the call was logged with, so it isn't needed here; pass --yes/-y to skip the paid-call confirmation prompt for non-Ollama models (Ollama runs locally and never asks).

See it in action

examples/rag_attribution_demo.py runs a small RAG conversation (one supporting chunk, two distractors, and a fabricated fact the model can't already know from pretraining) against a real local Ollama model and prints the ranked attribution table. Run it yourself:

python examples/rag_attribution_demo.py

This is real output from an actual run (ollama/qwen2.5:3b-instruct-q4_K_M, temperature 0), not illustrative numbers:

Closed-book response (no context, sanity check):
  'I need more specific context to answer this question accurately. The provided context does not contain information about the Aldemoor Lantern Festival or its founding year.'
  Good: no '1997' here - the model can't answer this without the supporting chunk.

Full-context response:
  'The Aldemoor Lantern Festival began in 1997.'

Ranked attribution results (query segment excluded):
  1. [seg_1] (rag_chunk) score=0.1789 ci=(0.1277834502965072, 0.21895693424139875) text="The Aldemoor Lantern Festival began in 1997, founded by local artist Mira Thorne to celebrate the town's bicentennial."
  2. [seg_2] (rag_chunk) score=0.0014 ci=(0.0, 0.025790114892243907) text='Mira Thorne is a sculptor known for her large-scale kinetic light installations across Aldemoor.'
  3. [seg_0] (system) score=-0.0025 ci=(-0.027141633458489772, 0.0) text="You are a helpful assistant. Answer the user's question using only the provided context snippets, as briefly as possible."
  4. [seg_3] (rag_chunk) score=-0.0059 ci=(-0.042601720261782446, -0.0) text='The neighboring Bellhaven Torch Parade started in 1985 and features similar lantern displays.'

The closed-book check confirms the model can't answer from pretraining alone. Once the supporting chunk is added back, the model answers correctly, and attribution correctly ranks that exact chunk first, with a confidence interval sitting entirely above zero. Every other segment, including a distractor that names a different year for a different event, scores near zero with a confidence interval that includes zero.

Architecture

larpie sits between your LLM application and the model provider as a thin logging and analysis layer:

User's LLM application
  -> larpie middleware (logs the call to SQLite)
  -> Segmenter (splits context into turns, RAG chunks, tool outputs; dedups near-identical chunks)
  -> Attribution algorithm (declares which engine capability it needs)
  -> Engine capability layer (checks what the model/runtime can actually do)
  -> LLM provider (via litellm: OpenAI, Anthropic, Ollama)

Engine is how larpie talks to a specific model or runtime. An engine can implement generate (produce text), embed (produce a vector), and score_continuation (return the log-likelihood of a fixed continuation given a context), but not every engine implements all three, and larpie checks for the ones it actually needs at runtime instead of assuming.

Algorithm is how attribution gets computed from whatever capabilities an engine offers. SimilarityAlgorithm needs generate and embed. LikelihoodAlgorithm, implementing the ContextCite mechanism, needs score_continuation instead: it compares the model's actual log-likelihood of the original response with and without each piece of context, rather than regenerating a response and comparing embeddings.

Capability matrix

Not every engine can run every attribution algorithm. algorithms.base.check_capability enforces this at runtime: a missing capability raises a clear error instead of silently falling back to something else.

Engine generate score_continuation embed Compatible algorithm(s)
LiteLLMEngine (OpenAI / Anthropic / Ollama, via litellm) yes no yes (local sentence-transformers) SimilarityAlgorithm
HFEngine (local Hugging Face transformers model) yes yes no LikelihoodAlgorithm
vLLM / SGLang (planned) yes yes n/a LikelihoodAlgorithm (planned support for larger models than a local transformers instance can comfortably run)

Likelihood attribution

LikelihoodAlgorithm measures the actual log-likelihood of the original response under each ablated context via teacher-forced scoring, not a regenerate-and-compare-embeddings proxy like SimilarityAlgorithm. Run it with:

larpie inspect <call_id> --algorithm likelihood --hf-model Qwen/Qwen2.5-0.5B-Instruct

--hf-model defaults to Qwen/Qwen2.5-0.5B-Instruct if omitted. It runs fully local: no Ollama server, no API key, no network access beyond the one-time model download from Hugging Face. A small model like that default runs fine on CPU; anything meaningfully larger needs real GPU memory to be usable in practice.

Real output from an actual run, on a small RAG conversation (a system prompt, one supporting chunk, one distractor) using the same fabricated "Aldemoor Lantern Festival" fact as the similarity example above:

This will make 8 engine.score_continuation() call(s): 7 ablation(s) + 1 baseline call.
[QUERY - not ranked] [seg_3] (user_turn) 'In what year did the Aldemoor Lantern Festival begin?'
segment    type               score  ci                         text
seg_1      rag_chunk         5.6799  (3.155, 7.061)             'The Aldemoor Lantern Festival began in 1997, founded by Mira Thorne.'
seg_2      rag_chunk         1.8188  (-0.016, 3.557)            'The neighboring Bellhaven Torch Parade started in 1985.'
seg_0      system           -2.2877  (-3.777, -0.134)           'Answer using only the provided context.'

Note: no noise-floor check for likelihood attribution; see README.

The supporting chunk (seg_1) scores clearly highest, with a confidence interval sitting entirely above zero - the same "CI excludes zero means a reliably detected effect" reading used everywhere else in larpie. Unlike the similarity table, there's no NOISE column here; see Limitations for why.

Limitations

  • Similarity attribution is a proxy. It compares the embedding of a regenerated response to the embedding of the original response; it does not measure the model's actual probability of the response (true likelihood attribution). Two different pieces of text can embed similarly even when the model's confidence in them differs a lot.
  • Likelihood attribution needs an engine that can compute score_continuation directly. That exists today for a local Hugging Face transformers model (HFEngine + LikelihoodAlgorithm, see above); vLLM and SGLang support doesn't exist yet and is still planned, mainly for models too large to run comfortably as a local transformers instance.
  • There is no noise-floor check for likelihood attribution, unlike similarity attribution. That check works by calling generate() against the same unablated context several times and measuring how much the sampled output varies; score_continuation is one deterministic forward pass over fixed input with no sampling step, so repeating it would just return the same number every time - there is nothing to measure. The residual risk: on a backend whose forward pass isn't actually bit-reproducible (see the point below about local quantized models), that non-determinism could still produce spurious variation in scores that nothing here catches.
  • If the model already knows a fact from its own pretraining, a retrieved chunk supporting that fact will correctly score near zero: removing it doesn't change the answer, because the model didn't need it to answer correctly. This is expected behavior, not a bug, but it means a low or zero score for a chunk doesn't mean the chunk was irrelevant to the topic, only that it wasn't necessary for this particular answer.
  • larpie is built for complex contexts: 10+ segments, RAG chunks mixed with agent tool call histories and long-running memory, where reading the raw trace by hand doesn't scale. It is not meant as a general debugger for small or simple prompts; if your context is two or three chunks, just read them.
  • Local quantized models are not guaranteed to be bit-reproducible even at temperature 0, since floating-point reduction order in batched or threaded inference isn't always deterministic on those backends. Caching gives you exact reproducibility for repeated inspection of the same logged call, since cached ablation results get reused instead of regenerated, but it does not make a fresh run against a live model deterministic on its own.
  • The dedup threshold (0.9 cosine similarity) can merge near-duplicate templated text, e.g. numbered filler turns differing only by a label ("follow-up question number 1" vs. "number 2"). This isn't just theoretical: it happened during scale testing and had to be fixed by making the filler text genuinely distinct, not just relabeled.
  • Likelihood attribution's real forward passes (HFEngine.score_continuation) must fit the full ablated context plus the original response within the local model's context window, for every ablation. This hasn't been tested at high segment counts - it's an open question, not a solved one.

License

MIT. See LICENSE.

Development

pip install -e ".[dev]"
pytest

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

larpie-0.1.0.tar.gz (51.8 kB view details)

Uploaded Source

Built Distribution

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

larpie-0.1.0-py3-none-any.whl (35.0 kB view details)

Uploaded Python 3

File details

Details for the file larpie-0.1.0.tar.gz.

File metadata

  • Download URL: larpie-0.1.0.tar.gz
  • Upload date:
  • Size: 51.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.9

File hashes

Hashes for larpie-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f0c3c403bb24c97b503c0f69ea9af3ceb9ccd260067da00e1475ec3b79c576c7
MD5 d3a4c50a084de6f5c5e481f8dd6fb377
BLAKE2b-256 d7c8ef6353ce803a84c347cb7e29694e510d4ab85e78cefd13fae23ae20e63b6

See more details on using hashes here.

File details

Details for the file larpie-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: larpie-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 35.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.9

File hashes

Hashes for larpie-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 35584fb303b952d600b82fa4247ece1b79b164dbe58dd8b1f61c1edd3441718b
MD5 bd3601924fe91a3ce003de88f8bf9bea
BLAKE2b-256 df34b814e8a37ad60db55d1d25f3c7800d2fb982700d16de4adc39e55ffd7d88

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