LLM落地购物车场景:Agent编排、RAG检索与MCP工具集成实践
2026/8/28 3:08:11 网站建设 项目流程

如果只看最近刷屏的 LLM 演示,很容易产生一种错觉:模型已经什么都能干了。它能写代码、能总结文档、能扮演客服,下一件事自然是让它直接替用户把东西买好。但真正做过业务系统的人心里都清楚,从“模型说可以下单”到“商品真的进入购物车、价格正确、库存够、状态可查”,中间隔着一条很宽的工程河流。From LLM to shopping cart (Portugal)这个标题,我理解就是把这条河画成了一条路径:左端是 LLM,右端是一个真实购物车,中间是 Agent、RAG、MCP、状态同步和人工确认。

为什么拿购物车当例子?因为购物车是最典型的“LLM 必须兑现结果”的场景。模型不能只回答问题,它必须对真实业务状态产生写操作。加购、改数量、删商品、清单汇总,每个动作背后都牵动商品服务、库存服务、价格服务和会话状态。一次加购失败,用户不会觉得是模型的问题,而是觉得这个系统坏了。换句话说,购物车是对 LLM 应用工程化的最好测试,比单纯的问答助手更能暴露问题。

这篇文章不打算停在概念层面。我会用购物车这个场景,从 LLM 应用开发的角度拆一条可落地的最小链路:整体架构、环境准备、工具定义、Agent 主循环、RAG 商品检索、MCP 工具标准化,以及上线前真正要小心的坑。读完之后,你可以把同样的模式移植到订单、预约、工单、审批等任何“要动真实数据”的 LLM 场景。项目标题里的 Portugal 只是场景背景,但它提醒我们:只要涉及多语言、本地化商品库和真实交易,LLM 落地就不再是“调一个 API”那么简单。

1. 为什么“LLM 到购物车”值得拆解

先回答一个问题:直接让模型把商品加入购物车,难在哪里?如果只是做演示,答案很简单,让模型输出一段 JSON,代码解析后调用购物车接口就够了。但放到真实业务里,问题会立刻变多:用户说“我要一个防水背包”,系统怎么确认商品库里真的有这个商品?模型把商品名写错了怎么办?用户说“加两件”,模型只加了一件怎么办?用户在多轮对话里反复修改数量,购物车状态存哪里?价格会不会被模型记错?

这些问题说明,LLM 从“能聊天”到“能办事”,关键不在模型本身,而在于模型外围的工程闭环。购物车场景把这个闭环压缩得非常具体:模型必须理解用户意图,必须访问真实商品数据,必须调用真实写接口,必须把结果回传给用户,整个过程还要满足电商对准确性的基本要求。购物车同时包含了读操作和写操作,这使它成为 LLM Agent 的最佳练习场。

把 Portugal 放回标题也能看出这个项目的另一层意图:购物车不是英文世界的专利。一个葡萄牙用户可能用葡萄牙语说“Quero uma mochila para caminhadas”,系统要能理解不同语言的商品描述,要在本地商品库里检索,要处理欧元计价和本地化的税费规则。这意味着 LLM 应用必须要支持多语言意图识别、本地化商品召回和真实交易状态同步,而不是只做英文 demo。

所以,这篇文章真正想解决的问题是:怎么把一个只会生成的 LLM,变成一个能对购物车产生准确写操作的 Agent?它适合三类读者:准备在电商业务里接入 LLM 的开发者,正在做 LLM Agent 应用但不确定工具调用怎么设计的人,以及想理解 RAG、MCP 在真实项目里到底有什么用的人。如果你只是想要一个能聊天的机器人,本文会超出你的需求;如果你要的是让模型真正帮助你完成业务操作,这篇文章正好是起点。

2. 核心概念:LLM、Agent、RAG、MCP 在购物车场景中的角色

很多 LLM 应用的文章会把概念名词堆在一起,读者看完还是不知道它们各自负责什么。这里直接放到购物车场景里解释。

