☰
从零构建AI Agent:ReAct范式与工具调用入门实战教程
2026/10/21 5:35:20 网站建设 项目流程

程序员必看!从零构建你的第一个AI Agent:超详细入门教程(建议收藏)

这几年AI发展速度大家有目共睹,各种Agent产品层出不穷,不少朋友看着热闹,也想动手搞一个属于自己的AI Agent,但总觉得门槛高、不知道从哪下手。这篇教程就是想帮你把这层窗户纸捅破。

我会从一个非常基础的“能调用外部工具的AI助手”开始,带你走完环境准备、代码实现、问题排查的完整流程。整个项目不需要你有一线算法工程师的水平,只要熟悉Python基础语法,能理解“函数是什么、字典怎么用”,就能跟下来。顺便说一句,这个示例不是那种只能聊天的玩具,具备多轮对话记忆和实时工具调用能力,跑通之后可以直接作为骨架,扩展你自己的各类需求。

1. 先把思路理清楚:Agent到底是个什么东西

1.1 拆解标题背后的三个关键词

说到AI Agent,很多人觉得这是个很玄乎的概念。其实剥开外壳看,它的核心就三件事:大模型负责“想”、外部工具负责“做”、代码逻辑负责“串”。

大模型本身只是一个“文本预测器”,你问它“今天北京的天气怎么样”,它能给出看起来很通顺的回答,但如果没接入实时数据,它只能根据训练时学到的知识去猜,可能给你个半真不假的答案。而Agent要做的事情,是让模型在回答之前,先判断“这个问题需不需要查资料”“该调用哪个API来获取信息”,然后真正去执行,拿到结果后再组织语言回复你。

这个“判断—执行—总结”的闭环,就是Agent区别于普通聊天机器人的本质所在。我们这篇教程里要构建的,就是一个拥有关键步骤的最小Agent:它能听懂你的自然语言指令,自主决定调用哪些工具,拿到工具返回的结果后再继续对话。

1.2 为什么主流的Agent都选“ReAct”这个范式

真正动手之前,你必须要了解一个概念叫ReAct(Reasoning + Acting)。这是目前绝大多数Agent产品的底层逻辑。

ReAct的核心思想,简单说就是让模型在回答问题时,先把思考过程“说出来”,再根据思考结果决定下一步动作。类比一下就是:你让一个实习生去调研市场,他不会直接甩给你一份几千字的报告,而是会先跟你汇报——“我准备先查行业报告,再整理竞品信息,然后汇总”,你批准后他才动手。ReAct就是把这个工作模式复刻到了AI上。

具体到代码层面,ReAct循环长这样:

  • 模型收到用户输入;
  • 模型开始“思考”,输出一段推理过程,说明它打算怎么做;
  • 如果需要调用工具,模型会输出一个特定格式的JSON,包含工具名和参数;
  • 我们的程序解析这个JSON,在实际环境里执行工具函数,把结果返回给模型;
  • 模型看到结果后,继续思考、直到它认为可以输出最终答案。

你可能会问,为什么不直接在代码里写死“遇到天气问题就调用天气API”?因为那样做你写的是规则系统,换个问法就失灵了。而ReAct模式等于把决策权交给了模型,模型可以灵活地选择不同工具来组合解决问题,这才是Agent的“智能”所在。

2. 环境准备与模型选型:选对工具才能事半功倍

2.1 本地开发环境的快速搭法

写Agent代码不需要什么重型IDE,也不用搭分布式集群,一台普通电脑就足够。我这里用的是Python,因为生态最全、示例最多,你换别的语言也完全可以,只是某些库的成熟度不如Python。

建议你用Python 3.10以上版本。先建一个干净的虚拟环境,避免把全局环境搞得乱七八糟。我用的是uv这个工具,比传统venv+pip快很多,一条命令就搞定:

# 安装uv(macOS/Linux) curl -LsSf https://astral.sh/uv/install.sh | sh # Windows PowerShell powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

