最近这两年,终端里的“智能感”明显变强了。以前我们在命令行里只是敲 Git 命令、执行测试、翻日志,现在越来越多的工具把大模型能力直接塞进了终端。这个“Hey!”系列项目,就是一种很典型的尝试:在终端里输入一句自然语言,就能触发一个 AI 助手的响应,而不是靠一长串参数去拼一个 API 请求。
很多开发者第一次看到这类项目时,会觉得“这不就是封装了一下大模型 API 吗”,但实际深入之后会发现,真正值得研究的是它背后的三层结构:输入层怎么解析自然语言、中间层怎么设计上下文和工具调用、输出层怎么处理流式内容和错误恢复。如果只看表面,很容易误以为这只是个玩具;但对经常在终端环境下工作的人来说,这类项目真正降低的是“离开终端去浏览器里找工具”的切换成本。
这篇文章会围绕“Hey!”这个终端 AI 助手项目,从技术原理、环境搭建、配置方法、核心代码实现、运行验证到生产环境的最佳实践,完整讲清楚。读完你不仅能跑通一个最小可用的终端 AI 助手,还能理解这类项目在工程设计上需要注意的关键点,以及哪些地方容易踩坑。
1. 这篇文章真正要解决的问题
先说说读者最关心的问题:我为什么要在终端里用 AI 助手?
最常见的开发场景是这样:你正在排查一个线上问题,日志文件在服务器上,命令在终端里,上下文也都在终端里。这时候想快速请教 AI,往往需要打开浏览器、登录平台、复制日志、粘贴问题、等回答、再复制答案回来。一套流程下来,少说也要两三分钟,而且上下文还经常丢。
终端 AI 助手想要解决的就是这种场景问题。它不是要取代图形化的 AI 服务,而是把“提问—理解—回答”这个闭环压缩在同一个终端环境里。你不需要切换窗口,不需要复制粘贴那么多文本,模型可以直接读取当前目录的文件、管道输入的内容,甚至帮你执行命令。
这篇文章特别适合以下读者:
- 每天大量时间在终端工作的后端开发、运维、测试工程人员。
- 对 LLM API 感兴趣,但不想一上来就研究完整 RAG 框架的开发者。
- 需要私密、可控方式对接模型能力的团队。
当然,也需要先说明边界:这类终端 AI 助手并不是“万能命令解释器”,它依赖模型能力、API 配置和工具权限设计。如果用不好,可能比不用更危险。所以这篇文章不仅教你跑起来,更重要的是帮你建立一套安全的、可维护的使用习惯。
2. “Hey!”是什么:核心概念与技术原理
2.1 终端 AI 助手的工作方式
“Hey!”本质上是一个命令行工具。你在终端里输入一个以hey开头的指令,例如:
hey 帮我解释一下这个目录下的 main.py 做了什么工具收到指令后,会做三件事:
- 读取指令文本和必要的上下文(如当前目录文件、剪贴板内容)。
- 把文本发给大模型 API,并附带系统提示词和对话历史。
- 接收模型返回结果,以流式或非流式方式输出到终端。
这里的关键点在于,很多新手以为“终端问 AI”就是把问题原样丢给 API。实际操作中,为了让模型更好地理解,通常需要构造一个 system 级别的 prompt,告诉模型“你是一个运行在终端中的助手,输出要简洁、准确、适合阅读”。如果没有这一步,模型可能给出冗长的营销式回答,这在终端里是比较尴尬的体验。
2.2 LLM 的对话协议
目前主流的模型服务大多提供了兼容 OpenAI 风格的 HTTP 接口。其中最核心的就是/chat/completions接口。这个接口接收一个 JSON 结构,里面包含模型名称、消息列表、温度参数等。
一个最简单的请求内容如下:
{ "model": "your-model-name", "messages": [ {"role": "system", "content": "你是一个终端助手,回答要简洁。"}, {"role": "user", "content": "ls 命令有哪些常用参数?"} ], "stream": false }其中role有三种常见取值:
system:系统级指令,用于设定助手的角色和行为准则。user:用户的输入。assistant:模型此前生成的内容,用于多轮对话。
理解了这套协议,你就掌握了整个终端的核心枢纽:无论前面套多漂亮的 CLI 外壳,最终都是要转换成上面这个 JSON 结构发送给模型。
2.3 终端工具与普通 Python 脚本的差异
有人会问:这不就是一个 Python 脚本调用 requests 库吗?为什么值得单独做成一个项目?
区别在于工程化程度上。一个合格的终端 AI 助手,需要处理:
- 参数解析:区分
--model、--temperature、--context等选项。 - 流式输出:模型生成过程中就逐步打印,而不是等完全生成后再一次性输出。
- 上下文管理:历史对话保存在内存或本地文件里,多轮对话不丢。
- 错误处理:API 超时、网络中断、模型返回异常,都要有明确的提示。
- 权限控制:哪些命令可以让模型调用执行,哪些不能,必须提前设计。
这些点单个拿出来都不复杂,但组合在一起,就是一个小型工程课题。这篇文章的实操部分就是围绕这些点展开的。
3. 环境准备与前置条件
在开始写代码之前,先把环境准备好。下面的版本以当前常见稳定版本为例,具体版本请以实际项目为准,本文重点演示通用思路。
3.1 操作系统与环境
建议使用 macOS 或 Linux 系统,因为终端工具在这两类系统上体验最自然。Windows 用户建议使用 WSL 2,或者 Windows Terminal + PowerShell Core。其实 Python 代码本身是跨平台的,但路径拼接和管道输入在非 Unix 环境下略有差异。
3.2 Python 版本
推荐使用 Python 3.10 及以上版本。原因有几个:
typing模块在 3.10 后支持更清晰的联合类型写法。asyncio的 API 更稳定。dotenv等配置库对环境变量的支持更友好。
检查命令:
python3 --version如果输出类似Python 3.10.12就满足要求。
3.3 API 获取与配置
这里需要你有一个可以访问的大模型 API 服务。目前市面上主流的做法是:找一个兼容 OpenAI 接口格式的服务商,获取对应的 API Key 和 Base URL。
注意:不要在生产环境中使用个人账号泄露的 Key,也不要把 Key 直接硬编码到代码里。正确做法是写入环境变量或本地配置文件中,并设置文件权限。
建议创建项目目录:
mkdir -p ~/projects/hey-assistant cd ~/projects/hey-assistant3.4 依赖安装
本项目最小化依赖,核心只需要两个库:
requests:发送 HTTP 请求。python-dotenv:加载.env配置文件。
安装命令:
pip install requests python-dotenv如果你想做更高级的流式响应,还可以安装httpx或openai官方 SDK。本文先以requests为例,等理解了原理后再升级到官方 SDK 也不迟。
4. 环境搭建与基础配置
4.1 创建项目结构
推荐的项目结构如下:
hey-assistant/ ├── hey.py ├── .env ├── .env.example ├── requirements.txt └── README.md其中:
hey.py:主入口文件。.env:存放 API Key 等敏感信息,不要提交到 Git。.env.example:配置模板,提交到 Git,方便协作。requirements.txt:依赖清单。
4.2 配置文件说明
先创建.env文件:
touch .env编辑内容:
# 请将 YOUR_API_KEY 替换为你的真实密钥 HEY_API_KEY=YOUR_API_KEY HEY_API_BASE=https://api.example.com/v1 HEY_MODEL=your-model-name这里解释一下每个配置项的用途:
HEY_API_KEY:调用模型接口的密钥,属于敏感信息。HEY_API_BASE:API 服务的根地址,不同服务商有所不同,通常以/v1结尾。HEY_MODEL:模型名称,需要向服务商确认你的账号是否开通了该模型的权限。
再创建.env.example:
HEY_API_KEY=replace-with-your-key HEY_API_BASE=https://api.example.com/v1 HEY_MODEL=your-model-name这个文件用于版本管理,让别人知道要配置哪些环境变量。
4.3 加载配置的通用代码
在hey.py顶部加载配置:
import os from dotenv import load_dotenv load_dotenv() API_KEY = os.getenv("HEY_API_KEY") API_BASE = os.getenv("HEY_API_BASE") MODEL = os.getenv("HEY_MODEL") if not API_KEY or not API_BASE or not MODEL: raise ValueError("请检查 .env 文件,必须配置 HEY_API_KEY、HEY_API_BASE、HEY_MODEL")这段代码的作用很明确:启动时强制检查三项关键配置。如果缺了任何一项,直接报错退出。这样比运行时接口返回 401 更容易排查问题。
5. 核心流程拆解
5.1 主流程设计
终端 AI 助手的运行流程可以拆成六个步骤:
- 解析用户输入。
- 读取可选上下文(如当前目录文件)。
- 构造 messages 列表。
- 调用 API。
- 处理输出。
- 保存历史。
用代码来表示主流程:
def main(): user_input = parse_args() context = read_context() messages = build_messages(user_input, context) response = call_api(messages) print_response(response) save_history(user_input, response)5.2 参数解析
参数解析决定了这个工具用起来顺不顺手。我们希望支持以下用法:
python hey.py "解释一下什么是死锁" python hey.py --file main.py "分析这个文件的代码质量" python hey.py --history "继续上一轮对话"这里用 argparse 实现:
import argparse def parse_args(): parser = argparse.ArgumentParser(description="Hey!终端 AI 助手") parser.add_argument("question", type=str, help="你要问的问题") parser.add_argument("--file", type=str, help="附加文件内容作为上下文") parser.add_argument("--history", action="store_true", help="继续上一轮对话") parser.add_argument("--model", type=str, default=MODEL, help="指定模型名称") return parser.parse_args()这里真正值得注意的地方是--file参数。它允许你在不手动复制代码内容的情况下,直接把文件内容作为上下文传给模型。这个功能看似简单,却在真实开发中非常高频。比如你写了一段有问题的代码,想问问模型哪里不对,如果还要手动复制粘贴,就失去了终端工具的意义。
5.3 读取文件上下文
读取文件时要注意两个陷阱:编码问题和文件过大问题。
def read_file_content(file_path): try: with open(file_path, "r", encoding="utf-8") as f: content = f.read() except UnicodeDecodeError: with open(file_path, "r", encoding="gbk", errors="ignore") as f: content = f.read() if len(content) > 30000: content = content[:30000] + "\n... (内容过长,已截断)" return content这里做了两件事:
- 兼容 UTF-8 和 GBK 两种常见编码。
- 限制上下文长度,避免一次请求超过模型 token 上限。
第二个限制非常关键。很多人在调用模型 API 时遇到 “context length exceeded” 错误,往往就是因为把整个大文件丢给模型。实际项目中,更推荐的做法是配合grep、sed等命令先缩小范围,再把精确片段传给模型。
5.4 构造消息列表
构造消息列表时要区分首轮对话和后续对话。首轮通常只包含 system 和 user;后续对话要追加 assistant 的历史回复。
def build_messages(question, file_content=None, history=None): system_prompt = ( "你是一个运行在终端中的 AI 助手。" "回答要求:简洁、准确、直接。" "不要输出与问题无关的营销文案。" "如果涉及代码,请用代码块格式输出。" ) messages = [{"role": "system", "content": system_prompt}] if history: messages.extend(history) if file_content: question = f"以下是文件内容:\n{file_content}\n\n用户问题:{question}" messages.append({"role": "user", "content": question}) return messages这里的 system prompt 值得反复调试。不同模型对指令的服从程度不同,如果你发现模型回答过于啰嗦,可以在 system prompt 里加强约束,例如“不超过 200 字”“不要客套”等。
5.5 调用 API
调用 API 是整个流程的核心。这里要区分流式和非流式。非流式写法简单,适合初版实现:
import requests import json def call_api(messages, model_name=MODEL): url = f"{API_BASE}/chat/completions" headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json" } payload = { "model": model_name, "messages": messages, "temperature": 0.7, "stream": False } try: resp = requests.post(url, headers=headers, json=payload, timeout=60) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"] except requests.exceptions.Timeout: return "请求超时,请检查网络或稍后重试。" except requests.exceptions.ConnectionError: return "无法连接到 API 服务,请检查 API_BASE 配置。" except requests.exceptions.HTTPError as e: return f"API 返回错误:{e.response.status_code},请检查 Key 和权限。" except (KeyError, IndexError): return "响应格式异常,请检查模型名称是否正确。"这段代码里最容易被新手忽略的是异常捕获。如果不对超时和连接错误做处理,一旦 API 服务不稳定,整个程序就会直接崩溃,用户连一个可读的错误提示都看不到。
5.6 保存对话历史
保存历史是为了支持多轮对话。最简单的做法是用 JSON 文件存储:
import json import os HISTORY_FILE = os.path.expanduser("~/.hey_history.json") def append_history(user_input, response): history = load_history() history.append({"role": "user", "content": user_input}) history.append({"role": "assistant", "content": response}) # 只保留最近 20 条消息,避免历史过长 history = history[-20:] with open(HISTORY_FILE, "w", encoding="utf-8") as f: json.dump(history, f, ensure_ascii=False, indent=2) def load_history(): if not os.path.exists(HISTORY_FILE): return [] with open(HISTORY_FILE, "r", encoding="utf-8") as f: return json.load(f)这里要注意:历史文件不能无限增长。对话轮数多了以后,历史消息会占用大量 token,也会拖慢响应速度。所以这里做了一个简单的截断策略:只保留最近 20 条消息。真实项目里还可以按会话 ID 区分多份历史文件。
6. 完整示例代码实现
完整示例把上面拆解的模块整合到一起。为方便阅读,下面的代码全部写在hey.py中,实际工程中你可以按模块拆分。
# 文件路径:hey.py import argparse import json import os import requests from dotenv import load_dotenv load_dotenv() API_KEY = os.getenv("HEY_API_KEY") API_BASE = os.getenv("HEY_API_BASE") MODEL = os.getenv("HEY_MODEL") HISTORY_FILE = os.path.expanduser("~/.hey_history.json") def parse_args(): parser = argparse.ArgumentParser(description="Hey!终端 AI 助手") parser.add_argument("question", type=str, help="你要问的问题") parser.add_argument("--file", type=str, help="附加文件内容作为上下文") parser.add_argument("--history", action="store_true", help="继续上一轮对话") parser.add_argument("--model", type=str, default=MODEL, help="指定模型名称") return parser.parse_args() def read_file_content(file_path): try: with open(file_path, "r", encoding="utf-8") as f: content = f.read() except UnicodeDecodeError: with open(file_path, "r", encoding="gbk", errors="ignore") as f: content = f.read() if len(content) > 30000: content = content[:30000] + "\n... (内容过长,已截断)" return content def load_history(): if not os.path.exists(HISTORY_FILE): return [] with open(HISTORY_FILE, "r", encoding="utf-8") as f: try: return json.load(f) except json.JSONDecodeError: return [] def append_history(user_input, response): history = load_history() history.append({"role": "user", "content": user_input}) history.append({"role": "assistant", "content": response}) history = history[-20:] with open(HISTORY_FILE, "w", encoding="utf-8") as f: json.dump(history, f, ensure_ascii=False, indent=2) def build_messages(question, file_content=None, history=None): system_prompt = ( "你是一个运行在终端中的 AI 助手。" "回答要求:简洁、准确、直接。" "不要输出与问题无关的营销文案。" "如果涉及代码,请用代码块格式输出。" ) messages = [{"role": "system", "content": system_prompt}] if history: messages.extend(history) if file_content: question = f"以下是文件内容:\n{file_content}\n\n用户问题:{question}" messages.append({"role": "user", "content": question}) return messages def call_api(messages, model_name=MODEL): url = f"{API_BASE}/chat/completions" headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json" } payload = { "model": model_name, "messages": messages, "temperature": 0.7, "stream": False } try: resp = requests.post(url, headers=headers, json=payload, timeout=60) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"] except requests.exceptions.Timeout: return "请求超时,请检查网络或稍后重试。" except requests.exceptions.ConnectionError: return "无法连接到 API 服务,请检查 API_BASE 配置。" except requests.exceptions.HTTPError as e: return f"API 返回错误:{e.response.status_code},请检查 Key 和权限。" except (KeyError, IndexError): return "响应格式异常,请检查模型名称是否正确。" def main(): args = parse_args() file_content = None if args.file: if not os.path.exists(args.file): print(f"文件不存在:{args.file}") return file_content = read_file_content(args.file) history = load_history() if args.history else None messages = build_messages(args.question, file_content, history) response = call_api(messages, args.model) print(response) append_history(args.question, response) if __name__ == "__main__": main()这段代码虽然只有一百多行,但已经具备一个终端 AI 助手的最小完整形态。它支持参数解析、文件上下文、多轮对话和基本的异常处理,可以直接跑通一个真实的问答流程。
7. 运行结果与效果验证
7.1 运行方式
先确认.env配置正确,然后运行:
python hey.py "Python 中 list 和 tuple 有什么区别?"预期输出类似:
list 是可变序列,tuple 是不可变序列。list 支持 append、pop 等修改操作,tuple 创建后不能修改。在需要防止数据被意外修改的场景下,优先使用 tuple。注意,实际模型输出语言和内容会因模型而异,但关键是要确认程序本身跑通了,并且没有报配置错误。
7.2 验证文件上下文
创建一个测试文件:
cat > test_example.py << 'EOF' def add(a, b): return a + b EOF然后运行:
python hey.py --file test_example.py "这个函数有没有问题?"如果输出中提到了add函数的参数和返回值,说明文件内容成功传给了模型。
7.3 验证多轮对话
第一轮:
python hey.py "给我一个快速排序的 Python 实现"第二轮:
python hey.py --history "刚才的代码里,如果数组很大,会不会有性能问题?"如果第二轮的回复涉及“上一轮提供的快速排序”,说明历史保存和追加逻辑正常。
7.4 验证失败场景
把.env里的HEY_API_KEY故意改错,再运行:
python hey.py "你好"预期会看到提示:
API 返回错误:401,请检查 Key 和权限。这说明异常处理逻辑生效了,而不是直接抛出一个难以理解的堆栈。
8. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 启动时报错,提示缺少 HEY_API_KEY | .env文件未创建或变量名拼写错误 | 检查.env是否存在并确认HEY_API_KEY拼写 | 复制.env.example为.env并填写真实值 |
| 请求返回 401 | API Key 无效或未在请求头中正确携带 | 打印headers检查 Authorization 字段 | 重新生成 API Key,确认没有多余空格 |
| 请求返回 404 | API_BASE 路径不对 | 确认接口地址是否为{API_BASE}/chat/completions | 查看服务商文档,确认 Base URL 是否以/v1结尾 |
| 返回内容很长,刷屏 | 模型没有按终端场景约束输出 | 修改 system prompt,要求“200字以内” | 在 system prompt 中加入长度限制 |
| 中文乱码 | 终端编码或文件编码不匹配 | 检查终端字符集和 Python 读取文件的编码方式 | 统一使用 UTF-8 编码,Windows 下设置chcp 65001 |
| 多轮对话答非所问 | 历史消息过多或历史文件损坏 | 打开~/.hey_history.json查看内容 | 删除历史文件或限制保留条数 |
| 大文件内容超限 | 一次性把整个文件塞进 context | 查看报错信息中的 token 数 | 用sed/grep先截取关键片段,或调整read_file_content中的截断长度 |
| 网络超时 | 网络不稳定或 API 服务响应慢 | 检查网络连接,用curl测试接口连通性 | 增加 timeout 值,或改为流式输出提升体验 |
这里的核心思路是:任何错误都要能归因到一个可操作的排查动作。不要只把异常堆栈抛给用户,这对终端工具的用户体验是致命的。
9. 最佳实践与工程建议
9.1 安全边界与权限管理
使用终端 AI 助手时,安全是头号问题。尤其是当你准备让工具具备“执行命令”能力时,必须设置严格的白名单机制。本文示例中的hey.py只负责输出文本,不执行任何系统命令,这是一个相对安全的边界。
如果你未来想扩展为“让模型帮你执行 shell 命令”,建议遵守以下原则:
- 执行命令前必须打印完整命令,并等待用户确认。
- 只允许执行白名单命令,例如
git status、kubectl get等只读命令。 - 禁止直接执行包含管道符、重定向、
sudo、rm等危险操作。 - 所有执行记录要写入日志,便于追溯。
9.2 密钥管理
永远不要把你的 API Key 提交到 Git 仓库,即使仓库是私有的,也可能因为协作成员误操作而泄露。
推荐做法:
- 在
.gitignore中加入.env。 - 使用
export HEY_API_KEY=xxx注入环境变量。 - 在 CI/CD 中使用密钥管理服务,而不是明文配置文件。
9.3 配置管理与多环境切换
实际项目中,你可能需要在开发、测试、生产环境中使用不同的模型服务。建议在.env之外,支持按环境加载配置:
import os from dotenv import load_dotenv ENV = os.getenv("HEY_ENV", "dev") load_dotenv(f".env.{ENV}")这样你就可以同时维护.env.dev和.env.prod两份配置,切换环境时只需要修改HEY_ENV变量。
9.4 日志记录
对于长期使用的工具,建议增加日志文件:
import logging logging.basicConfig( filename=os.path.expanduser("~/.hey_assistant.log"), level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s" )每次请求的模型、耗时、状态码都可以记录进去。当问题出现时,先查日志,而不是让用户反复重试。
9.5 模型版本兼容
大模型服务和开源模型迭代速度非常快。你昨天配置的模型名称,今天可能已经下线。所以在设计工具时,不要把模型名称写死在代码里,而是放在配置文件中。同时在异常处理中,遇到模型不存在的错误码时,要提示用户去检查HEY_MODEL配置。
9.6 交互体验优化
除了功能正确性,终端工具的体验也很重要。几个容易忽略的细节:
- 请求期间可以打印
思考中...,避免用户以为程序卡住了。 - 输出结束后打印分隔线,方便在长对话中区分不同轮次。
- 支持
Ctrl+C中断请求,避免长时间等待。
10. 总结与后续学习方向
把“Hey!”从零跑通,核心并不是写那几百行代码,而是理解终端 AI 工具在设计时需要考虑的完整链路:输入解析、上下文构造、配置管理、异常处理、历史存储和安全边界。这套思路完全可以迁移到其他 CLI 工具的开发中。
如果你想继续深入,建议按以下方向延伸:
- 流式输出。使用 SSE 或官方 SDK 的 streaming 模式,让模型逐字输出,大幅提升交互体验。
- 嵌入向量检索。在本地建立小型的代码知识库,让模型在回答前先检索相关内容,减少无关输出。
- 工具调用能力。让模型可以主动调用本地函数或外部 API,把“问答”升级为“任务执行”。
- 插件化架构。把不同的知识库、模型服务、上下文来源抽象成插件,做成可复用的脚手架。
最后提醒一点:这类工具的第一版,永远不要把权限做太大。先用只读模式跑通流程,确认稳定后再逐步扩展。终端工具的安全事故往往不是模型答错,而是权限边界没控制好。