<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[AI Research]]></title><description><![CDATA[AI Research]]></description><link>https://blog.n.ichol.ai</link><generator>RSS for Node</generator><lastBuildDate>Tue, 21 Apr 2026 16:47:00 GMT</lastBuildDate><atom:link href="https://blog.n.ichol.ai/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[LLM Activation Engineering: An Easy Foray]]></title><description><![CDATA[This is a recap of an old project from May 2024.
Credit to Neel Nanda for llm lens and Mihaiiii for llm steer python modules.

I've been playing around with steering LLM outputs by manipulating their ]]></description><link>https://blog.n.ichol.ai/llm-activation-engineering-an-easy-foray</link><guid isPermaLink="true">https://blog.n.ichol.ai/llm-activation-engineering-an-easy-foray</guid><dc:creator><![CDATA[Nicholai Mitchko]]></dc:creator><pubDate>Tue, 31 Mar 2026 23:57:47 GMT</pubDate><content:encoded><![CDATA[<p>This is a recap of an old project from May 2024.</p>
<p>Credit to Neel Nanda for llm lens and Mihaiiii for llm steer python modules.</p>
<hr />
<p>I've been playing around with steering LLM outputs by manipulating their internal activation vectors rather than just writing better prompts. It's a fascinating technique that sits somewhere between interpretability research and actual control. Here's what I've learned.</p>
<h2>The Core Idea</h2>
<p>When you inputs text through an LLM, every layer produces a set of activations. They represent what the model "thinks" at the moment . You can capture these activations for a specific concept (say, <code>"sad"</code>) and then add, subtract, multiply them back into the model during generation to force that behavior. Your new layer function looks like this:</p>
<p>$$H_{new} = H_{original} + \alpha * V_{concept}$$</p>
<p>Activation engineering (aka; llm steering, concept edits) are similar to emotional states. Normal AI Interaction:</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/69c14fca30a9b81e3a60c902/e2e5e2e3-6bac-4be9-8c47-a8b3f53d059a.png" alt="img" /></p>
<h2>A Simple Technique</h2>
<p>So to use in practice you'd basically try to activate a specific concept or token:</p>
<p><em>To capture "sad," you:</em></p>
<ol>
<li><p>Run a set of "sad" prompts through the model</p>
</li>
<li><p>Save the activations at your target layer</p>
</li>
<li><p>Average them to get a single vector</p>
</li>
</ol>
<h2>First Try:</h2>
<p>We capture and add back in the "sad" activation, and....</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/69c14fca30a9b81e3a60c902/e1b039b4-cee2-4ad2-ba56-2814401619a5.png" alt="img" /></p>
<p>$$H_{7} = H_{7old} + 0.5 * V_{sad}$$</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/69c14fca30a9b81e3a60c902/858ec2f1-c546-4c9f-acf4-122661c60c56.png" alt="img" /></p>
<p>...we got some weird results. It looks like song lyrics, bible verse, or something. But google can't find anything related to it. Surely a hallucination.</p>
<h2>Second Try:</h2>
<p>You may be tempted to simply try the activation weights and different concepts. You can get decent results per-concept if you hit the sweet spot.</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/69c14fca30a9b81e3a60c902/6db63c10-3942-4e4f-ab9e-bfa2c405de34.png" alt="img" /></p>
<p>$$H_{7} = H_{7old} + 0.85 * V_{tax}$$</p>
<p>This gives us some better formatted, and humorous results,</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/69c14fca30a9b81e3a60c902/1d87b9f6-f87e-4f0e-8e1e-b9ea088e68b0.png" alt="img" /></p>
<p>Nice! It's like I'm at an H&amp;R Block.</p>
<h2>Optimizing Blind:</h2>
<p>Eventually I got bored of try steering vectors, and instead decided to up the ante.</p>
<p><em>Could I get a model to beat an lm_eval benchmark using a single activation vector?</em></p>
<p>The activation space is largely unknown territory. We don't know which layer does what, or how different steering vectors interact. So I tried treating it as a black box optimization problem.</p>
<p>I used Particle Swarm Optimization (PSO) to automatically find the best steering parameters. Think of it as sending out several explorers in different directions on a dark mountain - the ones who find steep slopes tell the others where to go, and eventually you converge on the peak. It's similar to gradient descent but when you expect the search space to be non-differentiable.</p>
<p>The implementation uses lm_eval to benchmark performance. For each Loop:</p>
<ol>
<li><p>Add Steering Vectors</p>
</li>
<li><p>Loop through the fitness evolution,</p>
</li>
<li><p>At each particle step run lm_eval</p>
</li>
<li><p>Reset Steering Vectors</p>
</li>
<li><p>After enough iterations, return the optimal vectors</p>
</li>
</ol>
<pre><code class="language-python">def optimize_steering_to_eval(
    optimize_particles=3,
    optimize_iterations=5,
    optimize_batch_size=1,
    evaluation="pubmedqa",
    progress=gr.Progress(track_tqdm=True),
):
    if shared.steered_model is not None:
        steering_vectors_num = len(shared.steered_model.get_all())

        NUM_PARTICLES = optimize_particles
        NUM_DIMENSIONS = particle_size * steering_vectors_num
        X_MAX = 1
        X_MIN = 0

        x_max = X_MAX * np.ones(NUM_DIMENSIONS)
        x_min = X_MIN * np.ones(NUM_DIMENSIONS)

        optimizer = ps.single.GlobalBestPSO(
            n_particles=NUM_PARTICLES,
            dimensions=NUM_DIMENSIONS,
            options=options,
            bounds=(x_min, x_max),
        )

        cost, pos = optimizer.optimize(
            __swarm_fitness,
            iters=optimize_iterations,
            optimize_batch_size=optimize_batch_size,
            evaluation=evaluation,
        )

        # print out the scaled particle
        scaled_particle = str(__scale_particle(pos))

        # Build explanation of optimization
        steering_vectors = shared.steered_model.get_all()

        reset_steering_vectors()

        # add steering with parameteres in x[]
        particle_explanation = (
            f"Benchmark Optimum Found: {1 - cost} \n {str(scaled_particle)} \n"
        )

        n = 0
        for vector in steering_vectors:
            layer = scaled_particle[0 + n * particle_size]
            coeff = scaled_particle[1 + n * particle_size]
            offset_inner = int(0)

            # Build Explanation of optimization
            particle_explanation += (
                f"Layer: {layer} \t Coeff: {coeff} \t text: {vector['text']} \n"
            )
            # reset the vectors to the best optimization
            # add_steering_vector(layer, coeff, vector['text'], offset_inner)
            # increment the counter
            n = n + 1

        return particle_explanation
    else:
        return "Please add some steering vectors for optimization"
