SmartGateSmartGate

Agentic RAG Survey: How the Research Splits the Field

The agentic RAG literature splits along three lines: work that makes retrieval a tool the model may call, work that makes the model judge and re-retrieve its own evidence, and work that measures the result. The 2025 survey of the field organises the first two groups into named patterns, and the evaluation surveys cover the third with metrics rather than architectures.

Short answer: The agentic RAG literature splits along three lines: work that makes retrieval a tool the model may call, work that makes the model judge and re-retrieve its own evidence, and work that measures the result. The 2025 survey of the field organises the first two groups into named patterns, and the evaluation surveys cover the third with metrics rather than architectures. A practitioner's takeaway is smaller than the taxonomy: adopt the retrieval interface the papers converge on, keep the judge separate enough to be measured, and record the call count, because no published benchmark rewards a method for using fewer retrievals.

Key takeaways

  • The taxonomies are convergent, not competing. Naive, advanced and modular retrieval; then iterative, self-reflective, planning and multi-agent loops. Each layer assumes the one below it.
  • Retrieval is an interface before it is a method. Filters, a score threshold, a top-k and an optional rerank is the signature nearly every paper assumes and none defines precisely.
  • Reflection needs a judge you can measure. Self-critique helps only when the critique is scored against something; otherwise it is a second opinion with the same biases.
  • Evaluation work is where the field is thinnest. Accuracy dominates, latency is reported occasionally, and the number of retrieval calls per question is usually an implementation detail.
  • The operational layer is missing entirely. Keys, quotas and revocation never appear in a benchmark table, and they are what decides whether a method can ship.
  • Do this next: take one method from the survey, write down its retrieval count per question and its judge, then run it against your own hardest question. Most of the taxonomy collapses into two numbers there.

Memory, tools and reflection: the three axes the surveys split on

Two surveys do most of the organising. The 2023 retrieval survey (RAG for large language models) separated naive retrieval from advanced retrieval — pre-retrieval, retrieval and post-retrieval optimisation — and from modular retrieval, where the pipeline is a set of components an orchestrator arranges. The 2025 agentic survey (agentic RAG patterns) then took the orchestrator seriously and named what it does: iterative retrieval, self-reflective retrieval, query planning, and multi-agent arrangements where a separate agent owns retrieval. Read together, the two give three axes rather than a taxonomy: what the system remembers, what it is allowed to call, and whether it critiques its own output.

Axes are useful because a paper usually moves along one. Work on iterative retrieval changes when queries are issued; work on reflection changes who judges the result; work on memory changes what survives a hop. When two papers appear to disagree, the disagreement is often axis confusion — one measures a retrieval strategy against a fixed memory model, the other measures a memory model against a fixed strategy. Every benchmark inherits that confusion in its label:

# lib/analytics/price-table.ts — source lines 23–25 (formatSavingsBenchmarkLabel)
function formatSavingsBenchmarkLabel(): string {
  return `@ internal benchmark $${INTERNAL_SAVINGS_USD_PER_M.toFixed(2)}/M input · gateway estimate`;
}

The function is a label formatter, and that is the point worth carrying into the literature: a benchmark number is only as meaningful as the string attached to it. That one declares its unit, its basis and its provenance — internal, estimated, per million input tokens — in a single line, so a reader cannot mistake the comparison. Most published tables would be better if they did the same. When you adopt a method from a survey, write down the three axes it moves and the four parameters it fixed; that sentence is what determines whether the method transfers to your workload. The layers each axis assumes — retrieval, deduplication, compression — are laid out on the deployment diagram.

One boundary the surveys treat as a given is the one this site has to argue about explicitly: a retrieval pattern and an agentic runtime are different objects, and rag vs agentic ai is where that vocabulary is separated.

Search as a tool for LLM-based agents: the interface the papers assume

