AI Governance · Module 1
Do not just trust a prompt. Measure how well it works. Then see whether an optimiser, a program that tries changes to the prompt and keeps the ones that score best, can improve it. Everything runs on your own computer with a small open AI model. The example task is writing exam questions about AI governance. You can read the findings without running any code.
About 10 minutes, no setup. Read Why this matters, then What to do on Monday and the Lessons. Skip Setup and the code boxes; nothing on the page depends on them.
About 20 minutes to set up and a few minutes to run. You need a terminal, git, Python 3.11 or later and about 6 GB of disk. Go to Setup.
Most prompts are written, tried on two or three examples, and put to use because the output “looked fine”. After that, nobody can say how well the prompt works, or what the evidence is. Nobody can say if it still works after the AI model changes.
This module uses DSPy, a Python toolkit. DSPy treats a prompt like a piece of software: you can test it, score it and improve it. You do not need DSPy for the ideas; the same steps work with any chat tool. The task is to write exam questions from short passages about AI governance principles. There are two kinds of question:
One question and a short answer. Both must be grounded in the passage: based on what it actually says.
One question, four options, and which option is correct.
The model is Gemma 4 E2B, a small model from Google. It runs in llama.cpp, software that runs AI models on an ordinary computer. No data leaves your machine, and runs cost nothing.
The passages cover twelve principles that will not go out of date. They are: accountability, transparency, human oversight, risk-proportionate controls (stronger checks where the risk is higher), data minimisation (collecting only the data you need), bias testing, documentation, incident handling, supplier due diligence (checking the companies you buy AI from), record-keeping, contestability (letting people challenge a decision) and monitoring.
The same five steps apply to any prompt you rely on, with or without DSPy. Non-coders can skip the code boxes; the text around them carries the idea.
In DSPy you do not write the prompt itself. You list what goes in and what should come out. DSPy calls this list a signature, and it writes the actual prompt for you. This keeps what you want apart from how you ask. The “how you ask” part is what an optimiser can change.
class MultipleChoiceQuestion(dspy.Signature):
"""Write one multiple-choice exam question about the passage."""
passage: str = dspy.InputField()
question: str = dspy.OutputField()
options: list[str] = dspy.OutputField()
answer_index: int = dspy.OutputField()
There are 24 hand-written passages. Each comes with a hand-written short-answer question and a hand-written multiple-choice question. These are the reference questions: examples of what good looks like. Twelve passages are for training, which means improving the prompt. The other twelve are held back: kept aside and used only for testing. Each principle appears once in each half.
The optimisers only ever see the training half. Every score on this page comes from the held-back half. So a prompt cannot score well by memorising its test.
This is the most important step. The metric is the scoring rule: a small piece of code that gives one output a score. Here it is a checklist anyone can read. No second AI acts as a judge:
def short_answer_checks(passage, pred):
q, a = str(pred.question).strip(), str(pred.answer).strip()
return {
"is_a_question": q.endswith("?") and len(q.split()) >= 5,
"stands_alone": stands_alone(q),
"answer_is_short": 0 < len(a.split()) <= 12,
"answer_grounded": overlap(a, passage) >= 0.6,
"answer_not_leaked": overlap(a, q) < 0.6,
}
Before trusting it, the metric was run on the 48 hand-written reference questions, which should all pass. They did not, at first: see the lessons.
Run the plain prompt on the held-back passages and score every output. This first score is the baseline: the starting point that every change is compared with. Then run the same test again. If the score moves between identical runs, that movement is noise (random variation). Differences between prompts that are smaller than the noise mean nothing.
evaluate = dspy.Evaluate(devset=devset, metric=mcq_metric, num_threads=4)
evaluate(dspy.Predict(MultipleChoiceQuestion))
Here are four ways to improve the prompt. All four are scored the same way. The last three use a DSPy optimiser.
A person copies the checklist rules into the prompt’s instructions. This is traditional prompt engineering.
Runs the prompt on the training passages. It keeps up to three outputs that pass every check, and adds them to the prompt as worked examples.
Also asks the model to suggest new instructions. It then tries different mixes of instructions and examples, and keeps the best one.
MIPROv2 starting from the person's instructions rather than from scratch.
optimiser = dspy.MIPROv2(metric=mcq_metric, auto="light", seed=7)
better = optimiser.compile(dspy.Predict(MultipleChoiceQuestion), trainset=trainset)
better.save("results/mipro_mcq_program.json")
The saved file is the improved prompt: its instructions and its chosen examples. It is a plain text (JSON) file, so you can keep each version and review the changes.
Measured on the 12 held-back test passages the optimisers never saw (DSPy 3.4.0, Gemma 4 E2B (Q8) on llama.cpp, random seed 7). Each score is the share of checklist items passed, averaged over the passages, out of 100: 100 means every question passed every check. Where a prompt was run five times, the bracket shows the average and the typical spread. Single runs move by up to about 3 points by chance alone, so treat smaller gaps as ties.
| Prompt | Short answer | Multiple choice |
|---|---|---|
| Baseline The plain prompt DSPy writes from the signature. | 76.7 (5 reruns: 75.7 ± 0.9) | 85.4 (5 reruns: 85.6 ± 1.6) |
| Hand-written rules A person copies the checklist rules into the prompt's instructions. | 93.3 (5 reruns: 95.7 ± 1.9) | 89.6 (5 reruns: 87.9 ± 1.4) |
| BootstrapFewShot Adds up to three worked examples that pass the metric. | 81.7 | 84.4 |
| MIPROv2 Proposes new instructions and picks the best instruction and examples. It kept the plain prompt and no examples, so it scored the same as the baseline. | 76.7 | 85.4 |
| Hand-written + MIPROv2 MIPROv2 starting from the hand-written instructions. | 98.3 | 89.6 |
| Check | Baseline | Hand-written rules | BootstrapFewShot | MIPROv2 | Hand-written + MIPROv2 |
|---|---|---|---|---|---|
| Is a question | 100% | 100% | 100% | 100% | 100% |
| Stands alone (no “passage”) | 83% | 100% | 100% | 83% | 100% |
| Answer ≤ 12 words | 0% | 92% | 8% | 0% | 100% |
| Answer grounded in passage | 100% | 100% | 100% | 100% | 100% |
| Answer not in question | 100% | 75% | 100% | 100% | 92% |
| Check | Baseline | Hand-written rules | BootstrapFewShot | MIPROv2 | Hand-written + MIPROv2 |
|---|---|---|---|---|---|
| Is a question | 100% | 100% | 100% | 100% | 100% |
| Stands alone (no “passage”) | 67% | 83% | 83% | 67% | 92% |
| Four options | 100% | 100% | 100% | 100% | 100% |
| Options distinct | 100% | 100% | 100% | 100% | 100% |
| Correct option identified | 100% | 100% | 92% | 100% | 100% |
| Correct option grounded | 92% | 50% | 33% | 92% | 92% |
| Wrong options not copied from passage | 92% | 100% | 83% | 92% | 100% |
| Correct option not the longest | 33% | 83% | 83% | 33% | 33% |
Counts of which letter (A to D) held the correct answer. Across 12 multiple-choice questions a fair spread is about 3 per letter. No prompt fixed this, because each question is written on its own. Shuffle the options in code instead.
| Prompt | A | B | C | D |
|---|
Fails: Answer ≤ 12 words
Passes every check
Fails: Stands alone (no “passage”)
Fails: Correct option not the longest
Plus 3 worked example(s).
Plus 3 worked example(s).
Plus 3 worked example(s).
Plus 3 worked example(s).
Candidate instructions for the short-answer task, written by the same 2B model. None mentions answer length or the passage, because the proposer never sees the metric. Notice the narrowed topic list: the data covers twelve principles.
Each lesson comes from something that happened while building this module.
The first metric only checked that outputs had the right format. The plain prompt scored 83 and 96 out of 100. That looked good, but the questions were poor. Answers ran to 50 words. Questions said “according to the passage”, but students would never see the passage. A high score from a weak metric gives false comfort. Decide what good means, in terms you can check, before you try to improve anything.
Run the metric on answers you know are good. Also read a few outputs yourself. Here, one reference question failed because it was an instruction (“Give one way…”), not a question. A strict word match also marked a correct model answer as not grounded. The answer said “building” where the passage said “builds”. Then a stricter length check was added, and 15 of the 24 hand-written multiple-choice questions failed it. The author had made the longest option the correct one. That is exactly the flaw the check was added to catch in the model. People make the mistakes they look for in machines.
BootstrapFewShot showed the model three short, correct answers, and it still wrote long ones. One line of instruction, “The answer must be 12 words or fewer”, fixed almost every answer. Small models copy the format of examples more easily than qualities nobody states, such as being short.
MIPROv2 asks the model to suggest better instructions, based on the data and the program. But it never shows the model the metric. The suggestions added drama (“a high-stakes certification exam”). They also narrowed the topic to three principles the model happened to see. None mentioned answer length. When MIPROv2 started from the hand-written rules instead, it gave the highest short-answer score. That was a little above the rules alone. On multiple choice it tied. In this experiment the person’s rules did most of the work. The optimiser added examples and polish.
The hand-written rules told the model to keep all options the same length, and to write the wrong options in its own words. After that, fewer correct answers stood out. But the model also reworded the correct answer, so it drifted away from the passage. When MIPROv2 then rewrote those instructions, the correct answers matched the passage again. But the correct option was once again often the longest. Check every rule after every change, not only the one you were fixing.
Every prompt put the correct answer at B or C far more often than chance, and almost never at A. Each question is written separately, so no prompt can balance a set. Two lines of code can: shuffle the options and update the index. Use prompts for things that need judgement. Use code for fixed rules.
order = random.sample(range(4), 4)
options, answer_index = [options[i] for i in order], order.index(answer_index)
The same prompt, run five times on the same passages, moved by up to about 3 points. That is the noise. With only twelve test passages, one question is worth several points. Differences smaller than the noise are ties, however good the chart looks. More test cases shrink the noise. More tests always beat cleverer tests.
The metric checks format, and whether answers are grounded in the passage. It cannot tell if a question tests understanding or just memory. It cannot tell if a question is fair to every student, or even correct. That still needs a subject expert. Use the score to compare prompt versions, and to spot when a change makes things worse. Use people to approve the questions.
Every run writes the model, DSPy version, seed, data split, prompt, outputs and scores to results/. That record is what lets someone else check the claim, and what you rerun when the model, the brief or the data changes. Without that record, a prompt is just an experiment. With it, the prompt is a governed part of your system.
The same lessons, for a prompt you use in Copilot, ChatGPT or any chat tool.
This section is for people who want to run the code. If you only want the ideas, skip to the lessons. Setup takes about 20 minutes. You need a terminal, git, Python 3.11 or later and about 6 GB of disk space. A graphics card helps but is not required.
llama.cpp runs open AI models on an ordinary computer. Other programs can then talk to the model in the same way they talk to OpenAI’s online service (an “OpenAI-compatible API”).
brew install llama.cpp
llama-server --version
Apple Silicon Macs use the GPU automatically.
winget install ggml.llamacpp
llama-server --version
Open a new terminal after installing so llama-server is on your PATH. Prebuilt downloads for CUDA and Vulkan are also on the releases page.
Build from source. Drop -DGGML_CUDA=ON if you have no NVIDIA card.
git clone --depth 1 https://github.com/ggml-org/llama.cpp.git ~/src/llama.cpp
cd ~/src/llama.cpp
cmake -B build -DGGML_CUDA=ON
cmake --build build --config Release -j --target llama-server
./build/bin/llama-server --version
Needs cmake and a C++ compiler (sudo apt install cmake build-essential), plus the CUDA toolkit for GPU builds.
Gemma 4 E2B is the smallest model in Google's Gemma 4 family. It is built for laptops and phones. “E2B” stands for “effective 2 billion” parameters (the numbers the model learned in training). The model comes as a GGUF file, the file format llama.cpp reads.
pip install -U huggingface_hub
hf download ggml-org/gemma-4-E2B-it-GGUF gemma-4-E2B-it-Q8_0.gguf
Q8_0 is 5.0 GB. On a laptop with 8 GB of RAM, download gemma-4-E2B-it-Q4_0.gguf instead and point MODEL= at it in step 3.
git clone https://github.com/wmgmm/ai_governance.git
cd ai_governance/prompt_evaluation
./scripts/start_server.sh
The script finds the model you downloaded (in your Hugging Face cache). It makes the model available at http://localhost:8090, an address on your own computer. Leave it running. Open a second terminal window and check it is working:
curl http://localhost:8090/health
A reply containing "ok" means the server is ready. If it says the model is still loading, wait a few seconds and try again.
--reasoning off: by default, Gemma 4 “thinks” before it answers. For a five-word answer it used 269 tokens with thinking and 13 without (a token is a word or part of a word). For short answers in a fixed format, thinking mostly just costs time.--parallel 4: the server handles four requests at a time, so DSPy can test four examples at once.On Windows, run the command inside the script directly: llama-server -m PATH\to\gemma-4-E2B-it-Q8_0.gguf --alias gemma-4-e2b --port 8090 -c 16384 --parallel 4 -ngl 99 --jinja --reasoning off
The project uses uv, a tool that installs Python packages. It installs DSPy 3.4 in a separate space (an “environment”), so it does not clash with other Python software on your computer.
cd ai_governance/prompt_evaluation
pip install uv
uv sync
uv run python run.py all
uv run python make_results_page.py
run.py all takes about three minutes on a desktop GPU. It writes scores, prompts and every model output to results/, and the second command rebuilds the results section of this page from them. Expect your scores to differ from these by a few points. The model picks each word with some randomness, so no two runs are the same.
Success looks like one line per stage and task, such as baseline short_answer dev score 76.7, then a results/ folder full of JSON files. make_results_page.py rewrites your local copy of this page, site/prompt_evaluation/index.html, not the published one.
“Model not found”: the download in step 2 did not finish, or you downloaded a different file. For the smaller Q4_0 file, give its path: MODEL=/path/to/gemma-4-E2B-it-Q4_0.gguf ./scripts/start_server.sh.
Port 8090 already in use: start on another port and tell the scripts where it is: PORT=8091 ./scripts/start_server.sh, then in the second terminal export LLM_API_BASE=http://localhost:8091/v1 before uv run.
hf or llama-server not found: open a new terminal so the install is on your PATH. On Linux, set LLAMA_SERVER=~/src/llama.cpp/build/bin/llama-server if you built it elsewhere.
No graphics card: everything still works on the CPU, only more slowly. The three-minute figure is for a desktop GPU; CPU times were not measured.
Scores differ from this page: expected, by a few points. See lesson 07.