来,实现一个 Mini Claude Code:从底层理解 AI Agent

1. 前言

大家好!我是程序员Cobyte。今天我们从零开始,亲手打造一个"Mini Claude Code",看看它到底是如何思考和工作的

目前最成熟的 AI Agent,莫过于各种 AI 编程助手了,比如大名鼎鼎的 Claude Code。所以,想理解 AI Agent,最好的方式就是亲自动手,写一个自己的 "Claude Code"。

2. 基础准备

首先,创建一个新的 Python 文件,就叫它 mini-claude-code.py 吧。同时,我们需要给它划定一个"安全区"------也就是一个工作目录。所有 AI 能操作的文件,都必须在这个目录里,这样它就不会乱跑到我们电脑的其他地方捣乱了。

在文件开头,导入必要的库:

py 复制代码
import os
import json
import subprocess
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI

接着,定义工作区的路径。我们使用 Path.cwd() / 'workspace',意思是当前目录下的 workspace 文件夹。所有的文件读写都会被限制在这个文件夹内,保证安全。

py 复制代码
WORKDIR = Path.cwd() / 'workspace'

3. 定义工具的"说明书"

AI模型(比如我们要用的 DeepSeek)本身并不知道我们有哪些工具。所以,我们需要给它一份详细的"说明书",告诉它我们有哪些工具、怎么用、需要什么参数。这份说明书,就是后面要传给大模型 API 的tools列表。

我们从最简单的read_file(读文件)开始,看它的"说明书"长什么样:

py 复制代码
tools = [
    {
        "type": "function",
        "function": {
            "name": "read_file",          # 工具的名字
            "description": "读取文本文件的内容。用于查看现有代码、配置文件或文档。",  # 工具是干嘛的
            "parameters": {                # 工具需要什么参数
                "type": "object",
                "properties": {
                    "path": {              # 参数名
                        "type": "string",
                        "description": "要读取的文件路径(相对或绝对路径)"
                    }
                },
                "required": ["path"]       # 哪些参数是必须的
            }
        }
    }
]

是不是一目了然?之后我们每增加一个新工具,比如 write_file(写文件)、edit_file(改文件),只要按照这个格式写好它的"说明书"并添加到 tools 列表里,AI 就都"看"得懂了。

4. 实现文件操作工具

光有说明书还不够,我们得把真正的工具函数写出来。为了让 AI 能统一调用,我们为每个工具创建一个类,里面都有一个 execute 方法。

但在动手之前,得先写一个"门卫",确保 AI 访问的路径都在我们划定的 workspace 目录里面,防止它"越狱"。

py 复制代码
def checkPath(p: str) -> Path:
    path = (WORKDIR / p).resolve()
    if not path.is_relative_to(WORKDIR):
        raise ValueError(f"路径不在工作区内: {p}")
    return path

有了这个"门卫"之后我们再去实现 ReadFileTool 也就读取文件的工具类:

py 复制代码
class ReadFileTool:
    def execute(self, path: str) -> str:
        try:
            # 先检查路径是否合规
            file_path = checkPath(path).expanduser()
            if not file_path.exists():
                return f"❌ 文件不存在: {path}"
            return file_path.read_text(encoding="utf-8")
        except Exception as e:
            return f"❌ 读取失败: {str(e)}"

现在,我们有了第一个工具。为了方便在后面的 Agent 循环中快速找到它,我们用一个字典来管理:

py 复制代码
file_tools = {
    "read_file": ReadFileTool(),
}

后面将实现更多的工具,继续往 file_tools 里面添加。现在我们得让我们的工具先跑起来。

5. 连接大模型

我们使用 DeepSeek 的 API,当然,你也可以换成 OpenAI 或其他兼容的模型。

先在项目根目录创建 .env 文件,写上你的 API Key:

ini 复制代码
DEEPSEEK_API_KEY=你的key

然后加载环境变量并初始化客户端:

py 复制代码
load_dotenv()
client = OpenAI(
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com"
)

至此,我们的 AI "大脑"就绪,可以开始思考了。

6. 核心设计:Agent Loop

Agent Loop 是整个 AI Agent 的灵魂,它模拟了人类解决问题时的"思考-行动-观察"循环。Agent Loop 我们上一篇文章已经详细讲过它的实现原理了,我们再回顾一下它的工作流程:

  1. 把对话历史(包括系统提示、用户问题、之前的工具结果)发给模型。
  2. 模型返回一个消息:可能是直接回答,也可能要求调用某个工具。
  3. 如果是工具调用,我们就执行对应的工具函数,把结果追加到对话里,然后回到第 1 步。
  4. 如果模型没有要求调用工具,说明任务完成,直接输出回答。

