从零构建 AI Agent:没有魔法,只有确定性的循环
从零构建 AI Agent:没有魔法,只有确定性的循环
我每天都在用 Claude、Codex、Cursor、Gemini、Copilot 或 Junie,但我依然说不清到底是哪一行代码让“chatbot”变成了“agent”,也解释不了它们为何算是 agent。于是我从零写了一个最天真的版本,自己找答案。

对我来说,理解一个新概念的最好方式,就是把它做出来,再讲给别人听。本文两者兼顾。我把实验故事和实操教程结合在一起,我敢保证你会觉得有用。
我们将从区区 50 行 Python 开始,先接入 OpenAI,再通过 Ollama 切换到本地模型,构建本地+云端混合模式,加入 tools,接入 MCP,最后和 Claude CLI 做个对比。看完你就能清清楚楚地看到“底层到底发生了什么”。
不需要 LangChain。也不需要 LangGraph、CrewAI。只用 Python、一个 LLM,再加一个 while 循环。
我们要构建什么(规格说明)
在动手之前,先定义清楚它是什么、做什么(spec)。
一个 AI agent(下文统一用 Agent 指代“智能体”)是一个程序,它会:
-
接受用户给出的高层任务
-
推理“下一步该做什么”
-
采取行动(调用 tool、搜索网页、读取文件)
-
观察结果
-
决定是继续迭代还是返回最终答案
-
维护对话历史,让每次决策都建立在先前步骤之上
普通的 LLM 调用是一次性(one-shot):你发出 prompt,它给你回复,结束。Agent 不同在于它会循环。它接收高层任务,推理下一步,调用 action,观察结果,如此反复,直到任务完成。
这个“思考-行动-观察-决定”的循环,才是把语言模型变成 agent 的关键。
多数 Agent 采用一种叫 ReAct(Reason + Act)的模式。LLM 不会直接跳到最终答案,它会先产出“thought(想法/计划)”,再产出“action(工具调用)”,然后等待“observe(观察)”结果,再决定下一步做什么。

