Thoughts from a first look at RAG Evaluations

In order to skill up for this new AI world I have been working on a RAG project. Let me start by saying, yes, I understand that the hot frontier right now is harnesses, but I want an understanding of how to feed information to an LLM before we add in the complexities of tool calling (building an agent itself), and then also the enforcement of that tool calling/what to do with the results.

Last year, I started with the question “How do you have an LLM respond in the voice of a character”. There are a lot of grammar/word choice quirks that read like the output is AI and there is a lot of debate on whether that can be prompted out of the output. I do see a lot of clearly generated blog and social media posts. Some people claim that this is on account of the poster being lazy/inexperienced with guiding the output. So I set out to see if it could be different, and I was interested in building a character either way.

When I looked at the question of the voice of a character, I realized that I needed to understand who this character was in order to understand what their voice might be like. What I’ve learned over the course of building with an LLM is that your output is only as good as your input. I felt like that maxim also applied in this case. So, over the course of a couple weeks, I, along with my new sidekick, Claude, started generating a backstory, world, and voice around a character. This post though, isn’t about the generation of the character data, I want to explore a specific aspect of building a RAG, namely evaluations.

For now, I will gloss over the building of the RAG itself. Suffice it to say, I had this corpus of story and character building that I fed into a RAG to generate character responses. The important part for this post is that I wanted a mechanism to evaluate how the generated responses changed when I modified the prompt, added more data, changed to a different model, etc. I’ve always been big on testing so this is really a natural extension of that inclination.

I want to share what I’ve learned along the way although I don’t claim by any means to be an expert on this topic. (You’ll also see that I made lots of mistakes along the way.)

Preparation

The first obstacle I came up against was how to do the evaluations. The LLM’s inclination was to just build an evaluation framework from scratch, but that seemed like overkill to me so I evaluated some existing frameworks. This is not a comprehensive overview and as we probably all know, the field is changing very quickly so new frameworks are being released all the time.

Frameworks Considered:

  • Braintrust – rejected because it’s a hosted platform
  • LangSmith – same issue as above
  • DeepEval – has built in metrics that are used often in industry to score rag conversations

I ended up going with DeepEval because I didn’t want to rely on a hosted platform. The fact that it had some built in metrics that I could use also seemed like it might come in handy so that I could see what people with more experience than me were using to evaluate their RAGs.

One thing to understand first is DeepEval’s format for evaluating a case

{

   input: <this is the user input>,
   context: <system prompt, other information>,
   retrieval_context: <what is pulled from storage and passed to the LLM     to generate a response>,
   actual_output: <the response that the LLM generated>

}

Trial 1: Trying out the framework

So to start, I needed test user input in order to evaluate responses.

The test cases I was looking at were:

  1. a general greeting
  2. responding to a question about the character’s family
  3. responding to a question about the character’s hobbies.

Then, I chose the faithfulness metric since on the surface it seemed like being faithful to the source material would be something which I would want to test. This metric is actually four separate calls to the judge model as given below for the version of DeepEval that I was using.

Faithfulness metric prompts
# 1. Extract facts from the retrieval context

Based on the given text, please generate a comprehensive list of FACTUAL, undisputed truths, that can inferred from the provided text.
These truths, MUST BE COHERENT. They must NOT be taken out of context.

Example:
[same Einstein / photoelectric paragraph as step 2, with the answer under a "truths" key]
===== END OF EXAMPLE ======
**
IMPORTANT: Please make sure to only return in JSON format, with the "truths" key as a list of strings. No words or explanation is needed.
Only include truths that are factual, BUT IT DOESN'T MATTER IF THEY ARE FACTUALLY CORRECT.
**

Text:
{{ retrieval_context }}

JSON:

The words "FACTUAL, undisputed truths" come from the truths_extraction_limit parameter. If you set it to N, that phrase becomes "the N most important FACTUAL, undisputed truths per document".

# 2. Extract claims from actual_output

Based on the given text, please extract a comprehensive list of FACTUAL, undisputed truths, that can inferred from the provided actual AI output.
These truths, MUST BE COHERENT, and CANNOT be taken out of context.

