从问答到做事:
任务型侧边栏 Agent 是怎么搭起来的
以 Vercel AI Elements 和 AI SDK 7 为样本,拆开一个能规划、能调工具、能请你确认的侧边栏助理,一层一层看它由什么构成;最后回到墨笺 Markdown,对照现有代码给出改造路线。
三个会改变你计划的发现
核心结论:AI Elements 本身只是渲染层,任务能力来自它下面的 AI SDK 运行时和 UIMessage.parts 协议;你要学的是这套协议,组件库可以随时换。
你看的那个库要退役了
elements.ai-sdk.dev 的仓库公告:AI SDK Elements 将在 2026 年 11 月底弃用,由 shadcn/ui 10 月发布的 AI Elements(21 个模式)接替。新项目直接从 shadcn/ui 装,旧组件因为是拷进项目的代码,不会失效。
[来源: ui.shadcn.com changelog 2026-10;vercel/ai-elements README]
UI 是消息 parts 的投影
每个组件对应一种 part 或一个 part 状态:tool-* 的 approval-requested 渲染成确认卡,data-plan 渲染成计划。理解 parts,组件只是查表。
[来源: ui.shadcn.com/docs/ai-elements Tool / Plan / Confirmation]
墨笺不能直接装这些组件
墨笺前端是 DC runtime + 命令式渲染,没有 Tailwind 和 shadcn。能搬过去的是协议(UI Message Stream)和模式(Plan / Tool / Confirmation 的状态设计),渲染用墨笺自己的样式写。
[来源: mojian-markdown package.json、src/editor/aiMethods.ts]
Agent = 模型 + 工具 + 循环
AI SDK 文档对 Agent 的定义只有一句:在循环里使用工具完成任务的大模型。拆成三件事:模型决定下一步;工具把能力伸出文本之外(读文件、调接口、写库);循环负责上下文管理和停止条件。
这个循环在 SDK 里叫 ToolLoopAgent。每一轮叫一个 step:模型要么输出文本(结束),要么调用工具(SDK 执行工具,把结果塞回上下文,开始下一步)。循环在以下任一情况停下:
| 停止条件 | 含义 | 对侧边栏的意义 |
|---|---|---|
| finishReason ≠ tool-calls | 模型给出最终文本 | 正常收尾 |
| 工具没有 execute | 工具要由别处执行 | 客户端工具的基础:交给浏览器执行(比如改编辑器内容) |
| 工具需要审批 | 发出 approval 请求后暂停 | 人在回路:等用户点「允许」再继续 |
| stopWhen 命中 | 默认 isStepCount(20) | 防失控;可用 hasToolCall('done') 让模型主动交卷 |
[来源: ai@7.0.143 docs/03-agents/02-building-agents.mdx、04-loop-control.mdx]
四层架构:从模型到像素
一个侧边栏助理从后端到界面分四层。AI Elements 只占最上面一层,这是本次调研最需要摆正的认知:
[来源: ai@7.0.143 docs/04-ai-sdk-ui/50-stream-protocol.mdx、20-streaming-data.mdx]
Agent 的基础元素就是 parts
AI SDK 5 之后,一条消息不再是一段字符串,而是 parts 数组。一次任务里 Agent 想了什么、调了什么、产出了什么,都按顺序变成 part。AI Elements 定义「Agent 元素」的方式,就是给每类 part 配一个渲染模式。
| part.type | 是什么 | 关键字段 / 状态 | 对应模式 |
|---|---|---|---|
| text | 正文,流式增量 | text、state: streaming | done | Message(Markdown 渲染) |
| reasoning | 模型思考过程 | 流式文本 | Reasoning、Chain of Thought |
| tool-{name} | 静态声明的工具调用,类型安全 | toolCallId、input、output、state(见第 04 章) | Tool、Confirmation、Terminal、Stack Trace |
| dynamic-tool | 运行时才知道的工具(MCP 等) | 同上,toolName 是字符串 | Tool |
| source-url / source-document | 引用来源 | url、title | Sources、Inline Citation |
| file | 附件、生成的文件 | url、mediaType | Prompt Input 附件 |
| data-{name} | 你自定义的结构化数据 | id 相同则原地替换;transient 不入历史 | Plan、Queue、进度条…… |
| step-start | 新一步开始的分隔 | 无 | Trace 分组 |
[来源: ai@7.0.143 docs/04-ai-sdk-ui/03-chatbot-tool-usage.mdx、20-streaming-data.mdx、50-stream-protocol.mdx]
data-plan,用固定 id 反复写,前端那一块就原地更新。计划、进度、产物卡片都走这条路。21 个模式按「Agent 在做什么」分组
官方按字母排列。按任务型助理的需要重新分组更好记,每组回答一个问题:
它在说什么
- Chat流式时保持阅读位置
- Message一轮问答
- Prompt Input输入与附件
- Suggestion一键追问
- Model Selector选模型与推理档位
它在想什么
- Reasoning流式思考
- Chain of Thought步骤摘要
- Context上下文窗口用量
它打算做什么
- Plan步骤已知,显示进度
- Task步骤边做边出现
- Queue待办 / 已完成清单
- Subagent子代理的调用与结果
它在做什么
- Tool一次工具调用,从输入到输出
- Terminal命令与流式输出
- Stack Trace失败调用
- Test Results测试结果
它需要我什么
- Confirmation执行前审批
- Checkpoint回到某个会话点
它做出了什么
- File Tree改了哪些文件
- Sources用了哪些链接
- Inline Citation句内引用
[来源: ui.shadcn.com/docs/ai-elements,21 个模式清单]
模式,而不是包
AI Elements 的几条设计原则,直接决定了你接入时能改什么、该改什么:
代码归你
没有 npm 包。CLI 把源码拷进
components/ai-elements/,你直接改。代价是没有自动升级,这也是这次迁移「不必立即动手」的原因。组合式组件
Conversation包ConversationContent,Message包MessageContent再包MessageResponse。每块只做一件事,替换其中一块不影响其他块。所有 Agent 步骤一个形状
shadcn 版把 Tool、Plan、Reasoning、Subagent 都建在
Trace上:一行 = 图标 + 标题 + 可选描述 + 状态(running / error / pending),可折叠、可嵌套。界面统一,用户扫一眼就知道 Agent 走到哪。不碰状态
6 月发布的
MessageScroller只管滚动,不管消息、AI 状态、传输和持久化;滚动逻辑另有无样式版本@shadcn/react。数据层由你接。Props 贴着 SDK 类型
组件吃的就是
UIMessage的 part。迁移时导出名和 props 不变,只换渲染,调用方代码不用改。
[来源: elements.ai-sdk.dev/llms.txt;ui.shadcn.com changelog 2026-06、2026-10]
一次工具调用的七种状态
Tool 和 Confirmation 两个模式的全部逻辑,就是对下面这个状态机做 switch。把它记住,你就能自己写任何工具卡片:
preliminary: true:工具用 async generator 多次 yield 中间结果(子代理进度就是这样流出来的)。[来源: ui.shadcn.com/docs/ai-elements Tool;ai@7.0.143 docs/03-agents/06-subagents.mdx]
两个容易误判的边界
- 客户端工具停在
input-available,而聊天已停止:它在等addToolOutput。 - 渲染成「等待你操作」,不要标红。
- 服务端工具停在
input-available,聊天已停止:请求被打断。 - shadcn 的 Tool 模式此时显示为错误。
一次任务的流,逐帧回放
下面是一个墨笺场景的模拟:选中一段文字,让助理「改得更简洁,然后存到飞书」。左边是服务端发出的 UI Message Stream 事件,右边是前端把事件合并成 parts 后渲染的侧边栏。点「下一帧」,注意三件事:计划块是原地更新的;改写建议停下来等你采纳;发布动作要你审批。
模拟数据,事件格式按 ai@7 流协议文档编写;工具名为示意。
「会规划」到底是哪段代码
问答系统和任务型助理的差别,不在模型,而在三处设计:模型能否看到并维护一份计划;计划能否被用户看见;每一步能否被执行和验证。AI SDK 给了两种做法:
- 给模型一个
updatePlan工具,让它自己写、自己勾。 - Claude Code 的 TodoWrite 就是这个思路。
- 计划由模型维护,前端把这个工具的 part 渲染成 Plan / Queue。
- 服务端代码固定步骤,用
data-plan推进度。 - 适合流程固定的业务(审批流、报表生成)。
- 可靠,但不是 Agent 自主规划。
做法 A 的服务端骨架(AI SDK 7):
[来源: ai@7.0.143 docs/03-agents/02-building-agents.mdx、04-loop-control.mdx、06-tool-approvals.mdx]
三个进阶开关
| 开关 | 用途 | 侧边栏场景 |
|---|---|---|
| prepareStep | 每步前改模型、工具集、消息 | 第 0 步强制先调 updatePlan:toolChoice: { type: 'tool', toolName: 'updatePlan' } |
| runtimeContext / toolsContext | 不进 prompt 的服务端状态;按工具隔离的凭证 | 租户、用户权限、飞书 token 只给 publishToFeishu |
| 子代理 | 工具里再跑一个 Agent,async function* 逐步 yield | 长文调研交给子代理,主上下文只收摘要;注意子代理内不支持工具审批 |
[来源: ai@7.0.143 docs/03-agents/04-loop-control.mdx、06-subagents.mdx、02-building-agents.mdx]
侧边栏和聊天页的区别:它住在别人家里
独立聊天页只需要对话。侧边栏寄生在宿主页面上,它的价值来自对宿主的读和写。这引出工具的第二种分法,比「读 / 写」更重要:
| 工具类型 | 谁执行 | 怎么写 | 典型工具 |
|---|---|---|---|
| 服务端工具 | Agent 进程 | 带 execute | 查数据库、调业务 API、发布到飞书 |
| 客户端自动工具 | 浏览器,无需用户 | 不带 execute;前端 onToolCall 里 addToolOutput | 读当前选区、读表单值、滚动到某处 |
| 客户端交互工具 | 浏览器,等用户 | 渲染 UI,用户操作后 addToolOutput | 展示 diff 让用户采纳、让用户选一个选项 |
[来源: ai@7.0.143 docs/04-ai-sdk-ui/03-chatbot-tool-usage.mdx]
sendAutomaticallyWhen 接受函数这一点写的组合用法,文档分别示范了两个 helper。接入前在你的版本上跑一次确认。宿主上下文怎么进模型
三种方式,按优先级:① 让模型用工具按需取(readSelection、readPage),上下文最省;② 用 sendMessage 的 body 或 message metadata 带上当前页面标识,服务端在 prepareCall 里注入;③ 整页塞进 system,只适合小页面。墨笺现在用的是 ③(首轮带全文)。
业务侧边栏:按这个顺序学和建
每一步都能单独跑通、单独验证。前一步不稳,不要进下一步。
先定第 ② 层协议
你们已有 Agent 基建。如果它不是 AI SDK 写的,让它输出 UI Message Stream(SSE +
x-vercel-ai-ui-message-stream: v1头),前端就能直接用useChat。这一步定了,后面都好办。stream-protocoldata-* 命名约定message metadata跑通最小对话
useChat+DefaultChatTransport+ text part 渲染。鉴权头、租户信息放在 Transport 的 headers / body 里。useChatTransportstatusstop()接入 3 个只读工具
一个服务端、一个客户端自动、一个 dynamic(MCP)。目标是 Tool 模式的全部状态都见过一次。
tool()onToolCalladdToolOutputdynamic-tool加上计划
按第 06 章做法 A 加
updatePlan,前端渲染成 Plan。用prepareStep强制首步规划。prepareStepstopWhenhasToolCall加上写操作和审批
写操作统一走
toolApproval,前端 Confirmation 卡片。权限判断放在审批函数里,读runtimeContext,不要信前端传来的角色。toolApprovaladdToolApprovalResponseoutput-denied持久化与恢复
服务端存 UIMessage;服务端生成消息 id;只发最后一条消息;断线后恢复流。侧边栏会被频繁收起和展开,这一步比聊天页更重要。
message persistenceresume streamsvalidateUIMessages换上渲染层
到这一步再装 AI Elements,从 ui.shadcn.com 装,不要用即将弃用的
ai-elementsCLI。窄侧边栏里优先用 Trace 的折叠能力,默认折起工具输出。shadcn add trace code-blockChatToolPlanConfirmation可观测
生命周期回调记录每步耗时、工具错误、审批拒绝率;这三个数决定你下一步优化哪里。
onStepEndonToolExecutionEndtelemetry
[来源: ai@7.0.143 docs/04-ai-sdk-ui/*、03-agents/*;ui.shadcn.com/docs/ai-elements]
墨笺的助理为什么只会问答
读了 src/editor/aiMethods.ts 和 scripts/agent-bridge-*.js(提交 2a88f07)。墨笺其实已经有 Agent 模式:桥接进程拉起本机的 claude -p / codex exec,它们本身就是 Agent 循环。问题出在循环外面的四处设计:
| 环节 | 墨笺现状(代码事实) | 任务型的做法 |
|---|---|---|
| 要不要用工具 | 正则匹配关键词决定 chat / agent 模式(_questionNeedsProjectTools,aiMethods.ts:60) | 始终给工具,由模型判断;只读工具不需要授权 |
| 授权 | 发送前 window.confirm,整轮授权(aiMethods.ts:79) | 按工具调用审批,看到具体参数再批 |
| 过程可见 | Claude 渠道只流出纯文本(engines.js claudeInvocation 仅 -p);只有 Codex 的 JSON 事件被转成粗粒度 progress(engines.js:215) | 每次工具调用都是一个 part,有输入、输出、状态 |
| 消息模型 | assistant 是一段 text + progress 数组 + meta 字符串 | parts[],顺序记录思考、工具、文本 |
| 流协议 | 自定义 SSE 事件:delta / progress / artifacts / document-updated / meta / session-reset / error(aiMethods.ts:630) | UI Message Stream 标准事件 + 少量 data-* |
| 计划 | 无 | updatePlan 工具 + Plan 渲染 |
| 改原文 | CLI 改 scratch 文件,结束后整篇回写(agent-bridge-agent.js readAgentDocumentUpdate) | 客户端交互工具 proposeEdit:展示 diff,用户逐处采纳 |
三期改造,每期都能发布
一期 · 统一流协议(不改交互)
桥接层把 CLI 事件翻译成 UI Message Stream。Claude 改用 stream-json 输出(Claude Code 的
--output-format stream-json,需配合--verbose,接入前在本机确认参数),把其中的 tool_use / tool_result 映射为tool-input-available/tool-output-available;Codex 的item.*事件同理。现有meta、document-updated改成data-mojian-meta、data-document-updated。前端不引入 React 组件,用
ai包里与框架无关的readUIMessageStream把事件合成 UIMessage,替换手写的 SSE 解析(aiMethods.ts:677 起的 while 循环)。渲染函数改为遍历 parts。agent-bridge-engines.jsaiMethods.tsreadUIMessageStream二期 · 规划与审批
去掉正则路由和
window.confirm。分两条路径:文档类任务(改写、整理、发布):在桥接进程里跑自己的
ToolLoopAgent,工具都是墨笺领域工具:readDocument、readSelection(客户端)、proposeEdit(客户端交互)、updatePlan、publishToFeishu/publishToDingTalk(需审批)。粒度细,能逐步确认。工程类任务(读代码、跑命令):继续用 CLI。CLI 在非交互模式下很难中途等前端审批,所以保留白名单做边界。AI SDK 7 的
HarnessAgent可以把 Claude Code / Codex 包成同一套流,但它目前标注为 experimental,且 Claude Code、Codex 适配器的运行位置是 sandbox bridge 而非本机进程,与墨笺「直接读本机工程」的定位不一致,先观察。ToolLoopAgenttoolApprovalproposeEditupdatePlan三期 · 墨笺版渲染层
按 AI Elements 的模式,用墨笺自己的样式实现 5 个渲染器:Message(沿用现有 marked 渲染)、Reasoning(默认折叠)、Tool 步骤行(状态 → 图标)、Confirmation 卡片、Plan 清单。一个统一的步骤行组件撑起后三者。
aiPanel.css新文件 aiPartsRender.ts
aiMethods.ts 现在 798 行,一期就必须把流解析和渲染拆出去;交付前跑 npm run check。一期的最小验证
在桥接层加一个开关,同一个问题分别走旧事件和新协议,对比三项:首字延迟、工具调用是否全部可见、文档回写是否一致。三项都持平或更好,再删旧路径。
学完的标志
勾选会保存在这台浏览器里。
来源
- Vercel Academy · AI Elements 课程
- shadcn/ui changelog 2026-10 · Trace、Code Block 与 AI Elements
- shadcn/ui changelog 2026-06 · Chat 组件
- ui.shadcn.com/docs/ai-elements(Tool、Confirmation、Plan 各页)
- vercel/ai-elements README(弃用公告)
- elements.ai-sdk.dev/llms.txt
- AI SDK · Loop Control;ToolLoopAgent 参考
- npm 包
ai@7.0.143自带 docs 目录(agents、ai-sdk-ui、ai-sdk-harnesses),2026-10-17 读取 - xiaoji121/mojian-markdown @ 2a88f07(2026-10-15)