Skip to content
tnsaijava agent framework

RAG Strategy SPI

TnsAI 0.14.1 provides eight retrieval strategies in com.tnsai.intelligence.rag. You can construct them directly for an application-owned pipeline or select the matching mode through @Retrieval. Those entry points share strategy names, but they do not share every default or provider requirement.

TnsAI 0.14.0 (@since 0.14.0, TAN-5326) lets @Retrieval.queryParam and RetrievalConfig.queryParam name the action argument that becomes the query. Blank — the default — still binds the first String in declaration order. Named selection needs -parameters. A class-level name must exist on every action of that Role; otherwise put @Retrieval on each method. Missing names fail before @Retrieval.onFailure. Details: Declarative RAG.

RAGStrategy Interface

Every programmatic strategy implements this contract:

String name();
List<RetrievedDocument> retrieve(String query, RAGContext context);

RetrievedDocument contains content, source, score, and an unmodifiable Map<String, String> metadata view. RAGContext carries maxResults (default 10), minScore (default 0.0), and exact metadata filters. Use new RAGContext() or derive a new context with withMaxResults, withMinScore, and withMetadataFilters. RAGContext defensively copies its filter map. RetrievedDocument exposes an unmodifiable view of the metadata map supplied to its constructor, so copy that input first if its owner may mutate it later.

Programmatic Setup

The three base strategies use the in-memory stores from tnsai-core. A VectorMemoryStore accepts an application-provided EmbeddingFunction; use a production embedding provider whose output has a stable dimension. The small function below is deterministic only so the complete example can run without external services.

import com.tnsai.intelligence.rag.HybridRAGStrategy;
import com.tnsai.intelligence.rag.KeywordRAGStrategy;
import com.tnsai.intelligence.rag.RAGContext;
import com.tnsai.intelligence.rag.RAGPipeline;
import com.tnsai.intelligence.rag.RAGStrategy;
import com.tnsai.intelligence.rag.RetrievedDocument;
import com.tnsai.intelligence.rag.VectorRAGStrategy;
import com.tnsai.memory.advanced.BM25Index;
import com.tnsai.memory.advanced.EmbeddingFunction;
import com.tnsai.memory.advanced.VectorMemoryStore;
import com.tnsai.memory.strategy.MemoryEntry;
import com.tnsai.memory.strategy.MemoryTier;

import java.util.List;
import java.util.Map;

public final class RagStrategyExample {
    private RagStrategyExample() {
    }

    public static void main(String[] args) {
        EmbeddingFunction embedding = text -> new float[] {
            text.contains("agent") ? 1.0f : 0.0f,
            text.contains("memory") ? 1.0f : 0.0f
        };
        VectorMemoryStore vectors = new VectorMemoryStore(embedding);
        BM25Index keywords = new BM25Index();

        MemoryEntry semantic = new MemoryEntry(
            "memory-guide",
            "Agents can persist semantic memory.",
            MemoryTier.SEMANTIC
        );
        MemoryEntry working = new MemoryEntry(
            "working-note",
            "Agents use memory while running.",
            MemoryTier.WORKING
        );
        vectors.add(semantic);
        vectors.add(working);
        keywords.add(semantic);
        keywords.add(working);

        RAGStrategy vector = new VectorRAGStrategy(vectors);
        RAGStrategy keyword = new KeywordRAGStrategy(keywords);
        RAGStrategy hybrid = new HybridRAGStrategy(keywords, vectors);

        RAGContext context = new RAGContext()
            .withMaxResults(5)
            .withMinScore(0.0)
            .withMetadataFilters(Map.of("tier", "SEMANTIC"));
        List<RetrievedDocument> documents =
            hybrid.retrieve("agent memory", context);
        if (documents.isEmpty() || documents.stream().anyMatch(document ->
                !"SEMANTIC".equals(document.metadata().get("tier")))) {
            throw new IllegalStateException("tier filter was not applied");
        }

        RAGPipeline pipeline = RAGPipeline.builder(hybrid)
            .defaultContext(context)
            .queryPreprocessor(String::strip)
            .reranker(results -> results)
            .build();
        String formattedContext = pipeline.execute("agent memory");

        System.out.println(vector.name());
        System.out.println(keyword.name());
        System.out.println(documents.size());
        System.out.println(formattedContext);
    }
}

Metadata Filtering

RAGContext.metadataFilters() uses exact, case-sensitive string equality against RetrievedDocument.metadata(). Every filter entry must match. A missing key excludes the document, so two entries in the filter map are an AND predicate, not alternatives. Null or blank keys and values are rejected when the context is created.

The direct memory-backed VectorRAGStrategy, KeywordRAGStrategy, and HybridRAGStrategy emit two filterable fields for every result:

KeyValue
tierThe exact MemoryTier.name(), such as SEMANTIC or WORKING
entryIdThe exact MemoryEntry.id()

