Aider:终端里的AI结对编程助手

0 阅读9分钟

Aider:终端里的AI结对编程助手

Aider 是一个革命性的终端工具,它让你能够与先进的大语言模型(LLMs)直接“结对编程”。无论你是想从零开始一个新项目,还是对现有代码库进行修改、重构或调试,Aider 都能理解你的需求,并以代码块、差异(diff)或完整文件的形式,直接将修改应用到你本地的代码中。它就像一位精通所有编程语言的资深工程师,随时待命,与你并肩作战。

Aider 演示动画

功能特性

  • 广泛的模型支持:支持与数十种主流大语言模型无缝集成,包括 OpenAI (GPT-4o, o1, o3-mini)、Anthropic (Claude 3.5/3.7 Sonnet, Opus)、Google Gemini、DeepSeek 等。你也可以通过 OpenRouter 或本地运行的其他模型来使用 Aider。
  • 智能代码编辑:Aider 能理解你发出的自然语言指令,并直接在正确的文件中做出修改。它支持多种编辑格式(如 diffwholesearch/replace),以确保修改的准确性。
  • 强大的终端交互:在熟悉的终端环境中工作,无需离开你的开发流程。Aider 提供了命令自动补全、语法高亮、多行编辑等现代化终端体验。
  • 内置 Git 集成:自动为每次成功的修改创建 git 提交,让你可以轻松地跟踪 AI 所做的更改,并使用 /undo 命令随时回滚到之前的状态。
  • 实时 Diff 预览:在 AI 生成修改的过程中,Aider 会以动态 diff 的形式实时显示即将发生的代码变化,让你对改动一目了然。
  • 语音输入支持:你可以直接使用语音向 Aider 下达指令,解放双手,让沟通更加自然流畅。
  • 智能代码库映射:对于大型项目,Aider 能够智能地分析整个代码库的结构和文件关系,在需要时只向模型发送最相关的代码片段,从而高效地处理复杂问题,并有效管理 token 消耗。
  • 聊天记录管理:当对话历史过长时,Aider 会自动智能地总结之前的对话,确保模型始终在有效的上下文窗口内工作,避免信息丢失。
  • 便捷的 Web 抓取:你可以直接向 Aider 提供一个网页 URL,它会自动抓取内容并将其转化为上下文,让模型能够基于网页信息进行编程。

安装指南

Aider 推荐通过 Python 的包管理器 pip 进行安装。请确保你的系统已安装 Python 3.8 或更高版本。

1. 基础安装

打开你的终端,执行以下命令即可安装 Aider:

python -m pip install aider-chat

2. 安装额外功能支持

Aider 支持一些可选功能,你可以根据需要安装对应的依赖:

  • 语音支持:如需使用语音输入功能,请安装:
    python -m pip install 'aider-chat[voice]'
    
  • 网页抓取支持:如需使用网页抓取功能,请安装 Playwright:
    python -m pip install 'aider-chat[playwright]'
    # 然后安装 Chromium 浏览器
    playwright install --with-deps chromium
    

3. 配置 API 密钥

Aider 本身是免费的,但它需要调用大语言模型的 API,因此你需要配置相应的 API 密钥。

你可以在终端中设置环境变量,或者在项目的根目录下创建一个 .env 文件。例如,要使用 OpenAI 的模型:

# 设置 OpenAI API 密钥
export OPENAI_API_KEY="你的 OpenAI API 密钥"

如果你想使用 Anthropic 的 Claude 模型,则设置:

export ANTHROPIC_API_KEY="你的 Anthropic API 密钥"

如果你有多个密钥,可以在启动 Aider 时通过 --api-key 参数指定。

使用说明

快速开始

  1. 进入你的项目目录:

    cd /path/to/your/project
    
  2. 启动 Aider,并指定你想要使用的模型:

    # 使用 OpenAI 的 GPT-4o 模型
    aider --model gpt-4o
    
    # 使用 Anthropic 的 Claude 3.7 Sonnet 模型
    aider --model sonnet
    
    # 使用 DeepSeek
    aider --model deepseek/deepseek-chat
    
  3. 首次启动时,如果当前目录是一个 git 仓库,Aider 会询问你是否允许它创建提交。建议允许,以便更好地管理 AI 的修改。

  4. 现在,你就可以在 Aider 的提示符 (> ) 下开始提问或下达指令了。

