从零写一个 Harness CLI:我怎么把一个 AI 命令行工具真正做成“能干活”的样子

  • King
  • 发布于 1天前
  • 阅读 83

最近我写了一个小项目:harness-cli。一开始它只是个很朴素的想法:我想要的不是一个“能聊天的命令行”,而是一个真的能帮我做事的CLI。它不只是接一下模型接口,把回答打印出来,而是要能理解任务、拆步骤、调工具、执行动作,最后把结果交回来。于是,这个项目慢慢就长成了现在这个样子:有

最近我写了一个小项目:harness-cli

一开始它只是个很朴素的想法:我想要的不是一个“能聊天的命令行”,而是一个 真的能帮我做事的 CLI

它不只是接一下模型接口,把回答打印出来,而是要能理解任务、拆步骤、调工具、执行动作,最后把结果交回来。

于是,这个项目慢慢就长成了现在这个样子:

Planner / Executor 双层结构,有 Tool Registry,有 Skill System,有 Session Persistence,也有一个基于 Bubble Tea 的终端交互界面。

整个系统虽然不大,但已经不是一个简单的“聊天壳”了。

今天这篇文章,不讲模型有多强,也不讲 API 怎么接,而是想聊聊:

如果从零开始做一个 Harness CLI,我是怎么把它一步一步搭起来的。


先想清楚:你到底是在做聊天工具,还是执行工具

很多 AI CLI 项目,第一版都差不多:

  • 读用户输入
  • 调模型
  • 打印回答

这样当然能跑,但问题也很明显:它只能“说”,不能“做”。

我写 harness-cli 的时候,一开始就不想把它做成一个聊天机器人。我更在意的是,用户丢过来一个任务之后,它能不能自己先想清楚要怎么做,再调用工具把事情推进下去。

所以在架构上,我没有走单 Agent 路线,而是拆成了两个角色:

  • Planner:负责理解任务,输出执行计划
  • Executor:负责按计划调用工具,把事情做完

这个拆分看起来多了一层,其实非常值。因为一旦把“思考”和“执行”分开,整个系统就清楚了很多。

模型先负责想,再负责干。 计划先被结构化,工具调用才会更稳。 而且 UI 也终于能告诉你:它现在是在规划,还是已经开始执行了。


真正的关键,不是模型,而是工具系统

做这类项目,最容易陷进去的误区就是:总觉得能力强不强,取决于模型。

但写到后面我越来越确定,真正决定上限的,其实是工具系统

如果模型只是回复一段文本,那它永远只是个会说话的助手。只有当它能读文件、写文件、跑命令、执行代码,它才开始像一个真正能工作的 CLI。

所以我在这个项目里专门抽了一层统一的 Tool 接口,把工具注册、工具发现、工具执行都收拢到一套机制里。这样做最大的好处是,后面不管加多少工具,Executor 都不用改逻辑,只需要通过注册表调度就行。

目前这个项目里,比较核心的几类工具包括:

  • shell 执行
  • 文件读写
  • 代码执行
  • skill 调用
  • 以及可扩展的 HTTP / 脚本类工具

做到这里之后,整个 CLI 才终于有了“手和脚”。


Planner / Executor 这个拆分,是真的好用

这套架构不是为了好看,而是因为它真的解决问题。

Planner 解决的是“别一上来就乱做”

用户一句话抛过来,很多时候并不是一个直接可执行的动作,而是一个目标。比如:

  • 帮我 review 一下这个项目
  • 生成一个脚本处理这些文件
  • 看看这个报错是怎么回事

这类请求,如果直接扔给一个 Agent 去自由发挥,很容易出现两个问题:

第一,它想得不够清楚。 第二,它工具用得很乱。

所以我单独让 Planner 先把任务变成计划。步骤是什么,要不要调工具,先做什么后做什么,都先定下来。这样后面的执行器就轻松很多。

Executor 解决的是“把计划落实到工具调用”

有了计划之后,Executor 就很纯粹了:进入工具调用循环,执行动作,把结果回填上下文,继续下一步,直到整个任务结束。

这套东西听起来有点像“把 Agent 做重了”,但实际写完之后最大的感受是:整个系统终于稳定了。

你会发现,很多时候不是模型不行,而是没有一个清楚的执行框架去接住它。


Skill System,让它不只是“能用工具”

这是我比较喜欢的一层。

单靠工具,其实只能解决“能做什么”;但很多任务不是缺动作,而是缺上下文、缺方法、缺套路。比如代码审查、调试、重构,这些事情并不是一个独立工具能解决的,它更像一套工作方式。

所以我在项目里加了 Skill System。

Skill 本质上是一层可插拔的“能力上下文”。它可以被发现、被激活、被注入系统提示中,让模型在执行某类任务时,自动带着那套工作习惯和判断方式去做事。

现在这套 skill 机制已经把发现、安装、激活、切换这些链路串起来了。它还在继续长,但方向已经比较明确了:

Tool 是动作,Skill 是方法。

这两层分开之后,整个 CLI 的味道就不一样了。


会话和记忆,是命令行 Agent 必须补上的东西

如果一个 CLI 每轮都失忆,那基本只能拿来玩。

所以我后来把两层东西补上了:

  • 短期 Memory:保存当前会话里的偏好和约束
  • Session Persistence:把历史消息和状态落盘,支持恢复

这一步做完以后,整个工具的感觉立刻从 demo 变成了“能连续使用的系统”。

你不用每次都重复说:

  • 用中文回答
  • 简洁一点
  • 不要动这个目录结构
  • 优先保守修改

你也不用担心终端一关,前面的上下文全没了。

对命令行工具来说,这种连续性特别重要。因为很多真实任务本来就不是一轮完成的,而是反复试、反复修、逐步推进的。


最后才是 TUI,但它并不是装饰

很多人会觉得,终端 UI 不重要,能跑就行。

但 Agent CLI 不一样。 它不是一个单纯输出文本的程序,它是一个持续发生状态变化的系统。你得让用户知道:

  • 它现在是在想,还是在做
  • 正在执行第几步
  • 有没有调工具
  • 结果是怎么一步步出来的

所以我最后用 Bubble Tea 把整个交互层补了起来。不是为了好看,而是为了让这个系统更可理解

做完之后最明显的感受就是: 同样一套 Agent 逻辑,有没有 UI 状态表达,使用体验完全是两回事。


写到这里,我反而更确认了一件事

做这种项目,最重要的从来不是“接了哪个模型”,而是:

你有没有把它做成一个真正能执行任务的系统。

模型当然重要,但它只是其中一层。 真正决定这个 CLI 有没有价值的,是这些更底层的东西:

  • 架构有没有分层
  • 工具有没有抽象清楚
  • 技能系统能不能扩展
  • 会话能不能保存
  • 交互能不能把状态讲明白

harness-cli 对我来说,最有意义的地方不是“又做了一个 AI 工具”,而是我终于把这些东西在一个小项目里完整串起来了。


最后

老实说,这次写完还有一个很强烈的感受:

我已经很久没认真写 Go 了。

这次相当于重新捡起来,完整做了一遍。但写着写着,还是会越来越明显地感觉到:很多地方不是不能写,而是总觉得不够顺,不够舒服。

尤其是当项目开始往更复杂的 Agent Runtime、事件流、插件系统这些方向走的时候,这种感觉会更明显。

所以后面我大概率会基于这次的实践,再用 Rust 重新设计一套。

不是因为 Go 做不到。 只是现在的我,真的越来越看它不顺眼了。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
King
King
0x56af...a0dd
擅长Rust/Solidity/FunC/Move开发