拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

零基础手写最小AI Agent:从工具调用到ReAct循环的完整实践

零基础手写最小AI Agent:从工具调用到ReAct循环的完整实践

先说句实在话,这两年被 AI Agent 刷屏的人不在少数,但大部分人卡在同一个地方:看了无数概念帖、产品盘点、框架对比,到了自己动手的时候连个能跑的最小demo都搞不出来。这篇教程就是冲着"零基础可跑"去的,我不跟你扯太多玄乎的理论,直接带你从环境搭建开始,一步一步写出一个真正能自主调用工具、完成多步任务的 AI Agent。

这套内容我踩过不少坑才整理出来,适合三种人看:一是对 AI Agent 只有模糊概念、想快速入门的开发者,二是想在公司内部做技术预研或 demo 验证的工程师,三是打算用 Agent 做个人效率工具但不知道从哪下手的爱好者。你不需要有深度学习背景,只要会一点 Python 基础语法,跟着下面的步骤走完,就能拥有一个属于自己的最小 Agent。

1. AI Agent 到底是什么:先搞清楚你在搭什么

1.1 一句话说清 Agent 与普通对话助手的核心区别

很多人以为接了大模型 API、能聊天就是 Agent,这是个很常见的误区。普通的对话机器人,比如你写个 prompt 调一下 GPT 或国产大模型接口,它只能"输入文本-输出文本"。它没有手、没有脚,没法帮你查数据库、发邮件、订机票。

而 Agent 的本质是:让模型具备使用工具的能力,并且能自己规划行动步骤。你可以把它理解成一个"有手有脑"的员工——大脑是大模型,负责理解任务、拆解计划;手是各种工具,比如代码执行器、搜索接口、数据库查询、文件读写。

举个例子,你让普通聊天机器人"帮我查一下本周天气并生成穿衣建议",它只能凭训练数据编一个答案。但 Agent 会这样工作:先调用天气查询工具拿到实时数据,再根据温度数据分析穿衣建议,最后把结果整理成文字回复你。整个过程不是一次性生成的,而是"观察-思考-行动-再观察"的循环。

1.2 Agent 的三个核心循环:感知、决策、行动

任何 Agent,不管用 LangChain、MetaGPT 还是自己纯手写,底层都是围绕一个循环在转:

  • 感知(Perception):Agent 接收用户请求之后,需要理解当前的状态。这个状态可能是用户输入的一句话,也可能是它调完工具之后拿到的返回值。
  • 决策(Decision):大模型根据当前状态,决定下一步该做什么。是调用工具,还是直接给出最终答案,这是由模型推理出来的,不是代码写死的。
  • 行动(Action):执行具体的动作,比如调用一个函数、发起一次 HTTP 请求、执行一段 Python 代码。行动之后会产生新的观察结果,再喂给模型做下一轮决策。

这个过程跟人类做事的逻辑很接近。你做饭的时候,先看看冰箱里有什么(感知),决定做番茄炒蛋还是红烧肉(决策),然后开火下锅(行动),尝一口发现咸了(新的感知),于是加水补救(又一次决策和行动)。Agent 的 ReAct 模式就是把这个人类的思维过程搬到了代码里。

1.3 常见 Agent 形态与练手项目方向

2026 年这个时间点上,国内外的 Agent 产品已经相当丰富了。CrewAI 管多角色协作、Dify 做可视化编排、Coze 面向业务人员、AutoGPT 走全自动路线,再加上各大云厂商的 Agent 中台产品,确实让人眼花缭乱。

但我的建议很直接:练手别一上来就上重型框架。用纯代码手写一个最小 Agent,你才能真正理解工具调用和循环决策的机制。等你搞明白了内部逻辑,再去用那些框架,就像学过手动挡再去开自动挡一样,心里特别有底。

适合零基础练手的项目方向很多:个人日程助手(查日历、定提醒)、代码生成与执行工具、文档问答机器人、周报总结 Agent。后面我会挑一个"自动查询信息并汇总"的典型场景,带你把完整代码跑通。

