<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[YashoDev]]></title><description><![CDATA[YashoDev]]></description><link>https://yashodev.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 20 Oct 2026 06:35:08 GMT</lastBuildDate><atom:link href="https://yashodev.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How I made payments idempotent in Peshkar AI: a step-by-step walkthrough]]></title><description><![CDATA[Peshkar AI (peshkarai.in) lets Indian sellers turn a phone photo into a marketplace-ready listing. Sellers pay for it with credits, which they buy through Razorpay.  


Taking a payment looks like a s]]></description><link>https://yashodev.hashnode.dev/how-i-made-payments-idempotent-in-peshkar-ai-a-step-by-step-walkthrough</link><guid isPermaLink="true">https://yashodev.hashnode.dev/how-i-made-payments-idempotent-in-peshkar-ai-a-step-by-step-walkthrough</guid><category><![CDATA[ChaiCode]]></category><dc:creator><![CDATA[Yashovardhan Singh]]></dc:creator><pubDate>Mon, 12 Oct 2026 11:20:55 GMT</pubDate><content:encoded><![CDATA[<p>Peshkar AI (peshkarai.in) lets Indian sellers turn a phone photo into a marketplace-ready listing. Sellers pay for it with credits, which they buy through Razorpay.  </p>
<img src="https://cdn.hashnode.com/uploads/covers/6786929c41fe31e436f4076a/b276c5f1-f336-4a10-a90d-02bb54d7d42b.png" alt="" style="display:block;margin:0 auto" />

<p>Taking a payment looks like a solved problem: call the SDK, get a success, add credits. In practice, almost every bug in a payment system lives in the gaps. A callback arrives twice. A browser tab closes halfway. Two requests read the same row at the same moment. A server crashes between two writes.</p>
<p>This post walks through the whole flow, step by step, and explains <em>why</em> each decision was made. You can reuse these patterns with any payment gateway.</p>
<hr />
<h2>Step 1: Create the order on the server, and never trust the client's price</h2>
<p>When a seller picks a pack, the client sends only a <strong>pack ID</strong>, never an amount.</p>
<pre><code class="language-ts">packId: z.enum(["trial", "starter", "standard", "pro", "topup", ...]),
topupQuantity: z.number().int().min(1).max(2000).optional(),
</code></pre>
<p>The server looks up the price from its own table and creates the Razorpay order.</p>
<p><strong>Nuances:</strong></p>
<ul>
<li><p><strong>Amounts are integers in paise</strong> (<code>24900</code>, not <code>249.00</code>). Floating-point maths on money gives you answers like <code>0.1 + 0.2 = 0.30000000000000004</code>. Integers in the smallest currency unit avoid that whole class of bugs.</p>
</li>
<li><p><strong>Inputs are bounded.</strong> A custom top-up quantity is clamped to 1–2000. Without an upper bound, someone could request an absurd order. Without a lower bound, 0 or negative amounts become possible.</p>
</li>
<li><p><strong>We write a</strong> <code>Payment</code> <strong>row with</strong> <code>status: "created"</code> <strong>before any money moves.</strong> This row is our own record of the order. Every later step looks it up instead of trusting what the client or the webhook claims.</p>
</li>
</ul>
<h2>Step 2: Checkout, and why one confirmation isn't enough</h2>
<p>The seller pays inside Razorpay Checkout. When it finishes, Razorpay can tell us about it in <strong>two independent ways</strong>:</p>
<table>
<thead>
<tr>
<th>Path</th>
<th>Strength</th>
<th>Weakness</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Browser callback</strong> → our <code>verifyPayment</code> endpoint</td>
<td>Instant, so the seller sees credits immediately</td>
<td>The tab can close, the network can drop, or the user can navigate away</td>
</tr>
<tr>
<td><strong>Webhook</strong> → Razorpay calls our server directly</td>
<td>Reliable, and retried until we acknowledge it</td>
<td>Asynchronous: it can arrive before, after, or long after the callback</td>
</tr>
</tbody></table>
<p>You need <strong>both</strong>. The callback gives a good experience and the webhook guarantees delivery. But now two code paths can try to credit the same order, sometimes at the same millisecond. That race is the main subject of this post.</p>
<h2>Step 3: Verify the signature, because anyone can call your endpoint</h2>
<p>Both endpoints are public URLs. Before trusting anything, we prove the message really came from Razorpay, using <strong>HMAC-SHA256</strong>: a hash keyed with a secret only Razorpay and our server know.</p>
<p><strong>Client path:</strong> Razorpay hands the browser <code>order_id</code>, <code>payment_id</code> and a <code>signature</code>. We recompute:</p>
<pre><code class="language-ts">HMAC_SHA256(key_secret, `${order_id}|${payment_id}`)
</code></pre>
<p><strong>Webhook path:</strong> we compute the HMAC over the <strong>raw request body</strong> with a separate webhook secret.</p>
<p><strong>Nuance: why the <em>raw</em> body?</strong> By the time a normal Express handler runs, the JSON has already been parsed. If you <code>JSON.stringify(req.body)</code> to rebuild it, whitespace, key order or number formatting can differ from the bytes Razorpay signed, and a perfectly valid webhook fails verification. We capture the exact bytes as they arrive:</p>
<pre><code class="language-ts">express.json({ verify: (req, _res, buf) =&gt; { req.rawBody = buf; } })
</code></pre>
<p>If the raw buffer is missing, we reject the request instead of falling back to the parsed body.</p>
<p><strong>Nuance: an IDOR check on the client path.</strong> We look up the payment by <code>razorpayOrderId</code> <strong>and</strong> <code>userId</code>. Otherwise a logged-in user could submit someone else's order ID. (IDOR, "insecure direct object reference", means fetching an object by ID without checking who owns it.)</p>
<h2>Step 4: <code>timingSafeEqual</code>, the comparison most people get wrong</h2>
<p>After computing the expected signature, the obvious check is:</p>
<pre><code class="language-ts">if (expected === received) { ... }   // ❌
</code></pre>
<p>The problem is that <code>===</code> on strings <strong>stops at the first character that differs</strong>. Comparing <code>"a9f3..."</code> against <code>"b000..."</code> returns slightly faster than comparing it against <code>"a9f0..."</code>. That difference is tiny, but over thousands of requests it can be measured. In principle, an attacker can use that timing to discover a valid signature one character at a time. This is called a <strong>timing side-channel attack</strong>.</p>
<p><code>crypto.timingSafeEqual</code> always compares <strong>every byte</strong>, however early a mismatch occurs, so the response time reveals nothing:</p>
<pre><code class="language-ts">const a = Buffer.from(expected, "utf8");
const b = Buffer.from(received || "", "utf8");
if (a.length !== b.length) return false;   // timingSafeEqual throws on unequal lengths
return crypto.timingSafeEqual(a, b);
</code></pre>
<p><strong>Nuances:</strong></p>
<ul>
<li><p><strong>Check the length first.</strong> <code>timingSafeEqual</code> throws if the buffers differ in length. An unhandled throw becomes a 500, which is a bug and itself a signal to an attacker. Returning early on a length mismatch is safe here, because a SHA-256 hex digest is always 64 characters, so the length is public anyway.</p>
</li>
<li><p><code>|| ""</code> <strong>guards against a missing signature</strong>, so an absent header is a clean <code>false</code> rather than a crash.</p>
</li>
<li><p>We unit-test this exact function with: a valid signature, <strong>a tampered signature of the same length</strong> (the case <code>===</code> would also catch, but which must still fail), the wrong length, and an empty string.</p>
</li>
</ul>
<h2>Step 5: Atomic capture, where the database decides who wins</h2>
<p>Now both paths have a verified payment. The naive code looks like this:</p>
<pre><code class="language-ts">const p = await Payment.findOne({ orderId });
if (p.status !== "captured") {        // check…
  p.status = "captured"; await p.save();
  await addCredits(...);              // …then act
}
</code></pre>
<p>This is a classic <strong>check-then-act race</strong>. If the callback and the webhook both run <code>findOne</code> before either saves, both see <code>"created"</code> and <strong>both add credits</strong>. The seller pays once and gets credited twice.</p>
<p>The fix is to make the check and the write <strong>a single atomic operation</strong>:</p>
<pre><code class="language-ts">const won = await Payment.findOneAndUpdate(
  { _id: payment._id, status: { $ne: "captured" } },   // the condition…
  { $set: { status: "captured", razorpayPaymentId } }, // …and the write, in one step
  { new: true }
);

