Skip to content

Looperswag/poker

Repository files navigation

Texas Hold'em with Self-Iterating LLM Agents

8 人桌德州扑克:1 名真人 vs 7 名 LLM 驱动的 AI。每个 AI 有独立人设、独立长期记忆、独立反思与策略迭代——不是会下棋的 LLM,是会自我修正的 LLM。


与常见 AI 牌局项目的差异

维度 大多数 LLM-poker 项目 本项目
AI 决策 单次 LLM 调用,无记忆 每个 AI 拥有独立 output/agent_memory/{agent_id}.db,10 张表的分层记忆
多 AI 区分 模型温度或简短系统提示 人格 7 维数值 (bluff_frequency / aggression / tightness / skill_level / ...) → 中文 system prompt + temperature 微调
对手建模 没有,或仅本手内 跨手 per-street 统计(VPIP / PFR / 3-bet / fold-to-3bet / c-bet / fold-to-cbet / 摊牌胜率 / 平均下注比例 / 激进度),按时间衰减
反思 无,或单条字符串拼回 prompt 结构化反思kind ∈ {mistake, exploit, range_update, self_review, lesson} + topic + confidence + 句向量;置信度二次校正(小样本→强制 low)
长期策略迭代 重启即丢 每 20 手或反思 > 100 条触发 distillation:高频 topic 的多条反思被 LLM 压缩成 knowledge 规则,源反思标记 superseded_by 不再纳入检索
Prompt 装配 一次性塞满,无预算 按 spot embedding 检索 top-K 反思/关键决策点,余弦 + 时间衰减 + 置信度加权排序,整段 ≤1500 tokens
隐私正确性 经常给 AI"上帝视角" 每个 AI 的 hand log 只含它能观测的信息——公开动作 + 自己手牌 + showdown 摊到的牌;不给 AI 它看不到的卡牌
LLM 接入 OpenAI 或 Anthropic 写死 Anthropic SDK 指向 DeepSeek 兼容端点;模型名/超时/QPS 限流可配;任何环节失败都降级到 personality 启发式引擎而不是默认 fold
嵌入 调商用 embedding API 本地 sentence-transformers (BAAI/bge-small-zh-v1.5,95MB),零调用成本,离线可用;不可用时自动退到 recency + confidence 排序
UI 仅牌桌 右下角本场历史看盘:每手摘要卡 + 三键控制(继续 / 暂停 / 重新开始);重新开始只清显示,不动 SQLite 长期记忆

单局完整决策链(举例)

第 N 手轮到 Alice(TAG,紧手 + 激进)翻牌后决策:

hand_action_log + GameState
        │
        ▼
LLMDecisionEngine.make_decision
        │ 1. 构造 spot 描述: "flop pos=3/8 hole=As Ks board=Qd Jc 2h pot=400 to_call=200"
        │ 2. EmbedderClient 本地嵌入 spot
        │ 3. SQLite 查询:
        │      opponent_snapshot  (当前桌 7 人的 VPIP/PFR/c-bet/激进度)
        │      knowledge          (top-2 已蒸馏规则,cos+conf 排序)
        │      reflections        (top-4 相似历史反思,kind ∈ {exploit,lesson,range_update})
        │      notable_spots      (top-2 同 street 关键决策点)
        │      recent_session     (最近 20 手净盈亏)
        │ 4. pack_into_budget(sections, 1500 tokens)
        │ 5. build_system_prompt(personality, agent_id, memory_block)
        │      ↓ 静态部分带 cache_control:ephemeral(LLM 提示词缓存)
        ▼
LLMClient.decide(system, user)  ← DeepSeek-v4-flash, anthropic 兼容端点
        │   timeout=15s/attempt, 2 retries, token-bucket QPS=5
        │
        ▼
JSON  {"action":"raise","amount":500,"reason":"AKs 顶顺听 + 听花,对 Carol 的下注 raise 取得范围优势"}
        │
        ▼
_coerce_decision   (合法性夹紧:amount 钳到 [min_raise, stack],
                    check-when-bet-pending → 改 call,all-in 重写为 raise=stack)
        │
        ▼
engine.process_action  → 状态推进 → 广播

LLM 任何一环失败(网络、超时、JSON 解析、非法动作)都走 AIDecisionEngine(personality 数值启发式)兜底。绝不强制 FOLD——这是上一轮 bug 修复的核心。


单手结束后的迭代闭环

hand_complete
    │
    ▼
