最近不少朋友问我,零基础到底怎么上手AI大模型。我的答案一直很固定:别急着啃论文,也别一上来就装各种复杂框架,找一个能快速跑起来的小场景,亲手做一个Demo。我最近刚完成的一个小项目就是标题里这个"旅游规划智能体",核心玩法是用react_agent模式让大模型自动完成一次旅行规划。这篇文章就把我从原理到代码、从踩坑到排错的完整过程复盘一遍,适合没有任何Agent开发经验的读者直接对照复现。
先解释一下react_agent这个名字,懂前端的同学别误会,它跟React框架没有任何关系。它指的是ReAct模式,即Reasoning(推理)+ Acting(行动),是当前构建智能体最基础也最实用的一种范式。我的做法很简单:给大模型配上几个旅游相关的查询工具,让它在生成答案前先"想清楚",再通过工具查到真实信息,最终输出一份可落地的旅行计划。整个项目不依赖任何Agent框架,纯粹用大模型API加几百行Python代码实现。
1. 项目从哪来:旅游规划智能体到底解决了什么问题
1.1 这个Demo的边界与目标
项目一开始,我给自己定了一个非常明确的目标:做一个"能用、能演示、能继续扩展"的旅游规划智能体,而不是做一个生产级的商业应用。用户只需要输入一句大概的需求,比如"带5岁孩子去海滨城市玩3天,预算6000块,不想太赶",智能体就应该自动完成下面这些事:
- 分析出行需求,确定目的地和大致日期
- 查询目的地的基础信息,比如最佳旅游季节、消费水平
- 查询天气预报,判断适不适合出行
- 筛选适合亲子的景点,过滤掉刺激性强或者路程过远的项目
- 查找合适的亲子餐厅,避开辣、生冷等不适合儿童的选项
- 查询城际和市内交通方案,计算通勤时间
- 把上面所有信息综合成一份包含每日安排、预算参考的行程表
这个边界很重要。我没有要求它能实时预订酒店、购买门票,也没有要求它接入真实地图。先把"大模型能够理解需求、调用工具、反思结果、输出结构化工件"这条链路跑通,后续再加什么功能都简单。
很多初学者做Agent项目容易犯一个错误:一上来就想做一个万能助理,结果工具堆了二三十个,提示词写了上千字,最后模型频繁用错工具,项目烂尾。我这次刻意收敛范围,只有5个工具,每个工具职责单一,这样模型调用的准确率会高很多。
1.2 纯对话式大模型为什么搞不定旅行规划
我在设计这个项目前,先用普通的对话方式测试过大模型。直接把需求丢给它,让它生成一份旅行计划,效果看起来很唬人,景点列表、餐厅推荐、预算估算都有,但只要认真核对就会发现不少问题。
最重要的问题是信息陈旧。大模型的训练数据有截止时间,它不知道某个景点是不是正在维修、某条地铁线是不是已经开通、某个餐厅是不是已经停业。它甚至会一本正经地编造一个不存在的景点,配上看起来完全合理的位置和开放时间。这就是所谓的大模型幻觉。
其次,纯对话方式缺乏数据来源。旅行规划本质上是一个需要大量实时信息的任务:天气、交通、营业时间、票价、适合年龄……这些信息模型在训练时见过一些,但都是片段化的,大概率是过时的。用户问"明天该穿什么",模型需要知道明天真实的天气。
ReAct模式恰恰解决了这个问题。它把"推理"和"信息获取"拆成了两个动作:模型负责推理——现在需要知道什么信息,下一步该做什么;工具负责获取——查询真实数据,把结果喂给模型。模型不再需要凭记忆编造,只需要基于事实做规划和判断。
1.3 这个项目涉及哪些关键技术点
零基础的人看到"智能体Agent"这个词容易发怵,觉得这是很高级的东西。拆开来看,这个项目其实只有四个核心部分组成:
- 大模型API调用:模型负责理解需求、推理决策、生成文本
- 工具定义:把外部能力包装成模型可以调用的函数
- 提示词设计:告诉模型它有哪些工具、按什么格式调用、什么情况下结束
- ReAct循环:一个简单的while循环,让模型反复"思考→调用工具→观察结果→再思考",直到给出最终答案
我用最朴素的Python代码把这四部分串起来,不借助任何Agent框架。这样做的好处是每一步都透明可见,出了问题你能清楚知道是哪一环的锅。等到你理解了ReAct的精髓,再去用那些封装好的框架也不迟。
而且这个模式不止能做旅游规划。给模型配上文件读写工具就是文档助手,配上数据库查询工具就是数据分析师,配上代码执行工具就是编程助理。学会ReAct,等于掌握了一套通用的Agent构造方法。
2. 核心原理:ReAct循环到底在做什么
2.1 一个循环、三个关键字段
ReAct的核心可以概括为三句话:先想,再做,看结果再想。
这完全模拟人类处理复杂任务的方式。比如我自己规划一次旅行,不会直接打开某个App下单,而是先想:"我需要知道目的地最近天气怎么样,孩子适不适合去,有什么景点适合亲子。"这是Thought(思考)。接着我打开天气App查一下未来几天的气温,这是Action(行动)。App弹出降水概率和温度曲线,我发现第二天下雨,这是Observation(观察)。于是我调整计划,把室外景点安排在第一天,再查一下室内儿童乐园,这是新一轮Thinking。
Agent里的ReAct循环就是把这个过程代码化。模型每一轮输出两个关键字段:
- Thought:模型用自然语言描述自己当前的想法,比如"用户带5岁孩子出行,我需要先确认目的地天气"
- Action:模型输出一个结构化的JSON,声明要调用哪个工具、传入什么参数
程序解析出Action之后,执行对应的工具函数,拿到结果回来作为Observation,拼进对话历史,再交给模型继续推理。如此反复,直到模型输出一个特殊动作:Final Answer。
这里有个容易忽略的细节:Thought不是给用户看的装饰,它是模型推理链的一部分。大模型在生成Thought时相当于把"当前状态评估"和"下一步计划"显式表达了出来,这能显著降低它在复杂任务中出错的比例。你可以做个对比实验,不要求模型输出思考过程,直接让它给出最终答案,效果通常会差很多,尤其是在需要多步工具调用的场景。
2.2 为什么推理加行动能减少幻觉
我刚开始接触这套模式时有一个疑问:大模型本来就会编造,让它在思考之后调用工具,它不还是可能编造工具结果吗?
实验做下来你会发现,答案是否定的。大部分情况下不会,原因是设计上就把"编造空间"堵死了。
程序执行Action后,工具返回的是程序代码生成的真实结果,这段结果被明确标注为Tool Observation,拼接到对话里。模型在最终回答时必须引用这些观察结果,而不能凭空捏造。我在系统提示词里写得很清楚:"所有景点、天气、价格信息必须来自工具返回结果,不得编造。"模型遵循这个约束的概率很高,因为整个对话的上下文已经给了它充分的"事实材料",它只需要组织语言即可。
当然,工具返回的实时信息来自模拟数据源,真实度取决于你接入的数据源。但至少从机制上,ReAct割断了模型直接凭记忆编造实时信息的通道。你要做的,是让后面的工具执行环节返回高质量数据。
2.3 可观测性与灵活扩展的优势
ReAct还有一个很容易被忽略的优点是全程可观测。普通的大模型对话是一个黑盒:输入问题,输出答案,中间发生了什么你完全不知道。ReAct不同,每一轮Thought、Action、Observation都会打印出来,你可以清楚看到模型是经过怎样的路径得到最终结论的。
这个特性在调试时救了我很多次。有一次Agent反复调用同一个景点查询工具却不进入下一步,我查看轨迹后发现是工具描述写得有歧义,模型误以为还需要继续查。改了一句话之后问题立刻消失。如果没有轨迹日志,这种问题基本没法定位。
灵活性方面,新增工具非常容易。我只需要在工具注册表里加一个函数,再在系统提示词的工具列表里加一行描述,Agent立刻就会多出一项能力。这就跟人一样,你多学一个技能,以后做事时就多一种手段。我后来给Agent加过一个餐厅查询工具,改动不到20行代码,它马上能在规划行程时主动把"吃"这个维度考虑进去。
2.4 ReAct与其他Agent范式的对比
零基础学习阶段只做ReAct是完全够用的。但搞清楚它和其他范式的区别能帮你建立完整认知。业界常听到的Plan-and-Execute模式,会让模型先制定一个多步计划,再逐步执行,适合目标非常明确的任务;Reflexion模式则在ReAct基础上增加自我反思机制,失败后总结经验重新尝试,适合高难度推理。
ReAct的特点是灵活、简单、路径动态。它不需要预先规划所有步骤,模型每一步根据当前观察结果动态决定下一步。这个特性让它特别适合旅游规划这类开放度高的任务——用户需求可能随时变化,中途发现天气不好需要临时改行程,ReAct天然支持这种动态调整。
3. 动手准备:工具选型与大模型API封装
3.1 技术栈为什么这样选
整个项目我只依赖两样东西:一个可用的大模型API,以及Python标准库。没有引入任何第三方Agent框架。
我刻意这样选。零基础学习者最容易被各种框架搞得晕头转向,框架里有Agent、Tool、Memory、Callback一堆抽象概念,还没开始写业务逻辑就先陷进概念海洋。用最简单的技术栈,你可以把注意力全部放在ReAct模式本身。
大模型API我用的是一个统一的封装函数llm_chat,它的功能只有一个:传入消息列表,返回模型生成的文本。这个函数内部的实现方式对Agent逻辑完全不重要,只要你手上有一个能对话的大模型API,套进这个函数就能跑。
环境方面只需要Python 3.9以上,装一个requests库就够。代码我组织成四个文件,结构非常清晰:tools.py放工具定义,prompts.py放提示词,agent.py放ReAct主循环,config.py放配置参数。
travel_agent_demo/ ├── agent.py # ReAct主循环 ├── tools.py # 工具函数定义 ├── prompts.py # 系统提示词模板 └── config.py # API配置与参数3.2 旅游场景工具清单一览
我给智能体设计了5个工具,每个对应旅行规划中的一个关键信息维度。
- get_destination_info:查询目的地城市的基本情况,包括城市特点、建议游玩天数、消费水平
- get_weather:查询指定城市在指定日期的天气,包括气温、降水概率、风力
- search_attractions:查询一个城市的景点列表,返回每个景点的名称、类型、建议游玩时长、适合人群、门票参考价
- search_restaurants:查询指定区域的餐厅,返回菜系、人均消费、亲子友好程度
- get_transport:查询两个地点之间的交通方式和大致耗时,也支持查询市内通勤
工具数量的选择我斟酌过。最初想加一个酒店查询工具,但考虑到行程规划阶段不需要精确到具体酒店,只要给出住宿的区域建议即可,所以砍掉了。少一个工具,模型就少一分用错的概率。
每个工具在注册表里都有三个关键信息:真实的可调用函数、自然语言描述、参数说明。模型看到的是描述和参数说明,程序执行的是真实函数。描述写得好不好,直接决定模型能不能正确使用工具。
3.3 定义工具的标准格式与参数规范
工具函数本身很简单,核心是把返回结果处理成JSON字符串。我举一个天气查询的例子:
# tools.py import json import datetime def get_weather(city: str, date: str) -> str: """ 查询城市天气。这是模拟实现, 实际使用时可以替换为真实天气API。 """ # 模拟数据,实际项目在这里请求外部天气服务 weather_db = { ("阳海市", "2025-06-01"): {"weather": "多云", "high": 28, "low": 22, "rain_prob": 10}, ("阳海市", "2025-06-02"): {"weather": "小雨", "high": 25, "low": 21, "rain_prob": 70}, } key = (city, date) data = weather_db.get(key, {"weather": "晴", "high": 26, "low": 20, "rain_prob": 20}) return json.dumps({"city": city, "date": date, **data}, ensure_ascii=False)这个函数的特点是:入参简单清晰,返回结果用JSON格式。模型在拿到JSON字符串后,可以直接提取关键数据进行后续推理。
工具注册表则长这样:
# tools.py TOOL_REGISTRY = { "get_weather": { "function": get_weather, "description": "查询指定城市、指定日期的天气情况,返回气温、降水概率、风力等级。", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称"}, "date": {"type": "string", "description": "日期,格式YYYY-MM-DD"} }, "required": ["city", "date"] } }, # 其余工具类似,这里省略 }一开始我犯过一个错误:工具描述写得太简略,比如只写"查询天气",结果模型经常把参数传错。后来我把描述改成更详细的说明,不仅说明功能,还说明使用场景,比如在合适的地方提示"当需要判断某天是否适合户外活动时使用"。这之后模型的调用准确率明显上升。
工具返回的数据也需要考虑模型友好的问题。纯文本描述返回值对模型理解没问题,但JSON结构化的返回值能让模型更快定位关键字段。我在每个工具返回里都保证字段名语义明确,比如rain_prob比precipitation_probability好理解得多。
4. 核心实现:手写一个react_agent旅游规划智能体
4.1 大模型API的统一封装
在写主循环之前,先搞定大模型API封装。我在config.py里放好配置项,在agent.py里写封装函数:
# config.py MODEL_NAME = "your-model-name" # 换成你实际可用的模型名 API_BASE = "https://your-api-endpoint" # 换成你实际使用的API地址 API_KEY = "your-api-key" # 换成你的API密钥 MAX_STEPS = 8 TEMPERATURE = 0.2# agent.py import json import requests def llm_chat(messages, temperature=TEMPERATURE, max_tokens=2048): """调用大模型API。这里是通用封装,请按你的API要求调整请求体。""" headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json" } payload = { "model": MODEL_NAME, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, "stop": None } # 注意:不同大模型API的请求格式有差异,这里以最常见的结构示例 try: resp = requests.post(API_BASE, headers=headers, json=payload, timeout=30) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"] except Exception as e: print(f"[LLM调用失败] {e}") return ""为什么要单独封装这一层?因为后续所有代码都只调用llm_chat这个函数,如果哪天你想换一个模型API,只需要改这一个函数,ReAct主循环完全不用动。
4.2 编写ReAct主循环
这是整个项目的核心,逻辑用一个while循环就能说清楚:
# agent.py from tools import TOOL_REGISTRY from prompts import SYSTEM_PROMPT def parse_action(text): """从模型输出中解析动作JSON""" start = text.find("```json") if start != -1: text = text[start + 7:].split("```")[0].strip() else: # 去掉常见的自然语言前缀和多余字符 start = text.find("{") end = text.rfind("}") + 1 if start == -1 or end == 0: return None text = text[start:end] try: return json.loads(text) except json.JSONDecodeError: return None def execute_tool(name, arguments): """执行工具函数,返回观察结果""" if name not in TOOL_REGISTRY: return json.dumps({"error": f"未知工具: {name}"}, ensure_ascii=False) tool_func = TOOL_REGISTRY[name]["function"] try: result = tool_func(**arguments) return result except Exception as e: return json.dumps({"error": f"工具执行异常: {e}"}, ensure_ascii=False) def run_agent(user_query, max_steps=MAX_STEPS): messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_query} ] for step in range(1, max_steps + 1): print(f"\n===== Step {step} =====") response = llm_chat(messages) print("模型输出:", response) action = parse_action(response) if action is None: # 解析失败,给模型一个纠错机会 messages.append({"role": "assistant", "content": response}) messages.append({"role": "user", "content": "请严格按照JSON格式输出你的动作。"}) continue if action.get("type") == "final": return action.get("content", "") # 否则当作工具调用处理 tool_name = action.get("name", "") tool_args = action.get("arguments", {}) print(f"调用工具: {tool_name}, 参数: {tool_args}") observation = execute_tool(tool_name, tool_args) print("工具返回:", observation) messages.append({"role": "assistant", "content": response}) messages.append({"role": "tool", "content": observation}) return "Agent执行超过最大步数,未能生成最终计划。" if __name__ == "__main__": query = "帮我规划阳海市3日亲子游,孩子5岁,预算6000元,不想太赶" result = run_agent(query) print("\n===== 最终输出 =====") print(result)这段代码里隐藏了几个关键设计点,新手容易忽略。第一,我把模型的原始输出完整追加到消息列表里,再紧跟一条tool角色的消息。这个顺序对模型理解很重要:它先看到自己说了什么,再看到工具返回了什么,形成完整的因果链。
第二,parse_action函数做了容错。真实场景中,模型经常不会老老实实只输出一个JSON,它可能会先说一句"好的,我来查询天气",然后才输出JSON。如果解析失败,我让模型重新输出,而不是直接崩溃,这大大提升了稳定性。
第三,execute_tool内部做了异常捕获。即使工具函数抛了异常,也会被转换成一条error信息返回给模型,模型会发现"工具调用出错了",进而选择重试或换一种方式。这比让整个程序崩溃要友好得多。
4.3 关键的系统提示词设计
提示词是ReAct Agent的灵魂,它的作用是让模型理解自己的身份、拥有的工具、输出格式和结束条件。我贴出实际使用的提示词,并标出关键设计点:
# prompts.py SYSTEM_PROMPT = """ 你是一个专业的旅游规划智能体。你的任务是根据用户需求,利用提供的工具查询实时信息,制定出合理、可执行的旅行计划。 你可以使用以下工具,每次只能调用一个: 1. get_destination_info: 查询目的地的基础信息,包括城市特点、建议游玩天数、消费水平。当需要了解目的地整体情况时使用。 2. get_weather: 查询指定城市、指定日期的天气情况。当需要判断某天是否适合户外活动、需要携带哪些衣物时使用。 3. search_attractions: 查询一个城市的景点列表,返回景点类型、建议游玩时长、适合人群、门票价格。当需要筛选景点时使用,注意结合用户的亲子、老人等特殊需求。 4. search_restaurants: 查询指定地点附近的餐厅,返回菜系、人均消费、亲子友好程度。当需要安排餐饮时使用。 5. get_transport: 查询两个地点之间的交通方式和大致耗时,也支持查询同一城市内的通勤。当需要安排行程衔接时使用。 你的工作流程: 1. 先理解用户需求,拆解出关键约束:人数、年龄、预算、天数、偏好、城市。 2. 每次行动前,用一句话说明你的思考过程。 3. 需要信息时,输出一个JSON动作,格式为:{"type": "tool", "name": "工具名", "arguments": {"参数1": "值1"}} 4. 收到工具结果后,立刻根据结果继续思考,决定下一步行动。 5. 当信息收集完成时,输出最终规划:{"type": "final", "content": "完整的旅行计划"} 硬性约束: - 所有景点、天气、餐厅、交通信息必须来自工具返回结果,绝对不允许编造。 - 如果某工具调用失败,检查参数后重试最多两次,仍失败则更换策略。 - 不要重复调用同一个工具查询同样的内容。 - 用户预算有限时,优先推荐门票价格合理、性价比高的景点。 - 亲子游场景,不推荐刺激性强、路程过远、不适合低龄儿童的景点。 - 最终计划必须包含每日行程安排、备选方案、预算参考。 """提示词有几个细节值得单独讲。
"每次只能调用一个工具"这句话很重要。很多大模型API虽然支持并行工具调用,但在零基础项目里,串行调用更容易排查问题。每走一步,你都能确认这一步的结果,再决定下一步。
工作流程部分相当于给模型一个操作手册。模型其实是一个"人在回路"的模仿者,你写清楚流程,它就会按流程执行。你希望它先理解需求再动手,就把"先理解需求,拆解约束"放在流程第一步。
硬性约束部分是防幻觉的关键。我明确告诉模型"必须来自工具返回结果"、"不要重复调用同一个工具"、"重试最多两次"。这些规则模型大体上会遵守,偶尔会有偏差,但在后面配合解析逻辑兜底,基本不会失控。
4.4 工具执行细节:模拟数据也能跑通全流程
由于是Demo项目,我没有真的去申请各种天气、景点API,而是直接写了模拟数据源。每个工具内部都维护着一个字典,存储了"阳海市"这个虚构城市的完整旅游数据。
模拟数据这个做法看起来朴素,但对零基础学习反而是优点。真实API有访问频率限制、有付费限额,调试一次要等很久,还可能在演示现场掉链子。模拟数据让整个项目随时随地能跑,等学会了整套逻辑,再替换数据源就行。
景点工具我做得稍微复杂一些。景点信息里包含一个"适合人群"字段,主要目的是测试模型能否结合用户需求筛选。比如用户带5岁孩子,模型看到某个景点标注"适合成人,惊险刺激类",就应该把它排除掉。这个测试点在完整演示中确实触发了,效果很好。
5. 实战演示:让Agent规划一次亲子海滨3日游
5.1 启动智能体,输入用户需求
我输入的需求是:
帮我规划阳海市3日亲子游,孩子5岁,预算6000元,不想太赶,最好6月1日出发。
这个需求包含了几个约束条件:目的地是阳海市,时长3天,有5岁孩子,预算6000,节奏慢,出发日期明确。理想情况下,Agent应该逐一提炼这些信息,并在后续决策中时刻回扣。
5.2 完整的运行轨迹拆解
我把运行轨迹一步步拆开看。
第一步,模型输出Thought:"用户带孩子出行,孩子只有5岁,需要重点关注亲子友好项目。我先查询阳海市的基础信息,了解这个城市的整体特点和推荐游玩方向。"对应Action是调用get_destination_info。工具返回了城市概况:海滨城市,沙滩平缓,适合亲子,消费中等。
这个环节的作用是让模型先建立对目的地的基本认知。如果模型连目的地是什么类型都不清楚,后面筛选景点就容易瞎猜。
第二步,模型看到目的地是海滨城市,决定查看6月1日和6月2日的天气:"海边活动对天气依赖很大,如果有雨就需要调整室内外项目顺序。"于是调用get_weather查询两天天气。工具返回6月1日多云、气温适宜,6月2日有小雨。
这时候模型做了一个关键决策:"6月2日有雨,我应该在1日安排主要的户外海边活动,2日转为室内项目。"这一步充分体现了ReAct的价值:它会根据观察结果动态调整策略,而不是机械地按固定模板排行程。
第三步,模型查询阳海市的景点列表。search_attractions返回了8个景点,模型从中筛选出3个:海滨公园、海洋馆、儿童科技馆,并排除了刺激水上项目和远郊景区。它的思考过程完全扣住了"5岁孩子"这个约束。
第四步,模型觉得餐饮信息还不够,调用search_restaurants查询了景区附近适合亲子的餐厅,重点关注是否有儿童餐、口味是否清淡。
第五步,模型调用get_transport查询了主要景点之间的交通耗时,判断每天的路程安排是否会在1小时内,确保不赶。
至此,模型认为信息收集完毕,输出final类型的最终规划。
这个运行轨迹看似自然,其实每一步都经过了思考。换成一个纯对话模型,它不会主动去查天气,不会因为下雨调整项目顺序,可能直接给你一个模板化行程表。我拿同一需求测试过,差距非常明显。
5.3 最终输出的旅行计划长什么样
最终输出我用一个结构化文本返回,这里节选核心部分:
【阳海市3日亲子游规划(6月1日-6月3日)】 出行信息: - 人数:2大1小(孩子5岁) - 预算:6000元 - 节奏:轻松休闲,每日最多2个核心项目 Day1(6月1日,多云,26-28摄氏度): - 上午:海滨公园沙滩游玩,海边散步,让孩子玩沙 - 午餐:海滨公园附近亲子餐厅 - 下午:海洋馆,观看海洋动物表演 - 晚餐:市区海鲜餐厅(清淡口味) - 住宿:建议住在海滨公园附近区域,方便第二天出行 Day2(6月2日,小雨,21-25摄氏度): - 上午:儿童科技馆,室内为主,不受雨天影响 - 午餐:科技馆附近商场亲子餐厅 - 下午:返回酒店休息,傍晚在室内儿童游乐区活动 - 晚餐:酒店周边粤菜馆 - 提示:降雨天气注意携带雨具和薄外套 Day3(6月3日,多云,26摄氏度): - 上午:海滨栈道散步,逛本地老街 - 午餐:老街特色小吃 - 下午:根据回程交通时间,灵活安排海滩短时停留 预算参考: - 住宿:2晚,约1800元 - 餐饮:3天,约1500元 - 门票:约400元 - 交通:约800元 - 其他及备用:约1500元这个输出的质量超出了我最初的预期。模型不仅排了行程,还根据天气预报把室内外项目做了适配,甚至在餐饮和住宿安排上都有呼应。它能做到这一步,核心原因是整个流程中每一步都基于工具返回的真实数据,而不是凭空想象。
6. 常见问题与排查技巧实录
6.1 模型不按格式输出工具调用怎么办
我在开发过程中遇到最多的一个问题就是:模型有时候不输出JSON动作,而是直接输出一大段自然语言。比如模型可能会说"好的,让我先看看阳海市的天气情况",然后就没有然后了。
排查之后发现原因主要有三个:一是提示词里的JSON示例不够醒目,模型没有理解输出格式要求;二是模型能力较弱,对复杂指令遵循度不高;三是温度参数设置太高,导致输出随机性过大。
解决思路是多管齐下。第一,优化提示词,在格式说明处用json代码块强调,并给出一个完整示例。第二,把温度降到0.2左右,让输出更稳定。第三,在代码里做容错:调用parse_action解析不成功时返回None,触发系统提醒"请严格按照JSON格式输出",并给模型一次纠错机会。
6.2 Agent进入死循环,反复调用同一个工具
另一个高频问题是模型卡在某一步,反复调用同一个工具,比如连续三次查询同样的景点信息,就是不肯进入下一步。
我排查后发现,这通常是因为模型觉得"信息还不够",但它又不知道具体缺什么。我采取的措施有两条。第一,在提示词里明确写"不要重复调用同一个工具查询同样的内容"。第二,在执行层做限制:execute_tool里维护一个调用记录,如果同一个工具对同样的参数调用了两次,就直接返回一个警告信息,提示模型换一种方式。
还有一个策略是控制max_steps。我的主循环设置最大8步,如果超过步数就直接退出返回提示。这个机制是兜底,保证Agent不会无限循环烧API费用。零基础同学务必加上这个限制。
6.3 幻觉数据混入最终输出
即使有工具提供实时数据,模型偶尔还是会把记忆里的信息混进来。比如它可能会在行程里推荐一个工具结果中完全没有的景点,或者把某家餐厅的评分凭空提高。
针对这个问题,我在提示词里加了非常硬的约束:"所有景点、天气、餐厅、交通信息必须来自工具返回结果,绝对不允许编造。"同时,我让工具返回的数据足够丰富,让模型觉得"不需要"编造也有足够信息可用。
另外,我在运行逻辑里加了输出校验:最终计划文本生成后,简单检查一遍,如果出现工具返回数据里不存在的地点名称,就提示模型修正。虽然这个检查做不到完全自动,但作为辅助手段能显著减少幻觉输出。
6.4 上下文越来越长,怎么办
ReAct每执行一轮,就会往消息列表里追加一条模型输出和一条工具返回,多轮之后上下文会越来越长。我在测试时跑过12步的对话,token消耗明显增加,而且模型对早期信息的关注度会下降。
最直接的控制方法是减少max_steps,一般8步足够完成旅游规划。如果需要更多轮次,可以考虑只保留最近几轮的消息,或者让工具返回精简数据而不是长文本。在我的场景里,工具返回的JSON被刻意设计得短小精悍,这本身就在控制上下文膨胀。
6.5 大模型API调用出错的排查思路
API调用失败是新手最常碰见的拦路虎。常见的报错类型包括鉴权失败、请求超时、模型名错误、请求格式不对。我给llm_chat函数包了一层异常捕获,打印出错误信息,这样在终端里能直接看到失败原因。
这里分享一个排查技巧:先用一个最简单的消息列表"hello"测试API连通性,通了之后再跑完整Agent。别等到Agent跑了好几步才报错,那样你都不知道是API问题还是Agent逻辑问题。
6.6 问题排查速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 模型输出自然语言而非JSON | 提示词不明确 | 增加JSON格式示例,降低temperature |
| 反复调用同一工具 | 工具描述误导 | 检查description是否有歧义 |
| 输出出现幻觉信息 | 约束不足或数据不足 | 强化硬性约束,丰富工具返回数据 |
| 上下文越来越长 | 步数过多或返回数据冗长 | 控制max_steps、精简工具返回 |
| API频繁报错 | 密钥、模型名、格式错误 | 先用单条消息测试连通性 |
7. 我的实操体会与后续扩展方向
这个项目做下来,我最大的体会是:零基础学AI大模型,最忌讳的是追求一步到位。你先用模拟数据把一个ReAct Agent跑通,看到模型真的会调用工具、会根据结果调整计划,那种感觉和纯粹读理论完全不一样。这个正反馈很重要,它会支撑你继续学下去。
如果后续想继续扩展这个项目,我建议按以下顺序逐个做:把模拟数据换成真实天气API,让天气信息真正实时化;给Agent增加酒店预订和门票预订工具,把规划落地;接入语音输入,让用户说话就能完成规划;再加一层多Agent协作,比如一个Agent负责目的地研究,另一个负责预算控制,最后统一合并成计划。
但扩展之前,请先确保你真正吃透了这四件事:工具定义为什么用JSON格式、提示词里哪些约束是必要的、parse_action的容错是怎么运作的、执行工具的异常如何返回给模型。这四个点想明白,ReAct对你就不再是黑盒,而是可以随意改造的基础能力。