你说出的每一句话,城主都会记住 - 深渊纪元如何用端到端具身交互智能打造永不疲倦的 AI 地下城城主

传统 TRPG 跑团需要一位经验丰富的真人城主(DM)全程控场——构建世界观、扮演 NPC、裁决行动、掌控节奏,一场战役动辄数小时,对人的精力和创造力都是极大消耗。深渊纪元把这位城主交给了端到端具身交互智能:玩家说出任何行动,城主即时构建世界观响应,同时记住整场战役的角色关系、已探索地图、NPC 好感度,并在探索、社交、战斗、休憩四种阶段之间无缝切换叙事风格。
本文从数据建模到三栏界面,完整拆解这套 AI 城主的工程实现。
一、星云控制台配置:六步完成城主全链路
在动手写代码之前,需要先在魔珐星云控制台完成具身应用的全链路配置。以下六步决定了城主的形象、声音、大脑与交互能力。
1、创建具身应用
在魔珐星云控制台创建一个新的端到端应用,作为 AI 城主的具身交互智能体运行载体。

2、形象配置
为城主选择一位身披暗紫斗篷的神秘形象,契合地下城世界的奇幻调性。端到端 SDK 会自动处理形象的实时渲染——口型、表情、手势全部由平台统一驱动,无需前端手动编排。

3、场景配置
设定城主的交互场景参数,包括背景环境与镜头角度,让城主置身于地下城氛围的光影之中,增强跑团的沉浸感。

4、音色配置
选择低沉而富有叙事感的音色,与地下城城主的身份匹配。项目的参数流 TTS 与 SDK 紧密耦合,语音合成与具身表达在同一次交互中统一生成,不需要单独对接 TTS 服务。

5、表演配置
调整城主的表达风格——语速沉稳、手势克制、表情随叙事阶段自然变化,让具身表达与 TRPG 城主「掌控全局、营造氛围」的调性一致。

6、服务配置:ASR 与大脑
为当前应用配置语音识别和大模型服务。这一步体现了星云的可插拔架构——可插拔并非简单的 API 拼接,平台依靠标准化接口契约与统一状态管理,保障模块替换后会话状态、事件流转、交互闭环不受破坏。ASR 支持 Xmov 自研、豆包、腾讯等多家供应商,大脑支持魔珐自研、豆包、OpenAI 兼容协议等多种接入模式。

配置完成后,记录应用 ID 与密钥,后续代码中通过环境变量注入即可。接下来进入前端工程——用代码构建城主的战役引擎与三栏交互界面。
二、技术选型:一个依赖撑起一场战役

深渊纪元的项目依赖极简到只有一个运行时依赖:
// package.json
{
"dependencies": {
"vue": "3.5.18"
},
"devDependencies": {
"@vitejs/plugin-vue": "6.0.1",
"typescript": "~5.8.3",
"vite": "7.1.2",
"vue-tsc": "3.0.5"
}
}
来源:package.json
没有 Three.js,没有 OpenAI SDK,没有额外的状态管理库。具身表达由魔珐星云端到端 SDK 单文件自包含——在 index.html 中以 defer 脚本引入,SDK 自动挂载 window.XingyunAvatarAgent,业务侧只需调用 init / ask / startASR 三个方法。
<!-- index.html -->
<!-- 端到端具身交互智能 SDK(单文件自包含,defer 保证先于模块入口执行) -->
<script defer src="https://media.xingyun3d.com/xingyun3d/general/litesdk/xmovAvatar_e2e@latest.js"></script>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
来源:index.html
defer 脚本先于 type=module 执行,确保应用挂载时 SDK 已可用。如果顺序反了,module 天然 defer 会先执行,new XingyunAvatarAgent 时全局变量尚不存在,连接将永久挂起。
三、项目架构:三层结构一览
src/
├── components/ # 6 个 Vue 组件
│ ├── AgentPanel.vue # 右栏:城主智能体渲染
│ ├── AppIcon.vue # SVG 图标集
│ ├── CharacterSheet.vue # 左栏:角色卡 + 背包 + NPC 关系
│ ├── GameStage.vue # 中栏:阶段指示 + 叙事日志 + 行动输入
│ ├── LandingPage.vue # 首页:可滚动介绍页
│ └── WorldMap.vue # 左栏:迷雾地图
├── constants/ # 应用配置 + 城主人设提示词
├── data/ # 初始角色卡 + NPC + 地图
├── services/ # SDK 封装(唯一通道)
├── stores/ # 状态管理(游戏引擎 + SDK 串联)
├── types/ # TypeScript 类型定义
├── utils/ # 游戏引擎(纯函数 + 持久化)
├── App.vue # 双视图切换 + 三栏布局
├── main.ts # 入口
└── style.css # 全局暗色奇幻主题
来源:项目目录结构
整个项目分为三层:数据与引擎层(types + data + gameEngine)负责游戏世界的规则与状态流转;服务层(avatar.ts)封装 SDK 连接与交互;界面层(6 个组件 + App.vue)负责三栏呈现与双视图切换。
四、数据建模:一个完整的地下城世界

