<?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: Sean Graham</title>
    <description>The latest articles on DEV Community by Sean Graham (@sellotape06).</description>
    <link>https://dev.to/sellotape06</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%2F2796305%2F67cdf98f-578e-445f-89f2-25000fdef295.jpg</url>
      <title>DEV Community: Sean Graham</title>
      <link>https://dev.to/sellotape06</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sellotape06"/>
    <language>en</language>
    <item>
      <title>Building a Multi Agent Townhall simulation from scratch</title>
      <dc:creator>Sean Graham</dc:creator>
      <pubDate>Tue, 21 Jul 2026 14:13:05 +0000</pubDate>
      <link>https://dev.to/sellotape06/building-a-multi-agent-townhall-simulation-from-scratch-1a7d</link>
      <guid>https://dev.to/sellotape06/building-a-multi-agent-townhall-simulation-from-scratch-1a7d</guid>
      <description>&lt;p&gt;I wanted to understand how multi-agent systems actually work — not from a tutorial, but by wiring the pieces together myself. So I built a medieval townhall simulation where three AI characters (a &lt;strong&gt;mayor&lt;/strong&gt; with a French accent, a &lt;strong&gt;judge&lt;/strong&gt;, and a &lt;strong&gt;villager&lt;/strong&gt;) debate topics in a medieval townhall.  They can all hear each other in real time and can all contribute to the conversation.&lt;/p&gt;

&lt;p&gt;This project covers how to implement asynchronous agent communication, how to pass a shifting context window, how to store and retrieve memory using vector search (cosine similarity search) and finally lessons I learned on how to get the best outputs out of your agents based on the &lt;a href="https://arxiv.org/abs/2305.19118" rel="noopener noreferrer"&gt;Multi-Agent Debate (MAD) framework&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Find the repository &lt;a href="https://github.com/seangraham006/Multi-Agent-Townhall" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Architecture
&lt;/h1&gt;

&lt;h2&gt;
  
  
  How do we get asynchronous communication?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TLDR; Redis Streams&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Getting the agents to communicate directly to each other via APIs is tedious and not scalable.  An agent would need to send their comment to every single other agent, requiring them to already know every other entity and how to interface with them.&lt;/p&gt;

&lt;p&gt;To tackle this we need a message bus.  A central source of truth that any authorised entity can push data to and read data from at any time.  For our use case I chose to implement Redis Streams.&lt;/p&gt;

&lt;p&gt;Redis Streams is a built-in data structure in Redis that acts as an append-only log. Each entry has an auto-generated timestamp ID and a set of key-value fields. What makes it useful for multi-agent work is &lt;strong&gt;consumer groups&lt;/strong&gt;: multiple independent readers can each track their own position in the stream, so the same message gets delivered to every group without duplication.&lt;/p&gt;

&lt;p&gt;Crucially Redis Streams is easy to host locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do we get the context window?
&lt;/h2&gt;

&lt;p&gt;Once your Redis Streams setup is done and you have decided on a topic to insert data to, you can implement a context window.  This is simply a sliding window of the last n messages that you can dynamically feed into the prompt of your agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Memory Pipeline: Embeddings → Faiss → SQLite
&lt;/h2&gt;

&lt;p&gt;A context window is great but it does not allow for more meaningful agent development.  Instead of just reacting to the past n messages, the agents can instead draw on past experiences / accumulated knowledge to enrich their answers.&lt;/p&gt;

&lt;p&gt;This requires two things: memory insertion and memory retrieval.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: The ChronicleAgent Creates Summaries
&lt;/h3&gt;

&lt;p&gt;A dedicated &lt;code&gt;ChronicleAgent&lt;/code&gt; runs alongside the debate agents. It polls the Redis stream and, every 10 messages, asks Mistral to produce a concise summary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
You are an AI agent tasked with summarising the following events
from a townhall meeting.
Create an objective, concise summary...
Keep the summary under &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SUMMARY_SOFT_LIMIT_WORDS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; words.
Here are the events to summarise:
&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the rolling memory of the town — compressed, searchable, persistent.  By dedicating the role of memory to one agent we reduce duplicate memories / personal biases.  We also reduce complexity and expense of query execution.&lt;/p&gt;

