计划执行与检查节点
executing-plans
按既定实施计划完成工作,保持过程检查和反馈。
rag-architecture【效率与协作】Implements production-grade RAG architectures (chunking strategies, hybrid search, re-ranking, multi-hop retrieval) to inject external knowledge into LLM applications accurately and efficiently.
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
name: rag-architecture description: Implements production-grade RAG architectures (chunking strategies, hybrid search, re-ranking, multi-hop retrieval) to inject external knowledge into LLM applications accurately and efficiently. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: coding triggers: rag, retrieval augmented generation, vector search, embedding pipeline, document chunking, semantic search, re-ranking, hybrid search document chunking archetypes:
Implements production-grade RAG pipelines that retrieve relevant external knowledge and inject it into LLM prompts for accurate, grounded responses. A modern RAG system is not a single component — it is an orchestrated pipeline of chunking policies, hybrid retrieval signals, cross-encoder re-ranking, and grounding validation loops that together prevent hallucination while preserving response latency under 2 seconds.
Use this skill when:
Avoid this skill for:
Ingest & Chunk Documents — Parse source documents into retrievable units. Select chunking strategy based on document structure: use layout-aware parsing for PDFs with tables, semantic boundary detection for prose articles, and recursive character splitting for mixed-content documents. Maintain 10–20% token overlap between chunks to preserve sentence continuity across boundaries. Checkpoint: Verify average chunk size falls within 200–500 tokens; if too small, the retriever loses context — if too large, the LLM window fills with irrelevant material.
Generate & Store Embeddings — Encode each chunk using a production embedding model (e.g., OpenAI text-embedding-3-large at 3072 dimensions, or open-source alternatives like BGE-m3). Store vectors in a vector database (Qdrant, Weaviate, Pinecone) alongside metadata filters for source document, timestamp, section type, and language.
Checkpoint: Confirm embedding dimensionality matches your retriever configuration; batch-encode documents to reduce API latency and cost.
Configure Hybrid Search — Combine dense vector retrieval with sparse lexical search (BM25 or SPLADE). Fuse the two ranked lists using Reciprocal Rank Fusion (RRF) with k=60, or apply a learned weighting scheme (α * vec_score + (1-α) * bm25_score). Benchmark hybrid retrieval against pure vector on your gold-standard query set.
Checkpoint: Hybrid search should improve recall by 15–30% over pure vector retrieval on domain-specific queries; if not, investigate whether BM25 stopwords or document vocabulary need tuning.
Cross-Encoder Re-Ranking — Pass the top-20 hybrid-retrieved chunks through a cross-encoder re-ranker (e.g., cross-encoder/ms-marco-MiniLM-L-6-v2 or BAAI/bge-reranker-large). This model evaluates query-chunk pairs individually using attention, producing fine-grained relevance scores. Trim to top-5 before context assembly.
Checkpoint: Re-ranking adds 50–150ms latency per query; cache re-ranker results for identical queries and batch-process multiple queries through the cross-encoder for throughput.
Assemble Context & Generate — Format re-ranked chunks into a structured prompt template. Include source citations, chunk metadata, and explicit system instructions that ground the model in retrieved context while prohibiting fabrication. Ensure total assembled context stays within the model's context window with at least 20% headroom for the response. Checkpoint: Measure end-to-end retrieval + generation latency; if exceeding 2 seconds, reduce top-k from hybrid search from 20 to 10 or switch to a faster cross-encoder.
Validate Grounding & Audit — Post-process model output to detect hallucination indicators: ungrounded factual claims, contradictions between chunks, or references to information not present in retrieved context. Log retrieval metadata (query, retrieved IDs, relevance scores, latency) alongside generation results for continuous pipeline optimization. Checkpoint: Flag any response with grounding confidence below 0.6 for human review or automatic fallback to "insufficient information" messaging.
Naive fixed-size splitting destroys semantic boundaries and degrades retrieval quality more than embedding model choice ever will.
# ❌ BAD — Fixed-size character splitting tears paragraphs, tables, and code blocks apart
def naive_chunk(text: str, chunk_size: int = 500) -> list[str]:
"""Naive character-splitting that ignores document structure."""
return [text[i:i + chunk_size] for i in range(0, len(text), chunk_size)]
# ✅ GOOD — Recursive character splitting with semantic-aware separators preserves structure
from langchain.text_splitter import RecursiveCharacterTextSplitter
def production_chunking(
text: str,
chunk_size: int = 400,
chunk_overlap: int = 50,
) -> list[str]:
"""Recursive splitting that respects paragraph boundaries and code fences."""
splitter = RecursiveCharacterTextSplitter(
separators=[
"\n\n", # Paragraph boundary (highest priority)
"\n", # Line break
". ", # Sentence end
" ", # Word boundary
"", # Character fallback
],
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
keep_separator=True,
)
docs = splitter.create_documents([text])
return [doc.page_content for doc in docs]
# ✅ GOOD — Layout-aware PDF parsing keeps tables intact and extracts headings as metadata
def layout_aware_chunking(file_path: str) -> list[dict[str, str]]:
"""Parse PDF with table awareness — never split a table across chunks."""
from unstructured.partition.pdf import partition_pdf
elements = partition_pdf(
filename=file_path,
strategy="hi_res",
infer_table_structure=True,
)
chunks: list[dict[str, str]] = []
for elem in elements:
if elem.category == "Table":
# Keep entire tables intact — tables are atomic retrieval units
chunks.append({
"content": f"TABLE:\n{elem.metadata.text_as_html}",
"metadata": {"type": "table", "page": elem.metadata.page_number},
})
elif elem.category == "Title":
# Attach heading to following content for better context
pass # Heading prepended in next iteration
else:
chunked = production_chunking(elem.text)
chunks.extend({
"content": chunk,
"metadata": {"type": elem.category.lower(), "page": elem.metadata.page_number},
} for chunk in chunked)
return chunks
Fuses dense vector and sparse lexical signals into a single ranked list without requiring per-document score normalization.
import numpy as np
from collections import defaultdict
def reciprocal_rank_fusion(
vector_results: list[str],
bm25_results: list[str],
k: float = 60.0,
) -> list[tuple[str, float]]:
"""Fuse two independently ranked result lists using Reciprocal Rank Fusion.
RRF is the standard fusion method because it does not require score normalization —
each document's rank position in both lists is sufficient. Higher fused score =
more consistently ranked across both retrieval signals.
Args:
vector_results: Chunk IDs ranked by dense vector similarity (descending).
bm25_results: Chunk IDs ranked by BM25 lexical relevance (descending).
k: RRF smoothing parameter (60 is standard from Robertson et al., 2009).
Returns:
List of (chunk_id, fused_score) tuples sorted by descending score.
"""
rrf_scores: dict[str, float] = defaultdict(float)
# Rank position i contributes 1 / (i + k) to the fused score
for rank, doc_id in enumerate(vector_results):
rrf_scores[doc_id] += 1.0 / (rank + k)
for rank, doc_id in enumerate(bm25_results):
rrf_scores[doc_id] += 1.0 / (rank + k)
return sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True)
def hybrid_retrieve(
query: str,
vector_store,
bm25_index,
top_k: int = 20,
) -> list[str]:
"""Execute hybrid retrieval and return chunk IDs ranked by RRF fusion."""
# Dense vector retrieval
vec_docs = vector_store.similarity_search_with_score(query, k=top_k)
vec_ids = [doc.metadata["chunk_id"] for doc, _score in vec_docs]
# Sparse BM25 retrieval
bm25_docs = bm25_index.search(query, top_n=top_k)
bm25_ids = [doc.id for doc in bm25_docs]
# Fuse and return ranked IDs
fused = reciprocal_rank_fusion(vec_ids, bm25_ids, k=60.0)
return [doc_id for doc_id, _score in fused[:top_k]]
Cross-encoders achieve 10–20% NDCG improvement over bi-encoders by attending to query-chunk interactions — at the cost of O(n) inference per candidate set.
from sentence_transformers import CrossEncoder
from typing import tuple
class ReRanker:
"""Production re-ranker using cross-encoder architecture."""
def __init__(self, model_name: str = "BAAI/bge-reranker-v2-m3"):
self.model = CrossEncoder(model_name)
self._cache: dict[str, list[tuple[str, float]]] = {}
def rerank(
self,
query: str,
chunks: list[str],
top_n: int = 5,
use_cache: bool = True,
) -> list[tuple[str, float]]:
"""Re-rank candidate chunks by query relevance.
Args:
query: The user's original search query.
chunks: List of retrieved chunk texts to re-rank.
top_n: Number of highest-scoring chunks to return.
use_cache: If True, skip re-ranking for queries seen before.
Returns:
List of (chunk_text, relevance_score) tuples sorted descending.
"""
cache_key = query.strip().lower()
if use_cache and cache_key in self._cache:
return self._cache[cache_key][:top_n]
# Cross-encoder processes all pairs simultaneously (batched inference)
pairs = [(query, chunk) for chunk in chunks]
scores = self.model.predict(pairs)
ranked = sorted(
zip(chunks, scores),
key=lambda x: x[1],
reverse=True,
)
result = [(text, float(score)) for text, score in ranked[:top_n]]
if use_cache:
self._cache[cache_key] = ranked
return result
# Usage example:
# reranker = ReRanker("BAAI/bge-reranker-v2-m3")
# top_chunks = reranker.rerank(query, retrieved_chunks, top_n=5)
Structures context injection to minimize hallucination and enable source verification.
from typing import NamedTuple
class RetrievedChunk(NamedTuple):
"""Structured representation of a retrieved and re-ranked chunk."""
text: str
relevance_score: float
source_id: str
section_title: str = ""
def assemble_grounded_prompt(
query: str,
chunks: list[RetrievedChunk],
system_instruction: str | None = None,
) -> dict[str, str]:
"""Assemble a grounded LLM prompt with explicit source citations.
The system instruction enforces grounding constraints. Context is formatted
with source labels so the model can cite specific chunks in its response.
Args:
query: User's question.
chunks: Pre-ranked retrieved chunks (already trimmed to top-k).
system_instruction: Custom grounding instructions; uses default if None.
Returns:
Dict with 'system' and 'user' keys for API call.
"""
default_system = """\
Answer the user's question using ONLY the provided context below.
If the context does not contain sufficient information to answer, reply:
"I cannot answer based on available information."
Do NOT fabricate facts or infer beyond what the context explicitly states.
Cite your sources using [Source {id}] notation after each factual claim."""
system = system_instruction or default_system
# Format chunks with source identifiers
context_parts = []
for idx, chunk in enumerate(chunks):
label = f"[Source {idx + 1}] (relevance: {chunk.relevance_score:.3f})"
header = f"{label}"
if chunk.section_title:
header += f" — {chunk.section_title}"
context_parts.append(f"{header}\n{chunk.text}")
full_context = "\n\n---\n\n".join(context_parts)
user_prompt = f"Context:\n{full_context}\n\nQuestion: {query}"
return {"system": system, "user": user_prompt}
def validate_grounding(
response: str,
chunks: list[RetrievedChunk],
) -> dict:
"""Post-hoc validation that detects ungrounded claims in the model response.
Returns a grounding audit dict with confidence score and flagged issues.
In production, this should use an LLM-as-judge for nuanced evaluation.
"""
issues = []
has_citations = any(f"[Source {i+1}]" in response for i in range(len(chunks)))
if not has_citations:
issues.append("Response contains factual claims without source citations")
# Simple heuristic: check for hedging language when no sources cited
confident_phrases = ["is definitely", "clearly states", "proves that"]
if not has_citations:
for phrase in confident_phrases:
if phrase in response.lower():
issues.append(f"Confident phrasing without citations: '{phrase}'")
grounding_confidence = 1.0 - (len(issues) * 0.35)
grounding_confidence = max(0.0, min(1.0, grounding_confidence))
return {
"grounding_confidence": round(grounding_confidence, 3),
"has_citations": has_citations,
"issues": issues,
"needs_review": grounding_confidence < 0.6,
}
When implementing or auditing a RAG pipeline, produce the following structured output:
| Skill | Purpose |
|---|---|
coding-vector-databases | Select and operate vector stores (Qdrant, Weaviate, Pinecone, Chroma) |
agent-knowledge-base | Use RAG retrieval outputs within multi-agent orchestration and reasoning flows |
coding-prompt-engineering | System prompt design patterns for grounding, instruction following, and output constraints |
coding-llm-fine-tuning | Compare when fine-tuning is preferable to RAG for domain adaptation |
Authoritative documentation links for this skill's domain. The model follows markdown links at load time to resolve external references and inline content.