</code></pre>
<p>So let's try it out, adding some steering vectors and particles. <strong>An optimization run:</strong></p>
<p><img src="https://cdn.hashnode.com/uploads/covers/69c14fca30a9b81e3a60c902/1459bfc0-cf74-42b9-957a-27ba067f5881.png" alt="" /></p>
<h3>So did it work?</h3>
<p>At the time of this project, yes. a 2-3% difference in per-task performance was noted. Nothing extraordinary but enough to smile and never share the code.</p>
<p><em>It turns out that adding a steering vector of "the best doctor in the world" at the right spot, was enough to activate the right parts of the model. This was 3 raw bps better than baseline (using OpenBioLLM tested against MedMCQA)</em></p>
<p><img src="https://cdn.hashnode.com/uploads/covers/69c14fca30a9b81e3a60c902/c50ceeeb-4e39-4b52-b0f9-548b656643b7.png" alt="" /></p>
<p><img src="https://cdn.hashnode.com/uploads/covers/69c14fca30a9b81e3a60c902/ef63543e-bb9c-4fe6-be62-b415e595ebae.png" alt="" /></p>
<hr />
<h2>Conclusion</h2>
<p>Activation steering represents a powerful but underexplored approach to guiding LLM behavior. While traditional prompting relies on the model's inherent capabilities, steering vectors let us directly manipulate the model's internal representations to achieve specific outcomes.</p>
<p>The key takeaways from this project:</p>
<ol>
<li><strong>Manual steering works but requires intuition</strong> - Finding the right activation vector and coefficient is largely trial and error</li>
<li><strong>Automated optimization yields measurable gains</strong> - PSO-based approaches can discover steering parameters that improve benchmark performance</li>
<li><strong>The activation space remains mysterious</strong> - We still don't fully understand which layers control what behaviors</li>
</ol>
<p>This technique is far from production-ready, but it demonstrates the potential of interpretability-informed model control. As we gain better tools for understanding internal representations, steering approaches may become more predictable and practical.</p>
<p>The future could see automated steering discovery integrated into fine-tuning pipelines, or real-time activation adjustment for dynamic response control. For now, it remains an intriguing research direction that blurs the line between prompting and model modification.</p>
<hr />
<p>[1] <a href="https://github.com/nickmitchko/llm_steer-oobabooga">Github Repo</a></p>
<p>[2] <a href="https://github.com/Mihaiii/llm_steer">LLM Steer Python Module</a></p>
<p>[3] <a href="https://mihaiii-llm-steer.hf.space/">Steering Playground - Mihaiii on Hugging Face</a></p>
]]></content:encoded></item></channel></rss>