In a previous article I showed how to teach a local model a new domain through a combination of llm training and RAG. In this article I will discuss my strategy for defining evals to ensure the model is performing correctly.

Project

Just as a quick refresher, my previous article describes how I trained a local model (qwen 3.5 4B) to act as a travel advisor for a fictional subway system. The main idea is to rely on continued pretraining (CPT) to teach the model how to reason about the subway system. But to add support for real-time updates, I combined the internalized knowledge with RAG.

As a visual reference, I have added a picture of the fictional subway map below:

Awesomeville subway map showing the Green, Blue, and Gold lines
Awesomeville’s three subway lines connect at Central Station. Select the map to view it full-size.

Evals

One of the key components in determining if a model is performing well is to add comprehensive evals around it. I designed my evals around the following two requirements:

I need to measure if the model can generalize well, and answer questions not previously seen during training

I need to measure the model’s accuracy level as a baseline for future improvements or regressions

In my eval solution I ended up with three metrics:

  1. Exact Match
  2. Partial Match
  3. Semantic Match

Exact Match

When possible, exact match is likely the best way to measure success since it allows you to make simple comparisons between model output and some reference data. This is by design a strict comparison since the match must be exact. In some cases, this may be unrealistic, but I will show a technique that worked well in this project:

Initially my model was returning subway itineraries in the form of English sentence. This presented a challenge to exact match comparisons since two correct sentences can vary greatly but still represent the correct answer. As a solution to this, I decided to pivot my solution to instead output the subway itinerary in the form of a structured json schema. In some case this is not an option, but the legs of a subway journey lend themselves well to this format.

I have included a sample of a predicted subway journey consisting of three legs below:

[ { "from_station": "East Harbor", "to_station": "Central Station", "subway_line": "Gold Line" }, { "from_station": "Central Station", "to_station": "Innovation Park", "subway_line": "Green Line" }, { "from_station": "Innovation Park", "to_station": "Lake Harmony", "subway_line": "Green Line" } ]

Depending on the strength and size of your model, json output might work out of the box, but smaller models may need some help. In my case I decided to fine tune the model to reliably output the subway itineraries in the form of json.

For the actual fine tuning I am using Unsloth with Supervised Fine Tuning (SFT). The only goal of SFT in this case is to teach the model to output responses in the desired format. I have included a link to the SFT corpus here in case you want to have a look.

The SFT corpus defines entries following the Alpaca format, which is more of a Q&A format, but I did not want to rely on SFT for teaching knowledge, only format. One of the key reasons I want to avoid using SFT to train knowledge is that it usually leads to Q&A memorization that doesn’t generalize well to unseen questions. Instead, I want the subway routing knowledge to continue to come from CPT training.

I experimented a bit with SFT settings, but in the end, I ended up having success with a lightweight LORA setup. I have shared the Unsloth SFT config here. Post SFT I tested the model by running an eval set of 604 routing questions.

Results:

As I have reported previously, model performance is very good. Post CPT and SFT I was able to get the exact match evals up to 98% from 573/604 correct results.

Partial Match

573/604 results are an exact match, but what happened to the remaining 31 questions? In all 31 cases the model provided a partially correct subway route, but one or more legs were missing. It is useful to measure partial successes since it creates a baseline for later improvements or regressions when retraining.

I have added a summary of partial results below. This table shows the partial correctness scores for the 31 route predictions that failed the exact match test.

