概要
我们用2周时间,使用Rust+Python开发了这个名为MRAG的知识库系统。
体验地址:https://mrag.coderbox.cn/
支持私有化部署。
技术栈
- 知识库:使用向量数据库milvus,Python的API。目前我们主要维护了一个高考知识库,其中收集整理了近5年的高考相关内容,包含录取分数线、学校信息、专业信息、各专业在各个省份的录取线等,仍在完善中。我们通过对这些非结构化的数据归一化为统一的特征向量,来增强模型的知识能力。其他知识库正在补充中。
- 服务端:使用Rust(Rocket+Rbatis)。首次尝试使用Rust进行完整的服务端开发,也遇到了不小的阻力。我们使用默认的配置,在x86_64-unknown-linux-musl编译后,可执行文件仅有29M,运行内存稳定在70M左右,相比于Java小了很多。
- 前端:仍然使用Vue + Element UI。
- 模型:使用第三方模型,如Deepseek、Qwen等
遇到的问题
ORM框架的抉择
我们最初尝试了diesel,diesel性能确实很不错,但是鉴于不能很好的支持复杂和动态SQL,于是我们选择了Rbatis,一是因为它和MyBatis比较像,可以很快的切换过来,二是因为支持动态SQL,可以很方便的写复杂SQL,虽然性能比diesel差一点点(大概慢5~10ms左右),但是利于开发。
Rbatis事务问题
使用Rbatis时,目前需要手动提交事务,有时候会忘记提交,所以我们简单封装了一个事务函数,当业务逻辑执行成功后自动提交事务,否则回滚。
rust
pub async fn tx<'a, F, R, RV>(exec: F) -> AppResult<RV>
where
F: Fn(RBatisTxExecutor) -> R,
R: Future<Output = AppResult<RV>>,
{
let tx = match Pool::get()?.acquire_begin().await {
Ok(tx) => tx,
Err(e) => {
log::error!("事务异常: {}", e);
return Err(db_error!(e));
}
};
let result = exec(tx.clone()).await;
match result {
Ok(result) => {
match tx.commit().await {
Ok(_) => log::debug!("事务提交成功,事务ID:{}", tx.tx_id),
Err(e) => {
log::error!("事务提交失败,事务ID:{}, 原因: {}", tx.tx_id, e);
return Err(db_error!(e));
}
};
Ok(result)
}
Err(e) => {
log::debug!("事务闭包执行失败,即将回滚,错误原因: {}", e);
match tx.rollback().await {
Ok(_) => {
log::debug!("事务回滚成功,事务ID:{}", tx.tx_id);
Err(e)
}
Err(e) => {
log::error!("事务回滚失败,事务ID:{}, 原因: {}", tx.tx_id, e);
Err(db_error!(e))
}
}
}
}
}
Rocket的SSE连接及恢复
Web框架我们使用的是Rocket,Rocket对SSE的支持比较简单,不能向SpringBoot那样直接拿到连接对象,所以我们自己封装了一个SSE的连接池,并支持了连接恢复,使得页面刷新后仍然能接收流式输出。这个问题,在这里也有提到。
让人烦躁的错误处理
在Rust中,无法抛出一个异常,也无法捕获一个异常,需要我们手动处理每一个异常,因此不得不让人思考应该返回什么错误,如何处理这个错误。我们可以使用anyhow传递大部分错误,但是有些错误我们不希望传递到前端展示,比如数据库错误等,以防泄露一些信息,因为我们需要对错误进行分类。
我们对Error分为了以下几种类型:
rust
#[derive(Debug, thiserror::Error)]
pub enum AppError {
/// 初始化错误
#[error("init error: {0}")]
InitError(String),
/// 数据库错误
#[error("db error: {0}")]
DbError(rbatis::Error),
/// 系统错误
#[error("system error: {0}")]
SystemError(anyhow::Error),
/// 业务错误。改错误不会传递到前端,仅内部科可见。所有的业务逻辑处理均使用该错误。
#[error("business error: {0}")]
BusinessError(anyhow::Error),
/// 提示类错误。该错误会传递到前端,前端显示给用户,且不会输出到日志。
#[error("{0}")]
MessageError(String),
/// 提示类错误。该错误会传递到前端,并指定一个错误编码,前端显示给用户,且不会输出到日志。
#[error("{1}")]
MessageCodeError(i32, String),
}
其中只有MessageError错误需要传递到前端展示。
为此我们也提供了一些宏来简化错误处理,例如:
rust
#[macro_export]
macro_rules! message_error {
($e:expr) => {
AppError::MessageError($e.to_string())
};
($fmt:expr, $($arg:tt)*) => {
AppError::MessageError(format!($fmt, $($arg)*))
};
}
#[macro_export]
macro_rules! message_code_error {
($code:expr, $e:expr) => {
AppError::MessageCodeError($code, $e.to_string())
};
($fmt:expr, $($arg:tt)*) => {
AppError::MessageCodeError($code, format!($fmt, $($arg)*))
};
}
#[macro_export]
macro_rules! business_error {
($e:expr) => {
AppError::BusinessError(anyhow!($e))
};
($fmt:expr, $($arg:tt)*) => {
AppError::BusinessError(anyhow!(format!($fmt, $($arg)*)))
};
}
#[macro_export]
macro_rules! db_error {
($e:expr) => {
AppError::DbError($e)
};
}
于是对于大部分错误,我们可以将原始的错误类型包装为AppError,例如:map_err(|e| db_error!(e))?
移动端UI适配
移动端做的比较少,在UI适配时花了不少时间,但效果仍然不理想。可能是因为移动端也使用的时ElementUI,而ElementUI对移动端的支持不是很好的缘故吧。
Rust服务端项目架构问题
前些年使用Java开发后端服务均使用微服务架构,而现在使用Rust开发时时常回想需不需要微服务架构,目前来看是不需要的,一是因为数据量不大,二是因为Rust的性能较好,且比较稳定,可能不需要拆分服务,三是因为Cargo的强大的包管理能力,也很利于分模块或分团队开发,能够在不拆分服务的情况下降低系统耦合度。所以目前来看不需要微服务的,如果向提高并行处理能力,多节点部署即可。
最后
我们后续将开发更简单易用的RAG产品,让所有人都能够建立自己的个性化知识库。