n8n 客服实验:用知识约束减少编造
作者用 n8n、Ollama 和本地 Qwen 小模型搭建客服工作流,对 20 个问题开展共 120 次对照测试,报告知识约束使编造回答从 22 次降至零。实验给出模型配置、知识库范围与延迟,但结论限于小规模测试集。
AI 深度解读
客服机器人最危险的回答,往往语气很正常:「我们没有电话客服」「不支持 Slack」。但知识库没写,不等于业务没有。作者用一组刻意包含未知问题的测试发现,机器人答对了全部已知问题,却在未知问题上编出了 22 条答案。
这次实验值得看的,是 n8n 中如何用代码核验证据,以及核验规则如何误伤正确答案。作者最终把编造回答降到了 0,但这个结果有明确的测试边界,不能直接当作上线保证。
答对所有已知问题,为什么还不能上线
作者搭了一个很小的客服知识库,只有 6 条:14 天退款及联系邮箱、Starter $19 和 Pro $49 且无年付、客服时间、无需绑卡的 7 天试用、取消订阅,以及 Pro 的 CSV 导出。
测试环境也交代得比较完整:自托管 n8n 1.123.84,通过 Ollama 的 OpenAI-compatible /v1 端点,在 CPU 上运行 Qwen 1.8B(Q4_K_M),设置 temperature 0.2、response_format: json_object。两个工作流都由 Webhook、Config、Build Prompt、LLM、Code 节点和 Respond 组成。
作者准备了 20 个问题,一半能从知识库找到答案,一半不能。每题重复 3 次,分别测试原始版和带证据核验的版本,共 20 × 3 × 2 = 120 次运行,平均延迟 1.4 秒。
原始提示词并不离谱:扮演友好的客服,参考知识库,返回 {answer, escalate},只有无法帮助用户时才设置 escalate=true。已知问题全部答对,问题出在另一半。
学生优惠、电话客服、Slack 集成和 iPhone App 这几个未知问题,都在 3/3 次运行中得到确定的否定回答;uptime SLA 则有 2/3 次被回答为没有保障。询问添加团队成员时,模型甚至编出了每月 $49 的收费方案,另一次又说免费,并给出不存在的 Settings > Team Members 菜单。
密码重置的问题更能暴露机制:模型找不到对应资料,就拼接最近似的退款条目,让用户联系 support@yourco.com 并提供订单号。
知识库里没有答案,不能被模型解释成业务上的「不支持」。
原始版在 30 次未知问题测试中只升级人工 1 次。作者逐条审阅了其中 29 条回答,认定 22 条包含知识库没有的断言。其余 7 条里,3 条用 CSV 导出回答 GDPR,属于答非所问;3 条按 14 天政策否定购买 30 天后的退款,作者认为合理;还有 1 条承认资料缺失却没有升级人工。原文没有展开第 30 条的具体内容,因此不能把 22 直接包装成通用错误率。
要求模型交证据,比要求它「别乱说」更有用
作者也试过在提示词中强调只能依据知识库,1.8B 模型仍然会生成没有依据的内容。因此,改动的重点放到了 LLM 节点之后。
模型需要返回 {answer, kb_id, quote, escalate}:不仅给答案,还要交代使用哪条知识,以及从中复制的原句。接着由 Code 节点检查证据,不通过就替换成固定回复,并设置 escalate: true。
v1 的检查分为几层:模型是否要求升级人工,kb_id 是否存在,引文是否出现在对应条目里,以及答案中的数字和邮箱是否都来自该条目。固定回复是 I am not sure, a teammate will follow up shortly.
这套规则确实挡住了伪造证据。模型回答密码重置时,会把自己编出的操作步骤再复制到 quote 字段中;代码找不到对应原文,就拒绝放行。
但 v1 同时丢掉了 30 条正确答案中的 18 条。
原因不是这些答案全错了,而是模型不擅长填写辅助字段。它会把 refunds 写成 refund,把 trial 写成 free_trial,甚至一边逐字引用正确内容,一边输出 kb_id: "none"。
这里有个容易忽略的工程问题:如果把模型生成的标识符和布尔值直接当成裁决依据,校验系统也会继承模型的不稳定性。看起来规则很严格,实际可能只是在惩罚字段填错。
放宽标签检查,为什么反而更实用
v2 没有修改提示词,也没有重新调用模型。作者拿已经保存的 60 份 grounded 输出,重新跑了一遍 Code 节点。
变化很集中:忽略 kb_id 和模型自己的 escalate,在整个知识库里查找引文。只要找到足够长的真实片段,再核对答案里的数字和邮箱。
下面这段是作者的 v2 核验逻辑,用来判断模型交出的证据是否真实存在:
const q = norm(out.quote);
const hit = q.length >= 20 ? Object.values(KB).find(e => norm(e).includes(q)) : null;
if (!hit) reasons.push('quote not found in knowledge base');
else {
const facts = String(out.answer||'').match(/\$?\d+[\d.,]*|[\w.+-]+@[\w.-]+/g) || [];
const bad = facts.filter(f => !norm(hit).includes(norm(f)));
if (bad.length) reasons.push('facts not in quoted entry: ' + bad.join(' '));
}
要留意第一处判断:q.length >= 20 检查的是经过 norm 处理后的长度。原文的 norm 会转成小写,并删除所有非 a-z0-9 字符。这段代码依赖前面定义好的 KB、out、reasons 和 norm,并不是完整的独立程序。
重放结果仍是 0 条编造回答,同时只剩 4 条正确答案被错误升级人工,原因是模型改写了引文,没有逐字复制。未知问题中有 3 条通过检查,都是购买 30 天后能否退款的问题,回答引用了真实的 14 天退款政策,作者将其计为正确放行。
这个改进的价值,是把判断从「模型是否填对标签」转向「证据能否被程序找到」。但找到证据,只完成了一部分工作。
我的判断
我认同作者把最终放行权交给代码,也认同必须同时统计错误回答和错误拦截。v1 的编造数已经是 0,代价却是拦掉 18 条正确答案;只报前一个数字,会让一个很难用的客服系统显得非常成功。
不过,引文真实,不代表答案相关,也不代表整段回答都被引文支持。
原文自己就给出了反例:用户问 GDPR,模型引用真实的 CSV 导出说明,仍可能通过检查。数字和邮箱校验也只能覆盖有限事实,无法识别所有语义上的添油加醋。作者建议加入问题与条目的关键词重合检查,或者人工抽查前 50 条回答,但没有提供这些补充措施的实测结果。
代码还有中文团队不能直接照搬的前提:norm 会删掉中文。纯中文引文可能变成空字符串,含中文的不同句子也可能在清洗后只剩相同的英文或数字。这个规则适合作者当前的英文小知识库,迁移时必须重做文本处理。
与只加强提示词相比,这次实验说明外部核验有实际价值;与更完整的检索增强生成(RAG)或语义校验相比,原文没有对照数据,不能据此判断谁更可靠。6 条知识、单个 1.8B 模型和重复提问,也不足以覆盖真实业务中的表达变化。
此外,固定回复承诺了同事跟进,但原文没有说清 escalate: true 后如何创建工单、分配人员。标签只是数据,后续接单流程还需要落地。
能拿来做什么
- 给现有客服知识库补一组明确无法回答的问题,例如未记录的集成、收费例外和功能入口,并重复运行,观察模型是否把资料缺失回答成确定的「没有」。
- 在 n8n 或 Node 服务中保留答案与逐字引文,先做证据存在性检查,再决定是否放行;中文知识库要重新设计文本归一化规则。
- 同时记录编造回答、答非所问、正确答案被拦截和升级人工数量。调整规则时重放已保存输出,避免把模型波动误当成规则收益。
- 将兜底回复接到真实的人工处理流程,并抽查最初 50 条回答。作者的 support-FAQ 工作流尚未进入免费仓库,现有免费内容是 lead qualifier 和 review reply bot,接入时仍需自行补齐节点上下文。