Shuoxun Wen GitHub

文章 Q&A bot

只许大模型摘原句的问答机器人

一个在微信上回答同学和家长签证、考试、选课问题的机器人。为什么里面的大模型只能摘原句,程序怎么逐句核对,以及哪些句子明明是真的也不发。

马莫百科机器人在微信上回答 Monash 马来西亚校区同学和家长的问题。大部分回答靠规则:我教过的问答、Monash Hub 的接口,还有公众号自己的文章。答不上来的转给我。

规则会漏掉一些问题:答案其实就在这些资料里,只是说法不一样。所以我加了一个可选的 RAG 步骤,只在规则没命中或只有弱命中时才用。我给它定的规矩是:宁可说不知道,也不能说错流程。同学收到“我还答不上来,已经转给作者”,会再问一次;收到一个错的签证截止日期,是会照着去做的。

让模型自己写,不行

第一版让免费模型根据检索到的片段自己组织回答。读起来很顺,但错在要命的地方。它会加上资料里没有的步骤,比如“老师会替你申请特殊考虑”;还把“ISP 在贴纸剩 14 天或更少时收护照”写成了“过期后有 14 天宽限期”。

我又试了用第二个模型检查第一个模型写的句子。它漏掉了一些,还删掉了一些对的。拿大模型去检查大模型,检查的那个也有同样的问题。

模型只挑句子,程序来核对

所以模型不再写回答。提示词要求它从编号的资料里原样摘句子,一个字都不能改,最多 4 句,每行写成 [资料编号] 原句;资料里没有能回答的,就只回“这个我不确定”。

每一行都由程序核对:去掉空白后,摘的句子必须是它标的那段资料的子串,而且至少 6 个字。

def sentence(quote, h):
    text = h.text
    idx = [i for i, c in enumerate(text) if not c.isspace()]
    q = _squash(quote)
    at = "".join(text[i] for i in idx).find(q)
    if not q or at < 0:
        return None
    ...  # 扩展到它所在的整句

改写过的句子、加出来的步骤、把真句子标错了资料编号,这三种都过不了。有一个测试专门喂这三种,要求一句都不发。通过的句子会扩展成它所在的整句,不会只发半句。回复下面列出句子来自哪些页面和链接,并注明是原文摘录。

模型说不确定,或者摘的句子一句都没通过,问题就转给我。这时规则的弱匹配(“你问的是不是…”)也不发:对另一个问题的瞎猜,比“已转给作者”更糟。

真句子也可能误导人

一字不差也不够。早期有一次回复摘了续签页面上的这句:

你必须在到期前四个月提交续签申请;并且

页面上确实有这句话。但它写在“如果你没有满足上述任何一项要求:”下面,只适用于一部分同学。没有上面那一行,这句话就等于告诉所有人一个错的截止时间。

所以 _why_not 还会去掉离开上下文就会看错的句子:

  • 标题以冒号结尾的列表项;
  • 以“;并且”或“:”结尾的行;
  • 以“此流程”开头的行,或者以“不,”开头、回答的是一个读者看不到的 FAQ 问题的行;
  • 没有表头的表格行;
  • 里面的年份全都已经过去的句子,比如去年的考试日期。

教过的问答和 Hub 的 FAQ 是专门给这些同学写、核对过的,它们能回答时,不再在旁边混进一般网页的句子。以前就出现过一个旧页面写着“你必须选择参加考核”,旁边教过的问答说的却是可以不考。

马来西亚不是澳洲

来问的大多是马来西亚校区的同学,但 Monash 很多页面是写给澳洲校区的。Hub 的 FAQ 以前写过“休学会改变你的 CoE”。CoE 是澳洲学生签证的东西,对马来西亚的 Student Pass 没有意义。

现在每一句都按校区分类:来自澳洲校区页面的句子,或者提到 CoE、OSHC、ESOS、Centrelink 的句子,算澳洲;教过的问答、公众号文章和马来西亚校区页面算马来西亚。排序是马来西亚在前,各校区通用其次,澳洲最后。回复里只要有澳洲的句子,就加上小标题,澳洲的规定放在“澳洲校区(澳大利亚的规定,马来西亚校区不一定一样)”下面。签证、保险、修课量这些教过的问答也改成两个校区分开写;有个测试会检查,提到 CoE、OSHC 或 subclass 500 的回答,必须两个校区都标出来。

效果

评测用固定的 50 道题,分别只用规则、用规则加 RAG 回答,由一个裁判模型对照参考要点打分。第八轮是 2026-10-25 跑的(docs/EVAL.md):

  只用规则 规则 + RAG
回答正确(裁判判) 81%(34/42) 88%(38/43)
没有答案的题交给了我 14%(1/7) 86%(6/7)

我最看重第二行。有 7 道题在所有资料里都没有答案。只用规则时,其中 6 道它都瞎猜了一个回答;加上 RAG 后,7 道里有 6 道转给了我。

第一行要打个折扣。裁判只看参考要点有没有出现,回答里多一个错的步骤,照样可能判对。这也是为什么核对放在程序里,而不是再交给一个模型。

它用的是 Groq 的免费额度(qwen/qwen3.8-27b),2026-10-24 起在线上打开。第八轮里 95% 的回复不到 1 秒,远低于微信 5 秒的限制。回答缓存 7 天,同样的问题不会再调模型。模型慢了或者挂了(免费额度同时来几个问题就会返回 429),教过的问答、公众号文章和 Hub FAQ 照常发;规则只有弱匹配的话就不猜,转给我。

还做不到的

核对能保证发出去的每一句确实在它链接的页面上,但保证不了那个页面就是这个问题该看的。检索拿到一段讲别的情况的资料,模型摘得一字不差,也还是答非所问。所以每条回复都附上页面链接、注明是摘录,答不上来的问题也还是会转给我。

代码:rag.py、test_rag.py、evaluate.py。