TCTool Curator Technical Documentation BGE · FAISS · BM25 · RRF — University Test Bench

RAG-BASED TOOL CURATOR

RAG Tool Curator — Technical Documentation

Architecture, evaluation, and a 191-tool university use case — BGE dense retrieval, FAISS, BM25, and Reciprocal Rank Fusion, applied to tool selection rather than document search.

Dense (BGE)

get_cgpa
get_academic_summary
get_attendance_summary

Sparse (BM25)

get_exam_eligibility
get_cgpa
get_fee_dues

Fused + anchor

get_cgpaanchor: forced to top
get_exam_eligibility
get_academic_summary

RRF merges dense + sparse rankings · name-token anchor guarantees exact-name matches survive fusion

100%
No-curator baseline (281/281)
88.4%
TF-IDF accuracy score
94.8%
Hybrid curator accuracy score
−86.8%
Prompt token reduction
A note on scope. Most of what follows — dense embeddings, BM25, FAISS, and Reciprocal Rank Fusion — is standard information retrieval. What's specific here is applying that combination to tool selection for LLM agents rather than document search, where descriptions are short and the gap between how a user phrases a request and how a function is documented can be severe. The curator has been evaluated end to end against one deployment, a university system with 191 tools; a reuse guide covers porting it elsewhere, but that hasn't been run against a second deployment yet, so generalisation rests on design rather than a second result. The seven internal components haven't been tested in isolation against each other, and every accuracy number below comes from manually running 281 test queries and labelling the outcome by hand. Treat the architecture and the description-refinement finding as the solid parts; the percentages are results from one deployment, not universal constants.

Summary

Executive summary

As agentic LLM systems grow to handle hundreds of tools, two problems show up together. The context window fills with tool definitions the model won't use for a given query, and tool-selection accuracy drops as the candidate pool grows. This document describes a tool curator: a retrieval layer sitting between role-based access control and the LLM that narrows a large tool pool down to a small, query-specific shortlist before the model ever sees it.

The curator is a small, swappable component. A two-method interface (build an index, select tools for a query) sits in front of a hybrid retrieval implementation combining BGE dense embeddings, FAISS, BM25, and Reciprocal Rank Fusion, plus several mechanisms aimed at specific failure modes.

The single most important point: the LLM never sees all 191 tools. At startup, tools are discovered from MCP servers and filtered by role. For every user query the curator receives only the role-visible pool, selects ~15 candidates, and only those 15 are included in the model request. The no-curator baseline — all role-permitted tools sent at once — is the accuracy ceiling; it is impractical to deploy because of prompt-token cost, not because it works worse.

It was built and tested against a university system — eight MCP servers, 191 tools, four user roles, and a 281-case test suite — across three generations. A no-curator baseline reached 100% (281/281) once tool descriptions were rewritten into a tighter contract format, up from roughly 80%. A TF-IDF curator reached 83.6% correct (88.4% accuracy score), with failures concentrated in paraphrased queries. The hybrid curator reached 93.2% correct (94.8% accuracy score), cutting the prompt token cost for an initial student query from roughly 11,860 to roughly 1,561 tokens, an 86.8% reduction.

Token and accuracy comparison across three curator generations
Figure 1. Token and accuracy comparison across curator generations.

01  Problem statement

The problem: too many tools, too little context

Giving an LLM access to tools usually means listing them, either in the system prompt or as function declarations. For ten or twenty tools this is fine. Past roughly fifty, two problems start to compound.

The first is token cost. Each tool definition runs 100 to 200 tokens once serialised; a student account in the testbed has access to 87 tools, costing around 11,860 prompt tokens before the user types anything. Scale that to a few thousand tools, common on enterprise platforms, and the tool definitions alone can exceed the context window of most deployed models.

The second is that tool-selection accuracy doesn't stay flat as the pool grows. Picking the right tool from 87 candidates in one pass means overlapping vocabulary and similar names introduce noise, and that noise compounds when a query needs two or three tools in sequence.

A third issue sits upstream of both: the wording of the descriptions affects how well the model picks. Early versions of the testbed used documentation-style descriptions, with bulleted "use this tool when..." sections and repeated examples, and with no curator and full tool visibility scored around 80%. Rewriting all 191 descriptions into a shorter, contract-style format, stating what the tool returns and what its parameters are, took the same system to 100% with no other change. Every accuracy number in this document is measured against the refined descriptions, since a curator's job is to approach what a full, well-described tool list achieves, not compensate for badly written ones.

