跳至主要内容
搜索Ctrl+K
博客
LeoChat:一个agent客户端的前世今生

| 15 分钟阅读 |

其实,叫它 LeoChat 很早就不准确了。LeoMCP?LeoAgent?LeoHarness?这些能力它都涉及,但哪个名字都不太对,所以一直没改。

开源地址:cybermanhao/LeoChat(MIT 协议)

它的前身项目刚启动时,coding agent 才刚刚面世(而且因为在国内不方便购买,我当时还没用过);而现在,它的功能早已被 coding agent 覆盖了(做 agent 客户端的,谁又能不被覆盖呢?╮(╯▽╰)╭)。它不是没有用,只是凭我一个人,实在很难做得比精英云集的团队更好。

DeepSeek Harness 开源以后,我决定停止这个项目的迭代。

论完成度,LeoChat 和 dsh 差得太远,以后要做类似的东西,我大概会直接拿 dsh 这类成熟项目来魔改。但它依然是我最自豪的作品。

这篇想聊聊它是什么,以及它给我带来了什么。

如果你想体验 LeoChat,可以去本站的资源下载站 leo-releases 下载最新版(Windows 解压即用);需要测试用的 API key 的话,可以发邮件到 cybermanhao@gmail.com 联系我。


做 LeoChat 之前

2025 年 5 月,我正在 B 站搜 RAG 相关的视频,首页忽然推给我一个视频,作者是我在知乎关注了半年的答主——锦恢。

视频在这里:OpenMCP 介绍。

值得一提的是,我抢到了第一个播放,可见我当时的 AI 焦虑有多深(笑)。

抢到 OpenMCP 介绍视频的第一个播放

视频主要介绍了 OpenMCP,一个比 MCP Inspector 更好用的 MCP 调试器。

看完我非常激动:我第一次了解到了 MCP 这个概念,这实在太令人着迷了。我第一次知道这么简单就可以把 AI 接进应用里。我马上做了一个游戏王 RAG 知识库,通过 MCP 接进了 OpenMCP。

游戏王 RAG 知识库通过 MCP 接入 OpenMCP(一)游戏王 RAG 知识库通过 MCP 接入 OpenMCP(二)

当时我所在的公司正好在做 AI 客服,我提出用 MCP + RAG 来接入的方案:做一个独立的 Node.js 服务,由其他同事负责前端、对接旧系统,再不断往里加 MCP Server 来做数据和文本查询(感谢公司给我这个机会尝试)。

项目很顺利。在这个任务之余,我对 OpenMCP 越来越着迷,想读它的代码,自己也做一个类似的东西。

那时我虽然知道 OpenMCP 是个调试器,但光是”接入 MCP 并调用工具”这一点就已经足够震撼了。

直到很久以后我才冷静下来,思考它真正的意义(agent trajectory?数据回流?数据飞轮?我们仍未知道 2025 年 3 月的锦恢在想什么)。

带着这种震撼,我觉得必须立刻开发自己的客户端!于是我用古法编程写出了 LeoChat 的前身:zz-ai-chat。

LeoChat 的前身 zz-ai-chat

但到了 2025 年下半年,我逐渐开始用 Cursor、Copilot、Claude Code 这些工具做自动迭代;再加上 zz-ai-chat 当初为了炫技,搞了 4 种构建配置、分别构建不同的目标,项目开始变得难以维护。

经过一段短暂的搁置(我会在另一篇里细说),我开始了重写计划。

重来一次,我想要什么

重写的时候,我给自己定了几条很窄的目标:

  1. 流畅、可靠:多轮工具调用、流式输出都要流畅,反馈要快,错误处理要完备。
  2. 探索 LLM 给交互模式带来的变化:LLM 不应该只会输出文本和调用工具,我还想让它接管 UI 交互。
  3. 优先保证 Electron 的完成度,其它模式次之:我曾在 Web 优先还是 Electron 优先之间纠结。Web 才是 SaaS 能发挥的场景,但我割舍不下 Electron 带来的本地能力(Web 要实现同样的能力,大致有类似 GCP 的云服务和沙箱两条路线,有机会我会写一篇),所以最后把 Electron 定为主要目标。

LeoChat 能做什么

它首先是一个带 MCP 的 chatbox(这也是项目最初的定位,如今看来已经不够酷了),可以灵活接入各种通用 MCP 工具。

内置的 MCP Server 包括:文件系统(Filesystem)、知识图谱记忆(Memory)、网页抓取(Fetch)、Excel 读写、Word 读写、浏览器自动化(Playwright),以及用来测试的 Everything。

不过我往里面加了点特别的东西,这才是 LeoChat 和别的客户端不一样的地方。

用 AI 来控制 UI 交互

切换主题色,甚至调整窗口大小,让人眼前一亮(物理意义上的):

还可以让 AI 渲染卡片,比如生成一张名片、一张产品卡片,甚至是一个带按钮、可以交互的组件:

AI 渲染的卡片示例(一)AI 渲染的卡片示例(二)AI 渲染的卡片示例(三)AI 渲染的卡片示例(四)