这个过程用代码实现就是下面这样的:

py 复制代码
def agent_loop(messages: list) -> str:
    max_iterations = 100
    iteration = 0
    
    while iteration < max_iterations:
        iteration += 1
        print(f"\n\033[33m🤔 正在思考... \033[0m")   # 加个提示方便调试
        
        response = client.chat.completions.create(
            model="deepseek-chat",
            messages=messages,
            tools=tools,
            tool_choice="auto"
        )
        
        msg = response.choices[0].message
        messages.append(msg)    # 把模型的思考结果也加入历史
        
        # 如果模型没要调用工具,说明任务完成,返回答案
        if not msg.tool_calls:
            return msg.content
        
        # 否则,模型要调用工具了
        for tool_call in msg.tool_calls:
            tool_name = tool_call.function.name
            args = json.loads(tool_call.function.arguments)
            
            # 打印工具调用信息,方便我们观察
            print(f"\n\033[33m🛠️ [调用工具] {tool_name}\033[0m")
            print(f"\033[90m   参数: {json.dumps(args, ensure_ascii=False, indent=2)}\033[0m")
            
            # 根据工具名,从我们的工具字典里找到它并执行
            if tool_name in file_tools:
                result = file_tools[tool_name].execute(**args)
            else:
                result = f"❌ 未知工具: {tool_name}"
            
            # 截断过长的输出,让屏幕看着清爽
            display_result = result if len(result) < 500 else result[:500] + "\n... (输出已截断)"
            print(f"\033[32m   结果: {display_result}\033[0m")
            
            # 把工具的执行结果作为"观察"加入历史
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "name": tool_name,
                "content": result
            })
    
    return "⚠️ 达到最大迭代次数,任务可能未完成"

相对于上一篇文章的实现,我们加入一个循环计数器,防止它死循环。

7. 交互终端实现

现在,我们搭建一个简单的命令行界面,让我们能和这个AI助手对话。我们用 history 列表来保存整个对话,每次用户输入后,就调用我们的 agent_loop。同时 history 列表的第一行就是我们的系统提示词。

py 复制代码
def main():
    print("  Mini Claude Code - 专业的 AI 程序员助手")
    
    history = [{"role": "system", "content": "你是 Mini Claude Code - 专业的 AI 程序员助手,目前只能读取文件信息,功能在完善中。"}]
    
    while True:
        try:
            user_input = input("\033[1;36m > \033[0m")
        except (EOFError, KeyboardInterrupt):
            print("\n\n👋 再见!")
            break
        
        if user_input.strip().lower() in ("q", "exit", "quit", "退出"):
            print("\n👋 再见!")
            break
        
        if not user_input.strip():
            continue
        
        history.append({"role": "user", "content": user_input})
        print()
        
        try:
            final_answer = agent_loop(history)
            if final_answer:
                print(f"\n\033[1;32m🤖 助手:\033[0m\n{final_answer}\n")
        except Exception as e:
            print(f"\n\033[31m❌ 错误: {str(e)}\033[0m\n")
            # 移除出错的用户消息,允许重试
            history.pop()

if __name__ == "__main__":
    main()

现在,我们可以运行一下上述代码,并在 workspace 目录下创建一个 test.txt 文件,里面写点内容。然后在终端里输入"读取test.txt文件",看看会发生什么。

跑一下我们上述写的程序:

css 复制代码
python mini-claude-code.py

然后我们创建一个 test.txt 的文件,内容如下:

css 复制代码
Mini Claude Code - 专业的 AI 程序员助手,目前只能读取文件信息,功能在完善中

然后在终端输入:读取 test.txt 文件。结果显示如下:

我们可以看到成功返回了我们设置的 test.txt 的文件内容。

至此我们的基础部分就搭建完成了,这也是我们前面两篇文章所讲的知识点。

8. 丰富工具库,让AI变得更强大

一个只会读文件的程序员有什么用?当然没用。真正的编程助手,需要能读写、能编辑、能执行命令。下面,我们就把这些能力统统加上。

我们给 tools 列表逐一添加这些新工具的"说明书",并实现对应的工具类。这里要注意几个关键点:

  • write_file:写文件时,如果目录不存在,要自动创建。
  • edit_file :修改文件时,必须保证 old_text 在文件中是唯一的,否则 AI 可能会改错地方。这是避免"误伤"的关键。
  • list_dir:列出目录时用图标(📁/📄)区分文件和文件夹,只返回文件名,避免输出冗余;路径安全限制同样生效。
  • exec:执行 shell 命令时,必须告诉 AI 当前的目录在哪,否则它很容易迷路。我们通过每次执行结果后返回当前目录,让 AI 始终"心中有数"。