The question this project set out to answer: can a lightweight retrieval layer cut a large, role-filtered tool pool down to roughly fifteen tools per query, without losing the accuracy a full, well-described tool list achieves, in a way that isn't tied to one domain's vocabulary?

02  Prior approaches

Why simpler approaches aren't enough

Role-based filtering alone

The first layer of curation is access control: a student's view of 191 tools is cut to whatever their role permits — 87 for students, around 65 for faculty, around 27 for executives, and the full 191 for admins. This is necessary but doesn't touch the per-query problem, since eighty-seven tools is still too many for reliable single-query selection and role filtering does nothing to reflect that any given question only needs one or two of them.

TF-IDF keyword retrieval

The first active curator used TF-IDF with cosine similarity: vectorise each tool's name and description at startup, vectorise the query the same way, keep the highest-scoring matches. For queries where wording lines up, this works well: "What is my CGPA?" shares a token with "Returns the student's cumulative grade point average," so get_cgpa scores highly. A large share of real queries fall into this category.

Where it breaks is anywhere the user's vocabulary and the tool's don't overlap, even when the meaning is identical to a person. "Am I at risk of being barred from exams?" should map to an eligibility check, but "barred" appears nowhere in that description, so the score is near zero. "Raise a formal complaint about the projector" should map to a grievance tool, but neither "complaint" nor "raise" appears in "submit a new grievance ticket." These aren't tuning problems; they're a consequence of scoring token overlap with no notion of two words meaning the same thing.

There was also a second-order issue with action tools, the ones that change state. An early TF-IDF version sent only its top-k matches, which badly hurt accuracy on action queries, since those tools didn't always score highest for how users phrased requests. The fix, always including every action tool the role can access, helped, but pushed the effective tool count for a student query from fifteen to around thirty-three.

Dense retrieval on its own

Dense embeddings close the vocabulary gap TF-IDF can't: a semantic model places "raise a formal complaint" and "submit a new grievance ticket" close together with no shared words. But it can be too loose for queries that should have an exact match. "Get my CGPA" should retrieve get_cgpa and arguably nothing else, but a dense model might also surface get_course_performance_summary or get_student_academic_summary, all sitting in the same semantic neighbourhood.

Neither approach alone is sufficient. Lexical matching is precise when wording happens to align and falls apart otherwise; semantic matching closes that gap but loses precision on exact matches. The curator below combines both.

03  Architecture

The tool curator architecture

The curator sits between role-based filtering and the LLM. By the time a query reaches it, the tool pool is already filtered to whatever the role permits. The curator's job is to take that pool and the query and return a much smaller shortlist, typically around fifteen tools.

Overall system architecture showing role filter, curator, LLM and MCP servers
Figure 2. Overall architecture of the university MCP system and hybrid tool curator.

The interface

Everything the rest of the system needs to know is defined in tool_curator/curator_interface.py, an abstract base with two methods:

curator_interface.pypython
class ToolCurator(ABC):
    @abstractmethod
    def build_index(self, tools, extra_context=None) -> None: ...

    @abstractmethod
    def select(self, query, tools) -> list[tuple[str, str]]: ...

build_index runs once at startup, after tool definitions are collected and role-filtered. The resulting FAISS and BM25 structures are cached to disk under .tool_index/ — subsequent startups load the cache in ~50 ms rather than the 2–3 s needed to re-encode all tools from scratch. The cache key is an MD5 hash of the tool definitions plus domain_synonyms.json, so editing either file automatically invalidates it. select runs once per query. Neither signature mentions BGE, FAISS, or BM25; tool_curator/__init__.py reads TOOL_CURATOR_MODE from settings and returns whichever implementation matches, so swapping retrieval approaches doesn't touch the orchestrator. The same file exposes load_tool_context(), which reads domain_synonyms.json and passes it to build_index as extra_context.

Query processing sequence diagram
Figure 3. Query processing flow from user query through role filtering and tool curation.

The production implementation, tool_curator/hybrid_rag_curator.py, is built from seven pieces, each added because something simpler was tried first and didn't hold up.

