在这里插入图片描述

在这里插入图片描述

一、引言

推理游戏是移动端最受欢迎的游戏类型之一。将大语言模型(LLM)与推理游戏结合,可以实现「无限案件、永不重复」的游戏体验——每次 AI 都生成全新的谜题、线索和答案。

本文将以「重生 AI 推理大师」应用为案例,详细讲解在 HarmonyOS NEXT(API 24)上使用 ArkTS 原生框架构建 AI 推理游戏的完整技术路径。内容涵盖:

  1. System Prompt 工程:如何让 AI 生成结构化的 JSON 谜题数据
  2. SSE 流式响应解析:逐 token 接收 AI 回复
  3. JSON 解析与校验:从流式数据中提取完整的 JSON 对象
  4. 状态机驱动的游戏流程:WELCOME → LOADING → READY → RESULT
  5. ArkTS @Builder 组件化:利用 @Builder 构建可复用的 UI 片段
  6. 数据模型版本管理:对比三次迭代的字段演变

二、项目架构

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);
}

关键设计

  1. 流式累积rawContentonData 中累加,等 onDone 后再统一解析
  2. Markdown 清理:虽然 System Prompt 要求纯 JSON,但部分模型仍会包裹代码块,需要做兼容处理
  3. 字段校验:5 个字段缺一不可,任何一个缺失都视为失败
  4. 错误兜底: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 的所有属性和方法
  • 可以接收参数(如 indexhint
  • 比提取为独立的 @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 方法都做了空值安全处理,确保即使 caseDatanull 也不会崩溃。


七、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 教训

  1. 数据模型先行:先定义 TypeScript 接口,再根据接口编写 System Prompt
  2. 字段名一致性:System Prompt 中的 JSON key 必须与代码中的接口字段名完全一致
  3. 端到端验证:修改后必须从「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 的全链路开发。

核心技术点

  1. System Prompt 输出 JSON:通过精确的 JSON Schema 约束,让 AI 输出可解析的结构化数据
  2. SSE 流式累积 + 统一解析:避免逐 token 解析 JSON 的前 N-1 次必然失败问题
  3. 状态机驱动 UI:4 种 GameState + @State 驱动,代码逻辑清晰
  4. @Builder 组件化:轻量级 UI 复用,无需单独创建 Component 文件
  5. 空值安全:所有数据访问都经过安全 getter 和条件渲染

可以扩展的方向

  • 难度分级:让玩家选择简单/普通/困难模式
  • 评分系统:根据玩家查看线索的数量和推理准确度评分
  • 案件记录:将已破解的案件保存在本地数据库
  • 多轮互动:允许玩家向 AI 追问更多细节
  • 语音输入:集成语音转文字,提升推理输入便捷性
  • 排行榜:接入鸿蒙游戏服务,支持全球排名
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