资讯动态

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

发布时间:2026/10/17 8:42:04 来源:尧图企业网站定制
1. 别被智能体三个字吓住它没那么玄最近身边不少朋友都在聊智能体什么工作流编排、多智能体协作、自主规划任务听起来特别唬人。我一开始也以为那得是算法工程师才能碰的东西直到自己动手写了一个最简版本才发现——所谓智能体本质上就是一个会自己调用工具的大模型包装层。这层包装做的事情概括起来就三件把用户的任务和可用的工具信息一起丢给大模型。大模型返回我要调用哪个工具、传什么参数。程序去执行这个工具把结果再丢给大模型循环往复直到大模型认为任务完成。说白了智能体没有神秘魔法它就是一个思考—行动—观察的循环。这个循环跑起来的基础设施不过是一个大模型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运行环境建议用虚拟环境避免污染系统Pythonopenai 或 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_steps5): messages.append({role: user, content: user_input}) for step in range(max_steps): response client.chat.completions.create( model你的模型名, messagesmessages, toolstools, tool_choiceauto, ) 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 {step1}] 调用工具: {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_asciiFalse) }) 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_choiceauto的意思是让模型自己决定是否调用工具。你也可以设置成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}, timeout5) 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暴露给前端。这样前端的用户输入直接触发智能体循环执行结果再返回前端展示。到这一步你已经不是一个体验者了而是一个真正在造东西的开发者。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