RAG curator component structure
Figure 4. Internal structure of the hybrid retrieval curator.

Query flow — end to end

1Role filteraccess control, runs before the curator; narrows pool to role-permitted tools
2Compound split?if query has additive markers ("and then", "also", "additionally"), split into sub-queries
3Dense retrievalBGE embedding → FAISS top-k; handles semantic paraphrases
4Sparse retrievalBM25 top-k; handles exact keyword overlap; skipped if every score = 0
5RRF mergeReciprocal Rank Fusion combines both ranked lists by position, no score calibration needed
6Anchor checktools whose name tokens appear in the query are force-inserted at rank 1
7Dependency expandpost-retrieval: companion tools required by the selected tool's workflow are appended
8→ LLM~15 tools included in request; role-filtered full list never reaches the model

Seven components

1

Dense retrieval: BGE and FAISS

The embedding model is BAAI/bge-small-en-v1.5 (130MB, 384 dimensions), trained for asymmetric retrieval — short queries against longer passages — matching a ten-word query against a hundred-word tool description. Queries carry a prefix that descriptions don't:

hybrid_rag_curator.pypython
BGE_QUERY_PREFIX = "Represent this sentence: "
q_emb = model.encode(BGE_QUERY_PREFIX + query, normalize_embeddings=True)
doc_emb = model.encode(tool_text, normalize_embeddings=True)

Embeddings are L2-normalised, so inner product equals cosine similarity, which is what FAISS IndexFlatIP (an exact, not approximate, index) uses. For 191 tools at 384 dimensions the whole index is under half a megabyte and search completes in under a millisecond.

2

Domain context: domain_synonyms.json

This file is the main point where domain knowledge enters the system, deliberately separate from the curator code. It maps tool names to related terms appended to the tool's text before encoding:

domain_synonyms.jsonjson
{
  "submit_grievance": "raise complaint file issue lodge protest formal complaint",
  "get_exam_eligibility": "can I write exam barred eligible allowed to sit detained"
}

Once submit_grievance's text includes "raise complaint," a query like "raise a formal complaint about the broken projector" lands close to it in vector space, even though neither word appears in the actual description. This file is the first place to look when a specific query consistently fails: adding the missing phrasing is faster than tuning retrieval parameters.

What gets indexed: each tool becomes a single document — name + description + domain synonyms — concatenated before encoding. Both the FAISS index and the BM25 corpus are built over these enriched documents. If a tool has no synonym entry its document is simply name + description, which is still fully queryable.

3

Sparse retrieval: BM25, with a zero-score guard

BM25 replaces TF-IDF for keyword matching, with term-frequency saturation: a description mentioning "attendance" ten times scores roughly three times higher than one mentioning it once, not ten. The tokenizer splits on underscores as well as whitespace, so apply_for_hostel_leave matches "apply for hostel leave" instead of being one opaque token.

The less obvious piece: when a query shares no keywords with anything, every BM25 score is exactly zero, and sorting tied items falls back to insertion order — effectively random — which would inject an arbitrary boost into the fusion step below. The fix, found through testing rather than anticipated, is to skip BM25 entirely when it has nothing to contribute and resolve the query on dense retrieval alone.

4

Combining the two: Reciprocal Rank Fusion

Dense and sparse retrieval return lists on incompatible scales — cosine similarity between 0 and 1, BM25 unbounded. Averaging directly would require calibration tied to whatever corpus is running. RRF uses rank position only:

hybrid_rag_curator.pypython
RRF_K = 60
def _rrf_merge(dense_ranked, sparse_ranked, k=RRF_K):
    scores = {}
    for rank, (name, _) in enumerate(dense_ranked, 1):
        scores[name] = scores.get(name, 0.0) + 1.0 / (k + rank)
    for rank, (name, _) in enumerate(sparse_ranked, 1):
        scores[name] = scores.get(name, 0.0) + 1.0 / (k + rank)
    return scores

A tool ranked first in both lists gets 1/61 + 1/61, about 0.033, while one only the dense retriever found at rank 25 gets 1/85, about 0.012 — so tools both retrievers agree on rise to the top. The constant k=60, from the original RRF paper, flattens the curve so a strong result from one retriever can outweigh a weak one from the other.

5

Catching exact matches: the name-token anchor

