在终端优雅的集成 llm

展示图:

初体验

OpenRouter:

最近用习惯了 warp 后常常调用它自带的 llm。但是很不好用,每次返回的内容都不太符合预期,并且它的免费额度也比较少,所以最后被我弃用了。而最近恰巧又在看 jyy 的操作系统课,每次看到他在终端直接调用 deepseek 等模型行云流水,于是动起了自己在终端集成 llm 的念头。

但这想法一开始没落实。因为 api 的选择方面使人犯难:国内的如果只用 deepseek,便宜是便宜,但是未免太单调,并且由于众所周知的原因,deepseek 会过滤一些回复的内容;国外的 api 呢,经常是又贵又不好用(是的,我说的就是你,gfw),并且一般都需要国外信用卡,很不方便。基于以上的因素,这个想法被我搁置了好几周。

直到这周,订阅的 blog 和一位学长不约而同的推荐了 OpenRouter 。里面可调用的模型种类不仅完备——最重要的是不同模型的 api 接口是相同的!只需要更改调用模型名字的变量。这极大的简化了终端中调用所需要的设计。除此之外,Openrouter 支持链上支付,具有良好的匿名性,巧妙的避开了国外信用卡这一环(填写账单部分,地址随机生成器糊弄下就够了,反正也不可能让它有机会给我寄…),再加上这周赶上新币上市做多小赚了一笔😎(虽然大概率是被割韭菜前的小甜头),趁此机会就购买了它的 api,另外非常棒的一点是,只要你充了 10$,里面的免费模型的 api 都是随便调的,官方似乎说的是每天限量 1500次,这已经完全够使用了。

天才交易员cry(

里面官方文档对于 api 的调用讲的很详细,也有关于 mcp 的介绍,算是不错的官方文档(这里我要点名批评 nginx ,官方文档真是够简陋的…)。

说干就干,于是今天让 ai 写了小的脚本,我从旁辅助调了一会儿 bug,一个名为 ag 的小玩具就诞生了😁。


ag:

对应的一键安装放在 github 上了,非常轻量的小玩具,也不需要 py 的环境基础,仅仅是 SHELL 脚本。

ag 这个名字来自于 jyy 的指令😂。看着他用习惯了,我就把它命名为 ag,后面才发现与一个文件搜索工具的指令撞了。但命好名了我也懒得改,如果真在系统中下载了对应的搜索工具,那直接把它卸载就完了,可以考虑更改对应脚本中的名字。

ag 简陋的上下文调用

ag 的主要工作原理就是用 curl 请求,同时用 while 不断循环读取然后在内用 jq 处理输出,并且由于启用了 stream 流式传输便于阅读,所以每次请求其实是分成小段,因为流式传输是遵循 SSE 规范的,所以每次收到的响应行就像这样:

1
2
3
4
5
6
7
data: {"id":"chatcmpl-xxxxx",...,"choices":[{"index":0,"delta":{"role":"assistant","content":""}}]}
data: {"id":"chatcmpl-xxxxx",...,"choices":[{"index":0,"delta":{"content":"你好"}}]}
data: {"id":"chatcmpl-xxxxx",...,"choices":[{"index":0,"delta":{"content":",我"}}]}
data: {"id":"chatcmpl-xxxxx",...,"choices":[{"index":0,"delta":{"content":"是AI。"}}]}
data: {"id":"chatcmpl-xxxxx",...,"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
//在脚本里定义的其实是 msg user,但这里作为示例无关紧要

然后通过 echo “$json_chunk” | jq -r: 将提取到的 JSON 数据块通过管道传给 jq。 -r 标志告诉 jq 输出原始字符串结果,而不是带引号的 JSON 字符串,从而解析出返回的内容,营造出打字机的感觉(其实就是平常在网站上直接请求的感觉😂)。

原理非常简单,只需要 curl 和 jq 就可以完成,同时由于我很喜欢 fish,就顺便集成了 fish,也方便我每次在服务器上的初始配置,不然每次配环境都得浪费一段时间(这才是主要目的)。估计用 py 中的 json 解析实现可能会高效,但我 py 写的太少了… SHELL 脚本至少之前搭梯子的时候还研究过一小段时间,至少能看懂,这也是为什么直接选择用 curl 和 jq 解析的原因。

另外还支持对话记录,同样是以 json 的方式,将每次返回的 json 格式存储在本地的 json 文件里,在再次调用的时候可以将其发送过去,达到”伪记录“的作用。这里参考了 utool 里的模型本地存储设计,存储在本地也保证了自己的数据隐私,这在这个时代是非常难得的东西…

里面还有很多可以自定义修改的地方,譬如系统提示词,我默认给其设定了不要以 md 的语法输出到终端,不然会显得很杂乱。遗憾的是,这个提示词并不是特别有效,有些模型就是不听劝,硬要输出也没办法。


小插曲:

一键安装脚本文件基本是 gemini 在写,但是它一直不能正确的将本地的脚本文件结合到安装配置里,总是会出现奇奇怪怪的错误,最后还是亲自动手改了配置后才成功的,这再次告诉我们,尽信 ai 不如没有 ai😓。

其实我一开始还打算集成 glow 或者 rich-cli ,能在终端直接解析 md 格式以及导出 md 文件,但最后集成下来的效果还是不太好,似乎是每次分段请求不能让这些终端 md 解析器渲染的很好,具体的原因没有过度深究,因为我最后发现真要写代码这种需要代码块格式的,我估计还是在 copilot 或者 cursor 上写。并且导出 md 文件也没啥用,还不如直接在网站上问,这种命令行基本也是在 linux 这类不怎么使用图形界面的地方使用,md 的格式还得导到平常用的 windows 上,实在是多此一举。


感想:

这种类似的小玩具网上也有不少,而且现在有了 ai 后基本只要能看懂对应使用的语言,熟悉语法,就能比较方便的自己 diy,似乎重复造轮子显得意义不大,也没什么用,但其实真正做下来,也是一件很有趣的事,至少它是我造出来的第一件玩具,有着不同寻常的意义。

类似的心情,大概就是小时候搭积木建成第一个有模有样的小玩具差不多吧。这也是我将本篇归到”小玩具“中的原因。

毕竟编程,其实也就是用编程语言这种小积木,一步一步的搭成自己的小玩具😜

希望自己未来可以搭出一些更有意思的,独一无二的小玩具,现在的小玩具更像是中国制造,夹杂抄的成分太多了…努力学习的动力又多了一分呢hhh