<?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: Kulissa</title>
    <description>The latest articles on DEV Community by Kulissa (@kulissa).</description>
    <link>https://dev.to/kulissa</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%2F4211591%2Fecd6a73b-8d94-4f16-a313-146bd3a90336.png</url>
      <title>DEV Community: Kulissa</title>
      <link>https://dev.to/kulissa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kulissa"/>
    <language>en</language>
    <item>
      <title>How we built an AI buyer that pushes back (instead of agreeing with you)</title>
      <dc:creator>Kulissa</dc:creator>
      <pubDate>Tue, 27 Oct 2026 18:56:11 +0000</pubDate>
      <link>https://dev.to/kulissa/how-we-built-an-ai-buyer-that-pushes-back-instead-of-agreeing-with-you-1870</link>
      <guid>https://dev.to/kulissa/how-we-built-an-ai-buyer-that-pushes-back-instead-of-agreeing-with-you-1870</guid>
      <description>&lt;p&gt;If you've ever asked a chat model to "play a tough customer", you know what happens. It raises an objection, you give a so-so answer, and two turns later it says something like &lt;em&gt;"That's a great point, I think we can move forward!"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's a pleasant thing in an assistant and useless in a sparring partner. We're building &lt;strong&gt;Maya&lt;/strong&gt;, an AI sales coach where reps practise live voice roleplays with AI buyers that interrupt, object and hesitate. The whole product only works if the buyer &lt;em&gt;doesn't&lt;/em&gt; fold. This post covers how we got there: the voice pipeline, the persona harness that keeps the buyer honest, and the separate judge that grades the conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why LLMs want to agree with you
&lt;/h2&gt;

&lt;p&gt;Instruction-tuned models are trained to be helpful, and the human preference data behind that training tends to reward answers that agree with the user and sound accommodating. This is usually called &lt;strong&gt;sycophancy&lt;/strong&gt;. In a roleplay it shows up in three ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Concession drift.&lt;/strong&gt; Every turn the model re-reads the whole conversation, sees a friendly rep, and moves a little further towards yes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgotten constraints.&lt;/strong&gt; "Your budget is fixed" was in the system prompt 40 turns ago. Now it's competing with a lot of persuasive text from the rep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role leakage.&lt;/strong&gt; The model starts &lt;em&gt;coaching&lt;/em&gt; the rep ("You might want to mention ROI here") because helping is its default mode.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;More prompting ("Be skeptical! Do NOT agree easily!") helps a bit. But you're asking one model to hold a stance, track hidden facts and judge argument quality at once, while its context fills up with the other side's arguments.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea: keep the buyer's state outside the prompt
&lt;/h2&gt;

&lt;p&gt;In our &lt;strong&gt;buyer-persona harness&lt;/strong&gt;, the things that decide &lt;em&gt;whether&lt;/em&gt; the buyer concedes don't live in the prompt as prose. They live in code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Persona state:&lt;/strong&gt; who the buyer is, their current mood, how much they trust the rep, which objections are still open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hidden goals:&lt;/strong&gt; what the buyer actually cares about and doesn't say up front, for example "worried about rollout effort, not price".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concession rules:&lt;/strong&gt; explicit conditions that have to be met before the buyer moves, such as "won't discuss pricing until the rep has asked about the current process".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The LLM still does what it's good at: natural, in-character dialogue. But each turn it gets a short, current description of where the buyer &lt;em&gt;is&lt;/em&gt;, not the authority to decide where the buyer &lt;em&gt;goes&lt;/em&gt;. State changes happen in the harness, based on what the rep actually did.&lt;/p&gt;

&lt;p&gt;Here's a simplified version of the loop (illustrative pseudocode, not our production code):&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="c1"&gt;# Simplified illustration of an externally held buyer state.
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BuyerState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;persona&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trust&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;persona&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;initial_trust&lt;/span&gt;          &lt;span class="c1"&gt;# e.g. "low"
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open_objections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;persona&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objections&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hidden_goals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;persona&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hidden_goals&lt;/span&gt;    &lt;span class="c1"&gt;# never shown to the rep
&lt;/span&gt;        &lt;span class="n"&gt;self&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;discovery&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;take_turn&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;rep_utterance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Classify what the rep did, with a narrow, separate call.
&lt;/span&gt;    &lt;span class="n"&gt;moves&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;classify_rep_moves&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rep_utterance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# e.g. {"asked_open_question": True, "addressed": ["integration_effort"]}
&lt;/span&gt;
    &lt;span class="c1"&gt;# 2. Apply concession rules deterministically.
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;objection&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;list&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;open_objections&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;rule_satisfied&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;objection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;moves&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;open_objections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;objection&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;trust&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;raise_one_level&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;trust&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&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;open_objections&lt;/span&gt; &lt;span class="ow"&gt;and&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;trust&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&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;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;ready_to_commit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Only now ask the model to speak, given the current stance.
&lt;/span&gt;    &lt;span class="n"&gt;stance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render_stance&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="c1"&gt;# short, current, unambiguous
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;generate_buyer_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;:],&lt;/span&gt; &lt;span class="n"&gt;rep_utterance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things fall out of this design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The model can't drift into yes.&lt;/strong&gt; If the rules say an objection is still open, the stance says so every turn, however charming the rep is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hidden goals stay hidden but consistent.&lt;/strong&gt; The buyer can hint at them naturally, and the harness knows whether the rep uncovered them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's debuggable.&lt;/strong&gt; When a session feels off, we can look at state transitions instead of guessing what the model "was thinking".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Separating the &lt;em&gt;judgment&lt;/em&gt; ("did the rep earn this?") from the &lt;em&gt;performance&lt;/em&gt; ("say it like a skeptical CFO would") was the most important architectural choice we made.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it a real conversation: the voice pipeline
&lt;/h2&gt;

