第一章:零魔法宣言
如果你在过去两年中尝试构建人工智能应用,你很可能已经感受到了“框架疲劳“。
你安装了一个流行的库。你导入了一个 ReasoningEngine。你调用 .run()。对于“Hello World“示例,它像魔法一样工作。但当你试图做一些实际的事情时——比如在不删除导入语句的情况下编辑 Python 文件中的特定行——它就崩溃了。
而且因为你使用了框架,你无法修复它。你不得不深入挖掘抽象类、工厂模式和“链“的层层结构,试图找出导致幻觉的那个提示。
我们在这里不会这样做。
这本书是对“魔法“的反叛。我们将采用“零魔法“方法:用纯 Python 构建一个生产级的编码代理,称为 Nanocode。不用 LangChain,不用 AutoGPT,不用 Pydantic。
为什么?因为自主代理并不是魔法。它只是一个 while 循环。
代理到底是什么?
撇开风险投资营销不谈,一个“代理“就像一个恒温器。
恒温器读取温度(输入),将其与目标温度比较(决策),然后打开加热器(动作)。之后等待并重复。就这么简单。人工智能代理做的事情也一样,只是用文本替代了温度。

更具体地说,一个代理有四个部分。大脑就是 LLM——一个无状态函数,你发送文本它返回文本。它调用工具——像读取文件和运行命令这样的函数——来与外部世界交互。所有这些都位于一个循环(一个 while True)中,循环会一直持续到任务完成,同时内存——就是一个 Python 列表——会在此过程中累积对话历史。(当程序结束时列表就会消失;我们会在第 6 章添加持久存储。)
如果你会写 while 循环,你就能构建一个代理。
通过从零开始构建,你将拥有框架用户所没有的东西:控制权。当我们的代理陷入循环时,你会准确知道是哪行代码导致的。当 API 账单变得太高时,你会清楚地看到代币在哪里泄漏。
我们要构建什么
Nanocode 是一个在终端中运行的命令行工具。你可以像和同事交谈一样与它对话。它能读取你的文件。它能运行你的命令。它能编辑你的代码。
到本书结束时,你将把它连接到 Claude Sonnet 4.6(或 DeepSeek,或通过 Ollama 的本地模型)。你将赋予它双手——读取文件、写入文件和运行 shell 命令的工具——以及搜索代码库的双眼。你还将构建一个安全防护装置,这样它就不会意外执行 rm -rf /。
项目设置
1. 初始化项目
1 mkdir nanocode
2 cd nanocode
3 git init
2. 创建虚拟环境
切勿全局安装AI工具。这会与系统包发生冲突。
1 # Mac/Linux
2 python3 -m venv venv
3 source venv/bin/activate
4
5 # Windows
6 python -m venv venv
7 venv\Scripts\activate
3. 安装依赖项
我们只需要三个库:
requests— 用于与 LLM API 通信。python-dotenv— 用于从.env文件中加载 API 密钥。pytest— 用于在不进行 API 调用的情况下测试代码。
创建 requirements.txt:
1 requests
2 python-dotenv
3 pytest
安装:
1 pip install -r requirements.txt
4. 保护你的密钥
![]() |
**警告:**如果你将 API key 推送到 GitHub,机器人会在几分钟内抓取它并掏空你的账户。 |
创建 .gitignore:
1 .env
2 __pycache__/
3 venv/
4 .DS_Store
5 .nanocode/
AgentStop 异常
在编写事件循环之前,我们需要一个干净的退出机制。使用异常比在代码中分散使用 break 语句要好。
**背景:**异常不仅仅用于错误处理;它们也是一种控制流机制。当用户输入 /q 时,我们抛出 AgentStop。主循环捕获它并实现清理退出。
代码:
1 # --- Exceptions ---
2
3 class AgentStop(Exception):
4 """Raised when the agent should stop processing."""
5 pass
这段代码位于 nanocode.py 的顶部。这是一个标记异常——没有逻辑,仅作为一个信号。
Agent 类
现在来看核心抽象:Agent 类。它将状态和逻辑集中在一处,这使得测试变得容易。
**背景:**我们可以把所有逻辑都放在 main() 中。但那样的话,我们就必须模拟 input() 和 print() 来进行测试。通过将逻辑提取到 Agent.handle_input() 中,我们可以直接测试它。
代码:
10 class Agent:
11 """A coding agent that processes user input."""
12
13 def __init__(self):
14 pass
15
16 def handle_input(self, user_input):
17 """Handle user input. Returns output string, raises AgentStop to quit."""
18 if user_input.strip() == "/q":
19 raise AgentStop()
20
21 if not user_input.strip():
22 return ""
23
24 return f"You said: {user_input}\n(Agent not yet connected)"
演练:
- **第13-14行:**暂时为空的构造函数。我们将在后面的章节中添加
brain和tools。 - **第18-19行:**检查退出命令
/q。抛出AgentStop而不是返回特殊值。 - **第21-22行:**跳过空输入。返回空字符串(无需显示输出)。
- **第24行:**回显输入内容。这是一个占位符——稍后,我们会将其发送到大脑。
通过测试定义成功
在编写主循环之前,我们需要测试。
创建test_nanocode.py:
1 import pytest
2 from nanocode import Agent, AgentStop
3
4
5 def test_handle_input_returns_string():
6 """Verify handle_input returns a string for normal input."""
7 agent = Agent()
8 result = agent.handle_input("hello")
9 assert isinstance(result, str)
10 assert "hello" in result
11
12
13 def test_empty_input_returns_empty_string():
14 """Verify empty/whitespace input returns empty string."""
15 agent = Agent()
16 assert agent.handle_input("") == ""
17 assert agent.handle_input(" ") == ""
18 assert agent.handle_input("\n") == ""
19
20
21 def test_quit_command_raises_agent_stop():
22 """Verify /q raises AgentStop exception."""
23 agent = Agent()
24 with pytest.raises(AgentStop):
25 agent.handle_input("/q")
26
27
28 def test_quit_command_with_whitespace():
29 """Verify /q works with surrounding whitespace."""
30 agent = Agent()
31 with pytest.raises(AgentStop):
32 agent.handle_input(" /q ")
运行测试:
1 pytest test_nanocode.py -v
1 test_nanocode.py::test_handle_input_returns_string PASSED
2 test_nanocode.py::test_empty_input_returns_empty_string PASSED
3 test_nanocode.py::test_quit_command_raises_agent_stop PASSED
4 test_nanocode.py::test_quit_command_with_whitespace PASSED
一切正常。我们的代理程序正确处理了基本用例。
![]() |
**旁注:**为什么选择pytest?它能发现以 |
主循环
现在来看连接代理程序和终端的简单I/O包装器:
29 def main():
30 agent = Agent()
31 print("⚡ Nanocode v0.1 initialized.")
32 print("Type '/q' to quit.")
33
34 while True:
35 try:
36 user_input = input("\n❯ ")
37 output = agent.handle_input(user_input)
38 if output:
39 print(output)
40
41 except (AgentStop, KeyboardInterrupt):
42 print("\nExiting...")
43 break
44
45
46 if __name__ == "__main__":
47 main()
详解:
- 第30-32行: 创建代理并打印启动消息。
- 第36行:
input()阻塞并等待用户输入内容。 - 第37-39行: 调用
handle_input()并打印输出内容。 - 第41-43行: 捕获
AgentStop(来自/q)或KeyboardInterrupt(来自 Ctrl+C)并正常退出。
![]() |
附注: Python的 |
请注意这种分离:Agent.handle_input() 包含了所有的逻辑。main() 只是输入/输出的粘合代码。这使得代理可以在不需要模拟标准输入/输出的情况下进行测试。
运行程序
1 python nanocode.py
您应该看到:
1 ⚡ Nanocode v0.1 initialized.
2 Type '/q' to quit.
3
4 ❯ hello
5 You said: hello
6 (Agent not yet connected)
7
8 ❯ /q
9
10 Exiting...
这就是框架。接下来是引擎部分。
总结
这就是框架:一个Agent类、一个handle_input()方法和一个while True循环。目前它还没有任何实际功能——但我们接下来构建的所有内容都将插入这个骨架中。测试能确保我们在继续开发时不会破坏已有的功能。

