(三)MCP+A2A 从0到1构建类Manus多Agent全栈应用

(三)基于事件与后台运行的Agent模块开发

1、多Agent规划步骤与记忆模型的设计与开发

1、记忆模型

app/domain/models/memory.py

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time    : 2025/05/17 14:56
@Author  : thezehui@gmail.com
@File    : memory.py
"""
import logging
from typing import List, Dict, Any, Optional

from pydantic import BaseModel, Field

logger = logging.getLogger(__name__)


class Memory(BaseModel):
    """记忆类,定义Agent的记忆基础信息"""
    messages: List[Dict[str, Any]] = Field(default_factory=list)

    @classmethod
    def get_message_role(cls, message: Dict[str, Any]) -> str:
        """根据传递的消息来获取消息的角色信息"""
        return message.get("role")

    def add_message(self, message: Dict[str, Any]) -> None:
        """往记忆中添加一条消息"""
        self.messages.append(message)

    def add_messages(self, messages: List[Dict[str, Any]]) -> None:
        """往记忆中添加多条消息"""
        self.messages.extend(messages)

    def get_messages(self) -> List[Dict[str, Any]]:
        """获取记忆中的所有消息列表"""
        return self.messages

    def get_last_message(self) -> Optional[Dict[str, Any]]:
        """获取记忆中的最后一条消息,如果不存在则返回None"""
        return self.messages[-1] if len(self.messages) > 0 else None

    def roll_back(self) -> None:
        """回滚记忆,删除最后一条消息"""
        self.messages = self.messages[:-1]

    def compact(self) -> None:
        """记忆压缩,将记忆中已经执行的工具(搜索/网页源码获取/浏览器访问结果等)这类已经执行过的消息进行压缩检索"""
        # 1.循环遍历所有的消息列表
        for message in self.messages:
            # 2.判断消息的角色是否为tool
            if self.get_message_role(message) == "tool":
                if message.get("function_name") in ["browser_view", "browser_navigate"]:
                    message["content"] = "(removed)"
                    logger.debug(f"从记忆中移除对应工具的结果: {message['function_name']}")

            # 3.压缩记忆时reasoning_content内容可以去除压缩上下文
            if "reasoning_content" in message:
                logger.debug(f"从记忆中移除工具思考结果: {message['reasoning_content'][:50]}...")
                del message["reasoning_content"]

    @property
    def empty(self) -> bool:
        """只读属性,检查记忆是否为空"""
        return len(self.messages) == 0

2、创建任务

app/domain/models/plan.py

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time    : 2025/05/17 15:10
@Author  : thezehui@gmail.com
@File    : plan.py
"""
import uuid
from enum import Enum
from typing import List, Optional

from pydantic import BaseModel, Field


class ExecutionStatus(str, Enum):
    """规划/任务执行的状态"""
    PENDING = "pending"  # 空闲or等待中
    RUNNING = "running"  # 执行中
    COMPLETED = "completed"  # 执行完成
    FAILED = "failed"  # 失败


class Step(BaseModel):
    """计划中的每一个步骤/子任务"""
    id: str = Field(default_factory=lambda: str(uuid.uuid4()))  # 子任务id  每次创建一个新的 Step,如果用户没有提供 id,就重新调用这个函数生成一个值。
    description: str = ""  # 步骤的描述信息
    status: ExecutionStatus = ExecutionStatus.PENDING  # 子任务的执行状态
    result: Optional[str] = None  # 结果
    error: Optional[str] = None  # 错误信息
    success: bool = False  # 是否执行成功
    attachments: List[str] = Field(default_factory=list)  # 附件列表信息

    @property
    def done(self) -> bool:
        """只读属性,返回步骤是否结束"""
        return self.status in [ExecutionStatus.COMPLETED, ExecutionStatus.FAILED]


