当 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 原封不动搬给模型,本质上是在逼它适应一个不属于它的界面世界。
真正的问题,是鼠标
键盘其实没那么重要。人类需要键盘输入文本,模型刚好相反,它天生会输出文本。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 用户的软件基础设施。
夜雨聆风