Example:
Example Text:
"Albert Einstein, the genius often associated with wild hair and mind-bending theories, famously won the Nobel Prize in Physics—though not for his groundbreaking work on relativity, as many assume. Instead, in 1968, he was honored for his discovery of the photoelectric effect, a phenomenon that laid the foundation for quantum mechanics."

Example JSON:
{
  "claims": [
    "Einstein won the noble prize for his discovery of the photoelectric effect in 1968.",
    "The photoelectric effect is a phenomenon that laid the foundation for quantum mechanics."
  ]
}
===== END OF EXAMPLE ======

**
IMPORTANT: Please make sure to only return in JSON format, with the "claims" key as a list of strings. No words or explanation is needed.
Only include claims that are factual, BUT IT DOESN'T MATTER IF THEY ARE FACTUALLY CORRECT. The claims you extract should include the full context it was presented in, NOT cherry picked facts.
You should NOT include any prior knowledge, and take the text at face value when extracting claims.
You should be aware that it is an AI that is outputting these claims.
**

AI Output:
{{ actual_output }}

JSON:

# 3. Judge each claim

This is the rendered text version. <RETRIEVAL_CONTEXT> and <CLAIMS> are placeholders I passed in.

Based on the given claims, which is a list of strings, generate a list of JSON objects to indicate whether EACH claim contradicts any facts in the retrieval context. The JSON will have 2 fields: 'verdict' and 'reason'.
The 'verdict' key should STRICTLY be either 'yes', 'no', or 'idk', which states whether the given claim agrees with the context.
Provide a 'reason' ONLY if the answer is 'no' or 'idk'.
The provided claim is drawn from the actual output. Try to provide a correction in the reason using the facts in the retrieval context.

Expected JSON format:
{{
"verdicts": [
{{
"verdict": "yes"
}},
{{
"reason": <explanation_for_contradiction>,
"verdict": "no"
}},
{{
"reason": <explanation_for_uncertainty>,
"verdict": "idk"
}}
]
}}

**
IMPORTANT: Please make sure to only return in JSON format, with the 'verdicts' key as a list of JSON objects.

Generate ONE verdict per claim - length of 'verdicts' MUST equal number of claims.
No 'reason' needed for 'yes' verdicts.
Only use 'no' if retrieval context DIRECTLY CONTRADICTS the claim - never use prior knowledge.
Use 'idk' for claims not backed up by context OR factually incorrect but non-contradictory - do not assume your knowledge.
Vague/speculative language in claims (e.g. 'may have', 'possibility') does NOT count as contradiction.

**

Retrieval Contexts:
<RETRIEVAL_CONTEXT>

Claims:
['<CLAIMS>']

JSON:

The score for the faithfulness metric is supposed to work as follows:

  1. Pull claims from the actual_output. The judge LLM breaks actual_output into separate factual claims.
  2. Pull truths from the retrieval_context. It extracts factual statements from retrieval_context.
  3. Give each claim from (1) a verdict against those truths from (2):
    • – yes: the claim agrees with the retrieval_context
    • – no: the claim directly contradicts the retrieval_context
    • – idk: the retrieval_context doesn’t mention it

The default setting for pass/fail is

(yes+idk)/totalclaims>=0.5(yes + idk) / total claims >= 0.5

I ran into an issue almost immediately with one of the judges giving me the following reasoning for failing a response.

“The score is 0.00 because the actual output fundamentally misrepresents the retrieval context by attributing human activities to an AI. Specifically, the output incorrectly claims that ‘the AI’ engages in motorcycle riding and pickup basketball games, when the retrieval context clearly describes these as hobbies of a human individual, not an artificial intelligence.”

Turns out, in the prompt there is the line “You should be aware that it is an AI that is outputting these claims.” which made the judge reject responses for not acknowledging AI generation and speaking in first person. The responses are supposed to read as if the character is speaking to the user, making this criteria incorrect for my use case.