然后用它初始化项目:

uv init agent-demo cd agent-demo uv add openai python-dotenv

这里直接用了OpenAI的Python SDK。虽然很多人一听OpenAI就想到某个固定的服务商,其实这个SDK是个通用客户端,支持几乎所有兼容接口的大模型服务。你只需要改一下base_url和api_key,就能切换到各种模型平台,非常方便。

2.2 关于模型选择的几句大实话

做Agent,模型选型直接决定天花板。

我给初学者的建议是:优先选带“工具调用”能力的主流商业模型,而不是本地部署的开源小模型。原因很简单,Agent需要模型在输出里“夹带”结构化JSON,这对模型的指令跟随能力要求很高。小参数模型经常犯的毛病是——你要它输出JSON,它偏要跟你聊天,然后在JSON里写上一大段废话,解析直接报错。你可能会说,那我用提示词强约束不行吗?行,但你得花大量时间调提示词、调解析器,最后效果还不一定稳。我在本地试过用几个开源模型跑同样一套Agent代码,有些模型十次里有七八次工具调用格式是对的,有些模型五次里能错三次,这个稳定性差距用起来非常明显。

如果你有预算顾虑,可以选性价比高的模型,但一定要确认它支持函数调用或工具调用(tool calling)功能。另外,推理类模型(也就是带“思考过程”的模型)在Agent场景下有天然优势,因为它们长于规划,能自己把多步任务拆解好。

下面是几个我实测过的选型维度,做成表格给你参考:

维度要关注的细节我的建议
工具调用能力是否支持原生tool calling不支持的一票否决
上下文长度至少要能容纳工具返回的长文本建议选64K以上的
价格按次调用成本开发调试期选便宜的,上线再看情况
稳定性输出格式是否容易被带偏拿同一条测试用例跑20遍看成功率

3. 核心代码逐行拆解:教你搭建一个能调工具的最小Agent

3.1 先看整体文件结构

我不太喜欢把代码全堆在一个文件里,但第一次跑通时越简单越好,所以这里用一个主文件加一个工具文件就够了。等你理解全貌之后,再拆分成多模块也不迟。

agent-demo/ ├── agent.py # 主程序:对话循环 + ReAct核心逻辑 ├── tools.py # 工具函数集合(目前只有一个查询时间的工具) ├── .env # 存放API密钥(注意不要提交到git) └── requirements.txt # 依赖清单(用uv add生成即可)

工具是什么?对于Agent来说,工具就是一个普通Python函数。你把这个函数的名字、描述、参数格式告诉模型,模型就会在需要的时候“请求”调用它。你的程序收到这个请求后,去执行同名函数,再把返回值塞回给模型。就这么简单,不需要什么魔法。

3.2 写第一个工具函数

我选择先写一个“获取当前服务器时间”的工具。这个功能麻雀虽小五脏俱全,能完整展示工具定义、注册、调用的全链路。

在tools.py里写上:

from datetime import datetime def get_current_time(timezone: str = "Asia/Shanghai") -> str: """获取指定时区的当前时间。 参数: timezone: 时区名称,例如 Asia/Shanghai 或 UTC 返回: 格式化后的当前时间字符串,例如 2025-06-01 14:30:00 """ # 这里简化处理:Python标准库不带完整时区数据库,实际项目可以用zoneinfo now = datetime.now() return now.strftime("%Y-%m-%d %H:%M:%S")

然后定义一个TOOLS列表,这个是给模型看的“工具说明书”:

TOOLS = [ { "type": "function", "function": { "name": "get_current_time", "description": "获取当前服务器时间,当用户询问时间、日期、今天是几号时使用。", "parameters": { "type": "object", "properties": { "timezone": { "type": "string", "description": "时区名称,默认Asia/Shanghai", }, }, "required": [], }, }, } ]

