夜雨聆风学习资料网

ARTICLE · 1178378

AI 编程助手的核心秘密:一个 while 循环加三个工具

AI 编程助手的核心秘密:一个 while 循环加三个工具
Claude Code 一天能收你几十美元。Codex 在企业里跑个不停。Cursor 的 Agent 模式时不时帮你改一行代码。

这些工具看着像黑魔法。但把壳剥开,里面的东西比你想的简单。

一个 while 循环。三个工具(读文件、写文件、跑命令)。一个 LLM 调用。没了。

这不是我瞎编的。今年 2 月,一位叫 Prashant Anand 的工程师在东京办了一场工作坊,50 个人用 3 个小时从零写出了自己的 CLI 编程助手。他把这个练习开源成了一门自定进度的课程,名字叫 Alduin。

我跟着走了一遍。这篇文章就是我的实操笔记,同时也是一份中文世界的完整指南:你不需要任何 Agent 框架,不需要 LangChain,不需要向量数据库,只需要 Python 和一颗 Anthropic API Key。

读完你会得到一个能读代码、能改文件、能跑 Shell 命令的 AI 编程助手,而且代码是自己一行一行写出来的。

01

Quick Overview

快速概览

Alduin 用 7 个阶段带你从零搭建一个 CLI 编程助手:

阶段
目标
你会学到
1
接入 LLM
把 Anthropic API 接到一个对话循环里
2
读文件工具
给 LLM 传递工具 schema,让它知道可以读文件
3
执行工具
截获 LLM 的工具调用请求并执行
4
Agent 循环
把工具结果喂回 LLM,形成自主调用链
5
写文件工具
让 Agent 能创建和修改文件
6
Shell 工具
让 Agent 能跑命令(带用户确认)
7
自我改进
用 Agent 给 Agent 加功能:读取 AGENTS.md

每一阶段都有骨架代码和参考实现。你不需要从零码所有东西,LLM 调用封装、工具 schema 生成器、终端 UI 这些作者都帮你写好了。你要实现的是核心逻辑:Agent 怎么思考、怎么用工具、怎么在循环里不断推进任务。

三个阶段是真正的质变点:

1

阶段 2-3:Agent 从"能聊天"变成"能动手"

2

阶段 4:Agent 从"调用一次"变成"自主多步执行"

3

阶段 7:Agent 开始自己给自己写代码

02

Environmental preparation

环境准备

你需要三样东西:

bash

# 1. Python 3.11+

python3 --version

# 2. uv 包管理器

curl -LsSf https://astral.sh/uv/install.sh | sh

# 3. Anthropic API Key(在 console.anthropic.com 创建)

然后克隆仓库并配置:

bash

git clone https://github.com/primaprashant/alduin.git

cd alduin

cp .env.sample .env

# 编辑 .env,填入 ANTHROPIC_API_KEY=sk-ant-xxx

# 安装依赖

uv sync

还有一个重要细节:关掉你编辑器里的 AI 补全。Copilot、Codeium,全关掉。这个练习的目的是理解每一行自己写的代码,让 AI 替你写等于白做。

骨架仓库的代码结构很清晰:

文件
作用
alduin/main.py
入口 + Agent 循环,大部分代码写在这里
alduin/llm.py
Anthropic API 封装,已实现
alduin/tool.py
工具函数(read_file、edit_file、bash),你要实现
alduin/schema_converter.py
把 Python 函数转成 Anthropic tool JSON schema,已实现
alduin/system_prompt.py
System prompt,已实现
alduin/ui.py
Rich 终端 UI,已实现

03

Write a programming assistant in 7 steps

核心实操:7 步写一个编程助手

阶段 1:让 LLM 开口说话

骨架代码里有个占位的对话循环。它会回声一句老滚 5 的台词(“Krosis. That knowledge cannot be known to me...”),但不会真的调 LLM。

我们要把它换成真实的 API 调用。

改动都在 `alduin/main.py` 的 `agent_loop` 函数里:

...python

# 把原来的 canned reply 删掉,换成这段:

from alduin import llm, system_prompt

# 调用 LLM

llm_response = llm.call(

    client=client,

    console=console,

    system_prompt=system_prompt.SYSTEM_PROMPT,

    messages=conversation,

    tool_schemas=[],  # 阶段 1 还没有工具

)

