资料整理干净以后,我给自己那点内部资料接了一个问答 Agent:你问一句,它先去资料里检索,把相关的那份文件交给本地模型,模型照着原文答一句,最后附上出处。
搭起来不难,难的是怎么判断它“能用了”。之前我都是自己问几句,问的还总是我知道答案的那几个,次次都对。这次我逼自己先写验收标准,再谈上线。
部署长什么样
整个东西拆开就三块:
资料目录,分「现行」和「旧版归档」,只有现行进检索。
一个 ask.py,负责检索和调模型。检索用的是最土的办法:把问题和资料都切成两个字一组,数重合了多少,重合最多的那份就是答案来源。一个都没重合,就直接回答“资料里没有”,连模型都不叫。
模型是一个 0.5B 的小模型,用 llama-cpp-python 在 CPU 上跑。这么小的模型自己知道的东西不靠谱,所以我只让它干一件事:拿到检索出来的原文,用一句话转述,不许发挥。
考卷
验收的考卷一行一道题,竖线右边是答案里必须出现的词。资料里本来就没有的,写 NONE,要求它必须说“没有”:
五道题,三道该答出来,两道该老实说没有。我故意让“该说没有”的题占了快一半。对内部问答来说,答不上来不可怕,可怕的是不知道还硬编。
验收脚本做的事很简单:每道题真的调一次 Agent,看回答里有没有那个词,再把出处打出来。注意是真调,模型也在里面,不是只测检索。
第一轮:2 过 3 挂
挂了三道,挂法各不一样,挺有代表性的。
“报销最晚周几交”:一个出处都没找到。原因是第一版是按“行”去比的,问题里的字跟资料里任何一行都凑不够两组,于是判定“没有”。资料明明写着周四。
“新人第一天领什么”:出处找到了入职须知,但答案里没有“门禁卡”。因为按行检索,只翻到了标题那一行“入职第一天”,真正写着门禁卡的那一行没带上,模型看到的原文里根本没有答案。
“投影仪连不上找谁”:这道最有意思。资料里我当初写了一句“投影仪连不上:资料里还没写,先问行政”,本意是占个位,提醒自己以后补。结果检索一看,“投影仪”对上了,就把这句交给模型,模型就一本正经地回答了。占位的话,被当成了答案。
第二轮:改成按文件检索,删掉占位句
改了两处:检索从“按行”改成“按整份文件”,命中以后把整份文件交给模型;那句占位的话从资料里删掉,没写的东西就是没写,不在资料里留“还没写”。
4 过 1 挂。投影仪那题过了,现在它会老实说没有。
但报销那题还是挂,而且挂得更隐蔽了:出处变成了入职须知。我 grep 了一下“报销”才明白,入职须知第三条写着“阅读《报销流程_v3》”。按整份文件比,入职须知和报销流程跟问题的重合一样多,排在前面的入职须知就把题抢走了。模型拿着入职须知去答“报销最晚周几交”,当然答不出周四。
这种错我觉得比答不上来还危险,因为它带着出处,看起来很像回事。
第三轮:文件名加权
第三版只改了一行:问题跟文件名的重合,权重翻倍。报销流程这份文件名里就有“报销”,正主一下就排到前面了。
5 过 0 挂。单独问一次“报销最晚周几交”,回答“每周四下午五点前”,出处报销流程_v3,在这台 CPU 机器上用时不到两秒。
验收过了,离上线还差什么
我列了张验收清单,到这一步打勾的是:资料可读、检索能指到出处、没有的会说没有。
还没打勾的有两条。一条是开机自启,机器重启以后服务得自己起来,不能等人想起来去开。另一条是换资料以后重跑考卷。资料一换版,上面这三轮可能得重来一遍,这件事得变成习惯,而不是想起来才做。
还有一点我自己也很清楚:五道题太少了。考卷以后只会越来越长,尤其是用的人问出来的、资料里没有的那种。
几个踩过的点
按行检索会把答案切碎,问答 Agent 至少要按段或按文件给模型原文。
资料里别写“还没写”这种占位句,它会被当成答案。
文件之间互相引用(“请阅读某某流程”)会抢答,要么给文件名加权,要么把引用句排除。
验收要真调模型,只测检索会漏掉“翻到了文件、但没翻到答案那一行”的情况。
这几条都是在五道题、几份演示资料上踩出来的,资料一多、题一多会不会冒出新问题,我说不准。
这次部署和验收的记录我放在 evafang.com ,有兴趣可以看看。
你们给问答 Agent 做验收的时候,是怎么出“它该说不知道”的题的?