- Published on
RAG检索增强生成(一)
- Authors

- Name
- 游戏人生
RAG 原理及流程
RAG 原理
RAG 通过尽可能提供与答案相关的上下文,来提升搜索结果的准确性,用于解决 LLM 基于概率性的底层逻辑。
RAG 流程
- 加载数据: 加载不同数据源的数据,例如 pdf、code、现存数据库、云数据库等。
- 切分数据: 对数据集进行语意化的切分,根据内容的特点和目标大模型的特点、上下文窗口等,对数据源进行合适的切分。
- 嵌入(embedding): 将切分后的每一个文档块使用 embedding 算法转换成一个向量,存储到向量数据库中,这样每一个原始数据都有一个对应的向量用来检索。
- 检索数据: 当所有需要的数据都存储到向量数据库中后,就把用户的提问也 embedding 成向量,用这个向量去向量数据库中进行检索,找到相似性最高的几个文档块。
- 增强 prompt: 根据文档块去构建 prompt
- 生成结果
相关概念及API
prompt
PromptTemplate 可以帮助我们定义一个包含变量的字符串模版,可以通过向该类的对象输入不同的变量值来生成模版渲染的结果。
基础 prompt
无变量
import { PromptTemplate } from "@langchain/core/prompts";
const prompt1 = new PromptTemplate({
inputVariables: [],
template: "你是谁",
});
const msg = await prompt1.format();
console.log(msg);
// 你是谁
带变量
const paramPrompt = new PromptTemplate({
inputVariables: ["name"],
template: "我是:{name}",
});
const model = await paramPrompt.format({
name: "小明",
});
console.log(model);
// 我是:小明
多个变量
const paramPrompt = new PromptTemplate({
inputVariables: ["name", "age", "sex"],
template: "我是:{name},今年{age}岁,{{{sex}}}",
});
const model = await paramPrompt.format({
name: "小明",
age: 18,
sex: "男",
});
console.log(model);
// 我是:小明,今年18岁,{男}
简写
const simpleTemp = PromptTemplate.fromTemplate("我是 {name},今年{age}岁,{{{sex}}}");
console.log(simpleTemp.inputVariables);
// [ "name", "age", "sex" ]
const modal = await simpleTemp.format({
name: "小明",
age: 18,
sex: "男",
});
console.log(modal);
// 我是 小明,今年18岁,{男}
分步传参
const initialPrompt = new PromptTemplate({
inputVariables: ["name", "age"],
template: "我是:{name},今年{age}岁",
});
const partialedPrompt = await initialPrompt.partial({
name: "小明",
});
const format1 = await partialedPrompt.format({
age: 23,
});
console.log(format1);
// 我是:小明,今年23岁
const format2 = await partialedPrompt.format({
age: 16,
});
console.log(format2);
// 我是:小明,今年16岁
动态参数
const getCurrentTime = () => {
return new Date().toLocaleDateString();
};
const prompt1 = new PromptTemplate({
template: "现在是{date},{activity}。",
inputVariables: ["date", "activity"],
});
const partialedPt = await prompt1.partial({
date: getCurrentTime,
});
const formatVal = await partialedPt.format({
activity: "该吃饭了",
});
console.log(formatVal);
// 现在是2024/5/23,该吃饭了。
函数 getCurrentTime 在 format 被调用的时候实时执行,也就是可以在被渲染成字符串时获取到最新的外部信息。 目前不支持传入参数,如果需要参数,可以用 js 的闭包进行参数的传递。
chat prompt
chatPrompt 主要包括以下几种,分别对应一段 ChatMessage 不同的角色:
- ChatPromptTemplate : 描述了用户和 LLM 的对话,包括系统消息、用户消息和 LLM 消息
- SystemMessagePromptTemplate: 系统消息,通常用于设置对话的上下文或指定模型采取特定的行为模式
- AIMessagePromptTemplate: 用户消息,代表真实用户在对话中的发言
- HumanMessagePromptTemplate: AI模型的回复,这些消息是模型根据 system 的指示和 user 的输入生成的
具体使用:
1、构建系统 message
import { SystemMessagePromptTemplate } from "@langchain/core/prompts";
const sysTemplate = SystemMessagePromptTemplate.fromTemplate(`你是一个专
业的翻译员,你的任务是将文本从{source_lang}翻译成{target_lang}。`);
2、构建用户输入 message
import { HumanMessagePromptTemplate } from "@langchain/core/prompts";
const userTemplate = HumanMessagePromptTemplate.fromTemplate("请翻译这句话:{text}");
3、将sysTemplate 和 userTemplate 组合起来,构建一个 PromptTemplate 对象
import { ChatPromptTemplate } from "@langchain/core/prompts";
const chatPrompt = ChatPromptTemplate.fromMessages([
sysTemplate,
userTemplate,
]);
4、使用 formatMessages 格式化对话信息
const formatPt = await chatPrompt.formatMessages({
source_lang: "中文",
target_lang: " 日语",
text: "你好,小明同学",
});
console.log(formatPt);
5、简写写法如下
import { ChatPromptTemplate } from "@langchain/core/prompts";
const systemTemplate = "你是一个专业的翻译员,你的任务是将文本从{source_lang}翻译成{target_lang}。";
const humanTemplate = "请翻译这句话:{text}";
const chatPrompt = ChatPromptTemplate.fromMessages([
["system", systemTemplate],
["human", humanTemplate],
]);
6、测试
import { ChatAlibabaTongyi } from "@langchain/community/chat_models/alibaba_tongyi";
import { HumanMessage } from "@langchain/core/messages";
import { StringOutputParser } from "@langchain/core/output_parsers";
const model = new ChatAlibabaTongyi({
model: "qwen-turbo", // Available models: qwen-turbo, qwen-plus, qwen-max
temperature: 1,
});
const outputPrase = new StringOutputParser();
const simpleChain = chatPrompt.pipe(model).pipe(outputPrase);
const stream = await simpleChain.invoke({
source_lang: "中文",
target_lang: "日语",
text: "你好,小明同学",
});
console.log(stream);
// こんにちは、小明さん。
组合多个 prompt
PipelinePromptTemplate 可以将多个独立的 template 构建成一个完整且复杂的 prompt,以此提高独立 prompt 的复用性,进一步增强模块化带来的优势。
PipelinePromptTemplate 两个核心概念:
- pipelinePrompts 一组 object,每个 object 表示 prompt 运行后赋值给 name 变量
- finalPrompt 表示最终输出的 prompt
示例如下:
import {
PromptTemplate,
PipelinePromptTemplate,
} from "@langchain/core/prompts";
const getCurrentTime = () => {
return new Date().toLocaleDateString();
};
const fullPt = PromptTemplate.fromTemplate(`
你是一个智能管家,今天是 {date},你的主人的信息是{info},
根据上下文,完成主人的需求
{task}`);
const datePt = PromptTemplate.fromTemplate("{date},现在是 {period}");
const periodPt = await datePt.partial({
date: getCurrentTime,
});
const infoPt = PromptTemplate.fromTemplate("姓名是 {name}, 性别是 {sex}");
const taskPt = PromptTemplate.fromTemplate(`
我想吃 {period} 的 {food}。
再重复一遍我的信息 {info}`);
const composedPrompt = new PipelinePromptTemplate({
pipelinePrompts: [
{
name: "date",
prompt: periodPt,
},
{
name: "info",
prompt: infoPt,
},
{
name: "task",
prompt: taskPt,
},
],
finalPrompt: fullPt,
});
const formatPt = await composedPrompt.format({
period: "早上",
name: "小明",
sex: "男",
food: "煎饼",
});
console.log(formatPt);
// 你是一个智能管家,今天是 2024/5/23,现在是 早上,你的主人的信息是姓名是 小明, 性别是 男,
// 根据上下文,完成主人的需求
// 我想吃 早上 的 煎饼。
// 再重复一遍我的信息 姓名是 小明, 性别是 男
注意
- 一个变量可以多次复用,例如外界输入的 period 在 periodPt 和 taskPt 都被使用了
- pipelinePrompts 中的变量可以被引用,比如在 taskPt 使用了 infoPt 的运行结果
- 支持动态自定义和 partial,示例中也涉及到了这两种特殊的 template
- langchain 会自动分析 pipeline 之间的依赖关系,尽可能的进行并行化来提高运行速度
OutputParser
OutputParser 用于解析模型输出,将模型输出转换为我们需要的格式。
StringOutputParser
最简单的 Parser,返回 API 中的文本数据,即 content 部分。langchain 内部有错误处理和 stream 等的支持。
import { ChatBaiduWenxin } from "@langchain/community/chat_models/baiduwenxin";
import { HumanMessage } from "@langchain/core/messages";
import { StringOutputParser } from "@langchain/core/output_parsers";
const model = new ChatBaiduWenxin({
model: "ERNIE-Speed-8K", // ERNIE-Lite-8K、ERNIE-Lite-8K-0922、ERNIE-Tiny-8K、ERNIE-Speed-128K、ERNIE-Speed-8K、ERNIE Speed-AppBuilder
temperature: 1,
});
const outputPrase = new StringOutputParser();
const simpleChain = model.pipe(outputPrase);
const stream = await simpleChain.invoke([
new HumanMessage("你是谁"),
]);
console.log(stream);
StructuredOutputParser
引导模型引以需要的格式进行输出。
import { StructuredOutputParser } from "langchain/output_parsers";
import { PromptTemplate } from "@langchain/core/prompts";
const parser = StructuredOutputParser.fromNamesAndDescriptions({
answer: "用户问题的答案",
evidence: "你回答用户问题所依据的答案",
confidence: "问题答案的可信度评分,格式是百分数",
});
const prompt = PromptTemplate.fromTemplate("尽可能的回答用的问题 {instructions} {question}")
const model = new ChatBaiduWenxin({
model: "ERNIE-Speed-8K",
temperature: 1,
});
const chain = prompt.pipe(model).pipe(parser);
const res = await chain.invoke({
question: "蒙娜丽莎的作者是谁?是什么时候绘制的",
instructions: parser.getFormatInstructions(),
});
console.log(res);
输出格式为:
{
answer: "蒙娜丽莎的作者是列奥纳多·达·芬奇,大约是在15世纪末期绘制的。",
evidence: "这一信息来源于艺术史和相关的文化资料,蒙娜丽莎是一幅世界著名的画作,其作者和创作时间都是公众熟知的事实。",
confidence: "95%"
}
CommaSeparatedListOutputParser
CommaSeparatedListOutputParser 是一个输出解析器,用于解析成逗号分隔的列表。
import { CommaSeparatedListOutputParser } from "@langchain/core/output_parsers";
const parser = new CommaSeparatedListOutputParser();
const model = new ChatBaiduWenxin({
model: "ERNIE-Speed-8K",
temperature: 1,
});
const prompt = PromptTemplate.fromTemplate("列出3个 {country} 的著名的互联网公司. {instructions}")
const chain = prompt.pipe(model).pipe(parser)
const res = await chain.invoke({
country: "中国",
instructions: parser.getFormatInstructions(),
});
console.log(res);
输出内容如下:
[ "百度,阿里巴巴,腾讯。" ]
Auto Fix Parser
首先使用 zod 定义一个需要的类型:
import { z } from "zod";
const schema = z.object({
answer: z.string().describe("用户问题的答案"),
confidence: z.number().min(0).max(100).describe("问题答案的可信度评分,满分 100")
});
使用正常的方式,使用 zod 来创建一个 StructuredOutputParser
import { PromptTemplate } from "@langchain/core/prompts";
import { StructuredOutputParser } from "langchain/output_parsers";
import { ChatBaiduWenxin } from "@langchain/community/chat_models/baiduwenxin";
const parser = StructuredOutputParser.fromZodSchema(schema);
const prompt = PromptTemplate.fromTemplate("尽可能的回答用的问题 \n{instructions} \n{question}")
const model = new ChatBaiduWenxin({
model: "ERNIE-Speed-8K",
temperature: 1,
});
const chain = prompt.pipe(model).pipe(parser);
const res = await chain.invoke({
question: "蒙娜丽莎的作者是谁?是什么时候绘制的",
instructions: parser.getFormatInstructions(),
});
console.log(res);
输出内容如下:
{ answer: "蒙娜丽莎的作者是列奥纳多·达·芬奇,大约是在15世纪末期绘制的。", confidence: 100 }
构造一个可以根据 zod 定义以及错误的输出,来自动修复的 parser:
import { OutputFixingParser } from "langchain/output_parsers";
const fixParser = OutputFixingParser.fromLLM(model, parser);
修复一个错误类型的输出:
const wrongOutput = {
"answer": "蒙娜丽莎的作者是达芬奇,大约在16世纪初期(1503年至1506年之间)开始绘制。",
"sources": "90%",
};
const fixParser = OutputFixingParser.fromLLM(model, parser);
const output = await fixParser.parse(JSON.stringify(wrongOutput));
console.log(output);
输出内容如下:
{ answer: "蒙娜丽莎的作者是达芬奇,大约在15世纪末期至16世纪初期开始绘制。", confidence: 90 }
定义一个,数值超出限制的错误:
const wrongOutput = {
"answer": "蒙娜丽莎的作者是达芬奇,大约在16世纪初期(1503年至1506年之间)开始绘制。",
"sources": "-1"
};
const fixParser = OutputFixingParser.fromLLM(model, parser);
const output = await fixParser.parse(JSON.stringify(wrongOutput));
console.log(output);
输出内容如下:
{ answer: "蒙娜丽莎的作者是达芬奇,大约在15世纪末期至16世纪初期开始绘制。", confidence: 95 }
Embedding 多数据源加载
RAG 的本质是给 chat bot 外挂数据源,数据源的形式多种多样,可以是文件/数据库/网络数据/代码 等等。
TextLoader
用于对文件所在的路径进行加载。
import { TextLoader } from "langchain/document_loaders/fs/text";
const loader = new TextLoader("data/2.txt");
const docs = await loader.load();
console.log(docs);
输出内容如下:
[
Document {
pageContent: "鲁镇的酒店的格局,是和别处不同的:都是当街一个曲尺形的大柜台,柜里面预备着热水,可以随时温酒。做工的人,傍午傍晚散了工,每每花四文铜钱,买一碗酒,——这是二十多年前的事,现在每碗要涨到十文,——靠柜外"... 2150 more characters,
metadata: { source: "data/2.txt" }
}
]
返回对象就是一个 Document 对象的实例,其中 pageContent 是文本的原始内容,而在 metadata 中是跟这个对象相关的一些元数据, 这里就是加载原始文件的文件名。
PDFLoader
加载 PDF 文件作为外挂数据库。
在 Deno 环境下使用 PDFLoader 会有 bug,报错找不到 pdf 文件。 解决方法有两个,第一个是把这个文件放在项目根目录里。第二个是修改 deno.json 中 pdf-parser 的别名。
"pdf-parse": "npm:/pdf-parse/lib/pdf-parse.js"
加载 pdf 文件:
import * as pdfParse from "pdf-parse";
import { PDFLoader } from "langchain/document_loaders/fs/pdf";
const loader = new PDFLoader("./data/webpack.pdf");
const pdfs = await loader.load()
console.log(pdfs);
输出内容如下:
[
Document {
pageContent: "什么是前端工程化\n" +
"前端工程化: 在企业级的前端项目开发中,把前端开发所需要的工具,技术,流程,经验等进行规范化,标准化。\n" +
"前端工程化的解决方案\n" +
"早期的前端工程化解决方案:grunt ,gulp\n" +
"目前主"... 8041 more characters,
metadata: {
source: "./data/webpack.pdf",
pdf: {
version: "1.10.100",
info: {
PDFFormatVersion: "1.3",
IsAcroFormPresent: false,
IsXFAPresent: false,
Producer: "macOS 版本12.3.1(版号21E258) Quartz PDFContext",
CreationDate: "D:20220430083114Z00'00'",
ModDate: "D:20220430083114Z00'00'"
},
metadata: null,
totalPages: 1
},
loc: { pageNumber: 1 }
}
}
]
打印出来 pdfs是一个 Document 数组,每一个 Document 对象即 pdf 中的一页,这是 PDFLoader 的默认行为。 关闭这个特性:
const loader = new PDFLoader("./data/webpack.pdf");
const pdf = await loader.load();
DirectoryLoader
用于加载一个文件夹下多种格式的文件。
import { DirectoryLoader } from "langchain/document_loaders/fs/directory";
const loader = new DirectoryLoader(
"./data",
{
".pdf": (path) => new PDFLoader(path, { splitPages: false }),
".txt": (path) => new TextLoader(path),
}
);
const docs = await loader.load();
console.log(docs);
返回内容的每个 Document 都对应一篇文档。
Github loader
获取 Github 上某个仓库的代码,并构建数据库,然后根据用户提问寻找与此相关的代码片段回答用户问题
import { GithubRepoLoader } from "langchain/document_loaders/web/github";
import ignore from "ignore";
const loader = new GithubRepoLoader(
"https://github.com/qingxiang1/qingxiang1.github.io",
{
branch: "master",
recursive: false,
unknown: "warn",
ignorePaths: ["*.md", "yarn.lock", "*.json"],
accessToken: env["GITHUB_TOKEN"]
}
);
相关属性如下:
- branch 分支
- recursive 是否递归访问文件夹内部的内容,请求量比较大,等待比较久
- ignorePaths 使用的 git ignore 的语法,忽略掉一些特定格式的文件
- accessToken github API 的 accessToken,在没有设置的情况也可以访问,但有频率限制
设置 Github Token
WebLoader
一般使用 Cheerio 来提取网页中的静态内容,类似于 python 中的 BeautifulSoup。
import "cheerio";
import { CheerioWebBaseLoader } from "langchain/document_loaders/web/cheerio";
const loader = new CheerioWebBaseLoader(
"https://blog.qxmall.store/blog/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%9F%BA%E7%A1%80"
);
const docs = await loader.load();
console.log(docs);
输出内容为:
[
Document {
pageContent: "!function(){try{var d=document.documentElement,c=d.classList;c.remove('light','dark');var e=localSto"... 105007 more characters,
metadata: {
source: "https://blog.qxmall.store/blog/ai-model/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%9F%BA%E7%A1%80"
}
}
]
可以用类似 jQuery 的语法对 html 中的元素进行选择和过滤:
const loader = new CheerioWebBaseLoader(
"https://blog.qxmall.store/blog/ai-model/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%9F%BA%E7%A1%80",
{
selector: "h2",
}
);
Search API
给 chatbot 接入网络支持最重要的 API,在 langchain.js 中,常用的是 SearchApiLoader 和 SerpAPILoader,这两个提供的都是接入搜索的能力,免费计划都是每个月 100 次 search 能力,除了 google 外,也支持 baidu/bing 等常用的搜索引擎。
import { SerpAPILoader } from "langchain/document_loaders/web/serpapi";
// import { SerpAPILoader } from "@langchain/community/document_loaders/web/serpapi";
const apiKey = env["SERP_KEY"]
const question = "什么是 github"
const loader = new SerpAPILoader({ q: question, apiKey });
const docs = await loader.load();
console.log(docs);
申请 Search Key
输出内容如下:
[
Document {
pageContent: '{"title":"GitHub","type":"Software company","entity_type":"companies, company","kgmid":"/m/0ryppmg",'... 11896 more characters,
metadata: { source: "SerpAPI", responseType: "knowledge_graph" }
},
Document {
pageContent: '{"position":1,"title":"GitHub - 维基百科,自由的百科全书","link":"https://zh.wikipedia.org/zh-hans/GitHub","redi'... 767 more characters,
metadata: { source: "SerpAPI", responseType: "organic_results" }
},
Document {
pageContent: '{"position":2,"title":"Github_百度百科","link":"https://baike.baidu.com/item/Github/10145341","redirect_'... 548 more characters,
metadata: { source: "SerpAPI", responseType: "organic_results" }
},
Document {
pageContent: '{"position":3,"title":"Github是什么?看完你就了解一些了","link":"https://www.cnblogs.com/jiqing9006/p/5584848.htm'... 564 more characters,
metadata: { source: "SerpAPI", responseType: "organic_results" }
},
...
]