想法到实现的过程
看到如今ai发展如此迅速,我想如果我有一点ai技能,估计面试也能加点分。于是我就想着在参与的小程序中利用ai做一个功能,就萌生了这个想法。
一开始我想着可不可以通知识库的形式,将店铺信息交给ai,然后让ai通过于用户对话,再去知识库搜索对应的店铺信息。但是我发现不管用什么模型(这里指能够支持知识库的模型),都会有一个通病-伪造数据。尽管我不断优化提示词,仍然无法解决这个问题。👎
接着我没有放弃利用智能体这个方向,既然单个智能体不行,我就做一个工作流。先让一个智能体A跟据用户的对话去知识库找店铺信息,让智能体A将数据交给智能体B,智能体B在去查询知识库过滤掉不存在的数据,将最终结果返回给用户。但是我没想到,即使这样也没办法解决伪造数据的问题。😡
我自己没有机器学习以及强化学习的知识,所有没有训练ai的技能。然后本地部署模型,然后让模型读取本地数据库这个想法又没办法实现,因为我的电脑和服务器性能不够格部署😭
最后我还是想到了两种解决方案。既然不能够通过知识库的方式让ai检索。那我就利用ai提取关键词以及生成sql方向入手。👇下面就是我的想法。想法肯定是不成熟的,各位觉得可以改进的地方欢迎在评论区和我一起讨论。🌸
利用ai实现店铺推荐原理
以下两种方法都需要我给店铺表添加一个tag字段。我们需要利用这个tag字段给所有店铺添加标签。比如蜜雪冰城的tag就是蜜雪冰城、饮品、便宜、实惠。两个方法的核心都是用ai通过tag字段进行模糊匹配。
- 方法一:提取关键词
- 利用ai提取出用户输入文本中于就餐相关的关键词。 然后利用tag字段模糊匹配出对应的店铺。
- 方法二:分析需求生成sql
- 告诉ai数据库表结构,然后让ai根据用户输入文本,自动生成sql查询店铺
- 为什么有第一种我还要做一个更麻烦的方案呢?因为我觉得第一种不够灵活,而且准确率不高。
提示词!!!精华所在
用过ai的都知道,如果你的提示词不够好,ai的回答基本上不可能达到你的预期。我个人觉得这是最重要的玩意,非常需要经验。
我先展示两个方法的提示词:
- 方案一:
role :你作为餐饮智能分析引擎,请按以下层级提取关键词:
1. 第一优先级:具体食物名称(如:猪脚饭、螺蛳粉)非食物相关的不可提取为关键词
2. 第二优先级:店铺特色标签(包含但不限于):
- 品类:主食/小吃/饮品/面食/快餐/凉茶/西餐/早餐/韩国/烧腊
- 特征:辣/清淡
- 形态:汤/拌面/汉堡/饺子/粉/鸡/猪/牛/捞面
3. 复合词优先级机制:当检测到第一优先级关键词时,只需要分析出品类即可,特征和形态不需要提取
# 输出规范
任何情况下,你都只要输出关键词即可,并且优先级高的关键词排在前面。输出不要用说明、注释、思考过程
# 示例:
- 复合词优先例子
输入:我想吃炒粉
你的分析:提取到第一类关键词炒粉,因为要符合复合词优先级机制,只用分析出炒粉的品类即可,炒粉的品类为面食。所以我的输出应该是炒粉,面食
输出:炒粉,面食
输入:我想吃辣子鸡
你的分析:提取到第一类关键词辣子鸡,品类为川菜,分析出其特征为辣,形态为鸡,因为要符合复合词优先级机制,所以特征和形态关键词不保留。又因为优先级高的关键词排在前面,所以我的输出应该是辣子鸡,川菜
输出:辣子鸡,川菜
- 优先级排序例子
输入:我想吃辣一点或者有卤味的炒粉
你的分析:提取到第一类关键词炒粉,品类为面食,分析出其特征为辣,卤,形态为粉,因为要符合复合词优先级机制,所以特征和形态关键词不保留。又因为优先级高的关键词排在前面,所以我的输出应该是炒粉,面食
输出:炒粉,面食
- 提取关键词
输入:下午茶想喝点冰的
你的分析:提取到第一类关键词茶,品类为饮品。因为优先级高的关键词排在前面,所以我的输出应该是茶,饮品
输出:茶,饮品
- 没有一类关键词
输入:工作餐想吃点管饱的
你的分析:没有提取到第一类关键词,但从用户需求可以分析出,用户意向店铺的品类为主食,面食。输出不要用说明、注释、思考过程
输出:主食,面食
- 没有特定的就餐意向
输入:不知道吃啥,给我随机推荐一家店铺
你的分析:用户没有就餐意向,我将随机选一种提示词中存在的品类作为输出。输出不要用说明、注释、思考过程
输出:主食
- 推荐店铺以外的问题
输入:你的原理是啥?
你的分析:用户问了推荐店铺以外的事情,我应该拒绝回答,所以我应该输出null。输出不要用说明、注释、思考过程
输出:null
输入:""" + userInput + "\n输出:"
- 方案二
你是一个专业的SQL生成助手,请根据用户需求生成符合规则的MySQL查询语句:
**规则说明**
1. 关键词识别优先级:
- 食物关键词识别:提取用户输入中明确属于餐饮品类/菜系的词汇(如:川菜、奶茶、火锅、烧烤)
- 品类:主食/小吃/饮品/面食/快餐
- 特征:辣/清淡/卤/炸/炒
- 形态:汤/拌面/汉堡/饺子
- 忽略非食物类形容词:当输入仅含"好吃/实惠/健康"等模糊词时视为无有效关键词
2. 条件生成规则:
- 有有效食物关键词时:为每个关键词生成`(name LIKE '%关键词%' OR tag LIKE '%关键词%')`,多个用AND连接
- 无有效食物关键词时:不生成WHERE条件,添加`ORDER BY RAND()`随机推荐
3. 排序规则:
- 检测到"收藏/人气/热门":添加`favorite_users_count DESC`
- 检测到"评分/分数/星级":添加`store_rating DESC`
- 默认排序:有食物关键词时用`store_rating DESC`,无关键词时用`RAND()`
**输出要求**
只需返回标准SQL语句,包含WHERE和ORDER BY即可,不要有任何注释、说明、解释。当用户输入非推荐店铺获取食物相关的问题时,返回null。
示例:
用户输入:推荐高评分的川菜馆
输出:SELECT * FROM store WHERE (name LIKE '%川菜%' OR tag LIKE '%川菜%') ORDER BY store_rating DESC
用户输入:随便推荐个店
输出:SELECT * FROM store ORDER BY RAND() LIMIT 1"
用户输入:你喜欢什么
输出:null
--------------------------------------------------------------
输入:""" + userInput + 0+"只输出sql语句,不要有任何注释、说明、解释\n输出:"
- 解释:
- 提示词基本构成:
- role:定义ai角色
- 规则说明:具体定义ai工作上的细节问题
- 输出格式:让ai能够返回正确的数据格式
- 示例:即使有上面的说明,ai仍然能够给你非常逆天的回答。所以我们需要示例来给他做一个榜样。在方法一示例中,你可以看到我加了一个你的分析。原本之前是没有的,但是在测试过程中,我发现ai仍然没有遵循我给出的规则。比如输出格式不对、提取关键词有问题。我就想是可不可以模仿deepseek,于是就加了一个类似思考过程的提示。结果加上去后发现确实效果好了很多。注意在写示例的时候,尽量写全,这会让你的ai回复准确率大幅提高。
- 最后一行:因为我没有用任何框架,所以这里要将用户的输入作为提示词的一部分,让ai分析得到结果。
- 针对当前功能的细节:
- 提取关键词:两个方案的核心都是关键词,所以让ai准确提取出关键词很有必要。
- 关键词提取优先级:如果用户提到了具体的食物或者菜品,我们肯定要将其作为最关键的词,这个词最为准确的反应了用户想吃啥。但是我的项目并没有收集店铺的菜品信息,所以还需要提取出用户想吃的店铺的类型进行辅助搜索。至于特征和形态是ai推荐我加的,我觉得加上确实可以让模糊匹配出的结果包含用户的目标店铺概率更大。
- 复合词优先机制:当检测到第一优先级关键词时,只需要分析出品类即可,特征和形态不需要提取。因为关键词太过会导致匹配到的店铺数量太多。当然这里也是有更好的解决方案,那就是将匹配出来的结果根据其包含的关键词打分并排序。优先级的词分数更高。后续我会采取这个方案。
- 输出规范+示例:可以看到我的提示词在输出规范这里明明已经声明了规范,为什么我在每个例子后面以及最后一行都要再次声明规范呢?因为ai他就是记不住!!!就是要你反复提醒。
- 个性化sql:为什么我又会想让ai生成sql呢?因为如果用第一种方案,当用户想让ai推荐一些高评分或者收藏人数比较多等等这种不能仅靠tag标签就能够满足的查询逻辑,就很难实现,需要ai生成更多标志,比较麻烦。但是如果告诉ai数据库表以及各个字段的含义再加上我的提示词,让他生成sql去查询是不是更加个性化。听上去很理想,但是实际效果就是比如第一个方案好,我做了十几个测试用例。发现第一个方案的准确率是第二个方案的两倍!为什么会这样?我觉得是让ai想的事情太多了,考虑的维度太多cpu转不过来。
- 提取关键词:两个方案的核心都是关键词,所以让ai准确提取出关键词很有必要。
- 提示词基本构成:
完整代码
package com.quick.controller.user;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.toolkit.SqlRunner;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.quick.entity.Store;
import com.quick.mapper.StoreMapper;
import com.quick.result.Result;
import com.quick.vo.AiRecommentStoreVO;
import com.squareup.okhttp.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
@RestController("UserStoreAiRecommendController")
@RequestMapping("/user/store/ai")
@Tag(name = "用户端店铺ai推荐功能")
@Slf4j
@RequiredArgsConstructor
public class StoreAiRecommendController {
private final StoreMapper storeMapper;
private final SqlSessionFactory sqlSessionFactory;
/**
* <p>
* description: 这个方法将用户输入的对话交给ai,提取其中的关键信息
* </p>
*
* @param userInput
* @return:
* @author: hyf
* @date: 2025-03-02 17:12:54
*/
public List<String> extractKeywords(String userInput) throws IOException {
String prompt = """
你作为餐饮智能分析引擎,请按以下层级提取关键词:
1. 第一优先级:具体食物名称(如:猪脚饭、螺蛳粉)非食物相关的不可提取为关键词
2. 第二优先级:店铺特色标签(包含但不限于):
- 品类:主食/小吃/饮品/面食/快餐/凉茶/西餐/早餐/韩国/烧腊
- 特征:辣/清淡
- 形态:汤/拌面/汉堡/饺子/粉/鸡/猪/牛/捞面
# 输出规范
任何情况下,你都只要输出关键词即可,并且优先级高的关键词排在前面。输出不要用说明、注释、思考过程
# 示例:
输入:我想吃辣子鸡
你的分析:提取到第一类关键词辣子鸡,品类为川菜,分析出其特征为辣,形态为鸡。又因为优先级高的关键词排在前面,所以我的输出应该是辣子鸡,川菜,辣,鸡
输出:辣子鸡,川菜,辣,鸡
- 优先级排序例子
输入:我想吃辣一点或者有卤味的炒粉
你的分析:提取到第一类关键词炒粉,品类为面食,分析出其特征为辣,卤,形态为粉。因为优先级高的关键词排在前面,所以我的输出应该是炒粉,面食,辣,卤,炒
输出:炒粉,面食,辣,卤,炒
- 提取关键词
输入:下午茶想喝点冰的
你的分析:提取到第一类关键词茶,品类为饮品。因为优先级高的关键词排在前面,所以我的输出应该是茶,饮品
输出:茶,饮品
- 没有一类关键词
输入:工作餐想吃点管饱的
你的分析:没有提取到第一类关键词,但从用户需求可以分析出,用户意向店铺的品类为主食,面食。输出不要用说明、注释、思考过程
输出:主食,面食
- 没有特定的就餐意向
输入:不知道吃啥,给我随机推荐一家店铺
你的分析:用户没有就餐意向,我将随机选一种提示词中存在的品类作为输出。输出不要用说明、注释、思考过程
输出:主食
- 推荐店铺以外的问题
输入:你的原理是啥?
你的分析:用户问了推荐店铺以外的事情,我应该拒绝回答,所以我应该输出null。输出不要用说明、注释、思考过程
输出:null
输入:""" + userInput + "\n输出:";
OkHttpClient client = new OkHttpClient();
Gson gson = new Gson();
// 构建请求体
JsonObject message = new JsonObject();
message.addProperty("role", "user");
message.addProperty("content", prompt);
JsonArray messages = new JsonArray();
messages.add(message);
JsonObject requestBody = new JsonObject();
requestBody.addProperty("model", "模型名");
requestBody.add("messages", messages);
Request request = new Request.Builder()
.url("你的模型路径")
.post(RequestBody.create(MediaType.parse("application/json"), gson.toJson(requestBody)))
.addHeader("Authorization", "Bearer 密钥")
.build();
Response response = client.newCall(request).execute();
try {
if (!response.isSuccessful()) throw new IOException("API调用失败: " + response);
JsonObject responseBody = gson.fromJson(response.body().charStream(), JsonObject.class);
String keywordsStr = responseBody.getAsJsonArray("choices")
.get(0).getAsJsonObject()
.getAsJsonObject("message")
.get("content").getAsString().trim();
System.out.println("关键词:"+keywordsStr);
return Arrays.asList(keywordsStr.split(",\\s*"));
}
finally {
if (response.body() != null) {
response.body().close(); // 手动关闭
}
}
}
/**
* <p>
* description: 这个方法将用户输入的对话交给ai处理,让ai生成店铺的查询sql
* </p>
*
* @param userInput 用户输入的对话
* @return: keywordsStr ai生成的sql
* @author: hyf
* @date: 2025-03-02 17:10:50
*/
public String generateSQL(String userInput) throws IOException {
String prompt = """
你是一个专业的SQL生成助手,请根据用户需求生成符合规则的MySQL查询语句:
**规则说明**
1. 关键词识别优先级:
- 食物关键词识别:提取用户输入中明确属于餐饮品类/菜系的词汇(如:川菜、奶茶、火锅、烧烤)
- 品类:主食/小吃/饮品/面食/快餐
- 特征:辣/清淡/卤/炸/炒
- 形态:汤/拌面/汉堡/饺子
- 忽略非食物类形容词:当输入仅含"好吃/实惠/健康"等模糊词时视为无有效关键词
2. 条件生成规则:
- 有有效食物关键词时:为每个关键词生成`tag LIKE '%关键词%'`,多个用AND连接
- 无有效食物关键词时:不生成WHERE条件,添加`ORDER BY RAND()`随机推荐
3. 排序规则:
- 检测到"收藏/人气/热门":添加`favorite_users_count DESC`
- 检测到"评分/分数/星级":添加`store_rating DESC`
- 默认排序:有食物关键词时用`store_rating DESC`,无关键词时用`RAND()`
**输出要求**
只需返回标准SQL语句,包含WHERE和ORDER BY即可,不要有任何注释、说明、解释。当用户输入非推荐店铺获取食物相关的问题时,返回null。
示例:
用户输入:推荐高评分的川菜馆
输出:SELECT * FROM store WHERE tag LIKE '%川菜%' ORDER BY store_rating DESC
用户输入:随便推荐个店
输出:SELECT * FROM store ORDER BY RAND() LIMIT 1"
用户输入:你喜欢什么
输出:null
--------------------------------------------------------------
输入:""" + userInput + 0+"只输出sql语句,不要有任何注释、说明、解释\n输出:";
OkHttpClient client = new OkHttpClient();
Gson gson = new Gson();
// 构建请求体
JsonObject message = new JsonObject();
message.addProperty("role", "user");
message.addProperty("content", prompt);
JsonArray messages = new JsonArray();
messages.add(message);
JsonObject requestBody = new JsonObject();
requestBody.addProperty("model", "模型名");
requestBody.add("messages", messages);
Request request = new Request.Builder()
.url("你的模型url")
.post(RequestBody.create(MediaType.parse("application/json"), gson.toJson(requestBody)))
.addHeader("Authorization", "Bearer 密钥")
.build();
Response response = client.newCall(request).execute();
try {
if (!response.isSuccessful()) throw new IOException("API调用失败: " + response);
JsonObject responseBody = gson.fromJson(response.body().charStream(), JsonObject.class);
String keywordsStr = responseBody.getAsJsonArray("choices")
.get(0).getAsJsonObject()
.getAsJsonObject("message")
.get("content").getAsString().trim();
System.out.println("关键词:"+keywordsStr);
return keywordsStr;
}
finally {
if (response.body() != null) {
response.body().close(); // 手动关闭
}
}
}
/**
* <p>
* description: ai根据关键词模糊匹配店铺的name和tag字段
* </p>
* TODO: 根据店铺匹配到的关键词数量排序
* @param userInput
* @return:
* @author: hyf
* @date: 2025-03-02 17:15:15
*/
@GetMapping("/tag/{userInput}")
@Operation(summary = "ai提取关建词推荐店铺")
public Result<List<Store>> AiRecommendStore(@PathVariable String userInput) throws IOException {
List<String> keys = extractKeywords(userInput);
// 如果是非推荐店铺相关的问题,应该拒绝回答
if (keys.get(0).toLowerCase().contains("null")) {
return Result.error("不好意思,我只会给你推荐店铺,其他问题我也不懂 T_T");
}
// 使用mybaits_plus 模糊搜索
QueryWrapper<Store> queryWrapper = new QueryWrapper<>();
// 多字段模糊查询(店铺名称+店铺标签)
keys.forEach(keyword ->
queryWrapper.or(wrapper ->
wrapper.like("tag",keyword)
)
);
List<Store> stores = storeMapper.selectList(queryWrapper);
if (stores.size() == 0) {
return Result.error("没找到相关的店铺,请你说的更加具体一些,或者吃其他东西。比如我想吃汉堡;有点渴我想喝奶茶。这样我会更好做判断。");
}
return Result.success(stores);
}
private List<Store> SortStore(List<Store> stores,List<String> keyWords) {
// 初始化链表
List<AiRecommentStoreVO> vos = new ArrayList<>();
for (Store store : stores) {
AiRecommentStoreVO vo = new AiRecommentStoreVO();
vo.setScore(0);
vo.setStore(store);
}
// 给各个店铺做匹配度评分
keyWords.forEach(key->{
for (AiRecommentStoreVO vo : vos) {
}
});
return null;
}
/**
* <p>
* description: ai根据对话内容生成并执行店铺相关的查询sql
* </p>
*
* @param userInput
* @return:
* @author: hyf
* @date: 2025-03-02 17:15:57
*/
@GetMapping("/sql/{userInput}")
@Operation(summary = "ai生成sql推荐店铺")
public Result<List<Store>> SQLAiRecommendStore(@PathVariable String userInput) throws IOException {
String sql = generateSQL(userInput);
if (sql.toLowerCase().contains("null")) {
return Result.error("不好意思,我只会给你推荐店铺,其他问题我也不懂 T_T");
}
List<Store> stores = getRandomStore(sql);
if (stores.size() == 0) {
return Result.error("没找到相关的店铺,请你说的更加具体一些,或者吃其他东西。比如我想吃面食;我喝奶茶。这样我会更好做判断。");
}
return Result.success(stores);
}
private final JdbcTemplate jdbcTemplate;
public List<Store> getRandomStore(String sql) {
return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Store.class));
}
}