最近跟着github项目学习Agent开发现做实操记录。相关的代码在教学文档里均可查得但我还是喜欢动手敲敲内容可以加点自己的小料。如果直接复制本文的代码需要事先完成convert intendation to spaces (如果发现tab和空格有冲突。)创建main方法把循环体放进去。一、python环境准备创建项目目录并创建python虚拟环境# 创建项目目录mkdirfirst_agentcdfirst_agent# 创建python虚拟环境python3-mvenv .venv# macOS / Linuxsource.venv/bin/activate# Windows (cmd).venv\Scripts\activate# Windows (PowerShell).venv\Scripts\Activate.ps1# 退出虚拟环境deactivate# 安装软件pipinstallrequests tavily-python openai二、创建指令模板定义角色、工具、思考、行动AGENT_SYSTEM_PROMPT 你是一个智能旅行助手。你的任务是分析用户的请求并使用可用工具一步步地解决问题。 # 可用工具 - get_weather(city: str)查询制定城市的实时天气 - get_attraction(city: str, weather: str)根据城市和天气搜索推荐的旅游景点 # 输出格式要求 你的每次回复必须严格遵循以下格式包含一对Thought和Action Thought: [你的思考过程和下一步计划] Action: [你要执行的具体行动] Action的格式必须是以下之一 1. 调用工具function_name(arg_namearg_value) 2. 结束任务Finish[最终答案] # 重要提示 - 每次只输出一对Thought-Action - Action必须在同一行不要换行 - 当收集到足够信息可以回答用户问题时必须使用ActionFinish[最终答案]格式结束 请开始吧 三、定义工具类这里需要两个工具一个是查询真实天气的工具另一个是查询并推荐旅游景点的工具。查询真实天气的工具使用wttr.in查询并推荐旅游景点的工具使用Tavily Search API它是一个面向开发者的Web搜索API专为 AI代理和大语言模型LLM设计的实时搜索与信息提取接口用于增强生成式AI的知识获取能力。使用Tavily Search API之前需要先用邮箱注册并获取API Keys。程序中使用读取配置key的形式调用API因此需要把自己的key配置到系统的环境变量中。或者硬编码生产环境中不推荐。# 工具1查询真实天气# 了解更多接口内容可访问https://github.com/chubin/wttr.inimportrequestsdefget_weather(city:str)-str: 调用wttr.in API查询真实的天气信息 urlfhttps://wttr.in/{city}?formatj1try:resprequests.get(url)resp.raise_for_status()dataresp.json()current_conditiondata[current_condition][0]temp_Ccurrent_condition[temp_C]weather_desccurrent_condition[weatherDesc][0][value]returnf{city}当前天气{weather_desc}气温{temp_C}摄氏度exceptrequests.exceptions.RequestExceptionase:returnf错误网络异常 -{e}except(KeyError,IndexError)ase:returnf错误参数异常 -{e}# 工具2查询并推荐旅游景点#importosfromtavilyimportTavilyClientdefget_attraction(city:str,weather:str)-str: 根据城市和天气使用Tavily Search api搜索并返回优化后的景点推荐 # 方式一 通过系统的环境变量获取api_key需提前配置好api_keyos.environ.get(TAVILY_API_KEY)# 方式二 硬编码生产环境中不推荐api_keytvly-dev-xxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxifnotapi_key:return错误未配置TAVILY_API_KEY环境变量# 初始化客户端clientTavilyClient(api_keyapi_key)# 构造查询promptqueryf{city}在{weather}天气下最值得去的旅游景点推荐及理由try:# 查询返回结果responseclient.search(queryquery,search_depthbasic,include_answerTrue)# 如果结果干净直接使用ifresponse.get(answer):returnresponse[answer]# 如果没有综合性回答则格式化原始结果formatted_results[]forresultinresponse.get(result,[]):formatted_results.append(f-{result[title]}:{result[content]})ifnotformatted_results:return抱歉没有找到相关的旅游景点推荐。return根据搜索为您找到以下信息:\n\n.join(formatted_results)exceptExceptionase:returnf错误:执行Tavily搜索时出现问题 -{e}# 用工具字典统一管理所有工具available_tools{get_weather:get_weather,get_attraction:get_attraction}四、封装OpenAI大模型的客户端使用该封装的客户端需要准备好三个信息model_idapi_keybase_url以deepseek为例base_url (OpenAI)https://api.deepseek.comapi_key自行创建modeldeepseek-v4-flash 或者 deepseek-v4-profromopenaiimportOpenAIclassOpenAICompatibleClient: 一个用于调用任何兼容OpenAI接口的LLM服务的客户端 def__init__(self,model:str,api_key:str,base_url:str):self.modelmodel self.clientOpenAI(api_keyapi_key,base_urlbase_url)defgenerate(self,prompt:str,system_prompt:str)-str:调用LLM API来生成回答try:messages[{role:system,content:system_prompt},{role:user,content:prompt}]responseself.client.chat.completions.create(modelself.model,messagesmessages,streamFalse)returnresponse.choices[0].message.contentexceptExceptionase:print(f调用LLM API出错{e})return错误调用大模型服务出错五、创建循环体基本框架搭建好后需要把实例的信息提供完整并通过循环体执行计划。实例所需信息整理如下工具1城市city工具2城市city天气weatherapi_keyTAVILY_API_KEY大模型客户端modelapi_keybase_urlpromptsystem_prompt智能体的循环体调用工具和传统开发调用后端API稍微有些不同。传统开发调用API时需要明确给出具体参数名和对应的实例值而这个智能体是通过prompt和正则表达式截取到结果再提示大模型使用工具时应该传递什么参数。importre# 配置实例信息MODEL_IDdeepseek-v4-flashAPI_KEY补充你的deepseek api_keyBASE_URLhttps://api.deepseek.comTAVILY_API_KEY补充你的 tavily api_keyos.environ[TAVILY_API_KEY](补充你的 tavily api_key)llmOpenAICompatibleClient(modelMODEL_ID,api_keyAPI_KEY,base_urlBASE_URL)# 初始化promptuser_prompt帮我查询今天厦门的天气根据天气推荐一个合适的旅游景点prompt_history[f用户请求{user_prompt}]print(f用户输入{user_prompt}\n*40)# 循环体foriinrange(5):print(f--- 循环{i1}---\n)# 构建完整的promptfull_prompt\n.join(prompt_history)# 调用大模型思考其中system_prompt提示了大模型可以调用哪些工具工具对应有什么参数需要传递llm_outputllm.generate(promptfull_prompt,system_promptAGENT_SYSTEM_PROMPT)# 模型可能输出多余的Thought-Action对需要截断# re.DOTALL 让 . 可以匹配任意字符包括换行所以能跨行匹配内容。# 第一个括号匹配从 Thought: 开始到 Action: 及其后面内容的连续片段。非贪婪匹配任意字符# 第二个括号用来划定匹配内容的结束位置。下一行出现Thought: / Action: / Observation: 时当前匹配终止。# (?...) 是正向先行断言判断后面是否符合规则但不把这部分字符纳入匹配结果只做 “边界判断”# (?:...)非捕获组只分组、不单独提取内容matchre.search(r(Thought:.*?Action:.*?)(?\n\s*(?:Thought:|Action:|Observation:)|\Z),llm_output,re.DOTALL)ifmatch:truncatedmatch.group(1).strip()iftruncated!llm_output.strip():llm_outputtruncatedprint(f模型输出\n{llm_output})prompt_history.append(llm_output)# 从llm_output字符串里查找第一个以 Action: 开头的行 / 段落并提取Action:后面的全部内容action_matchre.search(rAction:(.*),llm_output,re.DOTALL)ifnotaction_match:observation错误未解析到Action字段。请确保你的回复严格遵循Thought:... Action:...的格式observation_strfObservation:{observation}print(f{observation_str}\n *40)prompt_history.append(observation_str)continue# 拿到 Action: 后面的完整内容。action_straction_match.group(1).strip()ifaction_str.startswith(Finish):final_answerre.match(rFinish\[(.*)\],action_str).group(1)print(f任务完成最终答案{final_answer})break# 提取函数名tool_namere.search(r(\w)\(,action_str).group(1)# 提取括号内参数args_strre.search(r\((.*)\),action_str).group(1)# 解析 keyvalue 成字典kwargsdict(re.findall(r(\w)([^]*),args_str))iftool_nameinavailable_tools:observationavailable_tools[tool_name](**kwargs)else:observationf错误未定义工具 {tool_name}# 记录观察结果observation_strfObservation:{observation}print(f{observation_str}\n*40)prompt_history.append(observation_str)六、第一个智能体成功运行运行结果如下用户输入帮我查询今天厦门的天气根据天气推荐一个合适的旅游景点 --- 循环1 --- 模型输出 Thought: 用户需要查询厦门今天的天气并根据天气推荐景点。我首先需要获取厦门的实时天气数据。 Action: get_weather(city厦门) Observation: 厦门当前天气Partly Cloudy 气温23摄氏度 --- 循环2 --- 模型输出 Thought: 厦门当前天气为局部多云适合外出游览。现在根据城市和天气推荐景点。 Action: get_attraction(city厦门, weatherPartly Cloudy) Observation: Under partly cloudy weather, visit Gulangyu Island for its artistic atmosphere and historical buildings, or explore the quaint village of Zengcuoan for its creative shops and food. --- 循环3 --- 模型输出 Thought: 根据查询结果厦门天气为局部多云适合户外活动推荐景点包括鼓浪屿和曾厝垵。选择其中一个作为推荐。 Action: Finish[根据厦门今天局部多云的天气建议前往鼓浪屿游览欣赏艺术氛围和历史建筑。] 任务完成最终答案根据厦门今天局部多云的天气建议前往鼓浪屿游览欣赏艺术氛围和历史建筑。七、其他至此本应该结束。但我总感觉有些东西堵着让我不痛快。AGENT_SYSTEM_PROMPT是不是可以抽成skills我咨询了一下AI回答是肯定的。AGENT_SYSTEM_PROMPT中关于“可用工具”的部分完全可以抽成独立的skills结构这会让代码更易维护、扩展比如动态增减技能 / 工具也能避免把工具定义硬编码在纯文本Prompt里。于是我开始改造源代码。fromdataclassesimportdataclassdataclassclassToolParameter:name:strtype:strdescription:strrequired:boolTruedataclassclassSkill:name:strdescription:strparameters:list[ToolParameter]# 用类定义SkillsSKILLS[Skill(nameget_weather,description查询指定城市的实时天气,parameters[ToolParameter(city,str,要查询天气的城市名称)]),Skill(nameget_attraction,description根据城市和天气搜索推荐的旅游景点,parameters[ToolParameter(city,str,旅游城市名称),ToolParameter(weather,str,该城市的天气描述)])]defgenerate_system_prompt(skills):# 生成工具说明的模板片段tools_template\n - {name}({params}):{desc}tools_strforskillinskills:# 拼接参数如city: str, weather: strparams_str,.join([f{p.name}:{p.type}forpinskill.parameters])tools_strtools_template.format(nameskill.name,paramsparams_str,descskill.description)# 完整的system_prompt模板system_prompt_template 你是一个智能旅行助手。你的任务是分析用户的请求并使用可用工具一步步地解决问题。 # 可用工具{tools} # 输出格式要求 你的每次回复必须严格遵循以下格式包含一对Thought和Action Thought: [你的思考过程和下一步计划] Action: [你要执行的具体行动] Action的格式必须是以下之一 1. 调用工具function_name(arg_namearg_value) 2. 结束任务Finish[最终答案] # 重要提示 - 每次只输出一对Thought-Action - Action必须在同一行不要换行 - 当收集到足够信息可以回答用户问题时必须使用ActionFinish[最终答案]格式结束 请开始吧 .strip()returnsystem_prompt_template.format(toolstools_str)# 生成最终的system_promptAGENT_SYSTEM_PROMPTgenerate_system_prompt(SKILLS)修改后智能体正常运行结果如下用户输入帮我查询今天厦门的天气根据天气推荐一个合适的旅游景点 --- 循环1 --- 模型输出 Thought: 用户需要查询厦门今天的天气并根据天气推荐景点。首先需要调用 get_weather 获取厦门实时天气信息。 Action: get_weather(city厦门) Observation: 厦门当前天气Overcast 气温23摄氏度 --- 循环2 --- 模型输出 Thought: 获取到厦门当前天气为阴天Overcast气温23摄氏度。现在需要根据这个天气推荐景点调用 get_attraction 工具。 Action: get_attraction(city厦门, weatherOvercast) Observation: Under overcast weather, visit Gulangyu Island for its quiet, scenic paths and cultural architecture. Its less crowded and perfect for leisurely strolls. --- 循环3 --- 模型输出 Thought: 已经获取到天气和推荐景点可以回答用户问题。 Action: Finish[厦门今天天气为阴天气温23摄氏度。推荐景点鼓浪屿。阴天时鼓浪屿人少适合悠闲漫步欣赏静谧的小路和建筑文化。] 任务完成最终答案厦门今天天气为阴天气温23摄氏度。推荐景点鼓浪屿。阴天时鼓浪屿人少适合悠闲漫步欣赏静谧的小路和建筑文化。然后也可以抽成skills.md只不过代码中需要自行解析md文件。这个工作不是我研究智能体的目的暂且放下了。智能体核心流程梳理智能体大循环是思考→执行工具→观察思考→再思考。核心目标是1大模型根据用户问题判断调用哪个工具或者结束2代码提取“要执行的动作”3执行动作反馈结果给大模型4循环。关于正则表达式这个案例中不论大模型输出多少都只截取了第一对的“Thought - Action”。我在程序中补充了log文件输出从log内容也可以证实这一点。从log文件内容可以看出1工作流程发送prompt → 获取llm回复 → 提取接口参数信息 → 传递参数调用工具 → 获取工具执行结果记录observation → 循环。符合“感知 - 思考 - 行动 - 观察”流程2发给LLM的完整上下文会不断“记忆”历史信息包含上一轮发给LLM的完整上下文 LLM第一对“Thought - Action”的输出 Observation内容3正则表达式每次只提取第一对的“Thought - Action”。[2026-06-12 10:36:11] 【新任务启动】 [2026-06-12 10:36:11] 智能体任务开始运行 [2026-06-12 10:36:11] 【初始用户请求】帮我查询今天厦门的天气根据天气推荐一个合适的旅游景点 [2026-06-12 10:36:11] 【第 1 轮循环】 [2026-06-12 10:36:11] 【发给LLM的完整上下文】 用户请求帮我查询今天厦门的天气根据天气推荐一个合适的旅游景点 [2026-06-12 10:36:14] 【LLM 原始完整输出】 Thought: 用户需要查询今天厦门的天气然后根据天气推荐景点。首先调用get_weather获取天气信息。 Action: get_weather(city厦门) [2026-06-12 10:36:14] 【提取到的 Action 内容】get_weather(city厦门) [2026-06-12 10:36:14] 【识别工具名】get_weather [2026-06-12 10:36:14] 【原始参数字符串】city厦门 [2026-06-12 10:36:14] 【解析后参数字典】{city: 厦门} [2026-06-12 10:36:14] 工具1get_weather开始运行 [2026-06-12 10:36:19] 【工具执行结果(Observation)】 Observation: 厦门当前天气Overcast 气温23摄氏度 [2026-06-12 10:36:19] 【第 2 轮循环】 [2026-06-12 10:36:19] 【发给LLM的完整上下文】 用户请求帮我查询今天厦门的天气根据天气推荐一个合适的旅游景点 Thought: 用户需要查询今天厦门的天气然后根据天气推荐景点。首先调用get_weather获取天气信息。 Action: get_weather(city厦门) Observation: 厦门当前天气Overcast 气温23摄氏度 [2026-06-12 10:36:21] 【LLM 原始完整输出】 Thought: 天气已获取为Overcast阴天现在根据此天气推荐景点。 Action: get_attraction(city厦门, weatherOvercast) [2026-06-12 10:36:21] 【提取到的 Action 内容】get_attraction(city厦门, weatherOvercast) [2026-06-12 10:36:21] 【识别工具名】get_attraction [2026-06-12 10:36:21] 【原始参数字符串】city厦门, weatherOvercast [2026-06-12 10:36:21] 【解析后参数字典】{city: 厦门, weather: Overcast} [2026-06-12 10:36:21] 工具2get_attraction开始运行 [2026-06-12 10:36:26] 【工具执行结果(Observation)】 Observation: Under overcast weather, visit Gulangyu Island for cultural and historical sites, and Xiamen Sea World for marine life exhibits. For indoor activities, go to Snow World for a snow experience or Fantasy Dream Kingdom for interactive attractions. [2026-06-12 10:36:26] 【第 3 轮循环】 [2026-06-12 10:36:26] 【发给LLM的完整上下文】 用户请求帮我查询今天厦门的天气根据天气推荐一个合适的旅游景点 Thought: 用户需要查询今天厦门的天气然后根据天气推荐景点。首先调用get_weather获取天气信息。 Action: get_weather(city厦门) Observation: 厦门当前天气Overcast 气温23摄氏度 Thought: 天气已获取为Overcast阴天现在根据此天气推荐景点。 Action: get_attraction(city厦门, weatherOvercast) Observation: Under overcast weather, visit Gulangyu Island for cultural and historical sites, and Xiamen Sea World for marine life exhibits. For indoor activities, go to Snow World for a snow experience or Fantasy Dream Kingdom for interactive attractions. [2026-06-12 10:36:31] 【LLM 原始完整输出】 Thought: 已获取厦门今日天气为阴天Overcast根据推荐适合游览文化历史景点。建议选择鼓浪屿作为今日推荐景点。 Action: Finish[推荐您今天去厦门鼓浪屿游览体验丰富的文化历史景点适合阴天出行。] [2026-06-12 10:36:31] 【提取到的 Action 内容】Finish[推荐您今天去厦门鼓浪屿游览体验丰富的文化历史景点适合阴天出行。] [2026-06-12 10:36:31] 【任务结束】最终答案推荐您今天去厦门鼓浪屿游览体验丰富的文化历史景点适合阴天出行。 [2026-06-12 10:36:31] 智能体全部循环执行完毕至此我认为对第一个智能体的整体认知算是完整了。