RRF handles rough agreement but doesn't guarantee an obvious, near-literal match makes the cut. After fusion, any tool whose name shares most of its words with the query is force-included:

hybrid_rag_curator.pypython
ALWAYS_INC_RATIO = 0.60
def _always_include(query, tool_name):
    tokens = [t for t in tool_name.lower().split("_") if len(t) > 3]
    if not tokens:
        return False
    hit_ratio = sum(1 for t in tokens if t in query.lower()) / len(tokens)
    return hit_ratio >= ALWAYS_INC_RATIO
# applied after RRF: matching tools get their score forced to 999

For "hostel allotment details," get_hostel_allotment has both tokens "hostel" and "allotment" in the query, a hit ratio of 1.0 and guaranteed inclusion; the 0.6 threshold stops long tool names from being pulled in on one incidental word.

6

Splitting compound queries

"Check my attendance and also book Lab B-204 for next Monday" contains two unrelated requests; encoding it as one vector blends both intents, and the tool for one half can end up under-ranked. The curator looks for markers signalling sequential or additive intent (and then, additionally, as well as, then, also, plus), deliberately excluding plain "and" and "or" as too common to use as a split signal. Each piece is retrieved separately with a proportional budget, then the full unsplit query is retrieved once more to catch anything relevant to the combination as a whole.

7

Dependency expansion

Some tools only make sense alongside others — checking exam eligibility depends on both attendance and fee status. Two small lookup tables inside hybrid_rag_curator.py encode this: DEPENDENCY_TOOLS maps a tool to companions it should pull in when selected, and QUERY_DEPENDENCY_HINTS does the same based on query patterns.

hybrid_rag_curator.pypython
DEPENDENCY_TOOLS = {
    "get_exam_eligibility": ("get_attendance_summary", "get_fee_dues"),
    "register_for_placement_drive": ("check_placement_eligibility",),
    "make_payment": ("get_fee_dues",),
}

This is the same idea as TF-IDF's action-tool augmentation, but more targeted: instead of adding every action tool to every query, it adds only what the selected tool's workflow depends on. These two tables are also the most domain-specific part of the curator, and rewriting them is core to the reuse guide later in this document.

Hybrid curator pipeline
Figure 5. End-to-end hybrid retrieval pipeline.

Supporting infrastructure

A few pieces around the curator affect reliability without being part of retrieval itself. orchestrator/hallucination_guard.py runs after the model picks a tool: it compares the name against known tools using difflib.SequenceMatcher, silently correcting above 0.82 similarity, returning a hint for the model to retry between 0.60 and 0.82, and failing outright below 0.60. This catches the common case of a model knowing which tool it wants but misspelling the name, like get_attendence_summary.

orchestrator/mutation_guard.py handles state-changing tools: before any such tool runs, it shows a confirmation panel and waits for approval, and injects identity fields (student_id, faculty_id) from the session so users never type their own ID. tool_curator/suggestion_validator.py scans the model's final text for snake_case-looking names that aren't real tools — a check for when the model invents a plausible tool name while suggesting a follow-up; it warns but doesn't block. tool_curator/terminal_display.py is the UI layer and carries no retrieval logic.

Tool execution sequence
Figure 6. Tool validation, confirmation and execution workflow.

One bug worth flagging because it would silently break any system built this way: the Gemini SDK's per-turn send_message(config=...) call replaces the session-level config rather than merging with it, and the session config is where the system instruction — including the user's identity — lives. A per-turn config containing only the curated tools wipes that out, and the model starts asking for the student ID on every query. The fix passes the system instruction explicitly on every call and, as a second layer, prepends a short session marker (role and user ID) directly into the message text.

Finally, the curator's index is cached on disk: a couple of seconds to build from scratch (encoding 191 tools, building FAISS and BM25), about fifty milliseconds to load. The cache key is an MD5 hash of the tool definitions plus domain_synonyms.json, so editing synonyms automatically invalidates the cache.

System startup sequence
Figure 7. Startup sequence including cache loading and index construction.
SettingValuePurpose
TOOL_CURATOR_MODE"rag"Selects which curator implementation is returned
TOOL_CURATOR_TOP_K10Base tools returned per query, before anchor/dependency additions
TOOL_CURATOR_EMBEDDING_MODEL"BAAI/bge-small-en-v1.5"Embedding model for dense retrieval
TOOL_CURATOR_INDEX_DIR.tool_indexDisk cache location

