☰
5分钟手写智能体:大模型工具调用循环实战
2026/10/17 12:11:24 网站建设 项目流程

1. 核心概念:智能体到底比聊天机器人多会了什么?

第一次听到“智能体”这个词的人,很容易把它想象成一个长着机器人身体、能满房间跑的AI。但在我自己动手做过几次AI应用开发之后,更愿意把它理解成一句话:带手带脚的大模型。

普通大模型是一个“嘴”——它只会说,不会做。你问它“今天某城市冷不冷”,它能回答“需要调用天气接口才能知道”,但也就到这里为止了。智能体不一样,它在“说话”这件事之外,多了一层“做事”的能力:它可以自己决定调哪个工具、传什么参数、拿回结果之后再继续推理,直到把问题答完。这个过程的专业叫法是ReAct循环(Reasoning + Acting),翻译成大白话就是“一边想一边做,做完再接着想”。

很多人对智能体有个误区,觉得它一定很复杂,又是规划又是记忆又是多模型协作。实际上,跑通一个能完成真实任务的智能体,只需要三样东西:

  • 一个大语言模型接口,负责“思考”;
  • 一组工具定义,告诉模型“你能用哪些能力”;
  • 一小段循环代码,负责“执行模型的想法并把结果喂回去”。

就这么简单。这篇文章要做的,就是把这三样东西拼起来,用5分钟跑通你人生第一个智能体。它适合谁?如果你写过一点点Python、申请过一个模型服务的API密钥,但一直不确定“智能体到底是怎么工作的”,照着这篇文章敲一遍就能彻底看明白。如果你之前只用网页聊天助手,还没碰过代码,那这篇文章同样能看懂,因为代码量很小,我会逐行解释。

先提前说清楚:我说的“5分钟”,不是“从零学编程到做出智能体5分钟搞定”,而是“在已有API访问权限、机器上装有Python的前提下,把一个最小可跑的版本跑起来,大约需要5分钟”。真实开发中的半小时、一下午,基本都花在调工具、处理异常、改提示词上——这些文章后面都会提到,但第一步,先把门推开。

2. 方案选型:为什么“手写最小循环”是最快的入门路径

2.1 框架搭好的路,不一定看得懂

现在市面上有不少现成的智能体开发框架,封装程度很高,确实能让你几行代码就跑起一个Agent。我第一次接触这类框架时的感受是:跑起来很容易,但一旦运行结果不符合预期,我完全不知道去改哪里。

框架帮你把“模型对话”“工具调度”“循环控制”全包了,你写起来轻松,但里面的决策逻辑对你是黑盒。比如模型传了一个非法参数,框架怎么兜底?工具返回异常,框架是否继续循环?这些细节真正决定了智能体稳不稳定,而框架不会主动告诉你。

所以这篇文章选择“手写一个最小循环”,不是否定框架的价值,而是先带你走一遍底层的路。当你亲手写过一次工具分发逻辑,再用框架时会发现那层神秘感消失了,出了问题也知道该查哪里。

这里有个选型建议,结合我的实践做了个简单对比:

方案上手速度可控性适合场景
现成智能体框架快,几行起步低,黑盒较多已有经验,需要快速搭业务原型
手写最小循环中,约70行代码高,每步可控学习原理、排查问题、做轻量工具
纯提示词假智能体最快最低演示用,但做不了真事

入门阶段,我强烈推荐手写。花一个小时搞懂循环是怎么回事,会给你省下未来无数个“为什么它不按我说的做”的夜晚。

2.2 模型不负责执行,它只负责“想”

这是理解智能体最关键的一点。以前写程序,函数的调用是人在代码里写死的:if句子包含“天气”就去查天气。智能体的方式完全反过来——模型看到用户的自然语言,自己对“应该调用哪个工具”做判断,然后返回一个结构化的“调用意图”,由你写的代码去执行。

