前言:回归本质

在构建复杂的 AI 编码助手时,我们往往会不断增加功能:文件读取工具、写入工具、搜索工具、编辑工具、子代理系统、任务调度器、权限管理……代码从 50 行增长到 500 行,再到 5000 行。

但如果我们停下来问一个问题:编码代理的本质到底是什么?

答案可能出乎意料的简单。


一、核心思想:一个工具,一切皆可

Unix 哲学的启示

Unix 哲学有两条核心原则:

  1. 一切皆文件 —— 无论是文件、目录、设备,还是进程,在 Unix 中都以文件形式存在
  2. 一切皆可管道 —— 任何程序的输出都可以成为另一个程序的输入

这意味着什么?

当你有一个 bash 终端时,你拥有了通往整个操作系统的入口:

操作类型 Bash 命令
读取文件 cat, head, tail, grep
写入文件 echo ‘…’ > file, sed -i
搜索内容 grep, find, rg, ls
执行程序 python, npm, make, go
派生子进程 python bash_agent.py “task”

这就是核心洞察:不需要多个专用工具,一个 bash 工具就足够了。

表层 vs 深层

表层视角(多个工具) 深层视角(一个工具)
read_file cat file.txt
write_file echo “content” > file.txt
edit_file sed -i ‘s/old/new/g’ file.txt
search grep -r “pattern” dir/
execute python script.py

表面上是不同的功能,深层上都是 bash 命令的变体。


二、递归:层级能力的涌现

最关键的一行命令

python bash_agent.py "task description"

这看起来只是一个普通的命令,但它隐藏着一个深刻的洞察:

当代理通过 bash 调用自身时,子代理就诞生了。

进程隔离 = 上下文隔离

想象一下这个调用链:

主进程 (Parent)
    │
    │ bash: python bash_agent.py "分析项目架构"
    ▼
子进程 (Child 1) - 全新的 history=[]
    │
    │ bash: find . -name "*.py"
    │ bash: grep -r "class.*Model" src/
    ▼
子进程 (Child 2) - 全新的 history=[]
    │
    │ bash: python bash_agent.py "总结每个模块的职责"
    │
    ▼
... 无限嵌套可能

每个子进程都拥有:

  • 独立的内存空间 —— history = [],不会污染父进程的上下文
  • 独立的执行环境 —— 工具调用的结果只影响当前进程
  • 自然的隔离边界 —— 操作系统提供的进程隔离机制

递归的力量

递归在计算机科学中有一个美妙的特性:它天然地产生层级结构

def agent(task):
    if is_simple(task):
        return solve_directly(task)
    else:
        subtask1, subtask2 = decompose(task)
        result1 = bash_agent(subtask1)  # 调用自己!
        result2 = bash_agent(subtask2)  # 调用自己!
        return combine(result1, result2)

这就是任务分解的优雅实现:

  • 不需要复杂的任务调度器
  • 不需要子进程池管理
  • 不需要父子进程通信协议
  • 操作系统已经为你做好了一切

信息传递的唯一通道

在 Bash Agent 中,所有信息都通过 stdout 传递给模型:

─────────────────────────────────────────────────────────
                    模型                                 
             history: [消息历史]      
                                                          
           工具调用: bash: ls -la                                  
                                                          
  ──────────────▶ stdout 捕获 ◀─────────────              
                                                          
  工具结果:                                               
    drwxr-xr-x  user  .git/                              
    -rw-r--r--  user  main.py                            
    -rw-r--r--  user  README.md                          
─────────────────────────────────────────────────────────

无论执行什么命令(ls, cat, grep, find…),结果都通过 stdout 返回给模型。这带来两个好处:

  1. 统一的接口 —— 所有操作的输出格式一致,模型容易理解
  2. 自然的过滤 —— stderr 可以单独处理,只将 stdout 作为主要信息源

三、四大法则

法则一:一个工具足够

Bash 是通往一切操作的统一接口。

不要因为操作看起来不同就创建不同的工具。文件读写、代码搜索、程序执行、网络请求——本质上都是 bash 命令。

统一接口的优势:

  • 认知负担低 —— 只需理解一种工具模式
  • 实现简单 —— 只需维护一个工具实现
  • 扩展性强 —— 任何新的操作类型都可以通过新命令实现

法则二:递归 = 层级

自我调用自然产生树形结构。

递归是计算机科学中最强大的抽象之一。通过递归,我们可以:

  • 将复杂任务分解为子任务
  • 并行处理独立的子任务
  • 形成自然的调用层次

无需显式的子代理系统,递归调用本身就实现了子代理。

法则三:进程 = 隔离

操作系统提供的隔离机制免费可用。

不要自己实现复杂的上下文管理。操作系统的进程隔离已经完美解决了:

  • 独立的内存空间
  • 独立的文件描述符
  • 独立的信号处理

你只需要:

subprocess.run(command, capture_output=True)

法则四:提示词 = 约束

通过提示词塑造代理的行为。

代码只是执行框架,真正的智能来自提示词:

你是一个 CLI 代理。使用 bash 命令解决问题。

规则:
- 优先使用工具而非长篇大论
- 对于复杂任务,派生子代理保持上下文清晰
  python bash_agent.py "探索 src/ 并总结架构"

何时使用子代理:
- 任务需要读取多个文件
- 任务独立且自包含
- 避免用中间细节污染当前对话

提示词定义了代理的"性格"和"行为模式"。


四、核心循环模式

无论代理多么复杂,其核心永远不变:

while True:
    # 1. 让模型思考并决定
    response = model(messages, tools)

    # 2. 如果模型决定不再调用工具,任务完成
    if response.stop_reason != "tool_use":
        return response.text

    # 3. 执行工具调用
    results = execute(response.tool_calls)

    # 4. 将结果反馈给模型,继续循环
    messages.append(results)

这个循环的精髓在于:

  • 模型是决策者 —— 决定调用哪个工具、以什么顺序、何时停止
  • 代码是执行者 —— 提供工具并执行模型的决定
  • 循环是催化剂 —— 让模型可以多轮思考、多步执行

其他一切——待办事项、子代理、权限、进度显示——都是围绕这个循环的装饰。


五、涌现式设计

复杂能力从简单规则中涌现。

我们不需要显式设计子代理系统。当我们提供:

  1. 一个 bash 工具
  2. 允许递归调用
  3. 通过提示词指导何时递归

子代理能力就自然涌现了。

这就是涌现式设计的魅力:简单规则 + 递归 + 约束 → 复杂行为。


六、完整代码示例(bash_agent.py)

#!/usr/bin/env python
"""
Bash is All You Need - 极简编码代理

核心思想:
1. 一个 bash 工具包罗万象
2. 通过递归调用实现子代理
3. 利用操作系统进程隔离
4. 提示词定义行为约束
"""

from zai import ZhipuAiClient
from dotenv import load_dotenv
import subprocess
import sys
import os

# 加载环境变量
load_dotenv()

# 初始化客户端(当前使用的智谱的模型,可以替换其他模型)
client = ZhipuAiClient(
    api_key=os.getenv("ANTHROPIC_API_KEY"),
)
MODEL = os.getenv("MODEL_NAME", "glm-4.7")

# 唯一的工具:bash
TOOL = [{
    "type": "function",
    "function": {
        "name": "bash",
        "description": """执行 shell 命令。常见用法:
- 读取:cat/head/tail, grep/find/rg/ls
- 写入:echo '...' > file, sed -i 's/old/new/g' file
- 子代理:python bash_agent.py 'task description' (派生隔离代理,返回摘要)""",
        "parameters": {
            "type": "object",
            "properties": {"command": {"type": "string"}},
            "required": ["command"]
        }
    }
}]

# 系统提示词
SYSTEM = f"""你是位于 {os.getcwd()} 的 CLI 代理。使用 bash 命令解决问题。

规则:
- 优先使用工具而非长篇大论。先执行,简短解释在后
- 读取文件:cat, grep, find, rg, ls, head, tail
- 写入文件:echo '...' > file, sed -i, 或 cat << 'EOF' > file
- 子代理:对于复杂的子任务,派生子代理保持上下文清晰:
  python bash_agent.py "探索 src/ 并总结架构"

何时使用子代理:
- 任务需要读取多个文件(隔离探索过程)
- 任务独立且自包含
- 避免用中间细节污染当前对话

子代理在隔离中运行,只返回最终摘要。"""


def chat(prompt, history=None):
    """
    代理的核心循环。

    模式:
        while not done:
            response = model(messages, tools)
            if no tool calls: return
            execute tools, append results

    参数:
        prompt: 用户请求
        history: 对话历史(跨轮次保持)
    """
    if history is None:
        history = []

    # 初始化:添加系统提示词
    if len(history) == 0:
        history.append({"role": "system", "content": SYSTEM})

    history.append({"role": "user", "content": prompt})

    # 主循环
    while True:
        # 调用模型
        response = client.chat.completions.create(
            model=MODEL,
            messages=history,
            tools=TOOL,
            max_tokens=8000,
        )

        message = response.choices[0].message
        text_content = message.content or ""
        tool_calls = message.tool_calls or []

        # 构建助手消息
        assistant_msg = {"role": "assistant", "content": text_content}
        if tool_calls:
            assistant_msg["tool_calls"] = tool_calls
        history.append(assistant_msg)

        # 如果没有工具调用,完成
        if not tool_calls or response.choices[0].finish_reason != "tool_calls":
            return text_content

        # 执行工具调用
        for tool_call in tool_calls:
            cmd = tool_call.function.arguments
            print(f"$ {cmd}")

            try:
                out = subprocess.run(
                    cmd,
                    shell=True,
                    capture_output=True,
                    text=True,
                    timeout=300,
                    cwd=os.getcwd()
                )
                output = out.stdout + out.stderr
            except subprocess.TimeoutExpired:
                output = "(timeout after 300s)"

            print(output or "(empty)")
            history.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": output[:50000]
            })


def main():
    """交互式 REPL 模式"""
    history = []
    while True:
        try:
            query = input(">> ")
        except (EOFError, KeyboardInterrupt):
            break
        if query in ("q", "exit", ""):
            break
        print(chat(query, history))


if __name__ == "__main__":
    if len(sys.argv) > 1:
        # 子代理模式
        print(chat(sys.argv[1]))
    else:
        # 交互模式
        main()

七、总结

什么是 Bash Agent?

约 50 行核心代码,1 个 bash 工具,完整的编码代理能力。

核心思想

概念 本质
一个工具 Bash 是通往一切操作的统一入口
递归 自我调用自然产生层级结构和子代理
进程隔离 操作系统提供的免费上下文分离
提示词 通过约束塑造代理的行为

精髓

复杂能力从简单规则中涌现。

你不需要:

  • 多个专用工具(read_file, write_file, edit_file…
  • 复杂的子代理系统(Agent Registry, Task Pool…)
  • 精细的上下文管理(Context Isolation, Memory Pool…)

你只需要:

  1. 一个 bash 工具
  2. 允许递归调用
  3. 核心代理循环

剩下的,交给模型和操作系统。


Bash is All You Need。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