Harmony os ------ Data Augmentation Kit 知识问答实战全流程(流式 RAG 问答踩坑记录)
这篇算是我啃完 HarmonyOS Data Augmentation Kit 知识问答 官方文档之后,给自己整理的一份"实战向笔记 + 踩坑总结"。
主要内容包括:约束说明、核心接口、流式问答实现流程(含
HttpUtils、MyChatLLM、RagSession、streamRun)等,方便以后我自己接着写 DEMO,也顺便给同学们一点参考。
一、知识问答在 Data Augmentation Kit 里的位置
在 Data Augmentation Kit 里,"知识问答"其实就是把三件事串起来:
- 知识库:数据源先经过"知识加工",变成向量库 + 倒排库;
- 检索:通过多路召回 + 重排,从知识库里找相关 chunk;
- 大模型生成:把检索到的内容 + 用户问题喂给 LLM,生成最终答案(RAG)。
这条链路在代码层面大概对应三个角色:
ChatLLM:你自己实现的大模型客户端(比如对接 ModelArts、Qwen、Llama 等);RagSession:一次知识问答会话的上下文;streamRun:真正触发一轮"流式问答"的入口。
后面所有代码基本就是围绕这三块来展开的。
二、上来先看约束,不然很容易踩坑
先把官方给的几条 "红线" 记清楚,会少很多莫名其妙的错误。
1. 必须先做知识加工
没有知识加工 → 就没有知识库 → 知识问答直接起不来。
也就是说,数据源库要先经过 Data Augmentation Kit 提供的知识加工链路,生成对应的:
- 向量数据库(
*_vector.db) - 倒排数据库(原始库)
知识问答本质上是在查这两张加工后的知识库表。
2. createRagSession / streamRun 不能多线程用
createRagSession和streamRun不支持多线程调用。
实战建议:
- 在一个会话里串行地去调
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-32KMistral-7B-Instruct-v0.2Llama-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 在两处会用到它:
- 检索前的问题预处理(比如改写问题、提取意图);
- 检索后的答案生成(把 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';
其他像 relationalStore、retrieval、http、hilog 等按需导入即可。
步骤 1:封装 HttpUtils,与大模型做"流式对接"
这里我按官方示例做一个 HttpUtils 工具类,用 HTTP 流式返回 的方式对接 LLM(也可以用 WebSocket,看个人习惯)。
核心思路:
-
拼请求参数(包括模型名、温度、API key、是否开启流式等);
-
用
requestInStream发起请求; -
用
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 retrievalConfigretrievalCondition
就可以把它们塞进 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 上的文本组件,就能看到完整的流式回答了。
六、从调用流程角度再串一遍
用文字替一下官方的"流式问答调用流程图":
- 用户在页面输入提问,触发
streamRun; RagSession.streamRun:- 先调用你实现的
ChatLLM.streamChat做问题预处理(可选,看配置); - 再根据处理后的问题做多路检索(倒排 + 向量);
- 根据
RetrievalCondition做重排; - 构造一个"带知识的 prompt",再次调
ChatLLM.streamChat请求 LLM 生成答案;
- 先调用你实现的
- 你的
HttpUtils收到 LLM 流式返回的数据,一块块解析成LLMStreamAnswer; - 通过
callback将THOUGHT/ANSWER/REFERENCE等流式数据回传给前端; - 前端把这些片段拼接起来,展示给用户。
七、个人一点小建议 & 踩坑提醒
- 不要忽视上下文长度限制
选模型时一定看清楚:是不是 32K 上下文版本,否则知识问答很容易挂在"上下文长度溢出"上。 - 敏感词风控要提早设计
不管是业务合规还是上架审核,敏感词过滤一定要做在你自己的应用里。 createRagSession和streamRun不要滥用并发
就当它是单线程接口吧,多轮问答就排队来,省心。- 善用
answerTypes做"可视化 debug"
开发阶段可以把THOUGHT - 也展示出来,看模型是怎么一步步回答的,很有助于调效果;上线时可以按需关闭。
- 知识加工 Schema 想清楚再定
一旦知识加工 schema 定完,后面改起来成本很高,表结构、字段名最好提前设计好。
八、总结 & 后续打算
这篇 Harmony os 知识问答博客,主要是把官方文档里的内容按我的理解"展开 + 串联"了一遍,从约束条件、接口职责,到具体的 LLM 对接、检索配置、RagSession 创建、streamRun 流式问答,都走了一遍流程。
后面我打算基于这套东西,做一个小 DEMO:
- 把自己的一些 HarmonyOS 学习笔记做知识加工;
- 用 Data Augmentation Kit 搭一个"本地鸿蒙学习助手";
- 看看在 PC / 2in1 设备上,端侧问答 + RAG 的实际体验。