# 把 LLM 的回复加到对话历史里

conversation.append({

    "role": "assistant",

    "content": llm_response.content

})

# 遍历回复块,只显示文本

for block in llm_response.content:

    if block.type == "text":

        ui.print_assistant_reply(console, block.text,

            input_tokens=llm_response.usage.input_tokens,

            output_tokens=llm_response.usage.output_tokens)

跑一下:

...code

🧑‍💻 You: What's the most common programming language in ML?

🤖 Python is the most common...

🧑‍💻 You: Who created it?

🤖 Guido van Rossum created Python in 1991...

第二个问题不需要重新说“Python”,因为对话历史带着上下文。现在我们有了一个能多轮对话的聊天机器人。

阶段 2:教 LLM 读文件

聊天机器人离编程助手差一步:工具。LLM 得知道它可以读文件、写文件、跑命令。

在 `alduin/tool.py` 里实现 `read_file`:

...python

def read_file(path: str) -> str:

    """Read the contents of a file at the given path.

    Args:

        path: The path to the file to read.

    """

    try:

        with open(path, 'r') as f:

            return f.read()

    except FileNotFoundError:

        return f"Error: File not found: {path}"

    except Exception as e:

        return f"Error reading file: {str(e)}"

然后在 `main.py` 里,用 `schema_converter` 把 Python 函数转成 Anthropic 认识的工具描述:

...python

from alduin import tool, schema_converter

# 注册工具

tools = [tool.read_file]

tool_schemas = [schema_converter.generate_tool_schema(t) for t in tools]

# 传给 LLM

llm_response = llm.call(

    client=client,

    console=console,

    system_prompt=system_prompt.SYSTEM_PROMPT,

    messages=conversation,

    tool_schemas=tool_schemas,  # 现在有工具了

)

LLM 回复的 content 里会出现 `type == "tool_use"` 的 block,而不是单纯的 `"text"`。暂时只打印出来看看:

...python

for block in llm_response.content:

    if block.type == "tool_use":

        ui.print_debug(console, f"tool call: {block.name}({block.input})")

试一下:

...code

🧑‍💻 You: Read the Makefile

🔧 tool call: read_file({'path': 'Makefile'})

LLM 知道要读文件了,但还没人帮它真读。这是下一阶段的事。

阶段 3:执行工具

现在把 LLM 请求的工具调用真的执行起来。你需要建一个工具名到函数的映射:

...python

# 在 agent_loop 里

tool_map = {"read_file": tool.read_file}

for block in llm_response.content:

    if block.type == "tool_use":

        tool_func = tool_map[block.name]

        result = tool_func(**block.input)

        ui.print_debug(console, f"tool result: {result[:200]}...")

        # 把结果加到对话里,让 LLM 看到

        conversation.append({

            "role": "user",

            "content": [{

                "type": "tool_result",

                "tool_use_id": block.id,

                "content": result

            }]

        })

现在 LLM 调 `read_file`,你真的打开文件、读内容、返回给 LLM,LLM 就能基于文件内容回答了。

...code

🧑‍💻 You: Read the Makefile and tell me what targets are available

🔧 read_file('Makefile') -> "install:\n\tuv sync\n..."

🤖 The Makefile has these targets: install, test, lint, format...

阶段 4:闭合循环

阶段 3 的工具结果只返回了一次。LLM 想“先看目录、再读文件、再总结”的话,它只能做完第一步就停下来。

把这个过程包在一个 while 循环里:

...python

while True:

    llm_response = llm.call(...)

    tool_results = []

    for block in llm_response.content:

        if block.type == "text":

            ui.print_assistant_reply(...)

        elif block.type == "tool_use":

            tool_func = tool_map[block.name]

            result = tool_func(**block.input)

            tool_results.append({

                "type": "tool_result",

                "tool_use_id": block.id,

                "content": result,

            })

    # 没工具调用 = 任务完成

    if not tool_results:

        break

    # 有工具调用 = 把结果喂回去,继续

    conversation.append({"role": "assistant", "content": llm_response.content})

    conversation.append({"role": "user", "content": tool_results})

