所有的 AI Coding Agents 其实都可以抽象成两个 while loop:


常用的存储形式:

都是基于 JSONL 格式:

AI Coding

开源 AI Coding 工具列表

Aider-AI/aider: aider is AI pair programming in your terminal

sigoden/aichat: All-in-one LLM CLI tool featuring Shell Assistant, Chat-REPL, RAG, AI Tools & Agents, with access to OpenAI, Claude, Gemini, Ollama, Groq, and more.

plandex-ai/plandex: Open source AI coding agent. Designed for large projects and real world tasks.

目前 AI 变成还有的问题:

  • diff edit mode 下,生成的 SEARCH/REPLACE 块过于庞大,其实只是改了两行,只有两行的 diff。

AI Coding Leaderboard

Aider LLM Leaderboards | aider

Ollama

Ollama environment variables

Global Configuration Variables for Ollama · Issue #2941 · ollama/ollama

Ollama 模型代码能力评测

Aider 有一个评测:Aider LLM Leaderboards | aider

这里可以搜 Ollama:Code editing leaderboard | aider

DeepSeek-R1: output token 太多。

命令行工具

Aider

Tips:

  • 不充分的使用体验下来:whole edit format 要比 diff edit format 准确率更高,尤其是对于 qwen2.5-coder 14B。同一个 prompt,以 whole 的方式输出对代码的效果更好。Code editing leaderboard | aider 这里在每一个模型后面写了 edit format,可以作为参考。

对于 L4 机型,性能最好的模型和搭配是:qwen2.5-coder 32B + whole edit format,但是 aider 给的对于这个模型的默认 format 是 diff format,输出慢点就慢点吧。

缺点:

  • 无法生成 diff chunks 到文件中,只能直接对文件修改,我们没有办法 review 中间修改的结果。

Gemini CLI

每天 1000 次免费调用额度,就是需要翻墙网络环境。即使有了 API Key,在发请求的时候 Google 也会去判断有没有翻墙。

Claude Code

Codex

OpenAI 出的,需要 OpenAI 的 API Key。

GUI App 比 CLI 好的地方在于:

  • 可以远程控制,用手机更加方便,手机可以继续对话;
  • 实时渲染 mermaid 图;
  • 拷贝更加方便。
  • 多模态输入输出更加方便。

CLI 比 GUI 好的地方在于:

  • 快捷键工作流更加流畅。

Pi Agent

每一个 Session 的记录存为一个 JSONL 文件(每行是一个完整的 JSON 对象)。好处是 append-only——追加新 entry 只需在文件末尾加一行,不需要读入整个文件再写。Pi 的 SessionManager 正是利用这一点实现高性能 append-only 会话持久化。

这个 JSONL 是一个有序树(ordered tree)。entry 就是 JSONL 里的一行,代表一个事件节点,有下面几种类型:

  • session — 文件头,包含 session ID、version、cwd
  • message — 一条对话消息(user / assistant),含 role 和 content
  • model_change — 模型切换记录
  • thinking_level_change — thinking 级别切换
  • compaction — 压缩摘要,含 summary 和 firstKeptEntryId
  • custom_message — 扩展注入的消息,如 merge 的 tmux-side-merge
  • custom — 扩展自定义数据,如 tmux-side-merge-state
  • label — 书签标记
  • session_info — session 显示名

每个 entry 都有 id、parentId(指向上一个 entry,形成树,因为只有一个 parentId 所以其实不会形成图),用内存 leafId 指针追踪当前位置。leafId 默认设为最后一个 entry 的 id(即文件最末行)。

Pi 的 loop:

// 这是一次交互(包含 follow up 也放在这次交互里)
// Outer loop: continues when queued follow-up messages arrive after agent would stop
while (true) {
    let hasMoreToolCalls = true;

    // Inner loop: process tool calls and steering messages
    while (hasMoreToolCalls || pendingMessages.length > 0) {
        // 第一回合不发送 turn_start 事件
        if (!firstTurn) {
            await emit({ type: "turn_start" });
        } else {
            firstTurn = false;
        }

        // Process pending messages (inject before next assistant response)
        if (pendingMessages.length > 0) {
            for (const message of pendingMessages) {
                await emit({ type: "message_start", message });
                await emit({ type: "message_end", message });
                currentContext.messages.push(message);
                newMessages.push(message);
            }
            pendingMessages = [];
        }

        // Stream assistant response
        const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);
        newMessages.push(message);

        if (message.stopReason === "error" || message.stopReason === "aborted") {
            await emit({ type: "turn_end", message, toolResults: [] });
            await emit({ type: "agent_end", messages: newMessages });
            return;
        }

        // Check for tool calls
        const toolCalls = message.content.filter((c) => c.type === "toolCall");

        const toolResults: ToolResultMessage[] = [];
        hasMoreToolCalls = false;
        if (toolCalls.length > 0) {
            // A "length" stop means the output was cut off by the token limit, so
            // every tool call in the message may carry truncated arguments. Fail
            // them all instead of executing potentially borked calls.
            const executedToolBatch =
                message.stopReason === "length"
                    ? await failToolCallsFromTruncatedMessage(toolCalls, emit)
                    : await executeToolCalls(currentContext, message, config, signal, emit);
            toolResults.push(...executedToolBatch.messages);
            hasMoreToolCalls = !executedToolBatch.terminate;

            for (const result of toolResults) {
                currentContext.messages.push(result);
                newMessages.push(result);
            }
        }

        await emit({ type: "turn_end", message, toolResults });

        const nextTurnContext = {
            message,
            toolResults,
            context: currentContext,
            newMessages,
        };
        const nextTurnSnapshot = await config.prepareNextTurn?.(nextTurnContext);
        if (nextTurnSnapshot) {
            currentContext = nextTurnSnapshot.context ?? currentContext;
            config = {
                ...config,
                model: nextTurnSnapshot.model ?? config.model,
                reasoning:
                    nextTurnSnapshot.thinkingLevel === undefined
                        ? config.reasoning
                        : nextTurnSnapshot.thinkingLevel === "off"
                            ? undefined
                            : nextTurnSnapshot.thinkingLevel,
            };
        }

        if (
            await config.shouldStopAfterTurn?.({
                message,
                toolResults,
                context: currentContext,
                newMessages,
            })
        ) {
            await emit({ type: "agent_end", messages: newMessages });
            return;
        }

        pendingMessages = (await config.getSteeringMessages?.()) || [];
    }

    // Agent would stop here. Check for follow-up messages.
    const followUpMessages = (await config.getFollowUpMessages?.()) || [];
    if (followUpMessages.length > 0) {
        // Set as pending so inner loop processes them
        pendingMessages = followUpMessages;
        continue;
    }

    // No more messages, exit
    break;
}