概念通俗理解在购物车场景中的角色
LLM 大语言模型根据上下文生成文本的模型理解用户“想买防水背包”的意图,生成自然语言回复
Agent 智能体控制模型进行多步推理和决策的框架决定“先搜索商品,再调用加购工具”,并循环执行直到完成
Tool 工具调用让模型输出结构化调用参数,由代码执行真实操作模型输出add_to_cart(item_id="PT-BP-001", quantity=1),代码真正写入购物车
RAG 检索增强生成先从外部知识库检索相关内容,再让模型基于检索结果回答从商品目录中找出真实存在的背包,避免模型编造商品名
MCP 模型上下文协议把外部工具能力标准化,让 Agent 统一连接把购物车服务暴露成标准工具,Agent 端无需定制开发
会话状态保存多轮对话和业务状态的存储购物车商品列表、数量、总价,应存在服务端而非模型上下文

在这组概念里,LLM 只负责“理解和表达”,真正干活的是工具调用。Agent 是控制循环,RAG 是为模型提供事实依据,MCP 是工具接入的标准化方式。很多人一开始把重点放在“选哪个模型”,实际上模型之外的工具链和状态设计才是决定项目成败的部分。

顺便说一个容易混淆的点:工具调用和 Agent 不是一回事。工具调用是模型的一个能力,它让模型输出一段结构化参数;Agent 则是在多轮循环里反复使用模型输出、执行工具、把结果回传,直到任务完成。购物车场景里,用户说“加一件防水背包”,模型先调用搜索工具,拿到商品 ID,再调用加购工具,这个过程就是一次典型的 Agent 循环。

还有一个值得记住的设计原则:不要让模型直接输出“购物车最终状态”。模型应该输出的是操作指令,真实状态永远由购物车服务计算并返回。这样才能保证价格、库存、商品信息都来自真实系统,而不是模型记忆。这个原则后面会反复用到。

3. 整体架构:从用户提问到购物车更新的完整链路

用购物车场景串起来看,一个 LLM 驱动的加购流程从用户输入到最后状态更新,通常包含七个环节:

  1. 用户输入:用户用自然语言描述需求,可能是葡萄牙语,也可能是中文、英文。
  2. 意图识别:Agent 让模型判断用户想做什么,是搜索商品、加购、改数量,还是删除商品。
  3. 商品检索:通过 RAG 或数据库全文检索,从商品目录中召回候选商品。
  4. 工具调用:模型输出add_to_cart等工具参数,代码执行真实写操作。
  5. 状态持久化:购物车商品和数量写入服务端存储,而不是放在模型上下文里。
  6. 关键动作确认:加购可以自动执行,但涉及金额敏感动作时必须加入人工确认。
  7. 结果回写:模型基于工具返回结果,生成一句用户能看懂的自然语言总结。

这个链路的核心是“状态分离”。购物车的真实状态始终在业务系统里,模型只是状态的修改者和解释者。这样做的好处非常直接:即使模型出错,用户看到的购物车状态仍然是准确可查的;即使换一个模型,购物车数据也不会受影响。

架构上通常把系统分成三层。最外层是对话接入层,负责接收用户消息、校验格式、管理会话;中间是 Agent 编排层,负责维护多轮消息、调用模型、执行工具、控制循环次数;最底层是业务服务层,包括商品服务、购物车服务、库存服务和确认服务。LLM 只出现在编排层,业务服务层不该被模型直接穿透。

这里要特别提醒一个新手容易踩的坑:不要为了“让模型更聪明”就把购物车状态塞进系统提示词。比如把整个购物车 JSON 放到 Prompt 里让模型自己维护,这在短期内能跑通,但一旦商品多了、并发来了、模型输出了错误 JSON,状态就会彻底丢失。更稳妥的做法是让模型每次通过工具读取真实状态,Prompt 里只保留指令和约束。

4. 环境准备与依赖

