Inspiration
We kept coming back to one question: what happens when AI agents aren't just tools, but citizens?
City planners face an impossible problem. They need to predict how thousands of people will react to a tax reform, a factory closure, or a natural disaster. Surveys lie. Models oversimplify. And you definitely can't run experiments on real cities. The gap between policy and people is enormous — and it's filled with guesswork.
We wanted to build something different. Not another chatbot. Not another dashboard. A living city — one where every citizen has a name, a job, a personality, memories, and relationships. Where you could throw a recession at them and watch what actually emerges. Not scripted responses. Not predictable outcomes. Real, emergent, messy human behavior — simulated by AI.
The inspiration came from watching how people actually make decisions. A barista doesn't worry about the stock market. She worries about rent. A retiree doesn't panic about automation — she's already secure. A factory worker hears "AI revolution" and feels existential dread. We wanted to capture that. The Big-5 personality model gave us the framework: Openness, Conscientiousness, Extraversion, Agreeableness, and Neuroticism. Give 16 agents different scores on those axes, different incomes, different beliefs — and suddenly you don't need to program "baristas worry about rent." It just happens.
That's the magic of emergence. And that's what we built.
What it does
AI City Simulation is a multi-agent platform where every citizen is an autonomous AI agent with their own personality, memories, goals, profession, income, beliefs, and daily routines.
The City
- 6 zones: Downtown, Residential-North, Residential-South, Industrial, Suburbs, Waterfront
- 16 citizens: Each with a unique Big-5 personality profile, profession, income level, belief system, and mood
- 12 relationships: Friends, neighbors, coworkers — creating a social fabric
The Simulation
Users act as city planners. You can:
- Inject real-world scenarios — Economic Recession, Tax Reform, Natural Disaster, Housing Shortage, Public Health Crisis, Automation Wave, or custom events
- Run simulation ticks — Each tick, every agent makes an independent decision based on their personality, memories, and current events
- Watch emergent behavior — Agents interact with each other, form new beliefs, and their moods shift based on what happens
The Memory System
Every agent has a vector-based memory stream. Memories are stored with embeddings and retrieved using a composite scoring formula:
$$\text{retrieval_score} = 0.5 \cdot \text{cosine_similarity} + 0.2 \cdot e^{-0.693 \cdot \frac{\text{age}}{10}} + 0.3 \cdot \text{importance}$$
This means agents remember things that are relevant (similar to the current situation), recent (happened lately), and important (high-impact events). Just like humans.
Every 5 ticks, old memories get compressed — summarized into reflection memories. Agents forget the details but keep the gist. This prevents unbounded memory growth while maintaining the illusion of a living mind.
Citizen Interviews
Click on any agent and you can interview them. Ask them how they're feeling about the city. Ask about their job. Ask about the recession. They respond in-character, drawing from their personality, beliefs, and memories. A barista won't give you the same answer as a software engineer.
City Metrics
Track the pulse of your city:
- Unemployment rate — derived from distressed low-income agents
- Average income — computed across all citizens
- Sentiment index — a composite of all agent moods
- Housing occupancy — pressure from low-income residents
- GDP proxy — a formula combining income, employment, and mood stress
How we built it
The Architecture
We built a 3-tier system with clean separation:
Frontend (React + TypeScript + Vite)
│ HTTP API (proxied via Vite dev server)
Backend (FastAPI + SQLAlchemy 2.0)
│ Async PostgreSQL + pgvector
Database (PostgreSQL 16)
The Backend
The backend is a FastAPI application with async everything. Here's why that matters:
- 16 agents make decisions simultaneously — not sequentially. We use
asyncio.gatherwith a semaphore of 8 concurrent LLM calls per tick. That's 16 parallel Qwen API calls happening at once. - Dual-provider failover — Qwen Cloud is the primary LLM. The agent never stops thinking.
- Robust JSON parsing — LLMs don't always return perfect JSON. We strip markdown fences, attempt
json.loads, and if all else fails, extract the first{...}substring. The simulation never crashes because of a formatting quirk.
The core simulation loop (simulation_engine.py) runs like this:
for each tick:
1. Fetch active events and all agents
2. For each affected agent (concurrently):
a. Retrieve top-6 relevant memories
b. Build persona prompt with profile + memories + events
c. Call LLM for decision (action, mood, belief updates, new memory)
d. Write new memory to database
3. Run agent-to-agent interactions (up to 3 pairs per tick)
4. Compute city-wide metrics
5. Every 5 ticks: compress old memories
The Memory Manager
The memory system (memory_manager.py) is where things get interesting. Each memory is stored with:
- Content — what happened
- Embedding — a 384-dimensional vector from Qwen's
text-embedding-v1 - Importance — a score from 0 to 1
- Memory type — observation, reflection, interaction, or decision
Retrieval uses the composite scoring formula above. The weights were tuned through experimentation:
| Factor | Weight | Why |
|---|---|---|
| Relevance (cosine similarity) | 0.5 | The most important factor — is this memory related to the current situation? |
| Recency (exponential decay) | 0.2 | Recent memories matter more, but old important ones shouldn't be forgotten |
| Importance | 0.3 | High-impact events (a recession, a disaster) should be recalled even when old |
The Seed City
We spent a lot of time on the 16 seed agents (seed_city.py). Each one is a fully realized character:
| Agent | Age | Profession | Income | Personality Trait |
|---|---|---|---|---|
| Maria Chen | 28 | Barista | $2,200 | High Neuroticism, High Agreeableness |
| James Wilson | 45 | Factory Worker | $3,800 | Low Openness, High Conscientiousness |
| Sofia Rodriguez | 22 | Student | $1,200 | High Openness, High Neuroticism |
| Alex Novak | 31 | Gig Worker | $2,800 | High Neuroticism, Low Conscientiousness |
| Robert Thompson | 62 | Retiree | $4,500 | Low Openness, High Agreeableness |
| ... | ... | ... | ... | ... |
Each agent also has beliefs on topics like taxation, housing, automation, and public transit. These beliefs aren't static — they evolve based on experiences and events.
The Frontend
The frontend is a React 18 application with a custom "blueprint and ledger" design system. No CSS framework — 1,640 lines of bespoke CSS implementing:
- Navy/cyan/amber color palette — evoking architectural blueprints and financial ledgers
- SVG city map — 6 zones with agent dots color-coded by mood (cyan for positive, amber for neutral, coral for distressed)
- Recharts visualizations — sentiment trend lines, metric deltas
- Slide-out agent profiles — showing Big-5 personality bars, belief sliders, memory streams, and chat interfaces
The whole thing is built with Vite 6 for instant hot module replacement and optimized builds.
Deployment
We deployed to Alibaba Cloud ECS (ecs.c7.large: 2 vCPU, 4 GB RAM) with:
- systemd for process management
- Nginx as reverse proxy (static files, gzip, rate limiting, security headers)
- Let's Encrypt for SSL
- A free-tier optimization guide for running on 4 GB RAM (swap tuning, PostgreSQL memory limits, worker process sizing)
The deployment is fully scripted — three bash commands to go from zero to production.
Challenges we ran into
1. LLM Output Unreliability
The biggest challenge was getting consistent JSON from LLMs. Qwen don't always return perfectly formatted JSON. Sometimes they wrap responses in markdown fences. Sometimes they add explanatory text. Sometimes they hallucinate extra fields.
Our solution: A multi-layered parsing strategy:
- Strip markdown code fences with regex
- Attempt
json.loadsdirectly - If that fails, extract the first
{...}substring - If all parsing fails, return a safe default decision object
This made the simulation rock-solid — it never crashes because of an LLM formatting quirk.
2. Memory Compression Without Losing Context
Agents accumulate memories quickly. After 20 ticks, they'd have dozens of memories, and the context window would balloon. But we couldn't just delete old memories — a recession from 10 ticks ago still matters.
Our solution: Selective compression every 5 ticks. Only memories that are:
- Older than 15 ticks
- Have importance < 0.4
- Haven't already been summarized
...get compressed into a single "reflection" memory via LLM summarization. The original memories are marked as summarized but kept for reference. This mimics human forgetting — you lose the details but keep the emotional weight.
3. Concurrent API Rate Limits
Running 16 concurrent LLM calls per tick is aggressive. We hit rate limits constantly during development.
Our solution:
- Semaphore-based concurrency limiting (8 concurrent calls max)
- Automatic provider failover (Qwen → Groq).
- Exponential backoff on rate limit errors
- Staggered agent processing to spread load
4. Making Agents Feel Real
The hardest part wasn't technical — it was prompt engineering. How do you make a barista sound like a barista and not a generic AI?
Our solution: Each agent's prompt includes:
- Their full profile (age, profession, income, zone)
- Their Big-5 personality scores (driving tone and decision style)
- Their beliefs on relevant topics
- Their current mood
- Their top-6 most relevant memories
- The active events affecting them
The prompt explicitly instructs the LLM to respond in-character. The result: Maria Chen worries about rent. James Wilson fears automation. Robert Thompson stays calm because he's "been through worse."
5. Designing the UI Without a Framework
We wanted a unique visual identity — not another Bootstrap dashboard. But building a full design system from scratch is daunting.
Our solution: A "blueprint and ledger" aesthetic. Navy backgrounds, cyan accents for data, amber for warnings, coral for distress. 8px grid spacing. IBM Plex typography (sans for body, mono for data). The whole thing feels like looking at an architect's blueprint crossed with a financial ledger. 1,640 lines of CSS, zero dependencies.
Accomplishments that we're proud of
100% Decision Diversity
Every single tick, all 16 agents produce unique actions. No two agents ever do the same thing. This isn't guaranteed — it emerges from the diversity of their personalities, incomes, beliefs, and memories.
Emergent Behavior
We didn't program "baristas worry about rent." That behavior emerged from Maria Chen's $2,200 income, her housing belief, and the recession context. The LLM synthesized her profile and produced a character-appropriate response. This is the holy grail of agent simulation — behavior that isn't scripted but arises from the system.
Production-Grade Reliability
The system has dual-provider failover, robust JSON parsing, graceful error handling, and automatic memory compression. It runs unattended. It doesn't crash. It handles API failures gracefully. This isn't a prototype — it's a production system.
Full Deployment Pipeline
Three bash commands to go from zero to a live, SSL-secured deployment on Alibaba Cloud. The deployment scripts handle everything: server provisioning, package installation, database setup, systemd services, Nginx configuration, and SSL certificates.
The Memory System
The composite memory retrieval algorithm is genuinely novel. Most AI memory systems use simple similarity search. Ours combines relevance, recency, and importance — just like human memory. The exponential decay function for recency:
$$\text{recency_score} = e^{-0.693 \cdot \frac{\text{age}}{10}}$$
...means memories have a "half-life" of 10 ticks. After 10 ticks, a memory's recency score drops to 0.5. After 20 ticks, it's 0.25. But important memories (importance > 0.8) stay retrievable much longer.
Multi-Agent Efficiency
We built an efficiency dashboard that compares our multi-agent architecture to a hypothetical single-agent approach:
| Metric | Multi-Agent | Single-Agent |
|---|---|---|
| Parallelization | 16x concurrent decisions | Sequential processing |
| Memory isolation | Per-agent relevant context | Full city state required |
| Context efficiency | ~6% of full state per call | 100% redundancy |
| Decision quality | Personality-driven responses | Generic decisions |
What we learned
1. Personality Is Everything
The Big-5 personality model was the right choice. It's well-researched, psychologically grounded, and — most importantly — it produces distinct behavior. Two agents with different Neuroticism scores面对 the same recession will react completely differently. One panics. The other stays calm. That's not magic. That's math.
2. Memory Shapes Identity
Agents with different memories of the same event develop different beliefs. Two agents who both experienced a factory closure might draw different conclusions — one becomes anti-automation, the other becomes pro-retraining. Memory isn't just storage. It's identity formation.
3. Emergence Is Real
We expected emergent behavior. We didn't expect it to be this dramatic. When we injected a recession, the low-income agents immediately started forming new beliefs about housing and taxation. The high-income agents stayed relatively stable. The agents with existing relationships started having conversations about cutting expenses. None of this was programmed. It emerged from 16 AI minds processing the same crisis through different lenses.
4. Failover Is Non-Negotiable
If you're building on LLM APIs, you need provider failover. Qwen went down twice during our development. Qwen hit rate limits. Without failover, the simulation would have crashed both times. With failover, it didn't even notice.
5. Prompt Engineering Is an Art
The difference between a generic chatbot and a believable character is prompt quality. We spent hours refining the persona prompts. The key insight: don't just list facts about the agent. Give them a voice. Tell the LLM how this person thinks, not just what they know.
What's next for AI City Simulation
Scale to 100+ Agents
16 agents is a proof of concept. The real value comes with 100+ agents across multiple neighborhoods, income brackets, and social networks. We're optimizing the memory system and concurrency model to handle this scale.
Real-Time Event Streaming
Currently, you inject a scenario and run ticks manually. Next: real-time streaming where events unfold automatically, and you watch the city evolve in real-time. Think "SimCity meets live AI."
User-Created Custom Scenarios
The 7 preset scenarios are just the beginning. We're building a scenario builder where users can define custom events with custom descriptions, severities, and scopes. "What if a tech company moves into the Industrial zone?" — you should be able to test that.
Export and Snapshot Capabilities
City planners need reports. We're adding export functionality — PDF summaries, CSV metric dumps, and shareable city snapshots. Save a city state, share it with colleagues, and let them inject their own scenarios.
Integration with Real Urban Data
The long-term vision: connect to real census data, economic indicators, and urban planning datasets. Seed agents from actual demographic distributions. Test policies against simulated populations that mirror real cities.
Multi-City Simulation
Run parallel cities with different policies and compare outcomes. "What if City A raises taxes while City B cuts them?" Watch the divergence over 50 ticks.
Built With
- alibaba-cloud-ecs
- fast-api
- pgvector
- qwen
- qwen-cloud
- react
- sqlalchemy
Log in or sign up for Devpost to join the conversation.