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 统一数据访问、本地库承载高频查询」的模式,也可以迁移到征信等场景,换的是协议和合规细节,主链路一致。

流程总览:

python 复制代码
用户输入
  → ① 意图识别 + 查询改写
  → ② 按意图路由到 A2A Agent
  → ③ Agent:NL2SQL
  → ④ Agent:调用 MCP(传 SQL)
  → ⑤ MCP:查 MySQL 返回 JSON
  → ⑥ 主控:结果润色输出
旁路:定时任务同步和风数据 → MySQL

1. 用户入口 ➕️ 2. 主控: 意图识别 + 查询改写

2. 主控:意图识别 + 查询改写 总述:

主控不是 Router 组件,而是 Prompt + LLM(intent_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交给AIAgentRouter,Router基于用户查询,对照network中各Agent的Card/Skill能力描述使用LLM做意图匹配,返回目标agent名称及置信度confidence;方式二:提示词方案;意图识别我们没用单独的Router组件,而是Prompt + LLM:在提示词里写清有哪些意图、如何改写、如何输出JSON,再把用户问题和历史喂给模型(这里的「历史」是主控自己维护的一串对话文本,不是数据库里的日志),由模型返回意图列表intents:

(意图列表:"intents": "weather", "train"是由LLM 吐出的 JSON 解析后得到的,例如:)

bash 复制代码
{
  "intents": ["weather", "train"],
  "user_queries": {
    "weather": "明天上海的天气",
    "train": "明天上海到北京的高铁"
  },
  "follow_up_message": ""
}

主控再根据这个JSON去调对应的A2A Agent:主控拿到列表后,用普通 if/elif 做映射,再agent.send_task_async(task)远程发Task:

bash 复制代码
for intent in intents:
    if intent == "weather":
        agent_name = "WeatherQueryAssistant"
    elif intent in ["flight", "train", "concert"]:
        agent_name = "TicketQueryAssistant"
    elif intent == "order":
        agent_name = "TicketOrderAssistant"
    ...
    elif agent_name:
        query_str = user_queries.get(intent, {})
        agent = agent_network.get_agent(agent_name)
        ...
        task = Task(...)
        raw_response = asyncio.run(agent.send_task_async(task))

JSON 里的: intent=weather、主控映射到WeatherQueryAssistant 5005; intent=train/flight/concert、主控映射到TicketQueryAssistant 5006; intent=order、主控映射到TicketOrderAssistant 5007;同时用 user_queriesintent 当该 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_response=asyncio.run(network.get_agent(agent_name).send_task_async(task))通过AgentNetwork 把 Task 发到对应端口的 A2A 服务去执行。

(本质是按这个 URL 远程调用已在跑的服务。启动顺序一般是:先起 MCP (8001--8003)→ 再起三个 Agent (5005--5007)→ 最后起 main.py / app.py ;只跑主控、不跑 Agent,分发时会连不上。

agent来源 :a2a_server/下 weather_server.py/ticket_server.py/order_server.py ---WeatherQueryServer(A2AServer)--- run_server(..., port=5005),每个服务里有 AgentCard / AgentSkill,并实现 handle_task(NL2SQL、调 MCP 等);

MCP来源:mcp_server/下 mcp_weather_server.py/mcp_ticket_server.py/mcp_order_server.py --- create_weather_mcp_server();)

方式二:

Prompt = 写给大模型的说明书(规则、意图列表、输出格式)

LLM = 真正做判断的模型(如 qwen-plus)

Prompt + 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 completion:in 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 intent=="weather" 这种分支) → 所以要把模型吐出的JSON字符串使用 json.loads 转成字典(如下),这时才能当程序数据用,再「解析」成 intents、user_queries,

python 复制代码
intent_output = json.loads(intent_response)
# 变成真正的 dict:
# {
#   "intents": ["weather"],
#   "user_queries": {"weather": "明天上海天气"},
#   "follow_up_message": ""
# }

intents = intent_output.get("intents", [])           # → ["weather"]
user_queries = intent_output.get("user_queries", {}) # → {"weather": "明天上海天气"}

主控才能按意图分发(即主控读字典里的 intents,决定调哪个 Agent);如果中间不做解析,主控拿着一串字没法可靠地 if intent == "weather"。所以「拼进去再解析回来」不是浪费步骤,而是 文本接口 → 程序可用数据 的必要转换。

Q:Router和提示词这两种意图识别方案的区别:

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 + Task,send_task_async 发给对应 Agent,拿回 artifacts 或失败信息。

天气 / 票务查询回来后,主控还会再过一层 总结 Prompt(summarize_weather_prompt / summarize_ticket_prompt),把结构化结果润色成自然语言;订票结果一般直接展示。多意图时把多段回复用空行拼在一起返回用户;

相关推荐
乱写代码6 小时前
LangChain学习--Document
python
风之所往_6 小时前
Python 3.11 新特性全面总结
python·python3.11
小小测试开发7 小时前
RAG检索评测实战:9种架构对比与自动化评估框架搭建
运维·人工智能·python·自动化
迷途呀7 小时前
conda使用指南
python·深度学习·机器学习·pycharm·conda
AC赳赳老秦7 小时前
OpenClaw 合规公开数据采集入门:合法边界、数据源选型与反爬规避实操指南
大数据·人工智能·python·信息可视化·php·deepseek·openclaw
阿童木写作8 小时前
Python实现跨境商品详情页自动翻译
开发语言·python·机器翻译
hhzz8 小时前
CNN猫狗图像分类实战:基于Keras+TensorFlow的卷积神经网络全流程解析
图像处理·python·深度学习·计算机视觉·cnn
老马聊技术17 小时前
HyperLPR3汽车车牌识别
python·汽车
梅雅达编程笔记20 小时前
编程启蒙|Scratch 转 Python 系列第9天:字典/哈希表积木双向对照(AI大模型参数配置表实战)
开发语言·人工智能·python·numpy·pandas