if (!won) return currentBalance;   // someone else already captured it: idempotent no-op
await addCredits(...);             // only the winner gets here
</code></pre>
<p>MongoDB guarantees that an update to a single document is atomic. Only one caller can move the order from <code>created</code> to <code>captured</code>. The other caller's filter no longer matches, so it gets <code>null</code> back.</p>
<p><strong>Nuances:</strong></p>
<ul>
<li><p><strong>The loser isn't an error.</strong> The payment <em>did</em> succeed, so the loser returns success and the current balance. That's what idempotent means: running it twice has the same effect as running it once.</p>
</li>
<li><p><strong>There's also a fast path.</strong> If <code>verifyPayment</code> sees the order is already <code>captured</code> before even checking the signature, it returns immediately. That's an optimisation; the atomic update is the actual guarantee.</p>
</li>
<li><p><strong>No distributed locks, no Redis mutex.</strong> The database row is the lock, which means fewer moving parts.</p>
</li>
<li><p><strong>A failed browser signature marks the payment</strong> <code>failed</code><strong>, but that isn't final.</strong> The capture filter is <code>status ≠ captured</code>, so a valid webhook can still capture it. Razorpay remains the source of truth for whether money actually moved.</p>
</li>
</ul>
<h2>Step 6: What the webhook sends back matters</h2>
<p>Razorpay <strong>retries</strong> a webhook until it receives a 2xx response. So each response code is a decision:</p>
<table>
<thead>
<tr>
<th>Situation</th>
<th>Response</th>
<th>Why</th>
</tr>
</thead>
<tbody><tr>
<td>Bad or missing signature</td>
<td><code>400</code></td>
<td>It isn't from Razorpay; never process it</td>
</tr>
<tr>
<td>Already captured (duplicate)</td>
<td><code>200</code></td>
<td>Acknowledge it, or Razorpay will keep retrying a no-op</td>
</tr>
<tr>
<td>Unknown order ID</td>
<td><code>200</code> + log</td>
<td>Retrying won't make it exist, so don't create a retry storm</td>
</tr>
<tr>
<td>Our database or processing failed</td>
<td><code>500</code></td>
<td>We <em>want</em> a retry; this is how a temporary failure recovers</td>
</tr>
</tbody></table>
<h2>Step 7: Add credits with <code>$inc</code>, never read-modify-write</h2>
<pre><code class="language-ts">User.findOneAndUpdate({ _id }, { $inc: { creditBalance: amount } }, { new: true })
</code></pre>
<p><strong>Nuance:</strong> <code>user.creditBalance += amount; await user.save()</code> reads a value, changes it in memory, and writes it back. Two concurrent updates can overwrite each other, which is a <strong>lost update</strong>. <code>$inc</code> makes the database do the arithmetic atomically.</p>
<h2>Step 8: An append-only ledger</h2>
<p>Every credit movement inserts one <strong>immutable</strong> row:</p>
<pre><code class="language-plaintext">{ type: PURCHASE | DEBIT | REFUND | BONUS | TRIAL, amount, balanceAfter, referenceId, referenceType }
</code></pre>
<p><strong>Nuances:</strong></p>
<ul>
<li><p><strong>Rows are never updated or deleted.</strong> A balance is a cached number; the ledger is the history that explains it. If a support ticket says "I paid but got nothing", you can answer from data.</p>
</li>
<li><p><code>balanceAfter</code> <strong>comes from the document returned by the atomic</strong> <code>$inc</code>, not from a separate read, so it's exact even under concurrency.</p>
</li>
<li><p><code>referenceId</code> <strong>links each row back to its payment or listing</strong>, and it's indexed (<code>userId + referenceId</code>), which makes reconciliation and "was this already refunded?" checks cheap.</p>
</li>
</ul>
<h2>Step 9: Side effects must never break the payment</h2>
<p>After a purchase, we pay the referrer's bonus if this was the buyer's first purchase:</p>
<pre><code class="language-ts">grantReferralRewardOnFirstPurchase(userId).catch(() =&gt; {});
</code></pre>
<p>It's fire-and-forget on purpose. A bug in the referral system must never turn a successful payment into a 500, which would cause a webhook retry or show the user an error. The referral claim is itself atomic, so it pays out once even if it's triggered twice.</p>
<hr />
<h2>The same principles on the spending side</h2>
<p>Buying credits is half the story. Spending them has the same problems.</p>
<p><strong>Atomic debit, no overdraw:</strong></p>
<pre><code class="language-ts">User.findOneAndUpdate(
  { _id, creditBalance: { $gte: cost } },     // only if they can afford it
  { $inc: { creditBalance: -cost } }
)
</code></pre>
<p>Two concurrent generations with a balance of 5 and a cost of 5 each: exactly one succeeds. The other gets <code>null</code>, which becomes a clean "insufficient credits" error.</p>
<p><strong>Compensating transaction:</strong> we charge first and then queue the AI job. If queuing fails (for example, Redis is down), we <strong>refund immediately</strong> and mark the listing failed. The seller is never charged for work that will never run.</p>
<p><strong>Refund exactly once:</strong> if the worker exhausts its retries, it first <em>claims</em> the refund atomically:</p>
<pre><code class="language-ts">ListingObject.findOneAndUpdate(
  { _id, creditsRefunded: false, creditsCharged: { $gt: 0 } },
  { $set: { creditsRefunded: true } }
)
</code></pre>
<p>Only the caller that flips the flag issues the refund. It's the same pattern as the payment capture, reused.</p>
<hr />
<h2>What's still imperfect, and how I'd fix it</h2>
<p>Being honest about the gaps is part of the engineering:</p>
<ol>
<li><p><strong>Capture and credit are two separate writes.</strong> If the process dies after marking the order <code>captured</code> but before <code>$inc</code>, the seller paid and got nothing, and neither path will retry because the order already looks captured. Two possible fixes:</p>
<ul>
<li><p>Wrap the capture, the credit and the ledger insert in a <strong>MongoDB multi-document transaction</strong>.</p>
</li>
<li><p>Run a <strong>reconciliation job</strong> that finds payments marked <code>captured</code> with no matching <code>PURCHASE</code> ledger row and repairs them. Real payment systems usually run something like this regardless.</p>
</li>
</ul>
</li>
<li><p><strong>The balance update and the ledger insert are also separate writes</strong>, so the same transaction would close that gap too.</p>
</li>
<li><p><strong>Webhooks aren't de-duplicated by event ID.</strong> The atomic capture makes duplicates harmless, but storing the event ID would make replays easy to spot in logs.</p>
</li>
</ol>
<hr />
<h2>Takeaways</h2>
<ul>
<li><p>Never trust the client's price. Compute amounts on the server, as integers in paise.</p>
</li>
<li><p>Expect every message to arrive <strong>twice, late, or out of order</strong>.</p>
</li>
<li><p>Verify signatures over <strong>raw bytes</strong>, with <code>timingSafeEqual</code>.</p>
</li>
<li><p>Replace check-then-act with <strong>one conditional atomic update</strong>. Let the database be the referee.</p>
</li>
<li><p>Use <code>$inc</code>, never read-modify-write.</p>
</li>
<li><p>Keep an <strong>append-only ledger</strong>, so every balance can be explained.</p>
</li>
<li><p>When a later step fails, <strong>compensate</strong> for the earlier one, and make the compensation itself run exactly once.</p>
</li>
</ul>
<p>Getting the happy path working is easy. The real work is making the code stay correct when things happen twice, out of order, or halfway through.</p>
<p><em>Code: github.com/Akashjaiswal-IT/ListingLift</em></p>
]]></content:encoded></item><item><title><![CDATA[What Is AI Evals And How Mem0 Implements Evaluations]]></title><description><![CDATA[Introduction
I recently made a memory layer for one of my Gen AI project (a lightweight Notebook LM clone ) to apply for Mem0 , After sharing it on X someone suggested me to implement AI evals in the project . At that time I didn’t knew about AI eval...]]></description><link>https://yashodev.hashnode.dev/what-is-ai-evals-and-how-mem0-implements-evaluations</link><guid isPermaLink="true">https://yashodev.hashnode.dev/what-is-ai-evals-and-how-mem0-implements-evaluations</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[Chaiaurcode]]></category><category><![CDATA[AI]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[gen ai]]></category><category><![CDATA[memory]]></category><category><![CDATA[agentic AI]]></category><category><![CDATA[Mem0 ]]></category><dc:creator><![CDATA[Yashovardhan Singh]]></dc:creator><pubDate>Sun, 12 Oct 2025 03:17:43 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>I recently made a memory layer for one of my Gen AI project (a lightweight Notebook LM clone ) to apply for Mem0 , After sharing it on X someone suggested me to implement AI evals in the project . At that time I didn’t knew about AI evals so I got curious and started learning about AI evals and how Mem0 incorporates evaluations for their Memory Layer .</p>
<p>So here I am sharing all that I have learned about AI evals and Mem0 evaluation process .</p>
<h2 id="heading-why-we-need-ai-evals">Why We Need AI Evals</h2>
<p>Generally most of the systems that we see or build is deterministic in nature meaning if you give the same input the output or expected output is always the same. For example , In a chatting app , if you send a message to someone , the expected output is straightforward , the message is delivered and received correctly at the other end .</p>
<p>Which is not the case with AI systems as they are non deterministic in nature meaning if you give the same input multiple times the output will be slightly different every time , you can’t have a definite expectation of output , which makes testing this systems difficult as compared to the traditional systems . Here AI evals is used to test and quantify how the system is working . AI evals defines the perfect output and how close the response is from the perfect output . You can also define the tone , quality and safety metrics of the response using AI evals .</p>
<p>AI Evals defines benchmarks on quantified criteria to determine the performance of the system . As the software grows more and more complex , it becomes very difficult to improve and correct things without evaluations . Not having evaluation may lead to very severe edge cases that can harm your business and image . For instance , Microsoft’s Tay chatbot had to be taken offline after it learned to produce harmful content within 24 hours. In 2022 Meta’s Galactica model similarly “hallucinated” wildly incorrect scientific content, prompting an immediate shutdown. If the AI evals are designed well it can catch all such failures , loopholes about factual accuracy , tone , safety metrics and all the other metrics in which you want your system to perform well . AI evals provide a feedback loop which can be utilized to improve , fine tune the system to make it more reliable &amp; safe and to work on the system to achieve a specific target .</p>
<h2 id="heading-high-level-overview-of-a-basic-ai-evaluation-system">High-Level Overview of a Basic AI Evaluation System</h2>
<p>A basic AI evaluation system starts by defining the tasks that are being performed by the AI system , and then writing the prefect response of various scenarios of input related to the tasks , for a very complex system this scenario’s may exceed a very large number for scenarios , as more the goldens better the evaluations , if needed take help from the LLM’s to cover the edge cases and large a lot of data set of goldens . Then comes the evaluation part , the team decides the criteria in which they want rate the response and then finalize a rating metrics for all and then from the data set created by LLM’s or from the input given by the user rate the response given by the system on the metrics , you can even automate the rating process as well by keeping it check with frequent human feedbacks and knowing the comparison between the reviews given by LLM and the reviews given by human so that the ratings can be trusted accordingly . And then using those metrics and ratings reiterate or improve the system accordingly . This can be summarized as :- define tasks → define goldens → run model → compare output to goldens → compute scores/ratings → analyze results.</p>
<h2 id="heading-building-a-basic-ai-eval-system">Building a Basic AI Eval System</h2>
<h3 id="heading-1-define-goldens">1) Define Goldens</h3>
<p>Goldens are the benchmark for the responses given by your system , it is an ideal input and output example for your system keeping in mind the factual correctness , tone , quality of output , safety metrics and all other metrics that you want your system to have . Start by manually defining the perfect and ideal input output examples for all the possible scenarios and edge cases that you can think of , for a complex system it can even be in hundreds or more . Things become easier in longer run if your goldens are sorted in the beginning .<br />An example of the goldens would be ,</p>
<p>For a customer support agent</p>
<p><strong>Input :-</strong></p>
<pre><code class="lang-plaintext">Hi, my order #45219 was supposed to arrive yesterday, but the tracking link still says “in transit.” Can you check what’s going on? I really needed it by today.
</code></pre>
<p><strong>Response :-</strong></p>
<pre><code class="lang-plaintext">Hello! I’m really sorry your order hasn’t arrived yet — I understand how frustrating that must be.  
I’ve checked your order #45219, and it’s currently delayed due to a logistics issue. The package is still on its way and should arrive by tomorrow evening.  

