Files
mirosociety/docs/问题记录/问题记录-汇总-2026-09-28.md
T

17 KiB
Raw Blame History

问题记录汇总:模拟规模、居民兜底与搜索后端

调查日期:2026-09-28
状态:首轮代码优化已完成,待实际运行验证
本文同时保留问题证据、决策依据和后续未覆盖项。

一、文档用途

本文汇总以下三份原始记录,并根据当前代码重新核对结论:

原始记录保留作为发现过程和现场观察;本文作为当前阶段的统一判断,避免后续继续沿用已经过时的描述。

本轮已落地的范围:SearXNG 配置化调用与短缓存、完整指标进入报告和 JSONL 观测、确定性数据盲区检测、报告 Pass 5 系统性总结、居民兜底补生成与来源标记、抽样上限修正和抽样快照记录、时间配置校验,以及报告 schema 版本变更后的自动重生成。由于本轮之后尚未重新运行完整模拟,LLM 生成质量、报告耗时和真实搜索连通性仍需验证。

二、结论总览

问题 当前判断 说明
每轮只抽 3–10 人 部分成立,原描述已过时 TimeConfig 默认值确实是 3–10,但正常世界生成会按人口提示约 40% 的上限;当前 API 人口上限又只有 50。
“千人社会模拟”是否已实现 尚未成立 当前 API 不接受超过 50 人;即使放开人口上限,当前每轮 LLM 调用量、状态更新方式和成本模型也需要重新设计。
抽样是否有代表性 问题成立 当前是带情绪、派系、桥接节点、沉睡补偿等因素的加权抽样,不能直接等同于总体民意。
60% 是抽样硬上限 判断不准确 当前公式并不能保证最终人数不超过人口的 60%。
semaphore=10 即 TPS 没问题 判断过强 semaphore 只限制本地并发,不代表供应商 TPS、TPM、总耗时或总成本没有风险。
居民数量不足时的硬编码兜底 问题确认存在 解析失败、返回数量不足等情况都会静默补齐,并继续进入 persona 及后续生成流程。
兜底居民会污染模拟 问题确认存在 模板姓名、角色、性格钩子和立场会被继续送入 LLM,并可能影响 beliefs、生活史和关系。
DuckDuckGo 搜索不稳定 工程风险成立 当前代码仍直接使用 duckduckgo_search,没有 SearXNG 配置或缓存。
换成 SearXNG 就能保证重复性 不能直接保证 SearXNG 减少单一后端限流问题,但聚合搜索结果仍会随时间和上游引擎变化;需要缓存或结果快照。
报告“缺数据”或“读不懂” 数据缺失判断不成立,报告解释层问题成立 DB 会完整保存当前 WorldMetrics 的 13 个字段;JSONL 运行日志只记录 6 个指标。当前报告缺少时间线串联、证据约束和数据盲区说明。

三、问题一:模拟规模与每轮居民抽样

3.1 当前实现事实

  1. TimeConfig 默认值是:365 天、每天 3 轮、每轮最少 3 人、最多 10 人。
  2. WorldGenerator 会向 LLM 提示 active_agents_per_round_max 约为人口的 40%,缺省值也按这个比例计算。
  3. 如果 LLM 自己返回了 time_config,当前代码会优先接受 LLM 返回的值,没有对上下限关系做严格业务校验。
  4. API 当前明确限制人口为 2–50 人,因此目前产品入口并不支持 1000 人。
  5. _select_active_agents 采用加权、无放回抽样。情绪激动、属于派系、处于桥接位置、市场特征明显或沉睡较久的居民,权重会提高。
  6. “沉睡”补偿记录的是最近一次非 DO_NOTHING 行动,而不是最近一次被选中。被选中但没有行动的居民仍可能继续获得沉睡加成。
  7. 当前 max_active = max(base_count, 60%人口) 不是严格的 60% 上限。当 base_count 本身大于 60% 时,仍会保留这个更大的数量。
  8. 居民生成使用 generate_fast 先生成 cast/persona,生活史和关系在后台补充。因此当前也不是“所有居民的完整背景先生成完,再开始模拟”。

3.2 哪些原始判断需要修正

原记录把当前行为概括为“每轮固定 3–10 人”,这只对直接使用 TimeConfig 默认值的路径成立,不足以描述正常世界生成路径。

