让 LLM 自己复盘——三段式 insight 是怎么跑起来的

一开始让 LLM 接管游戏决策的时候,思路特别朴素:把当前局面塞给它,让它出招。打完一局,对话扔掉,下一局从头开始。

跑了几天就坐不住了——同一种局面,同一个坑,它能给你掉进去十几次。每次都是新人入职的状态,前面踩过的雷一概不记得。塞进 system prompt?没几局就把上下文撑爆了;塞进 RAG?流水账日志一堆,检索出来全是噪音。

最后落地的方案是”让它自己整理笔记本”,分了三个角色:generator 写,reviewer 改,selector 挑。听起来像办公室流程,实际跑起来还挺顺。

为什么不一步到位

最早试过最偷懒的版本:打完一局直接让 LLM 总结一句”经验”,扔库里。

效果非常糟糕。

总结出来的东西大概长这样:

本局我打得不错,注意了体力管理,下次继续保持。

这玩意儿写进笔记本,下次检索出来等于没看。再仔细一想,问题挺典型——你让一个刚打完一局、还沉浸在结果里的人当场写复盘,他给你的多半是情绪,不是结论。

人写复盘也得过几道:先把流水账记下来(发生了什么),再回头审视(这事儿哪里能改),最后挑出真正值得记住的(下次别忘了)。这三件事性质完全不同,硬塞给一个 prompt 就是难为模型。

拆开。

三个角色各干什么

我直接把 prompt 目录贴出来,一看就明白:

1
2
3
4
gameplay/llm/prompts/
├── system_insight_generator.j2 # 生产者:从一局轨迹里榨经验
├── system_insight_reviewer.j2 # 审查员:评估这条经验值不值得记
└── system_insight_selector.j2 # 选择器:下一局从库里挑哪几条来看

对应的代码也是三个独立文件:

1
2
3
4
gameplay/llm/
├── insight_generator.py
├── insight_store.py
└── insight_data.py

存储那块由 insight_store.py 兜着——所有经验条目带标签、带场景、带打分一起持久化。三个角色谁都不直接动文件,写读都走 store。

Generator:把一局压成几条候选经验

输入是这局的轨迹:每个回合的局面、自己出了什么牌、对手反应、最后得分。输出是若干条候选 insight。注意是候选,不是直接入库。

prompt 的核心约束就两条:

  • 不要写”我做得很好”这种自我评价,只写”在 X 情况下做了 Y,结果是 Z”
  • 每条带触发条件——后面 selector 才知道什么时候该拿出来

举个真实输出的味道:

触发:剩余回合 ≤ 3 且手牌中有”集中+3”类卡 经验:优先打出集中堆叠卡,把最后一回合留给高倍率输出卡,比平均分高 ~15%

这种带条件、带结果的描述,比”注意体力管理”有用十倍。

Reviewer:把噪音过滤掉

Generator 产出的东西不能全信。模型有时候会把偶然事件当规律——这一局对手出了张烂牌,generator 总结成”对手往往会失误”,扔进库就完蛋。

Reviewer 干的就是泼冷水。它拿到候选 insight 后做三件事:

  1. 样本量够不够?只出现过一次的”规律”直接拒
  2. 跟已有 insight 冲突没有?冲突的话要么合并,要么标记为”待验证”
  3. 触发条件清不清晰?模糊的打回 generator 重写

实测下来 reviewer 大概会刷掉 60%~70% 的候选。一开始觉得太狠,看了几轮通过的,反而觉得这个比例正合适——通过的那批确实条条都能用。

Selector:下一局开打前挑笔记

新一局开始,库里可能已经攒了几十上百条 insight。全塞 system prompt?token 烧不起。随机抽?没意义。

Selector 拿到当前初始局面,去库里挑最相关的 N 条(一般是 3~5 条)。挑的依据就是 generator 写下的”触发条件”——这就是为什么前面非要要求条件写清楚。

selector 自己也是个 LLM 调用,但 prompt 很短:当前局面长这样,候选 insight 有这些(带触发条件),挑出现在用得上的。

为什么不直接用向量检索

刚架这套的时候有人问,为啥不上 embedding 检索,效率高多了。

试过。问题是“触发条件相似”和”语义相似”不是一回事

举个例子。库里有条经验:

触发:体力 ≤ 30 且场上没有恢复牌 经验:优先打高倍率单击牌速攻

当前局面是”体力 25,手里全是 buff 牌”。embedding 检索基于文本相似度,可能给你拉出来一堆”体力低”相关的经验,但这条最关键的”没有恢复牌”被淹没了。

让 LLM 做 selector 反而准。它能真正读懂触发条件里的逻辑,而不是字面相似。代价是多一次 LLM 调用——但这次调用只挑笔记,prompt 短、模型可以用小的,成本可控。

那些踩过的坑

insight 越攒越多,selector 也会糊

库小的时候 selector 很准。攒到上百条之后,候选列表本身就长,模型注意力开始飘。后来加了一道前置过滤:先按 insight 自带的”适用场景”标签做粗筛(这块用字符串匹配就够了),再扔给 selector 精筛。

Generator 太”会写”,容易过度归纳

模型这毛病很顽固——你让它从一局总结经验,它非要总结出五条。明明只有一条有价值。

解决办法是在 prompt 里反复强调”宁缺毋滥,没有可总结的就返回空数组”,并且把 reviewer 的拒绝率作为 generator prompt 的迭代信号——如果某次迭代 reviewer 把 generator 的产出全枪毙了,下次 generator 就该收敛。

Reviewer 不能和 generator 用同一个模型实例

最早图省事,三个角色复用同一个 client,结果发现 reviewer 对 generator 的产出特别宽容——你猜怎么着,模型对自己刚写的东西有偏爱,会下意识地认可。

换成不同的 client 实例(甚至不同模型)之后,reviewer 立马严厉起来。这个现象在论文里也有讨论,但自己撞到才印象深刻。

整体看下来

三段式拆完之后,最直观的感受是每一段的 prompt 都变短了,模型每次只做一件事,输出质量肉眼可见地稳。

更隐性的好处是可观测性——哪条 insight 从哪局来的、被 reviewer 怎么评的、selector 在哪些局调出来用过,全都可以单独追踪。出了问题不再是”模型怎么又抽风了”,而是”哪一段的判断错了”。

调试 Agent 系统最缺的就是这种”哪一步出问题”的拆解能力。把决策流拆开、让每一段都能单独打分,比换更大的模型管用得多。

下一篇打算聊聊另一块更恼火的事——这游戏一局打几十回合,对话历史涨得比谁都快,怎么压缩才能不丢关键信息。那是另一个坑。

欢迎关注我的其它发布渠道