一、问题背景
基于FastAPI+豆包方舟搭建的私有RAG知识库,已完成基础问答、文件解析、JWT鉴权、AD域LDAP登录改造。系统会将每一轮问答持久化存入SQLite数据库,前端侧边栏正常渲染全部对话记录列表。 出现异常交互问题:点击任意历史对话条目,只会把提问文字填充到底部输入框,聊天窗口不会加载对应AI回复内容,无法实现历史对话一键回溯查看,只能重复发送问题重新生成回答,使用体验割裂。
二、故障现象汇总
后端
/history查询接口返回数据结构完整,每条记录同时携带question提问、answer回答两段数据,数据库存储无丢失、无截断;前端历史列表渲染正常,可展示所有历史提问标题;
点击历史条目仅回填问题至输入框,对话面板空白,缺失AI回复气泡;
原有HTML字符串拼接写法,用户输入特殊脚本字符时存在XSS注入风险。
三、分层排查定位
1、后端校验
直接浏览器访问历史记录接口地址,校验返回JSON结构体,问答数据完整闭环,排除后端存储、接口输出问题,故障收敛至前端JS逻辑。
2、查看历史点击执行函数
原始loadHistoryItem函数逻辑极度简陋,仅做输入框赋值操作,没有聊天容器DOM渲染逻辑:
function loadHistoryItem(question) {
userInput.value = question;
}
3、历史数据传递缺陷
拼接历史列表DOM时,只把纯文本question传入点击事件,未传递整条问答对象,前端无全局数据集留存完整问答数据,拿到问题也找不到匹配的回答内容。
4、安全隐患:原生字符串拼接DOM
历史条目直接拼接HTML字符串写入页面,用户恶意脚本代码未经转义就挂载在onclick事件内,内网多人使用场景存在安全漏洞。
四、整体修复方案
一次性完成功能修复 + 安全加固两项优化:
全局定义数组
historyData,接收后端接口全量历史数据并持久缓存,留存每条完整question+answer;点击事件改为传递数组索引值,根据索引读取缓存内整条对话数据,清空聊天面板后分别渲染用户消息气泡、AI回复气泡,完整还原历史会话;
废弃字符串拼接DOM写法,使用
document.createElement动态创建节点,搭配textContent赋值内容,新增HTML转义函数防御XSS攻击。
五、核心代码修改对照
修复前(残缺逻辑,仅回填问题)
function loadHistoryItem(question) {
userInput.value = question;
}
修复后(完整对话还原 + 安全渲染)
// 全局缓存全部历史问答数据
let historyData = [];
// 点击历史记录,按索引还原整条对话
function loadHistoryItem(index) {
const record = historyData[index];
// 清空当前聊天面板
chatBox.innerHTML = '';
// 分别渲染用户消息、AI回答消息气泡
chatBox.appendChild(createUserBubble(record.question));
chatBox.appendChild(createAiBubble(record.answer));
}
配套补充通用escapeHtml转义方法,所有前端展示文本统一转义处理,拦截恶意脚本执行。
六、功能回归验证
正常对话交互完成后,侧边历史列表自动刷新入库记录;
点击任意历史条目,聊天面板立刻渲染出当时一问一答完整内容,输入框不再冗余填充问题;
测试注入载荷
<script>alert(1)</script>提问入库,历史加载、页面展示均不会触发脚本执行,XSS风险消除;原有流式问答、文件上传、鉴权、AD登录等原有功能不受任何改动影响。
七、问题复盘总结
本次bug属于项目迭代里典型的数据存储完成,但数据回显链路设计缺失。开发时只完成了“存对话、列标题”,忽略了点击查看详情的渲染链路。 本次修复除补齐交互逻辑,顺带优化了前端DOM生成方式,补齐内网部署的安全短板。 落地私有知识库类项目,只要存在数据持久化+前端读取展示的场景,必须保证:写入的全量数据,读取阶段可以完整获取、原样渲染,不能只截取部分字段使用。
后续规划
本专栏持续收录这套Rocky Linux私有RAG项目落地过程中各类典型bug、兼容问题、配置踩坑内容,搭建一份完整可参考的排坑手册。你在部署调试过程中遇到的报错、异常问题,均可反馈汇总收录至本系列文章。
本文隶属《Rocky Linux从零搭建私有RAG知识库》连载专栏,同系列包含基础部署、JWT鉴权、AD域LDAP登录改造、故障排坑多篇实操教程。