Harmony os —— Data Augmentation Kit 知识问答完整示例实战拆解(从 0 跑通流式 RAG)

Harmony os ------ Data Augmentation Kit 知识问答完整示例实战拆解(从 0 跑通流式 RAG)

这篇就是把 "完整示例代码" 好好理一遍,记录一下我自己在 HarmonyOS 上用 Data Augmentation Kit 跑通 知识问答 + 流式 RAG 的全过程。

文档里的东西偏"说明书风格",这一篇我就按"项目视角 + 文件视角"来拆:
从工程启动 → 知识加工 → 创建 RagSession → 问答 UI → 流式大模型交互 ,一条链子说清楚。鸿蒙开发者第四期活动


一、这个示例到底干了什么?

一句话概括整个 DEMO 做的事:

从内置 JSON 构造一张 email 表 → 自动触发知识加工生成知识库 → 创建 RagSession → 在页面上输入问题 → 调用 streamRun 做流式问答 → 把思考过程 + 最终答案一边生成一边展示出来。

整个流程大概是这样:

  1. App 启动时:
    • 初始化数据库(email 表);
    • sourceData.json 把模拟邮件数据插入数据库;
    • 根据 knowledge_schema.json 自动做"知识加工",生成向量库 & 倒排库;
    • 创建 RagSession 并丢到 AppStorage 里。
  2. 页面 Index.ets
    • 有一个输入框(问题)、一个"streamRun" 按钮、一个结果区;
    • 点击按钮 → 调用 session.streamRun()
    • Data Augmentation Kit 去检索知识库 + 调你实现的 MyChatLLM
    • 通过 callback 流式推回 THOUGHT(思考)ANSWER(答案),UI 里实时拼接显示。

下面按文件来拆每个角色。


二、工程关键文件总览

这个完整示例的核心文件可以简单理解成这张表:

角色 文件 主要职责
生命周期入口 EntryAbility.ets 启动时建表、插数据、创建 RagSession
数据源构造 SetUp.ets 连接数据库、建 email 表、插入 JSON 数据
LLM HTTP 工具 HttpUtils.ets 和大模型流式交互(ModelArts 示例)
LLM 适配层 MyChatLlm.ets 继承 ChatLLM,封装 streamChat
RAG 配置 Config.ets 组装 retrievalConfig / retrievalCondition / rag.Config
UI 页面 pages/Index.ets 输入问题、点击按钮、展示 thought + answer
知识加工 schema rawfile/arkdata/knowledge/knowledge_schema.json 定义如何从 email 表生成知识库
模拟数据源 rawfile/sourceData.json 一封"手机优惠政策"的测试邮件

搞清楚这几个文件之间的关系,基本就理解了整个 Demo 的结构。


三、EntryAbility:启动就把"底子"铺好

文件: src/main/ets/entryability/EntryAbility.ets

这个 Ability 做了三件大事:

  1. 加载首页 UI
  2. 初始化数据库 & 插入数据
  3. 创建 RagSession,并在销毁时负责善后

核心逻辑在 onWindowStageCreate

复制代码
onWindowStageCreate(windowStage: window.WindowStage): void {
  // 1. 加载首页页面
  windowStage.loadContent('pages/Index', (err) => { ... });

  // 2. 把 context 丢进 AppStorage,后面大家都从这里拿
  AppStorage.setOrCreate<common.UIAbilityContext>('Context', this.context);

  // 3. 初始化数据表 + 插入数据
  let setUp: SetUp = new SetUp();
  setUp.initTable().then(() => {
    setUp.insertData();
    AppStorage.setOrCreate<SetUp>('SetUpObject', setUp);
  });

  // 4. 创建 RagSession
  let config: Config = new Config();
  rag.createRagSession(this.context, config.getRAGConfig()).then((data) => {
    AppStorage.setOrCreate<rag.RagSession>('RagSessionObject', data);
  }).catch((err: BusinessError) => {
    ...
  });
}

销毁时记得把东西关掉:

复制代码
onWindowStageDestroy(): void {
  const session = AppStorage.get<rag.RagSession>('RagSessionObject') as rag.RagSession;
  session?.close().catch(() => {
    hilog.error(DOMAIN, 'testTag', 'close rag session failed');
  });
  const setup = AppStorage.get<SetUp>('SetUpObject') as SetUp;
  setup?.closeStore();
}