这就是 Agent 的内循环。不是魔法,是一个 while 循环加一段 if 判断。所有编程助手(Claude Code、Codex、Cursor Agent、Aider)用的都是同一套逻辑。

试一个真正像 Agent 的任务:

...code

🧑‍💻 You: Explore this codebase and write a summary of what each file does

🔧 list_files('alduin/') -> ['main.py', 'llm.py', 'tool.py', ...]

🔧 read_file('alduin/llm.py') -> "Wrapper around Anthropic API..."

🔧 read_file('alduin/tool.py') -> "Tool function stubs..."

🔧 read_file('alduin/ui.py') -> "Rich console helpers..."

🤖 Here's a summary of each file:

- main.py: Entry point and agent loop

- llm.py: Anthropic API wrapper

- tool.py: Tool implementations (read_file, edit_file, bash)

- ...

Agent 自己规划了探索路径、读了好几个文件、交出了一份总结。没有人工干预。

阶段 5:写文件

读文件只能“看”,编程助手得能“改”。实现 `edit_file`:

...python

def edit_file(path: str, old_string: str, new_string: str) -> str:

    """Edit a file by replacing old_string with new_string."""

    with open(path, 'r') as f:

        content = f.read()

    if old_string not in content:

        return f"Error: old_string not found in {path}"

    new_content = content.replace(old_string, new_string, 1)

    with open(path, 'w') as f:

        f.write(new_content)

    return f"Successfully edited {path}"

这里用精确字符串替换而不是行号编辑,是学 Claude Code 和 Codex 的做法。实践反复证明,精确字符串匹配比行号更可靠,上下文漂移时行号早就对不上了。

注册进 tool_map,LLM 就能改文件了:

...code

🧑‍💻 You: Add a docstring to main.py's agent_loop function

🔧 read_file('alduin/main.py') -> ...

🔧 edit_file('alduin/main.py', 'def agent_loop', 'def agent_loop:\n    """Run the..."""')

🤖 Done. I added the docstring to agent_loop.

阶段 6:Shell 命令

这是编程助手最危险也最强大的能力。实现 `bash`:

...python

import subprocess

def bash(command: str) -> str:

    """Execute a shell command and return the output."""

    # 这里需要用户确认(用 Rich 的 Confirm)

    try:

        result = subprocess.run(

            ["bash", "-c", command],

            capture_output=True,

            text=True,

            timeout=10,

        )

        output = result.stdout

        if result.stderr:

            output += f"\n[stderr]\n{result.stderr}"

        if result.returncode != 0:

            output += f"\n[exit code: {result.returncode}]"

        return output

    except subprocess.TimeoutExpired:

        return "Error: command timed out (10s)"

现在 Agent 可以跑测试、装依赖、看 git 状态:

...code

🧑‍💻 You: Run the tests

🤖 Let me check how to run tests first...

🔧 read_file('Makefile') -> "test:\n\tuv run pytest"

🔧 bash('uv run pytest') -> "3 passed in 1.23s"

🤖 All 3 tests passed!

六个阶段下来,代码总量不到 200 行,但你有了一个能读代码、改文件、跑测试的编程助手。和商用工具的核心引擎没本质区别。

阶段 7:让 Agent 给 Agent 写代码

这是最有意思的阶段。没有参考实现,没有骨架代码。用你自己刚写出来的 Agent,给它一个任务:

...code

🧑‍💻 You: Add a feature: at startup, check if AGENTS.md exists in the

current directory. If so, read it and append to the system prompt.

然后退后一步看。Agent 会读自己的 `main.py`、找到 `main()` 函数、在初始化 console 之后插入文件检查和 prompt 拼接逻辑。跑测试,如果不通过就自己改。

这是“元编程”在 Agent 时代的形态:你写的工具开始参与它自己的进化。

做完这一步,在项目目录里建一个 `AGENTS.md`:

...markdown

# Project Conventions

- Use f-strings, not .format()

- Functions longer than 20 lines must be refactored

重启 Agent,问它“刚刚加新函数超 20 行了,怎么办”。它会引用你写的规范。

04

ADVANCED

进阶技巧

用在其他项目上

Alduin 默认在自己的代码目录里跑。装成全局 CLI:

bash

# 在 alduin 仓库里

pip install -e .

# 或者用 uv

alias ald='uv run --no-sync python -m alduin.main'