Before reflection, routing or planning, there is one function that every method in the field calls. It searches a store and returns ranked passages. Nobody defines it in the papers, and everybody assumes it — filters for the entity or thread, a similarity threshold, a count, and a switch for reranking:

# backend/smartgate/modules/memory/algorithm.py — source lines 2541–2657 (AsyncMemory.search)
async def search(
        self,
        query: str,
        *,
        top_k: int = 20,
        filters: Optional[Dict[str, Any]] = None,
        threshold: float = 0.1,
        rerank: bool = False,
        **kwargs,
    ):
        """
        Searches for memories based on a query.

        Args:
            query (str): Query to search for.
            top_k (int, optional): Maximum number of results to return. Defaults to 20.
            filters (dict): Filter dict containing entity IDs and optional metadata filters.
                Must contain at least one of: user_id, agent_id, run_id.
                Example: filters={"user_id": "u1", "agent_id": "a1"}

                Enhanced metadata filtering with operators:
                - {"key": "value"} - exact match
                - {"key": {"eq": "value"}} - equals
                - {"key": {"ne": "value"}} - not equals
                - {"key": {"in": ["val1", "val2"]}} - in list
                - {"key": {"nin": ["val1", "val2"]}} - not in list
                - {"key": {"gt": 10}} - greater than
                - {"key": {"gte": 10}} - greater than or equal
                - {"key": {"lt": 10}} - less than
                - {"key": {"lte": 10}} - less than or equal
                - {"key": {"contains": "text"}} - contains text
                - {"key": {"icontains": "text"}} - case-insensitive contains
                - {"key": "*"} - wildcard match (any value)
                - {"AND": [filter1, filter2]} - logical AND
                - {"OR": [filter1, filter2]} - logical OR
                - {"NOT": [filter1]} - logical NOT
            threshold (float, optional): Minimum score for a memory to be included. Defaults to 0.1.
            rerank (bool, optional): Whether to rerank results. Defaults to False.

        Returns:
            dict: A dictionary containing the search results under a "results" key.
                  Example for v1.1+: `{"results": [{"id": "...", "memory": "...", "score": 0.8, ...}]}`

        Raises:
            ValueError: If filters doesn't contain at least one of user_id, agent_id, run_id,
                or if threshold/top_k values are invalid.
        """
        # Reject top-level entity params - must use filters instead
        _reject_top_level_entity_params(kwargs, "search")

        # Validate search parameters (before applying defaults)
        _validate_search_params(threshold=threshold, top_k=top_k)

        # Validate and trim entity IDs in filters
        effective_filters = filters.copy() if filters else {}
        if "user_id" in effective_filters:
            effective_filters["user_id"] = _validate_and_trim_entity_id(
                effective_filters["user_id"], "user_id"
            )
        if "agent_id" in effective_filters:
            effective_filters["agent_id"] = _validate_and_trim_entity_id(
                effective_filters["agent_id"], "agent_id"
            )
        if "run_id" in effective_filters:
            effective_filters["run_id"] = _validate_and_trim_entity_id(
                effective_filters["run_id"], "run_id"
            )

        # Validate filters contains at least one entity ID
        if not any(key in effective_filters for key in ("user_id", "agent_id", "run_id")):
            raise ValueError(
                "filters must contain at least one of: user_id, agent_id, run_id. "
                "Example: filters={'user_id': 'u1'}"
            )

        limit = top_k

        # Apply enhanced metadata filtering if advanced operators are detected
        if self._has_advanced_operators(effective_filters):
            processed_filters = self._process_metadata_filters(effective_filters)
            # Remove logical/operator keys that have been reprocessed
            for logical_key in ("AND", "OR", "NOT"):
                effective_filters.pop(logical_key, None)
            for fk in list(effective_filters.keys()):
                if fk not in ("AND", "OR", "NOT", "user_id", "agent_id", "run_id") and isinstance(effective_filters.get(fk), dict):
                    effective_filters.pop(fk, None)
            effective_filters.update(processed_filters)

        keys, encoded_ids = process_telemetry_filters(effective_filters)
        capture_event(
            "mem0.search",
            self,
            {
                "limit": limit,
                "version": self.api_version,
                "keys": keys,
                "encoded_ids": encoded_ids,
                "sync_type": "async",
                "threshold": threshold,
                "advanced_filters": bool(filters and self._has_advanced_operators(filters)),
            },
        )

        original_memories = await self._search_vector_store(query, effective_filters, limit, threshold)

        # Apply reranking if enabled and reranker is available
        if rerank and self.reranker and original_memories:
            try:
                # Run reranking in thread pool to avoid blocking async loop
                reranked_memories = await asyncio.to_thread(
                    self.reranker.rerank, query, original_memories, limit
                )
                original_memories = reranked_memories
            except Exception as e:
                logger.warning(f"Reranking failed, using original results: {e}")

        return {"results": original_memories}

