ARTICLE · 1178378
AI 编程助手的核心秘密:一个 while 循环加三个工具
这些工具看着像黑魔法。但把壳剥开,里面的东西比你想的简单。
一个 while 循环。三个工具(读文件、写文件、跑命令)。一个 LLM 调用。没了。
这不是我瞎编的。今年 2 月,一位叫 Prashant Anand 的工程师在东京办了一场工作坊,50 个人用 3 个小时从零写出了自己的 CLI 编程助手。他把这个练习开源成了一门自定进度的课程,名字叫 Alduin。
我跟着走了一遍。这篇文章就是我的实操笔记,同时也是一份中文世界的完整指南:你不需要任何 Agent 框架,不需要 LangChain,不需要向量数据库,只需要 Python 和一颗 Anthropic API Key。
读完你会得到一个能读代码、能改文件、能跑 Shell 命令的 AI 编程助手,而且代码是自己一行一行写出来的。
01
Quick Overview
快速概览
Alduin 用 7 个阶段带你从零搭建一个 CLI 编程助手:
每一阶段都有骨架代码和参考实现。你不需要从零码所有东西,LLM 调用封装、工具 schema 生成器、终端 UI 这些作者都帮你写好了。你要实现的是核心逻辑:Agent 怎么思考、怎么用工具、怎么在循环里不断推进任务。
三个阶段是真正的质变点:
阶段 2-3:Agent 从"能聊天"变成"能动手"
阶段 4:Agent 从"调用一次"变成"自主多步执行"
阶段 7:Agent 开始自己给自己写代码
02
Environmental preparation
环境准备
你需要三样东西:
# 1. Python 3.11+
python3 --version
# 2. uv 包管理器
curl -LsSf https://astral.sh/uv/install.sh | sh
# 3. Anthropic API Key(在 console.anthropic.com 创建)
然后克隆仓库并配置:
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 替你写等于白做。
骨架仓库的代码结构很清晰:
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` 函数里:
# 把原来的 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)
跑一下:
🧑💻 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`:
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 认识的工具描述:
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"`。暂时只打印出来看看:
for block in llm_response.content:
if block.type == "tool_use":
ui.print_debug(console, f"tool call: {block.name}({block.input})")
试一下:
🧑💻 You: Read the Makefile
🔧 tool call: read_file({'path': 'Makefile'})
LLM 知道要读文件了,但还没人帮它真读。这是下一阶段的事。
阶段 3:执行工具
现在把 LLM 请求的工具调用真的执行起来。你需要建一个工具名到函数的映射:
# 在 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 就能基于文件内容回答了。
🧑💻 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 循环里:
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 的任务:
🧑💻 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`:
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 就能改文件了:
🧑💻 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`:
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 状态:
🧑💻 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,给它一个任务:
🧑💻 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`:
# Project Conventions
- Use f-strings, not .format()
- Functions longer than 20 lines must be refactored
重启 Agent,问它“刚刚加新函数超 20 行了,怎么办”。它会引用你写的规范。
04
ADVANCED
进阶技巧
用在其他项目上
Alduin 默认在自己的代码目录里跑。装成全局 CLI:
# 在 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`。你可以换:
Claude Haiku:便宜、快,适合简单任务,但复杂推理容易崩
Claude Opus:最强推理,但贵(可能一个任务几十美元)
DeepSeek(通过 OpenAI 兼容接口):便宜、中文好,适合国内用户
要换模型,改 `alduin/llm.py` 里 `MODEL` 常量和 API 端点即可。换成 DeepSeek 还能把 API 成本压到 Claude 的十分之一。
05
Competitive product comparison
竞品对比
市面上至少有四个“从零写 Agent”的教程类项目:
Alduin 最大的优势是结构:7 个阶段,每个阶段有明确的“输入/输出/验证方式”。做完阶段 4,你看到了自己的 Agent 在自动探索代码库,这是所有竞品给不了的成就感。
mini-swe-agent 的 100 行代码更精炼,但它是结果展示,不是教学过程。你看完它的代码说“哦,原来这么简单”,但 Alduin 让你“写出来”。
06
SUMMARY
总结
Claude Code、Codex、Cursor 这些商用编程助手,不在乎你懂不懂它们的原理。钱照收,代码照改。
但如果你花一个下午写一个自己的,你会得到一个别人拿不走的东西:当一个 Agent 出问题的时候,你知道是模型在胡说、还是工具返回了垃圾、还是循环卡在了某个分支。
这篇文章是 7 阶段教程的第一份中文笔记。Alduin 的仓库在 https://github.com/primaprashant/alduin,MIT 协议,随便改。
三个建议的后续方向:
加 MCP 支持:让你的 Agent 能调用外部工具服务器(数据库、API、文件系统)
换成 DeepSeek:API 费用压到最低,适合高频使用
给 Agent 加 Vision:截图 + 多模态模型,让它能"看"前端 UI
写自己的编程助手这件事,做完才知道框架到底替你藏了什么。
07
Recent articles
最近发表文章
Claude Code Mods 发布:AI 编程 Agent 从工具变成了平台
Pi 1.0 的 Codemode:把 MCP 工具从上下文税变成可编程运行时
Agent Skills 大爆发:6 万个技能包、1.7 亿次安装,AI 编程的技能经济已来
Agent Harness:AI 编程 Agent 正在从工具变成操作系统
Agent Skills:给自己的 AI 编程助手装上专业技能的完整指南
如果你觉得今天这篇有收获,欢迎点赞、在看、转发三连,我们下篇见。















































