典型使用场景

  • 新建功能:在提示符下输入“创建一个新的 Python 函数,用于计算斐波那契数列,并保存在 utils.py 文件中”。Aider 会创建文件并写入代码。
  • 修改代码:输入“将 app.py 中的 hello 函数改为接受一个 name 参数,并输出 'Hello, {name}!'”。Aider 会找到对应代码并进行修改。
  • 代码解释与提问:输入“/ask 解释一下 src/models/user.py 中的 User 类是如何工作的”。Aider 会分析代码并给出解释。
  • 添加文件到对话:输入“/add src/main.js” 将 main.js 文件加入当前对话,这样 AI 就能看到并编辑它。
  • 执行命令:AI 有时会建议你运行一些 shell 命令(如安装依赖、运行测试),你可以在 Aider 中直接输入 !pip install requests 来执行。
  • 查看差异:在 AI 生成修改的过程中,你会看到实时的代码差异,修改完成后,你可以输入 /commit 来提交这些更改。

API 概览

Aider 的核心是 Coder 类,它封装了与不同模型交互、处理文件、管理 git 仓库和编辑代码的逻辑。Aider 为不同的任务定义了多种“Coder”:

  • AskCoder:主要用于问答,不会修改文件。
  • EditBlockCoder:使用搜索/替换块来精确修改代码,是最常用的 Coder 之一。
  • WholeFileCoder:通过提供完整的更新后文件内容来进行修改。
  • UnifiedDiffCoder:使用类似 diff -U0 的统一 diff 格式来应用修改。
  • ArchitectCoder:一种双模型模式,一个模型负责设计架构,另一个负责执行编辑。

这些 Coder 通过 main.py 中的 main() 函数进行调度和管理。Aider 还包含 io 模块用于处理输入输出,commands 模块用于处理 / 开头的特殊命令,以及 repo 模块用于与 git 仓库交互。

核心代码

以下是从项目中提取的几个核心模块,展示了 Aider 的关键机制:

1. 核心 Coder 基类 (aider/coders/base_coder.py - 简化示例)

所有 Coder 类型的基类,定义了与 LLM 交互、管理消息、处理文件修改的基础流程。

# aider/coders/base_coder.py (概念性展示)
from aider import prompts
from aider.llm import litellm

class Coder:
    def __init__(self, main_model, io, **kwargs):
        self.main_model = main_model
        self.io = io
        self.abs_fnames = set() # 当前对话中的文件绝对路径
        self.done_messages = []  # 已完成的对话历史
        self.cur_messages = []   # 当前轮次的对话

    def run(self, with_message=None):
        """主运行循环,获取用户输入,发送给 LLM,并处理响应"""
        if with_message:
            user_message = with_message
        else:
            user_message = self.io.get_input() # 从终端获取用户输入

        # 将用户消息添加到当前对话
        self.cur_messages.append(dict(role="user", content=user_message))

        # 构建发送给 LLM 的消息(包括系统提示、文件内容、历史记录等)
        messages = self.format_messages_for_llm()

        # 调用 LLM 并处理流式响应
        generator = self.main_model.streaming_completion(messages)
        self.partial_response_content = ""
        for chunk in generator:
            self.partial_response_content += chunk
            # 实时展示响应(如 diff)
            self.show_intermediate_response()

        # 完成响应后,解析并应用修改
        self.reply_completed()

    def reply_completed(self):
        """LLM 回复完成后调用,用于解析和应用修改"""
        edits = self.get_edits()  # 从回复中提取编辑操作(由子类实现)
        if edits:
            self.apply_edits(edits) # 应用编辑(由子类实现)
            if self.repo: # 如果有 git 仓库,自动提交
                self.repo.commit()

    def format_messages_for_llm(self):
        """构建完整的消息列表,供 LLM 使用"""
        # 包括系统提示、只读文件内容、可编辑文件内容、对话历史等
        chat_chunks = ChatChunks()
        chat_chunks.system = [{"role": "system", "content": self.gpt_prompts.main_system}]
        chat_chunks.readonly_files = self.get_readonly_files_content()
        chat_chunks.chat_files = self.get_editable_files_content()
        chat_chunks.done = self.done_messages
        chat_chunks.cur = self.cur_messages
        return chat_chunks.all_messages()

