乐于分享
好东西不私藏

当 Agent 成为软件的用户:如何把软件装进 Agent 的上下文里?

当 Agent 成为软件的用户:如何把软件装进 Agent 的上下文里?

过去几十年,我们做软件时默认了一件事:用户是人。

所以软件才会长成今天这个样子。按钮、颜色、布局、悬停态、动画,本质上都在服务人类的视觉、双手和持续感知能力。

可今天,软件里开始出现另一种用户:LLM Agent

这时真正的问题就不再是“模型会不会调用 Tool”,而是:

如果用户是 Agent,应用有没有给它一个能稳定理解、持续操作的界面?

需要一套按 Agent 感知方式重新设计的界面范式。暂把它叫做Agent-Oriented UI

Agent-Oriented UI 不是给模型看的网页,也不是 Tool Call 的包装层/CLI的集合。它本质上是把对象选择、状态读取、动作触发重新组织成文本原语的界面范式。

先看一个很普通的动作

你在微信里给JY Chen发一句“你好”。

人做这件事几乎没有门槛:

看见联系人列表

通过头像/名字认出哪个是JY Chen

点进去

输入“你好”

点发送

整个过程很顺。因为 GUI 已经替你做完了很多脏活。

你点的表面上是头像和名字,实际上选中的是一整段底层对象:联系人 ID、会话信息、服务端路由、加密上下文。你看不到这些,也不需要看到。界面已经把复杂性藏好了。

现在把执行者换成一个 LLM,问题立刻出来了。它看不到头像,不会点联系人卡片,也不会像人一样持续感知界面的变化。传统 GUI 里最自然的一套交互方式,对 Agent 并不成立。

为什么 GUI 对 Agent 天然不成立

人类能力
GUI 依赖它做什么
视觉
看颜色、层级、位置、头像、图标
双手
点击、拖拽、输入、切换焦点
持续感知
理解动画、过渡、局部刷新和实时反馈
LLM 缺少什么
直接后果
没有视觉
CSS、颜色、布局只是在浪费 token
没有双手
不能点击、拖拽、悬停
没有持续感知
它不会“看着”界面流动,只会读取当下快照

这不是模型能力不够强,而是输入输出模态压根不匹配。继续把 GUI 原封不动搬给模型,本质上是在逼它适应一个不属于它的界面世界。

真正的问题,是鼠标

键盘其实没那么重要。人类需要键盘输入文本,模型刚好相反,它天生会输出文本。Agent 真正缺的是握住鼠标的那双手,我们也不需要给LLM创造一双手。

因为鼠标在 GUI 里做的事,不只是“点一下”。它一直在帮用户完成两件事:

1. 选择对象

你点击联系人卡片,选中的不是一块 UI,而是一个底层数据对象。

2. 触发动作

你点击发送,不是在按一个蓝色按钮,而是在执行一个带参数的命令。

把这件事拆开,微信里“给 JY Chen 发消息”其实更像这样:

selectedContact = {  id: "jy_chen_id_392",  serverId: "sz-01",  encryptKey: "..."}sendMessage(recipient: selectedContact, message"你好!")

GUI 的厉害之处在于,它让人只看名字和头像,就能完成对象选择和动作触发。复杂参数都被界面默默接住了。

Agent 没有这座桥。Agent-Oriented UI 要做的,就是把这座桥在文本里重新搭起来。

Agent-Oriented UI 到底是什么

一句话说,Agent-Oriented UI 是一种把 LLM Agent 当一等公民来设计的文本界面范式。

它有三个核心元素。

View:把应用状态组织成可读的语义单元

在 Agent-Oriented UI 里,Agent 看到的不是像素,不是 DOM,也不是 accessibility tree,而是一个个边界清楚的View

你可以把它理解成“给模型看的页面”。

<view id="contacts" type="ContactList" name="联系人列表" app_id="wechat">联系人  - [Wills Guo](Contact:contacts[0]) — 在线  - [Emma Chen](Contact:contacts[1]) — 离开  - [JY Chen](Contact:contacts[2]) — 在线  ### 可用工具  - open_chat(contact: Contact):打开对话  - send_message(recipient: Contact, message: string):发送消息</view>

这里没有 CSS,没有头像,也没有视觉布局。可对模型来说,信息已经够用了。

Reference:用类型化引用完成“选择”

光有名字还不够。Agent 还得能精确地引用那个对象。

所以 Agent-Oriented UI 把引用直接写进文本:

[JY Chen](Contact:contacts[2])

这里的格式是:

[可读标签](类型:引用路径)

JY Chen是给模型识别的可读标签,Contact:contacts[2]是给 runtime 解析的引用。

模型不需要知道底层真实数据是什么。运行时会把contacts[2]解析回完整对象,再把它作为参数传给真实函数。

Tool:用类型化函数完成“触发”

鼠标点击在 Agent-Oriented UI 里不再存在,取而代之的是显式的 Tool 调用。

可用工具- open_chat(contact: Contact):打开对话- send_message(recipient: Contact, message: string):发送消息

这件事对今天的模型反而很自然。因为 Tool Calling 本来就是它们最擅长的交互方式之一。

这里还有个很关键的约束:

Tool 负责触发状态变化,Tool Result 不负责返回一大坨数据。新的界面状态通过下一次 Snapshot 进入上下文。

这个约束很朴素,但它会把系统做得干净很多。关键不是“少返回点数据”,而是不要把已经开始过期的结果,直接堆进 Agent 的上下文

