机器人反复给出错误建议,可能是因为后续处理结果没有关联到回答依据。文章建议记录每次调用使用的知识条目 ID,将用户或人工处理结果回写为评分,并让检索排序使用这些评分。
你的客服 bot 告诉客户,清除应用缓存就能解决同步错误。但这招没用。客户再次回复,团队里的一位同事找到了真正的解决办法:重新授权集成,然后关闭了工单。到了下周二,另一位客户报告了同样的错误,bot 又一次建议清除缓存,语气还是那么笃定。
简单来说:每次调用结束时,还没人知道它给出的建议是否有效,因此等反馈到来时,已经找不到可以关联的对象了。你需要为每次调用保留记录,记下生成答案时所依据的已存储笔记的 ID。等用户、工单系统或接手处理的同事给出结果判定后,把它作为评分回传给这些笔记,并让 bot 从会在排序时考虑评分的记忆中召回内容。这样到了下一次,无效的建议和有效的建议就不再处于同等地位。整个过程不会删除任何内容:失败的建议仍会保留,只是附上了它对应的结果。
典型的自定义 bot,就是在模型调用外面包上一层检索:一个 system prompt、一次对帮助文章或已存储笔记的搜索,再加上用户的问题。上周那个答案最终有没有用,并不在这些输入里,而是在客户的回复和客服工单系统里。
团队通常会依次尝试三种修复方法。
修改 system prompt。加上一句「遇到同步错误时,不要建议清除缓存」。prompt 确实适合承载策略,但它的扩展能力和任何人工维护的文档一样:每出现一次糟糕的回答,就加上一条规则;没人清理,重要规则还得和过时规则争夺模型的注意力。
把答案和纠正内容存进记忆。这样,纠正内容就能在调用结束后保留下来,但清除缓存的建议也同样留了下来,而相似度搜索是按内容与问题的接近程度排序的。两条笔记都匹配,都会被召回,然后由模型在上下文里挑选一条。错误笔记即使已经失败了十次,它与问题的相似度也和一次都没失败时一样。
把结果关联到笔记上。当某次调用的结果确定后,就把它记录到影响了答案的那些笔记上。这样,失败的笔记带着失败记录,有效的笔记带着成功记录,考虑结果的召回机制就能区分二者。只有这种修复方法,才打通了从实际结果到下一次召回内容的反馈路径。
答案是在调用过程中生成的,结果判定却是在调用结束后才到来,有时甚至要过几天,而且来自另一个进程。因此,每次调用都必须留下记录,让后来的判定能找到对应的对象:
调用记录:bot 为每次调用保留的信息,使之后到来的结果判定能够找到评价对象。记录包括问题、答案,以及生成答案时所依据的已存储笔记的 ID,并用一个结果判定也会携带的键来关联。
这些 ID 对应的是每条笔记,而不是整段对话。失败的是某一条建议,不是整场交流。如果模型能够指出自己依赖了哪些召回笔记,就保留这些笔记的 ID;否则,就保留 prompt 中所有笔记的 ID,并接受它们的评分会一起变化。
这个键必须是结果判定会携带的标识:比如工单 ID、讨论串 ID,或者你的反馈按钮已经在发送的 session ID。如果一个键对应多个答案,下面的代码会保留最新的那个。
把调用记录放在你自己的数据库里,与 bot 用于召回的笔记分开存储。如果写进同一个记忆库,每条记录下来的答案都会成为下一次召回的候选,错误答案也包括在内。
明确的评分很少见。其他处理结果往往已经记录在某个地方,把这些来源接入反馈流程,比一直等着用户点「踩」更有效。
其中两类情况需要谨慎处理。如果转交人工的原因本就超出了 bot 的职责范围,比如退款或客户情绪激动,那么转交这件事并不能说明建议是否有效。而最常见的情况——用户没有再回复——也不能算作结果判定。
第三类情况最关键。评分可以降低无效建议的排名,但只有笔记才能提供真正有效的建议,因此,人工找到的解决办法需要写进 bot 用于召回的记忆库。
评分无法改变已经给出的答案。它改变的是下一次召回返回内容的顺序。
保留失败的笔记,不要删除它。清除缓存在处理另一种错误时可能是正确的办法;而且,一旦删除笔记,就无法再回答「为什么 bot 在十月份让客户清除缓存」这个问题。
评分调整的是排名,而不是真伪。如果帮助文章本身有误,就去修正文章。这套反馈循环不依赖某个特定产品:让 bot 检查一份无效建议清单,也能近似实现;你自己维护的存储可以增加一个结果字段;托管的记忆层则可以代为维护这些记录。关键在于,要有一条从结果回到召回的路径。
这里使用的是 mnemoverse 0.4.0,也就是 Mnemoverse(作者所在公司)的 Python SDK,并按照 Python SDK 页面上的说明完成配置。模型调用只是占位示例,调用记录存放在 SQLite 中。
import json
import sqlite3
from mnemoverse import MnemoClient
memory = MnemoClient() # reads MNEMOVERSE_API_KEY
DOMAIN = "bot:support"
db = sqlite3.connect("calls.db")
db.execute("CREATE TABLE IF NOT EXISTS calls "
"(call_id TEXT PRIMARY KEY, question TEXT, answer TEXT, used TEXT, concepts TEXT)")
def answer(call_id: str, question: str) -> str:
recalled = memory.read(question, domain=DOMAIN, top_k=5)
reply, relied_on = bot.reply(question, recalled.items) # your model call
used = [str(item.atom_id) for item in relied_on]
db.execute("INSERT OR REPLACE INTO calls VALUES (?, ?, ?, ?, ?)", # latest answer per key
(call_id, question, reply, json.dumps(used), json.dumps(recalled.query_concepts)))
db.commit()
return reply
def on_verdict(call_id: str, outcome: float, fix: str | None = None) -> None:
row = db.execute("SELECT question, used, concepts FROM calls WHERE call_id = ?",
(call_id,)).fetchone()
if row is None:
return # no call record: the verdict has nowhere to land
question, used, concepts = row[0], json.loads(row[1]), json.loads(row[2])
if used:
result = memory.feedback(atom_ids=used, outcome=outcome, # -1.0 to +1.0
query_concepts=concepts, domain=DOMAIN)
print("updated:", result.updated_count)
if fix: # what actually worked, from the person who solved it
note = memory.write(f"Problem: {question}\nWhat worked: {fix}", domain=DOMAIN)
if not note.stored:
print("Not stored:", note.reason)
on_verdict("ticket-1042", -1.0, fix="Re-authorise the integration under Settings, Connections.")
on_verdict("ticket-1043", 1.0) # the customer confirmed it worked
feedback 使用的 domain 与 read 相同;SDK 页面指出,如果不提供这个参数,SDK 会发送 "general"。如果写入内容与同一 domain 中最接近的记忆过于相似,就不会存储,因此代码会检查 stored。
API 文档用一句话说明了它的作用:「评分会重新排列下一次返回的内容。」概览则说明了什么会保留:「feedback 改变的是下一次 read 的顺序,而不是已存储的文本。」运行在 MCP client 上的 bot 则通过 memory_feedback 工具进行评分。
先存入两组分别涉及不同问题的笔记,再通过 answer() 分别回答这两个问题,并检查两条调用记录没有共用任何 ID。打印每次调用所依赖笔记的 valence,也就是 API 参考文档中的 "Outcome polarity [-1, +1]",然后停止进程。
在一个新进程中,只对第一次调用提交负面判定。updated_count,也就是参考文档中的 "Number of atoms updated",应该等于该次调用记录的 ID 数量。如果为零,先检查 ID 和 domain。
再次读取这两个问题,并打印同一批笔记的 valence。需要观察的是:与第 1 步相比,第一次调用所用笔记的值是否向 -1 移动。
对照组:第二次调用没有收到任何判定,因此,它的笔记就是第 3 步的比较对象。如果第二次调用的笔记发生了变化,而第一次调用的没有,就检查结果判定关联到了哪个键。如果两组笔记的变化一致,那么第 3 步就无法证明判定产生了效果。
这个库的介绍页面也展示了同样的循环如何用于 coding agents,只不过那里的结果判定来自构建或测试运行,同时还更详细地讨论了结果记忆的局限。你的 bot 所给答案的结果判定,如今最终落在了哪里?
如果还想采取进一步措施,可以考虑屏蔽此人和/或举报滥用行为。