The signature is worth reading as a specification. The query is free text and everything else is a constraint: filters that must identify at least one of a user, an agent or a run, so a retrieval cannot silently span tenants; a threshold that drops weak matches instead of returning the top-k regardless; and rerank as an explicit, more expensive pass rather than a default. Published methods tend to differ only in how they set those four knobs — an agentic loop that retrieves three times per question is choosing a small count and a high threshold, while a memory-heavy agent chooses the opposite and compresses afterwards.

That is where the practitioner's version of the taxonomy starts. A method described as "iterative retrieval with query rewriting" is, at the call site, a loop that invokes this function more than once with a query it composed itself. If your retrieval tool has no threshold and no rerank switch, you cannot reproduce half the literature on your own data — and if it has no filter by identity, you cannot run any of it safely. Frameworks describe the same interface as a tool the model may invoke; the contract is what matters, not the wrapper. For systems where an LLM-based agent calls several tools, what the loop actually is starts from the same signature and works out what changes when the caller can re-query.

The other half of the same interface question is who owns the ranking: an index you can calibrate and a search engine you cannot are not interchangeable, and agentic search vs RAG is the comparison that prices the difference.

Self-reflection and the encoder underneath: OpenCode-style harnesses

The most-cited advance of 2023 and 2024 is self-critique. Self-RAG (retrieve, generate, critique) trains the model to emit retrieval and relevance tokens; Corrective RAG (corrective retrieval) adds a lightweight evaluator that decides whether retrieved material is good enough and corrects it before generation; FLARE (active retrieval) retrieves ahead of the next sentence when the model's own confidence drops. Different mechanisms, one shared dependency: something must judge relevance, and that judge is the part you have to evaluate rather than assume.

Underneath all of them sits the component nobody criticises, because it is the one stable piece. Whatever the harness — OpenCode-style coding agents, framework graphs, a bespoke loop — text becomes vectors through the same narrow interface:

# backend/smartgate/modules/dedup/utils.py — source lines 16–28 (encode)
def encode(
        self,
        inputs: Sequence[Any] | Any,
        **kwargs: Any,
    ) -> np.ndarray:
        """
        Encode a list of inputs into embeddings.

        :param inputs: A list of inputs to encode (strings, images, etc.).
        :param **kwargs: Additional keyword arguments.
        :return: The embeddings of the inputs.
        """
        ...  # pragma: no cover

Two properties of that interface decide what the literature can mean on your system. It is batched — inputs arrive as a sequence — because the encode call is the expensive part of every write path, and a harness that embeds one passage at a time pays the round trip for each. And it is declared, not implemented, in the class that owns it: an abstract encode leaves the concrete model somewhere else, which is why a paper's retrieval quality does not transfer when you swap embedding models, even at identical chunk sizes. If you take one design rule from the reflection work, take this one: keep the judge and the encoder separately measurable, because they fail differently and the aggregate score cannot tell you which one moved.

Initialization: when the harness starts and the clock begins

