<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lavitra</title>
    <description>The latest articles on DEV Community by Lavitra (@lavitra).</description>
    <link>https://dev.to/lavitra</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3987886%2F974886c2-ac0f-42dd-9f18-ca6dc097ae63.png</url>
      <title>DEV Community: Lavitra</title>
      <link>https://dev.to/lavitra</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lavitra"/>
    <language>en</language>
    <item>
      <title>The Algorithm Behind "Are You Also Experiencing X?"</title>
      <dc:creator>Lavitra</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:11:43 +0000</pubDate>
      <link>https://dev.to/lavitra/the-algorithm-behind-are-you-also-experiencing-x-4l2m</link>
      <guid>https://dev.to/lavitra/the-algorithm-behind-are-you-also-experiencing-x-4l2m</guid>
      <description>&lt;p&gt;A symptom checker that only reads one message and immediately names a diagnosis is not doing anything a doctor would recognize as diagnosis. A real intake asks a follow-up question, one that actually narrows things down, not a random one. Building the Neo4j knowledge graph project, that follow-up question turned out to be the single most interesting piece of logic in the whole app, more interesting than the graph schema itself, because it is a small, greedy algorithm making a real decision under uncertainty, three times in a row, before giving up and committing to an answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the naive versions of this don't&amp;nbsp;work&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The obvious bad version is a fixed checklist, ask about fever, then cough, then fatigue, in that order, every time, regardless of what the patient already said. That wastes questions on symptoms that do not actually distinguish between the diseases still under consideration. A slightly less bad version asks about a random unconfirmed symptom, which at least avoids a fixed order but still has no reason to expect the answer will be useful.&lt;/p&gt;

&lt;p&gt;What actually needs to happen is closer to how a doctor thinks: given everything the patient has said so far, what one additional piece of information would eliminate the most remaining possibilities. That is a real algorithmic question, and answering it requires two things: a ranked list of current candidate diseases, and a way to pick the most informative next question from that ranked list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The actual two-step algorithm&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Step one, rank candidate diseases by how well they match confirmed symptoms so far.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;s:&lt;/span&gt;&lt;span class="n"&gt;Symptom&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:INDICATES&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;d:&lt;/span&gt;&lt;span class="n"&gt;Disease&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;s.name&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;$confirmed&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;match_score&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s.name&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;matched&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;match_score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;d.name&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;disease&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;match_score&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;matched&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This finds every disease connected to at least one confirmed symptom, counts how many confirmed symptoms each one matches, and returns the top five, ranked by match count. Early in a conversation, with only one or two symptoms confirmed, this list is going to be broad, plenty of diseases share a single common symptom like fatigue or headache. That breadth is expected and fine, it is exactly what the next step is meant to narrow down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step two, and this is the actual clever part, find the symptom that appears most often across the current candidates, excluding anything already asked about.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;s:&lt;/span&gt;&lt;span class="n"&gt;Symptom&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:INDICATES&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;d:&lt;/span&gt;&lt;span class="n"&gt;Disease&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;d.name&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;$diseases&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="ow"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;s.name&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;$exclude&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;s.name&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;symptom&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;freq&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;freq&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;symptom&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;$diseases&lt;/code&gt; is the current candidate list from step one. &lt;code&gt;$exclude&lt;/code&gt; is every symptom already confirmed or already ruled out, so the same question never gets asked twice. The query counts, across only the current candidates, how many of them share each remaining symptom, and returns the most common one first. This is a real, if simple, information-theoretic idea: the symptom shared by the most current candidates is the one whose answer, yes or no, is most likely to split the candidate list meaningfully, confirming it strengthens several candidates at once, ruling it out eliminates several at once. A symptom relevant to only one candidate barely moves the needle either way.&lt;/p&gt;

