AI Agent 3 min read

Evaluate Multi-Agent Search Relevance with Judged Sources

Measure multi-agent search using deduplicated URLs, recorded relevance judgments and cost per relevant source. Includes a reproducible synthetic example.

(Updated: ) • 512 words

Multiple search agents can produce more candidates without producing more relevant evidence. Evaluate the combined result set after deduplication, with judgments recorded against a specific question. Keep the single-agent baseline, query budget and decision rules visible. Parallel execution and improved relevance are different claims.

Define the experiment before running it

Choose questions from the task you serve. For each question, define what counts as a relevant source and what information would be missing from an acceptable answer. Save the exact agent instructions, rewritten queries, market, tool versions and request budget. Use the same allowed sources and budget policy for the baseline and proposed workflow.

Score candidates with recorded judgments

from urllib.parse import urlsplit, urlunsplit

def canonical_candidate(url):
    p = urlsplit(url)
    if p.scheme not in ("http", "https") or not p.hostname:
        raise ValueError("Absolute HTTP URL required")
    # Query/path are preserved; only the fragment is removed.
    return urlunsplit((p.scheme.lower(), p.netloc.lower(), p.path or "/", p.query, ""))

def evaluate_candidates(agent_urls, judged_relevant, credited_calls):
    urls = list(dict.fromkeys(canonical_candidate(u) for lane in agent_urls for u in lane))
    if any(u not in judged_relevant for u in urls):
        raise ValueError("Every candidate needs a recorded relevance judgment")
    if any(type(judged_relevant[u]) is not bool for u in urls):
        raise ValueError("Use explicit binary relevance labels")
    if credited_calls < 0:
        raise ValueError("Credit count cannot be negative")
    hits = sum(judged_relevant[u] for u in urls)
    return {"unique_urls":len(urls), "relevant_urls":hits,
            "precision":hits/len(urls) if urls else None,
            "credits_per_relevant_url":credited_calls/hits if hits else None}

if __name__ == "__main__":
    lanes = [["https://example.com/a", "https://example.com/a#part"], ["https://example.com/b"]]
    print(evaluate_candidates(lanes, {"https://example.com/a":True,"https://example.com/b":False}, 3))

The synthetic example has three returned candidates but two unique URLs after removing a fragment. One is judged relevant, so precision is one half. Three credited calls divided by one relevant URL yields three credits per relevant URL. These are fixture calculations, not observed quality gains. Unjudged URLs are rejected instead of silently treated as relevant.

Measure the failures that averaging can hide

Record redundant sources, missing subquestions, conflicting passages and invalid citations separately. Candidate precision does not prove the final answer is correct. If you also measure coverage, define a judged reference set and publish how it was assembled; do not use a larger URL count as coverage. If costs include Reader or model calls, record those separately from search credits.

A fair comparison repeats the same question set and exposes raw per-question results, including failures. Do not report a universal percentage gain from a small fixture. For orchestration and permission boundaries, use the agent architecture. For timing, use the search benchmark contract.

Sources and reproduction

Tags:

AI Agent LLM Tutorial Python RAG
SearchCans Team

SearchCans Team

SERP API & Reader API Experts

The SearchCans engineering team builds high-performance search APIs serving developers worldwide. We share practical tutorials, best practices, and insights on SERP data, web scraping, RAG pipelines, and AI integration.

Ready to build with SearchCans?

Test SERP API and Reader API with 100 free credits. No credit card required.