beginner5 min· python · siftq · anthropic

RAG with Claude in 30 lines

Web-grounded Q&A using SiftQ retrieval + Claude Sonnet, with inline citations.

Prerequisites

  • $pip install anthropic requests
  • $export SIFTQ_API_KEY=mk-...
  • $export ANTHROPIC_API_KEY=sk-ant-...

How it works

The canonical RAG loop. Pull ranked passages from SiftQ, paste them into Claude's context window with strict instructions to cite numerically, and surface a verifiable answer. This pattern is the entire reason most products move from raw LLM calls to retrieval-augmented generation: the model stops hallucinating because every claim has to point at a source URL.

The recipe

rag_with_claude.py
import os
import requests
import anthropic

claude = anthropic.Anthropic()
SIFTQ_KEY = os.environ["SIFTQ_API_KEY"]


def search(q: str, n: int = 8) -> list[dict]:
    r = requests.post(
        "https://api.siftq.com/v1/search",
        headers={"Authorization": f"Bearer {SIFTQ_KEY}"},
        json={"q": q, "scope": "webpage", "size": str(n), "conciseSnippet": True},
        timeout=20,
    )
    r.raise_for_status()
    return r.json().get("webpages", [])


def answer(question: str) -> tuple[str, list[str]]:
    """Returns (answer text with [N] citations, list of source URLs)."""
    hits = search(question)
    if not hits:
        return "No sources found.", []

    sources = "\n\n".join(
        f"[{i + 1}] {h['title']}\n{h['snippet']}\nURL: {h['link']}"
        for i, h in enumerate(hits)
    )

    msg = claude.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        system=(
            "Answer using only the sources provided. "
            "Cite as [N] inline next to every factual claim. "
            "If the sources do not contain the answer, say so."
        ),
        messages=[{
            "role": "user",
            "content": f"SOURCES:\n{sources}\n\nQUESTION: {question}",
        }],
    )
    return msg.content[0].text, [h["link"] for h in hits]


if __name__ == "__main__":
    text, urls = answer("What did Anthropic ship in the past month?")
    print(text)
    print("\nSources:")
    for i, u in enumerate(urls, start=1):
        print(f"  [{i}] {u}")

Variations

Multi-scope retrieval

Call SiftQ twice — once with scope=webpage, once with scope=scholar — and concatenate. Useful for technical questions that span news + academic.

Add a reranker

Pass SiftQ's top 20 through Cohere Rerank or Jina Reranker before sending to Claude. Costs more, lifts precision 5-15% on hard queries.

Streaming responses

Replace messages.create with messages.stream and yield tokens as they arrive. Sub-second first-token under SiftQ + Claude Sonnet Fast.

Keep cooking

RAG with the OpenAI Responses API
Same recipe, different model — drop-in OpenAI grounding.
SiftQ as a LangChain agent tool
Wrap SiftQ in @tool, hand it to a ReAct agent, watch it search autonomously.
Test this query in the SiftQ API playground →