类型系统
深渊纪元的类型定义勾勒出一个完整的 TRPG 世界骨架:
// src/types/index.ts
// 战役阶段
export type CampaignPhase = 'exploration' | 'social' | 'combat' | 'rest'
// NPC 身份
export interface NPC {
id: string
name: string
role: 'ally' | 'neutral' | 'enemy'
description: string
personality: string
relationship: number // -100 ~ 100
}
// 地图区域
export interface MapArea {
id: string
name: string
description: string
discovered: boolean
connections: string[]
}
// 游戏状态
export interface GameState {
phase: CampaignPhase
currentArea: string
turnCount: number
npcs: Record<string, NPC>
mapAreas: Record<string, MapArea>
narrativeLog: NarrativeEntry[]
character: Character
combatState: {
enemyId: string | null
enemyHp: number
enemyMaxHp: number
}
}
来源:src/types/index.ts
四个战役阶段决定了城主的叙事风格切换;NPC 的 relationship 字段在 -100 到 100 之间动态变化,城主在回应中会自然引用好感度;MapArea 的 discovered 标记驱动地图迷雾系统;combatState 追踪战斗中的敌人血量。
内置战役数据
项目内置了一个完整的初始战役——游侠从断剑酒馆出发,面对四位性格迥异的 NPC:
// src/data/campaign.ts
/** 内置 NPC 阵容 */
export const INITIAL_NPCS: Record<string, NPC> = {
elara: {
id: 'elara',
name: '艾拉拉',
role: 'ally',
description: '银发精灵法师,精通预言系法术,受雇于学术公会调查地下遗迹',
personality: '冷静理性、博学多闻,偶尔流露出对古代文明的好奇与痴迷',
relationship: 30,
},
gronk: {
id: 'gronk',
name: '格隆克',
role: 'neutral',
description: '半兽人佣兵,左脸有深深的爪痕,据说曾独闯巨魔巢穴',
personality: '粗犷直率、重义气,但对背叛零容忍',
relationship: 0,
},
shadow: {
id: 'shadow',
name: '暗影·薇拉',
role: 'enemy',
description: '黑斗篷下的神秘女子,据传是深渊教团的使徒',
personality: '阴冷优雅、言辞间暗藏试探,从不直接表露真实意图',
relationship: -40,
},
innkeeper: {
id: 'innkeeper',
name: '老酒馆·巴特',
description: '「断剑酒馆」老板,消息灵通,是冒险者们的信息枢纽',
personality: '热情健谈、爱八卦,但关键时刻守口如瓶',
relationship: 20,
},
}
来源:src/data/campaign.ts
四位 NPC 从盟友到敌对,好感度起点各不相同。玩家与他们的每一次互动都会改变关系值,城主会在后续回应中自然引用这些变化——比如格隆克在好感度低于 -30 时可能拒绝提供帮助,而艾拉拉在高好感度时会主动分享预言线索。
地图由 6 个区域组成,从断剑酒馆出发,经镇中心广场可达暮光神殿、黑市集市,最终深入地下城入口与深渊大厅。每个区域有连接关系,玩家每踏出一步都可能触发新剧情。
五、游戏引擎:四阶段纯函数状态机