04  Example Use Case

Example Use Case: a university MCP System

The curator was built and tested against a fictional university system, an in-memory database (mock_db/university_mock_database.py) behind eight MCP servers exposing 191 tools, structured to resemble a real internal platform.

ServerToolsExamples
academics_mcp_server.py23get_cgpa, get_exam_eligibility, mark_attendance
student_services_mcp_server.py45submit_grievance, apply_for_hostel_leave
faculty_mcp_server.py44create_assignment, grade_assignment_submission
finance_mcp_server.py18get_fee_dues, make_payment
infrastructure_mcp_server.py22book_room, book_lab, raise_it_ticket
admin_analytics_mcp_server.py20get_academic_dashboard, get_cross_reference_report
placement_mcp_server.py10check_placement_eligibility
library_mcp_server.py9issue_book, get_all_overdue_books

Four roles see different slices, defined in orchestrator/persona_access_policy.py: students see 87 tools, faculty around 65, executives around 27 (mostly dashboards), admins all 191. About 55 of the 191 tools change state and are flagged as action tools, intercepted for confirmation by mutation_guard.py.

Module component overview
Figure 8. Project module structure and interaction between components.

Three generations

Three generations were built and run against the same 281-case suite: a no-curator version, where every tool the role permits goes to the model at session start, serving as the accuracy and token ceiling; a TF-IDF curator from Section 2, with the top-k-plus-action-tools augmentation layered on once top-k alone proved unreliable for action queries; and the hybrid curator described in Section 3 — BGE, FAISS, BM25, RRF, the anchor, compound splitting, and dependency expansion — which the rest of this document calls "the hybrid curator."

The description refinement step

Before any curated version could be measured meaningfully, the no-curator baseline needed measuring first, since it's the reference point. The first attempt scored around 80%, and the cause was how descriptions were written. The original style was documentation aimed at a person:

Original — ~80 words
Retrieves all internal assessment marks
for a student in a specific course.

Use this tool when the user asks about:
- CIA marks for a course
- Internal assessment scores
- How much I scored in CIA-1 or CIA-2

Parameters:
- student_id: Required. e.g. "21CS045"
- course_code: Required. e.g. "CS3503"

Returns: CIA-1, CIA-2, assignment marks,
total internal marks, percentage.
Refined — ~40 words
Returns all internal assessment marks
for a student in a specific course.

Parameters:
- student_id: Required. e.g. "21CS045"
- course_code: Required. e.g. "CS3503"

Returns: CIA-1, CIA-2, assignment marks,
total internal marks, and percentage.

Applied across all 191 tool descriptions → no-curator baseline: ~80% → 281/281 (100%)

Most of the original description was a "use this tool when" section restating the description in different words. Every one of the 191 descriptions was rewritten to drop that section, keeping only the contract. With this applied across all 191 tools and nothing else changed, the no-curator baseline went from roughly 80% to 281/281. The extra text seems to compete for the model's attention with the part that actually distinguishes tools, and it also roughly halves every tool's token footprint on top of whatever the curator saves. Every accuracy number in this document is measured against these refined descriptions.

05  Evaluation

Evaluation and results

All three versions ran against the same 281-case suite (MCP_Evaluation_Sheet_-_TF_IDF_Tool_Curator.xlsx), each case specifying a query, persona, up to four expected tools, and an expected output summary, split across seven categories:

Query typeCountWhat it tests
Single-tool direct189One expected tool, wording close to the description
Multi-tool complex25Two or more tools in one query
Semantic / paraphrase24Expected tool uses different vocabulary
Edge case / negative21No tool, access denied, or clarification needed
Access denied7Query targets a tool outside the role
Need clarification7Ambiguous — asking back is correct
No tool8Purely informational

Each case was run by hand and labelled Correct, Partially correct (some expected tools called), Wrong (the wrong tool called, with irrelevant output), or Skipped (the system said no tool was available when one exists), with Accuracy Score = (Correct + 0.5 × Partially Correct) / Total.

Evaluation pipeline
Figure 9. Manual evaluation workflow used during benchmarking.
VersionCorrectPartialWrongSkipped% CorrectAccuracy score
No curator, original descriptions~225/281~80%~80%
No curator, refined descriptions281/281000100.0%100.0%
TF-IDF curator235/2812791083.6%88.4%
Hybrid curator262/28193793.2%94.8%

