Agent 的 UI 才是前端主场:Chat 只是 fallback

1. 坑

打开 Cursor、Copilot Chat、ChatGPT、Claude Code、Windsurf,一屏截图裁掉 logo,你分不出谁是谁——右边一列消息气泡,底下一个多行输入框,左上角三条杠折叠出一个历史列表。Agent 时代的前端好像被压扁成了同一张模板。

我一度以为这是宿命。模型输出不确定、任务开放式、人机对话本来就适合聊天窗——那还能长什么样?直到有一天我数了数自己在 Claude Code 里花时间最长的界面,发现根本不是那块聊天区,是 /permissions 里那张工具权限清单、/mcp 那页 server 状态、/memory 里那几段项目规则、是每次 dispatch 之前跳出来的那道权限弹窗。真正让我留下来的从来不是气泡本身,是气泡之外那圈”看得见、管得住”的东西。

把 Agent 做成 chatbot 是懒惰。类比放在 Web 的历史上更清楚:早期 Web 就一句话——“页面是文档”,浏览器 = 文档阅读器,前端 = 排版师。后来 Gmail、Google Maps 出来把这个共识打碎:“页面是应用”,前端从此才叫前端。今天 Agent 的所有主流产品都还停在”页面是文档”的等价物——“Agent 是聊天窗”。文档没错,Gmail 也没错,但停在文档不走,那就是错。

这是《从 useEffect 到 Agent Loop》番外 B。主线十篇讲的是 Agent 引擎——loop、context、tools、prompt、memory、error、permission、subagent、evals、production。番外这几篇讲外围:A 讲 MCP 的供应链旧账,B 讲这个——Agent 产品的前端主场根本不在聊天窗

2. 桥

一句话:

Chat 是让人跟”不确定输出”建立信任的最低成本 UI;信任一旦建立、任务一旦清晰,交互就该被更专门的 UI 替代。

Chat 是 fallback,不是终点。fallback 不是贬义——HTML 里的 <noscript> 也不是贬义,它是 JS 挂了之后你还能看到点东西的保底。Chat 之于 Agent 就是这个位置:模型没法保证输出、用户没法提前描述需求,那就先给一个通用到极致的接口——你说人话,我回人话,先把这一轮闭环。这是最低成本的信任协议。

问题在于,很多产品把 fallback 当成了主界面。JS 没挂你也让人一直读 <noscript> 里的静态提示,那当然反人类。

前端过去二十年做了同一件事:把”通用容器”拆成”专门组件”。React 把 DOM 抽象成组件树,不是因为 DOM 不够用,是因为”我要一个能复用、能推理、能测试的原子”。Agent 现在也到了这一步——聊天窗不够用,需要把交互抽象成三类专门组件:

        通用容器                      专门组件
Web:    <div contenteditable>   →    <Editor> <Form> <Table> ...
Agent:  <ChatBubbleList>        →    <ToolPanel> <TracePanel> <DecisionGate>

右边那三类组件的抽象不是我发明的,业界都在往这个方向走。但其中 A/B/C gate 这种具体形态,是我自己在博客发布流程里长出来的——下面一节拆开讲。

3. 真

Agent 前端的三大主场:工具面板、状态观测层、决策 gate。分别对应三个前端老词:devtools、fiber tree、git rebase -i

3.1 工具面板——可控性

工具面板管一件事:让用户看见 Agent 有哪些手,能砍掉哪只,这一步它伸的是哪只

聊天气泡对工具的处理是”要么隐藏、要么灌进对话”。前者你不知道它到底能干什么,后者你要在几百行文字里找它到底调了哪个 tool——两种都反人性。

正解是把工具做成一个持久 UI 元素——像浏览器的 devtools 面板,常驻、可折叠、可交互。Claude Code 的 /permissions(工具的 allow/ask/deny 清单)加 /mcp(每个 server 的连接状态)拼起来就是一个 CLI 版。把两页合成一张面板,大概是这个形态:

Tools & servers (12 enabled, 3 disabled):

  [x] Read           read files (auto-allow)
  [x] Edit           edit files (ask)
  [x] Bash           run shell (ask, per-command allowlist)
  [x] WebFetch       fetch URL (ask)
  [ ] WebSearch      search web (disabled by user)
  [ ] mcp__gmail__*  Gmail MCP (disabled: no auth)
  ...

三个信息挤在一个视图里:认识谁(有哪些工具)、信任谁(allow/ask/deny)、关掉谁(disabled)。浏览器早就造过一个几乎一模一样的东西——地址栏那把小锁:最早只说”连接加密了”,后来点开长成一张站点权限清单,告诉你这站要了几个权限、你放行了哪几个、随时可以收回。工具面板就是 Agent 的那把小锁。

从 CLI 挪到图形前端,能做的事更多:hover 一个工具高亮它这一轮被调用的次数、点它拉开这几次调用的参数 diff、右键”暂时禁用直到本任务结束”。这些没一样在聊天气泡里做得出来。

3.2 状态观测层——可解释性