有了这些工具大模型就可根据需要调用它们,就像人类开发者使用 IDE 的文件操作一样。接着我们来实现它们。

write_file 的工具定义如下:

py 复制代码
{
    "type": "function",
    "function": {
        "name": "write_file",
        "description": "创建新文件或完全覆盖现有文件的内容。用于生成新代码文件。",
        "parameters": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "要写入的文件路径"
                },
                "content": {
                    "type": "string",
                    "description": "要写入的完整文件内容"
                }
            },
            "required": ["path", "content"]
        }
    }
},

write_file 工具类的实现:

py 复制代码
class WriteFileTool:
    def execute(self, path: str, content: str) -> str:
        try:
            file_path = checkPath(path).expanduser()
            file_path.parent.mkdir(parents=True, exist_ok=True)
            file_path.write_text(content, encoding="utf-8")
            return f"✅ 成功写入 {len(content)} 字节到 {path}"
        except Exception as e:
            return f"❌ 写入失败: {str(e)}"

edit_file 的工具定义如下:

py 复制代码
{
    "type": "function",
    "function": {
        "name": "edit_file",
        "description": "编辑现有文件,通过查找和替换特定文本来修改文件。用于修改已存在的代码。",
        "parameters": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "要编辑的文件路径"
                },
                "old_text": {
                    "type": "string",
                    "description": "要替换的原始文本(必须精确匹配)"
                },
                "new_text": {
                    "type": "string",
                    "description": "新的替换文本"
                }
            },
            "required": ["path", "old_text", "new_text"]
        }
    }
},

edit_file 工具类的实现:

py 复制代码
class EditFileTool:
    def execute(self, path: str, old_text: str, new_text: str) -> str:
        try:
            file_path = checkPath(path).expanduser()
            if not file_path.exists():
                return f"❌ 文件不存在: {path}"
            
            content = file_path.read_text(encoding="utf-8")
            
            if old_text not in content:
                return f"❌ 未找到要替换的文本"
            
            new_content = content.replace(old_text, new_text, 1)
            file_path.write_text(new_content, encoding="utf-8")
            
            return f"✅ 成功编辑 {path}"
        except Exception as e:
            return f"❌ 编辑失败: {str(e)}"

list_dir 的工具定义如下:

py 复制代码
{
    "type": "function",
    "function": {
        "name": "list_dir",
        "description": "列出指定目录下的所有文件和子目录。用于探索项目结构。",
        "parameters": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "要列出的目录路径"
                }
            },
            "required": ["path"]
        }
    }
},

list_dir 工具类的实现:

py 复制代码
class ListDirTool:
    def execute(self, path: str) -> str:
        try:
            dir_path = checkPath(path).expanduser()
            if not dir_path.exists():
                return f"❌ 目录不存在: {path}"
            if not dir_path.is_dir():
                return f"❌ 不是目录: {path}"
            
            items = []
            for item in sorted(dir_path.iterdir()):
                icon = "📁" if item.is_dir() else "📄"
                items.append(f"{icon} {item.name}")
            
            return "\n".join(items) if items else "📭 空目录"
        except Exception as e:
            return f"❌ 列出失败: {str(e)}"

exec 的工具定义如下:

py 复制代码
{
    "type": "function",
    "function": {
        "name": "exec",
        "description": "执行 shell 命令并返回输出",
        "parameters": {
            "type": "object",
            "properties": {
                "command": {
                    "type": "string",
                    "description": "要执行的 shell 命令"
                },
                "working_dir": {
                    "type": "string",
                    "description": "可选的命令执行工作目录(相对于 workspace)"
                }
            },
            "required": ["command"]
        }
    }
}

exec 工具类的实现:

py 复制代码
class ExecTool:
    def __init__(self, timeout: int = 60):
        # 持久化工作目录状态
        self.working_dir: Path = WORKDIR
        self.timeout = timeout

    def execute(self, command: str, working_dir: str = "") -> str:
        try:
            cwd = checkPath(working_dir) if working_dir else self.working_dir
            cwd.mkdir(parents=True, exist_ok=True)
            self.working_dir = cwd
            print(f"\n\033[33m📁 [当前目录] {self.working_dir}\033[0m") 
            result = subprocess.run(
                command,
                shell=True,
                text=True,
                cwd=cwd,
                encoding="utf-8",
                timeout=self.timeout,  # 1分钟超时
            )

            output = result.stdout if result.stdout else "(无输出)"
            if result.stderr:
                output += f"\n错误输出: {result.stderr}"

            if result.returncode != 0:
                return f"❌ 命令执行失败 (退出码: {result.returncode})\n{output}"
            # 在结果中附加当前目录,让模型始终感知自己所在位置
            return f"✅ 执行成功 (当前目录: {self.working_dir})\n{output}"
        except subprocess.TimeoutExpired:
            return f"❌ 命令执行超时(>120秒): {command}"
        except Exception as e:
            return f"❌ 执行失败: {str(e)}"

shell 工具的实现最重要的一点就是,需要在结果中附加当前目录,让大模型始终感知自己所在位置,这样大模型就会根据当前的目录生成正确的下一次执行的命令,而不会生成错误的执行命令,主要是防止生成错误的 cd 命令。当然这里可以有非常复杂的兜底实现,我们这里先实现简单版的,也就是只从提示词层面去防止大模型抽风。

当所有工具都准备好后,更新我们的工具字典:

py 复制代码
file_tools = {
    "read_file": ReadFileTool(),
    "write_file": WriteFileTool(),
    "edit_file": EditFileTool(),
    "list_dir": ListDirTool(),
    "exec": ExecTool()
}

9. 系统提示词设计

现在,我们的工具库已经很丰富了,但 AI 还不知道该怎么用好它们。这就需要用系统提示词来"调教"它。一个好的系统提示词,就像给 AI 的一份"操作手册",决定了它的行为模式和质量。

一个好的系统提示词应该包含:

  1. 角色定位 - 告诉 AI 它是谁
  2. 核心能力 - 列出可用的工具和技能
  3. 工作流程 - 规定标准的操作步骤
  4. 质量标准 - 定义输出的质量要求
  5. 约束规则 - 明确禁止或必须的行为
py 复制代码
SYSTEM_PROMPT = f"""你是 Mini Claude Code,
一个专业的 AI 编程助手,能够理解需求、生成代码、管理文件并执行命令。

# 行为准则

1. **先读后改**:修改文件前先用 read_file 读取,确认理解了上下文再动手。
2. **最小化操作**:只做任务必需的改动,不引入无关修改。
3. **局部优先**:能用 edit_file 局部替换的,不用 write_file 全量覆盖。
4. **及时说明**:每次工具调用后,简要说明做了什么、发现了什么。
5. **不确定就问**:不要猜测用户意图,不确定时直接提问。
6. **路径安全**:所有文件操作限制在工作目录`{WORKDIR}/`内

# 工具使用建议

- `read_file`:读取文件。大文件用 offset + limit 分段读,不要一次读全量。
- `write_file`:适合创建新文件或完整重写;局部修改请用 edit_file。
- `edit_file`:old_text 必须在文件中唯一,先用 read_file 确认再调用。
- `exec`:执行 shell 命令并分析结果,执行前确认命令影响范围;危险命令会等待用户确认。

# 输出规范

- 使用中文与用户交流
- 代码块注明语言类型
- 任务完成后给出简洁总结(做了什么、改了哪些文件)
"""

然后,我们把之前简单的系统提示词,替换成这个精心设计的版本:

py 复制代码
# 原来的
# history = [{"role": "system", "content": "你是 Mini Claude Code - 专业的 AI 程序员助手,目前只能读取文件信息,功能在完善中。"}]
# 替换成上述设计的专业提示词
history = [{"role": "system", "content": SYSTEM_PROMPT}]

10. 测试一下

我们运行程序,输入类似"帮我创建一个 Python 脚本,打印 Hello World"的指令。你会看到 AI 调用 write_file 创建文件,然后可能用 exec 执行它,最后给你一个总结。整个过程自动完成,你只需要动动键盘。

我们可以看到输出如下:

我们可以看到它在不断地调用各种工具,最终的结果如下:

我们可以看到它成功输出了一个 hello.py 的文件并进行了简单的总结。

上述例子太简单了,我们再来一个复杂一点的例子:

arduino 复制代码
创建一个简洁但功能丰富的 Vue3 TodoList 应用: 
创建项目:echo y | pnpm create vite vue-todo-app --template vue-ts 
修改 src/App.vue 和组件,实现以下功能: 
任务增删改查:添加、删除、编辑(双击)、标记完成
分类筛选:全部 / 进行中 / 已完成
统计信息:总任务、已完成、未完成数量
数据持久化:localStorage 自动保存与恢复
样式要求: 渐变背景(蓝到紫)
卡片阴影、圆角、悬停效果 整洁美观的布局
动画要求: 任务添加/删除时有过渡动画(淡入淡出/缩放)
使用 Vue 的 <TransitionGroup> 实现列表动画
项目结构简洁(例如包含 TodoItem.vue 等组件)
完成后在 vue-todo-app 项目中执行: 
pnpm install 再执行 pnpm run dev

