前言
之前去 山姆购物,每次都容易拿太多东西,最后不但超了预算而且东西也吃不完。于是和爱人探讨了一下出现问题的原因,有如下两个:
- 只有去山姆的动机,但没有先做预算。
- 一次性买了太多的食品,健康与不健康的食品比例不够合理。
如果这个问题在过去,我得和我爱人吵好多次才能得到 如何在有限的预算里买到最合适的食物配比,但现在有了 AI,有了 中间人 ,结论更科学🤣。但是如果单纯的去问 AI,它本身的知识和能力有限,因此我们需要实现一个 agent ,让 AI 能够:
- 🛒 查询商品价格
- 📊 按预算筛选商品
- 🛍️ 按分类浏览商品
- 💰 计算购物总价
同时最要保证的便是让 AI 在它不知道的时候别凭空想象,给我说不存在的商品和价格。
Agent 技术栈
| 技术 | 版本 | 角色 | 理由 |
|---|---|---|---|
| TypeScript | 5.9.3 | 开发语言 | 类型安全 |
| LangChain | 1.2.28 | Agent 框架 | AI 应用开发,除了 Python 外,它还支持 typescript |
| @modelcontextprotocol/sdk | 1.27.1 | MCP 协议实现 | 用来扩展和解耦 |
| Zod | 4.3.6 | Schema 校验 | 运行时类型检查 |
| @langchain/openai | 1.2.11 | LLM 接口 | 兼容 OpenAI API |
最后大模型提供商选择 硅基流动 的,上面的模型也很丰富。
整体架构图
AI 生图太好用了,除了文字差点🫢
时序图示例
下面展示了当我输入 “我有 30 块钱,能买什么?”时,系统各组件之间的交互时序:
时序说明:
| 阶段 | 步骤 | 说明 |
|---|---|---|
| 启动 | 1-5 | MCP 客户端连接服务端,动态发现工具 |
| 查询 | 6-7 | 用户输入,Agent 请求 LLM |
| 思考 | 8 | LLM 分析问题,决定调用工具 |
| 行动 | 9-14 | 通过 MCP 调用工具,查询数据库 |
| 观察 | 15-16 | LLM 接收工具结果,继续思考 |
| 回答 | 17-19 | 生成最终答案,展示给用户 |
关键交互点:
- 动态发现(步骤 3-5):Agent 运行时才获取工具列表
- Schema 转换(步骤 6):MCP JSON Schema → Zod Schema
- Stdio 通信(步骤 10):通过标准输入输出跨进程通信
- ReAct 循环(步骤 8-16):思考-行动-观察的循环过程
Agent 工程哲学
1. 关注点分离
服务端:只负责提供工具,不关心谁在用
客户端:负责思考和组织,不关心工具怎么实现
数据层:纯粹的数据存储,随时可以替换
2. 标准化接口
所有工具都遵循 MCP 协议。
3. 动态发现
AI 不需要预先知道有哪些工具,运行时自己探索。这就像我们去超市,不需要提前知道卖什么,到了现场自然能发现。
核心实现
1. 大模型选择
由于我们涉及到一些工具调用,不想写 prompt 进行维护,只能选择一些针对工具调用的微调模型
这里我选了 Qwen/Qwen3-Omni-30B-A3B-Thinking (主要是价格便宜🤣)
2. 具体实现
MCP 服务端:工具提供者
src/mcp.ts 是整个项目的"工具库",提供了 7 个实用工具。
1. 数据模型
interface Product {
name: string; // 商品名称
price: number; // 价格(单位:元)
category: string; // 分类:水果、饮品、食品、肉类、粮食、电子产品
description: string; // 描述
}
这里我们 mock 一些商品
const productDatabase: Record<string, Product> = {
"苹果": { name: "苹果", price: 5.5, category: "水果", description: "新鲜红富士苹果,500g" },
"香蕉": { name: "香蕉", price: 3.0, category: "水果", description: "进口香蕉,500g" },
"橙子": { name: "橙子", price: 4.5, category: "水果", description: "赣南脐橙,500g" },
"牛奶": { name: "牛奶", price: 8.0, category: "饮品", description: "纯牛奶,1L装" },
"面包": { name: "面包", price: 6.0, category: "食品", description: "全麦面包,切片装" },
"鸡蛋": { name: "鸡蛋", price: 12.0, category: "食品", description: "土鸡蛋,10枚装" },
"猪肉": { name: "猪肉", price: 25.0, category: "肉类", description: "新鲜五花肉,500g" },
"牛肉": { name: "牛肉", price: 45.0, category: "肉类", description: "澳洲牛腩,500g" },
"鸡肉": { name: "鸡肉", price: 18.0, category: "肉类", description: "农家土鸡,500g" },
"大米": { name: "大米", price: 3.5, category: "粮食", description: "东北大米,500g" },
"笔记本电脑": { name: "笔记本电脑", price: 5999, category: "电子产品", description: "轻薄本,16GB内存" },
"手机": { name: "手机", price: 3999, category: "电子产品", description: "智能手机,256GB存储" },
"耳机": { name: "耳机", price: 299, category: "电子产品", description: "无线蓝牙耳机" },
"键盘": { name: "键盘", price: 199, category: "电子产品", description: "机械键盘,RGB背光" },
"鼠标": { name: "鼠标", price: 99, category: "电子产品", description: "无线鼠标,静音设计" },
};
2. 工具列表
| 工具名 | 功能 | 输入参数 | 返回值 |
|---|---|---|---|
list_products | 列出所有商品 | 无 | 商品名称列表 |
get_price | 查询价格 | name: string | 商品价格 |
get_product_info | 获取详情 | name: string | 完整商品信息 |
list_by_category | 分类查询 | category: string | 分类下商品列表 |
list_categories | 获取所有分类 | 无 | 分类列表 |
find_within_budget | 预算筛选 | budget: number | 预算内商品列表 |
calculate_total | 计算总价 | products: string[] | 总价和明细 |
3. 典型工具实现
server.tool(
"find_within_budget",
"查找预算范围内的商品,返回价格低于或等于预算的所有商品",
{ budget: z.number().describe("预算金额(元)") },
async ({ budget }) => {
const affordableProducts = Object.values(productDatabase)
.filter((p) => p.price <= budget)
.sort((a, b) => a.price - b.price) // 按价格升序
.map((p) => `${p.name}(${p.price}元)`);
if (affordableProducts.length === 0) {
return {
content: [{
type: "text" as const,
text: `没有找到价格在 ${budget} 元以内的商品`
}],
};
}
return {
content: [{
type: "text" as const,
text: `价格在 ${budget} 元以内的商品有:${affordableProducts.join("、")}`
}],
};
}
);
4. 传输方式
const transport = new StdioServerTransport();
await server.connect(transport);
使用 Stdio (标准输入输出) 进行进程间通信,简单、可靠、跨平台。
Agent 客户端:思考者
src/index.ts 是整个项目的"大脑",负责连接服务、思考问题、组织答案。
1. MCP 客户端连接
async function connectMcpServer(): Promise<Client> {
const client = new Client(
{ name: "shopping-assistant-client", version: "1.0.0" },
{ capabilities: {} }
);
const transport = new StdioClientTransport({
command: "npx",
args: ["ts-node", "src/mcp.ts"], // 启动 MCP 服务端,动态链接
});
await client.connect(transport);
return client;
}
2. 动态工具发现
AI 在运行时动态发现可用工具,而不是硬编码。
//* 获取服务端提供的所有工具
const toolsList = await mcpClient.listTools();
//* 将 MCP 工具转换为 LangChain 工具
const tools = toolsList.tools.map((tool) => {
const inputSchema = convertMcpSchemaToZod(tool.inputSchema);
// 创建工具实例
return createLangChainToolFromMcp(
mcpClient,
tool.name,
tool.description!,
inputSchema
);
});
3. ReAct Agent 创建
使用 LangChain 的预构建 ReAct Agent:
const agent = createReactAgent({
llm,
tools,
messageModifier: systemPrompt,
});
ReAct (Reasoning + Acting) 模式:
- Thought (思考):分析问题,决定下一步
- Action (行动):选择并调用工具
- Observation (观察):获取工具返回结果
- 重复:直到得出最终答案
动态提示词生成:
function generateSystemPrompt(tools: { name: string; description: string }[]): string {
const toolsDescription = tools
.map((t) => `- ${t.name}: ${t.description}`)
.join("\n");
return `你是一个智能购物助手。你会使用 ReAct (Reasoning and Acting) 模式来处理用户的请求。
你的工作流程:
1. Thought: 思考用户的问题,决定下一步行动
2. Action: 选择合适的工具并执行
3. Observation: 观察工具返回的结果
4. 重复以上步骤直到得出最终答案
可用工具:
${toolsDescription}
重要提示:
- 当用户询问复杂问题时,需要多次调用工具收集信息
- 例如"我有50元能买什么",先调用 find_within_budget,再详细查看商品信息
- 例如"推荐一些水果",先调用 list_by_category,再查询具体价格
- 例如"买个早餐组合",需要选择多个商品并计算总价
请始终用中文回答用户问题。`;
}
一些效果
-
pnpm start启动 agent -
查询 我有 100 块用来买早餐,我想吃 5 天,怎么买合适
但在第一次回答中,AI 认为我一天可以吃 10 个鸡蛋,哈哈哈😄。
于是接着问它:你看看鸡蛋是几枚装的,我一天最多吃两个鸡蛋。 但由于没加 记忆,所以它的回答也是不相关的了,但还是走的工具,比较科学。
最后的话
目前这个状态算是只有我本地跑着玩,距离上线让我爱人使用还早,山姆的商品那么多,还得收集一段时间。后续有如下计划:
- 加入记忆,应对多轮对话。
- 加入 Rag,有好多信息,比如某个食品是否达到健康标准,是否好吃 这些还得从 小红书 上爬一爬。
- 服务上线,东西还很多,任重而道远呀。