12 KiB
问题记录: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_pointsmetric_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 需确认导出链路是否支持
八、相关代码位置
- 报告生成主流程:
backend/app/services/narrator.pygenerate_report - Pass 2 指标解释 prompt:
backend/app/services/narrator.pyINTERPRET_METRICS_SYSTEM - Pass 3 分群 prompt:
backend/app/services/narrator.pySEGMENT_ANALYSIS_SYSTEM - 纯统计计算:
backend/app/services/report_analyzer.py - 指标定义(14 项):
backend/app/models/world.pyWorldMetrics - 指标持久化:
backend/app/db/store.pysave_metrics/get_metrics_history - JSONL 日志(只 6 项):
backend/app/services/sim_logger.pylog_round - 报告前端:
frontend/src/views/ReportView.vue - 本次模拟日志:
backend/logs/8a2c6953-224.jsonl