&lt;p&gt;The loop asks, evaluates, and re-ranks, up to three times, and this is enforced explicitly, not left implicit. The actual stopping condition in the application code is direct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;question_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;next_symptom&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;diagnosed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three clarifying questions, or an earlier exit if the algorithm runs out of useful questions to ask, whichever comes first. This cap is a genuine product decision, not a technical limitation, more questions might narrow the candidate list further, but real users lose patience with an interrogation, and three rounds was the balance chosen between accuracy and not being annoying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Answers are not restricted to a fixed yes or no.&lt;/strong&gt; A patient can respond with plain free text instead of tapping a button, and that response gets analyzed separately, first checked for whether it confirms or denies the specific symptom asked about, and second, scanned for any other symptom the patient volunteered that was not explicitly asked for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
The doctor asked the patient if they have the symptom: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;current_symptom&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.
The patient responded: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Analyze the patient's response and extract:
1. Did they confirm having "{current_symptom}"? (true, false, or null if unclear).
2. Did they mention any other symptoms they have, from this allowed list:
{json.dumps(all_symptoms)}
"""
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second part matters more than it might look like. A patient answering "no, but I do have chest pain" should not have that volunteered detail thrown away just because it was not the specific symptom asked about, so any additional confirmed symptoms found in the response get added straight into the confirmed set, feeding directly back into the next round's candidate ranking. There is also a plain keyword-matching fallback for when no LLM API key is configured, checking for words like "yes" or "no" directly, which keeps the app functional in a degraded but still usable way without an API dependency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where this algorithm is honestly&amp;nbsp;limited&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is a greedy heuristic, not a real diagnostic reasoning system, and it is worth being direct about the gap. It optimizes one step at a time, picking whichever single question looks most useful right now, with no lookahead into how the answer might interact with a question two steps later. It also treats every disease-symptom connection as equally weighted, a genuinely rare, highly specific symptom and a common, low-specificity one count identically in the match score, when a real diagnostic process would weight a rare, specific symptom far more heavily, since it carries much more information. None of this makes the algorithm wrong for what it is being asked to do, narrow a candidate list reasonably well in three questions or fewer, it just means calling it "diagnostic reasoning" would be overselling a fairly simple, honest heuristic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this means practically&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A hard question limit is a legitimate product decision, not a compromise to apologize for. Optimizing for "most useful next question" only matters if the user actually sticks around to answer it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ranking a next question by how many current candidates share it is a simple, effective approximation of information gain, without needing the full statistical machinery of a real Bayesian update.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Free-text answers should be scanned for more than the literal yes or no you asked for. Discarding volunteered information because it did not fit the expected answer shape wastes a genuinely useful signal the user already gave you.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Be honest in the actual product about what kind of reasoning this is. A greedy, unweighted heuristic can produce a genuinely useful conversation without needing to be dressed up as more sophisticated than it actually is.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Are you also experiencing X" is not a random or scripted question in this project, it is the output of a two-step query: rank the current candidate diseases by symptom overlap, then find the single remaining symptom shared by the most of them, repeated up to three times, with every answer, confirmed, denied, or volunteered, feeding back into the next round. It is a small algorithm, honestly closer to a smart checklist than real medical reasoning, but it is the specific piece of this project that actually behaves like it is thinking, rather than just retrieving.&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>database</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Three.js Explained: What You're Actually Building With Scene, Camera, and Mesh</title>
      <dc:creator>Lavitra</dc:creator>
      <pubDate>Thu, 20 Aug 2026 10:22:36 +0000</pubDate>
      <link>https://dev.to/lavitra/threejs-explained-what-youre-actually-building-with-scene-camera-and-mesh-2elp</link>
      <guid>https://dev.to/lavitra/threejs-explained-what-youre-actually-building-with-scene-camera-and-mesh-2elp</guid>
      <description>&lt;p&gt;Every Three.js scene, no matter how impressive the final result looks, is built from the same four pieces: a scene to hold things, a camera to look at them from somewhere, a renderer to actually draw the picture, and one or more meshes, the objects themselves. Nothing about a spinning 3D product viewer or a particle-filled hero section changes that foundation, it just adds more of these same four ingredients and animates them over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What problem Three.js actually solves
&lt;/h2&gt;

&lt;p&gt;The browser can talk to a GPU directly through low-level graphics APIs, historically WebGL, more recently WebGPU. Both are genuinely low-level, closer to how a graphics driver thinks than how a developer thinks, full of buffers, shaders, and matrix math that has nothing to do with the actual 3D object you are trying to put on screen. Three.js exists to sit on top of that low-level API and give you a much higher-level vocabulary, scenes, cameras, meshes, lights, instead of raw GPU buffers, so building a 3D scene feels closer to assembling objects than programming a graphics pipeline by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four pieces, and what changed underneath them
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Scene, camera, renderer, mesh, the minimum you actually need.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;three&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scene&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Scene&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;camera&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PerspectiveCamera&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerWidth&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;renderer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WebGLRenderer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHeight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domElement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;geometry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BoxGeometry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;material&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MeshStandardMaterial&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0x2266ff&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cube&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Mesh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;geometry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;material&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cube&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;light&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DirectionalLight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xffffff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;light&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;light&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each piece has exactly one job. The &lt;code&gt;Scene&lt;/code&gt; is a container, nothing renders unless it has been added here. The &lt;code&gt;PerspectiveCamera&lt;/code&gt; defines a viewpoint, its field of view, aspect ratio, and near and far clipping distances, essentially how far away things can be before they stop being drawn at all. A &lt;code&gt;Mesh&lt;/code&gt; is not one thing, it is always two things combined, a &lt;code&gt;Geometry&lt;/code&gt;, the actual shape, and a &lt;code&gt;Material&lt;/code&gt;, how that shape's surface responds to light and color. A box with no light in the scene will render as a flat, unlit silhouette, because a standard material needs a light source to actually shade its surfaces, which is why the &lt;code&gt;DirectionalLight&lt;/code&gt; in that example is not optional decoration, it is what makes the cube look three-dimensional instead of a flat colored square.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The renderer is the piece that changed the most, and 2026 is specifically when that change became real.&lt;/strong&gt; For most of Three.js's history, &lt;code&gt;WebGLRenderer&lt;/code&gt; was the only serious option, translating your scene into WebGL draw calls. As of Three.js r171, released in late 2025, &lt;code&gt;WebGPURenderer&lt;/code&gt; became a genuinely production-ready alternative, not an experimental side project. The part worth knowing is what did not change, the Scene, Camera, and Mesh API stays identical either way, swapping renderers is close to a one-line change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;three/webgpu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;renderer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WebGPURenderer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;antialias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;WebGPURenderer&lt;/code&gt; automatically falls back to a WebGL 2 backend if the user's browser does not support WebGPU, so choosing it does not mean abandoning older devices, it means opportunistically getting WebGPU's benefits, lower CPU overhead and access to compute shaders for things like large particle systems, when the browser can provide them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shaders got a real upgrade too, through something called TSL.&lt;/strong&gt; Historically, custom shader code in Three.js meant writing raw GLSL, WebGL's shading language, which simply does not run on a WebGPU backend, which speaks WGSL instead. TSL, the Three.js Shading Language, lets you write shader logic once in a JavaScript-like syntax that gets transpiled automatically to either GLSL or WGSL depending on which renderer actually ends up running. This matters concretely for anyone who has hand-written shaders before, it removes the need to maintain two separate shader codebases just to support both rendering backends.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest caveat worth having before reaching for WebGPU
&lt;/h2&gt;

&lt;p&gt;It would be misleading to present this as a strict upgrade with no tradeoffs, so it is worth being direct about the real state of things in 2026. Browser support, while much better than a couple of years ago, is still not universal, several major browser and platform combinations, including Firefox on Linux and on Android, did not have stable WebGPU support as of mid-2026, which is exactly why the automatic WebGL fallback is not a minor detail, it is the thing keeping those users from silently getting a broken scene.&lt;/p&gt;

&lt;p&gt;It is also not automatically faster. Independent benchmarks on scenes with a large number of separate, non-instanced meshes have found &lt;code&gt;WebGLRenderer&lt;/code&gt; outperforming &lt;code&gt;WebGPURenderer&lt;/code&gt; in specific cases, and Three.js's own maintainers are explicit that simply swapping the renderer class, with no other changes, should not be expected to produce a free performance win. WebGPU's real advantages, lower CPU overhead, native compute shader access, better handling of very high draw call counts, show up specifically in compute-heavy or draw-call-heavy scenes, not universally. For an existing, non-trivial scene, migrating fully, including any custom postprocessing, which does not carry over automatically and needs rebuilding on the newer stack, has reportedly taken production teams a few months, not an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means practically
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Every Three.js scene reduces to the same four building blocks, regardless of how visually complex the final result is: a scene to hold objects, a camera defining a viewpoint, a renderer that draws the picture, and meshes made of geometry plus material.&lt;/li&gt;
&lt;li&gt;A mesh with a standard material needs a light in the scene to actually look three-dimensional. An unlit scene is a common first mistake, not a bug.&lt;/li&gt;
&lt;li&gt;For a new project today, starting with &lt;code&gt;WebGPURenderer&lt;/code&gt; is low risk, given the automatic WebGL fallback, but do not assume it hands you a performance improvement without profiling your specific scene.&lt;/li&gt;
&lt;li&gt;For an existing production scene, only invest in a full WebGPU migration if you are actually hitting a specific performance ceiling WebGL cannot solve, compute-heavy workloads or very high draw call counts, rather than migrating for its own sake.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Three.js's real contribution has never been the renderer choice, it is the higher-level vocabulary, scene, camera, mesh, light, that stays consistent no matter which low-level graphics API is doing the actual drawing underneath. The WebGL-to-WebGPU shift happening through 2025 and 2026 is a genuinely significant change to what is possible, particularly for compute-heavy scenes, but it is an addition to that vocabulary, not a replacement for understanding it, and treating a renderer swap as an automatic win, rather than something to profile and decide on deliberately, is the mistake worth avoiding.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Agentic RAG: What Happens When Retrieval Becomes a Decision Instead of a Step</title>
      <dc:creator>Lavitra</dc:creator>
      <pubDate>Thu, 20 Aug 2026 04:46:09 +0000</pubDate>
      <link>https://dev.to/lavitra/agentic-rag-what-happens-when-retrieval-becomes-a-decision-instead-of-a-step-3okm</link>
      <guid>https://dev.to/lavitra/agentic-rag-what-happens-when-retrieval-becomes-a-decision-instead-of-a-step-3okm</guid>
      <description>&lt;p&gt;An earlier post on RAG described it as handing a model the right reference right before it answers. That description was accurate, and it was also describing the simplest possible version of the idea: retrieve once, generate once, done. Agentic RAG is what you get when retrieval stops being a fixed step in a pipeline and becomes something an agent decides to do, evaluates, and redoes if the first attempt was not good enough. It is the same retrieval concept from before, placed inside the same think, act, observe loop covered in the agent posts, with the "act" step specifically being a search.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the fixed pipeline actually falls short
&lt;/h2&gt;

&lt;p&gt;Traditional RAG runs the same three steps for every query, regardless of what that query actually needs: embed the question, retrieve the closest matching chunks, hand them to the model to generate an answer. This works well for direct lookup questions, where the answer genuinely lives in one or two retrieved passages. It has no good answer for a question that needs comparing numbers from two different documents, or one where the first retrieval attempt returns irrelevant chunks because the query was phrased ambiguously. A fixed pipeline has no mechanism to notice that its own retrieval was weak, it just generates an answer from whatever came back, confidently, regardless of whether that evidence was actually sufficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrieval as a loop, not a step
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The core change is a control loop wrapped around retrieval.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Instead of retrieve once and generate, an agentic RAG system retrieves, reasons about whether what it got back is actually enough to answer the question, and either proceeds to generate or goes back and tries again, a different query, a different source, a different tool entirely. This is the exact same loop shape from the earlier agent posts, applied specifically to the retrieval step. Where a fixed pipeline treats retrieval as a single deterministic action, agentic RAG treats it as one possible action the agent can choose to take, evaluate, and repeat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A few named patterns are worth knowing, because they show up constantly in how this actually gets built.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Corrective RAG explicitly scores the relevance of retrieved chunks before generation, and if that score is low, triggers a fallback, often a web search, rather than feeding weak evidence to the model anyway. Self-RAG has the model reflect on its own retrieved context and its own draft answer, checking whether the response is actually grounded in what was retrieved before finalizing it. Adaptive and multi-hop patterns handle questions that genuinely require several rounds of retrieval, where each round's results inform what to search for next, closer to research than lookup. None of these are exotic, they are all specific implementations of the same underlying idea: do not trust the first retrieval blindly, build in a checkpoint that can catch a bad one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is also exactly where a framework like LangGraph earns its place.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;A fixed RAG pipeline fits naturally into a linear chain, which is why plain LCEL-style composition works fine for it. The moment retrieval needs to loop back on itself, retrieve, evaluate, maybe retrieve again, that is precisely the shape a linear chain cannot express and a graph with a conditional edge can. An agent node evaluates retrieved chunks, a conditional edge routes either forward to generation or back to a retrieval node with a reformulated query, and that backward edge is the loop that makes the whole pattern possible, the same mechanism covered in the LangGraph post, just applied to a retrieval-specific agent instead of a general tool-calling one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;None of this is free, and pretending otherwise would be dishonest.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Adding an evaluation and retry loop around retrieval genuinely increases both cost and latency, current estimates put agentic RAG in the range of three to ten times the token usage and two to five times the latency of a single-pass retrieval pipeline, because you are now potentially running multiple retrieval rounds and extra reasoning steps the simple version never needed. That is not a rounding error, it is a real operational tradeoff, and it only makes sense to pay for use cases where the fixed pipeline's blind spot actually matters, ambiguous or multi-part questions, high-stakes domains where a wrong answer built on weak evidence is genuinely costly, not for straightforward lookup questions where one good retrieval pass was always going to be enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means practically
&lt;/h2&gt;

&lt;p&gt;⁘ Agentic RAG is not a strictly better version of RAG, it is RAG with a loop wrapped around the retrieval step, and that loop costs real tokens and real latency every time it runs.&lt;/p&gt;

&lt;p&gt;⁘ The actual mechanism worth remembering is evaluation before generation, checking whether retrieved evidence is sufficient rather than assuming it is, and only that checkpoint is what justifies the added cost.&lt;/p&gt;

&lt;p&gt;⁘ If most of your queries are direct lookups with a clear answer in one or two documents, a fixed pipeline is not a lesser choice, it is the correctly scoped one for that problem.&lt;/p&gt;

&lt;p&gt;⁘ Reach for agentic RAG specifically when queries are multi-part, ambiguous, or high-stakes enough that a confidently wrong answer built on a bad first retrieval is a real cost worth paying extra latency and tokens to avoid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Agentic RAG is not a separate technology from RAG or from agents, it is the intersection of both ideas already covered separately: the retrieval mechanism from the RAG post, placed inside the loop structure from the agent posts, with a graph like LangGraph providing the conditional edge that lets a weak retrieval trigger a retry instead of getting passed straight to generation. The value is real for the specific class of question a fixed pipeline cannot handle well. The cost is also real, and skipping the honest tradeoff is how a genuinely useful pattern turns into something reached for by default when a simpler pipeline would have done the job for a fraction of the price.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>rag</category>
    </item>
    <item>
      <title>LangGraph Explained: Giving an LLM Pipeline the Ability to Loop Back on Itself</title>
      <dc:creator>Lavitra</dc:creator>
      <pubDate>Tue, 18 Aug 2026 07:11:11 +0000</pubDate>
      <link>https://dev.to/lavitra/langgraph-explained-giving-an-llm-pipeline-the-ability-to-loop-back-on-itself-14gc</link>
      <guid>https://dev.to/lavitra/langgraph-explained-giving-an-llm-pipeline-the-ability-to-loop-back-on-itself-14gc</guid>
      <description>&lt;p&gt;Part one of this series ended on a specific boundary:LangChain's chain model, even with LCEL's clean pipe syntax, is built for pipelines that flow forward, not ones that loop, branch based on a runtime decision, or need to pause and resume days later. That boundary is exactly where LangGraph starts, and it is worth being precise about what actually changes, because the difference is not "LangGraph is a more powerful LangChain," it is a genuinely different way of modeling a workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From a chain to a&amp;nbsp;graph&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A chain, as LCEL expresses it, is a straight line, step one feeds step two feeds step three. LangGraph replaces that shape entirely with a directed graph: named nodes, each one a function that does some work, connected by edges that say which node runs next. The difference that actually matters is that edges do not have to move only forward. A node can have an edge pointing back to an earlier node, which is what makes a real loop possible, and an edge can be conditional, decided at runtime based on the current state, rather than fixed at the moment the pipeline was written.&lt;/p&gt;

&lt;p&gt;That last part, an explicit, shared state object that every node reads from and writes back to, is the other core difference. A chain passes output from one step directly into the next. A graph maintains one persistent state across the entire run, and every node's job is to read what it needs from that state and return an update to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nodes, edges, and where the loop actually&amp;nbsp;lives&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The basic building blocks, stripped down. A LangGraph graph starts with a state definition, a node or two, and edges connecting them to the built-in &lt;code&gt;START&lt;/code&gt; and &lt;code&gt;END&lt;/code&gt; markers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing_extensions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TypedDict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.graph&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;START&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;call_api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;START&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;call_api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;call_api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This alone is not more powerful than a simple LCEL chain, it is a single node, run once. The actual value shows up once you add a second kind of edge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conditional edges are where branching enters the picture.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Instead of a fixed edge, &lt;code&gt;add_conditional_edges&lt;/code&gt; connects a node to a routing function that inspects the current state and decides, at runtime, which node should run next:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route_by_category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_conditional_edges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classify&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;route_by_category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing_support&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing_support&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;technical_support&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;technical_support&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;general_support&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;general_support&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The graph does not know in advance which support node will run, that decision genuinely depends on what the model produced in the &lt;code&gt;classify&lt;/code&gt; step and gets resolved fresh on every run. This is the kind of branching a linear chain has no clean way to express, because a chain's structure is fixed at write time, not decided at run time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The loop itself is just an edge pointing backward.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the part that actually solves the problem part one ended on. A tool-calling agent, the classic think, act, observe pattern, is expressed as a conditional edge from the agent node that either goes to a tool node or exits, combined with a normal edge sending the tool node's result straight back to the agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_agent_model_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;START&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_conditional_edges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;route_agent_graph_edge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trace the actual path: &lt;code&gt;email_agent&lt;/code&gt; decides whether a tool call is needed. If yes, execution moves to &lt;code&gt;email_tools&lt;/code&gt;, which runs the tool and then unconditionally routes straight back to &lt;code&gt;email_agent&lt;/code&gt;. That last edge is the loop, drawn explicitly as a graph structure instead of hidden inside a &lt;code&gt;while&lt;/code&gt; statement somewhere in application code. The agent keeps deciding, acting, and observing until its own routing function finally returns &lt;code&gt;END&lt;/code&gt; instead of the tool node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State and persistence turn a single run into something that can pause and resume.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Attaching a checkpointer to a compiled graph saves its state after every step, tied to a &lt;code&gt;thread_id&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.checkpoint.memory&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;InMemorySaver&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;checkpointer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InMemorySaver&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpointer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;checkpointer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;configurable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thread_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what makes a graph resumable across separate calls instead of only existing for the duration of one Python process. It is also what makes a real human-in-the-loop pattern possible: a node can call &lt;code&gt;interrupt()&lt;/code&gt;, which pauses graph execution entirely and waits, potentially for a long time, for an external value before continuing exactly where it left off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;human_review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;interrupt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Do you approve?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A chain has no equivalent to this. Once an LCEL chain starts running, it runs to completion or it fails, there is no built-in notion of pausing mid-pipeline and picking back up later with new information.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this is a different tool, not a strictly better&amp;nbsp;one&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everything LangGraph adds, explicit state, conditional routing, cycles, checkpointed persistence, comes with more to design and more to reason about compared to a linear chain. A straightforward RAG pipeline gains nothing from being modeled as a graph, it has no branches, no loops, and no need to pause mid-run, so wrapping it in &lt;code&gt;StateGraph&lt;/code&gt; just adds ceremony around something LCEL already expresses more simply. LangGraph earns its complexity specifically when a workflow has a genuine loop, a genuine runtime branch, or a genuine need to persist and resume, not by default for anything agent-shaped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this means practically&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;⁘ Model a workflow as a graph the moment it needs to loop, branch based on a runtime decision, or survive being paused and resumed, not before.&lt;/p&gt;

&lt;p&gt;⁘ A conditional edge pointing back to an earlier node is the actual mechanism behind a tool-calling agent loop, worth recognizing directly in the graph structure rather than treating agent behavior as a black box.&lt;/p&gt;

&lt;p&gt;⁘ A checkpointer and &lt;code&gt;thread_id&lt;/code&gt; are what separate a single in-memory run from a workflow that can genuinely pause for human input and resume later, which matters for anything that cannot or should not run start to finish unattended.&lt;/p&gt;

&lt;p&gt;⁘ Reaching for LangGraph on a workflow that is genuinely linear adds structure without adding capability. The graph model earns its cost specifically on the class of problem chains cannot express.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The gap identified at the end of part one, loops, runtime branching, and persistent state, is exactly what LangGraph exists to close, by replacing a fixed, forward-only chain with an explicit graph where edges can point backward and state persists across steps and even across sessions. Part one's chain and this post's graph are not competing answers to the same question, they are the right tool for two different shapes of problem, and knowing which shape you actually have is most of the decision.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