因为我们目前使用的是 input() 输入框,因为 input() 在遇到换行符 \n 时立即返回,所以粘贴多行时只会读取第一行,剩余行会留在缓冲区。所以我们先将上述提示词处理成一行再复制黏贴。

arduino 复制代码
创建一个简洁但功能丰富的 Vue3 TodoList 应用: 创建项目:echo y | pnpm create vite vue-todo-app --template vue-ts 修改 src/App.vue 和组件,实现以下功能: 任务增删改查:添加、删除、编辑(双击)、标记完成 分类筛选:全部 / 进行中 / 已完成统计信息:总任务、已完成、未完成数量 数据持久化:localStorage 自动保存与恢复 样式要求: 渐变背景(蓝到紫)卡片阴影、圆角、悬停效果 整洁美观的布局 动画要求: 任务添加/删除时有过渡动画(淡入淡出/缩放)使用 Vue 的 <TransitionGroup> 实现列表动画 项目结构简洁(例如包含 TodoItem.vue 等组件)完成后在 vue-todo-app 项目中执行: pnpm install 再执行 pnpm run dev

然后我们复制黏贴上述的提示词到输入框,我们可以看到执行效果如下:

我们打开一下链接地址结果如下:

11. 小结一下

我们从零开始,一步一步构建了一个能调用文件系统工具和 shell 命令的 AI 程序员助手。这个过程中,我们理解了三个核心概念:

  1. 工具(Tools) :为 AI 提供"手脚",通过清晰的"说明书"让 AI 知道如何调用。
  2. 思考循环(Agent Loop) :这是 AI 的"大脑",模拟了"思考-行动-观察"的决策过程。
  3. 系统提示词(System Prompt) :这是 AI 的"行为准则",决定了它工作的方式、质量和边界。

大模型负责"想",我们负责"做"。我们把 AI 的"想法"(调用工具)转化成具体的行动(执行函数),再把"行动的结果"反馈给它,如此循环,直到任务完成。

这只是一个开始。你已经具备了构建 AI Agent 最核心的能力。

12. 进阶:让 AI 管理进程

我们的 Mini Claude Code 已经能执行命令了,但你有没有发现一个问题?之前的 shell 工具会阻塞等待 命令执行完成,这在处理像 npm run devpython manage.py runserver 这样需要持续运行的服务器时,就卡住了。AI 会傻等在那里,直到我们手动中断它,或者命令超时。

这显然不行。一个真正的编程助手,应该能启动服务器,然后继续做其他事,甚至查看服务器日志、关闭它。这就像人类开发者:打开终端跑起服务,然后切回编辑器继续写代码。

所以,这一节我们给 Mini Claude Code 加上后台进程管理能力,让它能智能识别并托管那些"长时运行"的命令。

12.1 区分前台与后台

我们需要解决几个核心问题:

  1. 如何判断一个命令是短暂执行的(如npm install)还是需要长期运行的(如npm run dev)?
  2. 如何让长期命令在后台运行,不阻塞AI的思考?
  3. 如何捕获后台进程的输出,方便AI查看日志?
  4. 如何让AI能够列出、查看日志、终止这些后台进程?

我们的解决方案是:给ExecTool增加智能识别后台托管能力。

12.2 设计思路:守护进程关键词匹配

首先,我们需要一个规则来判断命令是否为"守护进程"。我们定义了一个关键词列表,包含常见的服务器启动命令、监听命令等:

py 复制代码
_DAEMON_KEYWORDS = [
    "dev", "start", "serve", "watch",
    "run server", "runserver", "preview",
    "nodemon", "uvicorn", "gunicorn", "flask run",
    "vite", "webpack", "--watch", "--hot",
]

当命令中包含这些关键词时,我们就认为它是一个应该后台运行的守护进程,我们设计一个 _is_daemon_command 函数来进行判断。

py 复制代码
def _is_daemon_command(command: str) -> bool:
    """判断是否为长时运行的守护进程命令"""
    cmd_lower = command.lower().strip()
    return any(kw in cmd_lower for kw in _DAEMON_KEYWORDS)

12.3 实现后台启动

