Henry
发布于 2026-01-06 / 11 阅读
0
0

Agentic AI - 第二课 - 多Agent协同

背景简介

随着人工智能技术的快速发展,单一Agent已难以满足复杂任务的需求。多Agent协同系统通过分工合作,能够更高效地处理多样化任务。本文基于ReAct范式,深入探讨如何构建一个包含Router、Executor、Validator和Responder四个角色的多Agent协同系统,实现智能化的任务处理流程。

前置信息

详细信息

系统依赖

# requirements.txt
ollama
aiohttp
pydantic
asyncio
typing

系统架构设计

我们的多Agent协同系统采用四层架构,每个Agent各司其职:

  • Router(路由器):负责用户意图识别和任务分发
  • Executor(执行器):负责工具调用和参数提取
  • Validator(验证器):负责结果验证和错误检测
  • Responder(响应器):负责生成自然语言回复
  • 多Agent 协同代码
import asyncio
from typing import Dict, Any, Callable
from app.ai_agent.agent_tools import tool_calculator, tool_get_weather
from app.ai_agent.agent_tools import TOOLS
from app.core.pers_ollama import OllamaClient

# ========== 工具注册表 ==========
TOOL_REGISTRY: Dict[str, Callable] = {
    "tool_get_weather": tool_get_weather,
    "tool_calculator": tool_calculator,
}

