<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Artificial Intelligence on Jahvon Dockery</title>
    <link>https://jahvon.dev/tags/artificial-intelligence/</link>
    <description>Recent content in Artificial Intelligence on Jahvon Dockery</description>
    <image>
      <title>Jahvon Dockery</title>
      <url>https://jahvon.dev/images/og-default.png</url>
      <link>https://jahvon.dev/images/og-default.png</link>
    </image>
    <generator>Hugo -- 0.153.4</generator>
    <language>en-us</language>
    <lastBuildDate>Tue, 01 Sep 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://jahvon.dev/tags/artificial-intelligence/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Nowhere to Put the Canary: Argo Rollouts &#43; vLLM</title>
      <link>https://jahvon.dev/notes/rollout-vllm/</link>
      <pubDate>Tue, 01 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/notes/rollout-vllm/</guid>
      <description>Running vLLM on a single GPU with room for two pods, and what that does to progressive delivery when there&amp;rsquo;s no capacity to spare for a canary.</description>
      <content:encoded><![CDATA[<p>A few years ago I was tasked with bringing <a href="https://argoproj.github.io/rollouts/">Argo Rollouts</a> to CarGurus. The hardest part wasn&rsquo;t the mechanics of canary analysis. It was helping teams figure out which metrics were worth gating on and defining global gates to apply across the organization. Spinning up an extra pod to shift traffic onto was <em>free</em>, or close enough to it that nobody thought about it. That assumption doesn&rsquo;t survive contact with a GPU. When I started running vLLM on a single VM, I had room for exactly two pods, and both were already serving live traffic.</p>
<p>Capacity wasn&rsquo;t the only assumption that broke. A vLLM pod can take minutes to start serving, and live traffic doesn&rsquo;t drain off as quickly as you would see with microservices. Every instinct I built came from workloads that start fast and drain fast, so this sounded like an interesting space to explore.</p>
<p>I decided to forget about surging canary rollouts with weighted traffic and focus solely on using Argo Rollouts for analysis across a couple of different deployment scenarios. Argo still calls the new revision the canary even with no traffic split, so that&rsquo;s the word I use for it throughout. This setup had the added benefit of controlling my GPU costs instead of wrestling with how to minimize waste as I temporarily spun up canaries. I decided to swap out a live pod for analysis, which I hypothesized would make this more of a networking and rollout configuration problem than a resource problem.</p>
<p>I want to give a disclaimer up front that this is a new area for me. These notes are my rough understanding of this process and my journey to learning and experimenting within this space. I&rsquo;m not exploring this through a production setup angle and I&rsquo;ll be explicit about the lines that I drew along my journey.</p>
<h2 id="the-architecture">The Architecture</h2>
<p>For this project, running a Kubernetes cluster with a GPU node was a clear starting place. To simplify the setup, I decided to keep it as a single node that I can spin up and down as needed. In my homelab, I use <a href="https://k3s.io/">k3s</a> as my Kubernetes distribution and since I didn&rsquo;t need all of the features that come with a managed cluster, I decided to spin up a Google Cloud Platform VM with k3s installed as my foundation.</p>
<p>I had separately landed on using vLLM as my inference engine so GPU compute was the next clear requirement. It was then pretty clear that I needed to run an accelerator-optimized VM, and I landed on the G2 series. The next big constraint that drove a lot of my architecture design was minimizing costs. I didn&rsquo;t want to be surprised by my cloud spend so keeping my experimentation cheap was important. That meant that I needed to use a small model that would fit on a small machine. This wasn&rsquo;t a big deal for me because the model wasn&rsquo;t what I was testing.</p>
<p>Given the nature of my experiment, I knew I needed at least 2 inference workers so that a rollout wouldn&rsquo;t kill traffic entirely. I eventually realized that I landed on a machine that didn&rsquo;t have native support for partitioning (MIG) so time-slicing had to be my path for sharing GPU resources. I configured the device plugin on my <code>g2-standard-8</code> instance to advertise 4 slices and assumed that meant 4 replicas. That was incorrect - more on that later.</p>
<p>Here&rsquo;s a summary of all of the components I deployed to my cluster:</p>
<p><img src="https://jahvon.dev/images/vllm-setup_hu_3081ff756f64ac9.png" srcset="https://jahvon.dev/images/vllm-setup_hu_581644ab2a9ce8c3.png 700w, https://jahvon.dev/images/vllm-setup_hu_3081ff756f64ac9.png 1400w" sizes="(min-width: 768px) 720px, 100vw" data-zoom-src="https://jahvon.dev/images/vllm-setup.bbd4b9f2b3226ae50cb2295fe1b092dd7934ac5d700ea912f93638916ccad024.png" width="1400" height="814"
     alt="Cluster Architecture"
     loading="lazy" decoding="async">
</p>
<h3 id="inference-workload">Inference Workload</h3>
<p><strong>vLLM serving <a href="https://huggingface.co/Qwen/Qwen3-0.6B">Qwen3-0.6B</a></strong></p>
<p>Deployed as a <a href="https://argo-rollouts.readthedocs.io/en/stable/features/specification/">Rollout</a> without traffic routing features. With no <code>trafficRouting</code> block, Argo has no connection to my networking layer. The weight instead sets the replication ratio (how many pods run the new revision). With <code>maxSurge=0</code> that ratio is satisfied by converting an existing pod rather than adding one.</p>
<p>The Qwen model was small enough to fit without causing OOM errors for replicas sharing resources. I had to install the <a href="https://github.com/nvidia/k8s-device-plugin">NVIDIA device plugin</a> so that the node would advertise the GPU as a schedulable resource. Nothing can request one without it.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">maxSurge</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">maxUnavailable</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">setWeight</span><span class="p">:</span><span class="w"> </span><span class="m">50</span><span class="w">
</span></span></span></code></pre></div><h3 id="networking">Networking</h3>
<p><strong><a href="https://www.envoyproxy.io/">Envoy Proxy</a> Deployment with a static config sitting in front of headless vLLM services</strong></p>
<p>I needed a way to talk to the inference engine. Based on my earlier hypothesis, I went in expecting to have to tune this a bit. I could have used <a href="https://github.com/vllm-project/router">vLLM router</a>, <a href="https://github.com/llm-d/llm-d-router">llm-d router</a>, or a standard k8s controller that I&rsquo;ve used in the past (like nginx ingress or an API gateway) but I didn&rsquo;t want something that would hide too much, too early.</p>
<p>My Envoy config uses the <code>LEAST_REQUEST</code> load balancing policy, which scores endpoints on in-flight request count and knows nothing about what&rsquo;s happening inside vLLM. A cache-aware router would have been the better production choice, which is exactly why I didn&rsquo;t use one. One of the things I wanted to see was what a naive policy does to a pod that just came up cold. In some ways I&rsquo;ll describe later, this decision caused some setup headaches but provided a lot of great learnings.</p>
<h3 id="observability">Observability</h3>
<p><strong>Prometheus &amp; Grafana</strong></p>
<p>Easiest decision I made since vLLM and Envoy metrics could be easily scraped by Prometheus. I also deployed the <a href="https://github.com/nvidia/dcgm-exporter">dcgm-exporter</a> so that I could see GPU device metrics.</p>
<h2 id="orchestration--tools">Orchestration &amp; Tools</h2>
<p>I also needed a way to interact with the Kubernetes control plane and the inference workloads. Since I was deploying into GCP I considered <a href="https://docs.cloud.google.com/iap/docs/concepts-overview">Identity-Aware Proxy (IAP)</a> or <a href="https://tailscale.com/">Tailscale</a> (I already use it for my homelab so was familiar with that setup). I decided to take a simpler path: SSH + port-forwarding.</p>
<p>One of my favorite parts about scaffolding this project was how I ended up orchestrating everything. Some of my early design choices when building my <a href="https://jahvon.dev/architecture/flow/">Flow CLI</a> project were influenced by how I would experiment in similar ways in the past and how I wanted a better tool to orchestrate those things. It was a no-brainer for me to call on it here.</p>
<p>Under the hood, I used a lot of standard tools like Terraform for the infra, Makefile and shell files for scripting, but Flow wrapped all that up in a nice package that provides some nice ergonomics for working on this project. The full cluster setup is one command (<code>flow provision cluster</code>) and includes preflight checks via the serial runner type. Managing the lifecycle of the VM and workloads is done with easy to remember commands (<code>flow start cluster</code>, <code>flow deploy workloads</code>, <code>flow show status</code>, etc.). Running and viewing the report for experiments is standardized (<code>flow run experiment &lt;X&gt;</code>, <code>flow analyze experiments</code>). All those things are documented and very easy to recall within the Flow CLI terminal UI and the <a href="https://jahvon.dev/architecture/mochi/">Mochi Desktop</a> that I&rsquo;ve been building around Flow.</p>
<p>I also leaned on Claude a lot in this project. I didn&rsquo;t want to have to spend a ton of time looking at Envoy documentation to understand how to configure outlier detection or how to turn on access logs, or NVIDIA documentation to figure out how to set up the vLLM workers so that they&rsquo;re time sharing on the node. Claude also gave me clear answers on the many things that were new to me, like inference benchmarking. It was important for me to set up some clear rules, though. I still wanted to learn and experiment myself so my <code>CLAUDE.md</code> anchored the coding agents to take a slower pace at implementing and to share many more details than they would have without my guidance. This was also a great test of some run provenance features I have been building into Flow &amp; Mochi. I now have a cleaner structure for following along and understanding what&rsquo;s running/ran and which of my agents ran it.</p>
<h2 id="analysis-metrics">Analysis Metrics</h2>
<p>A core aspect of this project was understanding which metrics were best suited for monitoring the state of LLM inference workload deployments. Coming into this project, I honestly didn&rsquo;t know too much about what would be important here. I used a variety of resources, like <a href="https://gradientupdate.substack.com/p/llm-inference-metrics-reference">this post</a>, to ground myself in the common inference metrics like time to first token (TTFT), time per output token (TPOT), goodput, and GPU utilization.</p>
<p>The initial analysis gating metrics that I landed on were the p95 TTFT, p95 TPOT, and the request error ratio. Those didn&rsquo;t give me the full story, though. With the help of Claude, I ended up scaffolding a bunch of different SLIs that I could monitor throughout this project, spanning the whole stack: the inference engine, Envoy, networking, and NVIDIA GPU usage. It was very interesting watching how tuning my traffic within the cluster changed the shape of metrics and how one metric alone didn&rsquo;t give the full story.</p>
<p>To generate enough data for the metrics to be meaningful I needed a traffic simulator. I initially started with <a href="https://docs.vllm.ai/en/stable/cli/bench/serve/">vllm bench serve</a>, which worked pretty well, but I struggled with getting a variety of output shapes over a longer period of time without more complexity. After doing some more research, I discovered the <a href="https://github.com/kubernetes-sigs/inference-perf">inference-perf</a> project. What was very interesting was that switching to this from <code>bench serve</code> showed pretty close to the same benchmark measurements, which I read as a good signal. I have two traffic generation paths:</p>
<ol>
<li>Benchmark runner: used to help me define some of the base metrics that I use within my AnalysisTemplate.</li>
<li>Sustained load runner: used to run continuous and varied load that includes short prompts and long prompts to simulate the batch and interactive user types while running my experiments.</li>
</ol>
<h3 id="where-i-landed">Where I landed</h3>
<p>I originally figured I could just point steady traffic at the cluster and not think too hard about the level, since I wasn&rsquo;t optimizing for capacity or speed. That was wrong. My first baseline sat at about 12% of my TTFT threshold with nothing ever queuing, so a rollout could do almost anything and the gate would still read green. My experiments were passing for the wrong reason.</p>
<p>So I cranked the batch tenant up until it failed, holding interactive (short) prompts steady at 3 req/s with 128 in / 64 out:</p>
<table>
  <thead>
      <tr>
          <th>batch tenant</th>
          <th>req/s</th>
          <th>TTFT p95</th>
          <th>TPOT p95</th>
          <th>output tok/s</th>
          <th>KV cache</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>3 x 512</td>
          <td>6.18</td>
          <td>0.080s</td>
          <td>0.024s</td>
          <td>~1000</td>
          <td>15%</td>
      </tr>
      <tr>
          <td>6 x 512</td>
          <td>6.27</td>
          <td>0.091s</td>
          <td>0.024s</td>
          <td>1011</td>
          <td>13%</td>
      </tr>
      <tr>
          <td><strong>6 x 1024</strong></td>
          <td><strong>5.58</strong></td>
          <td><strong>0.222s</strong></td>
          <td><strong>0.047s</strong></td>
          <td><strong>827</strong></td>
          <td><strong>26%</strong></td>
      </tr>
      <tr>
          <td>9 x 1024</td>
          <td>5.09</td>
          <td>0.222s</td>
          <td>0.047s</td>
          <td>818</td>
          <td>23%</td>
      </tr>
  </tbody>
</table>
<p>My target sat between 6 x 512 and 6 x 1024, where TTFT climbed 2.4x while output tokens per second fell. The 9 x 1024 row confirms it by delivering less for 50% more offered load. Having a real operating point meant I could set thresholds off measurement instead of estimated numbers. I pinned my generator load at 6 req/s &amp; 1024 input.</p>
<h2 id="experiment-harness">Experiment Harness</h2>
<p>Before I jump into talking about my results, I want to give a quick overview of the experiment harness that I had. As I mentioned above, Flow was my orchestrator for a lot of this. The last time I ran similar experiments I ended up using kustomize to apply variations on top of the cluster resources. That worked super well and inspired this setup, but with different tooling. I am using raw Kubernetes manifests and <code>envsubst</code> to drop in environment variables from a resolved envfile within those templates. I decided to go with this approach because the envfile configuration works natively with Flow executables and it also works well as a drop-in to my various scripts. I can define my configurations in one file and have that single source of truth be used across the orchestration stack. I considered Helm here as well, but that also would have required a translation layer from the values file to script / Flow inputs.</p>
<p>When it comes to experiments, I can just override some of the configurations that I had defined within that configuration env and run a script that deploys that change to my workloads. It starts the load generator and triggers the rollouts by incrementing a nonce that I have defined on the Rollout pod specs. This forces the analysis process and pod replacement (even without real workload changes).</p>
<p>At the end of this process, I can either jump right into Grafana to see what metrics report or I can run a Flow executable that will gather all the run data and render a standard markdown template with the results.</p>
<blockquote>
<p><em>You can see my entire repo setup, including my experiments, my Flow executables, my Kubernetes manifests, Terraform scripts, etc., all here: <strong><a href="https://github.com/jahvon/inference-cluster-ops">inference-cluster-ops</a></strong>.</em></p>
</blockquote>
<h2 id="experiments">Experiments</h2>
<p><img src="https://jahvon.dev/images/vllm-dashboard_hu_d0cbf2c34de9630d.png" srcset="https://jahvon.dev/images/vllm-dashboard_hu_ce44db0f39b2eca7.png 700w, https://jahvon.dev/images/vllm-dashboard_hu_d0cbf2c34de9630d.png 1400w" sizes="(min-width: 768px) 720px, 100vw" data-zoom-src="https://jahvon.dev/images/vllm-dashboard.4c81907fb277c800d428460389d7524571509c4915532e316fcbddc3e8689f2a.png" width="1400" height="878"
     alt="Experiments Dashboard"
     loading="lazy" decoding="async">
</p>
<h3 id="0-cluster-baseline">0. Cluster Baseline</h3>
<p>I didn&rsquo;t want to run into a case where I had 100% failures during a rollout so I knew that I needed at least 2 replicas. But I also didn&rsquo;t want my replicas to grow. With <code>maxSurge=0</code> and <code>maxUnavailable=1</code>, a stable pod is terminated to make room for the new revision rather than a new pod being added, so the cluster serves at N-1 for the entire pod-startup window.</p>
<p>This ended up turning from a policy preference to a requirement as I began tuning. I missed that time-slicing splits compute, not memory. The 4 slices I configured meant 4 workloads taking turns on the same device, but each one still needs its own full copy of everything resident in VRAM. Nothing gets shared. Time-slicing also means one bad neighbor can impact the rest, which is its own problem.</p>
<p>That&rsquo;s when I had to get a better sense of what these workloads were actually using. I admittedly still don&rsquo;t fully understand all the various components of the arithmetic behind this, but at a high level I found that I needed to account for the size of the model weights, the KV cache size (which I was able to configure upfront), and some framework-specific costs, like the CUDA context. I started to go a little bit too far into the weeds here. This was where I gave Claude more rein in terms of just running some tests within the cluster and finding the right setup. I landed on just the 2 pods after doing some calculations and after running some load against them and reviewing metrics, including the device metrics.</p>
<p>Based on my configurations, I came up with this math:</p>
<blockquote>
<p>1137 weights + 1679 runtime + 5120 cache = 7936 MiB per pod<br>
Budget: 23034 card − 472 driver = 22562 MiB usable.</p>
</blockquote>
<p>This leaves just <code>6690 MiB</code> free and a third pod needs 7936, so the 4 replicas I originally planned for were out at this size.</p>
<p>Note: only 2816 MiB of that per-pod number is fixed cost. The 5120 MiB of KV cache is what I picked. I wanted to start with a generous number on purpose so that cache pressure wouldn&rsquo;t be the thing shaping my results. Trimming the cache to around 4700 MiB would have fit a third but making that change while establishing a baseline would have also changed my batching behavior.</p>
<h4 id="tuning-analysis--networking">Tuning Analysis &amp; Networking</h4>
<p>While figuring this out, I ran into even more issues. I mistakenly defined a rollout that had an analysis template before validating that the pods would start up. While this slowed me down, it did give me some more useful insights. I was reminded that an analysis run needs traffic to evaluate against. This was a flaw in my Prometheus queries. They came back empty and the analysis run transitioned to an error state instead of passing. I ended up having to tune my template so that the lack of traffic resolved to a success for non-experiment spec changes.</p>
<p>Then I noticed many failed requests, which pointed to my first networking problem. One of the first things I had to do was disable the request timeout since these requests would use streaming and I didn&rsquo;t want to prematurely kill them before they were done. Then I saw that during a rollout, Envoy was still sending requests to the pod that had been destroyed! This, bundled with the load balancing policy that I had set for Envoy, made the whole routing situation a lot worse than I wanted to settle for at baseline.</p>
<p>I ended up having to reduce the DNS refresh interval, add request retries, and configure outlier detection so that requests wouldn&rsquo;t be stuck going to the missing pod for too long. The retries only cover requests that get sent to a pod that is already gone; Envoy doesn&rsquo;t retry once it starts streaming. That distinction turns out to matter a lot in the second experiment. This was the first sign that my hypothesis was right: the hard part here was configuration, not resources.</p>
<p><em>Snippet of the Envoy config on the cluster</em>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">outlier_detection</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">consecutive_5xx</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">base_ejection_time</span><span class="p">:</span><span class="w"> </span><span class="l">10s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">dns_refresh_rate</span><span class="p">:</span><span class="w"> </span><span class="l">2s</span><span class="w">
</span></span></span></code></pre></div><p><em>And on the route</em>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">retry_policy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">retry_on</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;5xx,reset,connect-failure,refused-stream&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">num_retries</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># without this, a retry can land on the same dead pod it failed against</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">host_selection_retry_max_attempts</span><span class="p">:</span><span class="w"> </span><span class="m">3</span><span class="w">
</span></span></span></code></pre></div><p>Finally, I had a good enough base state that allowed me to inject a couple of problem scenarios and see how Argo Rollouts captured and handled them.</p>
<h3 id="1-cold-pod-problem">1. Cold Pod Problem</h3>
<h4 id="what-i-injected">What I injected</h4>
<p>I ran 2 tests:</p>
<ol>
<li>First I updated the vLLM mount path so that, on rollout, the model&rsquo;s weights would have to be downloaded as if this was its first rollout.</li>
<li>Then I artificially increased the startup time for the pod by adding a 180s sleep before starting the vLLM process.</li>
</ol>
<h4 id="what-the-metrics-showed">What the metrics showed</h4>
<table>
  <thead>
      <tr>
          <th>run</th>
          <th>duration</th>
          <th>canary Ready</th>
          <th>gate</th>
          <th>capacity mean</th>
          <th>below full</th>
          <th>incidents</th>
          <th>breached by</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>good-revision</td>
          <td>432s</td>
          <td>138s</td>
          <td>passed</td>
          <td>0.710</td>
          <td>270s</td>
          <td>2</td>
          <td>truncation</td>
      </tr>
      <tr>
          <td>cold-rollout</td>
          <td>445s</td>
          <td>157s</td>
          <td>passed</td>
          <td>0.688</td>
          <td>300s</td>
          <td>2</td>
          <td>truncation</td>
      </tr>
      <tr>
          <td>cold-rollout / slow (180s)</td>
          <td>788s</td>
          <td>323s</td>
          <td>passed</td>
          <td>0.609</td>
          <td>645s</td>
          <td>1</td>
          <td>truncation</td>
      </tr>
  </tbody>
</table>
<ul>
<li>canary ready: how long the new revision took to start serving</li>
<li>capacity ratio: replicas_available / replicas_desired, read from Argo&rsquo;s controller metrics. With 2 replicas it&rsquo;s 1.0 when both pods are serving, 0.5 when one is.</li>
<li>capacity mean: the mean of that ratio across every 15s sample in the run window. 0.688 means that averaged over the whole rollout, 68.8% of desired capacity was actually available.</li>
<li>below full: the amount of time where capacity ratio was under 1</li>
<li>incidents: continuous stretches where the overall health gate failed. That gate is a composite of the TTFT p95, TPOT p95, error ratio and truncation ratio, each measured against its own objective. <code>breached by</code> records the measurement that caused the incident.</li>
</ul>
<table>
  <thead>
      <tr>
          <th>run</th>
          <th>peak TTFT p95</th>
          <th>peak TPOT p95</th>
          <th>peak error ratio</th>
          <th>peak truncation ratio</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>good-revision / stock</td>
          <td>0.244</td>
          <td>0.047</td>
          <td>0.000</td>
          <td><strong>0.0402</strong></td>
      </tr>
      <tr>
          <td>cold-rollout / stock</td>
          <td>0.241</td>
          <td>0.049</td>
          <td>0.000</td>
          <td><strong>0.0397</strong></td>
      </tr>
      <tr>
          <td>cold-rollout / slow (180s)</td>
          <td>0.246</td>
          <td>0.049</td>
          <td>0.000</td>
          <td><strong>0.0310</strong></td>
      </tr>
  </tbody>
</table>
<p>Doubling the startup time barely moved the peak numbers. The damage was the longer stretch of time that the cluster sat below full capacity.</p>
<h4 id="did-the-gate-catch-it">Did the gate catch it</h4>
<p>No, and the reason is my configuration rather than Argo itself. The important metric that I thought I needed to watch here was the time until the canary was ready. Given the small model size, I only saw a 19s difference in my first test, which is what convinced me to try artificially increasing the startup with a sleep.</p>
<p>I was running analysis as a canary step and a step doesn&rsquo;t start until the new pod has been transitioned to the <code>Ready</code> state. The entire startup window happens before the gate is ever evaluated, which is exactly the window I was injecting into. I should have used background analysis instead, analyzing across the whole rollout rather than at a single step.</p>
<p>Moving the gate wouldn&rsquo;t have been enough on its own, though. The metrics I gated on were tuned for canary monitoring. TTFT p95, TPOT p95, and the error ratio all answer the same question: how well is the new pod serving now that it&rsquo;s up? That&rsquo;s the right question at a step and the wrong one across a rollout. The tables above show why. Doubling the startup time barely moved any of those peaks because the slow pod wasn&rsquo;t serving badly, it just wasn&rsquo;t there yet. The damage only showed up in the cluster-shaped measurements: capacity mean fell from 0.710 to 0.609 and the time below full capacity went from 270s to 645s. Those were numbers I collected for the report, not numbers I gated on. Running analysis in the background means gating on the health of the fleet through the transition rather than the performance of a single revision after it lands.</p>
<p>The clear issue that I was able to draw from the data was the batch request truncation across all of the tests I ran.</p>
<h3 id="2-truncated-batch-requests">2. Truncated Batch Requests</h3>
<h4 id="what-i-injected-1">What I injected</h4>
<p>I ended up running several more tests to understand how to fix the truncation issue:</p>
<ol>
<li>Increased the batch output size so that there are always multi-second streams in flight when the pod goes away. This was an attempt to zoom into the truncation issues that I saw with the last experiment.</li>
<li>Then I ran a test with the same batch output size, but an increase in the pod&rsquo;s <code>terminationGracePeriodSeconds</code>.</li>
<li>I further optimized #2 by setting up a naive <code>preStop</code> hook that waits long enough for the requests to drain before killing the pod:</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">lifecycle</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">preStop</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">exec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;/bin/sh&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-c&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;sleep ${PRESTOP_SLEEP_SECONDS}&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">terminationGracePeriodSeconds</span><span class="p">:</span><span class="w"> </span><span class="l">${TERMINATION_GRACE_SECONDS}</span><span class="w">
</span></span></span></code></pre></div><h4 id="what-the-metrics-showed-1">What the metrics showed</h4>
<table>
  <thead>
      <tr>
          <th>grace</th>
          <th>preStop</th>
          <th>duration</th>
          <th>requests</th>
          <th>truncated</th>
          <th>incidents</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>30s (default)</td>
          <td>none</td>
          <td>438s</td>
          <td>1666</td>
          <td>15</td>
          <td>1</td>
      </tr>
      <tr>
          <td>90s</td>
          <td>none</td>
          <td>435s</td>
          <td>1749</td>
          <td>23</td>
          <td>2</td>
      </tr>
      <tr>
          <td>90s</td>
          <td><strong>60s</strong></td>
          <td>433s</td>
          <td>1670</td>
          <td><strong>0</strong></td>
          <td><strong>0</strong></td>
      </tr>
  </tbody>
</table>
<p>The grace period includes the preStop time so both of them needed to be set in the last test. This additional configuration didn&rsquo;t cost me any more time because the teardown runs while the replacement pod is starting up and startup is much longer.</p>
<table>
  <thead>
      <tr>
          <th>grace</th>
          <th>preStop</th>
          <th>stream killed at</th>
          <th>delivered before dying</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>30s</td>
          <td>none</td>
          <td>+27s after drain</td>
          <td>31% of a full response</td>
      </tr>
      <tr>
          <td>90s</td>
          <td>none</td>
          <td>+87s after drain</td>
          <td>38% of a full response</td>
      </tr>
      <tr>
          <td>90s</td>
          <td>60s</td>
          <td>—</td>
          <td>none truncated</td>
      </tr>
  </tbody>
</table>
<h4 id="did-the-fix-work">Did the fix work</h4>
<p>Eventually. At first, I thought the grace period would provide enough time for the requests to drain completely. I saw in the Envoy access logs that requests weren&rsquo;t being sent to the draining pod after I started the rollout process but the grace period configuration didn&rsquo;t solve the truncation issue. It seemed to make it slightly worse. I ran another pair of tests and saw 15 and 22 as my truncation values, which confirmed that this was just due to run variance.</p>
<p>The timings provided a more complete picture. The vLLM workloads stopped producing tokens when they received SIGTERM. This left the remaining streams in a zombie state until they died at 27s with a 30s grace and 87s with a 90s grace - when the pod received SIGKILL at the end of the grace period. Adding the <code>preStop</code> hook was the actual fix. The hook runs before the SIGTERM is sent, so it holds off the signal that stops generation instead of extending the window after it. The grace period still has to be long enough to cover the hook but on its own it was never going to help.</p>
<h3 id="3-aborting-a-bad-revision">3. Aborting a Bad Revision</h3>
<h4 id="what-i-injected-2">What I injected</h4>
<p>My focus here was to see how Argo&rsquo;s automatic rollback prevented long-running incidents. I held the changes from experiment 2 and then did the following test:</p>
<ol>
<li>Collapsed vLLM batching so the pod serves one sequence at a time and everything else queues. This triggered higher latency that I knew would trip the analysis gates</li>
<li>Same test as #1 but without the rollout gating</li>
</ol>
<h4 id="what-the-metrics-showed-2">What the metrics showed</h4>
<table>
  <thead>
      <tr>
          <th>metric</th>
          <th>gated (rolled back)</th>
          <th>ungated</th>
          <th>ratio</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>interactive e2e p95</td>
          <td>2.40s</td>
          <td>30.11s</td>
          <td>12.5x</td>
      </tr>
      <tr>
          <td>batch e2e p95</td>
          <td>8.47s</td>
          <td>31.33s</td>
          <td>3.7x</td>
      </tr>
      <tr>
          <td>degraded window throughput</td>
          <td>1.96 req/s</td>
          <td>0.64 req/s</td>
          <td>3.1x</td>
      </tr>
      <tr>
          <td>rollout phase</td>
          <td>Degraded (auto rollback)</td>
          <td>Healthy (no rollback)</td>
          <td>—</td>
      </tr>
      <tr>
          <td>pods on bad revision</td>
          <td>1 of 2 pods</td>
          <td>2 of 2 pods</td>
          <td>—</td>
      </tr>
  </tbody>
</table>
<h4 id="did-the-gate-catch-it-1">Did the gate catch it</h4>
<p>Yes, as expected. The p95 TTFT on the canary failed on 3/4 samples and aborted the rollout after 260s. On the test without the gate, the same revision was rolled out to all pods, leaving the overall experience in a degraded state until I restored baseline. My gated test didn&rsquo;t prevent the damage. Requests still hit the low performing pod once it was ready. However, it reduced the duration and impact of the bad change automatically.</p>
<h2 id="final-thoughts">Final Thoughts</h2>
<p>One of my biggest takeaways here was that the Envoy routing is what provided the most connection resiliency against rollouts, but still left a gap at the stream level. I came in thinking that using Envoy for networking would allow me to just see rollouts in action, and it did, but the retries and outlier detection absorbed enough of the disruption that the gates had very little left to detect at my baseline. It leaves me wondering how another iteration of this experiment with Argo handling traffic routing alongside the metrics analysis would play out.</p>
<p>I was able to validate the experience of Argo as an instrument during these scenarios; however in some cases it didn&rsquo;t give me as much of a signal as I would have expected. As I reflect on this, I think that has a lot to do with how I configured my load generation and metric thresholds. I had the advantage of knowing exactly what the inputs were and tuning things to get the state into what I hypothesize would happen. It was really cool seeing Argo Rollouts applied to this type of workload and seeing that understanding the system&rsquo;s behavior and core signals is key to using Argo.</p>
<p>If I were to take this exploration a step further, I would bring back the canary. The highest KV usage I saw was at around 70%. That peak didn&rsquo;t come at my saturation point, it came during a drain. The reduced capacity during the rollout added a lot more pressure that could have been mitigated if I could have trimmed the KV size to fit a 3rd temporary pod. That introduces a new challenge, though: what do you do with all that wasted space when you&rsquo;re not running a rollout?</p>
]]></content:encoded>
    </item>
    <item>
      <title>AI as a Development Partner</title>
      <link>https://jahvon.dev/notes/ai-development-partner/</link>
      <pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/notes/ai-development-partner/</guid>
      <description>Reflections on how I&amp;rsquo;ve been using AI as a development partner - what I delegate, what I don&amp;rsquo;t, and what it&amp;rsquo;s produced.</description>
      <content:encoded><![CDATA[<p>Last fall I wrote about <a href="https://jahvon.dev/notes/ai-creative-partner/">using AI as a creative partner</a> after helping with a HGSE module on vibe coding. The conclusion I landed on was careful: AI works best as a scaffold, not a substitute. Use it consciously, review everything, keep your judgment in the loop. I still believe that. But I&rsquo;ve spent the last few months testing what that actually looks like when the output has to live somewhere.</p>
<p>Creative work you experience once. Development work you live in. That difference changes what you need from a partner.</p>
<h2 id="the-bench">The Bench</h2>
<p>When I was at <a href="https://jahvon.dev/tags/recurse/">Recurse Center</a> last summer, I started integrating AI more intentionally into how I build. Not for speed, for learning. I wanted to experiment with architectures I wouldn&rsquo;t normally try, undo decisions cheaply, and see what held up. <a href="https://jahvon.dev/tags/flow/">Flow</a> was the natural workbench. It&rsquo;s my own tool and I know every corner of it.</p>
<p>Over the last few months I&rsquo;ve been building Flow Desktop and refactoring pieces of the core CLI with AI doing a lot of the implementation work. The experience has been different from vibe coding in ways that matter. In the HGSE projects, I was optimizing for something working. Here, I&rsquo;m optimizing for something I can read six weeks later, find when I need it, and build on without second-guessing what&rsquo;s underneath.</p>
<p>That changes what I actually delegate.</p>
<h2 id="the-delegation-model">The Delegation Model</h2>
<p>The architectural decisions stay with me. What the data model looks like, how executables get resolved, where state lives. What I hand off is the implementation of decisions I&rsquo;ve already made. I describe the shape of what I want, review what comes back against that shape, and merge when it aligns. When it doesn&rsquo;t, I say so explicitly.</p>
<p>A concrete example: I&rsquo;ve been building an AI proxy backed by <a href="https://www.cloudflare.com/developer-platform/products/ai-gateway/">Cloudflare AI Gateway</a> that sits across all of my tools. I decided on the architecture, what the proxy needs to do, how it integrates with the <a href="https://jahvon.dev/notes/cloudflare-experience/">Cloudflare platform</a>, what observability I want. AI implemented it. The Cloudflare MCP server made the feedback loop tight enough that I could test and iterate without switching contexts.</p>
<p>What makes this work is having a single place to see everything. Everything I&rsquo;ve configured, discoverable from one surface.</p>
<video class="demo-video"
       autoplay loop muted playsinline preload="metadata"
       aria-label="The flow v2 terminal UI">
  <source src="https://jahvon.dev/images/flow-v2-tui.mp4" type="video/mp4">
</video>

<p>One of the real risks of AI-assisted development is ending up with code you can&rsquo;t navigate. Outputs that don&rsquo;t connect to anything, a project that sprawls in ways you can&rsquo;t audit. The workspace model keeps that from happening. I know where things live because I designed where they live.</p>
<p>I&rsquo;ve also started using AI to enrich Flow itself, generating executable metadata, adding descriptions and tags, making the library more useful as it grows. Flow has an MCP server, so AI tools can interact with it directly. Watching an AI tool work with Flow rather than just producing files has been one of the more interesting parts of this.</p>
<p>Licklider&rsquo;s framing from the last post still holds here. Set the goals, determine the criteria, perform the evaluations. That&rsquo;s still your job. What&rsquo;s changed is my confidence in what I can hand off once those things are set.</p>
<h2 id="what-it-produced">What It Produced</h2>
<p>The review and iterate phase is where the real work happens. AI gets you to a first draft faster. Whether that draft is right is still a judgment call only you can make.</p>
<p>A few months of this produced Flow v2 and something I&rsquo;ve been sitting on: <a href="https://mochiexec.io">Mochi</a>. Development workflows have a way of becoming invisible. They exist, they&rsquo;re just not anywhere you can see them. It&rsquo;s a local-first dev ops dashboard built on Flow. Point it at a directory and it finds your development scripts and automations, turns them into a unified, AI-enriched dashboard. No cloud, no accounts, works with whatever you&rsquo;re already running.</p>
<p><img src="https://jahvon.dev/images/mochi-executables_hu_3937508efceff644.png" srcset="https://jahvon.dev/images/mochi-executables_hu_e108e5df939cf6e.png 700w, https://jahvon.dev/images/mochi-executables_hu_3937508efceff644.png 1400w" sizes="(min-width: 768px) 720px, 100vw" data-zoom-src="https://jahvon.dev/images/mochi-executables.aec2ad9fd23d482d0003d84a995b6ff0c0972ba228d8c392f64f92a3378ad175.png" width="1400" height="1279"
     alt="Mochi Executables View"
     loading="lazy" decoding="async">

<em>Executables view. Everything Mochi found across my workspaces, tagged and filterable.</em></p>
<p>Still early. If it sounds useful, the waitlist is at <a href="https://mochiexec.io">mochiexec.io</a>.</p>
<p>I&rsquo;m more convinced than I was last fall that the gap worth closing isn&rsquo;t between what AI can produce and what you can prompt. It&rsquo;s between what AI produces and what you actually understand. Building in a system you designed is one way to stay honest about that.</p>
]]></content:encoded>
    </item>
    <item>
      <title>AI as a Creative Partner</title>
      <link>https://jahvon.dev/notes/ai-creative-partner/</link>
      <pubDate>Wed, 19 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/notes/ai-creative-partner/</guid>
      <description>An essay reflecting on my time using GenAI as a creative partner for a HGSE course that I was a teaching fellow for.</description>
      <content:encoded><![CDATA[<p>Since the start of the year, I&rsquo;ve been on a journey with learning about and with Large Language Models, settling into new AI tooling workflows, and reflecting on how these technologies have been showing up in my work. It&rsquo;s been quite impossible to avoid the constant AI buzz, so I wanted to figure out if my earlier AI skepticism was misplaced. I would only delegate teeny tiny tasks and easily confirmable questions to these systems. My time at Recurse Center this past summer accelerated that exploration even more. I tried several intentional experiments with a range AI development tools and processes. It gave me my first experiences with vibe coding during a weekly interest group that had formed. <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>My own position has begun to develop even more over the last six weeks, as I served as a Teaching Fellow for a Harvard Graduate School of Education module on using generative AI as a creative partner. It followed a project-based structure where students sought to build vibe coded apps to respond to a weekly prompt. Build something that&hellip; &ldquo;makes your life easier&rdquo;, &ldquo;invites play&rdquo;, &ldquo;answers a question&rdquo;, etc. The studio group that I supported included 15 students coming from a variety of backgrounds but many have never coded or used AI tools, from grade school educators to EdTech entrepreneurs they all shared a similar desire of getting their hands dirty with AI so that they can learn how they can apply it with the work that they want to do. We used tools like Replit, Claude Code, Google Colab, and Figma Make to play with AI in a reflective space. Alongside each session and through 1:1 conversations, I got to engage in lots of thoughtful discussions about ideating, prompting, iterating, societal impacts of AI, limitations of the current tools, our routine usage of these tools, and much more. I deeply engaged in the coursework, not only as a teacher, but as a fellow learner.</p>
<h2 id="what-we-built">What We Built</h2>
<h3 id="the-collaborative-illusion">The Collaborative Illusion</h3>
<p>For the first project, the class was tasked with building something that tells a story. I decided to use Claude Code to create an interactive version of The Three Little Pigs. I didn&rsquo;t really have specific technologies in mind for this project so I just sent a straightforward prompt that described that I wanted animated visuals that matched the story as the viewer worked through it. I was inspired by the gentle animations of <a href="https://beta.hearingbirdsong.com/">Hearing Birdsong</a> so I tried to describe my experience with that site as a foundation for how I wanted my story to be. Claude&rsquo;s response to that design was far from what I imagined. I went back and forth a few times, trying to see if I could iterate to improve the size and positioning of the text, interactive actions, animations, and design elements but I was left unsatisfied overall.</p>
<p>I knew that Claude Code does not generate images but I would have loved to see it admit defeat. Explicitly tell me that it could not create a visually appealing animations without its current set of tools and assets. Or tell me that it made the wrong decision when it decided on the initial tech stack after getting more information from me. Instead, when I described what I wanted the pigs and homes to be modeled as, it stuck with unsatisfying SVG representations.</p>
<p>Reading about what Joseph Weizenbaum wrote in <em>Contextual Understandings by Computers</em> about ELIZA, his 1960s chatbot, a few weeks later reminded me of this experience:</p>
<blockquote>
<p>One of the principle aims of the DOCTOR program is to keep the conversation going&ndash;even at the price of having to conceal any misunderstandings on its own part.</p>
</blockquote>
<p>These modern AI systems seem to operate similarly - they&rsquo;re optimized to maintain the illusion of understanding and expertise rather than honestly calling out their limitations. Claude kept generating code, stating that it was making progress even though the questions that I continued to ask were clearly stating otherwise. I wasn&rsquo;t too surprised by this given my previous experiments with AI but many students struggled with this phenomena.</p>
<h3 id="drawing-the-line">Drawing the Line</h3>
<p>The fifth week of the course, we focused on building games! As a kid, I dreamed of creating my own video games. I ended up taking a different path with my software career so it felt a bit too ambitious for me to try to jump into as a side project. I decided to put Claude Code to test again for this. My vision was to create a game that combined two games that I played when I was a kid: Pokemon and Neopets. (Imagine being able to select a Neopet to go up against other wild Neopets) It was this week that I really started to feel the need for much more collaborative development with Claude. In the first three weeks, I stuck mostly to prompt-review-reprompt cycles but this week I was consistently unsatisfied with what was being created.</p>
<p>I decided to take a look at the code that was being written, edited some bits, and asked for clarification. Then eventually, I was able to tell it explicitly how I wanted it to implement some of the features that I needed. I also had to take a much more active role in getting the aesthetics to align with what I wanted. I did the work of researching assets that I can pull in, colors and fonts that I should use, and crafted detailed explanations for the placement of some elements.</p>
<blockquote>
<p>In the anticipated symbiotic partnership, men will set the goals, formulate the hypotheses, determine the criteria, and perform the evaluations. Computing machines will do the routinizable work that must be done to prepare the way for insights and decisions in technical and scientific thinking.</p>
<p><em>Man-Computer Symbiosis, J. C. Licklider</em></p>
</blockquote>
<p>Licklider&rsquo;s explanation of how he viewed the relationship between man and computer in his 1960 paper felt spot on in how my experience went. I was doing exactly that: formulating what &ldquo;good Pokemon-meets-Neopets gameplay&rdquo; meant. This productive collaboration only emerged when I stopped treating the AI as capable of independent creative judgment and started treating it as Licklider envisioned.</p>
<h2 id="what-we-uncovered">What We Uncovered</h2>
<h3 id="vibe-coding-in-practice">Vibe Coding in Practice</h3>
<p>I loved seeing the joy and excitement that spread across the room as students worked on and shared their projects. But I really appreciated the moments of shared frustration that brought up thoughtful questions as we wrestled with the limitations of using AI as a creative partner. Non-technical creators now have the ability to apply code to problems in their own lives and domains; in a way that was much more out of reach before. It was quite refreshing hearing how students want to use vibe coding to do things like spinning up interactive prototypes for professional development trainings they&rsquo;re building, teaching other entrepreneurs the strengths and limitations of AI use in the social innovation space, simplify the creation of classroom worksheets and activities, and much more.</p>
<p>To give you a sense of what <em>I</em> was able to create with AI, I vibe coded this interactive portfolio:</p>
<iframe src="https://vibes.jahvon.dev" width="100%" height="600px" frameborder="0"></iframe>
<p>We hear that the power is in the prompt but, for me, the whole process matters. I&rsquo;ve learned that you can come with a great, detailed prompt but without an understanding of what&rsquo;s possible and where AI should create versus where you should intervene, you&rsquo;ll end up disappointed or at risk. While vibe coding lowers the barrier to entry for creating, it doesn&rsquo;t guarantee that you won&rsquo;t get lost once you&rsquo;re inside. It can do very well with applying simple, common applications of code but fall apart in the obscure cases. And without AI having a full understanding of what you are intending to create and you having an idea of what it is creating, it can lead you down paths that may be harmful and unproductive.  A student shared how it has an &ldquo;addicting&rdquo; effect since you can instantly see an idea realized. As someone who has the understanding of the code these vibe coded projects produced, I would be hesitant to use it blindly for anything that requires care and attention. Especially not without some careful review and collaborative implementing&hellip; but I don&rsquo;t think it&rsquo;s vibe coding at that point.</p>
<h3 id="the-efficiency-trap">The Efficiency Trap</h3>
<p>A lot of the hype that I see with AI is around how much more efficient it makes people. I had many conversations with students about the potential for AI to take away jobs, weaken relationships, increase dependency on technology, and kill the individual learning and creative process.</p>
<p>Kate Crawford argues in <em>The Atlas of AI</em> that we need to ask &ldquo;what is being optimized, and for whom, and who gets to decide.&rdquo; When we optimize for speed in creating apps or generating content, what are we not optimizing for? Crawford points out that &ldquo;the true costs of this extraction is never borne by the industry itself&rdquo; - not the environmental costs of training models, not the labor costs of the workers who label data, not the costs to students whose critical thinking declines from over-reliance on generated answers.</p>
<p>The efficiency gains are real - I built 6 functional prototypes in hours that would have taken me weeks. But the costs are externalized: to my own learning, to the development of judgment and perspective, to the practice and growth of skills like problem solving.</p>
<h3 id="designing-dependency">Designing Dependency</h3>
<p>In one of my reading discussion, we talked about how companies like OpenAI, Google, and Anthropic are building LLMs with features that mimic human connection: memories of past conversations, empathetic language, customizable personalities, approachable voices. Someone shared how ChatGPT had referenced her previous chat about being sick in a completely unrelated conversation - unprompted, it checked in on her health. While the gesture may feel nice, it raised an unsettling question: should we be designing machines to provide emotional connection?</p>
<p>Crawford warns that AI systems &ldquo;are ultimately designed to serve existing dominant interests.&rdquo; What interests does artificial empathy serve? I think that the goal is to optimize for engagement metrics, not genuine human wellbeing - keeping users returning to the platform, deepening dependence on the system. These features don&rsquo;t seem to be about about connection; they&rsquo;re about retention.</p>
<p>I&rsquo;ve heard stories of people ending relationships based on the AI&rsquo;s advice or seeking emotional support primarily from chatbots. When we find ourselves turning to ChatGPT for thoughts on deeply personal matters, we should ask: Does it have the full context of our lives like a close friend would? Does it challenge us when needed, like a parent might? Can we trust its guidance when it doesn&rsquo;t know what we&rsquo;re not sharing?</p>
<p>Crawford describes AI as &ldquo;both embodied and material, made from natural resources, fuel, human labor, infrastructures, logistics, histories, and classifications.&rdquo; But these systems fundamentally lack what makes human connection meaningful: they have no stakes in our life, no shared history beyond collected data, no capacity to be changed by knowing us. A chatbot remembering you were sick is pattern-matching engineered to feel like care.</p>
<p>Sure, we may reach a point where AI convincingly simulates every feature of human relationship. These aspects may make the creative process feel more personal, but that still leaves actual messy, complicated, but irreplaceable connections at risk.</p>
<h2 id="a-working-philosophy">A Working Philosophy</h2>
<p>For quick MVPs and non-critical prototypes, these tools are genuinely useful. But they can&rsquo;t replace pair programming with a colleague who asks why you&rsquo;re solving the problem that way, whiteboarding with your team where someone sketches a better approach, or independent research that builds understanding from the ground up. The Pokemon-Neopets game required me to step in - researching assets, making aesthetic decisions, explicitly directing implementation. That&rsquo;s where I learned something. As one Recurser put it, LLMs are like e-bikes: great for getting somewhere quickly, but if your goal is to become stronger, they won&rsquo;t help you with that. I found most of the value with working with these tools when I critically engaged with what&rsquo;s being generated during the review and iterate phase.</p>
<p>A student told me she&rsquo;s learned to change her expectations when working with AI tools - we start with grand ideas of what they can do, but these systems lack the qualities that enable human imagination and creation. Earlier this year, I saw this work well when a friend asked if I could help him learn some Python. He was curious about automating data analysis that he does as a scientist in biotech. I decided to use Claude to help me craft a curriculum and some exercises for us to work through. After gathering some more information about the data formats, goals, and background for his work; we actually ended up with a decent set of lessons that got him comfortable with writing Python and using numpy and pandas to help with some tasks. When I sent him off on his own, he had both tools and understanding.</p>
<h3 id="ai-as-a-learning-partner">AI as a Learning Partner</h3>
<p>That difference between my earlier experience with AI and my more recent vibe coding experiences is in the way AI is collaboratively used as a scaffold for learning and creating versus replacement for it. LLMs risk creating a gap between the edge of what you can produce and what you can understand. I could see AI working as a much better learning partner than a creative partner. This requires more investment upfront from us but pays off in genuine capability rather than dependency. I&rsquo;ve started including explicit process instructions in my prompts: &ldquo;Before writing any code, summarize what you&rsquo;re about to do and ask for confirmation.&rdquo; &ldquo;Admit when questions are ambiguous.&rdquo; Unfortunately, some LLMs routinely ignore these instructions so you still have to be independently vigilant.</p>
<p>I&rsquo;ll keep using AI tools, but with clearer boundaries. For rapid prototyping where I need speed over quality. For handling boilerplate so I can focus on interesting problems. Always understanding that output requires review, refinement, and judgment only I can provide. This course reinforced something I suspected: the most important parts of learning and creating can&rsquo;t be automated, not because AI will never be technically capable, but because we must build our own mental structures. LLMs can give fast answers, but only you can determine which questions you care about, and which answers are meaningful. Being a teaching fellow for this module showed me that the students who thrived weren&rsquo;t the ones who generated the most code - they were the ones who asked the best questions, challenged the outputs, and built understanding through iteration. I&rsquo;m carrying forward a position, not of rejection or uncritical embrace, but of conscious engagement with these tools as supplements to my creative capability, never substitutes for it.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Check out RC&rsquo;s <a href="https://www.recurse.com/blog/191-developing-our-position-on-ai">position on AI</a> that dropped during my time in batch. The sentiments around balancing &ldquo;shipping mode&rdquo; and &ldquo;learning mode&rdquo; when considering AI usage really resonated with me and the experience that I had during that time.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
    </item>
  </channel>
</rss>
