11-tooling-cloud-ai-awareness

AI Awareness for Backend Engineers: LLMs and Tool Calling

A backend-focused primer on LLMs, tokens, prompt engineering, hallucinations, function calling, and safely integrating an LLM API into a service.

August 14, 2026
backend-engineeraillmprompt-engineeringfunction-callingtool-calling

AI Awareness for Backend Engineers

You don't need to train a model or understand transformer attention math to ship an AI feature responsibly. What you need — and what most backend teams are missing — is the same rigor you already apply to any external dependency: know its failure modes, bound its cost, validate its output, and never trust it with more authority than it's earned. This guide gives you exactly enough LLM literacy to design, review, and operate an AI-adjacent feature in a backend service. It is deliberately not a full LLM engineering course — for that depth (RAG architectures, fine-tuning, evaluation harnesses, agent frameworks), this site's dedicated Gen AI track goes much further.


1. What an LLM Actually Is, at the Level You Need

A large language model is a function: given a sequence of text, it predicts a probability distribution over the next token, samples one, and repeats. That's it. Everything else — chat behavior, "reasoning," tool use — is built on top of that one primitive, shaped by how the model was trained (pretraining on massive text corpora, then fine-tuned on instruction-following and human-preference data).

💡

The practical consequence of "next-token prediction": an LLM has no concept of "I don't know." It generates the statistically plausible continuation of the text so far, whether or not that continuation is factually true. This single fact explains most of what you need to design around — hallucination, why grounding matters, and why validation is not optional.

The API shape you'll actually integrate against

Every major LLM provider (Anthropic, OpenAI, and others) exposes essentially the same shape: you send a list of role-tagged messages, you get back generated text (optionally streamed token-by-token) plus usage metadata.

json
{
  "model": "claude-sonnet-4-5",
  "max_tokens": 1024,
  "system": "You are a support-ticket summarizer. Output valid JSON only.",
  "messages": [
    { "role": "user", "content": "Summarize: Customer reports checkout button unresponsive on mobile Safari after promo code applied." }
  ]
}

2. Tokens and Context Windows

Models don't operate on characters or words — they operate on tokens, sub-word chunks produced by a tokenizer. This matters for three very concrete backend reasons: cost, latency, and hard input limits.

ConceptWhat it meansBackend implication
TokenA sub-word unit (roughly 4 characters / 0.75 words in English)Pricing and limits are measured in tokens, not characters
Context windowMaximum tokens (input + output combined) a model can process in one requestA large document or long chat history can silently exceed the limit
Input tokensEverything you send: system prompt, history, user message, tool definitionsEvery retry, every few-shot example, every tool schema costs tokens
Output tokensEverything the model generatesUsually priced higher per-token than input; max_tokens caps this
java
// Rough token estimation for cost/limit checks before calling the API —
// use the provider's official tokenizer for anything precision-critical
int estimatedTokens = text.length() / 4;  // ballpark only, not exact
⚠️

A context window overflow doesn't fail gracefully by default in every integration path. Depending on the client and provider, sending more tokens than the model supports either truncates silently or returns a hard error. If your feature concatenates unbounded user content (a long support thread, a large document) into a prompt, enforce your own length limit and truncate/summarize deliberately — don't let the provider's limit be your first line of defense.


3. Prompt Engineering Fundamentals

A prompt is the only interface you have to steer model behavior — there's no config file, no method override. Treat it like an API contract you're writing against an unpredictable collaborator.

The three roles

RolePurposeBackend pattern
SystemSets persistent behavior, constraints, and output format for the whole conversationDefined once per feature, not per request — e.g., "You are a JSON-only classifier"
UserThe actual request/content to act onBuilt from request payload, user input, or retrieved context
AssistantThe model's prior responses (in multi-turn conversations)Included when maintaining conversational state

Practical techniques that consistently work

text
BAD (vague, underspecified):
"Summarize this support ticket."
 