Reading the table: the "No curator, refined descriptions" row at 100% is an accuracy ceiling, not a deployment target — sending all 87 role-permitted tools costs ~11,860 prompt tokens before a word of user input. The curator's goal is to approach that ceiling while cutting token cost by ~87%.

Moving from TF-IDF to the hybrid curator: correct rose 235→262, wrong fell 9→3, skipped fell 10→7, partial fell 27→9, and the accuracy score moved from 88.4% to 94.8%. The remaining seven-point gap to the 100% baseline is the subject of the failure analysis below.

VersionTools sent to modelPrompt tokens, first student query
No curator87~11,860
TF-IDF curator~33 (top-15 + all action tools)~9,135
Hybrid curator~15~1,561

The hybrid curator's token count is roughly 86.8% below the no-curator baseline. The gap between TF-IDF's ~33 tools and the hybrid's ~15 is mostly TF-IDF's blanket action-tool addition versus the hybrid's targeted dependency expansion: one adds all action tools to every query, the other adds only what's actually depended on.

06  Failure analysis

Failure analysis

Nineteen of 281 cases didn't come back fully correct: 3 wrong, 7 skipped, 9 partially correct. The patterns matter more than the individual cases.

Failure analysis mindmap
Figure 10. Categorization of remaining failure cases.
Wrong · 3 cases

Two involve a faculty member working with attendance for CS3503 — one marking today's session (expecting mark_attendance), the other correcting a past mistaken absence (expecting update_attendance). Both came back wrong. These two tools are about as close as any pair in the system gets — same server, overlapping names, both about "a student's attendance record for a course on a date." It's likely both were retrieved correctly and the model picked the wrong one of two near-identical tools — a model-reasoning failure rather than a retrieval miss, fixable by sharpening the distinction in both descriptions ("recording a new session" vs. "correcting an existing one").

The third is a faculty member asking how many students are "struggling or at risk of failing," expecting get_my_teaching_assignments plus get_students_needing_attention. Despite a synonym entry covering related vocabulary, this phrasing still didn't pull the right tool close enough. It's the one case combining a real vocabulary gap with a multi-tool requirement, and remains the hardest single query in the set.

Skipped · 7 cases, two different causes

Pool crowding (3 cases). Submitting CIA-2 marks, listing CS courses offered, and pulling a student's audit log are single-tool, plainly worded, with obvious vocabulary overlap (submit_internal_marks, get_course_catalog, get_audit_log). What they share is coming from roles with large pools (faculty and admin), where many other tools share words like "marks," "submit," or "log" with the expected one — a direct match gets crowded out by several near-matches that collectively push it outside the top-k, none individually outranking it. That's a top_k or anchor-threshold problem, not a vocabulary one.

Vocabulary gap (1 case). A student saying they were wrongly marked absent and asking to get it corrected, expecting submit_grievance. "Corrected" still doesn't connect to "grievance." A straightforward vocabulary gap, fixed by adding the phrasing to domain_synonyms.json.

Abstraction + multi-tool (3 cases). Multi-tool, heavily paraphrased queries — asking to "identify learners exhibiting concurrent academic underperformance and diminished instructional engagement," for "an institution-wide synopsis... encompassing academics, finances, infrastructure, and recruitment outcomes," and to "construct a consolidated representation of learner engagement, evaluative performance, and instructional progression." All three sit at the intersection of heavy abstraction and multiple required tools — the hardest combination for this curator.

Partially correct · 9 cases

Most are multi-tool queries missing one expected tool. Some use explicit conjunction markers the splitting step is designed for — "first check slots, then book," "and has any corrective action been logged" — yet still come back one short, either because the split fires but one half's retrieval still falls short, or, for plain "and," because it doesn't fire at all. Others are "give me everything" dashboard queries asking for three or four tools at once — a full institutional health briefing, a complete academic-plus-financial-plus-compliance picture — where the per-sub-query budget gets tight with three or four competing intents and a top-k of ten to fifteen. Typically two or three come through and the last doesn't.

07  Lessons learned

Lessons learned and practitioner guidance

The single largest move in this project wasn't a retrieval technique: it was rewriting tool descriptions.