# 任意目录下直接用

cd ~/my-project

ald

现在你有自己的编程助手,能探索任意项目的代码库。

生产环境的三个关键差异

你写的 Agent 和 Claude Code 之间差的不在核心逻辑,在这三层:

错误处理。商业 Agent 有层层 try/except,工具调用失败不会崩掉整个对话。你的 Agent 可能因为一次文件找不到就报错退出。加几层容错就能让它稳很多。

上下文压缩。 对话历史会无限增长,一轮长任务可能吃掉几美元 token。商业 Agent 有自动压缩:超过一定长度的对话,前面的内容被摘要后压缩。你可以加一个简单的:Token 数超过阈值时,把历史消息的前三分之一用 LLM 总结成一段话。

安全性。 Claude Code 默认不跑 destructive 命令。你的 bash 工具应该也加一层:命令字包含 `rm -rf`、`git push --force`、`sudo` 的直接拦截。

三个模型的选择

原作者用的是 `claude-sonnet-4-5`。你可以换:

1

Claude Haiku:便宜、快,适合简单任务,但复杂推理容易崩

2

Claude Opus:最强推理,但贵(可能一个任务几十美元)

3

DeepSeek(通过 OpenAI 兼容接口):便宜、中文好,适合国内用户

要换模型,改 `alduin/llm.py` 里 `MODEL` 常量和 API 端点即可。换成 DeepSeek 还能把 API 成本压到 Claude 的十分之一。

05

Competitive product comparison

竞品对比

市面上至少有四个“从零写 Agent”的教程类项目:

项目
语言
行数
特点
适用
Alduin
Python
~200
7 阶段渐进,每个阶段可验证,有骨架代码
想完整理解 Agent 架构的开发者
mini-swe-agent
Python
~100
SWE-bench 74% 准确率,Princeton 团队出品
想理解 SWE-bench Agent 捷径的人
tour-of-agents
Python (Pyodide)
~60
9 课,浏览器跑,零安装
只想看概念的初学者
buildyourowncodingagent
Python
~500
纯标准库,无 SDK,出书了
想深挖每条细节的老手

Alduin 最大的优势是结构:7 个阶段,每个阶段有明确的“输入/输出/验证方式”。做完阶段 4,你看到了自己的 Agent 在自动探索代码库,这是所有竞品给不了的成就感。

mini-swe-agent 的 100 行代码更精炼,但它是结果展示,不是教学过程。你看完它的代码说“哦,原来这么简单”,但 Alduin 让你“写出来”。

06

SUMMARY

总结

Claude Code、Codex、Cursor 这些商用编程助手,不在乎你懂不懂它们的原理。钱照收,代码照改。

但如果你花一个下午写一个自己的,你会得到一个别人拿不走的东西:当一个 Agent 出问题的时候,你知道是模型在胡说、还是工具返回了垃圾、还是循环卡在了某个分支。

这篇文章是 7 阶段教程的第一份中文笔记。Alduin 的仓库在 https://github.com/primaprashant/alduin,MIT 协议,随便改。

三个建议的后续方向:

1

加 MCP 支持:让你的 Agent 能调用外部工具服务器(数据库、API、文件系统)

2

换成 DeepSeek:API 费用压到最低,适合高频使用

3

给 Agent 加 Vision:截图 + 多模态模型,让它能"看"前端 UI

写自己的编程助手这件事,做完才知道框架到底替你藏了什么。

07

Recent articles

最近发表文章

Claude Code Mods 发布:AI 编程 Agent 从工具变成了平台

Pi 1.0 的 Codemode:把 MCP 工具从上下文税变成可编程运行时

Agent Skills 大爆发:6 万个技能包、1.7 亿次安装,AI 编程的技能经济已来

你的AI编程助手在偷偷埋雷:一份开发者安全自查清单

Agent Skills:给自己的 AI 编程助手装上专

Agent Harness:AI 编程 Agent 正在从工具变成操作系统

Agent Skills:给自己的 AI 编程助手装上专业技能的完整指南

AI 替你写代码之后,这 5 件事你得自己做

别再让 LLM 做选择题了

如果你觉得今天这篇有收获,欢迎点赞、在看、转发三连,我们下篇见。

相关学习资料