BETTER (explicit format, constraints, and failure behavior):
"Summarize the support ticket below in exactly 2 sentences.
Extract the customer's core issue and any error messages verbatim.
If no clear issue is stated, respond with exactly: {"issue": "unclear"}.
Respond only with valid JSON matching this schema: {"issue": string, "severity": "low"|"medium"|"high"}.
 
Ticket: <ticket text>"
TechniqueWhat it doesWhen to use
Be explicit about output formatReduces parsing failures downstreamAny time you programmatically consume the response (almost always, in a backend service)
Few-shot examplesShow 2-3 input/output pairs in the promptAmbiguous classification/extraction tasks
Constrain the failure modeTell the model exactly what to output when it's unsurePrevents free-text hedging that breaks a JSON parser
Separate instructions from dataUse clear delimiters (<ticket>...</ticket>, XML tags, or a dedicated system prompt)Prevents user-supplied content from being interpreted as instructions
Ask for structured output / tool useUse the provider's native structured-output or tool-calling feature instead of "please output JSON"Whenever the API supports it — far more reliable than prompt-only formatting requests

Prefer the provider's native structured-output or tool-calling mode over asking nicely for JSON in the prompt. Native structured output is validated against a schema server-side and is dramatically more reliable than hoping the model doesn't wrap its JSON in a markdown code fence or add a conversational preamble.


4. Why Hallucinations Happen, and How to Mitigate Them

A hallucination is the model generating fluent, confident, and factually wrong output — a fabricated API method, a citation to a paper that doesn't exist, a summary that includes a detail never present in the source. This isn't a bug in a particular model; it's an inherent property of next-token generation without a built-in fact-checking mechanism.

Mitigation strategies, from cheapest to most involved

StrategyHow it helpsEffort
Grounding — supply the actual source data in the prompt (retrieved docs, DB records) rather than relying on the model's training knowledgeThe model summarizes/extracts from provided text instead of recalling from memoryLow-medium (requires a retrieval step)
Ask the model to cite/quote its sourceForces traceability; a claim without a matching quote is a red flagLow
Constrain the task to extraction/classification over generationExtracting from provided text hallucinates far less than open-ended generationLow (design choice)
Lower temperature for factual tasksReduces creative sampling variance for tasks that need consistency, not creativityTrivial (a request parameter)
Validate output against ground truthProgrammatically check IDs, dates, amounts against your database before acting on themMedium
Human-in-the-loop for high-stakes actionsA person confirms before the model's output triggers a refund, an email send, or a data mutationMedium-high, but often mandatory
🚨

Never let raw LLM output directly trigger an irreversible action — a payment, an email to a customer, a database write, an account deletion — without validation or human confirmation in between. Treat the model's output the same way you'd treat unvalidated input from an untrusted client: parse it, validate it against your actual business rules and data, and only then act.


5. Function / Tool Calling

Tool calling (also called function calling) is how you let a model take actions or fetch live data instead of only generating text. You describe available functions with a schema; the model decides when to invoke one and with what arguments; your code executes the actual function — the model never runs anything itself.

Defining a tool

json
{
  "name": "get_order_status",
  "description": "Look up the current status and ETA for a customer order by its order ID.",
  "input_schema": {
    "type": "object",
    "properties": {
      "orderId": {
        "type": "string",
        "description": "The order ID, e.g. '4521'."
      }
    },
    "required": ["orderId"]
  }
}
⚠️

The model decides when to call a tool and what arguments to pass — but it never executes anything. Your backend code executes the function and is fully responsible for authorization, input validation, and rate limiting on that call, exactly as if the arguments came from any other untrusted client. A model can hallucinate an order ID that doesn't belong to the requesting user; your getOrderStatus implementation must still enforce that the caller is authorized to see that order.

Design principleWhy
Give tools narrow, specific scopesgetOrderStatus(orderId) is safer and more predictable than a generic runQuery(sql) tool
Never expose a raw SQL/shell-execution tool to a modelThis is effectively giving an unpredictable, prompt-injectable caller direct database/system access
Validate and authorize every tool argument server-sideThe model's arguments are untrusted input, full stop
Return structured, minimal data from toolsKeeps token usage and cost down, and reduces surface area for the model to misinterpret

