知识入库从 11 分钟到 3 秒:一次词库回填的批量化改造
客服机器人的知识问答效果,一大半取决于知识库覆盖度。我们把客户积累的话术词条做成了一键导入:Excel 进来,清洗、入库、生效,全程自动。功能上线那天没人觉得慢,直到一位词条量偏大的客户登录后,界面卡了十来分钟——排查到最后,问题不在算法,在一个典型的 N+1 式请求模式上。本文复盘这次从 108 个请求压缩到 4 个的完整过程。
现象:登录后的十分钟静默期
系统的词条回填逻辑是:登录成功后,比对本地词库与云端词库的差异,把本地新增或变更的条目逐条同步上云,云端再负责向量化与检索服务更新。
小数据量下这条链路毫无问题:几十条词条两三个请求就完事,用户无感。问题出在词条上千的客户身上。登录后的十分钟里,界面没有卡死,进度提示也在动,但问答能力的升级迟迟不生效。客户端日志显示回填还在进行,每一轮都在一条条地 POST。
把日志里的请求数一数:108 个。词条规模约百级,加上分批与校验,每个条目都要经历"提交、等对端处理、确认"的完整往返。单次往返两到六秒,串行乘下来就是十分钟量级。
排查:先排除嫌疑大的对象
遇到"登录慢",直觉方向是登录鉴权链路和网络环境。我们先把这两条排除了:鉴权本身的耗时在日志里是毫秒级;同一账号换网络环境,慢的时长几乎不变——耗时和条数成正比,而不是和带宽成反比。这说明瓶颈在服务端处理模式,不在传输。
顺着条数往下挖,找到了真正的放大环节:云端收到词条后,会同步调用对端的向量检索服务做更新。也就是说,链路上每一跳都在等下一跳:客户端等云端,云端等向量服务。逐条 POST 的模式下,这条串行等待被乘上了词条数量。
对端服务本身没有故障,单次处理的耗时也正常。它只是被我们的调用方式用错了——一个为"批量索引"设计的下游,被喂成了"逐条点射"。
这里值得多说一句:N+1 这个坑在数据库查询、ORM、HTTP 编排里反复出现,形态不同、本质一致——把与集合规模成正比的往返,留在了用户必然等待的关键路径上。识别它的信号也很统一:耗时与数据条数近似线性、与网络质量弱相关。对上了这个信号,基本可以直接锁定调用模式问题。
根因定位之后:先想清楚为什么不能只是"调快一点"
确认根因后,有人提议给回填请求加并发:逐条不变,同时发十条。我们否掉了,理由有三:
- 对端向量服务按请求计量计费,并发只是把十分钟压成一分钟,账单上的请求数一分不少。规模再涨十倍,问题原样回来。
- 并发写同一批词条会引入乱序与竞争,本地和云端的最终一致性更难保证。
- 请求数与数据规模解耦才是治本。并发是缓解,批量才是解法——目标应该是:不管词条是一百条还是一万条,请求数恒定在个位数。
改造方案:把逐条点射合并成批量提交
拆成三层来做。
接口层。 词库比对结束后,把所有差异条目收集成一个增量集合,调用云端的批量提交接口:一次 HTTP 请求,body 里装整个增量。服务端落库即返回,向量化改为服务端异步批量下发——对端调用的次数从"每词条一次"变成"每批次一次",计费与压力同步下降一个数量级。
客户端层。 收集差异、分片打包、失败重试。批量请求失败的代价高(一车全退),所以每个分片携带校验摘要,服务端按条目返回逐条结果,客户端只对失败的条目做精准重试,不重放整批。批量的副作用是失败面变大,所以幂等要前置设计:每条目带内容摘要作幂等键,服务端遇重复键直接跳过。重试因此变得安全——整个请求重放也不会产生脏数据。逐条时代这条要求形同虚设,批量时代它是地基。
进度层。 改造后登录不再被十分钟回填拖住:回填挪到后台执行,前端用同步进度条把当前阶段(比对、提交、生效)显式化,用户可以一边用一边等。回填期间产生的问答仍走本地词库,不阻塞业务。
灰度上线前我们做了一次双写对照:同一批增量同时走旧路径(逐条)与新路径(批量),事后拉两边的落库结果做集合比对,确认条目零丢失、内容零差异,才把旧路径下线。批量改造动的是钱袋子路径上的一环,对照成本很低,值得花。
踩到的坑:一个 TypeScript 闭包引出的重复提交
方案本身不复杂,真正花时间的是一次线上复测时发现的"重复提交":同一批词条被提交了两次,第二次还是旧内容。
代码检查下来,问题出在一个很不起眼的重构上。原来有一段逐条处理的逻辑被搬进一个循环闭包,搬运时把一个共享的缓冲变量留在了闭包外面。循环体内对这个变量的读写,全部指向同一块内存:上一轮的残留数据混进了下一轮的 payload,触发服务端按幂等键去重后,又因为内容不一致产生告警——看起来像"重复提交",其实是"脏数据混批"。
教训很直白:批量化改造时,逐条路径的变量不能原样搬进循环闭包,每一批的状态必须每轮新建。 这类 bug 单测很容易漏,因为单测通常只造"一批"数据,它只在"多批 + 跨批复用缓冲"的形态下显形。所以我们给批量路径加了针对性测试:三批连续提交,断言每批 payload 只含本批条目。测试补上后,同型问题在后续迭代里再没出现过。
效果与边界
实测:词条回填从十分钟量级降到秒级,请求数从 108 降到个位数;对端调用量下降一个数量级,计费同步下降。更重要的是这条链路的耗时不再随词条规模线性增长——客户数据涨十倍,登录体验不变。
两点边界要说清楚。批量不等于无限大:单次请求装几千条词条会让服务端处理压力和失败代价都变大,所以按固定上限分片,请求数仍是常数级(百条量级一两个分片)。批量的失败语义也比单条复杂:部分成功是常态,接口必须按条目返回结果,客户端按条目重试,否则一次网络抖动就会重放大批数据。这两条是批量接口从"快"走向"稳"的必要条件。
复盘
回头看,这次优化的难点从来不在写代码——批量接口的实现在总工时里占比很小。值得记下的反而是三件小事:一是"耗时与条数成正比、与网络无关"这个信号值得形成肌肉记忆,它能让你在十分钟里锁定别人排查一天的方向;二是加并发这种"看起来在解决问题"的方案,要先问它把问题推远了还是消除了——我们的账单替我们回答了;三是重构搬运代码时,变量的作用域边界比变量的名字更重要。慢不可怕,可怕的是慢得有规律却没人在找规律。
参考文章
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)