Another issue is that some of the things retrieved from storage aren’t facts. Some of them are directives for how the character should act and so framing things as facts doesn’t work for all my cases. For the family case though, facts is mostly fine, but I still had to deal with the issue of the judge wanting acknowledgement of the response being from an AI.

Trial 2: Trying out custom criteria

The DeepEval framework provides a way to use custom written criteria when the default ones don’t quite fit. So, since I still wanted to test whether the LLM was using the material correctly, I tried to write my own criteria to run across the evals.

As I began thinking about how to structure the criteria, I realized that one challenge with the family case was that the character should be reluctant to share all the details of his family, but the hobbies case didn’t have any such reluctance. This meant that the criteria I wrote had to be flexible with regards to how much of the retrieved_context was revealed in the response.

I tried to resolve this by using an injected attitude that each case could set which would determine the level of deflectiveness the response should have with regards to the retrieved_context. But if you remember from the structure of DeepEval’s cases, there’s no field called attitude so instead I appropriated the context field and directed the judge to look there for attitude.

My own criteria: Coloured By Memory

1. The expected attitudes for this input are given in the context

2. Take the input and come up with answer shapes based on the expected attitudes

3. Read the retrieval context and note whether any of it is relevant to the input

4. Check whether the reply uses any relevant retrieval context to answer the input

5. Penalize using retrieval context that is not relevant to the input

Different levels of deflectiveness
_LEVEL_STEPS = {
    "not deflective": (
        "Penalize using the retrieval context in a reply that does not try to answer the input",
        "Penalize not using relevant retrieval context",
    ),
    "lightly deflective": (
        "Penalize using the retrieval context only as evasion, with none of it reaching the answer",
        "Penalize not using relevant retrieval context. A reply that gives part of it and withholds part is using it",
    ),
}

Astute readers might note that I added complexity by having multiple attitudes without even trying just a single attitude to begin with. Those of us who adhered to TDD might call this taking too big of a step, and quite frankly, you would be correct. The pitfall of starting out using an LLM to help code is that it feels like I can take bigger steps than is prudent (especially when I”m still just trying to understand the problem space). I offer up only that I am human and make mistakes (may my machine overlords look kindly upon my foibles)

The end result of using this criteria was that the judge was unable to determine if the responses were evasive and penalized not using all of the retrieval context instead. It also had a hard time determining if the retrieval_context was relevant in the first place. I suspect that trying to use the context field for something it wasn’t originally designed for may have gotten in the way. Fighting your framework mostly leads to more headaches is something that I am learning anew.

Next time, we’ll look at taking a smaller step with just the family case.

The Hard Coloring Decisions

I’ve worked as a software engineer on many projects at this point so I have many opinions about the tech stack and how to test software. I never thought much about the colors of a website except to comment when I thought the client was adding too many colors to the palette.

Recently though, as I’ve embarked on the journey of starting a company, I’ve learned that picking colors is HARD. Our designer keeps reminding us that changing the colors is not that bad, but trying to pick a set of colors to convey the right tone of our business while also adhering to readability guidelines and navigating the likes and dislikes of the founders is complicated.

First on the list of things to convey: mood. We wanted a fun and inviting vibe to convey the familiarity and enjoyment of being a regular at someplace. We wanted to convey that feeling of home away from home. This meant warm tones and brighter colors, but not so bright as to be considered peppy. — we’re still a somewhat serious business after all.

Our designer put together a few different mood boards to help us nail down what we were going for. We ended up turning down most blues as being too somber or too business-like. Through the mood board we also realized we didn’t want jagged edges in our design accents which goes along with the warmer tone.

After the mood boards, came a round of refinement on the general families of colors. The designer set the colors on mockups of a generic website so that we could see what it would look like in a more realistic setting. It was at this point that accessibility became more of a focal point as we played with white text on colors as well as black text on colors. There were a range of palettes we couldn’t use simply because accessibility became an issue.

I’d always had a vague notion that picking colors was going to be a hard exercise, but it never really registered that it would take several rounds of refinement to settle on a color palette. And, we did finally settle on a palette that I think we’re both pretty happy with. I am glad we went through the whole thinking process, but for now, I’m glad it’s over.