这里我没有用 Vercel 开源的 stream-ui(有机会再聊),而是自己实现了一套。

拆开看看:LeoChat 独有的两样东西

聊天、流式输出、工具调用,这些别的客户端也都有。真正让我花心思的,是前面演示过的两样东西:UI 命令和卡片系统。

UI 命令:让模型伸手去”按”界面上的按钮

主题切换、窗口缩放这些效果,背后是一个很朴素的约定。

内置的 leochat-mcp 和普通 MCP Server 没什么两样,模型通过标准的工具调用使用它。区别在于,它的工具不读文件、不查数据,而是返回一小段 JSON:

{
"__ui_command__": true,
"command": "update_theme",
"payload": { "themeId": "dark-purple" }
}

前端发现返回值里带着 __ui_command__,就去命令注册表里找同名的处理函数来执行。

目前注册的命令有 11 个:

  • update_theme、resize_window:切主题色、调 Electron 窗口大小;
  • show_notification、show_confirm:弹通知、弹确认框;
  • open_panel、close_panel:打开或关闭侧边面板;
  • set_input、scroll_to_message、copy_to_clipboard、open_url:改输入框、跳到某条消息、复制、打开链接;
  • send_message:替用户发一条消息(后面讲卡片时会用到)。

这样设计的好处是两边互不依赖:MCP Server 不需要知道前端长什么样,前端也不关心命令是哪个模型发出来的。加一个新能力,只要在类型表里加一个名字,再写一个处理函数。

不过我给它定了一条纪律:leochat-mcp 不能膨胀成”遥控整个前端”的万能接口。

那条路的下场我能想象得到:边界越来越模糊,协议越堆越多,每个命令都是一份脆弱的约定,最后谁也维护不动。所以它只留”宿主级”的小动作,凡是需要展示内容、需要用户做选择的,都交给卡片。

卡片系统:LeoCard

一开始,LeoChat 的卡片是靠解析模型输出的文本标签(<card />)做的。

很快我就发现这条路走不通:模型写错一个括号,卡片就坏了;按钮点了之后,也只能靠约定好的文本格式去猜意图。

所以我重新设计了一套结构化的卡片协议,原则只有一条:命令不是内容。

  • ui-command 负责”动作”:切换主题、打开面板,是一次性的副作用;
  • card 负责”内容”:商品摘要、渲染进度、让用户二选一,是要画在聊天记录里的东西。

一张卡片的结构很小,也很好组合:

{
"id": "card_1",
"kind": "summary",
"title": "无线耳机",
"tone": "info",
"body": [
{ "type": "image", "image": { "url": "https://…/earbuds.jpg" } },
{ "type": "fields", "fields": [{ "label": "价格", "value": "¥199" }] },
{ "type": "badge", "badge": { "text": "有货", "tone": "success" } }
],
"actions": [
{
"id": "buy",
"label": "加入购物车",
"kind": "primary",
"action": { "type": "ui-command", "command": "send_message",
"payload": { "text": "把这个加入购物车" } }
}
]
}
  • kind 决定整体样式:summary、media、list、status、review、error。这个字段是开放的,外部工具可以自己起名字,不认识的 kind 会退回成普通的摘要样式;
  • body 是一组积木:文本(支持 Markdown)、键值对、单图、多图、进度条、徽标、分隔线;
  • tone 有五种:默认、信息、成功、警告、错误;
  • actions 是按钮,点击后会分发给三种目标之一:ui-command(执行宿主命令)、agent-event(派发事件给宿主应用)、link(打开链接)。危险操作可以带上 confirm,点击前先弹确认。

数据的流向也很直白:工具返回结果,前端解析 JSON、取出 MCP 的结果信封、规范化成 LeoCard,再作为消息里的一个内容项交给渲染器。

不需要新的传输通道,任何 MCP Server(不管是内置的还是第三方的)只要返回这个格式,就能在聊天里画出卡片。

让卡片和对话循环起来

卡片真正好玩的地方,是按钮可以通过 send_message 这个 UI 命令,替用户给模型发一条消息。

这样就形成了一个闭环:

用户提问 → 模型调用工具 → 工具返回卡片
→ 卡片显示按钮 → 用户点击
→ send_message 把意图发给模型 → 模型调用下一个工具 → ……

我拿它做了两个独立的演示 Server,也放在仓库里:

  • pokemon-quiz:猜宝可梦小游戏。每一题是一张带图片和四个选项按钮的卡片,点选项就是提交答案,点”下一题”继续。
  • ecommerce:一套完整的购物流程。搜索商品、看详情、加购物车、下单、付款、查物流,每一步都是卡片,按钮驱动流程往下走。

做 pokemon-quiz 时还踩到一个有意思的坑:卡片显示出来之后,模型总忍不住”自问自答”,把选项分析一遍甚至直接报答案。

解决办法是在工具返回里再塞一段只给模型看的指令(llmInstruction):“请直接展示上方卡片,不要分析选项,让用户自己点击按钮作答”。