class MultiAgentBot:
    """终极版:Router -> Executor -> Validator -> Responder (修复工具调用问题)"""
    
    def __init__(self, base_url: str = "http://localhost:11434", model: str = "llama3.1:8b"):
        self.client = OllamaClient(base_url=base_url, model=model, temperature=0.2)
        self.model = model

    async def close(self):
        await self.client.close()

    # ================= Agent 1: Router =================
    ROUTER_PROMPT = (
        "意图分类(只输出模式名):\n"
        "- CHAT: 闲聊、常识\n"
        "- WEATHER: 查天气\n"
        "- CALCULATOR: 算数\n\n"
        "示例:\n"
        "用户: 月亮为什么圆 -> CHAT\n"
        "用户: 上海冷不冷 -> WEATHER\n"
        "用户: 123*456 -> CALCULATOR\n\n"
        "用户问题: {user_message}\n模式:"
    )

    async def agent_router(self, user_message: str) -> str:
        response = await self.client.chat(messages=[{"role": "user", "content": self.ROUTER_PROMPT.format(user_message=user_message)}], tools=[])
        c = response.content.strip().upper()
        if "CALC" in c: return "CALCULATOR"
        if "WEATHER" in c: return "WEATHER"
        return "CHAT"

    # ================= Agent 2: Executor (修复版) =================
    EXECUTOR_SYSTEM_PROMPT = (
        "你是工具调用专家。你的任务是提取参数并调用指定工具。\n"
        "当前模式:{tool_type}\n\n"
        "重要规则:\n"
        "1. 必须使用系统提供的工具。\n"
        "2. 【中文城市名处理】:如果用户问的是中文城市名(如兰州、上海),"
        "必须**逐字复制**用户问题中的城市名作为参数,严禁翻译、拼音化或猜测。\n\n"
        "【示例】\n"
        "用户: 兰州的天气热吗? -> 调用 tool_get_weather(city='兰州')\n"
        "用户: 上海冷不冷? -> 调用 tool_get_weather(city='上海')\n"
        "用户: 123 * 456 是多少? -> 调用 tool_calculator(expression='123 * 456')\n\n"
        "3. 如果收到错误反馈,请根据反馈修正参数。"
    )


    async def agent_executor(self, user_message: str, tool_type: str, history_msgs: list = None):
        if history_msgs is None:
            history_msgs = []
        
        system_content = self.EXECUTOR_SYSTEM_PROMPT.format(tool_type=tool_type)
        
        # 构建消息列表
        # 如果有历史消息(说明是重试),则不再追加原始 user_message,直接让模型回复最后的反馈
        if history_msgs:
            messages = [{"role": "system", "content": system_content}] + history_msgs
        else:
            messages = [{"role": "system", "content": system_content}] + [{"role": "user", "content": user_message}]
        
        # 【关键修复】:根据类型筛选可用工具,并传递给 client
        allowed_tools = []
        if tool_type == "WEATHER":
            allowed_tools = [t for t in TOOLS if t["function"]["name"] == "tool_get_weather"]
        elif tool_type == "CALCULATOR":
            allowed_tools = [t for t in TOOLS if t["function"]["name"] == "tool_calculator"]
            
        # 【关键修复】:这里必须传递 allowed_tools,不能是空列表!
        return await self.client.chat(messages=messages, tools=allowed_tools)

    # ================= Agent 3: Validator (带具体纠错原因) =================
    # 关键点:允许输出 "FAIL: 原因",直接告诉模型改哪里
    VALIDATOR_PROMPT = (
        "你是参数审核员。请判断工具调用是否成功且符合用户意图。\n"
        "你需要查看:【用户问题】、【工具参数】、【工具执行结果】。\n\n"
        "输出要求:\n"
        "1. 成功:只输出 'PASS'。\n"
        "2. 失败:输出 'FAIL: 原因'。\n\n"
        "【示例】\n"
        "示例 1 (正确提取):\n"
        "用户: 兰州的天气\n"
        "参数: {{'city': '兰州'}}\n"
        "结果: 晴,25度\n"
        "结果: PASS\n\n"
        "示例 2 (提取错误导致未找到城市):\n"
        "用户: 兰州的天气\n"
        "参数: {{'city': '勗安'}}\n"
        "结果: 未找到城市: 勗安\n"
        "结果: FAIL: 工具未找到城市 '勗安'。用户问的是 '兰州',请直接使用 '兰州' 作为参数。\n\n"
        "示例 3 (中文乱码/错字):\n"
        "用户: 上海天气\n"
        "参数: {{'city': '上晦'}}\n"
        "结果: 无法获取天气信息\n"
        "结果: FAIL: 参数城市名 '上晦' 错误,请修正为 '上海'。\n\n"
        "示例 4 (错误的表达式):\n"
        "用户: 计算一个半径为3的圆的面积\n"
        "参数: {{'expression': 'π*3^2'}}\n"
        "结果: 表达式无效或包含非法操作:\n"
        "结果: FAIL: 表达式非法,转化为纯数字的公式,不使用特殊字符,例如'3.14159 * 3 * 3'。\n\n"
        "【当前任务】\n"
        "用户: {user_message}\n"
        "参数: {tool_args}\n"
        "工具结果: {tool_result}\n"
        "结果:"
    )


    async def agent_validator(
        self, 
        user_message: str, 
        tool_name: str, 
        tool_args: dict, 
        tool_type: str, 
        tool_result_str: str  # 【新增】接收工具执行后的结果字符串
    )  -> tuple[bool, str]:  
        
        prompt = self.VALIDATOR_PROMPT.format(
            user_message=user_message,
            tool_args=tool_args,
            tool_result=tool_result_str  # 【新增】注入结果
        )
        response = await self.client.chat(messages=[{"role": "user", "content": prompt}], tools=[])
        content = response.content.strip()
        
        if "PASS" in content.upper():
            return True, "PASS"
        else:
            return False, content


    # ================= Agent 4: Responder =================
    RESPONDER_PROMPT = (
        "你是一个友好的助手。请根据工具结果回答用户的问题。\n"
        "用户问题:{user_message}\n"
        "工具返回结果:{tool_result}\n\n"
        "请用自然、口语化的中文回答。"
    )

    async def agent_responder(self, user_message: str, tool_result: str) -> str:
        prompt = self.RESPONDER_PROMPT.format(
            user_message=user_message,
            tool_result=tool_result
        )
        response = await self.client.chat(messages=[{"role": "user", "content": prompt}], tools=[])
        return response.content

    # ================= 主流程 =================
    async def chat(self, user_message: str) -> str:
        print(f"\n👤 用户: {user_message}")
        print("-" * 60)

        # 1. Router
        print("🧠 [Router] 分析意图...")
        mode = await self.agent_router(user_message)
        print(f"🧠 [Router] 模式: {mode}")

        if mode == "CHAT":
            return await self.agent_responder(user_message, "这是一个常识问题,不需要工具。")

        # 2. Executor -> Validator Loop
        conversation_history = [] 
        
        for attempt in range(5):
            print(f"🔧 [Executor] 尝试 {attempt + 1}...")
            
            # 【关键修复】:调用 agent_executor,而不是直接调 client.chat
            # agent_executor 会处理历史消息和工具传递
            exec_response = await self.agent_executor(user_message, mode, conversation_history)
            
            if not exec_response.tool_calls:
                print(f"❌ [Executor] 没有生成工具调用。内容: {exec_response.content}")
                # 如果没有生成工具,构造一个错误反馈迫使重试(或者直接失败)
                if attempt == 0:
                    conversation_history.append({
                        "role": "user", 
                        "content": "你没有调用工具!请立即调用工具,不要只用文字描述。"
                    })
                    continue
                else:
                    return "Executor 执行失败,模型拒绝调用工具。"

            # 记录历史
            conversation_history.append({
                "role": "assistant",
                "content": exec_response.content,
                "tool_calls": exec_response.tool_calls
            })

            tool_call = exec_response.tool_calls[0]
            tool_name = tool_call["function"]["name"]
            tool_args = tool_call["function"]["arguments"]
            
            print(f"🔧 参数: {tool_args}")
            
            # 真实执行工具
            try:
                tool_result = TOOL_REGISTRY[tool_name](**tool_args)
                # 转成字符串方便 Validator 读取
                tool_result_str = str(tool_result)
                print(f"✅ [Tool Result] {tool_result_str[:50]}...")
            except Exception as e:
                tool_result = f"工具执行报错: {str(e)}"
                tool_result_str = tool_result

            # 3. Validator (传入 tool_result_str)
            print("🔍 [Validator] 检查中...")
            
            # 【修改】这里增加了 tool_result_str 参数
            is_valid, feedback = await self.agent_validator(
                user_message, 
                tool_name, 
                tool_args, 
                mode, 
                tool_result_str
            )
            
            if is_valid:
                print("✅ [Validator] 通过")
                return await self.agent_responder(user_message, str(tool_result))
            else:
                print(f"❌ [Validator] 拒绝: {feedback}")
                # 如果是因为“未找到城市”失败,反馈会非常明确告诉 Executor 改成 '兰州'
                conversation_history.append({
                    "role": "user",
                    "content": f"上一次调用失败。原因:{feedback}。请修正参数并再次调用工具。"
                })

        return "抱歉,尝试多次后仍未通过验证。"



