Prompt炼丹炉——一系列Prompt自动优化的实践记录
原由
随着对ChatGPT的使用深入和各种杂七杂八的论文阅读,我得到了一个很强烈的认知:让普通人直接与ChatGPT对话以期得到良好结果是过分乐观到大可不必的。所以,一定会出现足够多的,以openai为基础,以广义prompt为核心竞争力的垂类商业公司,且赚的盆满钵满。
由此继续推出:Prompt炼丹这件事是需要很好的自动化的。当然,各大厂也都在用小模型训练+大模型反馈的方式做了很多自动化的尝试。但是对于个人/小公司来说,这样的方式是承担不起的(主要成本在于自研小模型及其算法)。那openai的贫民赋能就体现出来了,可以直接用openai搞!
免责声明:本文只说思路,因为所有内容都是利用公司的资产实验出来的,所有细节法理上是公司产权。
Take 0,单prompt直接对话
这种有很多,大概思路有两个:
- 让ChatGPT假装prompt工程师,输入任务描述,ChatGPT给出完整的prompt。
- 让ChatGPT假装prompt工程师,输入老prompt,ChatGPT根据老prompt给出新的prompt。这时候通常只是改改语法,加一些例子。
这两者都是严重依赖对话的,且没有prompt泛化能力。是个人用户快速优化prompt的手段,而且第一个方式利用多轮对话和持续的信息补充效果是优于第二种的。
Take 1,利用训练集直接对话
思路很简单,把输入和预期输出给ChatGPT,让他直接根据这两者进行提示词优化,不做迭代过程。
问题很大,因为ChatGPT从原理上并不是语义理解的而是网络激活,预期会把对应网络直接激活,生成的prompt只有在这个对话中产生准确内容(COT的效果,与prompt无关)并不能真的用。
Take 2,利用pseudo-code进行训练
之前搞了一套基底prompt能够在ChatGPT中执行java-like伪代码,计划五一期间开源。
在这个基底上,利用极简单的if-else+prompt+for-loop试图跑一套自动化的迭代。分别试了:
- 无训练数据:完全不能用。自然语言的空间过于复杂,ChatGPT纯粹在盲猜,结果就像无头苍蝇一样,全是垃圾。
- 有训练数据,不清context:完全不能用。与take 1的问题一样,在COT的魔法下,第一次迭代的生成就会因为相关网络被激活直接返回完全正确的结果,但是prompt并不能用。
- 有训练数据,清context:完全不能用。由于清context是以子prompt的形式发生在for-loop中,整个运行时激活的网络都会被清掉,伪码执行器直接没了。
以下都开始进入到langchain训练阶段。迭代内容都会加上相似度得分的反馈。整体思路就是用langchain+ChatGPT在文字空间上做梯度下降。
Take 3,langchain 单prompt迭代
单次迭代过程只有一个prompt和单个数据的训练“集”,试过几个思路:
- 不给训练集内容,给history,直接让ChatGPT盲猜:完全不能用。相较于Take 2,这次给了相似度得分(目标),本以为会好一些,整体仍然是无头苍蝇。
- 给训练集内容,给history:不能用。一方面是由于单次迭代仍然是一个上下文,还是会有COT问题;另一方面history+训练集的内容很容易在迭代一两次之后就超过token限制了(这里主要是langchain的例子里,history是以HumanMessage内容的形式给过去的,而不是AIMessage)。不过由于明显有问题,未继续迭代。
以下开始进入到多条数据的训练集上。
Take 4, langchain 单prompt迭代
与 Take 3区别只有训练集个数增加,希望用数据打破过拟合。也试了给history和不给history,结果都不理想,突出的问题是单prompt承载的需求太多,明显ChatGPT不能“理解”prompt了。
以下使用新prompt在训练数据上进行生成的工作都使用独立的context执行。
Take 5,langchain 多prompt迭代
下面是优化的各种维度以及成效,优化是叠加的不是互斥的。
- target与history合并,不再独立给target和history。降低数据关联需要的“理解”成本,区别不大,优化用的prompt还是太复杂。
- 去掉target。有些效果,只给history似乎比都给要好一些。不能解释。
- 分数加上对长度的考虑。有效果,由于(据我所知)长度在embedding上没啥特别强的体现,之前的优化会让ChatGPT试图让生成的内容越来越长,但是这个在很多场景下是负优化。加上长度后,分数会更好的体现这个目标。
整体看,单个prompt还是太过复杂,且梯度下降的导数仍然不明确。
Take 6,langchain 多prompt + diff迭代
下文会有三个prompt:
- 优化prompt的prompt,简称优化prompt
- 被优化的prompt,简称目标prompt
- 计算差异的prompt,简称差异prompt
抽象来看,语言空间上的导数,其实就是语义化的差异解释。本文除了排除各种尝试的死胡同之外最有价值的一句话。下面是优化的各种维度以及成效,优化是叠加的不是互斥的。
- 再加一次调用,使用差异prompt让ChatGPT给出本次迭代中target和result的差异点。把差异点作为优化prompt的输入。非常有效果,明显可以看到优化开始有方向感了,给了一个模糊的导数,从这里开始有质变。
- 去掉history。非常有效果,干扰信息大幅减少,优化的理由更加聚焦了,又一次质变。而且由于优化prompt的长度与迭代轮次无关了,迭代轮次可以无限继续,也是工程角度的质变。
- 优化差异prompt,让diff更关注表达方式而不是表达内容。非常有效果,明显把过拟合的现象打掉了,结果会相对更general。
- 优化优化prompt,明确说让优化增强什么削弱什么。有效果,方向感更强了,类似于梯度下降的梯度函数的权重。
这次尝试的代码是基本可用的。两个训练数据的训练集的,简单输入输出的,相对简单目标prompt的情况下,9轮迭代可以得到语义上与目标prompt一致的结果。相似度+长度得分从0.59 涨到0.85。整个训练只需要5分钟左右时间。
其他观察
- Prompt优化用小数量的语言空间的梯度下降过拟合风险不大。因为最后的产出是prompt,且一定是多句、多约束类型的组合,人可以非常简单的判断出过拟合部分,进而直接删掉。不存在有过拟合而不自知的情况。
- 大模型做垂类任务的可解释性可能可以利用类似的方式解决掉一部分。不可解释的从完整流程变成prompt+模型中的模型部分。
- 上下文比臆想中的重要性还高很多,操纵上下文会是很多优化的重点。
- 控制输入量比很多优化都有价值,要做好O(1)的输入量,任何O(n)的输入量对迭代来讲都是毁灭性的。
- ChatGPT的prompt一定要是单任务的,多任务一定出幺蛾子。
- 各种不同模型的兼容最核心的是尽量把prompt拆碎,大的模型就多个prompt拼一起做复杂任务,小的模型就一个一个执行靠外部代码串联起来。