构建企业聊天机器人:向量检索 + 角色 Prompt 设计完整流程
大家好,今天我们来聊聊如何构建一个企业级的聊天机器人,重点在于如何利用向量检索和角色 Prompt 设计,来实现更智能、更贴合业务需求的对话体验。
一、整体架构设计
一个典型的企业聊天机器人系统可以分为以下几个核心模块:
- 用户界面 (UI): 负责接收用户输入,并将机器人的回复呈现给用户。可以是网页、APP、微信小程序等形式。
- 消息处理模块: 接收 UI 传递过来的用户消息,进行预处理,例如:文本清洗、分词等。
- 意图识别模块 (Intent Recognition): 识别用户的意图,例如:查询信息、办理业务、闲聊等。这里我们重点关注查询信息场景,并使用向量检索来高效实现。
- 知识库 (Knowledge Base): 存储企业内部的各种知识,例如:产品信息、FAQ、流程文档等。 我们将把知识库的内容转化为向量,方便后续的相似度检索。
- 向量检索模块: 根据用户的意图和输入,从知识库中检索最相关的知识片段。
- Prompt 生成模块: 根据用户的意图、检索到的知识片段以及预设的角色 Prompt,生成最终的 Prompt。
- 大语言模型 (LLM): 接收 Prompt,生成机器人的回复。
- 回复后处理模块: 对 LLM 的回复进行后处理,例如:格式化、过滤敏感信息等。
- 日志记录模块: 记录用户的对话、系统的运行状态等,用于后续的分析和优化。
二、知识库构建与向量化
2.1 知识库构建
首先,我们需要构建一个包含企业知识的知识库。 知识库的格式可以根据实际情况选择,例如:
- FAQ 列表: 每个 FAQ 包含一个问题和对应的答案。
- 文档集合: 可以是 PDF、Word、TXT 等格式的文档。
- 数据库: 如果知识存储在数据库中,可以直接从数据库中提取。
假设我们有一个简单的 FAQ 列表,存储在 faqs.csv 文件中,格式如下:
question,answer
"你们的产品有哪些型号?","我们有 A 型、B 型和 C 型三种型号的产品。"
"产品的保修期是多久?","产品的保修期为一年。"
"如何联系客服?","您可以拨打我们的客服电话:400-xxxx-xxxx。"
2.2 向量化
我们需要将知识库中的内容转化为向量,以便进行相似度检索。常用的向量化方法包括:
- TF-IDF: 一种传统的文本向量化方法,但效果相对较差。
- Word2Vec / GloVe: 词向量模型,可以将每个词转化为向量。
- Sentence Transformers: 专门用于生成句子向量的模型,效果更好。
这里我们选择使用 Sentence Transformers。 首先,安装 Sentence Transformers 库:
pip install sentence-transformers
然后,编写 Java 代码进行向量化:
import ai.djl.huggingface.tokenizers.Encoding;
import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer;
import ai.djl.inference.InferenceModel;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDList;
import ai.djl.repository.zoo.Criteria;
import ai.djl.training.util.PairList;
import com.opencsv.CSVReader;
import com.opencsv.exceptions.CsvException;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
public class VectorizeKnowledgeBase {
private static final String MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"; // 选择一个预训练模型
private static final String TOKENIZER_PATH = "tokenizers";
public static void main(String[] args) throws IOException, CsvException {
// 1. 加载知识库
List<String> questions = loadQuestionsFromCSV("faqs.csv");
// 2. 初始化模型和 Tokenizer
Criteria<String, NDList, NDList> criteria = Criteria.builder()
.setTypes(String.class, NDList.class, NDList.class)
.optModelName(MODEL_NAME)
.optModelPath(Paths.get("models").toAbsolutePath().toString()) // 模型下载路径
.optOption("device", "cpu") // 可选: "cpu" 或 "gpu"
.build();
try (InferenceModel model = criteria.loadModel()) {
HuggingFaceTokenizer tokenizer = HuggingFaceTokenizer.newInstance(Paths.get(TOKENIZER_PATH).resolve(MODEL_NAME).toString());
// 3. 向量化每个问题
List<float[]> vectors = new ArrayList<>();
for (String question : questions) {
float[] vector = vectorize(question, model, tokenizer);
vectors.add(vector);
System.out.println("Vectorized question: " + question);
}
// 4. 保存向量 (这里只是打印,实际应该保存到向量数据库)
for (int i = 0; i < vectors.size(); i++) {
System.out.println("Question: " + questions.get(i) + ", Vector: " + vectors.get(i).length + " dimensions");
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static List<String> loadQuestionsFromCSV(String filePath) throws IOException, CsvException {
List<String> questions = new ArrayList<>();
try (CSVReader reader = new CSVReader(new FileReader(filePath))) {
List<String[]> records = reader.readAll();
// 跳过标题行
for (int i = 1; i < records.size(); i++) {
String[] record = records.get(i);
questions.add(record[0]); // 第一列是问题
}
}
return questions;
}
private static float[] vectorize(String text, InferenceModel model, HuggingFaceTokenizer tokenizer) throws IOException {
Encoding encoding = tokenizer.encode(text);
long[] indices = encoding.getIds();
long[] attentionMask = encoding.getAttentionMask();
NDArray inputIds = model.getNDManager().create(indices);
NDArray attentionMaskArray = model.getNDManager().create(attentionMask);
NDList list = new NDList(inputIds, attentionMaskArray);
PairList<String, NDArray> inputs = new PairList<>();
inputs.add("input_ids", inputIds);
inputs.add("attention_mask", attentionMaskArray);
NDList output = model.predict(new NDList(inputIds, attentionMaskArray));
// Pooling strategy (mean pooling)
NDArray embeddings = output.get(0); // Assuming the first output is the embeddings
NDArray maskExpanded = attentionMaskArray.expandDims(-1).broadcast(embeddings.getShape());
NDArray maskedEmbeddings = embeddings.mul(maskExpanded);
NDArray sumEmbeddings = maskedEmbeddings.sum(new int[]{1});
NDArray sumMask = attentionMaskArray.sum(new int[]{1});
NDArray pooledEmbeddings = sumEmbeddings.div(sumMask);
return pooledEmbeddings.toFloatArray();
}
}
代码解释:
MODEL_NAME: 指定使用的 Sentence Transformers 模型。loadQuestionsFromCSV(): 从 CSV 文件中读取问题列表。vectorize(): 使用 Sentence Transformers 模型将文本转化为向量。- 首先使用
HuggingFaceTokenizer将文本分词并转化为 token ID。 - 然后将 token ID 传入模型,得到 embedding。
- 最后使用 Mean Pooling 的方式,将所有 token 的 embedding 聚合成一个句子向量。
- 首先使用
- 向量保存: 这里只是简单地打印了向量的维度,实际应用中应该将向量保存到向量数据库,例如:Milvus、Weaviate、Pinecone 等。
三、向量检索
当用户输入问题时,我们需要将问题转化为向量,然后在知识库中检索最相似的向量。
3.1 用户问题向量化
使用与知识库向量化相同的方法,将用户输入的问题转化为向量。
3.2 相似度计算
计算用户问题向量与知识库中每个向量的相似度。 常用的相似度计算方法包括:
- 余弦相似度 (Cosine Similarity): 最常用的相似度计算方法。
- 点积 (Dot Product): 如果向量已经归一化,点积和余弦相似度等价。
- 欧氏距离 (Euclidean Distance): 也可以用来计算相似度,但需要将距离转化为相似度。
3.3 检索
选择相似度最高的 N 个向量作为检索结果。
示例代码:
import ai.djl.huggingface.tokenizers.Encoding;
import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer;
import ai.djl.inference.InferenceModel;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDList;
import ai.djl.repository.zoo.Criteria;
import ai.djl.training.util.PairList;
import com.opencsv.CSVReader;
import com.opencsv.exceptions.CsvException;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;
public class VectorSearch {
private static final String MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2";
private static final String TOKENIZER_PATH = "tokenizers";
private static final int TOP_K = 3; // 返回最相似的 TOP K 个结果
public static void main(String[] args) throws IOException, CsvException {
// 1. 加载知识库
List<String> questions = loadQuestionsFromCSV("faqs.csv");
// 2. 初始化模型和 Tokenizer
Criteria<String, NDList, NDList> criteria = Criteria.builder()
.setTypes(String.class, NDList.class, NDList.class)
.optModelName(MODEL_NAME)
.optModelPath(Paths.get("models").toAbsolutePath().toString())
.optOption("device", "cpu")
.build();
try (InferenceModel model = criteria.loadModel()) {
HuggingFaceTokenizer tokenizer = HuggingFaceTokenizer.newInstance(Paths.get(TOKENIZER_PATH).resolve(MODEL_NAME).toString());
// 3. 向量化知识库
List<float[]> knowledgeBaseVectors = new ArrayList<>();
for (String question : questions) {
float[] vector = vectorize(question, model, tokenizer);
knowledgeBaseVectors.add(vector);
}
// 4. 用户输入问题
String userQuestion = "你们的产品保修多久?";
// 5. 向量化用户问题
float[] userQuestionVector = vectorize(userQuestion, model, tokenizer);
// 6. 向量检索
PriorityQueue<SearchResult> topKResults = search(userQuestionVector, knowledgeBaseVectors, questions, TOP_K);
// 7. 打印检索结果
System.out.println("User Question: " + userQuestion);
System.out.println("Top " + TOP_K + " Similar Questions:");
while (!topKResults.isEmpty()) {
SearchResult result = topKResults.poll();
System.out.println("Question: " + result.question + ", Similarity: " + result.similarity);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static List<String> loadQuestionsFromCSV(String filePath) throws IOException, CsvException {
List<String> questions = new ArrayList<>();
try (CSVReader reader = new CSVReader(new FileReader(filePath))) {
List<String[]> records = reader.readAll();
for (int i = 1; i < records.size(); i++) {
String[] record = records.get(i);
questions.add(record[0]);
}
}
return questions;
}
private static float[] vectorize(String text, InferenceModel model, HuggingFaceTokenizer tokenizer) throws IOException {
Encoding encoding = tokenizer.encode(text);
long[] indices = encoding.getIds();
long[] attentionMask = encoding.getAttentionMask();
NDArray inputIds = model.getNDManager().create(indices);
NDArray attentionMaskArray = model.getNDManager().create(attentionMask);
NDList list = new NDList(inputIds, attentionMaskArray);
PairList<String, NDArray> inputs = new PairList<>();
inputs.add("input_ids", inputIds);
inputs.add("attention_mask", attentionMaskArray);
NDList output = model.predict(new NDList(inputIds, attentionMaskArray));
// Pooling strategy (mean pooling)
NDArray embeddings = output.get(0);
NDArray maskExpanded = attentionMaskArray.expandDims(-1).broadcast(embeddings.getShape());
NDArray maskedEmbeddings = embeddings.mul(maskExpanded);
NDArray sumEmbeddings = maskedEmbeddings.sum(new int[]{1});
NDArray sumMask = attentionMaskArray.sum(new int[]{1});
NDArray pooledEmbeddings = sumEmbeddings.div(sumMask);
return pooledEmbeddings.toFloatArray();
}
private static PriorityQueue<SearchResult> search(float[] queryVector, List<float[]> knowledgeBaseVectors, List<String> questions, int topK) {
PriorityQueue<SearchResult> topKResults = new PriorityQueue<>((a, b) -> Float.compare(a.similarity, b.similarity)); // Min-heap
for (int i = 0; i < knowledgeBaseVectors.size(); i++) {
float[] knowledgeBaseVector = knowledgeBaseVectors.get(i);
double similarity = cosineSimilarity(queryVector, knowledgeBaseVector);
SearchResult result = new SearchResult(questions.get(i), similarity);
if (topKResults.size() < topK) {
topKResults.offer(result);
} else if (similarity > topKResults.peek().similarity) {
topKResults.poll(); // Remove the smallest
topKResults.offer(result);
}
}
return topKResults;
}
private static double cosineSimilarity(float[] vectorA, float[] vectorB) {
double dotProduct = 0.0;
double magnitudeA = 0.0;
double magnitudeB = 0.0;
for (int i = 0; i < vectorA.length; i++) {
dotProduct += vectorA[i] * vectorB[i];
magnitudeA += Math.pow(vectorA[i], 2);
magnitudeB += Math.pow(vectorB[i], 2);
}
magnitudeA = Math.sqrt(magnitudeA);
magnitudeB = Math.sqrt(magnitudeB);
if (magnitudeA == 0.0 || magnitudeB == 0.0) {
return 0.0;
}
return dotProduct / (magnitudeA * magnitudeB);
}
private static class SearchResult {
String question;
double similarity;
public SearchResult(String question, double similarity) {
this.question = question;
this.similarity = similarity;
}
}
}
代码解释:
search(): 实现向量检索功能。- 计算用户问题向量与知识库中每个向量的余弦相似度。
- 使用
PriorityQueue维护一个大小为topK的最小堆,用于存储相似度最高的topK个结果。
cosineSimilarity(): 计算两个向量的余弦相似度。
四、角色 Prompt 设计
Prompt 的质量直接影响 LLM 的回复质量。 一个好的 Prompt 应该包含以下信息:
- 角色 (Role): 指定 LLM 的角色,例如:客服代表、技术专家等。
- 任务 (Task): 明确 LLM 需要完成的任务。
- 上下文 (Context): 提供 LLM 需要的上下文信息,例如:用户的历史对话、检索到的知识片段等。
- 格式 (Format): 指定 LLM 的回复格式,例如:JSON、Markdown 等。
4.1 角色定义
根据不同的业务场景,定义不同的角色。 例如:
- 客服代表: 负责回答用户的问题,解决用户的疑问。
- 技术专家: 负责提供技术支持,解决技术问题。
- 销售顾问: 负责推荐产品,促成销售。
4.2 Prompt 模板
设计 Prompt 模板,方便后续生成 Prompt。 例如:
你是一位专业的{role},你的任务是{task}。
以下是一些相关的知识片段:
{knowledge_fragments}
用户的问题是:{user_question}
请根据以上信息,用简洁明了的语言回答用户的问题。
4.3 Prompt 生成
根据用户意图、检索到的知识片段以及 Prompt 模板,生成最终的 Prompt。
示例代码:
public class PromptGenerator {
public static String generatePrompt(String role, String task, List<String> knowledgeFragments, String userQuestion) {
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("你是一位专业的").append(role).append(",你的任务是").append(task).append("。nn");
promptBuilder.append("以下是一些相关的知识片段:n");
for (String fragment : knowledgeFragments) {
promptBuilder.append("- ").append(fragment).append("n");
}
promptBuilder.append("n用户的问题是:").append(userQuestion).append("nn");
promptBuilder.append("请根据以上信息,用简洁明了的语言回答用户的问题。");
return promptBuilder.toString();
}
public static void main(String[] args) {
String role = "客服代表";
String task = "回答用户关于产品保修期的问题";
List<String> knowledgeFragments = List.of("产品的保修期为一年。");
String userQuestion = "你们的产品保修多久?";
String prompt = generatePrompt(role, task, knowledgeFragments, userQuestion);
System.out.println(prompt);
}
}
输出的 Prompt:
你是一位专业的客服代表,你的任务是回答用户关于产品保修期的问题。
以下是一些相关的知识片段:
- 产品的保修期为一年。
用户的问题是:你们的产品保修多久?
请根据以上信息,用简洁明了的语言回答用户的问题。
五、大语言模型调用
将生成的 Prompt 传递给大语言模型 (LLM),获取 LLM 的回复。 常用的 LLM 包括:
- OpenAI GPT 系列: GPT-3, GPT-3.5, GPT-4 等。
- Google PaLM 系列: PaLM 2 等。
- 开源模型: LLaMA, ChatGLM 等。
示例代码 (调用 OpenAI API):
import okhttp3.*;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.util.List;
public class LLMCaller {
private static final String API_KEY = "YOUR_OPENAI_API_KEY"; // 替换为你的 OpenAI API Key
private static final String MODEL_NAME = "gpt-3.5-turbo"; // 选择一个模型
public static String callLLM(String prompt) throws IOException {
OkHttpClient client = new OkHttpClient();
MediaType mediaType = MediaType.parse("application/json");
JSONObject jsonBody = new JSONObject();
jsonBody.put("model", MODEL_NAME);
JSONArray messages = new JSONArray();
JSONObject userMessage = new JSONObject();
userMessage.put("role", "user");
userMessage.put("content", prompt);
messages.put(userMessage);
jsonBody.put("messages", messages);
jsonBody.put("temperature", 0.7); // 调整 temperature 控制生成结果的随机性
RequestBody body = RequestBody.create(mediaType, jsonBody.toString());
Request request = new Request.Builder()
.url("https://api.openai.com/v1/chat/completions")
.post(body)
.addHeader("Authorization", "Bearer " + API_KEY)
.addHeader("Content-Type", "application/json")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
}
String responseBody = response.body().string();
JSONObject jsonResponse = new JSONObject(responseBody);
JSONArray choices = jsonResponse.getJSONArray("choices");
JSONObject choice = choices.getJSONObject(0);
JSONObject message = choice.getJSONObject("message");
return message.getString("content");
}
}
public static void main(String[] args) throws IOException {
String prompt = "你是一位专业的客服代表,你的任务是回答用户关于产品保修期的问题。nn" +
"以下是一些相关的知识片段:n" +
"- 产品的保修期为一年。nn" +
"用户的问题是:你们的产品保修多久?nn" +
"请根据以上信息,用简洁明了的语言回答用户的问题。";
String response = callLLM(prompt);
System.out.println("LLM Response: " + response);
}
}
代码解释:
API_KEY: 替换为你的 OpenAI API Key。callLLM(): 调用 OpenAI API,获取 LLM 的回复。- 构建 JSON 请求体,包含模型名称、Prompt 和其他参数。
- 发送 HTTP POST 请求到 OpenAI API。
- 解析 JSON 响应,提取 LLM 的回复。
六、回复后处理
对 LLM 的回复进行后处理,例如:
- 格式化: 将 LLM 的回复格式化为易于阅读的格式。
- 过滤敏感信息: 过滤掉 LLM 回复中的敏感信息。
- 添加引用: 如果 LLM 的回复引用了知识库中的内容,添加引用链接。
七、持续优化
聊天机器人的构建是一个持续优化的过程。 可以通过以下方式进行优化:
- 分析用户对话: 分析用户的对话,了解用户的问题和需求。
- 优化知识库: 根据用户的问题和需求,不断完善知识库。
- 调整 Prompt: 根据 LLM 的回复质量,不断调整 Prompt。
- 评估指标: 使用评估指标,例如:准确率、召回率、满意度等,来评估聊天机器人的性能。
总结:
构建企业聊天机器人涉及多个环节,包括知识库构建、向量化、向量检索、角色 Prompt 设计、LLM 调用和回复后处理。 通过合理的架构设计和持续优化,可以构建一个智能、高效、贴合业务需求的聊天机器人。
项目总结:
从知识库的构建和向量化,到向量检索的实现,再到Prompt工程和LLM的对接,最终实现一个企业级的聊天机器人。需要注意的是,数据质量决定了上限,而Prompt设计决定了下限,后续的优化需要根据实际情况进行迭代和调整。