这也让我体会到,生成式 UI 里,UI 是给人看的,指令是给模型看的,两者要分开设计。

内置的 generate_waifu 是最简单的卡片示例:它返回一张 media 卡片,里面是几张随机生成的图。

内置 MCP:开箱即用的工具箱

MCP 客户端最劝退的一步,是让用户自己去装 Node、装 Python、配 JSON。

所以 LeoChat 启动时会自动连上一组内置的 MCP Server,打包版里连运行时路径都处理好了,Windows 解压就能用:

Server能做什么
LeoChat(自研)控制界面:切主题、调窗口、弹通知/确认框、开关面板、改输入框、复制、打开链接;另外带一个 bash 工具执行命令(rm、format、shutdown 这类危险命令直接拒绝),还有一个生成二次元图片、以卡片返回的 generate_waifu
Filesystem读写本地文件、列目录、搜索文件
Memory基于知识图谱的长期记忆,跨对话记住实体和关系
Fetch抓取网页内容,转成模型好读的文本
Excel读写 Excel 表格(Windows 上直接打包成可执行文件,不依赖 Python)
Word读写 Word 文档
Playwright浏览器自动化:打开网页、点击、填表、截图
EverythingMCP 官方的参考实现,用来测试协议的各种特性

组合起来,一句话就能跑完一条小流程,比如”抓取这个网页,把里面的表格整理进 Excel,存到桌面”。

内置之外,也可以在 MCP 设置页里添加任意第三方 Server,Stdio 和 Streamable HTTP 两种传输方式都支持。

法律版就是在这套底座上,多挂了一个法律知识库的 MCP。

剩下的基础设施,简单带过

下面这些是做一个可靠的 agent 客户端绕不开的部分,也是我花时间最多的地方,但它们并不是 LeoChat 独有的,所以只简单说几句:

  • TaskLoop:整个系统里最大的文件,负责一轮一轮地调用模型、执行工具、把结果喂回去。有几处细节要自己处理:流式的工具调用参数要按 index 拼回完整 JSON;一个任务最多跑 50 轮防止死循环;单个工具结果超过 3000 字符就截断;每一轮结束、暂停或出错时保存检查点,可以恢复。
  • 模型适配层:支持 OpenAI、OpenRouter、Anthropic、DeepSeek、Kimi、Gemini 和任意兼容地址。各家的默认地址、认证头、流式格式都不一样,适配层把差异关在里面,对外只暴露统一接口。这层现在已有成熟的社区方案,但自己写一遍,让我更清楚地理解了 OpenAI API 的各个参数,排查问题时比任何文档都有用。
  • 可靠性层:指数退避加 full jitter 的重试、每次尝试独立的超时、连续失败后”打开”的熔断器。这和我后来在 neo-studio 里对接各个外部平台的思路是同一件事:外部系统一定会出问题,系统要为失败设计。

一个项目的命运,不仅要看个人的努力,也要考虑历史的进程

最先让我感到迷茫的是 OpenClaw。它以最快的速度占领了非技术用户的注意力,而且它的重心放在了最繁杂的那部分工作上:gateway。

说实话,作为一个独立的聊天应用,LeoChat 今天已经没有什么商业价值了。

Claude Code、Codex 这样的通用 agent 已经非常成熟,完善程度远超一个人能做出来的东西。

但它留下了几样东西:

  1. 这个博客的 AI 助手:点这里去和它聊聊,或者点上方的”关于”页面(手机端点三横线展开)。那个助手是我用三天业余时间写出来的,算是我的替身。三天能写完,是因为之前在 LeoChat 上花了两个月,把 agent 循环、工具调用和卡片式 UI 这些问题都想清楚了。 ps:其实博客的 AI 助手也能让你”眼前一亮”,不妨试试。
  2. 协议层的理解:MCP 的传输方式、工具调用的流式拼装、检查点和恢复——这些是真正造过一遍才学透的,看文档替代不了。
  3. 一套处理不可靠外部系统的习惯:适配层、重试、熔断、为失败设计。后来做 neo-studio 的集成层时,几乎用的是同一套思路。
  4. 法律版:LeoChat 的一个分支,用 RAG 做了一个中国法律相关的知识库,目前正被一位法律从业者用于文书写作。如果你想体验,也可以发邮件联系我。

写在最后

这是目前(截至 2026 年 9 月)我做过的最复杂、最完整的个人项目,而且完全是出于”把 AI 落地到应用里”的热情去做的。

做这个项目的过程中,我逐渐理解了 OpenMCP 的意义,甚至开始学习 AI Infra;在开源社区的帮助下,我的技术视野进步飞快。

我想,LeoChat 碰到的”agent 客户端的意义”问题,每个 agent 开发者都会碰到。

尤其是当你想在日常里用上自己写的 agent 时,总会忍不住问自己:我为什么不直接用 Claude Code 呢?

但构建一个 agent 客户端、一个 agent runtime 的经历,是我目前技术经历里最不可或缺的一部分。

LeoChat 不是终点,而是起点。