在线咨询
专属客服在线解答,提供专业解决方案
工单支持
专业技术支持团队,随时响应服务需求

语音智能体版 RPG:模型讲故事,MCP 管规则

让大模型当地下城主持人,讲故事的部分它做得很好;但让它同时管生命值、掷骰和掉落,就会出现「刚才你还剩 30 点血,现在怎么变成 50 了」这种问题。同一个动作问两遍,可能得到两个互相矛盾的结果。

这篇教程的分工很明确:DeepSeek 只负责中文叙事,角色、战斗、随机数全交给确定性的 MCP 工具,进度存 SQLite。模型讲故事,引擎管数值,各干各的。

玩起来你会发现:每一个数字都能在工具返回值里找到出处;重启 MCP 之后,存档还在。

> 开始之前 游戏工具走 MCP,先看懂《给语音智能体接入 MCP 工具:让它真的去查订单》的接入方式。


一. 架构与准备工作

玩家语音 → 凤鸣 → DeepSeek 地下城主持人
                         │ MCP tool call
                         ▼
                Streamable HTTP MCP
                         │
             SQLite + 确定性游戏规则
                         │ 工具结果
                         ▼
             DeepSeek 中文叙事 → MiniMax 播报

需要 App ID、主要证书、DeepSeek、MiniMax,以及云端能访问的 HTTPS MCP 地址。依赖固定为 agora-agents==2.4.1mcp==1.12.3

交付代码是单人演示,一个 MCP 进程对应一个存档。真要多人上线,必须按已认证用户或房间隔离数据库——所有玩家共用一个 rpg.db,会变成大家在同一具身体里冒险。


二. 环境变量

AGORA_APP_ID=REPLACE_WITH_SHENGWANG_APP_ID
AGORA_APP_CERTIFICATE=REPLACE_WITH_PRIMARY_CERTIFICATE
DEEPSEEK_API_KEY=REPLACE_WITH_DEEPSEEK_KEY
MINIMAX_API_KEY=REPLACE_WITH_MINIMAX_KEY
MCP_ENDPOINT=https://REPLACE_WITH_PUBLIC_HOST/mcp
MCP_SHARED_SECRET=REPLACE_WITH_RANDOM_SHARED_SECRET
RPG_DB_PATH=rpg.db

三. 确定性游戏引擎

import json
import random
import sqlite3

CLASSES = {
    "战士": {"hp": 32, "die": 8, "spell": "盾击"},
    "法师": {"hp": 20, "die": 6, "spell": "火球术"},
    "游侠": {"hp": 24, "die": 6, "spell": "连射"},
    "祭司": {"hp": 26, "die": 6, "spell": "圣光"},
}

def connect(path: str = "rpg.db") -> sqlite3.Connection:
    db = sqlite3.connect(path)
    db.executescript("""
        CREATE TABLE IF NOT EXISTS character (
            id INTEGER PRIMARY KEY CHECK(id=1),
            class_name TEXT, hp INTEGER, max_hp INTEGER,
            gold INTEGER, inventory TEXT
        );
        CREATE TABLE IF NOT EXISTS enemy (
            id INTEGER PRIMARY KEY CHECK(id=1),
            name TEXT, hp INTEGER, attack INTEGER
        );
        CREATE TABLE IF NOT EXISTS settings (
            key TEXT PRIMARY KEY, value TEXT
        );
        INSERT OR IGNORE INTO settings(key, value)
        VALUES ('mode', 'narration');
    """)
    db.commit()
    return db

def create_character(
    db: sqlite3.Connection,
    character_class: str,
) -> str:
    if character_class not in CLASSES:
        return "请选择职业:战士、法师、游侠或祭司。"
    stats = CLASSES[character_class]
    db.execute("DELETE FROM character")
    db.execute("DELETE FROM enemy")
    db.execute(
        """INSERT INTO character(
            id, class_name, hp, max_hp, gold, inventory
        ) VALUES (1, ?, ?, ?, 0, ?)""",
        (
            character_class,
            stats["hp"],
            stats["hp"],
            json.dumps([], ensure_ascii=False),
        ),
    )
    db.execute(
        "UPDATE settings SET value='narration' WHERE key='mode'"
    )
    db.commit()
    return (
        f"你成为了{character_class},生命值 {stats['hp']},"
        f"招牌技能是{stats['spell']}。"
    )

def roll_damage(sides: int, rng: random.Random) -> int:
    return rng.randint(1, sides)

伤害、反击、掉落、状态更新全部在 game.py 里算完。LLM 只能复述工具返回的结果,不允许自己生成掷骰点数——这是整套设计的地基。


四. 暴露 MCP 工具

from mcp.server.fastmcp import FastMCP

import game

mcp = FastMCP("shengwang-voice-rpg")

def run_game(function, *arguments) -> str:
    db = game.connect()
    try:
        return function(db, *arguments)
    finally:
        db.close()

@mcp.tool()
def create_character(character_class: str) -> str:
    """创建角色;职业必须是战士、法师、游侠或祭司。"""
    return run_game(game.create_character, character_class)

