鸿蒙 HarmonyOS 6 | Data Augmentation Kit RAG知识问答开发实战
RAG 这类能力,真正有价值的地方不在概念本身,在于它能不能在项目里跑起来。知识库怎么建,检索怎么配,大模型怎么接,流式输出怎么落到界面上,这几步只要有一处没收好,最后做出来的问答系统就会显得很飘。
前言
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 这条线本身也是按这个思路来的,检索条件里既支持 responseColumns、deepSize 这类重排参数,也支持基于向量库和倒排索引的组合检索。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 模型下使用。项目里只要有长文本输入、并发提问或者多线程调度,这几个限制一定要先在上层拦住。
流式输出这一步,最常用的就是 THOUGHT、ANSWER、REFERENCE 三种输出类型。下面这段代码可以直接拿来接页面渲染:
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 刷新是同步打通的。回答一边生成,一边上屏,用户感知会好很多。answerTypes、THOUGHT、ANSWER、REFERENCE 这些输出类型都已经在接口和实践示例里给出来了。
四、提前处理项目里最容易踩坑的地方
第一类坑是把 RAG 当成一个开箱即用的安全模块。问答链路里,敏感词风控并不是系统内建能力,用户输入和模型输出的风控都要自己做。这一点一定要单独拉出来处理,不然后面会很被动。
第二类坑是提问太长。1000 字节的上限对普通问答够用,对长文档分析和复杂问题就不够了。项目里最好在输入框层就先做长度控制,再决定是否拆问题、压缩历史对话或改成多轮提问。
第三类坑是把会话对象乱用到多线程里。createRagSession 和 streamRun 都不支持多线程调用,这种限制不能等到报错再处理,应该在业务层先把并发入口收住。最省事的办法,是在页面层加提问锁,同一时刻只允许一轮问答在跑。
第四类坑是把历史上下文管理完全交给系统。项目如果需要更长的上下文能力,自己在前端维护最近几轮对话会更稳。可以只保留最近两轮或三轮,把它们拼进新问题里,再送进 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 会话、流式输出、检索配置、端侧问答模型,这些拼图放在一起,足够做出一套真正能用的知识问答系统。
项目落地时,更值得先做的事情有四件。先把知识库和检索结构搭稳,再把 RagSession 和 ChatLLM 接顺,接着把 streamRun 的流式输出打通到界面,最后把敏感词、长度限制和并发控制补齐。这样写出来的 RAG 应用,后面才更容易维护,也更容易持续优化。
更多推荐

所有评论(0)