2. 工具选型:三条路线,按你的基础挑一条

2.1 路线一:纯 Python + 大模型 API(适合想搞懂原理的人)

这条路线是我最推荐的入门方式。核心依赖只有两个:一个 Python 环境,一个大模型的 API 接口。不需要任何第三方 Agent 框架,逻辑全自己写。

优点非常明显:第一,没有框架的"黑魔法",每一步做了什么你都清清楚楚;第二,调试方便,出问题你能精确定位是模型的问题还是工具的问题;第三,代码量少,核心逻辑加起来不到 200 行。缺点也很直接:所有底层细节都要自己处理,比如对话历史怎么管理、工具返回结果怎么塞给模型,这些在框架里是现成的。

对于零基础的人来说,我反而觉得这些"麻烦"是好事。因为 Agent 的本质就是一个循环,你自己写一遍这个循环,比看十篇框架源码分析都管用。

2.2 路线二:LangChain + LangGraph(适合有 Python 基础的人)

如果你想跳过底层实现,直接做比较复杂的 Agent 应用,LangChain 和 LangGraph 是目前社区最成熟的方案。LangChain 提供了封装好的 Tool 抽象、模型接入、Prompt 模板;LangGraph 则在图结构上做 Agent 的状态管理和流程控制。

用框架的好处是开发效率高,很多边缘情况框架已经帮你处理了。比如 OpenAI Function Calling 的回调格式、工具参数的 JSON Schema 生成,框架里封装得都很完善。坏处是你需要花时间理解框架的概念,而且框架版本迭代快,API 变动频繁,在搜索引擎里找到的教程很可能已经过时了。

我的建议是:先用路线一跑通一个最小 Agent,把核心概念理解透了,再决定要不要进框架的坑。顺序反了的话,你会陷入"报错-查文档-改代码-再报错"的循环,非常打击信心。

2.3 路线三:可视化智能体平台(适合完全不写代码的人)

如果你完全不会编程,又确实需要用 Agent 解决实际问题,那 Dify、Coze 这类的可视化平台是可以考虑的。这些平台把 Agent 的构建过程做成了拖拽式的工作流,你只需要配置节点、填写 Prompt 模板,平台会自动帮你处理模型调度和工具集成。

不过说实话,这类平台对"零基础搭建"来说虽然门槛最低,但天花板也明显。你只能在平台提供的工具范围内做组合,一旦遇到定制化需求就会束手束脚。而且平台封装的逻辑太多,你在里面拖出来的 Agent 到底是怎么工作的,可能并不清楚。

所以这条路线适合做产品验证或者业务侧自助使用,不适合想深入学习 Agent 原理的人。这篇教程的主角是路线一,后面所有代码都是纯手写实现。

3. 动手前必看:环境准备与关键概念

3.1 Python 环境与项目结构

工欲善其事,必先利其器。我建议用 Python 3.10 以上版本,避免一些新版库的兼容问题。环境管理用 venv 就够了,不需要上 Conda 那么重的工具。

python3 -m venv agent_env source agent_env/bin/activate pip install openai

项目结构非常简单粗暴:

agent-demo/ ├── main.py # 主程序 ├── tools.py # 自定义工具函数 └── .env # 存放 API Key(别提交到 Git)

不要一上来就搞复杂的包结构。我们练手的目的是把 Agent 跑起来,工程化的事情以后再考虑。等你理解了核心逻辑,再拆分模块、加日志、做配置管理,完全来得及。

3.2 API Key 管理与成本意识

这个点我必须单独拿出来说,因为太多新手在这里翻车。第一,API Key 绝对不能硬编码在代码里,尤其不能提交到公开仓库。建议用环境变量或者 .env 文件管理,并在 .gitignore 里把 .env 排除掉。

