前言

RAG 这类能力,真正有价值的地方不在概念本身,在于它能不能在项目里跑起来。知识库怎么建,检索怎么配,大模型怎么接,流式输出怎么落到界面上,这几步只要有一处没收好,最后做出来的问答系统就会显得很飘。

鸿蒙 6 在把 Data Augmentation Kit 正式补了进来,能力范围包括知识加工、知识检索、RAG 问答和端侧问答模型,RAG 会话和流式问答从 6.0.0(20) 开始可用,主场景面向 PC/2in1 设备。

如果项目本身带有企业知识库、邮件助手、内部 FAQ、文档检索这类需求,这套能力是能直接接进业务里的。端侧模型这条路也给出来了,本地问答在隐私和时延上都有优势。要把这套能力接顺,最先要做的事只有一件,把知识加工、检索和问答三层职责分开,后面的代码结构才不会乱。

一、能力边界搭清楚

这套 Kit 真正落地时,核心链路分成三段。第一段是知识加工,把原始数据处理成可检索的知识库。第二段是检索,把问题对应的候选知识片段召回出来。第三段是问答,把问题和召回结果一起交给大模型生成答案。项目一开始就把这三层拆清楚,后面调试和优化会轻很多。

API 20 里,RAG 会话相关能力已经成型。createRagSession 用来创建会话,streamRun 用来发起流式问答,ChatLLM 负责和大模型交互。问答链路的一个关键点在于,大模型交互并没有被写死在系统里,应用可以自己继承 ChatLLM 去接云端模型,也可以走端侧模型这条路线。这样做的好处很直接,项目既可以先用云端模型把链路跑通,也可以在隐私和离线要求高的场景里切到本地

真正写代码时,先别急着上问答。知识库的准备工作必须先做好。知识加工如果没跑,后面的检索和问答都起不来。这一点在知识问答的约束里写得很清楚,先做知识加工生成知识库,问答链路才有输入。

二、向量库和倒排索引要一起建

RAG 检索效果要稳,单靠一种检索手段通常不够。向量检索适合语义相似度匹配,倒排索引适合关键词命中。项目里把这两条链路一起建出来,后面的召回质量会好很多。Data Augmentation Kit 这条线本身也是按这个思路来的,检索条件里既支持 responseColumnsdeepSize 这类重排参数,也支持基于向量库和倒排索引的组合检索。deepSize 默认值是 500,RAG 检索召回阶段最多返回 600 个 chunk。

知识库底层可以直接用 ArkData 的关系型数据库能力来承接。向量库这一层需要在 StoreConfig 里打开 vector: true,倒排索引这一层可以配 Tokenizer.CUSTOM_TOKENIZER。这套配置已经能把知识加工和后续检索链路串起来。

下面这段代码可以直接作为知识库初始化骨架:

import { relationalStore } from '@kit.ArkData';

// 向量库配置
const storeConfigVector: relationalStore.StoreConfig = {
  name: 'knowledge_vector.db',
  securityLevel: relationalStore.SecurityLevel.S3,
  vector: true
};

// 倒排索引库配置
const storeConfigInvIdx: relationalStore.StoreConfig = {
  name: 'knowledge.db',
  securityLevel: relationalStore.SecurityLevel.S3,
  tokenizer: relationalStore.Tokenizer.CUSTOM_TOKENIZER
};

检索配置这一步也要尽量收干净。项目里真正需要关注的是两件事,返回哪些列,重排阶段允许带多少候选结果进入下一步。下面这个检索条件写法就比较适合作为起步版本:

let recallConditionInvIdx: retrieval.InvertedIndexRecallCondition = {
  ftsTableName: 'knowledge_inverted',
  fromClause: 'SELECT knowledge_inverted.reference_id AS rowid, * FROM knowledge INNER JOIN knowledge_inverted ON knowledge.id = knowledge_inverted.reference_id',
  primaryKey: ['chunk_id'],
  responseColumns: ['reference_id', 'chunk_id', 'chunk_source', 'chunk_text', 'title'],
  deepSize: 500,
  similarityThreshold: 0.1
};

这里最重要的点有两个。第一,responseColumns 要和你的表结构对齐,列名写错了,后面问答阶段就拿不到内容。第二,deepSize 不要一上来就开得太大,项目早期先把链路跑顺,再根据召回质量和性能做调整。

三、RagSession、ChatLLM 和 streamRun 这条链路怎么接

会话创建是整个问答链路的中心。RagSession 负责把检索配置、检索条件和大模型客户端接到一起。项目里更稳的写法,是先把 RagSession 做成一个长期对象,页面和业务逻辑都围绕它展开,不要每次点击提问都重新建一份会话。

下面这段代码就是比较标准的会话创建方式:

import { rag } from '@kit.DataAugmentationKit';
import common from '@ohos.app.ability.common';

let context = getContext(this) as common.UIAbilityContext;

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

rag.createRagSession(context, config).then((session: rag.RagSession) => {
  AppStorage.setOrCreate('RagSessionObject', session);
});