Figure 1: ReAct 循环——先 Reason 再 Act,观察结果,再次推理。循环往复,直到模型有足够信息可以直接作答。
模型没有“意识”,也没有任何有意义的“自我反省”。它拥有的只是对话历史:它做过的每个动作、得到的每个结果,都在 context window(上下文窗口)里。ReAct 模式把这些变成类似“自我反省/自我纠错”的行为,而且确实有效。
每一轮循环会发生这些事:
-
你把当前对话发给 LLM:system prompt、用户消息、以及之前的 tool 结果
-
LLM 返回要么是最终答案,要么是一组它想调用的 tools
-
如果是最终答案,就结束
-
如果是 tool 调用,就执行它们,把结果追加到对话历史,再回到第 1 步
这就是全部架构。
最小实现(云端 API 作为大脑)
先用云端 API 当大脑。我选了 OpenAI,因为它的 tool calling 接口最干净,但任何 OpenAI-compatible API 都可以用。Gemini、Anthropic 等也都支持。
核心 Agent 机制就是这样:
def run_agent(task: str, client: OpenAI, model: str = "gpt-4o-mini") -> str:
messages = [
{
"role": "system",
"content": (
"You are a helpful assistant. Use tools when needed. "
"When you have a final answer, respond without calling any tools."
),
},
{"role": "user", "content": task},
]
while True:
response = client.chat.completions.create(
model=model,
messages=messages,
tools=TOOLS,
tool_choice="auto",
)
message = response.choices[0].message
messages.append(message)
# This is the decision point: does the model have an answer, or does it need tools?
if not message.tool_calls:
return message.content
# If we reach here, the model called one or more tools
for tool_call in message.tool_calls:
name = tool_call.function.name
args = json.loads(tool_call.function.arguments)
print(f" > calling {name}({args})")
fn = TOOL_FUNCTIONS.get(name)
result = fn(args) if fn else f"Unknown tool: {name}"
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
# End of iteration - go back to the top of the while loop
关键行是 if not message.tool_calls。如果模型返回的是文本、且没有请求任何 tool,就表示它已经具备回答所需的一切。Agent 退出并返回该文本。如果模型请求了 tool,Agent 就执行它们,把结果追加到对话历史,然后把所有内容再发回模型进入下一轮。
messages 列表就是 Agent 的短期记忆。每次 tool 调用和每个结果都会被追加进去。等 LLM 判断“完成”时,它已经“看过”自己做过的所有事和学到的所有信息。
system prompt 同样很重要。它是方向盘,告诉模型何时用 tool、何时停止、最终答案应该是什么样子。在真实的生产 Agent 中,这个 system prompt 往往相当庞大,这点从 Anthropic、Apple 等偶尔的泄露中也能看见。
定义 Tools
先来三个简单的,便于具体化:当前日期/时间、计算器和天气(stub)。在真实 Agent 里,你会把 stub 换成实际 API 调用。
import json
import os
from datetime import datetime
from openai import OpenAI
def get_current_date() -> str:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def calculate(expression: str) -> str:
try:
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
except Exception as e:
return f"Error: {e}"
def get_weather(city: str) -> str:
# Replace with a real weather API call
return f"Weather in {city}: 72°F, partly cloudy"
TOOL_FUNCTIONS = {
"get_current_date": get_current_date,
"calculate": calculate,
"get_weather": get_weather,
}
工具 schema 告诉 LLM 有哪些可用的东西。这段 JSON 就是模型在决定调用哪个 tool、用什么参数时所“看到”的内容:
TOOLS = [
{
"type": "function",
"function": {
"name": "get_current_date",
"description": "Returns the current date and time",
"parameters": {"type": "object", "properties": {}, "required": []},
},
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "Evaluates a math expression and returns the result",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "A Python math expression, e.g. '2 + 2' or '100 * 0.15'",
}
},
"required": ["expression"],
},
},
},
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Gets current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"}
},
"required": ["city"],
},
},
},
]
运行:
if __name__ == "__main__":
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
task = "What's today's date? Also, what is 15% of 847? And what's the weather in Tokyo?"
print(f"Task: {task}/n")
answer = run_agent(task, client)
print(f"/nAnswer: {answer}")
输出:
Task: What's today's date? Also, what is 15% of 847? And what's the weather in Tokyo?
> calling get_current_date({})
> calling calculate({'expression': '847 * 0.15'})
> calling get_weather({'city': 'Tokyo'})
Answer: Today is 2026-04-30 09:14:22. 15% of 847 is 127.05.
The weather in Tokyo is 72°F and partly cloudy.
在第一轮,LLM 识别到它需要三个 tool,逐个调用、拿到结果,然后组装出最终答案。没有框架、没有编排层。
用 Ollama 将云端 API 换成本地 LLM
Ollama 暴露的是 OpenAI-compatible API,所以同样的 Agent 代码只需要改一处就能跑在本地模型上:
ollama_client = OpenAI(
base_url="http://localhost:11434/v1",
api_key="ollama", # required by the client library, ignored by Ollama
)
answer = run_agent(task, ollama_client, model="qwen2.5")
就这样。代码根本不知道它是在和 OpenAI 的服务器对话,还是在和你机器上的模型对话。
让 Ollama 跑起来:
# install from ollama.com, then:
ollama pull qwen2.5
ollama serve
之后,Agent 就完全离线运行。我用这种方式测试新 tool,既不烧 API 点数,也能在敏感数据不出本机的场景下使用。
不是所有本地模型都支持 tool calling
这一点会坑到你。我一开始试了 mistral(Mistral 7B),它经常被推荐为很能打的本地模型。Agent 运行没报错,但输出类似:
Answer: I need to call get_current_date() to find today's date.
Let me use the calculate tool: calculate(expression="847 * 0.15")...
纯文字“描述”将要调用哪些 tool,但没有真正发出 tool 调用。response.tool_calls 每次都是空的,于是它立刻带着模型写下的文字就退出了。
这不是 Agent 代码的 bug。代码严格按写的逻辑工作:检查有无 tool 调用,没找到就返回。问题在于 Mistral 7B 并不支持 OpenAI 风格的结构化 function calling。它被训练成用散文“描述”动作,而不是产出结构化 JSON。模型“幻写”了它以为我想要的语法。
通过 Ollama、能稳定支持 function calling 的模型有:

Table 1
如果你的 Agent 立即返回、从不调用任何 tool,先怀疑模型,不要怀疑代码。换成 qwen2.5 试试,看看行为有没有变化。
构建混合模式(本地编排,云端委托)
你可以在本地做编排,只有在任务真的需要时才为云端调用买单。默认用本地模型跑 Agent 和简单工具,但提供一个 tool,把复杂推理问题丢给云端模型。本地模型负责循环和简单工具;遇到需要更深推理的内容,就委托。
def ask_cloud_expert(question: str) -> str:
"""Delegate complex questions to a cloud model."""
cloud_client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
response = cloud_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": question}],
)
return response.choices[0].message.content
把它加入 TOOL_FUNCTIONS,并把对应 schema 加进 TOOLS。现在当你运行:
answer = run_agent(
task="What's 2+2? Also, explain the philosophical implications of the Ship of Theseus paradox.",
client=ollama_client,
model="qwen2.5"
)
本地模型会处理 2+2(通过 calculator tool),并意识到哲学问题超出它的深度,于是调用 ask_cloud_expert(),从 GPT-4 拿到一个像样的答案。你只为一次云端 API 调用买单,而不是为数十次买单。
增加更多 Tools
让我们扩展出更贴近真实世界的工具:web_search、read_file 和 write_file。
from pathlib import Path
def web_search(query: str) -> str:
# Stub - replace with Brave Search API, SerpAPI, or Tavily
return (
f"Search results for '{query}':/n"
f"1. Wikipedia: comprehensive overview/n"
f"2. Recent article: explained in 5 minutes/n"
f"3. Official docs"
)
def read_file(path: str) -> str:
# Safe path validation omitted for brevity
return Path(path).read_text()
def write_file(path: str, content: str) -> str:
Path(path).write_text(content)
return f"wrote {len(content)} chars to '{path}'"
TOOL_FUNCTIONS = {
"get_current_date": get_current_date,
"calculate": calculate,
"get_weather": get_weather,
"web_search": web_search,
"read_file": read_file,
"write_file": write_file,
}
把它们的 schema 加进 TOOLS,Agent 现在就能搜索网页、并把结果持久化到磁盘。上述 web_search 是 stub,文件操作也做了简化。完整项目在 github.com/sergenes/mini_agent 中包含了更严谨的路径校验与错误处理。
有了这 6 个 tool,Agent 现在可以:
- 回答需要当前信息的问题(日期/时间)
- 进行计算
- 查询天气
- 搜索网页
- 读写文件
这已经足够做实事了。剩下的问题是:每个 tool 都是硬编码在脚本里的。没法和别的 Agent 共享,也没法直接使用别人做好的工具。
MCP 客户端:从外部服务器发现工具
上面的 Agent 缺少一件事:跨项目共享/复用工具的能力。一切都硬编码在脚本里。想让另一个 Agent 用同样的工具?复制粘贴。想用别人的工具?改写。
MCP(Model Context Protocol,模型上下文协议)由 Anthropic 于 2024 年 11 月推出,它就是为此而生的标准。它定义了一种统一方式,让任何 Agent 都能从任何服务器发现并调用工具:无论是你自己的,还是第三方的 GitHub、Slack、Postgres、Google Drive 等等,成百上千的工具。