class Plan(BaseModel):
    """规划Domain模型,用于存储用户传递消息拆分出来的子任务/子步骤"""
    id: str = Field(default_factory=lambda: str(uuid.uuid4()))  # 计划id
    title: str = ""  # 任务标题
    goal: str = ""  # 任务目标
    language: str = ""  # 工作语言
    steps: List[Step] = Field(default_factory=list)  # 步骤列表/子任务列表
    message: str = ""  # AI传递的消息
    status: ExecutionStatus = ExecutionStatus.PENDING  # 规划的状态
    error: Optional[str] = None  # 错误信息

    @property
    def done(self) -> bool:
        """只读属性,用于判断计划是否结束"""
        return self.status in [ExecutionStatus.COMPLETED, ExecutionStatus.FAILED]

    def get_next_step(self) -> Optional[Step]:
        """获取需要执行的下一个步骤"""
        return next((step for step in self.steps if not step.done), None)

app/domain/models/event.py

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time    : 2025/05/18 0:54
@Author  : thezehui@gmail.com
@File    : event.py
"""
import uuid
from datetime import datetime
from enum import Enum
from typing import Literal, List, Union, Optional, Any, Dict, Annotated

from pydantic import BaseModel, Field

from .file import File
from .plan import Plan, Step
from .search import SearchResultItem
from .tool_result import ToolResult


class PlanEventStatus(str, Enum):
    """规划事件状态"""
    CREATED = "created"  # 已创建
    UPDATED = "updated"  # 已更新
    COMPLETED = "completed"  # 已完成


class StepEventStatus(str, Enum):
    """步骤事件状态"""
    STARTED = "started"  # 已开始
    COMPLETED = "completed"  # 已完成
    FAILED = "failed"  # 失败


class ToolEventStatus(str, Enum):
    """工具事件状态类型枚举"""
    CALLING = "calling"  # 调用中
    CALLED = "called"  # 调用完毕


class BaseEvent(BaseModel):
    """基础事件类型"""
    id: str = Field(default_factory=lambda: str(uuid.uuid4()))  # 事件id
    type: Literal[""] = ""  # 事件的类型
    created_at: datetime = Field(default_factory=datetime.now)  # 事件创建时间


class PlanEvent(BaseEvent):
    """规划事件类型"""
    type: Literal["plan"] = "plan"
    plan: Plan  # 规划
    status: PlanEventStatus = PlanEventStatus.CREATED  # 规划事件状态


class TitleEvent(BaseEvent):
    """标题事件类型"""
    type: Literal["title"] = "title"
    title: str = ""  # 标题


class StepEvent(BaseEvent):
    """子任务/步骤事件"""
    type: Literal["step"] = "step"
    step: Step  # 步骤信息
    status: StepEventStatus = StepEventStatus.STARTED


class MessageEvent(BaseEvent):
    """消息事件,包含人类消息和AI消息"""
    type: Literal["message"] = "message"
    role: Literal["user", "assistant"] = "assistant"  # 消息角色
    message: str = ""  # 消息本身
    attachments: List[File] = Field(default_factory=list)  # 附件列表信息


class BrowserToolContent(BaseModel):
    """浏览器工具扩展内容"""
    screenshot: str  # 浏览器快照截图


class SearchToolContent(BaseModel):
    """搜索工具内容"""
    results: List[SearchResultItem]  # 搜索结果列表


class ShellToolContent(BaseModel):
    """Shell工具内容"""
    console: Any  # 控制台内容


class FileToolContent(BaseModel):
    """文件工具内容"""
    content: str  # 文件内容


class MCPToolContent(BaseModel):
    """MCP工具内容"""
    result: Any  # MCP工具结果


class A2AToolContent(BaseModel):
    """A2A智能体工具内容"""
    a2a_result: Any  # A2A智能体调用结果


ToolContent = Union[
    BrowserToolContent,
    SearchToolContent,
    ShellToolContent,
    FileToolContent,
    MCPToolContent,
    A2AToolContent,
]


class ToolEvent(BaseEvent):
    """工具事件"""
    type: Literal["tool"] = "tool"
    tool_call_id: str  # 工具调用id
    tool_name: str  # 工具箱/工具集的名字
    tool_content: Optional[ToolContent] = None  # 工具扩展内容
    function_name: str  # LLM调用函数/工具名字
    function_args: Dict[str, Any]  # LLM生成的工具调用参数
    function_result: Optional[ToolResult] = None  # 工具调用结果
    status: ToolEventStatus = ToolEventStatus.CALLING  # 工具事件状态


class WaitEvent(BaseEvent):
    """等待事件,等待用户输入确认"""
    type: Literal["wait"] = "wait"


class ErrorEvent(BaseEvent):
    """错误事件"""
    type: Literal["error"] = "error"
    error: str = ""  # 错误信息


class DoneEvent(BaseEvent):
    """结束事件类型"""
    type: Literal["done"] = "done"


# 定义应用事件类型声明
Event = Annotated[
    Union[
        PlanEvent,
        TitleEvent,
        StepEvent,
        MessageEvent,
        ToolEvent,
        WaitEvent,
        ErrorEvent,
        DoneEvent,
    ],
    Field(discriminator="type"),
]

2、LLM结构化输出缺陷与JSON修复解析器开发

python 复制代码
https://github.com/mangiucugna/json_repair

安装:

python 复制代码
pip install json-repair

用法:

python 复制代码
import json_repair

decoded_object = json_repair.loads(json_string)
# 或者
decoded_object = json_repair.repair_json(json_string, return_objects=True)

app/domain/external/json_parser.py

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time    : 2025/05/18 1:44
@Author  : thezehui@gmail.com
@File    : json_parser.py
"""
from typing import Protocol, Optional, Any, Union, Dict, List