更准确的说法是:抽样参数同时受到模型默认值、世界生成结果和运行时人口规模影响;但产品入口目前最多 50 人,所以“千人社会”更多是产品叙事/未来目标,而不是当前 API 已支持的运行模式。

原记录把 60% 描述为上限也不准确,应改成“事件加成在特定范围内会受到一个不严格的 60% 参考值影响”。

3.3 问题仍然成立的部分

  • 抽样数量属于决定模拟质量和成本的核心参数,却与世界时间配置放在同一模型里。
  • 加权抽样会主动放大高情绪、高参与倾向的小群体;这可能是设计意图,也可能造成民意代表性偏差。
  • 仅查看“每轮抽中了多少人”不足以证明千人社会产生了有效涌现,需要观察居民覆盖率、重复参与率、行动率、沉默率和群体分布。
  • semaphore=10 只能说明本地同时执行的 LLM 请求有上限,不能单独推出“供应商 TPS 没问题”。

3.4 建议方向与首轮实现

  1. 将“世界时间配置”和“居民参与/LLM 预算配置”拆开。后者至少应包含抽样策略、每轮预算、最小/最大参与数、事件加成和是否允许全量参与。
  2. 明确“人口数量”的产品含义:是状态空间规模,还是每轮实际接受 LLM 决策的人数。两者不能用同一个数字直接代表。
  3. 如果目标确实是 1000 人,优先研究分层/代表性抽样、群体级状态更新、局部精细模拟和按需唤醒,而不是简单把人口上限和 active_agents_per_round_max 调大。
  4. 需要严格上限时,应重新定义抽样公式,并通过测试验证事件发生、人口较小、人口较大等边界情况。
  5. 增加运行指标:每轮选中数、实际行动数、居民最近参与间隔、每个居民累计参与次数、LLM 调用数、失败数、耗时和 token 成本。

首轮已实现其中的抽样快照、事件标记和 JSONL 完整指标记录;LLM 成本和每居民累计覆盖率仍需在新一轮模拟中补充统计。

四、问题二:居民生成兜底的硬编码污染

4.1 当前实现事实

当 _generate_cast 返回的居民少于目标数量时,代码会无条件补齐到 count。中文和英文分支分别使用程序化姓名、固定角色、固定年龄模式、固定性格钩子和固定立场。

触发来源包括:

  • LLM 截断或确实返回数量不足;
  • 返回对象结构不符合预期;
  • JSON 解析失败;
  • 中文重试后仍没有得到足够居民。

parse_json 在最终失败时返回空字典,因此整批失败会直接转化为整批兜底居民。

4.2 已确认的影响

  1. 兜底居民不是终态数据,会进入 persona 生成。
  2. 固定的 personality_hook、stance、角色和年龄会成为后续 LLM 的输入条件。
  3. 兜底居民缺失的字段会触发中位 personality、随机 communication style 等默认逻辑。
  4. generate_fast 会先把这些居民投入模拟,生活史和关系生成在后台继续进行;如果后台成功,它们还会继续进入后续阶段。
  5. 没有结构化的“本居民由兜底生成”标记,前端、叙事和报告无法区分正常居民与降级居民。
  6. warning 和 JSON 解析 error 会写入后端日志,但不会把“整批 LLM 失败”和“少返回几人”作为不同状态传递给调用方。

4.3 额外发现

中文姓名检查阶段直接对列表元素调用 .get()。如果 LLM 返回的数组中混入非字典元素,可能在兜底逻辑之前直接抛异常。居民条目的类型和必需字段目前也没有统一校验。

4.4 建议方向与首轮实现

  1. 把“数量不足”“结构错误”“JSON 解析失败”“LLM 调用失败”分成不同错误类型并记录指标。
  2. 对数量不足进行一次有界重试,仍不足时由产品选择:让模拟失败并提示重试,或进入明确标记的降级模式。
  3. 如果保留降级模式,不应使用固定的政治立场和同质化模板;至少要带有来源标记,并避免把模板立场直接注入 beliefs。
  4. 对 cast 做逐项 schema/type 校验,避免非字典元素或缺失字段导致新的异常。
  5. 把兜底数量、触发原因、是否影响前端展示和是否允许继续运行纳入验收标准。

