LobeHub Builtin Tool UI 设计原则:让聊天流中的工具调用可读、可信、可验收
本文基于 LobeHub 仓库中 builtin-tool 技能包的设计原则文档(principles.md),系统讲解一套 builtin tool 的六个客户端界面(Inspector / Render / Placeholder / Streaming / Intervention / Portal)“应该做什么、做到什么程度”的判定标准。读完本文,你将掌握:为工具调用编写折叠态可读的 Inspector chip、按执行生命周期切换 loading/completed 文案、决定何时添加 Render/Streaming/Intervention,以及遵循样式与 Devtools 验收约定的完整方法,并在 LobeHub 源码中找到每条原则的落地证据。
一、背景:工具调用为什么需要专门的设计原则
在 LobeHub 中,builtin tool 是 Agent 运行时可调用的能力单元(如 Web 搜索、文件读写、子 Agent 调度)。一次工具调用在聊天界面中会经过多个阶段:参数还在流式生成、执行器运行中、结果返回、失败、空结果、超长结果……每个阶段用户看到的都是一行 chip 或一张结果卡片。
LobeHub 为每个 builtin tool 定义了最多六个客户端界面(见 ui/README.md):
| Surface | 是否必需 | 聊天中何时展示 | 注册位置 |
|---|---|---|---|
| Inspector | ✅ 始终需要 | 每个工具调用头部的一行 chip | packages/builtin-tools/src/inspectors.ts |
| Render | 可选 | 调用返回后的富结果卡片 | packages/builtin-tools/src/renders.ts |
| Placeholder | 可选 | “参数流式完成”与“结果到达”之间的骨架 | packages/builtin-tools/src/placeholders.ts |
| Streaming | 可选 | 执行期间的实时输出(如命令 stdout) | packages/builtin-tools/src/streamings.ts |
| Intervention | 可选 | humanIntervention 触发时的确认/编辑前执行对话框 |
packages/builtin-tools/src/interventions.ts |
| Portal | 可选 | 全屏详情视图(右侧或弹窗) | packages/builtin-tools/src/portals.ts |
只有 Inspector 是强制的,其余五个按需添加。Inspector 注册表通过 registerBuiltinInspectors 按 identifier -> apiName 两级组织,UI 侧通过 getBuiltinInspector(identifier, apiName) 取组件,实现见 inspectors.ts#L43-L53。官方建议端到端阅读的两个参考实现:
packages/builtin-tool-web-browsing/src/client/:Inspector + Render + Placeholder + Portal(无 Intervention/Streaming);packages/builtin-tool-local-system/src/client/:全部六个表面,含共享组件components/。
principles.md 的 15 条原则正是回答“这六个表面应该做什么、做到什么程度”的判定标准。下面按主题逐组展开。
二、Inspector 三条基石原则:折叠态可读、一句话、覆盖全生命周期
原则 1:先保证折叠态可读
每个 API 都必须有 Inspector;用户不展开也应该能看懂“正在做什么 / 对什么做 / 当前结果是什么”。Inspector 不应该只展示函数名和原始参数——这一条直接排除了 executeCode({ code: "..." }) 这类裸 dump 式展示。
原则 2:Inspector 是一句话,不是详情页
优先表达动作、关键对象、数量、状态,例如“分析图片 3 张”“搜索 12 个结果”“读取 config.json”。长文本、列表和结构化结果放到 Render 或 Portal——这保证了 Inspector 永远是一行 chip,不会撑爆聊天气泡。
原则 3:Inspector 要覆盖执行生命周期
args 还在 streaming、工具执行中、执行完成、执行失败时都应该有稳定展示;必要时同时读取 args、partialArgs 和 pluginState,避免出现空白、跳变或只显示半截参数。
LobeHub 将生命周期形式化为状态机(见 inspector.md):
| 阶段 | 可用数据 | 应展示内容 |
|---|---|---|
| 参数流式中,尚无有效字段 | isArgumentsStreaming === true,partialArgs.X 未定义 |
仅 API 标题,加 shinyTextStyles.shinyText 脉动动画 |
| 参数流式中,关键字段已到达 | partialArgs.X 已有值 |
标题 + 关键字段 chip,保持脉动动画 |
| 参数完整,执行器运行中 | args 已填充,isLoading === true |
同上,保持脉动 |
| 结果已到达 | pluginState 已填充,isLoading === false |
标题 + chips + 结果摘要(数量、标识、状态) |
对应的 Props 契约为 BuiltinInspectorProps<Args, State>,其中 args 是最终参数(助手停止流式输出后才有)、partialArgs 是流式中的部分 JSON、pluginState 是执行器成功后的 state。
源码印证——Search 的规范实现。 SearchInspector 完整演示了状态机:参数流式且 query 未到达时只渲染 t('builtins.lobe-web-browsing.apiName.search') 标题并加 shinyText;query 到达后追加高亮 chip;只有当 !isLoading && !isArgumentsStreaming && pluginState?.results 三者同时满足,才在尾部追加结果数 (N) 或 (no results) 提示——这正是原则 3 中“不要只显示半截参数”与“结果后缀只能在加载结束后出现”的落地。
三、原则 4 深挖:文案必须随状态切换时态
同一条 chip 会永久留在聊天记录里。如果执行完成后文案仍挂着“正在创建任务”,几小时后回看历史时会读起来像工具还在跑。因此约定:
- 执行中用现在进行时(“正在创建任务 / Creating task / 正在搜索”);
- 执行完成后切到完成态(“已创建任务 / Task created / 已找到 N 条”);
- 约定的 i18n 形式是
<api>.loading/<api>.completed一对键,渲染时按isArgumentsStreaming || isLoading决定取哪一个; - 只读 / 查询类动作(如“查看任务”,本来就是名词性的)可以共用一个键。
仓库中的 i18n 键证据。 在 packages/locales/src/default/plugin.ts 中可以确认这对键的约定确实存在,例如:
'builtins.lobe-claude-code.task.create.loading': 'Creating task: ',
'builtins.lobe-claude-code.task.create.completed': 'Task created: ',
文档中点名的 lobe-agent.apiName.callSubAgent.{loading,completed} 与 lobe-claude-code.task.{create,list,update,get}.{loading,completed} 均属于这一约定。
源码印证——CallSubAgentInspector 的规范模式。 CallSubAgentInspector 展示了另一个关键细节:当 pluginState 中既有运行期流式统计(progress)又有完成期权威统计时,优先取平铺的终态字段,保证统计尾巴“一旦完成就不再回退到过期的实时样本”:
const hasFinalStats =
pluginState?.totalTokens !== undefined || pluginState?.totalToolCalls !== undefined;
const stats = hasFinalStats ? pluginState : pluginState?.progress;
其折叠行布局为:机器人图标 + 标题(流式/加载期间加 shinyText 脉动)+ description chip + 实时累计的“工具调用数 · 模型 · tokens”统计尾巴,恰好是原则 2“动作 + 关键对象 + 数量 + 状态”一句话原则的具象化。
四、Render、Placeholder、Streaming 的增删判定(原则 5–9)
原则 5:只有结构化结果才需要 Render。 如果工具结果只是自然语言总结,通常不需要 Render;如果结果包含列表、媒体、文件、表格、代码、diff、地图、时间线、权限请求等结构,就应该提供 Render。
原则 6:Render 要帮助用户检查结果,而不是复述参数。 Render 的主体围绕工具产物组织:可预览、可比较、可筛选、可定位。参数只作为上下文辅助出现,不要把 Render 做成一块更大的 args dump。
原则 7:参数和结果要一起参与渲染。 好的 Tool UI 同时用 args 解释意图、用 pluginState 展示真实执行结果;但 pluginState 只放结果域数据,不要反向塞入可以从 args 推导出的内容。这与顶层工具作者指南中“state 只放结果域,绝不回显全部参数”的要求一致(见 SKILL.md)。
原则 8:慢操作要有 Placeholder。 如果工具通常需要等待网络、文件系统、模型或外部进程,Placeholder 应该先占住最终 Render 的版式,让用户知道即将看到什么,而不是只显示一个泛化的 loading。
原则 9:Streaming 只用于连续产物。 搜索列表、日志、长文本、文件分析、分阶段计划适合 Streaming;一次性小结果不需要强行做 Streaming。Streaming UI 要能渐进追加,并且完成后自然过渡到最终 Render。
这几条合起来构成一张判定表:无结构化结果 → 不加 Render;结果一次性且小 → 不加 Streaming;有可感知延迟 → 加 Placeholder;输出是连续流 → 加 Streaming。
五、风险动作、正式状态与信息密度(原则 10–12)
原则 10:有风险的动作必须 Intervention。 写文件、删除、发送、安装、执行命令、外部可见操作、权限敏感操作,都应该在执行前给出可理解的确认界面;确认文案要说明影响范围,而不是只问“是否继续”。参考实现是 builtin-tool-local-system 的 Intervention 组件(按 API 逐一提供审批组件)。
原则 11:错误、空态和截断都是正式状态。 Render 不能在失败、无结果、超长结果时退化成空白:
- 错误要说明发生在哪一步;
- 空态要告诉用户没有产物(Search Inspector 的
(no results)提示就是空态的一行式处理); - 超长内容要明确“展示前 N 项 / 还有 N 项”。
原则 12:信息密度要克制。 默认展示最有判断价值的部分:标题、来源、状态、摘要、少量关键字段。大对象、长列表、原文、调试数据放进可展开区域或 Portal,避免把聊天流撑成后台管理页。
六、视觉融入与验收闭环(原则 13–15)
原则 13:视觉上融入聊天流。 Tool UI 应使用 @lobehub/ui / base-ui、Flexbox、createStaticStyles 和 cssVar.*,遵循现有间距、圆角、颜色、字号;不要为单个工具发明一套独立视觉语言。跨表面的细则见 shared-rules.md,其中两个高频约定值得特别注意:
- 零运行时 CSS-in-JS:样式用
createStaticStyles + cssVar.*编译一次、运行时读取 CSS 变量,仅极少数需要运行时 token 计算时才退回createStyles + token; - 保持单层,不要卡片套卡片:框架已把每个 Render / Intervention 包进工具卡片,最外层容器不应再有填充背景;至多一个带填充的内容盒(用
colorFillTertiary),标签、键值对、chip 应平铺在表面上,用间距或 1px 分隔线而非嵌套盒子区分。
原则 14:Devtools fixture 是验收入口。 新增或修改 Tool UI 时,应在 /devtools 里准备覆盖典型态、loading/streaming、空态、错误态、长内容态的 fixture;一个 API 如果在真实聊天里会出现,就不应该在 devtools 中缺席。这条把原则 3、11 的“生命周期全覆盖”“错误/空态是正式状态”变成了可执行的验收动作。
原则 15:先做用户会看的 UI,再做调试 UI。 Raw JSON、trace、schema、内部 id 可以存在,但应默认收起或放到调试区;主界面先回答用户最关心的问题:工具做了什么,结果值不值得信任,下一步能做什么。
七、把 15 条原则用起来:一份自检清单
结合原则文档与 SKILL.md 中的 Authoring Checklist,提交一个工具 UI 前可以这样自检:
| 检查项 | 对应原则 |
|---|---|
| 每个 API 都有 Inspector,且覆盖 streaming / loading / 完成 / 失败四阶段 | 1、3 |
| Inspector 是一行“动作 + 对象 + 数量 + 状态”,无裸参数 dump | 2 |
进行性动作定义了 <api>.loading / <api>.completed 键对,按 isArgumentsStreaming || isLoading 切换 |
4 |
Render 只为结构化结果创建,主体围绕产物而非参数;pluginState 只放结果域数据 |
5、6、7 |
| 有可感知延迟的 API 有占住最终版式的 Placeholder | 8 |
| 连续产物才有 Streaming,且完成后过渡到最终 Render | 9 |
| 写/删/发/装/执行类动作有说明影响范围的 Intervention | 10 |
| 错误指明步骤、空态说明无产物、超长明确“前 N 项 / 还有 N 项” | 11 |
| 默认只展示高判断价值字段,重内容进可展开区或 Portal | 12 |
样式走 @lobehub/ui + createStaticStyles + cssVar.*,无卡片套卡片 |
13 |
/devtools 有覆盖典型/loading/空/错/长内容的 fixture |
14 |
| Raw JSON / trace 默认收起,主界面先回答“做了什么、值不值得信、下一步做什么” | 15 |
这套原则的本质,是把“聊天流中的工具调用”当作一条有完整生命周期的信息流来设计:每个阶段都有确定性的最小可读展示,每个风险动作都有可理解的确认,每个异常状态都是一等公民,而验收标准(devtools fixture)与运行标准(聊天 UI)使用同一套覆盖范围。对于正在为 Agent 产品编写工具 UI 的开发者,这 15 条原则可以直接作为设计评审的度量衡使用。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00