class JSONParser(Protocol):
    """JSON解析器,用于解析json字符串并修复"""

    async def invoke(self, text: str, default_value: Optional[Any] = None) -> Union[Dict, List, Any]:
        """调用函数,用于将传递进来的文本进行解析并返回"""
        ...

app/infrastructure/external/json_parser/repair_json_parser.py

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time    : 2025/05/18 1:46
@Author  : thezehui@gmail.com
@File    : repair_json_parser.py
"""
import logging
from typing import Optional, Any, Union, Dict, List

import json_repair

from app.domain.external.json_parser import JSONParser

logger = logging.getLogger(__name__)


class RepairJSONParser(JSONParser):
    """基于修复逻辑的json解析器"""

    async def invoke(self, text: str, default_value: Optional[Any] = None) -> Union[Dict, List, Any]:
        """传递文本,并使用json修复库进行修复"""
        # 1.记录日志并判断text是否传递
        logger.info(f"解析json文本: {text}")
        if not text or not text.strip():
            if default_value is not None:
                return default_value
            raise ValueError("json文本为空,且无默认值")

        # 2.存在数值则使用json_repair库修复并解析
        return json_repair.repair_json(text, ensure_ascii=False, return_objects=True)

3、工具基类与tool装饰器开发实现将函数转工具

相关推荐
the3clipse1 小时前
Windows 10 与 Windows 11 视频编解码区别:AV1、HEVC、HDR 与 D3D12 编码支持对比
windows·h.265·hevc·hdr·av1·视频解码·d3d12
the3clipse2 小时前
MacOS、Linux 与 Windows 视频编解码对比:AV1、HEVC、ProRes 与硬件加速优劣势
linux·windows·macos·hevc·av1·视频解码·硬件解码
明王明王2 小时前
PVE 上用 SPICE:统一连接 Debian、Arch 和 Windows 11 桌面
运维·windows·debian
学逆向的2 小时前
win32消息类型
windows·网络安全·mfc·api·win32
TeamDev2 小时前
JxBrowser 9.5.2 版本发布啦!
java·前端·windows·chromium·混合应用·jxbrowser·浏览器控件
段一凡-华北理工大学13 小时前
大模型应用开发 100 天:Python + LLM 从入门到精通 day26~Prompt 调试与优化——A/B 测试与效果评估
windows·python·大模型·prompt·智能体·提示词工程·高炉智能化
fox_charon14 小时前
Windows 下 CLI 参数的引号陷阱:为什么 --resume 'uuid' 会失败
windows·shell·cmd·cli·引号
染指111016 小时前
128.Agent-LangChain核心组件-中间件-调用模型的时候
人工智能·windows·中间件·langchain·agent
Frag0ut16 小时前
Chrome与Chromium内核浏览器在Windows 11上的新特性全景解析
前端·chrome·windows·web安全·chromium·gemini ai·playready drm