微信扫码
添加专属顾问
我要投稿
从零拆解AI Agent的核心循环,让你亲手实现自己的智能体。核心内容: 1. AI Agent的本质与ReAct循环模式解析 2. 从零构建Agent的完整实验记录与代码实现 3. 本地与云端混合模式的实战应用
作者:AI拉呱(Errol Yan)
定位:AI领域深度内容与实战方法分享
我每天都在用 Claude、Codex、Cursor、Gemini、Copilot 或 Junie,但很长一段时间里,我依然说不清“聊天机器人”到底是从哪一行代码开始,变成了“agent”。
我也讲不明白,究竟是什么让它们具备了 agent 的行为特征。于是我决定自己从零写一个最朴素的版本,把这件事彻底拆开看。
对我来说,理解一个新概念最好的方式,就是亲手做一遍,然后把它讲明白。本文正好做这两件事:它既是一次实验记录,也是一个实操教程。
我们会从大约 50 行 Python 开始,接上 OpenAI,切换到 Ollama 本地模型,再做一个本地 orchestration + 云端 delegation 的混合模式,随后加入 tools、实现 MCP,最后再和 Claude CLI 做一个对照。读完之后,你会清楚看到 agent 在底层到底发生了什么。
没有 LangChain,没有 LangGraph,没有 CrewAI。只有 Python、一个 LLM,以及一个 while 循环。
在你动手写之前,先得给它下定义。
一个 AI agent,本质上是一个程序,它需要:
普通的 LLM 调用是一锤子买卖:给 prompt,拿回答,结束。而 agent 的不同点在于它会循环。它会拿着高层目标不断重复“思考、行动、观察、再决定”的过程,直到任务完成。
这套循环,才是语言模型变成 agent 的关键。
大多数 agent 会遵循一个常见模式,叫 ReAct(Reason + Act)。模型不是直接跳到最终答案,而是先形成一个“想法”,再选择一个“动作”(通常是工具调用),然后根据“观察到的结果”继续推进。
图 1: ReAct 循环。先 Reason,再 Act,观察结果后再推理,直到模型认为已经足够回答。
模型没有真正意义上的意识,也没有人类式反思。它有的,是完整的上下文窗口:它此前做过什么、看到了什么、工具返回了什么,全部保留在消息历史里。ReAct 把这一点组织成了一种“看起来像会自我修正”的行为模式。
每次循环都非常简单:
这就是最核心的 agent 架构。
第一步,我们用云端 API 当大脑。这里用 OpenAI,是因为它的 tool calling 接口最直观;但只要兼容 OpenAI 风格,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)
if not message.tool_calls:
return message.content
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,
})
关键点就在这句:if not message.tool_calls。
如果模型返回的是普通文本,而没有请求调用工具,那就说明它认为自己已经拿到足够信息,可以直接回答。agent 于是退出并返回答案。
如果模型请求了工具,那 agent 就执行这些工具,把结果追加到 messages 里,再回到 while 循环顶部继续下一轮。
这里的 messages,其实就是 agent 的短期记忆。每次工具调用和结果都会被追加进去,因此当模型最终下判断时,它已经看见自己一路做过的所有事。
另外,system prompt 也很重要。它像方向盘一样,规定模型什么时候该用工具、什么时候该停止,以及最终答案应该长什么样。现实中的生产级 agent,system prompt 往往会比这个例子大得多。
为了让例子足够具体,我们先加三个最简单的工具:当前日期时间、计算器,以及一个天气 stub。真实项目里,你会把天气 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:
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:系统里有哪些工具、它们能干什么、需要什么参数。
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.
可以看到,模型在第一轮就识别出自己需要哪三个工具,调用完之后再把结果整合成最终答案。
没有框架,没有编排平台,只有一个循环。
Ollama 提供 OpenAI-compatible API,这意味着上面的 agent 几乎不用改,只改一个 client 配置就能跑本地模型:
ollama_client = OpenAI(
base_url="http://localhost:11434/v1",
api_key="ollama",
)
answer = run_agent(task, ollama_client, model="qwen2.5")
就这么简单。对 agent 代码而言,它根本不在乎对面是 OpenAI,还是你本机跑起来的 Ollama。
启动 Ollama 的方式一般是:
ollama pull qwen2.5
ollama serve
之后,这个 agent 就可以完全离线运行。我自己很喜欢用这个模式测试新工具,因为不用烧 API 预算,敏感数据也不会离开本机。
这是很多人第一次做本地 agent 时会踩到的坑。
比如我一开始试的是 mistral(Mistral 7B)。程序能正常跑,但输出长这样:
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 calls。结果就是 response.tool_calls 始终为空,于是 agent 直接退出,把那段描述文本当成最终答案返回。
这不是 agent 代码有 bug,而是模型本身不支持 OpenAI 风格的函数调用格式。
很多模型会模仿出“像是工具调用”的文本,但不会真正输出可执行的结构化 JSON。当前通过 Ollama 对 function calling 支持更稳定的,通常是像 qwen2.5 这类模型。
如果你的 agent 一上来就不调工具,先怀疑模型,再怀疑代码。
还有一种非常实用的模式:让本地模型负责 orchestration,只在任务真的复杂时,再调用云端大模型。
也就是说,默认跑本地 agent,但给它一个能“请云端专家出手”的工具:
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 和 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,但在遇到“忒修斯之船”这种更复杂的哲学问题时,调用 ask_cloud_expert()。你付费的云端调用,就从“整个任务都在云上跑”,变成“只为真正复杂的那一步买单”。
接下来再加几个更现实一点的工具:web_search、read_file 和 write_file。
from pathlib import Path
def web_search(query: str) -> str:
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:
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 就不只是会算数和报天气,而是开始具备:
到这一步,它已经能做很多实际工作了。
但还有一个明显限制:所有工具都被硬编码在脚本里。也就是说,工具无法复用,别的 agent 也没法直接拿来用。
这正是 MCP(Model Context Protocol)要解决的问题。
MCP 由 Anthropic 在 2024 年底推出,本质上是一个标准协议,定义了 agent 如何从任意 server 发现并调用工具。这个 server 可以是你自己写的,也可以是第三方提供的 GitHub、Slack、Postgres、Google Drive 等服务。
图 2: MCP 架构。一个 client(你的 agent)可以连接多个 server,而每个 server 暴露自己的工具。
当你的 DIY agent 变成 MCP client 后,工具定义就不再需要全部手写在本地脚本里,而是可以通过协议向 server 发现。
从 agent 视角看,MCP 工具和本地函数没什么区别:它们同样会出现在 TOOLS 里,被模型选择,被调用,再返回结果。
真正改变的是,工具从“写死在脚本里”变成“可共享、可复用、可独立维护”。
反过来,如果你希望自己的工具能被任意 MCP-compatible agent 使用,那就需要写一个 MCP server。
下面是一个极简的例子,它暴露了两个工具: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 通过 JSON-RPC 与它通信,工具调用和结果返回都遵循统一协议。
这意味着,你完全可以把它替换成远端服务,而 agent 代码本身几乎不用改。
任何 MCP-compatible agent 都能接上这个 server:Claude Desktop、Cursor、你自己写的 agent,或者其他支持 MCP 的客户端。
这正是 MCP 生态能扩展起来的原因。过去每个 agent 都要各自重写“调用 GitHub API”或“查询 Postgres”;现在,只要有人把能力封装成 MCP server,其他 agent 就能直接复用。
Claude Code 是生产级工具,而我写的这个 agent,更像学习工具。
它们的差别其实非常明确。
Claude Code 能做很多我这个 agent 做不到的事:
而我这个 agent,没有这些“安全护栏”和“成熟基础设施”。它只有几个工具、一个 messages 列表,以及一个循环。工具一旦抛异常,就可能直接崩。
但它的优势也很明确:
所以,如果我要交付稳定产品,我会用 Claude Code;但如果我要真正理解 agent 的底层机制,或者做一些我不想被框架束手束脚的实验,那我会从这个循环开始。
你不需要 LangGraph 才能理解 agent 是什么。但当 retries、checkpoints、approval gates 变成刚需时,框架就开始有意义了。
上面这个极简版本没有错误处理。工具抛错就崩,没有重试,没有人工审批,没有跨会话 memory,也没有多 agent 并行。
LangGraph 通过把 agent 建模成显式状态机,解决的是这些“工程化问题”:checkpointing、结构化错误处理、人类介入节点,以及更好的 observability。
CrewAI 和 AutoGen 更偏向多 agent 协作:你可以定义 research、writer、critic 等不同角色,让它们用不同 prompt 或模型协同工作。
Claude Agents SDK 和 OpenAI Assistants API 则是托管式运行时:你把状态管理、工具路由和线程管理交给平台,自己换取更快的交付速度。
那个 50 行版本,更像是一张草图。而 LangGraph 则是在这张草图之上,加上真正能承重的结构。
结论也很简单:
我最初只是想搞明白,AI agent 到底是怎么工作的。现在我明白了。
自己写这个东西之后,我终于有了完整的心智模型:
未来我还是会在很多项目里继续使用 LangGraph、Claude Agents SDK 这类框架,因为它们确实解决了真实问题。但我也会有一些项目,从这个极简版开始,因为我完全知道它在做什么,而且修改它时不需要先和一个庞大抽象层搏斗。
现在你也已经看到了同样的东西:并没有什么神秘魔法。模型只是看着会话历史,判断自己是否有足够信息回答,还是需要调用工具;然后不断重复,直到任务完成。
所谓 retry、human-in-the-loop、memory、multi-agent orchestration,都是在这条循环之上逐渐叠加出来的工程层。
先把最朴素的版本写出来。之后,你再决定自己到底需不需要框架。
Code:
Documentation:
Frameworks:
Papers:
如果这篇内容对你有启发,欢迎关注「AI拉呱」,获取更多 AI 前沿洞察、实战教程与趋势解读。
下期将继续带来该主题的进阶拆解与实操案例,建议先收藏本文,避免错过更新。
DeepSeek的核心创新点" data-itemshowtype="0" linktype="text" data-linktype="2">DeepSeek的核心创新点
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业
2026-05-16
AI Native 创业手册 | 蚂上WEEKLY VOL.172
2026-05-16
腾讯造了个“贾维斯”:替我签到改配置,还会打盹上厕所,一手实测来了
2026-05-15
成为ClaudeCode顶尖1%用户的完整指南
2026-05-15
在手机上用Codex写一下午代码,说实话,有点上头。
2026-05-15
腾讯混元推出轻量翻译大模型,无需联网,手机直接运行!
2026-05-15
谷歌Android重大更新!底层植入Gemini,苹果已掉队
2026-05-15
Codex更新远程控制,你也终于可以在手机上随时随地Vibe Coding了。
2026-05-15
2026年了,我强烈推荐你用一用Codex,功能太全面了!附使用指南
2026-04-15
2026-03-31
2026-03-13
2026-04-07
2026-03-17
2026-03-17
2026-04-07
2026-03-21
2026-02-20
2026-04-24
2026-05-09
2026-05-09
2026-05-09
2026-05-08
2026-05-07
2026-04-26
2026-04-22
2026-04-18