2. 搜索/替换编辑块实现 (aider/coders/editblock_coder.py)

这是最常用的编辑格式之一。它从 LLM 的回复中解析出 <<<<<<< SEARCH>>>>>>> REPLACE 标记的代码块,并进行精确替换。

# aider/coders/editblock_coder.py
from .base_coder import Coder
from .editblock_prompts import EditBlockPrompts
import re

class EditBlockCoder(Coder):
    edit_format = "diff"
    gpt_prompts = EditBlockPrompts()

    def get_edits(self):
        """从 LLM 回复中解析搜索/替换块"""
        content = self.partial_response_content
        edits = []
        # 使用正则表达式查找所有的 SEARCH/REPLACE 块
        # 格式: path/to/file\n```\n<<<<<<< SEARCH\n...\n=======\n...\n>>>>>>> REPLACE\n```
        pattern = r'(.+?)\n```\n<<<<<<< SEARCH\n(.*?)=======\n(.*?)>>>>>>> REPLACE\n```'
        matches = re.findall(pattern, content, re.DOTALL | re.MULTILINE)
        for match in matches:
            path, original, updated = match
            edits.append((path.strip(), original, updated))
        return edits

    def apply_edits(self, edits):
        """将解析出的编辑应用到文件中"""
        for path, original, updated in edits:
            full_path = self.abs_root_path(path)
            if not self.io.path_exists(full_path):
                 # 如果文件不存在,可能是创建新文件
                self.io.write_text(full_path, updated)
                continue

            content = self.io.read_text(full_path)
            # 在文件中查找精确匹配的 original 代码块
            if original in content:
                new_content = content.replace(original, updated, 1) # 只替换第一个匹配
                self.io.write_text(full_path, new_content)
            else:
                # 如果找不到完全匹配,尝试使用更模糊的匹配(如忽略空白)
                # 这里简化处理,实际代码会更复杂
                self.io.tool_error(f"警告:在 {path} 中找不到要替换的代码块。")

3. 实时 Diff 生成器 (aider/diffs.py)

这个模块负责生成实时的、正在进行的代码修改的 diff 视图,让用户能边看边确认。

# aider/diffs.py
import difflib

def diff_partial_update(lines_orig, lines_updated, final=False, fname=None):
    """
    生成一个智能的 diff,只显示到目前为止的修改,并忽略尚未完成的部分。
    这对于流式输出非常有用。
    """
    # ... (前面的代码用于计算进度条和找到最后一个未删除的行) ...

    # 创建一个定制的 diff,显示修改
    diff = difflib.unified_diff(lines_orig, lines_updated, n=3)
    diff = list(diff)[2:] # 去掉文件头信息
    diff = "".join(diff)

    # 用安全的 fence 包裹 diff 以防与 markdown 冲突
    show = f"```diff\n"
    if fname:
        show += f"--- {fname} (original)\n+++ {fname} (updated)\n"
    show += diff
    show += f"\n```\n\n"
    return show

4. Git 仓库集成 (aider/repo.py)

Aider 与 Git 深度集成,自动为更改创建提交,并支持撤销。

# aider/repo.py
class GitRepo:
    def __init__(self, io, fnames, ...):
        self.repo = git.Repo(Path.cwd())
        self.io = io

    def commit(self, message=None):
        """自动提交所有更改"""
        if not self.repo.is_dirty():
            return False

        # 自动生成提交信息或使用提供的
        if not message:
            message = self.generate_commit_message() # 调用模型生成提交信息

        self.repo.git.add(A=True) # 添加所有更改
        self.repo.index.commit(message)
        self.io.tool_output(f"已提交更改:{message[:50]}...")
        return True

    def undo(self):
        """撤销上一次 AI 提交的更改"""
        if not self.repo:
            self.io.tool_error("不在 git 仓库中,无法撤销。")
            return
        # 执行 git reset --hard HEAD~1 来撤销上一次提交
        self.repo.git.reset('--hard', 'HEAD~1')
        self.io.tool_output("已撤销上一次提交。")

oUjNIuxTucK0Qkc3YJBJUIGq0DFPdQoo/tO6vkN5+dM=