小结:

  • EntryAbility 负责"一次性初始化":表结构、数据、RagSession;
  • AppStorage 是全局共享的"上下文地图",后面页面、配置类都从这里拿 Context / RagSession / SetUp

四、SetUp:从 JSON 搞到数据库 & 触发知识加工

文件: src/main/ets/entryability/SetUp.ets

这个类的关键词有两个:

  • 源数据库配置里的 enableSemanticIndex: true
  • 以及和 knowledge_schema.json 一一对应的 storeName

1. 配置源数据库(触发知识加工的关键)

复制代码
storeName: string = 'testmail_store.db';  // 要和 knowledge_schema.json 里的 dbName 一致

storeConfig: relationalStore.StoreConfig = {
  name: this.storeName,
  securityLevel: relationalStore.SecurityLevel.S3,
  enableSemanticIndex: true,  // ⭐ 必须为 true 才会触发知识加工
  tokenizer: relationalStore.Tokenizer.CUSTOM_TOKENIZER
};

enableSemanticIndex: true + 正确的 knowledge_schema.json

= 系统会自动根据 schema 对数据做知识加工,生成向量表 & 倒排表。

2. 建表

复制代码
const createTableSql = `
  CREATE TABLE IF NOT EXISTS email(
    id integer primary key,
    subject text,
    content text,
    image_text text,
    attachment_names text,
    inline_files text,
    sender text,
    receivers text,
    received_date text
  );
`;
await tmpStore?.execute(createTableSql, 0, undefined);

3. 从 sourceData.json 插入模拟数据

这一段逻辑就是:

  1. 遍历 rawfile 目录下的 file;
  2. 找到 sourceData*.json
  3. 读取文件 → 转成字符串 → JSON.parse
  4. 把每封"邮件"映射成 email 表的一行,执行 insert

重点看几处小处理:

复制代码
const rawFileData = await context.resourceManager.getRawFileContent(file);
const fileData: string = buffer.from(rawFileData).toString();
const resultObjArr = JSON.parse(fileData) as Array<object>;

...

let sender: string = jsonObj?.['sender_name'];
if (!sender || sender.length == 0) {
  sender = 'undefined';
}

const receiverStr: string = JSON.stringify(jsonObj['to']);
const formattedDateStr: string = jsonObj?.['received_time']?.replace(' ', 'T');
let received_date = Date.parse(formattedDateStr);
if (!received_date || Number.isNaN(received_date)) {
  received_date = 0;
}