这里有个非常关键的细节:工具描述写得越具体,模型调用得越准。你写“获取当前服务器时间”也行,但如果加上“当用户询问时间、日期、今天是几号时使用”这种触发条件,模型就知道什么时候该调用、什么时候不该调用。你可能会问,描述写太长会不会浪费token?会,但换来的是准确率的大幅提升,这笔账值得算。

3.3 实现Agent主循环

接下来是重头戏,在agent.py里实现ReAct循环。我会把代码拆成几块来讲解。

首先是最基础的客户端初始化和环境变量加载:

import json import os from dotenv import load_dotenv from openai import OpenAI from tools import TOOLS, get_current_time load_dotenv() client = OpenAI( api_key=os.getenv("MODEL_API_KEY"), base_url=os.getenv("MODEL_BASE_URL"), ) MODEL_NAME = os.getenv("MODEL_NAME", "你的模型标识")

这里要说明一下.env文件的内容:

MODEL_API_KEY=你的密钥 MODEL_BASE_URL=https://你的网关地址/v1 MODEL_NAME=你的模型标识

注意base_url末尾的/v1不是随便写的,OpenAI SDK会在你给的base_url后面拼接/chat/completions等路径,如果你的网关地址不带/v1,请求就会404。

然后是对话历史管理。为了支持多轮对话,我们把历史消息存在一个Python列表里:

messages = [ { "role": "system", "content": "你是一个有用的助手,可以使用工具来帮助用户解答问题。" "当用户的问题需要实时信息时,请调用合适的工具。" "在调用工具之前,简要说明你的计划。" } ]

系统提示词这块,我劝你多花点心思。它相当于给Agent定人设和工作规范,后面你遇到模型“该调工具时不调”“不该调时乱调”的问题,八成都是系统提示词没写明白。

接下来是核心循环。这里我用一个while循环,内置最大轮次限制,防止模型陷入无限循环:

def run_agent(user_input: str, max_iterations: int = 5): messages.append({"role": "user", "content": user_input}) for step in range(max_iterations): response = client.chat.completions.create( model=MODEL_NAME, messages=messages, tools=TOOLS, tool_choice="auto", ) message = response.choices[0].message # 情况一:模型没有请求调用工具,直接返回最终回答 if not message.tool_calls: print("AI:", message.content) messages.append({"role": "assistant", "content": message.content}) return message.content # 情况二:模型请求调用一个或多个工具 messages.append(message) # 把带有tool_calls的assistant消息保存下来 for tool_call in message.tool_calls: function_name = tool_call.function.name function_args = json.loads(tool_call.function.arguments) print(f"[工具调用] {function_name}({function_args})") # 根据名字找到对应函数 if function_name == "get_current_time": result = get_current_time(**function_args) else: result = "未找到该工具" # 把工具结果返回给模型,注意role必须是tool messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result, }) # 超出最大迭代次数时强制返回 print("AI: 抱歉,这个问题需要的步骤太多了,我先告诉你目前的进展。")

这段代码浓缩了Agent的全部精髓。把它跑起来之后,你问“现在几点了”,模型会先输出一个工具调用请求,你的程序执行get_current_time,把时间字符串拿回来,再扔给模型,模型最后组织语言告诉你答案。这个流程看着简单,但每一步都有它存在的必要性。

3.4 把主流程跑起来

在agent.py末尾加上一个简易的交互界面:

if __name__ == "__main__": print("Agent已启动,输入'退出'结束对话。") while True: user_input = input("你: ") if user_input.strip() == "退出": break run_agent(user_input)

然后运行:

uv run python agent.py

对话效果大致是这样:

你: 现在几点了? [工具调用] get_current_time({'timezone': 'Asia/Shanghai'}) AI: 现在是北京时间2025年6月1日14:30:22。

看到[工具调用]这一行输出了吧?这就是Agent在自主决策调用工具的铁证。你什么都不用做,模型自己就知道该去调用工具、用什么参数,然后基于返回结果作答。