# ================= 测试入口 =================

async def main():
    bot = MultiAgentBot()
    
    test_questions = [
        #"上海现在冷不冷?",        # 期望:修正参数后回答
        #"123 * 456 是多少?",     # 期望:计算后回答
        #"今天北京天气怎么样?",     # 期望:直接回答
        #"月亮为什么是圆的",
        #"兰州的天气热吗?",
        # "随便告诉我一个中国城市的天气,不要是北京,上海,兰州",
        "计算一个半径为3的圆的面积",
        "半径为5cm的圆的周长是多少?",
        "计算一个半径为12,高为2cm的圆锥的体积"
    ]

    print("🚀 四 Agent 协同 (Router -> Executor -> Validator -> Responder)")
    print("=" * 60)

    try:
        for q in test_questions:
            answer = await bot.chat(q)
            print(f"🤖 最终回答: {answer}")
            print("=" * 60)
    finally:
        await bot.close()

if __name__ == "__main__":
    asyncio.run(main())

系统特色

智能错误恢复机制

系统实现了智能的错误恢复机制,当工具调用失败时,Validator会提供具体的错误原因,Executor根据反馈自动修正参数:

# 示例:城市名错误修正
用户: 兰州的天气热吗?
Executor 第一次尝试: tool_get_weather(city='勗安')  # 错误参数
Validator 反馈: FAIL: 工具未找到城市 '勗安'。用户问的是 '兰州',请直接使用 '兰州' 作为参数。
Executor 第二次尝试: tool_get_weather(city='兰州')  # 正确参数

中文参数处理优化

针对中文城市名处理,系统特别强调"逐字复制"原则,避免了翻译、拼音化等常见错误:

EXECUTOR_SYSTEM_PROMPT = (
    "【中文城市名处理】:如果用户问的是中文城市名(如兰州、上海),"
    "必须**逐字复制**用户问题中的城市名作为参数,严禁翻译、拼音化或猜测。"
)

数学表达式规范化

对于计算器工具,系统要求将包含特殊字符的数学表达式转换为纯数字格式:

# 错误示例:'π * 12^2 * 2 / 3'
# 正确示例:'3.14159 * 12 * 12 * 2 / 3'

调用结果示例

🚀 四 Agent 协同 (Router -> Executor -> Validator -> Responder)
============================================================

👤 用户: 上海现在冷不冷?
------------------------------------------------------------
🧠 [Router] 分析意图...
🧠 [Router] 模式: WEATHER
🔧 [Executor] 尝试 1...
🔧 参数: {'city': '北京'}
✅ [Tool Result] 城市: 北京
温度: 5.8°C
体感温度: 2.8°C
湿度: 67%
风速: 6.2 km/h
...
🔍 [Validator] 检查中...
❌ [Validator] 拒绝: 根据用户问题和工具执行结果,可以判断工具调用是否成功且符合用户意图。

用户问题:上海现在冷不冷?
参数:{'city': '北京'}
工具结果:城市: 北京
温度: 5.8°C
体感温度: 2.8°C
湿度: 67%
风速: 6.2 km/h
天气: 小雨

由于用户问题是关于上海的,而工具参数中设置的是北京,且工具执行结果显示的是北京的天气信息,因此可以判断工具调用失败。

输出:FAIL: 工具未找到城市 '上海'。用户问的是 '上海',请直接使用 '上海' 作为参数。
🔧 [Executor] 尝试 2...
🔧 参数: {'city': '上海'}
✅ [Tool Result] 城市: 上海
温度: 2.0°C
体感温度: -2.9°C
湿度: 54%
风速: 13.3 km/...
🔍 [Validator] 检查中...
✅ [Validator] 通过
🤖 最终回答: 上海现在还蛮冷的,外面只有2摄氏度呢!体感温度甚至是负数,感觉比实际温度还要冷。湿度也不是很高,风速也比较大,所以还是得穿暖和点。天气看起来是晴朗的,应该是干净利落的好天气。
============================================================

👤 用户: 123 * 456 是多少?
------------------------------------------------------------
🧠 [Router] 分析意图...
🧠 [Router] 模式: CALCULATOR
🔧 [Executor] 尝试 1...
🔧 参数: {'expression': '123 * 456'}
✅ [Tool Result] 56088...
🔍 [Validator] 检查中...
✅ [Validator] 通过
🤖 最终回答: 答案是:123乘以456等于56,088。
============================================================

👤 用户: 今天北京天气怎么样?
------------------------------------------------------------
🧠 [Router] 分析意图...
🧠 [Router] 模式: WEATHER
🔧 [Executor] 尝试 1...
🔧 参数: {'city': '北京'}
✅ [Tool Result] 城市: 北京
温度: 5.8°C
体感温度: 2.8°C
湿度: 67%
风速: 6.2 km/h
...
🔍 [Validator] 检查中...
✅ [Validator] 通过
🤖 最终回答: 今天北京的天气不太好,温度只有5.8摄氏度,感觉还要冷点,湿度也蛮高的67%,而且有小雨。风速也不是很大,只有6.2公里每小时。
============================================================

👤 用户: 月亮为什么是圆的
------------------------------------------------------------
🧠 [Router] 分析意图...
🧠 [Router] 模式: CHAT
🤖 最终回答: 这个问题其实很简单,月亮看起来像个圆圈,是因为它是球形的嘛!地球和其他行星都是这样子的,因为它们是在自转和绕太阳公转的过程中保持着平衡和稳定状态,所以才会呈现出圆形的外观。
============================================================

👤 用户: 兰州的天气热吗?
------------------------------------------------------------
🧠 [Router] 分析意图...
🧠 [Router] 模式: WEATHER
🔧 [Executor] 尝试 1...
🔧 参数: {'city': '勗安'}
未找到城市: 勗安
✅ [Tool Result] 无法获取天气信息...
🔍 [Validator] 检查中...
❌ [Validator] 拒绝: FAIL: 工具未找到城市 '勗安'。用户问的是 '兰州',请直接使用 '兰州' 作为参数。
🔧 [Executor] 尝试 2...
🔧 参数: {'city': '兰州'}
✅ [Tool Result] 城市: 兰州
温度: 4.7°C
体感温度: -0.2°C
湿度: 65%
风速: 18.0 km/...
🔍 [Validator] 检查中...
✅ [Validator] 通过
🤖 最终回答: 兰州的天气不算热,目前温度还比较低,接近4摄氏度,体感温度甚至有负数,湿度也不是很高,风速也相对较大。整体来说,兰州的天气还是比较凉爽的。
============================================================

