news 2026/10/17 8:42:03

从零手写一个智能体:用Python和大模型API实现ReAct循环与工具调用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零手写一个智能体:用Python和大模型API实现ReAct循环与工具调用

1. 别被"智能体"三个字吓住:它没那么玄

最近身边不少朋友都在聊智能体,什么工作流编排、多智能体协作、自主规划任务,听起来特别唬人。我一开始也以为那得是算法工程师才能碰的东西,直到自己动手写了一个最简版本,才发现——所谓智能体,本质上就是一个"会自己调用工具的大模型包装层"。这层包装做的事情概括起来就三件:

  1. 把用户的任务和可用的工具信息一起丢给大模型。
  2. 大模型返回"我要调用哪个工具、传什么参数"。
  3. 程序去执行这个工具,把结果再丢给大模型,循环往复,直到大模型认为任务完成。

说白了,智能体没有神秘魔法,它就是一个"思考—行动—观察"的循环。这个循环跑起来的基础设施,不过是一个大模型API、一小段调度代码和几个普通Python函数。

这篇内容适合完全没碰过智能体、但有一定Python基础的朋友。你不需要懂强化学习,不需要会搭向量数据库,甚至不需要GPU。只要会安装Python包、会写基本函数、有任意一家服务商的大模型API Key,跟着我上面的思路走一遍,你就能在5分钟内拥有自己的第一个智能体。

我写的这个最小版本不依赖任何智能体框架,直接用代码把循环逻辑讲透。原因是:如果一开始就上框架,很多细节会被封装藏起来,你只是"调通了"但没"懂得"。先用裸代码跑通,再去看框架,你的视角会完全不一样。

2. 动手前先弄明白:智能体的"大脑"是怎么思考的

2.1 智能体和大模型聊天的本质区别

普通的大模型聊天是"你说一句,它回一句",所有上下文都在对话里,模型本身没有能力去"查询天气"或"执行代码"——它只能通过训练数据里的知识来推断,而知识是有截止日期的,也没有实时性。

智能体的解法是:让大模型不再直接给出最终答案,而是先输出一个"意图"和"参数",比如:

"我需要查询武汉今天的天气,调用工具get_weather,参数city=武汉。"

然后由程序接管,去调用真实的API获取数据,再把真实数据作为新的上下文喂回给大模型。大模型看到真实天气数据之后,再组织语言回答用户。

这就是最核心的差异:大模型负责"想",外部代码负责"做"。想和做之间,靠一个结构化的、统一的对话格式来串联。

2.2 ReAct循环:为什么大模型能自己决定下一步

这个循环在学术圈有个经典叫法,ReAct,也就是Reasoning and Acting,推理和行动交替进行。每一步,大模型都被要求输出它的"思考过程"和"行动方案",程序解析方案并执行,再把结果作为新的观察继续输入。如此一步一步逼近目标。

我自己的理解是:ReAct相当于给大模型装上了一副"手"和"眼睛"。原本它只能动嘴输出文字,现在它能动手调用工具,并且能看到工具返回的结果,再基于结果决定下一步动作。这就像你做饭时不光有菜谱,还能随时打开锅盖看看食材熟透了没有,然后决定是继续炖还是关火。

理解了这层机制,你就能明白为什么智能体更像一个"行为决策系统"而不是一个"聊天机器人"——它的工作方式是决策、行动、反馈、再决策。

2.3 打造最简调度器的核心:系统提示词和设备契约

要让上面这套循环跑起来,关键在于设计好对话格式。大模型并不知道"什么时候该调用工具",它只知道"你让我输出JSON,我就输出JSON"。所以系统提示词里需要写清楚:

  • 你是智能体助理,可以调用以下工具。
  • 当你需要调用工具时,严格按下列JSON格式输出,不要输出任何其他内容。
  • 当问题答案可以直接给出时,直接回复用户。

这一段就是"设备契约"。如果契约写得模糊,大模型就会给你输出各种乱七八糟的格式。如果契约写得清楚,大模型会规规矩矩地按JSON输出,你的解析代码就能非常省事。

