Agent项目 用户一句话进来主控用 Prompt 做意图识别和查询改写再按意图把请求分发天气 / 票务查询 / 订票等A2A Agent。查询类 Agent 做 NL2SQL通过内网 MCP 查本地 MySQL天气数据由定时任务从和风同步进库实现一次写入多次查询避免每次外网调用的成本和延迟。订票会先 A2A 查余票再调订票 MCP。整体是主控编排、领域 Agent 推理、MCP 统一数据访问。技术栈是 LangChain Qwen、python-a2a、FastMCP、MySQL。SmartVoyage 是一个旅游多智能体项目。用户做攻略时要同时关注天气、交通、票务、景点等信息比较分散所以我们做了一站式自然语言入口用户一句话提问系统自动识别意图并完成查询或订票。整体链路是用户从前端或命令行进入后主控用 Prompt 加大模型做意图识别和查询改写结合对话历史输出意图、改写后的问题和必要时的追问。意图支持天气、机票、火车、演唱会、订票、景点等也可以多意图一起处理。然后按意图分发天气、查票、订票分别打到对应的 A2A Agent景点推荐目前由主控侧大模型直接生成。Agent 通过 Task 通信查票和天气结果回来后主控还会再做一次结果润色再返回给用户。以天气为例讲核心实现。天气 Agent 收到改写后的问题后做 NL2SQL把完整表结构写进 Prompt生成可执行的 SELECT信息不全就追问。生成 SQL 后Agent 作为 MCP 客户端调用天气 MCP把 SQL 当工具参数传入MCP 连本地 MySQL 执行查询并按约定返回状态和数据。之所以不让 Agent 每次直接调和风一是要把数据访问做成独立可复用的 MCP 服务和推理解耦二是第三方按次计费、走外网贵且慢。我们用定时任务按官方 API 拉取天气、解析入库查询时走内网 MCP 查本地库实现一次写入、多次查询。实时性靠调度频率平衡频率过高会压库。票务查询和天气同构也是 Agent 做 NL2SQL再经票务 MCP 查本地票表。订票更体现 Agent 协作订票 Agent 先通过 A2A 调查票 Agent 确认余票有票再调订票 MCP 完成下单。技术栈主要是 LangChain 加通义、python-a2a、FastMCP 和 MySQL。这套「主控编排、领域 Agent 推理、MCP 统一数据访问、本地库承载高频查询」的模式也可以迁移到征信等场景换的是协议和合规细节主链路一致。流程总览用户输入 → ① 意图识别查询改写 → ② 按意图路由到 A2A Agent → ③ AgentNL2SQL → ④ Agent调用 MCP传 SQL → ⑤ MCP查 MySQL 返回 JSON → ⑥ 主控结果润色输出 旁路定时任务同步和风数据 → MySQL1. 用户入口 ➕️ 2. 主控: 意图识别 查询改写2. 主控意图识别 查询改写总述主控不是 Router 组件而是 Prompt LLMintent_prompt → intent_agent。输入用户问题 最近几轮对话历史 当前日期。输出严格 JSON三类信息① intents如 weather、train、flight、concert、order、attraction支持多意图组合② user_queries按意图改写后的明确问题把「明天」补成日期、把缺省城市从上下文补全③ follow_up_message意图不清时追问超范围时直接回复引导语分支处理① out_of_scope → 直接用 follow_up 回复② 有追问 → 先问清再往下走③ 有效意图 → 进入分发口述要点主控只负责「看懂要干什么、把问题说清楚」不负责查库查库在子 Agent MCP。用户一句话进来系统启动时会①创建 LLM 实例②初始化AgentNetwork需要智能选型时多个Agent可注册进AgentNetwork通过network.add(name, url)将AgentServer登记进AgentNetwork。意图识别两种方案Router或提示词。方式一当使用AIAgentRouter时Network再按url地址发现各端的AgentCard/Skill交给AIAgentRouterRouter基于用户查询对照network中各Agent的Card/Skill能力描述使用LLM做意图匹配返回目标agent名称及置信度confidence方式二提示词方案意图识别我们没用单独的Router组件而是Prompt LLM在提示词里写清有哪些意图、如何改写、如何输出JSON再把用户问题和历史喂给模型(这里的「历史」是主控自己维护的一串对话文本不是数据库里的日志)由模型返回意图列表intents意图列表“intents”: [“weather”, “train”]是由LLM 吐出的 JSON 解析后得到的例如{intents:[weather,train],user_queries:{weather:明天上海的天气,train:明天上海到北京的高铁},follow_up_message:}主控再根据这个JSON去调对应的A2A Agent主控拿到列表后用普通 if/elif 做映射再agent.send_task_async(task)远程发Taskforintentinintents:ifintentweather:agent_nameWeatherQueryAssistantelifintentin[flight,train,concert]: agent_nameTicketQueryAssistantelifintentorder:agent_nameTicketOrderAssistant...elifagent_name: query_struser_queries.get(intent,{})agentagent_network.get_agent(agent_name)... taskTask(...)raw_responseasyncio.run(agent.send_task_async(task))JSON 里的 intentweather、主控映射到WeatherQueryAssistant 5005 intenttrain/flight/concert、主控映射到TicketQueryAssistant 5006 intentorder、主控映射到TicketOrderAssistant 5007同时用 user_queries[intent] 当该 Agent 的输入改写后的问题不是原始整句一股脑丢过去。即主控不是 Router 组件而是 Prompt LLM用一段写好的提示词模板把用户问题交给大模型让模型自己判断意图并输出结构化结果——不是框架里一个叫 Router 的现成组件在做路由拿到名字后再通过get_agent(name).send_task_async(task) 取Client发Task意图识别只产出意图列表intents、改写后的问题user_queries、追问话术或超范围时的直接回复(即可选的追问/兜底回复)follow_up_message(只有第三项为空且意图有效时才进入分发即按intents映射并调用对应Agent或景点LLM主控用 for intent in intents 查表映射到agent_name即Agent名字再raw_responseasyncio.run(network.get_agent(agent_name).send_task_async(task))通过AgentNetwork 把 Task 发到对应端口的 A2A 服务去执行。本质是按这个 URL 远程调用已在跑的服务。启动顺序一般是先起 MCP8001–8003→再起三个 Agent5005–5007→最后起 main.py / app.py只跑主控、不跑 Agent分发时会连不上。agent来源a2a_server/下 weather_server.py/ticket_server.py/order_server.py —WeatherQueryServer(A2AServer)— run_server(…, port5005)每个服务里有 AgentCard / AgentSkill并实现 handle_taskNL2SQL、调 MCP 等MCP来源mcp_server/下 mcp_weather_server.py/mcp_ticket_server.py/mcp_order_server.py — create_weather_mcp_server()方式二Prompt 写给大模型的说明书规则、意图列表、输出格式LLM 真正做判断的模型如 qwen-plusPrompt LLM 把说明书和用户问题拼在一起让模型「读完后按格式回答」— 本项目里用来做意图识别。main.pychain SmartVoyagePrompts.intent_prompt() | llm这是 LangChain 的写法① intent_prompt()一段带占位符的模板规则 {query} / {conversation_history} / {current_date}② | llm模板填好后交给大模型③ chain.invoke({…})得到模型返回的 JSON 字符串( Prompt要求尽量输出为JSON)再 json.loads 解析出 intents、user_queries分类逻辑写在 Prompt 里由 LLM 执行而不是 if/else 或独立 Router 模块。Q为什么要进行查询改写改写发生在意图识别 Prompt结合 conversation_history结果放在user_queries谁用分发时 query_str user_queries.get(intent)再放进 Task 发给对应 Agent主控改写不是替用户回答Prompt 也写了不要答题、不改原意只补上下文让问题更明确用户说话依赖上下文、经常省略Agent 办事需要明确的城市、日期、出发到达等。查询改写 在分发前把省略补全让每个 Agent 拿到「可执行」的问题。Q为什么要拼进去再解析回来因为大模型接口本质是 chat completionin text → out text即LLM 只吃文本、也只吐文本主控后面的代码却要拿结构化字段intents列表、user_queries字典去做 if intent “weather” 这种分支。中间必须有一次「约定格式的文本 ↔ 程序对象」转换。 Prompt是给模型看的说明书模型只认文本 → 所以要把问题「拼」进 Prompt 送进去模型只能回文字我们约定它回 JSON 这种文字模型按约定吐 JSON 文本字符串{intents: [weather], user_queries: {weather: 明天上海天气}, follow_up_message: }不能对字符串写for intent in intents后面代码需要按字段路由(即要拿 结构化字段intents 列表、user_queries 字典去做if intentweather这种分支) → 所以要把模型吐出的JSON字符串使用json.loads 转成字典(如下)这时才能当程序数据用再「解析」成 intents、user_queriesintent_outputjson.loads(intent_response)# 变成真正的 dict# {# intents: [weather],# user_queries: {weather: 明天上海天气},# follow_up_message: # }intentsintent_output.get(intents,[])# → [weather]user_queriesintent_output.get(user_queries,{})# → {weather: 明天上海天气}主控才能按意图分发即主控读字典里的 intents决定调哪个 Agent如果中间不做解析主控拿着一串字没法可靠地if intent weather。所以「拼进去再解析回来」不是浪费步骤而是 文本接口 → 程序可用数据 的必要转换。QRouter和提示词这两种意图识别方案的区别3. 按意图分发先调 intent_agent(prompt)得到 intents、user_queries、follow_up_message若超范围或有追问 → 直接返回不进循环否则进入 else执行 for intent in intents: ——按意图分发就在这里对每个 intent 循环处理如下循环内再做映射 Agent 名 → get_agent → send_task_async → 可选 summarize → 收集到 responses最后 “\n\n”.join(responses) 合并返回。一句话分发发生在意图识别之后、调用各 A2A Agent 之前对 Agent 类意图取出该意图改写后的 query_str拼一点历史封装成 A2A 的 Message Tasksend_task_async 发给对应 Agent拿回 artifacts 或失败信息。天气 / 票务查询回来后主控还会再过一层 总结 Promptsummarize_weather_prompt / summarize_ticket_prompt把结构化结果润色成自然语言订票结果一般直接展示。多意图时把多段回复用空行拼在一起返回用户