Going from roughly 80% to a perfect baseline score with zero code changes means any curator's ceiling is set by how well the underlying tools describe themselves. Check description quality before tuning top_k, the anchor threshold, or anything else.

Two of the seven components came from testing rather than design, and both are worth remembering. The BM25 zero-score skip stops a tied, effectively random sparse ranking from injecting noise into queries with no keyword overlap — anyone combining dense and sparse retrieval should specifically test that case. Dependency expansion replaced TF-IDF's blanket action-tool inclusion, which worked but nearly doubled the effective tool count for every query regardless of need; the targeted version adds only what a selected tool's workflow depends on, both cheaper and, given the wrong-case count dropping from 9 to 3, more accurate.

Skipped cases come from two genuinely different problems needing different fixes. Vocabulary gaps, where query and description share no meaning even after embedding, get fixed in domain_synonyms.json. Pool crowding, where a direct match gets pushed out by several near-matches in a large pool, is a top_k or anchor-threshold question specific to roles with bigger pools. Reaching for the synonym file every time fixes one and does nothing for the other.

Compound splitting clearly helps but isn't complete: plain "and" doesn't trigger it by design, and three-or-four-intent "give me everything" queries can still run into a tight per-sub-query budget, where the remaining partially-correct cases concentrate. Separately, when two tools differ only in intent rather than subject matter — like the mark_attendance / update_attendance pair from the failure analysis — that distinction needs to be explicit in both descriptions, since the model's choice between them depends entirely on it.

Finally, separate curator failures from model failures when reading results. A "wrong" label doesn't always mean the curator missed the right tool — sometimes it was available and the model picked differently anyway. Lumping both together as "curator accuracy" risks tuning retrieval to fix a problem that actually lives in the descriptions or the model's reasoning.

08  Reuse

Reusability guide: applying this curator elsewhere

The retrieval algorithm doesn't know anything about universities. It operates on (name, description) pairs and a query string; everything domain-specific lives outside it, in files written for this testbed that need rewriting for anything else.

CategoryWhat's in itNotes
Carries over unchangedCurator interface, suggestion validator, hallucination guard, stdio transport patchDomain-agnostic; the transport patch only matters if the new project also runs MCP over stdio
Needs small editstool_curator/__init__.py, hybrid_rag_curator.py, usage logger, terminal display, tool collector, eval benchmarkStructure carries over; DEPENDENCY_TOOLS/QUERY_DEPENDENCY_HINTS and labels/sources need rewriting per domain
Rebuild from scratchMock database, all MCP servers, domain_synonyms.json, equivalence rules, tool cache, role policy, mutation guard, system promptRole filtering must still run before the curator, regardless of domain

The curator's only requirement is (name, description) pairs, which can come from MCP list_tools(), OpenAI tool specs, LangChain registries, or a database of tool metadata, as long as something converts them to that shape.

PieceUniversity (this project)Banking equivalent
Identity fieldsstudent_id, faculty_idcustomer_id, account_id
Example dependency ruleget_exam_eligibility → attendance + feesinitiate_fund_transfer → balance + beneficiary verification + limits
Example synonym entrysubmit_grievance: "raise complaint, lodge issue"block_debit_card: "lost card, stolen card, freeze card"
High-risk action toolssubmit marks, book a lab, file a grievancetransfer money, close an account, block a card

A healthcare deployment follows the same shape: patient_id/doctor_id as identity fields, prescribe_medication depending on patient history and allergies.