3. 5分钟跑通第一个智能体:环境准备与最小实现

3.1 需要准备的东西

先用表格列一下你需要准备的环境和工具,不用多,也不需要复杂:

准备项说明备注
Python 3.9+运行环境建议用虚拟环境,避免污染系统Python
openai 或 anthropic SDK调用大模型如果你的服务商提供兼容接口,通用SDK也能搞定
大模型 API Key模型访问凭证任选支持函数调用或工具调用的大模型
requests 库调用简单外部接口用于给智能体挂工具时使用

我自己的习惯是先用一个"不会在环境问题上卡住"的服务商。很多云厂商都提供OpenAI兼容的接口地址,你在SDK里改base_url和api_key就行。这个方案适合国内网络环境,也适合只想快速体验的人。

3.2 核心循环代码:一个只有几十行的智能体内核

我不推荐一上来就安装LangChain之类的框架,先把下面这段精简的循环代码跑通。我自己在本地跑的时候,最简版本大约六十行:

import json from openai import OpenAI client = OpenAI( base_url="你的兼容接口地址", api_key="你的API Key" ) tools = [ { "type": "function", "function": { "name": "get_current_time", "description": "获取当前日期和时间", "parameters": { "type": "object", "properties": {}, "required": [] } } } ] def get_current_time(): from datetime import datetime return datetime.now().strftime("%Y-%m-%d %H:%M:%S") tool_map = { "get_current_time": get_current_time } messages = [ { "role": "system", "content": ( "你是智能体助理。你可以调用以下工具:\n" "1. get_current_time:获取当前日期和时间。\n" "当需要调用工具时,只输出JSON格式的调用,不要输出其他内容;" "当无需调用工具时,直接回答用户。" ) } ] def agent_run(user_input, max_steps=5): messages.append({"role": "user", "content": user_input}) for step in range(max_steps): response = client.chat.completions.create( model="你的模型名", messages=messages, tools=tools, tool_choice="auto", ) msg = response.choices[0].message if msg.tool_calls: for call in msg.tool_calls: fn_name = call.function.name fn_args = json.loads(call.function.arguments if call.function.arguments else "{}") print(f"[Step {step+1}] 调用工具: {fn_name}, 参数: {fn_args}") result = tool_map[fn_name](**fn_args) messages.append({ "role": "tool", "tool_call_id": call.id, "content": json.dumps(result, ensure_ascii=False) }) else: messages.append({"role": "assistant", "content": msg.content}) return msg.content if __name__ == "__main__": print(agent_run("现在几点?"))

这代码有几个要点需要解释:

第一,tools参数不是给大模型看的文档,而是给模型选择函数的"菜单"。模型拿到这个菜单后,如果它确实需要调用工具,就会在返回内容里带上一个tool_calls结构,里面包含函数名和参数字符串。

第二,我特意没用response = response.choices[0].message这一行直接结束循环,因为当模型调用工具时,msg.tool_calls不是空值,我们需要把每条工具结果都追加进messages,然后继续请求模型,让模型看到工具结果后给出最终回复。

第三,tool_choice="auto"的意思是让模型自己决定是否调用工具。你也可以设置成"required",强制模型必须调工具,但那样在多轮对话里很容易出问题,日常用auto最省心。

3.3 第一代"只回答时间"的智能体:跑通后的感受

我第一次跑通这个版本的时候,说实话有点失望,因为结果太简单了,就回了一句"当前时间是2025年3月某日"。但转念一想,这个简单的示例背后,其实已经跑通了完整链路:

用户输入 → 模型判断要调工具 → 程序执行真实函数 → 结果回填 → 模型组织最终回答。

你可以在控制台看到中间那行"调用工具: get_current_time"的日志,那一刻你会真正理解什么是智能体的"行动"——模型没有真正去读系统时钟,它只是发出了调用命令,真正读取时钟的是你写的那个函数。

这个版本虽然简单,但它是后续一切的基础。就像写Hello World一样,跑通了这个最小闭环,你对智能体的理解已经从"听过概念"变成"亲手握住了原理"。

