1. 为什么我要做OpenShell在AI时代重新审视终端先说个场景。我日常工作大量时间耗在终端里查日志、翻进程、批量操作文件、写脚本。按理说终端是效率最高的地方但有些时刻效率特别低——比如你记得find有个参数能按时间过滤但记不清是-mtime还是-newer比如你盯着一条上千字符的Python启动命令想问一句这命令到底干了什么却要复制到网页里问AI再复制回来再比如你想批量把一批MP4按视频时长重命名脑海里浮现的是写个循环还是用ffmpeg这种纠结。这时候我就在想为什么所有好用的AI工具都长在编辑器里、浏览器里唯独不直接长在我天天用的Shell里OpenShell这个项目就是针对这个痛点来的。OpenShell的定义很简单一个运行在终端里的、AI增强的Shell副驾驶。它的核心功能有三块把自然语言翻译成可以执行的Shell命令帮你解释你看不懂的历史命令以及在命令执行后把结果喂给AI做进一步分析。你不需要开第二个窗口去问网页也不需要在编辑器里切来切去一切都在你输入Shell命令的那个地方完成。这个项目适合谁如果你每天要跟命令行打交道不管是开发、运维还是喜欢折腾效率工具的人都会用得上。如果你是小白它也能帮你降低记命令的成本——你只管描述想干什么它给出方案你确认后执行边用边学。我前后写了两个版本才找到顺手的交互方式。这篇文章把OpenShell的设计思路、核心实现、踩过的坑都记录下来每一个模块都能直接拿来改造成你自己的工具。1.1 终端是最后一个还没被AI改造的地方你观察一下自己手头的工具链IDE里有AI补全和代码解释浏览器里有AI搜索引擎阅读器里有AI摘要甚至表格软件里都塞了AI公式助手。但终端不同。终端里能做的智能化大多停留在补全历史命令、展示git分支提示这种层面它本质上还是一个你键入什么就执行什么的输入框。很多人觉得这是因为终端本身足够简单高效不需要AI介入。我不太同意。终端的高效是对熟练用户而言的真正的高频痛点其实很明确一是命令的检索成本高二是长命令的理解成本高三是批处理类的临时性任务不值得你去写完整脚本。这三件事恰恰都是LLM擅长的事它们之间的结合点就差一层胶水——把自然语言、Shell命令和执行环境连接起来。OpenShell这个项目本质就是在写这层胶水。它不追求重写Shell不打算做一个带光标闪烁的AI对话框它做的事情是站在Shell旁边成为你跟Shell之间一个会说话的翻译官。1.2 项目边界不做什么才做得明白动手之前我先把不做什么列清楚了这比列功能清单更重要。OpenShell明确不做这几件事不重写终端模拟器不做GUI界面不自动执行高危命令不打算替代CI/CD里的脚本逻辑。原因很实际终端模拟器已经有很好的方案GUI消费的是更多系统资源而高危命令自动执行我无论如何都不放心。核心只聚焦三件事。第一自然语言转命令。你输入把当前目录下最近三天改过的Python文件按大小排序列出来它输出一条或一组命令你确认后执行。第二命令解释。你贴一条复杂的历史命令它逐段告诉你每个参数是什么意思、整体做了什么。第三结果分析。命令执行完产生一大堆输出你可以让它总结一下这些日志里出现了哪些错误级别的事件它基于实际输出回答。这三件事互相独立又共享同一个上下文管道。项目复杂度可控每件事都能单独测试。1.3 用起来是什么感觉直接看交互效果。你启动OpenShell进入对话REPL界面OpenShell 0.1.0 (输入 exit 退出, help 查看帮助) 找出当前目录里最大的5个文件OpenShell调用模型返回一段简洁解释和候选命令这条命令用 find 遍历当前目录及子目录以人类可读格式列出文件大小按大小倒序取前5条。 候选命令find . -type f -exec ls -l {} | sort -k5 -rn | head -5 确认执行[Y/n] y你按y命令执行输出结果会出现在下方。你还可以追加提问顺便解释一下 -exec ls -l {} 里的 {} 和 是什么意思它会基于刚才的上下文给出解释。整个过程没有任何网页跳转你的思路不会断。这就是OpenShell想达到的使用体验。2. 整体架构与关键设计决策功能看起来不复杂但实现过程中的几个设计决策决定了一个版本好用一个版本别扭。2.1 交互模型为什么选对话确认而不是全自动执行第一个决策是交互模型。我刚起手时的第一版是全自动模式用户输入自然语言AI生成命令程序直接执行再把结果交给AI继续分析。跑了两天我就发现不对。有一次我让它清理一下build目录里超过一周的临时文件它生成了一条带着sudo的删除命令我训练时的潜意识让我没看细节就放进去了——等察觉到风险已经晚了。其实那个目录是我本地的一个实验项目没有造成什么后果但属于典型的AI看起来懂其实理解错了场景。从那以后我把交互模型改成先解释、后确认、再执行。所有命令无论看起来多简单都先展示给用户按y才真正执行。这一步牺牲了一些流畅度但换来的是你永远知道接下来会发生什么的安全感。命令行环境的特殊性在于一次误操作的影响可能远大于编辑器里的一段错误代码代码错了可以撤销一条rm -rf执行下去可没有CtrlZ。REPL循环的核心逻辑可以抽象成这样用户输入自然语言组装上下文并调用模型模型返回候选命令和解释OpenShell从回复中提取命令打印出来用户确认或修改执行命令捕获输出输出回传给模型进入下一轮对话这个循环里没有复杂的任务计划、没有多智能体编排就是一个很朴素的人机轮流发言。但正是这种朴素让它容易预测、容易调试、出错时容易追责。2.2 供应商适配层你的模型不该被锁死第二个决策是模型接入方式。2024年前后的大模型市场已经足够多元同一个终端工具如果只绑定一家API等于把选择权交了出去。我在设计OpenShell的API层时就定了一条规则任何通过HTTP接口提供ChatCompletion风格服务的模型都应该能接进来。具体做法是抽象一个Provider接口。OpenAI兼容接口是事实上的通用协议几乎所有主流服务商都提供兼容端点所以适配层以它为核心。你只需要在配置文件里改一个base_url就能切换到另一家服务商甚至可以指向自己部门内网部署的模型网关。# providers/base.py class Provider(ABC): name: str abstractmethod def stream_chat(self, messages: list[dict], **kwargs) - Iterator[str]: ... # providers/openai_compat.py class OpenAICompatProvider(Provider): def __init__(self, base_url: str, api_key: str, model: str): self.client OpenAI(base_urlbase_url, api_keyapi_key) self.model model def stream_chat(self, messages, **kwargs): resp self.client.chat.completions.create( modelself.model, messagesmessages, streamTrue, **kwargs, ) for chunk in resp: if chunk.choices and chunk.choices[0].delta: yield chunk.choices[0].delta.content or 新模型第一天发布第二天可能就有服务商出了兼容接口。只要它的协议没跑偏OpenShell换个配置就能用代码一行不用改。2.3 上下文工程怎么把Shell环境信息变成模型的上下文第三个决策是上下文工程。模型对Shell上下文一无所知它不知道你在哪个目录不知道你的操作系统更不知道你之前执行过什么。但这些信息对生成准确命令至关重要。OpenShell构造的system prompt包含几类环境信息操作系统类型和Shell种类决定命令风格当前工作目录路径最近10条历史命令一条如果用户输入了文件路径通常指当前目录下的说明同时把系统信息放在一个系统消息里把用户的历史对话和实际命令输出放在后面的消息列表里。这样一个模型接到请求时它对用户是谁、在哪、之前做了什么有基本概念。这个设计的效果在实测中非常明显。同样一句看看日志里最近有什么错误不带路径上下文时模型给出的命令是journalctl -p err -n 50因为它默认你在systemd系统上带上cd /home/me/app/logs这个上下文后模型会自动换成grep -i error app-$(date %F).log | tail -50。这就是环境信息的价值。3. 五个核心模块的实战实现下面进入具体实现。我会按模块拆开讲每一步都给逻辑和代码你可以直接照着写。3.1 命令循环与流式打印交互入口我用了prompt_toolkit而不是裸的input()。原因有二一是它能稳定处理多行粘贴内容二是它有历史补全和快捷键支持而这些正是终端工具该有的手感。主循环的结构不复杂from prompt_toolkit import PromptSession session PromptSession(historyInMemoryHistory()) def run_repl(): print(OpenShell 0.1.0 (输入 exit 退出, help 查看帮助)) while True: try: text session.prompt( ) except KeyboardInterrupt: continue except EOFError: break if text.strip() in (exit, quit): break if text.strip() help: print_help() continue handle_user_turn(text)handle_user_turn组装消息、发起流式调用、打印模型输出。流式打印这里有个细节模型生成是一个字一个字蹦出来的如果你不加任何缓冲直接按字打印终端会显得很忙乱。我实现的方案是维护一个小缓冲区收集到30个字符左右再一次性刷到stdout用end配合flushTrue视觉上既流畅又不会逐字卡顿。3.2 从模型回复里干净地提取命令这是整个项目里我最想分享的一个踩坑点。模型在回复自然语言时很容易把命令以Markdown代码块的形式放在文字中间。OpenShell要做的是从这段混合文本里把真正可执行的命令拎出来忽略解释性的描述。我的提取过程分三档第一档整个回复被bash、shell、sh围起来直接取块内内容。第二档回复里有多段代码块按包含最多shell语法特征的规则选一段统计|、、、find这类命令关键词出现的频次。第三档没有代码块但回复本身就是一条命令比如短的ls -al直接trim后返回。有个细节要特别注意模型经常在命令后面跟一句上述命令将列出所有文件这种话。如果提取逻辑只找第一个换行符作为截断点这些尾巴就会被混进去。我的策略是找代码块优先没有代码块时再用以$开头或整行看起来没有自然语言特征的启发式规则。import re CODE_BLOCK_RE re.compile( r(?:bash|shell|sh|zsh)?\n(.*?), re.DOTALL, ) def extract_commands(reply: str) - list[str]: reply strip_ansi(reply) blocks CODE_BLOCK_RE.findall(reply) if blocks: return [b.strip() for b in blocks if b.strip()] lines [] for line in reply.splitlines(): cleaned line.strip() if not cleaned: continue if re.fullmatch(r[\w/\.\-*?\[\]{}|;!~$#%\^(),\\\: ], cleaned): lines.append(cleaned) return lines你可能好奇为什么要strip_ansi。因为某些模型服务会在输出里夹带ANSI颜色转义序列让整个正则匹配直接失效。我在调试时见过一条命令前面被套了一串\x1b[32m拿去subprocess执行直接报找不到命令。这个坑到后面踩坑章节还会细说。3.3 安全确认机制OpenShell的安全确认分两层。第一层是所有命令执行前必须按y确认。第二层是针对高危命令的模式匹配遇到就强制要求输入完整yes而不能只按Y并标红警告。高危命令清单我用了一个关键词集合凡是命中这些词的命令都会触发更严格的确认流程rm, mv, dd, mkfs, shutdown, reboot, curl, wget, kill, pkill, systemctl, usermod, chmod, chown, sudo注意我特意把curl和wget也放进去。因为它们经常和管道符连用curl xxx | sh这种用法在安全圈已经是老生常谈的危险操作OpenShell不允许在你眼皮底下偷偷出现这种组合。确认逻辑用代码表示就是RISKY_PATTERN re.compile( r(rm\s-[a-z]*r[a-z]*\s|dd\s|mkfs\.|curl.*\||wget.*\||shutdown|reboot) ) def require_confirmation(command: str) - bool: return bool(RISKY_PATTERN.search(command)) def confirm_and_run(command: str) - bool: level high if require_confirmation(command) else normal if level high: prompt_text f[高危命令] 确认执行(输入 yes 继续) if input(prompt_text).strip().lower() ! yes: return False else: if input(确认执行[Y/n] ).strip().lower() not in (y, yes, ): return False run_shell_command(command) return True这套机制曾经在一次演示中救过我。当时我让OpenShell删除过期备份文件它给出的命令是find /backups -name *.bak -mtime 30 -exec rm {} \;因为带rm且有-exec结构被判定为高危。我在确认前仔细看了一眼路径——/backups确实没错但如果我没看呢所以我认为这个确认步骤在AI工具里不是一个额外负担而是必须保留的护栏。3.4 执行结果回传与二次分析确认后执行命令输出需要被捕获并回传给模型这才是结果分析功能的地基。执行我用subprocess.run配合capture_output命令直接以列表形式传入而不是shellTrue拼字符串。这样能避免很多注入和转义问题也能拿到独立的stdout和stderr。这里有个我吃过亏的坑终端里跑得好好的命令到了subprocess.run(..., shellFalse)这里经常因为管道符、通配符无法解释而失败。因为find -name *.log | sort这整条命令本质上依赖Shell解析管道和统配不是单个可执行文件。所以我设计了一个小函数优先尝试把命令拆成列表直接执行执行失败且命令里含Shell特殊符号时退回到shellTrue但只允许在我们确认过的命令字符串上使用。import subprocess, shlex def run_shell_command(command: str) - dict: try: args shlex.split(command) proc subprocess.run(args, capture_outputTrue, textTrue, timeout30) except (FileNotFoundError, OSError): proc subprocess.run(command, shellTrue, capture_outputTrue, textTrue, timeout30) return { stdout: proc.stdout[:6000], stderr: proc.stderr[:3000], returncode: proc.returncode, }输出截断非常重要。一次find /的输出可能成千上万行如果全部塞给模型既烧token又把上下文窗口撑爆。OpenShell默认stdout保留前6000字符stderr保留前3000字符并在末尾追加一行提示输出已截断如需完整结果请直接在终端执行原命令。这个数字不是随手定的6000字符大概能覆盖绝大多数命令有效信息的完整范围又不至于让一次请求的token成本失控。3.5 历史与配置最后是工程化模块。OpenShell的对话历史保存在~/.openshell/history.jsonl每一行是一个JSON对象字段包括role(user/assistant/system/tool)、content、timestamp。这么做的好处是文件本身就是JSONL每条独立追加方便用jq也能直接查调试的时候特别爽。{role: user, content: 找出当前目录下最大的3个文件, timestamp: 2025-01-12T14:02:11Z} {role: assistant, content: find . -type f -exec ls -l {} | sort -k5 -rn | head -3, timestamp: 2025-01-12T14:02:13Z} {role: tool, content: {\stdout\: \...\, \returncode\: 0}, timestamp: 2025-01-12T14:02:15Z}配置文件用YAML格式支持环境变量引用默认内容长这样provider: type: openai_compat base_url: ${OPENSHOELL_BASE_URL} api_key: ${OPENSHOELL_API_KEY} model: gpt-4o-mini temperature: 0.2 history: max_messages: 20 safety: risky_keywords: [rm, dd, mkfs, shutdown, curl, wget] always_confirm: trueAPI密钥一律不落盘。用户在首次启动时通过环境变量或交互式输入提供写进~/.openshell/config.yaml的只保留${OPENSHOELL_API_KEY}这样的变量引用。密钥以纯文本躺在配置里是很多工具的通病谈不上多大的安全隐患但既然能避免就不必留着。4. 实测中踩过的坑与打磨记录这个项目跑通第一版只花了半天但让它在真实终端环境里变得好用花了整整一周。下面四个坑是我记忆最深的。4.1 模型输出里的ANSI颜色代码污染命令提取某次试运行我让OpenShell给我生成一条高亮当前目录下文件列表的命令。模型很贴心地在命令里给文件名加了一堆\x1b[0;32m颜色转义提取模块原样拿去执行Shell直接报command not found: \x1b[0;32m。当时第一反应是模型抽风了后来抓原始输出才发现是这个模型服务默认在文本流里带了颜色标记。排查链路大概是这样的我先在提取函数入口打印repr(reply)发现字符串开头有\x1b[然后搜了一下我调用的API文档发现它有个stream_options参数能关闭输出中的样式标记最后我意识到不能依赖服务商参数最终在提取前统一做strip_ansi清洗。修复后连续跑了一天再没出现过颜色转义混入命令的问题。4.2 流式输出的长行重绘错乱还有一个很折磨人的显示问题。当模型生成的一条解释特别长而终端窗口宽度不够时打印出的长行文本重叠、错位看起来像一堆乱码。排查发现是流式打印和终端自动换行之间打架——一个长行半截刷出来另一行又覆盖上去。解决方式是对输出行做宽度裁切获取终端宽度shutil.get_terminal_size((80, 20)).columns超过宽度的地方切成多段逐段输出或者直接整行输出前先算好长度在到达宽度边界时提前换行。这个修复对体验提升非常明显尤其是用SSH连到小窗口服务器时输出终于不再花屏。4.3 中文编码与shellTrue的连环坑Windows和部分Linux服务器的默认编码不一致OpenShell在Windows终端上跑时报过UnicodeEncodeError: gbk codec cant encode character。后来在运行时统一做了处理把stdout/stderr输出强制按UTF-8解码错误用errorsreplace兜底同时在发送给模型前把不可见控制字符剥掉。另一个坑是执行方式。早期为了省事用subprocess.run(command, shellTrue)后来有一次命令里拼接了用户输入的文件名文件名里带了个; echo hacked的字符串差点出事。我立刻把执行逻辑改成前面说的双轨方案优先shlex.split拆列表直执行失败才退回shell。Shell自由空间很大但OpenShell作为工具应该有责任把安全底线设置得更保守一些。4.4 上下文膨胀烧token第一版OpenShell把整个对话历史全部塞进每次请求跑了二十轮以后一次请求的输入token轻松突破2万。我发现问题的方式是看API账单一次普通对话的token消耗比之前翻了十几倍而且因为上下文太长模型响应变慢使用体验明显发木。处理方法是限制最大历史消息数默认保留最近20条消息超出时丢弃更早的内容。如果是更长的对话我再加一个可选的消息摘要功能——把20轮之前的对话压缩成一段摘要塞进system prompt这样既保留了大方向信息又控制住成本。5. 真实场景试跑与适用范围5.1 三次实测记录第一次实测是日志排查场景。我模拟了一个线上服务的error日志文件输入看看最近的日志里有哪些ERROR按出现次数从多到少排列。OpenShell生成了grep ERROR app.log | sort | uniq -c | sort -rn执行后输出条数统计然后我问了一句最常见的那条错误大概是什么原因基于执行结果它给出了比较合理的分析一个空指针异常字段名和堆栈信息都对得上。这个场景里OpenShell的执行→结果回传→分析链路完整跑通。第二次是文件批量操作。输入把当前目录下所有.jpg文件按修改时间重命名为01.jpg、02.jpg这样。它生成的命令用了循环加mv而且清醒地在确认前加了一句此操作会修改文件名请确认目录正确。我故意在确认界面停了几秒检查路径没问题后回车重命名结果和预期一致。这里可以看出上下文工程里当前目录信息起到了作用。第三次是解释历史命令。我贴了一条很绕的find /data -name *.log -mtime 7 -exec gzip {} \;问这条命令什么意思有什么风险。OpenShell给出的解释完全正确查找/data下七天前的log文件并逐个gzip压缩并提醒我这个操作不可逆、建议先备份。这个场景对运维新人非常友好相当于给每条历史命令配了一个随叫随到的老师。5.2 哪些场景我不建议用OpenShell工具都有边界。OpenShell不适合的场景我踩过之后才拎得清。一是生产环境的大批量删除或变更操作。即使有确认机制模型对哪些文件能删、哪些不能删的理解仍然是概率性的它判断不了业务语义贸然在生产环境用它执行批量操作风险由你自己买单。二是需要严格审计的操作场景比如合规要求每条命令都能追溯到具体的人和具体的目的这类场景不应该让AI生成的命令绕过审批流程OpenShell的确认机制并不是审计记录。三是对实时性要求极高的控制类任务它毕竟有一轮AI调用的延迟不适合做那种立刻执行的操作。我当前的使用习惯是它是我日常终端里的辅助大脑负责翻译、解释、快速给方案但最终的拍板权永远在我自己手里。5.3 下一步想做的事OpenShell目前是单机、单会话的工具后续我计划做三件事。一是添加本地模型接入。不少开源模型已经能胜任自然语言转命令这个任务如果能通过Ollama这类运行时把本地模型接进来敏感数据的隐私问题就解决了离线也能用。二是插件机制。Shell命令千差万别如果能让用户针对特定场景比如kubectl运维、git工作流写自己的提示词模板OpenShell就会从通用工具变成一个可生长的平台。三是多终端历史同步。对话历史存在本地JSONL是够用的但我个人希望将来能用自己的对象存储做跨机器同步这样在办公电脑上讨论过的命令回到家还能接着聊。这三件事里本地模型接入的探索价值最高因为通用API的联网延迟和费用始终是桌面级工具大规模使用的一个阻力。最后分享一个我实际使用下来最舒服的工作流把OpenShell当成终端里的草稿机。我想到一个操作意图就直接用自然语言说出来让它生成命令我不急着执行先看它怎么写有时候它给出的方案比我自己拼的要干净。看的过程也是一个学命令的过程尤其是find和awk组合这类容易忘的参数看AI生成的命令比查文档记得快。这个项目的核心收获不是AI能替代人记命令而是让我重新理解了命令行工具该有的交互方式。未来终端工具的竞争很可能不在于谁能塞进更多功能而在于谁能更自然地理解用户意图。OpenShell是我在这个方向上的一小步实验希望它也能给你一些启发。
阅读完成 · 觉得有帮助?