这个机制叫Function Calling(函数调用),或者叫“工具调用”。它的交互过程是:

  1. 你把自己的工具用JSON格式描述给模型;
  2. 模型看完用户问题,决定“我需要用天气工具”,于是返回一段特殊格式的消息,里面包含工具名称和参数;
  3. 你的代码拿到这个消息,解析出工具名和参数,去执行真实函数;
  4. 你把函数的返回结果追加到对话里,再次发给模型;
  5. 模型看了结果,决定是继续调用下一个工具,还是直接给出最终答复。

这种“模型做决策、代码做执行”的分工,是智能体与普通聊天机器人的分水岭。打个比方:模型是你的项目负责人,它负责拆任务、分派活;你写的函数是干活的技术员,只负责执行。负责人不动手,技术员不决策,两者通过一张任务单(结构化JSON)沟通。

想清楚这一层,后面写代码就有方向了。

2.3 跑之前的准备工作

动手之前需要确认三件事,缺一不可:

Python环境。3.9及以上版本都可以。只需要一个第三方库 requests,用来发HTTP请求调模型接口。实在不想装库的话,Python标准库里的 urllib 也能实现,但代码会繁琐一些,初学不推荐。

模型服务接口。你申请到的API密钥会对应一个服务地址,我用占位符表示。申请过程就是在某个模型开放平台注册账号、创建密钥,这一步按平台指引走就行,一般五分钟内能搞定。为了安全,建议用环境变量存密钥,不要直接硬编码写在文件里。

一个终端。Windows上可以用 PowerShell,macOS/Linux直接打开自带的终端。这篇文章里的代码都是单文件运行,不需要额外搭建项目结构。

我的建议是新建一个目录,比如叫 agent-demo,在里面建一个 agent.py 文件,代码贴进去就能跑。后面每一个改动都在这个文件上迭代,非常直观。

3. 动手实操:单文件Agent骨架与完整运行

3.1 最小可用的70行代码

直接上代码。这段代码实现了最核心的“推理循环”,包含两个工具:一个取当前时间,一个模拟查询天气。为了让初学的人不迷失在细节里,真正的第三方天气API我刻意没有接,文章后面会解释原因。

import json import os from datetime import datetime import requests def get_current_time(): """工具1:获取当前系统时间""" return datetime.now().strftime("%Y-%m-%d %H:%M:%S") def get_weather(city: str): """工具2:模拟查询某城市的天气""" # 演示版:真实项目中请替换为真实的天气服务接口 return f"{city}:多云,气温25摄氏度,东南风2级(模拟数据)" # 工具注册表:把函数名和可调用对象对应起来 TOOL_MAP = { "get_current_time": get_current_time, "get_weather": get_weather, } def build_tool_schemas(): """将工具的JSON描述返回给模型""" return [ { "type": "function", "function": { "name": "get_current_time", "description": "获取当前系统时间", "parameters": { "type": "object", "properties": {}, }, }, }, { "type": "function", "function": { "name": "get_weather", "description": "查询某个城市的天气情况", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称", }, }, "required": ["city"], }, }, }, ] def call_llm(messages, tools): """调用模型服务,返回完整响应""" api_key = os.environ.get("LLM_API_KEY") url = os.environ.get("LLM_API_URL") payload = { "model": os.environ.get("LLM_MODEL"), "messages": messages, "tools": tools, "temperature": 0.3, } headers = {"Authorization": f"Bearer {api_key}"} resp = requests.post(url, json=payload, headers=headers, timeout=30) resp.raise_for_status() return resp.json() def agent_loop(user_input): """完整的智能体推理循环""" messages = [ {"role": "system", "content": "你是智能体助手。需要用工具时,调用工具得到结果后再回答用户。"}, {"role": "user", "content": user_input}, ] tools = build_tool_schemas() max_iterations = 5 for _ in range(max_iterations): data = call_llm(messages, tools) msg = data["choices"][0]["message"] messages.append(msg) tool_calls = msg.get("tool_calls") or [] if not tool_calls: # 模型没有要求调用工具,说明它准备直接答复了 return msg.get("content", "") for tc in tool_calls: func_name = tc["function"]["name"] try: args = json.loads(tc["function"]["arguments"] or "{}") result = TOOL_MAP[func_name](**args) except Exception as e: result = f"工具执行失败:{e}" messages.append( { "role": "tool", "tool_call_id": tc["id"], "content": json.dumps(result, ensure_ascii=False), } ) return "达到最大迭代次数,已退出。" if __name__ == "__main__": question = input("请输入你的问题:") answer = agent_loop(question) print("智能体回答:" + answer)

