Langchain-Chatchat问答系统上线前必须进行的8项测试
在企业对数据隐私和合规性要求日益严格的今天,将智能问答能力部署于本地、实现“数据不出内网”已不再是锦上添花的功能,而是基本门槛。Langchain-Chatchat 正是这一需求下的理想选择——它不依赖任何云端API,所有文档解析、向量计算、检索生成均在自有服务器完成,真正做到了知识闭环。
但这套系统再强大,若未经充分验证就仓促上线,轻则答非所问误导用户,重则因安全漏洞导致信息泄露。我们曾见过团队花两周时间搭建好环境、导入上千页制度文件,结果首次演示时却因分块策略不当,把“离职赔偿标准”拆成两段,导致模型给出错误回答。这种尴尬完全可以避免。
关键在于:上线前必须做对这8件事。它们不是可有可无的“检查清单”,而是决定系统能否可靠运行的技术锚点。下面我们就从实际工程视角出发,逐一拆解每一项测试背后的原理、常见陷阱以及验证方法。
一、文档解析是否“读懂了”你的文件?
很多人以为上传个PDF就算完成了知识入库,殊不知第一步就可能出错。特别是企业文档中常见的扫描件、复杂排版表格、带水印的合同模板,稍有不慎就会让后续流程全盘失准。
比如某公司上传了一份扫描版《员工手册》,系统显示“导入成功”,但提问“病假工资怎么算?”时始终找不到答案。排查后发现,原始图像分辨率只有72dpi,OCR识别时将“80%”误判为“BO%”,关键数字丢失。
所以测试不能只看“有没有报错”,而要主动验证输出质量:
- 抽查三类典型文档:
- 原生PDF(由Word导出)
- 扫描图片转PDF
含复杂表格的Word文档
重点检查:
- 中文标点是否被替换为乱码或空格?
- 表格内容是否变成错位文本流?
- 页眉页脚中的无关信息是否混入正文?
推荐做法是写一个简单的比对脚本,抽取文档中几个明确的关键句(如“试用期最长不超过六个月”),检查其是否完整出现在解析后的文本中。对于表格类内容,建议引入专用工具如Camelot或PaddleOCR的表格识别模块增强处理能力。
from langchain.document_loaders import PyPDFLoader loader = PyPDFLoader("scanned_handbook.pdf") pages = loader.load_and_split() # 检查第5页是否包含关键词 assert "年假" in pages[4].page_content, "警告:年假政策未正确提取"别小看这个步骤——如果输入是垃圾,后面再先进的模型也只能输出垃圾。
二、文本分块切得“聪明”吗?
你有没有遇到过这样的情况:问题明明在文档里写得清清楚楚,系统却答不出来?很大概率是文本被不合理地切开了。
Langchain 默认使用RecursiveCharacterTextSplitter,但如果参数设置不当,很容易把一句话从中腰斩。例如,“根据《劳动合同法》第三十九条规定,劳动者严重失职……”被切成前后两块,前一块没有结论,后一块缺少主语,检索时哪怕命中也难以构成有效上下文。
解决办法是结合业务语义调整分隔符优先级。中文文档尤其要注意以句号、分号、问号作为高优先级分割点,而不是简单按字符数硬切。
from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=600, chunk_overlap=80, separators=["\n\n", "\n", "。", ";", "!", "?", " ", ""] )更进一步的做法是加入“语义感知”的预处理规则。例如,在法律文书场景中,可以识别“第X条”、“附则”等结构化标题,在这些位置强制切分,确保每一块都对应一个完整的条款。
验证方式也很直接:找几个典型问题,查看其相关原文是否完整保留在同一个chunk中。你可以通过打印source_documents输出来人工核验。
三、嵌入模型真的理解中文语义吗?
很多人直接用英文模型(如all-MiniLM-L6-v2)处理中文,结果发现“加班费”和“调休”距离很远,而“加班费”和“overtime pay”反而更近——这不是模型的问题,是你用错了工具。
中文场景必须选用专为中文优化的嵌入模型,目前效果较好的是智源研究院发布的BGE系列(如bge-small-zh-v1.5)。它在大量中文语料上进行了对比学习训练,能更好捕捉同义表达之间的关联。
举个例子:
用户问:“辞职要提前几天通知?”
知识库中有句话:“员工解除劳动合同需提前三十日书面告知单位。”
尽管词汇完全不同,但 BGE 能将两者映射到相近的向量空间区域,从而实现精准匹配。
测试建议如下:
准备一组“同义不同词”的查询对,例如:
- “工伤认定流程” vs “怎么申请工伤病历”
- “绩效考核周期” vs “KPI多久评一次”计算查询向量与目标段落向量的余弦相似度,观察是否显著高于随机文本。
如果相似度过低,考虑微调嵌入模型或更换更强版本(如
bge-base-zh-v1.5)。
from sentence_transformers import SentenceTransformer import numpy as np model = SentenceTransformer("BAAI/bge-small-zh-v1.5") sentences = [ "辞职需要提前几天通知公司", "员工解除劳动合同应提前多少天告知", "如何请假一天不去上班" ] embeddings = model.encode(sentences) similarity = np.dot(embeddings[0], embeddings[1]) / ( np.linalg.norm(embeddings[0]) * np.linalg.norm(embeddings[1]) ) print(f"相似度: {similarity:.3f}") # 应 > 0.7记住:嵌入质量决定了系统的“智商上限”。选错模型,整个RAG流程都会事倍功半。
四、向量数据库查得快且准吗?
即使文本分得好、嵌入模型强,如果向量检索不准,依然会漏掉关键信息。
常见误区是只测“能否返回结果”,却不关心返回的是不是最相关的那几条。你应该模拟真实查询,检查 top-3 返回的内容是否确实包含了问题的答案。
以 Milvus 或 FAISS 为例,可以通过以下方式压测:
- 构造10~20个代表性问题,手动标注每个问题对应的正确文档块ID。
- 运行检索,统计召回率(Recall@k)。
- 观察是否有高相关段落排在第4、5名之后却被忽略。
此外,还要注意索引构建的一致性。曾有团队在更新知识库时只重新生成了新文档的向量,却没有合并进原数据库,导致查询时只能搜到旧数据或新数据之一。
解决方案是统一使用FAISS.from_documents()接口重建完整索引,并定期校验.index文件与元数据文件的一致性。
db = FAISS.from_documents(all_chunks, embeddings) db.save_local("vectorstore/full_index")如果你的应用对延迟敏感,还可以启用 GPU 加速版 FAISS(faiss-gpu)或将底层引擎换成 Chroma,后者在小规模数据下具备更快的插入与查询性能。
五、RAG流程端到端跑通了吗?
很多开发者只单独测试检索或生成模块,却忽略了整个链路的协同工作。结果上线后才发现:检索回来的是正确段落,但最终回答却是“我不知道”。
根本原因往往是Prompt 工程出了问题。当多个相关段落被拼接成上下文送入 LLM 时,如果格式混乱、噪声过多,模型很可能无法聚焦关键信息。
正确的做法是设计清晰的提示模板:
请根据以下信息回答问题,不要编造内容: 【参考内容开始】 {context} 【参考内容结束】 问题:{question} 回答:同时限制最大输入长度,避免超出模型上下文窗口。对于 ChatGLM3-6B 这类支持 32k 上下文的模型,可适当放宽;但对于 Qwen-7B(默认8k),建议控制总token数在6k以内。
验证手段很简单:开启return_source_documents=True,查看每次回答是否都能追溯到合理的出处。如果发现模型频繁忽略检索结果,则需优化 Prompt 结构或尝试不同的 chain_type(如"map_reduce"更适合长文档摘要)。
六、本地大模型生成的回答可信吗?
即使前面所有环节都没问题,最后一步也可能翻车。LLM 本身存在“幻觉”倾向,尤其当检索结果模糊时,容易自行补全信息。
比如有人问:“2024年年终奖什么时候发?” 系统检索到一段话:“奖金发放时间由人力资源部另行通知。” 但模型却回答:“预计2025年1月10日发放。”——这就是典型的无中生有。
应对策略有两个层面:
- 模型侧:优先选用事实遵循能力强的本地模型,如
ChatGLM3-6B在指令遵从方面表现优于早期版本。 - 逻辑侧:在生成层增加约束机制,例如:
- 当检索置信度低于阈值时,返回“暂未找到相关信息”而非强行作答。
- 引入后处理规则,禁止输出具体日期、金额等敏感数值,除非原文明确提及。
人工评测必不可少。准备一份包含10~20个问题的测试集,涵盖明确答案、模糊匹配、无匹配三种类型,邀请非技术人员逐条打分:回答是否准确、有无虚构、语气是否自然。
七、Web界面稳定可用吗?
Gradio 和 Streamlit 确实能让快速搭建UI变得极其简单,但也隐藏着一些易忽视的风险。
最常见的问题是长时间对话导致内存溢出。因为默认的gr.ChatInterface会累积保存全部历史消息,随着对话轮次增加,传给模型的上下文越来越长,最终触发 OOM 错误。
改进方案包括:
- 设置最大记忆轮数(如仅保留最近3轮)
- 启用流式输出减少等待感
- 添加超时中断机制,防止模型卡死
def chat(message, history): # 截断历史记录,防止上下文过长 truncated_history = history[-3:] if len(history) > 3 else history # 构造prompt并调用qa_chain response = qa_chain.run({ "query": message, "history": truncated_history }) return response demo = gr.ChatInterface(fn=chat, title="企业知识助手") demo.launch(server_name="0.0.0.0", server_port=7860, max_threads=4)另外,务必关闭share=True,否则 Gradio 会生成公网链接,造成数据暴露风险。
八、权限与安全措施到位了吗?
虽然系统部署在内网,但不代表绝对安全。现实中发生过攻击者通过上传恶意.py文件获取服务器执行权限的案例。
基本防护措施应包括:
- 文件类型白名单:仅允许
.pdf,.docx,.txt,.md等静态格式。 - 存储路径隔离:不同部门/用户的上传目录分开管理,避免越权访问。
- 防注入机制:对上传文件重命名,去除特殊字符(如
../)防止路径穿越。 - 操作日志审计:记录谁在何时上传了什么、查询了哪些问题。
更高级的场景可集成 OAuth2 单点登录,对接企业 AD 账户体系,实现细粒度权限控制。
这套测试框架并非一次性任务。每当新增文档类型、更换嵌入模型或升级LLM时,都应重新跑一遍核心验证流程。只有这样,才能确保 Langchain-Chatchat 不只是一个技术玩具,而是真正值得信赖的企业级知识中枢。
毕竟,用户不会关心你用了多么先进的AI架构,他们只在乎一个问题:“我问的,你能答对吗?”
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考