第二,如果你调用的是国内大模型服务,通常会自动有免费额度或者比较低的计费标准。但不管用哪家,我都建议在代码里加上最大轮次限制,防止 Agent 陷入无限循环疯狂调 API,一晚上把你预算烧光。我见过不止一个人因为没设上限,一觉醒来账单几十块的。

第三,关于远程访问这些 API 服务的网络问题,不同服务商的配置方式不一样,遇到连接超时的情况,优先检查你的 API 地址配置是否正确、网络环境是否满足服务商的连接要求。

3.3 理解 Prompt 与 Function Calling

在写代码之前,有两个概念你得先明白。第一个是 Prompt 的角色设定,Agent 的系统提示词直接决定了它的行为风格和决策逻辑。比如你给 Agent 设定成"一个严谨的科研助手",它会更多调用数据查询工具;设定成"一个活泼的聊天伙伴",它可能就不太想调工具。

第二个是 Function Calling,这是 OpenAI 兼容接口提供的一种结构化能力。你把自己的工具函数用 JSON Schema 描述给模型,模型在需要调用工具的时候,不会直接输出一段文字让你去解析,而是输出一个结构化的 JSON,其中包含函数名和参数。你的代码收到这个 JSON 之后,执行对应的函数,再把结果作为新的消息喂回模型。

这里有个很关键的思路转变:Function Calling 的本质不是让模型执行代码,而是让模型决定"该调用哪个函数、参数是什么"。真正执行函数的是你的本地代码。模型输出的 JSON 只是它的"决策结果"。理解了这个,后面代码看起来就顺了。

4. 从 0 到 1 手写一个最小 Agent:完整代码跑通

4.1 ReAct 模式的核心逻辑

先不要急着看代码,我们把核心逻辑捋一遍。ReAct 是 Reasoning + Acting 的缩写,翻译成大白话就是"边想边做"。

一个最小 Agent 的主循环长这样:

  1. 把用户问题加入消息队列。
  2. 把整个消息队列发给大模型,带上所有工具的定义。
  3. 模型返回结果,分两种情况:
    • 结果里有工具调用的请求,那就解析出函数名和参数,执行本地函数,把结果作为新消息追加进队列,回到第 2 步。
    • 结果里没有工具调用请求,说明模型认为任务完成了,那这就是最终答案,退出循环。

这里每一步都值得细看。比如为什么要用消息队列而不是只发当前那一轮?因为 Agent 需要上下文记忆,它得知道之前调过什么工具、得到过什么结果,才能做出下一步决策。再比如工具返回的结果为什么是一条消息而不是直接改代码逻辑?为了把工具结果放进对话上下文里,模型才能"看见"自己行动的结果。

这个模式别看简单,它是几乎所有主流 Agent 框架的基石。LangChain 的 AgentExecutor、老版 AutoGPT 的核心循环,本质都在做这件事。

4.2 核心代码:60 行跑通最小闭环

下面这段代码是最小可运行版本,你复制到 main.py 就能跑。

import json from openai import OpenAI client = OpenAI( api_key="你的API_KEY", base_url="你的API服务地址", ) # 第一步:定义工具函数(真正执行的代码) def get_city_weather(city: str) -> str: """模拟天气查询,实际项目可替换成真实API调用""" weather_data = { "北京": "晴,25度", "上海": "多云,28度", "广州": "阵雨,30度", } return weather_data.get(city, f"暂无{city}的天气数据") # 第二步:用 JSON Schema 描述工具,让模型知道有这个函数可用 tools = [ { "type": "function", "function": { "name": "get_city_weather", "description": "查询指定城市的实时天气情况", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名,比如北京、上海", } }, "required": ["city"], }, }, } ] def run_agent(user_input: str, max_rounds: int = 5): # messages 就是 Agent 的"记忆" messages = [ {"role": "system", "content": "你是一个有用的助手,需要查询天气的时候调用工具,其他情况直接回答。"}, {"role": "user", "content": user_input}, ] for round_idx in range(max_rounds): # 把整个消息队列(含全部历史)发给模型 response = client.chat.completions.create( model="gpt-4o-mini", # 换成你实际使用的模型名 messages=messages, tools=tools, tool_choice="auto", ) msg = response.choices[0].message # 模型没有要求调用工具,说明任务完成,直接返回 if not msg.tool_calls: print(f"最终答案:{msg.content}") return msg.content # 模型要求调用工具:把模型的消息加入队列 messages.append(msg) # 逐个执行工具调用 for tool_call in msg.tool_calls: func_name = tool_call.function.name arguments = json.loads(tool_call.function.arguments) print(f"第{round_idx+1}轮:调用工具 {func_name},参数 {arguments}") # 在本地函数表里查找并执行 if func_name == "get_city_weather": result = get_city_weather(**arguments) else: result = f"未知工具: {func_name}" # 把工具执行结果作为消息追加到队列,给模型"看到" messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result, ensure_ascii=False), }) return "达到最大轮次,任务未完成" if __name__ == "__main__": run_agent("北京今天天气怎么样?适合出门跑步吗?")

