本文基于一个真实的 Agent 项目,介绍如何通过统一的 IKnowledge 接口和 KnowledgeFactory 工厂,将百炼、Dify、RAGFlow 三种知识库适配到同一套 Agent 架构中。文章会结合项目中的数据库结构、配置对象、适配器实现和 Agent 装配代码,说明不同供应商的配置如何转换成统一的 Knowledge 对象,并分析这种设计的优点和当前实现边界。
一、为什么 Agent 需要统一接入多个 RAG 服务
一个 Agent 的核心流程通常是:
用户输入问题
|
v
Agent 判断是否需要知识检索
|
v
调用知识库进行检索
|
v
把检索结果交给大模型
|
v
生成最终回答
如果项目只接入一个知识库,可以直接写成:
BailianKnowledge knowledge =
BailianKnowledge.builder()
.config(config)
.build();
但当平台同时支持百炼、Dify 和 RAGFlow 时,问题就会变成:
当前 Agent 使用哪个知识库?
百炼需要哪些参数?
Dify 的检索模式怎么配置?
RAGFlow 的数据集如何传递?
不同供应商的重排序配置如何统一?
如果没有适配层,Agent 代码可能出现大量判断:
if (kbType == BAILIAN) {
// 构造百炼配置
} else if (kbType == DIFY) {
// 构造 Dify 配置
} else if (kbType == RAGFLOW) {
// 构造 RAGFlow 配置
}
这种代码有几个问题:
本项目采用的解决方案是:
Agent 主流程
|
v
KnowledgeFactory
|
v
IKnowledge 统一适配接口
|
+—— BailianIKnowledge
|
+—— DifyIKnowledge
|
+—— RagFlowIKnowledge
二、项目中的核心设计
项目中知识库适配的核心接口是:
agentOri/core/src/main/java/com/hxh/apboa/core/knowledge/IKnowledge.java
代码如下:
public interface IKnowledge
extends SmartInitializingSingleton {
Knowledge build(
KnowledgeBaseConfig knowledgeBaseConfig
);
Object build(
KnowledgeBaseConfig knowledgeBaseConfig,
boolean isOld
);
KbType type();
@Override
default void afterSingletonsInstantiated() {
KnowledgeFactory.register(this);
}
boolean lightHealthCheck(
KnowledgeBaseConfig knowledgeBaseConfig
);
}
这个接口定义了每个知识库适配器必须具备的能力:
| build(config) | 根据数据库配置创建 Agentscope Knowledge |
| build(config, isOld) | 兼容旧构建方式的接口 |
| type() | 返回当前适配器对应的知识库类型 |
| lightHealthCheck() | 轻量健康检查接口 |
| afterSingletonsInstantiated() | Spring 启动后自动注册适配器 |
三种实现分别是:
BailianIKnowledge
DifyIKnowledge
RagFlowIKnowledge
对应类型:
public enum KbType {
BAILIAN("百炼"),
DIFY("Dify"),
RAGFLOW("Ragflow");
}
三、数据库如何保存统一知识库配置
项目使用 knowledge_base_config 表保存知识库配置:
CREATE TABLE `knowledge_base_config` (
`id` bigint NOT NULL,
`name` varchar(100) NOT NULL COMMENT '知识库名称',
`kb_type` enum('BAILIAN','DIFY','RAGFLOW')
NOT NULL COMMENT '知识库类型',
`rag_mode` enum('GENERIC','AGENTIC')
NOT NULL DEFAULT 'GENERIC'
COMMENT '集成模式',
`description` varchar(500) DEFAULT NULL,
`connection_config` text NOT NULL COMMENT '连接配置',
`endpoint_config` text DEFAULT NULL COMMENT '端点配置',
`retrieval_config` text DEFAULT NULL COMMENT '检索配置',
`reranking_config` text DEFAULT NULL COMMENT '重排序配置',
`query_rewrite_config` text DEFAULT NULL COMMENT '查询重写配置',
`metadata_filters` text DEFAULT NULL COMMENT '元数据过滤',
`http_config` text DEFAULT NULL COMMENT 'HTTP配置',
`enabled` tinyint(1) NOT NULL DEFAULT 1,
`health_status` enum(
'HEALTHY',
'UNHEALTHY',
'UNKNOWN'
) DEFAULT 'UNKNOWN',
`last_sync_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_kb_name` (`name`),
KEY `idx_kb_type` (`kb_type`),
KEY `idx_kb_enabled` (`enabled`)
) COMMENT='知识库配置表';
实体类:
@Getter
@Setter
@TableName(
value = "knowledge_base_config",
autoResultMap = true
)
public class KnowledgeBaseConfig
extends BaseEntity {
private String name;
private KbType kbType;
private RAGMode ragMode;
private String description;
@TableField(
typeHandler = JsonNodeTypeHandler.class
)
private JsonNode connectionConfig;
@TableField(
typeHandler = JsonNodeTypeHandler.class
)
private JsonNode endpointConfig;
@TableField(
typeHandler = JsonNodeTypeHandler.class
)
private JsonNode retrievalConfig;
@TableField(
typeHandler = JsonNodeTypeHandler.class
)
private JsonNode rerankingConfig;
@TableField(
typeHandler = JsonNodeTypeHandler.class
)
private JsonNode queryRewriteConfig;
@TableField(
typeHandler = JsonNodeTypeHandler.class
)
private JsonNode metadataFilters;
@TableField(
typeHandler = JsonNodeTypeHandler.class
)
private JsonNode httpConfig;
private HealthStatus healthStatus;
private LocalDateTime lastSyncTime;
}
这里没有把百炼、Dify、RAGFlow 的所有字段都拆成固定列,而是使用多个 JSON 配置字段:
connection_config
endpoint_config
retrieval_config
reranking_config
query_rewrite_config
metadata_filters
http_config
这样做的好处是:
通用表结构
供应商差异放入 JSON
新增配置字段不需要频繁修改数据库表
但代价是:
字段类型检查需要由适配器完成
配置错误只能在运行时或保存时发现
不同供应商的 JSON 结构需要清晰约定
四、KnowledgeFactory 如何选择适配器
项目中的工厂类:
@Component
@RequiredArgsConstructor
public class KnowledgeFactory {
private static final Map<KbType, IKnowledge>
KNOWLEDGE_MAP =
new ConcurrentHashMap<>();
private final KnowledgeBaseConfigService
knowledgeBaseConfigService;
public KnowledgeWrapper getKnowledge(
AgentDefinition definition) {
KnowledgeBaseConfig config =
knowledgeBaseConfigService.getByAgentId(
definition.getId()
);
if (config == null) {
return null;
}
if (!config.getEnabled()) {
return null;
}
IKnowledge knowledge =
KNOWLEDGE_MAP.get(config.getKbType());
if (knowledge == null) {
return null;
}
return KnowledgeWrapper.builder()
.ragMode(config.getRagMode())
.knowledge(knowledge.build(config))
.build();
}
public static void register(
IKnowledge knowledge
) {
KNOWLEDGE_MAP.put(
knowledge.type(),
knowledge
);
}
}
工厂的执行逻辑是:
1. 根据 Agent ID 查询绑定的知识库配置
2. 判断配置是否存在
3. 判断知识库是否启用
4. 根据 kbType 查找适配器
5. 调用适配器 build()
6. 返回 KnowledgeWrapper
KnowledgeWrapper 用于同时保存知识库对象和 RAG 模式:
@Getter
@Setter
@Builder
public class KnowledgeWrapper {
private RAGMode ragMode;
private Knowledge knowledge;
}
适配器注册依赖 Spring 生命周期:
@Override
default void afterSingletonsInstantiated() {
KnowledgeFactory.register(this);
}
例如:
@Component
public class BailianIKnowledge
implements IKnowledge {
@Override
public KbType type() {
return KbType.BAILIAN;
}
}
Spring 初始化完成后,百炼适配器会自动注册到:
KNOWLEDGE_MAP
最终得到:
BAILIAN -> BailianIKnowledge
DIFY -> DifyIKnowledge
RAGFLOW -> RagFlowIKnowledge
这就是工厂和适配器结合的地方:
业务代码只认识 IKnowledge
具体供应商由 KbType 决定
五、Agent 如何真正使用知识库
适配器创建出的 Knowledge 最终会在 Agent 构建阶段使用。
项目中的 ReActAgentHelper:
public ReActAgent.Builder getReActAgentBuilder(
AgentDefinition definition) {
Model model =
chatModelFactory.getModel(definition);
ReActAgent.Builder builder =
ReActAgent.builder()
.name(definition.getAgentCode())
.description(
FuncUtils.isEmpty(
definition.getDescription()
)
? definition.getName()
: definition.getDescription()
)
.maxIters(
definition.getMaxIterations()
)
.model(model)
.sysPrompt(
agentSysPromptFactory
.getAgentSysPrompt(
definition
)
);
KnowledgeWrapper knowledgeWrapper =
knowledgeFactory.getKnowledge(definition);
if (knowledgeWrapper != null) {
builder.knowledge(
knowledgeWrapper.getKnowledge()
);
builder.ragMode(
knowledgeWrapper.getRagMode()
);
builder.retrieveConfig(
RetrieveConfig.builder()
.limit(5)
.scoreThreshold(0.5)
.build()
);
}
return builder;
}
Agent 主流程并不关心当前使用的是:
百炼
Dify
RAGFlow
它只接收:
Knowledge
RAGMode
RetrieveConfig
这就是适配层带来的隔离效果。
六、百炼适配器的实现
百炼适配器:
core/knowledge/impl/BailianIKnowledge.java
核心方法:
@Component
public class BailianIKnowledge
implements IKnowledge {
@Override
public Knowledge build(
KnowledgeBaseConfig config) {
try {
validateConfig(config);
BailianConfig bailianConfig =
buildBailianConfig(config);
return BailianKnowledge.builder()
.config(bailianConfig)
.build();
} catch (Exception e) {
log.error(
"Failed to build Bailian knowledge configuration",
e
);
throw new IllegalArgumentException(
"Invalid Bailian knowledge configuration",
e
);
}
}
@Override
public KbType type() {
return KbType.BAILIAN;
}
}
6.1 百炼连接配置
百炼必需字段:
accessKeyId
accessKeySecret
workspaceId
indexId
校验代码:
private void validateConfig(
KnowledgeBaseConfig config) {
if (config == null) {
throw new IllegalArgumentException(
"KnowledgeBaseConfig cannot be null"
);
}
if (config.getConnectionConfig() == null
|| config.getConnectionConfig().isEmpty()) {
throw new IllegalArgumentException(
"Connection config is required for Bailian knowledge"
);
}
JsonNode connectionConfig =
config.getConnectionConfig();
validateRequiredField(
connectionConfig,
"accessKeyId",
"accessKeyId"
);
validateRequiredField(
connectionConfig,
"accessKeySecret",
"accessKeySecret"
);
validateRequiredField(
connectionConfig,
"workspaceId",
"workspaceId"
);
validateRequiredField(
connectionConfig,
"indexId",
"indexId"
);
}
构造百炼配置:
private void applyConnectionConfig(
BailianConfig.Builder builder,
JsonNode connectionConfig) {
builder.accessKeyId(
JsonUtils.getStringValue(
connectionConfig,
"accessKeyId",
true
)
);
builder.accessKeySecret(
JsonUtils.getStringValue(
connectionConfig,
"accessKeySecret",
true
)
);
builder.workspaceId(
JsonUtils.getStringValue(
connectionConfig,
"workspaceId",
true
)
);
builder.indexId(
JsonUtils.getStringValue(
connectionConfig,
"indexId",
true
)
);
}
实际配置结构可以是:
{
"accessKeyId": "your-access-key-id",
"accessKeySecret": "your-access-key-secret",
"workspaceId": "your-workspace-id",
"indexId": "your-index-id",
"saveRetrieverHistory": false
}
文章中不要填写真实密钥。
6.2 百炼端点配置
默认端点:
private static final String
DEFAULT_ENDPOINT =
"bailian.cn-beijing.aliyuncs.com";
自定义端点:
{
"endpoint": "bailian.cn-beijing.aliyuncs.com"
}
代码:
private void applyEndpointConfig(
BailianConfig.Builder builder,
JsonNode endpointConfig) {
if (endpointConfig != null
&& !endpointConfig.isEmpty()) {
String endpoint =
JsonUtils.getStringValue(
endpointConfig,
"endpoint",
false
);
builder.endpoint(
Objects.requireNonNullElse(
endpoint,
DEFAULT_ENDPOINT
)
);
} else {
builder.endpoint(DEFAULT_ENDPOINT);
}
}
6.3 百炼检索配置
百炼支持稠密检索和稀疏检索 Top K:
{
"denseSimilarityTopK": 100,
"sparseSimilarityTopK": 100
}
代码会限制范围:
private int clamp(int value) {
return Math.max(
0,
Math.min(100, value)
);
}
构造配置:
private void applyRetrievalConfig(
BailianConfig.Builder builder,
JsonNode retrievalConfig) {
int denseTopK =
JsonUtils.getIntValue(
retrievalConfig,
"denseSimilarityTopK",
100
);
int sparseTopK =
JsonUtils.getIntValue(
retrievalConfig,
"sparseSimilarityTopK",
100
);
denseTopK = clamp(denseTopK);
sparseTopK = clamp(sparseTopK);
builder.denseSimilarityTopK(denseTopK)
.sparseSimilarityTopK(sparseTopK);
}
6.4 百炼重排序
百炼重排序配置:
{
"enableReranking": true,
"rerankConfig": {
"modelName": "gte-rerank-hybrid",
"rerankMinScore": 0.3,
"rerankTopN": 5
}
}
代码:
private RerankConfig buildRerankConfig(
JsonNode node) {
String modelName =
JsonUtils.getStringValue(
node,
"modelName",
"gte-rerank-hybrid"
);
float minScore =
JsonUtils.getFloatValue(
node,
"rerankMinScore",
0.3f
);
int topN =
JsonUtils.getIntValue(
node,
"rerankTopN",
5
);
minScore = clamp(minScore);
topN = Math.max(topN, 1);
return RerankConfig.builder()
.modelName(modelName)
.rerankMinScore(minScore)
.rerankTopN(topN)
.build();
}
6.5 百炼查询重写
查询重写配置:
{
"enableRewrite": true,
"rewriteConfig": {
"modelName": "conv-rewrite-qwen-1.8b"
}
}
代码会根据 enableRewrite 决定是否设置重写配置。
七、Dify 适配器的实现
Dify 适配器:
core/knowledge/impl/DifyIKnowledge.java
核心方法:
@Component
public class DifyIKnowledge
implements IKnowledge {
@Override
public Knowledge build(
KnowledgeBaseConfig config) {
try {
validateConfig(config);
DifyRAGConfig difyConfig =
buildDifyRAGConfig(config);
return DifyKnowledge.builder()
.config(difyConfig)
.build();
} catch (Exception e) {
log.error(
"Failed to build Dify knowledge configuration",
e
);
throw new IllegalArgumentException(
"Invalid Dify knowledge configuration",
e
);
}
}
@Override
public KbType type() {
return KbType.DIFY;
}
}
7.1 Dify 连接配置
Dify 必填字段:
apiKey
datasetId
配置示例:
{
"apiKey": "your-dify-api-key",
"datasetId": "your-dataset-id"
}
校验:
private void validateConfig(
KnowledgeBaseConfig config) {
if (config == null) {
throw new IllegalArgumentException(
"KnowledgeBaseConfig cannot be null"
);
}
JsonNode connectionConfig =
config.getConnectionConfig();
if (connectionConfig == null
|| connectionConfig.isEmpty()) {
throw new IllegalArgumentException(
"Connection config is required for Dify knowledge"
);
}
validateRequiredField(
connectionConfig,
"apiKey",
"apiKey"
);
validateRequiredField(
connectionConfig,
"datasetId",
"datasetId"
);
}
项目还会对 datasetId 做 UUID 格式检查,如果格式不符合预期会记录警告。
7.2 Dify API 地址
默认地址:
private static final String
DEFAULT_API_BASE_URL =
"https://api.dify.ai/v1";
自定义配置:
{
"apiBaseUrl": "https://your-dify-host/v1"
}
代码会检查 URL 是否以:
http://
https://
开头。
7.3 Dify 检索模式
Dify 的检索配置:
{
"retrievalMode": "HYBRID_SEARCH",
"topK": 10,
"scoreThreshold": 0.2,
"weights": 0.6
}
项目默认值:
retrievalMode = HYBRID_SEARCH
topK = 10
scoreThreshold = 0.0
weights = 0.6
代码:
private void applyRetrievalConfig(
DifyRAGConfig.Builder builder,
JsonNode retrievalConfig) {
RetrievalMode mode =
getRetrievalMode(retrievalConfig);
builder.retrievalMode(mode);
int topK =
JsonUtils.getIntValue(
retrievalConfig,
"topK",
10
);
topK = clamp(topK);
double scoreThreshold =
JsonUtils.getDoubleValue(
retrievalConfig,
"scoreThreshold",
0.0
);
scoreThreshold = clamp(scoreThreshold);
builder.topK(topK)
.scoreThreshold(scoreThreshold);
if (mode == RetrievalMode.HYBRID_SEARCH) {
double weights =
JsonUtils.getDoubleValue(
retrievalConfig,
"weights",
0.6
);
builder.weights(clamp(weights));
}
}
检索模式转换:
private RetrievalMode getRetrievalMode(
JsonNode retrievalConfig) {
if (retrievalConfig == null
|| !retrievalConfig.has("retrievalMode")) {
return RetrievalMode.HYBRID_SEARCH;
}
String mode =
retrievalConfig
.get("retrievalMode")
.asText();
try {
return RetrievalMode.valueOf(
mode.toUpperCase()
);
} catch (IllegalArgumentException e) {
log.warn(
"Invalid retrieval mode: {}, using default",
mode
);
return RetrievalMode.HYBRID_SEARCH;
}
}
7.4 Dify 重排序
Dify 重排序配置:
{
"enableRerank": true,
"rerankConfig": {
"providerName": "cohere",
"modelName": "rerank-english-v2.0",
"topN": 5
}
}
代码读取:
private RerankConfig buildRerankConfig(
JsonNode node) {
String providerName =
JsonUtils.getStringValue(
node,
"providerName",
"cohere"
);
String modelName =
JsonUtils.getStringValue(
node,
"modelName",
"rerank-english-v2.0"
);
return RerankConfig.builder()
.providerName(providerName)
.modelName(modelName)
.build();
}
当前项目读取了 topN,但对应的构建调用被保留为 TODO:
// builder.topN(topN); // TODO
因此文章不能写成“Dify 的 topN 已经完整生效”,只能准确描述为:
当前适配器已经解析 Dify 的 topN 参数,但实际 builder 设置仍是待完善项。
7.5 Dify 元数据过滤
Dify 支持元数据过滤:
{
"logicalOperator": "AND",
"conditions": [
{
"name": "source",
"comparisonOperator": "=",
"value": "thesis"
}
]
}
支持的比较运算符:
=
!=
>
>=
<
<=
in
not in
contains
not contains
代码会逐条校验:
private boolean isValidComparisonOperator(
String operator) {
String[] validOperators = {
"=", "!=", ">", ">=",
"<", "<=", "in",
"not in", "contains",
"not contains"
};
for (String valid : validOperators) {
if (valid.equalsIgnoreCase(operator)) {
return true;
}
}
return false;
}
这类配置的价值是:
同一个 Dify 数据集
只检索某个来源
只检索某个部门
只检索某个文档版本
7.6 Dify HTTP 配置
Dify 支持:
{
"connectTimeout": "30s",
"readTimeout": "60s",
"maxRetries": 3,
"customHeaders": {
"X-Tenant": "demo"
}
}
项目默认值:
连接超时:30 秒
读取超时:60 秒
最大重试:3 次
代码:
private void applyHttpConfig(
DifyRAGConfig.Builder builder,
JsonNode httpConfig) {
Duration connectTimeout =
JsonUtils.getDurationValue(
httpConfig,
"connectTimeout",
Duration.ofSeconds(30)
);
Duration readTimeout =
JsonUtils.getDurationValue(
httpConfig,
"readTimeout",
Duration.ofSeconds(60)
);
int maxRetries =
JsonUtils.getIntValue(
httpConfig,
"maxRetries",
3
);
builder.connectTimeout(connectTimeout)
.readTimeout(readTimeout)
.maxRetries(Math.max(0, maxRetries));
}
八、RAGFlow 适配器的实现
RAGFlow 适配器:
core/knowledge/impl/RagFlowIKnowledge.java
核心构建逻辑:
@Component
public class RagFlowIKnowledge
implements IKnowledge {
@Override
public Knowledge build(
KnowledgeBaseConfig config) {
try {
validateConfig(config);
RAGFlowConfig ragflowConfig =
buildRAGFlowConfig(config);
return RAGFlowKnowledge.builder()
.config(ragflowConfig)
.build();
} catch (Exception e) {
log.error(
"Failed to build RAGFlow knowledge configuration",
e
);
throw new IllegalArgumentException(
"Invalid RAGFlow knowledge configuration",
e
);
}
}
@Override
public KbType type() {
return KbType.RAGFLOW;
}
}
8.1 RAGFlow 连接配置
RAGFlow 配置示例:
{
"apiKey": "your-ragflow-api-key",
"baseUrl": "https://your-ragflow-host",
"datasetIds": [
"dataset-001",
"dataset-002"
],
"documentIds": [
"document-001"
]
}
必填字段:
apiKey
baseUrl
datasetIds
校验代码:
private void validateConfig(
KnowledgeBaseConfig config) {
JsonNode connectionConfig =
config.getConnectionConfig();
validateRequiredField(
connectionConfig,
"apiKey",
"apiKey"
);
validateRequiredField(
connectionConfig,
"baseUrl",
"baseUrl"
);
if (!connectionConfig.has("datasetIds")
|| connectionConfig.get("datasetIds")
.isNull()
|| connectionConfig.get("datasetIds")
.isEmpty()) {
throw new IllegalArgumentException(
"At least one datasetId is required"
);
}
}
项目同时支持数组和逗号分隔字符串:
{
"datasetIds": [
"dataset-001",
"dataset-002"
]
}
或者:
{
"datasetIds": "dataset-001,dataset-002"
}
统一转换方法:
private List<String> getStringListValue(
JsonNode node,
String fieldName) {
JsonNode valueNode =
node.get(fieldName);
List<String> result =
new ArrayList<>();
if (valueNode.isArray()) {
for (JsonNode item : valueNode) {
if (item.isTextual()
&& StringUtils.hasText(
item.asText())) {
result.add(
item.asText().trim()
);
}
}
} else if (valueNode.isTextual()) {
String[] items =
valueNode.asText().split(",");
for (String item : items) {
if (StringUtils.hasText(item)) {
result.add(item.trim());
}
}
}
return result.isEmpty()
? null
: result;
}
8.2 RAGFlow 检索配置
RAGFlow 配置示例:
{
"topK": 1024,
"similarityThreshold": 0.2,
"vectorSimilarityWeight": 0.3,
"page": 1,
"pageSize": 30,
"useKg": false,
"tocEnhance": false,
"rerankId": 1,
"keyword": false,
"highlight": false,
"crossLanguages": [
"zh",
"en"
]
}
基础检索配置:
private void applyRetrievalConfig(
RAGFlowConfig.Builder builder,
JsonNode retrievalConfig) {
int topK =
JsonUtils.getIntValue(
retrievalConfig,
"topK",
1024
);
double similarityThreshold =
JsonUtils.getDoubleValue(
retrievalConfig,
"similarityThreshold",
0.2
);
double vectorSimilarityWeight =
JsonUtils.getDoubleValue(
retrievalConfig,
"vectorSimilarityWeight",
0.3
);
int page =
JsonUtils.getIntValue(
retrievalConfig,
"page",
1
);
int pageSize =
JsonUtils.getIntValue(
retrievalConfig,
"pageSize",
30
);
builder.topK(Math.max(1, topK))
.similarityThreshold(
clamp(similarityThreshold)
)
.vectorSimilarityWeight(
clamp(vectorSimilarityWeight)
)
.page(Math.max(1, page))
.pageSize(Math.max(1, pageSize));
}
8.3 RAGFlow 高级检索
项目支持以下高级能力:
useKg:是否启用知识图谱多跳查询
tocEnhance:目录增强
rerankId:重排序模型 ID
keyword:关键词匹配
highlight:高亮结果
crossLanguages:跨语言检索
代码示例:
private void applyAdvancedRetrievalConfig(
RAGFlowConfig.Builder builder,
JsonNode retrievalConfig) {
boolean useKg =
JsonUtils.getBooleanValue(
retrievalConfig,
"useKg",
false
);
boolean tocEnhance =
JsonUtils.getBooleanValue(
retrievalConfig,
"tocEnhance",
false
);
boolean keyword =
JsonUtils.getBooleanValue(
retrievalConfig,
"keyword",
false
);
boolean highlight =
JsonUtils.getBooleanValue(
retrievalConfig,
"highlight",
false
);
builder.useKg(useKg)
.tocEnhance(tocEnhance)
.keyword(keyword)
.highlight(highlight);
}
跨语言参数会进行简单的语言编码校验:
private boolean isValidLanguageCode(
String languageCode) {
return languageCode != null
&& languageCode
.trim()
.toLowerCase()
.matches("^[a-z]{2,3}$");
}
8.4 RAGFlow HTTP 配置
{
"timeout": "30s",
"maxRetries": 3,
"customHeaders": {
"X-Tenant": "demo"
}
}
代码:
private void applyHttpConfig(
RAGFlowConfig.Builder builder,
JsonNode httpConfig) {
Duration timeout =
JsonUtils.getDurationValue(
httpConfig,
"timeout",
Duration.ofSeconds(30)
);
int maxRetries =
JsonUtils.getIntValue(
httpConfig,
"maxRetries",
3
);
builder.timeout(timeout)
.maxRetries(Math.max(0, maxRetries));
}
九、三个适配器的差异
虽然三种实现都返回 Knowledge,但配置语义完全不同。
| 认证 | AccessKey | API Key | API Key |
| 数据定位 | workspace + index | dataset | dataset 列表 |
| 检索参数 | 稠密/稀疏 Top K | 检索模式、Top K、阈值 | Top K、相似度、分页 |
| 重排序 | 模型、最低分、Top N | 提供商、模型、Top N | Rerank ID |
| 查询重写 | 支持 | 当前适配器未配置 | 未配置 |
| 元数据过滤 | 未配置 | 支持 | 未配置 |
| 知识图谱 | 未配置 | 未配置 | 支持 |
| HTTP 超时 | 由底层配置 | 连接超时和读取超时 | 请求超时 |
| 跨语言 | 未配置 | 未配置 | 支持 |
这正是适配器存在的意义:
外部配置不同
内部对象不同
最终统一成 Agent 可使用的 Knowledge
十、后台表单如何根据类型生成配置
项目的 KnowledgeForm.vue 会根据 kbType 动态显示配置项。
百炼显示:
连接配置
端点配置
检索配置
重排序配置
查询重写配置
Dify 显示:
连接配置
端点配置
检索配置
重排序配置
元数据过滤
HTTP 配置
RAGFlow 显示:
连接配置
检索配置
HTTP 配置
前端根据类型组装提交数据:
if (formData.kbType === 'BAILIAN') {
data.connectionConfig = {
…bailianConnection
}
data.endpointConfig =
bailianEndpoint.endpoint
? { endpoint: bailianEndpoint.endpoint }
: null
data.retrievalConfig =
bailianRetrieval.denseSimilarityTopK
|| bailianRetrieval.sparseSimilarityTopK
? { …bailianRetrieval }
: null
}
Dify:
if (formData.kbType === 'DIFY') {
data.connectionConfig = {
…difyConnection
}
data.endpointConfig =
difyEndpoint.apiBaseUrl
? {
apiBaseUrl:
difyEndpoint.apiBaseUrl
}
: null
data.retrievalConfig = {
…difyRetrieval
}
data.metadataFilters =
metadataFilters.conditions.length > 0
? { …metadataFilters }
: null
}
RAGFlow:
if (formData.kbType === 'RAGFLOW') {
data.connectionConfig = {
…ragflowConnection
}
data.retrievalConfig = {
…ragflowRetrieval
}
data.httpConfig = {
timeout: ragflowHttp.timeout,
maxRetries: ragflowHttp.maxRetries
}
}
这样后台页面和后端适配器使用的是同一套配置结构。
十一、配置校验为什么必须放在适配器中
不能只依赖前端校验。
因为知识库配置还可能来自:
后台管理接口
数据库迁移脚本
历史数据
接口重放
内部服务调用
所以每个适配器都必须自行校验。
例如百炼:
validateRequiredField(
connectionConfig,
"workspaceId",
"workspaceId"
);
Dify:
validateRequiredField(
connectionConfig,
"datasetId",
"datasetId"
);
RAGFlow:
if (!connectionConfig.has("datasetIds")) {
throw new IllegalArgumentException(
"At least one datasetId is required"
);
}
适配器还负责限制数值范围。
Dify:
topK = Math.max(
1,
Math.min(100, topK)
);
百炼:
denseTopK = Math.max(
0,
Math.min(100, denseTopK)
);
RAGFlow:
similarityThreshold =
Math.max(
0.0,
Math.min(1.0,
similarityThreshold)
);
这比把校验全部写在 Controller 中更合理,因为配置构建逻辑和配置校验逻辑属于同一个供应商适配边界。
十二、健康状态字段的真实含义
数据库表中有:
health_status
last_sync_time
接口中也定义了:
boolean lightHealthCheck(
KnowledgeBaseConfig config
);
但是当前三个适配器的实现都是:
@Override
public boolean lightHealthCheck(
KnowledgeBaseConfig config) {
return false;
}
因此当前项目已经预留了健康检查扩展点,但并没有实现真实的百炼、Dify、RAGFlow 连通性检测。
文章不能写成:
系统已经实现三种知识库健康检查
准确的说法是:
项目在配置模型和适配器接口中预留了健康检查能力,但当前三个适配器的 lightHealthCheck() 尚未接入真实探测逻辑。
如果后续实现,健康检查应该注意:
不能执行完整大模型检索
不能消耗大量额度
不能把 API Key 写入日志
应该设置连接和读取超时
应该区分认证失败、网络失败和数据集不存在
十三、为什么不在 Agent 中直接判断供应商
不推荐这样写:
if (config.getKbType() == KbType.BAILIAN) {
BailianConfig bailian = …
builder.knowledge(…);
}
if (config.getKbType() == KbType.DIFY) {
DifyRAGConfig dify = …
builder.knowledge(…);
}
问题在于 Agent 构建类会逐渐承担:
供应商判断
参数校验
JSON 解析
默认值处理
SDK 对象创建
异常处理
最终形成一个很难维护的上帝类。
当前项目的正确做法是:
KnowledgeWrapper wrapper =
knowledgeFactory.getKnowledge(definition);
if (wrapper != null) {
builder.knowledge(
wrapper.getKnowledge()
);
builder.ragMode(
wrapper.getRagMode()
);
}
Agent 只关心统一接口:
拿到 Knowledge
配置 RAGMode
继续构建 Agent
十四、如何新增第四种知识库
假设以后要接入一个新的知识库供应商 XKnowledge,不需要修改 Agent 主流程。
只需要新增适配器:
@Component
public class XKnowledgeAdapter
implements IKnowledge {
@Override
public Knowledge build(
KnowledgeBaseConfig config) {
validateConfig(config);
XConfig xConfig =
buildXConfig(config);
return XKnowledge.builder()
.config(xConfig)
.build();
}
@Override
public KbType type() {
return KbType.X_KNOWLEDGE;
}
@Override
public boolean lightHealthCheck(
KnowledgeBaseConfig config) {
return false;
}
}
然后扩展枚举:
public enum KbType {
BAILIAN,
DIFY,
RAGFLOW,
X_KNOWLEDGE
}
再在前端增加对应配置表单。
Agent 主流程仍然是:
KnowledgeWrapper wrapper =
knowledgeFactory.getKnowledge(definition);
不需要增加:
if (X_KNOWLEDGE) {
…
}
这就是适配器模式和工厂模式带来的扩展性。
十五、当前实现中的几个边界
15.1 KnowledgeFactory 使用静态注册表
private static final Map<KbType, IKnowledge>
KNOWLEDGE_MAP =
new ConcurrentHashMap<>();
优点是读取简单,适配器在启动时注册一次即可。
但要注意:
适配器类型必须唯一
重复注册会覆盖旧实现
测试之间需要清理静态 Map
项目提供了:
public static void clear() {
KNOWLEDGE_MAP.clear();
}
这对测试和重新初始化有帮助。
15.2 配置读取是动态的,但对象构建不是缓存的
每次 getKnowledge() 都会读取配置并调用:
iKnowledge.build(config)
这意味着配置修改后,下一次构建 Agent 可以获得新配置,但也意味着每次构建可能重新创建知识库对象。
如果未来知识库对象创建成本很高,可以增加:
配置版本号
配置哈希
Knowledge 对象缓存
缓存失效机制
不过缓存知识库对象时必须处理:
配置修改
凭证轮换
供应商连接失效
多实例配置同步
15.3 当前固定了 Agent 的检索参数
ReActAgentHelper 中固定配置:
RetrieveConfig.builder()
.limit(5)
.scoreThreshold(0.5)
.build()
这意味着知识库适配器内部的检索参数和 Agent 层的最终检索限制可能同时生效。
文章中应区分:
供应商配置:适配器如何构造 RAG 客户端
Agent 配置:Agent 最终允许返回多少检索结果
这两层参数如果没有统一规范,可能出现:
底层 topK 配置很大
Agent 层 limit 仍然只有 5
十六、三个真实配置示例
以下示例中的密钥均为占位符。
16.1 百炼配置
{
"name": "论文百炼知识库",
"kbType": "BAILIAN",
"ragMode": "AGENTIC",
"connectionConfig": {
"accessKeyId": "your-access-key-id",
"accessKeySecret": "your-access-key-secret",
"workspaceId": "your-workspace-id",
"indexId": "your-index-id",
"saveRetrieverHistory": false
},
"endpointConfig": {
"endpoint": "bailian.cn-beijing.aliyuncs.com"
},
"retrievalConfig": {
"denseSimilarityTopK": 100,
"sparseSimilarityTopK": 100
},
"rerankingConfig": {
"enableReranking": true,
"rerankConfig": {
"modelName": "gte-rerank-hybrid",
"rerankMinScore": 0.3,
"rerankTopN": 5
}
},
"queryRewriteConfig": {
"enableRewrite": true,
"rewriteConfig": {
"modelName": "conv-rewrite-qwen-1.8b"
}
}
}
16.2 Dify 配置
{
"name": "论文 Dify 知识库",
"kbType": "DIFY",
"ragMode": "GENERIC",
"connectionConfig": {
"apiKey": "your-dify-api-key",
"datasetId": "your-dataset-id"
},
"endpointConfig": {
"apiBaseUrl": "https://your-dify-host/v1"
},
"retrievalConfig": {
"retrievalMode": "HYBRID_SEARCH",
"topK": 10,
"scoreThreshold": 0.2,
"weights": 0.6
},
"rerankingConfig": {
"enableRerank": true,
"rerankConfig": {
"providerName": "cohere",
"modelName": "rerank-english-v2.0",
"topN": 5
}
},
"metadataFilters": {
"logicalOperator": "AND",
"conditions": [
{
"name": "source",
"comparisonOperator": "=",
"value": "thesis"
}
]
},
"httpConfig": {
"connectTimeout": "30s",
"readTimeout": "60s",
"maxRetries": 3
}
}
16.3 RAGFlow 配置
{
"name": "论文 RAGFlow 知识库",
"kbType": "RAGFLOW",
"ragMode": "GENERIC",
"connectionConfig": {
"apiKey": "your-ragflow-api-key",
"baseUrl": "https://your-ragflow-host",
"datasetIds": [
"dataset-001",
"dataset-002"
],
"documentIds": [
"document-001"
]
},
"retrievalConfig": {
"topK": 1024,
"similarityThreshold": 0.2,
"vectorSimilarityWeight": 0.3,
"page": 1,
"pageSize": 30,
"useKg": false,
"tocEnhance": false,
"rerankId": 1,
"keyword": false,
"highlight": false,
"crossLanguages": [
"zh",
"en"
]
},
"httpConfig": {
"timeout": "30s",
"maxRetries": 3
}
}
十七、如何验证适配器是否工作
1. 创建知识库配置
通过后台知识库配置页面,选择:
百炼
Dify
RAGFlow
填写对应的连接配置和检索配置。
2. 绑定 Agent
将知识库配置绑定到某个 Agent。
项目通过 Agent 和知识库关联表保存绑定关系:
agent_knowledge_bases
3. 构建 Agent
Agent 构建时执行:
KnowledgeWrapper wrapper =
knowledgeFactory.getKnowledge(
definition
);
4. 查看类型选择
可以在日志中输出:
log.info(
"Build knowledge, type={}, agentId={}",
config.getKbType(),
definition.getId()
);
5. 验证参数错误
例如 Dify 缺少 datasetId:
{
"apiKey": "demo"
}
预期抛出:
datasetId is required in connection config
RAGFlow 缺少数据集:
{
"apiKey": "demo",
"baseUrl": "https://ragflow.example.com",
"datasetIds": []
}
预期抛出:
At least one datasetId is required
6. 验证 Agent 检索
向 Agent 提问一个只有知识库中才有答案的问题:
请根据论文资料说明第三章的研究方法。
如果配置正确,Agent 会通过统一的 Knowledge 对象完成检索。
十八、总结
项目通过以下结构接入百炼、Dify 和 RAGFlow:
knowledge_base_config
|
v
KnowledgeBaseConfig
|
v
KnowledgeFactory
|
v
IKnowledge
|
+—— BailianIKnowledge
|
+—— DifyIKnowledge
|
+—— RagFlowIKnowledge
|
v
Agentscope Knowledge
|
v
ReActAgent
这套设计解决了三个核心问题:
1. 隔离供应商差异
百炼、Dify、RAGFlow 的配置字段和 SDK 调用方式完全不同,但 Agent 只接收统一的 Knowledge。
2. 配置由数据库驱动
知识库配置不写死在 Java 代码中,而是保存为:
连接配置
端点配置
检索配置
重排序配置
查询重写配置
元数据过滤
HTTP 配置
3. 新增供应商不需要修改 Agent 主流程
新增知识库时,只需要增加:
新的 KbType
新的 IKnowledge 实现
新的后台配置表单
不需要在 Agent 构建代码中继续增加供应商判断。
但当前实现也有明确边界:
三个适配器的 lightHealthCheck() 目前都返回 false
Dify 的 topN 参数当前仍有 TODO
Knowledge 对象没有做版本化缓存
Agent 层固定了 limit = 5 和 scoreThreshold = 0.5
JSON 配置错误主要在构建时发现
因此,这不是一个“已经实现所有企业级能力”的知识库平台,而是一套已经完成多供应商适配、配置隔离和 Agent 装配的基础架构。
它的核心价值可以概括为:
让 Agent 面向统一的知识库接口编程,让每个 RAG 供应商的复杂配置都被限制在自己的适配器内部。




