乐于分享
好东西不私藏

AI Agent 手搓,自制一个命令行助手,告别繁琐的参数

AI Agent 手搓,自制一个命令行助手,告别繁琐的参数
尽管长期在命令行游走,但是仍常常需要翻阅帮助来查看命令的参数,光查看还不行,还要理解学习才能正确应用。
需要帮助时,通常的流程是问 AI,然后复制命令再去执行。
而如果可以不用手动去执行,而由其在理解后自行执行命令拿到结果整理返回,那就是 AI Agent 了。

1、什么是 AI Agent

一句话定义:AI Agent = LLM(大语言模型,Large Language Model) + 工具调用 + 循环决策。

一个 AI Agent 本质上是一个以 LLM 为大脑、以工具为手脚、以循环为心跳的自主系统。它接收用户指令,自主决定“要不要调工具、调哪个工具、调完之后怎么办”,直到给出最终答案。

也有人这样表示,Agent = LLM + 上下文 + 工具,并采取更直观的说法为 Agent = 大脑 + 眼睛 + 手脚。大脑负责思考和决策,眼睛接收环境提供的观察,手脚将决策转化为作用于环境的行动。

这里的上下文,就是丢给 LLM 的内容,内容随着工具的调用动态地变化,同时可附载更多有比如记忆、历史信息,从而让事情逐步往前推动。

2、实现 AI Agent 的主流架构 ReAct

ReAct(Reasoning + Acting,推理 + 行动),是实现 AI Agent 最经典、最主流的架构范式之一(这也意味着 Agent 不只有 ReAct 这一种实现方案)。它采用 Thought(思考)→ Action(行动)→ Observation(观察) 的迭代闭环,将大模型推理与外部工具调用交替执行,依靠工具返回的真实反馈持续调整决策,直至任务完成,能够有效缓解大模型幻觉问题,也是 LangChain、OpenAI Agent SDK 等开发组件内置支持的基础范式。

3、Agent 框架骨架之万能循环

一般 Agent 框架的共同核心:

while 没有得到最终答案:    1. 组装上下文 = 系统提示词 + 历史消息 + 当前输入(含可用工具列表)    2. LLM 决策    → 输出: 自然语言回答 OR 工具调用    3. 如果是工具调用: 执行工具, 把结果回填到消息列表    4. 如果是自然语言: 退出循环, 返回给用户

这时,容易想到死循环,因此在循环中加以最大循环次数的限制是需要的。

4、工具调用的约定

与大模型沟通中对于工具调用的方式,OpenAI 在 2023 年 6 月推出了 Function Calling,为 API 约定了特定的参数属性,包括工具清单描述(tools 哪些工具可用)、工具调用清单(tool_calls 回复哪些工具需要调用)以及调用结果描述(执行完工具后,把结果作为 role="tool" 的消息追加到消息列表)。

5、命令行助手练手

命令行助手这个 AI Agent 中,工具清单描述属性 tools 如果逐个命令去描述,不仅庞大,而且描述清楚是很难的。但实际上,AI 更清楚具体命令所需要的东西,而我们可以提供执行命令的命令让 AI 去发挥,使得工具清单描述只需要一项。这让结构变得简单,功能却可以十分强大。这时,也容易想到安全问题。是的,存在执行危险命令的风险,这也是一个成熟的面向大众的产品并不容易的原因之一。

看看工具清单描述。

RUN_BASH_TOOL = {    "name""run_bash",    "description": (        "在当前工作目录的 Git Bash 中执行一条 shell 命令,并返回命令的标准输出和错误信息。"        "支持所有常见 bash 命令:ls, cat, grep, find, git, python, pip, mkdir, cp, mv 等。"        "可以执行多步任务:先看目录结构,再读文件,再修改文件,等等。"        "【重要】如果命令会产生交互式提示(如需要输入 y/n),请加 -y 或 --yes 等非交互参数。"    ),    "parameters": {        "type""object",        "properties": {            "command": {                "type""string",                "description""要执行的 bash 命令字符串,例如 'ls -la'、'cat lesson-01.md | head -20'",            }        },        "required": ["command"],    },}

有了基本原理的了解与需求的确定,可以比较容易在 AI 辅助编码下生成 Python 代码。运行效果如下:

👤 你想让 Agent 做什么?(输入 quit 退出)> 本机的外网 ip 是什么============================================================🔄 第 1 轮 LLM 调用============================================================⚡ 调用工具 run_bash:curl ifconfig.me📋 工具返回(预览):222.244.218.30[stderr]  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current                                    Dload  Upload   Total   Spent    Left  Speed       0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0    100    14  100    14    0     0     18      0 --:--:-- --:--:-- --:--:--    18    100    14  100    14    0     0     18      0 --:--:-- --:-...============================================================🔄 第 2 轮 LLM 调用============================================================💭 LLM 思考/回答:我执行了 `curl ifconfig.me` 命令来获取本机的外网 IP 地址。您的本机外网 IP 地址是:**222.244.218.30**✅ LLM 选择不再调用工具,循环结束。============================================================🎯 最终结果:我执行了 `curl ifconfig.me` 命令来获取本机的外网 IP 地址。您的本机外网 IP 地址是:**222.244.218.30**============================================================

