← 返回博客
AI 工作流 2026.10.17 41 分钟
调研笔记 · 2026-10-17

从问答到做事:
任务型侧边栏 Agent 是怎么搭起来的

以 Vercel AI Elements 和 AI SDK 7 为样本,拆开一个能规划、能调工具、能请你确认的侧边栏助理,一层一层看它由什么构成;最后回到墨笺 Markdown,对照现有代码给出改造路线。

11 章 · 约 40 分钟ai@7.0.143mojian-markdown@2a88f07
一页文档旁的侧边栏:对话气泡沿虚线流向任务清单与执行齿轮,象征 AI 助理从问答走向做事
Chapter 00 · 先说结论

三个会改变你计划的发现

核心结论:AI Elements 本身只是渲染层,任务能力来自它下面的 AI SDK 运行时和 UIMessage.parts 协议;你要学的是这套协议,组件库可以随时换。

1

你看的那个库要退役了

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]

2

UI 是消息 parts 的投影

每个组件对应一种 part 或一个 part 状态:tool-* 的 approval-requested 渲染成确认卡,data-plan 渲染成计划。理解 parts,组件只是查表。

[来源: ui.shadcn.com/docs/ai-elements Tool / Plan / Confirmation]

3

墨笺不能直接装这些组件

墨笺前端是 DC runtime + 命令式渲染,没有 Tailwind 和 shadcn。能搬过去的是协议(UI Message Stream)和模式(Plan / Tool / Confirmation 的状态设计),渲染用墨笺自己的样式写。

[来源: mojian-markdown package.json、src/editor/aiMethods.ts]

Chapter 01 · 心智模型

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 Elements 模式:Message · Tool · Plan · Confirmation · Reasoning … 按 part.type / part.state 选组件 ③ 会话状态层 useChat / Chat:messages[]、status、sendMessage、addToolOutput、addToolApprovalResponse、Transport ② 流协议层 UI Message Stream(SSE,头 x-vercel-ai-ui-message-stream: v1):text-delta · tool-input-available · data-* … ① 运行时层 ToolLoopAgent / HarnessAgent / 你自己的 Agent 基建:模型、工具、循环、审批策略、上下文
第 ② 层是关键接口。只要后端吐出这个协议,第 ③④ 层就能直接工作;后端用 Python 还是 Java 都行。

[来源: ai@7.0.143 docs/04-ai-sdk-ui/50-stream-protocol.mdx、20-streaming-data.mdx]

Chapter 02 · 核心机制

Agent 的基础元素就是 parts

AI SDK 5 之后,一条消息不再是一段字符串,而是 parts 数组。一次任务里 Agent 想了什么、调了什么、产出了什么,都按顺序变成 part。AI Elements 定义「Agent 元素」的方式,就是给每类 part 配一个渲染模式。

part.type是什么关键字段 / 状态对应模式
text正文,流式增量text、state: streaming | doneMessage(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、titleSources、Inline Citation
file附件、生成的文件url、mediaTypePrompt 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 parts:服务端写 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 个模式清单]

Chapter 03 · 设计理念

模式,而不是包

AI Elements 的几条设计原则,直接决定了你接入时能改什么、该改什么:

  1. 代码归你

    没有 npm 包。CLI 把源码拷进 components/ai-elements/,你直接改。代价是没有自动升级,这也是这次迁移「不必立即动手」的原因。

  2. 组合式组件

    Conversation 包 ConversationContent,Message 包 MessageContent 再包 MessageResponse。每块只做一件事,替换其中一块不影响其他块。

  3. 所有 Agent 步骤一个形状

    shadcn 版把 Tool、Plan、Reasoning、Subagent 都建在 Trace 上:一行 = 图标 + 标题 + 可选描述 + 状态(running / error / pending),可折叠、可嵌套。界面统一,用户扫一眼就知道 Agent 走到哪。

  4. 不碰状态

    6 月发布的 MessageScroller 只管滚动,不管消息、AI 状态、传输和持久化;滚动逻辑另有无样式版本 @shadcn/react。数据层由你接。

  5. Props 贴着 SDK 类型

    组件吃的就是 UIMessage 的 part。迁移时导出名和 props 不变,只换渲染,调用方代码不用改。

[来源: elements.ai-sdk.dev/llms.txt;ui.shadcn.com changelog 2026-06、2026-10]

