Building a Retrieval Tool an Agent Can Call
A vector index is not an agent feature until the model can invoke it with the right arguments and receive usable evidence.
This page wraps similarity search as a tool: name, description, parameters, host-side execution, and a structured observation.
Summary
Implement search_knowledge(query, top_k, filters) in your host, back it with embed + vector query, return ids/text/scores/metadata, and register it with your agent framework under a precise tool description.
Recipe
- Pin embed model id, distance metric, and collection/index name.
- Define a small JSON-serializable argument schema:
query(required),top_k, optional filters (corpus,tenantapplied server-side). - Implement host logic: authorize caller → embed query → vector search → optional hybrid/rerank → cap result size.
- Return a uniform envelope:
ok,results[]withid,text,score,metadata, anderrorwhen failing. - Write tool name + description with triggers and non-goals (not for live orders; not for other tenants).
- Register the tool in your agent runtime (function calling, LangGraph node, LlamaIndex
FunctionTool, MCP server, etc.). - Log query, filters, hit ids, and latency; add max tool-call budgets in the agent loop.
- Evaluate with golden questions that must call the tool and cite real ids.
Working Example
Provider-agnostic host function (wire your real store client where noted):
from __future__ import annotations
from typing import Any
# embed_query / vector_search are adapters you implement for OpenAI+Pinecone,
# Chroma, pgvector, etc. Keep secrets on the server only.
def embed_query(text: str) -> list[float]:
raise NotImplementedError # call your embedding API
def vector_search(
vector: list[float],
*,
top_k: int,
filters: dict[str, Any],
) -> list[dict[str, Any]]:
raise NotImplementedError # return [{id, text, score, metadata}, ...]
def search_knowledge(
query: str,
top_k: int = 4,
corpus: str | None = "policies",
) -> dict[str, Any]:
"""
Search internal documentation and policies by semantic similarity.
Use for refund rules, support hours, plan limits, and product behavior.
Do not use for live order status, payments, or other customers' data.
"""
if not query or not query.strip():
return {"ok": False, "error_type": "bad_request", "message": "query required"}
top_k = max(1, min(int(top_k), 10))
filters: dict[str, Any] = {}
if corpus:
filters["corpus"] = corpus
# Inject tenant from auth context in real systems - never trust model-supplied tenant ids alone.
try:
vec = embed_query(query.strip())
hits = vector_search(vec, top_k=top_k, filters=filters)
except Exception as exc: # narrow in production
return {
"ok": False,
"error_type": "upstream",
"retryable": True,
"message": "retrieval failed",
"detail": str(exc)[:200],
}
return {
"ok": True,
"query": query.strip(),
"results": [
{
"id": h["id"],
"text": h["text"][:2000],
"score": h.get("score"),
"metadata": h.get("metadata") or {},
}
for h in hits
],
}Register with an OpenAI-style tools list (shape varies by SDK - verify at build):
retrieval_tool_schema = {
"type": "function",
"function": {
"name": "search_knowledge",
"description": (
"Semantic search over internal policies and product docs. "
"Use when the user asks about refunds, support hours, or plan rules. "
"Not for live account balances or order tracking."
),
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Full natural-language question to embed and search.",
},
"top_k": {
"type": "integer",
"minimum": 1,
"maximum": 10,
"default": 4,
},
"corpus": {
"type": "string",
"enum": ["policies", "product", "runbooks"],
"description": "Which document partition to search.",
},
},
"required": ["query"],
"additionalProperties": False,
},
},
}
TOOL_IMPL = {"search_knowledge": search_knowledge}
def dispatch_tool(name: str, args: dict[str, Any]) -> dict[str, Any]:
fn = TOOL_IMPL.get(name)
if not fn:
return {"ok": False, "error_type": "unknown_tool", "message": name}
return fn(**args)Minimal agent-loop sketch:
# Pseudocode - use your framework's native tool loop when possible
messages = [{"role": "user", "content": "Can I get a refund after 12 days?"}]
# 1) model returns tool_call name=search_knowledge args={query: "..."}
# 2) observation = dispatch_tool(name, args)
# 3) append tool result; model writes final answer citing result idsDeep Dive
Why a tool instead of silent always-on RAG?
Always-on RAG packs chunks every turn.
That is fine for pure FAQ bots.
Multi-tool agents also need CRM lookups, tickets, calculators, and escalation.
A callable retrieval tool lets the model skip search when the question is operational, not documentary.
Description quality drives selection
Weak: "search docs".
Strong: scope (which corpus), example intents, and explicit non-goals.
One tool per coherent corpus often beats one mega-tool over unrelated folders.
See tool-description habits in the function-calling section of this guide family.
Arguments the host should own
| Field | Model may set? | Notes |
|---|---|---|
query | Yes | Prefer full questions over keyword salad |
top_k | Optional | Cap hard in host |
corpus | Optional enum | Keep list short |
tenant_id | No (from auth) | Prevents cross-tenant probes |
| Admin filters / ACL | No | Derived from session identity |
Result shape the model can use
Return evidence, not a second LLM summary, unless you intentionally add a synthesize step.
Ids and short texts let the agent cite sources.
Truncate oversized chunks so one hit cannot flood the context window.
Framework mapping
| Runtime | Typical wrap |
|---|---|
| OpenAI Agents SDK / Responses tools | Function tool + host dispatcher |
| LangGraph | Node or @tool bound to the model |
| LlamaIndex | FunctionTool / QueryEngineTool |
| CrewAI | Tool class with description |
| MCP | Server tool exposing the same contract |
| Custom loop | JSON schema + allowlisted dispatch |
Keep the retrieval core framework-free so you can swap orchestrators.
Composing with hybrid and re-rank
Call hybrid merge and re-ranking inside the tool implementation.
The model should not orchestrate BM25 vs vector unless you deliberately expose advanced knobs to power users.
Gotchas
- Vague tool descriptions cause wrong-tool calls. Rewrite from production traces.
- Trusting model-supplied tenant filters. Bind tenant from auth context only.
- Returning huge raw PDFs. Chunk and truncate; keep top_k bounded.
- Embedding model drift. Query embed must match the index model.
- Empty hits with no error. Teach the agent to escalate or say unknown when
resultsis empty. - No timeouts. Vector and embed APIs hang; set deadlines and retry policy.
- Logging PII from queries and chunks. Redact in traces.
- Treating scores as calibrated probabilities. They are ranking signals, not truth.
Alternatives
| Approach | Pros | Cons |
|---|---|---|
| Callable retrieval tool | Optional, multi-tool friendly | Model may forget to call it |
| Always-on RAG packer | Simple Q&A | Wastes tokens; weak multi-tool |
| Query engine tool (LlamaIndex) | Batteries-included retrieve+synthesize | Heavier dependency |
| Multiple corpus tools | Clear routing | More descriptions to maintain |
| Router over indexes | One entrypoint | Extra moving part |
FAQs
Should the tool synthesize the final answer?
Usually no for multi-tool agents. Return evidence and let the agent (or a dedicated synthesizer step) write the user-facing answer. For pure FAQ bots, retrieve+synthesize in one engine is fine.
How long should `query` be?
A full natural-language question works best for embeddings. Ultra-short keyword bags lose semantics; multi-paragraph pastes add noise.
What top_k should I default to?
Start with 3-5 for agent tools. Raise only when evals show missed evidence and the model can still handle the tokens.
How do I stop infinite search loops?
Cap tool calls per run, detect repeated identical queries, and force a final answer or escalate path.
Can one agent have several retrieval tools?
Yes. Split by corpus (search_policies, search_runbooks) with non-overlapping descriptions.
Where do I put re-ranking?
Inside the tool after first-pass retrieval, before returning results to the model.
How should failures look?
Structured ok: false with error_type and retryable. Empty prose errors cause hallucinations.
Is MCP required?
No. MCP is a transport for exposing tools. The same search_knowledge function can be local or MCP-served.
How do I test the tool without the full agent?
Unit-test the function with a fixture index. Separately test that the agent selects the tool on golden prompts with stubbed retrieval.
Should scores be shown to users?
Optional. Show citations and titles to users; keep raw scores in logs and debug UIs.
Related
- Vector Databases & RAG Basics - embed and search fundamentals
- How Vector Search Gives Agents a Searchable Memory - why retrieval is memory
- Hybrid Search: Combining Vector Similarity with Keyword Filters - richer first pass
- Re-Ranking Retrieved Results Before Passing Them to an Agent - precision pass
- Building a Query Engine Agent Over Your Own Data - LlamaIndex-shaped variant
- Writing Tool Descriptions the Model Will Actually Use Correctly - description craft
- Vector Databases & RAG Best Practices - ops checklist
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.