鸿蒙原生 ArkTS 推理游戏实战:从 AI 生成 JSON 到互动解谜的完整实现


一、引言
推理游戏是移动端最受欢迎的游戏类型之一。将大语言模型(LLM)与推理游戏结合,可以实现「无限案件、永不重复」的游戏体验——每次 AI 都生成全新的谜题、线索和答案。
本文将以「重生 AI 推理大师」应用为案例,详细讲解在 HarmonyOS NEXT(API 24)上使用 ArkTS 原生框架构建 AI 推理游戏的完整技术路径。内容涵盖:
- System Prompt 工程:如何让 AI 生成结构化的 JSON 谜题数据
- SSE 流式响应解析:逐 token 接收 AI 回复
- JSON 解析与校验:从流式数据中提取完整的 JSON 对象
- 状态机驱动的游戏流程:WELCOME → LOADING → READY → RESULT
- ArkTS @Builder 组件化:利用 @Builder 构建可复用的 UI 片段
- 数据模型版本管理:对比三次迭代的字段演变
二、项目架构
2.1 三层架构
Index.ets(UI 层)
↓ 调用 queryAI() / cancelAI()
AIChatService.ets(服务层 - HTTP + SSE 解析)
↓
GitCode AI API / DeepSeek-V3(AI 模型层)
2.2 应用入口
// entryability/EntryAbility.ets
import { UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
console.error('Failed:', JSON.stringify(err));
}
});
}
}
2.3 项目文件结构
entry/src/main/ets/
├── entryability/
│ └── EntryAbility.ets # 应用入口
└── pages/
├── Index.ets # 推理大师游戏主界面(584行)
├── AIChatService.ets # AI 服务层(SSE + 非流式回退)
└── ...
三、System Prompt 工程:让 AI 生成结构化 JSON
System Prompt 是 AI 对话应用的核心。对于推理游戏,我们需要 AI 返回严格结构化的 JSON 数据,而不是自然语言。
3.1 System Prompt 代码
const SYSTEM_PROMPT: string =
'你是「重生AI推理大师」,一位穿越重生的顶级推理大师,擅长设计精巧的推理谜题。\n' +
'你的任务是为用户生成一个推理案件,并严格按照以下 JSON 格式返回' +
'(不要加 markdown 代码块标记,纯 JSON 字符串):\n\n' +
'{\n' +
' "problem": "案件描述文字,描述一个神秘的事件或场景",\n' +
' "hints": ["线索提示1", "线索提示2", "线索提示3"],\n' +
' "reasoning_results": ["对应线索1的推理结果", "对应线索2的推理结果", "对应线索3的推理结果"],\n' +
' "analysis": "对整个案件的正确性分析与推理过程详解",\n' +
' "clearance": "通关文字——恭喜用户破解案件,用一段气氛感十足的结案陈词"\n' +
'}\n\n' +
'要求:\n' +
'1. problem 要有足够的悬念和细节,让用户有推理空间。\n' +
'2. hints 数组至少 3 条线索,逐步揭示案件关键信息。\n' +
'3. reasoning_results 与 hints 一一对应。\n' +
'4. analysis 要逻辑严谨,给出正确答案。\n' +
'5. clearance 要有仪式感和成就感。\n' +
'6. 案件类型不限:悬疑、逻辑推理、密室逃脱等均可。\n' +
'7. 所有文字用中文。\n' +
'每次响应只返回上述 JSON,不要包含任何额外文字。';
3.2 JSON 结构设计
{
"problem": "案件描述文字,描述一个神秘的事件或场景",
"hints": [
"线索提示1——引导用户思考方向的第一个线索",
"线索提示2——进一步缩小范围的第二个线索",
"线索提示3——接近答案的第三个线索"
],
"reasoning_results": [
"对应线索1的推理结果——如果用户发现这条线索,能推理出什么结论",
"对应线索2的推理延伸——结合之前的线索能推导出什么",
"对应线索3的最终推理——指向正确答案的关键一步"
],
"analysis": "对整个案件的正确性分析与推理过程详解,200~300字",
"clearance": "通关文字——恭喜用户破解案件,气氛感十足的结案陈词"
}
3.3 提示词工程要点
| 技巧 | 说明 | 效果 |
|---|---|---|
| 角色锚定 | 「穿越重生的顶级推理大师」 | 让 AI 的回答风格符合推理大师气质 |
| JSON Schema | 在提示词中给出完整的 JSON 模板 | 确保输出格式严格可控 |
| 数量约束 | 「hints 数组至少 3 条」 | 确保生成足够数量的线索 |
| 范围约束 | 「案件类型不限:悬疑、逻辑推理等」 | 保证谜题多样性 |
| 纯 JSON 约束 | 「不要包含任何额外文字」 | 避免 markdown 代码块包裹 |
| 质量约束 | 「要有足够的悬念和细节」 | 提升谜题质量 |
四、数据模型:CaseData 接口
4.1 接口定义
/** AI 返回的推理案件数据结构 */
interface CaseData {
problem: string; // 案件描述
hints: string[]; // 线索列表(至少3条)
reasoning_results: string[]; // 推理结果列表(与hints一一对应)
analysis: string; // 正确性分析
clearance: string; // 通关文字
}
4.2 GameState 状态枚举
enum GameState {
WELCOME, // 欢迎页
CASE_LOADING, // 加载中
CASE_READY, // 案件已就绪,玩家可查看线索
RESULT_SHOWN, // 玩家提交推理,显示结果
}
4.3 @State 响应式状态
@State gameState: GameState = GameState.WELCOME;
@State caseData: CaseData | null = null; // 当前案件数据
@State userReasoning: string = ''; // 用户输入的推理
@State revealedHints: boolean[] = []; // 线索展开状态
@State isLoading: boolean = false; // 加载状态
@State errorMsg: string = ''; // 错误信息
@State showResults: boolean = false; // 是否显示结果
@State hintRevealCount: number = 0; // 已揭晓线索数
五、游戏核心流程
5.1 完整流程
用户点击「开始新案件」
↓
generateNewCase()
↓
gameState = CASE_LOADING
↓
queryAI() 发起 SSE 请求
↓
onData 逐 token 累积至 rawContent
↓
onDone → 解析 rawContent 中的 JSON
↓
JSON.parse + 字段校验
↓
成功 → caseData 赋值 → gameState = CASE_READY
失败 → errorMsg → gameState = WELCOME
↓
玩家查看线索(点击展开/收起)
↓
玩家在 TextArea 中输入推理
↓
点击「提交推理」
↓
showResults = true → gameState = RESULT_SHOWN
↓
显示:推理结果 / 正确性分析 / 通关文牒
↓
点击「下一个案件」→ 回到 generateNewCase()
5.2 案件生成核心代码
generateNewCase(): void {
if (this.isLoading) return;
this.gameState = GameState.CASE_LOADING;
this.isLoading = true;
this.showResults = false;
this.caseData = null;
this.errorMsg = '';
const chatHistory: ChatMessage[] = [
{ role: 'user', content: '请给我生成一个推理案件。' },
];
let rawContent = '';
queryAI({
onData: (text: string): void => {
rawContent += text; // 累积流式数据
},
onDone: (): void => {
this.isLoading = false;
try {
// 1. 清理 markdown 代码块标记
let cleanJson = rawContent.trim();
if (cleanJson.startsWith('```json')) {
cleanJson = cleanJson.slice(7);
} else if (cleanJson.startsWith('```')) {
cleanJson = cleanJson.slice(3);
}
if (cleanJson.endsWith('```')) {
cleanJson = cleanJson.slice(0, -3);
}
cleanJson = cleanJson.trim();
// 2. 解析 JSON
const parsed: CaseData = JSON.parse(cleanJson) as CaseData;
// 3. 校验 5 个字段
if (!parsed.problem || !parsed.hints || !parsed.reasoning_results ||
!parsed.analysis || !parsed.clearance) {
throw new Error('返回数据缺少必要字段');
}
// 4. 赋值
this.caseData = parsed;
this.revealedHints = new Array(parsed.hints.length).fill(false);
this.gameState = GameState.CASE_READY;
// 5. 滚动到底部
setTimeout(() => this.scroller.scrollEdge(Edge.End), 100);
} catch (e) {
this.errorMsg = '案件生成异常,请重试。';
this.gameState = GameState.WELCOME;
}
},
onError: (errMsg: string): void => {
this.isLoading = false;
this.errorMsg = '灵机中断:' + errMsg;
this.gameState = GameState.WELCOME;
},
}, chatHistory);
}
关键设计:
- 流式累积:
rawContent在onData中累加,等onDone后再统一解析 - Markdown 清理:虽然 System Prompt 要求纯 JSON,但部分模型仍会包裹代码块,需要做兼容处理
- 字段校验:5 个字段缺一不可,任何一个缺失都视为失败
- 错误兜底:JSON 解析失败或字段缺失时,回到欢迎页并显示错误信息
5.3 线索展开/收起
toggleHint(index: number): void {
if (index >= 0 && index < this.revealedHints.length) {
this.revealedHints[index] = !this.revealedHints[index];
this.revealedHints = [...this.revealedHints]; // 触发 @State 重渲染
if (this.revealedHints[index]) {
this.hintRevealCount++;
} else {
this.hintRevealCount--;
}
}
}
要点:this.revealedHints = [...this.revealedHints] 这行至关重要。直接修改数组中某个元素不会触发 @State 更新,必须创建新数组引用。
5.4 提交推理
submitReasoning(): void {
if (!this.caseData) return;
this.showResults = true;
this.gameState = GameState.RESULT_SHOWN;
}
无论用户是否填写了推理,都可以提交。空的推理不会在结果页展示。
六、UI 组件化设计(@Builder)
6.1 组件树
Index(主页面)
├── 标题栏(深色背景 + 金色标题)
├── 内容区(按 gameState 切换)
│ ├── WELCOME → buildWelcomeSection()
│ │ ├── 🕵️ 大图标
│ │ ├── 标题 + 副标题
│ │ └── 「开始新案件」按钮
│ ├── CASE_LOADING → buildLoadingSection()
│ │ └── LoadingProgress + 加载文字
│ └── CASE_READY / RESULT_SHOWN → buildCaseContent()
│ ├── 📋 案件描述
│ ├── 💡 线索列表(ForEach + buildHintCard)
│ │ └── buildHintCard(index, hint)
│ │ ├── 🔒/🔓 按钮
│ │ └── 展开后的线索内容
│ ├── 🧠 用户推理输入(仅 CASE_READY 时显示)
│ │ ├── TextArea
│ │ ├── 「跳过推理」按钮
│ │ └── 「提交推理」按钮
│ ├── 🔎 推理结果(仅 RESULT_SHOWN)
│ │ ├── ForEach 每条线索的推理分析
│ │ ├── 📊 正确性分析
│ │ └── 🏆 通关文牒
│ └── 「下一个案件」按钮
└── 底部留白
6.2 @Builder 使用示例
@Builder
buildWelcomeSection(): void {
Column() {
Blank().height(60)
Text('🕵️').fontSize(64).margin({ bottom: 16 })
Text('重生 AI 推理大师')
.fontSize(26).fontWeight(FontWeight.Bold)
.fontColor('#E8D5B7').margin({ bottom: 8 })
Text('穿越重生,化身顶级侦探\n每个案件都是一场智慧的较量')
.fontSize(15).fontColor('#8899AA')
.textAlign(TextAlign.Center).lineHeight(24)
.margin({ bottom: 32 })
Button() {
Text('📋 开始新案件')
.fontSize(18).fontColor(Color.White)
.padding({ left: 32, right: 32, top: 12, bottom: 12 })
}
.type(ButtonType.Capsule)
.backgroundColor('#C9A84C')
.onClick((): void => this.generateNewCase())
}
.width('100%')
.alignItems(HorizontalAlign.Center)
}
6.3 线索卡片
@Builder
buildHintCard(index: number, hint: string): void {
Column() {
Button() {
Row() {
Text(this.revealedHints[index] ? '🔓' : '🔒').fontSize(16).margin({ right: 8 })
Text('线索 ' + (index + 1)).fontSize(15).fontColor('#C9A84C')
Blank()
Text(this.revealedHints[index] ? '收起 ▲' : '展开 ▼').fontSize(12).fontColor('#667788')
}
.width('100%')
}
.width('100%').height(44)
.backgroundColor('#1E2A4A').borderRadius(10)
.onClick((): void => this.toggleHint(index))
if (this.revealedHints[index]) {
Column() {
Text(hint).fontSize(15).fontColor('#E0E0E0').lineHeight(22)
}
.width('100%').padding(14)
.backgroundColor('#0F1A36')
.borderRadius({ bottomLeft: 10, bottomRight: 10 })
}
}
.width('100%').margin({ bottom: 8 })
}
@Builder 的优势:
- 可以访问所在 struct 的所有属性和方法
- 可以接收参数(如
index和hint) - 比提取为独立的
@Component更轻量 - 直接在
build()方法中调用:this.buildHintCard(index, hint)
6.4 安全 Getter 方法
getProblem(): string {
if (this.caseData) return this.caseData.problem;
return '';
}
getHintsList(): string[] {
if (this.caseData) return this.caseData.hints;
return [];
}
getReasoningResultsList(): string[] {
if (this.caseData) return this.caseData.reasoning_results;
return [];
}
getAnalysis(): string {
if (this.caseData) return this.caseData.analysis;
return '';
}
getClearance(): string {
if (this.caseData) return this.caseData.clearance;
return '';
}
所有 getter 方法都做了空值安全处理,确保即使 caseData 为 null 也不会崩溃。
七、AI 服务层:SSE 流式解析
7.1 请求构建
export function queryAI(callbacks: AICallbacks, messages: ChatMessage[]): void {
// 取消上次请求
if (httpRequestTask) { httpRequestTask.destroy(); }
const httpRequest = http.createHttp();
httpRequestTask = httpRequest;
// 合并系统提示词
const fullMessages = [
{ role: 'system', content: SYSTEM_PROMPT },
...messages,
];
const requestBody = {
model: 'deepseek-ai/DeepSeek-V3',
messages: fullMessages,
stream: true,
max_tokens: 4096,
temperature: 0.8,
top_p: 0.95,
};
// 发起请求
httpRequest.request(API_URL, {
method: http.RequestMethod.POST,
header: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
Accept: 'text/event-stream',
},
extraData: JSON.stringify(requestBody),
connectTimeout: 30000,
readTimeout: 120000,
}, callback);
}
7.2 SSE 数据格式
SSE 流式数据以 \n\n 分隔每条消息:
data: {"choices":[{"delta":{"content":"{"}}]}
data: {"choices":[{"delta":{"content":"\"p"}}]}
data: {"choices":[{"delta":{"content":"ro"}}]}
data: {"choices":[{"delta":{"content":"blem"}}]}
...
data: [DONE]
每一条 data: 行包含一个 delta.content 字段,包含本次推送的文本片段。
7.3 SSE 解析核心
httpRequest.on('dataReceive', (data: ArrayBuffer) => {
const text = arrayBufferToString(data);
buffer += text;
const lines = buffer.split('\n');
buffer = lines.pop() ?? ''; // 最后一行不完整
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith('data:')) continue;
if (trimmed === 'data:[DONE]') { /* 结束 */ continue; }
const content = parseSSEDataLine(trimmed);
if (content) callbacks.onData(content);
}
});
八、字段名一致性:数据模型版本管理
8.1 三次迭代的字段演变
| 版本 | AI 返回字段 | 接口字段 | 状态 |
|---|---|---|---|
| v1 | "question" / "clearance_text" |
problem / clearance |
❌ 不匹配 |
| v2 | "problem" / "clearance" |
problem / clearance |
✅ 匹配 |
8.2 问题的发现与修复
在开发过程中,用户报告「返回有问题」。排查后发现:System Prompt 中写的 JSON 字段名与 TypeScript 接口中的字段名不一致。
System Prompt 中的模板:
{
"problem": "...",
"hints": [...],
"reasoning_results": [...],
"analysis": "...",
"clearance": "..."
}
接口定义:
interface CaseData {
problem: string;
hints: string[];
reasoning_results: string[];
analysis: string;
clearance: string;
}
修复方案:统一使用 "problem" 和 "clearance"。
8.3 教训
- 数据模型先行:先定义 TypeScript 接口,再根据接口编写 System Prompt
- 字段名一致性:System Prompt 中的 JSON key 必须与代码中的接口字段名完全一致
- 端到端验证:修改后必须从「AI 输出 → JSON 解析 → UI 渲染」全链路确认
九、ArkTS 关键字避坑
9.1 return 在 build() 中无效
// ❌ 错误:build() 中不能使用 return
build() {
if (!this.data) return; // 编译错误
// ...
}
// ✅ 正确:使用 if...else
build() {
if (!this.data) {
Column() { /* loading */ }
} else {
Column() { /* content */ }
}
}
9.2 数组 @State 更新
// ❌ 错误:直接修改数组元素不会触发 UI 更新
this.revealedHints[index] = true; // UI 不会重渲染
// ✅ 正确:创建新数组引用
this.revealedHints[index] = true;
this.revealedHints = [...this.revealedHints]; // 触发重渲染
9.3 private 属性传参
// ❌ 警告:private 属性不能通过构造函数传值
@Component
struct MyComp {
private value: string = '';
}
MyComp({ value: 'test' }) // 会有警告
// ✅ 推荐:移除 private
@Component
struct MyComp {
value: string = '';
}
9.4 @Builder 不能 return
// ❌ 错误:@Builder 中不能 return
@Builder
buildSomething(): void {
if (!this.data) return; // 编译错误
// ...
}
// ✅ 正确:使用 if...else
@Builder
buildSomething(): void {
if (!this.data) {
Text('empty')
} else {
Text(this.data)
}
}
十、性能优化
10.1 流式累积
onData 每次收到一个文本片段就追加到 rawContent。直到 onDone 才统一解析 JSON,避免每次数据到达都触发 JSON 解析(前 N-1 次必然失败)。
10.2 setTimeout 延迟滚动
setTimeout((): void => {
this.scroller.scrollEdge(Edge.End);
}, 100);
100ms 的延迟让 ArkUI 框架完成布局计算后再触发滚动,确保「滚到底部」效果正确。
10.3 创建新数组触发更新
this.revealedHints = [...this.revealedHints];
数组 spread 运算符创建新数组,强制 ArkUI 的 @State 检测到引用变化,触发子组件重渲染。
十一、视觉设计
11.1 颜色方案
| 用途 | 色值 | 说明 |
|---|---|---|
| 页面背景 | #16213E |
深海蓝 |
| 卡片背景 | #1E2A4A |
深蓝紫 |
| 输入背景 | #0F1A36 |
极深蓝 |
| 标题/强调 | #C9A84C |
古铜金 |
| 正文文字 | #E0E0E0 |
浅灰 |
| 次要文字 | #8899AA |
灰蓝 |
| 提示文字 | #667788 |
暗灰 |
| 通关文字 | #FFD700 |
金色 |
整体设计语言为「暗色调 + 古铜金点缀」,营造神秘、沉静、典雅的推理氛围。
11.2 UI 截图布局
┌──────────────────────────────┐
│ 🔍 重生 AI 推理大师 │ ← 标题栏(深色背景+金色文字)
├──────────────────────────────┤
│ │
│ 🕵️ │ ← 大图标
│ 重生 AI 推理大师 │
│ 穿越重生,化身顶级侦探 │
│ │
│ ┌────────────────────┐ │
│ │ 📋 开始新案件 │ │ ← 胶囊按钮
│ └────────────────────┘ │
│ │
├──────────────────────────────┤
│ 📋 案件描述 │ ← 案件描述区域
│ ┌────────────────────┐ │
│ │ 案件文字... │ │
│ └────────────────────┘ │
│ 💡 线索提示(已揭晓 0/3) │
│ ┌────────────────────┐ │
│ │ 🔒 线索 1 展开 ▼ │ │ ← 可展开/收起
│ └────────────────────┘ │
│ ┌────────────────────┐ │
│ │ 🔒 线索 2 展开 ▼ │ │
│ └────────────────────┘ │
│ 🧠 你的推理 │
│ ┌────────────────────┐ │
│ │ TextArea... │ │ ← 推理输入
│ └────────────────────┘ │
│ [跳过推理] [提交推理] │
├──────────────────────────────┤
│ 🔎 推理结果 │ ← 提交后显示
│ ┌ 线索1 → 分析... ┐ │
│ ├ 线索2 → 分析... ┤ │
│ ├ 线索3 → 分析... ┤ │
│ └────────────────────┘ │
│ 📊 正确性分析 │
│ ┌ ┐ │
│ │ 分析文字... │ │
│ └────────────────────┘ │
│ 🏆 通关文牒 │
│ ┌ ┐ │
│ │ 恭喜通关文字... │ │
│ └────────────────────┘ │
│ [📋 下一个案件] │
└──────────────────────────────┘
十二、错误处理与兜底
12.1 三层错误兜底
| 层级 | 处理方式 | 说明 |
|---|---|---|
| SSE 流式 | onData 累积 → onDone 解析 |
最佳体验 |
| 非流式回退 | dataReceive 未触发时,从 request 回调解析 |
兼容性保障 |
| 解析失败 | catch 块 + 错误信息提示 |
防止白屏 |
12.2 错误信息展示
try {
const parsed = JSON.parse(cleanJson) as CaseData;
if (!parsed.problem || !parsed.hints || /* ... */) {
throw new Error('缺少字段');
}
this.caseData = parsed;
this.gameState = GameState.CASE_READY;
} catch (e) {
this.errorMsg = '案件生成异常,请重试。';
this.gameState = GameState.WELCOME;
}
12.3 空值安全
所有 getter 方法都做了空值检查:
getProblem(): string {
if (this.caseData) return this.caseData.problem;
return '';
}
UI 中条件渲染:
if (this.caseData) {
// 显示案件内容
}
if (this.showResults && this.userReasoning.trim().length > 0) {
// 显示用户推理(仅当非空时)
}
十三、总结与展望
「重生 AI 推理大师」是一个完整的 AI 驱动推理游戏应用,涵盖了从 System Prompt 工程到 SSE 流式解析、从 JSON 数据模型到互动 UI 的全链路开发。
核心技术点
- System Prompt 输出 JSON:通过精确的 JSON Schema 约束,让 AI 输出可解析的结构化数据
- SSE 流式累积 + 统一解析:避免逐 token 解析 JSON 的前 N-1 次必然失败问题
- 状态机驱动 UI:4 种 GameState + @State 驱动,代码逻辑清晰
- @Builder 组件化:轻量级 UI 复用,无需单独创建 Component 文件
- 空值安全:所有数据访问都经过安全 getter 和条件渲染
可以扩展的方向
- 难度分级:让玩家选择简单/普通/困难模式
- 评分系统:根据玩家查看线索的数量和推理准确度评分
- 案件记录:将已破解的案件保存在本地数据库
- 多轮互动:允许玩家向 AI 追问更多细节
- 语音输入:集成语音转文字,提升推理输入便捷性
- 排行榜:接入鸿蒙游戏服务,支持全球排名
更多推荐




所有评论(0)