
A deliberation experiment: group composition and collective decisions
Source:vignettes/deliberation-experiment.Rmd
deliberation-experiment.RmdAgent-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.