本文的示例代码以 Python 为主,因为 Python 在处理工具调用、向量检索和快速原型时最直接。如果团队技术栈是 Java,也可以把同样的设计迁移到 Spring AI,配置方式以官方文档为准,本文不针对某个框架写死配置。

建议环境如下:

  • 操作系统:Linux、macOS、Windows 均可,示例不依赖特定系统。
  • Python 版本:3.10 或更高,主要使用类型标注和 dataclass。
  • Python 依赖:openaipython-dotenvnumpy
  • 模型接口:兼容 OpenAI Chat Completions 和 Embeddings 接口的服务,具体模型名称以你的实际可用模型为准。
  • 代码结构:建议按模块拆分,便于后续扩展。

安装依赖的命令如下:

pip install openai python-dotenv numpy

版本请以实际项目为准,本文重点演示通用思路。如果你使用pip时需要指定版本,可以安装后使用pip freeze | grep openai查看当前版本。

在项目根目录创建.env文件,写入模型服务地址和密钥。这里只做环境配置示例,不要把真实密钥提交到代码仓库:

# .env OPENAI_API_KEY=sk-your-key-here OPENAI_BASE_URL=https://api.openai.com/v1

代码中使用python-dotenv加载环境变量,并创建 OpenAI 客户端:

# cart_demo/config.py import os from dotenv import load_dotenv from openai import OpenAI load_dotenv() client = OpenAI() # 请替换为你的环境可用的对话模型和向量模型 MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4o-mini") EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "text-embedding-3-small")

这一段看起来简单,但有一个容易被忽略的点:OpenAI()默认会读取环境变量OPENAI_API_KEYOPENAI_BASE_URL,所以配置文件和代码要分离。同时,模型名称不要写死在代码里,通过环境变量控制,这样切换到其他模型时不用改代码。

5. 最小闭环实现:让 LLM 真正往购物车里加商品

现在进入核心部分。我们先用最少的代码跑通“用户说一句话,模型调用工具,购物车状态被真实更新”的闭环。示例文件按下面的结构组织:

cart_demo/ ├── config.py ├── cart_service.py ├── tools.py ├── agent.py └── main.py

先创建商品目录和购物车服务。为了演示,商品列表放在内存中,真实项目应替换为数据库或微服务接口。

# cart_demo/cart_service.py """ 购物车服务简化实现。 正式项目中,这里通常是对购物车微服务的调用,而不是进程内的 dict。 """ from dataclasses import dataclass, field # 商品目录,实际项目来自商品服务或数据库 PRODUCTS = [ { "item_id": "PT-BP-001", "name": "Porto Trail Backpack 30L", "price": 49.90, "tags": ["mochila", "backpack", "waterproof", "hiking", "trekking", "30l"], }, { "item_id": "PT-BP-002", "name": "Lisbon Urban Sling 8L", "price": 29.90, "tags": ["mochila", "bag", "urban", "lightweight", "daily", "sling"], }, { "item_id": "PT-SH-001", "name": "Algarve Beach Towel", "price": 19.90, "tags": ["towela", "beach", "summer", "banho"], }, ] PRICE_MAP = {p["item_id"]: p["price"] for p in PRODUCTS} @dataclass class CartService: session_id: str items: dict = field(default_factory=dict) # item_id -> quantity def add_item(self, item_id: str, quantity: int = 1) -> dict: if quantity <= 0 or quantity > 99: raise ValueError("quantity must be between 1 and 99") if item_id not in PRICE_MAP: return {"ok": False, "error": "商品不存在,请先调用 search_products"} self.items[item_id] = self.items.get(item_id, 0) + quantity return { "ok": True, "session_id": self.session_id, "item_id": item_id, "quantity": self.items[item_id], "cart": dict(self.items), "total_amount": self.total_amount(), } def remove_item(self, item_id: str) -> dict: self.items.pop(item_id, None) return {"ok": True, "session_id": self.session_id, "cart": dict(self.items)} def get_cart(self) -> dict: return {"ok": True, "session_id": self.session_id, "cart": dict(self.items)} def total_amount(self) -> float: return round(sum(PRICE_MAP.get(i, 0) * q for i, q in self.items.items()), 2) cart = CartService(session_id="demo-session")