4. 加一个真实的工具函数:从"玩具版"到"可用版"

4.1 为什么说"工具函数"才是智能体的真正肌肉

第一个版本能回答时间,但本质上不过是一个带了一点调用的聊天机器人。真正让它具备实用价值的,是你给它挂载的工具。工具的范围可以很广:

  • 调用天气API查询实时天气。
  • 调用数据库查询订单状态。
  • 搜索本地知识库。
  • 执行一段Python代码做计算。

我给这套骨架加了两个常用工具:calculate(计算器)和get_city_weather(天气查询)。计算器是直接执行表达式,天气查询用requests请求公开接口,如果接口挂掉,就返回一个默认文案。

关键在于:每个工具函数本身只是一个普通的Python函数,但它写给大模型看的description,直接决定了模型能不能准确理解"什么时候该用它"。很多教程不强调description的重要性,但我实际测试后才发现,description写得好不好,效果差很多。

4.2 工具描述(description)的重要性:测试对比

我做了两组对比测试,同一个模型,同一个任务"北京今天多少度",第一组的get_city_weather描述写的是"获取城市天气",第二组写的是"根据城市名称获取实时天气温度,城市用中文传入,适合回答天气查询类问题"。实际结果:

  • 第一组:模型偶尔会不调用工具,直接凭训练数据里的记忆回答"北京今天大约XX度",结果不准确,甚至有的模型会编一个数字。
  • 第二组:模型稳定触发工具调用,返回真实天气数据后再组织回答。

这就是为什么我建议在函数定义里把description当作"使用说明书"来写。你把使用条件、参数格式、适合场景说明白,模型的判断准确率立刻上一个台阶。

4.3 我再给你看一段完整扩展后的代码骨架