These strategies filter the ranked candidate set before applying maxResults. For example, a context with maxResults = 1 and tier = WORKING can return the highest-ranked WORKING entry even when an unfiltered SEMANTIC entry ranks first. Filtering by a key that these direct strategies do not emit, such as tenant, returns no results.

The unified annotation/declarative path has a wider metadata surface. It attaches source and custom-loader metadata after the base memory strategy returns, applies the same exact-match predicate to that attributed metadata, and only then limits the results to the configured top-K, or to top-N when reranking is enabled. Reserved tnsai.chat.* lifecycle fields are request context rather than document predicates and are removed before this filtering step. This separation lets a custom SourceLoader expose additional filterable fields without claiming that direct programmatic memory strategies emit them.

This behavior landed in framework PR #97 for TAN-5625. The eight-strategy inventory was established separately under TAN-5329.

The Eight Strategies

VectorRAGStrategy

VectorRAGStrategy(VectorMemoryStore) ranks entries by cosine similarity. It is the programmatic semantic option, and its quality depends on the EmbeddingFunction supplied to the store.

KeywordRAGStrategy

KeywordRAGStrategy(BM25Index) performs lexical BM25 retrieval. It needs no embedding provider and works well for identifiers, exact terms, and code-like queries.

TnsAI 0.14.0 also has a package-private write-back overlay that uses BM25 inside ControlledWriteBackStore. That store is default-off, not this public constructor, and does not join named-source or non-KEYWORD requests. See Controlled write-back.

HybridRAGStrategy

HybridRAGStrategy(BM25Index, VectorMemoryStore) combines lexical and vector results with Reciprocal Rank Fusion using the default RRF constant of 60. Alternatively, pass a configured HybridMemoryRetriever to its one-argument constructor.

A third constructor (@since 0.14.0) also accepts a source-fenced GraphRAGStrategy and RRF-fuses that ranking with BM25 and vector. A missing graph store does not collapse HYBRID into vector-only; the two lexical and semantic streams still fuse. Details: Hybrid graph stream.

GraphRAGStrategy

GraphRAGStrategy finds seed nodes in a GraphStore, then follows outgoing edges with bounded depth, score decay, cycle prevention, and an optional source fence. Its constructor accepts:

GraphRAGStrategy(
    String providerName,
    GraphStore store,
    Set<String> allowedSources,
    int maxDepth,
    double depthDecay,
    int maxVisitedNodes
)

Defaults exposed by the class are depth 2, depth decay 0.85, and 1,024 visited nodes. Apply those constants explicitly when constructing the strategy.

MultiQueryRAGStrategy

MultiQueryRAGStrategy asks a QueryExpander for query variations, runs each unique query through one base strategy, and merges the results with reciprocal rank fusion. It requires a non-NONE Retrieval.QueryExpansion mode, an exact provider model name, and a positive expansion limit:

MultiQueryRAGStrategy(
    RAGStrategy baseStrategy,
    QueryExpander expander,
    Retrieval.QueryExpansion mode,
    String model,
    int maxExpansions
)

An overload adds a CancellationToken. QueryExpander implementations own model inference and are discovered through ServiceLoader on the declarative path; TnsAI does not bundle an expansion model.

HierarchicalRAGStrategy

HierarchicalRAGStrategy starts with child hits and expands their parent chain through a validated HierarchyIndex. Its constructor accepts a child strategy, the index, an optional source fence, maximum parent depth, and per-level score decay. The class defaults are depth 3 and decay 0.85.

Documents in the index need stable hierarchy IDs. A child with a parent sets HierarchyMetadata.PARENT_ID to that parent's ID; duplicate IDs, missing parents, and cycles are configuration errors.

TemporalRAGStrategy

TemporalRAGStrategy takes candidates from a child strategy and applies a bounded freshness multiplier using a TemporalIndex. Its constructor accepts the child strategy, index, optional source fence, Clock, half-life, relevance floor, and maximum future clock skew. Defaults are a 30-day half-life, 0.8 relevance floor, and five-minute future skew.

Timestamped documents use TemporalMetadata.TIMESTAMP with an ISO-8601 instant. Undated documents retain the relevance floor; invalid timestamps and excessive future dates fail validation or retrieval instead of being silently accepted.

ReasoningRAGStrategy (TnsAI 0.14.0)

ReasoningRAGStrategy is @since 0.14.0 and ships in Maven Central 0.14.1. It is the opposite walk from HierarchicalRAGStrategy: root-to-leaf descent with an installed TreeNavigator choosing the branch. TnsAI bundles no navigator.

ReasoningRAGStrategy(
    HierarchyIndex index,
    TreeNavigator navigator,
    String model,
    Set<String> allowedSources,
    int maxNodesVisited,
    int maxDepth
)

A three-argument overload (index, navigator, model) uses empty sources and the class defaults: 15 visited nodes and depth 6. See vectorless reasoning retrieval.

