Function calling and tool-augmented retrieval: the detective who knows which file to ask for
Contents
This post goes deeper into the agentic reasoning mechanism that extends the RAG described in RAG with reranker and hybrid retrieval. The retriever invoked when the LLM chooses
vector_searchis exactly the pipeline in that article. The JSON Schema that defines each tool call is structured output applied to the tool interface. And the agent’s requests pass through the L7 inference gateway before reaching the model.
TL;DR
A naive RAG always queries the same source. Function calling gives the LLM the ability to decide which tool to invoke (vector store, SQL, web search) according to what the query actually needs. The ReAct pattern chains those invocations in a reasoned loop until there is enough evidence. A 3-iteration pipeline with Llama-3.1-70B on on-premise hardware takes ≈ 1.1 s against the ≈ 300 ms of a single-step RAG; the gain is not in speed but in queries that a naive RAG simply cannot answer. The critical eval metric is tool selection accuracy: the percentage of turns in which the model picks the right tool, measured over a synthetic eval set.
The analogy: the detective who knows which file to ask for
A detective in a novel does not go to the same filing cabinet regardless of the case that lands on his desk. When he takes a case, he reasons first: what kind of evidence do I need? If there are witnesses, he asks for the statements (vector search over unstructured documents). If there are financial transactions, he asks the bank for the accounting records (SQL over the structured database). If the suspect has recent activity that the company cannot possibly have indexed, he goes to the press archive (web search). He does not consult all three sources at once on every case: he picks the one the evidence requires, receives the result, reasons again if he needs more, and only writes the report when he has enough evidence.
A bad detective always goes to the same filing cabinet. A naive RAG is that bad detective: it vectorises the query, goes to the vector store, and returns whatever it finds even if the question was “how many contracts?”, something no PDF chunk can answer better than a COUNT(*) in SQL.
Function calling is giving the LLM the ability to reason about which source to ask for, and to invoke it in a structured way. The analogy has three edges worth keeping in mind:
- The detective does not improvise the file he asks for: there is a catalogue of available sources with a description of what each one contains. The tool description in the system prompt does that job.
- The detective can ask for several pieces of evidence at once if they are independent (parallel tool calling).
- The detective knows when to stop: if after N rounds he reaches no conclusion, he declares that he does not have enough evidence. The agent has an iteration limit for the same reason.
What function calling is: the anatomy of a tool call
Function calling, also called tool use, is a mechanism by which the LLM, instead of generating free text as its answer, generates a structured JSON object representing a tool invocation. The system intercepts that JSON, executes the real tool, and returns the result as a message with the tool role in the conversation.
Defining tools in the system prompt
Each tool is defined through a JSON Schema that specifies name, description and parameters. This JSON Schema is exactly the same mechanism described in structured output, applied here to the tool interface:
{
"tools": [
{
"name": "vector_search",
"description": "Search internal company documents about policies, contracts and procedures. Use when the query requires unstructured text, document context or semantic similarity.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Natural language search query"},
"top_k": {"type": "integer", "default": 5}
},
"required": ["query"]
}
},
{
"name": "sql_query",
"description": "Query the SQL database for structured metrics, counts, aggregations and financial data. Use when the query requires exact numbers, filters, sums or joins over structured records.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Parameterized SQL query with $1, $2 placeholders"},
"params": {"type": "array", "items": {}, "description": "Parameter values for the placeholders"}
},
"required": ["query"]
}
},
{
"name": "web_search",
"description": "Search public web for real-time information, recent news or current prices. Use only when data is public and not covered by internal sources.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"}
},
"required": ["query"]
}
}
]
}
The life cycle of a tool call
When the LLM decides to invoke a tool, the message it generates in place of free text has this structure (OpenAI-compatible format, the same one vLLM supports):
{
"role": "assistant",
"tool_calls": [
{
"id": "call_01",
"type": "function",
"function": {
"name": "sql_query",
"arguments": "{\"query\": \"SELECT COUNT(*), SUM(amount) FROM contracts WHERE amount > $1 AND year = $2 AND provider_region = $3\", \"params\": [100000, 2025, \"EU\"]}"
}
}
]
}
The system executes the tool and returns:
{"role": "tool", "tool_call_id": "call_01", "content": "{\"count\": 47, \"total\": 8300000}"}
The LLM receives that message as a continuation of the conversation and decides whether it needs more information or can generate the final answer.
Support in OSS models
In 2026, native function calling support (not emulated via system prompt) is available in:
- Llama 3.1 / 3.3+: native tool call format, supported in vLLM with
--enable-auto-tool-choice --tool-call-parser llama3_json - Qwen 2.5+: native support with
--tool-call-parser hermes - Mistral NeMo / Mistral 7B Instruct v0.3+: native format with
--tool-call-parser mistral - Qwen3 (2025+): extended native support with parallel tool calling
Without --enable-auto-tool-choice, vLLM can force the tool call format via guided decoding (JSON Schema), but the ability to decide when to invoke a tool versus generating free text requires specific model training.
The ReAct pattern: Reasoning + Acting
ReAct (Yao et al., 2022) is the loop that turns an LLM with tools into an agent with reasoning. The central idea is that the model interleaves thoughts (explicit reasoning about what to do) with actions (tool invocations) and observations (results received). This loop repeats until the model reckons it has enough information to answer, or until the iteration limit is reached.
Thought → Action → Observation → Thought → Action → Observation → ... → Response
A full ReAct trace: a real production query
Query: “How many contracts above 100k€ did we sign in 2025 with EU providers, and which special conditions recur?”
This query requires two different sources: the exact numbers live in SQL, and the context of special conditions lives in the contract PDFs indexed in the vector store.
Iteration 1
Thought: The query asks for aggregated data (count and sum) with exact filters over structured fields (amount, year, region). This is a SQL query, not a semantic search.
Action: sql_query
SELECT COUNT(*) AS num_contratos, SUM(amount) AS total_eur
FROM contracts
WHERE amount > $1 AND year = $2 AND provider_region = $3
params: [100000, 2025, "EU"]
Observation: {"num_contratos": 47, "total_eur": 8300000}
Iteration 2
Thought: I have the numbers. Now I need the special conditions that recur in these contracts. That kind of information is in the free text of the PDFs, not in the structured table. I go to the vector store.
Action: vector_search
"EU provider contracts 2025 amount above 100k special conditions recurring clauses"
top_k: 5
Observation:
[chunk_1] Provider A contract — clause 8.3: late-delivery penalty of 2% per month on the invoiced amount...
[chunk_2] Provider C contract — clause 7.1: price revision linked to the euro area CPI capped at 4% per year...
[chunk_3] Provider B contract — clause 8.3: late-delivery penalty of 2% per month...
Thought: I have enough data from both sources. Clause 8.3 on late-delivery penalties appears in at least two of the retrieved contracts. I can synthesise the answer.
Response: “In 2025 you signed 47 contracts with EU providers above 100,000 €, for a cumulative total of 8.3 M€. The special condition that appears most often in the higher-value contracts is the late-delivery penalty clause (8.3 in the contract templates of Provider A and B): 2% per month on the invoiced amount. The price revision linked to the euro area CPI, capped at 4% per year (clause 7.1), also recurs.”
This trace has 2 iterations. A more complex query (comparing with the previous year, cross-referencing with payment status) would add further iterations. Common practice is to set a limit of 5–10 iterations maximum to avoid loops.
The three sources and when to use each one
The choice of source is neither arbitrary nor left solely to the LLM: the architecture defines which tools exist and how they are described. The table below summarises the selection criteria:
| Characteristic | Vector store (Qdrant) | SQL / structured (PostgreSQL) | Web search |
|---|---|---|---|
| Data type | Free text, documents, PDFs | Tables with a fixed schema | Public pages, news |
| Structure | Unstructured | Highly structured | Semi-structured |
| Temporality | Static index (periodic refresh) | Real time (transactional) | Real time (crawl) |
| Typical latency | 5–50 ms | 10–200 ms | 200–2,000 ms |
| Privacy | Internal data, full sovereignty | Internal data, full sovereignty | Public data only |
| Natural query | Yes (natural language → embedding) | No (parameterised SQL) | Yes |
| Exact aggregations | No | Yes (COUNT, SUM, GROUP BY) | No |
| When to use | Document context, semantics, PDFs | Metrics, counts, exact filters, joins | Data that does not exist internally and is public |
The most important rule of thumb: if the question contains words like “how many”, “sum”, “total”, “more than X”, “in year Y” and the data is in a structured table, the right answer is sql_query. If the question asks for context, explanations, clauses, procedures or examples from documents, the answer is vector_search. If it asks for the current price of something or recent news about a third party, web_search, but only if no data sovereignty is involved.
Tool routing: how the LLM picks the right tool
The description of each tool in the system prompt is the most critical factor for routing accuracy. An LLM with good function calling capabilities can still choose badly if the descriptions are ambiguous or overlap.
Descriptions that work versus those that do not
Weak description (leads the LLM to use the wrong tool):
"search_docs" — Searches for information in the available sources.
"query_data" — Gets data from the system.
Strong description (delimits precisely when to use each one):
"vector_search" — Search internal company documents about policies, contracts and procedures.
Use when the query requires unstructured text, document context or semantic
similarity. NOT for counts, sums or exact filters.
"sql_query" — Query the SQL database for structured metrics, counts, aggregations and
financial data. Use when the query requires exact numbers, filters, sums or
joins over structured records. NOT for finding document context.
The difference lies in two elements: (1) positive use-case examples, and (2) explicit exclusions with NOT for. Both reduce the semantic overlap between tools and improve tool selection accuracy.
Parallel tool calling
When two tools are independent of each other, that is, when the result of one does not affect the query of the other, the LLM can invoke them simultaneously in the same turn:
{
"tool_calls": [
{"id": "call_01", "function": {"name": "sql_query", "arguments": "..."}},
{"id": "call_02", "function": {"name": "vector_search", "arguments": "..."}}
]
}
The system executes both in parallel and returns both observations before the LLM’s next turn. This cuts total latency when the queries are independent: instead of 2 sequential iterations (2 × tool_latency), you pay 1 × max(sql_latency, vector_latency). For the detective example: if he needs both the accounting records and the witness statements to answer, he can ask for them at the same time.
Tool selection accuracy: the eval metric
Tool selection accuracy is the percentage of turns in which the LLM picks the right tool given a set of evaluated queries:
\[ \text{TSA} = \frac{\text{turns with correct tool chosen}}{\text{total turns with expected tool call}} \]It is measured over a synthetic eval set built with (query, expected_tool, expected_args) triples. An example of a minimal eval set:
| Query | Expected tool | Failure indicator |
|---|---|---|
| “How many orders in March?” | sql_query | LLM uses vector_search |
| “What does the returns policy say?” | vector_search | LLM uses sql_query |
| “What is the price of copper today?” | web_search | LLM uses vector_search |
| “Sum the Q3 contracts” | sql_query | LLM uses vector_search |
A TSA < 0.85 in a production agent is a sign that the tool descriptions need revising before the model does. For more detail on how to build these evals, see LLM evals.
Error handling in tool calls
SQL injection via prompt
The most serious risk in tool-augmented retrieval is that the LLM generates malicious SQL, either because a user induced it via prompt injection, or because the model hallucinated a destructive query. This attack vector is covered in detail in LLM Guard, but the minimum rules on the tool side are:
Rule 1: parameterised queries always, never direct interpolation.
# NEVER this:
cursor.execute(f"SELECT * FROM contracts WHERE provider = '{llm_output}'")
# Always this:
cursor.execute("SELECT * FROM contracts WHERE provider = $1", [llm_output])
Rule 2: DB user with minimum permissions. The user the agent runs SQL as must have SELECT on the necessary tables and nothing else. No DROP, INSERT, UPDATE or DELETE.
Rule 3: table allowlist. The system validates that the query generated by the LLM only references tables on an allowlist before executing it.
Rule 4: per-query timeout. Queries that block for too long, potentially induced to DoS the database, are cancelled with a configured timeout.
Rate limits, timeouts and tool errors
When a tool fails, the error is returned to the LLM as an observation:
{"role": "tool", "tool_call_id": "call_01", "content": "{\"error\": \"timeout after 5s\", \"tool\": \"web_search\"}"}
The system prompt must instruct the LLM on what to do in this case:
If a tool returns an error or is unavailable, acknowledge the limitation in your response.
Do not retry more than once. If web_search is unavailable, state that real-time data
is not accessible at this moment and answer with available internal sources only.
This stops the agent from entering retry loops and handles graceful degradation: if web_search is unavailable, it answers with what it has in the internal sources.
Diagram of the ReAct loop with the three sources
Latency maths of the ReAct pipeline
Each iteration of the ReAct loop has three latency components:
\[ T_{\text{iter}} = \text{TTFT}_{\text{LLM}} + T_{\text{tool}} + \Delta_{\text{context}} \]where:
- TTFT_LLM: time to the LLM’s first token (dominated by the prefill of the accumulated context)
- T_tool: tool execution time
- Δ_context: overhead of the growing context window (each iteration adds the previous output to the context)
Reference values: Llama-3.1-70B on 4×H100 SXM (320 GB, NVLink)
With Llama-3.1-70B in FP8 on a node with 4×H100 SXM (320 GB HBM3, NVLink 900 GB/s), the typical values in production are:
| Component | Value |
|---|---|
| TTFT with context < 4k tokens | ≈ 150 ms |
| TTFT with context 8k tokens | ≈ 220 ms |
sql_query (simple query, indexed) | ≈ 50 ms |
vector_search (top-5, Qdrant in RAM) | ≈ 20 ms |
web_search (external API) | ≈ 600 ms |
Total latency by number of iterations
1-iteration pipeline (simple query, a single tool):
\[ T_1 = 150 + 50 = 200 \text{ ms} + \text{final synthesis} \approx 200 + 300 = 500 \text{ ms} \]2-iteration pipeline (SQL + vector_search in sequence):
\[ T_2 = (150 + 50) + (180 + 20) + 400 = 800 \text{ ms} \]The context in the second iteration already includes the result of the first, so TTFT rises slightly to ≈ 180 ms.
3-iteration pipeline (the most common case in complex queries):
\[ T_3 = (150 + 50) + (180 + 20) + (200 + 50) + 450 \approx 1,100 \text{ ms} \]Parallel tool calling (SQL + vector_search in parallel, a single iteration):
\[ T_{\text{parallel}} = 150 + \max(50, 20) + 400 = 600 \text{ ms} \]When the two queries are independent, parallel tool calling trims latency from ≈ 800 ms to ≈ 600 ms: a 25% improvement over the 2 sequential iterations case.
Comparison with a naive RAG
| Configuration | Latency | Queries it can answer |
|---|---|---|
| Naive RAG (1 retriever, 1 step) | ≈ 300 ms | Document context queries |
| ReAct 1 iteration (SQL) | ≈ 500 ms | Structured aggregation queries |
| ReAct 2 iterations (SQL + vector) | ≈ 800 ms | Hybrid numeric + context queries |
| ReAct 3 iterations | ≈ 1,100 ms | Complex multi-source queries |
| ReAct with web_search | ≈ 1,500 ms | Queries requiring real-time data |
The practical conclusion: multi-hop ReAct is 3–5× slower than a single-step naive RAG. The gain is not in speed but in query coverage: a naive RAG cannot answer “how many contracts?” because that answer is in no text chunk at all. For applications with a strict latency SLO (< 500 ms), you have to work out whether the use case really needs ReAct or whether a well-configured RAG with hybrid retrieval covers 90% of the queries.
On-premise hardware for ReAct agents
A ReAct agent with Llama-3.1-70B in production has different requirements from a naive RAG, because the context grows with each iteration and prefill throughput is more critical.
Recommended configuration: 4×H100 SXM (320 GB HBM3, NVLink 900 GB/s)
- Llama-3.1-70B in FP8: fits in 2×H100 (70B params × 1 byte/param ≈ 70 GB + KV cache). With 4×H100 you can serve it with tensor parallelism TP=4, cutting prefill TTFT by ≈ 2×.
- Qdrant instance: it can sit on the same node (if the collection fits in RAM) or on a dedicated node. For collections < 50M vectors of 768 dims: ≈ 150 GB, which fits in the RAM of a dual-socket server.
- PostgreSQL: separate node or managed instance. The agent adds no unusual load to SQL; the queries are simple and bounded by timeout.
- vLLM with
--enable-auto-tool-choice --tool-call-parser llama3_json --max-model-len 16384: a 16k token context comfortably covers the 5–10 turns of a ReAct pipeline.
Minimum viable configuration: 2×H100 SXM (160 GB)
- Llama-3.1-70B in FP8 with TP=2. TTFT ≈ 250–300 ms for 4k token contexts (a 60–100% increase over TP=4).
- Good enough for internal workloads with < 20 concurrent requests.
- Not advisable for an SLO < 1 s with more than 5 concurrent users and a long context.
What we have not covered
Agentic retrieval loops with planning. ReAct is the simplest agent pattern. When a query requires decomposition into sub-tasks with dependencies, you need orchestration frameworks such as LangGraph (state graphs), smolagents (Hugging Face, agents with Python code as actions) or llama-index Agents (planning + retrieval pipeline). These introduce a planning step before the execution loop.
MCP (Model Context Protocol). Anthropic’s emerging standard, with OSS implementations, for defining tools portably across frameworks and hosts. Instead of defining each tool’s JSON Schema separately in every application, MCP centralises those definitions in an MCP server that any compatible client can discover and invoke. Adoption in 2025–2026 is fast among OSS frameworks (LangChain, smolagents, OpenWebUI).
Tool caching. If the same tool call (same arguments, same tool) is going to be invoked several times within the same context or in very similar contexts, the result can be cached. The mechanism is analogous to the semantic cache described for RAG: before executing the tool, the hash of the arguments (or its embedding, for semantic matching) is compared against a cache with a TTL. Especially valuable for sql_query with frequent queries and data that changes little.
Multi-agent. When an orchestrating agent delegates sub-tasks to specialised agents, one for SQL, one for document retrieval, one for code generation, you enter multi-agent system territory. Each sub-agent can have its own tool set and its own LLM (possibly smaller and more specialised). Coordination between agents introduces additional tracing and observability complexity.
See also
- RAG with reranker and hybrid retrieval — the retriever invoked when the LLM chooses
vector_searchis exactly the pipeline described there: dense + sparse + cross reranker - Structured output: fundamentals — the JSON Schema that defines the contract of each tool call is exactly structured output applied to the tool interface
- Inference router and L7 LLM gateway — the L7 gateway that receives the ReAct agent’s requests and routes them to the right LLM; it also applies rate limiting per user and tenant
- LLM Guard: fundamentals — SQL injection via prompt is a real attack vector in tool-augmented retrieval; LLM Guard covers prompt injection detection before the request reaches the LLM
- Six-stage LLMOps pipeline — tool-augmented retrieval lives at the intersection of the pipeline’s Deploy and Observe stages: it is deployed as part of the inference system and observed via tracing of each agent turn
- LLM evals: the layer after tracing — tool selection accuracy is the critical eval metric for a ReAct agent; the golden eval dataset must include (query, expected tool, expected args) triples
References
- Yao, S. et al. (2022). ReAct: Synergizing Reasoning and Acting in Language Models. arXiv:2210.03629. https://arxiv.org/abs/2210.03629
- vLLM documentation. Tool calling. https://docs.vllm.ai/en/stable/features/tool_calling.html
- Qdrant documentation. Search. https://qdrant.tech/documentation/concepts/search/
- OpenAI. Function calling. https://platform.openai.com/docs/guides/function-calling
- Meta AI. Llama 3.1 Model Card. https://github.com/meta-llama/llama-models/blob/main/models/llama3_1/MODEL_CARD.md
- Qwen Team (Alibaba). Qwen2.5 Technical Report. arXiv:2412.15115. https://arxiv.org/abs/2412.15115
- Anthropic. Model Context Protocol. https://modelcontextprotocol.io
- OWASP. LLM Top 10 for Large Language Model Applications. LLM01: Prompt Injection. https://owasp.org/www-project-top-10-for-large-language-model-applications/