访问网络,写文件,都不在话下,因为 bash 下的命令丰富着。

6、附提示词与 Demo 代码

本示例在 trae 中实现,使用的提示词为:

对于AI Agent,如果我的 tool 就定义为 git bash 中运行命令,这样定义一个工具,后边的工具调用都很灵活了。刚好 bit bash 那些命令不熟悉,这样一来,一个自然语言执行命令的工具就出来了。请分析我这个描述,就形成一个 python 实现,模型我将使用 deepseek,apikey 位置预留即可。

完整示例代码如下:

"""bash_agent.py - 单工具 AI Agent:用自然语言驱动 Git Bash 执行命令核心设计思想:    AI Agent = LLM + 工具调用 + 循环决策    只定义 1 个工具 run_bash(command),LLM 把自然语言转化为 bash 命令执行,    再根据执行结果决定下一步,直到完成用户任务。模型:DeepSeek(兼容 OpenAI SDK 格式)工具:run_bash(command: str) -> str  在 Git Bash 中执行命令并返回 stdout/stderr"""import jsonimport subprocessimport osfrom openai import OpenAI# ============================================================# 配置区:DeepSeek API Key 预留,替换为自己的 Key 即可运行# ============================================================DEEPSEEK_API_KEY = "sk-24179d----------"   # <-- 替换你的 API KeyDEEPSEEK_BASE_URL = "https://api.deepseek.com/v1"DEEPSEEK_MODEL = "deepseek-chat"                      # 或 deepseek-coder# Windows 下 Git Bash 路径(根据实际安装位置调整)GIT_BASH_PATH = r"C:\Program Files\Git\bin\bash.exe"# 危险命令黑名单(LLM 再聪明也需要安全护栏)DANGEROUS_PATTERNS = [    "rm -rf /""rm -rf /*""rm -rf ~",    "mkfs""dd if="":(){ :|:& };:""fork bomb",    "del /s /q C:\\""format C:",]MAX_ITERATIONS = 15# ============================================================# 工具实现:run_bash —— 在 Git Bash 里跑一条命令# ============================================================def run_bash(command: str) -> str:    """在 Git Bash(Windows)中执行一条 shell 命令,返回 stdout 和 stderr。    Args:        command: 要执行的 bash 命令,如 "ls -la"、"cat README.md"    Returns:        命令的标准输出 + 标准错误(合并为一个字符串)    """    for pattern in DANGEROUS_PATTERNS:        if pattern in command:            return f"[安全拦截] 检测到危险命令模式 '{pattern}',已拒绝执行。"    try:        if os.name == "nt" and os.path.exists(GIT_BASH_PATH):            result = subprocess.run(                [GIT_BASH_PATH, "-c", command],                capture_output=True,                text=True,                timeout=60,                cwd=os.getcwd(),            )        else:            result = subprocess.run(                ["bash""-c", command],                capture_output=True,                text=True,                timeout=60,                cwd=os.getcwd(),            )        output = ""        if result.stdout:            output += result.stdout        if result.stderr:            output += ("\n[stderr]\n" if output else "[stderr]\n") + result.stderr        if result.returncode != 0:            output += f"\n[exit code: {result.returncode}]"        return output.strip() or "(命令无输出)"    except subprocess.TimeoutExpired:        return "[错误] 命令执行超时(60秒上限)"    except FileNotFoundError:        return f"[错误] 未找到 Bash。请确认 Git 已安装,或检查路径:{GIT_BASH_PATH}"    except Exception as e:        return f"[执行异常] {type(e).__name__}{e}"# ============================================================# 工具定义(Function Calling 的 JSON Schema)# ============================================================RUN_BASH_TOOL = {    "name""run_bash",    "description": (        "在当前工作目录的 Git Bash 中执行一条 shell 命令,并返回命令的标准输出和错误信息。"        "支持所有常见 bash 命令:ls, cat, grep, find, git, python, pip, mkdir, cp, mv 等。"        "可以执行多步任务:先看目录结构,再读文件,再修改文件,等等。"        "【重要】如果命令会产生交互式提示(如需要输入 y/n),请加 -y 或 --yes 等非交互参数。"    ),    "parameters": {        "type""object",        "properties": {            "command": {                "type""string",                "description""要执行的 bash 命令字符串,例如 'ls -la'、'cat lesson-01.md | head -20'",            }        },        "required": ["command"],    },}TOOLS = [RUN_BASH_TOOL]# ============================================================# System Prompt:告诉 LLM 它的身份和行为规范# ============================================================SYSTEM_PROMPT = """你是一个 Bash 命令执行助手 Agent。你拥有一个工具 run_bash,可以在用户的 Git Bash 中执行任意命令。## 你的工作方式(ReAct 循环)1. 收到用户的自然语言请求后,先思考需要执行哪些命令2. 调用 run_bash 工具执行命令3. 根据命令的输出结果,决定下一步是继续调用工具还是给出最终回答4. 重复以上步骤,直到任务完成## 行为准则1. **先看后改**:修改文件前,先用 cat/ls 查看现有内容和目录结构,不要瞎猜2. **小步前进**:一次不要执行太复杂的命令链,拆成几步便于你根据中间结果调整3. **善用组合**:学会用管道 |、grep、head、tail、wc 等过滤输出,避免把整个大文件塞给你4. **错误处理**:如果命令报错,仔细读错误信息,调整后重试,不要直接说"失败了"5. **安全意识**:   - rm -rf / 等破坏性命令已被系统拦截,你无法调用   - 删除文件前先用 ls 确认路径   - 覆盖文件前先看原文件内容6. **结果汇报**:完成任务后,用简洁的中文总结你做了什么、关键结果是什么,不需要把所有命令历史贴出来## 当前工作目录用户的 lessons 目录,包含课程 markdown 文件和示例代码。"""# ============================================================# Agent 核心循环:万能循环的 2.0 版本# ============================================================def agent(user_prompt: str, *, debug: bool = True) -> str:    """启动一个带单工具(run_bash)的 ReAct Agent。    Args:        user_prompt: 用户的自然语言任务描述        debug: 是否打印每一轮的思考/行动/观察日志    Returns:        Agent 最终给出的自然语言回答    """    client = OpenAI(api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL)    messages = [        {"role""system""content": SYSTEM_PROMPT},        {"role""user""content": user_prompt},    ]    tool_handlers = {"run_bash": run_bash}    for i in range(MAX_ITERATIONS):        if debug:            print(f"\n{'='*60}")            print(f"🔄 第 {i+1} 轮 LLM 调用")            print(f"{'='*60}")        resp = client.chat.completions.create(            model=DEEPSEEK_MODEL,            messages=messages,            tools=[{"type""function""function": t} for t in TOOLS],            temperature=0.2,        )        msg = resp.choices[0].message        messages.append(msg)        if msg.content and debug:            print(f"💭 LLM 思考/回答:\n{msg.content}\n")        if not msg.tool_calls:            if debug:                print("✅ LLM 选择不再调用工具,循环结束。")            return msg.content or "(无回答内容)"        for call in msg.tool_calls:            func_name = call.function.name            try:                args = json.loads(call.function.arguments)            except json.JSONDecodeError as e:                args = {}                result = f"[参数解析失败] JSON 格式错误:{e}\n原始 arguments:{call.function.arguments}"                if debug:                    print(f"❌ {result}")            else:                if func_name not in tool_handlers:                    result = f"[工具不存在] 没有名为 '{func_name}' 的工具"                    if debug:                        print(f"❌ {result}")                else:                    cmd = args.get("command""")                    if debug:                        print(f"⚡ 调用工具 {func_name}{cmd}")                    result = tool_handlers[func_name](**args)                    if debug:                        preview = result[:400] + ("..." if len(result) > 400 else "")                        print(f"📋 工具返回(预览):\n{preview}\n")            messages.append({                "role""tool",                "tool_call_id": call.id,                "content"str(result),            })    return f"⚠️  已达到最大迭代次数({MAX_ITERATIONS} 轮),任务未完成。"# ============================================================# 交互式运行:python bash_agent.py# ============================================================if __name__ == "__main__":    print("=" * 60)    print("  🤖 Bash Agent - 用自然语言驱动 Git Bash")    print(f"  模型:{DEEPSEEK_MODEL}   最大轮数:{MAX_ITERATIONS}")    print("=" * 60)    if DEEPSEEK_API_KEY.startswith("sk-your-"):        print("\n⚠️  请先在 bash_agent.py 顶部把 DEEPSEEK_API_KEY 换成你自己的 Key!")        print("    获取地址:https://platform.deepseek.com/\n")    examples = [        "列出当前目录下所有的 md 文件",        "统计 lesson-01.md 有多少行,包含多少个 'Agent' 关键词",        "看看 lesson-02.md 的前 30 行讲了什么",    ]    print("📌 可以试试这些示例:")    for idx, ex in enumerate(examples, 1):        print(f"   {idx}{ex}")    print()    while True:        try:            user_input = input("👤 你想让 Agent 做什么?(输入 quit 退出)\n> ").strip()        except (EOFError, KeyboardInterrupt):            print("\n👋 再见!")            break        if not user_input:            continue        if user_input.lower() in ("quit""exit""q"):            print("👋 再见!")            break        answer = agent(user_input, debug=True)        print(f"\n{'='*60}")        print(f"🎯 最终结果:\n{answer}")        print(f"{'='*60}\n")