Harmony os —— Data Augmentation Kit 知识问答实战全流程(流式 RAG 问答踩坑记录)

Harmony os ------ Data Augmentation Kit 知识问答实战全流程(流式 RAG 问答踩坑记录)

这篇算是我啃完 HarmonyOS Data Augmentation Kit 知识问答 官方文档之后,给自己整理的一份"实战向笔记 + 踩坑总结"。

主要内容包括:约束说明、核心接口、流式问答实现流程(含 HttpUtilsMyChatLLMRagSessionstreamRun)等,方便以后我自己接着写 DEMO,也顺便给同学们一点参考。

鸿蒙开发者第四期活动


一、知识问答在 Data Augmentation Kit 里的位置

在 Data Augmentation Kit 里,"知识问答"其实就是把三件事串起来:

  1. 知识库:数据源先经过"知识加工",变成向量库 + 倒排库;
  2. 检索:通过多路召回 + 重排,从知识库里找相关 chunk;
  3. 大模型生成:把检索到的内容 + 用户问题喂给 LLM,生成最终答案(RAG)。

这条链路在代码层面大概对应三个角色:

  • ChatLLM:你自己实现的大模型客户端(比如对接 ModelArts、Qwen、Llama 等);
  • RagSession:一次知识问答会话的上下文;
  • streamRun:真正触发一轮"流式问答"的入口。

后面所有代码基本就是围绕这三块来展开的。


二、上来先看约束,不然很容易踩坑

先把官方给的几条 "红线" 记清楚,会少很多莫名其妙的错误。

1. 必须先做知识加工

没有知识加工 → 就没有知识库 → 知识问答直接起不来。