ChatLLM 这层要自己实现,核心方法是 streamChat。项目如果走云端模型服务,这一层通常会接 HTTP。只要自己发网络请求,就别忘了在 module.json5 里申请 ohos.permission.INTERNET。端侧模型那条路线则更适合本地问答和隐私要求高的场景。

自定义 ChatLLM 的写法可以先按下面这个骨架搭起来:

import { rag } from '@kit.DataAugmentationKit';
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';

class MyChatLLM extends rag.ChatLLM {
  cancel(): void {
    // 可选:补取消逻辑
  }

  async streamChat(query: string, callback: Callback<rag.LLMStreamAnswer>): Promise<rag.LLMRequestInfo> {
    const requestBody = {
      model: 'qwen3-235b-a22b',
      messages: [{ role: 'user', content: query }],
      stream: true
    };

    const httpRequest = http.createHttp();

    try {
      const response = await httpRequest.request(
        'https://api.modelarts-maas.com/v1/chat/completions',
        {
          method: http.RequestMethod.POST,
          header: { 'Content-Type': 'application/json' },
          extraData: JSON.stringify(requestBody)
        }
      );

      const result = JSON.parse(response.result as string);
      callback(result);
      return { chatId: 0 };
    } catch (err) {
      const error = err as BusinessError;
      console.error(`LLM 请求失败: ${error.code}, ${error.message}`);
      throw error;
    }
  }
}

问答真正跑起来时,streamRun 负责把问题送进整个链路。这里有三个约束需要提前记住。第一,问题长度上限是 1000 字节。第二,streamRun 不支持多线程调用。第三,这条接口只能在 Stage 模型下使用。项目里只要有长文本输入、并发提问或者多线程调度,这几个限制一定要先在上层拦住。

流式输出这一步,最常用的就是 THOUGHTANSWERREFERENCE 三种输出类型。下面这段代码可以直接拿来接页面渲染:

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

const runConfig: rag.RunConfig = {
  answerTypes: [
    rag.StreamType.THOUGHT,
    rag.StreamType.ANSWER,
    rag.StreamType.REFERENCE
  ]
};

let thoughtStr = '';
let answerStr = '';

session.streamRun(userQuestion, runConfig, (err: BusinessError, stream: rag.Stream) => {
  if (err) {
    console.error(`streamRun failed: ${err.code}, ${err.message}`);
    return;
  }

  switch (stream.type) {
    case rag.StreamType.THOUGHT:
      thoughtStr += stream.answer.chunk;
      break;
    case rag.StreamType.ANSWER:
      answerStr += stream.answer.chunk;
      this.updateAnswerDisplay(answerStr);
      break;
    case rag.StreamType.REFERENCE:
      this.showReferences(stream.reference);
      break;
  }
});

这段代码的重点不在语法本身,在于流式输出和 UI 刷新是同步打通的。回答一边生成,一边上屏,用户感知会好很多。answerTypesTHOUGHTANSWERREFERENCE 这些输出类型都已经在接口和实践示例里给出来了。

四、提前处理项目里最容易踩坑的地方

第一类坑是把 RAG 当成一个开箱即用的安全模块。问答链路里,敏感词风控并不是系统内建能力,用户输入和模型输出的风控都要自己做。这一点一定要单独拉出来处理,不然后面会很被动。

第二类坑是提问太长。1000 字节的上限对普通问答够用,对长文档分析和复杂问题就不够了。项目里最好在输入框层就先做长度控制,再决定是否拆问题、压缩历史对话或改成多轮提问。

第三类坑是把会话对象乱用到多线程里。createRagSessionstreamRun 都不支持多线程调用,这种限制不能等到报错再处理,应该在业务层先把并发入口收住。最省事的办法,是在页面层加提问锁,同一时刻只允许一轮问答在跑。

第四类坑是把历史上下文管理完全交给系统。项目如果需要更长的上下文能力,自己在前端维护最近几轮对话会更稳。可以只保留最近两轮或三轮,把它们拼进新问题里,再送进 streamRun。这样做的好处很直接,历史长度自己可控,不会把问题一股脑塞进去导致超长。下面这段代码就是一个简单做法:

let historyContext = this.messages
  .slice(-4)
  .map(item => `${item.role}: ${item.content}`)
  .join('\n');

let enhancedQuestion = `历史对话:\n${historyContext}\n\n当前问题:${userQuestion}`;

这类增强提问方式很适合企业知识库、邮件助手、客服问答这类需要上下文连续性的场景。业务层自己控制历史范围,效果通常会比全量拼接稳得多。

总结

Data Augmentation Kit 这条能力线,已经把知识加工、检索和问答三段链路铺出来了。RAG 会话、流式输出、检索配置、端侧问答模型,这些拼图放在一起,足够做出一套真正能用的知识问答系统。

项目落地时,更值得先做的事情有四件。先把知识库和检索结构搭稳,再把 RagSessionChatLLM 接顺,接着把 streamRun 的流式输出打通到界面,最后把敏感词、长度限制和并发控制补齐。这样写出来的 RAG 应用,后面才更容易维护,也更容易持续优化。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