We’ve also refunded your express shipping fee as an apology for the delay.  
You’ll receive a confirmation email shortly.  

Thank you for your patience — please let me know if you’d like me to update the delivery address or time.
</code></pre>
<p><strong>Why it is a golden ?</strong><br />Because it’s the ideal, policy-aligned, and empathetic response that sets the benchmark for correct AI behaviour.</p>
<h3 id="heading-2synthetic-data-generation">2)Synthetic Data Generation</h3>
<p>Using the goldens written generate a larger set of such examples using an LLM increasing the coverage of examples . As the goldens written manually are not enough to test a system at scale . So generating a larger set of examples increases the chance of covering the edge cases that you might have over looked .<br />It makes testing more thorough resulting in more data to evaluate and improve on .<br />An example of prompt that can generate more examples from the existing set of goldens</p>
<blockquote>
<p>You are an AI assistant that generates <strong>synthetic customer support evaluation data</strong> using reference golden examples.<br />Use the given golden as a guide for tone, structure, and policy alignment.<br />Create new and diverse examples that test reasoning, empathy, and accuracy — including variations and edge cases.</p>
<p><strong>Output must be valid JSON.</strong></p>
<p><strong>Input (Golden Example):</strong><br />Customer: "Hi, my order #45219 was supposed to arrive yesterday, but the tracking link still says 'in transit.' Can you check what’s going on? I really needed it by today."<br />Expected Response: "Apologize empathetically, provide tracking update, and offer compensation if policy allows."</p>
<p><strong>Your Task:</strong><br />Generate 10 new synthetic examples following the same format.</p>
<p><strong>Output Format (JSON):</strong></p>
<pre><code class="lang-plaintext">{
  "examples": [
    {
      "id": 1,
      "customer": "string",
      "expected_response": "string",
      "intent": "string", 
      "category": "string"
    }
  ]
}
</code></pre>
<p><strong>Instructions for Generation:</strong></p>
<ul>
<li><p>Include different <strong>categories</strong>: late delivery, damaged product, refund issue, wrong item, cancellation, subscription, missing refund, rude staff, etc.</p>
</li>
<li><p>Ensure <strong>expected_response</strong> models empathy, accuracy, and policy compliance.</p>
</li>
<li><p>Keep tone <strong>natural and professional</strong>.</p>
</li>
<li><p>Avoid duplicates or trivial rewording.</p>
</li>
</ul>
</blockquote>
<p>Use prompt written by yourself according to your own requirements or use pre tested open source prompts . Try to avoid generating the prompts from LLMs as it might reduce the quality of your response due to drift and degradation .</p>
<h3 id="heading-3-unit-tests">3) Unit Tests</h3>
<p>Incorporate simple unit tests similar to traditional unit tests to rule out all the basic failures . For example , If you don’t want any specific keywords , a specific format data , if it’s too long or short . For this basic programmatic flaws to be picked before human you can write unit tests .<br />You can write unit test for either a specific task such as for a feature which fetches perfect product for you can write unit test according to the list of matching queries or a generic unit test which is applicable on all the features . An example will be if you don’t want your response to have any abbreviations , so you can write a regex test to check that .<br />You must write hundreds of tests to make your system as perfect as possible .<br />Do not shy away to take help of LLM to brainstorm on the edge cases that you might be over looking .</p>
<p>Talking of LLM , use it to generate the test cases that triggers all the unit tests you have to thoroughly check your system and every edge cases possible .<br />You can integrate this tests using CI infrastructure ( e.g., GitHub Actions, GitLab Pipelines, etc.) You should also collect metrics along with the results of tests to analyse and track your system .</p>
<h3 id="heading-4-grading-rating">4) Grading / Rating</h3>
<p>Define a clear rating system for the subjective quality dimensions of the response ,the closer the response is to the golden, the higher its rating should be. Rating and grading can be anything a numeric value , binary or bool . The guidelines for every rating should be pre defined strictly to maintain consistency of the rating system . Grading and rating should be done by human evaluation . Use the example data set generated or user’s data to generate response and then rate it on the pre determined rating system .<br />For example :-</p>
<pre><code class="lang-plaintext">{
  "example_id": 18,
  "customer": "Hi, I received a damaged lamp in my order. Can you send a replacement?",
  "golden_response": "I’m really sorry your lamp arrived damaged. I’ve arranged for a replacement to be shipped immediately, and you’ll receive the confirmation email shortly. You can dispose of the broken lamp safely — no need to return it.",
  "model_response": "Apologies for the damaged lamp. Please send it back so we can review and then send a new one.",
  "rating": {
    "empathy": 4,
    "accuracy": 4,
    "tone_professionalism": 4,
    "policy_alignment": true,
    "closeness_to_golden": 3,
    "overall_score": 3.8
  },
  "graded_by": "human_evaluator_07"
}
A Sample Of Customer Support Agent Evaluation And Rating
</code></pre>
<h3 id="heading-5-llm-auto-raters">5) LLM Auto-Raters</h3>
<p>Rating every response manually takes a lot of labour and time , that can be spent on something more productive . So you can implement an LLM auto-rater ( LLM as a judge ) whose job is to rate the response according to the goldens as reference and source of truth specified on rating system specified by you .<br />LLM auto-rater should be frequently compared to the human evaluator to know the accuracy of LLM’S evaluation and the minimum accuracy should be defined by team (For example , 80%) , above which the LLM auto-rater can be trusted to rate on behalf of humans . Although there should be a human in the loop always to give constant feedbacks and reviews to improve the auto-raters and the goldens . LLM auto-rater should always be seen as to a tool that reduces the time and labour and not as replacement of human . It should be checked by humans frequently to keep it in check .</p>
<h3 id="heading-6-tracing-and-logs">6) Tracing and Logs</h3>
<p>Instrument AI pipelines to show input , response , evaluation results / ratings for better understanding of the response given by the system to improve the system more . LangSmith and other observability tools were being used before to see the logs meaning history of all the user inputs and the model outputs but nowadays OpenAI gives this observability inbuilt reducing the hassle of setting up these tools . This logs can be used to trace back to the exact set of input , output which is causing the problem . Hence making it easier to debug , resolve and improve systems by making changes .</p>
<p>An example of logs of a project like Browser Use (Browser Automation Agent) - made by me</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760222904139/aeeff322-f43d-47fa-ab7f-965c47cb26bd.png" alt="Logs Of A Project Like Browser Use - Browser Automation Agent" class="image--center mx-auto" /></p>
<h2 id="heading-improve-with-the-help-of-evals">Improve with the Help of Evals</h2>
<p>The ultimate goal of evaluation is to inform improvements. Effective evals serve three purposes: <strong>analyze</strong> system behavior, <strong>measure</strong> performance quantitatively, and <strong>improve</strong> by fixing weaknesses. For example, evaluation may highlight that answers often lack factual detail now you can do prompt refinements , pass better contexts with factual data accordingly . You might use evals to label new training data (supervised fine-tuning) or to select better few-shot examples. Evals also help prioritize work , they identify the most common failure modes (e.g. hallucinations or misunderstanding), so engineers know where to focus. By cycling through this Analyze–Measure–Improve loop, teams can steadily raise their AI’s quality and reliability .</p>
<h2 id="heading-how-evaluation-happens-in-mem0">How Evaluation Happens in Mem0</h2>
<p>Mem0 has a very different technique and strategy for evaluation . It evaluates it’s system using a structured benchmark approach . The core of this is a LOCOMO dataset , a suite of long-term conversations designed to test memory recall . Mem0 compares it’s performance metrics with different tools set a baseline of benchmark to surpass . Let’s see this in detail :-</p>
<h2 id="heading-overview">Overview</h2>
<h3 id="heading-1-established-locomo-benchmarks">1) <strong>Established LOCOMO Benchmarks</strong></h3>
<p>Mem0 evaluates its performance against five established approaches to see where it stands ,</p>
<ul>
<li><p>LoCoMo</p>
</li>
<li><p>ReadAgent</p>
</li>
<li><p>MemoryBank</p>
</li>
<li><p>MemGPT</p>
</li>
<li><p>A-Mem</p>
</li>
</ul>
<h3 id="heading-2-open-source-memory-solutions">2) <strong>Open-Source Memory Solutions</strong></h3>
<p>Mem0 also tests on open-source tools like LangMem that handles memory layer in AI models to compare and benchmark results</p>
<h3 id="heading-3-rag-systems"><strong>3 ) RAG Systems</strong></h3>
<p>Mem0 implements Retrieval-Augmented Generation with various configuration . It tests different chunk sizes and the retrieval counts to optimize it’s performance .</p>
<h3 id="heading-4-full-context-processing">4) <strong>Full-Context Processing</strong></h3>
<p>Mem0 also examines the effectiveness of passing the whole conversation history within the context window of LLM as a baseline approach for its evaluation</p>
<h3 id="heading-5-proprietary-memory-systems">5) <strong>Proprietary Memory Systems</strong></h3>
<p>Mem0 evaluates OpenAI’s built in memory feature available in their ChatGPT interface to compare against commercial solutions.</p>
<h3 id="heading-6-third-party-memory-providers">6) <strong>Third-Party Memory Providers</strong></h3>
<p>Mem0 incorporates Zep, a specialized memory management platform designed for AI agents, to assess the performance of dedicated memory infrastructure.</p>
<h2 id="heading-dataset">Dataset</h2>
<p>Mem0 tests these techniques on the LOCOMO dataset, which contains conversational data with various question types to evaluate memory recall and understanding. Dataset has 10 multi-session conversations with 26 k tokens in each conversation , having 200 questions per conversation</p>
<p><a target="_blank" href="https://drive.google.com/drive/folders/1L-cTjTm0ohMsitsHg4dijSPJtqNflwX-">Download Dataset</a></p>
<p>This dataset is specially designed to test memory recalls and understanding across various question types and complexity level .</p>
<h2 id="heading-rating-metrics-and-evaluation">Rating Metrics And Evaluation</h2>
<h3 id="heading-project-flow">Project Flow</h3>
<ul>
<li><p>Mem0 is evaluated against to OpenAI , LangMem , Zep to determine a base benchmark of performance for Mem0 .</p>
</li>
<li><p>RAG techniques are tested to give the best retrieval of context for memory .</p>
</li>
<li><p>A script for metrics to calculate evaluation metrics and prompt used for the models is available .</p>
</li>
<li><p>An evaluation script :- Evaluates model responses using BLEU, F1 scores and LLM as a judge, then stores detailed evaluation results in JSON format.</p>
</li>
<li><p>Run experiment script :- Runs different memory architectures (Mem0, RAG, LangMem, Zep, OpenAI), adds or searches memories, and saves raw responses for evaluation.</p>
</li>
<li><p>Generate Score script :- Aggregates evaluation results, computes mean BLEU/F1/LLM scores per category, and prints overall performance metrics</p>
</li>
</ul>
<h3 id="heading-metrics">Metrics</h3>
<ul>
<li><p><strong>BLEU (Bilingual Evaluation Understudy) :-</strong> In Mem0’s evaluation pipeline, BLEU measures how <strong>textually similar</strong> a model’s generated response is to the <strong>ground truth (golden)</strong> answer.<br />  It captures <strong>n-gram overlap</strong> , meaning how many words or short phrases match , giving a sense of linguistic closeness between the system’s output and the ideal answer.<br />  A <strong>higher BLEU score</strong> means the memory system’s retrieval and response generation are more <strong>faithful to the expected context.</strong></p>
</li>
<li><p><strong>F1 Score (Harmonic Mean of Precision and Recall) :-</strong> Mem0 uses F1 to evaluate how <strong>accurate and complete</strong> the model’s response is. It checks whether the system retrieves <strong>correct factual pieces (precision)</strong> and whether it <strong>misses any relevant ones (recall)</strong> from memory.<br />  A <strong>higher F1</strong> means the memory retrieval and summarization are more balanced and contextually complete.</p>
</li>
<li><p><strong>LLM Score (Large Language Model Judge Score) :-</strong> This is Mem0’s <strong>LLM-as-a-judge</strong> metric , an additional evaluation layer where an <strong>external LLM</strong> compares the predicted response with the ground truth. It judges <strong>semantic correctness, coherence, tone, and factuality</strong>, going beyond simple text overlap.<br />  A <strong>higher LLM score</strong> indicates that the system’s memory retrieval and response <strong>feel more human-like, context-aware, and semantically aligned</strong> — even if the wording differs.</p>
</li>
</ul>
<h2 id="heading-evaluations">Evaluations</h2>
<p>In evaluation Mem0 processes each and every question and answer pair , Calculates <strong>BLEU</strong> ( Bilingual Evaluation Understudy .i.e. Measures <strong>text similarity</strong> between the model’s response and the golden response, based on <strong>n-gram overlap</strong>) and <strong>F1 scores</strong> ( Combines <strong>Precision</strong> and <strong>Recall</strong>) . Then Mem0 uses an LLM judge to evaluate answer correctness and then store the results in an output file .</p>
<h2 id="heading-generating-scores">Generating Scores</h2>
<p>Mem0 generates scores by loading evaluation metrics data . Then calculates mean scores for each category (BLEU, F1, LLM) . Also reports the number of question per categories and then calculates the mean scores across all the categories .  </p>
<hr />
<h2 id="heading-example-output">📊 Example Output</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Category</td><td>BLEU Score</td><td>F1 Score</td><td>LLM Score</td><td>Count</td></tr>
</thead>
<tbody>
<tr>
<td>1</td><td>0.xxxx</td><td>0.xxxx</td><td>0.xxxx</td><td>xx</td></tr>
<tr>
<td>2</td><td>0.xxxx</td><td>0.xxxx</td><td>0.xxxx</td><td>xx</td></tr>
<tr>
<td>3</td><td>0.xxxx</td><td>0.xxxx</td><td>0.xxxx</td><td>xx</td></tr>
</tbody>
</table>
</div><h3 id="heading-overall-mean-scores"><strong>Overall Mean Scores</strong></h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Metric</td><td>Mean Score</td></tr>
</thead>
<tbody>
<tr>
<td><strong>BLEU Score</strong></td><td>0.xxxx</td></tr>
<tr>
<td><strong>F1 Score</strong></td><td>0.xxxx</td></tr>
<tr>
<td><strong>LLM Score</strong></td><td>0.xxxx</td></tr>
</tbody>
</table>
</div><hr />
<h2 id="heading-evaluation-metrics">Evaluation Metrics</h2>
<p>Mem0 uses several metrics to evaluate the performance of different memory techniques:</p>
<ol>
<li><p><strong>BLEU Score</strong>: Measures the similarity between the model's response and the ground truth</p>
</li>
<li><p><strong>F1 Score</strong>: Measures the harmonic mean of precision and recall</p>
</li>
<li><p><strong>LLM Score</strong>: A binary score (0 or 1) determined by an LLM judge evaluating the correctness of responses</p>
</li>
<li><p><strong>Token Consumption</strong>: Number of tokens required to generate final answer.</p>
</li>
<li><p><strong>Latency</strong>: Time required during search and to generate response.</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>AI evaluations are the foundation of building reliable, safe, and measurable AI systems. They turn subjective model behaviour into quantifiable metrics that help teams understand, compare, and improve performance. By defining strong goldens, generating diverse synthetic data, setting clear grading standards, and leveraging both human and LLM-based raters, developers can systematically analyze how close their systems are to the ideal output.</p>
<p>Mem0’s structured evaluation framework illustrates how thoughtful benchmarking—using metrics like BLEU, F1, and LLM judge scores—can transform experimentation into evidence-based improvement. Through continuous evaluation, tracing, and iteration, AI systems evolve from unpredictable prototypes into consistent, trustworthy tools.</p>
<p>In short, <strong>AI evals are not just about scoring models — they’re about creating a feedback loop that drives progress, reliability, and accountability in every generation of AI.</strong></p>
<h2 id="heading-reference">Reference</h2>
<p><a target="_blank" href="https://github.com/mem0ai/mem0">https://github.com/mem0ai/mem0</a></p>
<p><a target="_blank" href="https://mem0.ai/blog/benchmarked-openai-memory-vs-langmem-vs-memgpt-vs-mem0-for-long-term-memory-here-s-how-they-stacked-up">https://mem0.ai/blog/benchmarked-openai-memory-vs-langmem-vs-memgpt-vs-mem0-for-long-term-memory-here-s-how-they-stacked-up</a></p>
<p><a target="_blank" href="https://hamel.dev/blog/posts/evals/">https://hamel.dev/blog/posts/evals/</a></p>
<p><a target="_blank" href="https://youtu.be/0Ta8ws4pU-8?si=wzGDCTFF0HaFeGw-">https://youtu.be/0Ta8ws4pU-8?si=wzGDCTFF0HaFeGw-</a>  </p>
<p><strong>If you use the dataset given in blog or code from the evaluation folder of Mem0 repo in your research paper , Please cite Mem0’s paper given below</strong></p>
<pre><code class="lang-plaintext">@article{mem0,
  title={Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory},
  author={Chhikara, Prateek and Khant, Dev and Aryan, Saket and Singh, Taranjeet and Yadav, Deshraj},
  journal={arXiv preprint arXiv:2504.19413},
  year={2025}
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Advance RAG Pipelines]]></title><description><![CDATA[Introduction to RAG
First of all let’s understand what is RAG (Retrieval Augmented Generation) , LLM’s can give reply only when either they are pre trained to to your query or they have context related to the query being asked .
What if you want to a...]]></description><link>https://yashodev.hashnode.dev/advance-rag-pipelines</link><guid isPermaLink="true">https://yashodev.hashnode.dev/advance-rag-pipelines</guid><category><![CDATA[ChaiCode]]></category><category><![CDATA[Chaiaurcode]]></category><dc:creator><![CDATA[Yashovardhan Singh]]></dc:creator><pubDate>Thu, 21 Aug 2025 21:00:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1755809697674/93e5a6f9-6c8d-49e5-9848-845addaacb77.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction-to-rag">Introduction to RAG</h2>
<p>First of all let’s understand what is RAG (Retrieval Augmented Generation) , LLM’s can give reply only when either they are pre trained to to your query or they have context related to the query being asked .</p>
<p>What if you want to ask questions from LLM related to your personal data , it might not be able to give very accurate answer . So we give context of our data to the LLM but it might exceed the context size .</p>
<p>So the solution of this problem is RAG , in this you first divide the given context into chunks and then convert it to embeddings according to the LLM and then store it in a semantic way in any vector DB , from where it get’s fetched according to the user query , the chunks which are very close to the queries are fetched and then passed as a context to LLM to give the output for the user query .</p>
<p>That’s a basic very high level knowledge of RAG . Now basic RAG’s works on indexing data and then Retrieval of relevant chunks leading to generation of desired output .</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755802918821/458f8319-4fce-4ab5-9ea0-07b4832453c2.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-success-metric-of-a-rag">Success metric of a RAG</h2>
<p>Now the question that arises is what is the success metric of a successful RAG . Is it Speed , Is it cost , Is it accuracy . Mostly we prefer accuracy as it’s the most important and crucial point . What we will do with the speed and less cost if the answer is inaccurate , it doesn’t add value .</p>
<p>Fun example :- If I say sun rises from west very quickly it is not a good thing and people might call me mad, if I take few second and I say sun rises from east no one is going to call me mad they might call me slow but not mad as it is factually accurate .</p>
<h2 id="heading-system-design-of-rag">System Design of RAG</h2>
<p>Now , there are few problems that we might have to face . Let’s suppose you applied RAG very beautifully . But the source file or data given is bad , then the output will surely be bad as the context is bad .</p>
<p>And even if the source file or data given as context is good and the user query is very bad it might cause you accuracy issue in your RAG .</p>
<p>There is a simple concept of GIGO ( Garbage In Garbage Out ) ,If you give bad input (Garbage ) to a LLM it will return you bad output ( Garbage ) in return .</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755804108711/6a98e336-ed9d-4cec-a6f0-327996f07f73.png" alt class="image--center mx-auto" /></p>
<p>Now you might shrug it off this issue by saying the mistake is from user’s side what can I do for that .</p>
<p>Actually , You can do a lot of things .Let’s see what :-</p>
<h3 id="heading-query-rewriting">Query Rewriting</h3>
<p>When the user gives a input , it might be good or it might be bad so what we can do is we can rewrite the whole query of the user and can correct it .<br />This might fix all the typos and can add more context to the query which can help in far better retrieval resulting in better and accurate output .</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755804672776/41900f7c-72a3-44c9-9483-eacfc7df9fbd.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-corrective-rag-crag">Corrective RAG ( CRAG )</h3>
<p>With the last approach you might thing by improving the user’s query we solved the issue’s . But as you know there’s always a better way of doing things .</p>
<p>Now what we can do is take more corrective measure to refine the chunks and get a better retrieved context which we can pass to get a better generation .</p>
<p>How we can do that ? It’s simple we will refine the query and when we retrieve the chunks we will pass it through a judge who can check the quality for the context and check if any chunk is non relevant and then with the original query , reframe the query again so that the quality of chunks produced can be increased leading to a better output and accurate generation .</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755806145344/e135f072-3042-4ad9-95bd-7430053e5cf9.png" alt="Corrective RAG ( CRAG ) " class="image--center mx-auto" /></p>
<h3 id="heading-sub-queries">Sub Queries</h3>
<p>Now the method which we are going to use to improve the queries is subqueries . What this method does this along with refining the user queries , we generate the query given by user in few different ways .</p>
<p>For example : - User query is :- How can I do console an error , then LLM may produce queries like “What is the correct way to display errors using console methods” , “How can I debug my backend” , etc ..</p>
<p>What this does is we get more chunks of retrieved data which can be relevant to the query and the user might have not known about it totally while making an query .</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755807294925/326731aa-911c-4bc0-a119-bd81eb7fae32.png" alt class="image--center mx-auto" /></p>
<p>But there are few issue in this way of retrieval :-</p>
<ul>
<li><p>A lot of chunks retrieved might cross context limit</p>
</li>
<li><p>A lot of unnecessary chunks can cause hallucinations</p>
</li>
<li><p>The main context can be missed due to a lot of unnecessary contexts</p>
</li>
</ul>
<p><strong>How to solve this issues ?</strong></p>
<p>It is pretty to tackle this , we can solve this issue by ranking , that means the chunks which is the most common and more relevant will be ranked and few top chunks will be passed as context .</p>
<p>And to find the ranking we can use Hash Map on JavaScript objects , we can hash it on the basis of meta data or anything which uniquely identifies a chunk .</p>
<h3 id="heading-hyde-hypothetical-document-expansion">HyDe ( Hypothetical Document Expansion )</h3>
<p>This method to improve the accuracy involves production of a document or answer of the query user gave via LLM and than that answer or document is used to retrieve relevant chunks , this hypothetical document is created to hit the right chunks which can give more accurate answer’s .</p>
<p>This method can be used on queries where user lacks specificity or lacks easily identifiable elements , as LLM will give specificity and more context to query with it’s created document or answer .</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755808757457/f9b99c90-e972-47e5-86ce-c187e848f9c8.png" alt class="image--center mx-auto" /></p>
<p>But it has a very major issue , that is it is not consistent . LLM are not trained on things like personal data so the document and answer provided can be pretty inaccurate causing inconsistency.</p>
<p>It can be used in RAG’s which are based on factual data or data for which LLM’s are trained for .</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>RAG’s are used for getting LLM’s brain used on our personal data or context . Optimizing to max accuracy helps RAG to scale up and grow , As no one wants faulty generation and everyone wants an accurate answer so this article covers different pipelines and methods to increase , improve and scale the accuracy of RAG’s .</p>
]]></content:encoded></item></channel></rss>