智能体初体验:实现一个“旅游景点推荐助手” 📅 发布时间:2026/9/16 6:46:38 👁 浏览次数: 作业打卡Datawhale《Hello-Agents》第一章实践1. 作业背景本章教程的目标是实现一个“旅游景点推荐助手”用户输入一句话智能体先查询天气再根据天气推荐景点。它体现的是典型的Thought-Action-Observation循环用户请求 - Thought: 先查天气 - Action: get_weather(city...) - Observation: 天气结果 - Thought: 根据天气选景点 - Action: get_attraction(city..., weather...) - Observation: 景点结果 - Action: Finish[最终建议]教程原版使用Tavily Search API做景点搜索。但我在注册 Tavily 账号时一直卡在验证环节无法获取 API Key同时考虑到国内网络访问部分搜索后端不稳定所以我把“景点搜索工具”从 Tavily 改成了高德地图 POI 查询 Web 服务。改动后流程不变仍然保留 ReAct 智能体的核心思想只是把外部工具换成了国内更容易跑通的数据源。2. 我的改动点相比教程代码我主要做了 4 处调整景点工具替换删除tavily-python改用高德https://restapi.amap.com/v3/place/text查询 POI。密钥管理所有 Key 都从环境变量读取不写死在代码里避免作业截图/文章泄露。解析器加固教程里Action: (.*)和Finish\[(.*)\]在多行输出时容易匹配失败我改成按行解析 Action并允许Finish[...]内出现多行最终答案。天气到景点关键词映射根据天气文本选择高德 POI 关键词例如天气差查“博物馆”偏热查“水上乐园”晴好查“公园、名胜古迹”。3. 环境与依赖pipinstallrequests openai需要准备两个 Key并设置到环境变量# ModelScope 或其他兼容 OpenAI 接口的大模型服务exportMODELSCOPE_API_KEY你的大模型Key# 高德开放平台 Web服务 Key注意不是 JS API KeyexportAMAP_KEY你的高德Web服务KeyWindows PowerShell 可用setx MODELSCOPE_API_KEY你的大模型Keysetx AMAP_KEY你的高德Web服务Key4. 核心代码4.1 系统提示词规定 Thought/Action 格式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必须在同一行不要换行 - 当收集到足够信息可以回答用户问题时必须使用 Action: Finish[最终答案] 格式结束 请开始吧 4.2 工具一查询天气importrequestsdefget_weather(city:str)-str:通过 wttr.in 查询真实天气。urlfhttps://wttr.in/{city}?formatj1try:responserequests.get(url,timeout10)response.raise_for_status()dataresponse.json()current_conditiondata[current_condition][0]weather_desccurrent_condition[weatherDesc][0][value]temp_ccurrent_condition[temp_C]returnf{city}当前天气:{weather_desc}气温{temp_c}摄氏度exceptrequests.exceptions.RequestExceptionase:returnf错误:查询天气时遇到网络问题 -{e}except(KeyError,IndexError)ase:returnf错误:解析天气数据失败可能是城市名称无效 -{e}4.3 工具二高德 POI 景点推荐这是替换 Tavily 的关键部分。高德返回的是结构化 POI适合作业演示对国内网络也更友好。importosimportrequests AMAP_KEYos.environ.get(AMAP_KEY,)defget_attraction(city:str,weather:str)-str: 用高德地图 POI 查询景点根据天气选择更合适的 POI 关键词。 不访问 Google/Brave/Yahoo/Tavily。 w(weatheror).lower()bad_weatherany(kinwforkin[smok,haze,fog,pm2,霾,雾,烟,rain,雨,snow,雪,storm,雷])hotany(kinwforkin[hot,heat,高温,炎热,晴热])ifbad_weather:keywords博物馆advice当前天气不佳优先推荐室内场馆。elifhot:keywords水上乐园advice当前偏热优先推荐水上/室内或避暑场所。else:keywords公园、名胜古迹advice天气总体适合出行优先推荐户外公园/景点。ifnotAMAP_KEY:return(f{city}当前天气为{weather}。未配置 AMAP_KEY临时建议天气差选博物馆/美术馆/图书馆天气好选城市公园/地标/历史街区。)try:rrequests.get(https://restapi.amap.com/v3/place/text,params{key:AMAP_KEY,keywords:keywords,city:city,citylimit:true,offset:5,page:1,extensions:base,output:json,},timeout8,)r.raise_for_status()datar.json()ifdata.get(status)!1:return(f高德POI查询失败{data.get(info)}infocode{data.get(infocode)}。f临时建议{advice}也可选择{city}本地标志性景点。)poisdata.get(pois,[])[:5]ifnotpois:returnf{city}当前天气为{weather}。高德未返回{keywords}结果。{advice}lines[]forpinpois:namep.get(name,未知)addrp.get(address,地址未知)ifisinstance(addr,list):addrlines.append(f-{name}{addr})returnf{city}当前天气为{weather}。{advice}\n根据高德POI推荐\n\n.join(lines)exceptExceptionase:returnf{city}当前天气为{weather}。高德POI查询异常{type(e).__name__}:{e}。临时建议{advice}available_tools{get_weather:get_weather,get_attraction:get_attraction,}4.4 兼容 OpenAI 接口的大模型客户端fromopenaiimportOpenAIclassOpenAICompatibleClient:调用任何兼容 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:print(正在调用大语言模型...)try:responseself.client.chat.completions.create(modelself.model,messages[{role:system,content:system_prompt},{role:user,content:prompt}],streamFalse)print(大语言模型响应成功。)returnresponse.choices[0].message.contentexceptExceptionase:print(f调用LLM API时发生错误:{e})return错误:调用语言模型服务时出错。配置时改成环境变量读取importos API_KEYos.environ.get(MODELSCOPE_API_KEY,)BASE_URLhttps://api-inference.modelscope.cn/v1MODEL_ID你的课程模型ID# 以课程/平台实际可用模型为准ifnotAPI_KEY:raiseValueError(请先设置 MODELSCOPE_API_KEY)llmOpenAICompatibleClient(modelMODEL_ID,api_keyAPI_KEY,base_urlBASE_URL)4.5 主循环与解析器加固教程原版有两个容易踩坑的点Action: (.*)加re.DOTALL可能把 Action 后面的多行内容一起吞掉re.match(rFinish\[(.*)\], action_str)默认不匹配换行模型把最终答案写成多行时会得到None进而触发AttributeError: NoneType object has no attribute group。我改成按行取 Action同时对Finish[...]使用re.DOTALLimportre user_prompt你好请帮我查询一下今天银川的天气然后根据天气推荐一个合适的旅游景点。prompt_history[f用户请求:{user_prompt}]print(f用户输入:{user_prompt}\n*40)foriinrange(5):print(f--- 循环{i1}---\n)full_prompt\n.join(prompt_history)llm_outputllm.generate(full_prompt,system_promptAGENT_SYSTEM_PROMPT)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(已截断多余的 Thought-Action 对)print(f模型输出:\n{llm_output}\n)prompt_history.append(llm_output)action_matchre.search(r^Action:\s*(.)$,llm_output,re.MULTILINE)ifnotaction_match:observation错误: 未能解析到 Action 字段。请确保你的回复严格遵循 Thought: ... Action: ... 的格式。prompt_history.append(fObservation:{observation})continueaction_straction_match.group(1).strip()ifaction_str.startswith(Finish):mre.search(rFinish\[(.*)\]\s*$,llm_output,re.DOTALL)ifnotm:prompt_history.append(Observation: 错误: Finish 格式不完整请使用 Action: Finish[最终答案]。)continuefinal_answerm.group(1).strip()print(f任务完成最终答案:{final_answer})breaktool_matchre.search(r(\w)\((.*)\),action_str,re.DOTALL)ifnottool_match:observationf错误:无法解析工具调用 {action_str}else:tool_nametool_match.group(1)args_strtool_match.group(2)kwargsdict(re.findall(r(\w)\s*\s*[\]([^\]*)[\],args_str))iftool_nameinavailable_tools:try:observationavailable_tools[tool_name](**kwargs)exceptExceptionase:observationf错误:工具{tool_name}执行失败 -{type(e).__name__}:{e}else:observationf错误:未定义的工具 {tool_name}print(fObservation:{observation}\n*40)prompt_history.append(fObservation:{observation})5. 运行结果我将城市改成了“银川”实际运行输出如下用户输入: 你好请帮我查询一下今天银川的天气然后根据天气推荐一个合适的旅游景点。 --- 循环 1 --- 模型输出: Thought: 用户需要查询今天银川的天气并根据天气推荐合适的旅游景点。我先调用天气工具获取银川实时天气。 Action: get_weather(city银川) Observation: 银川当前天气:Clear 气温22摄氏度 --- 循环 2 --- 模型输出: Thought: 已获取银川实时天气为晴朗Clear气温22摄氏度。接下来根据城市和天气搜索推荐的旅游景点。 Action: get_attraction(city银川, weatherClear) Observation: 银川 当前天气为 Clear。天气总体适合出行优先推荐户外公园/景点。 根据高德POI推荐 - 西夏陵国家考古遗址公园西夏陵景区内 - 贺兰山岩画遗址公园贺兰山岩画内(南侧) - 明长城-银川段遗址公园明长城银川段西北侧 --- 循环 3 --- 模型输出: Thought: 已获取银川天气和适合晴朗天气的景点推荐可以回答用户。 Action: Finish[今天银川天气为晴朗Clear气温约22℃非常适合户外出行。推荐你去“西夏陵国家考古遗址公园”……] 任务完成最终答案: 今天银川天气为晴朗Clear气温约22℃非常适合户外出行。推荐你去“西夏陵国家考古遗址公园”可以在晴好天气下参观西夏历史文化遗址、感受户外风光。另外贺兰山岩画遗址公园和明长城-银川段遗址公园也很适合今天游览。可以看到虽然底层工具从 Tavily 换成了高德 POI但智能体的运行范式没有变仍然是先规划、再调用工具、读取 Observation、最后合成答案。6. 踩坑记录Tavily 注册失败不代表作业做不了。智能体重点是“循环”和“工具协议”搜索源可以替换。替换成高德后反而更适合国内课堂环境。高德 Key 类型要选 Web服务。如果选成 JS API、Android 或 iOS常见报错是INVALID_USER_KEY或USERKEY_PLAT_NOMATCH。不要把真实 Key 写进代码或文章。哪怕是作业截图也建议打码仓库提交前用环境变量读取。Finish[...]要支持多行。模型最终答案经常会写成多行如果正则不支持换行会在最后一步崩溃。最大循环次数必须保留。没有上限时一旦工具持续失败或模型不守格式智能体可能反复重试浪费 Token。7. 总结这次作业让我对第一章的概念有了更直观的理解LLM 智能体不是“单次问答”而是一个由Thought-Action-Observation驱动的闭环系统。Thought负责规划Action负责调用工具Observation把环境反馈重新交给模型。只要这个协议稳定底层工具是可以替换的天气可以用wttr.in景点搜索可以从 Tavily 换成高德 POI甚至换成地图、票务或本地知识库。后续如果继续完善我计划加两个能力一是“记忆”让智能体记住用户偏好比如喜欢历史文化还是自然风光二是“失败熔断”同一个工具连续失败两次后禁止原样重试直接基于已有信息给出结论并说明限制。参考Datawhale《Hello-Agents》第一章初识智能体高德开放平台 Web 服务 POI 搜索接口wttr.in 天气查询服务