用AI代理自动订购麦当劳

为什么点击按钮时,代理可以为我们做这些?我是如何构建一个多代理系统,自动订购我的每周麦当劳套餐的,使用了谷歌的ADK、Selenium和太多工程热情

用AI代理自动订购麦当劳
免责声明

发表的观点和意见仅代表我个人,不代表雇主(麦当劳)的官方政策或立场。

自动化与网站交互,尤其是涉及用户账户和交易的自动化,可能会引发道德法律问题。始终确保您的自动化活动符合网站的服务条款及相关法律法规。此外,请考虑自动化任务与用户数据和隐私交互的伦理影响。

本项目仅用于说明性目的,旨在促进对基于代理系统的更深入了解。它严格用于测试教育用途,不得用于任何商业或生产用途

你可以在这里访问完整的代码库。

不会发生实际订购—— 该脚本在完成支付之前会停止——你需要手动完成结账。

1、周三麦乐鸡传统:一段个人旅程

每个星期三中午,我都会站在芝加哥麦当劳全球菜单餐厅的队伍中,满怀期待地等待他们轮换提供的国际美食精选。咬一口来自日本的辣黑蒜鸡块,再品尝意大利风味的开心果麦旋风,最后配上澳大利亚的芝士培根加载薯条,这种体验简直令人陶醉。

但作为一名软件工程师,有着固定的食物习惯:为什么还要手动操作,当你可以用AI代理来过度设计它呢?

从“我想知道是否能自动化我的麦当劳订单”的简单想法,很快演变成了一套完整的代理到代理(A2A)编排系统

2、架构:AI代理的交响曲

与其构建一个单一的食品订购机器人,我设计了一个多代理生态系统,其中每个代理都有特定的责任,通过结构化协议进行通信,并共同实现复杂的自主食品订购目标。

以下是序列图中的主要步骤简要分解:

  • 定时触发器(周三中午12:00)SchedulerAgent检测到预定时间并触发UserProxy开始每周订单。
  • 用户请求User发送自然语言命令(“订购我通常的周三特别套餐”),该命令被记录并传递给OrderAgent
  • 发现与委派OrderAgent查询MCP以获取可用工具/代理,然后相应地委派子任务。
  • 菜单解析MenuAgent使用Gemini将短语“通常的周三特别套餐”解析为具体的菜单项。
  • 网页自动化WebAgent利用Selenium自动化UberEats网站,将选定的物品添加到购物车中。
  • 预算与结账CheckoutAgent验证订单是否符合预算限制和配送地址。
  • 付款处理:安全登录和付款由系统处理,订单在UberEats上提交。
  • 结果与确认:系统记录结果,汇总结果,并向User发送最终确认。
  • 监控LoggerAgent持续跟踪系统健康状况、性能和分析。
来源

3、代理卡:AI的名片

我们生态系统中的每个代理都通过代理卡展示其能力——JSON描述符,描述代理可以做什么、如何联系它以及它提供的技能。这遵循了谷歌的A2A协议标准:

agent_card = AgentCard(  
    name="菜单理解代理",  
    description="AI驱动的菜单解析器和推荐器",  
    url=f"http://localhost:9004/",  
    version="1.0.0",  
    defaultInputModes=["text"],  
    defaultOutputModes=["json"],  
    capabilities=AgentCapabilities(streaming=True),  
    skills=[  
        AgentSkill(  
            id="parse_menu_intent",  
            name="解析菜单意图",  
            description="理解自然语言食物请求",  
            tags=["nlp", "菜单", "解析", "gemini"],  
            examples=["something spicy", "my usual", "healthy option"]  
        )  
    ]  
)

这些卡片在/.well-known/agent.json端点上可发现,从而实现动态代理发现和组合。

4、深入探讨:代理编排

4.1 UserProxy代理:对话的起点

UserProxy代理是我们的系统入口。无论是由周三中午12:00的计划任务触发,还是由手动的“I'm hungry”消息触发,这个代理都会标准化输入并将其转发给主协调器。

async def process_message(self, message: Dict[str, Any]) -> Dict[str, Any]:  
    self.logger.info(f"收到订单请求:{message}")  

    order_request = {  
        "type": "order_request",  
        "user_input": message.get("content", ""),  
        "timestamp": datetime.now().isoformat(),  
        "source": "user_proxy"  
    }  

    return await self._send_to_order_agent(order_request)