6. Wiring an LLM API into a Spring Boot Service

Once you get past the prompt, an LLM API call is an external HTTP dependency — treat it with the same production discipline as any third-party API call: timeouts, retries, circuit breaking, and observability.

java
@Configuration
public class LlmClientConfig {
 
    @Bean
    RestClient llmRestClient(@Value("${llm.api.base-url}") String baseUrl,
                              @Value("${llm.api.key}") String apiKey) {
        return RestClient.builder()
            .baseUrl(baseUrl)
            .defaultHeader("x-api-key", apiKey)
            .defaultHeader("anthropic-version", "2023-06-01")
            .requestFactory(clientHttpRequestFactory())
            .build();
    }
 
    private ClientHttpRequestFactory clientHttpRequestFactory() {
        var factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(3_000);   // fail fast on connection issues
        factory.setReadTimeout(20_000);     // generation can legitimately take several seconds
        return factory;
    }
}
java
@Service
public class TicketSummarizerService {
 
    private final RestClient llmRestClient;
    private final MeterRegistry meterRegistry;
 
    public TicketSummarizerService(RestClient llmRestClient, MeterRegistry meterRegistry) {
        this.llmRestClient = llmRestClient;
        this.meterRegistry = meterRegistry;
    }
 
    @CircuitBreaker(name = "llmApi", fallbackMethod = "fallbackSummary")
    @Retry(name = "llmApi")
    @RateLimiter(name = "llmApi")
    public TicketSummary summarize(String ticketText) {
        // 1. Bound the input — never send unbounded user content
        String truncated = truncateToTokenBudget(ticketText, 4_000);
 
        long start = System.nanoTime();
        LlmResponse response = llmRestClient.post()
            .uri("/v1/messages")
            .body(buildRequest(truncated))
            .retrieve()
            .body(LlmResponse.class);
        meterRegistry.timer("llm.request.latency").record(Duration.ofNanos(System.nanoTime() - start));
        meterRegistry.counter("llm.tokens.used",
            "type", "total").increment(response.usage().totalTokens());
 
        // 2. Validate before trusting the output
        TicketSummary summary = parseAndValidate(response.text());
        return summary;
    }
 
    private TicketSummary fallbackSummary(String ticketText, Throwable t) {
        // Degrade gracefully — never let an AI feature take down the whole request
        return TicketSummary.unavailable();
    }
}
yaml
# application.yml — Resilience4j config for the LLM dependency
resilience4j:
  circuitbreaker:
    instances:
      llmApi:
        sliding-window-size: 20
        failure-rate-threshold: 50
        wait-duration-in-open-state: 30s
  retry:
    instances:
      llmApi:
        max-attempts: 2
        wait-duration: 500ms
        retry-exceptions:
          - java.io.IOException
          - org.springframework.web.client.HttpServerErrorException
  ratelimiter:
    instances:
      llmApi:
        limit-for-period: 50
        limit-refresh-period: 1m
        timeout-duration: 0s
⚠️

Only retry idempotent, safe-to-repeat LLM calls, and never retry on a 4xx. A transient network error or 5xx is retry-safe. A 429 (rate limited) needs backoff, not an immediate retry. And retrying blindly on every failure multiplies your token spend on every transient blip — cap retry attempts low (1-2) and use a circuit breaker so a struggling provider doesn't cascade into your own service's latency budget.

Cost control checklist

ControlImplementation
Cap max_tokens on every requestBounds worst-case output cost and latency per call
Bound input size before sendingTruncate/summarize unbounded user content; don't let a 50-page document silently blow the budget
Cache repeated prompts/resultsIdentical classification/summarization requests shouldn't re-call the API
Track token usage as a first-class metricEmit llm.tokens.used per feature so cost is visible in the same dashboards as latency
Rate-limit per user/tenantPrevents a single caller (or a runaway loop) from consuming your entire budget
Set a hard monthly spend alert with the providerA safety net for bugs that slip past the above