ai/stats.compute_per_player_features(hand_log)
    │  → 每个玩家的 per-street features (VPIP / PFR / 3bet / fold-to-3bet /
    │     c-bet as aggressor / fold-to-cbet / barrel-2nd / barrel-3rd /
    │     showdown 参与 + 胜负 / 平均下注比例)
    ▼
ai/stats.detect_notable_spots()
    │  → 自我视角下的"关键决策点"(all-in、3-bet+、河牌大注、≥15 BB 大底池)
    ▼
SQLite 单事务: hands + actions + showdown_reveals +
              opponent_observations + opponent_stats(增量) +
              notable_spots + sessions(净盈亏累计)
    ▼
Reflector (后台 fire-and-forget):
    LLM 一次返回 ≤3 条结构化反思 {kind, topic, text, confidence, ref_opp}
    → confidence 二次校正
    → batch embed 一次写 BLOB
    → 与最近 30 条 cos > 0.92 视为重复 → merge + used_count++
    │
    ▼ (每 20 手 / 100 条反思触发)
distill_recent_reflections:
    按 topic 分桶,每桶 LLM 压缩成 ≤3 条 knowledge 规则
    源 reflections 标 superseded_by,不再入检索

下一手开始时,新 spot 嵌入向量去与这些反思/关键点做余弦检索,自然回流到 system prompt——这就是"策略迭代"的物理实现。


