尼日利亚学校重印2000份成绩单后,他们用图数据库造了个记忆

Ednovax因记忆断层重印2000份成绩单,遂用FalkorDB为客服团队建图数据库记忆。
Ednovax为尼日利亚学校提供校务系统,因未把“学校要求新成绩单格式”与“该校批量打印走独立代码路径”这两条信息关联起来,导致2000多份成绩单作废,自担超25万奈拉重印费。团队由此意识到问题不在代码而在记忆,遂构建Loopkeeper:挂在WhatsApp群上,用FalkorDB承载消息、工单、学校、代码与人员的关系,一校一图实现物理级租户隔离。“先复用再新建”是核心设计——模型读消息前,四条图查询先递上候选工单,向量提议、图成形、模型裁决。182条具名Cypher查询支撑自动指派、爆炸半径分析与跨校问题聚类,且HQ只存脱敏Signal,不存原文。
尼日利亚学校重印2000份成绩单后,他们用图数据库造了个记忆
一家为尼日利亚学校开发管理系统的公司,因为一次沟通断层,不得不自掏腰包重印超过2000份成绩单。他们没写检讨,也没加流程,而是给客服团队建了一个图数据库——让每一次对话都变成可查询的记忆。
一次代价25万奈拉的遗忘
Ednovax 为尼日利亚埃多州和三角洲州的学校提供校务管理系统。教育部调整成绩单格式后,多所学校在 WhatsApp 上要求更新。团队改好了成绩页面,却漏掉了一个关键事实:有一所学校因为技术原因,批量打印走的是独立功能,新格式从未覆盖到那条路径。
没人把“这所学校要新格式”和“这所学校打印走另一条代码路径”联系起来。学校没有先打样就印了2000多份,全部作废。Ednovax 承担了重印费用,超过25万奈拉,外加一段好不容易建立起来的信任关系。
作者在原文中写得直白:“代码不是问题,记忆才是。”

