第一章:零魔法宣言

如果你在过去两年中尝试构建人工智能应用,你很可能已经感受到了“框架疲劳“。

你安装了一个流行的库。你导入了一个 ReasoningEngine。你调用 .run()。对于“Hello World“示例,它像魔法一样工作。但当你试图做一些实际的事情时——比如在不删除导入语句的情况下编辑 Python 文件中的特定行——它就崩溃了。

而且因为你使用了框架,你无法修复它。你不得不深入挖掘抽象类、工厂模式和“链“的层层结构,试图找出导致幻觉的那个提示。

我们在这里不会这样做。

这本书是对“魔法“的反叛。我们将采用“零魔法“方法:用纯 Python 构建一个生产级的编码代理,称为 Nanocode。不用 LangChain,不用 AutoGPT,不用 Pydantic。

为什么?因为自主代理并不是魔法。它只是一个 while 循环。

代理到底是什么?

撇开风险投资营销不谈,一个“代理“就像一个恒温器

恒温器读取温度(输入),将其与目标温度比较(决策),然后打开加热器(动作)。之后等待并重复。就这么简单。人工智能代理做的事情也一样,只是用文本替代了温度。

代理循环:用户输入流经一个 while 循环,其中输入(传感器)流向大脑/LLM(控制器),后者触发工具(执行器),循环返回到输入,直到大脑输出响应。
图 1. 代理循环:用户输入流经一个 while 循环,其中输入(传感器)流向大脑/LLM(控制器),后者触发工具(执行器),循环返回到输入,直到大脑输出响应。

更具体地说,一个代理有四个部分。大脑就是 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. 保护你的密钥

An icon of a warning1

**警告:**如果你将 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行:**暂时为空的构造函数。我们将在后面的章节中添加braintools
  • **第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

一切正常。我们的代理程序正确处理了基本用例。

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或完整的文本用户界面。

请注意这种分离: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循环。目前它还没有任何实际功能——但我们接下来构建的所有内容都将插入这个骨架中。测试能确保我们在继续开发时不会破坏已有的功能。