Evaluation is where the agentic RAG literature is least reproducible, and the reason is usually the clock. A reported latency figure is only comparable if the reader knows what had already happened when the timer started: whether the index was warm, whether the embedder was loaded, whether the harness had started its background machinery. In the system quoted below, starting is one line of work rather than a health check:

# backend/smartgate/core/events.py — source lines 45–47 (start)
async def start(self):
        self._worker = asyncio.create_task(self._process_events())
        logger.info("EventBus started")

That is a scheduled background task — an event worker created and logged, with no readiness signal, no retry and no failure path. It is a reasonable shape for a long-running service and a poor one for a benchmark, because it means "the harness is up" is an assumption rather than a fact. Two practitioners' consequences follow. When you reproduce a published latency number, ask what was warm; the gap between a cold start and a warm one routinely exceeds the algorithmic gains a paper reports. And when you publish your own numbers, state the initialization assumptions — a table with an unstated warm start is a table nobody can compare against.

The same narrow reading applies to every harness in the survey: what starts, in what order, and what is allowed to fail silently. Most of the taxonomy is about what happens per question; this is about what happens once, before any question, and it is where a reported result is either reproducible or not.

Pre-memory initialization: the state before the first write

There is a window in every run that the surveys never name: after the harness is running and before the first memory write exists. It is short, it is easy to ignore, and it is where a cold-start budget is actually spent. Read the same three lines again, this time for what they do not do:

# backend/smartgate/core/events.py — source lines 45–47 (start)
async def start(self):
        self._worker = asyncio.create_task(self._process_events())
        logger.info("EventBus started")

The task is created, the process continues, and nothing waits. In the pre-memory window that is exactly right — there is no state to protect yet — and it is also why the first question of a run is systematically slower than the tenth: connection pools fill, embedding models load on first call, and caches populate, all of it after the harness declared itself ready. A benchmark that starts its timer at the prompt therefore charges the algorithm for work the runtime should have done during initialization.

The fix in production is unglamorous and worth doing before any loop work: warm the components you depend on while the process starts, then report readiness only when they are warm. The equivalent for an evaluation harness is to discard the first question of every run, or to report it separately as a cold-start figure. Neither changes the algorithm; both change whether the number you quote describes the method or your deployment. This is the same distinction the memory literature draws between what is stored and what is merely cached — and it is why what a memory design keeps starts with the write path rather than the read path.

What the benchmarks measure, and what they leave out

The evaluation surveys are the honest part of the field. One covers metrics and frameworks for retrieval-augmented systems (evaluation of RAG); a more recent one arranges the whole evaluation landscape in the era of large models (RAG evaluation survey). Between them they make the gap visible: the metrics are about answer quality and faithfulness, the systems are about behaviour under load, and few papers report both. Faithfulness and answer relevance can be scored without human labels (reference-free scoring), which is why those two dominate the tables.

Three omissions recur, and a practitioner should fill all three. Retrieval count per question is reported as an implementation detail even though it is the quantity that separates the agentic methods from the pipeline they replaced. Memory pressure — the resident cost of holding an index, an embedder and a growing scratchpad — appears in engineering blogs and almost never in a results table; it competes with the context budget, so the compression work sits in the same tables as quality (the context budget, token optimization). And adaptation is measured once: a method tested at one question complexity (learning to adapt retrieval) shows a different profile when the same system meets a mix of easy lookups and multi-hop comparisons, which is what a production traffic stream looks like. Read a benchmark table as a claim about one point in a space, and record the call count and the resident cost next to the accuracy your own run produces.

The gateway in the appendix: keys, revocation and reproducibility

Every published result was produced by calling a hosted endpoint, and almost none of them describe the operational layer that made it possible. That layer has a shape, and it is short:

