从零构建AI智能体:200行代码实现天气查询与建议助手
你是不是也刷到过各种“AI智能体”的教程感觉概念满天飞但真到自己动手却发现连个能跑起来的“Hello World”都搞不定从“智能体框架”到“Agent编排”各种新名词让人眼花缭乱但核心问题始终没变如何用最低的成本、最清晰的路径亲手搭建一个能理解指令、执行任务、并给出反馈的智能体这篇文章不空谈趋势也不堆砌晦涩的论文术语。我们将从一个最朴素的需求出发“帮我查一下北京的天气并建议我今天是否适合出门跑步。”我们将手把手从零开始用不到200行代码构建一个能完成这个任务的智能体。你会看到所谓的“智能体”开发核心并非高深的算法而是一套清晰的任务分解、工具调用与决策循环的工程化思想。通过本文你将彻底搞懂智能体Agent到底是什么它和普通程序、ChatGPT对话有什么区别构建智能体的最小核心组件有哪些我们如何用代码实现它们从想法到可运行程序的完整路径是怎样的环境如何搭建代码如何组织运行中会遇到哪些“坑”如何调试一个“发呆”或“报错”的智能体如何将这个简单的智能体扩展成更强大的自动化助手本文假设你具备基础的Python编程知识并对大语言模型LLM有初步了解。我们将使用当前最易获取的OpenAI API或兼容API作为“大脑”但整个架构是模型无关的你可以轻松替换为其他模型。1. 智能体开发解决的核心问题是什么在开始写代码之前我们必须先统一认知我们为什么要造“智能体”它解决了什么传统编程解决不了的问题传统程序的困境对于一个“查天气并给建议”的需求传统做法需要你手动调用天气API解析返回的JSON数据。自己编写一套逻辑规则来判断是否适合跑步例如温度在15-25度、无雨、PM2.5低于50。将结果拼接成一段人类可读的文字。这个过程高度确定但也极其僵化。如果需求变成“查天气并建议是否适合晾被子”你就得重写判断逻辑。每增加一个场景就要增加一段硬编码。智能体的优势智能体将“做什么”目标和“怎么做”逻辑解耦了。你只需要告诉它目标“查北京天气建议是否适合跑步”。它自己决定步骤它知道自己需要先“获取天气工具”然后“分析天气数据”最后“生成建议”。它具备泛化能力同样的架构稍加训练或提示它就能处理“晾被子”、“洗车”、“出游”等多种建议场景。所以智能体解决的核心问题是在开放、动态的环境下将复杂的人类目标自动分解为一系列可执行的操作序列并自主完成。这本质上是将一部分程序逻辑的“设计权”交给了AI。对于我们开发者而言构建智能体的核心工作就从“编写所有业务逻辑”转变为定义任务目标用自然语言描述。提供工具集告诉智能体它能调用哪些API或函数。设计决策循环让智能体学会何时、如何使用这些工具。接下来我们就围绕这三个核心开始搭建。2. 核心概念与架构理解智能体的“五脏六腑”一个最简单的智能体通常包含以下核心组件我们可以用一个“侦探破案”的类比来理解组件技术定义类比解释在我们的项目中的角色智能体Agent具备感知、规划、决策、执行能力的自治系统。侦探本人。他接收案件目标思考破案步骤规划决定去查什么线索决策并亲自或派人去执行行动。整个程序的核心调度中枢。大语言模型LLM智能体的“大脑”负责理解、推理和生成。侦探的推理能力和经验。他根据已有信息分析案情做出下一步该做什么的判断。我们使用OpenAI的gpt-3.5-turbo等模型作为推理引擎。工具Tools智能体可以调用的外部函数或API用于与环境交互。侦探可用的调查手段。如询问证人调用知识库、查看监控调用图像识别、化验物证调用科学分析API。我们将创建一个get_weather函数作为工具。提示词Prompt引导LLM行为的指令和上下文信息。给侦探的办案手册和当前案件简报。手册告诉他办案的基本原则和流程简报告诉他当前已知信息。我们将编写一个system_prompt来定义智能体的角色和行为规范。记忆Memory智能体存储和回忆历史交互信息的能力。侦探的笔记本。记录了他已经问过谁、查过哪里避免重复劳动也能串联线索。本文为简化使用单轮对话记忆即每次只处理当前query。复杂智能体会引入对话历史。执行循环Execution Loop智能体“思考-行动-观察”的重复过程。侦探的破案流程分析线索 - 决定调查行动 - 执行行动 - 获得新线索 - 继续分析...直到破案。我们将用while循环实现一个简单的ReAct模式。我们的项目架构图简化版用户输入“查北京天气建议跑步吗” | v [智能体核心] (包含LLM和Prompt) |-- 思考用户需要天气和跑步建议。我需要天气数据。 |-- 决策调用 get_weather 工具。 | v [工具执行] - 调用天气API获取结构化数据温度、天气、风速... | v [智能体核心] (接收工具返回结果) |-- 思考已获得天气数据。现在需要根据这些数据判断是否适合跑步。 |-- 决策无需再调用工具直接生成最终答案。 | v 输出“北京当前晴气温22度风力3级空气质量优。非常适合跑步”理解了这些概念我们就可以开始准备开发环境了。3. 环境准备与项目初始化我们将创建一个干净的Python项目。请确保你的Python版本在3.8以上。3.1 创建项目目录与虚拟环境打开终端命令行执行以下步骤# 1. 创建项目目录并进入 mkdir weather_agent cd weather_agent # 2. 创建虚拟环境推荐避免包冲突 python -m venv venv # 3. 激活虚拟环境 # 在 macOS/Linux 上 source venv/bin/activate # 在 Windows 上 # venv\Scripts\activate # 激活后命令行提示符前通常会出现 (venv) 标识3.2 安装核心依赖我们需要安装两个核心库openai用于调用OpenAI的LLM API。requests用于我们的自定义工具调用天气API。在激活的虚拟环境中运行pip install openai requests3.3 获取并配置API密钥OpenAI API Key访问 OpenAI平台 创建并复制你的API密钥。天气API Key我们将使用一个免费的天气API例如 OpenWeatherMap 。注册后在“My API keys”中获取你的Key。安全提醒API密钥是敏感信息绝对不要直接硬编码在代码中或提交到GitHub。我们将使用环境变量来管理。在项目根目录下创建一个名为.env的文件# .env 文件内容 OPENAI_API_KEY你的_openai_api_key_在这里 WEATHER_API_KEY你的_openweathermap_api_key_在这里然后我们需要安装python-dotenv库来读取这个文件pip install python-dotenv至此环境准备完毕。你的项目目录结构目前应该是weather_agent/ ├── venv/ # 虚拟环境目录通常被.gitignore忽略 ├── .env # 环境变量文件务必加入.gitignore └── (后续创建的.py文件)4. 核心模块拆解与实现我们将把智能体拆分成几个独立的模块这样代码更清晰也易于维护和扩展。4.1 第一步构建工具Tool——get_weather工具是智能体的“手和脚”。我们先实现一个最基础的天气查询工具。在项目根目录创建tools.py文件# tools.py import os import requests from dotenv import load_dotenv # 加载 .env 文件中的环境变量 load_dotenv() def get_weather(city: str) - str: 根据城市名称查询实时天气信息。 参数: city (str): 城市名称例如 Beijing 或 北京。 返回: str: 结构化的天气信息字符串。如果查询失败返回错误信息。 api_key os.getenv(WEATHER_API_KEY) if not api_key: return 错误未配置 WEATHER_API_KEY。请在 .env 文件中设置。 # 这里以OpenWeatherMap的Current Weather API为例 # 注意免费版API需要将城市名转换为英文这里做简单处理。实际项目可能需要更完善的地理编码。 base_url http://api.openweathermap.org/data/2.5/weather # 一个简单的城市名映射实际应用建议使用更专业的API或本地映射表 city_mapping { 北京: Beijing, 上海: Shanghai, 广州: Guangzhou, 深圳: Shenzhen, # 可以继续添加 } query_city city_mapping.get(city, city) # 如果映射里没有就用原名称 params { q: query_city, appid: api_key, units: metric, # 使用摄氏度 lang: zh_cn # 返回中文描述 } try: response requests.get(base_url, paramsparams, timeout10) response.raise_for_status() # 如果状态码不是200抛出HTTPError异常 data response.json() # 解析返回的JSON数据 city_name data.get(name, 未知城市) weather_desc data[weather][0][description] if data.get(weather) else 未知 temp data[main].get(temp, 未知) humidity data[main].get(humidity, 未知) wind_speed data[wind].get(speed, 未知) weather_info ( f城市{city_name}\n f天气状况{weather_desc}\n f温度{temp}°C\n f湿度{humidity}%\n f风速{wind_speed} m/s ) return weather_info except requests.exceptions.RequestException as e: return f网络请求失败无法获取天气信息{e} except (KeyError, IndexError) as e: return f解析天气API返回数据时出错{e} except Exception as e: return f获取天气信息时发生未知错误{e} # 本地测试这个工具 if __name__ __main__: # 测试前请确保 .env 文件中的 WEATHER_API_KEY 已正确设置 test_result get_weather(北京) print(工具测试结果) print(test_result)关键点解析安全通过load_dotenv()从.env文件安全读取密钥。健壮性使用了try...except捕获网络请求和数据处理中可能出现的异常。清晰的返回工具返回一个结构化的字符串便于后续的LLM理解。可测试文件底部有简单的测试代码方便单独验证工具是否工作。运行python tools.py如果配置正确你应该能看到北京的天气信息输出。4.2 第二步设计提示词Prompt与Agent角色提示词是智能体的“灵魂”它定义了智能体的身份、能力和行为规范。我们创建一个prompts.py文件来管理提示词。# prompts.py # 系统提示词定义了智能体的基本角色和行为准则 SYSTEM_PROMPT 你是一个天气与生活建议助手。你的核心能力是调用工具获取实时天气信息并基于此给出合理的生活建议如出行、运动、穿衣等。 ## 你的工作流程 1. **理解用户请求**判断用户是否需要天气信息或基于天气的建议。 2. **调用工具**如果需要天气数据你必须调用 get_weather 工具并提供**城市名称**作为参数。城市名称应从用户请求中提取如果未明确应主动询问。 3. **分析与建议**获得天气数据后结合用户的具体问题如“适合跑步吗”“要带伞吗”进行分析并给出友好、详细的建议。 4. **最终回答**将天气信息和建议整合成一段流畅、自然的回复。 ## 重要规则 - 你**必须**在需要天气数据时调用工具不能凭空编造天气。 - 工具返回的是原始数据你需要将其转化为易懂的描述。 - 如果工具调用失败如实告知用户并尝试提供通用建议或询问其他城市。 - 你的回答应简洁、有用、充满关怀。 # 工具的描述用于在请求LLM时告诉它有什么工具可用 TOOL_DESCRIPTIONS [ { type: function, function: { name: get_weather, description: 根据城市名称查询该城市的实时天气信息包括天气状况、温度、湿度和风速。, parameters: { type: object, properties: { city: { type: string, description: 需要查询天气的城市名称例如北京、Shanghai、New York。, } }, required: [city], }, }, } ]为什么这样设计提示词角色清晰让模型明确知道“我是谁”。流程明确给出了 step-by-step 的思考框架符合 ReActReasoning and Acting模式。规则具体强调了必须调用工具、不能编造、处理失败情况等减少了模型“胡言乱语”的可能。工具描述标准化TOOL_DESCRIPTIONS的格式遵循了 OpenAI 的 Function Calling 规范这是让LLM理解并决定调用哪个工具的关键。4.3 第三步实现智能体核心Agent Core与执行循环这是最核心的部分我们将实现一个简单的Agent类它负责与LLM对话、管理工具调用和执行循环。创建agent_core.py文件# agent_core.py import os import json from openai import OpenAI from dotenv import load_dotenv from tools import get_weather from prompts import SYSTEM_PROMPT, TOOL_DESCRIPTIONS # 加载环境变量 load_dotenv() class WeatherAgent: 一个简单的天气查询与建议智能体。 def __init__(self, modelgpt-3.5-turbo): 初始化智能体。 参数: model (str): 使用的OpenAI模型名称。 self.client OpenAI(api_keyos.getenv(OPENAI_API_KEY)) self.model model self.messages [{role: system, content: SYSTEM_PROMPT}] self.available_tools { get_weather: get_weather, } def _call_llm(self, messages, toolsNone): 调用OpenAI API支持工具调用。 try: kwargs { model: self.model, messages: messages, temperature: 0.1, # 低温度使输出更确定、更专注于工具调用 } if tools: kwargs[tools] tools kwargs[tool_choice] auto # 让模型自行决定是否调用工具 response self.client.chat.completions.create(**kwargs) return response.choices[0].message except Exception as e: print(f调用LLM API时出错{e}) return None def _execute_tool(self, tool_call): 执行被LLM选中的工具调用。 function_name tool_call.function.name function_args json.loads(tool_call.function.arguments) print(f[Agent 正在执行工具] {function_name}参数{function_args}) if function_name in self.available_tools: tool_function self.available_tools[function_name] try: # 根据工具函数的参数名传递参数 # 例如get_weather(city北京) result tool_function(**function_args) return result except Exception as e: return f工具 {function_name} 执行过程中出错{e} else: return f错误未知的工具 {function_name}。 def run(self, user_input: str): 运行智能体处理一次用户输入。 参数: user_input (str): 用户的自然语言指令。 返回: str: 智能体的最终回复。 print(f\n[用户输入] {user_input}) # 1. 将用户输入添加到对话历史 self.messages.append({role: user, content: user_input}) # 2. 开始执行循环这里简化为单轮工具调用循环复杂场景可能需要多轮 max_steps 5 # 防止无限循环 final_response None for step in range(max_steps): print(f\n--- 思考步骤 {step 1} ---) # 3. 调用LLM传入当前对话历史和工具描述 llm_message self._call_llm(self.messages, TOOL_DESCRIPTIONS) if llm_message is None: return 抱歉思考过程出现错误。 # 4. 将LLM的回复添加到对话历史 self.messages.append(llm_message.to_dict()) # 注意OpenAI SDK返回的是对象需转换 # 5. 检查LLM是否想要调用工具 if llm_message.tool_calls: # 6. 执行所有被请求的工具 tool_responses [] for tool_call in llm_message.tool_calls: tool_result self._execute_tool(tool_call) tool_responses.append({ tool_call_id: tool_call.id, role: tool, name: tool_call.function.name, content: str(tool_result), # 结果必须是字符串 }) print(f[工具返回] {tool_result[:100]}...) # 打印前100字符 # 7. 将工具执行结果作为消息追加让LLM继续分析 self.messages.extend(tool_responses) # 继续循环让LLM基于工具结果进行下一步思考 continue else: # 8. LLM没有调用工具生成了最终回复 final_response llm_message.content print(f[Agent 最终回复] {final_response}) break if final_response is None: final_response 经过多次尝试未能完成您的问题。请尝试更清晰的指令。 # 9. 将最终回复也加入历史为后续可能的对话扩展做准备 self.messages.append({role: assistant, content: final_response}) return final_response # 提供一个简单的运行示例 if __name__ __main__: agent WeatherAgent() test_queries [ 北京天气怎么样, 上海今天适合跑步吗, 帮我看看广州的天气我要去出差。, ] for query in test_queries: print(\n *50) response agent.run(query) print(*50)核心逻辑拆解初始化加载API Key设置系统提示词定义可用工具映射。_call_llm方法封装了对OpenAI API的调用。关键参数tools和tool_choice告诉模型有哪些工具可用并授权它自行决定调用。_execute_tool方法根据LLM返回的工具调用请求找到本地对应的Python函数并执行传入解析好的参数。run方法执行循环步骤3-4LLM根据当前对话历史和工具描述进行“思考”。步骤5-7如果LLM决定调用工具tool_calls不为空则执行工具并将结果以特定格式role: tool追加回对话历史。然后跳回步骤3让LLM基于新信息继续思考。这就是“思考-行动-观察”的循环。步骤8如果LLM不调用工具了说明它认为已经可以生成最终答案循环结束。保护机制设置了max_steps防止因逻辑错误导致无限循环。5. 运行与效果验证现在让我们将所有的模块组合起来运行我们的智能体。在项目根目录创建一个主入口文件main.py# main.py from agent_core import WeatherAgent def main(): print(天气助手智能体启动...) print(输入 quit 或 exit 退出程序。) print(- * 40) agent WeatherAgent() while True: try: user_input input(\n请输入您的问题).strip() if user_input.lower() in [quit, exit, q]: print(再见) break if not user_input: continue response agent.run(user_input) # 主程序里可以只打印最终回复详细过程在agent.run中已打印 # print(f\n助手{response}) except KeyboardInterrupt: print(\n\n程序被用户中断。) break except Exception as e: print(f\n程序运行出现未知错误{e}) if __name__ __main__: main()运行程序 在终端中确保虚拟环境已激活然后运行python main.py预期成功输出示例天气助手智能体启动... 输入 quit 或 exit 退出程序。 ---------------------------------------- 请输入您的问题北京今天适合跑步吗 [用户输入] 北京今天适合跑步吗 --- 思考步骤 1 --- [Agent 正在执行工具] get_weather参数{city: 北京} [工具返回] 城市Beijing\n天气状况晴\n温度22.5°C\n湿度45%\n风速2.5 m/s... --- 思考步骤 2 --- [Agent 最终回复] 北京当前天气晴朗气温22.5°C湿度45%风速2.5m/s。这种天气条件非常适合跑步气温适宜空气湿度适中风力较小。建议您做好热身享受跑步的乐趣验证要点工具调用观察控制台是否打印了[Agent 正在执行工具]和[工具返回]这证明智能体成功决定并执行了工具调用。最终回复最终回复是否结合了工具返回的原始数据22.5°C晴和你的问题适合跑步吗进行了推理和建议。循环控制对于简单问题应该在2个步骤内完成一步调用工具一步生成回复。对于更复杂的问题可能会触发更多轮思考。你可以尝试不同的问法“上海和北京的天气对比一下。”“我明天想去广州需要带伞吗”“深圳的湿度是多少”观察智能体如何理解你的意图并做出相应的工具调用和回答。6. 常见问题与排查思路在搭建和运行过程中你几乎一定会遇到下面这些问题。这里提供了详细的排查指南。问题现象可能原因排查方式解决方案ModuleNotFoundError: No module named openai依赖未安装或虚拟环境未激活。1. 检查命令行前缀是否有(venv)。2. 运行pip list查看已安装包。1. 激活虚拟环境source venv/bin/activate(Mac/Linux) 或venv\Scripts\activate(Win)。2. 安装依赖pip install openai requests python-dotenv。openai.AuthenticationErrorOpenAI API Key 错误或未设置。1. 检查.env文件是否存在且与代码在同一目录。2. 检查.env文件中OPENAI_API_KEY的值是否正确前后不能有空格。3. 在代码中临时print(os.getenv(“OPENAI_API_KEY”))看是否成功读取。1. 确保.env文件格式正确KEYvalue每行一个。2. 到 OpenAI 平台确认 API Key 有效且未过期。3. 重启你的终端或IDE。智能体不调用工具直接回答“北京天气是…”编造1. 提示词SYSTEM_PROMPT未强调必须调用工具。2. 工具描述TOOL_DESCRIPTIONS格式错误或未传入。3. 模型温度temperature设置过高导致“想象力”太丰富。1. 检查agent_core.py中初始化时self.messages是否包含了SYSTEM_PROMPT。2. 检查_call_llm函数调用时是否传入了toolsTOOL_DESCRIPTIONS。3. 查看LLM返回的完整消息确认是否有tool_calls字段。1. 强化 SYSTEM_PROMPT 中的规则如“你必须调用工具获取数据”。2. 确保TOOL_DESCRIPTIONS是包含字典的列表且格式符合OpenAI规范。3. 将temperature参数调低如0.1增加确定性。工具调用失败返回网络或解析错误1. 天气 API Key 错误或未设置。2. 网络问题或API服务不可用。3. 城市名称无法被天气API识别。1. 单独运行python tools.py测试工具函数。2. 检查.env中的WEATHER_API_KEY。3. 在tools.py的get_weather函数中打印完整的请求URL和响应进行调试。1. 注册并获取正确的天气API Key。2. 在工具函数中添加更完善的错误处理和日志。3. 考虑使用更健壮的地理编码服务将中文城市名转换为API接受的格式。程序陷入无限循环不断调用工具执行循环的退出条件有问题或者LLM在得到工具结果后仍然认为需要继续调用工具。1. 检查max_steps是否设置如5。2. 打印每一步的llm_message观察LLM在获得天气数据后为何还决定调用工具。1. 确保max_steps已设置并生效。2. 优化提示词明确告知“获得天气数据后应直接生成最终建议”。3. 检查工具返回的结果格式是否清晰便于LLM理解。错误AttributeError: ‘ChatCompletionMessage’ object has no attribute ‘to_dict’OpenAI Python SDK 版本更新API 返回的对象结构可能发生变化。查看openai库的版本 (pip show openai)并查阅其官方文档中ChatCompletionMessage对象的属性。新版本可能直接使用.model_dump()或属性访问。将llm_message.to_dict()改为llm_message.model_dump()或llm_message.dict()具体取决于你的SDK版本。7. 最佳实践与进阶扩展方向现在你已经拥有了一个可工作的智能体雏形。如何将它变得更好、更实用以下是一些关键的最佳实践和扩展思路。7.1 工程化最佳实践配置管理将模型名称、温度、最大循环次数等参数提取到配置文件如config.yaml或.env中避免硬编码。日志记录不要只使用print。集成logging模块将智能体的思考过程、工具调用、API请求和错误信息记录到文件便于调试和监控。错误处理与重试在_call_llm和_execute_tool中增加更细致的异常捕获和重试机制例如网络超时重试3次。超时控制为LLM API调用和工具执行设置超时防止某个环节卡死导致整个服务无响应。输入验证与清洗在run方法中对user_input进行基础检查如长度限制、敏感词过滤等。7.2 功能扩展方向增加更多工具智能体的能力取决于工具集。你可以轻松添加新工具search_web: 调用搜索API获取实时信息。calculate_distance: 计算两地距离。send_email: 发送邮件通知。query_database: 查询内部数据库。 只需在tools.py中定义函数并在agent_core.py的self.available_tools和prompts.py的TOOL_DESCRIPTIONS中注册即可。实现多轮对话记忆Memory目前的Agent是“失忆”的每次对话独立。要实现记忆需要维护一个不断增长的self.messages列表。注意上下文长度限制当对话过长时需要采用“摘要记忆”或“向量存储记忆”等策略进行压缩。在run方法开始时将历史对话也加载到self.messages中。引入规划Planning能力对于复杂任务如“规划一个北京三日游”智能体需要先制定一个高级计划再逐步执行。这可以通过在提示词中引入“Chain of Thought”或使用专门的规划模块来实现。使用更强大的Agent框架当项目复杂后手动管理循环、工具和记忆会变得繁琐。可以考虑迁移到成熟的框架如LangChain: 生态丰富组件齐全学习曲线稍陡。LlamaIndex: 专注于数据检索和RAG检索增强生成。Semantic Kernel(微软): 与.NET生态结合紧密。Dify / Coze 等低代码平台如果你更关注快速构建应用而非底层代码。7.3 生产环境部署考虑API成本与限流监控OpenAI API的调用量和费用。为智能体设置预算和速率限制。异步处理对于耗时较长的工具调用如爬虫使用asyncio进行异步处理避免阻塞主线程。构建Web服务使用FastAPI或Flask将你的智能体封装成HTTP API供前端或其他服务调用。可观测性除了日志接入监控系统如Prometheus跟踪请求延迟、工具调用成功率、Token消耗等关键指标。8. 总结从项目到认知回顾我们搭建的这个“天气助手智能体”代码虽短却完整演绎了智能体技术的核心范式感知用户输入- 规划LLM思考- 行动调用工具- 观察获取结果- 再规划生成回答。这个项目的价值不在于它本身的功能有多强大而在于它像一张清晰的地图为你揭示了智能体开发的全景起点是明确的用户需求而不是酷炫的技术。核心是LLM与工具函数的可靠交互这需要清晰的提示词和规范的接口定义。难点在于稳定、可控的执行循环要处理各种边界情况和错误。进阶之路在于工程化记忆、规划、多智能体协作、成本控制、监控告警。不要再被“Agent”、“智能体框架”、“编排”这些大词吓住。它们背后都是一些你可以逐步理解和掌握的工程组件。下一步你可以尝试用这个模式为你自己的工作流创建一个智能体比如自动整理会议纪要、监控系统日志并报警、或者辅助代码评审。动手去改去加新工具去处理更复杂的任务。在调试和解决问题的过程中你对智能体的理解才会真正深入骨髓。