第一章:零魔法宣言

如果你在过去两年里尝试过构建 AI 应用,你很可能已经体会到了“框架疲劳“。

你安装了一个流行的库,导入了一个 ReasoningEngine,调用了 .run()。在“Hello World“示例里,它像魔法一样运行。但当你试图做一些真正有意义的事情时——比如在不删除导入语句的情况下编辑 Python 文件中的某一特定行——它就出问题了。

而因为你用了框架,你也无从修复它。你只能在一层又一层的抽象类、工厂模式和“链“中层层翻找,试图找到那个导致幻觉的提示词。

我们不打算这样做。

这本书是对“魔法“的一次反叛。我们将采用“零魔法“方式:用纯 Python 构建一个生产级编程智能体,名为 Nanocode。不用 LangChain,不用 AutoGPT,不用 Pydantic。

为什么?因为自主智能体并不是什么魔法,它不过是一个 while 循环。

智能体到底是什么?

剥去风险投资的营销包装,“智能体“不过就是一个恒温器

恒温器读取温度(输入),与目标值比较(决策),然后打开加热器(动作)。接着等待,再重复。就这样。AI 智能体做的是同样的事,只不过用文本代替了温度。

智能体循环:用户输入流经一个 while 循环,输入(传感器)送入大脑/LLM(控制器),触发工具(执行器),再循环回输入,直到大脑输出响应。
图 1. 智能体循环:用户输入流经一个 while 循环,输入(传感器)送入大脑/LLM(控制器),触发工具(执行器),再循环回输入,直到大脑输出响应。

更具体地说,一个智能体由四个部分组成。大脑是大语言模型(LLM)——一个无状态函数,你发送文本,它返回文本。它调用工具——比如“读取文件“和“运行命令“这样的函数——来与外部世界交互。这一切都运行在一个循环while True)中,不断循环直到任务完成,而记忆——只是一个 Python 列表——则在此过程中不断积累对话历史。(程序结束时列表就消失了;我们将在第 6 章添加持久化存储。)

只要你会写 while 循环,你就能构建一个智能体。

通过从零开始构建,你将拥有框架用户所没有的东西:掌控力。当我们的智能体陷入死循环时,你能精确知道是哪一行代码造成的。当 API 账单过高时,你能清楚地看到 token 是在哪里泄漏的。

我们在构建什么

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. 保护你的密钥

An icon of a warning1

警告: 如果你将 API 密钥推送到 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 行: 暂时留空的构造函数。我们将在后续章节中添加 braintools
  • 第 18-19 行: /q 命令会抛出 AgentStop,而不是返回一个特殊值——由调用方决定如何处理退出。
  • 第 24 行: 回显输入内容。这只是一个占位符——稍后我们会将其发送给 Brain。

通过测试定义成功标准

在编写主循环之前,我们需要编写测试。

创建 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

全部通过。我们的智能体能正确处理基本情况。

An icon of a info-circle1

旁注: 为什么选择 pytest?它会自动发现以 test_ 开头的函数并运行它们。无需样板代码,无需类。测试代码本身就是纯 Python——毫无魔法。

主循环

现在来看将智能体与终端连接起来的轻量 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),并跳出循环。
An icon of a info-circle1

旁注: Python 的 input() 每次读取一行。本书中的所有提示均为单行。这样可以保持代码简洁——生产环境中的代理会使用更丰富的输入方式,例如 readline 或完整的 TUI。

注意这里的关注点分离:Agent.handle_input() 包含所有逻辑,而 main() 只是 I/O 粘合层。这使得代理无需模拟 stdin/stdout 即可进行测试。

运行程序

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 循环。它目前还做不了任何有用的事——但我们后续构建的所有内容都会接入这个骨架。测试确保我们在推进过程中,不会破坏已经正常运行的部分。