# lib/api-keys/index.ts — source lines 71–81 (revokeApiKey)
async function revokeApiKey(id: string, teamId: string) {
  const key = await prisma.apiKey.findFirst({
    where: { id, teamId, revokedAt: null },
  });
  if (!key) throw new Error("API key not found");

  return prisma.apiKey.update({
    where: { id },
    data: { revokedAt: new Date() },
  });
}

Read it for the discipline rather than the database calls. The key is looked up scoped to a team, missing keys raise instead of returning empty, and revocation is a timestamp write rather than a delete — so the record survives, and an audit row can still name the key that made a call. In a survey context the interesting property is pinning: a benchmark that does not state which endpoint, which quota and which key policy it ran under has published a number that a reader cannot reproduce, because the thing behind the API changes.

For practitioners, the operational layer is also the reproducibility layer. Record the endpoint, the quota in force and the date for any evaluation you keep, and rotate credentials on a schedule rather than after an incident. A gateway between your loop and its providers makes that record a by-product: the same configuration that enforces a quota writes the audit row that lets you compare a run in March with a run in September. The gateway layer is where those controls sit, and it is worth wiring before an evaluation, not after one — a number you cannot attribute to a configuration is a number you will re-derive by hand.

What is agent memory, in the literature's own terms

The first question in any memory taxonomy is scope: which writes may a later read see. Published work distinguishes short-term from long-term, episodic from semantic, and procedural from factual, and those names describe when a record is visible rather than what it contains. The implementation that has to enforce the distinction does it with filters, and the merging of filters is where scope becomes concrete:

# backend/smartgate/modules/memory/algorithm.py — source lines 1274–1280 (Memory.merge_filters)
def merge_filters(target: Dict[str, Any], source: Dict[str, Any]) -> None:
            """Merge source into target, deep-merging nested operator dicts for the same key."""
            for key, value in source.items():
                if key in target and isinstance(target[key], dict) and isinstance(value, dict):
                    target[key].update(value)
                else:
                    target[key] = value

The function merges a source constraint dictionary into a target one, recursing into nested operator dicts so that two filters on the same field combine instead of one replacing the other. That detail is the whole taxonomy in miniature. "Short-term" and "long-term" are not different stores to a query planner; they are different filter values over one store, and the moment a retrieval merges a caller's filters with a session's, the merge rule decides whether scope is honoured or silently overwritten. A system that replaces rather than combines will answer a question with the wrong session's memories exactly when both constraints exist — which is to say, in production, always.

The practitioner's translation is to define memory scope as a filter contract rather than a component diagram. Name the keys — user, agent, run, and whatever the product adds — decide for each whether it must be present or may be absent, and test the merge with two overlapping filters before writing any retrieval logic. The surveys describe what memory is for; that test determines whether your implementation has it.

How SmartGate compares

The choice the survey creates is not which method to adopt; it is what your loop calls, and whether you can tell afterwards what each call was for.

What the papers give you What you still have to build What it costs
A named method from the literature A retrieval strategy, a judge, and a benchmark number The store, the filters, the quotas and the record of each call Your infrastructure, plus evaluation time
A framework graph (LangChain, LangGraph) Nodes, edges and a state schema The tool contract, metering and the stop condition Maintenance, plus whatever you meter by hand
A bespoke loop Nothing but the idea Everything, including the parts the papers omitted Whatever you fail to instrument
SmartGate Seven tools with fixed descriptions, one endpoint Your retrieval strategy, your judge, your prompt Free tier: 2M tokens a month and all seven tools. Pro from $18 a month, Teams from $55 a month

The row worth noticing is the first. A method from a survey arrives as an algorithm and a table; the store, the scope filters and the per-call record are assumed, and they are the parts that decide whether it runs. What a loop costs to run prices the calls, the architecture hub (the architecture, stage by stage) covers the retrieval stages the methods sit on top of, and the per-workload comparison is where a method choice becomes a decision about latency, cost and answer shape.

