<?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" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by clara on Medium]]></title>
        <description><![CDATA[Stories by clara on Medium]]></description>
        <link>https://medium.com/@clarachong13?source=rss-c49929a81b40------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*MCNQhAnU9ozuJZNFH3f7UQ.png</url>
            <title>Stories by clara on Medium</title>
            <link>https://medium.com/@clarachong13?source=rss-c49929a81b40------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 05 Nov 2026 14:45:42 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@clarachong13/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Stop using LLMs like giant problem solvers]]></title>
            <link>https://levelup.gitconnected.com/stop-using-llms-like-giant-problem-solvers-9f59709fd1e8?source=rss-c49929a81b40------2</link>
            <guid isPermaLink="false">https://medium.com/p/9f59709fd1e8</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[ai-engineering]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[clara]]></dc:creator>
            <pubDate>Wed, 05 Aug 2026 19:06:38 GMT</pubDate>
            <atom:updated>2026-08-05T19:06:38.996Z</atom:updated>
            <content:encoded><![CDATA[<h4>How I turned 100 messy pdfs into structured insights by building a deterministic loop around agents</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Q-i6c6UgeJMNr1yry5va3w.jpeg" /><figcaption>Image by <a href="https://unsplash.com/@wesleyphotography">Wesley Tingey</a> from <a href="https://unsplash.com/">Unsplash</a></figcaption></figure><p>I recently worked on a feature where I had to transform 100 messy compliance pdfs into structured JSON rules.</p><p>The brute force approach was obvious: give the agent the source text, explain the task, provide examples, and ask it to generate the rules. Since it was the lowest-hanging fruit, I tried it first.</p><p>At a glance, the output looked fine. The output JSON was valid and matched what I expected.</p><p>But as I was manually sampling the results to check for accuracy, the cracks appeared. Some rules were too broad, others were missed. Some rules failed to preserve the nuances of the original text. I tried using another agent to catch and fix the errors but with such a huge corpus, it was impossible to confidently verify the output.</p><p>That was the frustrating part. The errors were not obvious. This was way too fragile of an implementation to scale.</p><p>Though I cannot share the exact implementation details, what I can share are the architectural lessons I learnt and how I eventually implemented it. Hopefully, these insights will be useful if you’re building AI systems that need to scale, stay reliable, and deal with messy data. And if you have better ways of doing things, <a href="https://www.linkedin.com/in/clarachongjy/">do reach out to chat</a>!</p><p>Okay let’s get to it.</p><h3>The problem</h3><p>The 100 pdfs I worked with had already been parsed and chunked before they reached me. But the raw content was still messy. There were bullet points, tables, OCR artefacts, translated sections, semi structured headings, footers, headers, inconsistent formatting and document specific quirks.</p><p>I chose to use an agent because deciding what mattered required semantic judgement. The documents did not follow one consistent pattern, so relevance could not be determined through simple rules alone.</p><p>You had to understand the surrounding context. None of this was difficult when done on a small chunk of data. <strong>The challenge was performing this reliably at scale.</strong></p><p>These rules were then processed by another downstream system to be evaluated deterministically.</p><h3>What eventually worked</h3><p>After a few experiments, I realised the biggest improvement did not come from a better prompt, a new tool, an MCP server, or a more sophisticated agent harness.</p><p>It came from changing the shape of the problem.</p><blockquote><strong>Instead of trying to make the agent smarter, I made the agent’s job smaller.</strong></blockquote><p>The first change was to prepare the source data upfront. Instead of asking the agent to query a database, retrieve records, decide whether it had the right inputs, and then perform the extraction, I gave it a more controlled starting point.</p><p>In my case, that meant temporarily storing the relevant raw data locally.</p><p>This may not always be practical. But the underlying principle is to reduce the amount of retrieval uncertainty the agent has to handle. If the agent’s job is to reason over content, do not also make it responsible for figuring out whether it has found the right content.</p><p>Another option would be to prepare the query upfront.</p><p>I also used a script to strip away unnecessary metadata and fields before passing the raw content to the agent. Less irrelevant context meant fewer distractions, fewer chances for the agent to latch onto the wrong details and a cleaner reasoning task overall.</p><blockquote><strong>But the most important change was the unit of work. </strong><br>Instead of processing everything at once, I did things iteratively and processed one document at a time.</blockquote><p>That made each job smaller, easier to inspect, easier to retry, and easier to audit. I spun up five subagents to process documents in parallel, with each agent logging its progress to a file.</p><p>If one document failed, I could retry only that document. If one output had formatting issues, I could fix that specific case without rerunning the whole batch. If the pipeline stopped halfway, the cached progress meant it could resume from the last successful checkpoint.</p><p>This was also where the separation of responsibilities became clearer.</p><p>The agent handled the semantic work: understanding the content, identifying the relevant parts and writing the JSON output.</p><p>The surrounding code handled the mechanical parts: parallelising jobs, enforcing the schema, generating IDs, writing files, caching progress, validating references, and checking whether the output could be traced back to the original source.</p><p>I also had an orchestrator watch over the progress of the script.</p><h3>Making the output auditable</h3><p>A useful design decision was adding reference IDs to every generated rule. This meant that each output item pointed back to a specific source.</p><p>This made the output easier to audit. Instead of asking, “Does this generated rule look right?”, I could ask more precise questions such as: does the referenced source chunk exist? Is the quoted source text actually present in that chunk?</p><p>I could also get another agent to selectively run audits on larger and more complex documents to ensure that important nuances were preserved.</p><p>On top of that, I did a lightweight version of evals. I ran a small batch of raw documents through the workflow and manually reviewed the results for coverage and accuracy. A full golden dataset was not practical for the scope of this task, but I still needed a way to prove to myself that the workflow was working.</p><p>My goal was not to build a perfect benchmark but to make the system auditable enough that I could inspect the outputs, catch failures, and iterate toward a higher accuracy bar.</p><p>If you’ve got ideas on how I could have done this better, let me know!</p><h3>My biggest takeaway</h3><p>The pattern that worked was to stop treating the LLM as the whole system.</p><blockquote>The system became more reliable not because the agent became perfect, but because the workflow made its outputs easier to trace, validate, and recover from.</blockquote><p>Coincidentally, I was building this shortly before attending the inaugural AI Engineer Singapore conference, held from 15–17 May 2026.</p><p>On the last day, JJ Geewax, Director of Applied AI at Google DeepMind, shared a framing that captured what I had been learning the hard way: <strong>we need to stop using LLMs like giant problem solvers.</strong></p><p>That resonated with me because it is such an easy trap to fall into. It is easy to just give the model the data, schema, business rules, edge cases, and the responsibility to verify itself. Then get frustrated when the result is inconsistent.</p><p>But for reliable production systems, the better pattern is usually a hybrid. Let the agent handle the parts that require semantic judgement, and let code handle the parts that require structure, validation, and control.</p><p>I’ll be sharing more reflections from AI Engineer Singapore and the workshops I attended. The YouTube snippet of JJ’s speech here:</p><p><a href="https://www.youtube.com/live/m12vGjfbNlo?si=RStWm3hd_PYWAJnm&amp;t=5563">- YouTube</a></p><p>That’s all from me. I hope this helped, and see you in the next article :)</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9f59709fd1e8" width="1" height="1" alt=""><hr><p><a href="https://levelup.gitconnected.com/stop-using-llms-like-giant-problem-solvers-9f59709fd1e8">Stop using LLMs like giant problem solvers</a> was originally published in <a href="https://levelup.gitconnected.com">Level Up Coding</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Deep dive into context engineering for AI agents]]></title>
            <link>https://code.likeagirl.io/deep-dive-into-context-engineering-for-ai-agents-584bf3e578df?source=rss-c49929a81b40------2</link>
            <guid isPermaLink="false">https://medium.com/p/584bf3e578df</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[ai-engineering]]></category>
            <category><![CDATA[agents]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[clara]]></dc:creator>
            <pubDate>Fri, 15 May 2026 17:14:09 GMT</pubDate>
            <atom:updated>2026-05-15T17:14:09.978Z</atom:updated>
            <content:encoded><![CDATA[<h4>How to optimize context: a precious finite resource for AI agents</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mg-K5droCFY7AjoXhMV-yQ.jpeg" /><figcaption>Image by <a href="https://unsplash.com/@hansjurgen007">Hans-Jurgen Mager</a> from Unsplash</figcaption></figure><p>We talk a lot about better models, larger context windows, and more capable agents. But most real-world failures don’t come from model capability — they come from how context is constructed, passed, and maintained.</p><p>This is a hard problem. The space is moving fast and techniques are still evolving. Much of it remains an experimental science and depends on the context (pun intended), constraints and environment you’re operating in.</p><p>In my work building multi-agent systems, a recurring pattern has emerged: performance is far less about how much context you give a model, and far more about how <strong><em>precisely</em></strong> you shape it.</p><p>This piece is an attempt to distill my learnings into something you can use.</p><p><strong>It focuses on principles for managing context as a constrained resource — deciding what to include, what to exclude, and how to structure information so that agents remain coherent, efficient, and reliable over time.</strong></p><blockquote>Because at the end of the day, the strongest agents are not the ones that see the most. They are the ones that see the right things, in the right form, at the right time.</blockquote><h3>Terminology</h3><h4>Context engineering</h4><p>Context engineering is the art of providing the right information, tools and format to an LLM for it to complete a task. Good context engineering means finding the smallest possible set of high signal tokens that give the LLM the highest probability of producing a good outcome.</p><p>In practice, good context engineering usually comes down to four moves. You <strong>offload</strong> information to external systems (<strong>context offloading</strong>) so the model does not need to carry everything in-band. You <strong>retrieve</strong> information dynamically instead of front-loading all of it (<strong>context retrieval</strong>). You <strong>isolate</strong> context so one subtask does not contaminate another (<strong>context isolation</strong>). And you <strong>reduce</strong> history when needed, but only in ways that preserve what the agent will still need later (<strong>context reduction</strong>).</p><p>A common failure mode on the other side is <strong>context pollution</strong>: the presence of too much unnecessary, conflicting or redundant information that it distracts the LLM.</p><h4>Context rot</h4><p>Context rot is a situation where an LLM’s performance degrades as the context window fills up, even if it is within the established limit. The LLM still has room to read more, but its reasoning starts to blur.</p><blockquote>You would have noticed that the effective context window, where the model performs at high quality, is often much smaller than what the model technically is capable of.</blockquote><p>There are two parts to this. First, a model does not maintain perfect recall across it’s entire context window. Information at the start and the end is more reliably recalled than things in the middle.</p><p>Second, larger context windows do not solve problems for enterprise systems. Enterprise data is effectively unbounded and frequently updated that even if the model could ingest everything, that would not mean it could maintain a coherent understanding over it.</p><p>Just like humans have a limited working memory capacity, every new token introduced to the LLM depletes this attention budget it has by some amount. The attention scarcity stems from architectural constraints in the transformer, where every token attends to every other token. This leads to a n² interaction pattern for n tokens. As the context grows, the model is forced to spread its attention thinner across more relationships.</p><h4>Context compaction</h4><p>Context compaction is the general answer to context rot.</p><p>When the model is nearing the limit of it’s context window, it summarises it’s contents and reinitiates a new context window with the previous summary. This is especially useful for long running tasks to allow the model to continue to work without too much performance degradation.</p><p><a href="https://openreview.net/forum?id=JaLXQnA2wi">Recent work on context folding</a> offers a different approach — agents actively manage their working context. An agent can branch off to handle a subtask and then fold it upon completion, collapsing the intermediate steps while retaining a concise summary of the outcome.</p><p>The difficulty, however, is not in summarising, but in deciding what survives. Some things should remain stable and nearly immutable, such as the objective of the task and hard constraints. Others can be safely discarded. The challenge is that the importance of information is often only revealed later.</p><p>Good compaction therefore needs to preserve facts that continue to constrain future actions: which approaches already failed, which files were created, which assumptions were invalidated, which handles can be revisited, and which uncertainties remain unresolved. Otherwise you get a neat, concise summary that reads well to a human and is useless to an agent.</p><h3>Agent harness</h3><p>A model is not an agent. The harness is what turns a model into one.</p><p>By harness, I mean everything around the model that decides how context is assembled and maintained: prompt serialization, tool routing, retry policies, the rules governing what is preserved between steps, and so on.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*iZen924NNBcWlvL8ps7D8Q.png" /><figcaption>Drawn by author</figcaption></figure><p>Once you look at real agent systems this way, a lot of supposed “model failures” now look different. I’ve encountered many of such at work. These are actually harness failures: the agent forgot because nothing persisted the right state; it repeated work because the harness surfaced no durable artefact of prior failure; it chose the wrong tool because the harness overloaded the action space; and so on.</p><p>A good harness is, in some sense, a deterministic shell wrapped around a stochastic core. It makes the context legible, stable, and recoverable enough that the model can spend its limited reasoning budget on the task rather than on reconstructing its own state from a messy trace.</p><h3>Communication between agents</h3><p>As tasks get more complex, teams have defaulted towards multi-agent systems.</p><p>The mistake is to assume that more agents means more shared context. In practice, dumping a giant shared transcript into every sub-agent often creates exactly the opposite of specialisation. Now every agent is reading everything, inheriting everyone else’s mistakes, and paying the same context bill over and over again.</p><p>If only some context is shared, a new problem appears. What is considered authoritative when agents disagree? What remains local, and how are conflicts reconciled?</p><p>The way out is to treat communication not as shared memory, but as <strong>state transfer through well-defined interfaces</strong>.</p><p>For discrete tasks with clear inputs and outputs, agents should usually communicate through artefacts rather than raw traces. A web-search agent, for instance, does not need to pass along its entire browsing history. It only needs to surface the material that downstream agents can actually use.</p><p>This means that intermediate reasoning, failed attempts, and exploration traces stay private unless explicitly needed. What gets passed forward are distilled outputs: extracted facts, validated findings, or decisions that constrain the next step.</p><p>For more tightly coupled tasks, like a debugging agent where downstream reasoning genuinely depends on prior attempts, a limited form of trace sharing can be introduced. But this should be deliberate and scoped, not the default.</p><h4>KV cache penalty</h4><p>When AI models generate text, they often repeat many of the same calculations. KV caching is an inference time optimisation technique that speeds up this process by remembering important information from previous steps instead of recomputing everything again.</p><p>However, in multi-agent systems, if every agent shares the same context, you confuse the model with a ton of irrelevant details and pay a massive KV-cache penalty. Multiple agents working on the same task need to communicate with each other, but this should not be via sharing memory.</p><p>This is why agents should communicate through minimal, structured outputs in a controlled manner.</p><h3>Keep the agent’s toolset small and relevant</h3><p>Tool choice is a context problem disguised as a capability problem.</p><p>As an agent accumulates more tools, the action space gets harder to navigate. There is now a higher probability of the model going down the wrong action and taking an inefficient route.</p><p>This has consequences. Tool schemas need to be far more distinct than most people realise. Tools have to be well understood and have minimal overlap in functionality. It should be very clear on what their intended use is and have clear input parameters that are unambiguous.</p><p>One common failure mode that I noticed even in my team is that we tend to have very bloated sets of tools that are added over time. This leads to unclear decision making on which tools to use.</p><h3>Agentic memory</h3><p>This is a a technique where the agent regularly writes notes persisted to memory outside of the context window. These notes get pulled back into the context window at later times.</p><p>The hardest part is deciding what deserves promotion into memory. My rule of thumb is that durable memory should contain things that continue to constrain future reasoning: persistent preferences. Everything else should have a very high bar. Storing too much is just another route back to context pollution, only now you have made it persistent.</p><p>But memory without revision is a trap. Once agents persist notes across steps or sessions, they also need mechanisms for conflict resolution, deletion, and demotion. Otherwise long-term memory becomes a landfill of outdated beliefs.</p><h3>To sum up</h3><p>Context engineering is still evolving, and there is no single correct way to do it. Much of it remains empirical, shaped by the systems we build and the constraints we operate under.</p><p>Left unchecked, context grows, drifts, and eventually collapses under its own weight.</p><p>If well-managed, context becomes the difference between an agent that merely responds and one that can reason, adapt, and stay coherent across long and complex tasks.</p><p>Thank you for staying with me till the end, and I hope this was useful for you. Subscribe to my content to follow along my journey.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=584bf3e578df" width="1" height="1" alt=""><hr><p><a href="https://code.likeagirl.io/deep-dive-into-context-engineering-for-ai-agents-584bf3e578df">Deep dive into context engineering for AI agents</a> was originally published in <a href="https://code.likeagirl.io">Code Like A Girl</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Lessons Learnt From Upgrading to LangChain 1.0 in Production]]></title>
            <link>https://medium.com/womenintechnology/lessons-learnt-from-upgrading-to-langchain-1-0-in-production-43b22e2e8cdb?source=rss-c49929a81b40------2</link>
            <guid isPermaLink="false">https://medium.com/p/43b22e2e8cdb</guid>
            <category><![CDATA[women-in-tech]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[agents]]></category>
            <category><![CDATA[langchain]]></category>
            <category><![CDATA[technology]]></category>
            <dc:creator><![CDATA[clara]]></dc:creator>
            <pubDate>Wed, 04 Mar 2026 10:41:00 GMT</pubDate>
            <atom:updated>2026-03-04T10:41:00.917Z</atom:updated>
            <content:encoded><![CDATA[<h4>What worked, what broke, and why I did it</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KkwHH4dZNv82BJLGSdHuaA.jpeg" /><figcaption>Image by <a href="https://unsplash.com/@tumbao1949">James</a> from <a href="https://unsplash.com/">Unsplash</a></figcaption></figure><p>LangChain shipped its first stable v1.0 release in late October 2025. After spending the past two months working with their new APIs, I genuinely feel this is the most coherent and thoughtfully designed version of LangChain to date.</p><p>I wasn’t always a LangChain fan. The early versions were fragile, poorly documented, abstractions shifted frequently, and it felt too premature to use in prod. But v1.0 felt more intentional, and had a more consistent mental model for how data should flow through agents and tools.</p><p>This <strong>isn’t</strong> a sponsored post btw — I’d love to hear your thoughts, so leave them in the comments or DM me <a href="https://www.linkedin.com/in/clarachongjy/">here</a>!</p><p>This article isn’t here to regurgitate the docs. I’m assuming you’ve already dabbled with LangChain (or are a heavy user). Rather than dumping a laundry list of points, I’m going to cherry-pick just four key points.</p><h4>A quick recap: LangChain, LangGraph &amp; LangSmith</h4><p>At a high level, LangChain is a framework for building LLM apps and agents, allowing devs to ship AI features fast with common abstractions.</p><p>LangGraph is the graph based execution engine for durable, stateful agent workflows in a controllable way. Finally, LangSmith is an observability platform for tracing and monitoring.</p><blockquote>Put simply: LangChain helps you build agents fast, LangGraph runs them reliably, and LangSmith lets you monitor and improve them in production.</blockquote><h4>My stack</h4><p>For context, most of my recent work focuses on building multi-agent features for a customer-facing AI platform at work. My backend stack is FastAPI, with Pydantic powering schema validation and data contracts.</p><h3>Lesson 1: Dropping support for Pydantic models</h3><p>A major shift in the migration to v1.0 was the introduction of the new create_agent method. It streamlines how agents are defined and invoked, but it also drops support for Pydantic models and dataclasses in agent state. Everything must now be expressed as TypedDicts extending AgentState.</p><p>If you’re using FastAPI, Pydantic is often the recommended and default schema validator. I valued schema consistency across the codebase and felt that mixing TypedDicts and Pydantic models would inevitably create confusion — especially for new engineers who might not know which schema format to follow.</p><p>To solve this, I introduced a small helper that converts a Pydantic model into a TypedDict that extends AgentState right before it’s passed to create_agent . It is critical to note that LangChain attaches custom metadata to type annotations which you must preserve. Python utilities like get_type_hints() strip these annotations, meaning a naïve conversion won’t work.</p><h3>Lesson 2: Deep agents, default middleware &amp; the lack of control</h3><p>Alongside the new create_agent API in LangChain 1.0 came something that caught my attention: the <a href="https://docs.langchain.com/oss/python/deepagents/overview">deepagentslibrary</a>. Inspired by tools like Claude Code and Manus, deep agents can plan, break tasks into steps, and even spawn subagents.</p><p>When I first saw this, I wanted to use it <em>everywhere</em>. Why wouldn’t you want “smarter” agents, right? But after trying it across several workflows, I realised that this extra autonomy was sometimes unnecessary — and in certain cases, counterproductive — for my use cases.</p><p>Each deep agent comes with some built-in middleware — things like ToDoListMiddleware, FilesystemMiddleware, SummarizationMiddleware, etc. These shape how the agent thinks, plans, and manages context. The issue is that, at the time of writing, you <strong>can’t control</strong> when these middleware run, nor can you disable the ones you don’t need.</p><p>After digging into the deepagents source code <a href="https://github.com/langchain-ai/deepagents/blob/9cdb42f821dd96483a94c16a27bdec0f128b2ca0/libs/deepagents/deepagents/graph.py#L40">here</a>, you can see that the middleware parameter is <strong>additional</strong> middleware to apply <strong>after standard middleware<em>. </em></strong>Any middleware that was passed in middleware=[...] gets appended after the defaults.</p><p>All this extra orchestration also introduced noticeable latency, without providing meaningful benefits for my specific workloads.</p><blockquote>I’m not saying deep agents are bad, they’re powerful in the right scenarios. However, this is a good reminder of a classic engineering principle: don’t chase the “shiny” thing. Use the tech that solves your actual problem, even if it’s the “less glamorous” option.</blockquote><h3>My favourite feature: Structured output</h3><p>Having deployed agents in production, especially ones that integrate with deterministic enterprise systems, getting agents to consistently produce output of a specific schema was crucial.</p><p>LangChain 1.0 makes this pretty easy. You can define a schema (e.g., a Pydantic model) and pass it to create_agent via the response_format parameter. The agent then produces output that conforms to that schema <strong>within a single agent loop </strong>with no additional steps.</p><p>This has been incredibly useful whenever I need the agent to strictly adhere to a JSON structure with certain fields guaranteed. So far, structured output has been very reliable too.</p><h3>What I want to explore more of: Middleware</h3><p>One of the trickiest parts of building reliable agents is <strong>context engineering</strong> — making sure the agent always has the right information at the right time. Middleware was introduced to give developers precise control over each step of the agent loop, and I think it is worth diving deeper into.</p><p>Middleware can mean different things depending on context (pun intended). In LangGraph, this can mean controlling the exact sequence of node execution. In long-running conversations, it might involve summarising accumulated context before the next LLM call. In human-in-the-loop scenarios, middleware can pause execution and wait for a user to approve or reject a tool call.</p><p>More recently, in the latest v1.1 minor release, LangChain also added a model retry middleware with configurable exponential backoff, allowing graceful recovery for transient endpoint errors.</p><p>I personally think middleware is a game changer as agentic workflows get more complex, long-running, and stateful, especially when you need fine-grained control or robust error handling.</p><p>This list of middleware is <a href="https://langchain-5e9cc07a.mintlify.app/oss/javascript/langchain/middleware/built-in">growing</a> and it really helps that it remains provider agnostic. If you’ve experimented with middleware in your own work, I’d love to hear what you found most useful!</p><h3>To end off</h3><p>That’s it for now — four key reflections from what I’ve learnt so far about LangChain. And if anyone from the LangChain team happens to be reading this, I’m always happy to share user feedback anytime or simply chat :)</p><p>Have fun building!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=43b22e2e8cdb" width="1" height="1" alt=""><hr><p><a href="https://medium.com/womenintechnology/lessons-learnt-from-upgrading-to-langchain-1-0-in-production-43b22e2e8cdb">Lessons Learnt From Upgrading to LangChain 1.0 in Production</a> was originally published in <a href="https://medium.com/womenintechnology">Women in Technology</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AI Engineering and Evals as New Layers of Software Work]]></title>
            <link>https://code.likeagirl.io/ai-engineering-and-evals-as-new-layers-of-software-work-dd8b7952e227?source=rss-c49929a81b40------2</link>
            <guid isPermaLink="false">https://medium.com/p/dd8b7952e227</guid>
            <category><![CDATA[ai-engineering]]></category>
            <category><![CDATA[llm-evaluation]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[clara]]></dc:creator>
            <pubDate>Tue, 06 Jan 2026 14:48:09 GMT</pubDate>
            <atom:updated>2026-01-06T14:48:09.576Z</atom:updated>
            <content:encoded><![CDATA[<h4>How to maintain reliability in inherently stochastic systems</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Rn3zSMdDcMkIFwrDrDaxrQ.jpeg" /><figcaption>Image by <a href="https://unsplash.com/@rebepascual7">Rebe Pascual</a> from <a href="https://unsplash.com/">Unsplash</a></figcaption></figure><p>My job doesn’t look quite the same as before. As a software engineer in the AI space, my work has been a hybrid of software engineering, AI engineering, product intuition, and doses of user empathy.</p><p>With so much going on, I wanted to take a step back and reflect on the bigger picture and the kind of skills and mental models engineers need to stay ahead. A recent read of <em>O’Reilly’s AI Engineering</em> gave me the nudge to also want to deep dive into how to think about evals — a core component in any AI system.</p><blockquote>One thing stood out: AI engineering is often more software than AI.</blockquote><p>Outside of research labs like OpenAI or Anthropic, most of us aren’t training models from scratch. The real work is about solving business problems with the tools we already have — giving models enough relevant context, using APIs, building RAG pipelines, tool-calling — all on top of the usual SWE concerns like deployment, monitoring and scaling.</p><p>In other words, AI engineering isn’t replacing software engineering — it’s layering new complexity on top of it.</p><p>This piece is me teasing out some of those themes. If any of them resonates, I’d love to hear your thoughts — feel free to reach out <a href="https://www.linkedin.com/in/clarachongjy/">here</a>!</p><h3>The three layers of an AI application stack</h3><p>Think of an AI app as being built on three layers: 1) Application development 2) Model development 3) Infrastructure.</p><p>Most teams start from the top. With powerful models readily available off the shelf, it often makes sense to begin by focusing on building the product and only later dip into model development or infrastructure as needed.</p><p>As O’Reilly puts it, <em>“AI engineering is just software engineering with AI models thrown into the stack.”</em></p><h3>Why evals matter and why they’re tough</h3><p>In software, one of the biggest headaches for fast-moving teams is <strong>regressions</strong>. You ship a new feature, and in the process unknowingly break something else. Weeks later, a bug surfaces in a dusty corner of the codebase, and tracing it back becomes a nightmare.</p><p>Having a comprehensive test suite helps catch these regressions.</p><p>AI development faces a similar problem. Every change — whether it’s prompt tweaks, RAG pipeline updates, fine-tuning, or context engineering — can improve performance in one area while quietly degrading another.</p><blockquote>In many ways, evaluations are to AI what tests are to software: they catch regressions early and give engineers the confidence to move fast without breaking things.</blockquote><p>But evaluating AI isn’t straightforward. Firstly, the more intelligent models become, the harder evaluation gets. It’s easy to tell if a book summary is bad if it’s gibberish, but much harder if the summary is actually coherent. o know whether it’s actually capturing the key points, not just sounding fluent or factually correct, you might have to read the book yourself.</p><p>Secondly, tasks are often open-ended. There’s rarely a single “right” answer and impossible to curate a comprehensive list of correct outputs.</p><p>Thirdly, foundation models are treated as black boxes, where details of model architecture, training data and training process are often scrutinised or even made public. These details reveal alot about a model’s strengths and weaknesses and without it, people only evaluate models based by observing it’s outputs.</p><h3>How to think about evals</h3><p>I like to group evals into two broad realms: quantitative and qualitative.</p><p>Quantitative evals have clear, unambiguous answers. Did the math problem get solved correctly? Did the code execute without errors? These can often be tested automatically, which makes them scalable.</p><p>Qualitative evals, on the other hand, live in the grey areas. They’re about interpretation and judgment — like grading an essay, assessing the tone of a chatbot, or deciding whether a summary “sounds right.”</p><p>Most evals are a mix of both. For example, evaluating a generated website means not only testing whether it performs its intended functions (quantitative: can a user sign up, log in, etc.), but also judging whether the user experience feels intuitive (qualitative).</p><h4>Functional correctness</h4><p>At the heart of quantitative evals is <strong>functional correctness</strong>: does the model’s output actually do what it’s supposed to do?</p><p>If you ask a model to generate a website, the core question is whether the site meets its requirements. Can a user complete key actions? Does it work reliably? This looks a lot like traditional software testing, where you run a product against a suite of test cases to verify behaviour. Often, this can be automated.</p><h4>Similarity against reference data</h4><p>Not all tasks have such clear, testable outputs. Translation is a good example: there’s no single “correct” English translation for a French sentence, but you can compare outputs against <strong>reference data</strong>.</p><p>The downside: This relies heavily on the availability of reference datasets, which are expensive and time-consuming to create. Human-generated data is considered the gold standard, but increasingly, reference data is being bootstrapped by other AIs.</p><p>There are a few ways to measure similarity:</p><ul><li>Human judgement</li><li>Exact match: whether the generated response matches one of the reference responses exactly. These produces boolean results.</li><li>Lexical similarity: measuring how similar the outputs look (e.g., overlap in words or phrases).</li><li>Semantic similarity: measuring whether the outputs mean the same thing, even if the wording is different. This usually involves turning data into embeddings (numerical vectors) and comparing them. Embeddings aren’t just for text — platforms like Pinterest use them for images, queries, and even user profiles.</li></ul><blockquote>Lexical similarity only checks surface-level resemblance, while semantic similarity digs deeper into meaning.</blockquote><h4>AI as a judge</h4><p>Some tasks are nearly impossible to evaluate cleanly with rules or reference data. Assessing the tone of a chatbot, judging the coherence of a summary, or critiquing the persuasiveness of ad copy all fall into this category. Humans can do it, but human evals don’t scale.</p><p>Here’s how to structure the process:</p><ol><li>Define a structured and measurable evaluation criteria. Be explicit about what you care about — clarity, helpfulness, factual accuracy, tone, etc. Criteria can use a scale (1–5 rating) or binary checks (pass/fail).</li><li>The original input, the generated output, and any supporting context are given to the AI judge. A score, label or even an explanation for evaluation is then returned by the judge.</li><li>Aggregate over many outputs. By running this process across large datasets, you can uncover patterns — for example, noticing that helpfulness dropped 10% after a model update.</li></ol><p>Because this can be automated, it enables <strong>continuous evaluation</strong>, borrowing from CI/CD practices in software engineering. Evals can be run before and after pipeline changes (from prompt tweaks to model upgrades), or used for ongoing monitoring to catch drift and regressions.</p><p>Of course, AI judges aren’t perfect. Just as you wouldn’t fully trust a single person’s opinion, you shouldn’t fully trust a model’s either. But with careful design, multiple judge models, or running them over many outputs, they can provide <strong><em>scalable approximations of human judgment.</em></strong></p><h3>Eval driven development</h3><p>O’Reilly talked about the concept of <strong>eval-driven development</strong>, inspired by test-driven development in software engineering, something I felt is worth sharing.</p><blockquote><strong>The idea is simple: Define your evals before you build.<br>In AI engineering, this means deciding what “success” looks like and how it’ll be measured.</strong></blockquote><p>Impact still matters most — not hype. The right evals ensure that AI apps demonstrate value in ways that are relevant to users and the business.</p><p>When defining evals, here are some key considerations:</p><h4>Domain knowledge</h4><p>Public benchmarks exist across many domains — code debugging, legal knowledge, tool use — but they’re often generic. The most meaningful evals usually come from sitting down with stakeholders and defining what truly matters for the business, then translating that into <strong>measurable</strong> outcomes.</p><p>Correctness isn’t enough if the solution is impractical. For example, a text-to-SQL model might generate a correct query, but if it takes 10 minutes to run or consumes huge resources, it’s not useful at scale. Runtime and memory usage are important metrics too.</p><h4>Generation capability</h4><p>For generative tasks — whether text, image, or audio — evals may include fluency, coherence, and task-specific metrics like relevance.</p><p>A summary might be factually accurate but miss the most important points — an eval should capture that. Increasingly, these qualities can themselves be scored by another AI.</p><h4>Factual consistency</h4><p>Outputs need to be checked against a source of truth. This can happen in two ways:</p><ol><li><strong>Local consistency</strong> <br>This means verifying outputs against a provided context. This is especially useful for specific domains that are unique to themselves and have limited scope. For instance, extracted insights should be consistent with the data.</li><li><strong>Global consistency</strong><br>This means verifying outputs against open knowledge sources such as by fact checking via a web search or a market research and so on.</li><li><strong>Self verification</strong><br>This happens when a model generates multiple outputs, and measures how consistent these responses are with each other.</li></ol><h4>Safety</h4><p>Beyond the usual concept of safety such as to not include profanity and explicit content, there are actually many ways in which safety can be defined. For instance, chatbots should not reveal sensitive customer data and should be able to guard against prompt injection attacks.</p><h3>To sum up</h3><p>As AI capabilities grow, robust evals will only become more important. They’re the guardrails that let engineers move quickly without sacrificing reliability.</p><p>I’ve seen how challenging reliability can be and how costly regressions are. They damage a company’s reputation, frustrate users, and create painful dev experiences, with engineers stuck chasing the same bugs over and over.</p><p>As the boundaries between engineering roles blur, especially in smaller teams, we’re facing a fundamental shift in how we think about software quality. The need to maintain and measure reliability now extends beyond rule-based systems to those that are inherently probabilistic and stochastic.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=dd8b7952e227" width="1" height="1" alt=""><hr><p><a href="https://code.likeagirl.io/ai-engineering-and-evals-as-new-layers-of-software-work-dd8b7952e227">AI Engineering and Evals as New Layers of Software Work</a> was originally published in <a href="https://code.likeagirl.io">Code Like A Girl</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Deep dive into RabbitMQ & Python’s Celery: How to optimise your queues?]]></title>
            <link>https://levelup.gitconnected.com/deep-dive-into-rabbitmq-pythons-celery-how-to-optimise-your-queues-02042cb4fd24?source=rss-c49929a81b40------2</link>
            <guid isPermaLink="false">https://medium.com/p/02042cb4fd24</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[task-queues]]></category>
            <category><![CDATA[celery]]></category>
            <category><![CDATA[rabbitmq]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[clara]]></dc:creator>
            <pubDate>Fri, 31 Oct 2025 01:49:14 GMT</pubDate>
            <atom:updated>2025-10-31T01:49:14.330Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*e2NWjzSOBYVp6EqjStS3vg.jpeg" /><figcaption>Image by <a href="https://unsplash.com/@pablomp">Pablo Martinez</a> from <a href="https://unsplash.com/">Unsplash</a></figcaption></figure><p>If you’re an engineer, have worked with machine learning or large-scale data pipelines, chances are you’ve used some sort of queueing system.</p><p>Queues let services talk to each other asynchronously: you send off work, don’t wait around, and let another system pick it up when ready. This is essential when your tasks aren’t instant — think long-running model training jobs, batch ETL pipelines, or even processing requests for LLMs that take minutes per query.</p><p>So why am I writing this? I recently migrated a production queueing setup to RabbitMQ, ran into a bunch of bugs, and found that documentation was thin on the trickier parts. After a fair bit of trial and error, I thought it’d be worth sharing what I learned.</p><p>Hope you will find this useful!</p><h3>A quick primer: queues vs request-response model</h3><p>Microservices typically communicate in two styles — the classic request–response model, or the more flexible queue-based model.</p><p>Imagine ordering pizza. In a <strong>request–response model</strong>, you tell the waiter your order and then wait. He disappears, and thirty minutes later your pizza shows up — but you’ve been left in the dark the whole time.</p><p>In a <strong>queue-based model</strong>, the waiter repeats your order, gives you a number, and drops it into the kitchen’s queue. Now you know it’s being handled, and you’re free to do something else till the chef gets to it.</p><p>That’s the difference: request–response keeps you blocked until the work is done, while queues confirm right away and let the work happen in the background.</p><h3>What is Rabbit MQ?</h3><p>RabbitMQ is a popular open-source message broker that ensures messages are reliably delivered from producers (senders) to consumers (receivers). First released in 2007 and written in Erlang, it implements AMQP (Advanced Message Queuing Protocol), an open standard for structuring, routing, and acknowledging messages.</p><blockquote>Think of it like a post office for distributed systems: applications drop off messages, RabbitMQ sorts them into queues, and consumers pick them up when ready.</blockquote><p>A common pairing in the Python world is Celery + RabbitMQ: RabbitMQ brokers the tasks, while Celery workers execute them in the background.</p><p>In containerised setups, RabbitMQ typically runs in its own container, while Celery workers run in separate containers that you can scale independently.</p><h3>How it works at a high level</h3><p>Your app wants to run some work asynchronously. Since this task might take a while, you don’t want the app to sit idle waiting. Instead, it creates a message describing the task and sends it to RabbitMQ.</p><ol><li><strong>Exchange</strong>: This lives inside RabbitMQ. It doesn’t store messages but just decides where each message should go based on rules you set (routing keys and bindings).<br>Producers publish messages to an exchange, which acts as a routing intermediary.</li><li><strong>Queues</strong>: They’re like mailboxes. Once the exchange decides which queue(s) a message should go to, it sits there till it’s picked up.</li><li><strong>Consumer</strong>: The service that reads and processes messages from a queue. In a Celery setup, the Celery worker is the consumer — it pulls tasks off the queue and does the actual work.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2Hy7Hegbtc9g4P7uN9skWg.png" /><figcaption>High level overview of Rabbit MQ’s architecture. Drawn by writer.</figcaption></figure><p>Once the message is routed into a queue, the RabbitMQ broker <strong>pushes</strong> it out to a consumer (if one is available) over a TCP connection.</p><h3>Core components in Rabbit MQ</h3><h4>1. Routing and binding keys</h4><p>Routing and binding keys work together to decide where a message ends up.</p><ul><li>A <strong>routing</strong> key is attached to a <strong>message</strong> by the producer.</li><li>A <strong>binding</strong> key is the rule a <strong>queue</strong> declares when it connects (binds) to an exchange.<br>A binding defines the link between an exchange and a queue.</li></ul><p>When a message is sent, the exchange looks at the message’s routing key. If that routing key matches the binding key of a queue, the message is delivered to that queue.</p><blockquote>A message can only have <strong>one</strong> routing key.<br>A queue can have<strong> one or multiple </strong>binding keys, meaning it can listen for several different routing keys or patterns.</blockquote><h4>2. Exchanges</h4><p>An exchange in RabbitMQ is like a <strong>traffic controller. </strong>It receives messages, does not store messages, and it’s key job is to decide which queue(s) the message should go to, based on rules.</p><p><em>If the routing key of a message does not match any the binding keys of any queues, it will not get delivered.</em></p><p>There are several types of exchanges, each with its own routing style.</p><h4>2a) Direct exchange</h4><p>Think of a direct exchange like an exact address delivery. The exchange looks for queues with binding keys that exactly match the routing key.</p><ul><li>If only one queue matches, the message will only be sent there (1:1).</li><li>If multiple queues have the same binding key, the message will be copied to all of them (1:many).</li></ul><h4>2b) Fanout exchange</h4><p>A fanout exchange is like shouting through a loudspeaker.</p><p>Every message is copied to<strong> all </strong>queues bound to the exchange. The routing keys are ignored, and it is <strong>always a 1:many broadcast</strong>.</p><p>Fanout exchanges can be useful when the same message needs to be sent to one or more queues with consumers who may process the same message in different ways.</p><h4>2c) Topic exchange</h4><p>A topic exchange works like a subscription system with categories.</p><p>Every message has a routing key, for example &quot;order.completed”. Queues can then subscribe to patterns such as &quot;order.*”. This means that whenever a message is related to an order, it will be delivered to any queues that have subscribed to that category.</p><p>Depending on the patterns, a message might end up in just one queue or in several at the same time.</p><p>There are two important special cases for binding keys:</p><ul><li>* (star) matches <strong>exactly one word</strong> in the routing key.</li><li># (hash) matches <strong>zero or more words</strong>.</li></ul><p>Let’s illustrate this to make the syntax alot more intuitive.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OuErWS-9a_9nLWf6Vt9cEQ.png" /></figure><h4>2d) Headers exchange</h4><p>A headers exchange is like sorting mail by labels instead of addresses.</p><p>Instead of looking at the routing key (like &quot;order.completed&quot;), the exchange inspects the <strong>headers</strong> of a message: These are key–value pairs attached as metadata. For instance:</p><ul><li>x-match: all, priority: high, type: email → the queue will only get messages that have <strong>both</strong> priority=high <strong>and</strong> type=email.</li><li>x-match: any, region: us, region: eu → the queue will get messages where <strong>at least one</strong> of the conditions is true (region=us or region=eu).</li></ul><p>The x-match field is what determines whether all rules must match or any one rule is enough.</p><p>Because multiple queues can each declare their own header rules, a single message might end up in just one queue (1:1) or in several queues at once (1:many).</p><p>Headers exchanges are less common in practice, but they’re useful when routing depends on more complex business logic. For example, you might want to deliver a message only if customer_tier=premium, message_format=json, or region=apac .</p><h4>2e) Dead letter exchange</h4><p>A dead letter exchange is a safety net for undeliverable messages.</p><h4>3. A push delivery model</h4><p>This means that as soon as a message enters a queue, the broker will push it out to a consumer that is subscribed and ready. The <strong>consumer doesn’t request messages</strong> and instead just listens on the queue.</p><blockquote>This push approach is great for low-latency delivery — messages get to consumers as soon as possible.</blockquote><h3>Useful features in Rabbit MQ</h3><p>Rabbit MQ’s architecture lets you shape message flow to fit your workload. Here are some useful patterns.</p><h4>Work queues — competing consumers pattern</h4><p>You publish tasks into <strong>one queue</strong>, and <strong>many consumers</strong> (eg. celery workers) all listen to that queue. The broker delivers <strong>each message to exactly one consumer</strong>, so workers “compete” for work. This implicitly translates to simple load-balancing.</p><p>If you’re on celery, you’ll want to keep worker_prefetch_multiplier=1 . What this means is that a worker will only fetch one message at a time, avoiding slow workers from hoarding tasks.</p><h4>Pub/sub pattern</h4><p><strong>Multiple queues</strong> bound to an exchange and each queue gets a <strong>copy</strong> of the message (fanout or topic exchanges). Since each queue gets its own message copy, so different consumers can process the same event in different ways.</p><h4>Explicit acknowledgements</h4><p>RabbitMQ uses explicit acknowledgements (<strong>ACKs</strong>) to guarantee reliable delivery. An ACK is a confirmation sent from the consumer back to the broker once a message has been successfully processed.</p><p>When a consumer sends an ACK, the broker removes that message from the queue. If the consumer <strong>NACKs</strong> or dies before ACKing, RabbitMQ can <strong>redeliver</strong> (requeue) the message or route it to a dead letter queue<strong> </strong>for inspection or retry.</p><p>There is, however, an important nuance when using Celery. Celery does send acknowledgements by default, but it sends them <strong>early</strong> — right after a worker receives the task, before it actually executes it. This behaviour (acks_late=False, which is the default) means that if a worker crashes midway through running the task, the broker has already been told the message was handled and won’t redeliver it.</p><h4>Priority queues</h4><p>RabbitMQ has a out of the box priority queueing feature which lets higher priority messages jump the line. Under the hood, the broker creates an <strong>internal sub-queue</strong> for each priority level defined on a queue.</p><p>For example, if you configure five priority levels, RabbitMQ maintains five internal sub-queues. Within each level, messages are still consumed in <strong>FIFO order</strong>, but when consumers are ready, RabbitMQ will always try to deliver messages from higher-priority sub-queues first.</p><p>Doing so implicitly would mean an increasing amount of overhead if there were many priority levels. <a href="https://www.rabbitmq.com/docs/priority">Rabbit MQ’s docs note that though priorities between 1 and 255 are supported, <strong>values between 1 and 5 are highly recommended</strong>.</a></p><h4>Message TTL &amp; scheduled deliveries</h4><p><strong>Message TTL</strong> (per-message or per-queue) automatically expires stale messages; and <strong>delayed delivery</strong> is available via plugins (e.g., delayed-message exchange) when you need scheduled execution.</p><h3>How to optimise your Rabbit MQ and Celery setup</h3><p>When you deploy Celery with RabbitMQ, you’ll notice a few “mystery” queues and exchanges appearing in the RabbitMQ management dashboard. These aren’t mistakes — they’re part of Celery’s internals. After a few painful rounds of trial and error, here’s what I learned about how Celery really uses RabbitMQ under the hood — and how to tune it properly.</p><h4>Kombu</h4><p>Celery relies on Kombu, a Python messaging framework. Kombu abstracts away the low-level AMQP operations, giving Celery a high-level API to:</p><ul><li>Declare queues and exchanges</li><li>Publish messages (tasks)</li><li>Consume messages in workers</li></ul><p>It also handles serialisation (JSON, Pickle, YAML, or custom formats) so tasks can be encoded and decoded across the wire.</p><h4>Celery events and the celeryev Exchange</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/694/1*AM6wOpfD0YUvclnp45UN0A.png" /><figcaption>Screenshot by writer on how a celeryev queue appears on the RabbitMQ management dashboard</figcaption></figure><p>Celery includes an event system that tracks worker and task state. Internally, events are published to a special <strong>topic exchange called </strong><strong>celeryev</strong>.</p><p>There are two such event types:</p><ol><li><strong>Worker events</strong> eg.worker.online, worker.heartbeat, worker.offline are always on and are lightweight liveliness signals.</li><li><strong>Task events</strong>, eg.task-received, task-started, task-succeeded, task-failed which are disabled by default unless the -E flag is added.</li></ol><p>You have fine grain control over both types of events. You can turn off worker events (by turning off gossip, more on that below) while turning on task events.</p><h4>Gossip</h4><p>Gossip is Celery’s mechanism for workers to “chat” about cluster state — who’s alive, who just joined, who dropped out, and occasionally elect a leader for coordination. It’s useful for debugging or ad-hoc cluster coordination.</p><p>By default, Gossip is enabled. When a worker starts:</p><ul><li>It creates an <strong>exclusive, auto-delete queue</strong> just for itself.</li><li>That queue is bound to the <strong>celeryev topic exchange</strong> with the routing key pattern worker.#.</li></ul><p>Because every worker subscribes to every worker.* event, the traffic grows quickly as the cluster scales.</p><blockquote>With N workers, each one publishes its own heartbeat, and RabbitMQ fans that message out to the other N-1 gossip queues. In effect, you get an N × (N-1) fan-out pattern.</blockquote><p>In my setup with 100 workers, that meant a single heartbeat was duplicated 99 times. During deployments — when workers were spinning up and shutting down, generating a burst of join, leave, and heartbeat events — the pattern spiraled out of control. <strong>The </strong><strong>celeryev exchange was suddenly handling 7–8k messages per second, pushing RabbitMQ past its memory watermark and leaving the cluster in a degraded state.</strong></p><p>When this memory limit is exceeded, RabbitMQ blocks publishers until usage drops. Once memory falls back under the threshold, RabbitMQ resumes normal operation.</p><p>However, this means that during the memory spike the broker becomes unusable — effectively causing downtime. You won’t want that in production!</p><p>The solution is to disable Gossip so workers don’t bind to worker.#. You can do this in the docker compose where the workers are spun up.</p><pre>celery -A myapp worker --without-gossip</pre><h4>Mingle</h4><p>Mingle is a worker startup step where the new worker contacts other workers to synchronise state — things like revoked tasks and logical clocks. This happens only once, during worker boot. If you don’t need this coordination, you can also disable it with --without-mingle</p><h4>Occasional connection drops</h4><p>In production, connections between Celery and RabbitMQ can occasionally drop — for example, due to a brief network blip. If you have monitoring in place, you may see these as transient errors.</p><blockquote>The good news is that these drops are usually <strong>recoverable</strong>. Celery relies on Kombu, which includes <strong>automatic connection retry logic</strong>. When a connection fails, the worker will attempt to reconnect and resume consuming tasks.</blockquote><p>As long as your queues are configured correctly, messages are <strong>not</strong> lost:</p><ul><li>durable=True (queue survives broker restart)</li><li>delivery_mode=2 (persistent messages)</li><li>Consumers send explicit ACKs to confirm successful processing</li></ul><p>If a connection drops before a task is acknowledged, RabbitMQ will safely requeue it for delivery once the worker reconnects.</p><p>Once the connection is re-established, the worker continues normal operation. In practice, occasional drops are fine, as long as they remain infrequent and queue depth doesn’t build up.</p><h3>To end off</h3><p>That’s all folks, these are some of the key lessons I’ve learned running RabbitMQ + Celery in production. I hope this deep dive has helped you better understand how things work under the hood. If you have more tips, I’d love to hear them in the comments and do reach out!!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=02042cb4fd24" width="1" height="1" alt=""><hr><p><a href="https://levelup.gitconnected.com/deep-dive-into-rabbitmq-pythons-celery-how-to-optimise-your-queues-02042cb4fd24">Deep dive into RabbitMQ &amp; Python’s Celery: How to optimise your queues?</a> was originally published in <a href="https://levelup.gitconnected.com">Level Up Coding</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to level up your technical skills in this AI era & contribute to open source]]></title>
            <link>https://medium.com/womenintechnology/how-to-level-up-your-technical-skills-in-this-ai-era-contribute-to-open-source-e9ee5e457536?source=rss-c49929a81b40------2</link>
            <guid isPermaLink="false">https://medium.com/p/e9ee5e457536</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[open-source]]></category>
            <category><![CDATA[vibe-coding]]></category>
            <dc:creator><![CDATA[clara]]></dc:creator>
            <pubDate>Fri, 20 Jun 2025 10:46:38 GMT</pubDate>
            <atom:updated>2025-06-20T10:46:38.989Z</atom:updated>
            <content:encoded><![CDATA[<h4>AI tools &amp; vibe coding have hidden tradeoffs. Here’s how to use them wisely and why open source is a great secret weapon.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7xaWTdSG9NEJisj2TVdNzg.jpeg" /><figcaption>Image by <a href="https://unsplash.com/@siderius_creativ">Gerard Siderius</a> from <a href="https://unsplash.com/">Unsplash</a></figcaption></figure><p>AI-assisted coding is here to stay. Tools like Cursor, V0, and Lovable have dramatically lowered the barrier to entry — building dashboards, pipelines, or entire apps can now be done in a fraction of the time.</p><p>I use these tools daily, and they’ve definitely made me faster. But as the codebase gets more complex, the tradeoffs become clear: cryptic bugs, tangled logic, and hours lost debugging code I didn’t truly understand.</p><blockquote><strong>AI tools are great — especially for beginners — but they come with a quiet cost. The more you let AI carry the load, the fewer chances you have to sharpen your instincts that come from wrestling with complexity.</strong></blockquote><blockquote><strong>Yes, AI will speed up your workflow, but you’ll also skip the formative steps where technical wisdom is earned.</strong></blockquote><p>“Vibe coding” — quickly cobbling together code with minimal planning — is great for demos or experiments. But for deeper technical growth or building systems with meaningful complexity, vibe coding isn’t enough. This trending Reddit post sums it up perfectly: left unchecked, vibe coding creates more problems than it solves.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/789/0*LeaGIkD0T4-HnBtn" /><figcaption><a href="https://www.reddit.com/r/ProgrammerHumor/comments/1jkd1gr/techdebt25x/?rdt=34387">Reddit post</a></figcaption></figure><p>In this piece, I’ll share tips on how to use AI-assisted tools wisely — and why contributing to open source might be the most underrated way to truly level up your technical skills.</p><h3>My experience vibe coding with Cursor</h3><p>Like many developers, I switched from VS Code (with GitHub Copilot) to Cursor and am currently subscribed to Cursor’s Pro plan ($20/month).</p><p>The feature I rely on most is Cursor’s integrated AI chat, which lets me directly interact with my <strong><em>entire</em></strong> codebase. Its agent can quickly grep through multiple files and even handle images — extremely useful when navigating large, unfamiliar repos. It also spots linter errors and auto-corrects them while directly editing files.</p><p>Initially, Cursor dramatically boosted my productivity, especially for simpler tasks. It felt powerful, almost magical. But as things got complex, I noticed some cracks. Cursor would sometimes generate spaghetti code, mix up similarly named files across directories, and occasionally struggle to follow intricate logic flows.</p><p>Vibe coding can get you thousands of lines of code in minutes — but without a strong mental model of what you’re building, you risk ending up with bloated, over-engineered systems.</p><p>Cursor does a decent job narrowing down the search space when debugging. But letting it make unchecked edits does introduce even more bugs than it solves.</p><blockquote>Beyond the usual advice to “write better prompts,” one strategy I’ve found especially helpful is telling Cursor <strong>not </strong>to make direct edits. (It’s surprisingly obedient about this!)</blockquote><p>Instead, I explicitly ask it to suggest changes first in the chat interface. Then, I would review each suggestion, decide which edits made sense, and apply them selectively — either manually or through Cursor. Unlike ChatGPT, Cursor’s biggest strength is its contextual awareness of the entire codebase and it’s ability to parse through lengthy files (over 5k lines of code) by processing them in manageable chunks.</p><h3>Contributing to open source</h3><p>So, how do you get technically stronger? Two ways stand out: side projects and open source contributions.</p><p>Side projects are great for exploring new tech or diving deep into something you’re passionate or curious about. Wonder how AI agents work or curious about MCP? Just building a simple weekend project teaches you far more than hours of tutorials or documentation. Thanks to open-source, tools and resources are freely accessible, leveling the playing field for everyone.</p><p>But solo projects have downsides. It’s easy to lose motivation — many of my own side projects never saw the light of day.</p><blockquote>Plus, you can find yourself in an echo chamber: your code works, but you’re not sure if it’s following best practices or industry standards. If you’re early in your career and lack mentorship, how do you know if you’re even on the right track?</blockquote><p>This is exactly where open source fills the gap. Open source projects aren’t just for coding wizards; they’re for everyone. Your favourite libraries like Pandas, Matplotlib, TensorFlow, and Keras rely heavily on community involvement.</p><h4>Why bother contributing?</h4><p>Open source lets you make a real impact used by thousands of developers — not just toy projects nobody sees. You’ll become proficient with version control (hello, GitHub!), sharpen your skills navigating complex codebases, pick up best practices, and build a network of people who can vouch for you when it matters.</p><p>There are career benefits too. It’ll add to your portfolio and personal brand, and you’ll ramp up faster when joining new teams.</p><p>But, contribute for the right reasons. <strong><em>If your only motivation is landing a job, DON’T contribute! </em></strong>Open source is not a ticket to get a job — it requires genuine interest and commitment. It shows you’ve a passion to build, and for many startups that begin from open source projects, that’s how they find their first hires.</p><h4>Picking a open source project that you care about</h4><p>Starting out can seem daunting. Many popular repos have enormous codebases, potentially outdated documentation, or a bunch of unclear issues. So how do you pick?</p><blockquote>First up, pick a project you<strong> genuinely care about. </strong>This might sound obvious, but it’s crucial — and underrated.</blockquote><p>Choose something you <strong><em>actually use, </em></strong>whether at work or in a side project. Jumping into an unfamiliar project with unfamiliar tech is simply overwhelming, and you’ll lose motivation fast.</p><p>Personally, I’m both a user and a big fan of PostHog — the product analytics platform built specifically for developers — so I started contributing there. Their docs were comprehensive and well-structured, which made it an awesome place to start. (And no, they didn’t pay me to say this!)</p><h4>What to contribute?</h4><p>There are a <strong><em>ton</em></strong> of things you can do. Here’s an approach that I found helpful.</p><ol><li>Find a feature you <strong><em>need</em></strong> or improve something you <strong><em>use</em></strong>.<br>Narrowing down contributions to features you genuinely care about gives clarity and motivation. The best code comes from solving problems you personally face.</li><li>Set up your local environment.<br>Fork the project, clone it locally, and get it running. Understand where logs are and how to test changes. Get a grasp on the project’s basic structure and coding style.</li><li>Start small and learn by doing <br>Many repos tag beginner-friendly issues (like “good-first-issue”). Pick these to start. Understand and replicate the bug; don’t hesitate to comment if you’re stuck. When you open a PR, ensure your changes pass all linting and tests.</li></ol><p>Learning to navigate the codebase is essential. You don’t need to read every line — that’s practically impossible. After grasping the high-level structure, dive in. Start small to get comfortable with the build, deployment, and PR review process. Write clear commit messages and PR descriptions. Check recently merged PRs to see successful examples or insightful discussions.</p><h3>Wrapping up</h3><p>Contributing to open source takes patience — popular repos are huge, and learning takes time. Becoming a consistent, valuable contributor takes at least a few months, so don’t get discouraged by initial setbacks. If your PR is rejected or you get stuck on a tricky bug, that’s perfectly normal — it’s all part of the learning process.</p><p>If you’re new to open source and want to chat, feel free to connect. While I didn’t dive deeply into technical details here (a quick Google or ChatGPT search can guide you there), I hope this gives you the big-picture perspective to get started. Open source has been rewarding for me — and I hope it will be for you too.</p><p>See you in the next article :)</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e9ee5e457536" width="1" height="1" alt=""><hr><p><a href="https://medium.com/womenintechnology/how-to-level-up-your-technical-skills-in-this-ai-era-contribute-to-open-source-e9ee5e457536">How to level up your technical skills in this AI era &amp; contribute to open source</a> was originally published in <a href="https://medium.com/womenintechnology">Women in Technology</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How LLMs work: Reinforcement learning, RLHF, DeepSeek R1, OpenAI o1, AlphaGo]]></title>
            <link>https://levelup.gitconnected.com/how-llms-work-reinforcement-learning-rlhf-deepseek-r1-openai-o1-alphago-9d0769cb06d1?source=rss-c49929a81b40------2</link>
            <guid isPermaLink="false">https://medium.com/p/9d0769cb06d1</guid>
            <category><![CDATA[openai]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[large-language-models]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[deepseek]]></category>
            <dc:creator><![CDATA[clara]]></dc:creator>
            <pubDate>Thu, 17 Apr 2025 14:47:02 GMT</pubDate>
            <atom:updated>2025-04-17T14:47:02.408Z</atom:updated>
            <content:encoded><![CDATA[<h4>Part 2 of the LLM deep dive</h4><p>Welcome to part 2 of my LLM deep dive. If you’ve not read Part 1, I highly encourage you to <a href="https://medium.com/gitconnected/how-llms-work-pre-training-to-post-training-neural-networks-hallucinations-inference-4ee34a6c0e9b">check it out first</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SDYQ20DixkZi9OD1x76FVA.jpeg" /><figcaption>Image by <a href="https://unsplash.com/@cgower">Christopher</a> from <a href="https://unsplash.com/">Unsplash</a></figcaption></figure><p>Previously, we covered the first two major stages of training an LLM:</p><ol><li>Pre-training — Learning from massive datasets to form a base model.</li><li>Supervised fine-tuning (SFT) — Refining the model with curated examples to make it useful.</li></ol><p>Now, we’re diving into the next major stage: R<strong>einforcement Learning (RL)</strong>. While pre-training and SFT are well-established, RL is still evolving but has become a critical part of the training pipeline.</p><p>I’ve taken reference from <a href="https://www.youtube.com/watch?app=desktop&amp;v=7xTGNNLPyMI">Andrej Karpathy’s widely popular 3.5-hour YouTube</a>. Andrej is a founding member of OpenAI, his insights are gold — you get the idea.</p><p>Let’s go 🚀</p><h3>What’s the purpose of reinforcement learning (RL)?</h3><p>Humans and LLMs process information differently. What’s intuitive for us — like basic arithmetic — may not be for an LLM, which only sees text as sequences of tokens. Conversely, an LLM can generate expert-level responses on complex topics simply because it has seen enough examples during training.</p><p>This difference in cognition makes it challenging for human annotators to provide the “perfect” set of labels that consistently guide an LLM toward the right answer.</p><blockquote>RL bridges this gap by allowing the model to <strong>learn from its own experience</strong>.</blockquote><p>Instead of relying solely on explicit labels, the model explores different token sequences and receives feedback — reward signals — on which outputs are most useful. Over time, it learns to align better with human intent.</p><h3>Intuition behind RL</h3><p>LLMs are stochastic — meaning their responses aren’t fixed. Even with the same prompt, the output varies because it’s sampled from a probability distribution.</p><p>We can harness this randomness by generating thousands or even millions of possible responses <strong>in parallel</strong>. Think of it as the model exploring different paths — some good, some bad. <strong>Our goal is to encourage it to take the better paths more often.</strong></p><p>To do this, we train the model on the sequences of tokens that lead to better outcomes. Unlike supervised fine-tuning, where human experts provide labeled data, <strong>reinforcement learning allows the model to <em>learn from itself.</em></strong></p><p>The model discovers which responses work best, and after each training step, we update its parameters. Over time, this makes the model more likely to produce high-quality answers when given similar prompts in the future.</p><p>But how do we determine which responses are best? And how much RL should we do? The details are tricky, and getting them right is not trivial.</p><h3>RL is not “new” — It can surpass human expertise (AlphaGo, 2016)</h3><p>A great example of RL’s power is DeepMind’s AlphaGo, the first AI to defeat a professional Go player and later surpass human-level play.</p><p>In the <a href="https://discovery.ucl.ac.uk/id/eprint/10045895/1/agz_unformatted_nature.pdf">2016 Nature paper</a> (graph below), when a model was trained purely by SFT (giving the model tons of good examples to imitate from), the model was able to reach human-level performance, <strong>but never surpass it</strong>.</p><p>The dotted line represents Lee Sedol’s performance — the best Go player in the world.</p><blockquote>This is because SFT is about replication, not innovation — it doesn’t allow the model to discover new strategies beyond human knowledge.</blockquote><p>However, RL enabled AlphaGo to play against itself, refine its strategies, and ultimately <strong>exceed human expertise</strong> (blue line).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1013/1*NNYI22xy_DLqxtcCEtKU6Q.png" /></figure><p>RL represents an exciting frontier in AI — where models can explore strategies beyond human imagination when we train it on a diverse and challenging pool of problems to refine it’s thinking strategies.</p><h3>RL foundations recap</h3><p>Let’s quickly recap the key components of a typical RL setup:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kIE3EhuAWF5tXD8r3SuhlQ.png" /></figure><ul><li><strong><em>Agent</em></strong><br>The learner or decision maker. It observes the current situation (<em>state</em>), chooses an action, and then updates its behaviour based on the outcome (<em>reward</em>).</li><li><strong><em>Environment</em></strong> — the external system in which the agent operates.</li><li><strong><em>State</em></strong> — a snapshot of the environment at a given step <em>t</em>.</li></ul><p>At each timestamp, the agent performs an <strong><em>action</em></strong> in the environment that will change the environment’s state to a new one. The agent will also receive feedback indicating how good or bad the action was.</p><p>This feedback is called a <strong><em>reward</em></strong>, and is represented in a numerical form. A positive reward encourages that behaviour, and a negative reward discourages it.</p><p>By using feedback from different states and actions, the agent gradually learns the optimal strategy to <strong>maximise the total reward</strong> over time.</p><h4>Policy</h4><p>The policy is the agent’s strategy. If the agent follows a good policy, it will consistently make good decisions, leading to higher rewards over many steps.</p><blockquote><strong>In mathematical terms, it is a function that determines the probability of different outputs for a given state — <em>(πθ(a|s))</em>.</strong></blockquote><h4>Value function</h4><p>An estimate of how good it is to be in a certain state, considering the long term expected reward. For an LLM, the reward might come from human feedback or a reward model.</p><h4>Actor-Critic architecture</h4><p>It is a popular RL setup that combines two components:</p><ol><li><strong>Actor</strong> — Learns and updates the <strong>policy</strong> (πθ), deciding which action to take in each state.</li><li><strong>Critic</strong> — Evaluates the <strong>value function</strong> (V(s)) to give feedback to the actor on whether its chosen actions are leading to good outcomes.</li></ol><p>How it works:</p><ul><li>The <strong>actor</strong> picks an action based on its current policy.</li><li>The <strong>critic</strong> evaluates the outcome (reward + next state) and updates its value estimate.</li><li>The critic’s feedback helps the actor refine its policy so that future actions lead to higher rewards.</li></ul><h4>Putting it all together for LLMs</h4><p>The state can be the current text (prompt or conversation), and the action can be the next token to generate. A reward model (eg. human feedback), tells the model how good or bad it’s generated text is.</p><p>The policy is the model’s strategy for picking the next token, while the value function estimates how beneficial the current text context is, in terms of eventually producing high quality responses.</p><h3>DeepSeek-R1 (published 22 Jan 2025)</h3><p>To highlight RL’s importance, let’s explore DeepSeek-R1, a reasoning model achieving top-tier performance while remaining open-source. <a href="https://arxiv.org/abs/2501.12948">The paper introduced two models: <strong>DeepSeek-R1-Zero and DeepSeek-R1.</strong></a></p><ul><li>DeepSeek-R1-Zero was trained solely via large-scale RL, skipping supervised fine-tuning (SFT).</li><li>DeepSeek-R1 builds on it, addressing encountered challenges.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YTye74wxJs5upVJD5ypwsw.png" /></figure><p>Let’s dive into some of these key points.</p><h4>1. RL algo: Group Relative Policy Optimisation (GRPO)</h4><p>One key game changing RL algorithm is Group Relative Policy Optimisation (GRPO), a variant of the widely popular Proximal Policy Optimisation (PPO). <a href="https://arxiv.org/abs/2402.03300">GRPO was introduced in the DeepSeekMath paper in Feb 2024.</a></p><p><strong><em>Why GRPO over PPO?</em></strong></p><p>PPO struggles with reasoning tasks due to:</p><ol><li>Dependency on a critic model.<br>PPO needs a separate critic model, effectively doubling memory and compute.<br>Training the critic can be complex for nuanced or subjective tasks.</li><li>High computational cost as RL pipelines demand substantial resources to evaluate and optimise responses.</li><li>Absolute reward evaluations <br>When you rely on an absolute reward — meaning there’s a single standard or metric to judge whether an answer is “good” or “bad” — it can be hard to capture the nuances of open-ended, diverse tasks across different reasoning domains.</li></ol><p><strong><em>How GRPO addressed these challenges:</em></strong></p><p>GRPO eliminates the critic model by using<strong> relative evaluation</strong> — responses are compared within a group rather than judged by a fixed standard.</p><p>Imagine students solving a problem. Instead of a teacher grading them individually, they compare answers, learning from each other. Over time, performance converges toward higher quality.</p><h4>How does GRPO fit into the whole training process?</h4><p>GRPO modifies how loss is calculated while keeping other training steps unchanged:</p><ol><li><strong>Gather data (queries + responses)</strong><br>- For LLMs, queries are like questions<br>- The old policy (older snapshot of the model) generates several candidate answers for each query</li><li><strong>Assign rewards </strong>— each response in the group is scored (the “reward”).</li><li><strong>Compute the GRPO loss</strong><br>Traditionally, you’ll compute a loss — which shows the deviation between the model prediction and the true label.<br><strong>In GRPO, however, you measure:</strong><br>a) How likely is the new policy to produce past responses?<br>b) Are those responses relatively better or worse?<br>c) Apply clipping to prevent extreme updates.<br>This yields a scalar loss.</li><li><strong>Back propagation + gradient descent</strong><br>- Back propagation calculates how each parameter contributed to loss<br>- Gradient descent updates those parameters to reduce the loss<br>- Over many iterations, this gradually shifts the new policy to prefer higher reward responses</li><li><strong>Update the old policy occasionally to match the new policy</strong>. <br>This refreshes the baseline for the next round of comparisons.</li></ol><h4>2. Chain of thought (CoT)</h4><p>Traditional LLM training follows pre-training → SFT → RL. However, DeepSeek-R1-Zero <strong>skipped SFT</strong>, allowing the model to directly explore CoT reasoning.</p><p>Like humans thinking through a tough question, CoT enables models to break problems into intermediate steps, boosting complex reasoning capabilities. OpenAI’s o1 model also leverages this, as noted in its September 2024 report: <strong>o1’s performance improves with more RL (train-time compute) and more reasoning time (test-time compute).</strong></p><p>DeepSeek-R1-Zero exhibited reflective tendencies, autonomously refining its reasoning.</p><blockquote>A key graph (below) in the paper showed <strong>increased thinking during training, leading to longer (more tokens), more detailed and better responses.</strong></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MecI09Ce4KERPtfDICihSw.png" /></figure><p>Without explicit programming, it began revisiting past reasoning steps, improving accuracy . This highlights chain-of-thought reasoning as an emergent property of RL training.</p><p>The model also had an “aha moment” (below) — a fascinating example of how RL can lead to unexpected and sophisticated outcomes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9IstV0vjYYf4eBKj0TxUOA.png" /></figure><p>Note: Unlike DeepSeek-R1, OpenAI does not show full exact reasoning chains of thought in o1 as they are concerned about a distillation risk — where someone comes in and tries to imitate those reasoning traces and recover a lot of the reasoning performance by just imitating. Instead, o1 just summaries of these chains of thoughts.</p><h3>Reinforcement learning with Human Feedback (RLHF)</h3><p>For tasks with verifiable outputs (e.g., math problems, factual Q&amp;A), AI responses can be easily evaluated. But what about areas like summarisation or creative writing, where there’s no single “correct” answer?</p><p>This is where human feedback comes in — but naïve RL approaches are unscalable.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9HoftPjP_wOEr9_bQeQBAw.png" /></figure><p>Let’s look at the naive approach with some arbitrary numbers.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6vrbBtNj613qwihCU6Zk1A.png" /></figure><p>That’s one billion human evaluations needed! This is too costly, slow and unscalable. Hence, a smarter solution is to train an AI “reward model” to learn human preferences, dramatically reducing human effort.</p><blockquote>Ranking responses is also easier and more intuitive than absolute scoring.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6B7rbHVx9Sz7QpFHq_fECg.png" /></figure><h4>Upsides of RLHF</h4><ul><li>Can be applied to any domain, including creative writing, poetry, summarisation, and other open-ended tasks.</li><li>Ranking outputs is much easier for human labellers than generating creative outputs themselves.</li></ul><h4>Downsides of RLHF</h4><ul><li>The reward model is an approximation — it may not perfectly reflect human preferences.</li><li>RL is good at gaming the reward model — if run for too long, the model might exploit loopholes, generating nonsensical outputs that still get high scores.</li></ul><blockquote><strong>Do note that RLHF is not the same as traditional RL.</strong></blockquote><p>For empirical, verifiable domains (e.g., math, coding), RL can run indefinitely and discover novel strategies. RLHF, on the other hand, is more like a fine-tuning step to align models with human preferences.</p><h3>To end off</h3><p>And that’s a wrap! I hope you enjoyed Part 2 :) If you haven’t already read Part 1 — <a href="https://medium.com/gitconnected/how-llms-work-pre-training-to-post-training-neural-networks-hallucinations-inference-4ee34a6c0e9b">do check it out here</a>.</p><p>Got questions or ideas for what I should cover next? Drop them in the comments — I’d love to hear your thoughts. See you in the next article!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9d0769cb06d1" width="1" height="1" alt=""><hr><p><a href="https://levelup.gitconnected.com/how-llms-work-reinforcement-learning-rlhf-deepseek-r1-openai-o1-alphago-9d0769cb06d1">How LLMs work: Reinforcement learning, RLHF, DeepSeek R1, OpenAI o1, AlphaGo</a> was originally published in <a href="https://levelup.gitconnected.com">Level Up Coding</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How LLMs work: Pre-training to post-training, neural networks, hallucinations & inference]]></title>
            <link>https://levelup.gitconnected.com/how-llms-work-pre-training-to-post-training-neural-networks-hallucinations-inference-4ee34a6c0e9b?source=rss-c49929a81b40------2</link>
            <guid isPermaLink="false">https://medium.com/p/4ee34a6c0e9b</guid>
            <category><![CDATA[large-language-models]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[hallucinations]]></category>
            <category><![CDATA[chatgpt]]></category>
            <category><![CDATA[neural-networks]]></category>
            <dc:creator><![CDATA[clara]]></dc:creator>
            <pubDate>Wed, 19 Feb 2025 15:29:31 GMT</pubDate>
            <atom:updated>2025-02-19T15:29:31.380Z</atom:updated>
            <content:encoded><![CDATA[<h4>The fundamentals — Part 1 of a deep dive into LLMs</h4><p>With the recent explosion of interest in large language models (LLMs), they often seem almost magical. But let’s demystify them.</p><p>I wanted to step back and unpack the fundamentals — breaking down how LLMs are built, trained, and fine-tuned to become the AI systems we interact with today.</p><p>This <strong>two-part deep dive</strong> is something I’ve been meaning to do for a while and was also inspired by <a href="https://www.youtube.com/watch?app=desktop&amp;v=7xTGNNLPyMI">Andrej Karpathy’s widely popular 3.5-hour YouTube</a> video, which has racked up 800,000+ views in just 10 days. Andrej is a founding member of OpenAI, his insights are gold— you get the idea.</p><p>If you have the time, <strong>his video is definitely worth watching</strong>. But let’s be real — 3.5 hours is a long watch. So, for all the busy folks who don’t want to miss out, I’ve distilled the key concepts from the first 1.5 hours into this <strong>10-minute read</strong>, adding my own breakdowns to help you build a solid intuition.</p><h4>What you’ll get</h4><p>Part 1 (this article): Covers the fundamentals of LLMs, including pre-training to post-training, neural networks, hallucinations, and inference.</p><p>Part 2: Reinforcement learning with human/AI feedback, investigating o1 models, DeepSeek R1, AlphaGo</p><p>Let’s go! I’ll start with looking at how LLMs are being built.</p><p>At a high level, there are 2 key phases: pre-training and post-training.</p><h3>1. Pre-training</h3><p>Before an LLM can generate text, it must first learn how language works. This happens through pre-training, a highly computationally intensive task.</p><h4>Step 1: Data collection and preprocessing</h4><p>The first step in training an LLM is gathering as much high-quality text as possible. The goal is to create a massive and diverse dataset containing a wide range of human knowledge.</p><p>One source is <a href="https://commoncrawl.org/">Common Crawl</a>, which is a free, open repository of web crawl data containing 250 billion web pages over 18 years. However, raw web data is noisy — containing spam, duplicates and low quality content — so preprocessing is essential.</p><p>If you’re interested in preprocessed datasets, FineWeb offers a curated version of Common Crawl, and is made available on <a href="https://huggingface.co/spaces/HuggingFaceFW/blogpost-fineweb-v1">hugging face</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cM7ySiq9MVstpOiZojAKlA.png" /><figcaption>Screenshot of the dataset from FineWeb</figcaption></figure><p>Once cleaned, the text corpus is ready for tokenization.</p><h4>Step 2: Tokenization</h4><p>Before a neural network can process text, it must be converted into numerical form. This is done through <strong>tokenization</strong>, where words, subwords, or characters are mapped to unique numerical tokens.</p><p>Think of tokens as the building blocks — the fundamental building blocks of all language models. In GPT4, there are 100,277 possible tokens.</p><p>A popular tokenizer, <a href="https://tiktokenizer.vercel.app/">Tiktokenizer</a>, allows you to experiment with tokenization and see how text is broken down into tokens. Try entering a sentence, and you’ll see each word or subword assigned a series of numerical IDs.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UWmNsN3RieBGJEjWCV1z7w.png" /><figcaption>Screenshot from Tiktokenizer</figcaption></figure><h4>Step 3: Neural network training</h4><p>Once the text is tokenized, the neural network learns to predict the next token based on its context. As shown above, the model takes an input sequence of tokens (e.g., <em>“we are cook ing”</em>) and processes it through a giant mathematical expression — which represents the model’s architecture — to predict the next token.</p><p>A neural network consists of 2 key parts:</p><ol><li><strong>Parameters (weights)</strong> — the learned numerical values from training.</li><li><strong>Architecture (mathematical expression) </strong>— the structure defining how the input tokens are processed to produce outputs.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OL8zXp5skbWkP5DTOXAJMw.png" /><figcaption>Self drawn high level illustration of how neural network training works</figcaption></figure><p>Initially, the model’s predictions are random, but as training progresses, it learns to assign probabilities to possible next tokens.</p><p>When the correct token (e.g., “food”) is identified, the model adjusts its billions of parameters (weights) through <strong>backpropagation</strong> — an optimization process that reinforces correct predictions by increasing their probabilities while reducing the likelihood of incorrect ones.</p><p>This process is repeated billions of times across massive datasets.</p><h4>Base model — the output of pre-training</h4><p>At this stage, the base model has learned:</p><ul><li>How words, phrases and sentences relate to each other</li><li>Statistical patterns in your training data</li></ul><p>However, <strong>base models are not yet optimised for real-world tasks</strong>. You can think of them as an advanced autocomplete system — they predict the next token based on probability, but with limited instruction-following ability.</p><p>A base model can sometimes recite training data verbatim and can be used for certain applications through <strong>in-context learning</strong>, where you guide its responses by providing examples in your prompt. However, to make the model truly useful and reliable, it requires further training.</p><h3>2. Post training — Making the model useful</h3><p>Base models are raw and unrefined. To make them helpful, reliable, and safe, they go through post-training, where they are fine-tuned on smaller, specialised datasets.</p><blockquote>Because the model is a neural network, it cannot be explicitly programmed like traditional software. <strong>Instead, we “program” it implicitly by training it on structured labeled datasets that represent examples of desired interactions.</strong></blockquote><h4>How post training works</h4><p>Specialised datasets are created, consisting of structured examples on how the model should respond in different situations.</p><p>Some types of post training include:</p><ol><li><strong>Instruction/conversation fine tuning </strong><br>Goal: To teach the model to follow instructions, be task oriented, engage in multi-turn conversations, follow safety guidelines and refuse malicious requests, etc. <br>Eg: <a href="https://arxiv.org/abs/2203.02155">InstructGPT (2022)</a>: OpenAI hired some 40 contractors to create these labelled datasets. These human annotators wrote prompts and provided ideal responses based on safety guidelines. Today, many datasets are generated automatically, with humans reviewing and editing them for quality.</li><li><strong>Domain specific fine tuning</strong><br>Goal: Adapt the model for specialised fields like medicine, law and programming.</li></ol><p>Post training also introduces <strong>special tokens </strong>— symbols that were not used during pre-training — to help the model understand the structure of interactions. These tokens signal where a user’s input starts and ends and where the AI’s response begins, ensuring that the model correctly distinguishes between prompts and replies.</p><blockquote>Now, let’s move on to some other key concepts.</blockquote><h3>Inference — how the model generates new text</h3><p>Inference can be performed at any stage, even midway through pre-training, to evaluate how well the model has learned.</p><p>When given an input sequence of tokens, the model assigns probabilities to all possible next tokens based on patterns it has learned during training.</p><p><strong>Instead of always choosing the most likely token, it samples from this probability distribution — similar to flipping a biased coin, where higher-probability tokens are more likely to be selected.</strong></p><p>This process repeats iteratively, with each newly generated token becoming part of the input for the next prediction.</p><p>Token selection is <strong>stochastic </strong>and<strong> </strong>the same input can produce different outputs. Over time, the model generates text that wasn’t explicitly in its training data but follows the same statistical patterns.</p><h3>Hallucinations — when LLMs generate false info</h3><h4>Why do hallucinations occur?</h4><p>Hallucinations happen because LLMs do not “know” facts — they simply predict the most statistically likely sequence of words based on their training data.</p><p>Early models struggled significantly with hallucinations.</p><p>For instance, in the example below, if the training data contains many “Who is…” questions with definitive answers, the model learns that such queries should always have confident responses, even when it lacks the necessary knowledge.</p><p>When asked about an unknown person, the model does not default to “I don’t know” because this pattern was not reinforced during training. Instead, it generates its best guess, often leading to fabricated information.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NXYTpKGSZA-sETS14alARg.png" /><figcaption>Self drawn example</figcaption></figure><h4>How do you reduce hallucinations?</h4><p><strong><em>Method 1: Saying “I don’t know”</em></strong></p><p>Improving factual accuracy requires explicitly training the model to recognise what it does not know — a task that is more complex than it seems.</p><p>This is done via <strong>self interrogation</strong>, a process that helps define the model’s knowledge boundaries.</p><p>Self interrogation can be automated using another AI model, which generates questions to probe knowledge gaps. If it produces a false answer, new training examples are added, where the correct response is:<br><em>“I’m not sure. Could you provide more context?”</em></p><p>If a model has seen a question many times in training, it will assign a high probability to the correct answer.</p><p><strong>If the model has not encountered the question before, it distributes probability more evenly across multiple possible tokens, making the output more randomised. No single token stands out as the most likely choice.</strong></p><p>Fine tuning explicitly trains the model to handle low-confidence outputs with predefined responses.</p><p>For example, when I asked ChatGPT-4o, “<em>Who is asdja rkjgklfj?</em>”, it correctly responded: “I’m not sure who that is. Could you provide more context?”</p><p><strong><em>Method 2: Doing a web search</em></strong></p><p>A more advanced method is to extend the model’s knowledge beyond its training data by giving it access to external search tools.</p><p>At a high level, when a model detects uncertainty, it can trigger a web search. The search results are then inserted into a model’s context window — essentially allowing this new data to be part of it’s working memory. The model references this new information while generating a response.</p><h3>Vague recollections vs working memory</h3><p>Generally speaking, LLMs have two types of knowledge access.</p><ol><li>Vague recollections — the knowledge stored in the model’s parameters from pre-training. This is based on patterns it learned from vast amount of internet data but is not precise nor searchable.</li><li>Working memory — the information that is available in the model’s context window, which is directly accessible during inference. Any text provided in the prompt acts as a short term memory, allowing the model to recall details while generating responses.</li></ol><p>Adding relevant facts within the context window significantly improves response quality.</p><h3>Knowledge of self</h3><p>When asked questions like <em>“Who are you?”</em> or <em>“What built you?”</em>, an LLM will generate a statistical best guess based on its training data, unless explicitly programmed to respond accurately.</p><p>LLMs do not have true self-awareness, their responses depend on patterns seen during training.</p><p>One way to provide the model with a consistent identity is by using a <strong>system prompt</strong>, which sets predefined instructions about how it should describe itself, its capabilities, and its limitations.</p><h3>To end off</h3><p>That’s a wrap for Part 1! I hope this has helped you build intuition on how LLMs work. In Part 2, we’ll dive deeper into reinforcement learning and some of the latest models.</p><p>Got questions or ideas for what I should cover next? Drop them in the comments — I’d love to hear your thoughts. See you in Part 2! :)</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4ee34a6c0e9b" width="1" height="1" alt=""><hr><p><a href="https://levelup.gitconnected.com/how-llms-work-pre-training-to-post-training-neural-networks-hallucinations-inference-4ee34a6c0e9b">How LLMs work: Pre-training to post-training, neural networks, hallucinations &amp; inference</a> was originally published in <a href="https://levelup.gitconnected.com">Level Up Coding</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Deep Dive into WebSockets and Their Role in Client-Server Communication]]></title>
            <link>https://medium.com/data-science/deep-dive-into-websockets-and-their-role-in-client-server-communication-aac387e10cb6?source=rss-c49929a81b40------2</link>
            <guid isPermaLink="false">https://medium.com/p/aac387e10cb6</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[system-design-concepts]]></category>
            <category><![CDATA[websocket]]></category>
            <category><![CDATA[client-server-model]]></category>
            <category><![CDATA[getting-started]]></category>
            <dc:creator><![CDATA[clara]]></dc:creator>
            <pubDate>Mon, 03 Feb 2025 11:02:00 GMT</pubDate>
            <atom:updated>2025-02-03T11:02:00.167Z</atom:updated>
            <content:encoded><![CDATA[<h4>How WebSockets work, its tradeoffs, and how to design a real time messaging app</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HzsgZYLeeRwRNm5gbwQWrg.jpeg" /><figcaption>Image by <a href="https://unsplash.com/@kellysikkema">Kelly</a> from <a href="https://unsplash.com/">Unsplash</a></figcaption></figure><p>Real-time communication is everywhere — live chatbots, data streams, or instant messaging. WebSockets are a powerful enabler of this, but when should you use them? How do they work, and how do they differ from traditional HTTP requests?</p><p>This article was inspired by a recent system design interview — “design a real time messaging app” — where I stumbled through some concepts. Now that I’ve dug deeper, I’d like to share what I’ve learned so you can avoid the same mistakes.</p><p>In this article, we’ll explore how WebSockets fit into the bigger picture of client‑server communication. We’ll discuss what they do well, where they fall short, and — yes — how to design a real‑time messaging app.</p><h3>Client-server communication</h3><p>At its core, client-server communication is the exchange of data between two entities: a client and a server.</p><p>The client requests for data, and the server processes these requests and returns a response. These roles are not exclusive — services can act as both a client and a server simultaneously, depending on the context.</p><p>Before diving into the details of WebSockets, let’s take a step back and explore the bigger picture of client-server communication methods.</p><h4>1. Short polling</h4><p>Short polling is the simplest, most familiar approach.</p><p>The client repeatedly sends HTTP requests to the server at regular intervals (e.g., every few seconds) to check for new data. Each request is independent and one-directional (client → server).</p><p>This method is easy to set up but can waste resources if the server rarely has fresh data. Use it for less time‑sensitive applications where occasional polling is sufficient.</p><h4>2. Long polling</h4><p>Long polling is an improvement over short polling, designed to reduce the number of unnecessary requests. Instead of the server immediately responding to a client request, the server <strong>keeps the connection open</strong> until new data is available. Once the server has data, it sends the response, and the client immediately establishes a new connection.</p><p>Long polling is also <strong>stateless</strong> and <strong>one-directional</strong> (client → server).</p><p>A typical example is a ride‑hailing app, where the client waits for a match or booking update.</p><h4>3. Webhooks</h4><p>Webhooks flip the script by making the server the initiator. The server sends <strong>HTTP POST</strong> requests to a client-defined endpoint whenever specific events occur.</p><p>Each request is <strong>independent</strong> and does not rely on a persistent connection. Webhooks are also <strong>one-directional</strong> (server to client).</p><p>Webhooks are widely used for asynchronous notifications, especially when integrating with third-party services. For example, payment systems use webhooks to notify clients when the status of a transaction changes.</p><h4>4. Server-Sent Events (SSE)</h4><p>SSEs are a <strong>native HTTP-based event streaming protocol</strong> that allows servers to push real-time updates to clients over a single, <strong>persistent connection</strong>.</p><p>SSE works using the EventSource API, making it simple to implement in modern web applications. It is <strong>one-directional</strong> (server to client) and ideal for situations where the client only needs to receive updates.</p><p>SSE is well-suited for applications like trading platforms or live sports updates, where the server pushes data like stock prices or scores in real time. The client does not need to send data back to the server in these scenarios.</p><h4>But what about two-way communication?</h4><p>All the methods above focus on one‑directional flow. For true two‑way, real‑time exchanges, we need a different approach. That’s where WebSockets shine.</p><p>Let’s dive in.</p><h3>How do WebSockets work?</h3><p>WebSockets enable <strong>real-time, bidirectional communication</strong>, making them perfect for applications like chat apps, live notifications, and online gaming. Unlike the traditional HTTP request-response model, WebSockets create a <strong>persistent</strong> connection, where both client and server can send messages independently without waiting for a request.</p><blockquote>The connection begins as a regular HTTP request and is upgraded to a WebSocket connection through a handshake.</blockquote><p>Once established, it uses a single TCP connection, operating on the same ports as HTTP (80 and 443). Messages sent over WebSockets are small and lightweight, making them efficient for low-latency, high-interactivity use cases.</p><p>WebSocket connections follow a specific URI format: ws:// for regular connections and wss:// for secure, encrypted connections.</p><p><strong>What’s a handshake?</strong></p><p>A handshake is the process of <strong>initialising a connection</strong> between two systems. For WebSockets, it begins with an HTTP GET request from the client, asking for a protocol upgrade. This ensures compatibility with HTTP infrastructure before transitioning to a persistent WebSocket connection.</p><ol><li><strong>Client sends a request, with headers that look like:</strong></li></ol><pre>GET /chat HTTP/1.1<br>Host: server.example.com<br>Upgrade: websocket<br>Connection: Upgrade<br>Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==<br>Origin: http://example.com<br>Sec-WebSocket-Protocol: chat, superchat<br>Sec-WebSocket-Version: 13</pre><ul><li>Upgrade — signals the request to switch the protocol</li><li>Sec-WebSocket-Key — Randomly generated, base64 encoded string used for handshake verification</li><li>Sec-WebSocket-Protocol (optional) — Lists subprotocols the client supports, allowing the server to pick one.</li></ul><p><strong>2. Server responds to resquest</strong></p><p>If the server supports WebSockets and agrees to the upgrade, it responds with a <strong>101 Switching Protocols</strong> status. Example headers:</p><pre>HTTP/1.1 101 Switching Protocols<br>Upgrade: websocket<br>Connection: Upgrade<br>Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=<br>Sec-WebSocket-Protocol: chat</pre><ul><li>Sec-WebSocket-Accept — Base64 encoded hash of the client’s Sec-WebSocket-Key and a GUID. This ensures the handshake is secure and valid.</li></ul><p><strong>3. Handshake validation</strong></p><p>With the 101 Switching Protocols response, the WebSocket connection is successfully established and both client and server can start exchanging messages in real time.</p><p>This connection will remain open till it is explicitly closed by either party.</p><p>If any code other than 101 is returned, the client has to end the connection and the WebSocket handshake will fail.</p><p>Here’s a summary.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hggiIeqxJYonnI-lyt4W5g.png" /><figcaption>Summary of WebSockets (drawn by me)</figcaption></figure><h3>WebSocket use cases</h3><p>We’ve talked about how WebSockets enable real-time, bidirectional communication, but that’s still pretty abstract term. Let’s nail down some real examples.</p><p>WebSockets are widely used in real-time collaboration tools and chat applications, such as Excalidraw, Telegram, WhatsApp, Google Docs, Google Maps and the live chat section during a YouTube or TikTok live stream.</p><h3>Trade offs</h3><h4>1. Having a fallback strategy if connections are terminated</h4><p>WebSockets don’t automatically recover if the connection is terminated due to network issues, server crashes, or other failures. The client must explicitly detect the disconnection and attempt to re-establish the connection.</p><p><strong>Long polling</strong> is often used as a backup while a WebSocket connection tries to get reestablished.</p><h4>2. Not optimised for streaming audio and video data</h4><p>WebSocket messages are designed for sending <strong>small, structured messages.</strong> To stream large media data, a technology like WebRTC is better suited for these scenarios.</p><h4>3. WebSockets are stateful, hence horizontally scaling is not trivial</h4><p>WebSockets are <strong>stateful</strong>, meaning the server must maintain an active connection for every client. This makes horizontal scaling more complex compared to stateless HTTP, where any server can handle a client request without maintaining persistent state.</p><p>You’ll need an additional layer of pub/sub mechanisms to do this.</p><h3>Design a real time messaging app</h3><p>Now let’s see how this is applied in system design. I’ve covered both the simple (unscalable) solution and a horizontally scaled one.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BL8s_5xlD31pPEvwoDrDIQ.png" /><figcaption>End-to-end flow for a horizontally scaled, real time 1–1 chat (drawn by me)</figcaption></figure><h4>Non-scalable single server app: How do two users chat real time?</h4><ol><li>All users connect via WebSocket to one server. The server holds an in-memory mapping of userID : WebSocket conn 1</li><li>user1 sends the message over its WebSocket connection to the server.</li><li>The server writes the message to the MessageDB (persistence first).</li><li>The server then looks up user2 : WebSocket conn 2 in it’s in memory map. If user2 is online, it delivers the message in real time.</li><li>If user2 is offline, the server writes to InboxDB (a store of undelivered messages). When user2 returns online, the server fetches all offline messages from InboxDB.</li></ol><h4><strong><em>Horizontally scaled system: How do two users chat real time?</em></strong></h4><p>A single server can only handle so many concurrent WebSockets. To serve more users, you need to horizontally scale your WebSocket connections.</p><blockquote>The key challenge: If user1 is connected to server1 but user2 is connected to server2, how does the system know where to send the message?</blockquote><p>Redis can be used as a global data store that maps userID : serverID for active WebSocket sessions. Each server updates Redis when a user connects (goes online) or disconnects (goes offline).</p><p>For instance:</p><ul><li>user1 connects to server1.<br>server1’s in memory map: user1 : WebSocket connection <br>server1 also writes to Redis: user1 : server1</li><li>user2 connects to server2.<br>server2’s in memory map: user2 : WebSocket connection <br>server2 also writes to Redis: user2 : server2</li></ul><p><strong><em>End to end chat flow: user1 sends a message to user2</em></strong></p><ol><li>user1 sends a message through it’s WebSocket on server1.</li><li>server1 passes the message to a Chat Service.</li><li>Chat Service first writes the message to MessageDB for persistence.</li><li>Chat Service then checks Redis to get the online/offline status of user2.</li><li><strong>If user2 is online</strong>, Chat Service publishes the message to a message broker, tagging it with: “user2: server2”.</li><li>The broker then routes the message to server2.</li><li>server2 looks up it’s local in memory mapping to find the WebSocket connection of user2 and pushes the message real time over that WebSocket.</li><li><strong>If user2 is offline (no entry in Redis)</strong>, Chat Service writes the message to the InboxDB. When user2 returns online, Chat Service will fetch all the undelivered messages.</li><li>Whenever a new WebSocket connection is opened or closed, the servers update Redis.</li><li>When a user first loads the app or opens a chat, the Chat Service fetches historical messages (e.g., from the last 10 days) from MessageDB. A cache layer can reduce repeated DB queries.</li></ol><h4>Some important design considerations:</h4><ol><li><strong>Persistence first</strong><br>All messages go to the DB before being delivered. If a push to WebSocket fails, the message is still safe in the DB.</li><li><strong>Redis</strong><br>Stores <strong>only</strong> <strong>active</strong> connections to minimize overhead.<br>A replica can be added to prevent a single point of failure.</li><li>Inbox DB helps to handle offline cases cleanly.</li><li><strong>Chat Service abstraction</strong><br>The WebSocket servers handle real‐time connections and routing.<br>The Chat Service layer handles HTTP requests and all DB writes.<br>This separation of concerns makes it easier to scale or evolve each piece.</li><li><strong>Ensuring in order delivery of messages</strong><br>Typical “real time push” workflows can have network variations, leading to messages arriving out of order.<br>Many message brokers also <strong>do not guarantee</strong> strict ordering.<br>To handle this, each message is assigned a timestamp at creation. Even if messages arrive out of order, the client can reorder them based on the timestamp.</li><li><strong>Load balancers</strong><br>L4 Load Balancer (TCP) for sticky WebSocket connections.<br>L7 Load Balancer (HTTP) for regular requests (CRUD, login, etc).</li></ol><h3>Wrapping up</h3><p>That’s all for now! There’s so much more we could explore, but I hope this gave you a solid starting point. Feel free to drop your questions in the comments below :)</p><p>I write regularly on Python, software development and the projects I build, so give me a follow to not miss out. See you in the next article.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=aac387e10cb6" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/deep-dive-into-websockets-and-their-role-in-client-server-communication-aac387e10cb6">Deep Dive into WebSockets and Their Role in Client-Server Communication</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Why Variable Scoping Can Make or Break Your Data Science Workflow]]></title>
            <link>https://medium.com/data-science/why-variable-scoping-can-make-or-break-your-data-science-workflow-5b449291ac73?source=rss-c49929a81b40------2</link>
            <guid isPermaLink="false">https://medium.com/p/5b449291ac73</guid>
            <category><![CDATA[clean-code]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[tips-and-tricks]]></category>
            <category><![CDATA[variables]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[clara]]></dc:creator>
            <pubDate>Mon, 06 Jan 2025 13:02:38 GMT</pubDate>
            <atom:updated>2025-01-06T13:02:38.231Z</atom:updated>
            <content:encoded><![CDATA[<h4>Let’s kick off 2025 by writing some clean code together</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wl78TOCMmjHevCD-VzJ1ow.jpeg" /><figcaption>Image by <a href="https://unsplash.com/@getswello">Swello</a> from <a href="https://unsplash.com/">Unsplash</a></figcaption></figure><p>When you’re deep in rapid prototyping, it’s tempting to skip clean scoping or reuse common variable names (hello, df!), thinking it will save time. But this can lead to sneaky bugs that break your workflow.</p><p>The good news? Writing clean, well-scoped code does not require additional effort once you understand the basic principles.</p><p>Let’s break it down.</p><h3>What is variable scope?</h3><blockquote>Think of a variable as a container that will store some information. <strong>Scope refers to the region of your code where a variable is accessible.</strong></blockquote><p>Scope prevents accidental changes by limiting where variables can be read or modified. If every variable was accessible from anywhere, you’ll have to keep track of all of them to avoid overwriting it accidentally.</p><p>In Python, scope is defined by the <strong>LEGB rule</strong>, which stands for: <strong>local, enclosing, global and built-in</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/976/1*AFDe2Pu3DSRw1huJnF2eGQ.png" /><figcaption>Scoping in Python, referred as LEGB (by author)</figcaption></figure><h3>Scoping in Python: LEGB rule</h3><p>Let’s illustrate this with an example.</p><pre># Global scope, 7% tax<br>default_tax = 0.07 <br><br>def calculate_invoice(price):<br>    # Enclosing scope<br>    discount = 0.10  <br>    total_after_discount = 0<br><br>    def apply_discount():<br>        nonlocal total_after_discount<br>        <br>        # Local scope<br>        tax = price * default_tax  <br>        total_after_discount = price - (price * discount)<br>        return total_after_discount + tax<br><br>    final_price = apply_discount()<br>    return final_price, total_after_discount<br><br># Built-in scope<br>print(&quot;Invoice total:&quot;, round(calculate_invoice(100)[0], 2)) </pre><h4>1. Local scope</h4><p><strong>Variables inside a function are in the local scope. </strong>They can only be accessed within that function.</p><p>In the example, tax is a local variable inside apply_discount. It is not accessible outside this function.</p><h4>2. Enclosing scope</h4><p>These refer to variables in a function that contains a <strong>nested</strong> function. These variables are not global but can be accessed by the inner (nested) function. In this example, discount and total_after_discount are variables in the enclosing scope of apply_discount .</p><p><strong>The </strong><strong>nonlocal keyword:</strong></p><p>The nonlocal keyword is used to <strong>modify</strong> variables in the enclosing scope, <strong>not just read them</strong>.</p><p>For example, suppose you want to update the variable total_after_discount, which is in the enclosing scope of the function. Without nonlocal, if you assign to total_after_discount inside the inner function, <strong>Python will treat it as a new local variable</strong>, effectively <strong>shadowing</strong> the outer variable. This can introduce bugs and unexpected behavior.</p><h4>3. Global scope</h4><p>Variables that are defined <strong>outside all functions</strong> and accessible throughout.</p><p><strong>The </strong><strong>global statement</strong></p><p>When you declare a variable as global inside a function, Python treats it as a reference to the variable outside the function. This means that changes to it will affect the variable in the global scope.</p><p>With the global keyword, Python will create a new local variable.</p><pre>x = 10  # Global variable<br><br>def modify_global():<br>    global x  # Declare that x refers to the global variable<br>    x = 20    # Modify the global variable<br><br>modify_global()<br>print(x)  # Output: 20. If &quot;global&quot; was not declared, this would read 10</pre><h4>4. Built-in scope</h4><p>Refers to the reserved keywords that Python uses for it’s built-in functions, such as print , def , round and so on. This can be accessed at any level.</p><h3>Key concept: global vs nonlocal keywords</h3><p>Both keywords are crucial for modifying variables in different scopes, but they’re used differently.</p><ul><li><strong>global</strong>: Used to modify variables in the global scope.</li><li><strong>nonlocal</strong>: Used to modify variables in the enclosing (non-global) scope.</li></ul><h3>Variable shadowing</h3><blockquote>Variable shadowing happens when a variable in an inner scope hides a variable from an outer scope.</blockquote><p>Within the inner scope, all references to the variable will point to the inner variable, not the outer one. This can lead to confusion and unexpected outputs if you’re not careful.</p><p>Once execution returns to the outer scope, the inner variable ceases to exist, and any reference to the variable will point back to the outer scope variable.</p><p>Here’s a quick example. x is shadowed in each scope, resulting in different outputs depending on the context.</p><pre>#global scope<br>x = 10<br><br>def outer_function():<br>    #enclosing scope<br>    x = 20  <br><br>    def inner_function():<br>        #local scope<br>        x = 30  <br>        print(x)  # Outputs 30<br><br>    inner_function()<br>    print(x)  # Outputs 20<br><br>outer_function()<br>print(x)  # Outputs 10</pre><h3>Parameter shadowing</h3><blockquote>A similar concept to variable shadowing, but this occurs when a local variable redefines or overwrites a parameter passed to a function.</blockquote><pre>def foo(x):<br>    x = 5  # Shadows the parameter `x`<br>    return x<br><br>foo(10)  # Output: 5</pre><p>x is passed as 10. But it is immediately shadowed and overwritten by x=5</p><h3>Scoping in recursive functions</h3><blockquote>Each recursive call gets <strong>its own execution context</strong>, meaning that the local variables and parameters in that call are independent of previous calls.</blockquote><p>However, if a variable is modified globally or passed down explicitly as a parameter, the change can influence subsequent recursive calls.</p><ul><li><strong>Local variables</strong>: These are defined inside the function and only affect the <strong>current</strong> recursion level. They do not persist between calls.</li><li><strong>Parameters passed explicitly</strong> to the next recursive call retain their values from the previous call, allowing the recursion to accumulate state across levels.</li><li><strong>Global variables</strong>: These are <strong>shared across all recursion levels</strong>. If modified, the change will be visible to all levels of recursion.</li></ul><p>Let’s illustrate this with an example.</p><h4><strong>Example 1: Using a global variable (not recommended)</strong></h4><pre>counter = 0  # Global variable<br><br>def count_up(n):<br>    global counter<br>    if n &gt; 0:<br>        counter += 1<br>        count_up(n - 1)<br><br>count_up(5)<br>print(counter)  # Output: 5</pre><p>counter is a global variable shared across all recursive calls. It gets incremented at each level of recursion, and its final value (5) is printed after the recursion completes.</p><h4><strong>Example 2: Using parameters (recommended)</strong></h4><pre>def count_up(n, counter=0):<br>    if n &gt; 0:<br>        counter += 1<br>        return count_up(n - 1, counter)<br>    return counter<br><br>result = count_up(5)<br>print(result)  # Output: 5</pre><ul><li>counter is now a <strong>parameter</strong> of the function.</li><li>counter is passed from one recursion level to the next, with it’s value updated at each level. The counter is not reinitialised in each call, rather, it’s <strong>current state is passed forward to the next recursion level</strong>.</li><li>The function is now <strong>pure</strong> — there are no side effects and it only operates within it’s own scope.</li><li>As the recursive function returns, the counter “bubbles up” to the top level and is returned at the base case.</li></ul><h3>Best practices</h3><h4>1. Use descriptive variable names</h4><p>Avoid vague names like df or x. Use descriptive names such as customer_sales_df or sales_records_df for clarity.</p><h4>2. Use capital letters for constants</h4><p>This is the standard naming convention for constants in Python. For example, MAX_RETRIES = 5.</p><h4>3. Avoid global variables as much as possible</h4><p>Global variables introduces bugs and makes code harder to test and maintain. It’s best to pass variables explicitly between functions.</p><h4>4. Aim to write pure functions where possible</h4><p><strong>What’s a pure function?</strong></p><ol><li><strong>Deterministic</strong>: It always produces the same output for the same input. It’s not affected by external states or randomness.</li><li><strong>Side-effect-free</strong>: It does not modify any external variables or states. It operates solely within its local scope.</li></ol><p>Using nonlocal or global would make the function impure.</p><p>However, if you’re working with a <strong>closure</strong>, you should use the nonlocal keyword to modify variables in the enclosing (outer) scope, which helps prevent variable shadowing.</p><p>A <strong>closure</strong> occurs when a nested function (inner function) captures and refers to variables from its enclosing function (outer function). This allows the inner function to “remember” the environment in which it was created, including access to variables from the outer function’s scope, even after the outer function has finished executing.</p><p><strong><em>The concept of closures can go really deep, so tell me in the comments if this is something I should dive into in the next article! :)</em></strong></p><h4>5. Avoid variable shadowing and parameter shadowing</h4><p>If you need to refer to an outer variable, avoid reusing its name in an inner scope. Use distinct names to clearly distinguish the variables.</p><h3>Wrapping up</h3><p>That’s a wrap! Thanks for sticking with me till the end.</p><p>Have you encountered any of these challenges in your own work? Drop your thoughts in the comments below!</p><p>I write regularly on Python, software development and the projects I build, so give me a follow to not miss out. See you in the next article :)</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5b449291ac73" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/why-variable-scoping-can-make-or-break-your-data-science-workflow-5b449291ac73">Why Variable Scoping Can Make or Break Your Data Science Workflow</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>