You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
9.4 KiB
9.4 KiB
Real City Inference & Direct Agent Control — Design
2026-09-18 位置:
docs/new_plan/(区别于docs/plans/里已归档的设计) 记录两个产品增强方向:(1) 规则文本里提到城市时自动启用真实人口结构;(2) 用户在模拟运行中可直接与居民交互(私聊、塞情报),把 MiroSociety 从「观察实验」升级为「可介入实验」。
背景
当前 MiroSociety 的产品定位是「看涌现」—— 用户定义规则,系统跑出结果。但在真实使用中,玩家常自然产生两类诉求:
- 我希望自己的规则能更接近真实:写「旧金山房租涨 30%」时,期望模拟的是真实的旧金山人,而不是 10 个随机抽样的居民。
- 我希望在中途介入:加天灾、跟某个人说句话、给某个人一条情报,看看这个社会对「定向刺激」会怎么反应。
这两类诉求都对应明确的代码改动,且与现有架构兼容。详见下方的「补齐方案」。
现状梳理
真实城市相关
- 后端字段定义:
backend/app/api/simulate.py:48city: str | None = None,无默认值 - 后端调用入口:
backend/app/api/simulate.py:139-155,仅在req.city有值时走CensusService.get_profile - 前端表单:
frontend/src/views/HomeView.vue:53城市输入框默认收起,只有点「+ 基于真实城市」才展开 - 前端 API 客户端:
frontend/src/api/client.js:23空字符串转null - 已支持城市列表:
backend/app/services/census.py:18-40,共 20 个美国城市 - 数据来源: 美国人口普查局 Census API,年龄段/收入/族裔/职业/贫困率
当前行为:用户在规则里写「旧金山」,系统不会自动匹配,智能体人口结构完全随机。这与「随便写就行」的产品直觉不符。
直接控制相关
| 玩家想做 | 当前能力 | 代码位置 |
|---|---|---|
| 加天灾 / 黑天鹅 | ✅ 支持,广播式 | engine.py:2116-2147 |
| 居民互相打听 | ✅ 支持(内部 INVESTIGATE 动作) | engine.py:1992-2047 |
| 居民互相私聊 | ✅ 支持(内部 SPEAK_PRIVATE 动作) | engine.py:1636 |
| 用户定向跟某居民说话 | ❌ 无接口 | — |
| 用户给某居民塞情报 / 改信念 | ❌ 无接口 | — |
| 暂停状态下介入 | ❌ 暂停期间只能 inject 事件,不能定向传话 | — |
技术上,运行时改智能体的 beliefs / core_memory / working_memory 已经支持(deliver_enriched_agents 是先例,见 engine.py:329)。缺的只是 API + 前端交互。
补齐方案
1. 规则文本中城市名自动匹配
思路
在 run_pipeline 的早期(状态切到 GENERATING_WORLD 之前),用轻量规则解析扫描 rules 文本:
- 若
req.city已有值 → 跳过,优先级用户显式 > 文本推断 - 若
req.city为空 → 在文本里找已知城市列表的别名(包括中英文、缩写),命中则自动填入req.city - 若未命中 → 行为不变,完全随机人口
数据:已知城市的别名表
在 census.py 里新增 CITY_ALIASES:
CITY_ALIASES: dict[str, str] = {
# 英文
"san francisco": "san francisco, ca",
"sf": "san francisco, ca",
"new york": "new york, ny",
"nyc": "new york, ny",
"los angeles": "los angeles, ca",
"la": "los angeles, ca",
# 中文
"旧金山": "san francisco, ca",
"纽约": "new york, ny",
"洛杉矶": "los angeles, ca",
"芝加哥": "chicago, il",
"西雅图": "seattle, wa",
"奥斯汀": "austin, tx",
"波士顿": "boston, ma",
"迈阿密": "miami, fl",
"亚特兰大": "atlanta, ga",
"底特律": "detroit, mi",
# ... 覆盖 20 个城市
}
改动范围
- 新增
backend/app/services/city_resolver.py— 文本扫描与别名匹配工具 - 修改
backend/app/api/simulate.py:139在if req.city:之前插入自动解析 - SSE 事件:新增
city_inferred事件,告诉前端「我们从规则里识别到了旧金山,已启用真实人口」 - 日志:记录推断过程,便于排查误识别
风险与缓解
- 误识别:「我旧金山的表弟今天结婚」这句话里出现「旧金山」,但用户是想讨论亲戚婚礼,不是想测试旧金山人 —— 缓解方式:把识别结果作为提示,前端弹出「我们识别到旧金山,是否启用真实人口结构?」让用户确认一次
- 歧义:某些城市名(如 Portland)在多个州都有 → 已在
CITY_FIPS里标注,匹配时选人口最多的那个 - 非美城市:目前只支持美国,匹配不到就不报错,行为不变
不做的事
- 不做模糊匹配(避免误识别),只做精确子串匹配
- 不做 NLP 抽取,只做字符串扫描 — 快、可控、易调试
2. 用户定向与居民交互
目标
让玩家可以在模拟运行中:
- 对某个居民说一句话(类似神谕/上帝视角 whisper),该居民下一轮会按自己性格回应
- 给某个居民塞一条情报(改她的 belief / core_memory),影响她后续判断
接口设计
新增两个 FastAPI 接口,都在 /simulation/{sim_id} 下:
class DirectSpeakRequest(BaseModel):
agent_id: int
message: str
as: Literal["god", "voice_in_head", "neighbor"] = "voice_in_head"
@router.post("/simulation/{sim_id}/direct-speak")
async def direct_speak(sim_id, req, request): ...
class PlantInfoRequest(BaseModel):
agent_id: int
claim: str # 例如「Alice 偷了你的东西」
target_agent_id: int | None # 可选,情报是关于某个人的
source_label: str = "reliable neighbor"
@router.post("/simulation/{sim_id}/plant-info")
async def plant_info(sim_id, req, request): ...
后端逻辑
direct-speak:
- 从
store.get_agent(sim_id, agent_id)拿到该居民 - 把
message写入agent.working_memory顶部:"Day {day}: You suddenly hear a voice in your head saying: \"{message}\". You're not sure where it came from, but it feels real." - 触发该居民在下一轮进入激活列表(
active_agents强制包含) save_agents_batch持久化- SSE 推
direct_speak_delivered事件
plant-info:
- 拿到目标居民
- 把
claim作为新信念加入agent.beliefs:"Day {day}: A {source_label} told you: {claim}" - 如果指定了
target_agent_id,同时降低目标居民在该居民relationships里的sentiment(0.2-0.4) - 持久化 + SSE 事件
info_planted
改动范围
- 新增
backend/app/api/direct_control.py - 修改
backend/app/main.py:110-113注册新路由 - 修改
backend/app/services/engine.py:407_select_active_agents支持「强制激活」列表 - 前端
frontend/src/components/AgentDetailPanel.vue增加两个按钮:- 「对她说句话」→ 弹小输入框
- 「给她一条情报」→ 弹输入框 + 可选「情报是关于谁的」下拉
状态机注意事项
- 暂停时调用:应当入队,resume 后下一轮再触发(避免和 main loop 抢锁)
- 取消时调用:直接拒绝,返回
409 simulation_cancelled - 已完成时调用:返回
410 simulation_complete,不允许干预 - 重复调用:同一居民同一轮多次 whisper,合并成一条 working_memory(避免爆栈)
隐私 / 公平设计
- 所有直接干预记入
action_log,标记来源是user_direct_speak/user_planted_info,便于事后复盘「这条信念是被植入的」 - 前端 UI 显示一个「✋ 用户干预」角标,让观众知道这个居民在某些点上被外力影响过
- 报告生成器(
narrator.generate_report)把用户干预事件作为单独章节列出
不做的事
- 不做多选批量干预(一次只针对一个居民,保持简单)
- 不做定时任务(不能在第 10 天自动给她塞情报),玩家手动控制时机
- 不做语音输入,只用文字
优先级建议
如果分阶段上线:
| 阶段 | 内容 | 预估工作量 |
|---|---|---|
| P0 | 城市自动匹配(方案 1) | 1-2 天,改动小,立刻提升「随手写规则」的体验 |
| P1 | direct-speak 接口 + 前端按钮 | 3-4 天,核心玩法升级 |
| P2 | plant-info 接口 | 2-3 天,在 P1 基础上复用持久化逻辑 |
| P3 | 干预标记 + 报告章节 | 1 天,可观测性补完 |
P0 单独立项即可上线,P1+P2 一起做可以串成「上帝模式」主题更新。
后续可能扩展
- 多选 / 批量干预:一次对一组人说话(类似群体喊话)
- 定时干预:cron 风格,在第 N 天自动触发某事件
- 干预模板:把常用的「告诉你邻居是坏人」「告诉你老板要裁员」做成快捷按钮
- 其他国家的城市:接入中国/欧洲的人口数据源,扩大可玩范围
- 真实天气事件 API:对接 NOAA / 和风天气,让天灾更真实