0
0

给前端 Agent 任务加缓存边界时,我会先把 queryKey 和 runId 拆开

最近在做一个前端 AI 工作台,Agent 任务会读取会话详情、工具清单、RAG 命中片段,也会提交草稿、触发检索和刷新结果区。早期我把这些请求都交给 TanStack Query 管,写起来很顺手,但很快遇到串任务:重试后旧结果回写,取消后页面还在 loading,mutation 成功后整个工作台一起重刷。问题表面看是缓存策略,实际牵着执行身份、事件顺序和用户正在看的上下文。

前端 Agent 从用户意图到缓存、事件账本、取消和失效刷新的流程示意图

原创示意图:把用户意图、queryKey 缓存、run ledger、可取消请求和 mutation 失效拆成五个可排查环节。 来源:Codex image generation

问题背景

TanStack Query 文档把 queryKey 放在缓存模型的核心位置,顶层需要是数组,并且要能唯一描述数据。文档也提醒,query function 依赖的变量应进入 queryKey。放到 Agent 场景里,conversationIdtoolNamefilters 描述要取哪份服务端状态;runIdstepIdattemptId 描述一次执行过程。两类标识混在一起,缓存就会变得很难解释。我的判断标准很简单:能被多个任务复用的数据进 queryKey,只属于一次运行的线索进 ledger。

踩坑和关键难点

第一个坑是把 runId 塞进所有 queryKey。每次重试都会生成新 runId,侧边栏、工具清单和历史消息也跟着反复请求。第二个坑是取消只停网络。TanStack Query 会给 queryFn 提供 AbortSignal,fetch 能接收 signal,但 UI 层仍要处理已经进入本地队列的 token 和工具结果。第三个坑是失效范围过大。Agent 改一个草稿字段后,如果直接刷新整个 conversation,用户正在看的工具输出会抖动,滚动位置也可能丢。还有一个隐蔽问题是旧回调迟到,接口请求已经结束,组件还保留闭包里的旧 runId,最后把新任务的面板覆盖掉。

解决思路

我把状态拆成三层。第一层是 server state,由 TanStack Query 管理,例如会话详情、工具目录、知识库列表和权限配置。第二层是 run ledger,只追加当前 Agent 执行事件,例如 started、token、tool_call、tool_result、cancelled、failed。第三层是 view projection,把服务端状态和执行事件合成用户当前看到的面板。

这样排障时可以快速定位:queryKey 决定哪份数据被缓存,runId 决定哪次任务在推进,projection 决定当前界面如何阅读这些事实。缓存失效影响服务端状态,任务取消影响 run ledger,界面折叠和筛选只影响 projection。三层之间只传稳定引用,少传整块对象,能减少重复渲染,也方便把日志打到同一条 run 链路上。

关键步骤

第一步是建立 queryKey 工厂。把 conversation.detail(conversationId)rag.snippets(conversationId, filters)tools.catalog(workspaceId) 这类稳定资源集中定义,避免组件里随手拼数组。对象参数进入 key 前做排序和空值清理,减少同义 key。

第二步是让 queryFn 明确支持取消。所有 fetch 都接收 TanStack Query 传入的 signal,封装层把 requestId 和 runId 写入调试日志。请求被取消后,本地 ledger 仍写入 run_cancelled 终态事件,防止页面停在处理中。

第三步是收窄 mutation 失效。更新草稿只失效草稿 query,提交工具配置只失效工具配置 query,完成一次 Agent run 时再按返回的 affectedResourceIds 做精准刷新。能从 mutation response 直接更新的轻量字段,优先写回缓存。

第四步是给 projection 加版本。每次 run ledger 追加事件时生成 projectionVersion,UI 只接受当前 conversationId 和当前 runId 的结果。旧请求回调进入时,如果版本不匹配,就写入丢弃日志,避免旧结果覆盖新任务。

第五步是固定观测字段。每条日志至少包含 queryKey 摘要、runId、requestId、mutationName、invalidatedKeys 和 projectionVersion。线上出现刷新抖动时,可以先看是哪一层触发,再决定调 staleTime、收窄 invalidate,还是修正事件投影。

可复用经验

前端 Agent 的缓存稳定性,关键是拆清“要取哪份数据”和“哪次任务正在跑”。queryKey 描述可复用的服务端状态,runId 描述一次执行,mutation 只刷新受影响资源,取消要同时结束网络和本地事件流。把这几条线拆开后,重试、取消、刷新、回看和排障都会稳定很多。这个拆法也适合 RAG 结果页、工具调用审计台和 Electron 桌面任务面板。

主要来源

TanStack Query Keys

TanStack Query Cancellation

TanStack Invalidations from Mutations

TanStack Important Defaults

MDN AbortController

评论