运行前设置好环境变量,然后python agent.py,程序会等待你输入问题。整个过程不会超过五分钟。

3.2 逐段拆解:每个关键步骤背后的意图

代码很简短,但每一段都承担着一个必不可少的功能,我逐个掰开讲。

工具定义部分,两个函数对应两个真实的能力。注意 write_weather 函数故意只返回了模拟数据,而不是真实请求。原因很简单:演示智能体循环机制时,工具内部逻辑越简单越好,避免把“工具本身的问题”和“循环机制的问题”混在一起排查。等到你把循环跑通了,再把函数替换成真实的HTTP请求,就是一次手术式的小改动。这里也体现了智能体开发的常见分层思路:一个函数就是一个可替换的工具壳,内部实现随便改,对外接口保持稳定。

TOOL_MAP 注册表是代码与模型的“契约”。模型告诉你工具名,你查表找到对应函数调用。这个做法比用 if-else 链可维护得多,新增工具只需三步:写函数、加进表、更新schema。强烈建议所有智能体项目都用这张“注册表”作为调度中心。

build_tool_schemas 函数返回的是JSON Schema格式的描述。这是Function Calling机制里最容易被忽略但最关键的部分。你给模型的函数描述越准确,模型判断就越准确。参数说明、必填项、每个字段的含义都要写清楚。很多时候智能体“不听话”,问题不是出在模型,而是函数的JSON描述写得模棱两可。

call_llm 函数负责发请求。temperature 设成了0.3,希望模型输出更稳定确定一些。如果是做创意文案这类的任务,可以调高到0.8甚至1.0;但在工具调用场景里,你希望模型每次对同一句话给出稳定的工具选择,低温度更可靠。超时时间30秒,这只是个经验值,工具逻辑很轻的场景10秒都够;如果你的工具要做重量级计算,再调大一些。

agent_loop 函数是整个智能体的心脏。主循环的逻辑:先带着用户问题调用模型,如果模型返回了 tool_calls,就执行工具,把工具结果追加进消息列表,再次调模型;如果模型返回的是普通文本,直接作为答案返回。这里有一个极其重要的设计:每次调用模型时,messages 都在变。工具结果不是另开一个新对话,而是作为一条“角色为tool”的消息接续在原来对话里。模型正是因为“看到了”工具返回的真实结果,才能进行下一步推理。

max_iterations 上限是必须的安全阀。如果没有这个限制,模型在某个极端情况下可能会无限循环调用工具,既浪费额度又卡死程序。正常对话一次循环就能结束,两三次迭代已经是较复杂的任务,5次足够。阈值设置的经验是“正常任务的最大次数×2”。

3.3 运行效果与验收标准

代码跑起来以后,试三个典型场景做冒烟测试:

**场景一:闲聊。**输入“你好,你是谁”,模型应该直接回复,不调用任何工具。这验证了普通对话路径是通的。

**场景二:直接命中一个工具。**输入“现在几点了”,模型应该返回一段工具调用消息,程序执行 get_current_time,把时间传给模型,模型整理后回答。看终端里最终输出的时间与系统时间一致,说明工具链路通了。

