☰
手写ReAct旅游规划智能体:零基础大模型Agent实战
2026/10/21 14:21:37 网站建设 项目流程

最近不少朋友问我,零基础到底怎么上手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对你就不再是黑盒,而是可以随意改造的基础能力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询