支持团队活在 WhatsApp 里
这不是一个典型的客服场景。Ednovax 的学校客户从不提工单,不用 Jira、Zendesk,也不打算用。校长直接给开发者发消息,考务官在群里插一句“早上好”、一段祷告和三个生日祝福之间,夹着一条功能请求。请求可能是深夜发来的截图,没有文字说明。
这带来三个持续存在的问题:谁在什么时候提了什么,散落在个人聊天和群组里;答应过的事转头就忘;每所学校的部署方式、定制项、例外情况只装在几个人的脑子里。
成绩单事故是这三个问题的叠加:请求在聊天记录里,例外在某个人记忆里,两者之间没有连线。他们需要的不是一个新收件箱,而是一个能读取现有聊天、记住关键信息及其关联的记忆。
为什么记忆必须是图
真正该问的问题是:哪些学校在用成绩功能?每所学校怎么部署?要改的文件还被哪些代码依赖?
这不是一次查表,而是一次遍历:从功能到使用它的学校,到每所学校的部署方式,到实现该功能的代码文件,到导入这些文件的依赖,再到代码的负责人。支持知识天然由关系构成——消息是问题的证据,问题影响功能,功能由代码实现,开发者拥有代码,学校以独立仓库部署。
在关系型表里,每一跳都是一次 join;在图里,关系本身就是数据。团队最终选了 FalkorDB,看中四件事:Cypher 查询让每个问题变成一个可读的模式;同一节点上的向量索引让“成绩存不进去”能找到标题为“提交时成绩丢失”的工单,不需要第二个数据库;内置图算法(连通分量、PageRank、最短路径)用于去重、排序和解释关联;单实例多图让每所学校拥有隔离的图空间。
他们的原则很明确:不要一个靠猜的聊天机器人,要一个有结构的记忆,以及一个只被允许基于该记忆回答的模型。
图数据库与属性图模型:图数据库以节点(实体)和边(关系)为基本存储单元,边本身可以携带属性,因此“谁拥有什么代码”“哪条消息对应哪个工单”这类关系与实体一样是一等数据。查询时沿边遍历的代价与路径长度相关,而不是像关系型数据库那样每跨一张表就做一次 join。Cypher 是面向属性图的声明式查询语言,用 (a)-[:REL]->(b) 这样的模式描述要匹配的结构,可读性接近自然语言,这也是文中强调“每个问题变成一个可读的模式”的原因。
向量索引与语义搜索:向量索引把文本经嵌入模型映射为高维向量,使语义相近的内容在向量空间中距离更近,从而支持“成绩存不进去”匹配到“提交时成绩丢失”这类字面不同但含义相近的检索。图数据库在同一节点上同时维护向量索引,意味着语义召回与关系遍历可以在一次查询中完成,不必再引入第二个专用向量库。
Loopkeeper 如何工作
这个项目叫 Loopkeeper,以 Ednovax Support 的形式上线,核心目标是让学校开启的每一个问题都不会被悄悄丢掉。它挂在学校的 WhatsApp 群上,老师那边什么都不用改。一条消息进来后经历五个步骤。
判断什么是工作。 学校群里大部分消息是问候、祷告和生日祝福。一个快速模型对每批消息做分诊,能识别尼日利亚英语和轻度皮钦语(比如“e no gree save”意思是存不进去)。闲聊只计数不入库,工作消息被分类为缺陷、提问、功能请求、承诺、解决、更新或决策,同时判断发送者的情绪是平静、担忧还是不满。
写之前先查记忆。 这是团队最得意的部分。模型读消息之前,FalkorDB 先把可能相关的工单递给它:发送者所在学校的未关闭工单、语义最接近的(向量搜索)、最近活跃的、以及几天内刚解决的。模型的第一条规则是“先复用再新建”。“Same here o”会变成对已有工单的确认;“It's working now, thank you”会关闭正确的工单,拿不准时标记给人工确认。
把工单连到一切。 新工单会关联到来源消息、学校、相关人员和产品功能。拥有该功能最多代码的开发者被自动指派。系统按语义搜索真实代码库,读取最接近的文件源码,记录哪些文件和函数最可能出问题以及原因。
记住承诺。 “我周四前把更新名单发过来”会变成一条带负责人和拉各斯时区截止时间的跟进项。到期时系统在群里温和提醒一次。决策(“模拟考改到周五”)同样被记录。
闭环。 一键把工单发到 GitHub,附带学校的原话、可能的代码和负责人。PR 合并后工单变为“可能已解决”,学校收到白话通知,老师在群里的确认才是最终关闭依据。
团队任何人可以用自然语言提问:“Henson 在等什么?”“成绩 bug 在代码哪里?”“这周谁答应了什么?”每个回答都来自具名 Cypher 查询,引用具体的 WhatsApp 消息,并高亮触及的节点。图不知道的,它会直说不知道。
分诊(triage)与小模型分工:分诊指在正式处理前对每条消息做快速分类和优先级判断,决定它是不是“工作”、属于哪一类、情绪如何。这里刻意用成本低、延迟小的模型(Claude Haiku)承担分诊与信息抽取,把需要权衡和裁决的环节留给更谨慎的流程,属于典型的“小模型做路由、大模型做判断”的分层设计。
嵌入模型 all-MiniLM-L6-v2:这是一个轻量的句子嵌入模型,输出 384 维向量,可在本地 CPU 上运行,无需调用外部 API。它让每条 WhatsApp 消息在入库时就能被向量化,代价是语义精度不如大型嵌入模型,因此文中才需要用图查询做候选并集来弥补单靠向量召回的不足。
技术底座:一校一图与182条具名查询
技术栈刻意保持精简:TypeScript 服务端(Hono)、React 仪表盘、Claude Haiku 负责分诊、抽取和问答,本地嵌入模型 all-MiniLM-L6-v2(384维),每条消息无需额外 API 就能向量化。FalkorDB 是唯一的数据库。
图的划分有三个层级:lk_hq 存 Ednovax 自身的产品、功能、代码、员工、SLA 策略和客户注册表;lk_client_<id> 每所学校一张图,存群组、人员、消息、工单、承诺、决策和代码引用副本;lk_hub 是员工转发多校消息的中心支持群。租户隔离不是靠容易忘记的 WHERE school_id = ?,而是查询一张学校的图时根本看不到别的数据。电话号码加盐哈希后才成为 ID,从不存储原文。
系统里有182条具名 Cypher 查询,仪表盘实时显示 FalkorDB 自己的执行耗时。几个关键查询:按语义找新消息可能关联的工单、按“谁拥有最多相关代码”自动指派、以及变更的爆炸半径——沿 IMPORTS 边向上遍历三层,找出所有依赖受影响文件的代码。
去重也用了图原生思路:新工单先通过向量索引与未关闭工单建立 SIMILAR_TO 关系,algo.WCC 把连通的候选聚成簇,模型只负责判断哪些真正相同,合并到最旧的工单并标记 DUPLICATE_OF。向量负责提议,图负责成形,模型只做裁决。
跨校问题发现遵循同样的逻辑但不泄露数据:每张工单在 HQ 留下一个抽象 Signal(功能、严重度、状态和嵌入向量,学校和联系人信息已抹除),相似的 Signal 聚成 Pattern,从而看到多所学校撞上同一个 bug,而每所学校只被告知自己的问题。如果 HQ 里出现消息原文或联系人姓名,构建会直接失败。
代码是读出来的,不是猜的。索引仓库时,导入关系变成 IMPORTS 边,CODEOWNERS 变成 OWNS,文件夹或 features.yaml 变成功能,每个文件定义的内容嵌入向量索引。源码本身从不存储——工单开启时,系统找到最接近的文件,从 GitHub 读取源码,模型只能说出它实际看到的文件和函数。
连通分量(WCC)与 PageRank:algo.WCC(弱连通分量)把图中通过边相连的节点划为同一簇,在这里用于把经由 SIMILAR_TO 关系连成一片的候选工单聚成组,避免逐对比较。PageRank 最初用于网页排序,在图数据库里可作为“重要性”打分,用于对工单、代码文件或人员排序。最短路径则用于解释两个实体之间是如何关联的。这些内置图算法让去重和排序不必把全部数据拉进应用层计算。
多租户与物理隔离:多租户指一套系统服务多个客户。常见做法是在同一张表里加 school_id 过滤,但一处遗漏就会泄露数据;FalkorDB 的单实例多图能力让每所学校独占一张图,查询时根本不存在跨校数据,等于把隔离从“纪律”变成“结构”。
如果这套系统早在那周存在
团队反复问自己:如果那周就有这个工作台,什么会不一样?
每所学校的“请更新我们的成绩格式”会变成各自图里的一张工单,关联到成绩功能和提出请求的那条消息。因为请求内容相似,它们的 Signal 在 HQ 聚成一个模式——看到的不是十几个聊天窗口,而是一个被多所学校请求的变更,以及具体是哪些学校。
学校不再是一个聊天里的名字,而是一个有形状的节点。HQ 知道每个客户如何部署:哪个仓库、哪个分支、是否有独立代码库。那所坑了他们的学校拥有别人没有的代码;一旦部署被登记、仓库被索引,那个批量打印函数就是另一个实现成绩功能的 CodeFile,带着它的导入关系和负责人。
工单会指向对该学校真正重要的代码。爆炸半径查询沿 IMPORTS 边走一遍,找出其他依赖成绩代码的地方。原本只装在一个人脑子里的例外,变成了图里的一条路径。修复不算“完成”,直到学校自己说完成。
踩过的坑
不是一切顺利。服务器 CPU 太旧,跑不了 transformers.js 导入时自动加载的 sharp 图像库,导致服务在 VPS 上崩溃而在笔记本上正常——他们只嵌入文本,最终用一个永远不会被调用的替身模块换掉了它。嵌入模型首次使用时才下载,导致部署后第一条 WhatsApp 消息要等模型下载完;现在每次部署都预下载并嵌入一句测试文本,校验返回384个数字,否则部署失败。
向量搜索单独用不够。“它现在能用了”和什么都接近,所以模型看到的候选是四条图查询的并集:发送者学校的未关闭工单、语义最近的、最近活跃的、最近解决的。这个混合才让“先复用再新建”真正跑通。而“谢谢,现在好了”是客服里最难的一句话——老师同时有两个未解决问题时,到底是哪个修好了?他们不再让模型假装知道,而是对每个可能工单记录低置信度解决,由人确认。
冷启动与部署校验:嵌入模型通常体积较大,首次调用时才下载会带来明显的首请求延迟,在消息驱动的系统里表现为“部署后第一条消息卡住”。解决办法是在构建或部署阶段预下载模型,并用一条已知输入做冒烟测试(smoke test),校验输出维度符合预期,否则让部署直接失败——这正是把“隐式依赖”变成“显式契约”的常见工程手段。
图是记忆,模型只是读者
这个项目参加了 WeMakeDevs × FalkorDB 的 Graph Hacks 黑客松,属于 Company Brain 赛道。该赛道的要求是:用一个图连接公司的人、团队、项目、文档和决策,理解公司如何运转。评测规则很直接——如果拿掉 FalkorDB 产品还能工作,图就不算数。在 Ednovax Support 里,拿掉 FalkorDB 什么都不剩:图决定模型看到哪些工单、读哪些代码、谁被指派、哪些工单重复、哪些学校共享问题、以及 Ask 被允许说什么。
作者总结的几条经验值得记下:让图引导模型,最大的质量跃升来自在模型读消息之前递上正确的候选工单;模型做判断,图决定它能判断什么;隐私作为结构比作为纪律更容易实现;用真实场景测试,测试套件跑在一个真实 FalkorDB 上,用一个虚构的三周 WhatsApp 群,里面满是问候、祷告和皮钦语。
目前 live desk 运行着一所真实学校 Henson(贝宁城),连接其真实 GitHub 仓库,四位老师由 AI 扮演。这原本是个黑客松作品,但团队说它不会停留在黑客松——之后它就是 Ednovax 服务学校的支持方式。“我们花了25万奈拉学到这一课,希望别人不用再花。”

相关推荐

零成本搭建AI超级员工系统:智能体自动工作流全解析
深度拆解B站演示的AI超级员工系统:Agent智能体编排、数字人口播、地图与企业获客、24小时智能客服及法务模块全覆盖。分析零成本开户背后的商业逻辑与合规风险,帮助创业者判断这套AI数字员工系统的真实投产比。

Chatbot与AI Agent的本质区别:从对话到执行的跨越
Chatbot和AI Agent有什么区别?本文基于Learn Tech频道的MCP系列讲解,用订机票、笔记摘要等实例说明:Chatbot只给答案,Agent通过思考-行动-观察循环真正完成任务,并拆解模型、工具、记忆、目标四大部件及MCP的桥梁作用,同时讨论Agent的适用边界与权限风险。

免费GitHub仓库:342小时AI课程,从数学到MCP服务器
一个免费GitHub仓库提供342小时AI课程,包含523节课、20个阶段,覆盖数学基础到Agent构建和MCP服务器。支持目标驱动学习,每节课产出可留存的prompt、skill或agent,已有近10万人开始学习。