这个代码你直接跑就能看到效果。过程会打印出每一轮的工具调用情况。注意看第二问"适合出门跑步吗",模型拿到天气数据之后,会结合"晴、25度"推理出"适合",然后给出最终答案。这就是 Agent 比普通聊天机器人强的地方:它真的拿到了数据,而不是编了一个答案。

4.3 关键代码逐行拆解

很多新手第一次看这段代码会有点懵,我逐块拆一下。

tools 列表是给模型看的"工具说明书",里面每一项描述了函数名、函数作用、参数类型和必填项。模型看了这个列表才知道你有什么工具可用、参数怎么传。这里有个细节:description 字段非常重要,写得不清楚模型就不会在正确的时候调用。比如你写"查询天气"和"查询指定城市的实时天气并返回温度和天气现象",后者明显更容易被模型理解和触发。

tool_choice="auto"表示让模型自己决定要不要调工具。你也可以设成 "none" 强制不调用,或者 "required" 强制必须调用,但日常用 auto 最合理。这里我建议新手可以调一下参数观察行为差异——把 tool_choice 改成 "none" 再跑一次,你会发现模型完全把天气数据当幻想了,直接开始自由发挥。

messages.append(msg)这一步很多人会漏。必须把模型返回的这条含 tool_calls 的消息追加进上下文,否则模型会忘记自己刚刚决定要调工具。后面再把工具执行结果也 append 进去,这样一个完整的"观察-思考-行动"闭环才建立起来。

有个很容易踩的坑是 role 字段。工具执行结果必须以 "role": "tool" 加入消息队列,并且要带上对应的 tool_call_id,这样模型才知道这个结果是为了响应哪一次工具调用。如果你写错 role 或者漏掉 tool_call_id,API 会直接报错。

4.4 实战进阶:增加代码执行工具和搜索工具

天气查询只是热身。真正让 Agent 变得强大的是给它接上代码执行和搜索能力。下面我们扩展 tools.py,添加两个更实用的工具。

# tools.py import json import subprocess import urllib.parse import urllib.request def run_python_code(code: str) -> str: """执行一段Python代码,返回标准输出""" try: result = subprocess.run( ["python3", "-c", code], capture_output=True, text=True, timeout=5, ) if result.returncode == 0: return result.stdout.strip() else: return f"执行报错:{result.stderr.strip()}" except subprocess.TimeoutExpired: return "执行超时(5秒限制)" def search_web(query: str) -> str: """调用公开搜索接口,返回前几条结果摘要""" try: base_url = "https://api.duckduckgo.com/" params = urllib.parse.urlencode({ "q": query, "format": "json", "no_html": "1", "skip_disambig": "1", }) req = urllib.request.Request(f"{base_url}?{params}", headers={ "User-Agent": "Mozilla/5.0" }) with urllib.request.urlopen(req, timeout=10) as resp: data = json.loads(resp.read().decode("utf-8")) abstract = data.get("AbstractText", "") related = data.get("RelatedTopics", [])[:3] lines = [f"摘要:{abstract}"] if abstract else [] for item in related: if "Text" in item: lines.append(f"- {item['Text']}") return "\n".join(lines) if lines else "没有搜索到相关结果" except Exception as e: return f"搜索失败:{str(e)}"