let subject: string = jsonObj?.['subject']?.replace(/'/g, '');
let doc: string = jsonObj?.['body']?.replace(/'/g, '');

let sql = `insert or replace into email VALUES(${dataIndex}, '${subject}', '${doc}', '',` +
  ` '', '', '${sender}', '${receiverStr}', '${received_date}');`

可以看到:

  • subject / body 里的单引号都被 replace(/'/g, '') 处理了一下,避免拼 SQL 炸掉;
  • received_time 被转成时间戳 received_date 存到表里;
  • to 被 JSON 序列化后存入 receivers

实战场景里,这些数据肯定不是从 JSON 读,而是从 UI / 服务器来,这里就是个"知识加工入口"的演示。


五、HttpUtils:和大模型流式聊聊天

文件: src/main/ets/entryability/HttpUtils.ets

这是一个很纯粹的 大模型 HTTP 工具类,做几件事:

  1. 组装 HTTP 请求参数(带上 modeltemperatureAPI Key 等);
  2. 使用 http.requestInStream 发起流式请求;
  3. 注册 dataReceive 回调,把流式数据丢给上层。

1. 请求配置

复制代码
initOption(question: string) {
  let option: http.HttpRequestOptions = {
    method: http.RequestMethod.POST,
    header: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ****replace your API key in here****`
    },
    extraData: {
      stream: true,
      temperature: 0.1,
      max_tokens: 1000,
      frequency_penalty: 1,
      model: 'qwen3-235b-a22b',
      top_p: 0.1,
      presence_penalty: -1,
      messages: JSON.parse(question),
      chat_template_kwargs: {
        enable_thinking: false
      }
    }
  };
  return option;
}

注意:

  • 这里的 model / url / API Key 都是示例,实际要换成你自己在 ModelArts 或其他平台上的配置;
  • messages 是标准 Chat 格式:[{ role, content}, ...],外面会把问题封成 JSON 字符串传进来。

2. 流式请求 + 回调注册

复制代码
async requestInStream(question: string) {
  if (!this.httpRequest) {
    this.httpRequest = http.createHttp();
  }
  this.httpRequest.requestInStream(this.url, this.initOption(question)).catch(...);
  this.isFinished = false;
}

on(callback: Callback<ArrayBuffer>) {
  if (!this.httpRequest) {
    this.httpRequest = http.createHttp();
  }
  this.httpRequest.on('dataReceive', callback);
}

cancel() {
  this.httpRequest?.off('dataReceive');
  this.httpRequest?.destroy();
  this.httpRequest = undefined;
}

这里没有做"半路中断某一条请求"的精细化处理,简单暴力地 destroy();对 demo 来说够用。

别忘了在 module.json5 里加网络权限:
"name": "ohos.permission.INTERNET",否则这里的 HTTP 全挂。


六、MyChatLlm:把大模型结果适配成 LLMStreamAnswer

文件: src/main/ets/entryability/MyChatLlm.ets

这个类是整个 RAG 链路的 大脑适配层

  • 继承 rag.ChatLLM
  • 实现 streamChat & cancel
  • 把 HTTP 层返回的二进制流解析成 LLMStreamAnswer

1. 流式数据解析 parseLLMResponse

这个示例假定大模型返回格式类似 OpenAI 风格的 SSE(data: {...} 形式),解析大概这样:

复制代码
function parseLLMResponse(data: ArrayBuffer): rag.LLMStreamAnswer | undefined {
  let decoder = util.TextDecoder.create(`"utf-8"`);
  let str = decoder.decodeToString(new Uint8Array(data));
  hilog.info(0, TAG, str);
  let chunk = '';
  let isFinished: boolean = (str.length < 20);

  for (let resultStr of str.split('data:')) {
    if (resultStr.trim() == ('[DONE]')) {
      isFinished = true;
      break;
    }
    if (resultStr.trim().length == 0) {
      continue;
    }
    try {
      let obj = JSON.parse(resultStr.trim());
      if ((obj as object)?.['choices'].length === 0) {
        continue;
      }
      if ((obj as object)?.['choices'][0]?.['delta']?.['reasoning_content']) {
        chunk += (obj as object)?.['choices'][0]['delta']['reasoning_content'];
      } else if ((obj as object)?.['choices'][0]?.['delta']?.['content']) {
        chunk += (obj as object)?.['choices'][0]['delta']['content'];
      }
    } catch (err) {
      hilog.error(0, TAG, `Parse LLM response failed, resultStr: ${resultStr}`);
    }
  }

  let answer: rag.LLMStreamAnswer = {
    isFinished,
    chunk
  };
  return answer;
}

这里有两个点:

  • 模型可能同时返回 推理过程(reasoning_content最终内容(content,示例里两个字段都兼容;
  • 当收到 [DONE] 或者数据很短时,就认为这轮回答结束。

2. 实现 streamChat

复制代码
export default class MyChatLLM extends rag.ChatLLM {
  async streamChat(query: string, callback: Callback<rag.LLMStreamAnswer>): Promise<rag.LLMRequestInfo> {
    let ret: rag.LLMRequestStatus = rag.LLMRequestStatus.LLM_SUCCESS;
    try {
      let dataCallback = async (data: ArrayBuffer) => {
        const answer = parseLLMResponse(data);
        if (!answer) return;

        HttpUtils.isFinished = answer.isFinished;
        callback(answer);
      };

      HttpUtils.on(dataCallback);
      HttpUtils.requestInStream(query);
    } catch (err) {
      hilog.error(0, TAG, `Request LLM failed, error code: ${err.code}, error message: ${err.message}`);
      ret = rag.LLMRequestStatus.LLM_REQUEST_ERROR;
    }
    return {
      chatId: 0,
      status: ret
    };
  }

  cancel(chatId: number): void {
    hilog.info(0, TAG, `The request for the large model has been canceled. chatId: ${chatId}`);
    HttpUtils.cancel();
  }
}

小结:

  • 对 Data Augmentation Kit 来说,它完全不关心你用的是什么模型,只要你把 问题 JSON 字符串 → LLMStreamAnswer 流 这件事实现好了就行;
  • 和不同大模型打交道时,只要换 HttpUtils & parseLLMResponse 的逻辑就可以了。

七、Config:RAG 的"大配置中心"

文件: src/main/ets/entryability/Config.ets

Config 做的是:组装一份 rag.ConfigcreateRagSession

1. RetrievalConfig:告诉系统"知识库在哪"

复制代码
getRetrievalConfig() {
  let storeConfigVector: relationalStore.StoreConfig = {
    name: 'testmail_store_vector.db',
    securityLevel: relationalStore.SecurityLevel.S3,
    vector: true
  };

  let storeConfigInvIdx: relationalStore.StoreConfig = {
    name: 'testmail_store.db',
    securityLevel: relationalStore.SecurityLevel.S3,
    tokenizer: relationalStore.Tokenizer.CUSTOM_TOKENIZER
  };

  let context = AppStorage.get<common.UIAbilityContext>('Context') as common.UIAbilityContext;

  let channelConfigVector: retrieval.ChannelConfig = {
    channelType: retrieval.ChannelType.VECTOR_DATABASE,
    context,
    dbConfig: storeConfigVector
  };
  let channelConfigInvIdx: retrieval.ChannelConfig = {
    channelType: retrieval.ChannelType.INVERTED_INDEX_DATABASE,
    context,
    dbConfig: storeConfigInvIdx
  };

  let retrievalConfig: retrieval.RetrievalConfig = {
    channelConfigs: [channelConfigInvIdx, channelConfigVector]
  };
  return retrievalConfig;
}

注意:

  • 向量库文件名里加了 _vector 后缀;
  • 倒排库就是原始库;
  • 两个通道最终都写入 RetrievalConfig.channelConfigs 中。

2. RetrievalCondition:多路召回 + 重排逻辑

复制代码
let recallConditionInvIdx: retrieval.InvertedIndexRecallCondition = {
  ftsTableName: 'email_inverted',
  fromClause: 'select email_inverted.reference_id as rowid, * from email INNER JOIN email_inverted ON email.id = email_inverted.reference_id',
  primaryKey: ['chunk_id'],
  responseColumns: ['reference_id', 'chunk_id', 'chunk_source', 'chunk_text', 'subject', 'image_text',
    'attachment_names'],
  deepSize: 500,
  recallName: 'invertedvectorRecall',
};

let floatArray = new Float32Array(128).fill(0.1);
let vectorQuery: retrieval.VectorQuery = {
  column: 'repr',
  value: floatArray,
  similarityThreshold: 0.1
};

let recallConditionVector: retrieval.VectorRecallCondition = {
  vectorQuery,
  fromClause: 'email_vector',
  primaryKey: ['id'],
  responseColumns: ['reference_id', 'chunk_id', 'chunk_source', 'repr'],
  recallName: 'vectorRecall',
  deepSize: 500
};

let rerankMethod: retrieval.RerankMethod = {
  rerankType: retrieval.RerankType.RRF,
  isSoftmaxNormalized: true,
};

let retrievalCondition: retrieval.RetrievalCondition = {
  rerankMethod,
  recallConditions: [recallConditionInvIdx, recallConditionVector],
  resultCount: 5
};

几点说明:

  • 倒排召回:fromClause 用 INNER JOIN 把原始表 email 和倒排表连起来;
  • 向量召回:只查 email_vector 表;
  • responseColumns 必须来自 fromClause 的结果列,不然会报错;
  • Float32Array(128).fill(0.1) 在示例中只是一个占位向量,真实场景下应该用 问题的向量表示
  • 重排采用 RRF(Reciprocal Rank Fusion)。

3. 拼成 RAGConfig

复制代码
getRAGConfig() {
  let retrievalConfig = this.getRetrievalConfig();
  let retrievalCondition = this.getRetrivalCondition();
  let config: rag.Config = {
    llm: new MyChatLlm(),
    retrievalConfig,
    retrievalCondition
  };
  return config;
}

八、Index 页面:一个按钮触发一次完整 RAG 流程

文件: src/main/ets/pages/Index.ets

UI 很简单,但已经包含了一个"最小可用"的问答界面:

  • 上面一个 TextArea 输入问题;
  • 中间一个 Button;
  • 下面一个区域展示模型的"思考过程 + 最终回答"。

关键逻辑在 Button 的 onClick

复制代码
Button('streamRun')
  .onClick(async () => {
    // 1. 拿到 RagSession
    let session: rag.RagSession = AppStorage.get<rag.RagSession>('RagSessionObject') as rag.RagSession;

    // 2. 配置输出哪些流
    let config: rag.RunConfig = {
      answerTypes: [rag.StreamType.THOUGHT, rag.StreamType.ANSWER]
    };

    this.thoughtStr = '';
    this.answerStr = '';

    // 3. 发起提问(流式)
    session.streamRun(this.inputStr, config, ((err: BusinessError, stream: rag.Stream) => {
      if (err) {
        this.answerStr = `streamRun inner failed. code is ${err.code}, message is ${err.message}`;
      } else {
        switch (stream.type) {
          case rag.StreamType.THOUGHT:
            this.thoughtStr += stream.answer.chunk;
            break;
          case rag.StreamType.ANSWER:
            this.answerStr += stream.answer.chunk;
            break;
          case rag.StreamType.REFERENCE:
          default:
            hilog.info(0, 'Index', `streamRun msg: ${JSON.stringify(stream)}`);
        }
      }
    })).catch((e: BusinessError) => {
      this.answerStr = `streamRun failed. code is ${e.code}, message is ${e.message}`;
    });
  });

配合下面的展示区:

复制代码
Text(this.thoughtStr)
  .fontSize(12)
  .fontColor(Color.Gray)
  ...

Text(this.answerStr)
  .padding(8)
  ...

实际体验:

  • 点击按钮之后,可以看到灰色的小字(THOUGHT)一点点刷出来;
  • 下面的答案区域(ANSWER)同步往后拼;
  • 这就是一个标准的 "流式 RAG 问答体验"

九、knowledge_schema.json:告诉系统怎么"加工知识"

路径: src/main/resources/rawfile/arkdata/knowledge/knowledge_schema.json

这个文件就是知识加工的配置书,核心字段如下:

复制代码
{
  "knowledgeSource": [{
    "version": 1,
    "dbName": "testmail_store.db",
    "tables": [{
      "tableName": "email",
      "referenceFields": ["id"],
      "knowledgeFields": [
        { "columnName": "subject", "type": ["Text"] },
        { "columnName": "content", "type": ["Text"] },
        { "columnName": "image_text", "type": ["Text"] },
        { "columnName": "attachment_names", "type": ["Text"] },
        {
          "columnName": "inline_files",
          "type": ["Json"],
          "parser": [
            {
              "type": "File",
              "path": "$[*].uri"
            }
          ]
        },
        { "columnName": "sender", "type": ["Scalar"], "description": "sender" },
        { "columnName": "receivers", "type": ["Scalar"], "description": "receivers" },
        { "columnName": "received_date", "type": ["Scalar"], "description": "received_date" }
      ]
    }]
  }]
}

关键要点:

  • dbName 必须和 SetUp.storeName 保持一致;
  • referenceFields 是主键/引用字段,这里用的是 id
  • knowledgeFields 告诉系统:
    • 哪些列参与向量化(Text);
    • 哪些作为结构信息存在(Scalar / Json);
    • 对 Json 类型如何解析出文件路径(parser)。

配合 enableSemanticIndex: true,系统就会自动根据这个 schema 对 email 表做知识加工,生成对应的倒排表 + 向量表。


十、sourceData.json:模拟的一封"优惠政策邮件"

路径: src/main/resources/rawfile/sourceData.json

里面就是一封结构化的"邮件":

  • subject:邮件标题
  • sender_namesender_email
  • received_time
  • recipients / to / cc / bcc
  • body:正文(会被存入 content 列)

SetUp 在 insertData() 里会把这些字段映射到 email 表对应列,后面知识加工就以这张表为源头。


十一、从启动到问答的一次完整时序回顾

最后用步骤再串一遍整个调用链:

  1. 应用启动 → EntryAbility.onWindowStageCreate
    • 设置首页 Index
    • context 存到 AppStorage
    • SetUp.initTable() 建 email 表;
    • SetUp.insertData() 插入 sourceData.json 中的模拟数据;
    • 由于 enableSemanticIndex: true + 正确的 knowledge_schema.json → 系统开始后台做知识加工,生成 testmail_store_vector.db / email_inverted 等;
    • Config.getRAGConfig() 组装 rag.Config
    • rag.createRagSession(...) 创建 RagSession,丢到 AppStorage
  2. 用户打开首页 Index
    • 输入问题(默认示例里是"知识问答开发指南完整示例代码");
    • 点击 streamRun 按钮。
  3. Index 里:
    • AppStorageRagSession
    • RunConfig.answerTypes = [THOUGHT, ANSWER]
    • session.streamRun(inputStr, config, callback)
  4. Data Augmentation Kit 内部:
    • 根据 retrievalConfig / retrievalCondition 对知识库做多路召回 + 重排;
    • 构造 prompt + 上下文;
    • 调用你实现的 MyChatLLM.streamChat()
  5. MyChatLLM
    • HttpUtils.requestInStream() 发起 HTTP 流式请求;
    • 注册 dataReceive 回调,每次收到一段数据 → parseLLMResponse → 得到 LLMStreamAnswer
    • callback(answer) 把结果抛给 RAG / UI。
  6. Index 页面里的 callback:
    • 根据 stream.type 把内容累加到 thoughtStr / answerStr
    • UI 实时刷新出一行行文字。
  7. 应用退出时:
    • onWindowStageDestroy 中关闭 RagSession 和数据库连接。

十二、踩坑 & 小结

实际在看这个例子的时候,我自己觉得需要特别注意的几个点:

  • 网络权限别忘了ohos.permission.INTERNET 不配好,HttpUtils 全部白写。
  • dbName & storeName 必须一致:不然知识加工直接不生效,后面检索会查不到表。
  • enableSemanticIndex: true 必须打开:这是触发知识加工的"电源开关"。
  • RetrievalCondition.responseColumns 一定要对应 fromClause 里能查到的列:一旦写错列名,会直接检索失败。
  • 向量维度要和知识库一致 :示例里是 Float32Array(128),真实项目里不要偷懒硬写,应该根据模型输出向量维度来定。
  • parseLLMResponse 强依赖模型返回格式:你换模型的时候,这块几乎肯定要跟着改。

这篇就当是我自己对 Harmony os + Data Augmentation Kit 知识问答完整示例 的一次"拆解+复盘"。

后面我准备在这个基础上再做两件事:

  1. 把邮件知识库换成我自己的 学习笔记 / 项目文档,做一个"本地知识小助手";
  2. 把 UI 做成多轮对话的聊天框,顺便接入不同大模型试试效果。

到时候再写一篇"实践篇"接着这篇往下更新,如果你也在玩鸿蒙 RAG,可以一起交流 😄

相关推荐
豫狮恒3 小时前
OpenHarmony Flutter 分布式软总线实战:跨设备通信的核心技术与应用
flutter·wpf·harmonyos
L、2183 小时前
Flutter 与 OpenHarmony 跨端融合新范式:基于 FFI 的高性能通信实战
flutter·华为·智能手机·electron·harmonyos
Wnq100723 小时前
鸿蒙 OS 与 CORBA+DDS+QOS+SOA 在工业控制领域的核心技术对比研究
物联网·性能优化·wpf·代理模式·信号处理·harmonyos·嵌入式实时数据库
解局易否结局3 小时前
鸿蒙UI开发中Flutter的现状与鸿蒙系统UI生态未来方向
flutter·ui·harmonyos
鸿蒙开发工程师—阿辉3 小时前
HarmonyOS5 极致动效实验室:基本动画的使用
harmonyos·arkts·鸿蒙
晚霞的不甘4 小时前
鸿蒙(HarmonyOS)UI 美化实战:打造美观、响应式的应用界面
ui·华为·harmonyos
晚霞的不甘4 小时前
鸿蒙(HarmonyOS)应用开发深度入门:ArkTS 语法、UI 构建与状态管理详解
ui·华为·harmonyos
花先锋队长4 小时前
升级鸿蒙6,华为Mate 70 Air智感握姿适配,接电话按键会“找手”了
华为·harmonyos
遇到困难睡大觉哈哈4 小时前
HarmonyOS收银台设计规范:构建简洁高效的支付体验
华为·harmonyos·设计规范