数据建模完成后,游戏的核心挑战转向状态管理——玩家的每一步探索、每一次与 NPC 的交锋、每一滴生命值的增减,这些状态需要在内存与持久化之间同步,并在每次向城主提问时组装成完整上下文。
游戏引擎由 src/utils/gameEngine.ts 驱动,核心设计原则是纯函数 + localStorage:每个状态变更函数接收旧状态、返回新状态,不修改原对象。
// src/utils/gameEngine.ts
/** 切换战役阶段 */
export function setPhase(state: GameState, phase: CampaignPhase): GameState {
return { ...state, phase, turnCount: state.turnCount + 1 }
}
/** 调整 NPC 好感度 */
export function adjustRelationship(state: GameState, npcId: string, delta: number): GameState {
const npc = state.npcs[npcId]
if (!npc) return state
const newRel = Math.max(-100, Math.min(100, npc.relationship + delta))
return {
...state,
npcs: {
...state.npcs,
[npcId]: { ...npc, relationship: newRel },
},
}
}
/** 移动到新区域 */
export function moveToArea(state: GameState, areaId: string): GameState {
const area = state.mapAreas[areaId]
if (!area) return state
const discovered = area.discovered ? state.mapAreas : {
...state.mapAreas,
[areaId]: { ...area, discovered: true },
}
return {
...state,
currentArea: areaId,
mapAreas: discovered,
turnCount: state.turnCount + 1,
}
}
来源:src/utils/gameEngine.ts
纯函数的好处是可预测、可测试——给定相同输入永远得到相同输出。状态持久化通过 loadState / saveState 完成,刷新页面后战役进度不会丢失:
// src/utils/gameEngine.ts
/** 从 localStorage 恢复状态,失败则返回初始状态 */
export function loadState(): GameState {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (raw) return JSON.parse(raw) as GameState
} catch { /* 损坏则忽略 */ }
return createInitialState()
}
/** 持久化状态到 localStorage */
export function saveState(state: GameState): void {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)) } catch { /* 存储满则忽略 */ }
}
来源:src/utils/gameEngine.ts
六、城主人设提示词:让智能体成为即兴叙事大师

游戏引擎构建好上下文后,下一步是定义城主的身份与行为准则。深渊纪元的城主不是通用聊天机器人,而是一位有明确叙事风格的即兴叙事大师。提示词定义了它的核心行为:
// src/constants/index.ts
/** 城主人设:即时构建世界观,记住一切 */
export const SYSTEM_PROMPT = `你是深渊纪元,一位经验丰富的 AI 地下城城主(DM)。
## 你的身份
- 名称:深渊纪元
- 身份:TRPG 跑团的 AI 城主,负责构建世界观、扮演 NPC、裁决行动
- 风格:叙事沉浸、细节丰富,善于营造氛围
## 你的行为准则
1. 根据当前战役阶段切换表达风格:探索时注重环境描写,社交时让 NPC 有鲜明个性,战斗时节奏紧凑,休憩时温暖舒缓
2. 玩家说出任何行动,你都要即时构建合理的世界观响应——不能拒绝合理行动
3. 记住所有 NPC 的关系值、已探索的地图区域、玩家背包内容,并在回应中自然引用
4. 掷骰结果由你根据行动难度合理裁定,不机械依赖数值
5. 每次回应控制在 3-5 句,营造沉浸感但不拖沓
6. 不能剧透主线剧情走向,不能替玩家做决定`
来源:src/constants/index.ts
关键设计点:第 1 条要求城主根据阶段切换风格——探索时描写环境细节,战斗时节奏紧凑,这让同一位智能体在不同场景下呈现截然不同的叙事质感。第 3 条强调记忆——NPC 好感度、已探索地图、背包内容都必须在回应中自然引用,而不是等玩家提醒。
七、具身表达层:SDK 服务封装