Figure 2: MCP 架构——一个客户端(你的 Agent),多个服务器。每个服务器暴露自己的工具。Agent 以同样的方式发现和调用它们,而不关心服务器背后是什么。
你的 DIY Agent 变成一个 MCP 客户端。你不再硬编码 tool 定义,而是去调用服务器,拿回它暴露出来的工具:已经被发现、已经带描述,可以直接传给你的 LLM。
Agent 的逻辑不变,变化的是工具的来源以及谁来维护它们。
配套项目里包含 mcp_client.py,它会把 MCP 服务器以子进程的方式拉起来,并通过 JSON-RPC 调用工具。从 Agent 的视角看,MCP 工具和本地定义的工具没有任何区别:同样出现在 TOOLS,同样被调用、同样返回结果。
关键洞见是:Agent 并不在乎一个 tool 是不是同一文件里的 Python 函数,还是在互联网上另一端的服务。只要它说 MCP 这门“语言”,就能用。
MCP 服务器:把你的工具暴露给任意 Agent
反过来:如果你想把自己的工具暴露给所有 MCP-compatible 的 Agent 使用,你就写一个 MCP server。
下面是一个完整的 MCP 服务器,暴露了两个工具——to_uppercase 和 count_words:
# mcp_server.py - a real MCP server in 10 lines
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("mini-tools")
@mcp.tool()
def to_uppercase(text: str) -> str:
"""Convert text to uppercase."""
return text.upper()
@mcp.tool()
def count_words(text: str) -> int:
"""Count the number of words in a string."""
return len(text.split())
if __name__ == "__main__":
mcp.run()
我故意让它很“平”,重点在边界:mcp_server.py 是一个独立进程。Agent 调用一个 tool,子进程启动、完成 JSON-RPC 握手、结果回传。你完全可以把它换成跑在互联网上另一头的服务器,而 Agent 代码一行都不用改。
现在,任何 MCP-compatible 的 Agent 都可以用你的工具——Claude Desktop、Cursor、你的 DIY Agent,谁都行。你发布 server,大家在配置里指过去,就能直接用。
生态就是这样扩展的。不是每个 Agent 都去重复实现“调用 GitHub API”或“查询 Postgres”,而是某个人写一个 MCP server,大家共用。
与 Claude CLI 的对比
Claude Code 是生产力工具。我的 Agent 是学习工具。这是诚实的对比,也值得弄明白为什么。
Claude Code 做了我的 Agent 做不到的事:当任务很大时,会启动带隔离上下文窗口的子 Agent;在执行破坏性命令前会请求确认;在会话间保持持久记忆;在工具调用失败时会调整参数重试;在逼近上下文上限时会压缩历史消息。我的 Agent 全都没有。它只有 6 个工具、一个 messages 列表,没有安全网。工具抛异常,它就崩。
我的 Agent 的优势是:我能读懂它的每一行。出问题时,我知道去哪一行看。我可以让它完全离线地用 Ollama 跑,或者接混合模式,只为确实需要的云端调用付费。Claude Code 是按消息计费。而我的 Agent,除非我让它调用 GPT-4,否则不花钱。
如果我要交付可靠的东西,我会用 Claude Code。如果我要搞清楚“底层到底在发生什么”,或者要原型实现一些用框架会“打架”的东西,我就从这个循环开始。
该轮到框架出场的时刻
你不需要 LangGraph 才能弄懂什么是 Agent。你需要它,是当重试、检查点、审批闸门变得不可或缺的时候。
上面的代码没有错误处理。工具抛异常,Agent 就崩。没有重试逻辑。没有在风险操作前暂停等待人工批准。除了单条对话,没有其它记忆能力。也不会为并行工作生成子 Agent。
LangGraph 用“状态机(state machine)”的方式解决这些问题:用显式的节点与边来建模 Agent。你定义每一步做什么、什么条件触发下一步。代价是前期搭建更多,但你能得到 checkpointing、结构化错误处理、human-in-the-loop 步骤,以及对 Agent 在干什么、为什么这么干的全量可观测性。
CrewAI 和 AutoGen 专注多 Agent 协作。不是一个 Agent 用很多工具,而是定义多个角色(researcher、writer、critic),编排它们如何沟通。适合复杂任务,需要不同步骤用不同 prompt 或不同模型的场景。
Claude Agents SDK 和 OpenAI Assistants API 则是托管运行时,你把状态管理、tool 路由、线程等交给平台。控制力少一些,但交付更快。
50 行版本是一张草图。LangGraph 则是把这张草图变成有“承重墙”的建筑。
要做生产:用框架。要搞懂本质:自己写这个循环。
我从这个项目里学到了什么
我想弄懂 AI Agent 是如何工作的。现在我懂了。
搭起来之后,我补齐了之前缺失的心智模型。我能看见 Agent 会卡在哪里、为什么会偏好某个 tool、以及什么时候“加更多工具”反而会变糟。我知道当 Claude Code 启动子 Agent 时发生了什么,或者 Cursor 决定重试一次失败操作时发生了什么。
我有一些项目需要 agentic behavior(Agent 式行为)。有些会用 LangGraph 或 Claude Agents SDK——这些框架确实解决了我不想重复造轮子的真实问题。但也有些会从这 50 行版本起步,因为我确切知道它做了什么,而且我可以在不与陌生抽象“搏斗”的前提下改它。
你现在也看到了:没有魔法。模型观察对话历史,判断自己是否“够了可以直接回答”,或者“还需要一个 tool”,然后不断重复,直到完成。其它的一切——重试逻辑、human-in-the-loop、记忆、多 Agent 编排——都是在这个循环之上搭起来的。
当你去用一个框架时,你会知道它在替你做什么。当你不需要它时,你也不会引入一个你调不通的依赖。
先把天真的版本造出来。再做决定。
参考资料
Code:
- 全部配套项目:github.com/sergenes/mini_agent
Documentation:
- OpenAI Function Calling:platform.openai.com/docs/guides/function-calling
- Ollama API:github.com/ollama/ollama/blob/main/docs/api.md
- Model Context Protocol (MCP):modelcontextprotocol.io
- FastMCP:github.com/jlowin/fastmcp
Frameworks:
- LangGraph:langchain-ai.github.io/langgraph
- CrewAI:crewai.com
- AutoGen:microsoft.github.io/autogen
- Claude Agents SDK:docs.anthropic.com/en/docs/agents
- OpenAI Assistants API:platform.openai.com/docs/assistants
Papers:
- ReAct: Synergizing Reasoning and Acting in Language Models(Yao 等,2022):arxiv.org/abs/2210.03629
这里给大家精心整理了一份全面的AI大模型学习资源,包括:AI大模型全套学习路线图(从入门到实战)、精品AI大模型学习书籍手册、视频教程、实战学习、面试题等,资料免费分享!
👇👇扫码免费领取全部内容👇👇
1. 成长路线图&学习规划
要学习一门新的技术,作为新手一定要先学习成长路线图,方向不对,努力白费。
这里,我们为新手和想要进一步提升的专业人士准备了一份详细的学习成长路线图和规划。可以说是最科学最系统的学习成长路线。
2. 大模型经典PDF书籍
书籍和学习文档资料是学习大模型过程中必不可少的,我们精选了一系列深入探讨大模型技术的书籍和学习文档,它们由领域内的顶尖专家撰写,内容全面、深入、详尽,为你学习大模型提供坚实的理论基础。(书籍含电子版PDF)

