Building QA tooling on LLM APIs
Small internal tools: a triage bot, a flake classifier, a release-note drafter. Enough API fluency to build the thing your team needs and nobody sells. This is where a test engineer starts producing leverage rather than output.
An LLM API is a single HTTP call. You send a system instruction, a message and a maximum length. You get text back.
That is the whole surface for most tooling. No training, no infrastructure, no machine learning knowledge. A tester who can write a script can build something useful in an afternoon.
The reason to build rather than buy is fit. A product has to serve everybody, so it knows nothing about your suite, your logs, your naming or your test management tool. Forty lines that know all four beat a generic feature you have to adapt your process to.
The reason not to build is maintenance. Anything you write becomes yours to keep working.
The terms you will hear
- System instruction. The standing role and rules for the call.
- Max tokens. The ceiling on the response length.
- Structured output. Asking for JSON matching a schema, so code can consume it.
- Streaming. Receiving the answer progressively. Worth it for long outputs.
- Model version. Pin it, and record it with any result you keep.
- Rate limit. The provider's cap on calls per minute.
What is worth building
- A failure triage helper. Paste distinct failure signatures, get candidate groupings. The highest value per line of code on this page.
- A log summariser. Forty thousand lines to the six that matter, for a specific log format you own.
- A case drafter with your house style. Your template, your naming, your data conventions.
- A release note summariser. Turn a set of merged pull requests into a plain list of what changed and what to test.
- A coverage question answerer. "Which tests touch the refund path?" over your own suite.
What to buy instead: anything needing a hosted interface, permissions, multiple users, or an audit trail. Those are products, not scripts.
A working example
For example, here is a triage helper in about forty lines, using the official SDK. It takes failure signatures and returns candidate clusters as JSON.
import Anthropic from '@anthropic-ai/sdk'
const client = new Anthropic() // reads ANTHROPIC_API_KEY
const SYSTEM = [
'You group software test failures by likely shared cause.',
'Reply with JSON only, matching:',
'{"clusters":[{"cause":string,"signatures":string[],"confidence":"high"|"low"}]}',
'Never invent a cause you cannot support from the text.',
'If two signatures are unrelated, keep them in separate clusters.',
].join(' ')
export async function clusterFailures(signatures: string[]) {
const response = await client.messages.create({
model: 'claude-opus-5',
max_tokens: 2048,
system: SYSTEM,
messages: [{ role: 'user', content: signatures.join('\n') }],
})
// Always check why it stopped before reading the text.
if (response.stop_reason === 'refusal') throw new Error('declined')
const text = response.content
.filter((block) => block.type === 'text')
.map((block) => block.text)
.join('')
// Validate before trusting. A malformed reply is a normal Tuesday.
let parsed: unknown
try {
parsed = JSON.parse(text)
} catch {
throw new Error(`not JSON: ${text.slice(0, 200)}`)
}
const clusters = (parsed as { clusters?: unknown }).clusters
if (!Array.isArray(clusters)) throw new Error('missing clusters array')
return clusters as { cause: string; signatures: string[]; confidence: string }[]
}Three details in there matter more than the model call. It checks stop_reason before reading the text, it validates the JSON rather than assuming, and the system instruction forbids inventing a cause. Those three lines are what make it safe to run unattended.
How to keep it alive
- Pin the model version and record it in the output.
- Cache by a hash of the prompt plus that version. Cheap, and it makes repeat runs free.
- Cap spend on the key and alert before the cap.
- Handle the malformed reply. Retry once, then fail loudly rather than passing rubbish downstream.
- Keep the prompt in version control beside the code, and review changes to it like code.
- Redact inputs. Logs carry emails and tokens, which is the rule in what never goes in a prompt.
- Delete it when it stops earning. A tool nobody runs is a maintenance cost pretending to be an asset.
The model is the commodity. Your logs, your suite, your naming and your definition of a good answer are the parts nobody can buy.
How to show you know it
- A small tool in use. Forty lines that a team actually runs beats a large one nobody does.
- The validation path. Showing what happens on a malformed reply proves you built it for Tuesday, not for the demo.
- A cost figure. "About four pounds a month, capped at twenty." It turns a curiosity into an operational tool.
- A build-versus-buy decision. "We built the triage helper and bought the eval platform, because one needs our logs and the other needs an audit trail."
Questions
Do I need to know machine learning?
No. This is an HTTP call with a string in it. Knowing how to write a script, validate a response and handle failure is the whole requirement.
Which model should I use?
Start with a capable general model, pin the version, and measure. For narrow structured tasks a smaller model is often enough and much cheaper, which you can only know by trying both on your own inputs.
How do I test a tool built on a model?
The same way you test any AI feature. A small eval set of inputs with known good groupings, run on a schedule, and a check that the JSON contract holds.
Should this live in the pipeline?
Only where it is cheap and stable enough, and the placement rules from cost, latency and determinism apply. Triage helpers usually run on demand rather than on every commit.