The package-private DeduplicatingRagStrategy used by the declarative runtime is a post-retrieval decorator, not an additional 0.14.1 declarative mode.

Programmatic and Declarative Retrieval

Direct construction gives the application ownership of stores, providers, indexes, clocks, and strategy composition. RAGPipeline is a programmatic orchestrator; it is not installed with an AgentBuilder.ragPipeline(...) method. Call the pipeline from application code or a custom action/executor.

The annotation-first path uses @KnowledgeSource plus @Retrieval on an action. The runtime ingests the selected sources and resolves the corresponding strategy at dispatch time. The table below is the immutable Maven Central 0.14.1 surface. The RAG overview shows the action and builder entry points; the declarative RAG tutorial provides the complete action-level walkthrough.

@Retrieval strategyRuntime compositionAdditional requirement
SEMANTICVector retrievalLoaded knowledge sources; 128-slot hash / token overlap unless one EmbeddingFunction is installed
KEYWORDBM25 retrievalLoaded knowledge sources
HYBRIDVector + BM25 with RRF; optional graph streamLoaded knowledge sources; one claiming GraphStoreProvider for the third stream
GRAPHGraph traversalOne matching GraphStoreProvider
MULTI_QUERYQuery expansion + semantic base strategyMatching QueryExpander and model
HIERARCHICALSemantic child hits + parent expansionValid hierarchy metadata
TEMPORALSemantic child hits + freshness rerankingValid temporal metadata
REASONINGRoot-to-leaf tree descentMatching TreeNavigator and navigatorModel@since 0.14.0

TnsAI 0.14.0: reasoning retrieval

Maven Central 0.14.1 adds an eighth declarative mode, REASONING, backed by ReasoningRAGStrategy. It performs root-to-leaf tree descent through one installed TreeNavigator; navigatorModel identifies the provider-owned model. The mode is @since 0.14.0.

TnsAI 0.14.0: declarative embedding provider

Programmatic vector and hybrid retrieval use the EmbeddingFunction supplied directly to VectorMemoryStore. Declarative Role bindings instead discover one process-wide application provider through Java ServiceLoader. Register the implementation class in exactly this service file:

META-INF/services/com.tnsai.memory.advanced.EmbeddingFunction

The file contains the fully qualified name of one public, no-argument EmbeddingFunction implementation. The runtime behavior is deterministic:

  • no installed provider uses the built-in, offline 128-dimensional hash fallback; it primarily measures token overlap
  • exactly one provider is wrapped, cached for the process, and shared by all vector-backed declarative strategies and embedding-based deduplication
  • multiple providers fail binding initialization as a configuration error; classpath order is never used to choose one
  • concurrent initialization or embedding calls fail immediately instead of waiting in a queue, so the application must control call concurrency and the provider must return promptly
  • returned vectors must be non-null, non-empty, at most 16,384 values, finite, and dimensionally stable; the wrapper snapshots the returned buffer before finite-value validation and before returning it

The first shape-valid vector fixes the process provider's dimension even if a later finite-value check rejects that vector. Provider transport failures keep their typed RetrievalException; other provider exceptions are classified as execution failures.

This ServiceLoader path is separate from direct programmatic construction. An application that passes an EmbeddingFunction to VectorMemoryStore owns that specific store and does not use the declarative process-provider selection.

RAGPipeline

RAGPipeline.builder(strategy) composes four phases in this order:

  1. queryPreprocessor(UnaryOperator<String>)
  2. RAGStrategy.retrieve(query, RAGContext)
  3. reranker(UnaryOperator<List<RetrievedDocument>>)
  4. numbered text formatting

execute(query) returns the formatted context string. Use retrieveDocuments(query, context) when you need the raw documents after optional reranking. Set the reusable default with defaultContext(context) or pass a context to execute(query, context).

Choosing a Strategy

StrategyStrengthsTradeoffsBest fit
VectorSemantic ranking when the caller supplies embeddingsDeclarative SEMANTIC uses 128-slot hash / token overlap unless one EmbeddingFunction is installedNatural-language similarity
KeywordFast lexical BM25 matchingDoes not understand paraphrasesTechnical terms, IDs, code
HybridCombines semantic, exact, and optional graph rankingTwo retrieval paths, or three when a graph store is suppliedMixed production corpora
GraphTraverses explicit relationshipsRequires a graph provider and bounded traversal designConnected entities and dependencies
Multi-queryImproves recall through query variationsAdds model calls and fusion costAmbiguous or underspecified questions
HierarchicalReturns relevant children with parent contextRequires valid parent metadataSections, manuals, and nested documents
TemporalPreserves relevance while favoring fresh materialRequires trustworthy timestampsNews, incidents, and changing policies
ReasoningRoot-to-leaf descent without embeddingsRequires an installed TreeNavigator and navigatorModelLong structured corpora where vocabulary overlap fails