&lt;p&gt;If you had different memory stores for different agents you would then likely wish to abandon this approach and instead make every agent responsible for creating its own memories.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Summaries Get Embedded and Stored
&lt;/h3&gt;

&lt;p&gt;Each summary is turned into a 768-dimensional vector using &lt;code&gt;sentence-transformers&lt;/code&gt; (&lt;code&gt;all-mpnet-base-v2&lt;/code&gt;), running locally on CPU. The summary text + embedding go into SQLite (the source of truth), and the vector goes into a Faiss index for fast similarity search:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_embedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;embedding_bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tobytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# SQLite: source of truth
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;SQLiteSummaryStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;summary_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary_record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Faiss: rebuildable search cache
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;FaissVectorStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;vector_store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;vector_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sqlite_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;summary_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Agents Retrieve Relevant Memories Before Responding
&lt;/h3&gt;

&lt;p&gt;When an agent needs to respond, it embeds the recent conversation context and queries Faiss for the most similar past summaries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;memories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;formatted_context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;formatted_memories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format_memories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memories&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;persona&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;formatted_context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;formatted_memories&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;retrieve()&lt;/code&gt; function wires the full pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;RetrievalResult&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;query_embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_embedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;FaissVectorStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;vector_store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;faiss_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vector_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;sqlite_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;faiss_results&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;SQLiteSummaryStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;summary_store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;summaries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;summary_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_summaries_by_ids&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sqlite_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;RetrievalResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;score_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sid&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summaries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sqlite_ids&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means the Villager remembers that "last meeting, we agreed bandits are driven by starvation" even though that conversation happened in a previous run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Faiss with Inner Product (Not L2)?
&lt;/h3&gt;

&lt;p&gt;Faiss offers several index types. I used &lt;code&gt;IndexFlatIP&lt;/code&gt; (flat inner product) rather than &lt;code&gt;IndexFlatL2&lt;/code&gt; (Euclidean distance). The trick: if you &lt;strong&gt;normalise vectors to unit length before insertion&lt;/strong&gt;, inner product becomes equivalent to cosine similarity — but faster, since Faiss is heavily optimised for dot products.&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;# In FaissVectorStore.add():
&lt;/span&gt;&lt;span class="n"&gt;euclidean_norm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;normalised_embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;euclidean_norm&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;faiss_index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalised_embedding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a standard trick in production vector search systems. Cosine similarity measures the &lt;em&gt;angle&lt;/em&gt; between vectors (semantic direction), ignoring magnitude. Two texts about "bandits and food shortages" will have high cosine similarity regardless of how long or short the embedding vectors happen to be.&lt;/p&gt;

&lt;p&gt;Depending on your use case you may choose a different type of vector search.  For instance if the magnitude of a result matters then a dot product search may better lend itself to your use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Agents Actually Produce
&lt;/h2&gt;

&lt;p&gt;Each agent has a distinct persona defined in its &lt;code&gt;generate_prompt()&lt;/code&gt; method. The Mayor speaks with a French accent, the Judge is stern and incensed, the Villager is Welsh and frustrated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Mayor] "Oui, oui, Capitaine—zere is no doubt ze threat grows! But we must
ask: do we crush ze bandits wiz force alone, or do we starve our own people
to pay for it? Sacré bleu, ze answer is not so simple!"

[Villager] "Ach, listen here now—if we're so keen on spendin' gold on swords
an' not seeds, then who's left to plant the bloody crops when half the village
is either dead or robbin' us blind, eh?"

[Judge] "Then let me ask plainly—shall we bleed the town dry for patrols now,
or watch it rot from within when the fields lie fallow and the granaries
stand empty?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see the conversation is entertaining but limited.  The agents are given a topic and will debate on it until each hits their hard limit on number of responses.  The conversation often spirals into dramatics with agents only ever seeming to escalate the stakes until they are all screaming that they are going to be killed.&lt;/p&gt;