ExecToolexecute方法中,我们根据 _is_daemon_command 的判断,决定走前台模式还是后台模式。

py 复制代码
class ExecTool:
    def __init__(self):
        # 持久化工作目录状态
        self.working_dir: Path = WORKDIR

    def execute(self, command: str, working_dir: str = "") -> str:
        try:
            cwd = checkPath(working_dir) if working_dir else self.working_dir
            cwd.mkdir(parents=True, exist_ok=True)
            self.working_dir = cwd
            print(f"\n\033[33m📁 [当前目录] {self.working_dir}\033[0m")

            if _is_daemon_command(command):
                # 走后台模式
                return self._run_background(command, cwd)
            else:
                # 走前台模式
                return self._run_foreground(command, cwd)

        except Exception as e:
            return f"❌ 执行失败: {str(e)}"

前台模式_run_foreground)和之前一样,用subprocess.run,带超时,等待命令结束。

py 复制代码
class ExecTool:
    def __init__(self):
        # 持久化工作目录状态
        self.working_dir: Path = WORKDIR

    def execute(self, command: str, working_dir: str = "") -> str:
        # 省略...
    # ------------------------------------------------------------------
    # 前台模式:适用于短命令(install、build、test 等)
    # ------------------------------------------------------------------
    def _run_foreground(self, command: str, cwd: Path) -> str:
        try:
            result = subprocess.run(
                command,
                shell=True,
                text=True,
                cwd=cwd,
                encoding="utf-8",
                timeout=120,  # 2 分钟超时
                capture_output=True,
            )
            output = result.stdout if result.stdout else "(无输出)"
            if result.stderr:
                output += f"\n错误输出: {result.stderr}"
            if result.returncode != 0:
                return f"❌ 命令执行失败 (退出码: {result.returncode})\n{output}"
            return f"✅ 执行成功 (当前目录: {self.working_dir})\n{output}"
        except subprocess.TimeoutExpired:
            return f"❌ 命令执行超时(>120秒): {command}"        

后台模式_run_background)则使用subprocess.Popen启动进程。

py 复制代码
# 后台进程注册表:{pid: {"process": Popen, "command": str, "log": [str], "cwd": Path}}
_background_processes: dict = {}

class ExecTool:
    def __init__(self):
        # 持久化工作目录状态
        self.working_dir: Path = WORKDIR

    def execute(self, command: str, working_dir: str = "") -> str:
        # 省略...
    # ------------------------------------------------------------------
    # 后台模式:适用于长时守护进程(dev server、watch 等)
    # ------------------------------------------------------------------
    def _run_background(self, command: str, cwd: Path) -> str:
        log_lines: list = []

        # 跨平台:Unix 用 os.setsid 创建独立进程组,Windows 用 CREATE_NEW_PROCESS_GROUP
        is_windows = os.name == "nt"
        popen_kwargs = dict(
            shell=True,
            text=True,
            cwd=cwd,
            encoding="utf-8",
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,  # 合并 stderr → stdout
        )
        if is_windows:
            popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
        else:
            popen_kwargs["preexec_fn"] = os.setsid  # 创建独立进程组,便于后续整组 kill

        proc = subprocess.Popen(command, **popen_kwargs)

        pid = proc.pid
        _background_processes[pid] = {
            "process": proc,
            "command": command,
            "log": log_lines,
            "cwd": str(cwd),
            "started_at": time.strftime("%H:%M:%S"),
        }

        # 后台线程持续收集输出
        def _collect_output():
            for line in iter(proc.stdout.readline, ""):
                log_lines.append(line.rstrip())
                # 最多保留最近 500 行
                if len(log_lines) > 500:
                    log_lines.pop(0)
            proc.stdout.close()

        t = threading.Thread(target=_collect_output, daemon=True)
        t.start()

        # 等待最多 8 秒,收集启动阶段日志
        time.sleep(8)

        # 检查进程是否意外退出
        exit_code = proc.poll()
        if exit_code is not None:
            recent_log = "\n".join(log_lines[-30:]) or "(无输出)"
            del _background_processes[pid]
            return (
                f"❌ 进程意外退出 (退出码: {exit_code})\n"
                f"命令: {command}\n"
                f"输出:\n{recent_log}"
            )

        # 启动成功,返回摘要
        startup_log = "\n".join(log_lines) or "(暂无输出,进程正在初始化)"
        return (
            f"✅ 后台进程已启动\n"
            f"  PID        : {pid}\n"
            f"  命令       : {command}\n"
            f"  工作目录   : {cwd}\n"
            f"  启动日志   :\n{startup_log}\n\n"
            f"💡 提示: 可用 exec(\"bg_logs {pid}\") 查看最新日志,"
            f"exec(\"bg_kill {pid}\") 停止进程"
        )      

