从 LLM 到购物车,这条链路看起来跨度很大,实际上是一个很适合做技术验证的 Agent 工程案例:用户说一句自然语言,模型理解意图,调用工具,最后把商品真正加进购物车,并返回结构化结果。这类 demo 在葡萄牙本地电商、社区商店、餐厅点单等场景里都很有用。这次我们就围绕 "From LLM to shopping cart (Portugal)" 这个方向,完整走一遍从模型对话到购物车落库的搭建和测试流程。
先给结论:这不是一个需要 100B 大模型的场景,常见做法是接入 OpenAI 兼容接口或本地 Ollama 部署的 Qwen / Llama 系列模型,配合 Function Calling / Tool Calling 能力,由 Agent 层的代码完成商品检索、加购、改数量、删商品等操作。真正的工程难点不在模型,而在工具函数怎么定义、购物车状态怎么管理、多轮对话怎么保持上下文,以及批量订单怎么处理。
接下来我会按"能跑起来的链路"来组织内容:先看这个方案能做什么,再讲环境怎么准备、核心代码怎么写、接口怎么测、批量任务怎么接、踩坑怎么排。想要在本地快速复现的同学,可以一路照着做下来。
1. 核心能力速览
先说整个方案的能力边界。这类 LLM + 购物车应用的通用能力如下:
| 能力项 | 说明 |
|---|---|
| 项目方向 | 基于 LLM Agent 的自然语言购物车操作 |
| 核心功能 | 自然语言加购、删商品、改数量、查购物车、结算辅助 |
| 模型接入 | OpenAI 兼容接口 / 本地 Ollama / 各类云模型 API |
| 关键机制 | Function Calling / Tool Calling、多轮上下文管理 |
| 服务形态 | FastAPI 中间层,外部系统可调用 HTTP API |
| 是否需要 GPU | 看模型选择;纯 API 方案不需要 GPU,本地小模型需 6G 以上显存 |
| 数据存储 | 开发阶段可用内存 dict 或 SQLite,生产建议接正式数据库 |
| 批量任务 | 支持按订单文件批量解析和加购 |
| 适用场景 | 葡语/多语言商店 Demo、Agent 应用教学、电商客服预处理 |
| 硬件门槛 | 低,普通开发机即可完成服务端开发测试 |
从这个表可以判断:这是一个偏"应用工程"的项目,不是模型训练项目。重点验证的是 LLM 的意图理解、工具调用和业务状态的一致性。
2. 这条链路解决什么问题
"From LLM to shopping cart" 的核心不是让模型聊天,而是让模型能真正操作业务系统。在葡萄牙本地电商或社区零售场景中,常见需求是:
- 用户用自然语言描述需求:"我要 2 瓶波特酒和一包盐渍鳕鱼。"
- 系统需要理解商品名称、数量、可能的规格偏好。
- 系统需要把商品信息映射到真实的商品 ID。
- 系统需要执行加购操作,并返回购物车总价。
如果只靠 Prompt 让模型输出文本,会出现两个问题:第一,模型可能把商品名"幻觉"成不存在的商品;第二,即使模型输出了正确的 JSON,也需要额外代码去解析、校验、调用后端接口。Function Calling 机制解决的就是这件事:模型只负责生成"调用哪个函数、传什么参数",真正执行动作的是你的代码。
从架构上看,这条链路是:
用户输入 -> LLM 意图识别与参数抽取 -> 工具函数调度 -> 购物车服务 -> 结构化响应这里有一个容易被忽略的点:购物车是一个"有状态"的系统。用户可能在一个会话里连续操作:"加一瓶波特酒,顺便把刚才的鳕鱼删掉。" 这意味着 Agent 层必须维护会话状态,或者每次请求都带上当前购物车 ID,让工具函数基于同一个购物车上下文去执行。
3. 适用场景与使用边界
适合用这个方案的人:
- 正在学 LLM Agent、Function Calling 的开发者,需要一个小而完整的练习项目。
- 想给当地小商店做自然语言点单 Demo 的人。
- 需要验证"模型输出 -> 业务动作"闭环的架构师。
- 做多语言电商客服预处理的团队,可以先在购物车场景跑通链路。
不适合的场景:
- 高并发、强一致的电商核心交易系统。生产环境不会让 LLM 直接写数据库,而是由 LLM 生成行为,业务层做完整校验和事务控制。
- 需要处理大量长尾商品的场景。如果商品库有几十万 SKU,必须配合 RAG / 商品搜索接口,不能靠模型记忆。
- 对结果可解释性要求极高的财务、医疗、法务场景。
使用边界和合规提醒也很重要:项目里如果出现真实用户地址、支付信息、历史订单,必须做脱敏;涉及真人声音、人脸、肖像的交互功能,必须获得明确授权;商品数据来自真实商店时,要确认是否有数据合规和版权要求。演示环境建议全部使用虚拟数据,不接入真实支付。
4. 整体架构设计
我这里给一个可落地的参考架构,本地开发完全够用。
核心组件有三个:
4.1 LLM 接入层
负责接收用户输入,拼装 system prompt,调用模型的 Function Calling 能力,获取结构化的工具调用请求。接入层要尽量做成可替换的,这样你可以先调云端 API 跑通,再切到本地 Ollama。
# llm_client.py 示例骨架 from openai import OpenAI client = OpenAI( base_url="http://localhost:11434/v1", # Ollama 的 OpenAI 兼容端点 api_key="ollama" # 本地服务可填任意值 ) def chat_with_tools(messages, tools): response = client.chat.completions.create( model="qwen2.5:7b", messages=messages, tools=tools, tool_choice="auto" ) return response.choices[0].message4.2 Agent 调度层
这是一个循环:模型返回工具调用请求 -> 执行工具 -> 把结果回传给模型 -> 模型生成最终回复。停止条件是模型不再请求工具调用。
def run_agent(user_input, cart_id, session_messages): session_messages.append({"role": "user", "content": user_input}) max_steps = 5 for _ in range(max_steps): message = chat_with_tools(session_messages, TOOLS) if message.tool_calls: session_messages.append(message) for tool_call in message.tool_calls: result = execute_tool(tool_call.function.name, tool_call.function.arguments) session_messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result }) else: session_messages.append(message) return message.content, session_messages return "处理步骤过多,请简化请求", session_messages4.3 购物车状态层
开发阶段可以直接用一个字典保存购物车数据。每个购物车有一个唯一 cart_id,内部保存商品明细。
carts = {} def get_cart(cart_id): if cart_id not in carts: carts[cart_id] = {"items": {}, "total": 0.0} return carts[cart_id]生产环境把 carts 替换为数据库表即可,接口设计保持一致。
5. 环境准备与前置条件
5.1 硬件和系统
- 操作系统:Windows / macOS / Linux 都可以。
- 纯 API 方案:不需要 GPU,普通开发机即可。
- 本地模型方案:建议 16G 内存 + 6G 以上显存,跑 7B 量化模型;CPU 推理也能跑,但响应会慢很多。
- 磁盘:代码项目很小,本地模型文件约 4~8G。
5.2 软件依赖
- Python 3.10 以上。
- pip 安装 openai、fastapi、uvicorn。
- 选装 ollama,用于本地模型推理。
pip install openai fastapi uvicorn requests5.3 模型准备
两种路线:
- 云端 API:准备一个 OpenAI 兼容的 API Key,模型可用 gpt-4o-mini、gpt-4o、claude 等。
- 本地模型:安装 Ollama 后拉取支持工具调用的模型,例如 qwen2.5:7b、llama3.1:8b。
ollama pull qwen2.5:7b这里要提醒:本地小模型的工具调用稳定性不如云端大模型。先跑通链路,再决定是否换更强模型。
6. 核心链路实现
下面开始写代码。先把工具函数定义好,再完成 Agent 调度和 FastAPI 服务。
6.1 定义工具函数
用 JSON Schema 格式描述工具,这一步最关键。字段名、类型、描述越清晰,模型抽取参数越准。
TOOLS = [ { "type": "function", "function": { "name": "add_to_cart", "description": "向购物车中添加商品,如果商品已存在则累加数量", "parameters": { "type": "object", "properties": { "cart_id": { "type": "string", "description": "购物车唯一标识" }, "product_name": { "type": "string", "description": "商品名称,必须是商品库中存在的名称" }, "quantity": { "type": "integer", "description": "商品数量,默认 1" } }, "required": ["cart_id", "product_name"] } } }, { "type": "function", "function": { "name": "remove_from_cart", "description": "从购物车中移除指定商品", "parameters": { "type": "object", "properties": { "cart_id": { "type": "string" }, "product_name": { "type": "string" } }, "required": ["cart_id", "product_name"] } } }, { "type": "function", "function": { "name": "get_cart_info", "description": "查看购物车当前所有商品和总价", "parameters": { "type": "object", "properties": { "cart_id": { "type": "string" } }, "required": ["cart_id"] } } } ]6.2 工具执行函数
这里用一个简单的葡萄牙商店商品库做演示数据:
# products.py PRODUCTS = { "vinho do porto": {"name": "Vinho do Porto", "price": 18.5}, "bacalhau": {"name": "Bacalhau salgado", "price": 12.9}, "pastéis de nata": {"name": "Pastéis de Nata", "price": 1.5}, "azeite": {"name": "Azeite Virgem Extra", "price": 8.9}, } # tools_impl.py import json from products import PRODUCTS from cart_store import get_cart def execute_tool(name, arguments_json): args = json.loads(arguments_json) cart = get_cart(args["cart_id"]) if name == "add_to_cart": product_name = args["product_name"].lower() if product_name not in PRODUCTS: return json.dumps({"error": f"商品 {args['product_name']} 不存在"}, ensure_ascii=False) qty = int(args.get("quantity", 1)) item = cart["items"].get(product_name) if item: item["quantity"] += qty else: cart["items"][product_name] = { "name": PRODUCTS[product_name]["name"], "price": PRODUCTS[product_name]["price"], "quantity": qty } cart["total"] = sum(i["price"] * i["quantity"] for i in cart["items"].values()) return json.dumps(cart, ensure_ascii=False) if name == "remove_from_cart": product_name = args["product_name"].lower() cart["items"].pop(product_name, None) cart["total"] = sum(i["price"] * i["quantity"] for i in cart["items"].values()) return json.dumps(cart, ensure_ascii=False) if name == "get_cart_info": return json.dumps(cart, ensure_ascii=False) return json.dumps({"error": f"未知工具 {name}"})这一段逻辑就是整个应用的核心:模型不直接改购物车,它只决定"调用哪个工具、传什么参数",你的代码校验参数、更新状态、返回结果。
6.3 启动 FastAPI 服务
用 FastAPI 暴露 HTTP 接口,方便后面用 curl 调试或接入其他系统。
# app.py from fastapi import FastAPI, HTTPException from pydantic import BaseModel from agent import run_agent import uuid app = FastAPI(title="LLM Shopping Cart Demo") class ChatRequest(BaseModel): message: str cart_id: str = None class ChatResponse(BaseModel): cart_id: str reply: str cart: dict @app.post("/api/chat", response_model=ChatResponse) def chat(req: ChatRequest): cart_id = req.cart_id or str(uuid.uuid4()) reply, messages = run_agent(req.message, cart_id, []) # 返回购物车最新状态 from cart_store import get_cart return ChatResponse(cart_id=cart_id, reply=reply, cart=get_cart(cart_id))启动命令:
uvicorn app:app --host 127.0.0.1 --port 8000启动后访问http://127.0.0.1:8000/docs可以看到 Swagger 文档。
7. 功能测试与效果验证
服务启动后,按下面的用例逐项验证。
7.1 基础加购测试
curl -X POST http://127.0.0.1:8000/api/chat \ -H "Content-Type: application/json" \ -d '{"message": "我要买 2 瓶波特酒和一包盐渍鳕鱼"}'预期结果:模型调用 add_to_cart 两次,返回购物车包含 vinho do porto 数量 2、bacalhau 数量 1,总价为18.5 * 2 + 12.9 = 49.9。
判断标准:cart字段中的商品名称、数量、总价完全正确。
7.2 多轮连续操作测试
curl -X POST http://127.0.0.1:8000/api/chat \ -H "Content-Type: application/json" \ -d '{"message": "加 3 个葡式蛋挞", "cart_id": "<上一步返回的cart_id>"}'然后继续发送:
curl -X POST http://127.0.0.1:8000/api/chat \ -H "Content-Type: application/json" \ -d '{"message": "把波特酒删掉", "cart_id": "<同一cart_id>"}'预期结果:第二次请求在原有购物车基础上追加,第三次请求移除波特酒。最终购物车只剩 bacalhau 和 pastéis de nata。
常见失败原因:模型没携带 cart_id,或者没有正确理解"删掉"对应 remove_from_cart。如果出现这类问题,优先检查工具描述是否有歧义。
7.3 商品不存在测试
curl -X POST http://127.0.0.1:8000/api/chat \ -H "Content-Type: application/json" \ -d '{"message": "我要买一碗 francesinha"}'预期结果:工具函数返回商品不存在,LLM 收到工具错误后应该用自然语言告知用户"该商品不在商品库中"。
判断标准:不会向购物车中写入任何不存在的商品,并且回复内容明确。
7.4 批量任务测试
这里再演示一个批量导入场景:用户提交一个订单文本,Agent 逐行解析并加购。
curl -X POST http://127.0.0.1:8000/api/chat \ -H "Content-Type: application/json" \ -d '{"message": "请根据以下清单加购:波特酒 1 瓶,蛋挞 6 个,橄榄油 2 瓶"}'预期结果:模型一次对话内连续调用多次 add_to_cart,全部执行成功后返回完整购物车状态。如果清单很长,注意设置 max_steps 上限,避免模型陷入死循环。
8. 接口 API 与批量任务设计
当这个 demo 要接入真实前端或收银系统时,API 层要考虑下面几个问题。
8.1 API 路径建议
| 方法 | 路径 | 功能 |
|---|---|---|
| POST | /api/chat | 对话并操作购物车 |
| GET | /api/cart/{cart_id} | 查询购物车状态 |
| POST | /api/cart/{cart_id}/items | 直接加购,不走 LLM |
| DELETE | /api/cart/{cart_id}/items/{product_name} | 删除商品 |
| POST | /api/batch/orders | 批量订单导入 |
直接操作购物车的接口要保留,因为生产环境不可能所有加购都经过 LLM,要留一条"确定性路径"。
8.2 批量订单导入实现
批量任务的核心是"按行解析 -> 逐条加购 -> 记录失败项"。可以先用 LLM 做信息抽取,再用确定性代码执行。
@app.post("/api/batch/orders") def batch_orders(orders: list[dict]): results = [] for order in orders: try: cart_id = order.get("cart_id") for item in order["items"]: execute_tool("add_to_cart", json.dumps(item)) results.append({"cart_id": cart_id, "status": "ok"}) except Exception as e: results.append({"cart_id": cart_id, "status": "failed", "error": str(e)}) return {"results": results}批量任务建议使用异步队列,并把失败项写入日志,方便重试。
8.3 Python 客户端调用示例
import requests url = "http://127.0.0.1:8000/api/chat" payload = { "message": "加购 4 个蛋挞", "cart_id": "demo-cart-001" } resp = requests.post(url, json=payload, timeout=60) print(resp.json())9. 资源占用与性能观察
这个项目本身对资源要求不高,主要观察两个点:一是 LLM 服务的响应时延,二是本地大模型的资源占用。
9.1 时延观察
- 云端 API 方案:单轮工具调用通常在 1~3 秒。
- 本地 Ollama + 7B 模型 CPU 推理:单轮可能 5~15 秒,多轮工具调用会成倍增加。
- 本地 GPU 推理:7B 量化模型约 1~3 秒。
如果连续多次工具调用,用户等待时间会叠加。解决方案是:给模型明确的指令,让它尽量一次性把参数都抽出来,减少来回次数。
9.2 模型服务占用
本地部署 Ollama 时,观察方法:
ollama ps这会列出当前常驻模型和显存占用。Qwen2.5 7B 的量化版本通常占用 5~7G 显存,具体以本机实际为准。如果显存不足,可以改用 3B 模型或直接切到云端 API。
9.3 应用服务资源
FastAPI 中间层本身占用内存不到 100M,主要开销在 LLM 请求的等待时间和日志记录。生产环境建议给服务设置请求超时,避免 LLM 卡住导致 HTTP 请求长时间挂起。
10. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 模型返回空内容 | 工具调用参数不合法 | 查看服务端日志中的 tool_calls | 简化工具描述,重试 |
| 商品加错数量 | 商品名映射失败或数量抽取错误 | 打印模型输出的 arguments | 商品库增加别名,比如 "波特酒" -> "vinho do porto" |
| 多轮对话丢失上下文 | 没有维护 session_messages | 检查 Agent 循环中的 messages 是否持续推进 | 保留 session_messages,不要每次重新创建 |
| LLM 调用超时 | 本地模型推理慢或 API 网络问题 | 单独测试模型接口 | 调大 timeout;本地模型换 GPU 或小模型 |
| 工具调用循环不退出 | 模型反复请求同一个工具 | 检查 max_steps 是否生效 | 增加步骤上限,工具执行失败时明确返回错误 |
| API 端口被占用 | 8000 被其他服务占用 | `netstat -ano | findstr 8000` |
| 购物车数据丢失 | 服务重启后内存数据清空 | 确认存储方式 | 开发阶段可接受,生产接 SQLite / Redis |
11. 最佳实践与使用建议
基于这个 demo 的工程化经验,整理几条建议。
第一,商品库字段设计要兼顾模型容易理解和代码容易校验。商品 ID 用稳定编码,商品名称提供中文/英文/葡语多语言别名,模型输入提示词里给出"商品清单 + 别名映射",能明显降低幻觉。
第二,所有工具函数都要做参数校验,不能信任模型的输出。模型可能抽出小数数量、负数量、不存在的商品名。函数开头必须校验类型、范围、存在性。
第三,Agent 循环要设置最大步数和超时。一个用户请求最多允许调用 3~5 次工具,超过就终止并返回错误。这样可以避免模型陷入重复调用。
第四,日志要能还原整个链路。建议记录用户输入、模型返回的工具调用、工具执行结果、最终回复。这是排查问题时最有效的依据。
第五,如果是面向真实用户的购物应用,不要直接用 LLM 写订单数据库。正确的做法是 LLM 生成候选动作,人类或规则层确认后再落库。尤其是涉及支付、折扣、优惠码的场景,必须走确定性代码。
第六,涉及用户购物车、地址、历史订单等信息时,做好最小化存储和脱敏。演示环境用假数据,生产环境遵守当地数据保护法规。
12. 总结与下一步
"From LLM to shopping cart" 这类项目最值得尝试的点,是用最少的代码把 LLM 从"聊天机器人"变成"业务操作员"。整个链路跑通后,你会发现真正难的并不是模型选择,而是工具定义和状态管理。
第一步建议先验证基础加购和查购物车,这是最核心的两个函数。跑通之后再扩展多轮修改、失败兜底和批量导入。最容易踩的坑是模型不按你的工具 Schema 输出参数,这时候不要急着换模型,先检查工具描述是否写清楚了"什么时候调用、参数怎么填"。
后续可以继续扩展的方向包括:接入 RAG 做更大规模商品检索,接入 MCP 连接真实电商后端,基于同一个 Agent 骨架做订单查询、物流跟踪等更多工具,以及把对话链路从单轮改为多轮记忆。这套模式的表达能力很强,购物车只是一个起点。