简介:这是一套面向有私有化需求的企业技术团队的智能客服问答系统源码,基于企业私有知识库与大语言模型,解决通用LLM无法直接回答内部数据、数据安全与合规等核心问题。压缩包共1302个文件,大小约34.01MB,包含Vue、JS、TS前端代码,Go后端服务,SQL初始化脚本,JSON配置,以及Dockerfile等部署文件,覆盖管理后台、移动端与接入SDK,结构清晰,便于本地部署和二次开发。目前已有596人浏览/学习。系统基于ChatWiki框架,内置20多种主流模型的一键接入能力,支持自动分段、QA分段、手动输入和CSV导入等知识构建方式,并自动完成文本预处理与向量化;发布端可选用H5链接、嵌入网站或桌面客户端,能较快落地为企业专属问答应用。
1. 企业私有知识库+LLM智能客服,为什么是“反直觉”的AI落地
做企业智能客服最怕的不是用户问倒你,而是模型一本正经地编制度。你问它“报销上限是多少”,它给你一个流畅的数字,结果公司制度里根本没这条——这是大模型落地的一个典型翻车现场。标题里的“企业私有知识库”就是冲着这个问题去的:把员工手册、产品文档、历史工单预先切好、向量化、存进内网,再让LLM只从这些资料里找答案。整条链路从模型推理到知识库存储都在企业内网完成,数据不出域,这就是“支持私有化部署”的核心价值。适合正在做企业AI客服、内部办公助手或知识库平台的工程师,也适合给业务部门做选型评估的技术负责人。
2. 把大模型从“懂王”变成“话务员”:RAG架构与知识库服务的基本盘
2.1 为什么智能客服优先选“检索增强”而不是“微调”
智能客服这个场景有一个明显特征:知识是快速变化的,而回答必须忠于给定事实。今天产品手册改一版,明天报销制度调一次,要是靠微调给模型灌知识,就得每改一版训一次,成本高还容易把旧知识和新知识混在一起。所以业界的主流做法是RAG(Retrieval-Augmented Generation,检索增强生成),它的思路很简单:模型不负责记忆知识,只负责“照着资料讲话”。每次收到问题,先到向量库里检索最相关的几个文本片段,再把这些片段拼进提示词交给大模型生成回答。
RAG和微调不是二选一的关系,但客服场景里RAG通常赢在四个维度。数据更新上,RAG换一批文档重建索引就能生效,微调要重新训练;事实准确性上,RAG可以追溯答案来自哪一段原文,微调则是把知识揉进参数里,出了问题很难定位;部署成本上,RAG只需要一个推理服务和向量库,微调需要准备训练集、GPU资源和调参时间;隐私边界上,RAG可以把全部数据留在内网,微调的数据处理环节更多。下面这张表是我在方案评审时常用来跟业务方对齐的:
| 对比维度 | RAG 检索增强 | 微调 Fine-tuning |
|---|---|---|
| 知识更新 | 更新索引即可,分钟级生效 | 重新训练,小时到天级 |
| 事实准确性 | 答案可溯源到原文片段 | 依赖模型参数记忆,易幻觉 |
| 数据合规 | 文档、索引、推理均可内网闭环 | 训练数据需离开业务系统 |
| 硬件要求 | 一个GPU推理服务即可起步 | 训练需要更多显存和算力 |
| 适用场景 | 知识库问答、客服、制度查询 | 风格迁移、意图分类、格式控制 |
还要说清楚一个常被混淆的点:知识库不只是“文本段落”。企业里其实同时存在三类知识:结构知识库(FAQ条目、SQL表、Excel参数表)适合用精确匹配或数据库查询;语义知识库(产品手册、技术文档这种非结构化文本)适合用向量检索;图谱知识库(KG)适合做多跳关系查询,比如“这个配件的上游供应商是谁”。智能客服系统里最常见的形态是“结构知识库做兜底、RAG做扩展”:用户问“退货政策”先查FAQ表,命中就直接返回标准答案,没命中再去向量库捞文档。别指望一个方案吃掉所有知识形态,这个认知能帮你少踩很多坑。
2.2 一条问答流拆成五段:解析、切片、向量化、召回、生成
把RAG客服拆开看,一条完整问答流水线是五个环节:文档解析、切片、向量化、召回、生成。文档解析负责把PDF、Word、网页、公众号文章这类非结构化内容抽成纯文本或Markdown。很多团队在这里栽跟头,因为PDF解析出来的文字经常乱序、丢表格,直接拿去切片会带出一堆噪声。我一般会把文档先统一转成Markdown格式,表格尽量转成Markdown表格而不是图片,这样后续无论用正则还是语义切分都更可控。图片和扫描件必须走OCR,否则图里的信息在向量检索阶段相当于不存在——向量库里可以存图片关联的文本描述,但检索依旧是“文字进、文字出”。
切片是决定检索质量的一个关键步骤。切片太粗,一个chunk里混了好几个主题,检索回来一堆无关内容;切片太细,同一段操作说明被切成碎片,语义不完整。中文文档我建议按段落标题和句号、问号、分号这些语义边界来切,而不是无脑按字符数硬切。切完之后做向量化,用同一个embedding模型把每个chunk转成向量,存进向量数据库;用户提问时用同一个模型转成向量去搜,这是很多人忽略的铁律——任何一边换了模型,检索效果立刻崩。召回阶段用向量做近似最近邻搜索,取回top_k个候选片段。生成阶段才是大模型登场:把“系统指令 + 检索到的片段 + 用户问题”拼成一个提示词,让模型生成回答。
这一步里有个很关键的设计:模型不是“被问了一个问题然后凭记忆回答”,而是“被给了一叠资料然后做阅读理解”。提示词里必须明确告诉模型“只能根据给定资料回答,资料里没有就直接说不知道”。否则模型还是会忍不住动用自己预训练里的知识来补全,幻觉就从这里冒出来。很多团队把注意力全放在调模型上,却忽略了第五步的提示词约束,这是导致客服答案“流畅但不可信”的主要原因。
2.3 私有化部署的三个组件选型:模型推理、向量库、知识库流水线
“支持私有化部署”这句话写起来容易,落到架构上是四个组件全部内网化:LLM推理服务、embedding模型、向量数据库、知识库管理后台。LLM推理服务常见做法是选一个中文能力强的开源基座模型,用Ollama或vLLM这类推理框架跑在内网GPU服务器上,对外暴露一个兼容HTTP的调用接口。选模型时不要只盯着参数规模和榜单分数,要拿你自己知识库里的真实问题去试:同一个问题问十个模型,看哪个“照章办事”的能力最强。embedding模型我会优先选中文语料训练过的开源模型,这类模型的语义相似度判断更贴合中文企业文档的表达习惯。
向量数据库选型主要看数据规模和并发量。小团队几十万条文本以内,Chroma这类轻量级方案最省心,一个目录就能持久化,部署简单,排查也方便;规模上千万条、并发要求高,Milvus这类分布式向量库更合适,但要接受更高的运维成本;如果公司已经有Elasticsearch,也可以直接用ES的向量检索能力,让文本搜索和向量搜索共用一个集群。下面这张表是我给团队做技术选型时常用的参考:
| 组件 | 常见方案 | 适合场景 | 注意点 |
|---|---|---|---|
| LLM推理 | Ollama、vLLM、Xinference | 内网GPU服务器部署开源模型 | 显存决定并发,7B量化模型适合起步 |
| Embedding模型 | 开源中文向量模型 | 中文文档向量化 | 查询和文档必须用同一模型 |
| 向量库 | Chroma、Milvus、ES | 按规模和运维能力选 | 小规模用轻量库,大规模用分布式 |
| 知识库流水线 | Dify、RagFlow或自建Python服务 | 非技术运营维护 vs 研发可控性 | 低代码平台快,自建更灵活 |
知识库流水线是很多人忽略的一个组件。Dify、RagFlow这类开源低代码平台把文档解析、切片、索引、API发布做成了可视化界面,运营同学自己就能更新知识库,不需要每次都找研发。如果团队有开发能力,或者业务对回答格式有很强的定制要求,自建Python服务也完全可以,只要把文档上传、切片入库、索引重建这几个能力做成后台任务就行。我的建议是:团队小于五个人,直接上开源流水线平台;团队有维护能力,再考虑自建,否则知识库维护会成为整个系统里最脆弱的一环。
3. 从零跑通最小闭环:知识库问答链路搭建与接口封装
3.1 装依赖、起服务、拉一个适合中文的开源模型
先搭一个能跑通的最小环境。到这一步不需要想太复杂,目标只有一个:让一条问题从API进去,经过检索和生成,再带着答案和引用来源出来。我习惯先用Python虚拟环境把依赖隔离好,避免把系统环境搞乱。下面这套组合是本地私有化部署里最常见的一套:FastAPI做接口服务,Chroma做向量库,sentence-transformers做向量化,本机用Ollama起一个开源模型当LLM推理服务。
# 创建独立环境,避免污染系统Python python3 -m venv .venv source .venv/bin/activate # 基础依赖:接口服务、向量库、向量化模型 pip install fastapi uvicorn chromadb sentence-transformers # 用Ollama拉取一个中文能力稳定的开源基座模型 ollama pull qwen2.5:7b-instruct这里每个依赖都有明确分工:FastAPI负责把问答能力封装成HTTP接口,Chroma负责把文本向量持久化到本地目录,sentence-transformers负责把中文文本转成向量,Ollama负责在本地跑大模型推理、对外暴露HTTP调用。拉模型这一步需要一点耐心,模型文件不小,但它是“私有化部署”的关键一步——拉下来之后,所有推理都在本机发生,不再依赖任何外部API。拉起之后先在终端里确认模型服务可用,再写业务代码,不然排错时你会分不清是模型问题还是代码问题。
# 验证本机推理服务是否正常 curl http://localhost:11434/api/generate \ -H "Content-Type: application/json" \ -d '{"model":"qwen2.5:7b-instruct","prompt":"你好,请简短回复","stream":false}'这条curl能返回内容,说明LLM推理服务已经就绪。注意这个服务默认监听11434端口,后面我们的问答API会直接复用它。这里有个参数容易踩坑:stream要设为false,让接口一次性返回完整结果,方便调试;实际生产环境如果要做打字机效果,再改成true走流式。
3.2 文档切片与向量化:chunk_size、overlap 和“图片怎么处理”
接下来把知识库文档变成可检索的向量。核心操作是三步:读入文档、切分成chunk、向量化后写入向量库。我选Markdown格式作为中间格式,因为企业内部文档无论是PDF还是公众号网页,最终都能转成Markdown或纯文本,接下来处理逻辑就完全统一了。切分这一步用递归字符切分器,它比固定长度切分更聪明,会优先按段落、句号这些自然边界断句。
from langchain_text_splitters import RecursiveCharacterTextSplitter from sentence_transformers import SentenceTransformer # 1. 加载文档为纯文本(示例:运营提供的产品手册) with open("product_manual.md", "r", encoding="utf-8") as f: raw_text = f.read() # 2. 按语义边界切片,中文场景推荐 300~500 字符 splitter = RecursiveCharacterTextSplitter( chunk_size=400, chunk_overlap=80, separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""], ) chunks = splitter.split_text(raw_text) # 3. 用同一个embedding模型做向量化,保证查询和文档在同一向量空间 model = SentenceTransformer("BAAI/bge-large-zh-v1.5", device="cuda") vectors = model.encode(chunks, normalize_embeddings=True, batch_size=32)两个参数值得单独解释。chunk_size=400表示每个切片段落最多约400个字符,中文场景下这个值既能保留一个完整语义单元,又不至于让无关内容混进来;chunk_overlap=80表示相邻两个切片有80个字符的重叠,目的是避免句子刚好被切断在边界处。如果文档里大量是短句的操作步骤,chunk_size可以降到300;如果是政策制度这类长段落,可以调到600。向量化时normalize_embeddings=True会把向量归一化,这样后面算余弦相似度就等价于点积,分数更容易解释。这一步完成后,向量还只存在内存里,需要入库才能实现持久化和检索。
import chromadb # 持久化到本地目录,这就是最简的私有化知识库存储 client = chromadb.PersistentClient(path="./kb_store") collection = client.get_or_create_collection( name="product_manual", metadata={"hnsw:space": "cosine"} ) collection.add( ids=[f"chunk_{i}" for i in range(len(chunks))], embeddings=vectors.tolist(), documents=chunks, )这段代码把向量和原始文本一起存进了本地目录kb_store。为什么要把原始文本也存进去?因为检索返回的是向量对应的文本片段,后续要拼进提示词、要展示给用户做引用溯源,都离不开原文。metadata里指定hnsw:space为cosine,意味着建索引时用余弦相似度作为距离度量,这和我们对向量做了归一化是对应的。到这里,一个完全不出内网的知识库就算建立完成了。
3.3 封装问答API:召回 + prompt拼接 + 流式返回引用
知识库建好了,下一步是把问答逻辑封装成一个HTTP接口。这个接口内部做三件事:用用户问题去向量库召回最相关的chunk,把chunk拼成一段带约束的提示词,交给本地大模型生成答案。注意这里不能直接把整个知识库塞给模型,每次只取top 5左右的chunk,这是控制成本和准确率的关键。
from fastapi import FastAPI import requests app = FastAPI() def retrieve(query: str, top_k: int = 5): """召回:问题向量化后在向量库中找最相似的文本片段""" qv = model.encode([query], normalize_embeddings=True) return collection.query(query_embeddings=qv.tolist(), n_results=top_k) def build_prompt(query: str, docs: list[str]) -> str: """拼提示词:把资料和问题组合成一段带约束的指令""" context = "\n\n".join(docs) return ( "你是一位企业智能客服。请只根据下面提供的资料回答问题;" "如果资料中没有答案,请直接说“资料中未找到相关内容”,不要编造。\n\n" f"【资料】\n{context}\n\n【问题】\n{query}" ) @app.post("/chat") def chat(body: dict): query = body["question"] hits = retrieve(query) prompt = build_prompt(query, hits["documents"][0]) answer = requests.post( "http://localhost:11434/api/generate", json={ "model": "qwen2.5:7b-instruct", "prompt": prompt, "stream": False, "temperature": 0.2, } ).json()["response"] return {"answer": answer, "sources": hits["documents"][0]}这段代码是最小的可运行闭环。retrieve函数里用同一个embedding模型处理用户问题,保证问题向量和文档向量在同一空间可比;collection.query返回的documents里是命中的原始文本,直接作为答案的依据。build_prompt里那句“只根据资料回答,不要编造”不是可有可无的客套话,它是抑制幻觉最有效的一道闸门。temperature设成0.2,是让模型输出更保守、更贴近原文,而不是自由发挥。
# 用curl直接测一下这个问答接口 curl -X POST http://127.0.0.1:8000/chat \ -H "Content-Type: application/json" \ -d '{"question":"差旅报销上限是多少?"}'返回结果里的sources字段值得好好利用。把它原样传给前端,用户就能看到“答案来自哪份文档的哪一段”,这就是引用溯源,是知识库客服和企业内部可信赖程度的分水岭。接入微信、千牛这类渠道时,原理也是一样的:渠道方把用户消息通过Webhook转发到这个/chat接口,再把返回的answer按渠道格式回传,核心机制没有变化。
4. 把答准率从“听天由命”调到“可控”:必调参数与两种实用策略
4.1 四个必调参数:top_k、score阈值、chunk_size与temperature
跑通最小闭环之后,真正的调优工作才开始。这个阶段你会明显感受到RAG系统是个“黑匣子”里的复杂联动系统:改一个检索参数可能影响生成质量,改一个模型参数又会暴露检索的短板。我调过上百个问答case之后,得出的结论是优先调四个参数:top_k、score阈值、chunk_size和temperature。
| 参数 | 建议范围 | 对结果的影响 | 调参方向 |
|---|---|---|---|
| top_k | 3~10,默认5 | 太漏答案,太多混入噪声 | FAQ短条目设3,长文档设5~8 |
| score阈值 | 0.3~0.7 | 低于阈值应拒答,防止模型硬编 | 按真实问题分数分布设定 |
| chunk_size | 300~800字符 | 决定一个切片里语义是否完整 | 操作步骤设300,制度文档设600 |
| temperature | 0~0.5 | 控制模型创造性 | 客服一律0.1~0.3,闲聊可0.5 |
四个参数里最容易忽略的是score阈值。向量检索的相似度分数不是一个绝对数值,不同embedding模型打分的分布差异很大,所以我一般会先用二三十个真实用户问题跑一遍,把“命中的相似度”和“没命中的相似度”打印出来看分布。正常情况两者之间会有一个明显的分界带,取中间值作为阈值。低于阈值的检索结果应该宁可答“未找到”也不要硬答,这是把幻觉率压到最低的一个关键手段。top_k和score阈值通常是配合使用的:top_k取5但第五个分数掉得厉害,实际只取前3个;如果全部低于阈值,直接走“资料未找到”的兜底话术。
chunk_size的调整有一个容易被忽视的联动:chunk切得越大,每个chunk包含的语义越杂,单个chunk的向量就越“中庸”,召回回来的片段相关性自然下降;chunk切得越小,召回越精准,但容易把一条完整操作说明切成两半,生成阶段会丢信息。我的经验是:以400字符为中心,向300或600两个方向试,哪个方案在你自己的测试集上分数高就用哪个,不要照搬别人的默认值。temperature的影响最直观,但要注意它不能弥补检索的缺失——检索回来的段落压根不含答案,temperature设多低模型都会胡说,所以调参顺序永远应该是先调检索,再调生成。
4.2 混合检索与查询改写:FAQ精确匹配和关键词漏召回怎么补
纯向量检索有一个经典短板:用户在问题里用了一个词,文档里用的是它的同义词或缩写,向量模型可能召不回来。比如用户问“报销上限”,文档里一直写“费用报销标准上限”,有时候向量模型能兜住,有时候就漏。业内通用做法是混合检索:一路走向量召回,另一路走BM25这类关键词召回,最后用RRF把两路结果融合。RRF算法的好处是不需要把两路分数归一化,只看排名,实现起来很干净。
def rrf_merge(vec_rank: dict, bm25_rank: dict, k=60): """RRF融合:把向量召回和关键词召回的排名合并成最终排序""" scores = {} for doc_id in set(vec_rank) | set(bm25_rank): scores[doc_id] = 0 if doc_id in vec_rank: scores[doc_id] += 1 / (k + vec_rank[doc_id]) if doc_id in bm25_rank: scores[doc_id] += 1 / (k + bm25_rank[doc_id]) return sorted(scores.items(), key=lambda x: x[1], reverse=True)这段代码里vec_rank和bm25_rank分别是两路检索对每个doc_id的排名,doc_id可以就是chunk的id。RRF给每路结果算一个基于排名的分数,排名越靠前贡献越大,然后把两路加和。k=60是经验值,控制排名权重下降的速率,一般60左右效果稳定。混合检索适合“文档里有大量专业名词、缩写、编号”的场景,能明显减少召回漏掉的情况。
还有一个在真实客服系统里更有性价比的补法:结构知识库精确匹配兜底。把高频FAQ整理成一张表,问题、标准答案、关键词三列,用数据库查询或Elasticsearch做精确匹配,命中就直接返回标准答案,不再走向量检索。这套“先精确匹配,后向量召回”的两级路由,能解决80%“用户想问的就是那条FAQ”的场景,而且响应速度极快。知识库分类上,这就是结构化知识库和RAG知识库的典型协同方式:FAQ和参数表走结构化查询,产品文档和操作手册走向量库,KG图谱只有在需要多跳关联查询时才引入。
4.3 多轮会话与引用溯源:让客服“记得上文”和“拿证据说话”
单轮问答跑通后,真实客服会话几乎都是多轮的。用户先问“退货怎么处理”,再问“那运费呢”,这里的“那运费呢”单独拎出来去检索,结果一定很散。多轮会话的常见做法是接口层接收一个history参数,把最近几轮对话的问答内容拼进提示词,让模型理解“那运费呢”指的是“退货那件事的运费”。但检索时有一个容易踩的坑:不要用整段历史去做向量检索,而是应该用“当前问题 + 最近一轮用户问题改写后的query”去检索,否则历史里的噪声会把向量检索带偏。
@app.post("/chat") def chat(body: dict): history = body.get("history", []) # 格式:[{"role": "user"/"assistant", "content": "..."}] query = body["question"] # 检索只用当前问题,历史对话只用来帮助模型理解上下文 hits = retrieve(query) prompt = build_prompt(query, hits["documents"][0], history=history[-4:]) ...history[-4:]表示只保留最近4条消息,避免提示词被历史撑爆,也防止太早的对话干扰当前意图。注意检索函数里用的还是query本身,这是有意为之的权衡:query改写做得好能提升召回,做不好反而引入歧义,对于第一版系统,先把历史交给生成阶段处理,检索阶段保持简单,后续再迭代查询改写模块。引用溯源方面,可以在build_prompt里加一句“回答每个要点后标注来源编号,如[1]”,然后让生成结果里的编号与返回的sources列表一一对应。前端拿到sources后,点击编号就能查看原文段落,这在企业内部推广时说服力非常强。
5. 私有知识库客服落地避坑:五条一线踩坑记录
5.1 答案很流畅但全是从模型“脑子”里编的
现象:用户问“年假能攒到第二年吗”,客服答了一大段逻辑严密的政策说明,细看发现知识库文档里根本没有这一条。原因:模型在生成阶段没有受到足够强的约束,或者检索阶段分数虽然低但阈值没挡住,模型就开始动用预训练知识补全。解决:双层防线。第一层,在build_prompt里把约束话术写死:“如果资料中没有答案,请回答‘资料中未找到相关内容’。”第二层,加score阈值门槛,所有召回片段分数低于阈值的直接拒答。我见过太多团队只加第一层不加第二层,因为模型“忍不住”想回答,所以两层都上才稳。
5.2 私有化换了模型后,效果断崖式下降
现象:本机用云端API测试时效果很好,换成私有化部署的开源模型后,答非所问、格式全乱。原因:不同基座模型的指令遵循能力和中文理解能力差异很大,云端API背后是很强的商用模型,本地开源模型如果选小了,或者prompt写法不兼容,效果立刻现原形。解决:换模型后不要只做“能通”验证,要拿同一套测试集跑一遍再上。优先选中文语料强的7B级以上模型,prompt写法也要为本地模型适配,有些模型对“请只根据资料回答”这种指令理解不到位,需要改成更直白的“你没有其他知识来源”这类表达。这一步没有捷径,只能用一个标准评估集反复验。
5.3 文档明明更新了,机器人还在答旧内容
现象:运营把新产品手册传上去了,客服系统回答的还是老产品的参数。原因:知识库后台有“上传文件”和“重建索引”两个动作,运营通常只完成了第一个。很多低代码平台里,上传新文件并不自动触发旧文件的向量删除和索引重建。解决:在知识库管理后台明确区分“全量重建”和“增量更新”。每次更新文档后,对已变更文件做内容hash比对,hash变了就重新切片入库、删除旧chunk;如果系统支持,定时每晚执行一次全量重建兜底,确保新文档一定生效。
5.4 下班高峰一并发,服务卡到被客服投诉
现象:白天几个人试用没问题,晚上业务高峰期涌入几十个并发,接口响应从2秒变成30秒,有的直接超时。原因:embedding模型和LLM推理都是计算密集操作,直接部署在API服务进程里时,每个请求都占CPU和GPU,又没有队列和并发控制,资源一挤就崩。解决:把LLM推理单独拆成服务,用vLLM或Xinference这类带连续批处理的推理引擎跑,吞吐量远高于每次请求单独加载模型的做法;API层加线程池和超时控制,向量库连接用连接池复用;embedding模型如果用的是sentence-transformers,要预加载到显存并加锁,避免重复加载模型。
5.5 办公网上“答非所问”,本机演示却全对
现象:同一份代码,开发者的笔记本上跑得好好的,部署到办公网服务器后各种奇葩回答,甚至直接报错。原因:办公网环境通常有访问控制、DNS重定向和网关鉴权,模型服务的内部调用地址一旦被改写,或者Python依赖版本和服务端不一致,就会出这种“环境性翻车”。解决:部署时不要手工装依赖,直接用Docker镜像固定Python版本和依赖版本;把模型服务、embedding服务、向量库、API服务拆成四个独立探活点,启动后用curl逐一验证连通性;所有关键链路打日志,把检索到的chunk和最终prompt打印出来,出问题一眼就能看到是哪一段断了。
6. 用一套验证集给机器人打分:答准率、命中率与AB实验
给自己搭一个“考卷”是RAG客服从demo走向生产最值得做的一件事。我现在的习惯是:从真实客服会话里抽出一百条问题,每条标注三个字段——标准答案、答案所依据的知识库文档片段、以及这条问题是否该拒答。这套数据就是黄金评估集,以后每次改prompt、换模型、调参数,先在这套集上跑一遍再决定要不要上生产。评估时看三个核心指标:答准率,人工判断回答内容与标准答案是否一致;召回命中率,返回的sources里是否包含了标注的依据片段;拒答准确率,对该拒答的问题是否真的拒答了,没有胡编。三条指标合格线我一般定在答准率90%以上、命中率85%以上、拒答准确率95%以上,否则不进生产。
# 批量评估脚本的最小形态 for case in test_set: result = requests.post( "http://127.0.0.1:8000/chat", json={"question": case["question"]} ).json() hit = any(case["reference"] in src for src in result["sources"]) answer_ok = judge(case["answer"], result["answer"]) # 人工或LLM打分 reject_ok = result["answer"].startswith("资料中未找到") print(case["question"], hit, answer_ok, reject_ok)这套东西最有价值的地方,是它能把很多“玄学”争论变成数据对比。我上过一回当:凭感觉换了一个排行榜更高的embedding模型,自认为效果更好,结果黄金集上答准率掉了六个点,从此所有改动都先过考卷再上线。这种评估机制也天然支持AB实验——把旧参数和新参数各跑一遍测试集,对比同一套指标,谁上去用谁的。如果你也打算把这类智能客服系统接进生产环境,我建议在建完知识库之后立刻做这套考卷,它会成为你整个系统里最大的一颗后悔药。希望帮到你。
本文还有配套的精品资源,点击获取