4. 不要只满足于跑通:从演示到可用的进阶路线

4.1 给Agent加一个“查天气”工具

现在你应该已经理解了“工具”的本质就是普通函数加一段描述。我再带你扩展一个真实场景——查天气,顺便看看同一个Agent如何管理多个工具。

我们需要一个能获取城市天气的工具,这里我先给出模拟数据的版本,方便你本地测试:

import random def get_weather(city: str) -> str: """获取指定城市的实时天气情况(模拟数据)。""" weather_conditions = ["晴", "多云", "小雨", "阴"] condition = random.choice(weather_conditions) temperature = random.randint(15, 32) return f"城市: {city},天气: {condition},气温: {temperature}℃"

然后把TOOLS列表扩展开,加上这个工具的定义。注意这里的description我写得更细致——不要只写“获取天气”,要告诉模型“用户询问天气、气温、是否下雨等情况时使用”,这样模型才能精准派发。

重新运行后,你会发现出个问题:模型可能会一次请求两个工具——一个查时间、一个查天气,我们的代码里已经有for tool_call in message.tool_calls:这个循环了,所以天然支持多工具并行调用,一步到位。这个设计不是我随手写的,而是实测后加的,否则模型一批请求多个工具时你会漏掉一半,然后模型看到缺失的结果就会开始编答案。

4.2 工具结果不是给你看的,是给模型看的

这里有个很多新手会忽略的认知:工具函数的返回值,是给模型继续推理用的“原材料”,不一定是给用户的最终答案。

比如get_current_time返回的是2025-06-01 14:30:22这一串字符串,用户并不能直接看到,他看到的最终内容是模型根据这个时间重新组织语言后的结果:“现在是北京时间2025年6月1日14时30分。”模型可能会加上“北京”这种上下文,也可能补充一句“今天是周六,工作日哦”,这才是Agent体验好的原因。

所以你在写工具函数时,返回值不一定要“漂亮”,关键是结构清晰、信息完整。比如天气工具返回JSON而不是一段话,模型反而更容易从中提取关键字段:

return json.dumps({ "city": city, "condition": condition, "temperature": temperature, "humidity": random.randint(40, 90), })

4.3 上下文管理:给Ag摸摸知识库的边界

跑过几轮之后,你一定会碰到一个问题:对话长了,模型开始“失忆”。这既不是模型变笨了,也不是你的代码有bug,而是上下文窗口塞满了。

每个模型都有最大上下文长度。你的每一轮对话、每个工具返回结果、每段历史总结,都会占用这个窗口。一旦超出,老的信息就被“截断”或“压缩”掉,模型自然忘了你最开始说了什么。

处理这个问题有三种策略,我建议按顺序做:

  • 滑动窗口:只保留最近N轮对话,最老的对话直接丢掉。实现最简单,但可能丢失关键信息。
  • 摘要替代:把超过窗口的老对话实时交给模型作总结,再以摘要形式存起来。信息密度高,但会增加一次额外调用。
  • RAG检索:把历史对话向量化存储,每次根据当前问题检索相关片段注入。最正规,但工程量大,不适合入门项目。

这个最小Agent里我建议先上滑动窗口,比如只保留最近8轮。在run_agent函数里加一个限制逻辑:

if len(messages) > 20: # 留下system消息,再保留最近18条 messages[:] = [messages[0]] + messages[-18:]

注意一点:在做裁剪的时候,必须保证消息边界完整,不能把一条tool消息截掉了而它的assistant请求消息还在,这会导致API校验报错。

5. 踩坑记录与排查技巧:实测中这些坑你大概率也会遇到

5.1 工具调用JSON解析失败