Piecing Together a Puzzle

So in July of last year, I started thinking about applying for full-time remote positions and read a bunch of job descriptions. I came across a company called Doist which is an all remote company. The part about their job description that caught my eye wasn’t the technology they were using or their philosophy, it was that they did scavenger hunts together… remotely.

This got me thinking about escape rooms since they’re a fun team-building activity offline, but not one online. The online versions of escape rooms tend to just be click around games. So, I thought, what if I built one that could be played remotely, together.

First, I started brainstorming scenarios that might fit the theme of an online escape game. I wanted players to scour the internet for clues, and at the time, the most on theme idea to me was a kidnapping. You may or may not have heard of the movie Searching which features a father looking for his missing daughter by looking through her social media. Despite not having watched the movie, it served as the inspiration. Because, who hasn’t wanted to play an internet private eye, combing the depths of the internet looking for the digital fingerprints of your quarry? (granted, maybe this is just wish fulfillment for me)

With an idea in mind, I started writing down a bunch of possible puzzles and paths this story could go. There were side characters that had to be introduced, parents who may or may not have been aware of things, and character development. At the end of a few days of brainstorming, I had a list of things that I wanted all in a jumble of thoughts.

As one does, in the fit of creative mania, I thought this idea was THE idea. At the very least, it had captured all of the space in my mind. So, I did what anybody does. I tried to recruit a friend to join me in my endeavors. The friend took one look at my notes and was said “I have no idea what the story is here. You need to make a flow diagram for this game.”

Apparently, normally people will set up a flow chart for puzzle games that show the relationships between puzzles. Who knew? I, at least, didn’t; even if it does make a ton of sense.

Armed with this new information, I set out to structure my game information better. First up, the story from the parents, and a couple of links to social media accounts that they ‘provided’. Those accounts should then have information the players can use to solve puzzles that are ‘hidden’ within the accounts which should then ‘unlock’ other locations to search….

This exercise forced me to think about what things were gated by what puzzles in a much more structured way. By having each puzzle in its own section, the ‘ingredients’ needed for each one became easier to track. This structure also allowed me to think about what things could be solved by random guesses and strategies to avoid that.

Now that I had a recipe for how to put the puzzle together, it was time to build it for real. I’m not sure which part I thought would be the hardest when I started, but it probably wasn’t signing up for social media accounts. Because of various bad actors trying to manipulate politics, opening accounts and keeping them open is much more of a hassle now than it used to be with phone verification and uniqueness checks, multiple accounts being opened from the same IP being flagged, etc. I would set up an account only to have it flagged and shut down a day later, which forced me to move to less restrictive platforms.

After much trial and error and some help from my friend, an alpha was ready for playtesting. I recruited my partner who, despite having helped with the initial ideation didn’t really know much about the forms the puzzles had taken at the end.

Playtest number 1 went not too poorly. There were a smattering of “Oh, that was kind of confusing” moments but they were generally easy enough to fix. I had a notice to clarify something that I had worried would be confusing… and the notice itself turned into an unintended red herring.

Playtest number 2 went much smoother than the first one with some of the tweaks I learned from the first play test. There was a puzzle that was a bit of distraction for the first playtest which I wasn’t sure should be removed since it was meant to add flavor, but when the second playtest also had issues with the same element, it became clear that it had to go.

I had wanted to do one more playtest before trying to charge for the game, but I didn’t pursue a third set of playtesters as aggressively so the last playtest never happened. I have a hole in my knowledge in regards to sales that unwittingly became a barrier. The closer I got to it, the harder it became to keep going. It is currently sitting in the think about it later pile.

Aksum Coffee House

Front of Aksum Coffee House

Aksum Coffee House
Address: Hoedenmakerstraat 17, 1000 Brussel
Hours: Tues-Sun: 10am-7pm
Rating: 4.0/5.0

Aksum’s drink menu

