当前位置:首页>排行榜>别设计评测集,去客服记录里捡五十条

别设计评测集,去客服记录里捡五十条

  • 更新时间 2026-11-11 08:41:33
别设计评测集,去客服记录里捡五十条

BUILD · 从零搭 RAG

上一篇那版跑起来之后,我又把切块改了一回。

改完觉得比原来好。

好多少,说不上来。

说不上来,是因为手上没有尺子。今天就把这把尺子做出来,五十条题。

别设计,去捡

闭门造车写出来的题,会长成文档的样子。

比如「退款期限是几天」。这句话文档里有,用户嘴上不这么说。

用户会说「我买的东西还能退吗」。

前一种问法,只要有一块里出现「退款」「期限」,就算命中。分数很漂亮,线上照样错。

别写,去捡。

客服会话导出、工单标题、用户群聊天、搜索框日志。最后一条有最好,没有也不耽误。

三十份文档的号,做了几个月,历史会话几百条是有的。挑五十条,两小时的事。

挑的时候只做一件事:把姓名、手机号、订单号抹掉。

错别字留着,口语留着。

你把它「规范化」成书面语,等于原地又造了一遍题,白捡。

一条长四个字段

[   {     "q": "我买的东西还能退吗",     "a": "签收起七天内、未拆封可退",     "src": "售后政策.md",     "level": "简单"   },   {     "q": "拆了吊牌还能退不",     "a": "不能,仅限未拆封",     "src": "售后政策.md",     "level": "要拼两段"   } ]

src 只写文件名,不写第几段。

段落号写死了,你下次一改切块,全变成误判。判定到文件这一级就够用。

跑之前,让切块带出处

上一篇的 chunks 是纯字符串,谁是谁都分不出来。先补一层。

from pathlib import Path  docs = [] for p in Path("kb").glob("*.md"):     for c in split(p.read_text()):         docs.append({"src": p.name, "text": c})  chunks = [d["text"] for d in docs]

search 也改一行,返回下标,别返回文本。

def search(q, k=5):     v = embed(q)     v = v / np.linalg.norm(v)     scores = vecs @ v     return [(float(scores[i]), int(i)) for i in scores.argsort()[::-1][:k]]

二十行,跑出一张清单

import json  cases = json.load(open("evalset.json", encoding="utf-8")) hit, miss = 0, []  for c in cases:     hits = search(c["q"], k=5)     ok = any(docs[i]["src"] == c["src"] for _, i in hits)     hit += ok     if not ok:         miss.append(c["q"])  print(f"命中 {hit}/{len(cases)} = {hit/len(cases):.0%}") for q in miss:     print("  漏:", q)

没有依赖,跑完两秒。输出的下半截才是重点。

第一次的数,别当真

第一遍跑完,多半不好看。这很正常。

五十条里中几条,取决于你的文档有多乱,跟模型关系不大。别人的数字别拿来对。

分数只说明现在什么样,清单才告诉你去改哪儿。

漏的那几题,扫一眼就能分出来:是切块切坏了,还是文档里压根没写。

前者改切块,后者改文档。两件事,别混着做。

改完再跑,先看回退

切块参数一改,总命中可能涨三条,同时掉两条。

涨的那三条是新的,掉的两条是老问题回来了。

只盯总数,这两条你就看不见。

所以每次跑,把 miss 存下来,跟上次比一次。

掉回去的题,比新修好的题更值得看。

这五十条会一直长

线上每答错一次,就往里补一条。

三个月后它可能一百多条。那时候它才算你真正的尺子。

不方便手打,就把客服导出的表丢给模型,让它按四个字段整理一遍。

整理完,答案和出处自己逐条核。这活儿不能省。

下一篇讲漏掉的那几题:先改切块,还是先上重排。

随机文章