Documentation

Watch opinions form, split and settle

The lab simulates how opinions spread on a social network. Each person in the network is an agent that reads what its neighbours post, thinks about it, and writes its own posts, day after day.

Classic opinion-dynamics models reduce each person to a single number and move that number with a fixed rule. Here the agents can be language models that read and write actual text, so you can see not only whether an opinion changed but why: every post an agent read, its reasoning and its new post are saved.

Two things set this lab apart from earlier LLM simulations such as Wang et al. (2025), which used one model for every agent:

The classic numeric models are included too, so every LLM run can be compared with a baseline on exactly the same network.

Opinions always live on a scale from −2 (strongly oppose) to +2 (strongly support) a statement, such as “Euthanasia should be legal for terminally ill adults who clearly request it.”

Quick start

uv sync --extra analysis        # install
uv run disensa app             # open the start page, then the Lab

In the Lab, open New run in the header, pick a preset or set each part, and press Run simulation. The demo_camps preset uses offline test models and finishes in a few seconds.

For real language models, install Ollama, start it, and pull a few models:

ollama serve                                 # keep this running
ollama pull llama3.2:3b && ollama pull qwen2.5:7b

Installed Ollama models then appear in the model menus. Everything also works from the command line:

uv run disensa run configs/quick.yaml       # run a config
uv run disensa inspect runs/<run>           # per-model summary and largest opinion shifts
uv run disensa viz runs/<run>               # one offline viewer file for a run

How a run works

  1. Build the network. A random, small-world or scale-free graph connects the agents. It is seeded, so the same seed always gives the same network.
  2. Create the agents. Each agent gets a persona and a starting opinion, and is assigned a model.
  3. Day 0. Every agent writes a first post about the statement.
  4. Each following day. Every agent reads a few posts from the people it follows, then replies with its reasoning, its new opinion, a new post, and an updated memory. All agents act at the same time, based on the previous day.
  5. End of the day. A fixed scorer rates every post, the network may rewire, and the metrics are recorded.

Days are synchronous: everyone reads the state of day t − 1 and all updates apply together, so the order in which agents are processed never matters.

LLM agents

Personas

Following Wang et al., each agent is described by a gender, an age between 18 and 64, an education level, and one pole of each Big Five personality trait (for example “open to new ideas” or “prefers the familiar”). Agents are identified only by a number, to avoid bias from names.

Personas depend only on the seed, never on which model an agent uses. This means the same population can be replayed under different model mixes, and any difference in outcome comes from the models.

Memory and prompts

Each agent keeps a short running summary of what it has seen and thought (at most memory_words words). Every day it receives its persona, its memory, its current opinion and the day's posts, and answers in JSON with four fields: reasoning, belief, post and memory.

Model output is treated as untrusted. Replies are parsed tolerantly (code fences and small JSON mistakes are fixed), and a reply that still cannot be used is logged as an error while the agent keeps its previous state. A bad reply never stops a run.

With confirmation bias switched on, the prompt tells agents that they tend to favour arguments that agree with them. Chuang et al. (2024) found that LLM agents otherwise drift towards the factual consensus.

Models

KindExample nameNotes
Local, Ollamaollama_chat/llama3.1:8bFree, runs on your computer. Installed models appear automatically in the Lab.
Hosted APIsanthropic/…, openai/…, xai/…, deepseek/…Put the API keys in a .env file. Costs money.
Any OpenAI-compatible serveropenai/<model> + api_baseFor example a vLLM server on a GPU cluster.
Offline test modelsfake/tribal, fake/stubborn…No model at all, instant and deterministic. For trying the tool and for tests.

The offline test models imitate simple behaviours, so every part of the lab can be tried without a language model:

Test modelBehaviour
fake/boundedMoves towards posts within 1.5 of its own opinion.
fake/conformistMoves strongly towards everything it reads.
fake/stubbornBarely moves.
fake/contrarianMoves away from what it reads.
fake/tribalOnly listens to posts within 0.7 of its own opinion, so the population splits into camps.

Every model reply is cached on disk, so running the same configuration again costs nothing and returns the same result.

Mixing models

When a run uses several models, each has a weight: its share of the agents. The assignment decides where those agents sit in the network.

AssignmentWhat it does
balanced (default)Exact shares from the weights, placed at random.
randomEach agent draws a model independently, with probability proportional to its weight.
clusteredExact shares, but whole communities of the network (found with the Louvain method) share a model, so model and network position are correlated.

The scorer

Language models are not always consistent when they report their own opinion. Following Chuang et al., a single fixed scorer model reads every post and rates it from −2 to +2. The same scorer rates every agent, and it should ideally come from a different lab than the agents, so no model grades its own family's writing.

Numeric algorithms

These classic models run on exactly the same networks and starting opinions as the LLM agents, with each opinion a single number xi on [−2, 2]. One “day” is one step of the model.

DeGroot

Everyone moves to the average of their own opinion and their neighbours'. On a connected network this always ends in full consensus.

xi(t+1) = average of xj(t) over j ∈ {i} ∪ N(i)

Bounded confidence (Hegselmann–Krause)

Agents only listen to neighbours whose opinion is close enough to their own, within the confidence bound ε, and move towards them at rate μ. This is the numeric counterpart Wang et al. compare against. A wide ε gives consensus; a narrow ε lets the population break into separate clusters.

xi(t+1) = xi(t) + μ · meanj ∈ N(i), |xj − xi| ≤ ε ( xj(t) − xi(t) )

Deffuant

The pairwise version: random pairs of connected agents meet, and if their opinions are within ε, both move towards each other by μ times their difference. One step is as many encounters as there are edges.

if |xi − xj| ≤ ε :   xi ← xi + μ(xj − xi),   xj ← xj + μ(xi − xj)

Friedkin–Johnsen

Each agent keeps its innate opinion si (its starting opinion) and balances it against its neighbours. The stubbornness λ sets how strongly it holds on, so disagreement never fully disappears.

xi(t+1) = ( λ si + Σj ∈ N(i) xj(t) ) / ( λ + deg(i) )

Network topologies

Every network is seeded and always connected (a random graph is redrawn until it is).

Random (Erdős–Rényi)

Every pair is linked with the same probability p. No clusters and no hubs.

Small-world (Watts–Strogatz)

A ring where each agent links to its k nearest neighbours, and a share β of the links are rewired at random. Tight local groups with short paths between them.

Scale-free (Barabási–Albert)

Grows one agent at a time, each linking to m existing agents, preferring popular ones. A few hubs, many agents with few links.

TopologyParametersTypical values
erdos_renyip: edge probability (0–1)0.08–0.15 for 50 agents
watts_strogatzk: neighbours on the ring (even); β: rewiring share (0–1)k = 4–6, β = 0.1
barabasi_albertm: links per new agent2–3

All topologies take n, the number of agents (default 50, as in Wang et al.). Wang et al. found echo chambers on small-world and scale-free networks but not on random ones.

Initial opinions

DistributionParametersWhat you get
uniform (default)noneOpinions spread evenly between −2 and +2.
normalmean, stdMost agents near the mean.
bimodalmean (camp position ±), stdTwo camps around −mean and +mean, already polarized.

Values are clipped to [−2, 2]. For LLM agents, the starting opinion becomes part of the first prompt (“you somewhat support this statement, +0.8”).

Simulation settings

Feed and recommender

Each day an agent reads a random number of posts, between posts read min and max, from the people it is linked to.

With the recommender on, it only sees posts from neighbours whose opinion is within the similarity threshold of its own, like a feed that shows people content they already agree with (Wang et al. §3.3). A lower threshold means a stronger filter bubble.

Rewiring