How to get started

  1. Pick two methods, not five. Choose one iterative method and one reflective method from the survey, and note the four retrieval parameters each assumes. Two is enough to see how much of a paper's result is the method and how much is the deployment.
  2. Write the retrieval interface down before you compare anything. Filters by identity, a threshold, a count, an optional rerank. If your tool cannot express those four, that limitation — not the method — is what your evaluation will measure.
  3. Separate the judge from the generator. Score the judge's decisions on a small labelled set before trusting a reflection loop; a critique that is never measured is a second opinion.
  4. Record the call count per question. It is the one number that lets you compare an agentic method with the pipeline it replaced, and it is nearly free to log if the audit row exists.
  5. State your initialization assumptions. Warm or cold, which components loaded, what the timer measured. This is what makes your number comparable with the paper's.
  6. Fill the two omissions the surveys leave. Resident cost and adaptation across question complexity. Both change the conclusion more often than a metric improvement does.

Start on the free tier — 2 million tokens a month and all seven tools — with start free; the per-plan limits are on the pricing page, the tool parameters are in the product docs, and contract traffic starts at the contact form.

Frequently Asked Questions

Limitations and what this does not do

  • Seven of the eight planned sections carry code. The benchmarking section matched no unique symbol in this codebase and is written from the published evaluation surveys with no quoted implementation.
  • Two sections read the same three lines. The plan assigned two near-identical phrases whose nearest symbol is the same construction path; the second reading asks what the code leaves out rather than what it does. That is the plan's shape, not a hidden repetition.
  • The code is operational, not research code. The slices come from a production gateway — a search signature, an encoder interface, a start task, a key revocation, a filter merge. They are used as concrete shapes for interfaces the papers assume and do not define.
  • This page is not a literature review. It names the surveys and the methods that describe the field's structure. It does not attempt to rank methods, and any accuracy figure belongs to the paper that measured it.
  • Nothing here is a benchmark result. The page argues about which numbers are worth recording; it does not claim to have measured a loop against a pipeline.

Sources

Method note

The code in this article is not transcribed. Each block was cut directly out of the slice body returned by the SmartGate slice API and re-asserted byte-for-byte as a substring of that body before publication; the first line inside every fence records the file and the exact source lines. Symbols were pinned with whole-name containment (rule A level 2) and confirmed by the service's slot-proof endpoint — 7 of 8 planned sections pinned, no abstentions. The benchmarking section matched no unique symbol and is written from the published evaluation surveys with no code, as the abstain rule requires. The implementation quoted here is the gateway's own retrieval, encoder, startup, key and filter-merge code, used as the concrete shape of interfaces the survey literature assumes.

Slice provenance

# SERP keyword Symbol File Source lines How it was pinned sha256(12)
1 memory os of ai agent formatSavingsBenchmarkLabel lib/analytics/price-table.ts 23–25 rule A L2 → slot-proof 966cf596af58
2 mirix multi agent memory system for llm-based agents AsyncMemory.search backend/smartgate/modules/memory/algorithm.py 2541–2657 rule A L2 → slot-proof b2d9db8cd80b
3 opencode agent memory encode backend/smartgate/modules/dedup/utils.py 16–28 rule A L2 → slot-proof ac025d9a740c
4 pre memory system agent initialization is started start backend/smartgate/core/events.py 45–47 rule A L2 → slot-proof ae3a2504b40c
5 pre-memory system agent initialization is started start backend/smartgate/core/events.py 45–47 rule A L2 → slot-proof ae3a2504b40c
6 vercel ai gateway api key revokeApiKey lib/api-keys/index.ts 71–81 rule A L2 → slot-proof 4faf8d3481f5
7 what is agent memory Memory.merge_filters backend/smartgate/modules/memory/algorithm.py 1274–1280 rule A L2 → slot-proof 17700d5c4493

Every fenced block above was cut from the slice body and re-asserted against it byte-for-byte before publication. 7 of 8 sections pinned, 0 abstentions, and 1 section(s) written from external sources because no unique symbol matched.