Automated testing and evaluation pipeline I built end-to-end for Camilla, the tender-assistance chatbot deployed at national scale for Italian Public Administration. At its core is a task-oriented evaluator: a DAG-based scheduler with dependency rules declared in PostgreSQL and FastAPI workers that scale horizontally, so the assistant’s behavior can be tested on many tenders in parallel. The volume of data added to the chatbot, all of it needing testing, matches every public tender published on INPA: roughly 100 new tenders a day.

Task and worker orchestration

I deliberately took the simplest route that would hold the requirements, without integrating any workflow framework: Postgres is the system’s single point of contact. The container that submits the run and the workers executing the tasks never talk to each other: everyone reads and writes state on the database, and the DAG dependencies are resolved by a SQL function that unlocks tasks as their prerequisites complete. Every task has retries, a timeout and an explicit lifecycle (queued, running, completed, failed, canceled), and resources are governed in the most prosaic way possible: docker compose –scale to add or remove workers. Less orchestration overhead makes it easier to understand why a run stopped, and to start it back up.

run start (Docker)              FastAPI workers × N
                               (docker compose --scale)
     |                                 ^    |
     v                                 |    v
Postgres: queue, task state, DAG in a SQL function
     |
     |  per tender: evidence --> ok/ko verdict
     |  global: red-teaming, Weaviate diagnostics
     v
dashboard + email to the service

If a task fails, the healthy ones are still carried to completion before the run halts: relaunch it, and only the failed tasks are automatically picked up again.

At the end of the chain, the cost and latency of every run are tracked in a dashboard that lets the service govern and monitor the chatbot’s performance across hundreds of new documents a day. This is the point where the data and the evaluations turn into real governability and observability of the service, rather than a verdict on one run.

Evaluation criteria and methods

Coming to this project I ran up against the problem that runs through much of the evaluation field: judging the behavior of a system this expressive without being able to state clear success criteria, or at least criteria that can be verified deterministically. So I tried to decompose the problem, asking two questions every time: what, in this scenario, can I still verify deterministically? And for everything else, with what mandate do I put a model in the judge’s seat?

What stays deterministic

The first question is answered by exploiting the constraints of the domain and of the scenario, which means turning every known property of the agent under evaluation to my advantage. An agent uses tools, and tools have a precise signature and semantics: if I ask a content question about a tender, I expect the assistant to invoke a ‘get_tender_text’ tool or similar. All of these expectations became deterministic rules to apply to the test scenarios. If the suite knows it is asking a content question, a clear and non-negotiable failure signal is the chatbot answering without consulting the tool.

The same holds for the domain. Tenders share common features, known to domain experts, and that makes it possible to predetermine a set of questions that any tender has to answer one way or another: the absence of an answer is a failure in itself, with no judgment required.

Two scenarios rest on the same principle, and in these an LLM does come into play, though not as a judge: it builds the test case, while the check itself stays deterministic.

  • Multi-turn conversations with dynamic scope switching. The assistant talks to a user simulated by an LLM that alternates between questions specific to one tender and general questions (FAQ). The scenario verifies that at every scope change (tender a → tender b, or tender → FAQ) the assistant still reaches for the right tools, showing it can hold the topic without getting stuck on the previous tender.
  • Search-engine behavior checks. Realistic search queries (for example “tenders for new graduates in Milan”) formulated by an LLM from the tender’s requirements and metadata. The model that builds the test case also settles what the correct outcome of the query is, positive (the tender must be found) or negative (the tender must not appear among the results).

That leaves the awkward question: how do you tell “an answer” from an answer grounded in the facts? Deterministic checking does not reach that far.

The judge’s mandate

To verify that answers hold to what the tender actually says, and that the assistant stands up under attack scenarios, I used the LLM-as-a-judge methodology: a separate model evaluates the assistant’s answers against a well-defined mandate stating the metrics, or at least the semantics of what “success” means. The quality of the evaluation depends almost entirely on how precise that mandate is, so I wrote a different one for each type of test.

  • Pseudo-ground-truth. Verifiable facts extracted from the source documents and put back to the bot as questions: the assistant does not see the exact answer, the judge does, and has to assess how closely the assistant’s answer tracks it.
  • Boilerplate questions. The generic questions described above. Once it has been established deterministically that an answer was given at all, what is left for the judge is its tone, form and completeness with respect to the question asked, without any comparison against the source.
  • Attack scenarios. The judge is handed the assistant’s answer together with the attack script, that is, a description of the behavior the attacker was trying to elicit (getting the assistant to reveal its own system prompt, for instance).