这是最高频的问题,没有之一。模型返回的tool_call.function.arguments应该是一个合法JSON字符串,但偶尔模型会生成类似于{"timezone": "Asia/Shanghai"这种残缺内容,或者多个JSON拼接在一起。

我的处理办法是在解析时加上异常兜底:

try: function_args = json.loads(tool_call.function.arguments) except json.JSONDecodeError: # 尝试修复常见错误:去掉多余逗号、补全花括号 raw = tool_call.function.arguments.strip() if not raw.endswith("}"): raw += "}" function_args = json.loads(raw)

但说句实话,这种补丁治标不治本。如果某个模型频繁产生格式错误,最好的解决办法是换模型,而不是无限打补丁。同样的代码,不同模型的输出稳定性差距能到十倍以上,这种差距不是提示词能拉平的。

5.2 模型“上头”了:陷入工具调用死循环

有时候模型会反复调用同一个工具,比如查了八次时间还不肯给出最终答复。原因可能是工具返回的内容没满足它的预期,也可能是系统提示词没告诉它“拿到结果后就可以作答了”。

我调试时会在系统提示词里加一句关键约束:

当你已经通过工具获得了足够的信息来回答用户的问题时,必须直接给出最终答案,不要再继续调用工具。

在代码层面,max_iterations这个参数就是最后的保险。初学者一定要设置这个限制,不然后台会跑出一长串无意义的调用记录,消耗的token让人肉疼。

5.3 提示token用量暴涨

很多人第一次跑Agent都会被账单吓一跳。一次简单的“现在几点”查询,看着没多少内容,实际消耗的token可能比你想象中多好几倍,原因在于每个工具定义都要跟着每次请求一起发送。

缓解办法有三个:

  • 工具描述写得精炼一点,不要太啰嗦;
  • 尽量用带上下文缓存能力的模型服务,重复的系统提示和工具定义不会重复计费;
  • 开发调试期把温度和模型版本都调成最便宜的配置,跑通了再上“满配”。

我用过一个比较极端的例子:某次测试时,工具定义写得特别详细,光描述就占了两千多token,这时候每次普通对话的固定开销就是两千多token,等于说还没开始干活就已经花了不少钱。从那之后我写工具描述的原则就是——能一句话说清的绝不放两句话。

5.4 工具安全边界要留个心眼

这个最小Agent里的工具都是无副作用的“读操作”,但如果你扩展成“发邮件”“删文件”之类的强力工具,就必须要考虑防护。

我强烈建议你在正式使用前,加一层“人工确认”机制:当Agent请求调用敏感工具时,程序先打印出参数,要求用户输入y确认后才真正执行。代码改动很小,但能避免很多不可挽回的后果。千万别高估模型对参数的判断力,你真的不知道它会哪天用rm -rf去清空你的文件系统,这算是AI编程里最值得刻在脑门上的教训之一。

5.5 别把所有逻辑都塞给模型

这是最后一个,也是最容易犯的思维误区。刚学会写Agent的人,很容易把一切问题都交给模型“思考”,结果就是模型在那边一本正经地胡编乱造。

我自己的分工原则是:能写代码判断的逻辑,就别让模型判断。比如,判断用户输入的语种、检测参数范围、解析特定格式数据,这些都是确定性逻辑,用代码处理又快又准。模型只负责“规划决策”和“语言生成”这两类机器做不了的事。这样分工后,你的Agent会稳得多、省得多,也更容易维护。

写在最后的几点个人体会

把第一个Agent跑通的感觉,确实很像学会骑自行车——一开始摇摇晃晃,但一旦掌握住了,后面所有扩展都变得顺理成章。我在做了这个Demo之后,陆续给它加了定时任务、文件搜索、网页内容摘要这几个工具,整个架构几乎没有改动,只是不断往工具列表里“塞”新函数,这是个很奇妙的体验。

几个我觉得特别值得记住的结论:模型选型决定了你的体验下限;提示词和工具描述决定了调用准确率;工程设计决定了能不能规模化;安全边界决定了你敢不敢真正用起来。按照这篇教程走一遍,你已经把Agent最核心的东西拿到手了。接下来把它做成什么样子,就看你的想象力了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询