ATRULE 技术博客ATRULE 技术博客
首页
博客
文档
关于
首页
博客
文档
关于
  • 技术文章

    • VuePress 2.x 完全指南
    • Gemini3 & GPT5 & DeepSeek3:AI时代程序员的身份转变
    • Agent 架构选型指南:单智能体 vs 多智能体
    • Agent 工程师转型学习路线图 (Full-Stack to Agent Engineer)
    • Agent 工程师学习笔记
    • 📖 Agent 系统架构与工程实践面试速记卡片
    • Agent 流式对话前端踩坑记录
    • RAG 检索工程问题记录
    • Agent 后端问题记录(Koa / MongoDB)
    • LLM 调用与 Agent 编排问题记录
    • Electron 桌面端问题排查记录
    • AI Agent 全栈开发问题排查记录
    • ES2026 与 TypeScript 进阶问题记录
    • 浏览器渲染与 V8 内存问题记录
    • React 19 与 Vue 4 新内核问题记录
    • 前端工程化与微前端问题记录
    • 性能、安全与可观测性问题记录
    • 前端架构实战问题记录
  • 项目实战

    • Node.js + Koa 抖音直播弹幕Agent 五大核心模块(面试项目完整版,TS技术栈)★★★★★
    • Poiclaw 项目蓝图:自主编程实体
    • AI旅行助手

Agent 后端问题记录(Koa / MongoDB)

Agent 服务和普通 Web 服务的本质差异:长连接、流式响应、长耗时任务、会话状态膨胀。

问题一:Koa SSE 响应被 gzip 中间件缓冲,流式变批量

现象:Koa 服务本地 SSE 流式正常,加了 compress() 中间件后变成攒到最后一次性输出。

原因:gzip 压缩需要凑满缓冲区才输出,SSE 这种持续小数据量的响应会被一直攒着。

解决:compress 中间件里对 text/event-stream 跳过压缩:

const compress = require('koa-compress')

app.use(compress({
  filter(content_type) {
    return !/text\/event-stream/i.test(content_type) // SSE 不压缩
  },
}))

配套:SSE 响应还要手动设置头并绕过 Koa 的 body 缓冲:

async function sseHandler(ctx) {
  ctx.set({
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'X-Accel-Buffering': 'no', // 告诉 Nginx 不要缓冲
  })
  ctx.res.write('data: start\n\n')

  for await (const token of llmStream()) {
    if (ctx.res.writableEnded) break // 客户端断开检测
    ctx.res.write(`data: ${JSON.stringify({ token })}\n\n`)
  }
  ctx.res.end()
}

问题二:500 轮会话把 MongoDB 文档撑到 10MB

现象:长会话 Agent 越用越慢,查一次会话要几百毫秒,监控显示文档已经 10MB+。

原因:把所有消息塞进一个 session 文档。MongoDB 单文档 16MB 硬限制,而且每次追加消息都要 rewrite 整个文档——读也读得多,写也写得多。

解决:分层存储,sessions 存元数据,messages 每条一个文档:

// sessions:元信息 + 持久记忆 + 已折叠摘要(文档 <100KB)
{
  session_id: "sess_abc",
  persistent_memory: { user_profile: {...}, key_facts: [...] },
  conversation_summaries: [          // 旧轮次折叠成摘要
    { summary: "...", source_turn_range: [1, 10] }
  ],
  last_active_at: ISODate(),         // TTL 用这个字段
}

// messages:单条消息独立文档
{
  session_id: "sess_abc",
  turn_index: 21,
  role: "assistant",
  content: "北京今天晴天 25°C",
  status: "done",                    // streaming/done/stopped/error
}

// 读最新 20 条:只查 messages,走索引,毫秒级
db.messages.find({ session_id })
  .sort({ turn_index: -1 })
  .limit(20)

旧消息折叠成摘要后归档到对象存储,messages 集合只保留热数据(最近 50 轮)。

问题三:TTL 索引把用户正在用的会话删了

现象:给会话加了 7 天过期(基于 created_at),用户第 7 天打开会话,正在聊天时文档被删了。

原因:TTL 索引的过期时间 = 索引字段的值 + expireAfterSeconds,created_at 是创建时的固定值,文档更新不会延后过期。

解决:TTL 基于 last_active_at 字段,每次访问更新它:

db.sessions.createIndex(
  { last_active_at: 1 },
  { expireAfterSeconds: 7 * 24 * 3600 }
)

// 每次用户操作会话时
db.sessions.updateOne(
  { session_id },
  { $set: { last_active_at: new Date() } }
)

注意:TTL 删除由后台线程每 60s 扫一次,过期文档最多存活 60s+,做逻辑要容忍这个延迟。

问题四:多 Agent 并发写会话状态,互相覆盖

现象:3 个子 Agent 并行执行,各自往 session 写结论,后写的把先写的覆盖了。

原因:经典的 read-modify-write 竞态。Agent 场景不需要强事务(可以重试),但需要保证不丢更新。

解决:乐观锁 + 版本号,冲突就重试:

async function updateWithOptimisticLock(sessionId, updateFn, maxRetries = 5) {
  for (let i = 0; i < maxRetries; i++) {
    const session = await Session.findOne({ session_id: sessionId })
    const result = updateFn(session) // 基于最新版本做修改

    const updated = await Session.updateOne(
      { session_id: sessionId, version: session.version }, // 乐观锁条件
      { $set: { ...result, version: session.version + 1 } }
    )
    if (updated.modifiedCount === 1) return updated // 写成功

    await sleep(2 ** i * 100) // 指数退避后重试
  }
  throw new Error('乐观锁重试耗尽,疑似写入饥饿')
}

为什么不用 MongoDB 多文档事务:Agent 一次执行 5-10s,事务持有锁的时间太长,会阻塞整个库。Agent 可重试的特性让乐观锁是更优解。

问题五:pm2 集群下 SSE 长连接找不到状态

现象:pm2 开 4 个 worker,用户的 SSE 连接在 worker 1,后续 HTTP 请求被负载均衡打到 worker 3,会话状态读不到。

原因:SSE 连接和内存态状态都绑定在单个 worker 进程里,普通负载均衡不保证会话亲和。

解决:

  • Nginx 层做 upstream hash,同一会话固定打到同一 worker:
upstream agent_backend {
  hash $http_x_session_id consistent;  # 按会话 ID 哈希
  server 127.0.0.1:3001;
  server 127.0.0.1:3002;
  server 127.0.0.1:3003;
  server 127.0.0.1:3004;
}
  • 状态外部化:会话状态放 Redis,跨 worker 可读;SSE 推送通过 Redis Pub/Sub 广播,哪个 worker 都能发。

小结

问题一句话方案
SSE 被 gzip 缓冲compress filter 跳过 event-stream
会话文档膨胀sessions + messages 分层存储
TTL 提前删数据TTL 基于 last_active_at 字段
并发写冲突乐观锁 version + 指数退避重试
集群会话丢失Nginx hash 亲和 + 状态外部化到 Redis
最后更新: 2026/9/5 17:19
Prev
RAG 检索工程问题记录
Next
LLM 调用与 Agent 编排问题记录