&lt;p&gt;Sales calls are spoken, so practising them in a text box only gets you part of the way. Maya runs in the browser with a mic, nothing to install, and the pipeline is the classic three stages, all streaming:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;streaming STT â†’ LLM â†’ streaming TTS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each stage starts working before the previous one finishes. Transcription produces partial text while the rep is still talking, the model starts generating as soon as the turn ends, and speech synthesis begins on the first sentence instead of waiting for the full reply.&lt;/p&gt;

&lt;h3&gt;
  
  
  Latency is a budget, not a number
&lt;/h3&gt;

&lt;p&gt;In conversation, people notice gaps quickly. A pause that's fine in a chat UI feels awkward on a call. So rather than optimising one component, it helps to think of a &lt;strong&gt;latency budget&lt;/strong&gt; for each turn, split across:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;detecting that the speaker has finished (endpointing),&lt;/li&gt;
&lt;li&gt;final transcription,&lt;/li&gt;
&lt;li&gt;time to the model's first token,&lt;/li&gt;
&lt;li&gt;time to the first audio chunk,&lt;/li&gt;
&lt;li&gt;network and audio buffering in the browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every stage gets a slice, and the harness work from the previous section has to fit in too. That's one reason the rep-move classifier is a narrow, separate step: small focused calls are easier to keep fast than one huge do-everything prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Barge-in: interrupting and being interrupted
&lt;/h3&gt;

&lt;p&gt;Real buyers cut you off. Real reps talk over buyers. So we support &lt;strong&gt;barge-in&lt;/strong&gt; in both directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rep interrupts buyer:&lt;/strong&gt; when voice activity is detected while the buyer is speaking, we stop playback, cancel the rest of the generation, and record what the buyer &lt;em&gt;actually got to say&lt;/em&gt;. That last part matters: the transcript and the state should reflect the half-sentence that was heard, not the full reply that was generated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buyer interrupts rep:&lt;/strong&gt; the persona can decide to jump in, for example when the rep has been pitching for a long time without asking anything. That's a state decision, not something the LLM improvises.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Simplified barge-in handling
on rep_voice_detected while buyer_speaking:
    stop_audio_playback()
    cancel_llm_and_tts_streams()
    history.append(buyer_reply.truncated_to(audio_played))
    resume_listening()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skip the truncation and the buyer "remembers" saying things the rep never heard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenarios from real material
&lt;/h2&gt;

&lt;p&gt;A generic "tough buyer" doesn't help a rep selling a specific product. Our &lt;strong&gt;scenario generator&lt;/strong&gt; takes a company's own playbooks, product sheets and pricing and turns them into scenarios: personas, the objections they'd realistically raise, hidden goals and the concession rules that go with them.&lt;/p&gt;

&lt;p&gt;The output is structured data that feeds the harness, not a big prompt, so a manager can see and adjust what the buyer cares about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The judge is not the buyer
&lt;/h2&gt;

&lt;p&gt;The last piece is evaluation. It's tempting to ask the buyer model at the end, "How did the rep do?" We don't, for the same reason you don't let the opposing team referee.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;evaluator&lt;/strong&gt; is a separate LLM-as-judge, with its own context and instructions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It scores against the customer's own methodology&lt;/strong&gt;, not a generic idea of "good selling". If a team uses a particular discovery framework, that's the rubric.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's evidence-first:&lt;/strong&gt; every score has to quote the transcript. No quote, no score. That makes feedback concrete ("here's where you skipped the pain question") and makes judge mistakes easy to spot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's checked against calibration sets:&lt;/strong&gt; transcripts with agreed-upon scores that we use to catch the judge drifting when prompts or models change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping them apart keeps each simpler: the buyer only has to be a convincing, stubborn human, and the judge only has to read carefully and cite.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we'd tell anyone building something similar
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't ask the model to be the source of truth for state it's under pressure to change.&lt;/strong&gt; Keep it in code and give the model a fresh, short view every turn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split judgment from performance.&lt;/strong&gt; Classify, apply rules, then generate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat latency as a budget across stages&lt;/strong&gt;, and stream everything you can.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle interruptions honestly:&lt;/strong&gt; the history should contain what was heard, not what was generated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never let the actor grade itself.&lt;/strong&gt; Use a separate judge that has to cite evidence, and calibrate it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it (and a small ask)
&lt;/h2&gt;

&lt;p&gt;Maya started as the Cursor Track-winning project at RAISE Summit 2026 in Paris, and we're now a small team in Zurich: Fernando Leyra (CEO), Sergey Novikov (CTO, PhD in AI), Yanik Almer (CPO) and Enes Sarituc (CRO).&lt;/p&gt;

&lt;p&gt;If you want to see whether our buyer actually pushes back, try a free roleplay at &lt;strong&gt;&lt;a href="https://kulissa.com" rel="noopener noreferrer"&gt;kulissa.com/maya&lt;/a&gt;&lt;/strong&gt;. It runs in your browser with your mic. And if you think it's worth it, we're launching on StartupBase, where feedback and support help a lot: &lt;strong&gt;&lt;a href="https://startupbase.io/products/kulissa" rel="noopener noreferrer"&gt;startupbase.io/products/kulissa&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Questions about the harness, barge-in or judge calibration? Ask in the comments, we're happy to go deeper.,&lt;/p&gt;

</description>
      <category>ai</category>
      <category>voice</category>
      <category>llm</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