也就是说,数据源库要先经过 Data Augmentation Kit 提供的知识加工链路,生成对应的:

  • 向量数据库(*_vector.db
  • 倒排数据库(原始库)

知识问答本质上是在查这两张加工后的知识库表。

2. createRagSession / streamRun 不能多线程用

createRagSessionstreamRun 不支持多线程调用

实战建议:

  • 在一个会话里串行地去调 streamRun
  • 如果真有并发场景,要么用多个会话分开,要么在自己这边做调用队列,不要多个线程同时怼它。

3. 提问长度限制

  • 提问长度上限:1000 个字符
  • UTF-8 下:一个汉字 ≈ 3 字节

所以中文场景大概就是 300 多字的提问,再长就要自己做裁剪 / 摘要。

4. 历史记录只看"上一轮"

RAG 在问答时最多只看最近 1 次 QA 历史。

也就是说,你别指望它能帮你记好多轮上下文,目前就是"上一问一答"级别的上下文能力。如果需要更长上下文,可以在前端自己拼 prompt,把历史对话带进问题里再丢给 RAG。

5. 检索召回上限:600 个 chunk

  • RAG 在检索阶段最多只会召回 600 个 chunk
  • 这会直接影响知识覆盖度和性能,配置检索条件时要注意 deepSize 等参数,不要设置得巨大又无意义。

6. 敏感词风控要你自己做

RAG 本身不做敏感词风控。

责任在开发者这边:

  • 用户输入 做敏感词过滤;
  • 大模型回答 再做一遍敏感词检测;
  • 有风险的内容要在你这边拦下来(比如替换、打码、拒绝回答等)。

7. LLM 上下文长度至少 30k Tokens

这是一个很容易忽略、但非常关键的点:

开发者选择的 LLM,上下文长度要 ≥ 30k Tokens,否则知识问答有可能因为超出上下文长度直接失败。

官方举了几个满足要求的例子:

  • Qwen2.5-7B-32K
  • Mistral-7B-Instruct-v0.2
  • Llama-3.1-8B(对应大上下文版本)

原因很好理解:

RAG 要把:

  • 用户问题
  • 检索召回的多个 chunk
  • 系统提示、指令、格式约束等

全部拼在上下文里,如果模型上下文太短,很容易爆掉。

8. LLM 由你选择,支持什么语言也看它

  • Data Augmentation Kit 不强制你用哪个大模型;
  • 你选什么 LLM,就决定了问答最终支持的语言范围、风格、能力。

三、核心接口梳理:谁负责干啥?

知识问答相关的几个关键接口:

1. streamChat(query, callback)

复制代码
abstract streamChat(query: string, callback: Callback<LLMStreamAnswer>): Promise<LLMRequestInfo>
  • 所在类:你自定义的 ChatLLM 子类里必须实现;
  • 作用:和 LLM 交互 的统一入口,RAG 在两处会用到它:
    1. 检索前的问题预处理(比如改写问题、提取意图);
    2. 检索后的答案生成(把 chunk + 问题丢给模型)。

简单理解:
Data Augmentation Kit 不直接管你怎么调大模型,只需要你实现一个 streamChat 并约定好入参与回调数据格式。

2. createRagSession(context, config)

复制代码
createRagSession(context: common.Context, config: Config): Promise<RagSession>
  • 输入:
    • context:UIAbilityContext;
    • config:包含 LLM、检索配置、重排方式等的一大坨配置。
  • 输出:
    • RagSession:一次"知识问答会话"的句柄。

这个会话对象你一般会丢到 AppStorage 之类的全局存储里,后面页面里直接拿来用。

3. streamRun(question, config, callback)

复制代码
streamRun(question: string, config: RunConfig, callback: AsyncCallback<Stream>): Promise<number>
  • 这是 真正触发问答 的接口;
  • 支持 流式输出,会多次走 callback,把思考过程 / 最终答案 / 参考信息一点点推出来;
  • 你可以用 answerTypes 控制输出哪些类型的数据,比如:
    • THOUGHT:模型"思考过程"(如果开启);
    • ANSWER:用户可见的最终答案;
    • REFERENCE:参考的知识库条目。

⚠️ 官方要求:这些接口要在 页面或自定义组件的生命周期内 调用,别在已经销毁的 context 里瞎用。


四、开发前的准备工作

1. 申请网络权限(访问 LLM)

因为 streamChat 是由你自己实现的,需要去调用云上 LLM 接口,所以要申请网络权限:

复制代码
// src/main/module.json5
"requestPermissions": [
  {
    "name": "ohos.permission.INTERNET"
  }
],

没有这一步,HTTP 请求直接起不来。

2. 知识加工配置

这部分文档单独一章,这里只强调一点:

知识问答依赖"知识加工之后"的数据库表 ,不是直接用业务原始表。

对应会生成:

  • 倒排表(如 email_inverted
  • 向量表(如 email_vector

后面配置 RetrievalConfig / RetrievalCondition 时,会用到这些表名和字段。


五、实战步骤:从 LLM 封装到流式问答

步骤 0:导入 Data Augmentation Kit

复制代码
import { rag } from '@kit.DataAugmentationKit';

其他像 relationalStoreretrievalhttphilog 等按需导入即可。


步骤 1:封装 HttpUtils,与大模型做"流式对接"

这里我按官方示例做一个 HttpUtils 工具类,用 HTTP 流式返回 的方式对接 LLM(也可以用 WebSocket,看个人习惯)。

核心思路:

  1. 拼请求参数(包括模型名、温度、API key、是否开启流式等);

  2. requestInStream 发起请求;

  3. on('dataReceive') 监听流式数据回调;

    import { BusinessError } from '@kit.BasicServicesKit';
    import { http } from '@kit.NetworkKit';
    import { hilog } from '@kit.PerformanceAnalysisKit';

    const TAG = 'HttpUtils';

    class HttpUtils {
    httpRequest?: http.HttpRequest;
    url: string = 'https://api.modelarts-maas.com/v1/chat/completions'; // 示例:ModelArts 的接口
    isFinished: boolean = false;

    // 组装请求参数
    initOption(question: string): http.HttpRequestOptions {
    return {
    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
    }
    }
    };
    }

    // 发起流式请求
    async requestInStream(question: string) {
    if (!this.httpRequest) {
    this.httpRequest = http.createHttp();
    }
    this.httpRequest.requestInStream(this.url, this.initOption(question)).catch((err: BusinessError) => {
    hilog.error(0, TAG, 'Failed to request. Cause: %{public}s', JSON.stringify(err));
    });
    this.isFinished = false;
    }

    // 注册流式数据回调
    on(callback: (data: ArrayBuffer) => void) {
    if (!this.httpRequest) {
    this.httpRequest = http.createHttp();
    }
    this.httpRequest.on('dataReceive', callback);
    }

    cancel() {
    // 根据实际情况决定是否需要中断 HTTP(官方示例略过了具体实现)
    }
    }

    export default new HttpUtils();

说明:

  • messages 是标准 Chat 接口格式,一般是一串 {role, content}
  • 这里采用流式 stream: true,可以一边生成一边推给前端,体验更好。

步骤 2:实现自己的 ChatLLM ------ MyChatLLM

ChatLLM 是 RAG 和大模型之间的桥梁,我们要继承它并实现 streamChat / cancel 两个方法。

复制代码
import { rag } from '@kit.DataAugmentationKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import HttpUtils from './HttpUtils';

const TAG = 'MyChatLLM';

export default class MyChatLLM extends rag.ChatLLM {
  async streamChat(
    query: string,
    callback: (answer: rag.LLMStreamAnswer) => void
  ): Promise<rag.LLMRequestInfo> {
    let status: rag.LLMRequestStatus = rag.LLMRequestStatus.LLM_SUCCESS;

    try {
      // 1. 注册大模型数据回调
      const dataCallback = async (data: ArrayBuffer) => {
        hilog.debug(0, TAG, 'on callback enter. data length: %{public}d', data.byteLength);

        // 2. 解析模型流式返回(根据具体模型协议实现)
        const answer = parseLLMResponse(data); // 这里需要你自己实现
        if (!answer) {
          return;
        }

        // 3. 更新结束标记 + 回调 LLMStreamAnswer
        HttpUtils.isFinished = answer.isFinished;
        callback(answer);

        hilog.debug(
          0,
          TAG,
          'Request LLM success. isFinished: %{public}s, data: %{public}s',
          Number(answer.isFinished).toString(),
          answer.chunk
        );
      };

      HttpUtils.on(dataCallback);

      // 4. 发起流式请求
      await HttpUtils.requestInStream(query);
    } catch (err: any) {
      hilog.error(0, TAG, `Request LLM failed, error code: ${err.code}, error message: ${err.message}`);
      status = rag.LLMRequestStatus.LLM_REQUEST_ERROR;
    }

    // chatId 暂时用不到的话可以先写死
    return {
      chatId: 0,
      status
    };
  }

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

关键点:

  • parseLLMResponse(data) 要把 HTTP 流式返回的数据,变成 LLMStreamAnswer 结构;
  • answer.isFinished 用来标识这轮流式回答是否结束;
  • 你可以根据异常情况返回不同的 LLMRequestStatus

步骤 3:配置 RetrievalConfig(知识库数据库配置)

知识问答用的是 "加工后的数据库",所以我们需要告诉 RAG:

  • 向量库在哪(哪个 .db);
  • 倒排库在哪;
  • 用什么方式打开(安全等级、分词器等)。

示例(以邮件知识库为例):

复制代码
import { relationalStore } from '@kit.ArkData';
import { retrieval } from '@kit.DataAugmentationKit';
import { common } from '@kit.AbilityKit';

let storeConfigVector: relationalStore.StoreConfig = {
  name: 'testmail_store_vector.db',   // 向量库:原来库名基础上加 _vector
  securityLevel: relationalStore.SecurityLevel.S3,
  vector: true                        // 向量库必须设为 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
};

// 最终的 RetrievalConfig
let retrievalConfig: retrieval.RetrievalConfig = {
  channelConfigs: [channelConfigInvIdx, channelConfigVector]
};

步骤 4:配置 RetrievalCondition(多路召回 + 重排)

这一块就是告诉 RAG:

  • 倒排召回怎么查?

  • 向量召回怎么查?

  • 两路结果融合后怎么重排?

    import { retrieval } from '@kit.DataAugmentationKit';

    // 倒排召回条件
    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
    };

    // 重排策略(示例使用 RRF)
    let rerankMethod: retrieval.RerankMethod = {
    rerankType: retrieval.RerankType.RRF,
    isSoftmaxNormalized: true
    };

    // 最终 RetrievalCondition
    let retrievalCondition: retrieval.RetrievalCondition = {
    rerankMethod,
    recallConditions: [recallConditionInvIdx, recallConditionVector],
    resultCount: 5 // 最终要多少条结果
    };

几点注意:

  • fromClause 可以理解成构造一个虚拟视图,用于把原始业务表和知识库表连起来;
  • responseColumns 必须是 fromClause 里能查到的列,否则检索会直接失败;
  • deepSize 影响候选集合大小,不要随便设置成几千几万;
  • resultCount 是最终返回给 RAG 的知识条目数量。

步骤 5:组装 Rag 的 Config

现在有了:

  • 自己实现的 MyChatLLM
  • retrievalConfig
  • retrievalCondition

就可以把它们塞进 rag.Config 了:

复制代码
import { rag } from '@kit.DataAugmentationKit';

let config: rag.Config = {
  llm: new MyChatLLM(),
  retrievalConfig: retrievalConfig,
  retrievalCondition: retrievalCondition
};

步骤 6:创建 RagSession,并全局保存

建议在 UIAbility 里创建一次 RagSession,然后用 AppStorage 存起来。

复制代码
import { rag } from '@kit.DataAugmentationKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

rag.createRagSession(this.context, config)
  .then((session: rag.RagSession) => {
    AppStorage.setOrCreate<rag.RagSession>('RagSessionObject', session);
  })
  .catch((err: BusinessError) => {
    hilog.error(0, 'Rag', `createRagSession failed, code: ${err.code}, message: ${err.message}.`);
  });

步骤 7:在页面里用 streamRun 做一轮完整问答

核心点:

  • 指定 answerTypes(想要哪些流式数据);

  • 用字符串把流式数据自己拼接起来;

  • 做好异常处理。

    import { rag } from '@kit.DataAugmentationKit';
    import { BusinessError } from '@kit.BasicServicesKit';
    import { hilog } from '@kit.PerformanceAnalysisKit';

    // 1️⃣ 拿到 RagSession
    let session: rag.RagSession = AppStorage.get<rag.RagSession>('RagSessionObject') as rag.RagSession;

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

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

    // 3️⃣ 发起提问
    session.streamRun(this.inputStr, runConfig, (err: BusinessError, stream: rag.Stream) => {
    if (err) {
    this.answerStr = streamRun inner failed. code is ${err.code}, message is ${err.message};
    return;
    }

    复制代码
    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};
    });