接入这两个工具之后,你的 Agent 能力边界一下子扩大了很多。比如你问它"帮我算一下 15 的阶乘",它不会直接编一个数字,而是生成一段 Python 代码,调用 run_python_code 去执行,再把真实结果返回给你。

如果你问它"最近 AI Agent 领域有什么新动态",它会调用 search_web 去搜索,然后把结果整理成一段摘要给你。这就是 Agent 从"聊天"到"干活"的质变。

不过要特别提醒:给 Agent 开放代码执行能力,等于给了它一把刀。如果你只在本地跑、处理自己的数据,风险可控;但如果做成了线上服务,必须加沙箱隔离、执行超时、资源限制。我这个 demo 里的 subprocess 实现只是教学用途,生产环境千万别直接照搬。

5. 实操中的高频问题与避坑手册

5.1 模型卡在循环里出不来

这是新手遇到最多的问题:Agent 调完工具,拿到结果,又继续调同一个工具,或者来回调两个工具,就是不输出最终答案。

出现这个问题的原因主要有三个。一是系统提示词没写清楚"什么时候该结束",比如你没告诉模型"任务完成后直接回复最终答案,不要再调用工具",模型就会倾向于一直调。解决办法是在 system prompt 里显式加一句"如果工具结果已经足够回答用户问题,请直接给出最终答案"。

二是工具返回的结果本身质量差,模型拿到的信息不足以做出决策,于是反复尝试。这时候要检查工具返回的文本是不是足够清晰。比如天气工具返回"晴,25度"就比返回"0"更容易让模型做决策。三是最大轮次限制设得太小,模型还没推理完就断了,但这种情况不会死循环,只是任务未完成。

我在项目里会同时设置最大轮次(比如 5~10 轮)和"相同工具连续调用次数限制"(比如同一个工具连续调用超过 3 次就强制停止),双重保险。

5.2 Token 成本比预期高很多

很多零基础同学第一次跑 Agent 的时候,会被 token 消耗吓了一跳。原因其实很简单:每一次工具调用之后,你要把整个历史消息队列重新发给模型,而且每轮还会追加模型回复和工具结果两段内容。轮次越多,上下文越长,成本呈线性甚至超线性增长。

控制成本的办法有几种。第一,能精简的上下文就精简:工具返回结果不用全量塞给模型,截取关键部分就行。比如搜索结果返回了 5000 字,你可以只保留前 500 字。第二,控制工具调用的粒度和频率:同一个 Agent 任务里,让模型先做规划再分批查询,而不是每问一句就调一次工具。第三,在开发调试阶段用便宜的小模型(比如 mini 版本),跑通逻辑之后再换强模型。

还有一个容易被忽略的点:模型的输出 token 也很贵。如果你发现 Agent 回复特别长,可以在 API 请求参数里设置 max_tokens 上限,既控制成本也防止模型话痨。

5.3 工具参数频繁传错或格式不对

Function Calling 虽然解决了解析问题,但模型仍然会传错参数。最典型的情况是:你定义的参数是 integer 类型,模型传了一个字符串 "3";或者参数是必填的,模型直接没传。

这个问题的根源在于你的 JSON Schema 写得不够"严格"。我调试过很多次之后总结出几个经验。一是描述的措辞要具体,要告诉模型"这里必须是整数"、"如果用户没有明确指定城市,询问后再调用工具,不要猜"。二是如果工具参数有默认值,就把 optional 的定义写清楚。三是要有容错逻辑:函数执行之前先做类型校验,不合法就返回一个明确的错误信息,比如"参数 city 缺失或格式不正确,请确认后再调用"。

