目录
Building Ai Agent From Scratch
1|---
2|title: 从零手写 AI Agent:50 行 Python,没有魔法,只有循环
3|name: building-ai-agent-from-scratch
4|date: 2026-05-12
5|tags: [AI, Python, LLM, Agent, MCP, 教程]
6|excerpt: 50 行 Python 代码,一个 while 循环,就构成了 AI Agent 的核心——没有 LangChain,没有框架,没有魔法。
7|categories: [AI]
8|---
9|
10|* 目录
11|{:toc}
12|
13|## 1. 什么是 AI Agent
14|
15|Sergey Nes 每天用 Claude、Codex、Cursor、Gemini,却说不清"聊天机器人"和"Agent"之间的那条线到底画在哪。最好的理解方式就是自己动手建一个,然后讲给别人听。
16|
17|**AI Agent 的定义很简单:**
18|
19|- 接受用户的**高层次任务**
20|- **推理**接下来该做什么
21|- **执行动作**(调用工具、搜索网页、读文件)
22|- **观察**结果
23|- **判断**是继续还是结束
24|
25|普通的 LLM 调用是一次性操作:发送 Prompt,收到回复,结束。Agent 之所以不同,是因为它**循环**。
26|
27|这个 think → act → observe → decide 的循环,就是把语言模型变成 Agent 的关键。
28|
29|[{: data-src="https://fla.cdn.bosyun.com/wangjundev/20260512/01-react-loop.jpg"}](https://fla.cdn.bosyun.com/wangjundev/20260512/01-react-loop.jpg)
30|
31|## 2. ReAct 模式:推理 + 行动
32|
33|大多数现代 Agent 遵循 **ReAct(Reason + Act)** 模式。LLM 不会直接跳到最终答案,而是:
34|
35|1. 产生一个"思考"——下一步该做什么
36|2. 执行一个"行动"——工具调用
37|3. 等待工具的返回结果
38|4. 把结果放回上下文,重复这个过程
39|
40|模型没有意识,也没有自我反思。它有的只是**对话历史**——每次行动和结果都留在上下文窗口中。
41|
42|每个循环发生的事情:
43|
44|```
45|发送当前对话给 LLM → system prompt + 用户消息 + 历史工具结果
46|LLM 返回最终答案或工具调用 → 如果是工具调用,执行并发结果追加到对话
47|再次发送给 LLM → 直到 LLM 返回最终答案
48|```
49|
50|**这就是整个架构。**
51|
52|## 3. 最小实现:50 行 Python
53|
54|核心 Agent 机制就是一个函数:
55|
56|```python
57|def run_agent(task: str, client: OpenAI, model: str = "gpt-4o-mini") -> str:
58| messages = [
59| {"role": "system", "content": "你是一个助手。可以使用工具。每次获得工具结果后,判断是否足够回答问题。"},
60| {"role": "user", "content": task}
61| ]
62|
63| while True:
64| response = client.chat.completions.create(
65| model=model,
66| messages=messages,
67| tools=TOOLS,
68| )
69| message = response.choices[0].message
70| messages.append(message)
71|
72| if not message.tool_calls:
73| return message.content
74|
75| for tool_call in message.tool_calls:
76| result = execute_tool(tool_call)
77| messages.append({
78| "role": "tool",
79| "tool_call_id": tool_call.id,
80| "content": result
81| })
82|```
83|
84|**关键行:** `if not message.tool_calls`——如果模型返回的文本不请求任何工具,说明它认为已经足够回答问题了。Agent 退出并返回文本。
85|
86|`messages` 列表就是 Agent 的**短期记忆**。每一次工具调用和结果都追加到它后面。当 LLM 决定完成时,它已经看到了自己做过的所有事和学到的一切。
87|
88|## 4. 换成本地模型:改一行代码
89|
90|Ollama 暴露了 OpenAI 兼容的 API,所以完全相同的 Agent 代码跑在本地模型上只需要**一个改动**:
91|
92|```python
93|ollama_client = OpenAI(
94| base_url="http://localhost:11434/v1",
95| api_key="ollama" # Ollama 不验证 key,但不能留空
96|)
97|run_agent("今天的日期是什么?", client=ollama_client, model="qwen2.5")
98|```
99| 100|代码完全不知道自己在跟 OpenAI 的服务器聊天,还是在跟本地机器上的模型说话。 101| 102|### 模型选择的坑 103| 104|作者试了 Mistral 7B,Agent 没有报错,但输出变成了这样: 105| 106|``` 107|Answer: I need to check the current date. 108|Tool: get_current_date() 109|Arguments: {} 110|``` 111| 112|**纯文本描述工具调用,没有实际的工具调用。** `response.tool_calls` 每次都是空的,所以 Agent 立即退出了。 113| 114|这不是 Agent 代码的 Bug,是**模型不支持 OpenAI 风格的结构化函数调用**。可靠支持函数调用的模型: 115| 116|[{: data-src="https://fla.cdn.bosyun.com/wangjundev/20260512/02-function-calling-table.jpg"}](https://fla.cdn.bosyun.com/wangjundev/20260512/02-function-calling-table.jpg) 117| 118|**如果你的 Agent 立即返回不调用任何工具,先怀疑模型,不要怀疑代码。** 119| 120|## 5. 混合模式:本地编排 + 云端推理 121| 122|可以在本地编排,只有在任务真正需要时才调用云端: 123| 124|```python 125|def ask_cloud_expert(question: str) -> str: 126| """当本地模型不足以回答复杂问题时的备选""" 127| cloud_client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) 128| response = cloud_client.chat.completions.create( 129| model="gpt-4o-mini", 130| messages=[{"role": "user", "content": question}], 131| ) 132| return response.choices[0].message.content 133|``` 134| 135|运行时: 136| 137|``` 138|Task: 2+2 等于几?还有,解释忒修斯之船的哲学含义 139|> Local model: 处理 2+2(通过计算器工具) 140|> Local model: 意识到哲学问题超出能力范围 141|> Local model: 调用 ask_cloud_expert() 142|> GPT-4: 返回完整的哲学分析 143|``` 144| 145|本地模型处理简单计算,复杂推理交给 GPT-4。**一次云端调用,成本几乎为零。** 146| 147|## 6. MCP:让 Agent 发现和调用任意工具 148| 149|DIY Agent 有一个缺失的功能:无法跨项目分享和复用工具。一切都被硬编码在脚本里。 150| 151|**MCP(Model Context Protocol)** 解决了这个问题。它定义了一套统一的标准,让任何 Agent 都能从任何服务器发现和调用工具: 152| 153|``` 154|Agent (MCP Client) → MCP Server → 工具 1 155| → MCP Server → 工具 2 156| → MCP Server → 工具 3 157|``` 158| 159|[{: data-src="https://fla.cdn.bosyun.com/wangjundev/20260512/03-mcp-architecture.jpg"}](https://fla.cdn.bosyun.com/wangjundev/20260512/03-mcp-architecture.jpg) 160| 161|### MCP Server 只需 10 行 162| 163|```python 164|# mcp_server.py 165|from mcp.server.fastmcp import FastMCP 166| 167|mcp = FastMCP("mini-tools") 168| 169|@mcp.tool() 170|def to_uppercase(text: str) -> str: 171| return text.upper() 172| 173|@mcp.tool() 174|def count_words(text: str) -> int: 175| return len(text.split()) 176| 177|mcp.run() 178|``` 179| 180|**关键洞察:** Agent 不关心工具是同一个文件里的 Python 函数,还是在互联网另一端运行的服务。只要它说 MCP 协议,就能用。 181| 182|## 7. vs Claude Code:理解 vs 生产 183| 184|作者的诚实对比: 185| 186|| 维度 | Claude Code | DIY Agent | 187||------|-------------|-----------| 188|| 子Agent | ✅ 隔离上下文窗口 | ❌ 不支持 | 189|| 危险操作确认 | ✅ 请求批准 | ❌ 直接执行 | 190|| 持久记忆 | ✅ 跨会话 | ❌ 仅单次对话 | 191|| 可读性 | ❌ 黑盒 | ✅ 每一行都懂 | 192|| 离线运行 | ❌ 依赖云端 | ✅ Ollama 完全离线 | 193|| 混合模式 | ❌ 不支持 | ✅ 本地+云端 | 194| 195|> "如果要交付可靠的东西,我用 Claude Code。如果要知道底层发生了什么,或者快速原型一个框架很难实现的功能,我手写循环。" 196| 197|## 8. 什么时候用框架 198| 199|> **你不需要 LangGraph 来理解什么是 Agent。当你需要重试、检查点和审批闸门时,你才需要它。** 200| 201|- **LangGraph**:把 Agent 建模为带显式节点和边的状态机 202|- **CrewAI / AutoGen**:多 Agent 协作,不同角色各司其职 203|- **Claude Agents SDK / OpenAI Assistants**:托管运行时,省去状态管理 204| 205|50 行的版本是草图。LangGraph 是同一张草图变成的带有承重墙的建筑。 206| 207|> **生产环境用框架。理解底层写循环。** 208| 209|## 9. 建这个项目教会我的 210| 211|作者说:"建这个给了我完整的思维模型。我现在能准确看到 Agent 可能在什么地方卡住、为什么它选这个工具而不是另一个、什么时候添加更多工具实际上会让事情更糟。" 212| 213|当需要使用框架时,你会知道它在帮你做什么。当不需要时,你不会引入一个无法调试的依赖。 214| 215|> **先建朴素版本。再决定。** 216| 217|知识链接: 218| 219|- 完整代码:github.com/sergenes/mini_agent 220|- OpenAI Function Calling:platform.openai.com/docs/guides/function-calling 221|- Ollama API:github.com/ollama/ollama 222|- MCP:modelcontextprotocol.io 223|- ReAct 论文 (Yao et al., 2022):arxiv.org/abs/2210.03629 224|
版权所有,本作品采用知识共享署名-非商业性使用 3.0 未本地化版本许可协议进行许可。转载请注明出处:https://www.wangjun.dev//2026/05/building-ai-agent-from-scratch/