其实,叫它 LeoChat 很早就不准确了。LeoMCP?LeoAgent?LeoHarness?这些能力它都涉及,但哪个名字都不太对,所以一直没改。
开源地址:cybermanhao/LeoChat(MIT 协议)
它的前身项目刚启动时,coding agent 才刚刚面世(而且因为在国内不方便购买,我当时还没用过);而现在,它的功能早已被 coding agent 覆盖了(做 agent 客户端的,谁又能不被覆盖呢?╮(╯▽╰)╭)。它不是没有用,只是凭我一个人,实在很难做得比精英云集的团队更好。
DeepSeek Harness 开源以后,我决定停止这个项目的迭代。
论完成度,LeoChat 和 dsh 差得太远,以后要做类似的东西,我大概会直接拿 dsh 这类成熟项目来魔改。但它依然是我最自豪的作品。
这篇想聊聊它是什么,以及它给我带来了什么。
如果你想体验 LeoChat,可以去本站的资源下载站 leo-releases 下载最新版(Windows 解压即用);需要测试用的 API key 的话,可以发邮件到 [email protected] 联系我。
做 LeoChat 之前
2025 年 5 月,我正在 B 站搜 RAG 相关的视频,首页忽然推给我一个视频,作者是我在知乎关注了半年的答主——锦恢。
视频在这里:OpenMCP 介绍。
值得一提的是,我抢到了第一个播放,可见我当时的 AI 焦虑有多深(笑)。
视频主要介绍了 OpenMCP,一个比 MCP Inspector 更好用的 MCP 调试器。
看完我非常激动:我第一次了解到了 MCP 这个概念,这实在太令人着迷了。我第一次知道这么简单就可以把 AI 接进应用里。我马上做了一个游戏王 RAG 知识库,通过 MCP 接进了 OpenMCP。


当时我所在的公司正好在做 AI 客服,我提出用 MCP + RAG 来接入的方案:做一个独立的 Node.js 服务,由其他同事负责前端、对接旧系统,再不断往里加 MCP Server 来做数据和文本查询(感谢公司给我这个机会尝试)。
项目很顺利。在这个任务之余,我对 OpenMCP 越来越着迷,想读它的代码,自己也做一个类似的东西。
那时我虽然知道 OpenMCP 是个调试器,但光是”接入 MCP 并调用工具”这一点就已经足够震撼了。
直到很久以后我才冷静下来,思考它真正的意义(agent trajectory?数据回流?数据飞轮?我们仍未知道 2025 年 3 月的锦恢在想什么)。
带着这种震撼,我觉得必须立刻开发自己的客户端!于是我用古法编程写出了 LeoChat 的前身:zz-ai-chat。
但到了 2025 年下半年,我逐渐开始用 Cursor、Copilot、Claude Code 这些工具做自动迭代;再加上 zz-ai-chat 当初为了炫技,搞了 4 种构建配置、分别构建不同的目标,项目开始变得难以维护。
经过一段短暂的搁置(我会在另一篇里细说),我开始了重写计划。
重来一次,我想要什么
重写的时候,我给自己定了几条很窄的目标:
- 流畅、可靠:多轮工具调用、流式输出都要流畅,反馈要快,错误处理要完备。
- 探索 LLM 给交互模式带来的变化:LLM 不应该只会输出文本和调用工具,我还想让它接管 UI 交互。
- 优先保证 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 渲染卡片,比如生成一张名片、一张产品卡片,甚至是一个带按钮、可以交互的组件:




这里我没有用 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 | 浏览器自动化:打开网页、点击、填表、截图 |
| Everything | MCP 官方的参考实现,用来测试协议的各种特性 |
组合起来,一句话就能跑完一条小流程,比如”抓取这个网页,把里面的表格整理进 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 已经非常成熟,完善程度远超一个人能做出来的东西。
但它留下了几样东西:
- 这个博客的 AI 助手:点这里去和它聊聊,或者点上方的”关于”页面(手机端点三横线展开)。那个助手是我用三天业余时间写出来的,算是我的替身。三天能写完,是因为之前在 LeoChat 上花了两个月,把 agent 循环、工具调用和卡片式 UI 这些问题都想清楚了。 ps:其实博客的 AI 助手也能让你”眼前一亮”,不妨试试。
- 协议层的理解:MCP 的传输方式、工具调用的流式拼装、检查点和恢复——这些是真正造过一遍才学透的,看文档替代不了。
- 一套处理不可靠外部系统的习惯:适配层、重试、熔断、为失败设计。后来做 neo-studio 的集成层时,几乎用的是同一套思路。
- 法律版:LeoChat 的一个分支,用 RAG 做了一个中国法律相关的知识库,目前正被一位法律从业者用于文书写作。如果你想体验,也可以发邮件联系我。
写在最后
这是目前(截至 2026 年 9 月)我做过的最复杂、最完整的个人项目,而且完全是出于”把 AI 落地到应用里”的热情去做的。
做这个项目的过程中,我逐渐理解了 OpenMCP 的意义,甚至开始学习 AI Infra;在开源社区的帮助下,我的技术视野进步飞快。
我想,LeoChat 碰到的”agent 客户端的意义”问题,每个 agent 开发者都会碰到。
尤其是当你想在日常里用上自己写的 agent 时,总会忍不住问自己:我为什么不直接用 Claude Code 呢?
但构建一个 agent 客户端、一个 agent runtime 的经历,是我目前技术经历里最不可或缺的一部分。
LeoChat 不是终点,而是起点。