For TOOL_CURATOR_TOP_K: roughly six to eight for a small, easily-distinguished tool set, ten to fifteen for many similar tools (this project's situation), and ten plus dependency expansion for high-risk workflows. BAAI/bge-small-en-v1.5 was the default used throughout; bge-base-en-v1.5 trades speed for better accuracy on paraphrased queries, while all-MiniLM-L6-v2 was noticeably weaker in informal comparisons and wasn't used for any reported result.

If only the retrieval algorithm is needed:

minimal usage examplepython
from tool_curator import get_curator, load_tool_context

curator = get_curator(mode="rag", top_k=10,
                       model_name="BAAI/bge-small-en-v1.5", index_dir=".tool_index")

tools = [("get_account_balance", "Returns account balance for a customer account.")]
curator.build_index(tools, extra_context=load_tool_context())
selected_tools = curator.select("Can I send money to Rahul?", tools)

selected_tools is what gets passed to the LLM; role filtering, confirmation, and identity injection remain the calling application's job.

To be precise about the data contract: build_index receives a list of (name, description) tuples plus an optional extra_context dict mapping tool names to synonym strings. It has no return value — it mutates internal index state and writes the cache to disk. select receives the same query string and the same tool list (the role-filtered subset of the full registry) and returns a list of (name, description) tuples in ranked order, typically 10–18 items depending on anchor and dependency additions. The caller is responsible for serialising those tuples into whatever format the target LLM expects (function declarations, tool specs, etc.).

The results in Section 5 came from four things working together: tool names that map onto how users phrase requests, descriptions written as contracts, a synonym file bridging this domain's vocabulary gaps, and dependency rules reflecting how this domain's workflows chain together. FAISS, BM25, and RRF don't care what they're indexing, but reusing only the algorithm without rewriting the other three is likely to underperform what's reported here.

09  Limitations

Limitations and future work

1

The curator has been evaluated end to end against one deployment; the reuse guide hasn't been run against a second real one, so generalisation rests on design reasoning, and the clearest way to close that gap is running it against a second deployment — banking or healthcare. The seven internal components also haven't been ablated against each other, so the contribution of each to the 93.2% isn't known; removing one at a time and re-running the suite would show which pieces carry the accuracy.

2

All 281 evaluations were manual — reliable in that the same rubric was applied consistently, but slow, with some judgment in partial-credit cases. Replacing this with an automated harness, deterministic checks for expected tool names plus LLM-as-judge for free text, is the highest-value next step, both for speed and for testing synonym or dependency changes without a full manual pass.

3

The attendance-pair explanation in the failure analysis is inferred from tool similarity, not confirmed against call logs — a reading rather than a measurement, though the fix it points to (an explicit description pass for such pairs) is small and low-risk regardless. The system also runs entirely against an in-memory mock database, and none of the engineering — interface, harness, cache, guards — has run against real users or load. "Production-shaped" is accurate; "production-deployed" would not be.

4

The curator is stateless between turns. When the model proposes a follow-up ("would you like me to apply for that bonafide certificate?") and the user replies "yes," the curator retrieves on that short reply alone with no memory of what was suggested; conversation history sometimes lets the model recover, but this isn't guaranteed. The fix is concrete: store the last suggested tool(s) in session state, and force-include it when the next input looks like a short confirmation.

5

The pool-crowding skips were all from faculty or admin, the largest-pool roles, suggesting top_k and the anchor threshold could be set per role rather than globally. For the remaining extreme-paraphrase cases, a lightweight query-rewriting step before embedding, normalising unusually abstract phrasing, would scale better than anticipating every phrasing by hand in the synonym file.

10  Conclusion

Conclusion

The curator at the centre of this project is a small, swappable component — a two-method interface in front of a hybrid BGE/FAISS/BM25/RRF implementation, tested across three generations of the same 191-tool university system: a no-curator baseline reaching 100% only after description refinement (from roughly 80%), TF-IDF at 83.6% correct (88.4% accuracy score), and the hybrid curator at 93.2% correct (94.8% accuracy score) with an 86.8% token reduction.

The remaining nineteen cases split into patterns, not noise: near-identical tools the model confuses even when both are retrieved correctly, direct queries crowded out in large pools, genuine vocabulary gaps the synonym file hasn't covered, and multi-intent queries asking for more than the current splitting and budgeting handle reliably. Each points at a specific, different fix.

The reusability guide lays out the cost of pointing this curator at a different domain: a few files carry over unchanged, a handful need small edits, and three — the synonym file, the dependency rules, and the role and identity configuration — need rebuilding. The algorithm doesn't know or care what it's retrieving. The numbers above came from that algorithm combined with domain work specific to a university, on top of descriptions deliberately rewritten to be easy to tell apart. Reuse the algorithm freely; just budget for that other work wherever it lands next.

~20mscurator overhead per query
~15msBGE query encoding
<1msFAISS + BM25 search (each)
<2%of total response latency vs. 1–3s LLM time
↑ Back to top

A hybrid tool curator for agentic LLM systems — a model use case and reusability guide. Condensed documentation generated from the full project write-up.