Files
mirosociety/docs/问题记录/问题记录-2026-09-28-报告缺失系统性总结.md

12 KiB
Raw Permalink Blame History

问题记录:2026-09-28 — 报告缺失"系统性总结",概念无数据来源

状态:待处理 记录人:项目相关方 触发场景:跑完一次 30 天模拟(sim_id=8a2c6953-224,10 人 / 90 轮 / 2238 秒 / 无报错),拿到报告 PDF 后读不懂。 与已有记录的关系:第四份问题记录。前三份见 目录


一、背景与现象

30 天模拟正常跑完(engine_finished + simulation_run_complete 都在日志里,无 pipeline_failed),但生成的报告读起来"不明不白":

  • 概念讲得含糊:"低冲突与高信任掩盖了深层不稳定"——读完不知道具体指哪个字段
  • 怀疑有数据没被记录/没被带出来:"可能是不完整,或者有些该说的东西没说或者没记录"

本文件拆解这个"看不懂",并给出改造方案。


二、先纠正一个误判:数据其实是全的

我最初怀疑"日志只存了 6 个指标,导致报告里其他概念找不到数据来源"。这个判断是错的,实际排查结果:

2.1 DB 里 14 个指标全存

WorldMetrics 定义了 14 个字段:

stability, prosperity, trust, freedom, conflict,
brand_sentiment, purchase_intent, word_of_mouth, churn_risk, adoption_rate,
information_spread, echo_chamber_index, rumor_distortion

store.save_metrics 用 metrics.model_dump_json() 全量序列化,get_metrics_history 原样反序列化返回。

所以 conflict(冲突)、freedom(自由度)这些"报告里提到但日志里没看到"的指标,DB 里一直有。

2.2 只有 JSONL 日志是 6 个

sim_logger.log_round 只写 6 个 market/social 指标:

"metrics": {
    "brand_sentiment": ..., "purchase_intent": ..., "churn_risk": ...,
    "adoption_rate": ..., "stability": ..., "trust": ...,
}