游戏引擎负责「想什么」,SDK 服务层负责「怎么说」——它需要将游戏引擎构建的上下文转化为城主的语音和表情。src/services/avatar.ts 是整个项目与魔珐星云 SDK 交互的唯一通道。
generation 令牌:防止新旧连接回调交叉污染
端到端 SDK 通过回调函数通知状态变化。当玩家断开重连时,旧连接的回调可能延迟到达,与新连接的回调交叉污染。深渊纪元用 generation 计数器解决这个问题:
// src/services/avatar.ts
export async function connectAvatar(container: HTMLElement, appId: string, appSecret: string) {
if (avatarState.status === 'connecting' || avatarState.status === 'online') return
if (!appId.trim() || !appSecret.trim()) throw new Error('请填写有效的应用凭证')
const token = ++generation
avatarState.status = 'connecting'
avatarState.error = ''
let candidate: Agent | undefined
let timer: ReturnType<typeof setTimeout> | undefined
try {
await loadSDK()
if (token !== generation) return
candidate = new window.XingyunAvatarAgent!({
container, appId: appId.trim(), appSecret: appSecret.trim(),
gatewayServer: SDK_CONFIG.GATEWAY_URL,
agentCallbacks: {
onAgentStateChange: (state: string | { type?: string }) => {
if (token !== generation) return
const value = typeof state === 'string' ? state : state?.type ?? ''
avatarState.speaking = value === 'speak' || value === 'speaking'
},
onASRResult: (result: { text?: string; isFinal?: boolean }) => {
if (token !== generation) return
avatarState.subtitle = result.text ?? ''
if (result.isFinal && result.text?.trim()) asrFinalHandler?.(result.text.trim())
},
onError: () => {
if (token !== generation) return
avatarState.error = '城主连接异常,请检查网络、应用配置或平台额度后重连'
avatarState.status = 'error'
void disconnectAvatar(true)
},
},
})
pendingAgent = candidate
const active = candidate
await Promise.race([
active.init({}).then(async () => { if (token !== generation) await active.destroy('cancelled') }),
new Promise<never>((_, reject) => { timer = setTimeout(() => reject(new Error('城主连接超时,请检查网络、凭证与平台额度')), 30000) }),
])
if (token !== generation) return
agent = candidate
pendingAgent = undefined
avatarState.status = 'online'
} catch (error) {
if (token !== generation) return
generation += 1
avatarState.status = 'error'
avatarState.error = error instanceof Error ? error.message : '连接失败'
void candidate?.destroy('failed').catch(() => {})
agent = undefined
pendingAgent = undefined
} finally { clearTimeout(timer) }
}
来源:src/services/avatar.ts
每次连接递增 generation 并捕获当前值 token,所有回调内先校验 token === generation,不匹配则静默丢弃。这确保旧连接的回调永远不会污染新连接的状态。
连接采用 Promise.race 加 30 秒超时兜底。SDK 的 init 方法需要建立感知、大脑、表达三条 WebSocket 连接,任一被服务端拒绝则 Promise 永久 pending,超时机制确保玩家不会卡在加载界面。
每次提问都注入完整上下文
深渊纪元不缓存提示词——每次调用 askAvatar 都重新构建完整上下文,确保城主的回应始终基于最新战役进度:
// src/services/avatar.ts
export async function askAvatar(text: string, context: string) {
if (!agent || avatarState.status !== 'online') return
avatarState.subtitle = text
await agent.ask(`${SYSTEM_PROMPT}\n当前战役上下文:\n${context}\n玩家行动:${text}`)
}
来源:src/services/avatar.ts
上下文由 gameEngine 的 buildCampaignContext 构建,包含当前阶段、所在位置、角色状态、背包内容、已探索区域、NPC 关系、战斗状态、最近事件——城主在回应时可以自然引用其中任何一项。
八、状态管理:战役全链路流程控制
src/stores/app.ts 是战役的流程控制器,将游戏引擎、SDK 服务层和 UI 状态串联成完整链路。
自然语言指令检测
玩家不需要记忆按钮操作——在输入框中说到「探索」「前进」时自动切换到探索阶段,说到「攻击」「战斗」时进入战斗阶段,说到「交谈」「对话」时进入社交阶段:
// src/stores/app.ts
/** 自然语言指令检测 */
const PHASE_KEYWORDS: Record<CampaignPhase, string[]> = {
exploration: ['探索', '调查', '搜查', '查看', '前进', '移动', '走去'],
social: ['交谈', '对话', '询问', '聊天', '交涉', '说服'],
combat: ['攻击', '战斗', '挥剑', '施法', '防御', '闪避'],
rest: ['休息', '扎营', '恢复', '治疗', '睡觉', '篝火'],
}
function detectPhase(text: string): CampaignPhase | null {
for (const [phase, keywords] of Object.entries(PHASE_KEYWORDS)) {
if (keywords.some(kw => text.includes(kw))) return phase as CampaignPhase
}
return null
}
来源:src/stores/app.ts
核心链路:向城主提问
askDM 函数是整个游戏的核心——检测阶段切换、记录玩家行动、构建上下文、发送给城主:
// src/stores/app.ts
/** 向城主提问(核心链路) */
async function askDM(text: string) {
// 检测阶段切换
const detectedPhase = detectPhase(text)
if (detectedPhase && detectedPhase !== appState.game.phase) {
appState.game = setPhase(appState.game, detectedPhase)
appState.game = addNarrative(appState.game, `进入${
{ exploration: '探索', social: '社交', combat: '战斗', rest: '休憩' }[detectedPhase]
}阶段`, 'system')
}
// 记录玩家行动
appState.game = addNarrative(appState.game, text, 'player')
// 构建上下文并发送
const context = buildCampaignContext(appState.game)
await askAvatar(text, context)
persist()
}
来源:src/stores/app.ts
这段代码展示了端到端架构的简洁——阶段检测、状态更新、上下文构建、SDK 调用、持久化,五步操作在一个函数内完成,不需要跨模块的状态同步胶水代码。
九、三栏交互界面:角色卡、叙事舞台、城主同屏呈现
深渊纪元的游戏界面采用三栏布局,将战役所需的全部信息平铺在同一屏幕:
<!-- src/App.vue -->
<script setup lang="ts">
import { computed, onBeforeUnmount, ref } from 'vue'
import { APP_CONFIG } from './constants'
import { appState, appStore } from './stores/app'
import Icon from './components/AppIcon.vue'
import LandingPage from './components/LandingPage.vue'
import CharacterSheet from './components/CharacterSheet.vue'
import WorldMap from './components/WorldMap.vue'
import GameStage from './components/GameStage.vue'
import AgentPanel from './components/AgentPanel.vue'
const avatar = appStore.avatarState
const view = ref<'landing' | 'game'>('landing')
function enterGame() {
view.value = 'game'
}
// 顶栏时钟
const now = ref(new Date())
const clockTimer = setInterval(() => { now.value = new Date() }, 30000)
onBeforeUnmount(() => clearInterval(clockTimer))
const clockText = computed(() => now.value.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }))
const dateText = computed(() => now.value.toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'long' }))
</script>
<template>
<!-- 首页:可滚动介绍页 -->
<LandingPage v-if="view === 'landing'" @enter="enterGame" />
<!-- 游戏视图:全屏三栏,不滚动 -->
<div v-else class="screen game-screen">
<header class="top-bar">
<div class="brand">
<span class="brand-mark"><Icon name="dice" :size="26" /></span>
<span class="brand-name">深渊纪元<small>ABYSS EPOCH</small></span>
<span class="brand-sub">AI 地下城城主 · {{ APP_CONFIG.STORE }}</span>
</div>
<div class="top-center">
<span v-if="avatar.listening" class="voice-live"><span class="live-dot" />正在聆听,请下达行动指令</span>
<span v-else-if="avatar.speaking" class="voice-live speaking"><span class="live-dot" />城主叙述中</span>
</div>
<div class="top-clock">
<strong>{{ clockText }}</strong>
<span>{{ dateText }}</span>
</div>
</header>
<main class="columns">
<!-- 左栏:角色卡 + 地图 -->
<aside class="left-col">
<CharacterSheet />
<WorldMap />
</aside>
<!-- 中栏:叙事舞台 -->
<GameStage />
<!-- 右栏:城主智能体 -->
<AgentPanel />
</main>
</div>
</template>
<style scoped>
.game-screen {
height: 100vh;
overflow: hidden;
}
</style>
来源:src/App.vue
双视图设计值得注意:首页是可滚动的介绍页(LandingPage),游戏视图是全屏三栏不滚动。overflow:hidden 只写在 .game-screen 上,不放在两视图的共享祖先上——这是从同系列项目踩坑中总结的经验,共享祖先的 overflow:hidden 会静默阻断长首页的滚动。
左栏:角色卡与迷雾地图
CharacterSheet 展示角色的 HP/MP 状态条、四维属性、背包物品和 NPC 好感度。所有数值随游戏进程实时更新——HP 条从满红渐变到危险红,NPC 好感度正值为绿、负值为红。
WorldMap 展示当前所在位置和可达区域,已探索的区域显示名称并可点击前往,未探索的区域显示为「未探索区域」的迷雾状态:
<!-- src/components/WorldMap.vue -->
<li
v-for="area in currentConnections"
:key="area.id"
class="area-node"
:class="{ discovered: area.discovered, fog: !area.discovered }"
@click="area.discovered && appStore.travelTo(area.id)"
>
<span class="area-dot">{{ area.discovered ? '◆' : '◇' }}</span>
<span class="area-name">{{ area.discovered ? area.name : '未探索区域' }}</span>
</li>
来源:src/components/WorldMap.vue
中栏:叙事舞台
GameStage 是游戏的核心交互区——顶部阶段指示条显示当前阶段与回合数,中部叙事日志按角色分色展示城主/NPC/玩家/系统的发言,底部行动输入框支持文字和语音两种方式。叙事日志自动滚动到最新条目,让玩家始终看到最新的城主回应。
右栏:城主智能体
AgentPanel 是城主的具身表达窗口——魔珐星云 SDK 渲染的城主形象占据整个面板,页面加载即自动连接并显示,不需要玩家手动点击。语音识别终稿通过 onASRFinal 回调直接注入 askDM 链路,实现「说完即行动」的零延迟体验。
十、一局实况推演:从玩家开口到城主回应
架构描述再详细,不如跑一局看。以下是一次真实交互的全链路拆解,展示前文所有模块如何协同运转。
玩家说出:「我走向酒馆老板,问他最近镇上发生了什么怪事」
这句话进入系统后,首先经过 stores/app.ts 的自然语言检测。detectPhase 扫描四组关键词,匹配到「交谈」→ 判定为社交阶段。当前阶段是探索,阶段不一致,状态机自动切换并写入一条系统日志:「进入社交阶段」。
接下来,玩家的这句话被记录为一条叙事日志,说话者为 player。然后 buildCampaignContext 开始组装上下文——它从 GameState 中抽取当前阶段(社交)、所在位置(断剑酒馆)、角色状态(游侠 Lv.1,HP 45/45,MP 20/20)、背包内容(铁剑、皮甲、治愈药水×3、火把×5、神秘信件)、已探索区域(断剑酒馆)、相邻可达区域(镇中心广场、后巷)、NPC 关系(艾拉拉 +30、格隆克 0、暗影·薇拉 -40、老酒馆·巴特 +20),以及最近 5 条叙事日志。这些信息拼接为一段结构化文本。
askAvatar 将城主人设提示词、战役上下文和玩家行动合并为一次 SDK 调用。魔珐星云端到端 SDK 接收到这段文本后,内部完成大模型推理、语音合成、口型驱动、表情与手势生成,全部在同一条链路中闭环。约 500ms 后,城主形象开口说话,声音低沉而沉稳,配合手势和表情,叙述酒馆老板的回应——
「巴特擦着杯子,压低声音说:『最近地下城入口那边不太平,有几个冒险者进去后再也没出来。你要是有兴趣,可以去镇中心广场打听打听。』」
整个过程中,业务侧只写了一个函数调用。没有 ASR 对接、没有 LLM 拼接、没有 TTS 分片——端到端 SDK 把感知、理解、决策、表达压缩成一次 ask,其余全部由平台内部完成。这就是端到端架构的核心价值:不是少写了几行胶水代码,而是消除了模块之间的状态断裂风险。
十一、战役设计手记:近两千行代码背后的工程抉择
回顾整个项目,有几个设计决策值得复盘。
为什么用纯函数而不是响应式状态库? 战役状态包含 NPC 好感度、地图迷雾、背包物品、叙事日志等多个互相关联的子系统。如果用分散的响应式变量管理,状态变更的副作用很难追踪。纯函数强制每次变更都是「旧状态 → 新状态」的显式转换,可预测、可回滚、可测试。localStorage 持久化只是 JSON.stringify 一行,刷新页面后战役进度不丢失。
为什么每次提问都重新构建上下文? 城主的回应质量完全取决于上下文完整度。如果缓存提示词,城主会「忘记」新发现的区域、好感度变化、战斗结果。buildCampaignContext 在每次 ask 前从 GameState 实时抽取全量信息,确保城主的每一句话都基于最新战役进度。代价是每次请求的 token 消耗略高,但对战役沉浸感来说,这笔开销完全值得。
为什么用自然语言检测而不是按钮切换? TRPG 的核心体验是自由——玩家不应该被限制在「点击探索按钮」或「点击战斗按钮」的框架里。PHASE_KEYWORDS 让玩家用自然语言描述行动,系统自动识别阶段切换。「我要攻击这个怪物」自动进入战斗阶段,「我想和艾拉拉聊聊」自动进入社交阶段。按钮仍然保留在阶段指示条上,作为快捷操作而非唯一入口。
generation 令牌解决了什么? 端到端 SDK 通过回调通知状态变化。当玩家断开重连时,旧连接的回调可能延迟到达。如果没有令牌机制,旧回调会污染新连接的状态,导致语音识别误触发、说话状态错乱。每次连接递增 generation 并捕获当前值,所有回调内先校验令牌再执行,不匹配则静默丢弃。这个模式在同系列多个项目中反复验证,是端到端 SDK 集成的关键防御机制。
最终,核心游戏逻辑(types + gameEngine + avatar.ts + stores)不到 700 行,加上 6 个 UI 组件与样式共计约 1900 行。代码量不大,但覆盖了一个完整 TRPG 战役的全链路:四阶段状态机、NPC 好感度动态追踪、地图迷雾系统、完整战役上下文注入、自然语言阶段检测、三栏同屏界面与双视图首页切换。
魔珐星云是一套端到端具身交互智能平台,让 AI 通过屏幕和机器人进入真实世界,成为能够感知、理解、表达并行动的智能体。
深渊纪元证明了同一套平台能力可以承载完全不同的产品形态——上一位城主在剧本杀里管理线索与嫌疑人,这一位在地下城裡构建世界观与 NPC 关系。身体可以变化,场景可以切换,但端到端的具身交互智能底座持续复用。
如果你想亲自体验端到端具身交互智能的魅力,或者基于魔珐星云平台搭建属于自己的具身交互智能体,欢迎访问魔珐星云官网,开启你的具身交互智能实战之旅:https://xingyun3d.com?utm_campaign=daily&utm_source=CSDNwanfen2&utm_medium=&utm_term=&utm_content=
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)