AI & LLM Engineering65 min total · 11 parts
AI & LLM Engineering Fundamentals: Prompting, RAG, Embeddings, and Function Calling
Part 7 of 11 · ~3 min
Function Calling and Tool Use
Everything so far answers questions from static help-center content. The next request Loopwork's support lead made was for something RAG structurally cannot do: "How many projects am I using out of my plan's limit?" is not a fact sitting in any document — it's a live number in Loopwork's own database, different for every customer, changing constantly. Retrieval finds text that already exists somewhere. This needs the model to trigger a real lookup.
Function calling (also called tool use) is how a model is given the ability to request that, without ever executing anything itself. You describe a function's name, purpose, and parameters in a schema; the model, mid-conversation, can respond not with an answer but with a structured request to call one of those functions; your backend is the one that actually runs it, with real code and real permission checks, and feeds the result back in for the model to finish its answer with.
const TOOLS = [
{
name: "get_plan_usage",
description: "Returns the current customer's project count and plan limit.",
parameters: { type: "object", properties: {}, required: [] },
},
];
async function askLoopWithTools(question, authenticatedCustomerId) {
const messages = [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: question },
];
const first = await callModel(messages, { tools: TOOLS });
if (first.tool_call?.name === "get_plan_usage") {
// The model asked for the customer's usage. It did NOT provide, and is
// never asked for, which customer — that comes from the session, not
// from anything the model said. More on exactly why in a moment.
const usage = await getPlanUsage(authenticatedCustomerId);
messages.push(first, { role: "tool", name: "get_plan_usage", content: JSON.stringify(usage) });
return callModel(messages); // second call: model turns the result into a sentence
}
return first;
}
Two calls to the model, one real function execution sandwiched in between. The first call is the model deciding whether a tool is needed and, if so, which one. The second is it turning a raw JSON result — { projects: 7, limit: 10 } — into "You're using 7 of your 10 available projects." Neither call runs any code; getPlanUsage is an ordinary function in your own backend, and the model has no way to reach past that boundary.
That boundary is the entire point, and it's worth stating as plainly as possible: the model proposes; your backend decides. A schema for getInvoice(customerId, invoiceId) might technically let the model ask for any customerId it likes — including one it invents, or one copied from earlier in a conversation with a different customer. The version above sidesteps that entirely by never trusting a model-supplied customerId in the first place: the real, authenticated customer ID comes from the session that's already been through your own login and authorization checks, and the tool-execution code substitutes that in regardless of what the model's tool-call arguments actually contained. If a schema needs a customer-scoped parameter at all, the fix is identical to how any backend handles a client-supplied ID: verify it against the authenticated session before the underlying function runs a single line, exactly the way you'd never trust a client-submitted user ID in an ordinary REST request either. A tool call is a request from the model, not a credential, and it gets zero more trust than any other unauthenticated input — arguably less, since it was generated by a system explicitly trained to sound plausible rather than to be correct.
The same discipline decides what tools get built in the first place. getPlanUsage and getInvoice are read-only — there's no path by which calling them changes anything, so the worst a bug or a bad model decision can do is show someone data instead of committing an unwanted action. A hypothetical issueRefund tool is an entirely different risk class, and chapter eight is about exactly why that one shouldn't be a tool the model can call directly at all.