一、前言

在做狼人杀 AI Agent 的过程中,我发现一个很有意思的问题:
很多时候,Agent 表现不好,并不是因为它“不知道怎么玩狼人杀”,而是因为它把不该当成事实的东西当成了事实。

比如它会突然说:

        User 是真人玩家,所以威胁更大

        Bot3 位置居中,可能是神职

        某玩家发言少,所以可疑

        狼队已经刀过某人,所以女巫可能救了他

这些判断看似像推理,其实很多都是幻觉。尤其在狼人杀这种强状态、强隐藏信息的游戏里,当前局事实必须由Game Engine管理,而不是交给 LLM 自己记忆或猜测

所以我没有直接接一个传统向量数据库 RAG,而是实现了一个更适合狼人杀 Agent 的轻量策略 RAG。

二、为什么不直接用传统 RAG

传统 RAG 很适合查资料,比如:狼人杀女巫怎么玩?预言家首夜应该验谁?狼人白天怎么发言?但如果直接把历史局、攻略、发言样例都放进向量库,Agent 很容易把“策略经验”误当成“当前局事实”。

比如检索到一条历史经验:强势玩家通常应该首刀。模型可能会在当前局里说:User 是核心玩家,应该首刀。但当前局里 User 未必发言,也未必强势。这就会污染推理。

所以我把信息分成两类:

当前局事实:GameState / WorldModel / PrivateKnowledge 管理
通用策略:Strategy RAG 提供参考

三、策略 RAG 的设计

我新增了一个独立模块并在里面定义了一个策略卡结构:

@dataclass(frozen=True)
class StrategyCard:
    id: str
    title: str
    role: str
    phases: Set[str]
    actions: Set[str]
    tags: Set[str]
    content: str
    priority: float = 1.0
    conflict_group: str = "general"
    max_per_team: int = 99

每张策略卡描述一个通用策略,比如:

StrategyCard(
    id="seer_night_first_neutral",
    title="预言家首夜中性查验",
    role="Seer",
    phases={"night"},
    actions={"seer_check"},
    tags={"night1", "low_info"},
    content="首夜没有公开信息时,查验应在有效目标中中性选择,禁止因 User/Bot、座位编号或玩家习惯做判断。",
    priority=1.5,
)

再比如狼人首夜:

StrategyCard(
    id="wolf_night_first_consensus",
    title="首夜随机一致",
    role="Wolf",
    phases={"night"},
    actions={"night_kill", "discussion"},
    tags={"night1", "low_info", "team"},
    content="第1夜没有公开发言和投票时,只能用随机或跟随队友的中性理由;后发言者优先保持队伍一致。",
    priority=1.5,
)

这样做的好处是策略是稳定的,但不会伪造成当前局事实。

鉴于策略数量有限性,我们的这个 RAG 没有使用向量数据库,而是用标签匹配和打分。根据当前局状态自动推断标签:

def infer_situation_tags(state, phase, action_type):
    tags = {phase, action_type, "public"}

    if turn <= 1:
        tags.update({"early", "night1"})
    elif turn <= 3:
        tags.add("mid")
    else:
        tags.add("late")

    if not state.get("messages"):
        tags.add("low_info")

    if state.get("day_vote_info") not in ("", "今日尚未投票"):
        tags.add("vote")

    if 发言里出现“预言家 / 查验 / 金水 / 查杀”:
        tags.add("seer_claim")

然后按角色、阶段、动作、标签重合度打分:

score =
  role_score
+ tag_overlap * 1.2
+ card.priority

最后返回最相关的几条策略。输出时会明确提醒模型:

策略RAG(通用策略参考,不是当前局事实;不得把策略内容当成已发生事件):
- 预言家首夜中性查验:...
- 预言家查验对跳与焦点:...

这是很关键的一句。它告诉 LLM:这些只是策略,不是当前局已经发生的事情。

我把策略 RAG 接入了几个核心位置。

  1. 狼人夜间讨论
retrieve_strategy_text("Wolf", "night", "discussion", state)

用于提醒狼人:第一夜没有信息只能随机或跟随队友;后发言者要知道前面队友说了什么;不要把讨论提议当成已经发生的刀人结果。

        2.狼人白天发言

原来狼人白天策略是随机选的:

wolf_strategies = ["悍跳预言家", "跳女巫", "指控某人可疑", "站边某人", "划水", "分析局势"]
wolf_strategy_assignments[wolf] = random.choice(wolf_strategies)

现在改成:

wolf_strategy_assignments = assign_wolf_day_strategies(wolves_ai, state)

这一步很重要,因为多个狼人如果随机策略,很容易出现冲突而导致狼人暴露。

        3.预言家夜间查验

retrieve_strategy_text("Seer", "night", "seer_check", state)

用于约束:首夜不能因 User/Bot 差异查验;不能因位置编号查验;中后期优先查验能解释局势的焦点位

        4.女巫夜间用药

retrieve_strategy_text("Witch", "night", "witch_night", state)

用于提醒:首夜只基于系统刀口和药水状态;不要因虚构发言或玩家名字用药;毒药尽量等到有明确矛盾或查杀时使用。

最后,我来讲解一下狼人如何避免决策冲突。为了避免同队策略互撞,我给策略卡加了两个字段:

conflict_group: str
max_per_team: int

分配函数是:

def assign_wolf_day_strategies(wolves, state):
    assignments = {}
    used_groups = {}

    for wolf in wolves:
        cards = retrieve_strategy_cards("Wolf", "day", "speech", state)

        for card in cards:
            if used_groups[card.conflict_group] < card.max_per_team:
                chosen = card
                break

        assignments[wolf] = chosen
并且每个狼人拿到策略时,都会附带协同约束:
狼队协同约束:
同一白天不要多人采用同一种强身份策略;
若队友已经跳身份,你只能支援、质疑对手或低调分析,不能重复悍跳。

这能明显减少狼队内部互相打架的问题。

四、为什么这个实现比“大 RAG”更适合当前项目

这个项目当前最需要的不是一个庞大的知识库,而是稳定性。

狼人杀 Agent 最怕三件事:

  1. 忘记自己的身份
  2. 编造当前局不存在的信息
  3. 队友之间策略互相冲突

传统 RAG 对第 1 点帮助不大,对第 2 点甚至可能有副作用。而这个轻量策略 RAG 主要解决第 3 点,同时辅助第 2 点。它遵守一个原则:

RAG 只提供策略,不提供事实。

这次我没有追求复杂的向量数据库,而是做了一个更贴合狼人杀 Agent 的小型策略 RAG。它的定位很明确:不是知识问答系统,不是当前局记忆系统,而是策略参考系统

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