对话式购物助手很容易做出一个演示:把商品列表塞进提示词,再让大模型生成推荐。但进入真实交易链路后,价格是否仍然有效、商品能否配送、用户是否过敏、购物车里已有何物,都不能靠模型猜测。
DoorDash 在介绍 Ask DoorDash 时,强调的正是这种工程边界:LLM 负责理解和表达,专用 AI Agent 负责拆解任务,MCP 工具连接实时后端,智能层则保存消费者的持久记忆。公开的早期结果显示,该系统带来了最高 24% 的结账转化率提升、17% 的购物篮金额增长;使用记忆支持的会话后,意图识别准确度也有所改善。
LLM 是交互入口,不是事实数据库
购物请求往往同时包含偏好、约束和实时状态。例如:
帮我找一家还营业的餐厅,给两个人点一顿不含花生、预算 40 美元以内的晚餐,最好和上周那次差不多。
这句话至少需要处理五类信息:
- 当前意图:搜索并构建一份晚餐订单。
- 长期偏好:用户喜欢什么菜系或商家。
- 强约束:花生过敏、预算上限和用餐人数。
- 历史上下文:上周点过什么。
- 实时数据:营业状态、库存、价格、配送范围和费用。
LLM 可以把自然语言转成结构化任务,但不能凭已有参数知识可靠回答实时问题。更稳妥的职责划分是:
- LLM 解析意图、补齐缺失信息并组织回复。
- Agent 根据任务类型选择搜索、推荐、购物车或订单等流程。
- MCP 工具以明确的输入输出契约访问后端能力。
- 智能层合并持久记忆、当前会话和实时业务数据。
- 确定性服务执行价格计算、库存校验与购物车修改。
这样即使模型生成了一条听起来合理的建议,系统仍会在展示或执行前用后端数据验证它。
专用 Agent 缩小每次决策的范围
单个通用 Agent 如果同时负责检索、推荐、预算计算和下单,提示词会越来越长,工具选择也更容易出错。专用 Agent 可以把问题拆成边界清晰的子任务,例如:
- 搜索 Agent:理解品类、菜系、地理位置和营业状态。
- 推荐 Agent:结合偏好、历史行为和候选商品进行排序。
- 购物车 Agent:处理数量、替代品、预算与冲突检查。
- 订单 Agent:查询进行中的订单或售后状态。
这类拆分的价值不只在于提示词更短。每个 Agent 可以拥有独立的工具白名单、超时策略、评估集和降级路径。推荐 Agent 可以读取偏好,却不应直接提交订单;购物车 Agent 可以提出修改方案,但高风险操作仍应要求用户确认。
MCP 在这里承担的是工具协议层角色。模型看到的不是数据库连接或内部 RPC,而是带名称、参数和返回结构的业务工具。工具契约越具体,模型越不容易把“搜索商品”和“修改购物车”混为一谈。
持久记忆必须区分偏好与事实
记忆支持的会话能够提升意图准确度,但“记住一切”并不是合格设计。购物助手至少要区分三类状态:
| 状态 | 示例 | 建议生命周期 |
|---|---|---|
| 当前会话 | “这次给 4 个人吃” | 会话结束后过期 |
| 稳定偏好 | 偏爱素食、常买某品牌 | 跨会话保存,可由用户修改 |
| 敏感约束 | 食物过敏、饮食禁忌 | 显式确认,展示来源和更新时间 |
历史偏好只适合参与排序,不能覆盖本次明确表达。例如用户过去常点牛肉,但当前说“今天吃素”,当前约束必须拥有更高优先级。过敏信息也不能只依赖模型从旧对话中总结;执行前应重新校验,并允许用户查看、纠正或删除记忆。
此外,实时数据与记忆发生冲突时,应以实时后端为准。记忆可以说明“用户常买某商品”,却不能证明该商品今天有库存或价格未变。
可以这样实践:一个可运行的最小编排器
下面是一个假设性示例,用标准库模拟 Agent 路由、MCP 风格工具注册、会话状态和持久偏好。它不是 DoorDash 的内部实现,但可以作为接入真实 LLM、数据库和 MCP Server 前的项目骨架。
将代码保存为 shopping_assistant.py,使用 Python 3.10 及以上版本运行。实际接入时,把 classify_intent 替换为返回结构化 JSON 的模型调用,把两个工具函数替换为后端或 MCP 客户端。
from dataclasses import dataclass, field
from typing import Any, Callable
Tool = Callable[..., dict[str, Any]]
@dataclass
class Memory:
preferences: dict[str, Any] = field(default_factory=dict)
sessions: dict[str, list[dict[str, str]]] = field(default_factory=dict)
def context(self, user_id: str, session_id: str) -> dict[str, Any]:
return {
"preferences": self.preferences.get(user_id, {}),
"recent_turns": self.sessions.get(session_id, [])[-4:],
}
def append(self, session_id: str, role: str, content: str) -> None:
self.sessions.setdefault(session_id, []).append(
{"role": role, "content": content}
)
class ToolRegistry:
def __init__(self) -> None:
self._tools: dict[str, Tool] = {}
def register(self, name: str, tool: Tool) -> None:
self._tools[name] = tool
def call(self, name: str, **arguments: Any) -> dict[str, Any]:
if name not in self._tools:
raise ValueError(f"Tool not allowed: {name}")
return self._tools[name](**arguments)
def search_catalog(query: str, vegetarian: bool = False) -> dict[str, Any]:
# Replace this fixture with live catalog, availability and delivery data.
products = [
{"id": "meal-1", "name": "Vegetable curry", "price": 14.0,
"vegetarian": True, "available": True},
{"id": "meal-2", "name": "Chicken bowl", "price": 16.0,
"vegetarian": False, "available": True},
]
matches = [
item for item in products
if item["available"] and (not vegetarian or item["vegetarian"])
]
return {"query": query, "items": matches}
def price_cart(item_ids: list[str], budget: float) -> dict[str, Any]:
prices = {"meal-1": 14.0, "meal-2": 16.0}
total = sum(prices[item_id] for item_id in item_ids)
return {"item_ids": item_ids, "total": total, "within_budget": total <= budget}
def classify_intent(message: str) -> str:
# In production, use constrained model output such as an enum or JSON schema.
return "recommend" if any(word in message.lower() for word in ["find", "meal", "推荐"]) else "unknown"
def handle(message: str, user_id: str, session_id: str,
memory: Memory, tools: ToolRegistry) -> dict[str, Any]:
memory.append(session_id, "user", message)
context = memory.context(user_id, session_id)
intent = classify_intent(message)
if intent != "recommend":
answer = "I need more detail about what you want to shop for."
memory.append(session_id, "assistant", answer)
return {"intent": intent, "answer": answer}
vegetarian = bool(context["preferences"].get("vegetarian", False))
results = tools.call("catalog.search", query=message, vegetarian=vegetarian)
candidate_ids = [item["id"] for item in results["items"][:2]]
cart = tools.call("cart.price", item_ids=candidate_ids, budget=40.0)
answer = (
f"Found {len(candidate_ids)} available option(s). "
f"Live total: ${cart['total']:.2f}; within budget: {cart['within_budget']}."
)
memory.append(session_id, "assistant", answer)
return {"intent": intent, "answer": answer, "cart_preview": cart}
if __name__ == "__main__":
memory = Memory(preferences={"user-42": {"vegetarian": True}})
tools = ToolRegistry()
tools.register("catalog.search", search_catalog)
tools.register("cart.price", price_cart)
result = handle(
"Find a meal for two under $40",
user_id="user-42",
session_id="session-1",
memory=memory,
tools=tools,
)
print(result)
运行命令:
python shopping_assistant.py
这个骨架刻意把“理解”和“执行”分开。生产环境还应在工具层增加身份认证、权限控制、幂等键、超时、重试和审计日志,并在修改购物车或提交订单前加入显式确认。
上线时盯住业务结果,也要盯住失败路径
转化率和购物篮金额能说明助手是否创造了交易价值,但不能单独证明系统可靠。落地时可以同时检查:
- 意图识别准确率,以及低置信度请求的澄清率。
- 工具选择正确率、参数有效率和后端调用失败率。
- 推荐商品展示前的价格、库存与配送资格校验率。
- 记忆命中后真正改善结果的比例,以及错误记忆的纠正成本。
- 用户确认前发生写操作的次数,理想值应为零。
- 延迟、模型成本,以及工具失败时的降级体验。
Ask DoorDash 的架构传递了一个清晰信号:交易型 AI 的核心竞争力并不是让模型说得更像导购,而是把模型限制在它擅长的理解与生成环节,再用 Agent、工具契约、持久记忆和实时后端共同完成可验证的决策。对于准备落地购物助手的团队,这通常比不断扩大提示词更值得投入。