Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Build Retrieval-Augmented Generation (RAG) systems for LLM applications with vector databases and semantic search. Use when implementing knowledge-grounded AI, building document Q&A systems, or integrating LLMs with external knowledge bases.
.claude/skills/dicklesworthstone-rag-implementation/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-16 | ✗→✓ | ▲ Improved | 178% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 95% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 203% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 130% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 295% | 0% |
Master Retrieval-Augmented Generation (RAG) to build LLM applications that provide accurate, grounded responses using external knowledge sources.
Purpose: Store and retrieve document embeddings efficiently
Options:
Purpose: Convert text to numerical vectors for similarity search
Models (2026): | Model | Dimensions | Best For | |-------|------------|----------| | voyage-3-large | 1024 | Claude apps (Anthropic recommended) | | voyage-code-3 | 1024 | Code search | | text-embedding-3-large | 3072 | OpenAI apps, high accuracy | | text-embedding-3-small | 1536 | OpenAI apps, cost-effective | | bge-large-en-v1.5 | 1024 | Open source, local deployment | | multilingual-e5-large | 1024 | Multi-language support |
Approaches:
Purpose: Improve retrieval quality by reordering results
Methods:
pythonfrom langgraph.graph import StateGraph, START, END from langchain_anthropic import ChatAnthropic from langchain_voyageai import VoyageAIEmbeddings from langchain_pinecone import PineconeVectorStore from langchain_core.documents import Document from langchain_core.prompts import ChatPromptTemplate from langchain_text_splitters import RecursiveCharacterTextSplitter from typing import TypedDict, Annotated class RAGState(TypedDict): question: str context: list[Document] answer: str # Initialize components llm = ChatAnthropic(model="claude-sonnet-4-5") embeddings = VoyageAIEmbeddings(model="voyage-3-large") vectorstore = PineconeVectorStore(index_name="docs", embedding=embeddings) retriever = vectorstore.as_retriever(search_kwargs={"k": 4}) # RAG prompt rag_prompt = ChatPromptTemplate.from_template( """Answer based on the context below. If you cannot answer, say so. Context: {context} Question: {question} Answer:""" ) async def retrieve(state: RAGState) -> RAGState: """Retrieve relevant documents.""" docs = await retriever.ainvoke(state["question"]) return {"context": docs} async def generate(state: RAGState) -> RAGState: """Generate answer from context.""" context_text = "\n\n".join(doc.page_content for doc in state["context"]) messages = rag_prompt.format_messages( context=context_text, question=state["question"] ) response = await llm.ainvoke(messages) return {"answer": response.content} # Build RAG graph builder = StateGraph(RAGState) builder.add_node("retrieve", retrieve) builder.add_node("generate", generate) builder.add_edge(START, "retrieve") builder.add_edge("retrieve", "generate") builder.add_edge("generate", END) rag_chain = builder.compile() # Use result = await rag_chain.ainvoke({"question": "What are the main features?"}) print(result["answer"])
pythonfrom langchain_community.retrievers import BM25Retriever from langchain.retrievers import EnsembleRetriever # Sparse retriever (BM25 for keyword matching) bm25_retriever = BM25Retriever.from_documents(documents) bm25_retriever.k = 10 # Dense retriever (embeddings for semantic search) dense_retriever = vectorstore.as_retriever(search_kwargs={"k": 10}) # Combine with Reciprocal Rank Fusion weights ensemble_retriever = EnsembleRetriever( retrievers=[bm25_retriever, dense_retriever], weights=[0.3, 0.7] # 30% keyword, 70% semantic )
pythonfrom langchain.retrievers.multi_query import MultiQueryRetriever # Generate multiple query perspectives for better recall multi_query_retriever = MultiQueryRetriever.from_llm( retriever=vectorstore.as_retriever(search_kwargs={"k": 5}), llm=llm ) # Single query → multiple variations → combined results results = await multi_query_retriever.ainvoke("What is the main topic?")
pythonfrom langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import LLMChainExtractor # Compressor extracts only relevant portions compressor = LLMChainExtractor.from_llm(llm) compression_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=vectorstore.as_retriever(search_kwargs={"k": 10}) ) # Returns only relevant parts of documents compressed_docs = await compression_retriever.ainvoke("specific query")
pythonfrom langchain.retrievers import ParentDocumentRetriever from langchain.storage import InMemoryStore from langchain_text_splitters import RecursiveCharacterTextSplitter # Small chunks for precise retrieval, large chunks for context child_splitter = RecursiveCharacterTextSplitter(chunk_size=400, chunk_overlap=50) parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000, chunk_overlap=200) # Store for parent documents docstore = InMemoryStore() parent_retriever = ParentDocumentRetriever( vectorstore=vectorstore, docstore=docstore, child_splitter=child_splitter, parent_splitter=parent_splitter ) # Add documents (splits children, stores parents) await parent_retriever.aadd_documents(documents) # Retrieval returns parent documents with full context results = await parent_retriever.ainvoke("query")
pythonfrom langchain_core.prompts import ChatPromptTemplate class HyDEState(TypedDict): question: str hypothetical_doc: str context: list[Document] answer: str hyde_prompt = ChatPromptTemplate.from_template( """Write a detailed passage that would answer this question: Question: {question} Passage:""" ) async def generate_hypothetical(state: HyDEState) -> HyDEState: """Generate hypothetical document for better retrieval.""" messages = hyde_prompt.format_messages(question=state["question"]) response = await llm.ainvoke(messages) return {"hypothetical_doc": response.content} async def retrieve_with_hyde(state: HyDEState) -> HyDEState: """Retrieve using hypothetical document.""" # Use hypothetical doc for retrieval instead of original query docs = await retriever.ainvoke(state["hypothetical_doc"]) return {"context": docs} # Build HyDE RAG graph builder = StateGraph(HyDEState) builder.add_node("hypothetical", generate_hypothetical) builder.add_node("retrieve", retrieve_with_hyde) builder.add_node("generate", generate) builder.add_edge(START, "hypothetical") builder.add_edge("hypothetical", "retrieve") builder.add_edge("retrieve", "generate") builder.add_edge("generate", END) hyde_rag = builder.compile()
pythonfrom langchain_text_splitters import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, length_function=len, separators=["\n\n", "\n", ". ", " ", ""] # Try in order ) chunks = splitter.split_documents(documents)
pythonfrom langchain_text_splitters import TokenTextSplitter splitter = TokenTextSplitter( chunk_size=512, chunk_overlap=50, encoding_name="cl100k_base" # OpenAI tiktoken encoding )
pythonfrom langchain_experimental.text_splitter import SemanticChunker splitter = SemanticChunker( embeddings=embeddings, breakpoint_threshold_type="percentile", breakpoint_threshold_amount=95 )
pythonfrom langchain_text_splitters import MarkdownHeaderTextSplitter headers_to_split_on = [ ("#", "Header 1"), ("##", "Header 2"), ("###", "Header 3"), ] splitter = MarkdownHeaderTextSplitter( headers_to_split_on=headers_to_split_on, strip_headers=False )
pythonfrom pinecone import Pinecone, ServerlessSpec from langchain_pinecone import PineconeVectorStore # Initialize Pinecone client pc = Pinecone(api_key=os.environ["PINECONE_API_KEY"]) # Create index if needed if "my-index" not in pc.list_indexes().names(): pc.create_index( name="my-index", dimension=1024, # voyage-3-large dimensions metric="cosine", spec=ServerlessSpec(cloud="aws", region="us-east-1") ) # Create vector store index = pc.Index("my-index") vectorstore = PineconeVectorStore(index=index, embedding=embeddings)
pythonimport weaviate from langchain_weaviate import WeaviateVectorStore client = weaviate.connect_to_local() # or connect_to_weaviate_cloud() vectorstore = WeaviateVectorStore( client=client, index_name="Documents", text_key="content", embedding=embeddings )
pythonfrom langchain_chroma import Chroma vectorstore = Chroma( collection_name="my_collection", embedding_function=embeddings, persist_directory="./chroma_db" )
pythonfrom langchain_postgres.vectorstores import PGVector connection_string = "postgresql+psycopg://user:pass@localhost:5432/vectordb" vectorstore = PGVector( embeddings=embeddings, collection_name="documents", connection=connection_string, )
pythonfrom langchain_core.documents import Document # Add metadata during indexing docs_with_metadata = [] for doc in documents: doc.metadata.update({ "source": doc.metadata.get("source", "unknown"), "category": determine_category(doc.page_content), "date": datetime.now().isoformat() }) docs_with_metadata.append(doc) # Filter during retrieval results = await vectorstore.asimilarity_search( "query", filter={"category": "technical"}, k=5 )
python# Balance relevance with diversity results = await vectorstore.amax_marginal_relevance_search( "query", k=5, fetch_k=20, # Fetch 20, return top 5 diverse lambda_mult=0.5 # 0=max diversity, 1=max relevance )
pythonfrom sentence_transformers import CrossEncoder reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2') async def retrieve_and_rerank(query: str, k: int = 5) -> list[Document]: # Get initial results candidates = await vectorstore.asimilarity_search(query, k=20) # Rerank pairs = [[query, doc.page_content] for doc in candidates] scores = reranker.predict(pairs) # Sort by score and take top k ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True) return [doc for doc, score in ranked[:k]]
pythonfrom langchain.retrievers import CohereRerank from langchain_cohere import CohereRerank reranker = CohereRerank(model="rerank-english-v3.0", top_n=5) # Wrap retriever with reranking reranked_retriever = ContextualCompressionRetriever( base_compressor=reranker, base_retriever=vectorstore.as_retriever(search_kwargs={"k": 20}) )
pythonrag_prompt = ChatPromptTemplate.from_template( """Answer the question based on the context below. Include citations using [1], [2], etc. If you cannot answer based on the context, say "I don't have enough information." Context: {context} Question: {question} Instructions: 1. Use only information from the context 2. Cite sources with [1], [2] format 3. If uncertain, express uncertainty Answer (with citations):""" )
pythonfrom pydantic import BaseModel, Field class RAGResponse(BaseModel): answer: str = Field(description="The answer based on context") confidence: float = Field(description="Confidence score 0-1") sources: list[str] = Field(description="Source document IDs used") reasoning: str = Field(description="Brief reasoning for the answer") # Use with structured output structured_llm = llm.with_structured_output(RAGResponse)
pythonfrom typing import TypedDict class RAGEvalMetrics(TypedDict): retrieval_precision: float # Relevant docs / retrieved docs retrieval_recall: float # Retrieved relevant / total relevant answer_relevance: float # Answer addresses question faithfulness: float # Answer grounded in context context_relevance: float # Context relevant to question async def evaluate_rag_system( rag_chain, test_cases: list[dict] ) -> RAGEvalMetrics: """Evaluate RAG system on test cases.""" metrics = {k: [] for k in RAGEvalMetrics.__annotations__} for test in test_cases: result = await rag_chain.ainvoke({"question": test["question"]}) # Retrieval metrics retrieved_ids = {doc.metadata["id"] for doc in result["context"]} relevant_ids = set(test["relevant_doc_ids"]) precision = len(retrieved_ids & relevant_ids) / len(retrieved_ids) recall = len(retrieved_ids & relevant_ids) / len(relevant_ids) metrics["retrieval_precision"].append(precision) metrics["retrieval_recall"].append(recall) # Use LLM-as-judge for quality metrics quality = await evaluate_answer_quality( question=test["question"], answer=result["answer"], context=result["context"], expected=test.get("expected_answer") ) metrics["answer_relevance"].append(quality["relevance"]) metrics["faithfulness"].append(quality["faithfulness"]) metrics["context_relevance"].append(quality["context_relevance"]) return {k: sum(v) / len(v) for k, v in metrics.items()}
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-16 | fail→pass | 13,560 | 8,170 | -40% | 1 | 1 | 0% | 2,130 | 5,921 | +178% | 0 | 0 | — |
case-05 | pass→pass | 4,629 | 4,175 | -10% | 1 | 1 | 0% | 843 | 5,250 | +523% | 0 | 0 | — |
case-01 | fail→pass | 14,928 | 10,737 | -28% | 1 | 1 | 0% | 2,605 | 5,091 | +95% | 0 | 0 | — |
case-02 | fail→pass | 13,117 | 10,428 | -21% | 1 | 1 | 0% | 2,103 | 6,381 | +203% | 0 | 0 | — |
case-03 | fail→pass | 16,173 | 10,431 | -36% | 1 | 1 | 0% | 2,743 | 6,308 | +130% | 0 | 0 | — |
case-04 | pass→fail | 10,743 | 8,689 | -19% | 1 | 1 | 0% | 1,889 | 6,059 | +221% | 0 | 0 | — |
case-06 | fail→pass | 9,074 | 10,711 | +18% | 1 | 1 | 0% | 1,521 | 6,002 | +295% | 0 | 0 | — |
case-07 | pass→pass | 10,315 | 6,835 | -34% | 1 | 1 | 0% | 1,817 | 5,660 | +212% | 0 | 0 | — |
case-08 | pass→pass | 9,963 | 8,497 | -15% | 1 | 1 | 0% | 1,949 | 6,106 | +213% | 0 | 0 | — |
case-09 | pass→pass | 11,717 | 11,548 | -1% | 1 | 1 | 0% | 2,056 | 6,381 | +210% | 0 | 0 | — |
case-10 | pass→pass | 9,071 | 7,220 | -20% | 1 | 1 | 0% | 1,622 | 5,751 | +255% | 0 | 0 | — |
case-11 | pass→pass | 10,478 | 7,231 | -31% | 1 | 1 | 0% | 1,921 | 5,784 | +201% | 0 | 0 | — |
case-12 | fail→pass | 6,628 | 5,833 | -12% | 1 | 1 | 0% | 1,098 | 5,375 | +390% | 0 | 0 | — |
case-13 | pass→pass | 12,137 | 11,766 | -3% | 1 | 1 | 0% | 1,997 | 6,593 | +230% | 0 | 0 | — |
case-14 | pass→pass | 7,388 | 18,985 | +157% | 1 | 1 | 0% | 1,196 | 5,728 | +379% | 0 | 0 | — |
case-15 | fail→pass | 12,713 | 12,633 | -1% | 1 | 1 | 0% | 2,186 | 6,796 | +211% | 0 | 0 | — |
case-17 | fail→fail | 16,587 | 17,330 | +4% | 1 | 1 | 0% | 2,804 | 8,012 | +186% | 0 | 0 | — |
case-18 | pass→pass | 11,945 | 10,553 | -12% | 1 | 1 | 0% | 2,101 | 6,471 | +208% | 0 | 0 | — |
case-19 | pass→pass | 9,200 | 4,640 | -50% | 1 | 1 | 0% | 1,326 | 5,344 | +303% | 0 | 0 | — |
case-20 | fail→pass | 5,921 | 3,062 | -48% | 1 | 1 | 0% | 859 | 4,980 | +480% | 0 | 0 | — |
case-21 | pass→pass | 6,499 | 6,221 | -4% | 1 | 1 | 0% | 1,078 | 5,313 | +393% | 0 | 0 | — |
case-22 | pass→pass | 12,616 | 10,050 | -20% | 1 | 1 | 0% | 2,234 | 6,283 | +181% | 0 | 0 | — |
case-23 | pass→pass | 17,864 | 16,284 | -9% | 1 | 1 | 0% | 2,873 | 7,931 | +176% | 0 | 0 | — |
case-24 | pass→pass | 19,373 | 17,088 | -12% | 1 | 1 | 0% | 3,725 | 7,998 | +115% | 0 | 0 | — |
case-25 | pass→pass | 13,722 | 17,684 | +29% | 1 | 1 | 0% | 2,646 | 7,897 | +198% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 25 cases were attempted. The headline lift of +28 percentage points is the difference between those two pass rates over the 25 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.