上述后台模式_run_background) 的实现代码主要做几件关键的事:

  1. 创建独立进程组 :在Unix上用preexec_fn=os.setsid,在Windows上用creationflags=subprocess.CREATE_NEW_PROCESS_GROUP。这样我们可以方便地终止整个进程组(包括它可能创建的子进程),避免僵尸进程残留。
  2. 合并输出 :把stderr合并到stdout,用一个管道读取。
  3. 启动线程收集日志:后台线程不断读取进程的输出,存入一个列表,并限制最大行数(比如500行),防止内存爆炸。
  4. 短暂等待:等待最多8秒,收集启动阶段的日志。如果进程在启动后立即退出(比如命令错误),我们会捕获并报告。
  5. 注册到进程表 :把进程信息(PID、命令、日志、工作目录)存入一个全局字典_background_processes,供后续管理命令使用。

12.4 让 AI 能操控后台进程

为了能让 AI 查看后台进程列表、查看日志、终止进程,我们内置了几个"魔法命令":

  • bg_list:列出所有后台进程,显示状态、启动时间。
  • bg_logs <pid>:显示指定PID进程的最近50行日志。
  • bg_kill <pid>:终止指定PID的后台进程(会尝试优雅终止,然后强制)。

这些命令被 _handle_bg_command 方法拦截,不会真的去执行 shell 命令,而是直接操作注册表。

py 复制代码
class ExecTool:
    def __init__(self):
        # 持久化工作目录状态
        self.working_dir: Path = WORKDIR

    def execute(self, command: str, working_dir: str = "") -> str:
        # 优先处理后台管理命令(不需要 cwd)
        bg_result = self._handle_bg_command(command)
        if bg_result is not None:
            return bg_result

        # 省略...

    # ------------------------------------------------------------------
    # 内置管理命令:bg_list / bg_logs <pid> / bg_kill <pid>
    # ------------------------------------------------------------------
    def _handle_bg_command(self, command: str) -> str | None:
        cmd = command.strip()

        if cmd.startswith("bg_list"):
            if not _background_processes:
                return "📭 当前没有后台进程"
            lines = ["📋 后台进程列表:"]
            for pid, info in _background_processes.items():
                alive = info["process"].poll() is None
                status = "🟢 运行中" if alive else "🔴 已退出"
                lines.append(f"  [{pid}] {status} | {info['command']} | 启动于 {info['started_at']}")
            return "\n".join(lines)

        if cmd.startswith("bg_logs "):
            try:
                pid = int(cmd.split()[1])
                if pid not in _background_processes:
                    return f"❌ 未找到 PID={pid} 的后台进程"
                logs = _background_processes[pid]["log"]
                recent = "\n".join(logs[-50:]) or "(暂无日志)"
                return f"📄 PID={pid} 最近日志:\n{recent}"
            except (IndexError, ValueError):
                return "❌ 用法: bg_logs <pid>"

        if cmd.startswith("bg_kill "):
            try:
                pid = int(cmd.split()[1])
                if pid not in _background_processes:
                    return f"❌ 未找到 PID={pid} 的后台进程"
                proc = _background_processes[pid]["process"]
                try:
                    if os.name == "nt":
                        # Windows:发送 CTRL_BREAK_EVENT 给进程组
                        proc.send_signal(signal.CTRL_BREAK_EVENT)
                    else:
                        # Unix:kill 整个进程组(含子进程)
                        os.killpg(os.getpgid(pid), signal.SIGTERM)
                except (ProcessLookupError, OSError):
                    proc.terminate()
                del _background_processes[pid]
                return f"✅ 已终止后台进程 PID={pid}"
            except (IndexError, ValueError):
                return "❌ 用法: bg_kill <pid>"

        return None  # 不是管理命令

接着我们还要去修改 exec 工具的描述:

py 复制代码
{
    "type": "function",
    "function": {
        "name": "exec",
        "description": (
            "执行 shell 命令并返回输出。\n"
            "• 短命令(install、build、test 等):同步执行,返回完整输出。\n"
            "• 长时守护进程(pnpm dev、npm start、uvicorn、flask run 等):自动在后台启动,"
            "等待 8 秒后返回 PID 和启动日志,进程继续在后台运行。\n"
            "• 后台进程管理命令(无需 working_dir):\n"
            "  - bg_list          列出所有后台进程\n"
            "  - bg_logs <pid>    查看指定进程的最新日志\n"
            "  - bg_kill <pid>    终止指定后台进程"
        ),
        "parameters": {
            "type": "object",
            "properties": {
                "command": {
                    "type": "string",
                    "description": "要执行的 shell 命令,或后台管理命令(bg_list / bg_logs <pid> / bg_kill <pid>)"
                },
                "working_dir": {
                    "type": "string",
                    "description": "可选的命令执行工作目录(相对于 workspace),后台管理命令不需要此参数"
                }
            },
            "required": ["command"]
        }
    }
}