**场景三:触发天气模拟。**输入“帮我看看某城市今天适合出门吗”,模型调用 get_weather 拿到模拟天气,结合“多云25度”给出建议。这个场景能观察到一次完整的两轮迭代:模型先调工具,再基于工具结果组织语言。

三个都能跑通,你的第一个智能体就算正式诞生了。如果中途遇到报错,先别急,大概率卡在第2节说过的环境变量上——密钥填了吗?地址填对了吗?模型名称填对了吗?这类问题的排查方法放在下一节统一讲。

4. 常见问题与排查技巧实录

4.1 高频报错速查

我自己跑这类Demo时踩过不少坑,把最高频的几类整理成一个速查表:

报错现象可能原因处理方式
401 / 密钥无效环境变量没设置或复制错误检查 LLM_API_KEY 是否完整,不要在末尾追加空格
404 / 地址不存在服务地址填错核对控制台给的接口地址,注意看域名和后缀
请求超时网络不稳定或服务端繁忙重试一次;确认没有把 timeout 设得太小
模型返回空 content工具调用后模型没拿到正确上下文检查工具结果是否以 role=tool 正确追加
工具名不存在 KeyErrorTOOL_MAP 缺函数新增工具时忘了同步注册表
JSON参数解析失败模型返回的参数不是合法JSON打印原始arguments,排查schema中描述是否有歧义

最让人头疼的是前两类,全都是配置问题。我的经验是:发现问题先打印环境变量做校验,再打印实际请求的URL和完整响应体,一步步从源头排查,不要在那个70行的代码里找逻辑错误。

4.2 工具异常时一定要把错误“喂”给模型

代码里有一行很不起眼的逻辑:

except Exception as e: result = f"工具执行失败:{e}"

这是非常关键的设计。工具执行出了异常,程序没有中断,也没有直接向用户报错,而是把错误信息作为工具结果回传给模型。模型看到“工具执行失败”这个结果后,会自己想办法——比如换一种说法、换一个参数重新调用,或者放弃工具直接回答。

这背后是“智能体设计里的优雅退化”思想:失败不应该终结整个流程,而应该成为模型下一步推理的输入。实际开发中,天气API返回超时、数据库查询报错,这些都是常态,一个能处理异常的循环,比一个遇错即崩的循环稳定太多。

如果想让程序更健壮,可以在这里做几层增强:捕获异常后把完整的异常信息格式化,再加上当前调用参数、函数名;对某些可重试的的异常(比如临时网络抖动)自动重试1-2次;工具连续失败两次就直接告诉模型“这个工具多次失败,请用已有信息回答”。这些都属于体验优化,核心循环结构不用动。

4.3 调试的独家技巧

初学智能体,最大的困难是“看不见内部发生了什么”。模型每一轮在想什么、决定调什么工具、参数是什么,全都不显示,出了问题只能干瞪眼。分享几个我用下来的调试习惯:

第一,打印消息流转。一进入循环就打印[第1轮] 模型返回tool_calls:get_weather 参数:{"city": "某城市"},工具执行完打印[工具结果] 某城市:多云...。这样每次运行都能看到智能体的完整思考路径,不用靠猜。这个贯穿始终的“留痕”习惯,是智能体调试最有力的武器。

第二,白盒测试工具函数。绕过模型,直接在代码里调用 get_weather(city="某城市"),确认工具本身没问题,再放进循环里。很多人一遇到bug就怀疑循环逻辑,但概率更大的其实是工具本身报错。先测底层函数,再测循环,排查范围逐层缩小。

第三,把模型返回的原始响应保存成JSON文件。模型调接口的响应格式偶尔会有意想不到的字段结构差异,写下来慢慢看,比用print看截断后的信息强得多。

5. 从最小实现到真实业务:我的进阶路线与体会

5.1 把模拟工具换成真实API

