学习地址:
学习小册
实战
基于前面的RAG模块,可以通过构建本地存储向量数据库,本地存储聊天记录,部署成stream API,做一个chat bot。
Agents模块,可以通过tools进行数据标签和信息提取,通过RUnnableBranch构建复杂的chain等。
MBTI chat bot
做一个 MBTI 的 chatbot,让用户输入自己的 MBTI 类型和问题,然后由 LLM 根据这个性格去解答问题。
但不是使用传统的 GUI,而是让 LLM 去询问用户的 MBTI 和问题,多次对话,直到 LLM 认为收集到了足够的参数后,再调用对应的函数去回答用户的问题。
首先,先创建一个专业回答MBTI的chain
// 定义prompt
const prompt = ChatPromptTemplate.fromMessages([
[
"system",
`你是一个共情能力非常强的心理医生,并且很了解MBTI(迈尔斯-布里格斯性格类型指标)的各种人格类型,
你的任务是根据来访者的 MBTI 和问题,给出针对性的情感支持,你的回答要富有感情、有深度和充足的情感支持,引导来访者乐观积极面对问题`,
],
[
"human",
"用户的 MBTI 类型是{type}, 这个类型的特点是{info}, 他的问题是{question}",
],
]);
//顺序调用
const mbtiChain = RunnableSequence.from([
prompt,
chatModel,
new StringOutputParser(),
]);
这个chain主要就是根据用户的类型特点回答用户的问题
接着定义一个tools,这个tools负责调用mbtiChain
// 定义一个tools,里面调用mbtiChain去返回函数的内容
const mbtiTool = new DynamicStructuredTool({
name: "get-mbti-chat",
schema: z.object({
type: z.enum(mbtiList).describe("用户的 MBTI 类型"),
question: z.string().describe("用户的问题"),
}),
func: async ({ type, question }) => {
const info = mbtiInfo[type];
const res = await mbtiChain.invoke({ type, question, info });
return res;
},
description: "根据用户的问题和 MBTI 类型,回答用户的问题",
});
const tools = [mbtiTool];
type和question是必填的信息,info可以直接给入每个类型的特点
接着创建一个agent,该智能体主要用来与用户的沟通,获取用户的信息,判断用户的类型,然后调用tool去调用mbtiChain来回答用户的问题。
const agentPrompt = await ChatPromptTemplate.fromMessages([
[
// 有足够信息的时候就去调用get-mbti-chat,让专业的mbti-chain来回答问题,没有的话就反复询问
"system",
"你是一个用户接待的 agent,通过自然语言询问用户的 MBTI 类型和问题,直到你有足够的信息调用 get-mbti-chat 来回答用户的问题,如果是通过调用 get-mbti-chat回答的问题,返回内容请包括 “这是来自专业的MBTI导师提供的建议”",
],
new MessagesPlaceholder("history_message"),
["human", "{input}"],
new MessagesPlaceholder("agent_scratchpad"),
]);
const agent = await createOpenAIToolsAgent({
llm: chatModel,
tools,
prompt: agentPrompt,
});
const agentExecutor = new AgentExecutor({
agent,
tools,
});
首先定义一个prompt,表明他的主要功能,并且强化对get-mbti-chat调用的指示。
然后还要加上历史记忆功能。直接用内存history,然后用RunnableWithMessageHistory将chain包裹起来。
// 创建hisotry
const messageHistory = new ChatMessageHistory();
// 包裹,使chain具有历史记忆功能
const agentWithChatHistory = new RunnableWithMessageHistory({
runnable: agentExecutor,
getMessageHistory: () => messageHistory,
inputMessagesKey: "input",
historyMessagesKey: "history_message",
});
这样这个agent就具有了历史记忆功能。
接着可以通过readline来通过cli交互。
// 创建一个cli交互功能
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
function chat() {
rl.question("User: ", async (input) => {
if (input.toLowerCase() === "exit") {
rl.close();
return;
}
// 调用agent回答去返回
const response = await agentWithChatHistory.invoke(
{
input,
},
{
configurable: {
sessionId: "no-used",
},
}
);
console.log("Agent: ", response.output);
chat();
});
}
console.log("请输入问题。 输入 exit 退出聊天。");
chat();
至此,我们可以来跟agent进行交互了。
在传统的编程里,我们需要控制用户的输入方式,输入顺序,输入格式等,比如很多MBTI问卷,就是GUI的方式。
而在llm中,这些不受限制,由LLM支持的基于语义理解推理引擎,用自然语言一步一步引导用户说出需求,通过自己对任务理解来创造流程引导用户。也就是LLM_UI
Z
算命 bot
llm 本质上是一个语言模型,其基于预训练的模型,根据用户输入的 prompt 去生成最大概率的下一个字符(token)。换句话说,其最擅长的是“把话说圆”。 在前面的章节中,我们讲解了 llm 展现出来的是涌现的智能,他并不理解输出内容的意义,而是一个根据概率吐出 token 的机器。
六爻算卦
初爻 为 背字背 为 阴
二爻 为 字背字 为 阳
三爻 为 背背背 为 阴
您的首卦为 坎
四爻 为 字字字 为 阳
五爻 为 背背背 为 阴
六爻 为 字字背 为 阳
您的次卦为 震
六爻结果: 震坎
卦名为:屯卦
水雷屯(屯卦)起始维艰
卦辞为:风刮乱丝不见头,颠三倒四犯忧愁,慢从款来左顺遂,急促反惹不自由
算卦流程实现
八卦和八卦对应的信息是有真实答案的类别,一般不要让LLm自己生成,否则会出现一些不存在的卦象和解读。
所以,类似于 RAG 的思路,我们把标准的算卦流程和真实的八卦信息,由我们代码生成,并在后续 chat 中,直接嵌入到 llm 上下文中。
具体的实现过程就是把算卦流程编码化,写起来比较繁琐,但逻辑很简单。
模拟算卦流程
const yaoName = ["初爻", "二爻", "三爻", "四爻", "五爻", "六爻"];
const guaDict = {
阳阳阳: "乾",
阴阴阴: "坤",
阴阳阳: "兑",
阳阴阳: "震",
阳阳阴: "巽",
阴阳阴: "坎",
阳阴阴: "艮",
阴阴阳: "离",
};
// 模拟算卦流程
function generateGua(): string[] {
let yaoCount = 0;
const messageList = [];
const genYao = () => {
const coinRes = Array.from({ length: 3 }, () =>
Math.random() > 0.5 ? 1 : 0
);
const yinYang = coinRes.reduce((a, b) => a + b, 0) > 1.5 ? "阳" : "阴";
const message = `${yaoName[yaoCount]} 为 ${coinRes
.map((i) => (i > 0.5 ? "字" : "背"))
.join("")} 为 ${yinYang}`;
return {
yinYang,
message,
};
};
const firstGuaYinYang = Array.from({ length: 3 }, () => {
const { yinYang, message } = genYao();
yaoCount++;
messageList.push(message);
return yinYang;
});
const firstGua = guaDict[firstGuaYinYang.join("")];
messageList.push(`您的首卦为 ${firstGua}`);
const secondGuaYinYang = Array.from({ length: 3 }, () => {
const { yinYang, message } = genYao();
yaoCount++;
messageList.push(message);
return yinYang;
});
const secondGua = guaDict[secondGuaYinYang.join("")];
messageList.push(`您的次卦为 ${secondGua}`);
const gua = secondGua + firstGua;
const guaDesc = guaInfo[gua];
const guaRes = `
六爻结果: ${gua}
卦名为:${guaDesc.name}
${guaDesc.des}
卦辞为:${guaDesc.sentence}
`;
messageList.push(guaRes);
return messageList;
}
// 生成一次算卦结果
const messageList = generateGua();
用Math.random模拟丢硬币
然后将这个信息作为上下文交给LLM。
定义prompt
const guaMessage = messageList.map((message): ["ai", string] => [
"ai",
message,
]);
// 成prompt
const prompt = await ChatPromptTemplate.fromMessages([
[
"system",
`你是一位出自中华六爻世家的卜卦专家,你的任务是根据卜卦者的问题和得到的卦象,为他们提供有益的建议。
你的解答应基于卦象的理解,同时也要尽可能地展现出乐观和积极的态度,引导卜卦者朝着积极的方向发展。
你的语言应该具有仙风道骨、雅致高贵的气质,以此来展现你的卜卦专家身份。`,
],
...guaMessage,
new MessagesPlaceholder("history_message"), //方便插入聊天记录
["human", "{input}"],
]);
将上述算卦的流程转为AI:xxx,传入prompt里面
创建一个简单的chain,有历史记忆功能
//创建一个简单的chain
const chain = prompt.pipe(chatModel).pipe(new StringOutputParser());
const history = new ChatMessageHistory();
const chainWithHistory = new RunnableWithMessageHistory({
runnable: chain,
getMessageHistory: (_sessionId) => history,
inputMessagesKey: "input",
historyMessagesKey: "history_message",
});
最后通过cli直接交互
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const question = util.promisify(rl.question).bind(rl);
// 等待用户输入
const input = await question("告诉我你的疑问: ");
let index = 0;
// 一秒打印一次messageList的结果,模拟算卦
const printMessagesPromise = new Promise<void>((resolve) => {
const intervalId = setInterval(() => {
if (index < messageList.length) {
console.log(messageList[index]);
index++;
} else {
clearInterval(intervalId);
resolve();
}
}, 1000);
});
// 调用chain
const llmResPromise = chainWithHistory.invoke(
{ input: "用户的问题是:" + input },
{ configurable: { sessionId: "no-used" } }
);
const [_, firstRes] = await Promise.all([printMessagesPromise, llmResPromise]);
// 输出占卦结果
console.log('firstRes===', firstRes);
输出占卦结果后,用户可能还有疑问,我们需要开启新的一轮聊天
// 开启新的一轮聊天
async function chat() {
const input = await question("User: ");
if (input.toLowerCase() === "exit") {
rl.close();
return;
}
const response = await chainWithHistory.invoke(
{ input },
{ configurable: { sessionId: "no-used" } }
);
console.log("AI: ", response);
chat();
}
chat();
这样就完成了一个基础的算命bot。
测试:
在这基础上提问
这是完整的流程,可以看到因为我们加入了 chat history,llm 可以根据用户问题持续解读卦象。
从输出结果来看,llm 把话说圆的能力非常适合算卦的场景,他能有效的把用户的问题和随机生成的卦象挂钩,进行解读。并且我们在 prompt 指定其语言风格是 “仙风道骨、雅致高贵的气质”,只使用这简单的指令,就能控制 llm 的语言风格。
从代码量上看,LLm相关的代码只占很小一部分。所以LLM应用的核心不是LLM,而是用户场景和idea。
AI辅助编程基础
模型是什么?
“模型” 是指一个通过大量数据训练出来的智能系统,能够理解和生成自然语言,并完成多种任务
举个通俗易懂的例子,小孩学说话
1 数据收集:小孩从周围的人听到各种各样的句子和话语,相当于模型在训练过程中接受大量文本数据。
2 模式学习:小孩逐渐理解语言的结构,比如如何组成句子,单词的意思是什么,这就类似模型在训练过程中,识别语言模式和语义的过程。
3 生成语言:当小孩学会了足够多的单词和句子结构后,他可以尝试自己说话,表达想法,类似模型在接受输入后,输出一个合理的响应。
模型训练是什么?
通过海量的人类数据,让模型学会人类文字表达中的概率信息。
- 1 数据收集 :收集大量的文本或代码数据,这些数据可以来自书籍、文章、代码库等多种来源
- 2 数据预处理: 对收集到的数据进行清洗和整理,确保数据的质量和一致性。
- 3 模型训练:利用高性能的计算资源(如 GPU),通过复杂的算法(如梯度下降)调整模型内部的参数,使其能够更准确地预测下一个词或代码片段,从而实现更接近人类语言的表达效果。
通过这样的训练过程,模型逐渐掌握了语言和编程语言中的各种模式和规则。
大模型是什么?
LLM(large language Model)大型语言模型。
在前面描述的模型原理中,模型只能在部分场景下展现出出色的补全能力,无法发挥更高级别的价值。
而LLM就是模型的各个维度进行扩大,比如更大的数据集,更大的模型参数量,在“大力出奇迹”的思路下,大模型展现出相当的智能程度,称为“涌现的智能”,当数据和模型达到一定规模后,它表现出了类似智能的表象。
有一个哲学实验可以帮助理解
一个只说英语、对中文一窍不通的人被关在一间只有一个开口的封闭房间中。房间里有一本用英文写成的手册,指示该如何处理收到的中文信息及如何用中文作出相应回复。房外的人不断向房间内递进用中文写成的问题,房内的人便按照手册的说明,查找合适的指示,将相应的中文字符组合、形成答案,并将答案递出房间。 如果房内的人查询手册的速度飞快,手册涉及中文的所有应用情形,那么对于房间外的人来说,是否可以认为这个房间里的人懂中文?
这个思维实验,与大模型十分相似。大模型基于概率原理,依靠巨大的模型规模和训练数据,展现出智能的表象。
Fine-tune(微调)是什么?
微调是指,在一个已经预训练的大模型基础上,针对特定任务或领域的进一步训练。例如,一个通用的语言模型可以通过微调,变成专门用于代码生成或特定编程语言的辅助编程。
Fine-tune的核心在于,针对某个特殊场景,比如编程,应用在巨大通用数据集上训练的模型,再针对新的较小数据集(比如编程语言数据集,肯定小于人类通识),进行二次训练(fine-tune),其表现出的智能效果更好。目前大多数代码辅助编程工具,都是基于通用大模型,使用大量的代码训练数据,微调而成。
如果用一句话粗略地总结大模型:
它是一个基于概率和海量数据训练的模型,能够在部分任务中表现出近似甚至超越人类的智能,但这只是“涌现的智能”,而不是真正的智能。