摘要本文深入拆解SmartVoyage智途v7.8的核心推理引擎——从意图识别的Prompt工程、三分支决策树到ReAct循环的依赖分组并行执行与反思机制。全文基于项目源码逐层展开配合流程图与关键代码片段帮助读者理解大模型是如何一步步思考并执行复杂多意图任务的。关键词ReAct、意图识别、Prompt工程、多Agent协作、A2A协议、依赖分组并行一、引言在上一篇文章中我们从宏观视角了解了SmartVoyage的六层七服务架构。但一个核心问题始终没有展开——当用户说出帮我查明天北京到上海的机票顺便看看上海天气再推荐几个景点这样一句包含三个意图的话时系统是怎么理解的又是怎么规划和执行的答案就在本文要深入剖析的推理核心层——VoyageChatCore类中。它是整个系统的大脑负责意图识别、任务规划、ReAct循环执行和结果汇总四大核心步骤。本文将从源码出发逐行拆解这个大脑的运作机制。二、推理引擎全景一次对话经历了什么在深入代码之前先用一张流程图建立全局认知。用户发送一条消息后系统内部会经历以下决策链路图1推理引擎决策链路全景——从用户输入到最终回复的每一步可以看到整个推理过程可以概括为识别→分支→执行→汇总四个阶段。下面逐一展开。三、意图识别让大模型充当翻译官3.1 为什么需要意图识别用户的自然语言是模糊的、多义的。一句帮我订明天去北京的机票对系统来说需要回答三个问题用户想做什么→ 意图order flight tickets具体查什么→ 改写查询明天(2026-08-17)出发目的地北京信息够不够→ 是否需要追问这就是意图识别模块的职责——把自然语言翻译成结构化的JSON三元组。3.2 意图识别的Prompt设计意图识别是整个系统中最考验Prompt工程功底的环节。SmartVoyage的做法是向LLM注入7个上下文变量让模型在充分理解场景的前提下做出判断# core/chat_service_core.py — _intent_agent() async def _intent_agent(self, user_input: str): chain SmartVoyagePrompts.intent_prompt() | self.llm current_date datetime.now(pytz.timezone(Asia/Shanghai)).strftime(%Y-%m-%d) intent_response (await chain.ainvoke({ available_tools: self._available_tools_text, # ① 可用工具列表 intent_info: self._intent_info_text, # ② 意图标识→释义 conversation_history: self.memory.get_short_term_text(), # ③ 对话历史 query: user_input, # ④ 当前查询 current_date: current_date, # ⑤ 当前日期 user_profile: self.memory.get_profile_text(), # ⑥ 用户偏好 task_context: json.dumps(self.memory.current_task, ensure_asciiFalse), # ⑦ 任务上下文 })).content.strip()这7个变量的作用如下表变量来源作用available_toolsAgentNetwork动态收集告诉LLM有哪些Agent和Skill可用intent_info从AgentCard.skills提取意图标识→自然语言释义映射conversation_historyMySQL短期记忆让LLM理解多轮对话上下文query用户当前输入本次需要分析的核心文本current_date系统时间解析明天下周等时间词user_profileMySQL用户偏好如偏好二等座经济舱task_contextMySQL任务上下文上一次任务的状态信息3.3 意图识别Prompt模板全文下面是intent_prompt()的完整模板这是系统中最长也最关键的Prompt# core/prompts.py — intent_prompt() 系统提示 角色您是一个专业的旅行意图识别专家 任务基于用户查询、对话历史和用户偏好识别其意图 用于调用专门的agent server来执行 为方便后续的agent server处理可以基于对话历史对用户查询进行改写。 严格遵守规则 - 支持意图及释义从子代理技能动态获取 {intent_info} 所支持意图从上述清单中提取意图标识如 query weather / order car rental 等 或其组合如 [query weather, attraction]。 如果意图超出范围返回 out_of_scope。 - 注意购买预定和查询要区分开 涉及购买、预定时以order开头仅查询以query开头。 - **查询改写是关键环节** 你必须主动从对话历史中提取关键信息出发城市、到达城市、日期等 补充到 user_queries 的改写查询中。 即使当前查询很简短如好的、明天也要结合历史形成完整查询。 - **追问消息的使用** 只有在即使结合对话历史仍然缺少必要信息时才追问。 如果历史中已有足够信息不要追问。 - 输出严格为JSON {{intents: [...], user_queries: {{...}}, follow_up_message: ...}} 绝对不要添加额外文本 当前可用代理及工具{available_tools} 用户偏好{user_profile} 当前任务上下文{task_context} 对话历史{conversation_history} 用户查询{query} 设计要点解读动态注入而非硬编码intent_info和available_tools都是从config.yaml和AgentCard动态获取的新增Agent后无需修改Prompt代码。查询改写规则这是最容易被忽略但极其重要的设计。用户说好的、明天这种省略式回复时LLM必须结合对话历史补全信息否则下游Agent无法执行。严格的JSON输出约束Prompt末尾反复强调输出严格为JSON、绝对不要添加额外文本因为后续代码直接json.loads()解析任何多余文字都会导致解析失败。order与query的区分订票和查票路由到不同的Skill必须让LLM明确区分。3.4 输出三元组与三分支决策LLM返回的JSON被解析为三元组intent_output json.loads(intent_response) intents intent_output.get(intents, []) # 意图列表 user_queries intent_output.get(user_queries, {}) # 改写查询字典 follow_up_message intent_output.get(follow_up_message, ) # 追问消息以实际例子说明三种输出情况情况一需要追问{ intents: [], user_queries: {}, follow_up_message: 请问您想查询哪个城市的天气 }→ 直接返回追问消息不进入执行阶段。情况二单一意图{ intents: [query weather], user_queries: {query weather: 北京2026-08-17的天气预报}, follow_up_message: }→ 直接调用_run_single_intent(query weather, 北京2025-08-16的天气预报)。情况三多意图{ intents: [query flight tickets, query weather, attraction], user_queries: { query flight tickets: 明天北京到上海的机票, query weather: 上海2026-08-17的天气, attraction: 上海热门景点推荐 }, follow_up_message: }→ 进入规划ReAct循环。三分支决策的代码实现非常简洁# core/chat_service_core.py — chat() if out_of_scope in intents or follow_up_message: return follow_up_message # 追问/拒绝 if len(intents) 1: response await self._run_single_intent(intents[0], qs) # 单意图 else: plan await self._plan_or_reflect(intents, user_queries) response await self._react_loop(plan, intents, user_queries) # 多意图四、意图路由从意图标识到具体Agent意图识别完成后系统需要知道这个意图该交给谁处理。这就是意图路由的职责。4.1 路由映射表的构建路由映射表在系统启动时自动构建不需要人工维护# core/chat_service_core.py — _load_intent_info() def _load_intent_info(self): intent_agentname_dict {} # 技能名 → 代理名 skillname_info_dict {} # 技能名 → 技能描述 for agent_name in self._network.agents.keys(): card self._network.get_agent_card(agent_name) for skill in card.skills: intent_agentname_dict[skill.name] agent_name skillname_info_dict[skill.name] skill.description # 特殊景点推荐由本地LLM直接处理 intent_agentname_dict[attraction] __local__ return intent_agentname_dict, _intent_info_text最终生成的映射表如下query weather → WeatherQueryAssistant query train tickets → TicketAssistant query flight tickets → TicketAssistant query concert tickets → TicketAssistant order train tickets → TicketAssistant order flight tickets → TicketAssistant order concert tickets → TicketAssistant query car rental → TripAssistant query tour group → TripAssistant query insurance → TripAssistant order car rental → TripAssistant order tour group → TripAssistant attraction → __local__本地LLM4.2 路由执行本地 vs 远程# core/chat_service_core.py — _run_single_intent() agent_name self.intent_agentname_dict.get(intent, None) if agent_name __local__: # 本地AgentLLM直接生成景点推荐 return await self._local_agent(query_str, intent) # 远程Agent通过A2A协议调用 agent self._network.get_agent(agent_name) msg Message(contentTextContent(textquery_str), roleMessageRole.USER) task Task(idtask- str(uuid.uuid4()), messagemsg.to_dict()) raw_response await asyncio.wait_for(agent.send_task_async(task), timeout150)这里有一个巧妙的设计景点推荐不需要查数据库LLM本身就有足够的地理知识所以标记为__local__直接本地处理省去了A2A→MCP→SQL的整条链路开销。五、任务规划把意图拆解为可执行步骤当用户有多个意图时系统不会一股脑全部执行而是先让LLM做一个执行计划。5.1 规划Prompt# core/prompts.py — reflect_plan_prompt() 系统提示你是任务反思及任务规划专家。 规则 1. 首次规划已完成步骤为空必须返回步骤数组 将每个识别到的意图拆解为至少一个步骤绝对不能返回空数组 [] 2. 反思阶段已完成步骤不为空 如果所有意图的信息已经齐全返回空数组 [] 否则返回新的完整步骤数组 3. 每个步骤指定 - step: 步骤序号从1开始 - action: 具体动作 - intent: 对应的意图 - depends_on: 依赖的前置步骤序号无依赖则为0必须是数字 对话历史{conversation_history} 当前用户查询{query} 识别到的意图{intents} 用户查询改写{user_queries} 已完成步骤及结果{observations} 输出严格 JSON[] 或 [{step: 1, action: ..., intent: ..., depends_on: 0}, ...] 关键设计同一个Prompt模板服务两种场景——首次规划时observations为空反思阶段observations有内容。LLM根据这个字段的有无自动切换行为。5.2 规划输出示例假设用户说帮我查明天北京到上海的机票和上海天气LLM首次规划输出[ { step: 1, action: 调用TicketAssistant查询北京到上海的机票, intent: query flight tickets, depends_on: 0 }, { step: 2, action: 调用WeatherQueryAssistant查询上海天气, intent: query weather, depends_on: 0 } ]depends_on全为0表示两个步骤互相独立可以并行执行。如果用户说先查天气如果下雨就帮我订租车LLM可能输出[ { step: 1, action: 调用WeatherQueryAssistant查询目的地天气, intent: query weather, depends_on: 0 }, { step: 2, action: 根据天气结果决定是否调用TripAssistant租车, intent: order car rental, depends_on: 1 } ]步骤2依赖步骤1必须串行执行。六、ReAct循环依赖分组并行执行这是整个推理引擎中最精妙的部分。SmartVoyage实现了一个基于依赖关系的分组并行执行机制——同组内并行、不同组间串行。6.1 分组算法# core/chat_service_core.py — _execute_all_stepgroups() async def _execute_all_stepgroups(self, steps, user_queries): # 第一步按 depends_on 分组 dep_groups {} for step_dict in steps: depends_on step_dict.get(depends_on, 0) if depends_on not in dep_groups.keys(): dep_groups[depends_on] [] dep_groups[depends_on].append(step_dict) # 第二步用 OrderedDict 按依赖序号排序 dep_groups OrderedDict(sorted(dep_groups.items())) step_group_list list(dep_groups.values())用一个具体例子说明分组过程。假设步骤列表为Step1: depends_on0 (查机票) Step2: depends_on0 (查天气) Step3: depends_on1 (基于机票结果订酒店)分组结果Group 0 (depends_on0): [Step1(查机票), Step2(查天气)] → 并行 Group 1 (depends_on1): [Step3(订酒店)] → 等Group0完成后执行6.2 并行执行# 第三步按依赖顺序逐组执行 step_results [] for step_list in step_group_list: # 构造并行任务列表 group_step_lst [self._execute_step(s, user_queries) for s in step_list] # asyncio.gather 并行执行return_exceptionsTrue 防止一个失败影响全部 ret_list await asyncio.gather(*group_step_lst, return_exceptionsTrue) # 收集结果 for step, result in zip(step_list, ret_list): if isinstance(result, Exception): result f执行失败{result} step_results.append({ step: step.get(step), description: step.get(description, step.get(action)), result: result }) return step_resultsasyncio.gather配合return_exceptionsTrue是一个工程实践要点即使某个Agent调用超时或报错其他并行任务不会中断错误会被捕获并记录到结果中。6.3 执行流程可视化图2依赖分组并行执行示意——Group内并行Group间串行七、反思机制执行完还不够还要回头看ReAct的核心在于Reasoning Acting的交替循环。执行完一组步骤后系统会进入反思阶段。7.1 反思触发条件# core/chat_service_core.py — _react_loop() async def _react_loop(self, steps, intents, user_queries): max_rounds 3 cur_rounds 1 all_results [] while steps and cur_rounds max_rounds: # 检查是否所有步骤互相独立 all_independent all(s.get(depends_on, 0) 0 for s in steps) # 执行当前步骤组 step_results await self._execute_all_stepgroups(steps, user_queries) all_results.extend(step_results) # 优化所有步骤互相独立 → 无需反思直接汇总 if all_independent: break # 构造观察文本 obs_text \n.join( f步骤{s[step]}({s[description]}): {s[result]} for s in all_results ) # 反思 重新规划 steps await self._plan_or_reflect(intents, user_queries, obs_text) if not steps: break # 无需更多步骤 cur_rounds 1反思机制有几个值得注意的设计独立步骤跳过反思如果所有步骤的depends_on都是0互不依赖说明信息已经齐全直接跳出循环进入汇总省去一次LLM调用。最多3轮max_rounds 3防止无限循环。实际测试中绝大多数任务1-2轮即可收敛。累积结果all_results放在循环外部每轮执行结果不断追加最终汇总时能看到所有轮次的完整信息。7.2 反思的实际场景什么情况下需要反思考虑这个例子用户说帮我规划一个成都三日游第一轮规划[ {step: 1, action: 查询成都旅游团, intent: query tour group, depends_on: 0}, {step: 2, action: 查询成都租车, intent: query car rental, depends_on: 0} ]执行后发现旅游团返回暂无三日游产品但有一日游和两日游。反思阶段LLM看到observations后可能输出[ {step: 3, action: 组合一日游和两日游产品, intent: query tour group, depends_on: 0} ]这就是反思的价值——让系统有机会根据中间结果动态调整策略。八、结果汇总把碎片拼成完整回复ReAct循环结束后all_results中可能有1到N个步骤结果。汇总策略分三种情况8.1 三种汇总策略# 情况1无结果 if not all_results: return 暂无结果 # 情况2单一结果 —— 直接返回 if len(all_results) 1: return all_results[0][result] # 情况3多个结果 —— LLM整合 text \n.join( f步骤{s[step]}({s[description]}): {s[result]} for s in all_results ) sum_chain SmartVoyagePrompts.react_summary_prompt() | self.llm result await sum_chain.ainvoke({ query: self.messages[-1][content], all_observations: text, }) return result.content.strip()8.2 汇总Prompt# core/prompts.py — react_summary_prompt() 系统提示你是一位专业的旅行顾问需要根据所有查询结果生成最终回复。 用户原始查询{query} 各步骤执行结果 {all_observations} 请综合以上结果生成一条完整、连贯的中文回复300字以内语气专业热情 不要出现用户名字。 这个Prompt的作用是把多个Agent返回的碎片化结果天气数据JSON、机票列表、景点文本等整合成一段连贯、自然的用户回复。九、子Agent的结果再加工还有一个容易忽略的细节子Agent通过A2A返回的是原始数据通常是JSON或数据库查询结果直接展示给用户体验很差。SmartVoyage的做法是用config.yaml中配置的自定义Prompt让LLM对原始结果进行再加工# core/chat_service_core.py — _run_single_intent() # 获取该子Agent的总结提示词从config.yaml动态读取 prompt_tpl self.a2aprompt.get(agent_name, None) sum_chain ChatPromptTemplate.from_template(prompt_tpl) | self.llm result await sum_chain.ainvoke({ query: query_str, raw_response: agent_result # 子Agent返回的原始JSON }) return result.content.strip()config.yaml中每个子Agent都有专属的总结Prompt例如天气Agent# config.yaml a2a_servers: weather: prompt: | 系统提示您是一位专业的天气预报员以生动、准确的风格总结天气信息。 - 核心描述点城市、日期、温度范围、天气描述、湿度、风向、降水等。 - 如果结果为空委婉提示未找到数据请确认城市/日期 - 保持中文100-150字。 查询{query} 结果{raw_response}这意味着每个Agent的说话风格都可以通过配置文件单独调整不需要修改任何代码。十、工程实践中的关键细节10.1 全局单例LLM# 进程启动时仅初始化一次所有用户共享 _llm ChatOpenAI( modelCONFIG.LLM_MODEL, api_keyCONFIG.LLM_API_KEY, base_urlCONFIG.LLM_BASEUR, temperatureCONFIG.LLM_TEMPERATURE, )避免每个请求都创建新的LLM连接显著降低延迟和资源消耗。10.2 超时控制的双层设计# A2AClient超时120秒 self._network.add(svc[name], A2AClient(a2a_url, timeout120)) # asyncio.wait_for超时150秒外层 raw_response await asyncio.wait_for( agent.send_task_async(task), timeout150)外层超时(150s) 内层超时(120s)确保外层能捕获内层超时异常并做兜底处理而不是被一起切断。10.3 JSON解析的容错处理# 清理LLM可能输出的markdown代码块标记 intent_response re.sub(r^json\s*|\s*$, , intent_response).strip() intent_output json.loads(intent_response)LLM有时会自作主张在JSON外面包一层json ... 这行正则提前清除避免json.loads()报错。十一、完整推理链路时序图以下用一张时序图总结从用户输入到最终回复的完整链路标注了每个阶段调用的方法和涉及的组件图3完整推理链路时序图——多意图并行执行的详细交互过程十二、总结与思考本文从源码层面完整拆解了SmartVoyage的推理核心核心设计可以概括为以下几点意图识别通过7变量注入的Prompt让LLM充当翻译官将自然语言翻译为结构化三元组。查询改写和追问机制确保了多轮对话场景下的信息完整性。配置驱动意图映射表、Agent地址、总结Prompt全部从config.yaml动态读取新增Agent只需修改配置文件无需改动推理代码。依赖分组并行depends_on OrderedDict asyncio.gather的组合实现了组内并行、组间串行的高效执行策略最大化利用并发能力。反思机制最多3轮的规划→执行→反思→再规划循环让系统能根据中间结果动态调整策略而不是死板地按初始计划执行。双层结果加工子Agent的原始数据先经各自专属Prompt加工再经汇总Prompt整合为最终回复确保输出质量。这套推理引擎的设计哲学是让LLM做决策识别、规划、反思、汇总让Agent做执行查数据库、调API各司其职。这也是当前Multi-Agent系统的主流范式。下一篇文章将深入A2A协议层详解子Agent的实现、AgentCard的能力声明、以及MCP工具调用的完整链路敬请期待。