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+ | 运行环境 | 建议用虚拟环境,避免污染系统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暴露给前端。这样前端的用户输入直接触发智能体循环,执行结果再返回前端展示。到这一步,你已经不是一个"体验者"了,而是一个真正在造东西的开发者。