PII and data handling

🚨

Treat every LLM API call as sending data to a third party, because it is one — even if it's the same company. Before sending request content:

  • Strip or mask PII you don't need the model to see (SSNs, full card numbers, raw passwords).
  • Check your provider's data retention and training-use policy — many enterprise API tiers explicitly do not train on your data, but confirm this for your specific contract/tier rather than assuming it.
  • Log prompts and responses carefully: application logs are often broader-access than production databases, and logging full user PII into a shared log aggregator can itself be a compliance violation.
  • For regulated data (health, financial), confirm the provider and your usage meet the applicable compliance framework (HIPAA, PCI-DSS, etc.) before sending real data — not after.

7. Observability for AI Features

An LLM call is a dependency like any other, but its failure modes are different enough to need their own signals.

SignalWhy it's distinct from a normal API dependency
Token usage per request/featureDirectly maps to cost — no equivalent in a typical REST dependency
Latency distribution, not just p50Generation latency scales with output length; p99 can be many multiples of p50
Output validation failure rateHow often does the model's output fail your schema/business validation? Rising trend = prompt regression or model drift
Fallback/circuit-breaker trip rateHow often are users getting the degraded, non-AI experience?
Refusal rateHow often does the model decline to answer? Can indicate a prompt that's inadvertently triggering safety behavior

Log enough of the prompt/response pair (with PII redacted) to debug a bad output after the fact — but sample it, don't log every full request/response body verbatim at scale. You need the ability to reproduce and diagnose a bad output; you don't need to store gigabytes of raw transcripts.


Key takeaways

  • An LLM predicts the next token by statistical plausibility, not by fact-checking — this single property explains hallucination and why validation is mandatory, not optional.
  • Tokens, not characters, are the unit of cost and context-window limits — bound your input size explicitly rather than trusting the provider's limit as your only safeguard.
  • Prefer native structured-output/tool-calling modes over "please respond in JSON" prompt instructions — they're validated and far more reliable.
  • Grounding (supplying real source data in the prompt) is the highest-leverage, lowest-effort mitigation for hallucination.
  • In tool calling, the model only decides what to call and with what arguments — your backend code executes it and must independently validate and authorize every argument.
  • Never let LLM output directly trigger an irreversible action (payment, email, deletion) without validation or human confirmation.
  • Wire an LLM API exactly like any other external dependency: explicit timeouts, capped retries, a circuit breaker, and cost/latency metrics — plus token-usage tracking, which has no equivalent in typical REST integrations.
  • Treat every LLM API call as third-party data transmission — mask PII, understand the provider's retention/training policy, and don't over-log full transcripts.

Interview Questions

  • In your own words, what does an LLM actually compute when generating a response?
  • Why do LLMs hallucinate, and why is this not simply a "bug that will be fixed" in the next model version?
  • What is grounding, and why does it reduce hallucination more reliably than prompt instructions alone?
  • What's the difference between a token and a word, and why does that distinction matter for cost and context-window limits?
  • How does function/tool calling actually work — what does the model do, and what does your backend code do?
  • Why should a tool exposed to an LLM never be a raw SQL-execution or shell-execution function?
  • If a model calls a tool with an order ID that doesn't belong to the requesting user, whose responsibility is it to catch that, and why?
  • How would you design retry behavior for a call to an LLM API? What should and shouldn't be retried?
  • What cost controls would you put in place before shipping an LLM-backed feature to production?
  • Why is max_tokens an important parameter to set explicitly on every request?
  • What PII-handling considerations apply specifically to sending data to an LLM API, beyond a normal third-party API call?
  • Why might you prefer a provider's native structured-output feature over asking the model to "output JSON" in a plain-text prompt?
  • Describe how you would build a circuit breaker and fallback path around an LLM dependency in a Spring Boot service.
  • What metrics would you add to a dashboard specifically for an AI-backed feature, beyond standard latency/error-rate?