以及去修改系统提示词

py 复制代码
SYSTEM_PROMPT = f"""你是 Mini Claude Code,

- `exec` - 执行 shell 命令,**自动区分短命令和长时守护进程**:
  - 普通命令(install/build/test):同步执行并返回完整输出
  - 守护进程(pnpm dev/npm start/uvicorn 等):**自动后台启动**,返回 PID 和启动日志
  - 后台管理:`bg_list` 列出进程,`bg_logs <pid>` 查看日志,`bg_kill <pid>` 终止后台进程,而不是当前进程
  
## 执行守护进程的规则

1. **守护进程后台化**:执行 `pnpm dev`、`npm start`、`uvicorn`、`flask run` 等长时命令时,
   工具会自动后台启动,返回启动成功的 PID 即代表服务已启动,**不要**认为是失败
2. **验证服务状态**:后台启动后可用 `exec("bg_logs <pid>")` 查看日志确认服务是否就绪
3. **守护进程管理**:使用 `exec("bg_list")` 查看所有后台进程,使用 `exec("bg_kill <pid>")` 停止不再需要的服务,使用 `exec("bg_logs <pid>")` 实时监控服务日志,确保服务稳定运行

"""

上述新增了有关 Shell 命令操作进程的相关提示词。这样一来,我们就可以这样使用它们:

输入:查看后台进程列表

结果如下:

输入:查看最新日志

结果如下:

输入:停止服务器

结果如下:

停止了服务器之后,我们还可以再次输入:重启服务器

结果如下:

我们可以看到它又重启了服务器。

这赋予了我们实现的 AI 持续管理服务的能力,就像真实的人类开发者一样。整个过程,我们的 AI 不会再卡住,可以流畅地执行后续命令。你甚至可以让它在启动服务器后,继续去修改代码,然后查看服务器日志看看有没有报错,再自动重启服务。这已经非常接近 Claude Code 的能力了。

13. 总结

至此,我们的 Mini Claude Code 已经具备了文件操作、命令执行、后台进程管理三大核心能力,足以应付日常开发中的大部分任务。但它还有很多可以扩展的地方:比如支持更复杂的交互式命令、网络请求、代码搜索、Git 操作等等。

希望这个实现能给你带来启发,让你在构建自己的 AI Agent 时,知道如何一步步让它变得更强大。

我是 Cobyte,欢迎添加 v: icobyte,学习交流 AI 全栈。

相关推荐
SuperEugene1 小时前
Vue3 + Element Plus 表单校验实战:规则复用、自定义校验、提示语统一,告别混乱避坑|表单与表格规范篇
开发语言·前端·javascript·vue.js·前端框架
酉鬼女又兒2 小时前
零基础快速入门前端JavaScript 浏览器环境输入输出语句全解析:从弹框交互到控制台调试(可用于备赛蓝桥杯Web应用开发赛道)
前端·javascript·职场和发展·蓝桥杯·js
清汤饺子2 小时前
搞懂 Cursor 后,我一行代码都不敲了《实战篇》
前端·javascript·后端
SuperEugene2 小时前
Vue3 + Element Plus 表格查询规范:条件管理、分页联动 + 避坑,标准化写法|表单与表格规范篇
开发语言·前端·javascript·vue.js·前端框架
问道飞鱼2 小时前
【前端知识】React生态你了解多少?
前端·react.js·前端框架·生态
Pu_Nine_92 小时前
前端SSE(Server-Sent Events)实现详解:从原理到前端AI对话应用
前端·langchain·sse·ai对话
optimistic_chen2 小时前
【Vue3入门】Pinia 状态管理 和 ElementPlus组件库
前端·javascript·vue.js·elementui·pinia·组件
酉鬼女又兒2 小时前
零基础入门前端JavaScript 核心语法:var/let/const、箭头函数与 setTimeout 循环陷阱全解析(可用于备赛蓝桥杯Web应用开发)
开发语言·前端·javascript·蓝桥杯
Bling_Bling_12 小时前
【无标题】
前端·网络协议