👤 用户: 随便告诉我一个中国城市的天气,不要是北京,上海,兰州
------------------------------------------------------------
🧠 [Router] 分析意图...
🧠 [Router] 模式: WEATHER
🔧 [Executor] 尝试 1...
🔧 参数: {'city': '北京'}
✅ [Tool Result] 城市: 北京
温度: 5.8°C
体感温度: 2.8°C
湿度: 67%
风速: 6.2 km/h
...
🔍 [Validator] 检查中...
❌ [Validator] 拒绝: FAIL: 工具参数错误,用户要求的城市不是 '北京'。
🔧 [Executor] 尝试 2...
🔧 参数: {'city': '北京'}
✅ [Tool Result] 城市: 北京
温度: 5.8°C
体感温度: 2.8°C
湿度: 67%
风速: 6.2 km/h
...
🔍 [Validator] 检查中...
❌ [Validator] 拒绝: FAIL: 工具参数错误,用户要求的城市不是 '北京',而是其他城市。
🔧 [Executor] 尝试 3...
🔧 参数: {'city': '上海'}
✅ [Tool Result] 城市: 上海
温度: 2.0°C
体感温度: -2.9°C
湿度: 54%
风速: 13.3 km/...
🔍 [Validator] 检查中...
❌ [Validator] 拒绝: 根据用户的要求和工具的执行结果,我发现以下问题:

1. 用户要求随便告诉一个中国城市的天气,不要是北京,上海,兰州,但是参数中传入了 'city': '上海',这与用户要求相矛盾。
2. 工具执行结果显示的是上海的天气信息,而不是其他城市。

因此,我判断工具调用失败。
🔧 [Executor] 尝试 4...
🔧 参数: {'city': '广州'}
✅ [Tool Result] 城市: 广州
温度: 11.4°C
体感温度: 7.0°C
湿度: 56%
风速: 19.4 km/...
🔍 [Validator] 检查中...
✅ [Validator] 通过
🤖 最终回答: 你想知道广州的天气啊!现在广州的温度是11.4摄氏度,体感温度是7.0摄氏度,湿度是56%,风速是19.4公里每小时,天气大部分是晴朗的。
============================================================

👤 用户: 计算一个半径为12,高为2cm的圆锥的体积
------------------------------------------------------------
🧠 [Router] 分析意图...
🧠 [Router] 模式: CALCULATOR
🔧 [Executor] 尝试 1...
🔧 参数: {'expression': 'π * 12^2 * 2 / 3'}
✅ [Tool Result] 表达式无效或包含非法操作: 非法的语法结构: Name...
🔍 [Validator] 检查中...
❌ [Validator] 拒绝: 根据示例要求,我将分析用户问题、工具参数和工具执行结果来判断工具调用是否成功且符合用户意图。

**用户问题:** 计算一个半径为12,高为2cm的圆锥的体积

**工具参数:** {'expression': 'π * 12^2 * 2 / 3'}

**工具执行结果:** 表达式无效或包含非法操作: 非法的语法结构: Name

根据用户问题和工具参数,表达式应该是正确的,但实际上出现了错误。让我们分析一下:

* 用户的问题要求计算圆锥的体积。
* 工具参数中的表达式似乎是正确的,但执行结果显示有非法的语法结构。

原因可能是由于特殊字符或符号的使用,例如 `^` 和 `/`,导致工具无法正确解析表达式。正确的表达式应该是纯数字的公式,不使用特殊字符。

因此,我将输出:

FAIL: 表达式非法,转化为纯数字的公式,不使用特殊字符,例如'3.14159 * 12 * 12 * 2 / 3'。
🔧 [Executor] 尝试 2...
🔧 参数: {'expression': '3.14159 * 12 * 12 * 2 / 3'}
✅ [Tool Result] 301.59263999999996...
🔍 [Validator] 检查中...
✅ [Validator] 通过
🤖 最终回答: 这个圆锥的体积是301.59立方厘米。
============================================================

以上便是本文的全部内容,感谢您的阅读,如遇到任何问题,欢迎在评论区留言讨论。



评论