
把大模型从“聊天”接到真实业务动作是很多开发者在 LLM 应用落地时遇到的第一个坎。最近我在做一个面向葡萄牙市场的电商演示项目时正好把这条链路完整走了一遍用户说一句“我想买一盒葡式蛋挞再加两瓶波特酒”系统的购物车里就真的多了对应商品数量、总价、商品信息全部对得上。这个过程中涉及 LLM Agent、Function Calling、工具调用编排、会话状态管理等问题网上资料比较零散很多教程只讲到“调通 API 聊天”就结束了距离真正能处理业务操作还差一大截。所以这篇文章会从零开始搭建一个“LLM 购物车助手”以葡萄牙电商场景为例支持中文、英语、葡萄牙语pt-PT输入用户说出购物意图后由 LLM 自动决定调用哪个工具、传入什么参数最终把商品正确写入购物车。文章会先讲清楚 LLM Agent 和工具调用的核心原理再给出完整的项目代码、运行验证方法、常见报错排查和工程化建议。无论你是刚接触 LLM 应用开发的新手还是已经在做 Agent 实战、跨境电商系统的开发者都可以直接照着跑一遍。1. 为什么需要从 LLM 到购物车1.1 传统购物车交互的局限传统电商购物车的交互模式通常是这样用户打开商品列表点“加入购物车”再点“结算”。每一步都由前端事件驱动后端接收固定参数。这种模式稳定、可控但问题也很明显用户必须自己完成“搜索商品 → 筛选 → 加购 → 修改数量”的完整流程系统无法理解“我想买点甜的和一瓶酒”这种模糊意图。更麻烦的是多语言场景。面向葡萄牙市场的电商网站用户可能用葡萄牙语写下“Quero comprar um pastel de nata e duas garrafas de vinho do Porto”传统表单很难把这句自然语言拆解成两个商品、三个数量的结构化订单。要么让用户手工搜索要么做复杂的意图识别规则维护成本非常高。1.2 LLM 购物助手的价值LLM 购物助手的思路完全不同用户直接描述需求LLM 负责理解意图、拆解需求、决定调用哪些工具系统只需要提供一组“工具函数”让 LLM 执行。上面的葡萄牙语句子LLM 会先解析出“pastel de nata × 1”和“vinho do Porto × 2”然后依次调用搜索商品、加入购物车这两个工具最终把购物车状态返回给用户。这种方案的价值在于交互门槛低用户不需要知道商品 ID 或分类路径。多语言支持天然成立只要把语言要求写进系统提示词。意图拆解能力强一次对话可以完成多个购物车操作。后续可以轻松扩展“查物流”“算运费”“结算”等更多工具。1.3 为什么以葡萄牙购物场景为例选择葡萄牙场景并不是为了“加个地名”而是因为欧洲电商有几个非常典型的工程问题多语言尤其葡萄牙语区分欧洲葡语 pt-PT 和巴西葡语 pt-BR、欧元计价、地区商品词汇差异大。比如葡萄牙人说“购物车”更常用 “carrinho de compras”结算是 “finalizar compra”同一商品在不同语种下的说法完全不同。这些问题恰好是 LLM 擅长的也是传统关键词搜索容易翻车的地方。用这个场景做案例既能演示技术链路也更接近真实跨境业务的形态。2. 核心概念梳理2.1 LLM Agent 是什么LLM Agent智能体可以理解为一个“会使用工具的大模型”。普通 LLM 只能根据输入生成文字而 Agent 在生成文字的基础上还能决定“是否需要调用某个外部函数”并根据函数返回结果继续推理直到完成用户目标。在购物车场景里LLM Agent 的循环可以简单描述为接收用户消息“我想买一盒葡式蛋挞和两瓶波特酒”。判断需要搜索商品于是调用search_products工具。拿到商品列表后判断需要加购于是调用add_to_cart工具。所有操作完成后生成一段自然语言回复告诉用户购物车现状。这个“感知 → 决策 → 行动 → 观察结果 → 继续决策”的循环就是 Agent 的核心运转方式。2.2 Function Calling 与工具调用Function Calling函数调用是让 LLM 能够调用外部工具的关键机制。开发者先声明一组工具每个工具包含名称、描述、参数结构LLM 在对话时根据用户意图输出一个结构化的“调用请求”而不是直接执行代码。举个例子工具声明大概是这样的{ type: function, function: { name: add_to_cart, description: 把指定商品加入购物车, parameters: { type: object, properties: { product_id: {type: string, description: 商品ID}, quantity: {type: integer, description: 购买数量} }, required: [product_id] } } }当用户说“加两瓶波特酒”LLM 会输出类似add_to_cart(product_idp002, quantity2)的结构。我们的代码负责真正执行这个函数再把执行结果回传给 LLM。这里的关键点是LLM 只负责“决定调用什么”真正操作购物车数据的还是我们自己写的 Python 函数这一点保证了业务逻辑可控、可审计。2.3 MCP、RAG 在其中扮演什么角色随着项目变大工具数量可能从几个变成几十个这时候就需要更规范的工具接入方式。MCPModel Context Protocol提供了一套标准化的工具接入协议让模型服务、工具提供方可以按统一格式对接避免每家 SDK 各写一套。本文为了控制复杂度会直接使用 Function Calling 的原生方式实现但你会发现工具的定义结构本身已经具备“协议化”的影子后续迁移到 MCP 并不困难。RAG检索增强生成则是另一种增强手段。当商品数量达到上千甚至上万时单纯靠关键词匹配很难覆盖葡萄牙语的各种写法。RAG 的思路是先把商品描述做向量化用户提问时先用语义检索召回最相关的商品再交给 LLM 做下一步决策。后面第 6 节会给出引入 RAG 的建议。3. 环境准备与项目结构3.1 环境清单本文的示例代码以 Python 实现环境要求如下操作系统Windows / macOS / Linux 均可命令以 bash 风格示例。Python建议 3.10 或更高版本主要使用类型注解和标准库特性。LLM 服务需要一个兼容 OpenAI Chat Completions 接口的模型服务并准备好 API Key。无论是官方接口还是第三方兼容服务都可以本文代码只依赖openaiPython SDK。依赖库fastapi、uvicorn、openai、pydantic、python-dotenv。版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。建议安装时直接取 PyPI 上的最新稳定版。3.2 项目结构项目命名为llm-shopping-cart目录结构如下llm-shopping-cart/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI 入口暴露 HTTP 接口 │ ├── catalog.py # 商品目录与搜索 │ ├── cart.py # 购物车服务内存态 │ ├── tools.py # 工具定义与执行器 │ └── agent.py # LLM Agent 编排逻辑 ├── .env.example # 环境变量模板 ├── requirements.txt # Python 依赖 └── README.md # 项目说明3.3 依赖与配置文件先创建requirements.txtfastapi uvicorn openai pydantic python-dotenv再创建.env.exampleLLM_API_KEYsk-xxxxxxxx LLM_BASE_URLhttps://api.your-llm-provider.com/v1 LLM_MODELyour-model-name使用前复制为.env并填入真实配置。注意.env不要提交到 Git 仓库。4. 实战开发LLM 购物车助手下面进入核心部分我会按照文件逐个编写代码。为了便于理解每次只用一个工具函数负责一件事保证代码可以直接复制运行。4.1 商品目录模型创建app/catalog.py定义葡萄牙商品的示例数据和一个简单的关键词搜索函数。这里每个商品都准备了中、英、葡相关的关键词方便不同语言的用户搜索。# app/catalog.py PRODUCTS [ { id: p001, name: Pastel de Nata, keywords: [pastel, nata, 蛋挞, 葡式蛋挞, 甜点], price_eur: 1.5, category: pastry, description: 葡萄牙经典葡式蛋挞 }, { id: p002, name: Vinho do Porto, keywords: [porto, 波特酒, 葡萄酒, wine], price_eur: 12.0, category: wine, description: 葡萄牙波特酒适合餐后搭配奶酪 }, { id: p003, name: Sardinhas, keywords: [sardinhas, 沙丁鱼, 海鲜, seafood], price_eur: 8.5, category: seafood, description: 葡萄牙家庭常见的烤沙丁鱼 }, { id: p004, name: Azeite, keywords: [azeite, 橄榄油, olive oil], price_eur: 9.9, category: essentials, description: 葡萄牙特级初榨橄榄油 }, { id: p005, name: Queijo da Serra, keywords: [queijo, cheese, 奶酪, 塞拉奶酪], price_eur: 15.0, category: dairy, description: 葡萄牙塞拉山区的特色软质奶酪 } ] def get_product_by_id(product_id: str): for p in PRODUCTS: if p[id] product_id: return p return None def search_products(keyword: str): keyword_lower keyword.strip().lower() if not keyword_lower: return {items: PRODUCTS, count: len(PRODUCTS)} result [] for p in PRODUCTS: searchable .join(p[keywords]).lower() searchable p[name].lower() searchable p[description].lower() if keyword_lower in searchable: result.append(p) return {items: result, count: len(result)}这里的keywords字段是关键。如果用户用葡萄牙语说 “dois vinhos”LLM 会先意识到需要搜索“vinho”而search_products能通过wine和波特酒等关键词把它召回。没有这一层多语言搜索就很难做对。4.2 购物车服务创建app/cart.py。购物车按session_id隔离每个会话维护一份独立的商品列表。为了演示方便这里使用内存存储生产环境建议替换为 Redis 或数据库。# app/cart.py from .catalog import get_product_by_id class ShoppingCartService: def __init__(self): self._carts {} def _get_cart(self, session_id: str): if session_id not in self._carts: self._carts[session_id] [] return self._carts[session_id] def add_item(self, session_id: str, product_id: str, quantity: int 1): cart self._get_cart(session_id) quantity max(1, quantity) product get_product_by_id(product_id) if not product: return {error: f商品 {product_id} 不存在} for item in cart: if item[product_id] product_id: item[quantity] quantity return self.get_cart(session_id) cart.append({ product_id: product_id, name: product[name], price_eur: product[price_eur], quantity: quantity, }) return self.get_cart(session_id) def remove_item(self, session_id: str, product_id: str, quantity: int None): cart self._get_cart(session_id) for item in cart: if item[product_id] product_id: if quantity is None or quantity item[quantity]: cart.remove(item) else: item[quantity] - quantity break return self.get_cart(session_id) def get_cart(self, session_id: str): cart self._get_cart(session_id) total round(sum(i[price_eur] * i[quantity] for i in cart), 2) return {items: cart, total_eur: total} def clear_cart(self, session_id: str): self._carts[session_id] [] return self.get_cart(session_id)这里有几个设计点需要说明数量下限为 1防止模型传入 0 或负数导致购物车异常。加购时先检查商品是否存在错误信息会回传给 LLM让模型有机会重新决策。get_cart每次都会重新计算总价避免出现“数量改了但总价没更新”的脏数据。4.3 工具定义创建app/tools.py。这一步把“LLM 能调用的能力”明确声明出来并实现真正的执行逻辑。# app/tools.py import json from .catalog import search_products as _search_products from .cart import ShoppingCartService cart_service ShoppingCartService() TOOLS [ { type: function, function: { name: search_products, description: 根据关键词搜索商品目录关键词支持中文、英文或葡萄牙语, parameters: { type: object, properties: { keyword: {type: string, description: 搜索关键词} }, required: [keyword] } } }, { type: function, function: { name: add_to_cart, description: 把指定商品加入购物车, parameters: { type: object, properties: { product_id: {type: string, description: 商品ID}, quantity: {type: integer, description: 购买数量默认1} }, required: [product_id] } } }, { type: function, function: { name: view_cart, description: 查看当前购物车内容和总价, parameters: { type: object, properties: {} } } }, { type: function, function: { name: remove_from_cart, description: 从购物车移除或减少某个商品, parameters: { type: object, properties: { product_id: {type: string, description: 商品ID}, quantity: {type: integer, description: 要减少的数量不传则移除整行} }, required: [product_id] } } }, { type: function, function: { name: clear_cart, description: 清空购物车, parameters: { type: object, properties: {} } } } ] def execute_tool(name: str, arguments: str, session_id: str): args json.loads(arguments or {}) if name search_products: return _search_products(args.get(keyword, )) if name add_to_cart: return cart_service.add_item(session_id, args[product_id], args.get(quantity, 1)) if name view_cart: return cart_service.get_cart(session_id) if name remove_from_cart: return cart_service.remove_item(session_id, args[product_id], args.get(quantity)) if name clear_cart: return cart_service.clear_cart(session_id) return {error: f未知工具: {name}}工具描述写得越清楚LLM 就越不容易选错工具。特别是description字段里的“支持中文、英文或葡萄牙语”这句话是引导模型正确使用搜索能力的关键。4.4 Agent 编排逻辑创建app/agent.py这里实现整个 Agent 的核心循环。代码使用openaiSDK 发起对话并把工具列表传给模型。# app/agent.py import json import os from openai import OpenAI from .tools import TOOLS, execute_tool client OpenAI( api_keyos.getenv(LLM_API_KEY), base_urlos.getenv(LLM_BASE_URL), ) MODEL os.getenv(LLM_MODEL, gpt-4o-mini) SYSTEM_PROMPT 你是一个面向葡萄牙电商场景的购物助手。 你可以搜索商品、把商品加入购物车、查看购物车、修改或移除购物车商品。 用户可能使用中文、英文或葡萄牙语pt-PT交流请尽量使用与用户一致的语言回答。 如果用户使用葡萄牙语请使用欧洲葡萄牙语pt-PT表达 例如购物车用 carrinho de compras结算用 finalizar compra。 每次工具调用后用一句自然语言向用户说明刚才发生了什么并概括当前购物车总价。 def run_agent(session_id: str, user_message: str, history: list None) - str: messages [{role: system, content: SYSTEM_PROMPT}] if history: messages.extend(history) messages.append({role: user, content: user_message}) while True: response client.chat.completions.create( modelMODEL, messagesmessages, toolsTOOLS, tool_choiceauto, ) message response.choices[0].message # 如果没有工具调用说明 Agent 已经可以回答用户了 if not message.tool_calls: return message.content or # 先把模型的工具调用消息加入上下文再逐个执行工具 messages.append(message) for tool_call in message.tool_calls: result execute_tool( tool_call.function.name, tool_call.function.arguments, session_id, ) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse), })这个while True循环是整个 Agent 的心脏。一次用户请求可能触发多轮工具调用比如先搜索、再加购、再查看购物车每轮执行结果都会回填到消息上下文里让模型基于最新状态继续决策。直到某次返回没有tool_calls说明模型认为任务已经完成此时直接把回复返回给调用方。4.5 FastAPI 接口封装创建app/main.py把 Agent 能力暴露成 HTTP 接口便于前端或测试工具调用。# app/main.py import os from typing import List, Optional from dotenv import load_dotenv from fastapi import FastAPI from pydantic import BaseModel load_dotenv() from .agent import run_agent from .tools import cart_service app FastAPI(titleLLM Shopping Cart (Portugal)) class ChatRequest(BaseModel): session_id: str message: str history: Optional[List[dict]] None class ChatResponse(BaseModel): reply: str cart: dict app.post(/api/chat, response_modelChatResponse) def chat(req: ChatRequest): reply run_agent(req.session_id, req.message, req.history) cart cart_service.get_cart(req.session_id) return {reply: reply, cart: cart} app.get(/api/cart/{session_id}) def get_cart(session_id: str): return cart_service.get_cart(session_id) app.delete(/api/cart/{session_id}) def clear_cart(session_id: str): return cart_service.clear_cart(session_id)这里要注意session_id是购物车隔离的维度前端可以按登录用户或浏览器会话生成唯一值传过来。生产环境不要沿用示例里的临时字符串应该使用用户身份体系中的唯一标识。4.6 启动与验证按顺序执行下面的命令# 安装依赖 pip install -r requirements.txt # 配置环境变量 cp .env.example .env # 编辑 .env填入 LLM_API_KEY、LLM_BASE_URL、LLM_MODEL # 启动服务 uvicorn app.main:app --reload服务启动后用 curl 模拟一次用户请求curl -X POST http://localhost:8000/api/chat \ -H Content-Type: application/json \ -d { session_id: demo-001, message: 我想买一盒葡式蛋挞和两瓶波特酒 }正常情况下你能看到类似下面的响应{ reply: 好的我已经帮你把 1 个 Pastel de Nata 和 2 瓶 Vinho do Porto 加入购物车当前总价为 25.5 欧元。, cart: { items: [ {product_id: p001, name: Pastel de Nata, price_eur: 1.5, quantity: 1}, {product_id: p002, name: Vinho do Porto, price_eur: 12.0, quantity: 2} ], total_eur: 25.5 } }再用葡萄牙语试试curl -X POST http://localhost:8000/api/chat \ -H Content-Type: application/json \ -d { session_id: demo-001, message: Quero comprar dois pastéis de nata e remover uma garrafa de vinho do Porto }模型应该能识别出“两个蛋挞”和“从购物车移除一瓶波特酒”两个意图分别调用对应工具完成操作。整个过程中你不需要在代码里写任何针对这句话的规则全部由 LLM 完成语义理解。5. 常见问题与排查思路在实战中最容易踩的坑集中在配置、工具调用和状态隔离上。下面整理成一张表格方便你直接对照排查。问题现象常见原因解决思路调用 OpenAI 客户端报 401API Key 未配置、写错或已失效检查.env是否被加载打印 key 前几位确认模型不调用工具直接返回文字模型不支持 Function Calling或提示词不够明确确认模型是否支持 tool calls强化 system prompt工具参数解析失败LLM 返回的 JSON 参数与 schema 不符在工具描述中写清字段类型执行端加 try/except购物车数据串到其他用户所有请求共用了同一个 session_id按用户维度生成唯一 session_id越聊越慢或 token 超限历史消息无限累积做滑动窗口截断或历史摘要用葡萄牙语搜不到商品商品关键词未覆盖 pt-PT 常用说法在 keywords 中补充同义词或引入向量检索注入风险用户让模型直接改金额没有限制工具的调用边界金额计算只发生在后端不给 LLM 操作优惠字段的权限下面对几个高频问题展开说明。5.1 API 接入失败如果你使用的是兼容 OpenAI SDK 的第三方服务需要确认base_url是否包含/v1。常见的格式是https://api.example.com/v1有些服务厂商会省略导致请求路径错误。排查时可以打开 debug 日志直接看发出的 URL 是否完整。5.2 模型不按预期调用工具这类问题通常不是代码 bug而是提示词和工具描述不够精细。可以检查三点模型本身是否支持 Function Calling部分轻量模型的工具能力不稳定。工具名称和描述是否足够直观add_to_cart就比op1更容易被模型理解。system prompt 中是否有“你必须使用工具完成操作”的明确指令。5.3 对话上下文无限增长Agent 的while循环会把所有工具调用和结果都追加到messages里。一次复杂请求可能产生 5 到 10 条内部消息如果再叠加多轮对话历史很容易超过模型的上下文窗口。解决办法是给history做一个截断窗口只保留最近的 N 轮或者用摘要压缩早期对话。5.4 多语言搜索匹配不完整葡萄牙语存在明显的词汇变体比如pastel de nata在巴西葡语中可能写成pastel de Belém。单一关键词匹配无法覆盖全部写法。短期方案是在keywords里人工补充同义词长期方案是引入 RAG把商品描述向量化用语义相似度替代字符串包含判断。6. 最佳实践与工程建议代码能跑通只是第一步真正要上线到生产环境还需要关注下面这些工程细节。6.1 工具函数是业务安全边界LLM 永远不应该直接操作数据库或支付系统。正确的做法是LLM 只输出“意图”由工具函数校验参数、执行事务、返回结果。比如金额计算、库存扣减、优惠券核销这类敏感操作都必须放在工具函数内部并且只暴露最小可用能力给模型。不要把“修改商品单价”“修改订单金额”之类的工具开放给 LLM。6.2 金额计算不要用 float示例代码为了演示简洁使用了 float但真实电商涉及欧元金额计算强烈建议改用 Python 的Decimal类型避免浮点精度导致的金额误差。加购、删除、清空这些操作要考虑幂等性同一请求重复调用不应该产生重复扣减。6.3 会话与状态隔离购物车是强状态数据session_id必须严格按用户维度隔离。生产环境推荐把购物车放在 Redis 中并设置过期时间避免内存无限增长。同时要记录每次工具调用的日志包括 session_id、工具名、参数和结果方便事后审计和多语言问题的排查。6.4 提示词版本管理系统提示词、工具描述都属于需要迭代的配置建议像管理代码一样管理它们。每次修改 system prompt 后先在一组固定测试用例上回归确认模型行为没有发生意外变化。工具描述越稳定模型调用的准确率越高。6.5 追踪与观测Agent 的调用链比普通接口复杂得多一次请求可能涉及多次内部工具调用。生产环境应该为每次用户请求生成 trace_id并记录完整的消息流转过程。这样即使模型行为异常也能定位是意图理解问题、工具参数问题还是业务数据问题。6.6 从关键词搜索升级到 RAG当商品数量增长到几百上千时多语言搜索的痛点会非常明显。此时建议引入向量数据库把商品名称、描述、关键词统一做 embedding用户提问后先做语义检索召回 Top N 商品再把结果拼接到工具返回中。关于 embedding API 的配置要注意不同的向量服务参数差异很大接入前先确认好维度、距离算法和索引类型避免出现“向量 API 未配置”之类的初始化报错。7. 后续扩展思路本文已经完成了一个最小可用的 LLM 购物车 Agent。如果你想把项目继续往前推下面几个方向值得尝试。其一在购物车基础上增加calculate_shipping、apply_coupon、checkout等工具把“从 LLM 到购物车”延伸为“从 LLM 到完整订单”。注意结算类工具必须引入确认流程不能让模型在用户没有明确确认时直接提交订单。其二如果你的技术栈是 Java 体系可以关注 Spring AI 社区配合 MCP 协议把工具接入标准化。Spring AI MCP RAG Agent 的组合在电商场景里有不少现成实践整体思路与本文一致只是把 Python 代码换成了 Spring 的 Bean 与配置。其三可以把商品数据从内存迁到 MySQL 或 PostgreSQL购物车改为 Redis 存储并把搜索逻辑替换成向量检索。这样才更接近真实生产系统的形态。建议你先跑通本文的最小闭环用不同语言、不同意图的句子压测工具调用的稳定性再逐步替换模型服务、增加商品数据、接支付网关。先把工具调用做稳再谈体验优化。如果这篇文章对你有帮助可以收藏备用动手实现时遇到问题也欢迎在评论区一起讨论。