AI Agents
所有的 AI Coding Agents 其实都可以抽象成两个 while loop:
常用的存储形式:
都是基于 JSONL 格式:
AI Coding
开源 AI Coding 工具列表
Aider-AI/aider: aider is AI pair programming in your terminal
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:
- 不充分的使用体验下来:
wholeedit format 要比diffedit 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;
}