7 个人设(ai/personalities.py

座位 角色 style 紧手度 激进度 诈唬频率 技术
Alice 外科医生 THE SURGEON tight_aggressive 0.78 0.72 0.14 0.76
Bob 浪客 THE MAVERICK loose_aggressive 0.18 0.86 0.28 0.57
Carol 图书馆员 THE LIBRARIAN tight_passive 0.74 0.24 0.05 0.60
David 度假客 THE TOURIST loose_passive 0.22 0.20 0.03 0.44
Eve 新手 THE NEWBIE fish 0.34 0.42 0.18 0.22
Frank 鲨鱼 THE SHARK pro 0.58 0.66 0.12 0.95
Grace 磐石 THE ROCK nit 0.96 0.18 0.02 0.72

数值通过 prompt_builder._style_brief() 转中文人设段落 + temperature 微调 (0.7 + (bluff_frequency - 0.15) × 0.3),所以 7 个 AI 共享同一个模型但行为分布显著不同。实际跑 30 手后,Alice 写"作为 TAG 应持续下注施压"、Bob 写"激进度偏低应更多诈唬"、Grace 写"本手 fold 符合 nit 策略"——人设固化在自我反思里。


模块布局

poker/
├── server.py                 # FastAPI + WebSocket + 牌桌编排 + 暂停/继续闸门
├── core/                     # 引擎(与 AI 解耦)
│   ├── engine.py             # 状态机、动作合法性、街道推进、摊牌结算
│   ├── pot_manager.py        # 主池/边池
│   ├── hand_evaluator.py     # 牌力评估
│   └── deck.py
├── models/                   # 数据模型
│   ├── game_state.py         # GameState + hand_action_log + dealt_hole_cards 快照
│   ├── player.py             # Player + ActionType
│   └── card.py
├── ai/                       # 全部 LLM 相关 + 记忆
│   ├── personalities.py      # 7 个 frozen dataclass
│   ├── decision_engine.py    # 启发式引擎 (failure fallback)
│   ├── llm_client.py         # Anthropic SDK 封装 + token bucket + 重试 + JSON 解析
│   ├── llm_decision.py       # LLM 决策入口(与启发式同签名,便于无感降级)
│   ├── prompt_builder.py     # system / user / reflection / memory_block_v2
│   ├── memory.py             # AgentMemory facade,分发到 v1 JSON / v2 SQLite
│   ├── memory_schema.sql     # 10 张表 DDL (WAL + 索引)
│   ├── memory_migrations.py  # v1 JSON → v2 SQLite 迁移
│   ├── stats.py              # 纯函数:hand_log → per-street features + notable_spots
│   ├── reflection.py         # 后手末结构化反思 + 嵌入 + 去重
│   ├── distillation.py       # 反思 → knowledge 蒸馏
│   ├── embedder.py           # 本地 sentence-transformers + 余弦 + 降级
│   ├── retrieval.py          # top-K + 时间衰减 + confidence boost + token budget
│   └── adapter.py
├── static/                   # 原生 HTML + JS + CSS(无构建工具)
│   ├── index.html
│   ├── app.js                # WebSocket 状态机 + 历史面板渲染 + 控制按钮
│   └── style.css
└── tests/                    # pytest,POKER_LLM_DISABLED=1 下完全离线可跑

快速启动

# 1. 装依赖(含 anthropic、sentence-transformers)
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
# 代理用户可能需要:pip install "httpx[socks]"

# 2. 复制 .env.example → .env,填入你的 DeepSeek API key
cp .env.example .env
$EDITOR .env

# 3. 预下载嵌入模型(首次约 95 MB,缓存到 ~/.cache/huggingface/)
python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('BAAI/bge-small-zh-v1.5')"

# 4. 启服务
PYTHONPATH=. python -m uvicorn server:app --host 127.0.0.1 --port 8001

浏览器开 http://127.0.0.1:8001,点 START GAME。每手结束右下角看历史卡和三键控制。


配置开关(.env

变量 默认 说明
DEEPSEEK_API_KEY (必填) 留空则全部 AI 走启发式
DEEPSEEK_MODEL deepseek-v4-flash 任何 anthropic 兼容模型名
DEEPSEEK_BASE_URL https://api.deepseek.com/anthropic 改它就能换 provider
POKER_LLM_TIMEOUT 15.0 单次 LLM HTTP 超时(DeepSeek-flash p50≈10-15s)
POKER_AI_DECISION_TIMEOUT 30.0 决策外层 wall-clock 上限,超时走启发式
POKER_REFLECTION_TIMEOUT 30.0 后台反思批次总超时
POKER_LLM_DISABLED 0 设 1 完全离线,全 AI 走启发式
POKER_MEMORY_V2 1 SQLite 后端(设 0 退到旧 JSON 快照)
POKER_MEMORY_RETRIEVAL 1 嵌入检索(设 0 退到 recency + confidence 排序)
EMBEDDING_MODEL BAAI/bge-small-zh-v1.5 可换 bge-m3 (多语 1024d) 或 all-MiniLM-L6-v2 (英文 80MB)

测试

# 全量离线回归(mock LLM)
PYTHONPATH=. POKER_LLM_DISABLED=1 python -m pytest tests/ -q

# 三种配置组合
PYTHONPATH=. POKER_LLM_DISABLED=1                                  pytest tests/ -q   # v1 JSON
PYTHONPATH=. POKER_LLM_DISABLED=1 POKER_MEMORY_V2=1                pytest tests/ -q   # v2 SQLite
PYTHONPATH=. POKER_LLM_DISABLED=1 POKER_MEMORY_V2=1 POKER_MEMORY_RETRIEVAL=1 pytest tests/ -q  # v2 + 检索

104 个测试覆盖:引擎合法性 / pot manager / 7 personality 区分度 / 决策回归 / v1↔v2 迁移 / per-street features 准确性 / 反思去重 / 蒸馏 / token budget / 暂停-继续-重启控制 / E2E smoke 等。


观测

跑几手后:

# LLM 健康
curl -s http://127.0.0.1:8001/api/metrics | jq

# 单个 AI 的反思与蒸馏规则
sqlite3 output/agent_memory/ai_1.db <<'SQL'
SELECT kind, topic, confidence, substr(text, 1, 60)
FROM reflections WHERE superseded_by IS NULL ORDER BY id DESC LIMIT 5;
SELECT topic, confidence, rule_text FROM knowledge;
SELECT opponent_id, hands_observed,
       round(1.0*vpip_hits/nullif(vpip_opps,0), 2) AS vpip,
       round(1.0*pfr_hits/nullif(pfr_opps,0), 2) AS pfr,
       round(1.0*aggressive_actions/nullif(aggressive_actions+passive_actions,0), 2) AS agg
FROM opponent_stats ORDER BY hands_observed DESC;
SQL

已知边界

  • DeepSeek-v4-flash p95 偶发 60-80s,命中外层 30s 超时 → 走启发式 fallback(不再 FOLD)
  • 嵌入模型首次启动需要下载 (~95 MB),离线场景需提前 huggingface-cli download BAAI/bge-small-zh-v1.5
  • 重新开始按钮只清前端显示,SQLite 长期记忆故意保留——这是设计意图,便于跨 session 持续训练
  • 多 tab 共享同一个服务端状态机,一处点 继续 影响所有 tab

许可

代码 MIT,嵌入模型权重 Apache-2.0(BAAI)。

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages