← 返回博客 XFENG · INSIGHTS

为持续服务设计 Agent 记忆

客服 Agent 的长期记忆不能停在对话里说已经记下。本文记录跨会话偏好如何抽取、审核和落库,以及为什么当轮应答不等于保存成功,下一轮是否照做才是验收标准。

会话中记录的客户偏好,在下一轮对话里被真正执行,并在记忆表中留下对应记录

我们重做了客服系统的客户长期记忆,验收的办法很土。打开访客端的聊天窗口,用一个全新的访客身份,跟自己的系统聊一次上门维修。聊到约时间那一步,我打了一句"上门维修的时间只用微信跟我确认,别打电话"。

AI 回答得很好。它说好的已经记下了,后续会通过微信跟您联系。

然后我关掉窗口,重新开一个会话,什么都不提,直接问维修师傅什么时候到。这一次它没有提微信,问我方不方便留个手机号。

第一轮走查到这里就停了。那句"已经记下了"是不是真话,要看你怎么定义记住。模型在那一轮里确实理解了这句话,只是没有把它写进任何地方,下一次它是一张白纸。

这句话本身也当不了凭据。抽取、审核、落库是异步的,一次抽取要几十秒才走完,模型说这句话的时候那条候选还在半路上,它不知道最后有没有存住。对话里的应答不构成保存确认,保存确认只看记忆表里那一行和访客卡片上显示出来的那一条。

本文讲的是网页客服渠道上的跨会话客户记忆,问题都出在发布前的内部走查里。这样的走查我们做了四轮,"只用微信联系我"这条偏好前三轮一次都没存下来,第四轮才通过。

记忆的价值在于未来的服务有没有因此做出更合适的判断,存下了多少信息本身不算数。"只用微信,别打电话"这句话既是一条偏好,也是后续每一次联系的限制。把它塞进聊天摘要,和真的让它管住下一次服务,是两件事。

会话一里客户提出偏好,会话二里 AI 主动照做,面板底部那一枚是这条偏好在记忆表里的行

验收标准是下一轮对话有没有变

第三轮走查里出过一件让我们警觉的事。AI 主动复述了客户的微信偏好,还顺带提了之前说过的预算区间,看上去记忆生效了。我们去记忆表里查,两条都不在。它读的是这个访客最近的对话历史,从里面翻出一句几分钟前说过的话不难。这个行为很像记忆,只是同一个访客隔一周再来,这句话就没有了。

我们后来把验收标准写死成两条。第一条,记忆表里真的多了这一行,有 schema、有值、有来源。第二条,开一个新会话,什么都不提,AI 的行为真的变了。

这两条一起成立,还不足以把行为归到记忆头上。同一个客户的近期对话历史照样会进上下文,造出来的效果跟记忆生效长得一样。更完整的做法是保持同一个客户身份、开新会话、把旧历史隔离掉,再对比开启和关闭长期记忆时的行为差异。这一步我们还没做,现在的验收覆盖的是落库和新会话行为两项。

这里要分清客户身份和会话身份。关掉窗口、换台设备本身都不决定记忆可不可用,决定它的是新请求能不能关联回同一个客户。

这个标准是被一次假绿逼出来的。第一轮走查之前,这套记忆系统有一千九百多个单元测试全绿,而它在真实系统里一条记忆都落不下来。测试里的抽取器是个替身,被替换掉的那一环恰好是唯一会失败的一环。后面三轮都用真实的模型和数据库跑完整段对话,再去查表。

改和删也在验收范围里,进度不一样。删除路径已经验过,访客记忆卡片上单条"忘记"有二次确认,"清空全部"会把相关的行一起清掉。客户改口的情况还没验过,上一次说只用微信,这一次说微信也别发了,旧的那一行该覆盖还是该作废,现在只有设计上的说法。

需要执行的约束,不能只存成一段话

自由文本和结构化记忆并不对立,各有各的位置。联系方式这类要被执行、要被审计的约束需要结构化,理由是它得能被检查、被优先、被撤销。一句"客户说只用微信"存成文本以后,下一轮谁来读它,放在提示词的哪个位置,跟别的信息比谁更优先,全都没有答案。

每条记忆有一个 schema,规定它有哪些字段、字段能取什么值,另外带着来源(哪个会话的第几轮)、作用域和衰减方式,以及隐私标记。模型抽出来的只是候选,要过一道审核,审核给出明确的理由码,通过了才落库。

微信这件事最后就卡在 schema 上。联系偏好这个 schema 原来是一个渠道枚举加一个布尔,四个值,电话、短信、邮件、在线聊天,允许或者不允许。"只用微信,不要打电话"在这套字段里没有位置,微信不在枚举里,"只用 X,不要 Y"也不是一个布尔装得下的。

模型把微信归到最近的那个值上,抽成了在线聊天、允许。审核那一关拿这条候选去对原话,判定渠道与证据不符,同时判定约束不完整,拒绝落库。前两轮死于超时,这一轮死于 schema。

schema 表达不了的偏好,模型再聪明也存不下来。中文业务场景里的联系渠道绕不开微信,家电售后的"只在周末上门"也是"只用 X 不要 Y"这个形状,这些要在设计字段的时候就进来。

我们的改法是渠道枚举里加上微信,再把单值加布尔换成两个列表,首选渠道和禁止渠道。这条偏好第一次存下来了,同时留下两处已知没做到的地方。"只用微信"里的那个"只"没有被表达,禁止列表里只有电话,短信和邮件并没有被排除。原话里"上门维修的时间"这个适用范围也没有编码,落进库里它是一条无差别的联系偏好。偏好哪个渠道、只允许哪些、禁止哪些、约束管哪件事,是四层不同的语义,这一版做到的是前两层的一部分。

还有一处要收口。每条记忆会同时生成一行给模型看的正文,这行正文只能和字段说得一样多。这一版的正文说得比字段满,字段禁掉的是电话,正文写成了只走微信。模型听的是正文,所以要改的是正文,让它退回字段的语义。

左边是自由文本,右边是同一条偏好落进 schema 之后的样子

出现新信息时该相信谁

记忆一旦能影响回答,就有了第二个问题,几份说法对不上的时候听谁的。客户本轮的原话、系统确认过的事件、库里存着的历史记忆、模型自己的推断,这四样在我们这里的可信度不一样。

前面第三轮走查那件事已经露出了一种混淆。同一个客户的近期对话历史会进上下文,它算不上记忆,却能造出记忆的样子。把它和长期记忆分开算,是因为它随会话消失,而记忆要跨会话活下去。

会话内的上下文也要挑着用。我们在流程里给多轮上下文定了几条规则,只保留最近的十二轮;某一轮如果失败或者被取消,客户发出的原话保留,模型那半截回复排除,并且标明这一轮没有完成。原先的做法是把整轮删掉,复现过一次后果,客户先说型号是 A,改口说 B,那一轮回复恰好失败,下一轮客户说"继续",模型手里的历史仍然只有 A。

改写环节同样受约束。多轮对话里模型会把客户这一轮的话改写成一句完整的办理指令,改写必须保留原文里的事实、否定、时间条件和动作范围,"先不预约"不能被改成"取消预约"。

写进长期记忆的门槛更高。模型抽出的候选要过审核,审核拿候选对原话,渠道与证据不符、约束不完整都会被拒绝,前面那条微信偏好第三轮就是这样被拦下的。事件也一样,一条普通的客户消息不能自动开出一件服务事项,只有转人工、坐席接手这类系统确认过的动作才算开单证据。我们曾经让每个新访客的第一句话都开了一张单,后来收了回来。

这些规则合起来说的是同一件事。排在最前面的是客户说过的原话,然后是系统确认过的事,历史记忆排在它们后面,模型自己推出来的东西垫底,后面的永远不能覆盖前面的。

必需上下文与可选召回分开设预算

每一轮对话开始之前,系统要为这个客户编译一份上下文交给模型。里面有四样东西,结构化的槽位比如设备型号,硬约束比如"不要打电话",进行中的服务事项比如没走完的上门预约,还有一份向量召回来的历史记忆。

前三样都是本地读,毫秒级。第四样要出网,到租户配置的 embedding 供应商往返一次,耗时不由我们决定。旧做法是给整段编译加一个两秒的超时,超时就整体降级,这一轮不给模型注入任何客户上下文。

供应商慢的那几天,这个做法的问题露出来了。向量召回把两秒耗光,同一个超时把已经在内存里、几毫秒就能读到的硬约束也一起扔了。客户明明说过不要打电话,这一轮模型却不知道,起因是一次跟这条约束毫无关系的网络等待。

修法是把超时挪进去,只框住出网的那一步。召回超时,这一轮就少一段历史记忆,剩下三样照常给。降级路径本来就建好了,只是从来没人给它设过闸。

更稳的做法是,必需上下文和可选召回各自设预算、各自定降级策略,外面再留一层总的保护。本地那三步同样需要超时,数据库连接排队一样会把它们卡住。要防的是让一个可选步骤的超时把必需步骤一起拖下去。

预算本身也差点定错。旧注释写着 p95 一点四秒,两秒看着很宽裕,而那个分布里绝大多数样本是"这个客户压根没有记忆,直接返回空"。只看真有候选要召回的请求,p90 超过五秒。

改造前一个超时罩住四段,改造后只框住出网的那一段

调用方已经放弃的调用不算成功

同一轮排查里我们还看到,账本在撒谎。

网关那边两秒读超时,放弃了这次召回,这一轮的客户上下文里没有历史记忆。记忆服务不知道有人已经走了,六秒多的时候把结果算完,往自己的事件表里写了一行,结果成功,注入两条。翻这张表成功率很好看,客户那一轮什么都没拿到。

修法是让调用方在请求里声明自己的预算,它实际执行的读超时是多少就写多少。记忆服务发现自己已经超过这个数,就把这次调用记成已放弃,同时不写那些条目级的"这条记忆被用过"的记录。那些记录是同一个谎的细节版本,会污染客户档案里的使用记录。

预算声明管住的是截止时间这一段。服务端算完了、调用方收到并且采用了、这一轮上下文里真的注入了,是三个状态。改完之后我们能分开前两个。"已注入两条"这一句仍然由服务端按自己这边的完成情况写,调用方到底有没有采用,没有回传过来。让调用方回一次采用确认,是下一步要做的。

原则是一句话。账本记录的是客户实际得到了什么,不是服务器做完了什么。调用方已经放弃的调用,被调用方无论算没算完,都不能记成功。

调用方两秒放弃,被调用方六秒多记成功,这一行就是账本在撒谎

只用微信联系我

第四轮走查,schema、超时、账本都改完了。同一套动作,新访客,聊上门维修,还是那句"上门维修的时间只用微信跟我确认,别打电话"。

这一次记忆表里多了一行。schema 是联系偏好,首选渠道微信,禁止渠道电话,来源写着这次会话的第几轮,隐私标记是不含个人身份信息,审核通过。

接着是第二条标准。关掉窗口重新进,开新会话,我没提微信,直接问维修的事。AI 的第一句话就是"我们只通过微信联系您,不会打电话"。后面整个时间确认,它一次都没有再问电话。这句话里的"只"就是上一节说的那处正文比字段说得满,字段只禁了电话,模型照着正文把话说死了,正文退回字段语义之后它会改口成"优先通过微信联系您,不会打电话"。

这里有一条边界要守住。访客端是网页窗口,我们并没有主动往微信发消息的通道。记忆约束能约束的是系统做得到的行为,它不该反过来让模型替系统承诺一个还不存在的能力。走查里模型说过一句时间定了发到您微信上,字段没让它说,通道也支持不了,这种措辞要压掉。

这是行为验收通过的样子。前三轮里 AI 也说过"已经为您记录"这类很好听的话,那些话跟判断标准没有关系。

模型越强,越要看行为

四轮走查里,AI 的回答质量一直不错,每一轮都理解了"只用微信不要打电话",也每一轮都答得得体。一个足够强的模型在当前这一轮里就能翻出五分钟前说过的话,把它组织成一句让人放心的回复,不需要任何记忆系统参与。拿回答的样子去验收记忆,量到的是模型的语言能力。

所以评估一套 AI 客服的记忆功能,可以把"支持长期记忆"这句话换成几件具体的事问回去。让对方展示这条记忆存在哪里,字段是什么。把长期记忆关掉,再开一个新会话,看行为差在哪。客户改口以后,那一行变成什么样。客户要求删除以后,下一轮还认不认得这件事。

落到客户这一头,要的是说过一次的偏好不用再解释,后续的服务不违背明确的约束。还有一条反面的,一次没有经过证实的回答不该被一直带进下一次沟通。

我们自己这套现在能答上来的有四项。记忆落库、跨会话生效、删除、召回超时之后的降级,都在内部走查里验过了。改口以后的更新验收,以及关掉记忆做对比这个实验,还没有做。

注:本文为 2025 年底的项目实践记录。