@mcp.tool()
def get_character() -> str:
    """查询职业、生命、金币、物品和当前战斗状态。"""
    return run_game(game.get_character)

@mcp.tool()
def start_encounter() -> str:
    """开始随机遭遇。"""
    return run_game(game.start_encounter)

@mcp.tool()
def attack() -> str:
    """完成普通攻击、反击和战利品结算。"""
    return run_game(game.attack)

@mcp.tool()
def cast_spell(spell_name: str) -> str:
    """施放角色技能并结算本轮战斗。"""
    return run_game(game.cast_spell, spell_name)

@mcp.tool()
def flee() -> str:
    """逃离当前战斗。"""
    return run_game(game.flee)

完整服务还在 ASGI 外层强制校验 Authorization: Bearer,并且只挂载 Streamable HTTP MCP。


五. 配置智能体

import os

from agora_agent import Area, AsyncAgora
from agora_agent.agentkit import Agent
from agora_agent.cn import DeepSeekLLM, FengmingSTT, MiniMaxTTS

client = AsyncAgora(
    area=Area.CN,
    app_id=os.environ["AGORA_APP_ID"],
    app_certificate=os.environ["AGORA_APP_CERTIFICATE"],
)
mcp_servers = [{
    "name": "voice-rpg",
    "endpoint": os.environ["MCP_ENDPOINT"],
    "transport": "streamable_http",
    "headers": {
        "Authorization": f"Bearer {os.environ['MCP_SHARED_SECRET']}"
    },
    "allowed_tools": [
        "create_character", "get_character", "start_encounter",
        "attack", "cast_spell", "flee",
    ],
    "timeout_ms": 10000,
}]
llm = DeepSeekLLM(
    api_key=os.environ["DEEPSEEK_API_KEY"],
    base_url="https://api.deepseek.com/chat/completions",
    model="deepseek-v4-flash",
    system_messages=[{"role": "system", "content":
        "你是中文奇幻语音 RPG 的地下城主持人。所有职业、生命、"
        "掷骰、伤害、金币、物品和战斗结果必须调用工具,绝不能编造。"
        "工具返回后只能基于工具结果继续叙述。"}],
    greeting_message=(
        "欢迎来到雾岭大陆。请先选择战士、法师、游侠或祭司。"
    ),
    max_history=30,
    max_tokens=512,
    temperature=0.6,
    params={"thinking": {"type": "disabled"}},
    mcp_servers=mcp_servers,
)
tts = MiniMaxTTS(
    key=os.environ["MINIMAX_API_KEY"],
    model="speech-01-turbo",
    voice_id="female-shaonv",
    sample_rate=16000,
    language_boost="Chinese",
)
agent = (
    Agent(
        client=client,
        turn_detection={"language": "zh-CN"},
        advanced_features={"enable_rtm": True, "enable_tools": True},
        parameters={"audio_scenario": "chorus", "data_channel": "rtm"},
    )
    .with_stt(FengmingSTT())
    .with_llm(llm)
    .with_tts(tts)
)

六. 运行与验证

cd code/voice-rpg
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env.local
uvicorn mcp_service:app --host 0.0.0.0 --port 9500

把 9500 端口部署成 MCP_ENDPOINT 对应的公网 HTTPS,再启动智能体后端:

uvicorn app:app --host 0.0.0.0 --port 8000

按这个脚本玩一遍:

玩家:我要当法师。       → create_character
玩家:查看我的状态。     → get_character
玩家:我要探索洞穴。     → start_encounter
玩家:施放火球术。       → cast_spell
玩家:继续攻击。         → attack

验收标准很硬:每一个数字都能在 SQLite 的工具结果里找到出处,重启 MCP 后存档还在。

还要试试这些边界情况:没建角色就开打、重复开始战斗、用不存在的技能、角色死亡、逃跑、MCP 返回 401、工具超时,以及最关键的一条——确认 DeepSeek 没法凭空修改生命值。


七. 故障排查

  • LLM 编造伤害数字:系统提示里要强制要求调用工具,同时检查 enable_toolsmcp_serversallowed_tools 配置正确。
  • MCP 返回 401:智能体配置里的 Bearer 要和服务端 MCP_SHARED_SECRET 一致。
  • 所有玩家共享存档:教程默认单进程单玩家。生产环境按认证用户隔离存储,或者为每局部署独立的游戏实例。
  • 最后再强调一次:随机数、结算、状态变更都要留在可测试的确定性服务端。交给 LLM 的那一刻,游戏就没有规则了。

八. 下一步

想给游戏加更多工具,先回到 MCP 那篇把工具接入这件事吃透。

《给语音智能体接入 MCP 工具:让它真的去查订单》

在声网,连接无限可能

想进一步了解「对话式 AI 与 实时互动」?欢迎注册,开启探索之旅。

本博客为技术交流与平台行业信息分享平台,内容仅供交流参考,文章内容不代表本公司立场和观点,亦不构成任何出版或销售行为。