This was a really nice find in Brussels right next to The Grand Place. That area  is full of tourists, but this quiet little coffee house just a couple blocks away didn’t have any of the bustle that is characteristic of The Grand Place. It was really refreshing to have an entire section of drinks mentioned that they made with soymilk. Brussels overall didn’t seem to be as adventurous with their non-dairy options, and soymilk is pretty much your only alternative.

Inside of Aksum

 They have a decent amount of seating and the tables are a good height for working although the benches you see in the back are wooden and thus, less comfortable. I never tried any of their cakes, but they did have 2-3 of them to order. The hot chocolate (not pictured) was really good. Unfortunately there are a couple of steps to go down to the bathrooms so they are not accessible as well as being separated by gender.
While there are several outlets, only one of them seemed to be working. It’s located all the way in the back of the room.

Speed test!

Per the suggestion of a friend, there will now be speed tests for the wifi at cafes. The wifi at Aksum was pretty stable for the 2 times that we visited.

Le Mur de Lyon

Le Mur de Lyon
Address: 11 Rue Lortet, 69007 Lyon, France
Hours: 8am – 12am everyday
Rating: 5.0/5.0

This is possibly my favorite bouldering gym. Their facilities are top notch, and it has a lot of bouldering space. They even have a cafe attached in case you’re hungry and outdoor seating for those nice Lyon days/nights. Turns out Lyon is really warm into much of October at least.

Pricing: Happy Hour pricing 10-pass for 95€, regular single entry pass 15€, Regular 10-pass 130€, Shoes are 4€

The pricing here is really reasonable especially if you’re able to go during happy hours, which is what I ended up doing. The 10-pass can be used by multiple people. They also have an annual pricing as well as per 4 months, which didn’t make sense for the amount of time we were staying.

Traverse section
Yay for traverses!

One of my favorite things about this gym is that they have an entire section almost as big as the problems area for traverses. They also have traverses that scale from beginner to expert which was a pleasant surprise. I ended up spending the entire last week in Lyon on just traverses because they were so much fun. I also wanted to build up more stamina and well, traverses are good for that. The labeling for the different grades are very clearly labeled.  None of the traverses changed during the 3 weeks we were there.

Problems and other exercise area
Problems section with a peek at the weights/treadmills area above

They don’t use the V scale here; instead they use colors in the problems area and a colors + numbers grading for the traverse section. The really nice thing about the problems area is that there are a bunch of routes for each level although the walls aren’t so crowded with problems that it’s hard to tell where your holds are. It also really helps that the holds for each problem are all the same color as the level they are meant to be for. I really enjoyed how easy it was to tell where my next moves were even if I often couldn’t actually get to them.

The picture doesn’t really do the problems area justice as it is pretty big. The height of the walls is maybe a bit higher than I’m comfortable with, which is another reason why I like traverses – they tend to have lower walls. About 1/5 of the problems were switched out one time while we were there. The mats kind of have a more solid thunk when you hit them than you initially expect.

You can only see a little bit of the weight lifting/cardio area as I didn’t take a specific picture of it. It’s not big as is the case in most rock climbing areas, but it seemed fairly comprehensive. I don’t do weight training on top of the climbing although I will sometimes spend a bit of time running so having the treadmills was really nice.

There’s also an area to train on a bunch of more rock climbing specific holds/ladders/ etc which I also didn’t take a picture of. I know! I was lazy T_T. I kept forgetting to take pictures. That section was also much better equipped than the other rock climbing gyms that I’d been to so far. I enjoyed doing some core exercises in that area.

The regular partner rope climbing area

Of course, they also had a massive amount of the regular rope climbing walls. Harnesses are also 4€ to rent. I have no idea how nice the setup is, but maybe the picture is helpful.

In addition, they had a bunch of sections for children to climb in, which seemed pretty kid friendly although I didn’t actually see many kids maybe since one of the main areas for children was up a staircase away from the bouldering areas.

All in all, this is just a great bouldering gym. I really hope we spend more time in Lyon another time so that we can go back here.

Rock Climbing Note

Throughout this traveling adventure, I’ve picked up rock climbing as an exercise that’s easy to do while traveling. Since I move locations so often, I’ve been to a bunch of gyms (10+) in different countries at this point so I thought it might help someone to know what the different gyms are like.