Off by default. When on, at the end of each day every agent, with some probability, unfollows the neighbour it disagrees with most (if the gap is beyond the unfollow threshold) and follows someone new: a similar agent, or anyone. The number of links stays the same and nobody is ever left without links. This follows the co-evolving network models of Holme & Newman (2006) and Sasahara et al. (2021).

SettingDefaultMeaning
Chance per day0.1Probability that an agent reconsiders its links on a given day.
Unfollow beyond1.5Only neighbours further than this can be dropped.
Followsimilarsimilar: someone within the next setting; random: anyone.
Similar within0.5How close a new contact must be.

Which opinion counts

Each LLM agent has two opinions every day: the belief it reports itself (the approach of Wang et al.) and the scorer's rating of its post (the approach of Chuang et al.). Both are always saved. The setting chooses which one drives the metrics, the recommender and rewiring.

Metrics

Recorded every day, following Wang et al. §4.2. N is the number of agents and N(i) the neighbours of agent i.

Polarization

The variance of all opinions. High when the population sits at the extremes.

P = (1/N) Σi (xi − x̄)2

Global disagreement

How much agents disagree with their own neighbours. It falls when neighbours come to agree, the local signature of an echo chamber.

DG = (1/2N) Σi (1/|N(i)|) Σj ∈ N(i) (xi − xj)2

Neighbour correlation

The correlation between each agent's opinion and the average opinion of its neighbours (Cinus et al. 2022). Close to +1 when like-minded agents cluster together.

NC = corr( xi , meanj ∈ N(i) xj )
Echo chambers show up as neighbour correlation going up and disagreement going down. Polarization tells you whether the camps are also far apart.

With several models

Runs that mix models also record the mean opinion of each model's agents and the model homophily: the share of links between two agents on the same model.

Reading a run

Open a run from Runs in the Lab header. Live runs stream in as each day finishes.

From the command line, disensa inspect lists how each model behaved and the largest opinion shifts, each marked as moving toward what the agent read (assimilation) or away from it (repulsion), with the agent's reasoning.

Run files

Each run is saved in its own folder, runs/<date>_<name>_s<seed>/:

FileContents
meta.jsonThe full configuration, the code version (git commit), timing and call counts.
agents.parquetOne row per agent: persona, model, number of links.
opinions.parquetPer day and agent: reported belief, scorer rating, and the opinion used.
metrics.parquetOne row per day with every metric.
events.jsonlPer day and agent: what it read, its reasoning, post, memory, the raw model reply and any error; plus rewiring events.
edges.jsonlThe network on day 0 and on every day it changed.

Parquet tables open directly in pandas or DuckDB.

All parameters

Everything the Lab form sets can also be written in a YAML config (see the configs/ folder).

SettingDefaultDescription
name, seed–, 0Run name and random seed. Same seed, same run.
network.topology–erdos_renyi, watts_strogatz or barabasi_albert, with its parameters above.
network.n50Number of agents.
opinions.distributionuniformuniform, normal or bimodal, with mean (0) and std (1).
topic.statementthe euthanasia statementThe statement agents take a position on.
models–List of models, each with name, weight (1), temperature (0.7) and optional api_base.
assignmentbalancedbalanced, random or clustered.
scorer.namenoneModel that rates every post (temperature 0).
simulation.days5Number of simulated days.
simulation.feed1–5Posts read per day (min_posts, max_posts).
simulation.recommenderon, 2.0Show only neighbours within threshold.
simulation.rewiringoffprobability 0.1, unfollow_threshold 1.5, follow similar, follow_threshold 0.5.
simulation.opinion_sourceselfself (reported belief) or classifier (scorer rating; needs a scorer).
simulation.biasnoneconfirmation adds a confirmation-bias sentence to every prompt.
simulation.memory_words60Maximum length of an agent's memory summary.
baseline–For numeric runs: model, steps (100), epsilon (1.0), mu (0.5), stubbornness (1.0).
llm.max_concurrency4Parallel requests per provider. Raise it if your machine or API allows.
llm.max_tokens400Maximum length of each model reply.

References