五、问题三:DuckDuckGo 替换为 SearXNG

5.1 当前实现事实

当前 ResearchService 仍然直接导入并调用 duckduckgo_search.DDGS,配置中只有搜索开关和每轮最大搜索次数,没有 SearXNG 地址、token 或 timeout 字段。

当前研究流程已经具备每轮最多 5 个搜索请求的限制,并会并发执行这些请求;但没有查询短缓存,也没有记录搜索结果快照。

5.2 替换动机是否成立

成立:DuckDuckGo 依赖外部服务,存在反爬、CAPTCHA、限流和结果波动风险。自托管 SearXNG 可以把入口和认证控制收回来,但不能自动解决上游引擎波动,也不能单独提供严格重复性。

5.3 建议方案与首轮实现

  1. 使用 skill 文档中确认的部署地址、GET 方法和 token 认证方式;token 只通过环境变量注入,不写入代码或提交文件。
  2. 将 base_url、token、timeout、language、safesearch 和可选的 time range 配置化。timeout 暂定 15 秒是较平衡的起点。
  3. 将 SearXNG 的 title/content/url 适配为当前内部使用的 title/body/href,保持后续摘要逻辑稳定。
  4. 复用 httpx 的异步客户端,设置明确的连接、读取和总超时,并对非 200、JSON 错误和空结果分别记录。
  5. 默认不保留 DDG fallback,以免搜索结果在失败时悄悄切换来源;若确实需要,应做成显式的降级开关。
  6. 为“可重复模拟”增加按模拟 ID 或查询键保存的结果快照。单纯换成 SearXNG 不足以保证同一 query 得到同一结果。
  7. safesearch 不应硬编码在业务代码中。普通产品默认可以取 1;需要完整社会议题研究时,再由明确配置选择 0,并记录该选择。

六、问题四:报告缺少系统性总结与数据盲区

6.1 先区分“数据缺失”和“报告没有解释”

新增记录中最初的怀疑是“报告提到的冲突、自由度等指标可能没有被记录”。根据当前实现,这个判断需要修正:

  • WorldMetrics 当前实际定义了 13 个指标(原始记录写成 14 个,是数量统计误差);
  • store.save_metrics 使用完整的 model_dump_json() 保存指标;
  • get_metrics_history 会把完整 JSON 读回;
  • 报告生成使用 metrics_history 和 ReportAnalyzer,并非以 JSONL 日志为主要指标来源。

因此,DB 层的指标数据并没有因为 JSONL 日志缺字段而丢失。真正存在的是两个不同层面的问题:

  1. JSONL 日志只记录 6 个指标,导致人工排查和审计时容易误以为其他指标不存在;
  2. 报告生成 pipeline 没有把已有数据、时间线、人物行为、指标关系和数据盲区组织成一条可追溯的解释链。

6.2 当前报告 pipeline 的结构性缺口

当前报告大致包含 epoch 摘要、数据统计/预测分析、指标解释与洞察、分群分析、行动建议,最终组装为 executive brief、scorecard、insights、segments、action items、risks 等字段。

但最终报告没有专门的系统性总结字段,因此读者需要自行拼接:

  • epoch 摘要没有和后续洞察按时间轴串联;
  • 每个指标的解释基本独立生成,缺少指标之间的因果/矛盾关系;
  • insights 虽然在 prompt 中要求 evidence,但代码没有 schema 校验,LLM 仍可能返回没有具体 agent、day 或 action 支撑的结论;
  • 报告没有 data_gaps、limitations 或“本次模拟未覆盖什么”的正式字段;
  • 报告中的 narrative.summary 目前主要复用 executive brief,不等同于完整的历史叙事。

所以“报告读不懂”是一个真实的产品问题,但更准确的名称是“报告解释层和可追溯性不足”,不是数据库指标丢失。

6.3 数据盲区记录的使用边界

新增记录中列出的 adoption_rate、defect_count、trust、purchase_intent 和张力事件,是某次 30 天模拟的观测结果,适合成为该次报告的 data_gaps 或限制说明,但不能直接当作所有模拟都成立的系统规律。

还需要注意:trust 在 resolver 中并非只由 DEFECT 影响,其他行动也会对 trust 产生增减;因此“trust 只能因 DEFECT 衰减”的表述应改成“本次样本中没有出现足以让 trust 明显下降的有效行动,具体原因需要结合全部行动和指标轨迹判断”。