I’m definitely still a beginner climber so I can’t speak to what gyms are like for experienced folks, but hopefully the information is still useful. Also, I only boulder so the gyms here will skew for those that have or are exclusively for bouldering.

Les Cafetiers

back area
View of the back/outside area of Les Cafetiers

Les Cafetiers
Address: 36 Rue Ferrandière, 69002 Lyon, France
Hours: M/W/Th/F: 9am-7pm, Sa: 10am-6pm, Su: 10am-5pm
Rating: 4.5/5.0

While we were in Lyon, Les Cafetiers quickly became our favorite place to work  at outside of the apartment we were staying at. It was started by a couple of British guys (one of the owners is apparently a pilot for British Airways), and the staff seem to all be native English speakers, which was really convenient for us.

front area
View of the front area of Les Cafetiers

The most important part of being able to work out of a cafe is of course the quality of its wi-fi, which in this case, was really good. The speed didn’t fluctuate. There were no random disconnections, and the information to connect was printed on the menu on each table.

There were an abundance of tables to work at, and after the lunch hour, the cafe was pretty quiet and uncrowded which made for a really comfortable environment to work in. As for outlets, there were 5/6 of them spread throughout the cafe, and all the indoor seating was within distance of at least 1 outlet. The outside seats didn’t have any outlets that I could see. Sometimes people would smoke at the outdoor seats and the smoke would waft in.

drink menu
The drinks menu at Les Cafetiers

They serve all your expected coffee types (I assume anyway since I’m not actually a regular coffee drinker), but they’ve also got a nice selection of teas. I ordered the hot chocolate every time and was pleasantly surprised to learn that they could replace the milk with oat milk since I’m lactose intolerant. Turns out oat milk is pretty tasty. I enjoyed oat milk much more than  conventional soy/almond milk replacements that are usually offered (although I think this has more to do with the quality of the soy/almond milk options out there rather than soy/almond milk themselves.

hot chocolate
Hot chocolate with oat milk

carrot cake
Delicious carrot cake!

 

In addition to drinks, they had a selection of pastries. You can also order salads although we always ate lunch before coming here to work so we never did. We had the carrot cake and the cheesecake (eaten too quickly to be pictured) which were both fantastic.

There is only one bathroom so it’s non-gendered, and it’s kept fairly clean. It’s on the same level as the rest of the shop without a step to access although because that area is a little narrow, it might not be entirely accessible.

Do be aware that they are super busy up until about the end of the lunch period. It’s not a good place to work until about 1:30pm. They are probably also fine in the morning before lunch, but we’re not up that early ever.

An Introduction of Sorts

So, I should probably introduce myself a bit. I’m Laura as the site name would imply and I’m currently traveling the world while working at the same time (with a boyfriend in tow). I’m not doing this through any particular program (e.g. remoteyear, etc) although I’ve certainly considered them.

Originally, I wasn’t planning on working quite so regularly while traveling, and this trip was supposed to only last a year. Along the way though, I was offered a fairly long contract which meant that I could travel for a lot longer. I’m not sure how long the travels will be in the end, but I’m not tired of the traveling lifestyle 10 months into it yet so we’ll see.

Now, you might be wondering, why start a blog 10 months into the journey rather than when it first began. I didn’t really have a topic that I felt particularly compelled to write about when I first started traveling.  I’m not all that interested in writing about seeing a new place or summarizing my travels;  plus, there are a lot of travel blogs out there that you can read for stuff like that.

The other part of it was that I spent the first 6 months or so really just relaxing, spending a lot of time with family, and generally enjoying the freedom of setting my schedule around mostly things that I wanted to do. Once I signed the contract to work though, I quickly realized that finding places to work out of was not the easiest thing. That’s when I decided it might be worthwhile to compile a list of places that I’ve found nice to work out of and maybe some other tidbits along the way such as ratings of climbing gyms in different places. (Yes, remoteyear would solve this issues, but that’s a topic for another time)

At any rate, I hope you will find this site useful 🙂