你好,我是徐昊,今天我们来继续学习 AI 时代的软件工程。
上节课我们讲解了如何使用大语言模型(Large Language Model,LLM)稳定地构造基于语义的自动化脚本,并介绍了三种今日常用的技巧。
今天我们要讨论一个更为前沿(cutting edge)的问题,在大语言模型时代(Large Language Model,LLM),框架与工具将何去何从?
知识复用还是载体复用?
你的第一个疑问可能是,为什么大语言模型会影响框架与工具?要知道,在大语言模型出现之前,我们复用知识的主要形式就是框架和工具。 无论你是使用 ReactJS,还是 Spring Boot 编写前后端应用,实际上你复用了凝聚在这些框架中的知识。
所以使用框架和工具的过程,也是一个知识过程。你还会发现,当你能够熟练应用框架时,你基本上处于庞杂认知行为模式(Complicated),即按照框架提供的思路解决具体问题。而使用工具,或者工具性质的库,则更接近于清晰认知行为模式(Clear),即按照问题选取对应工具。
那么我们不禁要问一个问题,如果我掌握了凝聚在框架和工具中的知识,我是否还需要这些框架和工具?在 LLM 出现之前,答案是肯定的,因为我们无法直接复用知识,我们只能通过知识的载体——软件——对这些知识进行复用。
然而今天,LLM 带来了更高效的知识复用方式,它使得我们可以直接复用知识。
让我们看一个简单的例子,假设我们现在使用 JavaScript 在开发前端应用,我们会经常用到一些小的工具方法,当然有很多库都为我们提供了封装好的工具方法,我们之前可以通过这些库来复用这些功能,但是现在我们有了另一个选择,就是让 LLM 为我们构造这些工具方法。
比如我们想要 escape 字符串中的特殊字符,我们当然可以使用诸如 underscore.js 之类的库,通过 _.escape 实现这个功能。
现在让我们对比一下,直接复用知识和复用载体的两种做法。直接复用知识,我们可以使用下面的提示词:
处理输入的文字,escape掉字符串中的所有的特殊字符,&, <, >, ", `, 和 '。用js编写
function escapeSpecialChars(str) {
return str.replace(/[&<>"'`]/g, function(match) {
switch(match) {
case '&':
return '&';
case '<':
return '<';
case '>':
return '>';
case '"':
return '"';
case '\'':
return '''; // ' is not recommended
case '`':
return '`'; // ` is not recommended
}
});
}
// 示例用法
var originalString = 'This is a <b>bold</b> statement & a "quote"';
var escapedString = escapeSpecialChars(originalString);
console.log(escapedString);
复用载体,我们可以使用这样的提示词:
处理输入的文字,escape掉字符串中的所有的特殊字符,&, <, >, ", `, 和 ', 用js编写,并使用underscore.js
从实现上来看二者没有什么差别,一个自己写了实现,另一个调用了 underscore.js 中对应的函数。但如果比较提示词的话,我们会发现,复用载体的方法对我们有更大的认知负载。它需要我们知道存在一个 underscore.js 库,并且知道在这个库中存在一个可以 escape 特殊字符的函数。而从结果上来看,知不知道这个额外的知识,并不构成显著的差异。换句话说,我们付出了额外的认知负载,但并没有得到额外的收益。
那么我们能在多大程度上直接复用知识呢?让我们看一下我们这个课程中可以用到的例子,我们前面讲过,对于提示词的模板化(prompt templating)是非常重要的技巧,但是 OpenAI 默认并不支持提示词模板。不过几乎所有使用 LLM 的框架,都支持提示词模板。
其实,我们也不需要任何的框架,只要我们明白我们所需要做的事情,很容易就能通过 LLM 实现对应的功能。
编写一个调用openai chat api的js方法。这个方法接受prompt做为输入。prompt支持参数化。
async function callOpenAIChatAPI(prompt) {
const apiKey = 'YOUR_API_KEY'; // 替换为你的 OpenAI API 密钥
const response = await fetch('https://api.openai.com/v1/engines/text-davinci-003/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
prompt: prompt,
max_tokens: 150,
temperature: 0.7,
}),
});
const data = await response.json();
if (data && data.choices && data.choices.length > 0) {
return data.choices[0].text.trim();
} else {
throw new Error('Failed to generate response from OpenAI Chat API.');
}
}
// Example usage:
const prompt = "你好,我的名字是{{ name }}。我今年{{ age }}岁。";
const parameters = { name: "小明", age: 25 };
const formattedPrompt = prompt.replace(/{{\s*(\w+)\s*}}/g, (match, p1) => parameters[p1]);
callOpenAIChatAPI(formattedPrompt)
.then(response => {
console.log("Response:", response);
}).catch(error => {
console.error("Error:", error);
});
可以看到,这虽然算不上一个结构良好的框架,但是基本功能都在,我们甚至可以在这个基础上继续扩展功能。比如对于更复杂的 prompt 应用,我们需要构成 chain 或者 flow。那么我们可以这么写:
现在我有一组prompt希望组成一个chain。每一个prompt执行之后,给一个指定function处理openai的返回结果。然后作为参数,传递给下一个prompt
async function processPromptsInChain(prompts, handleResponseFunction) {
if (prompts.length === 0) {
return; // 如果没有剩余的 prompt,结束递归
}
const currentPrompt = prompts[0];
const formattedPrompt = formatPrompt(currentPrompt); // 格式化 prompt,如果有必要的话
try {
const response = await callOpenAIChatAPI(formattedPrompt); // 调用 OpenAI API
// 调用指定的处理函数处理返回的结果
const processedResponse = handleResponseFunction(response);
// 继续处理剩余的 prompt
await processPromptsInChain(prompts.slice(1), handleResponseFunction);
} catch (error) {
console.error("Error processing prompt:", error);
// 可能需要添加适当的错误处理逻辑
}
}
// 格式化 prompt,替换参数
function formatPrompt(prompt, parameters) {
if (parameters) {
return prompt.replace(/{{\s*(\w+)\s*}}/g, (match, p1) => parameters[p1]);
} else {
return prompt;
}
}
// 调用 OpenAI API
async function callOpenAIChatAPI(prompt) {
const apiKey = 'YOUR_API_KEY'; // 替换为你的 OpenAI API 密钥
const response = await fetch('https://api.openai.com/v1/engines/text-davinci-003/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
prompt: prompt,
max_tokens: 150,
temperature: 0.7,
}),
});
const data = await response.json();
if (data && data.choices && data.choices.length > 0) {
return data.choices[0].text.trim();
} else {
throw new Error('Failed to generate response from OpenAI Chat API.');
}
}
// 处理返回结果的示例函数
function handleResponse(response) {
// 在这里可以处理 OpenAI 返回的结果,例如日志记录、分析等
console.log("Received response from OpenAI:", response);
// 这里可以返回处理后的结果,如果不需要,则返回原始结果
return response;
}
// 示例用法:
const prompts = [
"你好,我的名字是{{ name }}。我今年{{ age }}岁。",
"我来自{{ city }}。",
"我的职业是{{ occupation }}。",
];
const parameters = { name: "小明", age: 25, city: "北京", occupation: "工程师" };
processPromptsInChain(prompts, handleResponse)
.then(() => {
console.log("All prompts processed successfully.");
}).catch(error => {
console.error("Error processing prompts:", error);
});
可工作的知识(Working Knowledge)
上面的例子几乎就是一个 LangChain 最初版的极简版本。我在提出需求的时候,脑子里的确也是在以 LangChain 作为对照参考的。我们只需要大概了解某个框架的核心模式,那么借助 LLM 就不难构造一个简易版本。
在过去的三十年里,我们非常强调可工作的软件(Working Software),敏捷方法将可工作的软件作为度量进度的唯一标准。这是因为我们需要一个可执行的载体,来验证知识传递的效果。而今天,在大模型的帮助下,提炼出的知识几乎可以直接工作。我们是否还需要强调可工作的软件?还是应该直接关注可工作的知识?
这并不是一个容易回答的问题。简单来说就是太早了,目前仍难有定论。
支持这种做法的人主要是从 LLM 使用效率出发,来考虑这个问题的。
想让 LLM 使用任何框架或工具,要么就是将该框架的文档资料汇入大模型的基础语料当中(作为预训练模型,或者 fine tuning 模型),要么就是通过上下文、少样本学习(few shots examples)和思维链(Chain of Thought,CoT)让大模型掌握应用框架或工具的诀窍。
如果 LLM 基础语料中关于模型的知识已经落伍(Out of date),那么 LLM 就无法生成有效的代码。比如,ChatGPT 采用了某框架的 1.0 版本,但是目前已经是 2.0,并存在破坏性改变(Breaking change)。那么大模型生成代码的效率就大大降低,还会大大增加使用 LLM 的认知成本。与其这样还不如让大模型从头写,减少对于框架的依赖。
对于自己提炼、构造的框架也存在同样的问题。提炼框架需要花时间花成本,训练大模型或是接入大模型还需要额外的时间与成本。从 LLM 使用效率的角度上说,还需要自己提取框架吗?把核心概念和模式说清楚是不是就够了?
从 LLM 使用效率的角度出发,可工作的知识的确比可工作的软件具有更高的效率。
而反对这种做法的,主要是基于现实情况的考量。除了核心模式之外,框架和工具中还包含了 1000 个细节。就以 LangChain 为例,构造基本的 Prompt Chain 并不困难,但是错误处理、超时、异步等等为了让系统更健壮、更稳定或更有效率的处理,想完全靠 LLM 重现,认知负载并不低。这样还不如直接使用框架或工具。
另外还有一个关于智力资产保护的问题,今天的知识版权保护法是建立在对于知识的特定呈现(representation)上的,知识产权保护法并不保护一个想法。这一前提的假设就是,孤立的、提炼的知识是没用的。现在这个情况改变了,孤立的、提炼的知识可以是有用的。
那么试想一下,只要去听了一个产品的发布会,他们讲解了解决问题的核心思路,我们就不难通过 LLM 复制这个产品的主体框架,而不用担心任何对于智力资产的侵犯。那么可工作的知识还会为我们带来一个繁荣和开放的社区吗?
小结
过去我们关注于可工作软件,因为除此之外我们难以直接应用知识。但现在对比一下直接复用知识和复用载体的两种做法,就会发现有些情况下,大模型帮我们提炼出的知识几乎可以直接工作。
可工作的知识(Working Knowledge)是一种非常有吸引力的知识载体,但是目前仍然还是太早了,我们无法作出有效的判断。
目前我们能知道的是,在未来一段时间,我们评估任何框架、工具和产品,都需要考虑 LLM 友好性(LLM Friendly)。
思考题
请将一个你自己构造的框架,变得对 LLM 更友好。
欢迎在留言区分享你的想法,我会让编辑置顶一些优质回答供大家学习讨论。