之后你只需要把:

  • thoughtStr 绑定到一个调试区域(或者不展示);
  • answerStr 绑定到 UI 上的文本组件,就能看到完整的流式回答了。

六、从调用流程角度再串一遍

用文字替一下官方的"流式问答调用流程图":

  1. 用户在页面输入提问,触发 streamRun
  2. RagSession.streamRun
    • 先调用你实现的 ChatLLM.streamChat 做问题预处理(可选,看配置);
    • 再根据处理后的问题做多路检索(倒排 + 向量);
    • 根据 RetrievalCondition 做重排;
    • 构造一个"带知识的 prompt",再次调 ChatLLM.streamChat 请求 LLM 生成答案;
  3. 你的 HttpUtils 收到 LLM 流式返回的数据,一块块解析成 LLMStreamAnswer
  4. 通过 callbackTHOUGHT / ANSWER / REFERENCE 等流式数据回传给前端;
  5. 前端把这些片段拼接起来,展示给用户。

七、个人一点小建议 & 踩坑提醒

  • 不要忽视上下文长度限制
    选模型时一定看清楚:是不是 32K 上下文版本,否则知识问答很容易挂在"上下文长度溢出"上。
  • 敏感词风控要提早设计
    不管是业务合规还是上架审核,敏感词过滤一定要做在你自己的应用里。
  • createRagSessionstreamRun 不要滥用并发
    就当它是单线程接口吧,多轮问答就排队来,省心。
  • 善用 answerTypes 做"可视化 debug"
    开发阶段可以把 THOUGHT
  • 也展示出来,看模型是怎么一步步回答的,很有助于调效果;上线时可以按需关闭。
  • 知识加工 Schema 想清楚再定
    一旦知识加工 schema 定完,后面改起来成本很高,表结构、字段名最好提前设计好。