4.2 菜单理解代理:语言天才

这里变得有趣起来。我没有硬编码菜单项,而是构建了一个由Gemini 2.0 Flash驱动的代理,它可以理解自然语言的食物请求:

  • “Something spicy” → 辣黑蒜鸡块
  • “My usual Wednesday special” → 国际轮换菜品
  • “Surprise me” → 基于全球菜单的精选
menu_agent = LlmAgent(  
    model='gemini-2.0-flash',  
    name='menu_parser',  
    instruction=(  
        "你是一位麦当劳全球菜单专家。解析用户的请求为具体的菜单项。 "  
        "可用的项目包括:日本的辣黑蒜鸡块, "  
        "意大利的开心果麦旋风,澳大利亚的芝士培根加载薯条, "  
        "以及标准的麦当劳菜单。返回一个包含具体项目的JSON列表。"  
    ),  
    tools=[]  
)

这种方法的美妙之处在于语义理解。代理不仅仅是匹配关键词——它理解上下文、偏好,甚至可以在项目不可用时做出智能替换。

4.3 网页自动化代理:数字木偶师

这就是关键所在。网页自动化代理利用Selenium通过MCP(模型上下文协议)服务器与UberEats互动,就像人类一样,但具有超人的精确度和速度。

async def get_selenium_tools(self):  
    """初始化Selenium MCP工具"""  
    if not self.selenium_tools:  
        mcp_params = StdioServerParameters(  
            command="selenium-mcp-server",  
            args=["--headless", "--ubereats-mode"]  
        )  

        toolset = MCPToolset("selenium_automation", mcp_params)  
        self.selenium_tools, _ = await toolset.get_tools_async()  

    return self.selenium_tools

该代理采用系统化的方法:

  1. 导航到UberEats.com
  2. 搜索芝加哥的麦当劳®(全球菜单餐厅)
  3. 选择全球菜单位置
  4. 添加项目到购物车基于解析的菜单意图
  5. 准备结账交接

4.4 结账代理:安全金库

安全性在处理付款信息时至关重要。结账代理是隔离的,并处理:

  • 认证存储的凭据
  • 地址验证(我的芝加哥位置)
  • 付款处理通过安全API
  • 订单确认
async def process_message(self, message: Dict[str, Any]) -> Dict[str, Any]:  
    checkout_steps = [  
        "验证用户凭据",  
        "验证芝加哥的配送地址",   
        "处理付款方式",  
        "确认订单详情",  
        "向餐厅提交订单"  
    ]  

    for step in checkout_steps:  
        self.logger.info(f"结账步骤:{step}")  
        await asyncio.sleep(0.5)  # 模拟处理时间

4.5 调度代理:时间管理员

调度代理体现了我的周三传统。它运行一个连续循环,检查那个神奇的时刻:周三中午12:00

async def start_scheduling(self):  
    self.is_running = True  

    while self.is_running:  
        now = datetime.now()  

        # 检查是否是周三且午餐时间(中午12:00)  
        if now.weekday() == 2 and now.hour == 12 and now.minute == 0:  
            self.logger.info("周三午餐时间!触发订单...")  
            await self._trigger_weekly_order()  

        await asyncio.sleep(60)

5、技术深度解析:A2A协议的实现

5.1 消息传递架构

每个代理通过结构化的JSON消息进行通信,实现松耦合并便于调试:

order_request = {  
    "type": "order_request",  
    "user_input": "给我一些辣的东西",  
    "timestamp": "2025-06-08T12:00:00",  
    "source": "user_proxy"  
}

5.2 异步处理

整个系统基于async/await模式构建,允许多个代理并发工作而不阻塞彼此:

# 步骤1:解析意图(非阻塞)  
parsed_order = await menu_agent.process_message(parse_request)  

# 步骤2:网页自动化(与日志记录并发)  
automation_task = asyncio.create_task(web_agent.process_message(automation_request))  
logging_task = asyncio.create_task(logger.log_activity("web_automation_started"))  
automation_result = await automation_task

5.3 错误处理与弹性

每个代理都实现了全面的错误处理和优雅降级:

try:  
    result = await web_agent.process_message(automation_request)  
    if result.get("status") == "ready_for_checkout":  
        return await self._proceed_to_checkout(result)  
    else:  
        return self._handle_automation_failure(result)  