Agent 每一步的中间态:这一轮吃了多少 token、context 剩多少 headroom、上一步 tool 返回了什么、下一步准备干什么、有没有 subagent 分身在跑——这些信息你在纯聊天窗里看不见,只能通过模型自己碎碎念”我先读一下文件”来猜。

这一层的类比是 React devtools 的组件树 + Chrome 的 Performance timeline。前者是结构:一个 Agent 循环长成什么形状,是主 agent 独跑,还是 fork 出了三个 subagent,还是嵌套调了 MCP。后者是时间:每一步花了多久、卡在哪一步、context 曲线什么时候陡。

我自己 tiny-agent 里最信任的一个信号,是每轮 stderr 末尾那行 [turn · in=6120 out=95]——in 是 context 输入 token、out 是模型输出 token,v0.2 就写进来了(blog02 给 blog01 那 32 行加的第一块观测器官);Claude Code 里对应的是 /context、/cost 和 statusline 上的 token 计数。它长得像日志,其实是最小可用的状态观测层——两个数字告诉我账单曲线和 context 头顶还剩多少空间。等哪天做成图形前端,就是一条会实时爬升的曲线、一个像 CPU 温度计的 headroom 计。

subagent 那层更需要观测。blog213 讲过 subagent 起来之后主循环看不见它内部发生什么,只能等它 return——如果不给一个树状可视化,你不知道它是死循环了、还是正在稳步推进、还是已经翻车但还没退出。fiber tree 那种”一眼看清整棵组件树 + 每个节点当前 state”的界面,对 Agent 是刚需,不是可选。

3.3 决策 gate——human-in-the-loop

决策 gate 管的是 blog212 那件事:“对了也不能让它直接干”——权限弹窗、A/B/C 三选一、rollback 控件、diff 预览、“这一步先跑 dry-run 让我看看再决定”。

聊天气泡装不下这类 UI。你能想象在气泡里塞一个”覆盖 README.md,现有 214 字节将丢失,请输入 yes 继续”吗?——那就是把 CLI 的交互硬灌进对话框,反人性。

正解是独立的、模态或半模态的决策组件,弹出、卡住主流程、要求一次明确决策再放行。前端有现成的对应物:window.confirm 是最原始的一版、GitHub 删仓库那个”抄一遍仓库名”是最认真的一版、git rebase -i 是最工程化的一版——把每一条 pending 操作列出来,让你逐条 pick / squash / drop,本质就是给一队 tool_use 挨个走一遍 gate。

三种控件在真实 Agent 产品里都能找到位置:

  • 权限弹窗:Claude Code 每次要动手前那个 Yes / Yes-and-don’t-ask-again / No 三选项确认就是;每一次 dispatch 卡住等你选一项。
  • A/B/C 选择:让模型一次生成三版结果、让人挑一版继续。比”重生成”高效——你不用把已经生成好的两版扔掉。
  • rollback 控件:允许用户在 Agent 走了 5 步之后回到第 2 步的状态重跑。类比 Git 的 reset + reflog,前端要有一个能一眼看清”我现在在时间线的哪一格、能回到哪几格”的可视化。

三类加起来,Agent UI 就不再是”读一堵消息墙 + 敲下一句话”,是”看一张工具地图 + 追一条执行时间线 + 在关键路口按一次按钮”。

4. 干

最能说服我”Chat 不是终点”的证据是我自己在 Claude Code 上的 UI 演化史——半年里几乎每两个月长一层新的非 chat 界面。

阶段一(用上 Claude Code 之前):纯 Chat。 网页聊天框时代,一个输入框、一个响应区、什么都看不见。写代码基本是”我说一句、它答一段、我复制回编辑器”。话痨版搜索引擎。

阶段二:工具面板出现。 /permissions 可以逐工具查看和调整 allow/ask/deny、/mcp 能看每个 server 挂没挂、还能按会话禁用。第一次有”可控性”的感觉——终于知道它到底伸出了几只手。

阶段三:memory 展示层。 /memory 把跨会话记住的规则摊开来给你看、可以编辑、可以按项目/用户维度分层。blog210 讲 memory 的那一层,产品化之后就长这样:把一个不可见的状态,变成一个可见、可改、可 diff 的 UI 对象。

阶段四:subagent 树。 主 agent fork 子任务时会显示一个层级化的进度视图——哪个子 agent 在跑、跑了多久、返回了什么。blog213 那套 spawn 语义有了 UI 之后,我第一次敢让它开三个 subagent 并跑:因为我看得见任何一个卡住。

阶段五(自己加的):A/B/C gate。 这一层是我在自己的博客发布流程里手工加进去的。翻译中文稿的时候,早期是一版翻译直接发我审——我看不出问题就 approve、我看得出问题就退回重翻。两个月里翻车了三次:一次把技术术语翻成了泛化词、一次把作者第一人称语气翻成了教科书腔、一次把中文文化梗直接删掉。

后来我改了个规则:中文完稿先贴我过目,翻译前必须给我三个候选(A/B/C)——不同侧重、不同风格、不同术语选型,我选一个再往下走。写进 memory 里就三段:

