我是如何让 LLM 只用真实情报推理,构建多 Agent 闭合世界推演系统的

1 阅读6分钟

起点:LLM 推理的核心缺陷

大模型做"未来推演"有一个根本性问题——它会幻觉

你问它"如果美国封锁霍尔木兹海峡会发生什么",它会给你一个听起来非常合理但完全脱离现实的答案。这是因为 LLM 在回答时混用了训练数据里的"通识知识"和它凭空推断的"情报",你无法分辨哪些是真实存在的,哪些是捏造的。

我的解决方案:先把现实世界的信息搜下来,建成一个闭合的小世界快照,然后只允许推理在这个快照内进行

这就是 MiniWorld-Evolution 的核心——两层管线,L1 构建闭合世界,L2 在其中演变。


L1:闭合小世界构建

L1 的目标是从真实世界搜索信息,构建一个包含实体和关系的闭合图谱。

假设驱动的三维度搜索(v2 升级)

最初的设计是让 LLM 直接生成扁平的搜索关键词列表——效果很差,因为关键词没有结构,搜索结果噪声大。

v2 改成了假设驱动的三维度框架

维度 1: 影响因素    分析背景+关注点,列出5个以上关键影响力量
维度 2: 关键实体    识别8-15个参与实体(human + nature 双类型)
维度 3: 关键问题    生成5个以上需要搜索回答的核心问题

基于三个维度,生成带有 target_source 路由的 SearchTask 列表:

  • "news" → Tavily 新闻搜索(适合时事、政策动态)
  • "social" → Bocha 多模态(适合舆论观点、各方讨论)
  • "report" → Tavily 深度 + Bocha 复合(适合研究报告)

按维度分别聚类采样(自实现 KMeans++,不依赖 scikit-learn),确保每个维度的信息都有代表性结果进入提取阶段。

三级收敛检测

L1 是一个迭代管线,什么时候停是个关键问题。我设计了三级收敛检测:

# Level 1 — 结构完整性(NetworkX)
G = nx.Graph()
# 条件1: 弱连通图(所有实体互相可达)
is_connected = nx.is_connected(G)
# 条件2: 无孤立节点
isolated = [n for n in G.nodes() if G.degree(n) == 0]

# Level 2 — 功能完整性(算法)
# 检查: human/nature 比例、关系密度、实体覆盖关注点

# Level 3 — 语义完整性(LLM 反思)
# 仅 L1+L2 都通过后才执行
# LLM 判断: 快照是否能自洽解释背景+关注点

三级全过才算收敛,否则继续下一轮搜索。这比固定跑几轮要靠谱得多——有时候 2 轮就够,有时候 4 轮才收敛。

能力边界而非具体动作(v2 动作空间升级)

收敛后,L1 为所有 human 类实体生成 agent_promptaction_space

这里有一个踩过的坑,值得单独说:

v1 的做法是从已搜索到的 evidence 中推导"具体动作",比如:

{"do": ["发动空袭击杀伊朗最高领袖哈梅内伊"]}

问题来了——搜索资料里记录的是截至今天已发生的事。如果搜索结果里已经有"哈梅内伊在空袭中遇害",那这个动作就不应该再出现在动作列表里。结果我们在演变中看到了 Agent 在 Tick 1 和 Tick 3 反复"击杀同一个已死的人"。

v2 的做法是让 L1 定义能力边界,不定义具体动作:

{
  "do": {
    "enabled": true,
    "scope": "对伊朗军事目标实施精确打击",
    "influence_targets": ["伊朗", "伊朗石油设施", "霍尔木兹海峡"]
  }
}

具体行动由 L2 的 Agent 在每个 Tick 根据实时世界状态自主创造。


L2:闭合小世界演变

L2 的核心是 WorldLLM(世界规则引擎)× AgentRunner(Agent 决策执行器) 协作。

每个 Tick 的流程

Step 1: WorldLLM.assess()
  → 基于全量历史叙事评估当前局势
  → 输出:核心矛盾 + 关键决策实体

Step 2: WorldLLM.plan()(v2 改动:不再调用 LLM)
  → 仅从 state.get_all_status_summary() 获取统一世界快照
  → 所有 Agent 看到相同的客观世界
  → 为每个 Agent 单独准备专属情报(信息不对称分发)

Step 3: AgentRunner × N(并行)
  → 每个 human Agent 都参与,自主决定 act/wait
  → 输入:统一世界快照 + 专属情报 + 历史行动记录
  → 输出:do/decide/say/wait + 具体行动描述 + 推理过程

Step 4: WorldLLM.propagate()
  → 将 Agent 行动按关系图传播
  → 更新实体状态(human + nature)

Step 5: WorldLLM.narrate()
  → 生成本 Tick 叙事摘要
  → 判断是否触发终止条件

信息不对称的设计

这是我觉得最有意思的机制。在真实世界里,不同实体获取信息的能力天差地别:

  • 军事力量能拿到情报机构的卫星侦察数据
  • 经济组织更擅长获取供应链内部数据
  • 政治人物可以拿到外交渠道的非公开信息

Step 2 时,WorldLLM 为每个 Agent 定制一份"专属情报",代表该实体凭借自身能力额外获取的信息——不重复公开事件时间线中已有的内容,只补充该实体视角的独家信息。

所有 Agent 基于相同的公开事件 + 差异化的专属情报做决策,这才是演变结果多样化的根源。

v2 Agent 升级:历史记忆

v2 之前,Agent 决策时只看到当前世界状态,不知道自己之前做了什么。导致了"反复击杀同一人"的问题。

v2 的解决方案:为每个 Agent 构建 action_history,在每次决策时把该 Agent 自己的所有历史行动传进去。Agent 知道"我在 Tick 1 发动了空袭,在 Tick 2 宣布了停火",就不会再去重复执行已无意义的动作。


可视化层

后端 FastAPI + SSE 实时流式推送,前端 Vue 3 + Cytoscape.js 渲染实体关系图。

关键设计原则:零侵入。可视化层完全通过 import 复用引擎代码,不修改任何已有文件。

效果是:

  • 构建阶段:能看到实体和关系边一条一条地冒出来
  • 演变阶段:每个 Tick 的叙事滚动出现,节点高亮显示受影响的实体

可视化截图转存失败,建议直接上传图片文件


踩过的坑总结

问题解法
搜索结果噪声大假设驱动三维度框架 + 工具路由 + 按维度聚类采样
不知道何时停止构建三级收敛检测(图论 → 功能覆盖 → LLM 反思)
Agent 重复执行已发生的事action_space 从"具体动作"改为"能力边界"
Agent 没有历史记忆每次决策传入该 Agent 完整的行动历史
先后执行的 Agent 有信息差所有 Agent 并行决策,基于相同的上一 Tick 快照

快速开始

git clone https://github.com/zn-ffmb/MiniWorld-Evolution
pip install -r requirements.txt

# 配置 .env(抄 .env.example)
# 至少需要: LLM API Key + Tavily 或 Bocha 搜索 Key

# 构建世界
python run_build_world.py --background "美伊战争" --focus "石油价格与航运"

# 演变
python run_evolution.py \
  --world worlds/xxx.json \
  --perturbation "伊朗宣布完全封锁霍尔木兹海峡" \
  --max-ticks 10

GitHub:github.com/zn-ffmb/Min…

欢迎 Star ⭐,有问题 / 建议可以开 Issue 讨论。