现在你已经有一个能跑通“推理循环”的骨架,接下来就是把演示工具升级成真刀真枪的技能。以天气工具为例,改造方式是:保留 get_weather(city: str) 这个函数签名不变,内部把模拟数据改成“调用天气服务提供商的第三方HTTP接口,解析返回后格式化输出”。由于TOOL_MAP和schema完全不用动,这个替换风险很低,改完直接跑。

真实工具接入的难点通常不是“请求怎么写”,而是“返回的数据怎么给模型”。第三方服务返回的JSON常常又乱又长,直接把完整响应塞给模型,既浪费上下文又干扰判断。我在项目里的做法是:在工具内部做一层提炼——只返回时间、温度、天气描述这些与问题直接相关的字段,并且拼成简洁的文本。记住一个原则:给模型的内容不是越详细越好,而是越结构化越好。

5.2 让智能体记住前文:上下文管理入门

跑通基础循环后,你会发现一个明显的短板:智能体没有“记忆”。你在同一段对话里问完“某城市天气”再问“那适合爬山吗”,模型大概率已经忘了前文——因为每次 agent_loop 都新建了一个空的 messages 列表。这就是上下文管理的起点。

最小改造方案:把 messages 列表提升到对话级别,每次用户输入时用 messages.append 追加用户消息,而不是清空重建。这样多轮问答之间就有了连续性。

再往后,当对话轮数多到撑爆上下文窗口时,就需要更高级的“摘要记忆”或“滑动窗口裁剪”。但我要特别说一句:很多文章一谈记忆就推向量数据库,对初学阶段的场景完全没必要。几千字的短对话,一个列表就够用;等到对话长度真正构成瓶颈,再去研究检索方案也不迟。

5.3 别急着一上来就搞多智能体协作

搜索“智能体”相关的内容,很容易被“多智能体编排”“规划器-执行器架构”这些词吓住。我的建议是:第一版,先完成一个能在单循环里稳定工作的智能体;第二版,给它接上三五个真实工具;第三版,再考虑把一个复杂任务拆成两个子智能体,分别负责不同环节。

为什么这么建议?因为多智能体协作的本质是“多个循环之间的协调”,它所有调试难度都是单循环难度的叠加——每个子智能体都可能调用工具、都可能出幻觉、都可能互相等待。如果单循环你还没有摸熟,翻倍排查复杂度会非常痛苦。我自己做模拟项目X时就有过教训:一开始设计了三个智能体相互协作,结果跑起来后完全不知道问题出在谁身上,后来退回单智能体把基础打牢,再逐步拆分,反而顺利得多。

5.4 我踩过几次坑之后总结出的三条铁律

最后分享三条这些年做智能体实际项目的体会,每一条都是被现实教育过之后才真正理解的。

第一条,把模型当实习生,不给绝对权限。模型输出不可预知,所以工具调用必须校验参数、对敏感操作加确认、给循环设上限。有些代码里直接让模型执行删除操作,不出事是运气,出事了就是事故。智能体再聪明,也要设护栏。

第二条,先让工具成功,再谈智能。智能体的智能程度,上限由工具决定的。工具只能查天气,那模型再聪明也干不了订机票的事;工具返回的数据质量差,模型后续推理也会跟着胡说八道。把每个工具当成产品打磨,比花时间调提示词更见效。

第三条,日志是智能体的眼睛。这句话我几乎对所有尝试智能体的人都说过。运行时的每一轮推理、每一个工具参数、每一次异常,都值得记录。没有日志,智能体对你就是一个黑盒子;有了日志,你才能像一个真正的开发者去理解它、控制它、改进它。

如果你照着这篇文章跑通了第一个智能体,下一步我建议你做的不是急着看更复杂的架构,而是把手边真正在用的某个接口——查日历、发邮件、读文件、算数据都行——封装成一个工具函数,接进这个循环。当你看到模型为了回答你的问题,独立决定调用它并给出合理参数的那一刻,你对“智能体”这三个字的理解,会和今天彻底不一样。

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

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

立即咨询