为什么这个约束重要

无论是 CLI 还是普通 Tool Call,只要结果一返回,它基本就被写死在上下文里了。世界继续变化,这些结果不会自己更新,只会慢慢变成一块块真实但过时的碎片。

Agent-Oriented UI 不一样。它的原则是:系统只把最新的状态和最新的能力暴露给大模型。

这里给出 Agent 读写 hello.txt 文件的 case

t1,模型调用Read File,不需要把 hello.txt 放进 Tool Result,只需要插入一个File View。

t2,模型调用Edit File,不需要再返回一整份新文件,只需要更新 hello.txt 对应的File View。

t3,如果这个文件不重要了,模型再调用Close File,这个File View就可以直接从上下文里移除。

CLI 给模型返回的是对象的快照。Agent App 给模型的是由 runtime 管理的活对象。

给 JY Chen 发消息,在 Agent-Oriented UI 里会怎么发生

应用先给 Agent 一份 Snapshot:

<view id="contacts" type="ContactList" name="联系人列表" app_id="wechat">联系人  - [Wills Guo](Contact:contacts[0]) — 在线  - [Emma Chen](Contact:contacts[1]) — 离开  - [JY Chen](Contact:contacts[2]) — 在线  ### 可用工具  - open_chat(contact: Contact):打开对话  - send_message(recipient: Contact, message: string):发送消息</view>

用户说:“给 JY Chen 发一句你好。”

模型读取这份 Snapshot,识别出contacts[2]对应JY Chen,然后构造调用:

{  "tool": "send_message",  "arguments": {    "recipient": "contacts[2]",    "message": "你好!"  }}

运行时把contacts[2]解析成真实联系人对象,执行发送。之后系统再推一份新的 Snapshot:

<view id="chat_jy" type="ChatDetail" name="与 JY Chen 的对话">与 JY Chen 的对话  - 你:你好! — 刚刚  ### 可用工具  - send_message(message: string):发送另一条消息  - close():关闭此对话</view>

整个过程里,没有像素,没有鼠标,也没有“让模型学着看屏幕”。它只是在做一件更适合自己的事:

读取当前状态 -> 推理 -> 调用动作 -> 接收新状态

这套范式为什么成立

Agent-Oriented UI 不是因为“文本更高级”才成立,它成立是因为它顺着 Agent 的限制来设计。

没有视觉,反而省掉了大量无效复杂性

如果用户是模型,就没必要为它渲染像素。CSS、响应式布局、hover、z-index,这些都是给眼睛服务的。

对 Agent 来说,语义化文本已经够了,而且通常更好。

没有持续感知,状态模型反而更简单

人会经历一个连续流动的界面。Agent 不会。它只会在某个时刻读到一份完整快照,然后基于这份快照行动。

这会逼着我们把系统整理成更干净的循环:

当前状态是什么

现在能做什么

做完以后状态怎么更新

很多前端里最麻烦的“半状态”,在这里根本没有必要存在。

更重要的一点,是只把最新的状态和能力暴露给大模型

暴露给 Agent 的永远是当前对象的最新状态

暴露给 Agent 的永远是当前上下文下最新的能力

不必要的历史碎片不会继续堆在它面前

也就是说,系统不是不断把旧结果塞给 Agent,让它自己维护状态;系统会主动维护一个活的工作现场,再把这个现场同步给 Agent。

这件事非常重要。因为 Agent 真正难的,从来不是会不会发动作,而是能不能始终站在最新、干净、可操作的世界模型上继续往前做事。

没有鼠标,所以必须把选择机制做成文本原语

这是 Agent-Oriented UI 最关键的一步。

如果一个系统只有 Tools,没有可引用对象,模型会知道“能做什么”,却不知道“该对谁做”。

如果一个系统只有文本列表,没有类型化引用,模型又会卡在对象绑定这一步。

Agent-Oriented UI 的价值,不是把 Tool 暴露出来,而是把识别、选择、触发这三件事在文本里重新接上。

为什么实现层还会用 HTML 和 JS

一个常见问题是:既然最后给模型的是 Markdown,为什么不直接手写 Markdown?

因为开发者生态已经在 Web 上了。

更现实的路径是:

React / JSX 组件  -> DOM 中的 HTML  -> 语义化 Markdown Snapshot

开发者继续用熟悉的组件模型、组合方式和状态管理。框架负责把这套结构转换成对 LLM 更友好的表示。

也就是说,HTML 在这里不是最终界面,而是中间表示。

Agent-Oriented UI 不是 GUI 的缩水版

它不是“没有样式的网页”,也不是“给 Tool Call 配一层文案”。

GUI 解决的是人怎么操作软件。

Agent-Oriented UI 解决的是 Agent 怎么进入软件、理解当前世界、并持续在最新状态上工作。

两者面向的是不同用户,所以范式也应该不同:

GUI 暴露视觉页面

GUI 通过鼠标选择对象

GUI 通过按钮和控件触发动作

GUI 消耗屏幕空间

这不是替代 GUI。人类还是需要 GUI。

但当软件开始把 Agent 当成真实用户,问题就不再是“模型能不能调用工具”,而是:

应用有没有为它提供一个可稳定理解、可持续操作、且只暴露最新状态和能力的界面。

如果没有,Agent 再强,也只是在一个不属于它的世界里凑合工作。

如果有,Agent-Oriented UI 讨论的就不是一个表现层技巧,而是面向 Agent 用户的软件基础设施。