结论:logs/*.jsonl 缺 conflict / prosperity / freedom / information_spread / echo_chamber_index / rumor_distortion 是日志抽样行为,不是数据丢失。报告的数据源是 DB,数据完整。

2.3 真正的问题定位

既然数据是全的,那"看不懂"只能是报告生成 pipeline 的加工方式问题。这才是本记录的核心。


三、报告生成 pipeline 现状

narrator.generate_report 现在是 5 段式:

Pass 输入 输出 实现位置
0 全部叙事文本 每 30 天一个 epoch 摘要(历史学家口吻) L240-L288
1 指标历史 + 行动 + 居民 metric_stats / agent_stats / segment_stats / inflection_points / confidence L290-L297
1b metrics_history forecast 因果 / 反事实 / 一致性 L299-L326
2 epoch 摘要 + metric_stats + inflection + agents executive_brief(headline+summary)、metric_explanations、insights(3-5 条) L328-L380
3 segment_stats 每 segment 的 reaction / objection / champion / 代表语 L382-L415
4 brief + insights + segments + scorecard action_items / risks / second_order_effects L417-L441

最终 report 字典在 L452-L478 组装。


四、"看不懂"的四个具体成因

4.1 缺少"时间轴串联",产物是拼盘不是故事

Pass 0 产出了按时间顺序的 epoch 摘要,Pass 2/3/4 又产出了洞察、分群、建议,但没有任何一个 pass 把这些按时间轴串成一篇完整叙述。

最终结构是:

executive_brief.summary   ← 1 段话
scorecard                 ← 9 行「指标: 评级 (值) — 解释」
insights                  ← 3-5 条
segments                  ← N 段
action_items / risks      ← 各 3-5 条

读者要自己在脑子里把这些拼回去,所以"每个部分都懂,合起来不知道在讲什么"。

4.2 metric_explanations 是逐指标独立生成,没有交叉引用

INTERPRET_METRICS_SYSTEM 要求"每个指标一句解释 WHY"。这导致:

  • 每条解释孤立,不说"trust 高是因为 conflict 低"这类指标间关系
  • 读者看到 trust=100/100 和 conflict=低/xx 两行,不会自动联想到"这俩矛盾"
  • 于是报告只能靠 Pass 2 的 insights 补一句文学化描述(如"掩盖了深层不稳定")——这句在替数据做结论,但没给出推导过程

4.3 insights 允许"不可追溯",且有 surprise 类型

INTERPRET_METRICS_SYSTEM 要求 insights "Things that were NOT predictable from the simulation setup alone",且 evidence 是可选字段(L71-L75)。

后果:LLM 可以写一条听起来很有洞察、但没有具体 agent / day / action 支撑的结论。这正是"讲得不明不白"的直接来源——读者无法判断哪句有据、哪句是猜的。

4.4 没有任何地方标注"数据盲区"

report 字典里(L452-L478)没有 data gaps / limitations 字段。

本次 30 天模拟的真实盲区(从 logs/8a2c6953-224.jsonl 尾部实测):

盲区 实测值 说明
adoption_rate 全程 0.0 90 轮无人 PROPOSE_RULE → "制度演化"叙事无数据支撑
defect_count 每轮 0 has_defected 一次性标记 + 无回退机制(见 resolver.py:178)→ 叛徒事件结构性稀缺
trust 恒定 1.0 衰减只由 DEFECT 触发(resolver.py:545-L549),DEFECT=0 → trust 永不下滑
purchase_intent 0.458→0.458 30 天内几乎不动 → "经济演化"在 30 天尺度不可见
张力事件 15 次 但 tension_event 内容未进报告任何字段

报告完全没提这 5 点。 读者只能靠自己去翻 JSONL 才发现——这就是"该说的东西没说"的实质。


五、方案:在现有 pipeline 后加 Pass 5 / Pass 6

5.1 Pass 5:系统性总结(核心)

目的:把前面所有产物按时间轴 + 矛盾剖析 + 数据盲区串成一篇完整叙述。

输入(全部来自已有产物,不新增数据源):

  • epoch_summaries(Pass 0)
  • scorecard 含 explanation(Pass 2)
  • insights(Pass 2)
  • segments_final(Pass 3)
  • action_items / risks / second_order_effects(Pass 4)
  • inflection_points
  • metric_stats 全量 14 字段(DB 已有)
  • meta(agent_count / total_days / total_actions / rules / world_name)

建议 prompt 骨架:

你是 {world_name} 的历史学家,刚刚目睹了一场为期 {total_days} 天、
{agent_count} 人、{total_actions} 次行动的社会实验。

# 观测数据(全部)
## 时间轴摘要
{epoch_text}

## 指标全表(含 14 项原始值 + 评级 + 逐项解释)
{metrics_full_table}

## 关键转折点
{inflection_summary}

## 居民快照
{agent_summary}

## 分群结果
{segments_text}

## 已有洞察 / 风险 / 建议
{insights_text}

请写一篇系统性总结,结构如下:

1. 【开场】150 字:一句话点出这场模拟最核心的故事
2. 【起承转合】按时间轴分段,每段 300-500 字:发生了什么、为什么、谁推动
3. 【关键人物】3-5 个对结果影响最大的居民,各 200 字:性格 → 选择 → 后果
4. 【矛盾剖析】300 字:把"表面读数"和"深层动力"分开,指出哪些是规则必然、哪些是涌现
5. 【数据盲区】200 字:明确列出这份数据**不能**回答的问题

硬性要求:
- 每个断言必须能追溯到上面某个具体指标值、某个具名居民、或某个具体 day
- 不许用"也许/可能/或许";要么写"数据显示 X",要么写"数据未覆盖"
- 不要重复 executive_brief 的 verdict,但要呼应它
- 指标必须带原始数值出现(如"trust 从 0.50 到 1.00"),不许只说"很高"
- 中文人名直接用原文

返回 JSON:

{
  "narrative_summary": "完整正文(Markdown,1500-2500 字)",
  "chapters": [
    {"title": "...", "body": "...", "evidence_days": [1, 15, 30]}
  ],
  "key_figures": [
    {"name": "...", "arc": "...", "evidence_days": [3, 20]}
  ],
  "contradictions": [
    {"surface": "...", "underlying": "...", "evidence": "..."}
  ],
  "data_gaps": [
    {"gap": "...", "why_it_matters": "...", "suggested_fix": "..."}
  ]
}

data_gaps 字段是这次改造的关键收益——它把第四节列的 5 个盲区变成报告的正式输出,读者不用再自己去翻日志。

5.2 Pass 6:开场旁白(可选,营造"故事感")

目的:你提到的"像旁白一样讲完这个故事"。

输入:executive_brief.headline + top3 居民 + top3 转折点

prompt 骨架:

你是 {world_name} 的旁白者,为一份模拟报告写开场词。

核心事件:{headline}
主线人物:{top3_agents_with_traits}
关键转折:{top3_inflections}

用 300 字写一段旁白:
- 第一句必须是一个具体画面或场景(有人/有地方/有动作),不要"在这个社会中"
- 中间三句讲清起承转合
- 最后一句留一个未解的问题
- 不解释数据,不下结论,不出现指标名

5.3 前端改动

ReportView.vue 增加两个区块:

  • narrative_summary(全文渲染,支持 Markdown)
  • chapters(时间轴折叠列表,点击跳转到对应 day)
  • data_gaps(高亮显示,建议用警示色,因为这是"诚实度"卖点)

key_figures 和 contradictions 可折叠,默认收起。


六、改动量评估

方案 改动 LLM 调用增量 效果
A. 只加 Pass 5 narrator.py +100 行、1 个新 prompt、ReportView.vue +40 行 +1 次(约 10-20s) 报告从拼盘变故事,且带数据盲区
B. A + Pass 6 再 +40 行 +1 次 额外开场旁白,阅读体验更好
C. A + epoch 粒度细化 epoch 从 30 天改 7 天 +3~4 次 时间轴更细,但收益递减
D. 修 sim_logger 指标缺失 sim_logger.py +4 行 0 只影响日志可观测性,不影响报告(报告读 DB)

建议先做 A,验证效果后再决定要不要 B/C。D 是独立的可观测性改进,成本极低可顺手做。


七、待澄清

  • Pass 5 的 1500-2500 字对 30 天模拟够吗?长模拟(365 天)是不是要按 epoch 动态伸缩?
  • data_gaps 要不要让 LLM 生成,还是用代码硬规则检测(如"adoption_rate 全程为 0 → 报为盲区")?后者更可靠但覆盖不全
  • insights.evidence 是否应改为必填,LLM 填不出证据就不让出该 insight?这样能根治"讲得不明不白"
  • 报告 PDF 是渲染型(pypdf 抽不出文字),Pass 5 的 Markdown 需确认导出链路是否支持

八、相关代码位置