背景

正在开发的作业批改系统需要支持 QQ 群聊答疑功能。学生通过 QQ 向机器人提问时,希望机器人能够基于上传的教学文档(知识库)进行回答,而不是仅依赖模型自身的知识。

架构设计

最终架构

1
学生QQ提问 → NapCat(WS) → OpenClaw OneBot插件 → RAG增强 → 贾维斯Agent → OpenClaw → 回复学生

关键点:

  • 在 OneBot 插件层拦截消息,调用后端 RAG 接口增强 prompt
  • 增强后的消息再发给 Agent 处理
  • 复用现有的向量检索和文档存储系统

实现过程

1. 后端接口开发

创建 OnebotRagController.java,提供 RAG 增强接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@PostMapping("/rag")
public ResponseEntity<RagResponse> rag(@RequestBody RagRequest request) {
// 1. RAG 检索
List<String> relevantContents = documentService.searchRelevantContent(
request.getMessage(), RAG_TOP_K);

// 2. 组装增强 prompt
String enhancedMessage;
if (relevantContents.isEmpty()) {
enhancedMessage = request.getMessage(); // 无相关知识,原样返回
} else {
String context = String.join("\n\n---\n\n", relevantContents);
enhancedMessage = String.format("""
基于以下参考文档内容回答问题。如果文档中没有相关信息,请基于你的知识回答。

参考文档内容:
%s

用户问题:%s
""", context, request.getMessage());
}

return ResponseEntity.ok(new RagResponse(enhancedMessage, !relevantContents.isEmpty()));
}

2. 插件层修改

修改 openclaw-onebot 插件,在消息转发前插入 RAG 调用:

config.js - 添加 RAG 配置读取:

1
2
3
4
5
6
7
export function getRagEnabled(cfg) {
return cfg?.channels?.onebot?.ragEnabled === true;
}

export function getRagEndpoint(cfg) {
return cfg?.channels?.onebot?.ragEndpoint ?? "http://localhost:8080/api/onebot/rag";
}

process-inbound.js - 添加 RAG 调用逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// RAG 增强
const shouldUseRag = () => {
if (!getRagEnabled(cfg)) return false;
if (isGroup) {
const targetGroups = getRagTargetGroups(cfg);
if (targetGroups.length === 0) return true;
return targetGroups.includes(Number(msg.group_id));
}
return getRagTargetPrivate(cfg);
};

if (shouldUseRag()) {
const enhanced = await callRagApi({
qq: String(msg.user_id),
message: messageText,
endpoint: getRagEndpoint(cfg)
});
if (enhanced) messageText = enhanced;
}

3. 配置

openclaw.json

1
2
3
4
5
6
7
8
9
10
{
"channels": {
"onebot": {
"ragEnabled": true,
"ragEndpoint": "http://localhost:8080/api/onebot/rag",
"ragTargetGroups": [], // 空数组表示所有群都启用
"ragTargetPrivate": true // 私聊也启用
}
}
}

遇到的问题与解决方案

问题 1:401 鉴权失败

现象: 插件调用后端接口返回 401

原因: 后端有 JWT 过滤器拦截,未放行 /api/onebot/rag

解决:JwtAuthenticationFilterSecurityConfig 中添加放行路径:

1
2
3
4
5
6
// JwtAuthenticationFilter
if (path.startsWith("/api/auth/") || path.startsWith("/api/chat/health")
|| path.startsWith("/api/onebot/rag")) {
filterChain.doFilter(request, response);
return;
}

问题 2:变量未定义错误

现象: Cannot access 'cfg' before initialization

原因: RAG 代码块放在 cfg 变量定义之前

解决: 调整代码顺序,确保 cfg 先定义再使用

问题 3:检索质量差,返回无关内容

现象: 问”今天天气如何”,返回的是 C语言字符串赋值相关内容

原因: 向量检索没有相似度阈值,返回了低相关度的结果

解决:DocumentServiceImpl 中添加余弦相似度阈值过滤:

1
2
3
4
5
6
7
8
9
private static final double SIMILARITY_THRESHOLD = 0.75;

return results.stream()
.map(chunk -> new ScoredChunk(chunk, cosineSimilarity(queryEmbedding, chunk.getEmbedding())))
.filter(sc -> sc.score >= SIMILARITY_THRESHOLD) // 阈值过滤
.sorted((a, b) -> Double.compare(b.score, a.score))
.limit(topK)
.map(sc -> sc.chunk.getContent())
.toList();

问题 4:响应慢

现象: RAG 调用耗时较长,影响用户体验

原因: 向量检索和 embedding 计算需要时间

待优化:

  • 添加 Redis 缓存热门查询
  • 减少 topK 数量
  • 异步处理 RAG 请求

最终效果

学生 QQ 提问时:

  1. 消息先经过 RAG 检索,查找知识库相关内容
  2. 如果找到相关内容,组装成增强 prompt 发给 AI
  3. AI 基于知识库内容回答问题
  4. 如果没有相关知识,AI 基于自身知识回答

总结

通过在后端和插件层分别开发 RAG 功能,实现了 QQ 机器人的知识库问答能力。关键在于:

  1. 解耦设计:插件负责消息拦截和转发,后端负责检索和存储
  2. 质量把控:添加相似度阈值,避免低质量检索结果干扰
  3. 灵活配置:支持按群、私聊分别配置是否启用 RAG

后续可以进一步优化检索性能和缓存策略。