尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

前端转AI第13天:用Node.js打造命令行AI助手clai

发布时间:2026/9/26 6:15:08

资讯中心
01
ARTICLE

前端转AI第13天:用Node.js打造命令行AI助手clai

前端转AI第13天:用Node.js打造命令行AI助手clai
100 天计划走到第 13 天我终于把前 12 天学的东西缝合成了一个能真正跑起来的东西命令行 AI 助手 v1代号 claicommand line AI。这个名字是我临时取的以后大概率会改但今天它已经能在我终端里正常工作了。这一天的意义不在功能多炫而在于第一次把前端转 AI这条路上零散的知识点打成一个整体Node.js 脚本编写、大模型 API 接入、流式输出解析、Function Calling 能力调用、配置管理、错误处理、以及一个还算体面的终端交互界面。如果你是跟我一样正在从 Web 前端往 AI 方向转的人或者是个对 AI Agent 感兴趣的开发者这篇记录可以给你一个完整的整合样本——吃什么、怎么吃、踩了哪些坑、为什么这么做。先交代背景我做了 12 天的基础学习内容大致是对齐 API 调用规范、理解上下文机制、跑过几个模型接口的 demo、也补了 Node.js 的工程化脚手架。但零散的 demo 有个毛病——用完就忘知识点没有串起来。所以第 13 天不再学新东西而是逼自己做一个综合项目把学过的全用上做完之后还能真的在日常命令行场景里帮上忙。1. 为什么第 13 天必须做一次缝合怪1.1 前端转 AI 的第 13 天困境前两周是学习新鲜感最足的时间窗口每天都在接触新概念token 怎么算、temperature 参数调高调低有什么反应、流式输出为什么像打字机一样吐字这些对我来说全是新世界。但问题也就出在这——学得太碎知识在大脑里是一堆独立的小纸片。我当时的状态是会写调用 ChatGPT 接口的 Node.js 脚本会调参数但让我从头独立做一个能对话、能记住上下文、能执行工具调用的东西脑子是空的。遇到这种状态最佳策略不是继续学第 13 个新知识点而是掉头做一个需要把所有学过的东西同时用上的项目。综合项目就是知识的缝合线把纸片拼成一张地图。前端转 AI 的人尤其容易掉进这个坑前端生态里组件化、状态管理、构建工具太成熟了写完 demo 有 Vite 帮你收尾缺失的部分框架替你补了。但做 AI 终端工具时没有框架兜底所有断点都暴露得干干净净。所以你不可能假装自己会编译器会在第一行 NPM install 之外就给你脸色看。1.2 为什么是命令行为什么是AI 助手一开始我也想过要不要用 React 套个网页毕竟那是我的舒适区。但仔细想了一下目标——用最简单的交付形态把 AI 能力内核做扎实——网页就太重了。一个命令行工具天然没有 CORS、没有前端路由、没有 UI 框架的额外心智负担你只管跟 API 对话、处理输入输出、管理配置和文件。另外从使用场景看命令行 AI 助手的价值甚至比网页版更实在它就在终端里离你的工作流最近。我写代码时有个需求想快速问一下切到浏览器打开网页再粘贴问题再复制答案回来这中间至少损失 10 秒心流命令行工具里敲一句clai 帮我看看这个报错什么意思答案直接打在当前终端省掉所有切换成本。选定形态后项目范围也很明确这是一个对话式助手v1 只做四件事——基础对话、多轮上下文记忆、一个执行终端命令的工具调用能力、以及完整的配置文件与会话管理。功能不贪多但每个模块都要经过真实使用验证而不是 demo 跑通就完事。2. 前 12 天的知识地图这次到底整合了什么既然叫整合前 12 天我需要诚实地盘点一下这 12 天到底学了哪些东西、在综合项目里分别用在哪。这张知识地图是项目开始前画的没有它动手时大概率会写到哪算哪。2.1 语言基础与脚本能力第 1-4 天前四天我重新夯实了 Node.js 的现代特性ESM 模块规范、async/await异步流程控制、child_process进程调用、fs/promises文件操作、环境变量的读取与安全处理。这些是任何 AI 工具的地基缺了它们后面接 API、读写配置文件全都无从下手。在 clai 里这些知识对应的是配置文件的读写用 JSON 存 API Key 和模型偏好、终端命令的执行用child_process调用系统命令、以及整个异步对话流程的状态管理。第 13 天做项目时我发现前几天的练习没有白费——虽然当时的题目非常无聊比如用 Node.js 读一个 JSON 文件再写回去但正是这种肌肉记忆让我今天写配置模块时不用查文档。2.2 与大模型对话的技术链路第 5-8 天这四天是关键打通了程序与 LLM 对话的完整链路。我用 Node.js 实现对 chat/completions 接口的调用先跑通了普通请求再深挖了流式响应处理——理解 SSEServer-Sent Events的数据格式一个data:开头的块就是一个事件[DONE]是结束标记。同时搞懂了 messages 数组的逻辑系统提示词、用户消息、助手回复全部在同一个数组里多轮对话就是把历史消息不断追加进去再整体发送。这是上下文记忆的底层原理也是所有聊天功能的基石。参数方面重点玩了temperature控制随机性、max_tokens限制回复长度和stream是否流式返回。Function Calling 也是这几天接触的。模型不直接执行工具而是根据你的函数描述输出一个结构化的调用请求真正的执行权在你自己手里。我的第一反应是这有什么难的不就是个 JSON 解析结果做项目时才体会到工具调用比单纯对话多了一整个回合的协作逻辑踩了不少坑。2.3 工程化、生态与体验第 9-12 天后半段聚焦工程化与体验优化用 dotenv 管理环境变量、把一次性的脚本重构为可维护的模块化代码、用 Inquirer.js 做终端交互下拉选择、输入框、用 Chalk 给终端文字上色、用 Spinner 做加载动画。这些库原本我只在 Web 端用过类似概念到了终端里发现完全是另一套心智模型没有 DOM、没有 CSS、所有界面都得靠字符串和 ANSI 转义序列画出来。Inquirer 的交互模型对标 Web 表单但它跑在纯文本终端上每一次重绘都需要考虑终端宽度。这也是综合项目比单个 demo 有挑战的根源——你不只要让代码逻辑正确还得让界面在人眼看起来不难受。2.4 一张表看懂整合关系我习惯在项目开始前画一张表格把自己学过的知识点和项目模块一一对应起来这样做的时候方向清晰不会做着做着忘记初衷前 12 天学的内容在 clai 中对应的模块Node.js ESM、异步流程控制整个项目的运行骨架文件读写与 JSON 解析配置文件、会话存储Chat Completions API 基础调用LLM 对话核心模块流式响应与 SSE 解析流式打字机输出多轮消息管理上下文记忆与会话恢复Function Calling 机制工具调用执行 shell 命令环境变量管理API Key 的安全加载Inquirer.js 终端交互启动菜单、确认框Chalk / Spinner终端上色与加载动画这张表花了我半小时但让后面三小时的编码变得非常流畅不需要边想边猜。我建议所有做综合项目的人动手前都先做一次这样的映射哪怕是写在草稿纸上。3. 架构设计先画图再写码省掉三小时重构3.1 模块划分与职责边界命令行项目最容易犯的错就是把所有逻辑塞进一个index.js里前端的组件化思维应该迁移到后端脚本上。我把 clai 拆成了六个模块每个模块职责单一、之间只通过明确接口沟通cli.js入口文件负责命令分发和参数解析prompt.js交互层Inquirer 的输入框、确认框都在这里llm.js大模型核心封装 API 调用、流式解析、Function Calling 回合逻辑config.js配置管理读取/写入用户配置文件session.js会话管理保存与恢复历史对话tools/工具函数目录目前只有一个执行 shell 命令的函数模块划分的原则很简单我能在一个文件里看懂它想干什么它不需要知道其他文件内部怎么实现。比如session.js只需要知道消息数组是长什么样的不需要知道消息是怎么发给模型的tools/里的函数只负责执行命令、返回结果字符串不管模型怎么决定调用它。3.2 为什么不做成 Web 应用有人可能觉得奇怪一个前端开发者的第一个整合项目怎么不做成 Web 应用我的考虑有三层。第一Web 应用要处理的东西实在太多端口、路由、HTTP 请求、前端构建打包、状态管理……这些跟整合 AI 能力这个核心目标无关只会分散有限的学习精力。第二命令行工具是 AI Agent 最自然的早期形态——它的输入输出都是纯文本天然适合 LLM 处理等以后需要图片、语音、复杂交互时再演进到 Web 或客户端那时候架构演进也有清晰路径。第三我自己有 Web 能力所以做 Web 版很容易掉进舒适区比如花两小时调一个左对齐还是右对齐的样式纯属浪费时间。这算是我前 12 天想明白的一个道理学习新领域时刻意避开自己的舒适技能树反而能逼自己把注意力放在真正陌生的核心上。3.3 目录结构一览最终目录结构是这样的clai/ ├── index.js # 入口 ├── package.json ├── .env # 存放 API Key已加入 .gitignore ├── src/ │ ├── cli.js # 参数解析与命令分发 │ ├── prompt.js # 终端交互 │ ├── llm.js # 大模型对话逻辑 │ ├── config.js # 配置读写 │ ├── session.js # 会话管理 │ └── tools/ │ └── shell.js # shell 命令执行 └── data/ └── sessions/ # 会话记录 JSON 文件入口文件只做一件事调用cli.js里的run()函数。run()拿到命令行参数后判断用户是要进入对话模式、查看历史会话、还是清理会话数据分别交给不同的函数处理。目录结构本身不复杂但这已经足够支撑起一个能持续演进的工具雏形了。4. 核心模块逐个拆解从对话到工具调用4.1 CLI 入口与参数解析入口逻辑看起来很普通但它是所有功能的门面。我用的方案是第一层判断是对话模式没有任何子命令还是指令模式--list列出历史会话、--clear清理会话。用 Node.js 内置的process.argv手动解析没有引入复杂 CLI 框架因为项目还小用 commander 等库反而增加依赖和管理成本。对话模式下先加载配置如果没有配置就引导用户进行初始化——输入 API Key、选择默认模型。整个初始化过程都是在终端里完成的用 Inquirer 做交互const inquirer require(inquirer); async function initConfig() { const answers await inquirer.prompt([ { type: input, name: apiKey, message: 请输入你的 API Key:, validate: v v.length 0 || API Key 不能为空 }, { type: list, name: model, message: 选择默认模型:, choices: [gpt-4o-mini, gpt-4o, gpt-4-turbo] } ]); return answers; }这里有个细节用户输入的 API Key 需要写入配置文件但绝不能明文写成一个全局文件。我的做法是优先读取环境变量OPENAI_API_KEY如果没有再尝试从配置文件读取配置文件的权限在类 Unix 系统上设置成仅当前用户可读写。后续我会再说这个问题的坑。4.2 终端对话界面没有 DOM 的前端对话界面的交互流程是这样的启动后先问用户是新建会话还是继续之前的会话然后进入一个while循环——读取输入、发送消息、流式打印回复、再等待下一轮输入。退出命令是exit或者CtrlC。终端界面的三个关键组件都是第三方库Chalk 负责颜色Inquirer 负责交互Spinner 用在等待模型响应的缓冲期。有一个教训必须提chalk 千万别手滑装 v5v5 已经改成 Pure ESM 了CommonJS 项目里直接require会给你报一个极其迷惑的错误。我的解决办法是固定使用chalk4版本——与其花一小时折腾模块兼容不如直接锁定稳定版本工程问题别用情怀解决。对话界面的伪代码骨架const messages [{ role: system, content: 你是 clai一个运行在终端里的 AI 助手。回答尽量简洁、准确。 }]; while (true) { const { input } await inquirer.prompt([ { type: input, name: input, message: 你: } ]); if (input exit) break; messages.push({ role: user, content: input }); const reply await streamChat(messages); messages.push({ role: assistant, content: reply }); }看到没有多轮对话的本质其实就是维护这个messages数组逐轮追加。这是最基础也最可靠的上下文实现方式也是所有高级记忆方案摘要、向量检索的退路。4.3 流式输出与 SSE 解析实战流式输出是一个看着炫酷、实则容易踩坑的模块。我封装了一个streamChat(messages)函数内部用原生fetch发起请求Node 18 自带不需要装 axiosasync function streamChat(messages) { const response await fetch(https://api.openai.com/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${config.apiKey} }, body: JSON.stringify({ model: config.model, messages, stream: true }) }); const reader response.body.getReader(); const decoder new TextDecoder(); let buffer ; let fullReply ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n); buffer lines.pop(); // 保留可能不完整的一行 for (const line of lines) { const trimmed line.trim(); if (!trimmed.startsWith(data:)) continue; const data trimmed.slice(5).trim(); if (data [DONE]) { console.log(); return fullReply; } const json JSON.parse(data); const delta json.choices[0].delta?.content; if (delta) { fullReply delta; process.stdout.write(delta); // 不换行地打印 } } } return fullReply; }这段代码的核心逻辑是按行切分 SSE 数据流buffer保留最后一段可能不完整的字符串避免 JSON 解析报错。每个data:块里的delta.content就是本轮新吐出来的文字。这里有个容易被忽略的细节传给decoder.decode时要加{ stream: true }否则遇到多字节中文时可能因为字符被拆到两次 chunk 里而产生乱码——这个问题我后面会在踩坑清单里再强调一遍。max_tokens的设置也有讲究。命令行助手需要的是短回复我把默认max_tokens设为 500并在系统提示词里写了一句回答尽量控制在 200 字以内。这不是在教模型做事而是在给用户控制成本——终端对话的使用节奏很快长回复反而干扰阅读。4.4 Function Calling让 AI 具备动手能力这算是 clai 最让我兴奋的部分。Function Calling 机制让模型不再只是回答问题而是可以输出一个我想执行某条命令的请求。它可以帮你查当前目录、看文件结构、甚至执行一些无副作用的系统命令。我的工具定义长这样{ type: function, function: { name: run_shell, description: 在用户的终端中执行一条 shell 命令并返回命令的 stdout/stderr 输出。仅在需要查看文件、目录信息或执行简单操作时使用。, parameters: { type: object, properties: { command: { type: string, description: 要执行的 shell 命令 } }, required: [command] } } }对话流程因此多了一个关键分叉模型返回的内容有两种可能——普通文本回复或者tool_calls数组。后者意味着模型要调用工具。流程变成把用户消息和工具定义一起发给模型如果响应里有tool_calls解析出工具名和参数执行对应工具函数把工具执行结果作为一条role: tool的消息追加到 messages 里带着这个结果再次请求模型让它根据工具输出生成最终回复上面这个工具结果回填 二次请求的逻辑就是 Function Calling 的核心循环。我一开始天真地以为模型会自己循环直到解决问题实际上它会等你把工具结果喂回去循环的控制权在你的代码里。这个理解花了我一晚上调试才彻底闹明白。工具执行本身用了 Node.js 的child_process的exec被包裹在一个 Promise 里顺手加了个 10 秒超时防止命令挂死。值得警惕的是权限问题——让 AI 执行 shell 命令是一把双刃剑。我的安全策略是只执行非交互式命令并且对rm、sudo、shutdown等危险命令做了硬拦截。这个必须写死不是可选项。这也是我要特别提醒所有做类似项目的人的一点。4.5 配置文件与会话管理配置文件我放在用户主目录下的.clai/config.json内容长这样{ apiKey: , model: gpt-4o-mini, temperature: 0.7 }config.js 模块封装了三个方法load()负责读取并合并默认配置save(newConfig)负责写回get(key)是快捷取值。如果配置不存在自动触发初始化流程。会话管理则简单粗暴但有效每次对话保存为一个 JSON 文件文件名是时间戳文件内容是完整的 messages 数组。这样会话恢复就是读取文件 - 把 messages 装回内存没有任何魔法。启动时用 Inquirer 的列表组件让用户选择新建会话还是加载某个历史会话。会话列表界面? 请选择会话: (Use arrow keys) ❯ 新建对话 2026-01-12 22:31 - 帮我看看这个报错 2026-01-12 20:07 - 写一个 Node.js 脚本 2026-01-11 15:20 - 解释一下 SSE 协议这个标题是 v1 偷懒的做法直接取第一句用户输入。等 v2 可以考虑让模型生成摘要那又是一个可以玩的功能。5. 踩过的坑直接给你避雷5.1 Bug 之一中文乱码与编码问题第一个老老实实踩进去的坑是中文乱码。我在流式输出里发现长一点的回复总会在中途出现锟斤拷——这是 UTF-8 左移右移的经典事故。排查后定位到两个根源。第一个是上面提过的TextDecoder不加{ stream: true }导致跨 chunk 的字符被错误解码。第二个则更隐蔽Windows 终端默认不是 UTF-8 编码而是 GBK。解决办法是代码开头强制写入if (process.platform win32) { process.stdout.write(\x1b[?25l); // 隐藏光标 }但这个不是万能的更根治的办法是在 Windows 终端里执行chcp 65001切换代码页。这个问题跨平台项目基本都躲不掉提前了解能省不少时间。5.2 Bug 之二流式中断与 JSON 解析崩溃流式解析中第二个坑是当 API 出错时响应不是 SSE 格式而是一个普通的 JSON 错误体。这时候response.body依然存在reader.read()也能读出数据但解析成 SSE 格式时 JSON.parse 就会直接抛异常。我的解法有两步第一步在调用前检查!response.ok如果不是 2xx 就吞掉整个流直接解析错误体打印给用户看。第二步在真正的流式解析里给JSON.parse包一层 try/catch一旦发现格式不对就跳过这个事件。这个处理救了我很多次——网络抖动、代理中断、模型端异常都可能产生半截数据。5.3 Bug 之三险些把 API Key 提交到 GitHub这是我这次最有惊无险的教训。项目做到一半我突然想给仓库加上 git 管理习惯性输入了git init git add .提交前正好回头看了一眼.env文件的名称赫然在列。我立刻退出提交在.gitignore里补上了.env和data/sessions/。这不是危言耸听API Key 一旦泄露别人就能用它调用模型跑账单完全是免费抽奖。后来我总结了一套规则.env永远不进 git配置文件的权限在 Unix 上设置为700只有自己可读写输出日志里永远不带apiKey字段。另外建议在 API 管理后台开启消费上限作为最后的保险丝。5.4 问题速查表症状根因解决办法中文乱码终端编码或 TextDecoder 分块解码错误终端chcp 65001解码加{ stream: true }流式输出中途崩溃SSE 数据块不完整或返回了 JSON 错误体用 buffer 保留半行try/catch 包裹 JSON.parseAPI Key 失效环境变量没设置或配置读错路径检查process.env.OPENAI_API_KEY和配置文件位置chalk 的 require 报错装了 v5ESM only固定chalk4命令执行没有输出exec 的回调没正确拿到 stdout/stderr用 Promise 包装正确 resolve/reject 两个输出流工具调用后模型不回复工具结果没有作为role: tool回填检查 messages 追加顺序与角色字段这些问题是我一晚上调试的浓缩希望后来的你不要逐条重新踩一遍。6. 前端转 AI 的顿悟时刻6.1 从页面思维到工具链思维这个项目让我第一次强烈感受到身份转变的实感。前端的核心交付物是页面用户通过视觉和交互跟系统对话而命令行 AI 助手的核心交付物是一个 API 闭环——输入、模型推理、工具调用、输出整个过程不需要一个像素的 UI。前端工程师的技术栈重心在浏览器DOM、CSS、状态管理、构建工具。AI 方向的技术栈重心在协议、数据流和自动化JSON、SSE、异步 I/O、进程控制、工具编排。两者底层都是输入-处理-输出的循环但前端的复杂度在界面左右AI 的复杂度在信息流转和决策逻辑上。当你放下对界面的执念你会发现很多问题可以更直接地解决。6.2 前端经验没有白费它换了一种方式起作用之前有人问我前端转 AI 是不是等于清零重来做完这个项目我可以较有底气地回答不是清零是技能迁移。比如组件化思想让我天然地把 clai 拆成了模块而不是一个大文件用户体验敏感性让我注意到流式输出的打字机效果、命令行的响应速度、错误提示的友好程度异步编程功底让我能自然处理流式读取和并发。甚至调试技巧都相通——就像通过 Chrome DevTools 看网络请求一样我在终端里用console.log打印每次 API 请求的完整 messages 数组直观地看到上下文是怎么越长越大的。这些能力都是前端带给我的隐形资产。6.3 clai 接下来的演进方向v1 做完我已经看到了很多可以继续挖的方向。最实际的是把工具调用扩展成一组插件式工具集除了 shell还能加文件读写、HTTP 请求、依赖查询。再进一步可以让模型在回答之前主动决定调用哪些工具——这就开始像真正的 AI Agent 了。会话管理也能做得更聪明现在的完整消息数组存整个历史但超过一定长度后 token 成本会飙升。v2 可以引入摘要机制——把早期对话压缩成一段摘要只保留最近 N 轮完整消息。这也是业界处理长上下文的标准思路之一。界面层面可以考虑引入 ANSI 转义序列画一个简单的状态栏显示当前模型、token 估算用量、会话时长。这些东西对日常使用的体验提升会很明显。长远规划里如果 clai 变得足够好用我会给它加上一个交互式的 Tool Selector让用户自己决定是否授权某个工具的调用——比现在的硬拦截更灵活也更安全。做到这一步我反倒变得务实了很多一个 AI 产品真正重要的不是 UI 有多炫而是它的工具链顺不顺畅、信息流转准不准确、安全边界清不清晰。这大概就是前端转 AI这个过程中最值得的一课。最后分享一个这次实践中的小体会我第一次跑通clai让它帮我查了当前目录的文件列表它用到的是 Function Calling 里的工具调用能力整个流程完全自主——用户输入一句自然语言模型决定调工具工具结果回填模型再生成回答。那一瞬间我意识到AI 助手的形态从聊天窗口变成能干活的工作搭档之间差的不是想象力而是把接口、工具、上下文、安全这些细节耐心砌完的功夫。这个项目我准备继续迭代后面可能做成一个真正每天在用的效率工具。如果你也在走前端的转 AI 之路强烈建议选一个跟工作流相关的综合项目尽早动手别等所有知识学完再开始——在项目中补课比在书里补课快十倍。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。