千万不要假设模型每次都会完美地传参,代码里一定要做健壮性检查。

5.4 国内大模型服务的兼容性问题汇总

如果你的 API 服务是国内大模型厂商提供的,大概率走的是 OpenAI 兼容接口,但细节上会有差异。我实测下来最常见的有三类。

第一,有些国产模型对 Function Calling 的支持是通过一个额外的"工具描述解析层"实现的,实际 API 的请求格式会有微调,最稳妥的做法是找到你所用服务商的文档,看它要求的 tools 字段格式。第二,部分模型的 tool 调用结果需要放在特定的消息角色里,跟 OpenAI 标准格式不完全一样,比如有的需要 "role": "function" 而不是 "role": "tool"。第三,部分国产模型对 system prompt 的优先级处理跟 OpenAI 不同,如果你的 Agent 行为不符合预期,优先排查这一块。

所以我建议你写代码的时候,把模型接入逻辑稍微抽象一下,不要把所有细节都写死在 main.py 里。这样以后换模型或者换服务商,只改配置就好了。

5.5 高频提问速查表

现象最可能的原因解决办法
Agent 不调用工具,直接编答案tool_choice 设置不对 / 工具描述不清晰确认 tool_choice 为 "auto",优化工具 description
工具被调用了但报错参数格式不符合函数签名检查 JSON Schema 参数类型,函数入口增加校验
循环调工具不停缺少停止条件 / 工具结果不充分在 system prompt 明确停止条件,增加最大轮次限制
模型回复特别长输出 token 没限制设置 max_tokens,精简 system prompt
上下文太长导致费用高历史消息累积过多截断工具返回结果,减少不必要的工具调用轮次
API 返回格式解析不了不同服务商兼容性差异查对应文档,调整消息角色或 tools 格式

6. 从最小 Demo 走向真实项目:我的扩展建议

6.1 增加长期记忆层:让 Agent 记住之前的交互

简单 Agent 的上下文只在当前会话里有效,一重启就全忘了。真实项目里你需要让 Agent 具备"记忆"。这个记忆不需要很复杂,最简单的方案是引入一个记忆文件或者数据库表,每次对话结束后把关键信息提取出来存进去,下次对话开始的时候把相关记忆注入 system prompt。

比如我的日程 Agent,会把用户说过的"我每周三上午有例会"提取成结构化记录。下次用户说"帮我把会议推迟一小时",Agent 就能从记忆里找到周三例会这个日程,然后调用日历工具做修改。没有记忆层的 Agent 根本做不到这件事。

6.2 用 Agent 中台或者框架做工程化

一旦你的 Agent 从"能跑"走向"能用",就要考虑工程化问题了。这时候再用纯手写的方式就有点吃力了,你需要引入框架或者公司内部的 Agent 中台。

框架层面,LangGraph 的状态图很适合管理复杂流程,CrewAI 适合做多 Agent 协作;平台层面,Dify 之类的工具可以做可视化管理。但无论用哪个,前面手写循环打下的基础都会让你理解得更透彻,遇到框架的报错你能一眼看出是状态管理问题还是工具调度问题。

6.3 我的个人经验:学习路径上的三个不要

最后说几个我实战下来的体会。第一个是不要贪多,别一上来就搞多 Agent 协作、知识库增强、流式输出这些花活,先把一个最小 Agent 跑得非常熟练,再逐步加功能。第二个是不要只抄代码,把核心循环的逻辑讲给别人听,讲不明白就说明没掌握。第三个是不要怕报错,Agent 的调试过程本身就是学习过程,我第一次跑通这个循环的时候,前前后后改了两个多小时,但理解深度远超看十小时视频教程。

学习 AI Agent 这件事,最关键的从来不在于你看了多少资料,而在于你有没有亲手把那个循环跑通。哪怕你只实现了天气查询这一个工具,跑通的那一瞬间,你脑子里对 Agent 的理解就会发生质变。剩下的路,就顺了。

返回列表