多智能体协作案例实践(一):基于AgentScope框架

在之前《几种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"

相关推荐
前端程序猿之路12 小时前
30天大模型学习之Day 2:Prompt 工程基础系统
大数据·人工智能·学习·算法·语言模型·prompt·ai编程
极客小云12 小时前
【Python pip换源教程:国内镜像源配置方法(清华/阿里云/中科大源)】
开发语言·python·pip
Mintopia12 小时前
2025,我的「Vibe Coding」时刻
前端·人工智能·aigc
创客匠人老蒋12 小时前
从“经验驱动”到“系统智能”:实体门店经营的结构性升级
大数据·人工智能
安达发公司12 小时前
安达发|APS自动排产排程排单软件:让汽车零部件厂排产不“卡壳”
大数据·人工智能·汽车·aps高级排程·aps排程软件·aps自动排产排程排单软件
草莓熊Lotso12 小时前
脉脉独家【AI创作者xAMA】| 多维价值与深远影响
运维·服务器·数据库·人工智能·脉脉
V搜xhliang024612 小时前
常规超声联合影像组学预测肾透明细胞癌核分级的列线图模型构建和验证
人工智能·计算机视觉
shughui12 小时前
实现Python多版本共存
开发语言·python·pip
BoBoZz1912 小时前
TextureCutQuadric 利用3D隐式函数(Quadrics)来生成2D纹理坐标
python·vtk·图形渲染·图形处理
dhdjjsjs12 小时前
Day58 PythonStudy
开发语言·python·机器学习