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.
Search across all documentation pages
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.
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.
query (required), top_k, optional filters (corpus, tenant applied server-side).ok, results[] with id, text, score, metadata, and error when failing.FunctionTool, MCP server, etc.).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 idsAlways-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.
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.
| 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 |
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.
| 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.
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.
results is empty.| 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 |
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.
A full natural-language question works best for embeddings. Ultra-short keyword bags lose semantics; multi-paragraph pastes add noise.
Start with 3-5 for agent tools. Raise only when evals show missed evidence and the model can still handle the tokens.
Cap tool calls per run, detect repeated identical queries, and force a final answer or escalate path.
Yes. Split by corpus (search_policies, search_runbooks) with non-overlapping descriptions.
Inside the tool after first-pass retrieval, before returning results to the model.
Structured ok: false with error_type and retryable. Empty prose errors cause hallucinations.
No. MCP is a transport for exposing tools. The same search_knowledge function can be local or MCP-served.
Unit-test the function with a fixture index. Separately test that the agent selects the tool on golden prompts with stubbed retrieval.
Optional. Show citations and titles to users; keep raw scores in logs and debug UIs.
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.
Reviewed by Chris St. John·Last updated Jul 16, 2026