&lt;p&gt;It is crucial to note here that I did not add a conflict resolution mechanism.  The agents are prompted to debate, but the judge can never issue an order and end the conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Breaks: The Circular Debate Problem and the Degeneration of Thought
&lt;/h2&gt;

&lt;p&gt;Here's the hard ceiling I hit. After about 30 messages, the conversation degenerates into a loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Villager: "We're starving!"&lt;/li&gt;
&lt;li&gt;Judge: "The lords hoard grain!"&lt;/li&gt;
&lt;li&gt;Mayor: "But we can't afford to give it away!"&lt;/li&gt;
&lt;li&gt;Villager: "We're starving!"&lt;/li&gt;
&lt;li&gt;...forever&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each Agent is prompted to respond with their concerns about the topic.  They are not told to propose solutions.  Additionally no mechanism is in place to determine if a reasonable solution has been identified or to synthesize one from the debaters' contributions.&lt;/p&gt;

&lt;p&gt;Worse, the retrieved memories reinforce the loop. Past summaries all say "No decision reached; further debate needed" — because that's what happened every time. The agents are literally being reminded that they've failed to decide before, then asked to express more concerns.&lt;/p&gt;

&lt;p&gt;To find the best solution to this problem I did some research and discovered that this is a well documented problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Liang et al. (2023)&lt;/strong&gt; call it &lt;strong&gt;"Degeneration of Thought"&lt;/strong&gt; — once an LLM establishes confidence in a position, self-reflection can't break it out. Their &lt;a href="https://arxiv.org/abs/2305.19118" rel="noopener noreferrer"&gt;Multi-Agent Debate (MAD) framework&lt;/a&gt; introduces a &lt;strong&gt;judge with an "adaptive break"&lt;/strong&gt; that ends debate when positions converge, plus distinct affirmative/negative roles that force genuine disagreement.&lt;/p&gt;

&lt;p&gt;They actually break down their debates into rounds to keep it more structured and to leave the Judge more opportunities to identify a solution from their discussions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'll Do Next
&lt;/h2&gt;

&lt;p&gt;I believe that the MAD approach is the best solution to our current problem so I plan to do the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Give the debaters prompts encouraging them to propose ideas (perhaps with an affirmative and a negative side)&lt;/li&gt;
&lt;li&gt;Give the judge the ability to assess whether a consensus has been reached and if not to determine whether a reasonable solution can be extracted from the debaters answers&lt;/li&gt;
&lt;li&gt;Add structured outputs for the LLMs to make aggregation and voting possible&lt;/li&gt;
&lt;li&gt;Testing to see how often agents disagree / agree with each other, who the judge sides with more often and other core metrics&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Your prompts define a behaviour ceiling.&lt;/strong&gt; Every agent did exactly what it was told: express concerns. No amount of architectural sophistication will overcome a prompt that asks for the wrong thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory can reinforce failure.&lt;/strong&gt; If your summaries record "no consensus reached," and agents retrieve those summaries, you've built a system that remembers it can't decide — and acts accordingly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A message bus is the foundation.&lt;/strong&gt; Redis Streams with consumer groups gave me fan-out messaging with acknowledgment, decoupled agents from each other, and made the whole system trivial to extend with new roles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normalised inner product = cosine similarity.&lt;/strong&gt; A standard trick, but one worth understanding from first principles. If you normalise your vectors before inserting them into a Faiss &lt;code&gt;IndexFlatIP&lt;/code&gt; index, you get cosine similarity for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the literature&lt;/strong&gt; When I was trying to find out how people improved their agent dialogues I came across various research papers; The MAD framework and Generative Agents (Park et al).  This really opened my eyes to a new information source I would not have used before.  These papers are brilliant and I highly recommend using research papers in the future&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>tutorial</category>
      <category>python</category>
      <category>ai</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
