在之前《几种AI Agent开发框架对比》这篇文章中,我们设计了一个高考信息查询智能助手业务场景,使用几种AI Agent开发框架分别实现进行对比分析,因为文档太长,其中多智能体协作章节就没有详细展开。
本文基于AgentScope框架,对多智能体协作场景进行详细实践和对比分析,后面还会有一篇基于LangGraph框架的实践分析。
为了更好地模拟,我们还是基于之前高考信息查询这个业务场景,将原有的单智能体拆分为四个模块,对应四个智能体如下:
Template Agent:模板匹配,我们将SQL Agent生成的SQL保存为模板,提取年份参数,当下一次追问仅仅是对年份的追问时,直接取模板内的SQL查询分析即可。
Rewrite Agent:意图识别,根据上下文改写问题完善条件,支持多轮追问场景。
SQL Agent:通过自然语言匹配表结构等信息,然后生成SQL,再执行SQL查询出数据。
Analysis Agent:分析结果数据并简要回答用户问题。
🧠 多智能体协作实现逻辑
首先看一下业务场景,打算基于以上四个智能体协作,来支撑用户进行高考信息查询,并且要支持多轮对话,后续会基于以下案例来进行验证:
第一轮提问:2016年考生人数有多少?
第二轮追问年份:2018年呢?
第三轮追问其他:录取人数呢?
针对该业务场景,四个智能体之间的协作逻辑设计如下:
1)用户输入问题
2)TemplateAgent判断是否匹配模板,未匹配NO_MATCH转3,无上下文NO_CONTEXT则转4,匹配则转5
3)未匹配NO_MATCH即匹配失败,进入意图识别改写RewriteAgent,利用上下文对话背景进行问题改写,然后进入SQLAgent,RAG检索元数据、生成SQL、查询数据,最后进入AnalysisAgent分析回答
4)无上下文NO_CONTEXT即首次提问,直接进入SQLAgent,RAG检索元数据、生成SQL、查询数据,最后进入AnalysisAgent分析回答
5、直接AnalysisAgent分析回答
整体协作逻辑概览如下:
css
[用户输入]
↓
[TemplateAgent]
↓
┌─────────────────────匹配结果──────────────────────┐
↓ ↓ ↓
[匹配成功-管道1] [匹配失败有上下文-管道2] [匹配失败无上下文-管道3]
↓ ↓ ↓
[AnalysisAgent] [RewriteAgent] [SQLAgent]
↓ ↓
[SQLAgent] [AnalysisAgent]
↓
[AnalysisAgent] ↓
↓ ↓
└────────────输出结果──────────────┘
↑
└──────── Memory + Context Reuse ────────────┘
针对以上协作逻辑中的三种分支处理逻辑,实际还是要通过手写代码方式来进行逻辑控制,然后其中每钟分支流程中如果涉及到多个Agent,则可以通过AgentScope框架提供的管道模式(sequential_pipeline)进行串联,具体代码如下:
python
# --- 对话主流程 ---
async def chat():
print("启动 AgentScope 多智能体系统(输入 exit 退出)")
template_agent = TemplateAgent()
rewrite_agent = RewriteAgent()
sql_agent = SQLAgent()
analysis_agent = AnalysisAgent()
while True:
user_input = await asyncio.get_event_loop().run_in_executor(
None, input, "👤 用户: "
)
if user_input.strip().lower() == "exit":
print("退出程序")
break
user_msg = Msg("user", user_input, role="user")
# 所有 Agent 观察输入
for agent in [template_agent, rewrite_agent, sql_agent, analysis_agent]:
await agent.observe(user_msg)
# Step 1️⃣ 模板匹配
template_res = await template_agent.reply(user_msg)
if template_res.content.strip().upper() == "NO_MATCH":
# 模板失败 → 走 改写 → SQL → 分析
print("🌀 模板匹配失败,进入改写流程")
final_msg = await sequential_pipeline([rewrite_agent, sql_agent, analysis_agent], user_msg)
elif template_res.content.strip().upper() == "NO_CONTEXT":
# 无上下文 → 走 SQL → 分析
print("🌀 无上下文,进入SQL流程")
final_msg = await sequential_pipeline([sql_agent, analysis_agent], user_msg)
else:
# 模板成功匹配 → 走 SQL + 分析
print("🎯 模板匹配成功,直接进入分析流程")
final_msg = await sequential_pipeline([analysis_agent], template_res)
# 所有 Agent 观察结果
for agent in [template_agent, rewrite_agent, sql_agent, analysis_agent]:
await agent.observe(final_msg)
print("\n" + "-" * 50)
代码中在启动时先创建了四个智能体,通过if else 来串联业务分支,一个分支对应由 sequential_pipeline() 构建一个包含多Agent组成的管道,管道内部每个 Agent 实现独立职责,并通过 Msg 消息结构传递数据与上下文记忆(InMemoryMemory)。
🔁 多轮对话之上下文记忆+全局变量
先看上下文记忆的相关机制,每个 Agent 都可以观察多轮对话,通过上下文memory,支持多轮追问与问题补全。如下面这段代码所示,将用户输入问题user_msg,以及最终输出结果final_msg,通过调用agent.observe这个方法来告知所有的智能体:
ini
user_msg = Msg("user", user_input, role="user")
# 所有 Agent 观察输入
for agent in [template_agent, rewrite_agent, sql_agent, analysis_agent]:
await agent.observe(user_msg)
......
# 所有 Agent 观察结果
for agent in [template_agent, rewrite_agent, sql_agent, analysis_agent]:
await agent.observe(final_msg)
同时在所有Agent智能体的实现代码中,都定义了observe这个方法,来接收用户输入和系统输出的上下文对话历史,并保存到Agent内部的记忆模块:
python
async def observe(self, msg: Msg | list[Msg] | None) -> None:
await self.memory.add(msg)
智能体内部记忆模块都会持有多轮对话的上下文历史,供处理用户新输入时使用,当然实际本案例中只有问题改写RewriteAgent才需要使用多轮对话上下文,其从记忆模块中获取上下文并用于问题改写的代码逻辑如下:
python
async def reply(self, msg: Msg) -> Msg:
"""理解意图并改写"""
print("=== 🟨 Rewrite Agent ===")
user_query = msg.content
# 从记忆中取出历史上下文
history = await self.memory.get_memory()
# 构造 prompt
user_prompt = f"""
用户的问题可能是不完整的追问或模糊描述,请结合上下文补全成一个清晰的问题。
【当前问题】
"{user_query}"
请将上面的问题改写成一个**可独立理解、完整表达查询意图**的自然语言问题。
要求:
1. 根据原意图内容补充信息,完善条件即可,无须发散联想
2. 不超过 50 字
3. 仅输出改写后的问题内容
"""
prompt = await self.formatter.format([
Msg("system", "你是一个查询意图理解与改写专家。", "system"),
*history,
Msg("user", user_prompt, "user"),
])
result = await self.model(prompt)
final_res = await get_response(result)
print(f"✍️ 改写后问题:{final_res}")
基于这段代码,针对用户简写的问题"录取人数呢?",根据历史对话"2018年考生人数"相关的记忆来改写,改写补全后就变成"2018年录取人数有多少?"的完整问题。
除了记录多轮对话输入/输出Msg的上下文记忆模块Memory之外,还可以利用全局变量方式在多个智能体Agent之间进行结构化的参数传递,如下定义一个全局变量stata,存放历史查询SQL的模板,方便后续同类问题直接使用:
python
# ========================
# 🧠 全局变量定义
# ========================
class State(TypedDict):
original_intent: Optional[str] # 如 "2016年考生人数有多少?"
last_sql_template: Optional[str] # 带占位符的 SQL 模板,如 "SELECT ... WHERE year = {year}"
state = State()
就可以实现在前一轮对话时,在SQLAgent智能体中将根据用户问题+匹配到的表结构信息生成对应查询SQL后,将该SQL和用户问题都置入全局变量中:
python
# 生成 SQL 模板(简单年份/数字替换)
template_sql = re.sub(r'\b(19|20)\d{{2}}\b', '{year}', sql) # 年份
template_sql = re.sub(r'\b\d+\b', '{year}', template_sql, count=1) # 兜底替换第一个数字
print(f"🧠 原始意图: {user_query}")
print(f"📦 保存 SQL 模板: {template_sql}")
state["original_intent"] = user_query
state["last_sql_template"] = template_sql
然后在下一轮对话时,在TemplateAgent智能体中再从全局变量中取出该信息,利用LLM判断用户最新查询意图和全局变量中的用户问题是否同一种问题,只是查询条件不同的追问(如只是从2016年换成2018年),如果是的话,就直接利用全局变量中的SQL,替换掉查询条件进行查询即可,这样就不用走后续的SQLAgent,再去重新利用LLM去构建SQL了。
python
print("🔄 检测到历史上下文,尝试参数化追问")
try:
# 1. 用 LLM 从追问中提取参数
extract_prompt = f"""
原始意图: {state['original_intent']}
当前问题: "{user_query}"
请判断一下用户是否在进行年份的追问。
1、如果不是对年份的追问,直接输出空JSON '{{}}'
2、如果是追问,从中提取**变化的参数值**(年份),以 JSON 格式返回。
示例:
- "那2017年呢?" → {{"year": "2017"}}
只输出 JSON,不要其他内容。
"""
formatted = await self.formatter.format([
Msg("system", "你是一个参数提取器。", "system"),
Msg("user", extract_prompt, "user"),
])
result = await self.model(formatted)
json_str = await get_response(result)
print(f"🔍 提取参数: {json_str}")
params = json.loads(json_str)
if not "year"inparams:
print("⚠️ 年份参数提取失败,进入改写流程")
returnMsg(self.name, "NO_MATCH", role="assistant")
# 2. 填充 SQL 模板
try:
new_sql = state["last_sql_template"].format(**params)
print(f"🛠️ 生成新 SQL: {new_sql}")
按上面三段代码所示,通过全局变量state,就可以实现多轮次对话之间的结构化参数传递。
▶️ 案例测试结果
运行验证结果如下:
sql
启动 AgentScope 多智能体系统(输入 exit 退出)
👤 用户: 2016年考生人数有多少?
=== 🟦 Template Agent ===
⚠️ 不存在上下文,进入SQL Agent流程
🌀 无上下文,进入SQL流程
=== 🟦 Sql Agent ===
调用大模型llama2向量化:2016年考生人数有多少?
好的,用户问的是2016年的考生人数有多少。首先,我需要查看提供的表结构。表名是college_entrance_examination,主键是examination_year,类型是int。考生人数是candidates_count,类型是decimal(10,2)。
因为examination_year是主键,所以每个年份只有一条记录。因此,查询2016年的考生人数不需要用聚合函数,直接SELECT candidates_count即可。所以正确的SQL应该是SELECT candidates_count FROM college_entrance_examination WHERE examination_year =2016; 这里不需要聚合函数,因为数据是直接存储的,不是需要统计的。
💡 大模型生成的SQL: [select candidates_count from college_entrance_examination where examination_year =2016 limit 1]
💡 数据查询成功: [{'candidates_count': 940.0}]
🧠 原始意图: 2016年考生人数有多少?
📦 保存 SQL 模板: select candidates_count from college_entrance_examination where examination_year = {year} limit 1
=== 🟩 Analysis Agent ===
好的,我现在需要处理用户的问题:"2016年考生人数有多少?"首先,我要确认用户的需求是获取2016年的考生人数。根据提供的表结构信息,表名是college_entrance_examination,包含高考年份、考生人数和复读人数三个字段。用户提供的SQL查询已经明确筛选了2016年的考生人数,结果是940.0万人。
接下来,。。。。。(略)
🧠 最终回答: 2016年全国高考考生人数为**940.0万人**。该数据来源于考生人数与复读人数信息表(college_entrance_examination)中2016年的记录。
--------------------------------------------------
👤 用户: 2018年呢
=== 🟦 Template Agent ===
🔄 检测到历史上下文,尝试参数化追问
好的,现在我需要处理用户的问题:"2018年呢"。首先,用户之前问了2016年的考生人数,现在问2018年的情况,这明显是在进行年份的追问。根据之前的示例,用户希望得到一个包含年份的JSON对象。问题中的年份是2018,所以需要提取这个参数。检查表结构,确认表中确实有examination_year字段,类型是int,所以2018是有效的输入。用户没有提到其他参数,所以只需要返回年份。因此,正确的JSON应该是{"year": "2018"}。
🔍 提取参数: {"year": "2018"}
🛠️ 生成新 SQL: select candidates_count from college_entrance_examination where examination_year =2018 limit 1
✅ 追问查询成功,返回 1 行
🎯 模板匹配成功,直接进入分析流程
=== 🟩 Analysis Agent ===
好的,用户之前问过2016年的考生人数,现在又问2018年的,我需要快速找到对应的答案。根据提供的表结构和数据信息,用户给出的SQL查询是针对2018年的,返回的结果是975.0万人。所以。。。。。。就可以直接给出答案了。
🧠 最终回答: 2018年全国高考考生人数为**975.0万人**。该数据来源于考生人数与复读人数信息表(college_entrance_examination)中2018年的记录。
--------------------------------------------------
👤 用户: 录取人数呢?
=== 🟦 Template Agent ===
🔄 检测到历史上下文,尝试参数化追问
好的,我需要分析用户的问题是否在进行年份的追问。用户之前询问了2016年和2018年的考生人数,现在问的是"录取人数呢?"。首先,用户的问题从"考生人数"转向了"录取人数",这可能涉及到另一个数据点,而不是同一字段的不同年份。根据提供的表结构,表中只有考生人数和复读人数,没有录取人数的字段。因此,用户可能在询问另一个未在表中提供的数据,而不是同一表中的不同年份。此外,问题中没有提到具体的年份,也没有使用"那...呢?"这样的结构,所以这应该不是对年份的追问。因此,应该返回空JSON。
🔍 提取参数: {}
⚠️ 年份参数提取失败,进入改写流程
🌀 模板匹配失败,进入改写流程
=== 🟨 Rewrite Agent ===
好的,用户之前问了2016年和2018年的高考考生人数,现在问"录取人数呢?"。我需要结合上下文补全问题。根据之前的对话,用户可能是在比较考生人数和录取人数。所以应该明确年份和具体是哪个年份的录取人数。可能指的是最近提到的2018年,所以改写为"2018年全国高考录取人数是多少?"
✍️ 改写后问题:2018年全国高考录取人数是多少?
=== 🟦 Sql Agent ===
调用大模型llama2向量化:2018年全国高考录取人数是多少?
好的,用户现在问的是"录取人数呢?",结合之前的对话历史来看,他们之前已经询问过2016年和2018年的考生人数,现在想知道2018年的录取人数。根据用户提供的表结构,录取人数存储在college_entrance_admission表中,具体字段是admission_count,并且以万为单位。
首先,我需要确认用户的问题是否需要使用聚合函数。因为录取人数在表中是按年份记录的,每个年份对应一个具体的admission_count值,所以不需要SUM或AVG,而是直接查询该年份的记录即可。
接下来,检查表结构,admission_year是主键,所以2018年只会有一条记录。因此,查询应该是SELECT admission_count FROM college_entrance_admission WHERE admission_year =2018。
然后,确保字段类型是decimal(10,2),所以结果会是精确到小数点后两位的数值。
💡 大模型生成的SQL: [select admission_count from college_entrance_admission where admission_year =2018]
💡 数据查询成功: [{'admission_count': 790.99}]
🧠 原始意图: 2018年全国高考录取人数是多少?
📦 保存 SQL 模板: select admission_count from college_entrance_admission where admission_year = {year}
=== 🟩 Analysis Agent ===
好的,用户现在问的是"录取人数呢?",看起来他们之前已经询问过2016年和2018年的考生人数,现在想知道同一时期或相关年份的录取人数。我需要先回顾之前的对话历史,确保回答的一致性。
首先。。。。。。(略)
用户。。。。。。(略)
根据。。。。。。(略)
此外。。。。。。(略)
总结。。。。。。(略)
🧠 最终回答: 2018年全国高考录取人数为**790.99万人**。该数据来源于录取人数与普通高校数信息表(college_entrance_admission)中2018年的记录。若需了解其他年份的录取人数,可提供具体年份以便进一步查询。
--------------------------------------------------
👤 用户:
第一轮问题:2016年考生人数有多少?
第一轮没有上下文,也没有全局变量模板,所以直接走SQL Agent + Analysis AgentRAG检索、生成SQL、查数、分析。同时SQL Agent生成成功后记录SQL模板到全局state。
第二轮问题:2018年呢?
第二轮我们追问2018年,这时存在全局变量模板,所以走模板匹配提取年份参数,直接查数然后通过业务判断存在数据,走Analysis Agent分析。
第三轮问题:录取人数呢?
第三轮我们追问录取人数,但是匹配不到模板,所以上层业务判断走Rewrite Agent + SQL Agent + Analysis Agent改写问题完善条件。改写时,参考Memory中上下文2016和2018年回答,改写为2018年全国高考录取人数是多少?然后走RAG检索、生成SQL、查数、分析。
🧩 AgentScope实现小结
AgentScope 提供了一个面向多智能体协作的开发框架,核心是「消息驱动 + 管道式执行」模型:
- 通过 sequential_pipeline() 串联多个 Agent,每个智能体只需实现 reply() 方法。
- 使用 Msg 对象封装上下文,支持多模态内容(文本、结构化数据等)。
- 每个智能体内部InMemoryMemory 提供上下文记忆,可实现多轮对话追问。
- 多个智能体之间通过自定义全局变量State,实现跨轮次跨智能体的结构化参数传递。
- 利用OllamaChatFormatter 自动拼装 System Prompt + 历史记忆,实现语义连续性。
特点:
- 上手快、API 简洁,适合快速搭建多智能体应用。
- 内置异步流式输出,可直接打印模型"思考过程"。
- 强调「职责分离」:每个 Agent 只负责一类任务。
适用场景 适合对「智能体职责清晰」「调用链固定」的任务。
附录:AgentScope中主要管道类型
AgentScope中主要有以下三类管道:
1)顺序 Pipeline (sequential_pipeline / SequentialPipeline)
函数式调用:使用 sequential_pipeline(agents=[...], msg=...)。
- agents 参数:一个 agent 列表,按顺序执行。
- msg 参数:初始消息(可以是 None 或一个 Msg 实例)作为第一个 agent 的输入。
- 返回值:最后一个 agent 的输出消息。
类式调用:
ini
pipeline = SequentialPipeline(agents=[...])
msg = await pipeline(msg=initial_msg)
- 可以多次调用 pipeline(...) 重用该流水线。
- 适用场景:你希望依次将一个 agent 的输出传给下一个 agent------典型的流水线模式。
2)分发/并行 Pipeline (fanout_pipeline / FanoutPipeline)
函数式调用: fanout_pipeline(agents=[...], msg=..., enable_gather=False|True)。
- 将同一个输入 msg 分发给 agents 列表中的每一个。
- 返回值:一个列表(每个 agent 的输出消息)。
- enable_gather=True(默认)会并发执行所有 agent(利用 asyncio.gather()),适合 I/O 密集型场景。
- enable_gather=False 会顺序执行,但是仍是分发模式(每个 agent 接收同一个输入)。
类式调用:
ini
pipeline = FanoutPipeline(agents=[...])
msgs = await pipeline(msg=initial_msg)
- 同样可以重用。
- 适用场景:你希望多个 agent 基于同一个输入做"并行评估/多视角分析",而不是一个接一个。
3)MsgHub 使用模式
- MsgHub 是一个异步(async)上下文管理器,传入一个 participants 列表(agent 实例列表)和一个 announcement 消息。
- 在上下文(async with MsgHub(...) as hub:)内部,参与者 agent 可以互相"观察"(observe) 消息,无需手动将一个 agent 的输出专门传给另一个。系统会自动广播。
- MsgHub 支持动态管理参与者:通过 hub.add(...), hub.delete(...) 方法。
示例(简化版):
csharp
async with MsgHub(participants=[alice, bob, charlie],
announcement=Msg("user", "Introduce yourselves.", "user")) as hub:
await alice()
await bob()
await charlie()
在这个例子中,alice,bob,charlie 会接收到 announcement,互相广播消息,无需手动传递。
源码参考:参与协作的四个智能体,以及具体协作控制逻辑代码,详见github.com/MuKunZiAI/c...
本文总结:本文通过高考信息智能查询的业务场景,基于AgentScope开发框架实践多智能体协作机制,通过管道+控制逻辑进行协作,通过上下文记忆+全局变量实现多轮对话和跨智能体之间参数传递,最后分享了AgentScope的几种管道类型。下一篇基于LangGraph框架的多智能体协作机制实践,敬请期待。
本文作者:Chaiys
本文原载:公众号"木昆子记录AI"