Case Journeys Correctness Missing route legs or issue
generated_heldout_route_0762 journey33.3%Missing: north terminal to founder's square on blue line. Extra: north terminal to central station on blue line; central station to founder's square on blue line.
generated_heldout_route_1032 journey33.3%Missing: south gardens to university commons on blue line. Extra: south gardens to central station on blue line; central station to university commons on blue line.
generated_heldout_route_1632 journey33.3%Missing: founder's square to south gardens on blue line; south gardens to central station on blue line. Extra: founder's square to central station on blue line.
generated_heldout_route_1712 journey50.0%Missing: westgate to east harbor on gold line. Extra: westgate to central station on gold line; central station to east harbor on gold line.
generated_heldout_route_2083 journey60.0%Missing: central station to founder's square on blue line; founder's square to south gardens on blue line. Extra: central station to south gardens on blue line.
generated_heldout_route_2193 journey60.0%Missing: central station to founder's square on blue line; founder's square to south gardens on blue line. Extra: central station to south gardens on blue line.
generated_heldout_route_2343 journey60.0%Missing: central station to founder's square on blue line; founder's square to south gardens on blue line. Extra: central station to south gardens on blue line.
generated_heldout_route_2533 journey80.0%Missing: founder's square to south gardens on blue line.
generated_heldout_route_2843 journey50.0%Missing: old mill station to westgate on gold line; westgate to central station on gold line. Extra: old mill station to central station on gold line.
generated_heldout_route_2973 journey60.0%Missing: north terminal to river market on blue line. Extra: north terminal to central station on blue line; central station to river market on blue line.
generated_heldout_route_3294 journey40.0%Missing: westgate to east harbor on gold line; east harbor to old mill station on gold line; old mill station to central station on gold line. Extra: westgate to central station on gold line.
generated_heldout_route_3384 journey71.4%Missing: river market to south gardens on blue line; south gardens to central station on blue line. Extra: river market to central station on blue line.
generated_heldout_route_3484 journey85.7%Missing: south gardens to central station on blue line. Extra: blue_station_s六 to central station on blue line.
generated_heldout_route_3524 journey50.0%Missing: central station to founder's square on blue line; founder's square to central station on blue line; central station to lake harmony on green line; lake harmony to central station on green line.
generated_heldout_route_3664 journey42.9%Missing: central station to south gardens on blue line; south gardens to central station on blue line; central station to emerald hills on green line; emerald hills to museum district station on green line. Extra: central station to museum district station on green line.
generated_heldout_route_3804 journey66.7%Extra: central station to river market on blue line; river market to central station on blue line; central station to sunrise point on gold line; sunrise point to central station on gold line.
generated_heldout_route_3924 journey75.0%Missing: sunrise point to old mill station on gold line. Extra: sunrise point to central station on gold line; central station to old mill station on gold line.
generated_heldout_route_4044 journey42.9%Missing: central station to founder's square on blue line; founder's square to south gardens on blue line; south gardens to university commons on blue line. Extra: central station to university commons on blue line; university commons to south gardens on blue line; south gardens to founder's square on blue line; founder's square to north terminal on blue line.
generated_heldout_route_4104 journey57.1%Missing: east harbor to old mill station on gold line; central station to university commons on blue line. Extra: east harbor to central station on gold line; central station to university commons on green line; central station to old mill station on gold line.
generated_heldout_route_4515 journey71.4%Missing: central station to founder's square on blue line; founder's square to south gardens on blue line. Extra: central station to south gardens on blue line.
generated_heldout_route_4525 journey60.0%Missing: central station to founder's square on blue line; founder's square to central station on blue line; central station to east harbor on gold line; east harbor to central station on gold line.
generated_heldout_route_4575 journey77.8%Missing: central station to founder's square on blue line; founder's square to south gardens on blue line. Extra: central station to south gardens on blue line.
generated_heldout_route_4585 journey80.0%Missing: westgate to east harbor on gold line. Extra: westgate to central station on gold line; central station to east harbor on gold line.
generated_heldout_route_4645 journey54.5%Missing: sunrise point to old mill station on gold line; central station to east harbor on gold line. Extra: sunrise point to central station on gold line; central station to second_station on gold line; second_station to central station on gold line; central station to old mill station on gold line; central station to east harbor on green line.
generated_heldout_route_4745 journey75.0%Missing: central station to north terminal on blue line; north terminal to river market on blue line. Extra: central station to river market on blue line.
generated_heldout_route_4795 journey77.8%Missing: east harbor to old mill station on gold line; old mill station to central station on gold line. Extra: east harbor to central station on gold line.
generated_heldout_route_4825 journey80.0%Missing: river market to south gardens on blue line. Extra: river market to central station on blue line; central station to south gardens on blue line.
generated_heldout_route_4905 journey66.7%Missing: westgate to east harbor on gold line; east harbor to central station on gold line; central station to founder's square on blue line. Extra: westgate to central station on gold line; central station to river market on blue line; river market to founder's square on blue line.
generated_heldout_route_4925 journey38.9%Missing: central station to founder's square on blue line; founder's square to central station on blue line; central station to east harbor on gold line. Extra: central station to river market on blue line; river market to central station on blue line; central station to westgate on gold line; westgate to central station on gold line; central station to river market on blue line; river market to central station on blue line; central station to westgate on gold line; westgate to central station on gold line; central station to river market on blue line; river market to central station on blue line; central station to westgate on gold line.
generated_heldout_route_4985 journey80.0%Missing: central station to founder's square on blue line; founder's square to central station on blue line. Extra: university commons to central station on blue line; central station to university commons on blue line.
generated_heldout_route_4995 journey75.0%Missing: north terminal to river market on blue line; river market to central station on blue line. Extra: north terminal to central station on blue line.

Semantic Match

The third category is semantic matching, which is less common in a schema-based response, but I noticed a few cases during testing.

In this experiment I noticed that the model would occasionally output superfluous route legs (e.g. an unnecessary but correct intermediate station during a journey). While these additions make the route more verbose, it doesn’t interfere with the factual accuracy of the full route since it’s just additional information. I decided to capture this nuance in the evals, even though the final solution didn’t contain any semantic match cases.

Eval Set

As I mentioned above, the full eval set consists of 604 routing questions. None of these eval cases appear exactly in either CPT or SFT training corpuses. There is, however, some overlap between some route pairs in multi-part journeys (i.e. an itinerary that contains multiple sub journeys).

Out of the 604 eval questions, 457 questions are completely held out with no overlap. 137 eval questions have partial overlap with sub routes defined in the CPT corpus, but only 10 eval questions have full overlap

There is no overlap with the routes defined in the SFT corpus.

Based on this I have included the performance of the respective eval questions below. As you can tell, performance is good regardless of overlap. The table covers both raw exact correctness as well as adjusted correctness with partial matches included.

Held-out evaluation results

Training exposure Cases Exact matches Exact-match rate Partial answers Partial rate Weighted correctness
Completely held out 457 437 95.6% 20 4.4% 98.3%
Partially held out 137 126 92.0% 11 8.0% 96.8%
Fully CPT-covered 10 10 100.0% 0 0.0% 100.0%
Total 604 573 94.9% 31 5.1% 98.0%

Conclusion

Overall, the evals tell a good story regarding the trained qwen 3.5:4B model’s ability to act as a travel advisor in this fictional subway domain. As always, the code is available on Github in case you are interested in checking it out.