DoppelBot:用 LLM 模拟企业 CEO 的概念验证
Modal 发布的实验性项目,用 LLM 模拟 CEO 决策过程;热度高但更多是概念演示而非生产工具。
Modal 发布的实验性项目,用 LLM 模拟 CEO 决策过程;热度高但更多是概念演示而非生产工具。
(快速链接:添加到你自己的 Slack;源代码)
在 Modal 内部,我们花了很多时间在 Slack 上互相交流。如今,随着开源大语言模型的出现,我们开始怀疑,这些交流是不是显得有点多余了。能不能让这些语言模型替我们在 Slack 上为琐碎的细节争论不休,好让我们把时间花在杠杆率更高的活动上,比如去塔希提岛玩桨板?
为了验证这个想法,我们使用 Erik 的 Slack 消息对 Llama 3.1 进行了微调,于是 @erik-bot 诞生了。
从那以后,@erik-bot 成了我们不可或缺的宝贵资产,在 API 设计、法律建议乃至思想领导力等领域都发挥了重要作用。
我们原本计划向全世界发布 @erik-bot 的权重,但自从上线它之后,我们的各项指标都有点过于一路向右上方增长了……
所以,我们决定发布退而求其次的最佳方案。DoppelBot 是一个 Slack bot,你可以把它安装到自己的 workspace 中,并使用自己的 Slack 消息对它进行微调。按照这里的说明操作,今天就用一个 LLM 替换掉你自己的 CEO。
所有组件——抓取、微调、推理以及 Slack 事件处理程序——都运行在 Modal 上,代码本身也是开源的,可以在这里获取。如果你刚接触 Modal,值得再次强调的是:所有这些组件同样都是 serverless 的,并且可以缩容到零。这意味着你可以在部署之后就不再操心,因为只有当应用实际被使用时,你才需要为计算资源付费!
DoppelBot 使用 Slack SDK 从 Slack workspace 中抓取消息,并将它们转换成 prompt/response 对。随后,它使用这些数据,通过 Low-Rank Adaptation(LoRA)对语言模型进行微调。LoRA 不会修改基础模型中的所有参数,而是生成一个体积较小的 adapter,需要时可以将其与基础模型合并。为每位用户微调得到的 adapter 都存储在 Modal Volume 中。当用户 @ bot 时,Slack 会向 Modal 发送 webhook 调用,Modal 随即加载该用户对应的 adapter 并生成回复。
下面我们会详细介绍其中的每个步骤,并提供分别运行各个步骤的命令。如果你想跟着操作,请 clone 这个 repo,并为自己配置一个 Slack token。
抓取程序使用 Modal 的 .map(),并行获取所有公开 channel 中的消息。每个 thread 都会被拆分成两类连续消息:目标用户发送的连续消息,以及其他用户发送的连续消息。它们会按照以下格式,作为 prompt 输入模型:
[system]: You are {user}, employee at a fast-growing startup. Below is an input conversation that takes place in the company's internal Slack. Write a response that appropriately continues the conversation.
[user]: <slack thread>
[assistant]: <target user's response>
模型的早期版本很容易生成简短回复——这并不令人意外,因为 Slack 上的大多数交流本来就相当简洁。为目标用户的消息设置最小字符长度后,这个问题得到了解决。
如果你也在跟着操作,可以使用以下命令运行抓取程序:
modal run -m src.scrape::scrape --user="<user>"
抓取结果会存储在 Modal Volume 中,以便下一步使用。
接下来,我们使用这些 prompt 对语言模型进行微调。我们选择 Llama 3.1,是因为它的许可证较为宽松,而且相对于较小的模型体积而言,质量很高。微调使用 Low-Rank Adaptation(LoRA)完成。这是一种参数高效的微调技术,会生成一个体积较小的 adapter,需要时可以将其与基础模型合并(按照我们使用的 rank,大小约为 60MB)。
我们的微调实现使用了 torchtune,这是一个新的 PyTorch 库,可以轻松配置微调任务。
由于我们面对的样本量通常很小,训练超过几百个 step(batch size 为 128)后,很快就会出现过拟合。坦率地说,我们还没有全面评估过整个超参数空间——如果你有兴趣和我们一起研究,欢迎联系我们!
要亲自尝试这一步,请运行:
modal run -m src.finetune --user="<user>"
我们使用 vLLM 作为推理引擎,它现在已经原生支持动态切换 LoRA adapter。
借助参数化函数,每个用户模型都会拥有自己独立的容器池:收到请求时自动扩容,没有请求时缩容到 0。去掉非必要细节后,代码大致如下:
@app.cls(gpu="L40S")
class Model():
@modal.enter()
def enter(self):
self.engine = AsyncLLMEngine.from_engine_args(AsyncEngineArgs(...))
self.loras: dict[str, int] = dict() # per replica LoRA identifier
@modal.method()
def generate(self, input: str):
if (ident := f"{user}-{team_id}") not in self.loras:
self.loras[ident] = len(self.loras) + 1
lora_request = LoRARequest(
ident, self.loras[ident], lora_local_path=checkpoint_path
)
tokenizer = await self.engine.get_tokenizer(lora_request=lora_request)
prompt = tokenizer.apply_chat_template(
conversation=inpt, tokenize=False, add_generation_prompt=True
)
results_generator = self.engine.generate(prompt, lora_request=lora_request,)
如果你已经在上一步微调好了模型,现在就可以使用它运行推理:
modal run -m src.inference --user="<user>"
(我们在文件中准备了一组示例输入,不过你也可以用自己的消息试试!)
最后,所有部分都会在 bot.py 中组合起来。你可能已经猜到了,来自 Slack 的所有事件都由 serverless Modal 函数处理。我们会处理三种事件:
url_verification:为了验证这是一个 Slack app,Slack 要求我们返回一个 challenge string。
app_mention:当 bot 在 channel 中被提及时,我们会获取该 thread 最近的消息,做一些基本清理,然后调用对应用户的模型生成回复。
model = OpenLlamaModel.remote(user, team_id)
result = model.generate(messages)
doppel slash command:这条命令会为用户启动“抓取 -> 微调”流水线。
要完整部署这个 Slack bot,你需要运行:
modal deploy -m src.bot
到目前为止,我们讨论的都是单 workspace Slack app。要让它支持多个 workspace,就需要使用 OAuth 处理 workspace 的安装和身份验证,同时还要为每个 workspace 存储一些状态。
幸运的是,Slack 的 Bolt framework 提供了一套完整的 OAuth 实现,只是文档写得颇为节省篇幅。它有一个很实用的特性:OAuth 状态可以由文件系统提供存储。因此,我们只需让 Bolt 指向一个 Modal Volume,就不必再操心如何自行管理这些状态。
为了存储每个 workspace 的状态,我们使用了 Neon。它是一个 serverless Postgres 数据库,配置非常简单,而且开箱即用。如果你有兴趣开发多 workspace app,可以按照我们的说明,了解如何搭配 Modal 配置 Neon。
如果你已经读到了这里,那就意味着你刚刚找到了一种让团队生产力提升 10 倍的方法!好好享受这场应得的假期吧!🎉
如果你想进一步了解 Modal,可以查看我们的文档和其他示例。