微信扫码
添加专属顾问
想用NodeJS搭建自己的知识库?这篇手把手教程带你从零开始实现,使用Qdrant向量数据库和阿里云百炼模型构建智能知识库系统。 核心内容: 1. 搭建知识库所需的前置工具与环境配置 2. Qdrant向量数据库的安装与关键参数解析 3. 基于Express+TypeScript的服务端代码编写实战
看了很多知识库的文章,但是一直不了解知识库是如何使用NodeJS搭建的,索性就自己使用NodeJS搭建一个知识库。
以下是整个项目的生命周期:
这里需要安装Qdrant数据库。Qdrant是一个开源的向量数据库,它可以存储和检索大量的向量,并提供高效的搜索和聚类功能。
Qdrant支持多种数据类型,包括字符串、数字、日期、布尔值等。它还支持多种索引类型,包括基于LSH的索引、基于KD树的索引、基于哈希的索引等。
Qdrant数据库中的distance参数介绍:
docker pull qdrant/qdrant
安装完成之后启动数据库
docker run -d -p 6333:6333 -p 8080:8080 qdrant/qdrant
这里就正式开始写代码了,首先开始写服务端的代码。新建一个文件夹server,然后初始化
npm init -y
npm install axios cors dotenv express openai @qdrant/qdrant-js
npm install @types/cors @types/express @types/node rimraf ts-node typescript -D
这里采用 express + typescript 编写服务器代码。根目录下面添加tsconfig.json文件并写入以下内容
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src",
"moduleResolution": "node",
"resolveJsonModule": true,
"typeRoots": ["./types"],
"baseUrl": "."
},
"include": ["src/**/*", "types/**/*", "types/**.d.ts"],
"exclude": ["node_modules", "dist"]
}
根目录下面添加src文件夹、types文件夹、.env配置文件 然后在package.json中添加
{
"scripts": {
"clean": "rimraf dist",
"build": "npm run clean && tsc",
"start": "node dist/app.js"
}
}
这里使用dotenv管理配置,在.env配置文件中添加如下配置
# 服务器配置
PORT=3000
# 阿里云百炼
DASHSCOPE_API_KEY=这里替换成你自己的API-KEY
DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
DASHSCOPE_API_URL=https://dashscope.aliyuncs.com
DASHSCOPE_API_RERANK_PATH=/api/v1/services/rerank/text-rerank/text-rerank
# Qdrant 向量数据库
QDRANT_API_URL=http://localhost:6333
然后在src文件夹中新建app.ts文件
import express from'express';
import dotenv from'dotenv'
import cors from'cors'
dotenv.config();
const app = express();
app.use(express.json());
app.use(cors()); // 允许跨域
app.post('/search', async (req, res) => {
res.send("Hello World!")
})
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log('服务器启动成功,监听端口: 3000...');
});
运行 npm run build然后在运行npm run start,然后打开网页。在打开的网页中输入 http://localhost:3000,如果看到 'Hello World!'则代表服务器搭建成功.
在/src/utils/db.ts中编写数据库初始化代码。dotenv是用来找到.env里面的配置项。
import { QdrantClient } from'@qdrant/qdrant-js'
import dotenv from'dotenv';
dotenv.config();
// 向量数据库配置
const qdrant = new QdrantClient({ url: process.env.QDRANT_API_URL });
let init = false;
// 初始化知识库集合
asyncfunction initQdrant () {
if (init) return qdrant;
// 知识库集合名称
const collectionName = "knowledge-base";
try {
// 检查集合是否存在
const { exists } = await qdrant.collectionExists(collectionName);
if (!exists) {
// 创建知识库集合
await qdrant.createCollection(collectionName,{
vectors: {
size: 1024, // 这里使用的是 multimodal-embedding-v1 模型,所以向量维度为 1024,向量维度需和模型对齐
distance: 'Cosine'// 相似度算法(可选 Cosine/Euclidean/Dot/Manhattan)
}
});
console.log("知识库集合创建成功.");
} else {
console.log("知识库集合已存在.");
}
} catch (error) {
console.error("初始化知识库集合失败:", error);
}
init = true;
return qdrant;
}
exportdefault initQdrant;
在/src/request/index.ts中编写代码,dotenv.config()只需要在入口文件中执行一次就行,所以这里不需要
import axios from "axios"
const instance = axios.create({
baseURL: process.env.DASHSCOPE_API_URL,
headers: {
'Authorization': `Bearer ${process.env.DASHSCOPE_API_KEY}`
}
});
export default instance;
阿里云百炼中的rerank模型只能通过HTTPS请求调用,所以需要写一个接口调用的函数。/scr/api/index.ts中编写代码。
import { AxiosResponse } from "axios"
import axios from "../request/index.js"
// 文本排序模型
export function rerank (data:RData):Promise<AxiosResponse<RerankOutput, any>> {
return axios.post(process.env.DASHSCOPE_API_RERANK_PATH, data)
}
由于各个模型之间需要的参数格式不一样,这边还需要两个对查询结果格式进行转换的函数。在/src/utils/common.ts中编写代码。
// qdrant查询的向量转换成文本格式
exportfunction vectorFormat(docments: QdrantResult[]):string[] {
const arr = docments.map(docment => {
if (docment.payload && docment.payload.text) {
return docment.payload.text as string
}
return""
})
return arr
}
// reranked查询结果进行转换
exportfunction rerankedFormat(docments: RerankedResult[]):string[] {
const arr = docments.map(docment => {
if (docment.document && docment.document.text) {
return docment.document.text as string
}
return""
})
return arr
}
这里进行步骤拆分,分别编写输入结果转成向量、数据库搜索、 查询结果排序 、文本生成这4个步骤的函数。在/src/services/index.ts中编写代码。
import initQdrant from"../utils/db.js"
import OpenAI from"openai"
import { rerank } from"../api/index.js"
const openai = new OpenAI({
apiKey: process.env.DASHSCOPE_API_KEY,
baseURL: process.env.DASHSCOPE_BASE_URL,
});
// 单一搜索
exportasyncfunction queryDocuments(query: string) {
const qdrant = await initQdrant();
const vector = await queryVector(query).catch(error => { console.error(error);return []; });
const results = await qdrant.search("knowledge-base", {
vector: vector,
limit: 10, // 最多返回10个结果
with_payload: true// 包含payload
});
return results;
}
// 混合搜索
exportasyncfunction hybridSearch (query: string): Promise<QdrantResult[]> {
const qdrant = await initQdrant();
// 获取多模态向量
const vector = await queryVector(query);
// 向量搜索
const vectorResults = await qdrant.search("knowledge-base", {
vector: vector,
limit: 10, // 最多返回10个结果
with_payload: true// 包含payload
});
// 关键字搜索
const keywordResults = await qdrant.search("knowledge-base", {
vector: vector,
filter: {
should: [
{ key: 'text', match: { text: query } }
]
},
limit: 10, // 最多返回10个结果
with_payload: true// 包含payload
});
// 合并搜索结果
const combineResults = Array.from(newSet([...vectorResults, ...keywordResults].map((item) =>JSON.stringify(item)))).map((item) =>JSON.parse(item));
return combineResults;
}
// 使用模型将查询文本转换为向量
exportasyncfunction queryVector(query: string) {
const response = await openai.embeddings.create({
model: "text-embedding-v3",
input: query
});
return response.data[0].embedding
}
// 重排序文档
exportasyncfunction rerankDocuments (query: string, documents: string[]) {
try {
const response = await rerank({
model: "gte-rerank-v2",
input: {
query: query,
documents: documents
},
parameters: {
return_documents: true, // 返回排序后的文档列表
top_n: 5// 最多返回5个结果
}
});
if (response.status === 200) {
return response.data.output.results;
} else {
returnPromise.reject(response.data);
}
} catch (error) {
console.log("rerank-api-error: ", error);
returnPromise.reject(error);
}
}
// 生成聊天消息
exportasyncfunction generateMessage(query: string, documents?: string[]) {
let prompt = "";
if (documents) {
prompt = `
请基于```内的内容回答问题。
```
${documents.join('\n')}
```
我的问题是:${query}。
`;
} else {
prompt = query;
}
const response = await openai.chat.completions.create({
model: "qwen-plus",
messages: [
{ role: "user", content: prompt }
],
stream: true // 创建流式数据
});
return response;
}
以上代码中数据库搜索分为单一搜索和混合搜索,混合搜索可以提高准确性。
现在回到入口函数中,重新编写代码,/src/app.ts中编写代码,返回给前端的数据采用流式数据的形式返回。
import express from'express';
import dotenv from'dotenv'
import cors from'cors'
import { hybridSearch, rerankDocuments, generateMessage } from"./services/index.js"
import { vectorFormat, rerankedFormat } from'./utils/common.js'
dotenv.config();
const app = express();
app.use(express.json());
app.use(cors()); // 允许跨域
app.post('/search', async (req, res) => {
const { query, isKnowledge } = req.body;
console.log(query, isKnowledge)
// 设置流式响应头
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
try {
// 是否为知识库查询
if (isKnowledge === 'true') {
// 混合查询
const docs = await hybridSearch(query);
// 重新排序
const rerankedDocuments = await rerankDocuments(query, vectorFormat(docs));
// 生成消息
const messages = await generateMessage(query, rerankedFormat(rerankedDocuments));
// 流式传输响应
forawait (const chunk of messages) {
const content = chunk.choices[0]?.delta?.content || '';
res.write(`data: ${JSON.stringify({ content })}\n\n`);
}
res.end();
} else {
// 生成消息
const messages = await generateMessage(query);
// 流式传输响应
forawait (const chunk of messages) {
const content = chunk.choices[0]?.delta?.content || '';
res.write(`data: ${JSON.stringify({ content })}\n\n`);
}
res.end();
}
}
catch (error:any) {
if (!res.headersSent) {
res.status(500).json({ error: 'API Error' });
}
}
});
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log('服务器启动成功,监听端口: 3000...');
});
由于数据库中没有数据,所以我们这里需要往数据库中添加一些测试数据进行测试。在/src/mack.ts中写入测试数据。
import initQdrant from"./utils/db.js";
import { queryVector } from"./services/index.js"
import dotenv from'dotenv'
dotenv.config();
// 生成测试数据
asyncfunction generateMockData() {
const qdrant = await initQdrant();
const mockData = [
{
id: 1,
vector: await queryVector(
"秦牧第一个在幽都出生的胎生生灵秦凤青被封印后身体产生的第二意识,后来与秦凤青分成两个不同的人物。幼年被司婆婆收养。本身是凡体,但被村长苏幕遮骗为霸体,开始了一段波澜壮阔的人生 [85]。秦牧走出大墟之后,成为天魔教教主 [126]。助江白圭平叛,开启了延康变法,后成为人皇,机缘巧合下遇到了生父,揭开了自己开皇帝族遗孤的身份 [250],得成神法门,开启了新的道法神通改革的大世 [262]。其兄秦凤青破封,大闹佛界、悬空界;和初祖人皇等人一起带领诸神逃出毁灭的太皇天,入天阴界复活天阴娘娘 [382];回百万年前,大闹天庭;于幽都见其母,与秦凤青合体在玉锁关大战幽都诸神;元界解封,见地母元君。昊天尊下界为斩杀地母元君,延康遭到连累,秦牧因此不得不帮助古神天帝、地母元君复活,一人对抗天庭的四位大帝时,舍弃自己的第三只眼,变作没有魂魄的人,后开天辟地赋神魂,获得新生。去往天庭引发瑶池事件,被派遣到太虚之中,意外成为造物主一族的圣婴,并因此促成造物主一族和无忧乡的和解。"
),
payload: {
text: "秦牧第一个在幽都出生的胎生生灵秦凤青被封印后身体产生的第二意识,后来与秦凤青分成两个不同的人物。幼年被司婆婆收养。本身是凡体,但被村长苏幕遮骗为霸体,开始了一段波澜壮阔的人生 [85]。秦牧走出大墟之后,成为天魔教教主 [126]。助江白圭平叛,开启了延康变法,后成为人皇,机缘巧合下遇到了生父,揭开了自己开皇帝族遗孤的身份 [250],得成神法门,开启了新的道法神通改革的大世 [262]。其兄秦凤青破封,大闹佛界、悬空界;和初祖人皇等人一起带领诸神逃出毁灭的太皇天,入天阴界复活天阴娘娘 [382];回百万年前,大闹天庭;于幽都见其母,与秦凤青合体在玉锁关大战幽都诸神;元界解封,见地母元君。昊天尊下界为斩杀地母元君,延康遭到连累,秦牧因此不得不帮助古神天帝、地母元君复活,一人对抗天庭的四位大帝时,舍弃自己的第三只眼,变作没有魂魄的人,后开天辟地赋神魂,获得新生。去往天庭引发瑶池事件,被派遣到太虚之中,意外成为造物主一族的圣婴,并因此促成造物主一族和无忧乡的和解。",
source: "test.txt"
},
},
{
id: 2,
vector: await queryVector(
"秦凤青第一个在幽都出生的胎生生灵,被尊为幽都神子。出生便有凌霄战力,拥有古神、半神、后天生灵的优点,不受幽都大道规则控制,相当于一个不受限制的土伯,因作恶多端被土伯封印,身体孕育出了第二个意识:弟弟秦牧。原本同秦牧共用一个身体一个元神,后来因为秦牧要保护秦凤青,将自身与秦凤青分离开来。秦凤青是一个大头娃娃,原本被封印在秦字大陆,实力恐怖,凶悍但只是孩童心性,对他来说,除了娘亲珍王妃之外,世间万物,所有生灵,只有能吃的和不能吃的分别。他能掌控魔性,但对神通道法不感兴趣。由于他算是土伯的弟弟,故此别人也可以对着他立下小土伯之约,违约便会被他吃掉。幽都之战继承土伯的生死簿和部分力量。天庭和延康大战,土伯和虚天尊在幽都中陨落,继承幽都大道,成为新的土伯。最终成道,成为延康的领袖之一。"
),
payload: {
text: "秦凤青第一个在幽都出生的胎生生灵,被尊为幽都神子。出生便有凌霄战力,拥有古神、半神、后天生灵的优点,不受幽都大道规则控制,相当于一个不受限制的土伯,因作恶多端被土伯封印,身体孕育出了第二个意识:弟弟秦牧。原本同秦牧共用一个身体一个元神,后来因为秦牧要保护秦凤青,将自身与秦凤青分离开来。秦凤青是一个大头娃娃,原本被封印在秦字大陆,实力恐怖,凶悍但只是孩童心性,对他来说,除了娘亲珍王妃之外,世间万物,所有生灵,只有能吃的和不能吃的分别。他能掌控魔性,但对神通道法不感兴趣。由于他算是土伯的弟弟,故此别人也可以对着他立下小土伯之约,违约便会被他吃掉。幽都之战继承土伯的生死簿和部分力量。天庭和延康大战,土伯和虚天尊在幽都中陨落,继承幽都大道,成为新的土伯。最终成道,成为延康的领袖之一。",
source: "test.txt"
}
},
{
id: 3,
vector: await queryVector("灵毓秀是秦牧妻子。身份最初是延康七公主。最初她女扮男装跟随秦飞月将军来到大墟,因身材丰满被秦牧戏称其为肥七公子。与秦牧一同开创了元神引,由于一起修炼了元神引,故是彼此最为亲近的人,多次暗示秦牧奈何秦牧却是感情白痴,曾被瞎子强行拉着秦牧结婚。延康劫后因延丰帝入狱,灵毓秀登基被称为延秀帝,幽都之战后退位与秦牧成婚。与秦牧育有一女,名为秦灵筠。"),
payload: {
text: "灵毓秀是秦牧妻子。身份最初是延康七公主。最初她女扮男装跟随秦飞月将军来到大墟,因身材丰满被秦牧戏称其为肥七公子。与秦牧一同开创了元神引,由于一起修炼了元神引,故是彼此最为亲近的人,多次暗示秦牧奈何秦牧却是感情白痴,曾被瞎子强行拉着秦牧结婚。延康劫后因延丰帝入狱,灵毓秀登基被称为延秀帝,幽都之战后退位与秦牧成婚。与秦牧育有一女,名为秦灵筠。",
source: "test.txt"
}
},
{
id: 4,
vector: await queryVector("苏幕遮,残老村村长。坐着担架,无手无脚,为了继续维系残老村众人的关系,欺骗众人秦牧为霸体 [85]。真实身份是人皇,剑法通神,剑法为剑图。残老村第一高手。苏幕遮是秦牧上个时代最强的人,一口剑照耀了一个时代,与上苍诸神争斗,打遍上苍无敌手,最后被上苍背后的真神击败,砍去了手脚。出村后,一剑开皇血汪洋,指引国师剑法入道 [258]。迎战上苍之时,斩杀一尊神之后,与乔星君同归于尽 [647],神魂被引入酆都 [292],因吹嘘霸体以及历代人皇被秦牧殴打,经常被前代人皇围攻群殴。复活后修炼了无漏斗战神功后手脚亦重新生长出来。曾由于秦牧穿越时空,在过去留下了霸体传说,导致连苏幕遮也被自己骗了过去 [645],但后来鬼船一事使得苏幕遮意识到了真相。苏幕遮见开皇之后便受困于心中神,剑斩开皇发丝一缕后,勘破心中神。最终成道。"),
payload: {
text: "苏幕遮,残老村村长。坐着担架,无手无脚,为了继续维系残老村众人的关系,欺骗众人秦牧为霸体 [85]。真实身份是人皇,剑法通神,剑法为剑图。残老村第一高手。苏幕遮是秦牧上个时代最强的人,一口剑照耀了一个时代,与上苍诸神争斗,打遍上苍无敌手,最后被上苍背后的真神击败,砍去了手脚。出村后,一剑开皇血汪洋,指引国师剑法入道 [258]。迎战上苍之时,斩杀一尊神之后,与乔星君同归于尽 [647],神魂被引入酆都 [292],因吹嘘霸体以及历代人皇被秦牧殴打,经常被前代人皇围攻群殴。复活后修炼了无漏斗战神功后手脚亦重新生长出来。曾由于秦牧穿越时空,在过去留下了霸体传说,导致连苏幕遮也被自己骗了过去 [645],但后来鬼船一事使得苏幕遮意识到了真相。苏幕遮见开皇之后便受困于心中神,剑斩开皇发丝一缕后,勘破心中神。最终成道。",
source: "test.txt"
}
},
{
id: 5,
vector: await queryVector("司婆婆曾为天圣教圣女。天圣教教主厉天行被司幼幽迷住,想与司幼幽结婚。司幼幽在结婚当日杀掉了厉天行,但厉天行的意识也进入了她的身体。司幼幽化身为司婆婆,带走了有《大育天魔经》,来到了残老村。此后天圣教一直在寻找司幼幽。司婆婆不愿回教当教主,便和天圣教的少年祖师商量,让秦牧做少教主 [125]。之后,厉天行传位给秦牧以后,最后的羁绊褪去,一心一意只想占领司婆婆的身体,但被瞎子和马爷镇压了,最终二人达成协议,司婆婆白天出现,厉天行晚上出现 [646]。厉天行领悟“大育”之道后,为了救司幼幽,离开她的身体,附身到星犴上展开天魔解体而亡 [284]。司幼幽同文元祖师、幽溟太子研究出四天门境界,并因此在后世被称为天尊,并最终成道。"),
payload: {
text: "司婆婆曾为天圣教圣女。天圣教教主厉天行被司幼幽迷住,想与司幼幽结婚。司幼幽在结婚当日杀掉了厉天行,但厉天行的意识也进入了她的身体。司幼幽化身为司婆婆,带走了有《大育天魔经》,来到了残老村。此后天圣教一直在寻找司幼幽。司婆婆不愿回教当教主,便和天圣教的少年祖师商量,让秦牧做少教主 [125]。之后,厉天行传位给秦牧以后,最后的羁绊褪去,一心一意只想占领司婆婆的身体,但被瞎子和马爷镇压了,最终二人达成协议,司婆婆白天出现,厉天行晚上出现 [646]。厉天行领悟“大育”之道后,为了救司幼幽,离开她的身体,附身到星犴上展开天魔解体而亡 [284]。司幼幽同文元祖师、幽溟太子研究出四天门境界,并因此在后世被称为天尊,并最终成道。",
source: "test.txt"
}
},
{
id: 6,
vector: await queryVector("马爷在都护府做过几十年的捕快,后来到大理寺任职。破了一场大案之后,名动天下,于是大雷音寺寻到了他 [202]。后来,他因故被大雷音寺追杀,但靠神通打出去。等他有了妻儿,大雷音寺又找上门来,为了妻儿的安危甘愿自断一臂,还了大雷音寺的神通。但大雷音寺依旧追杀马爷,让他妻离子散,家破人亡 [111]。马爷是青龙灵体。他的拳突破声音的局限,突破空气的束缚,爆发出无以伦比的力量。一只手也可以练拳,一只手也是一千只手,一手也可以发出雷音雷霆。修习雷音八式中的千手佛陀。后被秦牧续上断臂。老如来退位,他前去坐镇大雷音寺,一步一阶梯,到了山顶勘破心魔,修成二十诸天,悟破大梵天境,自然而然的成了如来。在魔猿战空成长了起来后,他脱去袈裟,变回了马神捕。"),
payload: {
text: "马爷在都护府做过几十年的捕快,后来到大理寺任职。破了一场大案之后,名动天下,于是大雷音寺寻到了他 [202]。后来,他因故被大雷音寺追杀,但靠神通打出去。等他有了妻儿,大雷音寺又找上门来,为了妻儿的安危甘愿自断一臂,还了大雷音寺的神通。但大雷音寺依旧追杀马爷,让他妻离子散,家破人亡 [111]。马爷是青龙灵体。他的拳突破声音的局限,突破空气的束缚,爆发出无以伦比的力量。一只手也可以练拳,一只手也是一千只手,一手也可以发出雷音雷霆。修习雷音八式中的千手佛陀。后被秦牧续上断臂。老如来退位,他前去坐镇大雷音寺,一步一阶梯,到了山顶勘破心魔,修成二十诸天,悟破大梵天境,自然而然的成了如来。在魔猿战空成长了起来后,他脱去袈裟,变回了马神捕。",
source: "test.txt"
}
},
];
// 将Mock数据插入Qdrant
await qdrant.upsert("knowledge-base", {
points: mockData,
});
console.log("测试数据插入成功!");
}
generateMockData();
以上代码写入了6条测试数据以提供给我们使用
首先在package.json文件中添加一行
{
...以前的代码不变
"scripts": {
...以前的代码不变
"mock": "node dist/mock.js"
},
...以前的代码不变
}
然后执行 npm run build之后再执行npm run mock,执行完成之后看到测试数据插入成功!则代表测试数据插入成功。可以打开网址http://localhost:6333/dashboard查看测试数据。
点击 knowledge-base
可以看到以上内容。
以上的服务端代码编写完成之后,我们还需要写一个对应的客户端代码才行,这里采用Vite + React + tailwindcss + AntD 快速搭建一个客户端项目。
使用Vite快速搭建一个React项目
npm create vite@latest
创建完成之后进入项目目录安装需要的依赖
npm install antd react-router tailwindcss
npm install @tailwindcss/vite @types/node -D
import { defineConfig } from'vite'
import react from'@vitejs/plugin-react-swc'
import tailwindcss from'@tailwindcss/vite'
import { resolve } from'path'
const pathResolve = (dir: string): string => {
return resolve(__dirname, '.', dir)
}
// https://vitejs.dev/config/
exportdefault defineConfig({
base: "/",
resolve: {
alias: {
'@': pathResolve('./src/'),
}
},
plugins: [
react(),
tailwindcss()
],
})
在 /src/request/index.ts初始化fetch调用方法
const BASE_URL = 'http://localhost:3000';
interface Headers {
[key: string]: string;
}
const formatParams = (params:Record<string, string>) => {
returnObject.keys(params).map(key => {
returnencodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
}).join('&');
}
const instance = {
get (url:string, params:Record<string, string>, headers:Headers) {
return fetch(`${BASE_URL}${url}?${formatParams(params)}`, {
method: 'GET',
headers: Object.assign({
'Content-Type': 'application/json'
}, headers),
})
},
post (url:string, data:Record<string, string>, headers:Headers) {
return fetch(`${BASE_URL}${url}`, {
method: 'POST',
headers: Object.assign({
'Content-Type': 'application/json'
}, headers),
body: JSON.stringify(data)
})
}
}
exportdefault instance;
在 /src/api/search.ts中编写接口调用函数,由于服务端使用了流式数据返回,所以客户端也需要使用流式数据接收。
import instance from "@/request/index"
export function search(data: Record<string, string>) {
return instance.post('/search', data, {
responseType: "stream"
})
}
在 /src/views/home/index.tsx中编写页面代码,页面是一个聊天窗口
import { useState, useRef, KeyboardEvent, useEffect } from'react';
import { Switch, Input } from'antd';
import { search } from'@/api/search'
exportfunction Home() {
const [messages, setMessages] = useState<Array<{text: string; isUser: boolean}>>([]);
const [inputValue, setInputValue] = useState('');
const [useKnowledgeBase, setUseKnowledgeBase] = useState(false);
const inputRef = useRef(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const handleSendMessage = async () => {
if (inputValue.trim()) {
const oldMessage = [...messages, { text: inputValue, isUser: true }];
setMessages([...oldMessage]);
setInputValue('');
const message = { text: "", isUser: false };
const response = await search({ query: inputValue, isKnowledge: useKnowledgeBase + '' });
// 流式读取数据
const reader = response.body?.pipeThrough(new TextDecoderStream()).getReader();
if (!reader) return;
// eslint-disable-next-line no-constant-condition
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 解析SSE数据块
value.split("\n\n").forEach(chunk => {
if (chunk.startsWith('data: ')) {
const data = JSON.parse(chunk.replace("data: ", ""));
message.text += data.content;
setMessages([...oldMessage, message]);
}
})
}
}
};
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
};
return (
<div className="w-4xl border-2 border-gray-200 rounded-md p-4 mx-auto mt-7 h-5/6 flex flex-col">
<div className="flex-1 overflow-auto mb-4 space-y-2 scrollbar-thin">
{messages.map((msg, index) => (
<div
key={index}
className={`p-3 flex ${msg.isUser ? 'justify-end' : 'justify-start'}`}
>
<div className={`p-3 rounded-lg max-w-xl ${msg.isUser ? 'bg-blue-500 text-white' : 'bg-gray-200 mr-auto'}`}>
{msg.text}
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
<div className="flex items-center gap-2">
<div className="flex items-center">
<span className="mr-2">知识库</span>
<Switch
checked={useKnowledgeBase}
onChange={setUseKnowledgeBase}
checkedChildren="开"
unCheckedChildren="关"
/>
</div>
<Input.TextArea
ref={inputRef}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="输入消息..."
autoSize={{ minRows: 1, maxRows: 4 }}
className="flex-1"
/>
<button
onClick={handleSendMessage}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
发送
</button>
</div>
</div>
);
}
客户端根目录下使用命令行启动项目
npm run dev
服务端根目录下使用命令行启动项目
npm run start
这是客户端窗口:
这是服务端窗口:
首先测试一下不使用知识库的情况:
返回的结果不是我们知识库里面的数据。
打开知识库开关重新测试:
返回的是知识库中的结果
以上代码还有知识库上传、上传文件切分等还没有实现,这里先记录一下等待后续实现
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业
2026-07-11
别再为 AI 调用超支头疼:Credits 配额,让每一笔消耗都透明可控
2026-07-11
阿里腾讯 AI 大战
2026-07-10
OpenAI“杀死了”Codex,一个超级应用诞生
2026-07-10
OpenAI 重磅推出超级 APP 及 GPT 5.6
2026-07-10
GPT-5.6 正式开放:三个型号一起放出完整成绩单,ultra 其实是 4 个智能体并行
2026-07-10
GPT-5.6深夜上线,首发实测,Claude Fable5 慌了!
2026-07-10
刚刚,GPT-5.6全面上线,Codex被合并,生产力工具ChatGPT Work来了
2026-07-09
Claude Design 迎来一次重大更新
2026-04-15
2026-04-24
2026-04-17
2026-04-14
2026-04-24
2026-05-19
2026-04-22
2026-04-24
2026-04-24
2026-04-16
欢迎您使用【53AI 官方网站】(以下简称“本网站”或“我们”)。本《会员服务协议》(以下简称“本协议”)是您(以下简称“会员”或“用户”)与【深圳市博思协创网络科技有限公司】之间关于注册、登录及使用本网站会员服务所订立的法律协议。
在您注册或登录前,请务必审慎阅读、充分理解各条款内容,特别是免除或限制责任的条款、知识产权条款、争议解决条款等。此类条款将以加粗形式提示您注意。 当您通过微信公众号授权、手机验证码验证或其他方式成功登录本网站时,即视为您已完全理解并同意接受本协议的全部内容。
一、 定义
本网站:指由【深圳市博思协创网络科技有限公司】运营的,域名为【53ai.com】的网站及相关移动端页面。
会员服务:指本网站向注册会员提供的知识库文章查阅、内容检索及其他相关增值服务。
知识库内容:指本网站发布的包括但不限于文字、图表、数据、研究报告、行业分析等数字化内容资源。
二、 账号注册与登录
登录方式:本网站支持以下登录方式,您可根据实际情况选择:
微信公众号授权登录:您同意将您的微信OpenID信息授权给本网站,用于创建或关联会员账号。
手机验证码登录:您需提供真实有效的手机号码,并通过短信验证码完成身份验证与登录/注册。
账号安全:您的账号仅限您本人使用,禁止赠与、借用、租用、转让或售卖。因您保管不善导致的账号被盗、密码泄露等损失,由您自行承担。
实名认证:根据相关法律法规要求,我们可能要求您在特定功能下完成实名认证。如您拒绝提供,可能无法使用部分或全部服务。
未成年人保护:若您未满18周岁,请在法定监护人的陪同下阅读本协议,并在征得监护人同意后使用本服务。
三、 服务内容与规范
知识库查阅权限:会员登录后,有权按照其会员等级对应的权限范围,在线浏览、检索本网站知识库中的相关文章及内容。
服务变更:我们有权根据业务发展需要,调整、变更或终止部分服务内容,并将以网站公告、公众号消息等方式提前通知。
禁止行为:您在使用服务时不得实施以下行为:
利用技术手段批量爬取、下载、转存知识库内容;
将知识库内容用于商业目的或未经授权地向第三方传播;
干扰本网站正常运行或侵犯其他用户合法权益;
发布违法违规信息或从事违反公序良俗的活动。
四、 知识产权声明
权利归属:本网站知识库中的排版设计、软件代码等内容的知识产权均归【公司全称】或原权利人所有,受《中华人民共和国著作权法》等法律保护。
有限许可:本网站授予会员一项非独占、不可转让、不可转授权的普通许可,仅限于个人学习、研究之目的在线查阅知识库内容。
侵权追责:未经书面许可,任何单位或个人不得以任何形式复制、转载、摘编、镜像、汇编或以其他方式使用上述内容。一经发现,我们保留追究其法律责任的权利。
五、 个人信息保护
我们重视对您个人信息的保护。关于我们如何收集、使用、存储和保护您的个人信息,请单独阅读 《隐私政策》。
您通过微信公众号授权或手机号验证所提供的信息,我们将严格按照《个人信息保护法》的规定处理,仅用于身份识别、服务提供及安全验证等必要用途。
您可以随时通过网站设置或联系客服行使查阅、更正、删除个人信息及撤回授权同意的权利。
六、 免责声明
内容准确性:知识库内容仅供参考,不构成专业建议。我们不对其完整性、准确性、时效性作任何明示或暗示的保证,您应自行判断并承担使用风险。
不可抗力:因自然灾害、政策法规变化、网络故障、第三方平台接口异常(如微信接口维护、运营商短信通道故障)等不可抗力导致的服务中断或延迟,我们不承担违约责任。
第三方链接:本网站可能包含指向第三方网站的链接,该等网站的内容和服务不受我们控制,请您自行甄别风险。
七、 违约责任
如您违反本协议约定,我们有权视情节采取警告、限制功能、暂停服务、注销账号等措施,并保留要求赔偿损失的权利。
如因您的违约行为导致我们遭受行政处罚、第三方索赔或商誉损失,您应承担全部赔偿责任(包括但不限于罚款、赔偿金、律师费、公证费等)。
八、 法律适用与争议解决
本协议的订立、执行和解释均适用中华人民共和国大陆地区法律。
因本协议产生的或与本协议有关的任何争议,双方应友好协商解决;协商不成的,任何一方均可向【公司所在地】有管辖权的人民法院提起诉讼。
九、 其他
本协议构成双方就本服务达成的完整协议,取代此前任何口头或书面约定。
本协议任一条款被认定为无效或不可执行的,不影响其他条款的效力。
我们对本协议享有最终解释权,并在法律允许的范围内保留随时修改的权利。修改后的协议一经公布即生效,继续使用服务即视为同意修订内容。