更稳妥的做法是将数据盲区分为两类:

  • 代码可检测盲区:例如某指标全程不变、某类行动次数为 0、某类事件发生但没有进入报告;
  • LLM 解释盲区:已有数据存在,但报告没有给出证据、时间点或指标间关系。

6.4 建议方向与首轮实现

  1. 增加一个独立的“系统性总结”阶段,把 epoch 时间线、指标全表、转折点、居民行为、分群结果、洞察和风险统一输入,输出带章节的完整叙事。
  2. 增加 data_gaps 或 limitations 字段,明确列出本次模拟无法回答的问题、原因和建议补充数据。
  3. 将 insights.evidence 设为结构化必填项;缺少具体 agent、day、action 或指标值的洞察不应直接进入最终报告。
  4. 对不变量和盲区优先使用代码规则检测,例如指标全程为 0、全程不变、某类行动从未发生,再让 LLM 负责解释其意义。
  5. 把 sim_logger 的 6 指标限制视为独立的可观测性问题修复;它不等于修复报告数据源,但能降低人工排查时的误判。
  6. 前端报告可以增加时间线章节、关键人物、矛盾剖析和数据盲区区块;这些属于后续实现,不在本次文档整理中执行。

首轮已增加系统性总结、章节、数据盲区展示和 Markdown 导出;关键人物/矛盾字段已由后端预留,但尚未做完整的前端专门区块。

6.5 推荐实施顺序(未来)

优先做“只增加 Pass 5 系统性总结 + data_gaps”,先验证报告是否从“指标拼盘”变成“有证据的故事”;再考虑开场旁白、缩短 epoch 粒度或扩展前端交互。不要先增加更多文学化文本,否则可能进一步掩盖证据不足的问题。

七、共同的业务问题

三个问题指向同一件事:产品对外描述的是“有大量居民、能产生社会涌现的模拟”,而当前实现中:

  • API 尚不支持千人规模;
  • 每轮真正进行 LLM 决策的是一个经过偏置的子集;
  • LLM 失败时可能静默制造模板居民;
  • 外部搜索结果不稳定且无法复现。

因此不能只把这些问题分别当成“一个参数、一个 fallback、一个搜索库”的工程整理。需要先确定产品承诺:模拟追求的是全体居民的细粒度决策,还是大规模社会的统计/分层涌现。

八、建议的处理顺序

阶段 0:先定口径

  • 明确支持的人口规模和“千人模拟”是否是当前能力还是未来目标。
  • 明确每轮居民参与率、可接受延迟、LLM 成本和结果重复性要求。
  • 决定居民生成失败时是阻断模拟,还是允许带标记的降级运行。

阶段 1:补观测,不先调参数

  • 记录抽样覆盖率、行动率、居民重复参与和沉睡补偿效果。
  • 记录居民生成短缺、解析失败、兜底数量和各阶段耗时。
  • 记录搜索成功率、超时率、结果来源和缓存命中率。

阶段 2:分别处理三条技术线

  1. 抽样策略与规模模型;
  2. 居民生成错误边界与降级策略;
  3. SearXNG 配置、适配、缓存和可重复性。

阶段 3:用小规模实验验证

先使用 25/50 人进行不同抽样策略和兜底策略的 A/B 对比,再决定是否扩大人口上限。不能先把人口直接放大到 1000,再用运行成本和结果噪声反推设计是否合理。

九、本轮尚未覆盖的事情

  • 尚未把人口上限扩展到 1000,也没有实现分层/群体级模拟;
  • 尚未根据真实供应商限流数据校准 LLM 成本和 TPS;
  • 尚未实现搜索结果按模拟 ID 的持久化快照;当前只有进程内短缓存;
  • 尚未实现报告 Pass 6 开场旁白、关键人物和矛盾区块的完整前端交互;
  • 尚未安装 pytest 并执行完整自动化测试套件;
  • 尚未重新跑一轮完整模拟验证新增 Pass 5、兜底补生成和 SearXNG 连通性。

本文用于后续决策和实施前对照。任何代码修改前,应先在本文对应章节补充具体决策、验收指标和回滚方案。