对你的启发:给墨笺写渲染层时照抄第 3、4 条。先定一个统一的「步骤行」组件,所有 Agent 活动都用它表达;渲染函数只吃 parts,不持有会话状态。
Chapter 04 · 工具状态机

一次工具调用的七种状态

Tool 和 Confirmation 两个模式的全部逻辑,就是对下面这个状态机做 switch。把它记住,你就能自己写任何工具卡片:

input-streaming input-available approval-requested approval-responded output-denied output-available output-error 参数生成完 需审批 用户点击 拒绝 允许后执行 直接执行(服务端 execute 或客户端 addToolOutput) 执行抛错
output-available 还可能带 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 模式此时显示为错误。
Chapter 05 · 动手看一遍

一次任务的流,逐帧回放

下面是一个墨笺场景的模拟:选中一段文字,让助理「改得更简洁,然后存到飞书」。左边是服务端发出的 UI Message Stream 事件,右边是前端把事件合并成 parts 后渲染的侧边栏。点「下一帧」,注意三件事:计划块是原地更新的;改写建议停下来等你采纳;发布动作要你审批。

模拟数据,事件格式按 ai@7 流协议文档编写;工具名为示意。

② 流事件 · SSE0 / 0
点「下一帧」开始。
④ 侧边栏渲染ready
Chapter 06 · 规划能力

「会规划」到底是哪段代码

问答系统和任务型助理的差别,不在模型,而在三处设计:模型能否看到并维护一份计划;计划能否被用户看见;每一步能否被执行和验证。AI SDK 给了两种做法:

做法 A · 推荐
  • 给模型一个 updatePlan 工具,让它自己写、自己勾。
  • Claude Code 的 TodoWrite 就是这个思路。
  • 计划由模型维护,前端把这个工具的 part 渲染成 Plan / Queue。
做法 B · 编排式
  • 服务端代码固定步骤,用 data-plan 推进度。
  • 适合流程固定的业务(审批流、报表生成)。
  • 可靠,但不是 Agent 自主规划。

做法 A 的服务端骨架(AI SDK 7):

