Skip to contents

Agent-based simulation with language models can pose questions that are hard to randomize with human participants, at low cost and with full transcripts: does group composition change a collective decision? Here we run a small factorial deliberation study, the kind a class or a pilot grant could extend. The design is small: panels of three deliberate on a workplace proposal and then vote privately. We vary one factor, whether the panel contains a fiscal skeptic, and replicate each cell.

A caution before the code: simulated agents are models of discourse, not of people. Results characterize the model under the personas given, a useful tool for theory development and instrument piloting, not a substitute for human subjects.

library(LLMRagent)

cfg <- LLMR::llm_config("groq", "openai/gpt-oss-20b", temperature = 0.9)

base_personas <- c(
  "An operations manager who values predictability. Plain speech.",
  "A young engineer enthusiastic about flexible work. Optimistic."
)
skeptic  <- "A finance director fixated on costs and risks. Blunt."
neutral  <- "An HR generalist who weighs evidence carefully. Even-keeled."

design <- expand.grid(
  composition = c("with_skeptic", "no_skeptic"),
  stringsAsFactors = FALSE
)

run_cell <- function(cond, rep) {
  third <- if (cond$composition == "with_skeptic") skeptic else neutral
  panel <- list(
    agent("Morgan", cfg, persona = base_personas[1], quiet = TRUE),
    agent("Sam",    cfg, persona = base_personas[2], quiet = TRUE),
    agent("Ren",    cfg, persona = third, quiet = TRUE)
  )
  deliberate(
    panel,
    proposal = "Adopt a four-day work week for a one-year pilot, full pay.",
    rounds = 2, quiet = TRUE
  )
}

Run the experiment (2 cells x 5 replications = 10 deliberations; with LLMR::llm_log_enable() on, every call is archived):

LLMR::llm_log_enable("deliberation_runs.jsonl")
res <- agent_experiment(design, run_cell, reps = 5, quiet = TRUE)
LLMR::llm_log_disable()
res[, c("composition", "rep", "error", "duration")]

Tidy the outcomes and compare:

votes <- do.call(rbind, lapply(seq_len(nrow(res)), function(i) {
  d <- res$result[[i]]
  if (is.null(d)) return(NULL)
  cbind(composition = res$composition[i], rep = res$rep[i], d$votes)
}))

# share of 'yes' votes by composition
aggregate(I(vote == "yes") ~ composition, data = votes, FUN = mean)

# and the decisions
decisions <- vapply(res$result, function(d)
  if (is.null(d)) NA_character_ else d$decision, character(1))
table(res$composition, decisions, useNA = "ifany")

Everything is inspectable: each res$result[[i]]$transcript is a tidy utterance-level frame (speaker, round, text) for content analysis, and the private votes carry one-sentence reasons. Natural extensions: vary the proposal framing as a second factor, replace the vote schema with a continuous support scale, score transcripts with LLMR::llm_mutate() for argument types, or re-run the same design across providers to check that a finding is not one model’s idiosyncrasy.