Feedback:
  1. 中文完稿必须先贴我看,不要偷偷开始翻译。
  2. 翻译前必须给出 A/B/C 三个候选:
     - A:直译,最忠实
     - B:意译,最地道
     - C:混合,术语直译 + 句式意译
  3. 我回复 A / B / C 之后再动手;不许自作主张选一个。

这三段生效之后,同样的翻译流程翻车率降到接近零。不是翻译模型变强了——是把”翻译”这件事从”一次性 dispatch”变成”一次带三个候选的决策 gate”。这是纯 UI/流程层的改造,一行模型代码都没改。

呼应 blog213 的 subagent 血教训:翻车不是 subagent 干得烂,是 brief 写得烂——A/B/C 让我在花五分钟决策之前,先看到三个可能的 brief 具象化成三段真实翻译,比让我提前把 brief 写完备准确得多。

这一段的元结论A/B/C gate 是我用 memory + prompt 手工实现的一个 UI 元素。它没有图形界面,就是一段文字规则;但它在我的工作流里占的位置,和一个模态弹窗一模一样——卡住主流程、要求一次明确决策、决策之后才放行。前端产品经理今天缺的很多”Agent UI 组件”,用户已经在用规则和 prompt 补丁凑出来了——那就是市场信号:这些组件该做成一等公民。

5. 界

写到这你以为我要下结论”Chat 已死,去做工具面板吧”——不。Chat 有它撑得住的场景,说清楚这个场景比全盘否定它更诚实。

Chat 的强势区在不确定任务

  • 我还没想清楚要做什么,先随便聊聊看看方向
  • 我在探索一个陌生领域,需要一问一答式的对话来构建认知
  • 我在做纯文本创作(写作、翻译、头脑风暴),本来就没什么”工具”要调
  • 我在情感或咨询场景,我要的是回应本身,不是 diff 也不是执行

这些场景装工具面板反而累赘——你根本没有 5 个工具要选,硬摆一个空列表在那只会分散注意力。

Chat 的弱势区在确定任务

  • 我要走一遍固定的 7 步博客发布流程
  • 我要让 Agent 帮我批量 refactor 30 个文件的 import 路径
  • 我要审 PR 里 20 处 diff 并逐条决定接受/拒绝
  • 我要让 Agent 起 3 个 subagent 并跑我要看着它们

这些场景里”看得见、管得住”比”回复自然”重要一百倍。让我在气泡里跟 Agent 商量”你现在应该改哪个文件”是浪费我时间——直接给我一张文件树 + 20 个 diff 卡片 + 一个 Approve All / Reject All 按钮,我一分钟能干完的事,聊天窗要花二十分钟。

所以成熟的 Agent 产品应该两套 UI 并存——起手是 Chat(低成本进入),任务清晰之后主界面切成工具面板 + 决策 gate,任务结束回到 Chat。Cursor 的 Ask 模式 vs Agent 模式(早年的 Chat / Composer 双面板)已经有这个雏形,Claude Code 的 / 命令面板算是把工具面板塞进 CLI 的 UX 妥协。但都远远不够——业界现在的普遍水位是”70% Chat + 30% 工具面板”,我押注三年内会翻转成”30% Chat + 70% 专门 UI”。

一句话收界:Chat 是入口,不是主厅。把入口装修得富丽堂皇没错,但一个连主厅都没盖的房子,不该叫房子。

6. 钩

这一篇的动作项就一条:打开你现在最常用的 Agent 产品,数一数你在纯 Chat 区花的时间,和在非 Chat 元素(工具列表、权限弹窗、subagent 树、diff 视图)花的时间。如果非 Chat 那半接近或超过一半——恭喜,你已经在用一个”部分成年”的 Agent UI,只是产品还没把这半正名。如果几乎全在 Chat——那你要么是任务确实全都不确定(合理),要么是这个产品还停在 Web 1.0(该换或该催)。

前端同行的动作项:下一个前端主场不是”再造一个 chat 组件库”——市面上已经有二十个 ChatUI 库了,再造一个没人会用。真正没人做的是 <ToolPanel><AgentTrace><DecisionGate> 这三类组件的设计系统——设计 tokens、交互规范、可访问性、状态机。谁把这一层做成 shadcn/ui 那样的社区默认选择,谁就是 Agent 时代的 Ant Design。

番外 C 预告:《Fiber 双缓冲教 Agent state 三招:workInProgress、commit、rollback》。这一篇讲的是 UI 该长什么样,下一篇讲的是 UI 底下那层数据该怎么组织——React 十年前解决的”渲染中途出错要能回滚不能污染屏幕”和 Agent 现在遇到的”subagent 中途翻车不能污染主循环 messages”是同一个问题的两次相遇。Fiber 的 workInProgress / current 双缓冲,几乎就是一份现成的 Agent state 设计图。

呼应系列 spine——控制权交给概率函数(blog01)、context 是它每轮的输入(blog02)、执行留在你手里(blog03)、blog07 讲 window.confirm 那道门、blog216 讲配置和 memory 怎么长成规则中枢——本篇是把这些”引擎里的机制”翻译到”用户看得见的 UI 组件”上。引擎和界面本来就应该同一天成年。