ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

给Nanobot写UI:从CLI到可视化Agent交互的设计与实现

给Nanobot写UI:从CLI到可视化Agent交互的设计与实现 给Nanobot写UI这事完全是我自己给自己找的活。Nanobot这个项目我用了大概两个月最初只是当个命令行工具用本地跑AI Agent场景是真的顺手——轻量、能挂多个模型提供商、工具调用直接靠自然语言。但问题也恰恰出在这里纯CLI工具在交互层能给你的东西太少了。所以后来我动了念头抽了几天时间给它写了个Web界面取名叫NanobotUI。这篇文章就把我的设计思路、实现细节、踩过的坑都整理出来给同样在用Nanobot、或者打算给命令行工具写前端的同学做个参考。NanobotUI做的事情简单说就是给Nanobot套上一层图形界面核心能力包括流式对话展示、工具调用过程可视化、多会话管理、历史记录持久化、模型和参数切换。它解决的核心问题是让不习惯终端的用户可以平缓上手也让习惯终端的人能在更直观的界面里审查Agent思考过程。适合的人群主要有三类正在用Nanobot但觉得纯命令行看推理过程太费眼的开发者想给AI Agent工具做前端、但不确定界面层该怎么设计的同学还有单纯对给开源CLI项目写UI这件事感兴趣的折腾型玩家。1. 为什么给Nanobot写UI1.1 Nanobot本身是什么先简单交代一下Nanobot是什么方便没接触过的朋友衔接后面的内容。Nanobot是一个本地优先的AI Agent框架用Go写的核心思路是自然语言定义工具调用。你在配置文件里声明好工具日常使用的时候直接用大白话提需求Nanobot会自己判断该调用哪个工具、传什么参数再拿工具的返回结果继续往下推理。它支持OpenAI、Anthropic、Gemini、Ollama这些主流模型提供商也支持MCP协议本地文件、API服务都能接进来。这个项目的设计理念很明确把复杂的东西藏起来让你专注在跟Agent对话本身。启动之后是一个轻量的命令行交互输入问题模型推理如果需要工具就自动调用工具然后继续。整个过程在终端里是能看到日志的模型每步在想什么、调用了什么函数、参数是什么都会以文本形式打出来。1.2 纯命令行交互的痛点命令行工具虽然灵活但实际用起来有几个很现实的问题。第一推理过程的展示逻辑是平铺的。模型在思考、调用工具、拿到结果、继续推理这些信息在终端里全是一行行地打印出来。如果Agent连着调了三四个工具屏幕上就是一坨挤在一起的文本你根本分不清哪条是模型的最终回答、哪条是工具返回结果、哪条是中间的推理过程。对话稍微长一点想回头翻某一步操作只能靠滚轮和眼睛硬找。第二没有上下文的结构化展示。你问一个问题Agent调用了你本地的Python脚本返回了一堆JSON结果然后基于这个结果继续回答。终端里那堆JSON打印出来非常占地方而且没有语法高亮辨识度极低。你要是想让一整个会话能被别人看到直接截图发过去对方大概率也看不懂。第三模型切换和参数调节靠记命令行。Nanobot本身支持多提供商多模型配置但每次用CLI的时候必须通过参数或环境变量指定用久了容易忘来回切换也很烦。1.3 NanobotUI的定位我想要的界面不复杂但需要把Agent的执行过程这件事讲清楚。核心需求有三条消息要分角色展示、工具调用要单独卡式呈现、会话要能保存和回看。这也对应着Agent UI产品里最常见的三个要素对话流、工具调用轨迹、会话状态。说得直白一点我想把它做成类似ChatGPT那样的大气泡对话风格但左侧加一个会话栏中间是对话窗口每个工具调用渲染成一张独立的折叠卡片点开能看到完整的输入参数和返回结果。这样Agent执行了什么操作每一步是什么状态一目了然。2. 整体架构与设计思路2.1 功能边界与核心交互动手之前我先给自己画了几条边界避免做着做着就失控。只做Web端不做桌面端。Nanobot本身是本地服务Web前端够用装个浏览器就能开。身份定位是前端界面层不是重写后端。所有与模型提供商的通信、工具调度、推理过程都在Nanobot内部完成UI只负责把现有数据流可视化。会话数据默认保存在浏览器本地不额外堆后端存储。界面要支持明暗主题移动端能看能用优先保证桌面体验。这个边界定下来之后整个项目的重心就很明确了怎么把Nanobot暴露出来的信息以最舒服的方式呈现给用户。2.2 技术选型为什么是Vue 3技术选型我纠结了不到半天最后还是选了Vue 3加Vite。原因有三一是Vue 3的组合式API写起来顺手逻辑复用简单特别是像WebSocket重连这种有状态逻辑用composable封起来很干净二是生态成熟Element Plus、Naive UI这些组件库随便挑三是Vite的开发体验确实好秒级热更新改完代码立刻能看到效果。组件库方面我对比了Element Plus和Naive UI。Element Plus组件全文档详细但样式识别度太高一眼就是后台管理系统的味道。Naive UI更轻风格更现代主题定制也灵活。考虑到我想要的聊天界面气质偏产品化Naive UI更合适。这里有个小建议如果你也想给某个CLI工具写UI不要在技术选型上反复横跳。随便选一个你熟悉的框架能快速跑起来才是最重要的。界面好不好看后面慢慢调都来得及。2.3 与Nanobot的通信方式Nanobot支持通过--serve参数启动本地HTTP服务这就好办了。我的方案是NanobotUI作为一个静态前端工程通过HTTP接口与Nanobot通信对话消息的流式返回用SSEServer-Sent Events接收会话列表和工具调用记录通过REST接口拉取。前端工程和Nanobot进程是两个独立的东西。你本地起一个Nanobot服务再在另一个端口起NanobotUI的静态服务前端配一个后端地址就能连上。这样解耦的好处是你完全可以把NanobotUI部署到别的机器上只要网络能访问到Nanobot的端口就行。3. 核心功能实现细节3.1 流式对话输出AI对话UI最核心的体验就是流式输出。模型一个字一个字往外蹦的时候前端要实时渲染不能有卡顿也不能等到全部生成完再一次性显示。我用的是SSE。Nanobot在流式返回的时候会以事件流的形式不断推送增量内容前端收到一个chunk就追加到当前消息的显示区。具体做法是用EventSource或者fetch的ReadableStream来读取数据流每拿到一段就更新Vue的响应式数据。这里的关键点是不要在每次更新时重新渲染整条消息而是维护一个累积字符串只更新显示文本对应的DOM。Vue的虚拟DOM diff本身就做了优化实际用下来Vue 3在长文本流式渲染场景下足够流畅。消息渲染的时候要注意一个问题Markdown是在流式过程中做的增量解析还是等整个消息接收完再一次性渲染我最初是一股脑往v-html里灌Markdown渲染结果发现模型在生成一半的时候Markdown语法往往是残缺的比如代码块只有一个开始的三反引号还没闭合渲染出来的样式一团糟。后来我改成节流渲染以50到100毫秒为间隔做一次Markdown解析和重绘。这样既保证了实时性又不会每敲一个字就去跑一次解析性能也能接受。// streaming过程中定时渲染的简化逻辑 let renderTimer null const scheduleRender () { if (renderTimer) return renderTimer setTimeout(() { renderMarkdown(currentContent.value) renderTimer null }, 80) }3.2 工具调用的可视化还原工具调用是NanobotUI和普通聊天UI最大的区别点也是最花心思的部分。默认情况下Nanobot在调用工具时会输出类似call_tool: python_script这样的日志行参数以JSON格式打印。我的做法是在流式数据处理时把工具调用的信息单独截获出来不混在对话气泡里而是沉淀成一个结构化的ToolCall对象渲染成一张独立的卡片。这张卡片包含这么几个部分工具名称、调用状态执行中、成功、失败、输入参数、返回结果。默认折叠点开展开这样对话流不会被大段JSON污染需要看细节的时候又能展开检查。// 数据结构参考 { id: call_abc123, toolName: python_script, status: success, input: { script: print(hello) }, output: hello, startTime: 1710000000000, endTime: 1710000001200 }这里我最想强调的是执行状态这个字段。Agent调用工具是有耗时和失败率的如果UI上能实时显示正在执行中的转圈状态体验会提升很多。我通过流式消息里的状态位来同步这个卡片的状态工具执行完成后把最终结果回填进去。整个执行链条在界面上看起来非常清晰模型说了一句话然后卡片显示调用Python脚本成功模型基于返回结果继续说下一句。3.3 会话管理与历史记录会话管理我采用的是本地优先方案。所有会话数据用IndexedDB存浏览器里结构上用一个会话列表加消息列表每条消息带上工具调用ID的引用方便还原。选IndexedDB而不是localStorage是因为消息体里可能包含大段代码和JSONlocalStorage只能存字符串且容量有限通常5MB左右IndexedDB的容量和结构化存储能力都更胜一筹。我用idb-keyval这个库包了一层API简单读写的代码量也不大。// idb-keyval 读写示例 import { get, set, keys } from idb-keyval const saveSession async (session) { await set(session:${session.id}, session) }会话管理看起来是个不起眼的模块但实际是日常使用频率最高的功能。我加了几个设计会话标题默认取第一句话的前20个字侧边栏支持搜索单个会话可以删除全部会话可以一键清空。对于本地工具类的Agent来说会话本身就是工作日志能保存并能随时回看价值非常大。3.4 Markdown渲染与代码高亮Markdown渲染我没有用现成的完整组件而是选了marked加highlight.js的组合。marked负责把Markdown转HTML代码块的语法高亮单独交给highlight.js处理。之所以不直接用v-html一把梭是因为需要处理代码安全、样式冲突和流式渲染的兼容性。marked支持自定义渲染器我可以对代码块单独做处理在渲染前先把内容转义再交给高亮器。这样既能防止HTML注入又能保证代码块高亮正常。样式方面我针对代码块做了暗色主题适配。对话里的代码块和普通文本的视觉区分要明显因为Agent工具调用返回的结果里经常含代码片段看的人需要快速抓住代码内容。浅色主题下用浅灰背景加左边框暗色主题下用深灰背景加细微内阴影效果都还不错。4. 踩坑记录与排查实录4.1 SSE断线重连与消息乱序实战中第一个让我头疼的问题就是SSE连接不稳定。Nanobot服务偶尔会因为本地网络波动或者长时间空闲主动断开连接前端一旦断掉正在生成的回复就卡在半截重新连上之后又不知道从哪里续上。我的处理办法分两层。第一层是NanobotUI本地做了自动重连断线后每隔3秒尝试重新建立连接如果当前消息还没结束重连成功后从Nanobot拉取当前会话的完整最新状态用增量补齐的方式刷新界面。第二层是给每一条消息都打上时间戳和序号前端渲染时按序号排序防止重连后消息顺序错乱。// 断线重连的简化逻辑 const connect () { source new EventSource(apiUrl) source.onerror () { source.close() setTimeout(connect, 3000) } }这个坑给我的教训是任何涉及网络状态的前端应用一定要把断线重连后数据如何对齐想在前头不要等真断了再临场设计。4.2 流式Markdown的XSS风险流式渲染还有个安全隐患模型生成的内容里如果包含HTML标签直接渲染到页面里会有XSS风险。比如你让模型输出一个HTML页面模型真的输出了带script标签的内容如果前端直接不转义渲染代码就直接执行了这是个很严重的坑。我的解决办法是在marked里把所有原始字符串先做HTML转义再走Markdown解析。同时代码块内容一律通过highlight.js的highlightElement处理经过这样两道过滤之后即使模型输出恶意结构也不会执行。这个点必须重视。给AI工具做前端和给普通内容展示做前端的风险边界完全不一样——模型输出不可控的程度远高于用户输入所有渲染路径都要假定内容是不可信的。4.3 工具调用JSON的递归渲染刚开始做工具卡片的时候我直接把JSON序列化成字符串塞进pre里凑合能用但一旦工具返回嵌套很深的对象看的人要自己在字符串里数括号体验极差。后来我写了一个递归渲染JSON的组件遇到对象就缩进展开数组就渲染成列表基础类型直接显示。嵌套层级也能控制默认展开两层更深的内容折叠起来。这样工具返回的结果在卡片里就是结构化的可读性提升了一大截。!-- 递归渲染JSON的组件骨架 -- template div classjson-viewer template v-iftypeof value object value ! null div v-for(val, key) in value :keykey span classkey{{ key }}/span: JsonViewer :valueval / /div /template span v-else{{ value }}/span /div /template4.4 界面卡顿与长会话性能长会话场景下界面会变卡这个问题在对话超过上百条消息后开始显现。一开始没优化每条消息都完整渲染整个页面的DOM节点数量爆炸滚动和输入都有明显延迟。优化方案做了三步。第一步是虚拟滚动只渲染视口附近的几条消息其他消息用占位高度撑住滚动条。第二步是长内容折叠超过一定高度的代码块和JSON卡片默认折叠需要时再展开。第三步是样式隔离避免全局样式互相干扰导致重排开销变大。三步做下来即使上千条消息的会话也能保持滚动流畅。这部分的优化逻辑对任何聊天类前端都有参考价值核心思路永远只有一句话不要渲染用户当前看不到的东西。5. 工具/方案对比与部署扩展5.1 与现有方案对比做之前我也翻了翻社区给其他AI CLI工具写UI的项目有一些但针对Nanobot的成熟图形界面并不多。要么是简单的聊天壳子只有输入框和气泡式输出没有工具调用轨迹的展示要么是维护状态不活跃依赖的依赖版本已经过时就懒得动。NanobotUI比较鲜明的差异是把工具调用可视化作为一个一等公民来处理而不是把它藏在消息里。另外本地优先的会话存储、离线可用的静态页面、一次起服务就能用的部署方式对日常使用者来说都更友好。5.2 快速部署与使用指引如果你想自己跑起来试试步骤非常简单。先确保本机已经装好Nanobot并配好模型提供商然后执行nanobot --serve把后端服务跑起来。接着克隆NanobotUI代码配置VITE_NANOBOT_API_URL指向Nanobot的地址执行npm install和npm run dev浏览器打开本地开发端口就能看到界面了。生产部署更简单npm run build之后把静态产物扔到任意Web服务器Nginx、Caddy都行或者直接用vite preview预览访问一个静态页面就完事。前端只有一个静态目录不依赖服务器端渲染和数据库。5.3 后续迭代计划目前用的顺手但还有一些想加的东西排在计划里。第一个是MCP工具市场。既然Nanobot已经支持MCP以后在UI侧做一个可视化的工具管理面板让用户能浏览、启用、停用可用的MCP工具比纯写配置文件直观得多。第二个是Prompt模板。把常用的系统提示词和任务模板做成可保存的预设一键填充到输入框省去反复写的功夫。第三个是更精细的模型参数面板。温度、top_p、max_tokens这些参数直接在界面上放滑杆和数字输入框面向非开发者的使用场景会更友好。这个项目给我最深的体会是给命令行工具补一个UI看起来是面子工程实际上是在打磨交互的本质。一个工具如果能把内部决策过程用清晰、结构化的方式展示给用户用户对它的信任感会成倍增加。NanobotUI目前还谈不上完美但它确实让我的日常Agent使用频率上了一个台阶——毕竟很多时候界面友好本身就是一种生产力。
返回列表