import requests import json def calculate(expression: str): """执行数学计算表达式""" try: result = eval(expression, {"__builtins__": {}}, {}) return {"result": result} except Exception as e: return {"error": str(e)} def get_city_weather(city: str): """查询城市实时天气,城市用中文传入""" try: url = "https://mockapi.example.com/weather" resp = requests.get(url, params={"city": city}, timeout=5) data = resp.json() return {"city": city, "temperature": data["temp"], "weather": data["desc"]} except Exception: return {"city": city, "temperature": "未知", "weather": "接口暂不可用"} tools = [ { "type": "function", "function": { "name": "calculate", "description": "执行数学计算表达式,适合处理加减乘除、乘方等数学问题", "parameters": { "type": "object", "properties": { "expression": {"type": "string", "description": "数学表达式字符串"} }, "required": ["expression"] } } }, # 另一个工具的schema类似,不重复写 ] tool_map = { "calculate": calculate, "get_city_weather": get_city_weather }

这里要提醒一句:我故意用eval做计算器,只是演示方便。你在自己的项目里如果是处理不可信输入,千万不要用eval,应该用ast.literal_eval或者更安全的计算库。安全问题的坑不要等踩到才懂。

4.4 多工具选择场景下的模型决策逻辑

挂载多个工具之后,你可能会好奇:模型到底怎么知道该用哪个工具?实际上模型并不是真的"理解"你的Python函数,它只是根据工具描述和用户问题之间的语义相似度来做选择。

比如用户问"230乘以14.5等于多少",模型看到calculate的描述里有"数学计算"字样,就触发了这个工具。比如用户问"武汉现在热不热",模型看到get_city_weather的描述里有"天气温度"字样,就触发了天气工具。

但要注意,模型的触发并不总是准的。如果描述含糊,它可能把数学题误判成天气查询。所以当工具数量变多时,给每个工具写清楚"适用边界"就更加重要。比如"只处理纯数学计算,不处理单位换算、汇率换算"之类的话,就比一句笼统的"计算数学表达式"要更容易让模型做对决策。

5. 智能体跑不稳的几个常见坑:我踩过的你别踩

5.1 频繁触发工具导致死循环

第一个容易踩的坑是死循环。你给max_steps设了5,但如果模型每次都返回一个工具调用,而且调用的结果又被模型继续视为"需要再调工具",循环就永远走不完。最常见的一种情况是:

模型调用了某个工具,程序执行并返回结果,模型看着结果又调用同一个工具,比如"查询时间"然后又是"查询时间"。

这个问题常见于模型对"什么时候该停"理解不足。解决办法是:系统性约束模型"只要工具结果足以回答用户问题,必须直接输出答案并停止"。还可以在代码里加一个max_steps的硬性上限,超过就直接返回当前结果。

5.2 工具结果格式不统一导致的报错

第二个坑是工具返回结果的格式五花八门。有的函数返回dict,有的直接返回字符串,有的返回一个数组。你把这种混合格式塞进messages里,模型虽然能勉强解析,但偶尔会理解错。

我的做法是:所有工具函数都统一返回dict,并且用JSON序列化后再塞入messages。这样模型看到的一定是结构清晰的数据,格式统一,不容易出幻觉。

另外,工具函数本身也要做异常兜底。假设天气接口超时了,你要让函数返回一个"接口暂不可用"的dict,而不是直接抛异常导致整个循环崩溃。在智能体这个场景里,容错比正确性更重要——因为模型会拿你给的结果继续编后续内容,一旦程序崩了,整个对话就断了。

5.3 API接口不支持工具调用参数

第三个坑更隐蔽。并不是所有大模型API都支持tools这个参数。如果你用的服务商API文档里没有tools字段,你传了这个参数,服务端要么报错,要么直接忽略。

遇到这种情况,可以退回到"纯文本JSON约定"方案:不传tools参数,而是把工具说明直接写进系统提示词,要求模型输出固定JSON格式。代码里解析JSON来判断是否调用工具。这样做虽然少了一些结构化的可靠性,但兼容性更好,能在不支持tools的模型上跑通智能体。

5.4 上下文越来越长,费用越来越高

最后提醒一个被很多人忽视的坑:每一轮工具调用,都会把所有对话历史重新发给大模型,而对话历史会越积越长。在一个跑了很多步骤的智能体任务里,调用成本会肉眼可见地涨。

缓解办法有两个方向:一是把每轮的工具结果精简,只保留关键字段,不要在返回里塞一大段原始数据;二是尽量缩短任务本身,把复杂的任务拆成多个小任务,每个小任务独立调用,而不是一轮对话里无限叠加。我自己在做一个项目时,把返回天气结果从完整JSON缩写为"25度/多云"之类的简短文本,成本下降了不少,效果反而更稳。

6. 从5分钟Demo到真实项目:进阶思路和我的经验

6.1 让智能体"看见"更多工具:增加一个系统时间之外的敏感维度

很多人做完天气+计算器之后,就开始琢磨怎么把它接入自己的真实业务。我的经验是:先别急着接入重业务,先扩充"能感知真实世界"的工具。

比如增加一个search_knowledge工具,内部调用一个本地关键词匹配函数,或者一个远程搜索API。这样你的智能体就从一个"只能回答我预设信息"的小程序,变成了"能根据用户问题动态检索信息"的系统。这一步跨越的感觉,跟你第一次跑通Hello World完全不一样。

等你把工具数量扩充到5个以上,你自然就会发现:模型对工具的选择变得越来越重要。此时你可以考虑在大模型输出之后加一层规则校验,比如某些工具必须经过确认才执行,避免模型误触发。

6.2 接入业务系统:工具函数就是你的API网关

在真实项目里,工具函数往往不是模型直接执行的代码,而是封装好的业务API调用。我见过一个很典型的Demo结构:

大模型 → 决定调用"查订单"工具 → 工具函数做权限校验 → 调用内部订单服务 → 返回脱敏后的订单数据 → 大模型组织回答。

这个结构下,工具函数就像一个API网关,你可以在这里加鉴权、加日志、加限流。这样设计的好处是,所有对业务系统的访问都集中经过这个网关,安全和审计都很容易做。

6.3 最后分享一个我从"能跑"到"好用"的细节

我自己在实际开发中,越来越觉得"系统的反馈"比"模型的聪明程度"更影响最终效果。也就是说,你给模型返回的数据越精炼、越贴近用户的问法,模型生成的回答就越靠谱。

举个例子,用户问"杭州天气怎么样",如果你的工具返回的是{"city": "hangzhou", "code": "200", "temp": "35", "humidity": 80},模型会依照这一堆字段组织语言,但"code": "200"这种工程字段对用户毫无意义。如果你把返回内容预处理成"杭州,晴,35度,湿度80%",模型就能直接引用,生成结果既准确又自然。

这个预处理不是模型做的,而是工具函数做的。也就是说,你的工具函数不仅是"取数据",还承担了"把数据翻译成人话"的责任。这个设计对回答质量的影响之大,超乎很多人的直觉。

6.4 关于未来:你其实已经摸到了进阶的门槛

当你跑完上面这些内容,你的工具箱里至少有:一个大模型调用通道、一个统一的工具执行循环、几个真实的工具函数、一套成熟的错误处理思路。这就是智能体的地基。

接下来不论你是往"多智能体协作"方向走,还是往"工作流编排"方向走,底层都是这套循环。区别在于,多智能体是把多个这样的循环用消息队列串起来,工作流是把工具调用变成有向无环图。地基稳了,上层怎么搭都可以。

如果这个5分钟Demo没能让你尽兴,还有一条更进阶的路:把这个循环封装成一个可以被外部调用的服务,比如通过HTTP暴露给前端。这样前端的用户输入直接触发智能体循环,执行结果再返回前端展示。到这一步,你已经不是一个"体验者"了,而是一个真正在造东西的开发者。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 8:39:30

AI数据库不是向量库:从概念拆解到选型,看清数据管理本质

这两年我陆续看了不少声称"AI数据库"的项目方案,其中相当一部分,本质上就是往一个向量检索引擎外面包了一层管理界面。也有人拿一堆向量检索的性能参数来咨询,问这套"AI数据库"能不能落在生产环境。每次我都得先花十分钟…

作者头像 李华
网站建设 2026/10/17 8:39:25

Ax Gateway技术解析:网关服务原理与典型应用场景

我无法根据当前输入生成符合要求的博文。原因如下:项目标题“ax”过于简略且无明确指向:它既不是完整的技术名词、工具名、产品名,也不是可识别的缩写(如未提供上下文说明是AX Framework、AX Protocol、AX Gateway、或某厂商私有系…

作者头像 李华
网站建设 2026/10/17 8:38:52

行人重识别训练全解析:从表征学习到train_model.py实战

行人重识别系列写到第12篇,前面把数据集、评价指标、特征提取这些基础打完了,今天终于要进入最核心的部分:训练模型。这篇我重点讲表征学习这条路线下的训练脚本 train_model.py 该怎么写、每个模块为什么这么设计、以及我实际跑代码时踩过的…

作者头像 李华
网站建设 2026/10/17 8:38:09

AI在机器人运动控制中的应用盘点:从强化学习到Sim-to-Real落地

在做机器人运动控制的这几年里,我接触过的项目从最简单的单关节位置伺服,到后来带视觉引导的六轴机械臂抓取,再到双足动态步态,可以说每一次方案升级的背后,都能看到AI技术渗透的痕迹。很多人一听到”AI控制”就想到端…

作者头像 李华
网站建设 2026/10/17 8:36:56

AI Agent落地不崩的根基:意图识别与变量聚合实战解析

先声明一下,这篇东西不是我坐在家里拍脑袋想出来的。过去两个月我一直在调一个基于大模型的客服质检与工单自动流转系统,表面上看是“对话管理”,真正动手以后发现,最折磨人的根本不是模型推理,而是两个看起来不起眼、…

作者头像 李华
网站建设 2026/10/17 8:36:32

CAN总线数据帧格式与DLC映射、位同步原理解析

搞CAN总线调试这些年,我最大的体会是:这个协议看着简单,真正上手后翻车的地方一个接一个。网上一搜CAN总线的资料,要么是直接搬运协议标准原文,要么是教你调库函数、配寄存器,很少有一个帖子把“数据帧格式…

作者头像 李华