这里最关键的设计是:购物车服务自己维护商品数量和总价,模型不参与价格计算。total_amount完全基于PRICE_MAP,避免模型记忆价格导致结算错误。

接着定义工具。工具必须遵循模型能理解的结构,描述要写清楚“什么时候用”和“注意什么”。

# cart_demo/tools.py TOOLS = [ { "type": "function", "function": { "name": "search_products", "description": "按用户描述搜索可购买商品。调用 add_to_cart 之前必须先调用本工具确认商品存在。", "parameters": { "type": "object", "properties": { "query": {"type": "string", "description": "商品关键词或用户描述,例如 mochila para caminhadas"} }, "required": ["query"], }, }, }, { "type": "function", "function": { "name": "add_to_cart", "description": "将指定商品加入购物车,成功返回最新购物车内容和总价。", "parameters": { "type": "object", "properties": { "item_id": {"type": "string", "description": "商品 ID"}, "quantity": {"type": "integer", "description": "数量,默认 1"}, }, "required": ["item_id"], }, }, }, ]

工具定义的描述直接决定模型会不会正确使用工具。如果描述里没有“先搜索再加购”的约束,模型很可能从自己的记忆里编一个商品 ID,然后调用add_to_cart。这也是幻觉问题在工具链里最常见的表现。

接下来是 Agent 主循环。它做的事情只有三件:把用户消息发给模型、查看模型是否要求调用工具、如果要求就执行工具并把结果回传,然后继续下一轮,直到模型给出最终回答。

# cart_demo/agent.py import json from cart_demo.cart_service import PRICE_MAP, PRODUCTS, cart from cart_demo.config import client, MODEL_NAME from cart_demo.tools import TOOLS def search_products(query: str, top_k: int = 3) -> dict: q = query.lower() results = [] for p in PRODUCTS: text = f"{p['name']} {' '.join(p['tags'])}".lower() if q in text or any(len(token) > 2 and token in text for token in q.split()): results.append(p) return {"results": results[:top_k]} def add_to_cart(item_id: str, quantity: int = 1) -> dict: return cart.add_item(item_id, quantity) TOOL_IMPL = { "search_products": search_products, "add_to_cart": add_to_cart, } def execute_tool(name: str, args: dict): if name not in TOOL_IMPL: return {"ok": False, "error": f"unknown tool: {name}"} return TOOL_IMPL[name](**args) def run_agent(user_message: str, max_steps: int = 5): messages = [{"role": "user", "content": user_message}] for _ in range(max_steps): response = client.chat.completions.create( model=MODEL_NAME, messages=messages, tools=TOOLS, ) message = response.choices[0].message if not message.tool_calls: return message.content messages.append(message) for tool_call in message.tool_calls: name = tool_call.function.name args = json.loads(tool_call.function.arguments) result = execute_tool(name, args) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result, ensure_ascii=False), }) raise RuntimeError("工具调用超过最大轮数,终止 Agent")

这段代码有几个细节值得注意。max_steps是为了防止模型陷入死循环,比如模型反复调用同一个工具而不再给出最终回答。每条工具调用结果都通过tool_call_id与对应的调用关联,这是当前模型接口的标准要求。json.loads解析模型返回的参数,如果模型输出了非法 JSON,这里会直接抛异常,真实项目应该加上异常捕获和重试。

最后是入口文件:

# cart_demo/main.py from cart_demo.agent import run_agent if __name__ == "__main__": user_msg = "Quero uma mochila para caminhadas. Adiciona 1 ao carrinho." print(run_agent(user_msg))

这句葡萄牙语的意思是“我想要一个徒步背包,加 1 个到购物车”。运行后看到的预期结果,会在第 7 章详细

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询