agent.tsimport { ToolLoopAgent, tool, isStepCount, hasToolCall } from 'ai'; import { z } from 'zod'; export const sidebarAgent = new ToolLoopAgent({ model: 'anthropic/claude-sonnet-5.5', instructions: `先调用 updatePlan 列出步骤,每完成一步再调用一次更新状态。 完成全部步骤后调用 done 汇报结果。`, tools: { updatePlan: tool({ description: '写入或更新当前任务计划', inputSchema: z.object({ steps: z.array(z.object({ title: z.string(), status: z.enum(['pending', 'running', 'done']), })), }), execute: async ({ steps }) => ({ ok: true, steps }), }), readDocument, // 服务端工具:读正文 proposeEdit, // 客户端工具:没有 execute,交给浏览器 publishToFeishu,// 服务端工具:需要审批 done: tool({ inputSchema: z.object({ summary: z.string() }), execute: async ({ summary }) => ({ summary }) }), }, toolApproval: { publishToFeishu: 'user-approval' }, stopWhen: [isStepCount(30), hasToolCall('done')], });
app/api/chat/route.tsimport { createAgentUIStreamResponse } from 'ai'; export async function POST(req: Request) { const { messages } = await req.json(); return createAgentUIStreamResponse({ agent: sidebarAgent, uiMessages: messages }); }

[来源: 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]

Chapter 07 · 侧边栏特有

侧边栏和聊天页的区别:它住在别人家里

独立聊天页只需要对话。侧边栏寄生在宿主页面上,它的价值来自对宿主的读和写。这引出工具的第二种分法,比「读 / 写」更重要:

工具类型谁执行怎么写典型工具
服务端工具Agent 进程带 execute查数据库、调业务 API、发布到飞书
客户端自动工具浏览器,无需用户不带 execute;前端 onToolCall 里 addToolOutput读当前选区、读表单值、滚动到某处
客户端交互工具浏览器,等用户渲染 UI,用户操作后 addToolOutput展示 diff 让用户采纳、让用户选一个选项
Sidebar.tsxconst { messages, sendMessage, addToolOutput, addToolApprovalResponse } = useChat({ // 所有工具结果或审批都到齐后,自动发起下一轮 sendAutomaticallyWhen: ({ messages }) => lastAssistantMessageIsCompleteWithToolCalls({ messages }) || lastAssistantMessageIsCompleteWithApprovalResponses({ messages }), async onToolCall({ toolCall }) { if (toolCall.dynamic) return; if (toolCall.toolName === 'readSelection') { // 不要 await,避免死锁 addToolOutput({ tool: 'readSelection', toolCallId: toolCall.toolCallId, output: editor.getSelection() }); } }, });

[来源: ai@7.0.143 docs/04-ai-sdk-ui/03-chatbot-tool-usage.mdx]

上面把两个 helper 组合在一个函数里,是我根据 sendAutomaticallyWhen 接受函数这一点写的组合用法,文档分别示范了两个 helper。接入前在你的版本上跑一次确认。

宿主上下文怎么进模型

三种方式,按优先级:① 让模型用工具按需取(readSelection、readPage),上下文最省;② 用 sendMessage 的 body 或 message metadata 带上当前页面标识,服务端在 prepareCall 里注入;③ 整页塞进 system,只适合小页面。墨笺现在用的是 ③(首轮带全文)。

Chapter 08 · 搭建路径

业务侧边栏:按这个顺序学和建

每一步都能单独跑通、单独验证。前一步不稳,不要进下一步。

  1. 先定第 ② 层协议

    你们已有 Agent 基建。如果它不是 AI SDK 写的,让它输出 UI Message Stream(SSE + x-vercel-ai-ui-message-stream: v1 头),前端就能直接用 useChat。这一步定了,后面都好办。

    stream-protocoldata-* 命名约定message metadata
  2. 跑通最小对话

    useChat + DefaultChatTransport + text part 渲染。鉴权头、租户信息放在 Transport 的 headers / body 里。

    useChatTransportstatusstop()
  3. 接入 3 个只读工具

    一个服务端、一个客户端自动、一个 dynamic(MCP)。目标是 Tool 模式的全部状态都见过一次。

    tool()onToolCalladdToolOutputdynamic-tool
  4. 加上计划

    按第 06 章做法 A 加 updatePlan,前端渲染成 Plan。用 prepareStep 强制首步规划。

    prepareStepstopWhenhasToolCall
  5. 加上写操作和审批

    写操作统一走 toolApproval,前端 Confirmation 卡片。权限判断放在审批函数里,读 runtimeContext,不要信前端传来的角色。

    toolApprovaladdToolApprovalResponseoutput-denied
  6. 持久化与恢复

    服务端存 UIMessage;服务端生成消息 id;只发最后一条消息;断线后恢复流。侧边栏会被频繁收起和展开,这一步比聊天页更重要。

    message persistenceresume streamsvalidateUIMessages
  7. 换上渲染层

    到这一步再装 AI Elements,从 ui.shadcn.com 装,不要用即将弃用的 ai-elements CLI。窄侧边栏里优先用 Trace 的折叠能力,默认折起工具输出。

    shadcn add trace code-blockChatToolPlanConfirmation
  8. 可观测

    生命周期回调记录每步耗时、工具错误、审批拒绝率;这三个数决定你下一步优化哪里。

    onStepEndonToolExecutionEndtelemetry

[来源: ai@7.0.143 docs/04-ai-sdk-ui/*、03-agents/*;ui.shadcn.com/docs/ai-elements]

Chapter 09 · 墨笺现状诊断

墨笺的助理为什么只会问答

读了 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 已经有了;缺的是把循环里的每一步翻译给前端的那一层,以及让用户在步骤粒度上介入的机会。
Chapter 10 · 墨笺改造路线

三期改造,每期都能发布

  1. 一期 · 统一流协议(不改交互)

    桥接层把 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
  2. 二期 · 规划与审批

    去掉正则路由和 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
  3. 三期 · 墨笺版渲染层

    按 AI Elements 的模式,用墨笺自己的样式实现 5 个渲染器:Message(沿用现有 marked 渲染)、Reasoning(默认折叠)、Tool 步骤行(状态 → 图标)、Confirmation 卡片、Plan 清单。一个统一的步骤行组件撑起后三者。

    aiPanel.css新文件 aiPartsRender.ts
仓库 AGENTS.md 的约束同样适用:先写失败测试;单文件不超过 800 行,aiMethods.ts 现在 798 行,一期就必须把流解析和渲染拆出去;交付前跑 npm run check。

一期的最小验证

在桥接层加一个开关,同一个问题分别走旧事件和新协议,对比三项:首字延迟、工具调用是否全部可见、文档回写是否一致。三项都持平或更好,再删旧路径。

Chapter 11 · 练习与清单

学完的标志

勾选会保存在这台浏览器里。

来源