<?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: Valipireddy Kowshik</title>
    <description>The latest articles on DEV Community by Valipireddy Kowshik (@k0wsh1k_0x).</description>
    <link>https://dev.to/k0wsh1k_0x</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%2F4125336%2F8808a527-e17c-475f-9cd0-d996d7e92fe7.jpg</url>
      <title>DEV Community: Valipireddy Kowshik</title>
      <link>https://dev.to/k0wsh1k_0x</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/k0wsh1k_0x"/>
    <language>en</language>
    <item>
      <title>Why I Ditched "Just Let the LLM Handle It" for a State Machine (And Slept Better at Night)</title>
      <dc:creator>Valipireddy Kowshik</dc:creator>
      <pubDate>Tue, 15 Sep 2026 02:56:27 +0000</pubDate>
      <link>https://dev.to/k0wsh1k_0x/why-i-ditched-just-let-the-llm-handle-it-for-a-state-machine-and-slept-better-at-night-4i1p</link>
      <guid>https://dev.to/k0wsh1k_0x/why-i-ditched-just-let-the-llm-handle-it-for-a-state-machine-and-slept-better-at-night-4i1p</guid>
      <description>&lt;p&gt;When I started building my AI technical interviewer, I did what most people do: I threw a big system prompt at the LLM and told it to "act like an interviewer, ask coding questions, give hints when the candidate is stuck, and score them at the end."&lt;/p&gt;

&lt;p&gt;It worked... about 80% of the time.&lt;/p&gt;

&lt;p&gt;The other 20% is what this post is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with trusting an LLM to run your whole flow
&lt;/h2&gt;

&lt;p&gt;Here's what kept happening. Mid-interview, the model would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Randomly decide the interview was "done" after one question&lt;/li&gt;
&lt;li&gt;Give away the answer while trying to give a "gentle hint"&lt;/li&gt;
&lt;li&gt;Forget it already asked a question and ask it again&lt;/li&gt;
&lt;li&gt;Jump straight to scoring before the candidate even submitted code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this was a prompting skill issue. I rewrote that system prompt probably fifteen times. The real issue is structural: an LLM generating the &lt;em&gt;next thing to say&lt;/em&gt; has no actual memory of "what phase are we in," unless you spoon-feed it that context perfectly, every single turn, forever. And even then, it can just... decide to do something else. It's a language model, not a state tracker. Treating it like one is where things fall apart.&lt;/p&gt;

&lt;p&gt;For a casual chatbot, that's fine — mild chaos is charming. For a product where someone's actual hiring decision depends on the interview going through every stage correctly, "mild chaos" is a support ticket and an angry candidate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did instead
&lt;/h2&gt;

&lt;p&gt;I pulled the &lt;em&gt;structure&lt;/em&gt; of the interview out of the LLM entirely and put it into a plain old finite state machine.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SETUP → GREETING → QUESTION_ASKED → CANDIDATE_CODING →
HINT_CHECK → EVALUATING → SCORING → DONE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The state machine — not the model — decides what phase we're in and what's allowed to happen next. The LLM only gets called &lt;em&gt;inside&lt;/em&gt; a state, to do the one thing that state needs: generate a question, generate a hint, or generate a scorecard. It never gets to decide "we're done now" or "let's skip to scoring." That's not its job anymore.&lt;/p&gt;

&lt;p&gt;Practically, this meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The LLM can't hallucinate a phase transition because it doesn't control phase transitions&lt;/li&gt;
&lt;li&gt;Hints only trigger off a real signal — 35+ seconds of no keystroke activity — not the model deciding "the candidate seems stuck"&lt;/li&gt;
&lt;li&gt;Scoring only fires after code is actually submitted and executed in the sandbox, not whenever the model feels like wrapping up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The LLM still does all the hard, "actually intelligent" work — writing a good question, phrasing a helpful hint, writing a fair evaluation. It's just not allowed to drive the car anymore. It's a really good passenger with really good opinions.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simplified look at the transition logic
&lt;/h2&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;InterviewState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;GREETING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;greeting&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;QUESTION_ASKED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question_asked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;CANDIDATE_CODING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;candidate_coding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;HINT_CHECK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hint_check&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;EVALUATING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evaluating&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;SCORING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scoring&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;DONE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;done&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# The FSM owns "what happens next" —
&lt;/span&gt;    &lt;span class="c1"&gt;# the LLM only fills in content within a state
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;InterviewState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CANDIDATE_CODING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;idle_35s&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="n"&gt;InterviewState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HINT_CHECK&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code_submitted&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="n"&gt;InterviewState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EVALUATING&lt;/span&gt;
    &lt;span class="c1"&gt;# ...deterministic, testable, no hallucinated jumps
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that to letting the model implicitly track state through conversation history alone — there's no guarantee, no test coverage, and no way to catch a bad transition before it reaches the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that surprised me
&lt;/h2&gt;

&lt;p&gt;I expected this to make the product feel more robotic. It did the opposite.&lt;/p&gt;

&lt;p&gt;Because the &lt;em&gt;structure&lt;/em&gt; is now guaranteed, I could actually let the LLM be more creative and natural within each state, without worrying about it going off the rails. Constraining the skeleton let me loosen up the muscle. Counterintuitive, but it checks out — a lot of "unpredictable AI" complaints aren't really about the model being too creative, they're about the model having too much control over things it was never designed to control.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If you're building something where an LLM is orchestrating a multi-step process — not just answering one-off questions — ask yourself: does the model actually need to decide &lt;em&gt;what happens next&lt;/em&gt;, or does it just need to generate good content &lt;em&gt;within&lt;/em&gt; a step someone else decides?&lt;/p&gt;

&lt;p&gt;Most of the time, in my experience, it's the second one. And the moment I stopped asking the LLM to be both the actor and the director, my "why did the interview just end after one question" bugs basically disappeared overnight.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about the messier, more practical side of building AI products — the stuff that doesn't make it into the demo. If you're curious, I built this exact system as a live product: &lt;a href="https://ai-interviewer-ten-delta.vercel.app/" rel="noopener noreferrer"&gt;AI Technical Interviewer&lt;/a&gt;. Happy to talk through the architecture more in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
