Multi-Agent Simulation for Prediction
Multi-agent simulation spawns thousands or millions of autonomous AI agents — each with unique personalities, knowledge, and decision-making patterns — and lets them interact in simulated environments. The emergent behaviors that arise from these interactions produce predictions about social dynamics, market movements, political outcomes, and technological adoption that no single model could generate. This approach moves beyond traditional forecasting by modeling the complex adaptive systems that actually generate the events we want to predict.
## Key Points
1. Agent-based models capture nonlinear dynamics, tipping points, and emergent behaviors that equation-based models miss
2. Agent personality diversity is critical: use realistic distributions from behavioral science, not uniform random
3. Network topology dramatically affects outcomes: small-world networks produce different dynamics than scale-free ones
4. Hierarchical update scheduling and spatial partitioning enable scaling to millions of agents
5. Run multiple simulations with different random seeds and aggregate results for robust predictions
6. The MiroFish dual-platform approach (microblog + forum) captures different types of social dynamics
7. LLM-driven agents produce more realistic behaviors than rule-based agents, but require batching and caching for scale
8. Emergent behavior detection (consensus, polarization, cascades) is where the predictive value lies
## Quick Example
```
Traditional: GDP = f(consumption, investment, government, exports)
Agent-Based: 10,000 consumer agents + 500 firm agents + 1 government agent
→ Each follows behavioral rules
→ GDP emerges from their interactions
→ Captures nonlinear dynamics, tipping points, cascades
```skilldb get prediction-skills/multi-agent-simulationFull skill: 727 linesMulti-Agent Simulation for Prediction
Overview
Multi-agent simulation spawns thousands or millions of autonomous AI agents — each with unique personalities, knowledge, and decision-making patterns — and lets them interact in simulated environments. The emergent behaviors that arise from these interactions produce predictions about social dynamics, market movements, political outcomes, and technological adoption that no single model could generate. This approach moves beyond traditional forecasting by modeling the complex adaptive systems that actually generate the events we want to predict.
Agent-Based Modeling Foundations
What Makes ABM Different
Traditional forecasting uses equations to model aggregate behavior. Agent-based modeling (ABM) starts from individual agents following simple rules, and complex macro-level patterns emerge from their interactions.
Traditional: GDP = f(consumption, investment, government, exports)
Agent-Based: 10,000 consumer agents + 500 firm agents + 1 government agent
→ Each follows behavioral rules
→ GDP emerges from their interactions
→ Captures nonlinear dynamics, tipping points, cascades
Core Agent Architecture
from dataclasses import dataclass, field
from typing import Optional
import random
import uuid
@dataclass
class AgentPersonality:
"""Defines an agent's behavioral tendencies."""
risk_tolerance: float # 0 (risk-averse) to 1 (risk-seeking)
information_sensitivity: float # How much new info changes beliefs
social_conformity: float # Tendency to follow neighbors
contrarianism: float # Tendency to go against the crowd
attention_span: int # How many time steps of memory
emotional_volatility: float # How much mood affects decisions
expertise_level: float # Domain knowledge (0 to 1)
@classmethod
def random(cls) -> 'AgentPersonality':
"""Generate a random personality from realistic distributions."""
risk = random.betavariate(2, 5) # Skewed toward risk-averse
conformity = random.betavariate(3, 2) # Skewed toward conformist
return cls(
risk_tolerance=risk,
information_sensitivity=random.betavariate(2, 2),
social_conformity=conformity,
contrarianism=max(0, 1 - conformity + random.gauss(0, 0.1)),
attention_span=int(random.expovariate(0.1)) + 1,
emotional_volatility=random.betavariate(2, 3),
expertise_level=random.betavariate(1.5, 5)
)
@dataclass
class AgentState:
"""Mutable state of an agent at a point in time."""
beliefs: dict = field(default_factory=dict) # topic -> probability
mood: float = 0.0 # -1 (negative) to 1 (positive)
wealth: float = 1000.0
social_connections: list = field(default_factory=list)
memory: list = field(default_factory=list)
position: tuple = (0, 0) # Spatial position
class Agent:
"""An autonomous agent in the simulation."""
def __init__(self, personality: Optional[AgentPersonality] = None):
self.id = str(uuid.uuid4())[:8]
self.personality = personality or AgentPersonality.random()
self.state = AgentState()
def perceive(self, environment: dict, neighbors: list['Agent']) -> dict:
"""Gather information from environment and neighbors."""
perceptions = {
'environment_signals': environment.get('signals', []),
'neighbor_beliefs': [
n.state.beliefs for n in neighbors
],
'neighbor_moods': [n.state.mood for n in neighbors],
'neighbor_actions': [
n.state.memory[-1] if n.state.memory else None
for n in neighbors
]
}
return perceptions
def decide(self, perceptions: dict) -> dict:
"""Make a decision based on personality and perceptions."""
action = {}
# Update beliefs based on new information
for signal in perceptions['environment_signals']:
topic = signal['topic']
new_evidence = signal['value']
current = self.state.beliefs.get(topic, 0.5)
# Bayesian-ish update weighted by personality
weight = self.personality.information_sensitivity
self.state.beliefs[topic] = (
current * (1 - weight) + new_evidence * weight
)
# Social influence on beliefs
if perceptions['neighbor_beliefs']:
for topic in self.state.beliefs:
neighbor_avg = sum(
nb.get(topic, 0.5) for nb in perceptions['neighbor_beliefs']
) / len(perceptions['neighbor_beliefs'])
social_pull = self.personality.social_conformity
contrarian_push = self.personality.contrarianism
self.state.beliefs[topic] += (
social_pull * (neighbor_avg - self.state.beliefs[topic])
- contrarian_push * (neighbor_avg - 0.5)
)
self.state.beliefs[topic] = max(0, min(1, self.state.beliefs[topic]))
# Mood contagion
if perceptions['neighbor_moods']:
avg_mood = sum(perceptions['neighbor_moods']) / len(perceptions['neighbor_moods'])
self.state.mood += (
self.personality.emotional_volatility * (avg_mood - self.state.mood) * 0.1
)
# Decide on action based on beliefs and personality
action['trade'] = self._trading_decision()
action['communicate'] = self._communication_decision()
return action
def _trading_decision(self) -> dict:
"""Decide whether to buy, sell, or hold."""
confidence = max(self.state.beliefs.values()) if self.state.beliefs else 0.5
threshold = 1 - self.personality.risk_tolerance
if confidence > threshold + 0.1:
return {'action': 'buy', 'size': confidence * self.personality.risk_tolerance}
elif confidence < threshold - 0.1:
return {'action': 'sell', 'size': (1 - confidence) * self.personality.risk_tolerance}
return {'action': 'hold', 'size': 0}
def _communication_decision(self) -> dict:
"""Decide what to share with neighbors."""
# Extroverted agents share more
share_prob = self.personality.social_conformity * 0.5 + 0.2
if random.random() < share_prob:
return {'share': True, 'beliefs': self.state.beliefs.copy()}
return {'share': False}
def act(self, action: dict, environment: 'SimulationEnvironment'):
"""Execute the decided action in the environment."""
self.state.memory.append(action)
if len(self.state.memory) > self.personality.attention_span:
self.state.memory = self.state.memory[-self.personality.attention_span:]
OASIS-Style Large-Scale Simulation
Architecture for Million-Agent Simulations
The OASIS (Open Agent Social Interaction Simulations) framework demonstrates how to scale agent-based simulations to millions of agents by using LLMs to drive realistic behavior.
class OASISEngine:
"""
Large-scale social simulation engine.
Inspired by OASIS: supports millions of agents interacting
on simulated social platforms.
"""
def __init__(self, config: dict):
self.agents = []
self.environment = SimulationEnvironment(config)
self.social_network = SocialNetwork()
self.time_step = 0
self.metrics = SimulationMetrics()
self.event_queue = []
def spawn_population(self, n_agents: int, demographics: dict):
"""Create a diverse agent population matching demographic targets."""
for i in range(n_agents):
# Generate personality from demographic distribution
demographic = self._sample_demographic(demographics)
personality = self._personality_from_demographic(demographic)
agent = Agent(personality)
agent.state.beliefs = self._initial_beliefs(demographic)
self.agents.append(agent)
# Create social network
self.social_network.build_network(
self.agents,
topology='small_world',
avg_connections=15,
clustering=0.3
)
def inject_event(self, event: dict, time_step: int):
"""Schedule an external event to hit the simulation."""
self.event_queue.append({
'event': event,
'time': time_step,
'affected_fraction': event.get('reach', 1.0)
})
def step(self):
"""Execute one simulation time step."""
# Process any scheduled events
for event in self.event_queue:
if event['time'] == self.time_step:
self._apply_event(event)
# Parallelize agent updates
environment_state = self.environment.get_state()
for agent in self.agents:
neighbors = self.social_network.get_neighbors(agent.id)
neighbor_agents = [a for a in self.agents if a.id in neighbors]
perceptions = agent.perceive(environment_state, neighbor_agents)
action = agent.decide(perceptions)
agent.act(action, self.environment)
# Update environment based on aggregate agent actions
self.environment.update(self.agents)
# Collect metrics
self.metrics.record(self.time_step, self.agents, self.environment)
self.time_step += 1
def run(self, n_steps: int, events: list = None):
"""Run the full simulation."""
if events:
for event in events:
self.inject_event(event['event'], event['time'])
for _ in range(n_steps):
self.step()
return self.metrics.summarize()
def _apply_event(self, event: dict):
"""Apply an external event to affected agents."""
affected = random.sample(
self.agents,
int(len(self.agents) * event['affected_fraction'])
)
for agent in affected:
for topic, impact in event['event'].get('belief_impacts', {}).items():
current = agent.state.beliefs.get(topic, 0.5)
sensitivity = agent.personality.information_sensitivity
agent.state.beliefs[topic] = current + impact * sensitivity
agent.state.beliefs[topic] = max(0, min(1, agent.state.beliefs[topic]))
mood_impact = event['event'].get('mood_impact', 0)
agent.state.mood += mood_impact * agent.personality.emotional_volatility
class SocialNetwork:
"""Manages agent connections and network topology."""
def __init__(self):
self.adjacency = {} # agent_id -> set of neighbor_ids
self.influence_weights = {} # (from, to) -> weight
def build_network(self, agents: list, topology: str = 'small_world',
avg_connections: int = 15, clustering: float = 0.3):
"""Build a social network with specified topology."""
n = len(agents)
ids = [a.id for a in agents]
if topology == 'small_world':
# Watts-Strogatz small-world network
k = avg_connections
rewire_prob = 1 - clustering
# Start with ring lattice
for i, agent_id in enumerate(ids):
self.adjacency[agent_id] = set()
for j in range(1, k // 2 + 1):
neighbor = ids[(i + j) % n]
self.adjacency[agent_id].add(neighbor)
if neighbor not in self.adjacency:
self.adjacency[neighbor] = set()
self.adjacency[neighbor].add(agent_id)
# Rewire edges
for i, agent_id in enumerate(ids):
neighbors = list(self.adjacency[agent_id])
for neighbor in neighbors:
if random.random() < rewire_prob:
new_neighbor = random.choice(ids)
if new_neighbor != agent_id and new_neighbor not in self.adjacency[agent_id]:
self.adjacency[agent_id].discard(neighbor)
self.adjacency[agent_id].add(new_neighbor)
elif topology == 'scale_free':
# Barabasi-Albert preferential attachment
for i, agent_id in enumerate(ids):
self.adjacency[agent_id] = set()
if i < avg_connections:
for j in range(i):
self.adjacency[agent_id].add(ids[j])
self.adjacency[ids[j]].add(agent_id)
else:
degrees = [len(self.adjacency.get(aid, set())) + 1 for aid in ids[:i]]
total_degree = sum(degrees)
probs = [d / total_degree for d in degrees]
targets = set(random.choices(ids[:i], weights=probs, k=min(avg_connections, i)))
for target in targets:
self.adjacency[agent_id].add(target)
self.adjacency[target].add(agent_id)
def get_neighbors(self, agent_id: str) -> set:
return self.adjacency.get(agent_id, set())
Emergent Behavior Analysis
Detecting Emergent Patterns
class EmergenceDetector:
"""Detect and classify emergent behaviors in agent populations."""
def __init__(self, agents: list, history: list):
self.agents = agents
self.history = history # List of snapshots over time
def detect_consensus_formation(self, topic: str, threshold: float = 0.8) -> dict:
"""Detect when agents converge on a shared belief."""
consensus_timeline = []
for snapshot in self.history:
beliefs = [a['beliefs'].get(topic, 0.5) for a in snapshot]
mean_belief = sum(beliefs) / len(beliefs)
std_belief = (sum((b - mean_belief)**2 for b in beliefs) / len(beliefs)) ** 0.5
agreement = 1 - std_belief * 2 # Normalize
consensus_timeline.append({
'mean': mean_belief,
'std': std_belief,
'agreement': agreement,
'is_consensus': agreement > threshold
})
# Find consensus formation point
for i, point in enumerate(consensus_timeline):
if point['is_consensus']:
return {
'formed': True,
'time_step': i,
'consensus_value': point['mean'],
'strength': point['agreement']
}
return {'formed': False}
def detect_polarization(self, topic: str) -> dict:
"""Detect opinion polarization (bimodal distribution)."""
latest = self.history[-1]
beliefs = [a['beliefs'].get(topic, 0.5) for a in latest]
# Check for bimodality
low_cluster = [b for b in beliefs if b < 0.4]
high_cluster = [b for b in beliefs if b > 0.6]
middle = [b for b in beliefs if 0.4 <= b <= 0.6]
polarization_index = (
(len(low_cluster) + len(high_cluster)) / len(beliefs)
- len(middle) / len(beliefs)
)
return {
'polarization_index': polarization_index,
'is_polarized': polarization_index > 0.5,
'low_fraction': len(low_cluster) / len(beliefs),
'high_fraction': len(high_cluster) / len(beliefs),
'undecided_fraction': len(middle) / len(beliefs)
}
def detect_cascade(self, topic: str, window: int = 5) -> list:
"""Detect information cascades (rapid belief shifts)."""
cascades = []
for i in range(window, len(self.history)):
current_beliefs = [a['beliefs'].get(topic, 0.5) for a in self.history[i]]
past_beliefs = [a['beliefs'].get(topic, 0.5) for a in self.history[i - window]]
current_mean = sum(current_beliefs) / len(current_beliefs)
past_mean = sum(past_beliefs) / len(past_beliefs)
shift = abs(current_mean - past_mean)
if shift > 0.15: # Significant shift
cascades.append({
'time_step': i,
'shift_magnitude': shift,
'direction': 'positive' if current_mean > past_mean else 'negative',
'speed': shift / window
})
return cascades
def detect_clustering(self) -> dict:
"""Detect spatial or network clustering of opinions."""
latest = self.history[-1]
# Group agents by similar beliefs
clusters = []
assigned = set()
for i, agent in enumerate(latest):
if i in assigned:
continue
cluster = [i]
assigned.add(i)
for j, other in enumerate(latest):
if j in assigned:
continue
belief_distance = sum(
abs(agent['beliefs'].get(k, 0.5) - other['beliefs'].get(k, 0.5))
for k in agent['beliefs']
)
if belief_distance < 0.3:
cluster.append(j)
assigned.add(j)
if len(cluster) > 1:
clusters.append(cluster)
return {
'n_clusters': len(clusters),
'largest_cluster': max(len(c) for c in clusters) if clusters else 0,
'singleton_fraction': len([a for a in range(len(latest)) if a not in assigned]) / len(latest)
}
MiroFish-Style Prediction Simulation
The MiroFish Approach
MiroFish demonstrates a dual-platform approach where LLM-powered agents interact on simulated social media and discussion platforms to generate predictions about social phenomena.
class MiroFishSimulation:
"""
Dual-platform simulation for social prediction.
Agents interact on simulated Twitter-like and Reddit-like platforms.
"""
def __init__(self, seed_scenario: str, n_agents: int = 1000):
self.scenario = seed_scenario
self.platforms = {
'microblog': MicroblogPlatform(max_post_length=280),
'forum': ForumPlatform(thread_based=True)
}
self.agents = self._create_diverse_population(n_agents)
self.llm_cache = {} # Cache LLM responses for efficiency
def _create_diverse_population(self, n: int) -> list:
"""Create agents with diverse backgrounds for the scenario."""
archetypes = [
{'type': 'expert', 'fraction': 0.05, 'expertise': 0.9},
{'type': 'informed_citizen', 'fraction': 0.15, 'expertise': 0.6},
{'type': 'casual_observer', 'fraction': 0.50, 'expertise': 0.3},
{'type': 'contrarian', 'fraction': 0.10, 'expertise': 0.5},
{'type': 'influencer', 'fraction': 0.05, 'expertise': 0.4},
{'type': 'bot_like', 'fraction': 0.05, 'expertise': 0.1},
{'type': 'lurker', 'fraction': 0.10, 'expertise': 0.4}
]
agents = []
for archetype in archetypes:
count = int(n * archetype['fraction'])
for _ in range(count):
agent = self._create_agent_from_archetype(archetype)
agents.append(agent)
return agents
def _create_agent_from_archetype(self, archetype: dict) -> dict:
"""Create a richly defined agent from an archetype template."""
return {
'id': str(uuid.uuid4())[:8],
'archetype': archetype['type'],
'personality': AgentPersonality.random(),
'backstory': self._generate_backstory(archetype),
'platform_preference': random.choice(['microblog', 'forum', 'both']),
'posting_frequency': random.expovariate(1.0),
'follower_count': int(random.paretovariate(1.5) * 10),
'memory': [],
'beliefs': {}
}
def _generate_backstory(self, archetype: dict) -> str:
"""Generate a unique backstory for an agent."""
# In production, this would call an LLM
backgrounds = {
'expert': "Professional with deep domain knowledge",
'informed_citizen': "Engaged citizen who follows news closely",
'casual_observer': "Occasional social media user with moderate interest",
'contrarian': "Skeptic who questions mainstream narratives",
'influencer': "Content creator with large following",
'bot_like': "Account with repetitive behavior patterns",
'lurker': "Mostly reads, rarely posts"
}
return backgrounds.get(archetype['type'], "Generic participant")
def inject_scenario(self, scenario_event: dict):
"""Inject a scenario event into the simulation."""
# Post the event as breaking news on both platforms
self.platforms['microblog'].inject_content({
'type': 'breaking_news',
'content': scenario_event['headline'],
'details': scenario_event['details'],
'timestamp': self.current_time
})
self.platforms['forum'].inject_content({
'type': 'megathread',
'title': scenario_event['headline'],
'body': scenario_event['full_text'],
'timestamp': self.current_time
})
def run_prediction_cycle(self, question: str, n_rounds: int = 50) -> dict:
"""Run simulation and extract prediction from emergent behavior."""
self.current_time = 0
for round_num in range(n_rounds):
# Each agent takes actions based on personality
for agent in self.agents:
if random.random() < agent['posting_frequency']:
self._agent_action(agent, question)
self.current_time += 1
# Analyze emergent consensus
return self._extract_prediction(question)
def _extract_prediction(self, question: str) -> dict:
"""Extract a prediction from the simulation state."""
all_beliefs = [a['beliefs'].get(question, 0.5) for a in self.agents]
expert_beliefs = [
a['beliefs'].get(question, 0.5)
for a in self.agents if a['archetype'] == 'expert'
]
return {
'population_mean': sum(all_beliefs) / len(all_beliefs),
'expert_mean': sum(expert_beliefs) / len(expert_beliefs) if expert_beliefs else None,
'std_dev': (sum((b - sum(all_beliefs)/len(all_beliefs))**2 for b in all_beliefs) / len(all_beliefs)) ** 0.5,
'consensus_level': self._measure_consensus(all_beliefs),
'dominant_narratives': self._extract_narratives(),
'polarization': self._measure_polarization(all_beliefs)
}
def _measure_consensus(self, beliefs: list) -> float:
mean = sum(beliefs) / len(beliefs)
variance = sum((b - mean)**2 for b in beliefs) / len(beliefs)
return max(0, 1 - variance * 4) # Normalize to 0-1
def _measure_polarization(self, beliefs: list) -> float:
below = sum(1 for b in beliefs if b < 0.3)
above = sum(1 for b in beliefs if b > 0.7)
return (below + above) / len(beliefs)
def _extract_narratives(self) -> list:
"""Extract dominant narratives from platform content."""
# In production, would analyze posts with NLP
return []
def _agent_action(self, agent: dict, question: str):
"""Have an agent take an action on their preferred platform."""
pass # LLM-driven in production
Scaling to Millions of Agents
Performance Optimization Strategies
class ScalableSimulation:
"""Techniques for scaling agent simulations to millions."""
def __init__(self, n_agents: int):
self.n_agents = n_agents
def hierarchical_update(self, agents: list, n_levels: int = 3):
"""
Not every agent needs full LLM-driven updates every step.
Use hierarchical update frequencies:
- Level 1 (influencers, 1%): Full LLM update every step
- Level 2 (active users, 10%): Full update every 5 steps
- Level 3 (passive users, 89%): Rule-based update, LLM every 20 steps
"""
update_schedule = {
1: {'fraction': 0.01, 'llm_frequency': 1},
2: {'fraction': 0.10, 'llm_frequency': 5},
3: {'fraction': 0.89, 'llm_frequency': 20}
}
return update_schedule
def spatial_partitioning(self, agents: list, grid_size: int = 100):
"""
Divide agents into spatial cells.
Only compute interactions within and between adjacent cells.
Reduces O(n^2) interactions to O(n * k) where k is cell size.
"""
cells = {}
for agent in agents:
cell_x = int(agent.state.position[0] * grid_size)
cell_y = int(agent.state.position[1] * grid_size)
cell_key = (cell_x, cell_y)
if cell_key not in cells:
cells[cell_key] = []
cells[cell_key].append(agent)
return cells
def belief_compression(self, beliefs: dict) -> bytes:
"""
Compress agent beliefs for memory efficiency.
Instead of storing full belief dicts, use fixed-point arrays.
"""
import struct
topics = sorted(beliefs.keys())
values = [int(beliefs[t] * 255) for t in topics]
return struct.pack(f'{len(values)}B', *values)
def batch_llm_calls(self, agents: list, prompt_template: str,
batch_size: int = 50) -> list:
"""
Batch multiple agent decisions into single LLM calls.
Group similar agents and use one call per group.
"""
groups = {}
for agent in agents:
group_key = (
agent['archetype'],
round(agent['beliefs'].get('main_topic', 0.5), 1)
)
if group_key not in groups:
groups[group_key] = []
groups[group_key].append(agent)
results = []
for group_key, group_agents in groups.items():
# One LLM call represents the whole group
prompt = f"""You represent {len(group_agents)} {group_key[0]} agents
with current belief ~{group_key[1]}. {prompt_template}
Generate {min(len(group_agents), 5)} diverse responses."""
# response = await call_llm(prompt)
# Apply variations of response to all agents in group
results.extend(group_agents)
return results
Prediction Extraction from Simulations
Converting Agent States to Forecasts
class PredictionExtractor:
"""Extract calibrated predictions from simulation results."""
def __init__(self, simulation_results: list):
self.results = simulation_results # Multiple simulation runs
def ensemble_prediction(self, question: str) -> dict:
"""Combine predictions from multiple simulation runs."""
predictions = []
for result in self.results:
pred = result.get(question, {})
if 'population_mean' in pred:
predictions.append(pred['population_mean'])
if not predictions:
return {'error': 'No predictions available'}
mean = sum(predictions) / len(predictions)
std = (sum((p - mean)**2 for p in predictions) / len(predictions)) ** 0.5
return {
'point_estimate': mean,
'confidence_interval_90': (
mean - 1.645 * std,
mean + 1.645 * std
),
'n_simulations': len(predictions),
'inter_simulation_variance': std,
'reliability': 'high' if std < 0.1 else 'medium' if std < 0.2 else 'low'
}
def scenario_probabilities(self, scenarios: list[str]) -> dict:
"""Estimate probability of each scenario based on simulation outcomes."""
scenario_counts = {s: 0 for s in scenarios}
total = len(self.results)
for result in self.results:
closest_scenario = self._match_scenario(result, scenarios)
if closest_scenario:
scenario_counts[closest_scenario] += 1
return {
scenario: count / total
for scenario, count in scenario_counts.items()
}
def _match_scenario(self, result: dict, scenarios: list) -> str:
"""Match a simulation outcome to the closest predefined scenario."""
# Simplified matching logic
return scenarios[0] if scenarios else None
Key Takeaways
- Agent-based models capture nonlinear dynamics, tipping points, and emergent behaviors that equation-based models miss
- Agent personality diversity is critical: use realistic distributions from behavioral science, not uniform random
- Network topology dramatically affects outcomes: small-world networks produce different dynamics than scale-free ones
- Hierarchical update scheduling and spatial partitioning enable scaling to millions of agents
- Run multiple simulations with different random seeds and aggregate results for robust predictions
- The MiroFish dual-platform approach (microblog + forum) captures different types of social dynamics
- LLM-driven agents produce more realistic behaviors than rule-based agents, but require batching and caching for scale
- Emergent behavior detection (consensus, polarization, cascades) is where the predictive value lies
Install this skill directly: skilldb add prediction-skills