互客鱼 返回主站

架构

热路径和延迟

热路径是访客消息 → 首个 token 管道。它有严格的 1 秒 p95 契约——超过这个时间,感知的"这东西还活着吗?"的紧张感就会破裂。 所有不必在热路径上发生的事情都被推离它。

延迟预算

首个 token 在 p95 的目标分解:

阶段p95 目标
HTTP 接收 + 身份验证30 ms
精选短路检查5 ms
嵌入查询120 ms
向量搜索(ANN)80 ms
重新排名120 ms
提示词组装10 ms
LLM 到首个 token 的时间500 ms
总计~865 ms

其余部分的余量 < 150ms。首个 token 之后的任何内容都会增量流式传输—— 完整响应时间由 token 速率限制,而不是预算。

硬性规则

这些由代码审查和测试强制执行:

  1. 热路径上无 DB 写入。持久化在流结束后异步进行。
  2. 无同步 webhook。出站 webhook 作为队列作业分发。
  3. 无重试。如果提供商在流中间失败,用户看到优雅的错误,小部件在客户端自动重试。服务器不循环。
  4. 无 N+1 查询。所有读取都是批量的。最近的历史来自 Redis(conv:{id}:history),而不是 Postgres。
  5. 每轮一次 LLM 调用。没有多步骤智能体推理扇出到多个模型调用。

什么不在热路径上

以下所有内容都在流完成后之后分发。它们都不会阻塞访客:

  • PersistTurnJob ——保存用户 + 助手消息。
  • IncrementUsageJob ——增加工作区的每月对话计数器。
  • DetectGapJob ——聚类低置信度问题用于缺口报告。
  • DispatchWebhookJob ——分发到订阅的客户端点(工作流侧;当访客提交潜在客户表单时,lead-captured webhook 通过 SignedDispatcher 内联触发)。
  • AutoIndexPageVisit ——在 /init 时触发的同步服务,当启用自动索引时为访问的 URL 排队 CrawlPageJob。不是热路径作业,但值得了解自动索引运行的位置。

缓存

两个缓存保持热路径紧凑:

  • 检索缓存 ——Redis,rag:retrieve:{agentId}:{hash(query|currentPageUrl)},30 分钟 TTL。同一页面上的相同问题命中缓存。当知识源更改时失效。
  • 对话历史缓存 ——Redis,conv:{convId}:history,2 小时 TTL,上限为 12 条消息(6 轮)。每轮都从这里读取而不是 Postgres。

流式机制

SSE 非常简单——保持活动的 HTTP,每个 token 写入 data: {...}\n\n, 刷新。小部件通过 EventSource 读取(或在没有 POST 上的 EventSource 的旧浏览器中通过 fetch + reader)。

关键是,SSE 响应在任何 RAG 工作运行之前构建。我们在收到请求时立即 开始写入标头,以便我们前面的任何代理(Cloudflare、负载均衡器)尽早承诺流式传输。 当 token 到达时,连接已经打开。

span 的位置

OpenTelemetry span 包装每个阶段:

  • widget.message.receive
  • rag.curated.match
  • rag.embed
  • rag.vector.search
  • rag.rerank
  • rag.prompt.assemble
  • rag.llm.first_token
  • rag.llm.stream
  • rag.persist.async

Honeycomb / Grafana 显示每个的 p95。当预算破裂时,span 热力图通常直接指向违规者。

故障模式

故障行为
LLM 提供商在流中间 5xx流发出 error 事件。小部件自动重试最多 3 次。
向量存储无法访问管道返回没有基础的问题。置信度为 0.3 → low_confidence 标志 → “我不知道”答案。
嵌入调用超时相同——在没有基础的情况下继续,标记 low_confidence。
配额超出/init 捕获,永远不会到达消息。返回 429。

原则:访客总是得到响应,即使是"我不确定"。允许智能体无知; 不允许它静默破裂。