A few remaining tests are diagnostic rather than behavioral, such as quality checks on the embedding collections: verifying that there are not too many null vectors, a symptom of anomalies in ingestion, and that the collection’s silhouette stays within admissible values. That last metric flags whether the tender vectors are packed too tightly, which can point to problems in the embedding or in the chunking strategy and shows up as degraded search quality.

Red-teaming: the attack scenarios

The attack scenarios are the most important proving ground in the whole suite. On top of this infrastructure I designed 200+ multi-turn scenarios probing institutional role preservation under adversarial pressure: prompt injection and jailbreak resistance, information-boundary protection, fairness and non-discrimination, temporal integrity, recovery to baseline behavior after an attack.

It did not start out that way. At first this test scenario was handled manually, by a platform component that sent the questions to the chatbot and checked that every attempt at attack or manipulation was turned away. Examining that starting point led me to two conclusions, and the current taxonomy follows from them.

Risk scenarios specific to public administration

Most of the scenarios already prepared were concerned with the assistant holding its tone and its mandate, not with real attacks or prompt injection. However catastrophic a prompt injection failure is, especially for companies where the assistant is the way in to sensitive data, in Camilla’s case the largest risk was reputational (an assistant talking in rhymes, like a pirate, or voicing discriminatory opinions on the national PA tenders portal) and informational (getting it to invent facts, run arbitrary searches, and the like).

Red-teaming therefore also means knowing how risk takes shape in the particular domain you work in. Those tests kept all of their relevance even once the manual setup had been absorbed into the suite.

Single-turn or multi-turn attacks?

The second point concerned the nature of the tests: nearly all of them were single-message, one attack attempt and the assistant’s answer. But real manipulation cases, surfaced by continuously monitoring the answers during our own tests, showed that the assistant rarely gave in on the first request, and far more often after two or three variations, or when legitimate and malicious requests were alternated and mixed together.

So I built the red-teaming around multi-turn attacks, both for the “institutional” scenarios described above and for the more technical ones: prompt injection, homoglyphs, invisible characters.

The resulting taxonomy is my own design, derived from production interactions and from domain-specific cases, and complemented with the OWASP Top 10 for LLM Applications.

200+ adversarial scenarios
~100/day new tenders tested
OWASP LLM Top 10 in the taxonomy

Entity extraction: verifying the metadata

Surprisingly, one of the hardest things to get right while designing and testing this pipeline was metadata verification. As explained on the search engine page, Camilla’s end-to-end behavior rests on each tender being correctly tagged with metadata: which degrees and requirements it demands, work locations and so on. This information is already attached to each tender by the time the evaluator tests it, but inaccuracies and omissions with respect to what is actually written in the tender text were hard to intercept upstream. So I added a dedicated task that runs entity extraction over the full tender text and diffs what it finds against the metadata already attached to the tender, flagging every discrepancy.

The ambiguity of real data

The difficulties showed up when I had to face the real data: public tenders are incredibly heterogeneous documents, but above all the entities we cared about (geographic ones, degrees) very often appeared with a meaning different from the expected one. For instance, the name of a city where the written exams would take place (but not a work location), or degrees that granted preference points in the ranking without being “required”. On top of that, the data often sat in tables spanning page after page, and came in every imaginable format (degrees especially): with a degree code, without one, with or without a field of study, in broken-up lists or lists stating legal equivalences.

How the extraction works

A multi-step approach turned out to be essential: first locating the articles of the tender that contained information about requirements, locations or job profiles. From there, we use regex wherever possible to extract a series of text “islands” (not too disconnected from each other, so as to preserve the surrounding context), and only then run entity extraction, whose system prompts have been revised dozens of times to spell out every business rule needed. The comparison between extracted metadata and the metadata attached to the tender is currently done deterministically wherever possible (e.g. removing degrees that match on degree code from both sides, and other regex-based rules), leaving only the genuinely ambiguous cases to the LLM.

A first description of the pipeline is in the article on Camilla (in Italian); a dedicated article on red-teaming and behavioral evaluation (also in Italian) goes deeper on the security side.