except Exception as e:  
    self.logger.error(f"严重故障:{str(e)}")  
    return {"status": "system_error", "fallback": "manual_order_required"}

5.4 协调器:指挥交响乐

McDonaldsA2AOrchestrator充当指挥家,管理代理生命周期和代理间通信:

class McDonaldsA2AOrchestrator:  
    def __init__(self):  
        self.agents = {  
            "user_proxy": UserProxyAgent(),  
            "order_agent": OrderAgent(),  
            "web_automation": WebAutomationAgent(),  
            "menu_understanding": MenuUnderstandingAgent(),  
            "checkout": CheckoutAgent(),  
            "scheduler": SchedulerAgent(),  
            "logger": LoggerAgent()  
        }  

    async def start_system(self):  
        print("🍟 启动麦当劳A2A订购系统...")  
        await self.agents["scheduler"].process_message({"command": "start_scheduling"})  
        print("✅ 所有代理已初始化并准备就绪!")

5.5 可观察性:日志代理

在分布式系统中,可观察性至关重要。日志代理提供集中式日志记录和分析:

def get_analytics(self) -> Dict[str, Any]:  
    total_logs = len(self.log_storage)  
    error_logs = len([log for log in self.log_storage if log["level"] == "error"])  

    return {  
        "total_events": total_logs,  
        "error_rate": error_logs / total_logs if total_logs > 0 else 0,  
        "agents_active": len(set(log["agent"] for log in self.log_storage)),  
        "weekly_orders": self._count_successful_orders()  
    }

5.6 要克服的挑战

  1. 网站更改:UberEats经常更新他们的UI。网页自动化代理需要适应性选择器和回退策略。
  2. 速率限制:过多的快速请求触发了反机器人措施。增加了智能延迟和类似人类的交互模式。
  3. 菜单可用性:全球菜单项目轮换不可预测。构建了当标志性项目不可用时的回退逻辑。

6、从.env到CIBA:在浏览器之外保护A2A身份验证

在这个早期的不安全版本中,我天真地通过.env文件暴露敏感凭据——接下来,我将重点转向在浏览器之外保护A2A身份验证,使用CIBA,这是一种无需浏览器参与的身份验证流程,客户端直接向OpenID提供者发起身份验证请求。

import os  
from dotenv import load_dotenv  

load_dotenv() # 加载变量从.env  
ubereats_username = os.getenv("DATABASE_URL")  
ubereats_password = os.getenv("SECRET_KEY")  
debug_mode = os.getenv("DEBUG")

在我的下一篇文章中——我将探索客户端启动的后通道身份验证(CIBA),它不需要客户端应用程序通过浏览器重定向用户来进行登录/身份验证过程。相反,客户端应用程序直接通过后通道请求向OpenID提供者发起身份验证流。

7、结束语

这个项目是过度工程和个人享受的完美结合。我现在有了一个AI驱动的仪式,满足了我的灵魂(和胃)。快乐餐操作,确实如此。

但价值不在此处——

为什么我们应该关心AI代理?这不是另一个技术热潮吗? 实际上,不是——这感觉像是一个真正的转折点。

AI模式正在改变一切。谷歌不再是单纯的搜索引擎;它正在进化成完全不同的东西。它不再只是显示链接列表,而是现在生成这些AI制作的“微型网站”,直接在结果页面上构建,以回答你的问题。所以猜猜怎么着?人们点击进入实际网站的次数减少了。他们直接从AI版本的内容中获取所需的信息。

这是个大事。这意味着我们需要重新思考如何创建和组织内容。这不再仅仅是关键词和SEO——这是关于让你的内容易于大型语言模型理解。这就是MCPs(模型可消费页面)的来源。这些页面是为了让AI代理能够轻松消化和使用你的数据,而无需加载你的完整站点。

这是网络的未来吗?老实说,我觉得可能是这样。时间会证明,但我对此充满希望。

想想像mcdonalds.com这样的网站——人们可能不会再直接访问它相反,AI只是从站点中提取它需要的内容,并给用户答案。没有点击,没有页面浏览量——只有机器到机器的交互。


原文链接:How I Built an AI Agent to Order McDonald’s for Me Every Wednesday

汇智网翻译整理,转载请标明出处