AI Agent从0到1实战:50行Python手写核心循环,一次看懂所有Agent框架的底层逻辑
本文从零开始,用50行Python代码实现一个完整的AI Agent核心循环。不依赖LangChain、不引入任何Agent框架,让你彻底看清Agent的本质 ------ 读完这篇文章,所有Agent框架在你眼里都只是同一件事的不同封装。
一、为什么要"手写"Agent
2026年,AI Agent赛道已经卷到飞起。LangChain、CrewAI、AutoGPT、Dify、Coze......每个框架都在宣称自己是"最佳实践"。但当你真的用起来时会发现:
- LangChain 500MB+ 的依赖树,一个简单的工具调用要翻三层抽象
- 出了问题不知道该怪框架还是怪 prompt,排查全靠猜
- 每个框架的"创新"本质上都是同一件事的重新封装
剥掉所有框架的外衣,AI Agent 的核心循环只有 50 行代码。 理解了这 50 行,你就拥有了一把万能钥匙------无论未来出现什么新框架,你都能一眼看穿它的底层在干什么。
本文所有代码基于 DeepSeek API(国内直连、注册即用、免费额度够跑几千次实验),你不需要任何付费 API Key 就能完整复现。
二、Agent 的本质:一个公式说清楚
在写代码之前,先用一句话定义 Agent:
Agent = LLM推理 × 工具执行 × 循环迭代
展开来说,Agent 做的事情只有三件:
- LLM 推理:把"当前任务 + 可用工具清单 + 历史执行记录"拼成一段 prompt,发给 LLM
- 工具执行:解析 LLM 的返回结果,如果它说"我要调用工具 A",就执行工具 A 并拿到结果
- 循环迭代:把工具执行的结果追加到上下文中,回到第 1 步,直到 LLM 说"任务完成"
就这么简单。所有 Agent 框架的差异,只是在"怎么拼 prompt"和"怎么管理上下文"这两个维度上做了不同的选择。 核心循环是完全一样的。
下面我们直接写代码。
三、50行实现Agent核心循环
3.1 环境准备
bash
pip install openai
只需要这一个依赖。不用装 LangChain,不用装向量数据库,不用装任何东西。
3.2 完整代码
python
import json
from openai import OpenAI
# ====== 配置 ======
client = OpenAI(
api_key="your-deepseek-api-key", # 从 platform.deepseek.com 获取
base_url="https://api.deepseek.com",
)
MODEL = "deepseek-v4-flash"
MAX_ITER = 10 # 最大循环次数,防止死循环
# ====== 工具注册表 ======
def get_weather(city: str) -> str:
"""查询指定城市的天气"""
weather_data = {
"北京": "晴天,25°C,湿度45%",
"上海": "多云,28°C,湿度60%",
"深圳": "阵雨,30°C,湿度75%",
}
return weather_data.get(city, f"未找到{city}的天气数据")
def calculator(expression: str) -> str:
"""执行数学计算,支持加减乘除"""
try:
return str(eval(expression))
except Exception as e:
return f"计算错误: {e}"
TOOLS = {
"get_weather": {
"function": get_weather,
"description": "查询指定城市的天气,参数 city: 城市名称(如'北京'、'上海')",
},
"calculator": {
"function": calculator,
"description": "执行数学计算,参数 expression: 数学表达式(如'25*4+10')",
},
}
# ====== Prompt 构造 ======
SYSTEM_PROMPT = """你是一个智能助手,可以调用工具来完成任务。
当你需要调用工具时,请严格按以下JSON格式输出,不要输出任何其他内容:
{"tool": "工具名", "args": {"参数名": "参数值"}}
当你不需要调用工具、任务已完成时,请直接输出最终答案,不要使用JSON格式。
可用工具:
{tools_desc}"""
def build_tools_desc():
"""构建工具描述文本"""
lines = []
for name, info in TOOLS.items():
lines.append(f"- {name}: {info['description']}")
return "\n".join(lines)
# ====== Agent 核心循环 ======
def agent_loop(user_query: str) -> str:
"""
Agent 核心循环:LLM推理 → 工具执行 → 循环迭代
这是所有 Agent 框架共有的底层逻辑
"""
messages = [
{"role": "system", "content": SYSTEM_PROMPT.format(tools_desc=build_tools_desc())},
{"role": "user", "content": user_query},
]
for iteration in range(MAX_ITER):
# 第1步:LLM 推理
response = client.chat.completions.create(
model=MODEL,
messages=messages,
temperature=0.0,
)
reply = response.choices[0].message.content.strip()
# 第2步:判断是否需要调用工具
if reply.startswith("{") and reply.endswith("}"):
try:
tool_call = json.loads(reply)
tool_name = tool_call.get("tool", "")
tool_args = tool_call.get("args", {})
if tool_name in TOOLS:
# 执行工具
tool_fn = TOOLS[tool_name]["function"]
result = tool_fn(**tool_args)
# 将工具调用和结果追加到上下文
messages.append({"role": "assistant", "content": reply})
messages.append({
"role": "user",
"content": f"工具 '{tool_name}' 的执行结果:{result}\n\n请根据这个结果继续完成任务。如果任务已完成,请给出最终答案。"
})
continue
else:
messages.append({
"role": "user",
"content": f"错误:未知工具 '{tool_name}'。可用工具:{', '.join(TOOLS.keys())}。请重新选择。"
})
continue
except json.JSONDecodeError:
pass
# 第3步:没有工具调用 → 返回最终答案
return reply
return "已达到最大循环次数,任务可能陷入了死循环。请检查工具定义和任务复杂度。"
# ====== 测试 ======
if __name__ == "__main__":
# 测试1:单工具调用
result = agent_loop("北京今天天气怎么样?")
print(f"测试1结果:{result}")
# 测试2:多步推理(先计算,再查天气)
result = agent_loop("帮我算一下 365 * 24,然后告诉我上海天气")
print(f"测试2结果:{result}")
这就是全部。 一个完整的、能用的 AI Agent,50 行核心逻辑,零框架依赖。
四、逐行拆解:这50行到底在做什么
4.1 工具注册表 ------ 一行 if/elif 搞定
python
TOOLS = {
"get_weather": {
"function": get_weather,
"description": "查询指定城市的天气...",
},
"calculator": { ... },
}
LangChain 里定义一个工具需要:继承 BaseTool 类 → 实现 _run 方法 → 用 @tool 装饰器注册 → 传给 ToolExecutor。
但其实工具的本质就是:一个名字 + 一段描述 + 一个函数指针 。一个 dict 完全够用。
4.2 System Prompt ------ Agent 的"大脑说明书"
python
SYSTEM_PROMPT = """你是一个智能助手...
当你需要调用工具时,请严格按以下JSON格式输出:
{"tool": "工具名", "args": {"参数名": "参数值"}}
"""
这是整个 Agent 最关键的部分。你用 prompt 定义了 LLM 的"行为规范"------什么时候用工具、用什么格式返回。LangChain 的 AgentExecutor、CrewAI 的 Agent 类,本质上都是在帮你拼这行 prompt。
这正是很多人用框架踩坑的根源:框架帮你拼了 prompt,但拼出来的格式和你的任务不匹配,你只能对着黑盒猜问题。手写 prompt 的好处是------每条指令都是你自己写的,出了问题你知道该调哪里。
4.3 核心循环 ------ 30行的 while True
python
for iteration in range(MAX_ITER):
# 1. LLM 推理
response = client.chat.completions.create(...)
# 2. 判断是否需要工具调用
if reply.startswith("{") and reply.endswith("}"):
tool_call = json.loads(reply)
# 2a. 执行工具
result = tool_fn(**tool_args)
# 2b. 追加结果到上下文
messages.append(...)
continue # 回到第1步
# 3. 返回最终答案
return reply
这 30 行是 LangChain 几千行 AgentExecutor 源码的等价实现。看懂这个循环,你就看懂了所有 Agent 框架。
关键细节:
MAX_ITER = 10:防止死循环的最后防线。LangChain 里叫max_iterations,AutoGPT 里叫max_cycles,名字不同,做的事完全一样。messages.append(...):每轮把工具结果追加到消息列表,让 LLM 知道"上一步干了什么"。这是 Agent 的"记忆"机制。json.loads(reply):强制 JSON 解析。如果 LLM 返回了非标准格式,这里会报错并让 LLM 重试------这是最原始的"自我纠错"机制。
五、5个翻车现场:从Demo到能用的距离
写完 50 行代码跑通 Demo 只是第一步。从 Demo 到"能放心用",中间有 5 个常见的坑。我把它们一一列出来,并告诉你怎么改。
翻车现场 1:LLM 返回了非法 JSON
现象:
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
LLM 有时会返回类似 好的,我来调用天气工具:{"tool": "get_weather"...} 这种带前缀的格式,导致 JSON 解析失败。
修复(加3行代码):
python
# 在 json.loads(reply) 之前加入:
import re
json_match = re.search(r'\{.*\}', reply, re.DOTALL)
if json_match:
reply = json_match.group()
用正则把 JSON 部分从任意文本中提取出来,容错率瞬间提升。
翻车现场 2:死循环 ------ Agent 不停调用同一个工具
现象:
第1轮:调用 get_weather("北京")
第2轮:调用 get_weather("北京")
第3轮:调用 get_weather("北京")
...
第10轮:被 MAX_ITER 强行终止
LLM 没有从工具结果中得出"任务已完成"的结论,陷入了重复调用。
修复(2种策略):
python
# 策略A:在工具返回结果后主动加上"强制总结"指令
result_prompt = f"工具结果:{result}\n\n请基于以上结果直接给出最终答案,不要再调用工具。"
messages.append({"role": "user", "content": result_prompt})
# 策略B:检测连续3次调用同一工具且参数相同 → 强制返回
last_calls = [] # 记录最近3次调用
# ... 在循环中检测 ...
if len(set(last_calls[-3:])) == 1:
return "任务执行异常:检测到重复工具调用,已自动终止。"
实际生产环境建议策略 A(温和引导)和策略 B(硬性兜底)结合使用。
翻车现场 3:上下文爆炸 ------ 消息越来越长
现象:运行 5-6 轮后,messages 数组积累了几千个 token,LLM 开始"遗忘"最早的工具调用结果,回复质量断崖式下降。
修复(滑动窗口):
python
# 在 messages.append 之后加入:
MAX_CONTEXT_MESSAGES = 20 # 只保留最近20条消息
if len(messages) > MAX_CONTEXT_MESSAGES:
# 保留 system prompt + 最近 N-1 条
messages = [messages[0]] + messages[-(MAX_CONTEXT_MESSAGES - 1):]
LangChain 的 ConversationBufferWindowMemory 做的就是这件事。
翻车现场 4:LLM 调用了一个不存在的工具
现象 :LLM 返回 {"tool": "send_email", ...} 但你根本没定义 send_email。
这是 LLM 的"幻觉"在工具调用场景的体现------它"脑补"了一个它觉得应该存在的工具。
修复(硬校验 + 重试):
python
if tool_name not in TOOLS:
retry_prompt = f"错误:'{tool_name}' 不是可用工具。可用工具有:{', '.join(TOOLS.keys())}。请重新选择。"
messages.append({"role": "user", "content": retry_prompt})
continue # 回到循环开头,让 LLM 重试
原代码里已经有了这个处理,但要注意最多重试 2-3 次,重试太多次可能是 prompt 本身有问题。
翻车现场 5:凌晨三点模型抽风 ------ 非确定性输出
现象:白天跑得好好的 Agent,凌晨突然开始用英文回复、输出格式乱掉、甚至开始拒绝执行工具。
LLM 不是确定性系统。同一段 prompt 在不同时刻可能得到不同的输出。
修复(多重保险):
python
# 保险1:temperature=0.0 ------ 降低随机性(已在上方代码中设置)
# 保险2:结果格式校验
def validate_final_answer(reply: str) -> bool:
"""检查最终答案是否看起来合理"""
# 不应包含工具调用的 JSON
if reply.strip().startswith("{"):
return False
# 不应为空
if len(reply.strip()) < 5:
return False
return True
# 保险3:降级策略 ------ 连续失败后直接返回原始结果
if consecutive_failures >= 3:
return "抱歉,当前服务暂时不可用,请稍后重试。"
六、对比主流框架:它们的源码和这50行是同构的
理解了上面的 50 行代码后,我们来看看主流框架的底层实现。
LangChain AgentExecutor
LangChain 的 AgentExecutor._take_next_step() 方法本质就是我们的核心循环:
| 概念 | 我们的实现 | LangChain 对应 |
|---|---|---|
| 工具注册 | TOOLS = {...} dict |
Tool 类 + BaseTool 抽象 |
| Prompt 构造 | SYSTEM_PROMPT.format(...) |
ChatPromptTemplate + format_log_to_str |
| 解析 LLM 输出 | json.loads(reply) |
AgentOutputParser 子类 |
| 执行工具 | tool_fn(**tool_args) |
ToolExecutor.invoke() |
| 循环控制 | for iteration in range(MAX_ITER) |
while not stop + max_iterations |
| 上下文管理 | messages.append(...) |
AgentAction + AgentFinish 事件流 |
你看,LangChain 把这 50 行的每一部分都拆成了独立的抽象层。好处是可插拔,代价是------出了问题你要穿过 3 层继承链才能找到真正的执行逻辑。
CrewAI
CrewAI 的核心是 Agent 类 + Task 类。它的执行循环跟上面一模一样,区别在于:
- 多了"角色扮演"(给每个 Agent 分配不同的 system prompt)
- 多了"任务依赖"(Task A 的输出作为 Task B 的输入)
- 多了"顺序/并行执行"调度
剥掉这些,底层还是一个 while not done: think → act → observe → repeat 循环。
AutoGPT
AutoGPT 的核心循环是一个经典的 ReAct 模式:
THOUGHT: 我需要先查天气,再决定带不带伞
ACTION: get_weather("北京")
OBSERVATION: 北京今天晴天,25°C
THOUGHT: 晴天不需要带伞,任务完成
FINAL ANSWER: 今天北京是晴天,不用带伞
和我们 50 行代码的区别只在于 prompt 格式------AutoGPT 要求 LLM 输出 "THOUGHT/ACTION/OBSERVATION" 三段式,而我们的实现用 JSON 格式。本质上都是:推理 → 行动 → 观察 → 再推理。
七、从Demo到生产:3个升级方向
50 行代码是一个完美的学习起点。如果要用于生产环境,建议按以下路径迭代:
方向 1:用 Function Calling 替代 JSON 解析
DeepSeek API(以及 OpenAI 兼容的所有模型)原生支持 Function Calling:
python
response = client.chat.completions.create(
model=MODEL,
messages=messages,
tools=[{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"}
},
"required": ["city"]
}
}
}],
)
优点:模型级别保证 JSON 格式正确,不再需要正则提取和重试逻辑。
缺点:被锁定在 OpenAI 兼容的 API 格式上。
方向 2:引入工具执行沙箱
50 行代码里 eval(expression) 是个安全隐患------如果 LLM 返回了恶意代码,eval 会直接执行。
python
import ast
import operator
def safe_calculator(expression: str) -> str:
"""安全的数学计算,不使用 eval"""
allowed_ops = {
ast.Add: operator.add, ast.Sub: operator.sub,
ast.Mul: operator.mul, ast.Div: operator.truediv,
}
tree = ast.parse(expression, mode='eval')
# 递归遍历 AST,只执行允许的操作
...
对于文件系统操作、网络请求等敏感工具,建议加权限校验层。
方向 3:加入人工确认点(Human-in-the-Loop)
某些高风险操作(如删除数据、发送邮件、执行支付)应该加入人工确认:
python
DANGEROUS_TOOLS = {"delete_data", "send_email", "execute_payment"}
if tool_name in DANGEROUS_TOOLS:
confirm = input(f"Agent 请求执行高风险操作 '{tool_name}',参数: {tool_args}\n确认执行?(y/n): ")
if confirm.lower() != 'y':
messages.append({"role": "user", "content": f"用户拒绝了 '{tool_name}' 操作。请尝试替代方案。"})
continue
这是生产环境 Agent 的最后一道安全防线。
八、总结
回到最开始的公式:
Agent = LLM推理 × 工具执行 × 循环迭代
这 50 行代码是你理解所有 Agent 框架的"根知识"。以后无论遇到什么新框架、新概念,你都可以用这三个维度去拆解:
- 它怎么拼 prompt?(LLM 推理层)
- 它怎么定义和调度工具?(工具执行层)
- 它怎么控制循环和上下文?(循环迭代层)
答案永远在这三个问题里。
最后,把完整的、带所有容错机制的代码贴在这里。你可以直接复制到一个 .py 文件里运行:
python
import json
import re
from openai import OpenAI
client = OpenAI(
api_key="your-deepseek-api-key",
base_url="https://api.deepseek.com",
)
MODEL = "deepseek-v4-flash"
MAX_ITER = 10
TOOLS = {
"get_weather": {
"function": lambda city: {"北京": "晴天25°C", "上海": "多云28°C", "深圳": "阵雨30°C"}.get(city, "未知城市"),
"description": "查询城市天气,参数 city: 城市名称",
},
"calculator": {
"function": lambda expr: str(eval(expr)),
"description": "数学计算,参数 expression: 表达式",
},
}
SYSTEM_PROMPT = """你是智能助手,可以调用工具完成任务。
需要工具时输出JSON:{"tool":"工具名","args":{"参数":"值"}}
任务完成时直接输出答案。可用工具:
{tools_desc}"""
def agent_loop(query: str) -> str:
messages = [
{"role": "system", "content": SYSTEM_PROMPT.format(
tools_desc="\n".join(f"- {n}: {i['description']}" for n, i in TOOLS.items())
)},
{"role": "user", "content": query},
]
last_calls = []
for _ in range(MAX_ITER):
resp = client.chat.completions.create(model=MODEL, messages=messages, temperature=0.0)
reply = resp.choices[0].message.content.strip()
# JSON 提取(容错)
m = re.search(r'\{.*\}', reply, re.DOTALL)
if m:
try:
tc = json.loads(m.group())
name, args = tc.get("tool", ""), tc.get("args", {})
if name in TOOLS:
# 死循环检测
call_key = f"{name}:{json.dumps(args, sort_keys=True)}"
last_calls.append(call_key)
if len(last_calls) >= 3 and len(set(last_calls[-3:])) == 1:
return "检测到重复工具调用,任务已终止。请检查工具定义或尝试换个方式提问。"
result = TOOLS[name]["function"](**args)
messages.append({"role": "assistant", "content": reply})
messages.append({"role": "user", "content": f"工具结果:{result}\n请基于此结果给出最终答案,不要再调用工具。"})
continue
except (json.JSONDecodeError, TypeError):
pass
return reply if len(reply) > 3 else "抱歉,未能生成有效回答,请重试。"
return "达到最大循环次数,任务终止。"
if __name__ == "__main__":
print("测试1:", agent_loop("北京今天天气怎么样?"))
print("测试2:", agent_loop("365 * 24 等于多少?然后告诉我深圳天气"))
如果这篇文章对你有帮助,欢迎点赞、收藏、关注。你的支持是我持续输出硬核内容的动力。
本文使用的 DeepSeek API 当前定价(2026年7月):
- deepseek-v4-flash:输入 ¥0.001/1K tokens,输出 ¥0.002/1K tokens
- deepseek-v4-pro:输入 ¥0.003/1K tokens,输出 ¥0.006/1K tokens
- 新用户注册即送免费额度,足够完成本文所有实验