ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

LLM Agent实战:从自然语言到购物车的Function Calling与MCP应用

2026/8/29 8:38:40 拓冰建站 浏览量
LLM Agent实战:从自然语言到购物车的Function Calling与MCP应用 先问大家一个问题你见过能“聊天”的大模型但见过能自己把商品“放进购物车”的大模型吗如果你正在做 LLM 应用开发或者刚接触 Agent、Function Calling、MCP 这类概念那么这篇实战笔记值得看完。在不少电商和零售场景里我们都希望用户用一句自然语言就能完成下单动作例如“帮我把那台意式咖啡机加入购物车”。这句话里既有商品识别也有加入购物车的操作。把“理解意图”交给 LLM把“执行动作”交给工具函数二者通过一套标准协议衔接起来就能形成一条从 LLM 到购物车的完整链路。这篇文章来自于一个真实落地过的演示项目From LLM to shopping cart (Portugal)。项目名称里的 Portugal 并不是指要在葡萄牙部署服务器而是指整个交互语言采用葡萄牙语商品目录、用户提问、系统回复都围绕葡语电商场景展开这样更容易暴露多语言意图识别和商品匹配中的真实问题。本文会从核心概念讲起逐步拆解 Agent 工具调用、MCP 思想、RAG 搜索、购物车服务对接并给出完整可运行的代码示例。哪怕你之前只写过简单的模型 API 调用也能跟着本文从零跑通一个“自然语言 - 商品搜索 - 加购成功”的 LLM 应用。1. 项目背景从“会聊天”到“会下单”的跨越1.1 一句话理解From LLM to shopping cart传统 LLM 应用最常见的形态是“问答机器人”用户提问模型回答。这种形态能解决“知识获取”问题但无法解决“任务执行”问题。From LLM to shopping cart 这类项目的核心思路是把 LLM 当作大脑把购物车服务当作手和脚。用户用自然语言表达购物诉求LLM 负责理解、拆解、决策然后调用预设的搜索工具和购物车接口最终完成“把商品加入购物车”这个业务动作。换句话说这个项目不是让 LLM 用文字描述“应该购买什么”而是让 LLM 直接驱动系统去执行“加入购物车”这个操作。1.2 为什么选择葡萄牙语购物场景在演示项目中加入 Portugal 这个限定词是为了验证两个重要问题多语言意图识别的稳定性葡萄牙语虽然不是小语种但在中文技术社区里很多开发者默认只做中英文测试。换成葡语之后商品名词、动词搭配、单复数变化都会影响意图识别和实体抽取。商品匹配的模糊性用户说“uma máquina de café”一台咖啡机系统需要把它映射到商品库里的具体 SKU。这种场景非常适合用 RAG 或向量检索来补充商品搜索的召回能力。如果直接用中文“把咖啡机加入购物车”很多模型都能轻松理解。但换成“Quero adicionar uma máquina de café ao carrinho”就需要系统具备更好的实体识别和意图理解能力这也是这个项目的技术价值所在。1.3 从 Prompt 到 Agent 的演进早期 LLM 应用只靠 Prompt 就能完成一些简单分类或抽取任务。但一旦涉及“调用外部系统”靠 Prompt 是做不到的。于是出现了 Function Calling函数调用机制模型根据用户输入判断需要调用哪个函数并输出结构化的调用参数程序再去执行这个函数然后把结果交回给模型继续生成回复。Function Calling 是 Agent 的基础能力。Agent 在 Function Calling 之上增加“多轮决策”能力模型可以连续调用多个工具每次调用之间根据工具返回结果不断调整下一步计划。再往后MCPModel Context Protocol模型上下文协议出现它把“工具”标准化成一种协议让模型可以动态发现和调用远程工具。简单理解Function Calling 是模型能力MCP 是工具接入标准。两者并不冲突反而可以结合使用。2. 核心概念拆解LLM Agent 工具调用的关键知识点在进入代码之前先把几个关键概念讲清楚。如果这些概念模糊后面调试工具调用时很容易被“模型为什么不按格式输出”“工具返回了但模型不认”这类问题卡住。2.1 LLM 生成与 LLM 动作的区别很多初学者会把“生成回复”和“执行动作”混为一谈。生成回复模型只输出文本不产生任何副作用例如“我可以帮您搜索咖啡机”。执行动作系统调用真实接口产生副作用例如“商品已加入购物车购物车数量变为 1”。From LLM to shopping cart 项目里加购动作就是典型的副作用操作。这类操作必须由代码执行不能由模型直接“假装执行”。模型只负责决定“是否调、调哪个、传什么参数”真正执行权在程序手里。这一点非常重要。在生产环境中任何涉及订单、支付、库存的操作都要把执行权收回到受控代码中并且做权限校验、参数校验、重复提交校验。2.2 Function Calling / Tool CallingOpenAI 在 2023 年引入了 Function Calling 能力后来 Anthropic、Google Gemini、开源 Qwen 等模型也陆续支持类似能力。不同厂商命名不同有的叫 Function Calling有的叫 Tool Calling但底层思路一致开发者提前声明一组工具用 JSON Schema 描述工具名称、功能描述、参数结构。用户输入问题后模型根据上下文决定是否需要调用工具。如果需要调用模型输出包含工具名称和参数的结构化结果而不是直接执行。开发者拿到结构化结果后在自己的程序里执行工具函数。执行结果以消息形式返回给模型模型基于结果继续生成最终回复。示例工具声明如下{ 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, quantity] } } }这里需要注意的是模型输出的是“建议”不是“命令”。如果参数校验不通过程序应该拒绝执行并返回错误信息而不是盲目信任模型。2.3 Agent多轮工具调用循环单次 Function Calling 只能解决“一步到位”的简单需求。实际购物场景中用户可能先问“你们有哪些咖啡机”然后说“第二款帮我加两件”最后说“顺便看一下购物车”。这个过程中模型需要多次调用搜索、详情、加购、查看购物车等多个工具。Agent 的核心就是维护一个多轮循环接收用户输入。把历史消息和工具列表一起发送给模型。模型返回文本或工具调用请求。如果是工具调用请求执行工具把结果作为消息追加到上下文。回到第 2 步直到模型不再调用工具输出最终答案。这个循环也被称为 ReAct 模式的简化版模型在“思考 - 行动 - 观察”之间交替前进。商品搜索、二次确认、加购、返回结果每一步都是一次工具调用。2.4 MCP工具调用标准化如果你只做单机演示自己定义工具列表就够了。但当工具越来越多比如商品搜索、用户画像、库存查询、优惠券计算、购物车、订单系统它们分布在不同的微服务里手动维护工具列表就会变得很痛苦。MCPModel Context Protocol解决的就是“工具发现与调用协议”标准化问题。它把工具封装成服务端资源模型客户端通过协议去发现工具、获取工具 Schema、发送调用请求。在 From LLM to shopping cart 项目里我们暂时不需要引入完整 MCP Server但可以借鉴 MCP 的设计思想工具描述独立维护、参数 Schema 统一声明、调用结果统一封装。这样后续如果要迁移到 MCP 协议代码改动会非常小。2.5 RAG 在选品搜索中的作用购物场景天然适合 RAGRetrieval-Augmented Generation检索增强生成。用户说“我想要一台银色、1.2 升、水箱可拆卸的咖啡机”这时如果直接在 SQLite 里做模糊匹配很难处理“银色”“水箱可拆卸”这类属性描述。更合理的做法是提前把商品名称、属性、描述、标签转换成向量存入向量数据库。用户提问后系统先从商品库召回候选商品。把候选商品信息拼进 Prompt让模型基于真实商品数据做判断。RAG 的好处是模型不需要“背下”所有商品信息只需要基于实时检索结果做决策商品上下架也不需要重新训练模型。2.6 LLM 应用为什么需要编排框架如果你只写一个几十行的测试脚本确实不需要框架。但真实项目里存在这些需求需要维护多轮对话状态。需要管理多个工具的生命周期。需要处理模型返回格式错误、超时、限流。需要记录完整的调用链路日志。需要做用户权限隔离。这些需求驱动了 LangChain、LlamaIndex、Spring AI 等编排框架的出现。框架提供一套标准组件模型封装、Prompt 模板、工具注册、内存管理、回调日志。使用框架不等于放弃控制权。相反理解底层原理后再用框架你会知道框架帮你封装了什么也能在出问题时快速定位。3. 环境准备与版本说明3.1 运行环境这个 demo 在 macOS 和 Linux 上测试通过Windows 建议使用 WSL2 或 Git Bash。代码主要依赖 Python 3.9没有用到特别复杂的系统特性。项目结构规划如下llm-to-cart/ ├── main.py ├── agent.py ├── tools.py ├── cart_service.py ├── catalog.py ├── data/ │ └── products.csv ├── static/ │ └── index.html └── requirements.txt3.2 依赖安装这里是以 OpenAI 兼容接口为例如果你使用的是其他模型服务只要它支持 Function Calling / Tool Calling代码思路基本一致具体 SDK 换成对应厂商即可。pip install openai fastapi uvicorn python-dotenv需要说明的是版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。不同 SDK 的版本之间差异很大尤其是 OpenAI Python SDK 1.x 版本和 0.x 版本的调用方式完全不同。3.3 模型服务选择演示项目默认使用 OpenAI 兼容 API你可以在.env中配置OPENAI_API_KEYyour-api-key OPENAI_BASE_URLhttps://api.openai.com OPENAI_MODELgpt-4o-mini如果你使用的是国内模型服务商或本地部署模型把OPENAI_BASE_URL换成对应地址即可。本地模型建议选择支持 Function Calling 的中大杯模型因为工具调用对模型指令遵循能力要求较高太小的模型经常出现参数格式错误。3.4 使用 docker 快速启动 FastAPI 服务虽然不是必须但为了演示完整“从 LLM 到购物车”的调用链路下面给出一份示例DockerfileFROM python:3.10-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD [uvicorn, main:app, --host, 0.0.0.0, --port, 8000]启动命令docker build -t llm-to-cart . docker run -p 8000:8000 --env-file .env llm-to-cart4. 系统设计与核心流程4.1 整体架构我们把这个系统拆成四个部分前端页面用户输入葡萄牙语文本展示对话结果和购物车状态。FastAPI 服务接收前端请求管理会话调用 Agent。Agent 核心循环与 LLM 交互决定调用哪个工具。工具层商品搜索、商品详情、购物车服务。用户输入 │ ▼ FastAPI 接口 │ ▼ Agent 循环 ────► LLM │ │ │ ▼ │ Function Calling 请求 │ │ ▼ ▼ 工具执行层搜索 / 商品详情 / 购物车 │ ▼ 返回结构化执行结果 │ ▼ 模型生成最终回复 │ ▼ 前端页面展示可以看到LLM 是整个链路的决策中心但所有实际动作都由工具执行层完成。这保证了系统在模型误判时仍可被代码兜底。4.2 核心流程拆解以“Quero adicionar uma máquina de café ao carrinho”我想把一台咖啡机加入购物车为例完整流程如下用户发送文本到后端接口。后端把消息追加到会话历史调用 Agent。Agent 判断这属于商品购买类请求决定先调用search_products。search_products在商品库中检索“máquina de café”返回候选商品列表。Agent 看到候选商品后如果商品唯一则直接调用add_to_cart如果商品不唯一可以询问用户要哪一款。add_to_cart执行成功后返回购物车状态。Agent 基于工具结果生成最终回复“A máquina de café foi adicionada ao carrinho.”整个流程的关键在于第 5 步。一个成熟的 Agent 不能“猜”用户要什么而是要在信息不足时主动澄清。这也是判断 Agent 可用性的重要标准。4.3 会话状态与幂等真实购物场景中用户可能会说“再看一下购物车”或者“把我刚才看的那款删掉”。“刚才那款”这种表达依赖上下文状态所以后端必须维护会话历史。另外一个重要设计是幂等。每次add_to_cart都应该带一个幂等键防止用户连续点击“加入购物车”导致重复加购。在 demo 中可以简单用request_id实现。5. 完整实战从 LLM 到葡萄牙购物车下面进入代码实现环节。我会按照项目结构逐个文件给出内容并且保证每个文件都可以独立运行。这里的代码是“直接可复制”的但模型服务商、商品数据、API Key 需要替换成你自己的。5.1 商品目录先准备一份葡语商品目录放在data/products.csv。为了演示效果我们准备五款常见家用商品id,nome,categoria,preco_eur,descricao 1001,Cafeteira Italiana 3 xícaras,Café,19.90,Cafeteira clássica para preparo de café tradicional. 1002,Máquina de Café Expresso 15 bar,Café,299.00,Máquina de café expresso com vaporizador de leite. 1003,Cafeteira Elétrica 1.2L,Café,89.90,Cafeteira elétrica com jarra de vidro e placa quente. 1004,Filtro de Papel para Café, 40 unidades,Café,3.50,Filtros de papel compatíveis com cafeteiras elétricas. 1005,Chaleira Elétrica 1.7L,Cozinha,39.90,Chaleira elétrica com desligamento automático. 1006,Aspirador de Pó Vertical,Limpeza,149.90,Aspirador de pó sem fio com bateria de longa duração.字段说明id商品唯一 ID。nome商品名称葡萄牙语。categoria商品分类。preco_eur价格单位欧元。descricao商品描述。为了简单我们直接使用 CSV 文件模拟商品库。真实项目中这里一般会连接数据库或商品中心 API。5.2 商品搜索工具创建catalog.py负责加载商品数据并实现关键词匹配。import csv from pathlib import Path from typing import List, Dict, Optional DATA_FILE Path(__file__).parent / data / products.csv def load_products() - List[Dict]: products [] with open(DATA_FILE, r, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: row[preco_eur] float(row[preco_eur]) products.append(row) return products def search_products(keyword: str, limit: int 5) - List[Dict]: 根据关键词在商品名称、描述、分类中匹配。 keyword_lower keyword.lower().strip() products load_products() results [] for p in products: searchable_text f{p[nome]} {p[categoria]} {p[descricao]} {p[id]} if keyword_lower in searchable_text.lower(): results.append(p) return results[:limit] def get_product_by_id(product_id: str) - Optional[Dict]: 根据商品ID查询商品。 products load_products() for p in products: if p[id] str(product_id): return p return Nonesearch_products就是一个最简单的 RAG 思路实现先召回再筛选。如果你后续接向量数据库只需要把search_products的实现替换成向量检索Agent 侧逻辑不用改。当前搜索逻辑是子串匹配所以用户说“café”能匹配所有咖啡相关商品说“máquina de café”也能匹配多款商品。信息不唯一时Agent 需要主动询问。5.3 购物车服务创建cart_service.py模拟购物车服务。这里没有真实数据库使用字典存储购物车数据。from typing import Dict, List from datetime import datetime import uuid class CartService: def __init__(self): # key: cart_id, value: list of items self._carts: Dict[str, List[Dict]] {} def create_cart(self) - str: cart_id str(uuid.uuid4()) self._carts[cart_id] [] return cart_id def add_item(self, cart_id: str, product_id: str, quantity: int, product_name: str, price: float) - Dict: if cart_id not in self._carts: raise ValueError(购物车不存在) if quantity 0: raise ValueError(商品数量必须大于0) # 如果商品已在购物车中数量叠加 for item in self._carts[cart_id]: if item[product_id] product_id: item[quantity] quantity item[subtotal] item[quantity] * item[price] return {status: updated, cart_id: cart_id, items: self._carts[cart_id]} item { product_id: product_id, product_name: product_name, quantity: quantity, price: price, subtotal: price * quantity, added_at: datetime.now().isoformat(), } self._carts[cart_id].append(item) return {status: added, cart_id: cart_id, items: self._carts[cart_id]} def list_items(self, cart_id: str) - List[Dict]: return self._carts.get(cart_id, []) def total(self, cart_id: str) - float: return sum(item[subtotal] for item in self.list_items(cart_id)) # 全局单例 cart_service CartService()在实际电商系统中购物车属于核心交易链路需要接入真实商品中心、价格服务、库存服务并且要记录操作日志。这里为了演示直接把部分商品信息冗余到购物车条目中。5.4 工具注册与定义创建tools.py把工具定义和工具执行逻辑放在一起。这里用字典的方式注册工具便于后续扩展。from catalog import search_products, get_product_by_id from cart_service import cart_service TOOL_SCHEMAS [ { type: function, function: { name: search_products, description: 根据用户输入的关键词搜索商品返回匹配的商品列表。, parameters: { type: object, properties: { keyword: { type: string, description: 商品搜索关键词。 } }, required: [keyword] } } }, { type: function, function: { name: get_product_detail, description: 根据商品ID查询商品详情。, parameters: { type: object, properties: { product_id: { type: string, description: 商品ID例如 1002 } }, required: [product_id] } } }, { type: function, function: { name: add_to_cart, description: 将指定商品加入购物车需要提供商品ID和数量。, parameters: { type: object, properties: { cart_id: { type: string, description: 购物车ID }, product_id: { type: string, description: 商品ID }, quantity: { type: integer, description: 商品数量默认1 } }, required: [cart_id, product_id, quantity] } } }, { type: function, function: { name: list_cart, description: 查看当前购物车中的商品列表。, parameters: { type: object, properties: { cart_id: { type: string, description: 购物车ID } }, required: [cart_id] } } } ] def call_tool(tool_name: str, arguments: dict): 执行工具并返回标准格式的结果。 if tool_name search_products: keyword arguments.get(keyword, ) products search_products(keyword) return {products: products} if tool_name get_product_detail: product_id arguments.get(product_id) product get_product_by_id(product_id) if not product: return {error: f商品 {product_id} 不存在} return {product: product} if tool_name add_to_cart: cart_id arguments.get(cart_id) product_id arguments.get(product_id) quantity arguments.get(quantity, 1) product get_product_by_id(product_id) if not product: return {error: f商品 {product_id} 不存在} result cart_service.add_item( cart_idcart_id, product_idproduct_id, quantityquantity, product_nameproduct[nome], priceproduct[preco_eur], ) return { result: result, cart_total: cart_service.total(cart_id) } if tool_name list_cart: cart_id arguments.get(cart_id) items cart_service.list_items(cart_id) return {items: items, total: cart_service.total(cart_id)} raise ValueError(f未知工具: {tool_name})这里一个关键点是工具返回的结构。我统一封装成 dict并用{products: ...}、{error: ...}这样的结构返回。模型会把这些结构化结果当作上下文继续推理。如果你返回的格式不统一模型很容易产生幻觉或格式混乱。5.5 Agent 核心循环创建agent.py这是整个项目的核心。它负责与模型交互、解析工具调用、调用工具、维护多轮循环。import json import os from openai import OpenAI from dotenv import load_dotenv from tools import TOOL_SCHEMAS, call_tool load_dotenv() client OpenAI( api_keyos.getenv(OPENAI_API_KEY), base_urlos.getenv(OPENAI_BASE_URL), ) MODEL os.getenv(OPENAI_MODEL, gpt-4o-mini) SYSTEM_PROMPT 你是一个葡萄牙电商购物助手使用葡萄牙语与用户对话。 你可以搜索商品、查看商品详情、将商品加入购物车、查看购物车。 在执行加入购物车操作前如果商品信息不唯一必须先询问用户具体选择哪一款。 所有操作结果以简洁的葡萄牙语回复用户并保留关键信息商品名称、价格、数量。 def run_agent(user_message: str, cart_id: str, history: list): # 初始化消息列表系统提示 历史 最新用户输入 messages [{role: system, content: SYSTEM_PROMPT}] messages.extend(history) messages.append({role: user, content: user_message}) for step in range(6): # 最多循环6次防止死循环 response client.chat.completions.create( modelMODEL, messagesmessages, toolsTOOL_SCHEMAS, tool_choiceauto, ) message response.choices[0].message messages.append(message.model_dump()) if not message.tool_calls: return message.content, messages for tool_call in message.tool_calls: function_name tool_call.function.name function_arguments json.loads(tool_call.function.arguments) result call_tool(function_name, function_arguments) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse), }) return Desculpe, não consegui concluir a operação. Tente novamente., messages代码要点说明tools参数传入工具 Schematool_choiceauto表示让模型自己决定是否调用工具。每次循环都要把消息列表完整发送给模型保证模型能“记住”之前的工具调用结果。message.model_dump()是把消息对象转成字典这样后续追加工具结果时格式不会乱。设置最大循环次数为 6避免模型陷入无限调用工具的循环。工具执行结果使用json.dumps序列化成字符串作为消息内容。这个循环本质上是“Agent 的一个极简实现”。如果你想接触更复杂的 Agent 框架理解了这段代码之后再看 LangGraph、Spring AI 等框架会轻松很多。5.6 FastAPI 接口创建main.py提供 HTTP 接口并管理每个会话的cart_id。from fastapi import FastAPI, HTTPException from pydantic import BaseModel from cart_service import cart_service from agent import run_agent import uuid app FastAPI(titleLLM to Shopping Cart (Portugal)) class ChatRequest(BaseModel): session_id: str message: str class ChatResponse(BaseModel): session_id: str cart_id: str reply: str cart_items: list [] # 会话存储 sessions {} app.post(/chat) def chat(req: ChatRequest): if not req.message.strip(): raise HTTPException(status_code400, detail消息不能为空) session_id req.session_id or str(uuid.uuid4()) if session_id not in sessions: sessions[session_id] { cart_id: cart_service.create_cart(), history: [], } session sessions[session_id] reply, history run_agent(req.message, session[cart_id], session[history]) # 截断历史避免上下文过长 session[history] history[-8:] cart_items cart_service.list_items(session[cart_id]) return ChatResponse( session_idsession_id, cart_idsession[cart_id], replyreply, cart_itemscart_items, ) app.get(/cart/{cart_id}) def get_cart(cart_id: str): items cart_service.list_items(cart_id) return {cart_id: cart_id, items: items, total: cart_service.total(cart_id)} app.get(/) def index(): return {message: LLM to Shopping Cart (Portugal) API}这里采用简单的内存字典保存会话服务重启后数据会丢失。生产环境建议使用 Redis 等外部存储保存会话状态和购物车数据。同时要注意接口暴露了cart_id在真实场景中必须做用户鉴权保证用户只能操作自己的购物车。5.7 前端页面为了直观演示创建一个简单的 HTML 页面static/index.html通过浏览器与后端交互。!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleLLM to Shopping Cart (Portugal)/title style body { font-family: Arial, sans-serif; max-width: 800px; margin: 40px auto; padding: 0 16px; } .message { padding: 10px; border-radius: 8px; margin-bottom: 8px; } .user { background: #e3f2fd; } .assistant { background: #e8f5e9; } #cart { border: 1px solid #ccc; padding: 12px; margin-top: 16px; } /style /head body h2 LLM 葡萄牙购物车助手/h2 div idchat/div input idinput stylewidth: 70%; placeholder例如Quero adicionar uma máquina de café ao carrinho / button idsendEnviar/button div idcart strongCarrinho:/strong vazio /div script const sessionId crypto.randomUUID(); const chatEl document.getElementById(chat); const inputEl document.getElementById(input); const cartEl document.getElementById(cart); function addMessage(role, content) { const div document.createElement(div); div.className message role; div.textContent content; chatEl.appendChild(div); } async function send() { const message inputEl.value.trim(); if (!message) return; addMessage(user, message); inputEl.value ; const res await fetch(/chat, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ session_id: sessionId, message }) }); const data await res.json(); addMessage(assistant, data.reply); if (data.cart_items data.cart_items.length 0) { cartEl.innerHTML strongCarrinho:/strongbr data.cart_items.map(i ${i.product_name} x ${i.quantity} €${i.subtotal.toFixed(2)} ).join(br) brstrongTotal:/strong €${data.cart_items.reduce((s, i) s i.subtotal, 0).toFixed(2)}; } else { cartEl.innerHTML strongCarrinho:/strong vazio; } } document.getElementById(send).onclick send; inputEl.addEventListener(keydown, e { if (e.key Enter) send(); }); /script /body /html前端代码用crypto.randomUUID()生成本地会话 ID在浏览器端保存。实际项目中session 应该由后端统一管理。5.8 启动服务并验证后端启动uvicorn main:app --host 0.0.0.0 --port 8000启动成功后访问http://localhost:8000在输入框输入葡萄牙语例如“Quero adicionar uma máquina de café ao carrinho”我想把一台咖啡机加入购物车“Adicione 2 chaleiras elétricas”加两个电水壶“O que tem no carrinho?”购物车里有什么预期交互流程如下输入“Quero adicionar uma máquina de café ao carrinho”模型调用search_products(máquina de café)工具返回多款咖啡机模型回复“Claro! Encontrei as seguintes máquinas de café: ... Qual você gostaria de adicionar?”用户回复“A segunda”第二款模型调用add_to_cart参数为商品 ID1002数量1模型回复“A Máquina de Café Expresso 15 bar foi adicionada ao carrinho.”页面的购物车区域显示商品名称、数量、小计和总价。注意由于模型供应商的返回可能存在差异对话措辞不一定要和上面完全一致但关键步骤应该相同。6. 常见问题与排查思路在开发这个项目的过程中最容易遇到的问题集中在工具格式、模型返回、会话状态三个层面。下面整理一份排查清单可以直接对照检查。问题现象常见原因解决思路模型完全不调用工具模型不支持 Function Calling或工具 Schema 格式不对确认模型支持 Tool Calling检查 tools 参数是否传对换用 gpt-4o-mini 等支持工具调用的模型工具调用参数缺失工具参数描述不清晰在 Schema 中写清每个参数的用途和示例值例如description: 商品ID例如 1002模型生成了 JSON 但无法解析模型输出不稳定或参数类型和 Schema 不一致捕获json.JSONDecodeError把错误信息返回给模型让它重新生成限制最大重试次数加购后商品重复用户重复点击或 Agent 被要求重复执行在购物车逻辑中做幂等处理同一商品 ID 数量叠加接口层加 request_id上下文越来越长每轮对话都把 history 全部塞给模型只保留最近 4~8 条消息必要时对历史消息做摘要模型把工具结果写进最终回复但数据不对工具返回结构不清晰模型理解错误工具返回时补充提示语例如products: [...]改为结构化列表在 Prompt 中强调必须基于工具返回值回答葡萄牙语实体识别不准未加语言提示商品库分词匹配太弱系统 Prompt 明确使用葡萄牙语商品搜索支持同义词表考虑接入向量检索接口 500 错误代码 bug 或模型响应异常查看 uvicorn 日志定位是工具执行错误还是模型调用错误给 FastAPI 添加全局异常处理器购物车数据重启丢失使用了内存存储生产环境替换为 Redis、PostgreSQL 等持久化存储授权拦截用户直接调用 API 修改购物车FastAPI 接口增加身份认证中间件校验请求头 token每个 cart_id 绑定用户 ID排查工具调用问题时建议先做一个最小实验固定用户消息打印模型返回的message.tool_calls完整结构确认模型是否按预期输出工具调用。也可以通过日志记录每次工具调用的入参和出参方便溯源。7. 最佳实践与工程建议从“能跑 demo”到“能上生产”还需要补齐很多细节。下面总结几个关键方向。7.1 工具参数安全校验模型生成的参数本质上是“不可信输入”。call_tool函数里必须对参数做严格校验尤其是quantity这类数字参数必须验证类型和取值范围。如果传入负数或超大数要直接返回错误不能透传到下游服务。def safe_quantity(value): if not isinstance(value, int) or value 0 or value 99: raise ValueError(quantity 必须是 1~99 之间的整数) return value7.2 日志与链路追踪每次工具调用都要记录审计日志至少包含用户请求 ID模型返回的工具调用内容工具入参和出参耗时是否成功这样当用户投诉“为什么我的购物车多了两个商品”时你能快速追踪到是哪一次工具调用导致的。7.3 模型幻觉的兜底LLM 最大的风险是幻觉。它可能“编造”一个不存在的商品 ID或者“认为”商品已加入购物车。我们的应对策略是所有商品数据必须以工具返回值为准。所有业务动作必须以程序执行结果为准。在系统 Prompt 中明确约定如果商品不在列表中必须说明未找到不能猜测。7.4 会话隔离与权限控制购物车属于用户私有数据。生产环境中cart_id不应该由前端随意生成而应该在后端根据登录用户生成并做权限校验。FastAPI 可以使用 OAuth2、JWT 等方式做身份认证。app.post(/chat) def chat(req: ChatRequest, current_user: User Depends(get_current_user)): cart_id get_or_create_cart(current_user.id) ...7.5 测试策略Agent 类应用测试不能只测代码还要测“模型行为”。建议为每个工具写单元测试验证入参出参。用固定的 mock 模型响应测试 Agent 循环验证循环终止条件。做回归测试时记录一组标准用户问题和预期工具调用序列。例如下面这个用例def test_add_to_cart_success(): result call_tool(add_to_cart, { cart_id: test-cart, product_id: 1002, quantity: 1, }) assert result[result][status] added7.6 性能优化Agent 多轮循环会多次调用 LLM每一次都是延迟和成本。优化方向工具返回内容精简不要返回无关字段。历史消息截断或摘要。优先让一次工具调用返回足够的信息减少循环轮数。对高频搜索接口加缓存。8. 总结与扩展方向从 LLM 到购物车Portugal这个项目虽然只是一个演示规模但它完整覆盖了 LLM 应用开发的核心链路意图理解、工具注册、Function Calling、Agent 循环、RAG 召回、外部系统对接。你现在已经理解了“模型只做决策、代码执行动作”的核心原则也知道如何在多轮对话中维护工具调用结果。下一步可以根据你的实际场景继续扩展研究 LangChain、LangGraph 或 Spring AI学会用框架管理 Agent 状态和工具生命周期。深入研究 MCP把工具调用从本地函数升级为标准远程协议。把商品搜索从关键词匹配升级为向量检索建立葡萄牙语商品词向量库。在项目中增加订单确认、支付模拟、库存扣减让链路更完整。如果对硬件感兴趣可以尝试把“加入购物车”的触发条件改成实体设备按钮或语音指令这也是电子 DIY 与 LLM 结合的常见玩法。这条路线的关键词就是LLM Agent、Function Calling、MCP、RAG、编排框架。它们会反复出现在后续的学习资料里。建议你先按本文的最小闭环跑通一次把日志打开亲眼看一看模型返回的 tool_calls 和工具执行结果然后带着真实体感去做扩展。如果本文对你有帮助可以收藏备用。也欢迎在评论区分享你的葡萄牙语购物车运行效果或者你在工具调用环节遇到的其他坑。