AI CRM 架构复盘:把 LLM 从写入路径挪到读取路径
保存一条跟进记录要等 3 秒,是个架构问题不是性能问题。这篇讲清楚为什么 LLM 不该出现在写入路径上,以及异步补摘要的具体实现。
一句话结论
LLM 出现在写入路径上,就是在让用户替模型的速度买单。
本文目录 · 5 节
用户反馈很简单:「记一条跟进要等好几秒。」
我第一反应是去优化模型调用——换更快的模型、压缩 prompt、加缓存。 做到最后从 3.2 秒降到 2.4 秒,仍然卡。
问题不在模型速度,在架构把模型放错了位置。
原来的设计#
用户点保存
│
├─► 写入数据库
├─► 调用 LLM 生成摘要 ──── 2.8s ────┐
├─► 写入摘要 │ 用户在这里等
└─► 返回成功 ────────────────────────┘用户等的是模型生成摘要,而摘要其实不是他此刻需要的东西。 他需要的是「记录已经保存了」。
这是一个典型的把非关键路径放进了关键路径。
改法:读写分离#
写入路径(用户等待)
│
└─► 写入记录,标记 summary_status = 'pending' ──► 200ms 返回
读取路径(后台异步)
│
└─► 队列消费 → 调用 LLM → 回填摘要
└─► summary_status = 'ready'用户点保存后立刻看到记录出现,摘要位置显示一个极轻的占位状态, 几百毫秒后自己变成摘要文本。
保存耗时从 3.2s → 180ms。
// 写入路径:只做必须同步完成的事
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;
}// 读取路径:后台消费,失败可重试
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. 失败不再阻塞用户
原来模型调用超时,用户看到的是「保存失败」——但数据其实已经写进去了。 现在模型失败只影响摘要,记录本身永远保存成功。
// 失败重试 + 最终降级,用户永远不会因为摘要失败而丢数据
retry: { attempts: 3, backoff: "exponential" },
onFinalFailure: async ({ activityId }) => {
// 三次都失败就标记为不可用,前端展示原文
await db.activity.update({
where: { id: activityId },
data: { summaryStatus: "unavailable" },
});
},3. 可以做批量优化
异步之后,同一客户的连续几条记录可以合并成一次模型调用。 成本降了大概 40%,摘要质量反而更好——因为模型能看到完整的上下文。
一个推广开来的原则#
改完之后我回头检查了整个项目,把所有 LLM 调用按同一个标准过了一遍:
问:用户此刻需要这个结果才能继续吗?
- 需要 → 留在同步路径,但要考虑能不能用更小的模型
- 不需要 → 挪到异步,让用户先走
按这个标准重新分类:
| 功能 | 原位置 | 新位置 | 理由 |
|---|---|---|---|
| 跟进摘要 | 同步 | 异步 | 用户不需要立刻看到 |
| 下一步动作建议 | 同步 | 异步 | 同上 |
| 邮件意图分类 | 同步 | 同步 | 决定数据存到哪个客户下,必须同步 |
| 时间线排序 | — | 纯代码 | 不需要模型,用规则排序更稳定 |
最后一行是另一个收获:有些功能压根不该用模型。
时间线排序我一开始用模型做「重要性排序」,结果不稳定—— 同一批数据两次排序结果不一样。改成规则(最近优先 + 阶段权重)之后, 确定性有了,用户也不再抱怨「顺序老是变」。
如果重来一次#
我会在写第一行代码之前先画一条线:
┌─────────────────────────────────────┐
│ 同步路径:用户等待,只放必须的事 │
├─────────────────────────────────────┤
│ 异步路径:用户不等,可以慢、可以重试 │
├─────────────────────────────────────┤
│ 不需要模型:用代码更准更快的部分 │
└─────────────────────────────────────┘然后强制每个功能先归位,再动手写。
AI 产品最常见的性能问题,不是模型太慢,是架构把模型放在了不该放的地方。
项目状态与架构说明在 AI CRM 项目页。
更新的一篇
AI Comic Studio 开发日志:把角色一致性从 40% 提到 78%
跨页角色一致性是 AI 漫画最难的一环。这篇记录我试过的三种方案,为什么前两种失败,以及结构化角色卡为什么有效。
更早的一篇
已经是最后一篇了
写于 2026 年 8 月 5 日