chatbot 的世界有了统一客户端——Open WebUI、LobeChat、LibreChat,随便哪一个,都能接入各大模型厂商,给你一个熟悉的对话框。
agent 的世界没有。
这个问题最近在社区里开始被讨论。最值得关注的尝试是 AG-UI(Agent-User Interaction Protocol),2025 年中由 CopilotKit 团队发布,已有 LangGraph、CrewAI、Mastra、Microsoft Agent Framework 的官方支持。它定义了约 16 种标准事件类型,用事件流连接 agent 后端和前端 UI,解决"把 agent 嵌入 Web 应用"的问题。
AG-UI 是一个好的开始,但它没有解决全部问题。
我在想,如果从头设计这个协议,应该怎么做。以下是我的思考。
首先要想清楚:agent 和 chatbot 的根本差异
chatbot 是被动响应的。用户说话,它回答,结束。这个模型很简单:request → response。
agent 是自主运行的。它有记忆、有目标、会主动行动、会跨渠道推送消息、会在执行到一半时暂停等待人的确认。它的生命周期不以一次对话为边界,它的交互模式不是一来一往,它不总是等你先开口。
所以给 agent 设计统一客户端,不能只是"更好的聊天框"。需要从头定义一套抽象。
核心抽象概念
Agent
有身份、有记忆、有技能集、有当前状态(idle / thinking / acting / waiting)。不只是一个模型实例,而是一个持续运行的实体。两次对话之间,它还在。
Channel(信道)
agent 与外部世界交互的通道。飞书、Telegram、Web UI、Email 都是 channel,每个 channel 有自己的渲染能力——纯文本、富文本、交互卡片,能力不同,渲染层需要降级适配。
Session(会话)
一次有边界的交互上下文。一个 agent 可以同时在多个 channel 维护多个 session,session 之间共享 memory 但隔离对话历史。
Session 有两种发起方式:用户发起(pull 模型)和 agent 发起(push 模型,比如 cron 触发的主动通知)。这两种看起来不同,本质上可以统一——agent 发起的 push,就是一个"由 agent 先说第一句话"的 session,用户可以选择接着回复,session 就延续了。不需要在协议层把它们割裂成两条路。
Event(事件)
信息流的基本单位,分两个方向:
下行(agent → 用户):TextChunk、ToolCall、ToolResult、Interrupt、StateUpdate、Artifact
上行(用户 → agent):UserMessage、Approval、Rejection、ContextUpdate
Message(消息)
Event 的子集,专指"有完整语义、面向人类可读"的那类。用户发的一句话是一条 Message,对应一个 UserMessage Event,天然原子。agent 流式输出的一段话也是一条 Message,但在底层是多个 TextChunk Event 聚合的结果。
Event 和 Message 的层次不同:底层传输用 Event(细粒度、实时),UI 渲染用 Message(聚合后展示给用户)。把两者混用,要么传输粒度太粗丢失实时性,要么 UI 直接渲染 Event 导致界面碎片化。
Memory(记忆)
分两层:Short-term(当前 session 上下文)和 Long-term(跨 session 持久化事实)。Memory 是 agent 的内部状态,client 只读展示,不直接编辑——用户通过对话来影响 memory,而不是直接操作它。
Skill(技能)与Tool(工具)
这两个概念需要分开。Tool 是原子能力:调一个 API、执行一段代码、读一个文件。Skill 是 Tool 的组合加上使用策略,是更高层的能力封装。client 需要感知 ToolCall 的入参和结果(才能正确渲染执行过程),也需要知道当前激活了哪些 Skill(才能向用户展示 agent 的能力边界)。
Interrupt(中断)
agent 执行中途需要人工介入的时刻——确认、补充信息、拒绝操作。这是 agent 交互区别于 chatbot 最关键的机制,也是最容易被设计忽视的地方。Interrupt 不是一个异常,是一个一等公民。
Identity / Auth(身份与授权)
谁在和 agent 说话?同一个人的飞书账号和 Telegram 账号,agent 应该认出是同一个人,memory 才能跨 channel 共享。这是跨渠道 agent 的核心价值所在。另外 agent 执行某些操作时(发邮件、调外部 API),用的是谁的凭证?Identity 和 Credential 需要一起设计。
Context(上下文注入)
用户和 agent 交互时,往往有隐式的环境信息需要传递:当前时间、当前位置、粘贴的截图、打开的文档。这些不是 Message,也不是 Memory,是每次交互时动态注入的运行时上下文。需要作为独立概念设计,否则会被随意塞进 Message 里,破坏协议边界。
Artifact(产出物)
agent 经常产出有结构的东西——生成的代码、写好的文档、分析报告。这些不是聊天消息,是可以被引用、存储、版本管理的持久化对象。没有 Artifact 概念,这些东西只能作为文本 dump 在对话里,失去了结构性。
Trace(执行轨迹)
区别于 Event stream。Trace 是面向调试和可观测性的——agent 为什么做了这个决策,调用链是什么,哪一步耗时最长,哪里出错了。Event 是给用户看的,Trace 是给开发者看的。两者数据来源重叠,但目的不同,需要独立建模。
Policy(行为策略)
哪些操作需要人工审批,哪些可以自动执行,哪些被禁止。Interrupt 是一个事件,但触发 Interrupt 的规则需要有地方来定义,就是 Policy。没有 Policy,Interrupt 就是一个没有规则的随机打断。
协议分层
┌─────────────────────────────────────────────┐
│ Rendering Layer │
│ Web UI / Telegram / 飞书 / Email / ... │
│ (各 channel 自己渲染,按能力降级) │
├─────────────────────────────────────────────┤
│ Agent Client Protocol │
│ 连接任意前端 ↔ 任意 agent 后端 │
│ 统一事件流 + 状态同步 │
├─────────────────────────────────────────────┤
│ Agent Runtime │
│ memory · skills · tools · scheduler │
│ identity · policy · session │
└─────────────────────────────────────────────┘
事件流
一次典型的用户发起 session:
用户输入 [UserMessage]
│
▼
Agent Runtime
│
├──► [TextChunk × N] 文本流式输出
│
├──► [ToolCall] 工具调用开始
│ │
│ [ToolResult] 工具返回结果
│ │
│ [Artifact] 如果产出了文件/报告
│
├──► 需要人工介入?
│ ├── 是 → [Interrupt] → 用户 Approve/Reject → 继续/终止
│ └── 否 → [RunEnd]
│
└──► [StateUpdate] memory 或状态变更通知
agent 发起的 session(push 模型):
Scheduler / Trigger
│
▼
[SessionStart](agent 发起)
│
▼
[TextChunk × N] 或 [Artifact]
│
▼
[RunEnd]
│
▼
用户可选择回复 → session 延续
完整关系图
┌──────────────────────────────────────┐
│ Agent Runtime │
│ │
│ Identity ──► Memory │
│ │ (short / long) │
│ │ │
│ │ Skills ──► Tools │
│ │ │
│ └──► Policy │
│ │ │
│ └──► Interrupt 触发规则 │
│ │
│ Session ──► Event Stream │
│ ├── Message │
│ ├── ToolCall/Result │
│ ├── Artifact │
│ ├── Context │
│ └── Interrupt │
│ │
│ Trace(可观测,独立于 Event Stream) │
└──────────────────┬───────────────────┘
│
Agent Client Protocol
(统一事件流 + 状态同步)
│
Channel Router
(身份映射 + 渲染降级)
│
┌────────────────────────┼────────────────────────┐
▼ ▼ ▼
Web UI 飞书 Telegram
┌─────────┐ (卡片降级) (文本降级)
│Timeline │ ← 对话流(主视图)
│ Status │ ← agent 当前状态(idle/thinking/acting)
│ Tools │ ← 工具调用可折叠展示
│Interrupt│ ← 待审批的中断队列
│ Memory │ ← memory 快照只读面板
│ Skills │ ← 当前激活 skill 列表
│Artifact │ ← 产出物管理
│ Trace │ ← 执行轨迹(开发者模式)
└─────────┘
和 AG-UI 的差异
AG-UI 解决的是"把 agent 嵌入 Web 应用"的问题:用户打开一个网页,网页里有 agent 的对话区域。它的设计前提是 pull 模型,单一 Web UI。
我这里想解决的是"agent 作为中心,各种界面来接入它"的问题:agent 持续运行,有自己的身份和记忆,通过不同 channel 和用户交互,有时主动找你说话,有时等你发起。主客关系反过来了。
| 维度 | AG-UI | 这里的设计 |
|---|---|---|
| 交互模型 | 纯 pull(用户发起) | pull + push 统一为 Session |
| Channel | 单一 Web UI | 多 channel 路由 + 渲染降级 |
| Identity | 不涉及 | 跨 channel 身份映射 |
| Memory | 不涉及 | 一等公民,前端只读可见 |
| Skills/Tools | 不涉及 | 动态感知,区分 Skill 和 Tool |
| Interrupt | 支持 | 支持,由 Policy 驱动 |
| Artifact | 不涉及 | 独立产出物模型 |
| Trace | 不涉及 | 独立可观测层 |
| Policy | 不涉及 | 行为策略,Interrupt 的规则来源 |
两者并不是竞争关系——AG-UI 专注于 Web 嵌入场景,我这里的设计更接近一个 personal agent 的完整客户端协议。如果 AG-UI 继续演进,覆盖多 channel 和 push 模型,两者会逐渐收敛。
最后
这个设计里,Identity 和 Policy 是最值得重视的两个遗漏概念。
Identity 决定了 memory 能不能跨 channel 共享。如果你在飞书和 Telegram 用的是同一个 agent,但 agent 认不出你是同一个人,那"个人 agent"的核心价值——了解你这个人——就打了折扣。
Policy 决定了 agent 的可信边界。没有 Policy,Interrupt 就是一个没有规则的随机打断,人机协作就变成了随机审批。只有当 agent 清楚地知道哪些事情自己可以自主决定、哪些需要你确认,它才真正成为一个可靠的合作者。
nanobot 和 openclaw 目前对这两个问题的处理都还很粗糙。这可能是这类 personal agent 框架下一步值得认真解决的问题。