跳到主要内容
文章
AI Products3 分钟阅读

AI CRM 架构复盘:把 LLM 从写入路径挪到读取路径

保存一条跟进记录要等 3 秒,是个架构问题不是性能问题。这篇讲清楚为什么 LLM 不该出现在写入路径上,以及异步补摘要的具体实现。

一句话结论

LLM 出现在写入路径上,就是在让用户替模型的速度买单。

本文目录 · 5

用户反馈很简单:「记一条跟进要等好几秒。」

我第一反应是去优化模型调用——换更快的模型、压缩 prompt、加缓存。 做到最后从 3.2 秒降到 2.4 秒,仍然卡。

问题不在模型速度,在架构把模型放错了位置

原来的设计#

text
用户点保存

   ├─► 写入数据库
   ├─► 调用 LLM 生成摘要 ──── 2.8s ────┐
   ├─► 写入摘要                          │ 用户在这里等
   └─► 返回成功 ────────────────────────┘

用户等的是模型生成摘要,而摘要其实不是他此刻需要的东西。 他需要的是「记录已经保存了」。

这是一个典型的把非关键路径放进了关键路径

改法:读写分离#

text
写入路径(用户等待)

   └─► 写入记录,标记 summary_status = 'pending'   ──► 200ms 返回
 
读取路径(后台异步)

   └─► 队列消费 → 调用 LLM → 回填摘要
                                └─► summary_status = 'ready'

用户点保存后立刻看到记录出现,摘要位置显示一个极轻的占位状态, 几百毫秒后自己变成摘要文本。

保存耗时从 3.2s → 180ms

ts
// 写入路径:只做必须同步完成的事
export async function createActivity(input: NewActivity) {
  const activity = await db.activity.create({
    data: { ...input, summaryStatus: "pending", summary: null },
  });
 
  // 投递到队列,不等待
  await queue.enqueue("summarize-activity", { activityId: activity.id });
 
  return activity;
}
ts
// 读取路径:后台消费,失败可重试
export async function summarizeActivity({ activityId }: JobPayload) {
  const activity = await db.activity.findUnique({ where: { id: activityId } });
  if (!activity || activity.summaryStatus === "ready") return;
 
  const summary = await llm.summarize(activity.rawContent, {
    maxTokens: 120,
  });
 
  await db.activity.update({
    where: { id: activityId },
    data: { summary, summaryStatus: "ready" },
  });
}

这个改动带来的三个额外收益#

1. 可以换更贵的模型

既然用户不等,就没什么理由用便宜的小模型了。 我换成了能力更强的模型做摘要,质量明显提升,用户感知不到任何延迟。

2. 失败不再阻塞用户

原来模型调用超时,用户看到的是「保存失败」——但数据其实已经写进去了。 现在模型失败只影响摘要,记录本身永远保存成功。

ts
// 失败重试 + 最终降级,用户永远不会因为摘要失败而丢数据
retry: { attempts: 3, backoff: "exponential" },
onFinalFailure: async ({ activityId }) => {
  // 三次都失败就标记为不可用,前端展示原文
  await db.activity.update({
    where: { id: activityId },
    data: { summaryStatus: "unavailable" },
  });
},

3. 可以做批量优化

异步之后,同一客户的连续几条记录可以合并成一次模型调用。 成本降了大概 40%,摘要质量反而更好——因为模型能看到完整的上下文。

一个推广开来的原则#

改完之后我回头检查了整个项目,把所有 LLM 调用按同一个标准过了一遍:

问:用户此刻需要这个结果才能继续吗?

  • 需要 → 留在同步路径,但要考虑能不能用更小的模型
  • 不需要 → 挪到异步,让用户先走

按这个标准重新分类:

功能原位置新位置理由
跟进摘要同步异步用户不需要立刻看到
下一步动作建议同步异步同上
邮件意图分类同步同步决定数据存到哪个客户下,必须同步
时间线排序纯代码不需要模型,用规则排序更稳定

最后一行是另一个收获:有些功能压根不该用模型

时间线排序我一开始用模型做「重要性排序」,结果不稳定—— 同一批数据两次排序结果不一样。改成规则(最近优先 + 阶段权重)之后, 确定性有了,用户也不再抱怨「顺序老是变」。

如果重来一次#

我会在写第一行代码之前先画一条线:

text
┌─────────────────────────────────────┐
│  同步路径:用户等待,只放必须的事    │
├─────────────────────────────────────┤
│  异步路径:用户不等,可以慢、可以重试 │
├─────────────────────────────────────┤
│  不需要模型:用代码更准更快的部分     │
└─────────────────────────────────────┘

然后强制每个功能先归位,再动手写。

AI 产品最常见的性能问题,不是模型太慢,是架构把模型放在了不该放的地方。


项目状态与架构说明在 AI CRM 项目页