八、总结 & 后续打算

这篇 Harmony os 知识问答博客,主要是把官方文档里的内容按我的理解"展开 + 串联"了一遍,从约束条件、接口职责,到具体的 LLM 对接、检索配置、RagSession 创建、streamRun 流式问答,都走了一遍流程。

后面我打算基于这套东西,做一个小 DEMO:

  • 把自己的一些 HarmonyOS 学习笔记做知识加工;
  • 用 Data Augmentation Kit 搭一个"本地鸿蒙学习助手";
  • 看看在 PC / 2in1 设备上,端侧问答 + RAG 的实际体验。
相关推荐
kirk_wang3 小时前
Flutter app_settings 库在鸿蒙(OHOS)平台的适配实践与解析
flutter·移动开发·跨平台·arkts·鸿蒙
L、2183 小时前
Flutter + OpenHarmony 全栈实战:打造“鸿蒙智联”智能家居控制中心(系列终章)
flutter·华为·智能手机·electron·智能家居·harmonyos
马剑威(威哥爱编程)5 小时前
【鸿蒙开发案例篇】火力全开:鸿蒙6.0游戏开发战术手册
华为·harmonyos
kirk_wang5 小时前
Flutter tobias 库在鸿蒙端的支付宝支付适配实践
flutter·移动开发·跨平台·arkts·鸿蒙
L、2186 小时前
Flutter + OpenHarmony 分布式能力融合:实现跨设备 UI 共享与协同控制(终极篇)
javascript·分布式·flutter·ui·智能手机·harmonyos
鸿蒙开发工程师—阿辉6 小时前
HarmonyOS 5 极致动效实验室: Canvas 高阶动画的实现
华为·harmonyos
鸿蒙开发工程师—阿辉6 小时前
HarmonyOS 5 极致动效实验室:给 UI 注入“物理动效”
ui·华为·harmonyos
晚霞的不甘6 小时前
Flutter + OpenHarmony 发布与运维指南:从上架 AppGallery 到线上监控的全生命周期管理
运维·flutter·harmonyos
yuegu7776 小时前
Electron for鸿蒙PC实战项目之麻将游戏
游戏·electron·harmonyos