3. 大模型视频教程
对于很多自学或者没有基础的同学来说,书籍这些纯文字类的学习教材会觉得比较晦涩难以理解,因此,我们提供了丰富的大模型视频教程,以动态、形象的方式展示技术概念,帮助你更快、更轻松地掌握核心知识。

4. 2026行业报告
行业分析主要包括对不同行业的现状、趋势、问题、机会等进行系统地调研和评估,以了解哪些行业更适合引入大模型的技术和应用,以及在哪些方面可以发挥大模型的优势。

5. 大模型项目实战
学以致用 ,当你的理论知识积累到一定程度,就需要通过项目实战,在实际操作中检验和巩固你所学到的知识,同时为你找工作和职业发展打下坚实的基础。

6. 大模型面试题
面试不仅是技术的较量,更需要充分的准备。
在你已经掌握了大模型技术之后,就需要开始准备面试,我们将提供精心整理的大模型面试题库,涵盖当前面试中可能遇到的各种技术问题,让你在面试中游刃有余。

7. 资料领取:全套内容免费抱走,学 AI 不用再找第二份
不管你是 0 基础想入门 AI 大模型,还是有基础想冲刺大厂、了解行业趋势,这份资料都能满足你!
现在只需按照提示操作,就能免费领取:
👇👇扫码免费领取全部内容👇👇
更多推荐


所有评论(0)