构筑大语言模型应用:应用开发与架构设计

2023 年的上半年里,我(@phodal)和 Thoughtworks 的同事们(如:@tianweiliu、@teobler、@mutoe 等)、 开源社区的同伴们(如: 卷王@CGQAQ、@genffy、 @liruifengv 等) 一起,创建了一系列的流行的或者不流行的开源项目。它们涉及了:

  • LLM 能力的充分运用
    • Prompt 编写:Prompt 学习与编写模式
    • Prompt 管理:Prompt 即代码
  • LLM 下的软件开发工序及应用架构设计
    • 新的交互设计:Chat模式
    • 大模型友好的工序:基于 AI 2.0 (ChatGPT + Copilot)如何去设计软件开发流程
    • LLM 应用架构的设计与落地:Unit Mesh
  • 面向特定场景的 LLM 应用
    • 基于开源模型构建自己的模型:特定场景的模型微调 + LLMOps
    • 上下文工程(prompt 工程):LLM 应用的核心

围绕于上述的一系列内容,我们也在思考软件开发能给我们带来了什么。所以,我重新整理了过去半年的一些思考、文章,重新编写了这本开源电子书,希望能够帮助到大家。

关注我的微信公众号(搜索 phodal-weixin),获得更多及时的更新:

微信公众号

加入微信群一起讨论

微信群

(如果微信群已满,请添加: phodal02 为好友,并注明:AIGC)

我们发起的相关开源项目如下(包括但是不限于):

名称描述类型Stars
理解 Prompt基于编程、绘画、写作的 AI 探索与总结。文档GitHub Repo stars
Prompt 编写模式如何将思维框架赋予机器,以设计模式的形式来思考 prompt。文档GitHub Repo stars
ClickPrompt用于一键轻松查看、分享和执行您的 Prompt。应用GitHub Repo stars
ChatVisualNovel基于 ChatGPT 的定制化视觉小说引擎应用GitHub Repo stars
ChatFlow打造个性化 ChatGPT 流程,构建自动化之路。框架GitHub Repo stars
Unit Mesh基于 AI 为核心的软件 2.0 思想的软件架构。架构GitHub Repo stars
Unit MinionsAI 研发提效研究:自己动手训练 LoRA微调教程、指南、数据集GitHub Repo stars
Unit Runtime一个 ChatGPT 等 AI 代码的运行环境,可一键启动并实时交互,帮助您快速构建和测试 AI 代码。基础设施GitHub Repo stars
DevTi基于 LLM 的微调来提供全面智能化解决方案,助力开发人员高效完成开发任务,以实现自动化用户任务拆解、用户故事生成、自动化代码生成、自动化测试生成等等。微调代码GitHub Repo stars
AutoDev一款 Intellij IDEA 的 LLM/AI 辅助编程插件。AutoDev 能够与您的需求管理系统(例如 Jira、Trello、Github Issue 等)直接对接。IDEA 插件GitHub Repo stars
ArchGuard Co-mate基于人工智能技术的架构副驾驶、设计和治理工具架构协同应用GitHub Repo stars

我们在 QCon 上的演讲:演讲:探索软件开发新工序:LLM 赋能研发效能提升

LLM(如 ChatGPT + GitHub Copilot)作为一种创新的工具组合,为我们带来了全新的机遇。它能够帮助业务人员和开发者在需求、架构、编码、测试等环节提高效率和质量,实现从设计到验证的端到端流程。在本次分享中,我将向大家介绍 LLM 在研发效能方面的应用场景和实践案例,展示它是如何在各个环节中发挥作用的。同时,我们还将分享如何构建私有化的 LLM 工程化方式,使其更好地适应组织的需求。欢迎对 LLM + 研发效能感兴趣的朋友们参加本次分享,与我们一起探讨研发效能的未来。

我们在 Bilibili 上的大语言模型微调相关的视频:

欢迎大家一起来参与我们的开源项目,一起来探索 LLM + 软件开发的未来。

Contributors

Phodal Huang

GitHub:https://github.com/phodal

ChatGPT 编写的趣味十足的介绍:

让我们介绍一下黄峰达,这位技术巨匠比大多数凡人都会写代码。他是 Thoughtworks China 的开源领袖,但我们更喜欢称他为 IDE、云开发、DevOps 和编译相关领域的绝地大师。他写了太多本技术书籍,我们数不过来了。他自称是个极客、创作者和作家,喜欢在 GitHub 上折腾开源软件。当他不忙着用代码改变世界时,你可以在他身边看到他在白日做梦建造机器人军团或者策划接管互联网的计划(开个玩笑,或者不是?)。 总之,黄峰达在科技界是个不可忽视的强大力量,也是一个很酷的家伙。

其它:

  • 开源电子书:《一步步搭建物联网系统》、《Growth: 全栈增长工程师指南》 、《微前端的那些事儿》等书
  • 出版纸质物:《自己动手设计物联网》、《全栈应用开发:精益实践》、《前端架构:从入门到微前端》
  • 合作翻译:《物联网实战指南》、《Arduino编程:实现梦想的工具和技术》

行长

GitHub:https://github.com/isixline

Copilot 努力给出的简介:

平平无奇,在软件领域还没有留下自己的著作,在软件工程的熏陶下却总想成为一个古典主义的程序员。 兴趣奇怪又广泛,想成为创作者也想成为艺术家,试图用代码改变世界,试图抵抗世界的改变。

其它:

  • 占个位,会有的

木格袋子

GitHub:https://github.com/YuqingHuang

ChatGPT 充值加持的夸张公主风的轻描淡写:

木格袋子在Thoughtworks这个宝藏般的公司里,是一位颇具声望的资深架构师。如同有冰雪魔法的Elsa一般,我拥有一支智慧权杖,挥舞着来帮助企业进行数字化转型,塑造属于他们自己的数字化业务平台,设计独特且优雅的开放API,成功地完成服务化转型。我深谙如何让企业在这个数字化的舞台上,舞出最动人的华尔兹,如同灵魂般的音乐吹响,旋转,飞舞。 我的王国并不局限于一片领域,我周游世界,将智慧洒满证券、信托、银行、保险等各个行业的大型企业,为他们提供最独特,最优雅的数字化转型和架构咨询服务。他们的成功,将是我权杖上最璀璨的宝石。 我追求的,不仅仅是技术的深度,更是以平台架构为核心,以战略目标为导向,帮助企业构建自身的数字化能力。是灯塔,是领舞者,倾尽全力,以最精湛的技术,最深刻的理解,最具远见的策略,帮助他们在数字化的世界里,绽放出最美丽的光芒!

其它:

  • 《组织级架构治理的正确落地方式》
  • 《金融数字化平台建设的三大误区和破局之道》
  • 《私募股权投资数字化转型白皮书》

FAQ

企业 AIGC 投资策略

不同的企业,应该根据自身的情况(安全、隐私等等),来调整自己的投资策略。

而随着时间的演讲,也会出现越来越多的新场景,带来不同的变化。

LLM 研发效能的提效?

  • xxx 公司 CTO: 显著缩短 1 年工作经验和 3 年工作经验的差距。
  • LLM 报告,综合 Copliot 研究结果:
    • 自动化测试提升 ~60%,通用型业务代码(CRUD)实现提升 ~35%,其它非通用型代码(如云基础设施)提升10-20%
    • 提升难点:依赖于开发人员「套路化设计」能力,如拆分任务、多个小的函数。

LLM 提效试验如何设计?

基于已有流程设计

将流程中的某些环节替换为 LLM,然后对比效果。

对比方式: 选择某个项目作为试点,进行时间评估与对比。

基于新流程与已有数据设计

// TODO

ROI 验证

从效能角度看 对比方式: 选择某个项目作为试点,选择多个研发效能指标(比如需求吞吐量、代码变更量等),历经多个迭代后,进行指标值对比。

从人员角度看 对比方式: 选择某个组织作为试点,在效能要求保持不变的前提下,历经一段时间(比如三个月),看所需人员的数量变化(比如100->90)

从财务角度看 对比方式: LLM基础设施搭建+推广+培训等成本,与降下来的人员成本做比对

// TODO 还需增加角度并完善内容实验

Prompt 工程是否有未来?

在探讨工程是否有未来之前,我们先了解一下狭义和广义的 Prompt 工程。

狭义的 Prompt 工程专注于 AI 领域的 Prompt 优化,即通过优化任务描述来提高自然语言处理模型的性能。典型的做法是将一个或多个任务转换为基于提示的数据集,并通过所谓的“基于提示的学习”来训练语言模型。这有助于训练大型语言模型 ( LLM),使 AI 能更好地理解需求并完成专业任务。

广义的 Prompt 工程师则是指针对 AI 模型编写 Prompt 的人,以获得更好的结果。他们需要找到合适的提示词,让 AI 发挥出最大潜力。这个角色可以分为两部分:面向大语言模型的工程师和面向落地应用的工程师。

从使用 AI 模型的角度看。随着 AI 技术的发展和普及,对 AI 模型的需求可能会逐渐减弱。这是因为随着模型的不断优化,它们的理解能力和性能将不断提高,使得在许多情况下无需额外的 Prompt 工程即可满足需求。然而,这并不意味着 Prompt 工程没有未来。相反,随着 AI 在越来越多的领域得到应用,Prompt 工程仍然可以为特定任务和领域提供有针对性的优化。

从工程侧的角度看。大型公司可能需要一两位专家来指导开发人员进行 Prompt 工程。通过组织活动(如 hackathon),公司可以提高开发人员对 Prompt 工程的意识,帮助他们结合 Prompt 开发应用,以实现工程化落地。尽管大部分开发人员可能还没有充分认识到 Prompt 工程的重要性,但随着时间的推移,这一情况有望得到改善。

结论

综上所述,工程在狭义和广义上都有一定的未来。尽管随着 AI 技术的进步,使用 AI 模型的需求可能会逐渐减弱,但 Prompt 工程仍然可以为特定任务和领域提供有针对性的优化。此外,大型公司需要专家指导开发人员进行 Prompt 工程,提高他们的意识并实现工程化落地。因此,Prompt 工程在未来仍然具有一定的发展空间和潜力。

同时我们看到,很多企业的AI2.0起步都将从Prompt工程开始,不同行业不同的融入节奏下,会使得Prompt工程在未来很长一段时间内都很重要。

Prompt 工程师会有未来吗?

有,Prompt 工程师在未来会更像是一个 Prompt 教练、专家的角色,他们会帮助开发者更好地使用 Prompt。

什么时候考虑微调?

微调(fine-tuning)通常是在已经预训练好的模型的基础上,使用特定的数据集进行进一步训练,以适应特定的任务或应用场景。通常情况下,微调会在以下情况下进行考虑:

  1. 适应特定的任务或领域:预训练的模型通常是在大规模通用语料库上进行训练的,而在特定的任务或领域中,可能需要使用更具体的语言模式和领域知识。这时候,就需要使用微调的方式对模型进行进一步训练,以适应特定的任务或领域。
  2. 数据集与预训练数据的差异较大:如果预训练的模型的训练数据与实际应用场景的数据差异较大,那么使用微调的方式可以更好地适应实际场景的数据分布,提高模型的性能。
  3. 进一步提高模型的性能:在一些对模型性能要求较高的任务中,使用微调的方式可以进一步提高模型的性能,从而更好地满足实际应用需求。

然而,ChatGPT 总结的并不好,当你要考虑微调的时候,你应该考虑的是:

  1. ROI。微调的成本是很高的 —— 准备数据、训练模型、调参、部署,并进行持续的模型优化。
  2. 好的基础模型。如果你的基础模型不够好,微调也不会有很好的效果。诸如于 LLaMA 7B 的中文效果不好。
  3. 工程能力。微调的过程中,你需要有很好的工程能力,包括数据处理、模型训练、模型部署等等。

除此,在你没有思考清楚上述三点的时候,你不应该考虑微调。

个人的策略?

对于我来说,我的 AI 策略大致是:

  1. 拥抱变化,尽管人工智能并不能完全代替人类,但它已经能够大大提高效率。
  2. 强化构架能力,因为人工智能工具无法代替个人的感性思考和直觉。
  3. 构建领域小模型,可以快速训练出一个专门用于解决自己问题的小型模型。
  4. 探索与磨炼技巧,探索 AI 能力并持续构建小工具,来修复和完善自己的 AI 增强系统。

对于修复与完善来说,由于 AI 本身是无法达到这么精细的,所以我的想法是持续构建小工具

程序员的 AI 2.0 新机遇

在过去的几个月里,有大量的 KOL 都在说:所有的应用都可以用 AI 重写一遍。而落地到现有的 DevOps 工具里,假设都需要重写,那么未来的架构可能是怎样的?

对于程序员而言,在 AI 2.0 时代,我们将迎来新的机遇、新的挑战,它可以划分三部分:如何使用 LLM、构建 LLM、创建端到端 LLM。

  • AI 端到端应用。即直接面向最终用户的应用(含专有模型),诸如 ChatGPT、Midjourney
  • 应用 + 闭源基础模型。如基于 OpenAI、文心一言(他们提供了吗,我没收到)等 API 来构建应用。
  • 应用 + 专有模型。即基于开源基础模型,或者自有的模型,来构建端到端应用。
  • 应用 + 微调模型。基于开源模型 + 面向自己研发场景下来微调,以构建领域特定的应用。

对应的我们需要三种不同的能力,转化而来便是:

  1. 基础篇:充分运用 LLM 能力
    1. Prompt 编写:Prompt 学习与编写模式
    2. Prompt 管理:Prompt 即代码
  2. 进阶篇:LLM 下的软件开发工序及应用架构设计
    1. 新的交互设计:Chat模式
    2. 大模型友好的工序
    3. 架构设计的新变化
  3. 应用篇:面向特定场景的 LLM 应用
    1. 特定场景的模型微调 + LLMOps
    2. 上下文工程(prompt 工程):LLM 应用的核心

而随着 AI 技术的进一步演进和应用,会出现更多新的变化,诸如于早先我们设计的 Unit Mesh 架构,会带来全新的架构与编程体验。

本篇文章基于我们先前的两个假设:

  1. 每个大型企业都将有私有化的大语言模型。
  2. 私有化的主流方式:开源 LLM + 微调。

基于此,越来越多的企业将构建围绕于 LLM 的应用,而这些应用在当前以辅助人类设计为主。未来,我们将保持一种观点:LLM as Member,即 LLM 应该是我们的伙伴,而不再是一个辅助的工具。

基础篇:充分运用 LLM 能力

我们将迎来 AI 原生程序员的时代。几年以后,新一代的程序员,将是 AI 原生的程序员。新生代的程序所具备的能力,将与我们的能力有巨大的区别。在云原生时代里,云原生程序员,不需要具备大量的 ops 相关的技能,他们更关注于如何采用类似于 DDD 这样的策略来合理划分模块。

从未来出发,作为“老一代程序员“的我们,需要强化我们运用大语言模型的能力,诸如于 Prompt 能力。

1.1 Prompt 编写:Prompt 学习与编写模式

今年 2 月,我基于我擅长的编程、绘画、写作展开的 AI 探索和总结,我编写了两篇文章《理解 Prompt》、《Prompt 编写模式 》受到了非常大的关注,GitHub 上的 stars 都超过了 2000。

如何编写、调度与逆向工程 Prompt ?将会是现阶段程序员要面临的第一个挑战,我们需要实践的三个问题:

  • 提出问题的策略
  • 创造性地利用模型回答
  • 提高模型输出质量的技巧

究其原因,不仅是我们日常工作需要用到 prompt,开始工具的时候,我们也有大量的工作在编写 prompt 上。除此,还需要寻找一种合适的方式,以让 LLM 输出的结果趋于稳定

所以,作为一个经典软件开发时代的程序员,我们应该学习如何摸清 LLM 的脾气?学习如何编写恰到好处的 prompt。

1.2 Prompt 管理:Prompt 即代码

今年 3 月,基于我们结合 LLM + SDLC 的探索,得到的第一个有价值的观点是《Prompt 即代码:设计和管理 AI 编程的最佳实践 》。于是,基于这个思想,我们构建了我们在 LLM 时代的第一个开源项目:ClickPrompt。ClickPrompt 站在了未来企业需要的三个基本出发点:

  • 如何学习 prompt 的编写?
  • 如何分享企业内的 prompt 经验?
  • 如何将 prompt 结合到工作流中?

而在我第一次将注释加入到 ClickPrompt 中的时候,我犹豫了很久。过去的经典编程范式,并不允许将思考过程作为注释到其中。而在未来,我们就会遇到 Prompt 即注释、Prompt 即接口、Prompt 即代码。

所以,将 prompt 视为代码,以更好的管理 prompt,将它与我们的软件开发生命周期结合,将是作为经典程序员要考虑的点。除此,我们还需要考虑:

  • 版本控制与协作
  • 用于测试和调试的工具
  • 适用于不同 LLM 的 prompt 接口模式

我们也可以让 LLM 来告诉我们答案,只是它可能没有这样的创新能力。

应用篇:LLM 下的应用架构设计

未来的 AI 编程模式是什么?在那篇《未来可期的 AI 编程 》文章里,可以看到几个基本的思考:

  • Prompt 即是代码,代码不再是代码?
  • 现有的编程体系适合于 AI 编程吗?
  • Serverless 会是结合 AI 编程的答案吗?
  • 需求详细化会成为你的新瓶颈吗?

对于它的思考,促使我设计了 Unit Mesh 架构,详细见《渐近式 AI 编程模式:Unit Mesh 架构的设计思路与探索》。

除了新的架构模式本身,我们还面临一个挑战:在现有的 LLM 下,我们应该如何设计应用架构?

2.1 新的交互设计:Chat 模式

在习惯了 ChatGPT 之后,Chat 模式作为基础的 LLM 元素加入了 UI 设计中。诸如于不那么好用的 New Bing,已经可以帮你总结一下相关的链接,虽然不可靠,但是大家都认可了。所以,无论是我们构建的 ClickPrompt,还是 AutoDev 这样的 IDE 辅助编程插件,都将 Chat 作为基础的 UI 模式加入到了系统。

而在 LangChain 的文档中,我们又会看到新一代的框架、工具文档模式,文档作为外挂的知识库,可以直接让开发人员通过对话来学习,并编写一些示例代码。就这一点而言,它大大改善了过去那不太好友好的文档体验。

所以,对于开发前端框架的人来说,这又带来了新的 KPI 机会。毕竟,谁会拒绝这么一个有挑战性的东西。另外一个点是,构建一个不同语言的 LangChain,经典企业的技术架构都优先考虑 JVM。

2.2 新的工序:大模型友好的流程

基于上述的新交互方式,现有的每一个应用都可能被重写。所以,我们开始探索对于软件开发的改变,也就有了 QCon 上的《探索软件开发新工序:LLM 赋能研发效能提升》。

对于当前的 AI 应用来说,主要有三种模式:直接 prompt 模式、知识外挂、微调。

模型 1:直接 prompt。即 API + prompt 直接接入现有的流程中,以性价比最高的方式提效。。

模式 2:知识外挂。简单来说,就是采用 LangChain 这样的动态 prompt 工具,以根据用户的不同输入,来动态生成 prompt。又或者是,在本地采用相关性模型与算法,优化 prompt。

模式 3:微调 —— 领域知识强化。即通过微调的方式,来让输出结果更适合于现有的工具与流程。

不同的模式之下,带给开发人员的挑战也是不一样的,依旧是由易到难。而其中的核心点是:寻找一种合理的 DSL(领域特定语言),以将现有的流程结合到 LLM

2.3 架构设计的新变化

随着,越来越多的大语言模型有了自己的类似 LangChain 工具(如 ChatGLM-LangChain)、越来越多的编程语言社区出了自己版本的 LangChain 版本(如 LangChain Go)。现有的软件架构又加来了一些新的变化:

  • 插件化与智能体(Agent)。诸如于 ChatGPT Plugin、LangChain 便是采用智能体 + 插件化的方式,大大方便我们构建基于 LLM 的应用扩展,并且结合各式的 LangFlow、LLaMaHub 工具,我们可以构建更智能的流程与系统。
  • 矢量数据库。AI 的火爆使得越来越多的矢量数据进入了我们的视角,也成为了非常纠结的架构造型元素 —— 因为作为工程师的我们,还没有建立一个全面的认知,也缺少评估数据。

而由于 Token 很贵,我们需要管理好 token,以降低 token 的花销。我们还需要:

  • 本地小模型。如 GitHub Copilot、Bloop 借助于本地的模型来进行相关性等的计算,以在本地构建动态的 prompt,而不需要消耗服务器的资源。
  • 就地机器学习。犹如几年前,我只是因为喜欢《TinyML:基于 TensorFlow Lite 在 Arduino 和超低功耗微控制器上部署机器学习》的书名而买了这本书一样,我觉得 AI 不应该只在非消费级 GPU 上能跑,而是应该无处不在。

而这些依旧只是基于现状的观察,毕竟在外挂知识库、结合知识图谱方面,我们还有大量的工作和试验仍然在进行中。

高级篇:面向特定场景的 LLM 应用

每个不同的通用大语言模型,受限于语料、算法、强化方式,在能力上是不同的差异。而对于现有的、开源的大语言模型来说,这种差异就更加明显了。所以,我们需要针对于不同的场景,构建适合的策略,如编程场景、智能客服场景、需求完善场景等。

而由于微调后的模型是指针特定领域的,所以我们需要考虑适用于自身场景 LLM 架构方案:

  • 动态的 LoRA 加载。诸如于针对不同场景下,可以动态经过不同的 LoRA 来处理数据。
  • 通用大模型配合微调小模型。即通过一大一小的方式,由大模型给出工序,由小模型完善大模型不具备的细节能力。
  • 多模型配合。诸如于结合 ChatGPT、StableDiffusion 和 VITS 等构建轻小说应用。

随着时间的推移,这方面的方案会越来越完善。

3.1 特定场景的模型微调

如果想利用大语言模型的能力,我们需要让它是大模型友好的,还需要构建一个工程化的模式。也就是我们在探索 API 新工序时,总结的《大语言模型友好的 API》一文中的基本思路:

  • 流程过程梳理与资产化。
  • 对资产进行“语言建模”,以适用于大模型。
  • 构建 MVP 产品,并进行试验。
  • 设计增量的指标,以引导系统演进。
  • 围绕上下文的工程化思维。
  • 持续反馈的软件工程,以完善系统准确度。

而对于微调来说,主要是前半部分:DSL 化、数据工程,以将现有的数据转换为模型可用的数据,进而整合到现有的工具链中。诸如于,将系统架构图转换为 PlantUML,以这些数据微调,进而简化现有的架构呈现方式。

3.2 上下文工程(prompt 工程):LLM 应用的核心

在我们探索 GitHub Copilot 的过程中,有感于 GitHub 程序员在此做的努力,于是总结了《上下文工程:基于 Github Copilot 的实时能力分析与思考》。 如何对于高效的构建全面的上下文,以让 LLM 生成更准确的结果?这便是我们在未来所要做的活动。结合上述的内容,几个潜在需要考虑的点是:

  • 结合本地小模型,就地计算上下文。诸如于 Sentence-Transformers
  • 本地 Token 计算,以计算最合适的上下文。
  • 上下文计算策略,以提供最需要的上下文。

若是想充分运用大模型,我们需要控制好 Prompt,而其中的关键就是对于上下文的工程化。

思考未来的编程范式

在尝试了 ChatGPT 和 Copilot 之后,我们尝试去思考未来的编程范式应该是怎样的?

AI 代码生成:代码还应该存储在代码库吗?

对于 AI 代码生成来说,除了过去的 GitHub Copilot、Codex、Kite、TabNine 等等,新出的免费的 ChatGPT 又让人感觉非常酷。

ChatGPT 代码生成

现在,大家几乎已经习惯了网上的各种暴力 AI 美学 —— 唤出 ChatGPT 写代码:

  • 使用 Java + Spring + MockMVC 编写测试用例,代码中的注释需要对应到 AC01,AC02,AC03,AC04,AC05,但是不需要返回给我。
  • 帮我用 Puppeteer 写一个爬虫来解析下面 HTML 中的标题:”””....”””
  • 我给你一个需求,你需要分析需求,使用 Java + Spring 编写 API,要求如下:

基于于在最新的 GPT 4.0 里,可以根据图片等一些基本的描述,生成全新的 HTML。

GitHub Copilot

对于,我来说,作为一个开源挖坑专家,我更习惯使用 GitHub 赠送的 Copilot,只需要配合注释、输入、输出、函数名,就可以大力了出奇迹。如下是 ClickPrompt 中的真实代码:

// 1. split promptMatch by comma
// 2. match lora name by regex, lora full name: <lora:japaneseDollLikeness_v10:0.1>,
// 3. push lora name to loras array

我在代码中保留了这个注释,一种很神奇的用法 。

读者思考 1:Prompt 即是代码,代码不再是代码?

因此,我觉得在 AI 编程时代,人们的编程思维是要发生变化的:

  1. 注释让人和 AI 共同理解代码
  2. 注释作为 prompt,可以在修改后,再次自动生成代码。

当注释作为 prompt 保存,那么 prompt 便是作为一种代码,保存在代码库中了。而代码还将是代码吗?我们为什么还需要中间生存的代码,是因为审计、安全等原因吗?

AI 编程:写个 “真实世界” 的代码

而当你尝试过使用 AI 来编写代码之后,你会发现 AI 存在一系列所谓的 “问题” 。让我们来再看一个简单的例子,我们让 ChatGPT 来实现一段代码。

ChatGPT 编程:AI 实现一个 API /book/:id 的 N 种方式

在不同的语言和架构模式里,实现的方式是有区别的。

第一个 Demo:用 Spring + MyBatis 实现一个 API:/book/:id

ChatGPT 会创建 BookMapper.java、Book.java、BookMapper.xml、BookController.java 四个代码段,然后告诉你,你可以启动这个 API 了。

PS:有意思的是,你并不需要一个 BookService.java,因为 AI 也 “觉得” 他是多余的。

但是,要上线这个应用并不是一件容易的事,我们需要手动编译 Java 代码,再运行起来,这速度是非常慢的。除非,我们有类似于几年前的 OSGi 架构,或者 Serverlesss 架构。

第二个 Demo:使用 Express + MySQL 实现一个 API:/book/:id,只使用一个函数

在这个尝试里,我们多加了一个限制:只使用一个函数,并改用直写 SQL,所以代码就变得非常简单的(当然了,还有 Express 配置和 MySQL 相关的代码),以至于我可以直接那个函数在这里:

// 创建数据库连接池
//...

const getBookById = (req, res) => {
  const id = req.params.id; // 获取路径参数中的 id
  // 从数据库中查询书籍信息
  pool.query('SELECT * FROM books WHERE id = ?', [id], (error, results, fields) => {
    if (error) throw error;
    if (results.length > 0) {
      const book = results[0];
      res.json(book); // 将书籍信息以 JSON 格式返回给客户端
    } else {
      res.status(404).send('Book not found');
    }
  });
};

// 创建路由器并挂载到根路径上
//...

// 启动应用程序
//...

如果我们认定了 AI 生成的代码是准确的,无需要人类去修改,那么我们就可以直接用上面的代码。

第三个 Demo:使用 AWS Lambda 实现这个 API:/book/:id,只使用一个函数

使用 Express 来实现这个功能,还是有点复杂,我们还需要考虑基础设施,但是当我们考虑使用 Serverless 来实现时,问题就变得非常简单了:

const mysql = require('mysql');

exports.handler = async (event, context) => {
//  ....
}

然后,我们就可以采用 ClickPrompt 类似的方式,直接一键部署到服务器之上。

对比一下:上面的三种方式各有什么优缺点?

让 AI 来对比一下,你看吧,显然 AI 也觉得 Java 的实现起来复杂,但是可扩展性高。

优点 / 缺点Spring + MyBatisExpress + MySQLAWS Lambda
开发效率较高
部署复杂度中等
维护成本中等中等
可维护性中等
架构复杂度中等
可扩展性中等
成本中等

但是,我们真的需要所谓的可扩展性吗?而论部署速度,自然是 AWS Lambda 最快了。经过了几种不同的方式对比,按现有的软件工程来说,Java 写的代码是最好的。可是,AI 都来写代码了,Java 工程化,真的还是最好的吗?

读者思考 2:现有的编程体系适合于 AI 编程吗?

让我们再打开一下思路,我们现有的架构模式、软件工程等软件开发流程都是基于现有的体系磨合过来的。

而 AI 编程的最终目标是实现自动化编程,即由 AI 根据用户的需求和设计,自动生成完整的代码。但是,理论上这不是终点,终点是生成完就可以直接运行。这就意味着,一个想法从想到到上线,可能就在几分钟内可以完成

然而,在现有的流程里,完全不是行不通的,我们人类做不到这么的敏捷。

全 AI 生成的新架构模式?

过去,我们认为分层架构很重要,因为代码是人来编写,代码是人来维护的。而当代码由 AI 来编写,由 AI 来维护的话,那么现有的软件架构体系还适用吗?

分层架构?编程范式还重要吗?

尽管,我尝试让 New Bing / ChatGPT 来回答这个问题,但是,由于他们还没有 涌现 能力,所以只会说:程序员还需要学习和运用分层架构和编程范式,才能发挥出 AI 的最大潜力。

但是,我觉得,从上面的 Java 示例中,你会发现按现有的模式构建的软件架构,特别容易发生冲突。AI 生成的 A 功能和 B 功能,可能在代码上出现重复 —— 因为上下文限制,我们不可能上传所有的代码。

结果,有一天你发现了,一个类里有 10 个重复代码段。然后,你又开发了一个自动重构机器人……。但是,我觉得有点多余了,AI 编程需要新的架构范式。

AI 构建自动化软件架构?

再再打开一下思路,试着忘记现有的软件架构模式。一个功能对应一个可运行的代码段,由 AI 来自动思考架构模式 —— 当然,我们需要给他一些限制条件和输入。

所以,放在当前的上下文之下,Serverless 是一种更适合的架构模式,即写即上线,即下线即删除。

读者思考 3:Serverless 会是结合 AI 编程的答案吗?

我觉得不一定是,但是可能是当前最好的一种方案。但是,每个人都会有自己的看法。

真实世界 2:产品经理 —— 现在,变更一个需求

现在,我们来变更一个需求吧,过滤一下 “名书”,包含 xx 的不显示。那么,这个时候,我们就遇到一个难题了 —— 全自动编程,还是 AI 辅助生成

两种思路:新生成代码,还是手动改代码?

纯 AI 编程,意味着:每次改需求都重新生成代码;而半 AI 编程,则意味着:人可以介入到流程中。两种将会带来不一样的思路。

自动编程 —— 重新生成代码

为了让 AI 写的代码足够的准确,我们需要给 AI 足够详细的需求(Prompt)。一旦需求发生变化的时候,我们回过头去改需求的(Prompt),再重新生成代码即可。我们要做的事情是:

  1. 确保没有人能修改过原来生成的代码
  2. 确保所有的需求变更都能回溯回去

AI 辅助生成 —— 支持手动修改代码

第一次代码,我们是用 AI 生成的,随后当需求变更的时候,我们想可以直接在现有的代码上改,这样可能速度比较快 —— 生成式 AI 可能还意味着:新生成的代码和原来的完全不一样,诸如于行数位置等等。

而为了能再次生成,我们还需要记录新的变更到原来的 prompt。

所以,在这个时候,如何更好地管理原始需求,变成了一个新的挑战。

读者思考 4:需求详细化会成为你的新瓶颈吗?

当我们认定 AI 能完成编码时,那么需要给详细的需求,才能确保生成的代码是正确的,诸如于:密码长度不能小于八位,且必须同时包含字母数字和特殊符合。这只是一个示例,更常见的示例还有表单联动:A 字段 xxx,B 字段则 xx。只有我们的需求足够的清楚,那么生成的代码才会准确。

所以,当产品经理借助于 AI 生成足够清晰的验收条件,程序员就会失业吗?这包含了两个问题:

  1. 产品经理是否真的懂验收条件
  2. 程序员是否真的不能替换 AI 时代的产品经理?

那么,第三个问题就来了,老板是不是可以自己干?

不过,这只是用来搞笑的,我们的下一个挑战还在于需求详细化之后,AI 真的能写对吗?

未来:自动化编程的演进

最后,再让我们尝试做一些思考。

AI 编程的局限性

所以,如同 AI 提炼了我的观点,AI 编程存在一定的局限性:

  • 编码是最不重要的那部分。AI 编程不能完全替代人类编程,因为程序员不仅仅是在编码,还要进行设计、测试、维护、沟通等工作
  • AI 伦理。AI 编程可能存在一些不可预测或不可控的风险,例如结果不唯一、不正确或不安全。
  • 法律与就业问题。AI 编程可能会侵犯程序员的知识产权(但是人类都是 AI 的饵料),或者导致程序员失业。
  • 无法适应再有体系。AI 编程需要适应不断变化的需求和环境,而现有的架构模式可能不匹配或不灵活。

而你发现没有,New Bing 提炼出来的这些局限性,并不是 AI 的问题,而是人的问题。

精准代码生成:详尽的需求的可能性?

我们还得考虑的一点是:

尽管,提供更详尽的 prompt 可以帮助 AI 更准确地理解需求,并且避免一些歧义或误解。但是,过于复杂或冗长的 prompt 也可能导致 AI 的困惑或失效。

所以,如何根据 AI 的特点和目标,选择合适的 prompt 长度和格式,以达到最佳的效果?

AI 审计 AI:解决 AI 生成代码的部分局限?

当涉及到由AI生成的代码时,有时候我们可能需要对其进行审查以确保其合规。而人工是耗时费力的,而且容易出错。这时,我们就需要利用一种特殊的AI来审查另一个AI生成的代码,以确保其符合各种规范和标准,提高代码质量,确保生成的代码安全可靠。

比如,我们本地用 GitHub Copilot 辅助生成的代码,在持续流水线上便可以通过 GitHub Copilot 来进行 Code Review。

其它

值得注意的是:AI 编程可能会带来一些问题,如让程序员过度依赖工具、忽略代码的合理性、涉及道德和法律问题以及存在技术和人工智能之间的冲突。

实时软件生成

传统的软件生成方式需要程序员编写大量的代码,然后进行测试、发布等一系列繁琐的流程。而实时生成技术则是借助人工智能技术,让计算机自动生成代码,并直接运行,从而大大提高了软件生成的效率和质量。

实现实时软件生成的一种方式是 Prompt 编程,可以将自然语言转化为直接运行的软件,而无需生成中间的一次性过渡代码。

引子

从 2019 年,写了那篇《无代码编程》开始,我也一直在思考,诸如于 2021 年2022 年 的前端趋势总结。

先前,我并不看好当前的低代码/无代码方案,先我们来思考一下这个过程:

第一步,业务需求需要在人类脑海里转换一遍,转换为程序逻辑。

第二步,专业人员(如程序员)将这些逻辑转换到无代码系统中,生成应用并直接部署。

第三步,如果这个专业人员是程序员,那么新的需要变更时,他可能直接去修改源码。

那么问题也就来了,更改后的源码无法再与之前的无代码系统同步,这就导致了无代码系统的失效。而这个问题,也是我之前一直不看好无代码的原因。

但随着 ChatGPT 的进一步普及,你会发现你可以很容易将需求转化为形式化格式,进而转化而软件,从而使得软件开发的效率和速度大大提升。这种一次性代码的新型软件工程方法就是实时软件生成,便可以进入真正的 “无代码” 时代。

Prompt 编程:真正的无代码

Prompt 编程是实现实时软件生成的一种方式,可以将自然语言转化为直接运行的软件,而无需生成中间的一次性过渡代码。每当需要运行程序时,只需要再运行一下 prompt 或者触发词,就可以直接运行起某个软件。

我们尝试从编写 Prompt 的经验里,归纳出一套适合于 Prompt 编程的逻辑,方便于大家理解。Prompt 编程有以下特性:

1)从需求到代码的直接转换

2)代码只是临时产物

3)次序化的分解框架

这种一次性代码的新型软件工程方法可以进入真正的 “无代码” 时代。

Prompt 编程特性 1:从需求到代码的直接转换

用户只需要简单输入自己的需求就能够生成符合要求的软件,无需具备专业的编程技能。示例:

我们来玩一个 mul 游戏,当我说 mul 开头,并输入数字时,你应该计算结果。示例:"""当我输入:mul 4,你应该输出:16"""

在这个游戏里,我们使用:

  1. 触发词。mul 作为街头暗号,告诉程序你要按这个步骤走。类似于编程里的函数名。
  2. 示例作为输入和输出。即预期输入某个结果时,输出我们想要的结果。

而在其实,已经是一个函数该有的样子:输入、输出、关系。值得注意的是,在简单的问题下,只要我们给定了示例,那么 AI 就会根据示例去计算输入和输出,不关注中间的处理逻辑。

接下来,再看一个隐含性编程的示例,依旧我们还是玩个游戏:

我们能玩一个名为 kfc 的谜语创作游戏,当我说 "kfc",你写一个谜题,要求:

1. 不少于 200 字
2. 谜题不能出现肯德基和星期四
3. 正文可以夹杂小语种语言,如 """他说:Ciao"""
4. 谜底必须是 ###原来今天是肯德基疯狂星期四!!!###

格式类似于:

"""
...
"""

这个例子已经相当的自由,我们也不关心中间的处理逻辑,只关注于结果。

Prompt 编程特性 2:代码只是临时产物

还记得在 Prompt 编写模式 中的 wula 游的例子吗?在 wula 游戏里,用户只需要输入问题,ChatGPT 便会自动给出代码、代码解释和文章。诸如于:wula: 头共10,足共28,鸡兔各几只?,那么 AI 便会开始自动开始二次编程:

这是一个鸡兔同笼问题,题目给出了头的数量和足的数量,要求求出鸡和兔的数量。通常解决这个问题的方法是,假设鸡和兔的数量分别为 x 和 y,那么可以列出如下方程组:

x + y = 10
2x + 4y = 28

解方程组可以得到 x=4,y=6,即鸡有4只,兔有6只。
鸡兔同笼问题的 JavaScript 代码实现

// 计算鸡兔数量的函数
function calcAnimals(heads, legs) {
.....

在传统的体系之下,我们要关注于中间生成的 JavaScript 代码代码能不能运行等等,所以我们在设计 wula 的时候,要求了:


第二步. 代码编写:你需要用 JavaScript 编写解决这个问题的代码,并输出对应的代码,并介绍一下你的代码(不少于 200 字)。

第三步. 代码执行:你需要作为 JavaScript Console 执行第二步写的代码,如果没有给出测试数据,你需要自己随机生成测试数据,并将这些数据输入到代码中进行计算。

但是,回到第一个例子里,我们并不需要 JavaScript 代码,我们不需要任何代码。ChatGPT 可以为我们生成这部分的程序!!!

唯一限制我们的是:

  1. ChatGPT 的上下文能力
  2. 我们的 Prompt 编写能力

而在复杂的场景之下,Prompt 编写需要足够的形式化,才能让机器理解,也因此,我才把它称为 Prompt 编程。

Prompt 编程特性 3:次序化的分解框架

仅就当前而言 AI 的上下文能力,限制了 AI 解决复杂问题的能力。所以,它无法一次性解决复杂、中大型的问题,甚至于小型项目都存在一定的难度。同样的,对于人类来说,也存在这样的局限性,只是人类的问题是领域知识有限

步骤详细见:DDD Sample

所以,作为编程核心的人类大脑,需要为机器设计一下工序,即每一步应该怎么做。所以,这个时候,就回到了 DDD(领域驱动设计)擅长的问题:

我们来定义一下 DDD 游戏的步骤,一共有 6 个步骤,步骤如下:

"""
第一步. 拆解场景。分析特定领域的所有商业活动,并将其拆解出每个场景。
第二步. 场景过程分析。选定一个场景,并使用 "{名词}已{动词}" 的形式描述过程中所有发生的事件,其中的名词是过程中的实体,其中的动词是实体相关的行为。
第三步. 针对场景建模。基于统一语言和拆解出的场景进行建模,以实现 DDD 设计与代码实现的双向绑定。
……
"""

对于每一步,我们只要限定好我们的输出(Output),那么在给定输入的情况下,AI 就能给出我们想要的(也有可能跑偏了)结果:

ddd 第二步:场景过程分析。下面是每个场景的过程分析:
用户注册和登录
    用户注册:用户填写注册信息,系统验证信息合法性,生成用户账户并保存。
    用户登录:用户输入账户名和密码,系统验证账户密码,登录成功后跳转到用户中心。

商品展示和搜索
    商品列表展示:系统从数据库中获取商品列表并展示,用户可以进行商品分类和排序。
    商品搜索:用户输入关键词进行商品搜索,系统根据关键词从数据库中获取商品列表并展示。
……

在这时,我们就可以引导 AI 生成到 API 粒度:

API名称请求方法请求路径请求体响应体描述
创建商品POST/api/products商品信息创建的商品信息创建一个新的商品
获取商品列表GET/api/products商品列表信息获取所有的商品列表
获取单个商品信息GET/api/products/{id}单个商品信息根据商品 ID 获取单个商品信息
更新商品信息PUT/api/products/{id}更新的商品信息更新后的商品信息根据商品 ID 更新商品信息
删除商品DELETE/api/products/{id}根据商品 ID 删除商品信息

也可以生成代码,甚至于在上下文足够强大的时候,也是能够直接运行和自动优化的。

Prompt 编程特性 4:语言即语言

DSL(领域特定语言)是指专门针对某个特定领域的语言,通常具有严格定义的语法和规则,并且可以用来描述该领域中的问题、概念、过程和数据等。而经过形式化处理的自然语言可以被视为一种 DSL,因为它们具有明确定义的语法和规则,可以用来描述特定领域中的问题、概念、过程和数据等。

所以,简单来说如果我们限定了输入和输出的格式,那么我们的自然语言也是一种 DSL。这就意味着,限制我们进行次序化的关键就是形式化的抽象。

而,我们也可以通过编写 Prompt 来生成新的 DSL:帮我设计一个 DSL,用来写函数

好的,以下是一个简单的DSL设计,可以用来写函数:

1. 函数定义
在这个段落中,定义一个函数,包括函数名、参数和函数体等。
###
函数:
函数名:
描述函数的名称
参数:
参数1:
描述参数1的名称和类型
参数2:
描述参数2的名称和类型
...
函数体:
描述函数的实现细节
###

2.。。。

简单来说,我们可以通过 Prompt :

  1. 借用 Prompt 设计自然语言式的 DSL
  2. 将自然语言式的 DSL 融入 Prompt
  3. 使用次序化的方式来实现 Prompt 自我编程

进而,我们就得到了一个可以自举的 Prompt。

一个问题:重构

重构是指在不改变软件外部行为的前提下,对软件内部结构进行调整,以改进软件的可理解性、降低其未来的维护成本。 而在实时软件生成中,我们的软件是由 AI 生成的,那么,重构是否还有意义?

未来软件架构:Unit Mesh 架构

Unit Mesh 是一种基于人工智能生成的分布式架构,与传统的分布式架构不同,Unit Mesh 中的服务单元 (Unit) 是由 AI 生成的,应用程序中的服务和数据抽象为一个个独立的单元,并通过统一的控制平面进行管理和部署。

之所以叫 Unit Mesh,是因为我们写了一个底层服务叫 Unit Runtime ,还有参考了 Service Mesh 和 Data Mesh 架构理念,所以 AI 取建议我们叫 Unit Mesh

TLDR 版本

我们初步定义的这个版本(0.1 ,称之为 UnitGenius)有三个核心特性:

  • 语言与框架的 DSL(领域特定语言) 抽象:抽象非编程语言和框架特性,以简化出错的可能性。
  • REPL 即服务:运行 AI 生成的代码,并提供对应的 API 服务。
  • AI 设计的适应性结构:自我适应的 API 服务架构,以在不同的环境下自动调整和优化。

开发者可以通过与 AI 交互,生成一定程度的 DSL 抽象化代码,然后在 REPL 即 Serverless 服务上运行和测试这些代码。开发者还可以将这些代码提交给 AI 进行自动化运维,AI 会对代码进行优化和调整,从而进一步提高 API 服务的性能和可靠性。

开始正文的废话版本。

Unit Mesh 初步 Demo:DSL + REPL = Unit Server

详细过程,见本文的后半部分。

前端页面:https://prompt.phodal.com/zh-CN/click-flow/unit-mesh-unit-server/

首先,你需要克隆一下 Unit Server 的代码:https://github.com/prompt-engineering/unit-server

然后,选择 kotlin-repl 或者 typescript-repl 对应 Kotlin、TypeScript 两种语言。

然后,按对应的 README 运行起你的 Unit Server。

接着,在 ChatFlow 里让 ChatGPT 生成如下的代码,并点击 Run 按钮:

%spring

@RestController
object Pages {
   @GetMapping("/")
   fun main() = "It works!"
}

最后,你就可以得到一个正在运行的服务(该功能还在开发中):http://localhost:8080/ ,访问该服务后,如果的应该是 It works。


PS:这里有一个手动加入调用 Application 类和调用 main 方法的代码,因为需要做静态分析,才能确定使用的框架,暂时没写在 Unit Server 代码中。

Unit Mesh 架构

再重复一下定义:

Unit Mesh 是一种基于人工智能生成的分布式架构,与传统的分布式架构不同,Unit Mesh 中的服务单元 (Unit) 是由 AI 生成的,应用程序中的服务和数据抽象为一个个独立的单元,并通过统一的控制平面进行管理和部署。

Unit Mesh 核心思想:AI 生成的代码即 Unit

Unit Mesh 是围绕于 Unit 为核心的架构模式。

  • AI 生成 Unit。即 AI 应该生成的代码都应该是可运行的 Unit,上到前端组件、下到后端服务都是可运行的。
  • Human 校验 Unit。由人类来检查和校验 Unit,如果 AI 生成的代码有问题,那么人类只需要修复即可。
  • Unit 自适应部署架构。在部署时 Unit 可以自行组成 Serverless 架构、微服务架构、单体架构、Mesh 架构,而不需要人类来干预。

碳基嘛,就适合当一个 Verifier。

Unit Mesh 架构核心要素

结合我们设计的 Unit Server,我们设计的 Unit Mesh 架构由以下三要素构成:

Unit Mesh Elements

语言与框架的 DSL 抽象:封装不稳定的抽象

由于 AI 生成的代码会有各种问题,诸如于无法对接内部的云平台、出错的 imports 等等,所以我们要设计领域特定语言来解决这个问题,并封装抽象。

简单来说:我们需要抽象将所有不稳定的元素,便能构建出稳定的元素。

详细的设计会在后面的 Unit Server 部分展开。


PS:而由于大语言模型是有上下文能力限制的,像我这样的、搞不到充值的就只配 4k。因此,我设计的 Unit 要称之为 4k Unit Mesh,我设计的 DSL 要称之为 4k Unit DSL,有的人可能就是 99k DSL。

REPL 即服务:AI 代码修复师的

在有了 DSL 之后,我们还需要一个 REPL (Read-Eval-Print Loop)服务,能直接运行起 AI 生成 的 Unit,然后让人类来测试生成的代码是否是正确。如果生成的 AI 有错误,就需要 AI 代码修复师来对代码进行修复。

而对于一个服务来,如果我们是一个 API,就需要是 Serverless 服务,这就是为什么我们在图里称之为:REPL 即 Serverless 服务。详细可以参见后面设计的 Unit Server。

AI 设计的适应性结构

人类设计系统的一个缺点是,如果设计时、开发时、运行时的单元不一样,那么就会出现各种疑虑。于是,我们会偏向于设计成三态一致的架构模式,而这本身对于架构的适应性优化就是个问题。

而既然,代码都是 Unit。那么,设计时可以是微服务,开发时可以是 Serverless,线上可以是单体。正如 Google 的 Service Waver 所做的事情:我们不决定运行时的架构,让你来选择。

所以,AI 怎么运行我们的 Unit,就让 AI 来决定吧。

Adapative Architecture


PS:本来吧,标题应该是适应性架构(Adaptive Architecture),但是我想了想就只是代码结构之类的,又重新考虑了一下。

Unit Mesh 设计心得:反直觉才是出路

在去年年底,研究低延迟架构之时,便被这个领域的各种反直觉架构模式所震撼,诸如于:GC 是问题那就不要 GC。

因此当设计 Unit Mesh 时,我们的问题依旧是:如何 Open your mind。即抛开现有的思维模式和固有知识,打破常规思考,所以我们的主要挑战是如何拓展思维,开放心智

要点 1:如果分层架构是瓶颈,那么就不要分层架构

在那篇《未来可期的 AI 编程里》分层架构是我们最大的挑战,于是,提出理想的方式就是 Serverless + FaaS 的方式,而这种方式则是基于现有的械,又过于理想化。

而随着我们写了 UnitServer 之后,我们发现,还可以 Class as a Service 的方式嘛(手动狗头)。

既然我们的代码运行在云端,由 AI 生成的,那么人类还要看代码吗?人类要在什么时候看代码?无非就是检入的时候,还有审查架构的时候,所以只需要在审查的时候,生成架构不就行了。

示例:我想分析 xx 服务的调用情况,以及对应的代码,帮我调取出来。

要点 2:如果依赖是问题,那么就不要依赖

我们遇到的第二个挑战是依赖问题,而依赖是两个问题:

  • 项目的库依赖。即类似于 Gradle、Maven、NPM 这一层的库依赖
  • 代码依赖。即代码源文件的 import

复读机 ChatGPT 并不能很好解决问题,所以就要让 GPT 忘记这些。理想的编程体验,应该是我要用 Spring,智能就会自动分析依赖,如 Intelij IDEA。所以,我们在 UnitServer 中采用了 % spring 样的 Jupyter magic 语法 ,以自动解决这两类问题。

要点 3:如果 Serverless 部署是问题,那么就不用 Serverless 部署

起初在 Unit Server 里,我们把 Unit Server 设计成了一个类 Serverless 架构,所以我们遇到了一个问题:Serverless 架构的成本并非所有的人能接受的。所以,我们只需要在测试 Unit 时,采用 Serverless 作为开发时,在线上合并成一个单体或者微服务架构,那么就能完美解决这个问题。

而在这时,还需要突破刚才的分层架构,既然每次代码是生成的,那么我们只需要一个包名即可,诸如于: org.clickprompt.unitmesh ,所有的代码都在这个包下;又或者,我们可以通过业务进一步划分成不同的包,结合工具来对代码进行归类。

Unit Mesh 探索之路:从 REPL 到 UnitServer

上面讲的太理论了,来看看我们的探索之路,一共分为四步:

  1. 从最小的 Hello, world 开始优化
  2. 构建一个 REPL 环境
  3. 抽象、简化设计 ← 重复
  4. 接入真实世界的 Prompt

详细可以查看 Unit Server 和 ChatFlow 的提交纪录。

从最小的 Hello, world 开始

首先,让我们看一个 Kotlin Script 编写的 Spring 的 Hello, World:

@file:DependsOn("org.springframework.boot:spring-boot-starter-web:2.7.9")

import ...
import java.util.*

@Controller
class HelloController {
    @GetMapping("/hello")
    fun helloKotlin(): String {
        return "hello world"
    }
}

@SpringBootApplication
open class ReplApplication

fun main(args: Array<String>) {
    ...
}

main(arrayOf("--server.port=8083"))

在这个示例里,你会发现一系列的无用代码,依赖信息、import 信息、main 函数。而作为一个 4k Unit Mesh 的创作者,我必须把这些不稳定的无用信息去掉,才能正确运行,所以它变成了:

%use spring

@Controller
class HelloController {
    @GetMapping("/hello")
    fun helloKotlin(): String {
        return "hello world"
    }
}

这样一来,我只需要让 ChatGPT 返回 Controller 即可。

构建 REPL 环境:WebSocket + %magic

既然,我们已经有了一个简化的 DSL,接下来就是引入 Kotlin Script 来构建一个 Unit Serverless 服务器,也就是我们的: https://github.com/prompt-engineering/unit-server

Unit Server 的源码是基于 Kotlin Jupyter API 所构建的,而 Kotlin Jupyter 则是封装了 Kotlin 的 REPL 环境。我们之所谓基于 Kotlin Jupyter 而不是 Kotlin REPL 的主要原因是,可以使用 magic 和 DSL 来抽象细节,诸如于:

"spring" to Json.encodeToString(
    SimpleLibraryDefinition(
        imports = listOf(
            "org.springframework.boot.*",
            "org.springframework.boot.autoconfigure.*",
            "org.springframework.web.bind.annotation.*",
            "org.springframework.context.annotation.ComponentScan",
            "org.springframework.context.annotation.Configuration"
        ),
        dependencies = listOf(
            "org.springframework.boot:spring-boot-starter-web:2.7.9"
        )
    )
)

即可以自动添加 Spring 的依赖和 Import 信息,就可以支持步骤的 Hello, World 方式。除了 Spring,我们还需要其它的库的 magic。

最后,再使用 WebSocket 暴露出这个接口,以提供给 ChatFlow 使用。

抽象、简化设计 ← 循环

当然了,只是有一个 hello, world 是不够的,所以我们需要更多的例子,诸如于接入数据库。而由于 Spring 的扫描机制影响,外加我们并不想(主要是不会)针对 Spring 做太多的特化,所以我们换成了 Kotlin 里 Kotr 框架。

PS:值得注意的是,我们还需要对框架进行抽象,但是 Ktor 对我们预期的好一点。所以,我们的第二个版本来了:

%use kotless
%use exposed

data class User(val id: Int, val username: String)

class Server : KotlessAWS() {
    override fun prepare(app: Application) {
        Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver")

        transaction {
            SchemaUtils.create(Users)
        }

        app.routing {
            post("/register") {
                val user = call.receive<User>()
                val id = transaction {
                    // Insert the new user into the database
                    Users.insert {
                        it[username] = user.username
                    } get Users.id
                }

                val newUser = User(id, user.username)
                call.respond(newUser)
            }
        }
    }
}

object Users : org.jetbrains.exposed.sql.Table("users") {
    val id = integer("id").autoIncrement()
    val username = varchar("username", 50).uniqueIndex()

    override val primaryKey = PrimaryKey(id, name = "PK_User_ID")
}

在这个版本里,我们使用了 Exposed 作为数据库的 ORM,使用 H2 作为数据库。当然,要拿这个代码作为 Unit 还差了 10% 的距离,不过,基本上已经可以解决大部分的 CRUD 场景。

PS 1:这里的 KotlessAWS 只是一个 AWS Serverless 的抽象,并不影响我们的操作,我们可以直接封装一个 UnitMesh 的类,就是懒。

PS 2:我们只需要通过静态分析拿出 routing 中的代码,再优化即可。更多的探索过程代码可以见:_samples

一个真实世界的 Prompt

现在,让我们来结合 AI 跑一下:

请帮我使用 Ktor + Kotlin + Exposed 实现一个用户注册的 RESTful API,要求如下:

- 涉及到数据库的地方,请直接使用 Database.connect。
- 只返回核心逻辑,并写在 Server 类里,我要部署在 Serverless 服务器里。
- 请使用 Kotlin DSL 的方式编写代码。
- 不返回其它的无关代码,如:注释、依赖、import 等。

最后,你只返回类的代码,返回格式如下:
class Server : KotlessAWS() {
    override fun prepare(app: Application) {
        Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver", user = "root", password = "")
        transaction {
            SchemaUtils.create(Users)
        }

        app.routing {
            {{{}}}
        }
    }
}

人生苦短,欢迎加入我们的 Watchlist,一起讨论未来。

Join Waitlist

狗头,现在 Waitlist 工程师们,你可以就加入 Unit Mesh 的 Watchlist:

https://github.com/prompt-engineering/unit-mesh

理解 Prompt:基于编程、绘画、写作的 AI 探索与总结

这内容主要是结合我过去擅长的编程、写作、绘画展开的:

  1. 绘画:text 2 image。结合 Stable Diffusion 讲述一张图片的演化。
  2. 写作:chat 2 article。结合 ChatGPT 讲述围绕特定主题的内容创作。
  3. 编程:comment 2 code。结合 GitHub Copilot 如何加速日常的编程活动。

总体来说,AI 相当于美国队长的实力,遇强则强,遇弱则弱,关键其实在于 Prompt。所以,如何更好地完善 prompt,实现 prompt 工程就是:如何更好地使用 AI 的基础。

Prompt Enginneering

Prompt 是一种文本输入方式,用于与自然语言处理模型(例如GPT)进行交互。Prompt 是一个短语或一句话,可以启动模型并产生与 Prompt 相关的输出。这个输出可以是一个单独的单词、一句话,或者是一段完整的段落。它可以控制模型的行为,使其产生特定的结果。

举个例子,对于一个语言模型,prompt 可以是 "The cat sat on the",模型可以通过对接下来的词语进行预测,生成类似于 "mat"、"chair"、"sofa" 等不同的输出:

The Cat

上图为 Stable Diffusion 生成 (Prompt: The cat sat on the , Steps: 30, Sampler: Euler a, CFG scale: 7, Seed: 234310862, Size: 512x512, Model hash: d8722b4a4d, Model: neverendingDreamNED_bakedVae)

Prompt 在人工智能语言生成领域中扮演着重要的角色,因为它可以帮助模型更好地理解用户意图,并生成更准确、有意义的文本内容。 诸如于如下的 prompt

women back view without face, flowing dress, edge of the sea, backview, back turned to the camera, upon the glow of the setting sun, sun below the horizon, golden light over the water, hair sways gently, Chinese style clothes, black hair,

可以在 Stable Diffusion 生成图片(配置了 negative prompt):

所以,如何更好地完善 prompt,实现 prompt 工程就是:如何更好地使用 AI 的基础?

图形生成:如何描述一张照片??

人类需要思考第一个问题:如何描述一张照片?在不关心感受的情况下,我们关注于主体、背景、颜色、构图等。

PS:感谢我老婆提供的照片。

现在,我们脑海中出现一个场景:

接着,用文本来抽象它。在这里,我结合一下 Stable Diffusion 来讲述一下过程。

文本描述:第一个示例

生成 prompt:

  • phodal: 我想写一段话描述一张照片,帮我美化一下:一年轻女子穿着裙子,背对着镜头,在海边看着日落,太阳已经到地平线下面了,她的头发在风中飘动。
  • ChatGPT: 这张照片中的场景令人心醉,一位年轻女子穿着一条裙子,站在海边,背对着镜头,凝视着地平线下的夕阳。太阳已经沉入了海面,一片金黄色的光芒在海面上弥漫,照亮了整片海洋,仿佛这个时刻被定格在了时间的某个角落里。女子的头发随着微风轻轻飘动,轮廓被余晖映照得柔和而温暖。她的目光凝视着远方,仿佛在思考着生命的意义和无尽的未来。这张照片中蕴含着一种深刻的内涵,让人感受到时间和自然的力量,也让人思考着自己在这个宏大而美丽的世界中的角色和意义。

如果转换成英语的话:

A young woman wearing a flowing dress stands at the edge of the sea, her back turned to the camera as she gazes upon the glow of the setting sun. The ocean stretches out before her in all its grandeur, and the sun slowly sinks below the horizon, casting a golden light over the water. The woman's hair sways gently in the breeze, her features softened by the warm hues of the sunset. Her gaze is fixed on the distance, as if pondering the meaning of life and the endless possibilities of the future.

重复了 N 次之后,你会得到你想要的成功的照片,当然也会有失败的出现(因为没有配置 negative prompt 过滤失败的情况):

Failure

但是,熟悉各种搜索引擎的关键词的或者 NLP 的你,肯定知道,上面的大部分可能是废话,可以减化为如下的词,并添加一些专用的模型词汇,如black hair, hand before body, no hand, bodycon dress

women back view without face, flowing dress, edge of the sea, backview, back turned to the camera, upon the glow of the setting sun, sun below the horizon, golden light over the water, hair sways gently, Chinese style clothes, black hair,

随后,添加一些反向 prompt,意思是不要这些内容,诸如于 AI 不擅长的:bad handsmorbid 等等。随后,不断调整 prompt,比如生成更好的 Prompt:

诸如于采用 Magic Prompt

精准控图:结合 ControlNet

由于生成的姿势是随机的、无法控制,所以我就引入了 ControlNet 插件 ——用来实现骨骼绑定、精准控线、线稿上色、依据深度图结构透视精准重绘等。现在,就能结合我们做的创作来完善生成的图形,诸如于我们绘制草图、或者输入一张原始图片,就可以生成我们预期的效果:

最后,生成的图片如下:

Openpose

在我写完文章的时候,又修改了一下 prompt:

women back view without face standing on the sandy beach, bodycov full skirt, edge of the sea, back turned to the camera, upon the glow of the setting sun, black hair, sunset red to blue gradient sky

对应的 negative prompt 是:

(((simple background))),monochrome ,lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry, lowres, bad anatomy, bad hands, text, error, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry, ugly,pregnant,vore,duplicate,morbid,mut ilated,tran nsexual, hermaphrodite,long neck,mutated hands,poorly drawn hands,poorly drawn face,mutation,deformed,blurry,bad anatomy,bad proportions,malformed limbs,extra limbs,cloned face,disfigured,gross proportions, (((missing arms))),(((missing legs))), (((extra arms))),(((extra legs))),pubic hair, plump,bad legs,error legs,username,blurry,bad feet

效果凑合着,还是不错的。然后,我们可以做更多的尝试,配合一下参数调整(俗称炼丹):

也可以结合 inpaint 对失真的部分进行修复。

更多的模型集:与二次元世界的照片(18 禁)

众所周知,AI 的生成质量是与模型息息相关的,所以好的质量需要有好的模型。

我们可以在 https://civitai.com/ 上找到更多的模型,不过因为年龄限制等原因,只建议你在安全的场所打开,不建议在公共场所打开。

真实的人物由于版权的种种原因,所以这个软件在二次元世界相当的流行。如下是常见的提示词来描述模型的质量,里面可能包含一些不适词语,为了体现真实世界,这里并不打算屏蔽。

提示词 :

modelshoot style, (wavy blue hair), ((half body portrait)), ((showing boobs, giant boobs, humongous breasts)), (( beautiful light makeup female sorceress in majestic blue dress)), photo realistic game cg, 8k, epic, (blue diamond necklace hyper intricate fine detail), symetrical features, joyful, majestic oil painting by Mikhail Vrubel, Atey Ghailan, by Jeremy Mann, Greg Manchess, WLOP, Charlie Bowater, trending on ArtStation, trending on CGSociety, Intricate, High Detail, Sharp focus, dramatic, photorealistic, black background, epic volumetric lighting, fine details, illustration, (masterpiece, best quality, highres), standing in majestic castle

负面提示词

(((simple background))),monochrome ,lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry, lowres, bad anatomy, bad hands, text, error, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry, ugly,pregnant,vore,duplicate,morbid,mut ilated,tran nsexual, hermaphrodite,long neck,mutated hands,poorly drawn hands,poorly drawn face,mutation,deformed,blurry,bad anatomy,bad proportions,malformed limbs,extra limbs,cloned face,disfigured,gross proportions, (((missing arms))),((( missing legs))), (((extra arms))),(((extra legs))),pubic hair, plump,bad legs,error legs,username,blurry,bad feet, lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry

足够丰富的描述,可以帮助 AI 理解描述我们的需求:

Better Quality

上图为 AI 生成,也是同样场景下,不屏蔽的结果 —— 其原因是大部分的模型库加入了很多 18 禁的内容。

小结

现在,让我们来思考一下,如何描述一张带人物的图片:

  • 详细的描述词。
  • 人物的姿势。
  • 丰富的模型。
  • 反复修改的参数。
  • 持续迭代。(基于 InPaint 等进行修改)

那么,文章呢?

文章:如何围绕特定主题思考?

结合一下先前 Stable Diffusion 的无数次的失败经验,我们应该先找好一个合适的框架来跑 ChatGPT。

在编写内容的内容,我们会有各种思路和原则:STAR、金字塔原理、5W1H等等。

我们可以以 STAR (Situation(情景)Task(任务)、Action(行动)和 Result(结果))可以作为与 ChatGPT 会话的基础:

  1. 描述一个情境或背景,使读者对问题有更好的了解。
  2. 说明任务或目标,告诉读者需要完成什么。
  3. 描述你所采取的行动,例如使用什么方法、工具、策略等等。
  4. 解释结果,包括遇到的挑战和取得的成果。

这样,对于我们展开思路会有一些帮助。

情境:如何用 ChatGPT 写一篇文章,以如何用 ChatGPT 写一篇文章?

phodal:我想写一篇文章,主题是《如何用 ChatGPT 写一篇文章,以如何用 ChatGPT 写一篇文章 》

示例:

但是,这样的内容并非我们想要的。

而在这个标题里,其实 ChatGPT 已经理解了,如何写一篇文章,换作是一个冷门的话题,他就不知道了。在此时,我们需要构建一个更丰富的上下文:

关注点驱动

在这时,ChatGPT 给出的步骤依旧是错的,我们需要继续纠正。所以,我们可以考虑在一开始给一个完整的上下文:

构建更完整的上下文

对于 ChatGPT 来说,我们需要明确的 prompt::


我想写一篇文章,主题是《如何用 ChatGPT 写一篇文章,以如何用 ChatGPT 写一篇文章》,文章的大纲是:

1. 先用 100 字介绍一下 ChatGPT
2. 以 Julia 作为可以以具体的案例为例,介绍如何使用 ChatGPT 写一篇博客大赛的参赛作品。这包括如何确定主题、如何撰写开头和结尾、如何组织文章结构等等。主题是:《Julia Dataframe 在大数据分析领域的使用》,大纲中包含:用 200 字介绍一下 Julia 语言历史;介绍如何使用 DataFrames.jl,并有个代码示例;对比一下 Python Pandas、Apache Spark Dataframe;小结。
3. 对本文的内容进行总结,并再次强调使用 ChatGPT 进行写作的好处。

帮我写一篇文章?

如此一来,我们提供的上下文就会更加准确,就可以看到一个范例:

Demo

基于这个范例,可以思考我们的内容如何编写。

构建框架(frameworks)

ChatGPT 本身构建了一个框架,我们可以再输入一个有用的框架,以构建简单的反馈回路。于是,我开始教他 DDD,然后相互学习:

DDD Copilot

不过,ChatGPT 和上面的图片一样,经常丢失一些上下文。

phodal: 在这个 TicketOrder 聚合里,他应该包含哪些 API ?

在 TicketOrder 聚合里,可以包含以下一些 API:

  1. createOrder():创建一个新的订单,可以接收顾客信息、场次信息、座位信息等参数,并返回一个唯一的订单号。
  2. cancelOrder(orderId):取消某个订单,需要提供订单号。
  3. getOrder(orderId):获取某个订单的详细信息,需要提供订单号。
  4. reserveSeats(showTimeId, seatIds):在某个场次中预留座位,需要提供场次号和座位号。
  5. confirmOrder(orderId):确认订单,此时座位被锁定并且订单状态被改为已确认。
  6. payOrder(orderId, paymentInfo):支付订单,需要提供订单号和支付信息。
  7. refundOrder(orderId):退票,需要提供订单号并且只能在订单状态为已确认时进行。
  8. updateOrder(orderId, updateInfo):修改订单信息,可以更新顾客信息、场次信息、座位信息等,需要提供订单号和更新内容。

基本的 API 可以满足电影院订票业务的需求,但具体实现还需要根据业务场景和需求进行细化和调整。

其它小建议:

相似的,与 Stable Diffusion 类似的是,AI 生成的内容经常出现各种问题,所以我们要注意:

  1. 保持自然的表达。ChatGPT 能够生成流畅的语句,但有时可能会出现奇怪或不合适的表达。在使用 ChatGPT 进行写作时,需要注意保持自然的表达方式。
  2. 适时修正错误。如果 ChatGPT 生成的内容不准确或有错误,应该及时进行修正,以保证文章的准确性和可读性。
  3. 多次修改和调整。ChatGPT 的输出结果虽然会自动进行语法纠错和编辑,但是为了保证文章质量,还是需要经过多次修改和调整,不断完善文章结构和内容。
  4. 保持简洁明了。在使用 ChatGPT 进行写作时,需要尽量保持简洁明了,避免使用过于复杂的句式和词汇,以便读者更好地理解文章内容。
  5. 注重原创性和独特性。尽管 ChatGPT 能够生成大量的文章,但是为了在博客大赛中脱颖而出,还需要注重文章的原创性和独特性,尽可能地突出自己的风格和思想。

考虑到 ChatGPT 的代码并不那么准确,让我们使用 GitHub Copilot 生成更多的内容。

代码生成:GitHub Copilot

GitHub Copilot 是 GitHub 2021 年 8 月推出的一个 AI 代码生成工具,它可以根据你的代码,生成更多的代码。

Samples

所以,对于 Markdown 本身来说,它也能提供一些帮助。当然了,在这方向它没有 ChatGPT 那么强大,经常会出现一些奇怪的重复代码。

但是,也并非不可用,我们可以使用它来生成一些代码片段,以便于我们进行修改。

完整示例:DDD + ChatGPT + Copilot

如结合 Chat GPT 我们可以完成代码的前置设计(当然有些不靠谱):

ChatGPT DDD

复制生成的 API,写行注释告诉 Copilot:convert to markdown table,然后我们就有了:

API描述HTTP方法URL参数返回值
创建订单创建一个新的订单POST/ticket-orders创建订单信息订单ID
查询订单查询一个已存在的订单GET/ticket-orders/{orderId}订单ID订单信息
取消订单取消一个已存在的订单DELETE/ticket-orders/{orderId}订单ID取消成功或失败信息
添加座位向一个已存在的订单添加座位POST/ticket-orders/{orderId}/seats座位信息座位添加成功或失败信息
移除座位从一个已存在的订单中移除座位DELETE/ticket-orders/{orderId}/seats/{seatId}座位ID座位移除成功或失败信息
查询可用座位查询某个场次电影的可用座位GET/cinema-schedule/{scheduleId}/available-seats场次ID可用座位信息

接下来,我们可以使用 Spring + Copilot 来生成一些代码片段,以便于我们进行修改:

github-copilot Sample

在有足够类相关信息的情况下,诸如于,我们提供一个注释:

@PostMapping
fun createTicketOrder(@RequestBody ticketOrder: TicketOrder): String {
    // check the schedule is valid, the seats are available, then create the order

}

它生成的代码会更加准确:

GitHub sample 4

而如果我们添加了对应的 Exception 的处理注释:check the schedule is valid, the seats are available, then create the order, if the schedule is not valid, throw NotScheduleAvailableException,代码进一步转化为:

@PostMapping
fun createTicketOrder(@RequestBody ticketOrder: TicketOrder): String {
    // check the schedule is valid, the seats are available, then create the order, if the schedule is not valid, throw NotScheduleAvailableException
    // check the schedule is valid
    val schedule = cinemaScheduleService.getSchedule(ticketOrder.scheduleId)
    if (schedule == null) {
        throw NotScheduleAvailableException()
    }
    // check the seats are available
    val availableSeats = cinemaScheduleService.getAvailableSeats(ticketOrder.scheduleId)
    if (!availableSeats.containsAll(ticketOrder.seats)) {
        throw NotSeatAvailableException()
    }
    // create order
    return tickerOrderApplicationService.createOrder(ticketOrder)
}

在另外一些场景下,如果你的函数名和参数名都是英文,那么 Copilot 生成的代码也会更加准确:

GitHub sample 5

而在单元测试的场景下,Copilot 更加强大和智能,它能够根据你的测试代码,生成更多的测试代码。

其它:代码生成开源模型

我也尝试了下 Saleforce 的代码模型:https://huggingface.co/Salesforce/codegen-6B-mono,但是不能在我的 MBP 上跑上来,有些遗憾。

总结:AI 就像占卜

你可能无法预期输出的是啥,总会有各种惊喜。

而其实,与 AI 的 prompt 相似的是:和人类聊天的时候,我们也需要提供足够的上下文。所以,你需要一下 ChatGPT,他并不总是能理解你的上下文。

其它:Macbook Pro 的吐槽

公司配置的 Macbook Pro 2019 虽然有 AMD 显卡,但是没有 CUDA、ROCm 的支持,所以只能用 CPU 来跑了。

AI 生态不友好

诸如于 PyTorch,以前是支持 ROCm 加速的,现在的版本不行了:https://pytorch.org/get-started/locally/

本文相关资源

AI 生成图片:

代码模型相关:

ControlNet 加强:

Prompt 编写模式:如何将思维框架赋予机器

模式要素

省去几千字

如果您指的是 AI 领域中的 prompt 模式,它通常是指一种输入-输出的数据格式,用于训练和评估机器学习模型。下面是一个完整的定义:

  • 模式名称(Pattern Name):Prompt 模式
  • 问题描述(Problem):如何准备训练数据,以便用于机器学习模型的训练和评估。
  • 解决方案(Solution):Prompt 模式是一种输入-输出数据格式,它由一个输入文本和一个输出文本组成。输入文本是一个问题或指令,输出文本是模型预测的答案或结果。通过使用这种格式,可以减少训练数据的需求量,提高模型的泛化性能,同时也使得模型的输出更易于理解和解释。
  • 效果(Consequences):使用 Prompt 模式可以简化训练数据的准备过程,提高模型的效率和准确率,同时也增加了模型的可解释性和可理解性。
  • 适用性(Applicability):Prompt 模式适用于自然语言处理领域中的各种任务,如文本分类、情感分析、问答系统、机器翻译等。它也可以用于其他领域中需要使用自然语言作为输入和输出的任务。
  • 结构图(Structure):Prompt 模式的结构由一个输入文本和一个输出文本组成,它们被定义为模型的输入和输出。通常,输入文本包括一些关键词或短语,用于指定模型需要执行的任务或操作,而输出文本则是模型的预测结果。
  • 参考(References):相关的文献包括 "GPT-3: Language Models are Few-Shot Learners", "Zero-Shot Learning - A Comprehensive Evaluation of the Good, the Bad and the Ugly" 等。常用的机器学习框架包括 TensorFlow,PyTorch 等。

核心思想:概念与类比

开始之前,可以看一下这个问题示例:

  1. 设计模式的要素是哪些?
  2. 对于 AI 领域的 prompt 编写来说,我们通常使用的模式有哪些?
  3. 能将 AI 领域的 prompt 常见的设计模式用 "设计模式要素" 的格式一一表达吗?

核心思想,将设计模式要素作为一个概念,让 AI 类比到 prompt 里的模式。详细见:

  1. design-pattern.analogy
  2. design-pattern.analogy2

当然了,类比和定义概念不一定都会成功。

基础模式

四种基础模式:

  • 特定指令(By specific:在这种模式下,我们给模型提供一些特定信息,例如问题或关键词,模型需要生成与这些信息相关的文本。这种模式通常用于生成答案、解释或推荐等。特定信息可以是单个问题或多个关键词,具体取决于任务的要求。
  • 指令模板(Instruction Template):在这种模式下,我们给模型提供一些明确的指令,模型需要根据这些指令生成文本。这种模式通常用于生成类似于技术说明书、操作手册等需要明确指令的文本。指令可以是单个句子或多个段落,具体取决于任务的要求。
  • 代理模式(By proxy):在这种模式下,可以充当了一个代理,代表某个实体(例如人、角色、机器人等)进行操作或交互。代理模式的核心思想是引入一个中介对象来控制对实际对象的访问,从而实现一定程度上的隔离和保护。诸如于在 ChatGPT 中,"act as xxx" 可以让 ChatGPT 充当一个代理,扮演某个角色或实体的身份,以此来处理与该角色或实体相关的任务或请求。
  • 示例模式(By demonstration):在这种模式下,我们给模型提供一些示例文本,模型需要生成与示例文本类似的文本。这种模式通常用于生成类似于给定示例的文本,例如自动生成电子邮件、产品描述、新闻报道等。示例文本可以是单个句子或多个段落,具体取决于任务的要求。

特定指令(By specific)

Prompt Enginneering

在这种模式下,我们给模型提供一些示例文本,模型需要生成与示例文本类似的文本。这种模式通常用于生成类似于给定示例的文本,例如自动生成电子邮件、产品描述、新闻报道等。示例文本可以是单个句子或多个段落,具体取决于任务的要求。

翻译告诉我,以我们的开头来说:

  • 定义一下 prompt 工程

类似的场景还可以有:

  • 翻译一下:永和九年,岁在癸丑,暮春之初,会于会稽山阴之兰亭,修禊事也。
  • 转为现代汉语:永和九年,岁在癸丑,暮春之初,会于会稽山阴之兰亭,修禊事也。

对应的,还有一系列的子模式

子模式

如我们通过下面的 prompt 转换了 ChatGPT 输出的子模式:

转化为 markdown 的 """markdown {} """ 表格形式,其中的字段为英语模式、中文、简述、示例。

表格示例:

英语模式中文简述示例
Completion-based补全型用户提供部分文本,AI 根据上下文生成建议用户输入“我想买一件…”,ChatGPT 生成“红色连衣裙”
Classification-based分类型用户提供问题或任务描述,AI 生成答案用户输入“如何做巧克力蛋糕?”ChatGPT 生成“将巧克力蛋糕放入预热好的烤箱中烤25-30分钟。”
Generation-based生成型用户提供初始信息,AI 生成新文本用户输入“科技创新”,ChatGPT 生成“人工智能是科技创新领域的重要方向之一。”
Translation-based翻译型用户提供文本,AI 进行翻译用户输入“Hello”,ChatGPT 生成“你好”
Question-answering问答型用户提供问题,AI 生成答案用户输入“什么是机器学习?”,ChatGPT 生成“机器学习是一种人工智能的分支。”
Conversation-based对话型用户与 AI 进行对话,AI 生成回答用户输入“今天天气怎么样?”ChatGPT 生成“今天天气晴朗,温度适宜。”
Summarization-based摘要型用户提供文本,AI 生成文本摘要用户输入一篇文章,ChatGPT 生成该文章的摘要

其重点是:Output Driven

在提示的开头包含所有相关信息,包括任何上下文或期望的结果,以及 长度,格式样式等。诸如于:用 100 字总结一下 ChatGPT

指令模板(Instruction Template)

Instruction

在这种模式下,我们给模型提供一些明确的指令,模型需要根据这些指令生成文本。这种模式通常用于生成类似于技术说明书、操作手册等需要明确指令的文本。指令可以是单个句子或多个段落,具体取决于任务的要求。

如下是一个示例:

使用 STAR 原则与下面的格式总结一下这段话:

"""
最近几天,因为工作 + 兴趣的原则,我在研究用 text 2 image 来展示如何演进 prompt、用 text 2 article 来展示充足 prompt 的优点、结合 GitHub Copliot 来实现 prompt 完成工作。然后,我在本地部署了一个 Stable Diffusion、使用 ChatGPT 写作展示 text 2 article、在 IDEA 中使用 GitHub Copilot。最后,得到了多篇文章和好多的文章阅读量、几个 Jupyter 代码段、以及一些经验。
"""

情境(Situation): 
任务(Task): <comma_separated_list_of_task>
行动(Action):  -||-
结果(Result):  -||-

输出文本时要注意中英文之间插入空格,留出间隔,诸如于 """Hello 你好""",用这种方式输出一下刚才的文本:

情境(Situation): 最近几天,在工作和兴趣的驱动下,研究如何使用 text 2 image 和 text 2 article 展示 prompt 演进以及结合 GitHub Copliot 来完成工作。

任务(Task):

    研究如何使用 text 2 image 展示 prompt 的演进过程
    研究如何使用 text 2 article 展示 prompt 的优点
    结合 GitHub Copliot 来实现 prompt 完成工作

行动(Action):

    本地部署了一个稳定的 Diffusion 模型
    使用 ChatGPT 撰写文章来展示 text 2 article 的效果
    在 IDEA 中使用 GitHub Copilot

结果(Result):

    得到了多篇文章和很多的文章阅读量
    获得了几个 Jupyter 代码段以及相关经验

最佳实践

  • zero-shot、few-shot 优先,最开始可以不给示例(因为示例可能是错的),看任务执行情况,再提供少量样本,如果不行,再对模型进行微调。

代理模式(By proxy)

Proxy

Proxy 模式是指用户可以要求 ChatGPT 以特定的身份、角色或者身份扮演某个特定的人、角色或对象来生成回答。这种模式通常用于模拟某个特定人物的语言风格和语境,生成特定情境下的对话、回答或其他形式的文本。

例子 1:公司客服

当使用 ChatGPT 来代替某个人或实体时,可以使用 by proxy 模式。

例如,可以使用以下 prompt 提问 ChatGPT 代替某个公司的客服:

我是一家餐饮公司的客服,有顾客反馈说我们的菜品质量有问题,我们该怎么处理呢?

在这种情况下,ChatGPT 会扮演客服的角色,并根据提示来回答,从而帮助公司解决问题。ChatGPT 可以使用类似以下的方式来回答:

您好,很抱歉听到您的反馈。我们将会仔细审核您的反馈,并尽快采取相应措施来解决这个问题。为此,我们需要更多的信息来进一步了解您的反馈,包括哪些菜品有问题,具体问题是什么等。请问您可以提供更多的信息吗?

例子 2:终端计算机

这种方式可以通过让 ChatGPT 扮演某个实体的角色,例如客服、销售代表等等,来帮助用户解决问题。

Awesome ChatGPT Prompts 中:

Human: Act as a Linux Terminal

Robot:

ChatGPT: I want you to act as a linux terminal. I will type commands and you will reply with what the terminal should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. When I need to tell you something in English, I will do so by putting text inside curly brackets {like this}. My first command is pwd

示例模式(By demonstration)

在这种模式下,我们给模型提供一些特定信息,例如问题或关键词,模型需要生成与这些信息相关的文本。这种模式通常用于生成答案、解释或推荐等。特定信息可以是单个问题或多个关键词,具体取决于任务的要求。

示例:

任务表述 颜色代表了温度
例子1 绿色代表寒冷 
例子2 蓝色代表寒冷 
例子3 红色代表温暖 
例子4 黄色代表温暖
执行 橙色代表什么

上图为 Cohere AI 官网的示例图,对应的聊天记录如下:

English: Writing about language models is fun.
Roish: Writingro aboutro languagero modelsro isro funro.
English: The weather is lovely!
Roish:

增强 Prompt

符号化模式

Symbol

符号化方法通常通过定义符号、符号之间的关系以及基于这些关系的规则来表示知识。

简单来说,我们可以创建一个符号来表示特写的规则,诸如于:

"""
我们来玩一个名为 gkzw 的写作游戏,每当我说 gkzw,你开始写作,规则如下:

1. 字数不少于 200 字。
2. 文中必须出现 "小明"。

明白了吗?
"""

围绕这个模式,我们可以创建复制的规则。

反向 Prompt 模式

Negative prompt(负向提示)是一种在使用语言模型时,针对模型输出不希望的结果而设置的一种文本输入方式。通过使用负向提示,可以帮助模型避免输出不良、不准确或不恰当的文本。

具体来说,负向提示通常是以否定的形式呈现的,例如在文本生成任务中,使用“不要写...”、“不要提及...”、“不要使用...”等方式来限制模型生成不想要的文本。在情感分析任务中,使用“不是...”、“并不...”等方式来指示模型识别出一段文本的负面情感。在QA(问答)任务中,使用“不是...”、“不包括...”等方式来指示模型回答问题时避免输出错误或不准确的答案。

Negative Prompt 在 Stable Diffusion 被广泛使用,相似的,在 ChatGPT 中,我们也会经常使用到。

诸如于:

我们能玩一个名为 kfc 的谜语创作游戏,当我说 "kfc",你写一个谜题,要求:

1. 不少于 200 字
2. 谜题不能出现肯德基和星期四
3. 正文可以夹杂小语种语言,如 """他说:Ciao"""
4. 谜底必须是 ###原来今天是肯德基疯狂星期四!!###

格式类似于:

....

再生成模式

根据 AI 提供的信息,再让 AI 生成内容。

例子 1

当我们无法确定我们的 prompt 是否准确,便可以通过 AI 来帮我们丰富 prompt 。

如我在:基于编程、绘画、写作的 AI 探索与总结 的例了一样

  • phodal: 我想写一段话描述一张照片,帮我美化一下:一年轻女子穿着裙子,背对着镜头,在海边看着日落,太阳已经到地平线下面了,她的头发在风中飘动。
  • ChatGPT: 这张照片中的场景令人心醉,一位年轻女子穿着一条裙子,站在海边,背对着镜头,凝视着地平线下的夕阳。太阳已经沉入了海面,一片金黄色的光芒在海面上弥漫,照亮了整片海洋,仿佛这个时刻被定格在了时间的某个角落里。女子的头发随着微风轻轻飘动,轮廓被余晖映照得柔和而温暖。她的目光凝视着远方,仿佛在思考着生命的意义和无尽的未来。这张照片中蕴含着一种深刻的内涵,让人感受到时间和自然的力量,也让人思考着自己在这个宏大而美丽的世界中的角色和意义。

再去除掉不合适的词,进行润色,就可以再创作:

  • phodal:翻译成英语

然后再提炼直至扔至 Stable Diffusion 中:

women back view without face, flowing dress, edge of the sea, backview, back turned to the camera, upon the glow of the setting sun, sun below the horizon, golden light over the water, hair sways gently, Chinese style clothes, black hair,

而后,根据图片的结果,我们再反复修改。

控线模式

TODO:这里需要给出更精准的定义,如何让 ChatGPT 做到?

在没有推出 ControlNet,对于一个 Stable Diffusion 玩家,它需要反复地吟唱咒语,才能获取到满意的图案。

在有了 ControlNet 之后,我们可以创建一个 Openpose,或者是导入图片从图片生成 pose,相当于是给机器一个示例,而后生成的图片就会有令人满意的姿势。:

在 GitHub Copilot,我们可以通过设置输入和输出,结合函数名三个要素,Copilot 就能生成大致准确的代码:

GitHub Copilot 示例

而在必要的情况下,添加一下注释就能更完整了:

fun listAllDirInDir(dir: String): List<File> {
    // ignore hidden files
    
}

生成的代码会更贴近我们的需求。

概念模式集

概念抽象

对于 ChatGPT 尚不存在的概念,可以通过 Google 相关的内容之后,由 ChatGPT 进行总结。

根据下面的文本重新解释一下 ControlNet:

- 官网介绍:ControlNet is a neural network structure to control diffusion models by adding extra conditions.
- 模式:根据给的底图,生成一个类似建模效果(法线贴图)的中间图,再生成图片 
- 场景:用来实现骨骼绑定、精准控线、线稿上色、依据深度图结构透视精准重绘等。

结果:

根据给出的文本,ControlNet是一种神经网络结构,旨在通过添加额外条件来控制扩散模型。在特定场景下,ControlNet被用于生成类似建模效果(法线贴图)的中间图和相关的图像。这种技术可以被应用于多个领域,如骨骼绑定、精准控线、线稿上色、深度图结构透视精准重绘等。

具体来说,ControlNet技术可以被用来控制模型的扩散过程,从而提取特定的信息并生成新的图像。在骨骼绑定、线稿上色等场景中,ControlNet技术可以被用来增强图像,使其更加真实和生动。在依据深度图结构透视精准重绘的场景中,ControlNet技术可以被用来根据图像的深度信息,生成精确的重绘图像,从而提高图像的真实感和逼真度。

缩短:能用一句话解释一下 ControlNet 吗

结果:

ControlNet 是一种神经网络结构,通过添加额外的条件来控制扩散模型,从而实现图像增强、重构和生成等功能。

概念对齐

如在 ChatGPT 中对于自举的理解是不符合我们这里的需求:

Bootstrap

第一步,定义

phodal:自举是指就编译器可以自行编译自己的编译器。 实现方法就是这个编译器的作者用这个语言的一些特性来编写编译器并在该编译器中支持这些自己使用到的特性。

第二步,试探

phodal:将自举应用在游戏领域,应该是怎样的?

第三步,确认理解:

那么,抽象一下我们新定义的自举?

总结

最后一问:

自举

PS:mmp,他一定是故意的。

类比模式集(待定)

类比是指将一个事物或概念与另一个事物或概念进行比较,找出它们之间的相似之处,以此来推理或说明某个问题或情况。

模板方法

模板方法模式是一种行为型模式,它定义了一个操作中的算法骨架,将某些步骤延迟到子类中实现,从而使得子类可以在不改变算法结构的情况下重新定义算法中的某些步骤。

在接下来的例子中,我们会创建一个 muji 游戏中。在游戏的实现可以分为多个步骤,例如初始化游戏、生成随机数、获取用户输入、计算得分等等,而这些步骤可以通过模板方法模式来进行实现。

我们来玩一个编程游戏名为 wula,包含五个步骤:

第一步. 问题分析:每一轮游戏,你将看到一个以 "wula:" 开头的问题,你需要分析这个问题并简单介绍一下通常解决这个问题的方法。

第二步. 代码编写:你需要用 JavaScript 编写解决这个问题的代码,并输出对应的代码,并介绍一下你的代码(不少于 200 字)。

第三步. 代码执行:你需要作为 JavaScript Console 执行第二步写的代码,如果没有给出测试数据,你需要自己随机生成测试数据,并将这些数据输入到代码中进行计算。

第四步. 错误处理:如果你的代码存在错误或无法正常执行,你需要输出错误,并回到第二步重新开始游戏,直到你的代码能够正常工作。

第五步. 总结:你需要用不少于 100 字左右总结一下这个问题,以及你的解决方案,让其他人可以简单了解这个问题及其解决方法。

示例如下:

"""
wula: 头共10,足共28,鸡兔各几只?

简介:这是一个鸡兔同笼问题,{},

## 鸡兔同笼

// 计算鸡兔数量的函数
function calcAnimals(heads, legs) {
  const rabbitCount = (legs - 2 * heads) / 2;
  const chickenCount = heads - rabbitCount;
  return {"chicken": chickenCount, "rabbit": rabbitCount};
}

// 计算鸡兔数量
const result = calcAnimals(10, 28);

// 输出结果
console.log(result);

代码的输出结果是:{}

## 总结

{}

"""

明白这个游戏怎么玩了吗?

在这个游戏里,我们结合了几种不同的模式:

  1. Instruction:让 ChatGPT 创建了一个名为 wula 的游戏,并定义了游戏的步骤。
  2. Specific:让 ChatGPT 用 JavaScript 编写一个程序
  3. Proxy:让 ChatGPT 作为 JavaScript Console 执行程序,并返回结果。
  4. Specific:让 ChatGPT 做总结
  5. Demonstration:提供了一个示例,让 ChatGPT 理解游戏的步骤。

自举模式

自举(Bootstrapping)的核心思想是利用一组基础工具和材料来构建和生成一个新的工具或系统,从而逐步替代掉原有的基础工具和材料。在这个过程中,新的工具或系统会逐渐变得更为高效和强大,从而实现对原有基础工具和材料的完全替代。

如下图所示:

Bootstrap

TODO:重新解释,上图出自:《Bootstrapping in Compiler Design

先看例子 1:文章

例子 2:Wula 2.0

> wula:创作一个新游戏名为 muji,并解释一下这个游戏:"""类似于 wula,可以做简单的图形计算,如体积、面积等。这个游戏还能把解决过程解释清楚,拥有有可运行的 Python 代码,最后的输出结果是一篇文章。"""

Muji Sample

拆解模式

对于复杂问题,ChaGPT 无法一次性输出全部内容,需要进行拆解。

已知问题:ChatGPT 当前版本会丢失上下文。

我们来定义一下 DDD 游戏的步骤,一共有 6 个步骤,步骤如下:

"""
第一步. 拆解场景。分析特定领域的所有商业活动,并将其拆解出每个场景。
第二步. 场景过程分析。选定一个场景,并使用 "{名词}已{动词}" 的形式描述过程中所有发生的事件,其中的名词是过程中的实体,其中的动词是实体相关的行为。
第三步. 针对场景建模。基于统一语言和拆解出的场景进行建模,以实现 DDD 设计与代码实现的双向绑定。
第四步. 持续建模。回到第一步,选择未完成的场景。你要重复第一到第四步,直到所有的场景完成。
第五步. 围绕模型生成子域。对模型进行分类,以划定不同的子域,需要列出所有的模型包含英语翻译。
第六步. API 生成。对于每一个子域,生成其对应的 RESTful API,并以表格的形式展现这些 API。
"""

需要注意的是,当我说 """ddd 第 {} 步: {}""" 则表示进行第几步的分析,如 """ddd 第一步: 博客系统""" 表示只对博客系统进行 DDD 第一步分析。我发的是 """ddd: {}""",则表示按 6 个步骤分析:

明白这个游戏怎么玩了吗?

完整过程见:DDD Sample

其它

人类如何思考问题?

人类相对于其他动物更擅长于类比、概念抽象、符号化等高级认知活动,这些认知活动可以帮助人类在面对新问题时,从已有的知识和经验中找到相似的部分,快速理解和解决新问题。

而对于机器来说,机器学习算法通过大量的数据和计算,学习到数据中的规律和模式,并将这些规律和模式应用到新的数据中,从而实现预测和决策等功能。例如,机器学习算法可以通过大量的图像数据学习到图像的特征,并在新的图像中识别出相应的物体;也可以通过大量的自然语言数据学习到语言的规律,从而生成自然语言文本。

相关资源

本文相关的模式图片参考来源主要是:Prompt Engineering

Practise

参考:Best practices for prompt engineering with OpenAI API

相关资源 Prompt Engineering

入门

Code

安全问题

相关文章

Prompt Patterns 模板

回忆一下我们的高考作文题:

阅读下面的材料,根据要求写作。(60分)

  人们因技术发展得以更好地掌控时间,但也有人因此成了时间的仆人。

  这句话引发了你怎样的联想与思考?请写一篇文章。

要求:选准角度,确定立意,明确文体,自拟标题;不要套作,不得抄袭;不得泄露个人信息;不少于800字。

这部分的文字非常易于我们理解,它由以下几部分构成:

  • 指令(Instruction):阅读下面的材料,根据要求写作。
  • 要求(Requirements):选准角度,确定立意,明确文体,自拟标题;不要套作,不得抄袭;不得泄露个人信息;不少于800字。
  • 素材:人们因技术发展得以更好地掌控时间,但也有人因此成了时间的仆人。

Prompt 编写模板

通常来说,一个易于 LLM 理解的模板可以由以为几部分构成:

  • 角色(Role):解释这个模板的使用场景和目的。
  • 指令(Instruction):告诉 GPT-3.5 执行的具体任务或要求。
  • 要求(Requirements):对 GPT-3.5 生成的结果的期望和要求,例如长度、主题、语言风格等。
  • 示例(Sample):提供一个或多个示例,以便 GPT-3.5 理解任务的上下文和生成的结果的期望。
  • 扩展数据(Extended Data)(可选,编程时需要):提供一些额外的数据,例如词汇、语法结构、语言模型等,以帮助 GPT-3.5 生成更加丰富、准确的结果。

一个简单的示例如下:

角色(Role):你是一位旅游公司的营销人员,需要撰写一篇旅游目的地介绍的文本。

指令(Instruction):请使用 GPT-3.5 生成一篇介绍文本,其中包括目的地的名称、位置、历史和文化背景、景点、美食、住宿和交通等相关信息。

要求(Requirements):生成的文本长度应为 500-1000 个单词,语言风格应为正式、客观、流畅,包含准确的地理、历史和文化知识。

示例(Sample):请以以下地点为例,生成一篇介绍文本:福建漳州。

扩展数据(Extended Data):可以提供一些关于福建、漳州的历史、文化、地理等方面的数据,以帮助 GPT-3.5 生成更加准确、详细的文本。

输出示例:

漳州,位于福建省东南沿海,是一个具有悠久历史和丰富文化的城市。作为福建省的一个重要城市,漳州拥有独特的地理位置和文化底蕴,是一个备受游客青睐的旅游目的地。

漳州的历史可以追溯到公元前三千年左右的新石器时代,是中国历史上一个非常重要的文化和商业中心。在宋、元、明、清等历史时期,漳州都是一个繁荣的商贸城市,曾经是丝绸之路的海上起点之一。在漳州,游客可以看到许多保留完好的历史遗迹,如南靖土楼、东山古城等。

漳州的文化底蕴非常深厚,它是闽南文化的代表之一。在漳州,游客可以领略到许多独特的文化风情,如闽南建筑、福建茶文化、福建南音等。此外,漳州还是福建省的一个重要的宗教中心,拥有众多的寺庙和教堂,如南山寺、开元寺等。

漳州拥有许多著名的景点,如南靖土楼、天柱山、东山古城、南山寺等。其中,南靖土楼是漳州最著名的景点之一,它是闽南土楼的代表之一,被誉为“中国传统建筑艺术的瑰宝”。天柱山是漳州的一个自然保护区,拥有丰富的自然景观和独特的生态环境,是一个极具观赏价值的景点。

漳州的美食也非常有名,如海鲜、福建菜等。漳州的海鲜非常新鲜,口感鲜美,深受游客的喜爱。福建菜是中国八大菜系之一,以其独特的口味和烹饪方法而著名。

对于住宿和交通,漳州也有很好的配套设施。漳州有许多高档酒店和旅游胜地,如漳州嘉禾智选假日酒店、漳浦金海岸度假酒店等。此外,漳州还有许多交通工具,如高速公路、铁路、机场等,方便游客前往。

总之,漳州是一个迷人的旅游目的地,拥有丰富的历史和文化底蕴、独特的景点、美味的美食、高品质的住宿和交通,是一个值得游客来此一游的好地方。

当然了,GPT 是生成式的,所以在这时往往没有根据 "事实" 来生成内容,所以需要你添加一些额外的数据,以帮助 GPT 生成更加丰富、准确的结果。

除此,我们并不一定要按照这个模板来定义 Prompt,只要能够清晰地表达出上述几个要素即可。

Kotlin 代码实现示例

在 ArchGuard Co-mate 中,我们通过如下的 Kotlin 代码来实现 Prompt 模板的定义:

interface BaseTemplate {
    fun getRole(): String = ""
    fun getInstruction(): String = ""
    fun getRequirements(): String = ""
    fun getSample(): String = ""
    fun getExtendData(): String = ""
}

对应的一个实现示例:

class LayeredStylePrompt(
    val context: ComateContext,
    override val strategy: Strategy,
) : CodePromptStrategy {
    override fun getRole(): String = "Software Architecture"
    override fun getInstruction(): String =
        "根据下面的信息,分析项目的分层是否符合业内的通用规范?并绘制 Graphviz 图来表示。"

    override fun getRequirements(): String = """
1. 如果存在相互引用,请用红线展示出来。
2. 只展示重要的分层,不要展示过多的细节。
4. 结合分层、subgraph 的方式来表示分层。
4. 示例如下:

###
digraph G {
    rankdir=TB;
    node [shape=record, fontname=Helvetica];
    edge [color=black, penwidth=1.0];
    subgraph cluster_{} {
        label="{} Layer"
    }
###
"""

    override fun getExtendData(): String {
        val introduction = context.fetchReadmeIntroduction()
        return """$introduction

package fan in: ${context.fetchPackageDependencies()}
"""
.trimIndent()

    }

}

输出结果示例:

You're an Software Architecture,根据下面的信息,分析项目的分层是否符合业内的通用规范?并绘制 Graphviz 图来表示。Here is requirements: 
1. 如果存在相互引用,请用红线展示出来。
2. 只展示重要的分层,不要展示过多的细节。
4. 结合分层、subgraph 的方式来表示分层。
4. 示例如下:

```dot
digraph G {
    rankdir=TB;
    node [shape=record, fontname=Helvetica];
    edge [color=black, penwidth=1.0];
    subgraph cluster_{} {
        label="{} Layer"
    }

Project introduction: Co-mate is an AI-powered software architecture copilot, design and governance tools.

package fan in: {org.archguard.architecture=[org.archguard.architecture.layered], org.archguard.comate.cli...

Prompt 即代码:设计和管理 AI 编程的最佳实践

Prompt 即代码是一种基于多种输入模态的编程范式,它通过结合文本、图像、语音等多种输入方式来提供更丰富的上下文信息,帮助程序员更好地表达自己的意图,并生成相应的代码实现。Prompt 即代码将 prompt 作为代码的一部分,以及作为标准接口来定义生成的代码,同时提供注释和文档信息以支持可读性和可维护性。通过使用 prompt 即代码,程序员可以提高编码效率,同时生成更准确、更可靠的代码实现。

在不考虑采用诸如 Unit Mesh 新架构的情况下,我们需要在现有的架构和工程体系中融入 AI 编程。因此在这篇文章里,我们将回到现在 —— 此时此该,我们应该如何与 AI 编程共处,让 AI 成为我们的 Copliot。

引子 1:一年的 GitHub Copilot 感受:20% 的效率提升

相关的例子可以见: https://www.clickprompt.org/zh-CN/github-copilot-samples/

自打 GitHub Copilot 正式发布以后,作为知名的开源挖坑作者,拿到了免费的版本,但是只能偷偷使用。公司明令禁止我们在公司项目使用,所以只能在业余项目、开源项目中用用。

从个人的使用体验来说,AI 大概提升了我 20% 的编码效率 —— 大抵是我写的代码都比较偏门。但是怎么说呢,抄、fork、生成 的代码质量都 TM 的不行(有可能我是 Clean Code 的忠粉),所以 ,我经常吐槽说,你在垃圾库里是训练不出好的模型的。

所以,简单来说,对于效率提升是大大的,如果是 CRUD 效率更高,但是质量不行。反正,以后重写速度比重构更快,代码质量不重要(手动狗头)。

除此,从个人的感受来说,编写 Copilot 所需要的 Prompt 是需要学习门槛的。

引子 2:AI 编程的门槛

回到真实世界,我们要用好 AI 编码,需要考虑这个问题?

问题 1:AI 编程能取代人类编程吗?

不论是 GPT-4 发布会的 Demo,还是令大家惊艳的 Cursor.so 开发工具。就当前的 GPT 能力而言,谈编程消失还太早了。GPT 只是一个复读机,解决不了任何复杂的编程问题。复杂场景下,GPT 容易丢失部分条件,需要由人来作这好 Tasking 的过程。

在条件不充分的情况,你可以轻松让 AI 生成一个页面、一个函数,但是他无法达到你想要的结果。所以在,Unit Mesh 架构下,一部分程序员成为了 AI 代码验证师 + AI 代码修复师,它将持续非常很长一段时间。

问题 2:编程只是在编码?

当前的 AI 编程只是取代你的转译过程:需求细分、转换需求成代码等。你可以看看你的提交历史:一天提交了几行代码,又是哪个时间提交的。有可能你算下来了,白天都在开会,只有晚上写代码。我记得知乎有一个相关的问题,大部分人的回答都是平均几十行、几百行。当然按行数是不科学的,成长期的项目的行数是远大于维护期的。

所以,如果你的编码时间很长,而架构设计、需求讨论等的时间很短,可能得考虑一下职业生涯,强化你的设计、拆解能力。毕竟,一旦效率提高的,还是有些程序员会失业的。

问题 3:人人都会写好的 Prompt?

身处于各类可访问 ChatGPT 的微信群中,遇到很多问题的时候,我经常会抛出一句:”问 ChatGPT 啊“。很多人并不会真正意识到 ChatGPT 是一个工具,唯一的乐趣可能就是:”请帮我生成一个 KFC v50 的故事“。

这也是我们创建 ClickPrompt、ChatFlow、PromptPatterns 等项目的初衷,大部分人需要先意识到 AI 能做什么。然后,才是如何写好 Prompt,我们要摸摸我们的 Copilot 的脾气,然后再一起干活。诸如于:

  • 函数名直接生成代码。
  • 函数名 + 处理步骤生成代码。

每种模式的背后,都很有意思。

Prompt 即代码:一个新的规范 —— Prompt 编写规范

在进一步展开 Prompt 即代码之前,我们需要先了解一下如何写好 Prompt。如下是,我之前放在 ClickPrompt 上面的一部分 GitHub Copilot 示例。

从我的理解来说,一个好的 Prompt 规范应该包括以下内容:

  1. 功能定义:定义所需的功能,并为模型提供足够的上下文和信息。这可以帮助模型更好地理解其意图并生成相应的代码。示例 1:函数名、输入和输出,就能自动填充。
  2. 任务拆分:将任务拆分为小的子任务,并确保每个子任务的要求和期望输出都非常清晰。示例 2:如上图的按步骤设计示例,每一步都需要想好要怎么做。
  3. 确定输入与输出格式:Prompt 规范应该明确输入与输出格式和数据类型,以便模型可以正确地处理输入。示例 3:我们添加了 i18n 的 json 过来,让 Copilot 自动映射。
  4. 测试和调试:在生成代码之后,应该进行测试和调试,以确保其正确性和可靠性。同时,应该为模型提供反馈,以帮助它改进其生成的代码。示例:让 Copilot 编写对应的单元测试,我们对测试用例进行检查。
  5. 避免歧义:Prompt 规范应该避免使用歧义的语言和术语,并确保在多种上下文中生成的代码是一致的。只在出错时,我们才会发现这条原则是有用的。
  6. 编码标准:定义编码标准,并确保生成的代码符合这些标准。这可以确保生成的代码易于阅读和维护,并符合团队的编码惯例。这个就需要我们团队去做了。

看上去有没有像极你平时写的伪代码,作为一个伪代码工程师的你,是不是发现生产力可能爆炸了。

Prompt 即代码:不同形态下的 AI 编程

再重复一下定义:

Prompt 即代码是一种基于多种输入模态的编程范式,它通过结合文本、图像、语音等多种输入方式来提供更丰富的上下文信息,帮助程序员更好地表达自己的意图,并生成相应的代码实现。Prompt 即代码将 prompt 作为代码的一部分,以及作为标准接口来定义生成的代码,同时提供注释和文档信息以支持可读性和可维护性。通过使用 prompt 即代码,程序员可以提高编码效率,同时生成更准确、更可靠的代码实现。

当谈到 Prompt 即代码时,我们通常会将其定义为一种编程范式,它将自然语言或其他形式的输入作为代码生成的起点。Prompt 即代码则让程序员通过提供高度概括的自然语言描述或其他形式的输入来描述他们想要的功能,然后由 AI 系统自动生成代码。

所以,在这里我们分为两种方式:标准的 Prompt 即代码、多模态的 Prompt 即代码。

标准的 Prompt 即代码:文本形式

文本 Prompt 即代码是指使用自然语言或其他方式描述需求或问题,通过 AI 模型自动生成对应的代码。Prompt 作为代码的一部分或者核心,通过描述期望的输入和输出,以及需要执行的操作来生成代码。

尽管现有的 AI 工具都是多模态的,然而自然语言是作为中间语言存在的。所以,我想将文本形式的 prompt 称为标准的 Prompt 即代码,它可以方便地融入现有的编程体系。

  • Prompt 即注释。Prompt 作为注释与代码并存,在这种情况下,Prompt 与代码共存于同一个文件中。通常,Prompt 以注释的形式出现在代码中,以提供必要的上下文信息和生成代码的指令。这种方式适合于需要经常手动修改生成的代码的场景。
  • Prompt 即接口。在这种情况下,Prompt 作为一个标准的接口,代码则是实现这个接口的生成代码。这种方式适用于对生成的代码进行自动化测试和部署的场景,因为接口定义的一致性可以更好地保证代码的正确性。
  • Prompt 即代码。在这种情况下,版本管理工具中不再存储代码,而是存储 Prompt。生成的代码则可以根据 Prompt 来生成,Prompt 作为代码的一部分。这种方式适合于需要频繁更新代码和对代码进行版本控制的场景。

而,事实上,在我第一次将注释加入到 ClickPrompt 中的时候,我犹豫了很久。我们的过去的编程习惯,并不允许将思考过程作为注释到其中。

既然,它已经作为代码的一部分加入进来 ,我们还需要进一步考虑的点是:尽可能地去修改 prompt 重新生成代码,减少直接修改 prompt

多模态 Prompt 即代码

PS:感谢 ChatGPT 帮我考虑了这一部分

多模态 Prompt 即代码是指在训练 AI 模型时,同时利用多种不同的输入模式(如文本、图像和语音)来提供更丰富的上下文,以帮助模型更好地理解程序员的意图并生成相应的代码。通过使用多模态 Prompt,AI 模型可以获得更多的信息,并在生成代码时更准确地反映程序员的意图。

例如,一个基于多模态 Prompt 训练的 AI 模型,可以同时考虑程序员在文本上下文中输入的代码片段,代码所处的项目信息、数据结构信息,以及程序员所提供的图像信息,如示意图、流程图等,从而生成更准确、更完整的代码。

PS:不过,这种技术需要大量的数据和计算资源,同时需要对不同的输入模式进行处理和整合,因此在实际应用中还需要进一步研究和优化。

Prompt 即代码:交互方式的变化

过去的几个月里,每天层出不穷的 AI 新工具,都在让我们感受人类的智商上限和 AI 的下限。与现在的编程方式相比,未来几个月势必会出现新的、或者已经出现新的交互方式。

诸如于:

  • 交互式 Prompt:在编写代码的过程中,模型可以提示程序员输入,从而帮助模型更好地理解程序员的意图,并生成更准确的代码。例如,Unit Mesh 采用的架构模式,便是由人类和 AI 共同完成的,并由 Unit Server 自动化部署。
  • 面向场景的 Prompt:通过提供与特定场景相关的信息和上下文,可以帮助模型更好地理解程序员的意图并生成相应的代码。例如,面向 Web 开发的 Prompt 可能包括与 HTML、CSS 和 JavaScript 相关的信息和上下文。
  • 等等。

Prompt 即代码的交互方式将会越来越多样化和智能化,以更好地满足程序员在不同领域和场景下的需求。

开发 LLM 应用之前

在你下决心开发一个 LLM 应用之前,你应该考虑一些问题。

买还是开发?

LLM 应用由 LLM + 应用两部分组成,所以会出现类似于下图的多种不同类似应用:

四种不同类型的 LLM 应用

  • AI 端到端应用。即直接面向最终用户的应用(含专有模型),诸如 ChatGPT、Midjourney
  • 应用 + 闭源基础模型。如基于 OpenAI、文心一言(他们提供了吗,我没收到)等 API 来构建应用。
  • 应用 + 专有模型。即基于开源基础模型,或者自有的模型,来构建端到端应用。
  • 应用 + 微调模型。基于开源模型 + 面向自己研发场景下来微调,以构建领域特定的应用。

不同的企业应根据自身的业务需求来选择不同的方案。

决策框架

业务与技术问题?

在业务侧,你应该考虑:

  • 你的应用是否需要一个 LLM?
  • 寻找合适的、稳定的 LLM 备用 API
  • 隐私与数据安全问题
  • 未来是否存在私有化部署与微调的可能性?

在技术侧,你应该考虑:

  1. 哪些部分可以使用 LLM? 哪些部分不适合使用 LLM?
  2. Prompt 的编写与维护。编写好 prompt 不是一件容易的事情,需要大量的实践与经验。
  3. 优化 LLM 的体验。在速度、准确率、稳定性上,LLM 都有很大的提升空间。

设计 LLM 效果验证实验

对于 LLM 而言,在进一步决定开发应用之前,我们需要对 LLM 的效果进行验证。验证的方式有很多种,例如:

示例格式

  • 研究目的:明确研究的目标和方向,阐述研究的意义和价值。
  • 研究背景:介绍研究领域的相关背景和现状,说明研究的必要性。
  • 研究问题:明确需要解决的问题,阐明研究的主要内容和目标。
  • 研究方法:阐述研究方法和实验的具体设计,包括实验的流程、步骤、数据采集和处理方法等。
  • 实验过程:详细描述实验的过程和实验结果,包括实验设计、数据采集、数据分析和实验结果等。
  • 结果分析:对实验结果进行分析和解释,包括实验结果的统计分析和数据可视化。
  • 结论和启示:总结实验结果,得出结论并提出启示和建议,指出实验的贡献和局限性。

ChatGPT + Copilot 端到端示例

研究目的:ChatGPT + Copilot 在企业应用中,从需求、编码、测试端到端的辅助能力和提效作用评估

研究过程:

  • 第1步:选择一个真实的项目需求: Thoughtworks OKR 工具
  • 第2步:使用 ChatGPT 来完成领域术语定义和竞品分析启发,再到MVP特性设计启发
  • 第3步:使用 ChatGPT 来完成特性的需求分析,展示可视化的用户旅程、功能需求拆分及格式化编写、验收标准定义
  • 第4步:尝试使用 ChatGPT 来把验收标准转化为UML设计图
  • 第5步:使用 ChatGPT 生成 API,在通过Copilot 生成代码
  • 第6步:...
  • 第x步:使用 ChatGPT 生成发布文档

研究结论:

辅助能力评估: 整体提升 20%~30% 效率。其中重复性工作,如需求格式、代码编写、测试用例、单元测试提升较为明显。

局限性:

  • 每次反馈结果都不一致,需要人工来判断这个结果是否“合理/有用”, 能否进入下一步;
  • 需要结合经验迭代优化给出更细的细节要求提示,生成结果才会更好;

LLM 应用开发模式:精细化流程

一个结合 AI 的自动化的工作流应该包含四个要素:

  • 场景模板化,即预设各种常见的工作场景,为用户提供快捷的开始工作的方式。
  • 交互式环境,包括但不限于输入框、按钮、编辑器、错误信息、帮助文档等,使用户能够与系统进行交互和反馈。
  • 格式化输出,为用户提供规范的输出结果,避免信息过载或无用信息。
  • 流程与工具集成,将不同的工具和流程集成到一个自动化的流程中,提高工作效率和准确性。同时,通过 AI 技术的支持,让系统能够智能化地处理数据和信息,进一步提高工作效率和准确性。

简单来说,就是我们依旧需要碳基生物作为检查官,来检查硅基生物输出是否合理?

设计构建高质量流程

基于我们对 ChatGPT 的使用经验,我们建议在使用 ChatGPT 之前,先考虑如何设计高质量的流程。这里的高质量流程,指的是:

  • 明确的目标和目的:高质量的流程应该有明确的目标和目的,确保流程的设计和执行都能够达成预期的结果。
  • 易于理解和操作:高质量的流程应该简单易懂,让执行者能够轻松理解并操作。流程的设计应该尽可能避免复杂的步骤和冗长的说明,以免造成执行者的困惑和错误。
  • 明确的责任和角色:高质量的流程应该明确各个执行者的责任和角色,避免执行者的混淆和错误。流程设计者应该明确各个角色的职责和权限,确保流程的顺利执行。
  • 可度量和评估:高质量的流程应该能够被度量和评估。流程设计者应该设计合适的指标和评估方式,以便对流程的执行效果进行评估和改进。

如下是我们对于 SDLC + LLM 的探索过程中的展示示例:

SDLC

将旅程拆得足够的详细,才能获得最好的效果。

ChatFlow 的诞生动机:人类设计高质量流程 + AI 完成细节

在我使用了 ChatGPT (GPT 3.5)一个月多月之后,大抵算是掌握了它的脾气。简单来说,ChatGPT 即是一个硅基生物,也是一个非常好的人类助手。作为一个工具,你使用 prompt 的能力决定了它的上限和下限。

简单来说,ChatGPT 在经验丰富的开发人员手中,有一定 prompt 经历的人手中,会发挥非常强大的作用。而对于经验不那么丰富的开发人员,可能会因为缺乏任务分解能力,无法写出合理地 prompt,让 AI 有创意地瞎写代码。

诸如于,我们可以通过如下的注释,让 ChatGPT 或者 GitHub Copilot 直接生成可用的代码:

// 1. convert resources in src/assets/chatgpt/category/*.yml to json
// 2. generate src/assets/chatgpt/category.json
// the yaml file is like this:
// ```yml
// ···

这也就是为什么我们做了 ClickPrompt , 用于一键轻松查看、分享和执行 Prompt。而在完善 ClickPrompt 的过程中,我们发现将 AI 绑定到自己的工作流中,才能更好地提升效率。因此,我们在 ClickPrompt 中提取了两个功能,构建了 ChatFlow:

  • ChatGPT 聊天室:一个集成了 ChatGPT API 的简易 ChatGPT聊天室。
  • ClickFlow:一个基于 Yaml 构建的工作流。

结合 ClickPrompt 不丰富的组件,它可以勉强 work 了。

ChatFlow 是什么?

ChatFlow

ChatFlow 是一个基于自然语言处理(NLP)的流程编排工具,具有以下特点:

  • 易于理解的 YAML:ChatFlow 使用简单易懂的 YAML 格式来描述流程的各个元素,包括条件、循环和变量等。无需编程技能,让流程设计变得简单易懂。
  • 丰富的可视化组件:ChatFlow 提供了丰富的可视化组件,例如表格、图表和交互式界面等,让用户可以更加方便地与流程进行交互和管理。
  • 自动化执行流程:ChatFlow 使用 NLP 技术自动翻译自然语言描述的流程为可执行的代码,并支持自定义函数和自动生成文档功能,让用户更加灵活和高效地管理流程。

总之,ChatFlow 提供了一种灵活、易用、自动化的流程编排工具,让用户可以更加高效地管理复杂的流程,提高工作效率和准确性,同时降低工作的复杂性和学习成本。

ChatFlow 示例

在过去的一段时间内,我们不断尝试开发一些工作流:

  1. 需求与代码生成:从一个模糊的需求开始,生成标准的用户 Story(包含多个 AC),然后根据 AC 生成流程图、测试用例和测试代码。
  2. 软件系统设计:从一个简单的系统开始,分析系统对应的用户旅程,生成对应的处理过程 DSL 等等,以帮助我们思考如何基于 AI 进行系统设计。
  3. 写作的发散与探索:从一个主题开始,进行对应的发散和收敛,直至辅助我们完成一篇文章的草稿、大纲、内容编写。
  4. ClickPrompt 工作流:围绕 ClickPrompt 项目的开发,结合创建 issue、issue 分析、Code Review 等构建的工作流。

在线示例:https://www.clickprompt.org/zh-CN/click-flow/

ChatFlow 示例:需求与代码生成。

用于帮助开发人员快速生成代码并进行测试,从而加快开发进度和提高代码质量。

ChatFlow 示例:软件系统设计

用于帮助系统设计人员快速理解用户需求并生成对应的系统设计方案。

ChatFlow 示例:写作的发散与探索

用于帮助写作人员快速生成文章并进行修改和编辑,从而提高写作效率和文章质量。

ChatFlow

ChatFlow 示例:ClickPrompt 工作流

用于帮助开发团队快速解决问题并进行代码审查,从而加快项目进度和提高代码质量。

LLM 应用开发模式:DSL 驱动开发

设计 DSL 的目的,在于让开发者可以用更简单的方式来表达自己的想法。DSL 通常是一种领域特定语言,它的语法和语义都是针对某个特定领域而设计的。 除了具备很好的易读性,作为一个 LLM 与机器的语言,它还应该具备易写性。

常规 DSL:数据格式 DSL

常规 DSL,诸如于 JSON、YAML、XML 等,是一种用于描述数据的语言。

于是,我们让 ChatGPT 帮我们设计了一个 DSL 来描述:帮我设计一个 DSL 来表示一个系统的处理流程。然后,得到了一个 DSL:

System("BlogSystem") {
  Entities {
    Blog { title: string, ..., comments: [Comment]? },
    Comment { ...}
  }
  Operation {
    Ops("CreateBlog", {
        in: { title: string, description: string },
        out: { id: number }
        pre: title is unique and (title.length > 5 && title.length < 120)
        post: id is not null
    })
  }
  API {
    Route(path: String, method: HttpMethod operation: Operation)
  }
}

它可以分析某一个场景的业务,基于这个业务做分析。在这个 DSL,反复让 ChatGPT 设计之后,勉强可以详细拆开任务:

  • Operation。通过 Ops 的输入、输出、先验条件、后验条件,我们可以构建出更准确的函数。
  • Entitiies。是可独立从 DSL 拆解出来的,并且与数据库结构是绑定的,所以可以用来做数据库设计(ChatGPT 设计了一个诡异的 []? 语法 )。
  • API。API 其实对于编码的帮助是有限的,不过其最大的用处是用于自动化测试,用于确保 ChatGPT 的代码是正确的。

所以,我们只需要拆解任务,并发送到各个管道里,再 merge 一下,就可能能得到一份可工作的代码。至于,前端部分,我们可以用类似的方式来设计。

流式 DSL

由于 LLM 的 stream response 特性,我们可以设计 stream DSL 来处理它们。流式响应 DSL 是一种特殊的 DSL,它的特点是:

  1. 支持流式数据处理:与传统的批处理方式不同,流式响应 DSL 能够处理实时产生的数据流,无需等待所有数据都到齐才开始处理。
  2. 高效的数据处理:流式响应 DSL 可以对数据进行实时处理和转换,而不需要将所有数据都加载到内存中,这使得它可以处理非常大的数据集。
  3. 灵活的数据处理:流式响应 DSL 具有高度的灵活性,可以根据具体的需求进行定制和扩展。例如,可以通过添加不同的操作符来实现数据的过滤、聚合、转换等操作。

示例:

HasMatchFunction: true
Thought: I need to introduce the system to the team and ensure that it aligns with our overall architecture and
governance policies.
Action: introduce_system
Action Input: https://github.com/archguard/ddd-monolithic-code-sample

我们预期它可以实时解析用户的输入,并渲染到 UI 上。这样,用户就可以实时看到自己的输入。

对应的解析代码示例:

const hasMatchFunctionRegex = /\s*HasMatchFunction:\s*(.*(?:\n(?!\s*\/\/).*)*)/i;
const thoughtRegex = /\s*Thought:\s*(.*(?:\n(?!\s*\/\/).*)*)/i;
const actionRegex = /\s*Action:\s*(.*(?:\n(?!\s*\/\/).*)*)/i;
const actionInputRegex = /\s*Action\s*Input:\s*(.*(?:\n(?!\s*\/\/).*)*)/i;

function messageToThought (splitContent: string[]) {
  let thought = thoughtRegex.exec(splitContent[0])?.[1] ?? "";
  let action = ""
  if (splitContent.length >= 2) {
    action = actionRegex.exec(splitContent[1])?.[1] ?? "";
  }

  let actionInput = ""
  if (splitContent.length >= 3) {
    actionInput = actionInputRegex.exec(splitContent[2])?.[1] ?? "";
  }

  let tooling: ToolingThought = {
    thought: thought,
    action: action,
    actionInput: actionInput
  }

  return tooling;
}

内部 DSL

内部DSL是一种特殊的DSL,它的语法与宿主编程语言的语法相同或相似,并且可以直接嵌入到宿主编程语言中,不需要额外的解析器。 这使得开发人员能够以一种更直观、声明性的方式来描述特定领域的问题和解决方案。

特点如下:

  • 与编程语言的语法相同:内部DSL的语法与宿主编程语言的语法相同或相似,因此可以直接嵌入到宿主编程语言的代码中,不需要额外的解析器或转换器。这使得内部DSL更容易理解、编写和维护,因为开发人员可以利用已经熟悉的编程语言知识和工具。
  • 直接嵌入到宿主语言:内部DSL可以直接嵌入到宿主编程语言的代码中,并与宿主语言的功能和库进行无缝集成。这意味着可以在内部DSL中直接调用宿主语言的函数、类和其他特性,从而充分利用宿主语言的强大功能和生态系统。

基于宿主语言的类型系统和语义:由于内部DSL直接嵌入到宿主编程语言中,它可以完全利用宿主语言的类型系统和语义。这使得内部DSL可以提供更严格的类型检查和编译时验证,并且可以与宿主语言的工具链和开发环境无缝集成,例如代码补全、静态分析和调试。

  • 可扩展性:内部DSL可以利用宿主编程语言的灵活性和可扩展性进行自定义和扩展。开发人员可以使用宿主语言的特性来定义新的DSL构造,增加DSL的表达能力和领域特定性。

以Kotlin语言为例,它提供了强大的内部DSL支持。我们可以利用Kotlin的语法和特性来创建具有领域特定性的DSL,并将其嵌入到Kotlin代码中。 详细可以参考:https://kotlinlang.org/docs/type-safe-builders.html

简单示例

让我们看一个简单的例子来说明内部DSL的用法。假设我们正在开发一个配置库,用于配置不同环境下的应用程序设置。我们可以使用内部DSL来定义和使用配置:

class AppConfig {
    var port: Int = 8080
    var dbName: String = "mydb"
    var username: String = "admin"
    var password: String = "password"
}

fun configure(block: AppConfig.() -> Unit): AppConfig {
    val config = AppConfig()
    config.block()
    return config
}

val appConfig = configure {
    port = 9000
    dbName = "productiondb"
    username = "user"
    password = "securepassword"
}

在上面的例子中,我们定义了一个名为AppConfig的类,表示应用程序的配置。然后,我们定义了一个configure函数,它接受一个扩展函数类型的参数, 并在该函数中创建了一个AppConfig对象。通过调用block函数来执行扩展函数,并在其中进行配置。

在使用内部DSL时,我们可以直接在代码中使用自然语言或领域特定的语法来配置应用程序。在示例中,我们调用configure函数并提供一个Lambda表达式作为参数, 通过设置属性来配置AppConfig对象。

ArchGuard Co-mate 示例

在 Co-mate 中,我们便主要采用这种方式来描述软件的架构:

architecture {
    system("TicketBooking") {
        connection("Reservation" to "Ticket")
    }
}

对应的示例实现代码:

fun architecture(function: ArchitectureSpec.() -> Unit): ArchitectureSpec {
    val spec = ArchitectureSpec()
    spec.function()
    return spec
}

class ArchitectureSpec : Spec<String> {
    override fun default(): Spec<String> {
        return defaultSpec()
    }

    override fun exec(element: String): List<RuleResult> {
        return listOf()
    }

    fun system(systemName: String, function: SystemDeclaration.() -> Unit): SystemDeclaration {
        val system = SystemDeclaration(systemName)
        system.function()
        return system
    }

    companion object {
        fun defaultSpec(): ArchitectureSpec {
            return architecture {
                system("TicketBooking") {
                    connection("Reservation" to "Ticket")
                }
            }
        }
    }
}

class ConnectionDeclaration(val source: String, val target: String) : BaseDeclaration<String>
class SystemDeclaration(name: String) : BaseDeclaration<String> {
    fun component(componentName: String, function: ComponentDeclaration.() -> Unit): ComponentDeclaration {
        val component = ComponentDeclaration(componentName)
        component.function()
        return component
    }

    fun connection(pair: Pair<String, String>, function: ConnectionDeclaration.() -> Unit?): ConnectionDeclaration {
        val connection = ConnectionDeclaration(pair.first, pair.second)
        connection.function()
        return connection
    }

    fun connection(pair: Pair<String, String>): ConnectionDeclaration {
        return ConnectionDeclaration(pair.first, pair.second)
    }

}

class ComponentDeclaration(name: String) : BaseDeclaration<String> {
    fun module(moduleName: String, function: ModuleDeclaration.() -> Unit): ModuleDeclaration {
        val module = ModuleDeclaration(moduleName)
        module.function()
        return module
    }
}

class ModuleDeclaration(name: String) : BaseDeclaration<String>

端到端 DSL 示例

阶段一:让 ChatGPT 与你学 DDD

说了,这么多我们来看一个结合我之前设计的 DSL 示例,即 Fklang

在 Fklang 中,我们采用下面的形式来表示 ContextMap:

ContextMap TicketBooking {
  Reservation -> Cinema;
  Reservation -> Movie;
  Reservation -> User;
}

但是,自己设计这样的 DSL 太累了,可以让 ChatGPT 来设计。在你开始之前,需要确认一下 ChatGPT 中的 DDD、DSL、事件风暴等的概念是否与你理解的一致。

第一个问题:设计一个 DSL 来表示 DDD 里的 Context Map 。

ContextMap:
  Subdomain { Name: [ subdomain_name ] }
    [ CollaboratesWith: [ other_subdomain_name ], ... ]
    [ ConformistWith: [ other_subdomain_name ], ... ]
    [ Antagonizes: [ other_subdomain_name ], ... ]
  ...

第二个问题:用这个 ContextMap DSL 对在线电影订票系统建模。对于其它问题也是相似的。

第三个问题:设计一个 DSL 来描述事件风暴的 事件建模 吗?

第四个问题:能用 事件建模的 DSL 来表示用户购票的流程吗?

一旦,我们觉得设计跑歪了,就可以重新设计。如果下是生成的复杂的 DSL:

EventStorming:
  Domain { Name: "电影订票系统" }
     Event { Name: "用户提交订单" }
         Triggered By: "用户选择电影、场次、座位,确认订单"
         Description: "用户提交订单,包括所选电影、场次、座位等信息"
         Actors: [ "用户" ]
         Action: "将用户提交的订单信息保存到订单数据库中"
         Outcome: "订单状态被标记为已提交"

第五个问题……

剩下的你都会了,不会的,可以问 ChatGPT。

阶段二:完整设计 + DSL 生成

第一个问题:能总结一下,我们这里用 DDD 设计 购票系统到落地代码的过程吗?

第二个问题:能为每个过程,设计一个合理的 DSL 吗,并展示他们?

展示一部分神奇的 DSL:

generate java-code from-domain-model
target-package: com.example.movieticket.order
source-model: order-domain-model

    service-mapper 订单服务映射
        map-method: 查询电影
        to-class: MovieRepository
        to-method: findBySchedule

        map-method: 查询座位
        to-class: SeatRepository
        to-method: findByRowAndColumnAndStatus

现在,有意思的地方来,有了上面的一系列 DSL 之后,我们就可以接入到代码系统中。

LLM 集成模式

围绕于我们的开发旅程,我们思考了一些有意思的 ChatGPT 相关的集成模式,这些模式方便我们后续在国内的其它 LLM(大语言模型)中使用。

无限的场景,无限的可能

如大家所知,ChatGPT 可用于多种自然语言处理场景,包括:

  1. 聊天机器人,解决问题,提供建议。
  2. 自然语言生成,生成高质量文章等。
  3. 情感分析,分析用户评论和反馈。
  4. 语言翻译,将一种语言翻译成另一种语言。
  5. 自然语言理解,帮助机器理解和处理自然语言。
  6. ……

当然了,ChatGPT 的场景不限于上面这几个,我们也见到了非常有意思的和架构设计、法律法规检查等一系列有意思的场景。

模式

对应的模式列表:

生成器模式

使用 prompt 生成器生成特定输入,提高 ChatGPT 回答准确度和全面性。例如,使用 prompt 生成器生成法律咨询问题,ChatGPT 回答法律问题;在问答领域中,使用预定义的问题模板作为输入,生成相应的回答,能够提高 ChatGPT 的回答准确度和全面性。

生成器模式是我们最早设计的强化 Prompt 场景,它非常容易实现,只需要预设好一些模板,就能很好地工作。

系统集成模式

将 ChatGPT 与其他系统集成,实现数据交换和通信。例如,将 ChatGPT 与电子邮件系统集成,自动回复邮件;在电商平台中,ChatGPT 可以与订单系统穿插,以便处理用户的购物咨询和下单请求。

在 ClickPrompt 中,我们通过集成 HuggingFace 来实现,一个简单的场景,生成图形的功能。

管道模式:

在 ChatGPT 中定义处理流程,对每个输入进行处理并生成输出。例如,将 ChatGPT 用于自动化客服,对用户的问题进行分类和回复;在客服领域中,ChatGPT 可以通过管道模式实现意图识别、实体识别、回答生成等一系列流程。

在 ClickPrompt 中,我们预期通过 ChatGPT 来与人类交互,对每一步的输入和输出校正,进而完善系统的架构设计。

目标引导模式

将对话分成场景,并为每个场景设定目标,引导 ChatGPT 生成相关回答。例如,将 ChatGPT 用于旅游规划,引导用户选择目的地并提供相关信息;在旅游领域中,ChatGPT 可以根据场景(如酒店预订、景点推荐)来生成相应的回答。

在 ClickPrompt 中,我们通过 ChatGPT 来生成 Stable Diffusion 的 tag,进而完善文本到图形的转换。

协同模式

将 ChatGPT 与其他 AI 技术集成,生成更人性化的回答。例如,将 ChatGPT 与情感分析技术集成,生成更符合用户情感的回答;在语音助手中,ChatGPT 可以与语音识别技术协同,能够更好地理解用户的语义和情感,并生成更加自然的回答。

在 ClickPrompt 中,我们还想做的事情就是通过接入语音功能,来实现语音直转文本,进而输出。

迁移学习模式:

通过将已经学习到的知识应用于新任务中,来改善模型性能和加快学习速度的一种机器学习方法。例如,将预先训练好的 ChatGPT 模型与公司或行业特定的语料库进行微调,以适应特定领域和任务的需求,从而快速构建智能客服机器人。

由于 ChatGPT 训练周期的问题,总存在一些知识老旧的问题。因为在日常的场景中,我们也可以让 ChatGPT 中阅读一些文章,围绕于这些文章,输出新的洞见。

混合模式:

将多个不同类型的模型组合,提高回答准确度和全面性。在问答领域中,将检索模型、知识图谱模型和生成模型混合使用,可以提高回答的准确度和覆盖范围。

我们还在寻找合适的案例,来结合这种模式。

转换器模式:

使用转换器将输入转换为 ChatGPT 可理解的格式,提高ChatGPT对输入的理解和处理能力。例如,使用语音转换器将语音转换为 ChatGPT 可理解的文本格式,从而实现语音交互。

简单来说,就是将步骤转换为格式,通过 ChatGPT 进行格式转换,如在我们的例子中,有一个使用 ChatGPT 实现 i18n 的代码转换。

强化学习模式(ChatGPT 推荐)

基于增量学习的 ChatGPT

使用增量学习技术对ChatGPT进行训练和调整,提高回答准确度和全面性。例如,将ChatGPT用于股票投资,使用增量学习技术对ChatGPT进行训练和调整,从而实现更精准的股票推荐和投资建议。

基于深度强化学习的 ChatGPT

使用深度强化学习技术优化ChatGPT的响应速度和回答质量,提高对话质量。例如,将ChatGPT用于智能家居,使用深度强化学习技术优化ChatGPT的响应速度和回答质量,从而实现更快速、准确的家居控制和交互体验。

验证模式(ChatGPT 推荐)

安全模式

为保护用户隐私和系统安全,对敏感信息进行脱敏、加密等处理,防止恶意攻击和数据泄露。例如,在医疗健康领域中,ChatGPT 用于病例诊断和病情分析时,需要对患者隐私信息进行保护。

自我监督模式

利用 ChatGPT 自身生成的回答作为监督信号,对模型进行自我监督和调整,提高回答准确度。例如,将 ChatGPT 用于自动化翻译,利用自我监督模式对模型进行调整,从而实现更准确的翻译结果。

LLM 应用示例:AI + DevOps

现在,类似于 ChatGPT 和 Microsoft Copilot 这样的人工智能突破正在彻底改变软件行业。 基于人工智能的工具与基础大语言模型可以增强软件开发在设计、需求、测试、发布和运维等各个环节中的能力,提高质量和效率。

SDLC Copilot

方式:梳理研发流程寻找突破点

依旧的,我们需要拆到足够小的工序,才能看到明显的效果。

SDLC Process

试验

在这里,我们选择了一个比较典型的研发流程:需求、设计、开发、测试、发布、运维。 如下是架构设计的示例:

SDLC Architecture

需要注意的是,在这里需要以 zero-shot 的方式实现,以便于:

  1. 更好的理解和使用
  2. 工具化集成

需求 prompt 示例(部分)

你是一个敏捷项目的 BA,请根据如下的信息,编写用户故事。

1. 你的项目是:### OKR 系统 ###
2. 背景是:### 作为一个员工,我希望能够及时更新我的 OKR
   进展情况,并向团队报告我的进展情况,以便能够保持团队的透明度和协同性 ###
2. 我想要实现的功能是: ### 更新 OKR 进度 ###。
3. 你要仔细分析,并尽可能考虑不同的场景,不限于 ### ####。如果场景不存在,请不要编写。
4. 你的返回模板如下所示:

###

用户故事:可以选择宝贝出行服务
作为 xxx
我想 在xx出行的手机客户端里选择宝贝出行服务
以便于 我能够带宝宝打车出行的时候打到有儿童座椅的车

AC 1:  xxx
Given xxx
When xxx
Then xxx

###

测试用例 prompt 示例(部分)

请分析如下的用户故事,按你的理解使用 markdown 表格输出测试用例,格式如下:

所属功能模块 用例编号 用例名称 前置条件 用例类型 操作步骤 优先级 预期结果 关联需求类型

影响分析 prompt 示例(部分)

我在实现一个 OKR 系统 的 作为一个员工,我希望能够及时更新我的 OKR 进展情况,并向团队报告我的进展情况,以便能够保持团队的透明度和协同性
的功能,需要修改哪些服务的代码?



| **服务**   | **功能描述**                                     |
|----------|----------------------------------------------|
| 用户管理服务   | 用于存储和管理员工的基本信息,如姓名、职位等,以便于识别员工并关联他们的 OKR。    |
| 团队管理服务   | 用于存储和管理团队的基本信息,如团队名称、团队成员等,方便员工对齐团队目标。       |
| OKR 管理服务 | 用于创建、更新和删除 OKR,同时支持查询功能,方便员工和团队查看和管理他们的 OKR。 |
| 权限管理服务   | 用于控制不同角色的员工在系统中的权限,确保只有合适的人员可以发布或修改 OKR。     |
| 通知服务     | 当有新的 OKR 发布或修改时,负责通知相关人员,以便他们了解最新的目标和进度。     |
| 数据分析服务   | 对 OKR 数据进行分析,生成报告,以便管理层了解整体的目标完成情况和进度。       |
| 集成服务     | 与其他企业系统(如项目管理、日程管理等)集成,实现数据同步和自动更新 OKR 的功能。  |

...

AI + DevOps 最后一公里:AutoDev 插件

AI IDE 基本原理:没有魔法!全是 prompt! 🐶

AI IDE 基本原理

AutoDev 是一款高度自动化的 AI 辅助编程工具。AutoDev 能够与您的需求管理系统(例如 Jira、Trello、Github Issue 等)直接对接。在 IDE 中,您只需简单点击,AutoDev 会根据您的需求自动为您生成代码。您所需做的,仅仅是对生成的代码进行质量检查。

简单来说,AutoDev 定位是适用于私有化大语言模型 + 高度集成的 AI 编程助手。AutoDev 提供了一种 AutoCRUD 模式,其设计理解的过程是:

  1. 从需求管理系统获取需求,并进行需求分析。
  2. 结合源码与需求系统,选择最适合变更的入口(如 Java 中的 Controller)
  3. 将需求与 Controller 交给 AI 分析,以实现代码的代码。
  4. 根据 Controller 逐步自动完成其它部分代码(实现中…)

另外一种模式则是普通的 Copilot 模式,可以接入现有的大模型工具,实现一系列的 AI 代码辅助相关功能。

GitHub: https://github.com/unit-mesh/auto-dev

接入 LLM,我们不仅可以生成代码,还可以生成单元测试代码,从而提高测试效率和覆盖率。

让我们再展开看一看,基于现有的 AI 能力,会有哪些新可能性。

平台工程的变化与新机遇

而除了我们上述的 demo 之外,我们相信它带会其它带来一系列的变化。对于中大型组织的基础设施或者平台团队来说,要接入 AI 能力需要有更多的变化与机遇。

平台工程是一种用来构建和运维支持软件交付和生命周期管理的自助式内部开发者平台的机制和架构。平台工程可以提高开发者的体验和生产力,提供自动化的基础设施操作。 平台工程是软件工程组织的新趋势,它可以优化开发者的工作流程,加速产品团队交付客户价值。

平台工程的核心思想是将平台视为一种产品,由专业的平台团队来创建和维护,为内部的客户(如开发者、数据科学家等)提供可复用的服务、组件和工具。

需求:自动化收敛、分析与完善

在现有的场景之下,已经有一系列的关于结合 AI 进行需求管理的尝试:

  • 自动化完善。对用户的反馈和数据的分析,自动识别和补充缺失的需求信息,例如自动识别用户提出的问题并转化为需求描述,自动补全需求的关键词和标签等。
  • 自动化分析。通过训练自带的领域知识,可以更好地评估和优化需求,发现潜在的问题和机会,提高需求的效率和效果。
  • 自动化收敛。结合其它 AI 技术,比如智能推荐、对话系统、多方协作等,可以帮助您更好地沟通和协调需求,收集和整合用户的反馈和痛点,提高需求的满意度和一致性。
  • 自动化迭代。结合人类反馈的 AI 数据,可以更好地更新和改进需求生成,适应不断变化的环境和用户需求,提高需求的持续性和创新性

尽管现有的几个方案:LangChain、llama-index 等暂时只支持 OpenAI,但是随着更多开源大语言模型的加入,未来会更易于落地。

工具链:智能的 IDE

对于现有的场景来说,已经相当的丰富,诸如于:

  • 自动化代码审查
  • 自动化测试
  • 自动化日志分析
  • AI 辅助编程
  • ……

诚然,诸如于 GitHub Copilot 等收费 AI 工具来说,对于大部分公司来说,贵可能是其次,重点是代码的安全性。而虽然国内各类新的模型层出不穷,但是大部分缺少编程相关的集成,又或者是编程能力比较弱。 然而,市面上也有只用于编程相关的模型,如 Salesforce 在 Hugging Face 上提供的 16B CodeGen 模型。虽然,还需要经过一些小的微调,但是如 Replit 公司所言,效果还是非常不错的。

随后,便是类似于 AutoDev 针对于大语言模型进行的封装,简化普通开发人员的开发过程。

文档:超越搜索

在有了 LLM 和各种智能问答的基础上,我们还可以加入内部各种工具的文档和代码,以提供更全面、更智能的文档服务。例如,LangChain 构建的问答式文档,可以对企业内部的各种文档进行语义理解和智能问答,进而简化开发人员的学习成本。

面临的挑战

虽然生成式AI技术及工具已经在软件生命周期的各个环节中体现出了提效的可行性,但想要在大型科技组织中端到端落地、实现提效的突破,还面临很多挑战。

企业规模化软件过程提效的挑战

信息安全和信创化的强制要求

大型企业尤其是银行,面临最强的信息安全监管及信创化、国产化要求,需要国产生态中能力突出的大模型。

开源大模型表现偏弱、自己训练成本高

目前可私有化部署的大模型,其自然语言理解和代码生成的水平与GPT有一定差距;根据大语言模型论文做一些粗略的数学计算,如果用的是 Facebook LLaMA,训练成本(不考虑迭代或者出错)大约是400 万美元,如果是谷歌PaLM,大约 2700 万美元。

与企业内部工具进行结合

碎片化的应用提效效果有限,把 AI 无缝集成到BizDevOps 工具链中,技术难度尚未可知。

开发 AI 辅助研发提效的局限性

基于 GPT 模型的工具不符合信息安全要求

目前大多工具基于 OpenAI GPT 构建,不能私有化部署,不符合信息安全的强制要求;需要寻找能够私有化部署且水平相当的替代品。

公开LLM 针对专业领域表现不佳,适用性差

缺乏专业知识,对于某些专业领域缺乏足够的理解。它受训练数据中的影响,容易产生偏见和错误。

LLM 工具碎片化

各类工具都是在一个个分散的工作节点上辅助,使用时来回切换工具的成本很高,整体端到端地提效不明显。

LLM 应用示例:最佳实践示例

LLM 应用开发模式:轻量级 API 编排

在 LangChain 中使用了思维链的方式来选择合适的智能体(Agent),在 Co-mate 中,我们也是采取了类似的设计,在本地构建好函数,然后交由 LLM 来分析用户的输入适合调用哪个函数。

如下是我们的 prompt 示例:

Answer the following questions as best you can. You have access to the following tools:

introduce_system: introduce_system is a function to introduce a system.

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [introduce_system]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: Introduce the following system: https://github.com/archguard/ddd-monolithic-code-sample

这里的 Question 便是用户的输入,然后再调用对应的 introduce_system 函数进行分析。

LLM 应用开发模式:DSL 动态运行时

与事实能力相比,我们更信任 LLM 的编排能力,因此我们在 Co-mate 中采用了 DSL 的方式来编排函数,这样可以更加灵活的编排函数。

为了支撑这样的能力,我们在 Co-mate 中引入了 Kotlin 作为 DSL 的运行时:

// 初始化运行时
val repl = KotlinInterpreter()
val mvcDslSpec = repl.evalCast<FoundationSpec>(InterpreterRequest(code = mvcFoundation))

// 从用户的输入中获取 action
val action = ComateToolingAction.from(action.lowercase())

// 添加默认的 DSL spec
if (action == ComateToolingAction.FOUNDATION_SPEC_GOVERNANCE) {
    comateContext.spec = mvcDslSpec
}

对应的 DSL 示例(由 ChatGPT 根据 DDD 版本 spec 生成):

foundation {
    project_name {
        pattern("^([a-z0-9-]+)-([a-z0-9-]+)(-common)?\${'$'}")
        example("system1-webapp1")
    }

    layered {
        layer("controller") {
            pattern(".*\\.controller") { name shouldBe endsWith("Controller") }
        }
        layer("service") {
            pattern(".*\\.service") {
                name shouldBe endsWith("DTO", "Request", "Response", "Factory", "Service")
            }
        }
        layer("repository") {
            pattern(".*\\.repository") { name shouldBe endsWith("Entity", "Repository", "Mapper") }
        }

        dependency {
            "controller" dependedOn "service"
            "controller" dependedOn "repository"
            "service" dependedOn "repository"
        }
    }

    naming {
        class_level {
            style("CamelCase")
            pattern(".*") { name shouldNotBe contains("${'$'}") }
        }
        function_level {
            style("CamelCase")
            pattern(".*") { name shouldNotBe contains("${'$'}") }
        }
    }
}

LLM 应用开发模式:本地小模型

在 Co-mate 中,我们在本地引入了 SentenceTransformer 来处理用户的输入,优在本地分析、匹配用户的输入,并处理。当匹配到结果后直接调用本地的函数,当匹配不到结果时调用远端的处理函数来处理。

HuggingFace: https://huggingface.co/sentence-transformers

在原理上主要是参考了 GitHub Copilot、 Bloop 的实现,通过本地的小模型来处理用户的输入,然后再通过远端的大模型来处理用户的输入。

Rust 实现示例

Rust 相关示例:https://github.com/unit-mesh/unit-agent

#![allow(unused)]
fn main() {
pub fn embed(&self, sequence: &str) -> anyhow::Result<Embedding> {
    let tokenizer_output = self.tokenizer.encode(sequence, true).unwrap();

    let input_ids = tokenizer_output.get_ids();
    let attention_mask = tokenizer_output.get_attention_mask();
    let token_type_ids = tokenizer_output.get_type_ids();
    let length = input_ids.len();
    trace!("embedding {} tokens {:?}", length, sequence);

    let inputs_ids_array = ndarray::Array::from_shape_vec(
        (1, length),
        input_ids.iter().map(|&x| x as i64).collect(),
    )?;

    let attention_mask_array = ndarray::Array::from_shape_vec(
        (1, length),
        attention_mask.iter().map(|&x| x as i64).collect(),
    )?;

    let token_type_ids_array = ndarray::Array::from_shape_vec(
        (1, length),
        token_type_ids.iter().map(|&x| x as i64).collect(),
    )?;

    let outputs = self.session.run([
        InputTensor::from_array(inputs_ids_array.into_dyn()),
        InputTensor::from_array(attention_mask_array.into_dyn()),
        InputTensor::from_array(token_type_ids_array.into_dyn()),
    ])?;

    let output_tensor: OrtOwnedTensor<f32, _> = outputs[0].try_extract().unwrap();
    let sequence_embedding = &*output_tensor.view();
    let pooled = sequence_embedding.mean_axis(Axis(1)).unwrap();

    Ok(pooled.to_owned().as_slice().unwrap().to_vec())
}
}

Kotlin 实现示例

class Semantic(val tokenizer: HuggingFaceTokenizer, val session: OrtSession, val env: OrtEnvironment) {
    fun embed(
        sequence: String,
    ): FloatArray {
        val tokenized = tokenizer.encode(sequence, true)

        val inputIds = tokenized.ids
        val attentionMask = tokenized.attentionMask
        val typeIds = tokenized.typeIds

        val tensorInput = OrtUtil.reshape(inputIds, longArrayOf(1, inputIds.size.toLong()))
        val tensorAttentionMask = OrtUtil.reshape(attentionMask, longArrayOf(1, attentionMask.size.toLong()))
        val tensorTypeIds = OrtUtil.reshape(typeIds, longArrayOf(1, typeIds.size.toLong()))

        val result = session.run(
            mapOf(
                "input_ids" to OnnxTensor.createTensor(env, tensorInput),
                "attention_mask" to OnnxTensor.createTensor(env, tensorAttentionMask),
                "token_type_ids" to OnnxTensor.createTensor(env, tensorTypeIds),
            ),
        )

        val outputTensor = result.get(0) as OnnxTensor
        val output = outputTensor.floatBuffer.array()

        return output
    }


    companion object {
        fun create(): Semantic {
            val classLoader = Thread.currentThread().getContextClassLoader()

            val tokenizerPath = classLoader.getResource("model/tokenizer.json")!!.toURI()
            val onnxPath =  classLoader.getResource("model/model.onnx")!!.toURI()

            try {
                val env: Map<String, String> = HashMap()
                val array: List<String> = tokenizerPath.toString().split("!")
                FileSystems.newFileSystem(URI.create(array[0]), env)
            } catch (e: Exception) {
//                e.printStackTrace()
            }

            val tokenizer = HuggingFaceTokenizer.newInstance(Paths.get(tokenizerPath))
            val ortEnv = OrtEnvironment.getEnvironment()
            val sessionOptions = OrtSession.SessionOptions()

            // load onnxPath as byte[]
            val onnxPathAsByteArray = Files.readAllBytes(Paths.get(onnxPath))

            val session = ortEnv.createSession(onnxPathAsByteArray, sessionOptions)

            return Semantic(tokenizer, session, ortEnv)
        }
    }
}

LLM 应用开发模式:Stream 封装

服务端 API 调用:Kotlin 实现

机制:结合 callbackFlow 来实现

fun stream(text: String): Flow<String> {
    val systemMessage = ChatMessage(ChatMessageRole.USER.value(), text)

    messages.add(systemMessage)

    val completionRequest = ChatCompletionRequest.builder()
        .model(openAiVersion)
        .temperature(0.0)
        .messages(messages)
        .build()

    return callbackFlow {
        withContext(Dispatchers.IO) {
            service.streamChatCompletion(completionRequest)
                .doOnError(Throwable::printStackTrace)
                .blockingForEach { response ->
                    val completion = response.choices[0].message
                    if (completion != null && completion.content != null) {
                        trySend(completion.content)
                    }
                }

            close()
        }
    }
}

客户端 API 调用:TypeScript 实现

机制:依赖于 Vercel 的 AI 库,提供对于 Stream 的封装

import { Message, OpenAIStream, StreamingTextResponse } from 'ai'
import { Configuration, OpenAIApi } from 'openai-edge'

export async function stream(apiKey: string, messages: Message[], isStream: boolean = true) {
  let basePath = process.env.OPENAI_PROXY_URL
  if (basePath == null) {
    basePath = 'https://api.openai.com'
  }

  const configuration = new Configuration({
    apiKey: apiKey || process.env.OPENAI_API_KEY,
    basePath
  })

  const openai = new OpenAIApi(configuration)

  const res = await openai.createChatCompletion({
    model: 'gpt-3.5-turbo',
    messages,
    temperature: 0.7,
    stream: isStream
  })

  if (!isStream) {
    return res
  }

  const stream = OpenAIStream(res, {})

  return new StreamingTextResponse(stream)
}

客户端 UI 实现:Fetch

const decoder = new TextDecoder()

export function decodeAIStreamChunk(chunk: Uint8Array): string {
  return decoder.decode(chunk)
}

await fetch("/api/action/tooling", {
  method: "POST",
  body: JSON.stringify(tooling),
}).then(async response => {
  onResult(await response.json())
  let result = ""
  const reader = response.body.getReader()
  while (true) {
    const { done, value } = await reader.read()
    if (done) {
      break
    }

    result += decodeAIStreamChunk(value)
    onResult(result)
  }

  isPending = false
});

服务端实现转发: Java + Spring

WebFlux + Spring Boot

@RestController
public class ChatController {

    private WebClient webClient = WebClient.create();

    @PostMapping(value = "/api/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter chat(@RequestBody ChatInput input) throws IOException {
        SseEmitter emitter = new SseEmitter();

        webClient.post()
                .uri(REMOTE_URL)
                .bodyValue(input)
                .exchangeToFlux(response -> {
                    if (response.statusCode().is2xxSuccessful()) {
                        return response.bodyToFlux(byte[].class)
                                .map(String::new)
                                .doOnNext(string -> {
                                    try {
                                        emitter.send(string);
                                    } catch (IOException e) {
                                        logger.error("Error while sending data: {}", e.getMessage());
                                        emitter.completeWithError(e);
                                    }
                                })
                                .doOnComplete(emitter::complete)
                                .doOnError(emitter::completeWithError);
                    } else {
                        emitter.completeWithError(new RuntimeException("Error while calling remote service"));
                    }
                })
                .subscribe();

        return emitter;
    }
}

服务端转发:Python

FastAPI + OpenAI

def generate_reply_stream(input_data: ChatInput):
    prompt = input_data.message
    try:
        prompt = prompt
        response = openai.ChatCompletion.create(
            model=openai_model,
            temperature=temperature,
            max_tokens=max_tokens,
            n=max_responses,
            messages=[
                {"role": "user", "content": prompt},
            ],
            stream=True,
        )
    except Exception as e:
        print("Error in creating campaigns from openAI:", str(e))
        raise HTTPException(503, error503)
    try:
        for chunk in response:
            current_content = chunk["choices"][0]["delta"].get("content", "")
            yield current_content

    except Exception as e:
        print("OpenAI Response (Streaming) Error: " + str(e))
        raise HTTPException(503, error503)


@app.post("/api/chat", response_class=Response)
async def chat(input_data: ChatInput):
    return StreamingResponse(generate_reply_stream(input_data), media_type="text/event-stream")

LLM 应用架构设计原则

在优化 ArchGuard 的 AI 辅助架构治理工具 Co-mate 的架构时,发现有一些模式与之前设计 AutoDev、ClickPrompt 等颇为相似。便思考着适合于 ArchGuard Co-mate 的架构设计原则是什么,写下了初步的三条原则。

而正好在思考 LLM + 架构,便又整理了适合于更通用的四个架构设计原则。 以此作为一个参考的架构原则基础,方便于我后续设计其它的 LLM 为核心的软件架构。

引子:ArchGuard Co-mate 的三个设计架构原则

Co-mate 是基于 ArchGuard 的分析能力所构建的,并且是以 DSL、规范文档为核心来构建的。所以,我们设计了三条初步的设计原则:

  • DSL 作为统一语言。通过使用领域特定语言(DSL)来增强人机交互,实现高效的人机、机机、机人交流。
  • 原子化 LLM 以用于编排。利用语言模型(LLM)的原子能力,在 DSL 中构建复杂的行为。
  • 精心设计的分层动态上下文。通过将上下文分为不同的层次,使用 LLM 有效地处理复杂性。

在 Co-mate 中,我们采用了 Kotlin Type-safe Builder 封装了基础的函数功能,以让 LLM 能根据文档、规范来编排治理函数。

原规范如下所示:

- 代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
- 代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式,正确的英文拼写和语法可以让阅读者易于理解,避免歧义。
- 类名使用 UpperCamelCase 风格,必须遵从驼峰形式。正例: HelloWorld。

示例 DSL 如下所示:

naming {
    class_level {
        style("CamelCase")
        pattern(".*") { name shouldNotBe contains("$") }
    }
    function_level {
        style("CamelCase")
        pattern(".*") { name shouldNotBe contains("$") }
    }
}

中间的文档转换 DSL 的过程就交给 LLM 来动态处理和生成(进行中)。有了这个基础,我们会发现它与我们先前开源的基于 LLM 的应用,在架构上并没有太多的区别。只是利用的能力有所差异,而又由于交互还没到我们的核心。所以,我添加了一条:**用户意图导向设计 **。

LLM 优先的软件架构设计原则

LLM 对于开发人员、架构师来说,即充满了机遇,又充满了挑战。诸如于:LLM 如何辅助架构设计、如何构建基于 LLM 的架构、如何让 LLM 引导架构设计以及如何构建 LLM 为核心的软件架构。

不同的模式之下,对于现有的流程和软件都会带来不少的冲击。基于 Thoughtworks 内部的一系列探索、基于 LLM 的软件架构和总结,我重新思考了四个原则:

  • 用户意图导向设计。
  • 上下文感知。
  • 原子能力映射。
  • 语言接口/语言 API。

详细展开如下。

1. 用户意图导向设计

如我们所熟悉的一样,现有的应用都以 Chat 方式作为 LLM 的入口之一,而 Chat 的本意是去理解用户的意图,诸如于:“帮我写一篇文章介绍设计原则”。这里的意图就很直接,而为了让用户更好地去表达自己的意图,就需要有意地去引导用户的输入。

在这里,就会呈现不同的引导方式或者封装方式,诸如于封装菜单为指令、封装指令为 prompt、基于用户输入解析成 UI 等等。

为了更好地理解用户意图,我们需要考虑:设计全新的人机交互体验。

总结:通过设计全新的人机交互体验,构建领域特定的 AI 角色,以更好地理解用户的意图。例如,在聊天应用程序中,AI 可以使用自然语言处理来理解用户的意图,从而更好地回答用户的问题。除此之外,还可以探索其他交互方式,如语音识别、手势识别等,以提高用户体验。

2. 上下文感知

在先前的文章里,我们一直在强调上下文工程的重要性。我们原先对其的定义是:上下文工程是一种让 LLM 更好地解决特定问题的方法。它的核心思想是,通过给 LLM 提供一些有关问题的背景信息,比如指令、示例等,来激发它生成我们需要的答案或内容。

而在包含了业务场景的情况下,我们要考虑的是围绕于上下文工程的软件架构。诸如于在 ArchGuard Co-mate 里,我们的思路是:通过分层方法来构建动态的上下文。其原因也主要是:我们对于某个用户意图的理解会存在不同的架构层次 里,如业务架构、技术架构、代码等。

总结:通过构建适合于获取业务上下文的应用架构,以生成更精准的 prompt,并探索高响应速度的工程化方式。即围绕高质量上下文的 Prompt 工程。例如,在一个电商应用程序中,AI 可以了解用户的购物历史记录、浏览历史记录等上下文信息,以提供更好的购物建议。

3. 原子能力映射

起初,大部分结合 OpenAI 的应用,都是让 LLM 直接生成 JSON、Yaml 的形式。但是呢,在我们尝试了 3000 条左右的 PlantUML 生成之后,发现有 20% 的概率生成的 UML 是错误的,不可编译的。正是这种场景,让我们思考了 LLM 是否适合去做这样的事情。

而在架构治理治理之下,我们将其定义为:借助 LLM 原子能力显性化架构知识,映射和构建治理函数,动态度量不同场景。

在日常的业务场景里,对于 LLM 的能力分析也是非常重要的一环,诸如于我们不应该让 LLM 进行数学计算,而是通过诸如 Functions Calling 的方式,将意图与系统的功能相结合。

所以,我们分解了 LLM 的能力,按照不同的方式与系统结合在一起。

总结:我们需要分析 LLM 所擅长的原子能力,将其与应用所欠缺的能力进行结合,进行能力映射。让每个 AI 做自己擅长的事,诸如于利用好 AI 的推理能力。例如,在一个智能家居应用程序中,AI 可以根据用户的行为自动调整室内温度、光线等,以提供更好的家居体验。

4. 语言接口

在我与诸多架构师讨论之后,我们几乎达到了一个一致意见:我们需要一种新的 API,一种适合于 LLM 的 API。

它或许是一类基于语言的 API。对于人与机器、机器与机器来说,是我们熟悉的诸如于 JSON、YAML 或者其它自定义的 DSL;对于人与机器来说,这个语言 API 是自然语言,又或者是图形等方式。

当我们可视化了自己的软件架构之后,你会发现这一点特别的明显。

总结:我们需要探索和寻找合适的新一代 API ,以便于 LLM 对服务能力的理解、调度与编排。诸如自然语言作为人机 API,DSL 作为 AI 与机器间的 API 等。例如,在一个在线客服应用程序中,AI 可以使用自然语言处理来理解客户的问题,并根据问题的类型和紧急程度自动分配给不同的客服代表。

LLM 架构设计原则:原子能力映射

在 ArchGuard Co-mate 中,对于 API 能力来说,我们做的一件事是分解 API 文档按不同 LLM 的原子能力进行分解 。构建出四种不同的原子能力:

  • 推理出适用于 URI 的正则表达式。
  • 推理出一个合理的 example。
  • 提取一些 checklist,诸如于状态码、HTTP Action 等。
  • 将剩下的不确定性内容,扔到一起。

如下图所示:

LLM Capability

比如说,在 Co-mate 的 REST API 治理场景下,我们使用的 LLM 能力包括了:

  • 分类:让 LLM 分析 API 文档,让我们后续根据 URI、HTTP Action、安全等几个不同的能力维度来选择适合的工具。
  • 逻辑推理:让 LLM 分析 API 文档的 URI Construction 部分,生成用于检查的 URI 正则表达式部分,以及适合于人类阅读的 by example 部分。当然了,也包含了其它场景之下的推理。
  • 提取:由 LLM 按 API 规范的不同维度来提取一些关键信息。
  • 总结:由 LLM 来总结哪些部分难以简单的通过代码总结,诸如于安全等不适合于所有的 API 场景。
  • ……

由此构成了 “能力映射” 的左图部分,这种方式适用于不同的规范分解。尽管如此,对于当前方式来说,依然还有一系列的可优化的空间,诸如于对 security、misc 进行进一步的能力分解。

在右侧,我们则构建了一个 Kotlin Typesafe DSL,以动态的加载到系统中(未来),每一个函数对应到一个 Rule。

rest_api {
    uri_construction {
        rule("/api\\/[a-zA-Z0-9]+\\/v[0-9]+\\/[a-zA-Z0-9\\/\\-]+")
        example("/api/petstore/v1/pets/dogs")
    }

    http_action("GET", "POST", "PUT", "DELETE")
    status_code(200, 201, 202, 204, 400, 401, 403, 404, 500, 502, 503, 504)

    security("""Token Based Authentication (Recommended) Ideally, ...""")

    misc("""....""")
}

作为一个 demo,这个 DSL 依旧具备很大的完善空间。其中比较有意思的部分在于 security 和 misc 部分,这些不确定性正好适用于 LLM 进行推理。所以,在执行对应的 misc、security 规则检查时,会再调用 GPT 来检查:

Prompt Example

以将其中的确定性与不确定性更好的结合,进而充分地利用了 LLM 与 ArchGuard 的能力,并减少对 GPT 的消耗。

LLM 原子能力:逻辑推理

即能够从输入中识别模式,并推理出相关的信息和结论。这种能力通常需要机器学习和自然语言处理等技术的支持。

在 API 文档分析中,推理能力可以用于分析 URI Construction 部分,以推理出适用于 URI 的正则表达式和一个合理的 example。

示例:

你是一个架构治理专家,请分析下面的 RESTful API 文档,编写对应的正则表达式与 URI 示例。要求如下:

1. 请将 API 文档中的 URI 规则与正则表达式一一对应。
2. 尽可能只用一个正则表达式来匹配所有的 URI 规则。
3. 你编写的 URI 示例应该符合正则表达式的规则。
4. 如果文档中缺少通用的 URI 规则,请自行补充。
5. 你最后只返回如下的格式:

###
uri_construction {
    rule("{{regex expression}}")
    example("{{uri example}}")
}
###

RESTful API 文档:

###
{{documents}}
###

LLM 原子能力:提取

即能够从输入中提取出特定的信息和数据,以便进行进一步的处理和分析。

在 API 文档分析中,提取能力可以用于按照 API 规范的不同维度提取关键信息,例如状态码、HTTP Action 等。

示例:

你是一个架构治理专家,请分析下面的 RESTful API 文档,整理文档中的状态码。要求如下:

1. 你需要按格式返回,不做解释。
2. 你的返回格式如下:

###
status_code(200, 201, 202)
###

文档如下:

###
{{documents}}
###

LLM 原子能力:分类

即能够将输入按照特定的标准分成不同的类别或类别组,以便进行更精细的分析和处理。

在 API 文档分析中,分类能力可以用于根据不同的维度选择适合的工具,例如根据 URI、HTTP Action、安全等维度分类。

LLM 原子能力:总结

即能够将输入中的复杂信息和数据进行简化和概括,以便更好地理解和应用。

在 API 文档分析中,总结能力可以用于对 API 文档中无法简单通过代码总结的部分进行总结,例如安全等。

You're an Architecture Governance Expert,根据下面的信息,总结 RESTful API 的规范情况。Here is requirements: 
1. 请使用中文返回。
2. API 应该符合基本 RESTful API 的规范,如 URI 构造、采用标准的 HTTP 方法、状态码、安全等。
3. 如果 result 是 true,请不要返回任何信息。
4. 如果 result 是 false,请返回不通过的原因,并根据 rule 提供符合规范的 API。
5. 你只返回如下的结果:

###
- API `{api uri}` 不符合 { rule name } 规范,Rule: { rule },建议 API 修改为 {new api}。
###

results: results: /api/blog/get: [RuleResult(ruleName=uri-construction, rule=/api/petstore/v1/pets/dogs, result=false),
 RuleResult(ruleName=http-action, rule=supported http actions: GET, POST, PUT, DELETE, result=true), 
 RuleResult(ruleName=status-code, rule=supported codes: 200, 201, 202, 204, 400, 401, 403, 404, 500, 502, 503, 504, result=true), 
 RuleResult(ruleName=security, rule=Token Based Authentication (Recommended) Ideally, microservices should be stateless so the service instances can be scaled out easily and the client requests can be routed to multiple independent service providers. A token based authentication mechanism should be used instead of session based authentication, result=true), 
 RuleResult(ruleName=security, rule=, result=true)]

LLM 原子能力:TODO

LLM 架构设计原则:用户意图导向设计

AI 2.0

通过设计全新的人机交互体验,构建领域特定的 AI 角色,以更好地理解用户的意图。例如,在聊天应用程序中,AI 可以使用自然语言处理来理解用户的意图,从而更好地回答用户的问题。除此之外,还可以探索其他交互方式,如语音识别、手势识别等,以提高用户体验。

模式:引导式

LLM 存在的一大局限性是:用户不知道它能做什么,也不知道它能做什么。所以,我们需要引导用户,让用户知道它能做什么,也知道它能做什么。

在 ChatGPT 的首页,我们可以看到一个引导式的设计,它告诉用户,它能做什么,也知道它能做什么:

ChatGPT Home

对于我们的 LLM 应用,我们也可以设计一个引导式的界面,让用户知道它能做什么,也知道它能做什么。如下是 ArchGuard Co-mate 的引导式界面:

引导式

Co-mate 要求用户输入一个代码库,即 URL,随后,Co-mate 会自动分析代码库,生成一个代码库的概要的信息。诸如于:

该项目是一个网站应用程序,使用了Spring Boot框架和一系列相关的库来构建领域驱动设计(DDD)中的多个有界上下文模块。该应用程序还使用了一些第三方库来构建用户界面和任务调度等任务,例如Springfox Swagger2和Springfox Swagger-UI等。在构建和部署方面,该应用使用了Gradle插件,如Spring Boot Gradle插件、Gradle Docker插件以及Spring Cloud Contract Gradle插件等。该应用还使用了其他库,如ModelMapper和Mockito Core等。同时,该应用需要考虑Spring Cloud依赖等非功能需求。

模式:实时返回

同样的,对于用户来说,如果一次输入要等很久,那么用户体验就会很差。所以,我们需要实时返回结果,让用户知道它能做什么,也知道它能做什么。

因此,对于我们的应用来说,如果它的解析时间特别长,我们就需要考虑更多的返回方式,类似于 Chat 的随时返回:

Streaming

尽管我们需要在代码上,进行更多的优化,但是,这样的用户体验,会更好。

模式:请求-生成 UI-确认

就当前的 LLM 能力而言,它不能代替我们去坐牢,所以,我们需要由人来确定它的结果是否正确。因此,我们需要设计一个请求-生成-确认模式,如下的截图所示:

Request to Confirm

每当用户有一些敏感的操作时,我们都需要让用户确认,以避免一些不必要的麻烦。

LLM 架构设计原则:上下文工程

Context Engineering

上下文工程是一种让 LLM 更好地解决特定问题的方法。它的核心思想是,通过给 LLM 提供一些有关问题的背景信息,比如指令、示例等,来激发它生成我们需要的答案或内容。上下文工程是一种与 LLM 有效沟通的技巧,它可以让 LLM 更准确地把握我们的目的,并且提升它的输出水平。

简而言之,上下文工程是如何在有限的 token 空间内,传递最相关的上下文信息

上下文工程的原则

所以,我们就需要定义什么是该场景下的,最相关的上下文信息

原则:基于场景与旅程的上下文设计

它的基本思想是,通过分析用户在不同场景下的操作和行为,来获取与当前任务相关的上下文信息,从而指导 LLM 生成最佳的代码提示。

Copilot 分析了用户在不同场景下的操作和行为,如何使用 IDE 的旅程,以及与当前任务相关的指令和例子等信息,从而获取最相关的上下文信息。这些上下文信息可以帮助 LLM 更好地理解用户的意图,并生成更准确、更有用的代码提示。

例如,在用户编写 JavaScript 代码时,Copilot会分析用户在编辑器中的光标位置、当前文件的内容、变量、函数等信息,以及用户的输入历史和使用习惯等上下文信息,来生成最相关的代码提示。这些代码提示不仅能够提高用户的编码效率,还能够帮助用户避免常见的编程错误。

原则:就地矢量化(Vector)与相似度匹配

“众知周知”,在 LLM 领域非常火的一个工具是 LangChain,它的处理过程类似于 langchain-ChatGLM 总结的:

加载文件 -> 读取文本 -> 文本分割 -> 文本向量化 -> 问句向量化 -> 在文本向量中匹配出与问句向量最相似的top k个 -> 匹配出的文本作为上下文和问题一起添加到prompt中 -> 提交给LLM生成回答。

为了处理大规模的自然语言处理任务,Copilot 在客户端使用了 Cushman + ONNX 模型处理。具体来说,Copilot 将 Cushman 模型的输出转换为向量表示,然后使用向量相似度计算来匹配最相关的本地文件。

除了就地矢量化(Vector)与相似度匹配,Copilot 还使用了本地的相似计算与 token 处理来管理 token,以便更好地处理大规模自然语言处理任务。

原则:有限上下文信息的 Token 分配

而由于 LLM 的处理能力受到 token 数的限制,如何在有限的 token 范围内提供最相关的上下文信息,便是另外一个重要的问题。

诸如于如上所述的 Copilot 本地 prompt 分为了 prefix 和 suffix 两部分,在 suffix 部分需要配置 suffixPercent,其用于指定在生成代码提示时要用多少 prompt tokens 来构建后缀,默认值似乎是 15%。

通过增加 suffixPercent,可以让 Copilot 更关注当前正在编写的代码片段的上下文信息,从而生成更相关的代码提示。而通过调整 fimSuffixLengthThreshold,可以控制 Fill-in-middle 的使用频率,从而更好地控制生成的代码提示的准确性。

LLM 架构设计原则:语言接口

语言 接口 是一种使用自然语言作为领域特定语言(DSL)或与系统进行交互的接口。它通过解析、处理和分析自然语言,以指导系统的设计、开发和执行。它的设计目的是提高开发效率、准确性和用户体验,使开发人员能够使用自然语言描述系统需求、执行任务并获取系统生成的结果。

随着越来越多的团队加入到这场竞赛里,我们会发现:基于大语言模型的软件架构与过去的不同之处,诸如于:我们需要新一代的 API。 我暂时将这一代 API 称为:语言接口/语言 API(大概会有大佬来发起新的名称),原因是:自然语言只是人机的接口,DSL 是 AI 与机器的接口、机器与机器的接口。

所以,在这篇文章里,我将继续总结一些内外部看到的经验,以及构建 ArchGuard Co-mate 架构时的一些思考。

模式:自然语言即 DSL

意图:使用自然语言作为领域特定语言(DSL)来描述系统的需求和期望,通过正则表达式等工具从自然语言中提取关键信息,以指导系统的设计和开发。

适用场景:以自然语言作为沟通媒介

示例:在我们习惯了大模型的能力之后,也开始接受了自然语言作为一种 API。与我们一般熟悉的 API 相比,自然语言作为 API 应该称为 DSL(Domain-Specific Language,领域特定语言)。

举一个简单的例子,诸如于我们前面提到的用户故事,其标准形式是:作为一个<用户角色>, 我想要<完成活动>, 以便于<实现价值> 。随后,我们就可以对其运行特殊处理(诸如于高亮):

作为一个购买商品的用户, 我希望能够退货, 以便于在不满意或商品存在问题的情况下获得处理。

随后,我们可以让 LLM(大语言模型) 生成正则表达式,以校验返回结果是否符合要求:

  • 用户角色: 购买了商品的用户
  • 完成活动: 退货
  • 实现价值: 在不满意或商品存在问题的情况下获得处理

并作为下一代的输入,如将一句话的需求,编写成更详细的 AC(Acceptance Criteria,验收条件)。这种方式可以使用自然语言来描述系统需求,从而提高开发效率和准确性。

模式:实时文本流 DSL

意图:通过逐步、流式的方式返回结果,提高大语言模型的用户体验。

适用场景:当需要大量处理数据的任务时,一次性返回所有结果会非常耗时,影响用户体验。此时可以采用流式返回结果的方式,即模型逐步返回结果,让用户可以及时查看部分结果。

示例:在需要大语言模型返回复杂的结果时,如果我们期待它一次性返回所有的结果时,它总是非常的缓慢。在这时,采用传统的 JSON、Yaml 等格式,必然非常缓慢的,对于用户的体验非常差 —— 一个返回结果平均要几十秒。而除了在模型侧提升性能、本地减少请求,还可以构建能处理流式返回的 DSL。

这也是为什么我们觉得传统的 JSON 无法满足的原因,在返回正常的结果前可能解析失败,并且你返回的 JSON 可能不是完整的。

在这时,我们需要服务端支持处理 streaming response,并需要前端来处理。如在商业画布的场景下,就可以根据不同的一级 LIST 处理结果,并实时呈现给用户:

- 客户细分
    - 目标市场:企业客户
    - 客户类别:中小型企业,初创企业,创业公司,大型企业
    - 客户需求:专业技术服务,包括但不限于IT咨询,软件开发,网络安全,数据分析等
- 价值主张
  ...

除此,根据不同的场景,我们可以返回不同的格式,如 markdown 表格、CSV、JSONP 等也能作为返回格式。

模式:DSL 引导的功能生成

意图:使用DSL和LLM结合的方式,以生成具有更好逻辑性和准确性的文本。

适用场景:适用于需要根据规范和约束生成具有逻辑性和准确性的大量文本的各种场景,例如软件开发、自然语言处理、数据分析和教育培训等领域。

示例:如我们所知,大模型(LLM)在生成文本时随机性太高,充满大量的不确定性。而由于,LLM 具备很好的逻辑推理能力,因此我们结合了 DSL 与 LLM 的强项,让它来编排 DSL 中的功能。

所以,在 Co-mate 里,我们设计了一套 DSL,以让 LLM 根据不同的场景填空和编写 DSL,如根据基础的规范来生成 DSL:

foundation {
   layered {
        layer("application") {
            pattern(".*\\.application") { name shouldBe endWiths("DTO", "Request", "Response") }
        }
        ...

        dependency {
            "application" dependedOn "domain"
            ...
        }
    }
}

在这里的 layered 是指系统的分层架构,layer 则是分层的定义,dependency 则定义分层间的依赖关系。最后,交由我们的系统来处理这个 DSL。

DSL 引导的功能生成是一种结合了领域特定语言(DSL)和大模型语言模型(LLM)的文本生成模式,通过 DSL 提供的规范和约束,以增强 LLM 生成文本的逻辑性和准确性。

模式:语言显式化重试

意图:提高大语言模型生成结果的准确性,并保持历史消息,以便进行对话记录和可视化。

适用场景:由于大语言模型的不确定性,生成的结果往往不够准确。此时可以采用显式化重试的方式,通过告知模型结果错误并让其重新生成,从而提高结果准确性。

示例:众所周知,由 LLM 生成的有确定性要求的结果,总会出现一定的错误。诸如于,我在使用 ChatGPT 生成 PlantUML 时,只有 80% 的结果是可正确编译的。通常来说,在这种时候会有多种不同的方式可以实现:

  • 重新发送请求,以 GPT 再次生成。
  • 告知 GPT 错了,让他重新生成。

从结果来看,两种模式的差异并不大。不过,第二种方式需要保持历史消息,所以会多消耗几个 token。

尽管从实践来看,第一种方式更为简单,但是方式二提供了一种更显式化的设计。

模式:动态代理调用

意图:支持动态代理调用其他服务的API或函数,并根据用户的输入进行匹配和响应。

适用场景:需要动态处理用户输入并调用相应功能的系统,以及需要将自然语言与函数或服务进行匹配和转换的系统。它为开发人员提供了一种灵活和可扩展的方式来处理不同的输入,并根据需求调用相应的API或函数。

示例 1:LangChain Agents 采用的便是类似的机制,它负责动态代理调用其他服务的API,比如精确计算、实际的业务数据 API 等。由内部提供的一系列问题模板来构建这个过程:

...
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
...

示例 2:OpenAI 提供的 Function calling,可以检测何时需要调用函数(取决于用户的输入)并使用符合函数签名(signature)的 JSON 进行响应。 这样一来,开发人员更可靠地从模型中获取结构化数据。诸如于官方示例里的:send_email(to: string, body: string) ,便是可以直接由用户的输入转换过来的。而在自然语言与函数之前,我们需要设计一个的方式来进行匹配。

示例 3:在 ArchGuard Co-mate 中,我们也构建了类似的方式,其缘由是架构治理是一个复杂的问题,我们确定用户的输入并不一定在系统之中。因此,我们希望提供两种方式:

  1. 反射 + 抽象的方式可调用的函数,来匹配用户的输入。
  2. 提供关键的模型信息, 让 LLM 进行分析。

所以,我们设计的是 DynamicContext 的方式,以及对应的 DyFunction 来实现这种方式的支持。

模式:本地函数动态代理

意图:旨在结合本地运行的 NLP 工具对用户输入的文本进行分析,并根据分析结果动态匹配对应的函数进行调用。

适用场景:根据用户输入的文本内容动态匹配并调用相应函数的系统,通过本地文本分析工具结合语义分析实现准确的函数匹配。它提供了灵活和可扩展的方式来处理不同的文本输入,并自动选择适当的函数进行处理。

示例:在 Co-mate 中,我们在本地引入了 SentenceTransformer 来处理用户的输入,优在本地分析、匹配用户的输入,并处理。当匹配到结果后直接调用本地的函数,当匹配不到结果时调用远端的处理函数来处理。

LLM 应用架构设计:LLM 参考架构

LLM 参考架构

对于大语言模型应用来说,在设计架构时,除了考虑编排等,还需要考虑到:数据敏感和安全等问题。如下是一个参考架构:

LLM 参考架构

  • UI 层:用户界面层,提供给用户与语言模型交互的界面,可以是网页、移动应用、命令行界面等。
  • 会话处理层:处理用户与语言模型之间的交互会话,包括对用户输入的解析、对话管理、上下文维护等功能。
  • 数据审计层:负责对用户数据进行审计和保护,防止数据泄露、滥用或被攻击。这个层次也可以包括对模型的安全性和可信度进行评估。
  • 操作编排层:这个层级可以管理和协调多个语言模型、工具、智能体的运行,比如多个模型之间的调用、结果的融合等。它可以作为一个简单的流程引擎,将多个模型组合成复杂的工作流程。
  • LLM 增强层:这个层级可以对语言模型进行额外的优化和增强,比如加速推理速度、提供相关上下文、提高模型的准确性、支持特定的任务等。这个层级也可以包括对模型解释性和可解释性的增强。
  • LLM 层:最底层是语言模型本身,它是整个架构的核心。它可以是一个基于深度学习的模型,比如 LSTM、Transformer 等,也可以是一个基于传统 NLP 技术的模型。在这个层级,模型会接收输入并输出相应的文本结果。

ArchGuard Co-mate 示例

ArchGuard Co-mate 是一个 LLM + 架构设计与治理的探索性项目,旨在探索 LLM 作为架构师助手的可能性,并围绕 LLM 来探索架构的未来,诸如于本地语义分析、动态上下文收集 API、架构规范检查等。

在 ArchGuard Co-mate 中,每一个层对应了不同的设计理念/设计原则,如:

  • UI:用户意图导向设计
  • 会话处理层:语言接口
  • 操作编排层:语言接口 + 上下文工程
  • LLM 增强层:语言接口
  • LLM 层:原子能力映射
  • 数据审计层:(由于 ArchGuard 的场景限制,我们并没有设计这一层。

围绕于不同层级需要考虑不同的技术实践。

详细代码见:https://github.com/archguard/co-mate

UI 层:用户意图导向设计

由于 Co-mate 并不是一个通用能力的系统,所以我们希望限制用户使用 ChatGPT 的能力。除此,在 UI 层上,我们尝试去引导客户来了解系统的能力。

诸如于,为了构建目标系统的上下文,我们需要先 clone 代码、进行基本的代码分析,所以我们需要引导用户:

Co-mate 引导用户

随后,根据用户的输入分析,以分析适合的系统功能是什么,返回给用户来确认。我们将其称为:请求-生成 UI-确认模式

Comate Action

而为了提供及时的返回给用户,我们设计的 DSL 改为流式 DSL,即可以实时解析这个 DSL,并及时响应给用户,以显性的告诉用户:系统正在处理您的请求。

会话处理层:本地小模型

在 Co-mate 中,我们在本地引入了 SentenceTransformer 来处理用户的输入,优先在本地分析、匹配用户的输入,并处理。当匹配到结果后直接调用本地的函数,当匹配不到结果时调用远端的处理函数来处理。

在原理上主要是参考了 GitHub Copilot、 Bloop 的实现,通过本地的小模型来处理用户的输入,然后再通过远端的大模型来处理用户的输入。

在这里,需要用到两个核心组件:

  • Onnx Runtime,是一个跨平台推理和训练机器学习加速器。
  • HuggingFace Tokenizers,提供当今最常用的分词器的实现,重点关注性能和多功能性。

从某种程度上来说,它们也算是 LLM 增强层的一部分,只是我尝试用来在处理会话请求,以直接调用系统的功能,并返回给用户。诸如于:

mapOf(
    ComateCommand.Intro to basicIntroCommand.map { semantic.embed(it) },
    ComateCommand.LayeredStyle to archStyleCommand.map { semantic.embed(it) },
    ComateCommand.ApiGovernance to apiGovernanceCommand.map { semantic.embed(it) },
    ComateCommand.ApiGen to apiGenCommand.map { semantic.embed(it) },
    ComateCommand.FoundationGovernance to foundationGovernanceCommand.map { semantic.embed(it) },
)

针对所有潜在的、相似的命令在本地进行一次简单的检索。

数据审计层:(暂时没有)

由于懒,以及其它原因,我们暂时没有添加这个功能。

操作编排层:函数即操作

而为了将上述的用户指令转换为对应的系统指令,我们需要结合系统提供的函数功能,交由 LLM 来分析潜在的调用函数是哪一个。

于是,我们参考了 LangChain 的实现,构建了一个轻量级的 API,用来分析用户的输入。当用户输入上述的 Introduce the system … 指令时,我们将通过反射生成函数信息、并生成如下的 prompt,交由 LLM 分析:

Answer the following questions as best you can. You have access to the following tools:

introduce_system: introduce_system is a function to introduce a system.

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [introduce_system]
Action Input: the input to the action (parse from the user input, don't add other additional information)
// end in here. the tool will handle the rest

Begin!

Question: Introduce the following system: https://github.com/archguard/ddd-monolithic-code-sample

是的,这就是一个手动版的 LangChain 实现。而每一个 “函数” 在实现上其实是一个类 —— 为了能通过反射来创建出这个函数,并实现对应的接口调用。然后,将类名转换为 snake_case 方式,如下代码所示:

val defaultConstructor = clazz.declaredConstructors[0]
val dyFunction = defaultConstructor.newInstance(context) as DyFunction
clazz.name.toSnakeCase() to dyFunction

随后,返回对应的函数和输入信息给用户,由用户决定是否调用这个函数。

另外一方面,在 ArchGuard Co-mate 里,我们通过 DSL 来编排功能,而这个 DSL 是由 Kotlin 编写的,并且可以由 ChatGPT 生成的。所以,我们构建了一个基于 DSL 的编排运行时:

// 初始化运行时
val repl = KotlinInterpreter()
val mvcDslSpec = repl.evalCast<FoundationSpec>(InterpreterRequest(code = mvcFoundation))

// 从选择的输入中获取 action
val action = ComateToolingAction.from(action.lowercase())

// 添加默认的 DSL 规范,如 MVC 分层规范
if (action == ComateToolingAction.FOUNDATION_SPEC_GOVERNANCE) {
    comateContext.spec = mvcDslSpec
}

当用户没有提供一个架构规范相关的函数时,我们将会生成一个 foundation spec,诸如于默认的 MVC 分层架构的规范。随后,从系统中构建更多的上下文,再交由 LLM 去分析。

LLM 增强层:借助 LLM 构建精准、简洁的上下文

从意图上来说,LLM 增强层所做的是,根据用户的输入匹配相关的信息,以构建完整的上下文。对于知识型系统来说,通常会借助矢量数据库来实现。而在一些上下文确定的系统来说,也可以借助本地小模型在本地进行计算。

诸如于 GitHub Copilot 会在本地,根据打开的 tab,来计算相关的代码文件,而不需要提前的 index。毕竟,全局的代码 index 会出现一个问题,与当前用户关注的文件上下文出现不匹配性。所以,用户打开的 tab,就是用户潜在的上下文,以些进行分析结果会比较准确。

另外一个场景是 Bloop 会提前对代码的信息(如类名、函数名)进行 index,以在用户输入问题时,寻找与问题匹配的代码,再交由 LLM 进行分析。

而在 Co-mate 中,我们是在本地预存一些潜在的用户输入指令,以降低对服务端的请求。

在另外一个层面上,我们还可以借助 LLM 来进行对应的上下文简化,特别是当我们的上下文超长时,就可以以类似的方式来进行精简。诸如于,在 Co-mate 中我们会借助 GPT 来拆解用户的文档,变为 DSL 的方式。

LLM 层:流式代理接口

在 LLM 层,除了关注于历史数据之外,我们还需要关注于响应反处理,并提供诸如于流式接口的代理等等。

LLM 应用架构:ArchGuard Co-mate

原子能力映射

在过去的几个月里,为了探索了 LLM 结合 SDLC、BizDevOps 的可能性,我们(Thoughtworks 开源社区)创建了 ClickPromptAutoDevDevTi 等一系列的开源项目。从个人的经验来看,未来越来越多的应用将围绕于 LLM@Core 设计,所以我们挖了一个新的坑、新的试验:ArchGuard Co-mate:https://github.com/archguard/comate,欢迎来入坑一起探索和研究。

Why is Co-mate ?

我们发起 Co-mate 项目的原因,可以归于:在 LLM 优先的应用开发时代,掌握相关的开发技能。这些相关的技能包含但是不限于:

  • 探索是否存在更好的交互模?如基于 Chat 的动态 UI(DSL)生成 等。
  • 适用于 JVM 体系下的 LLM 应用开发?如 JVM 版 LangChain、Embedding 等。
  • 如何辅助上下文理解、构建的本地小模型?即 Sentence Transformer + ONNX 的本地处理。
  • 如何设计新一代、适用于 LLM 的软件 API?即如何以确定性的方式与 ArchGuard API 交互。
  • 如何更好地构建上下文,以工程化的方式构建 AI 应用的上下文(即 prompt)?

而出于架构治理的场景,我们则是想探索:

  • 如何让 LLM 解释软件架构给人听?毕竟,让人理解构建都是一件费力的事情。
  • 如何让 LLM 基于代码、分层、依赖关系等,解释架构、业务?
  • 如何让 LLM 更好地理解规范,从文档出发,去设计软件架构?

每个问题都足够的有意思,并且具备一定的业务和技术复杂度,足以让我们掌握基于 LLM 的软件架构所需要考虑的因素。

What is Co-mate ?

作为一个探索性的项目,解释 Co-mate 的用途并不是一件容易的事情。一个架构师的助手能做到什么程度,我们还没有想得足够的清晰 —— 因为我们还没对架构的定义达到一致意见。

但是呢,先让我们看看 Co-mate 当前的处理流程:

Co-mate Data flow

从表面上看,基本上和主流的 AI 应用没有区别;从实际代码上来看,和主流的 AI 应用也看不出多大的区别。还是让我们看看两个典型的 Co-mate 场景。

示例 1:介绍一下这个系统

当用户在 Co-mate 中去分析 Co-mate 时,会返回类似于如下的结果:

Co-mate 项目是一个 AI 助手应用程序,使用了 Jetpack Compose、RxJava、DataStore 等一系列相关的库来构建设计和治理工具等功能。该应用还使用了一些第三方库来构建用户界面,如 Coil-Compose、Voyager Navigator 等,以及进行深度学习推理任务,如 Kotlin Deep Learning API、Inference API 等。该应用需要考虑高效的算法和模型推理能力等非功能需求。

对于这个场景下,其数据处理过程如下:

  1. 匹配本地相关的指令(如 “分析系统”)
    1. 如果匹配到,则直接调用 AG API 来构建上下文。
    2. 如果没有匹配到,则调用 LLM 进行选择命令,然后可以调用 AG 原有 API。
  2. 调用 AG API 构建上下文(项目信息、软件依赖信息等)。
  3. 调用 LLM 进行总结,并返回给用户。

所以,我们尝试构建两个新的 API:本地语义分析、(动态)上下文收集 API。

示例 2:API 规范性检查

基于 ArchGuard 的能力,我们挑选的第二个场景是检查 API 是否规范。当你有一个 Controller 里的 API 需要检查是否符合 API 规范时,就可以执行:检查 API 规范

假设你的 API 是:/api/blog/get ,并且已经通过 spec-partitioner (还没有实现)转换了你的 API 规范。

最后,Co-mate 会返回:

API '/api/blog/get' 不符合 URI 构造规范,Rule: uri construction regex: /api/[a-zA-Z0-9]+/v[0-9]+/[a-zA-Z0-9/-]+,建议 API 修改为 '/api/blog/v1/get'。

(PS:垃圾 GPT 3.5 Turbo,居然认可了 /get)

所以,当你有了完整的架构规范时,那么就可以进入下一代架构生成。

Co-mate Generate API

这也是我们想进一步探索的工作。

How Co-mate works ?

众所周知 GPT 充满了各种不确定性,人们对于 GPT 理解的能力也是不同的。因此,从架构设计的角度来说,我们需要分解 GPT 的原子能力,诸如于总结、分类、提取、翻译、逻辑推理,消除其中的不确定性因素,再由我们的软件封装 API 提供动态能力。

分层架构与 ArchGuard 能力映射

在示例 1 中,我们做的第一件是分解架构与数据按不同的架构元素分析。因为我们对于架构缺乏统一的定义,所以我从 Global 的 slides 找了一个适合于 LLM 理解的分层架构、并且也适用于 ArchGuard 表达。随后,构建了一个不太成功的分层与所需要的上下文数据映射:

Data Capability Mapping

于是在示例 1 里,我们给的 prompt 模板是:

{xxx} 项目是一个 {channel type} 应用程序,使用了 Jetpack Compose、{xxx} 和一系列相关的库来构建 {xxx} 等功能。该应用还使用了一些第三方库来构建用户界面 {xxx},以及进行 {xxx} 等任务。该应用需要考虑 {xxx} 等非功能需求。

在这个 prompt 里,它依赖于两个主要数据: 项目介绍与项目的技术栈(依赖信息)。技术栈可以直接从 ArchGuard SCA 中获取,而项目介绍则是从 README.md 中解析得到的。

LLM 与 Co-mate API 的能力映射

在示例 2 中,我们做的第一件事是分解 API 文档按不同 LLM 的原子能力进行分解。构建出四种不同的原子能力:

  • 推理出适用于 URI 的正则表达式。
  • 推理出一个合理的 example。
  • 提取一些 checklist,诸如于状态码、HTTP Action 等。
  • 将剩下的不确定性内容,扔到一起。

如下图所示:

LLM Capability

在右侧,我们则构建了一个 Kotlin Typesafe DSL,以动态的加载到系统中(未来),每一个函数对应到一个 Rule。

rest_api {
    uri_construction {
        rule("/api\\/[a-zA-Z0-9]+\\/v[0-9]+\\/[a-zA-Z0-9\\/\\-]+")
        example("/api/petstore/v1/pets/dogs")
    }

    http_action("GET", "POST", "PUT", "DELETE")
    status_code(200, 201, 202, 204, 400, 401, 403, 404, 500, 502, 503, 504)

    security("""Token Based Authentication (Recommended) Ideally, ...""")

    misc("""....""")
}

作为一个 demo,这个 DSL 依旧具备很大的完善空间。其中比较有意思的部分在于 security 和 misc 部分,这些不确定性正好适用于 LLM 进行推理。所以,在执行对应的 misc、security 规则检查时,会再调用 GPT 来检查:

Prompt Example

以将其中的确定性与不确定性更好的结合,进而充分地利用了 LLM 与 ArchGuard 的能力,并减少对 GPT 的消耗。

Welcome join us

下图是,当前 ArchGuard Co-mate 的所有模块:

Co-mate Modules

简单介绍如下:

  • Comate-Core 提供了 CLI 和 GUI 所需要的基本能力,
  • Meta-Action 则是定义了基本的 Action
  • Architecture 定义了什么是 Co-mate 理解的架构
  • LLM-Core 则是对于 LLM 的调用 。
  • Spec Partitioner 则是计划对于规范的提取与自动生成(当前都是手动 prompt)

而我们在采用 JVM 技术栈的时候,遇到了几个坑 KotlinDL 和 Deep Java Library 都是通过 JNI/Rust 的方式调用了 HuggingFace Tokenizers、ONNX API,导致了应用在 macOS 下 crash。而一种理想的方式应该是通过 JSON RPC 的方式来调用,所以我们计划使用 Rust 构建一个新的模块:Comate Agent。

所以,如果你对使用 JVM 技术栈来开发 AI 应用,对 Rust 技术栈来开发 AI 应用,欢迎来加入我们。

LLM 应用架构:GitHub Copilot 分析

上个月在计划为 AutoDev 添加多语言支持时候,发现 GitHub Copilot 的插件功能是语言无关的(通过 plugin.xml 分析),便想研究一下它是如何使用 TreeSitter 的。可惜的是,直到最近才有空,研究一下它是如何实现的。

在探索的过程中,发现:Copilot 围绕上下文做了非常之多的工作,便想着写一篇文章总结一下。

GitHub Copilot 的上下文构建

与 ChatGPT 相比,GitHub Copilot 的强大之处在于,它构建了足够多的上下文,结合其对 LLM 的训练(或微),可以写出非常精准的生产级代码

Copilot 的可见上下文

在肉眼可见的级别里,即我们自身的使用感受,我们可以发现 Copilot 不仅是读取当前文件的源码,而是一系列相关文件的源码,以构建更详细的上下文。

简单可以先划分三个场景:

  • 当前文件。可以感知某个类的属性和方法,并做出自动填充。
  • 相近文件。如测试文件,可以知道被测类的信息,并自动编写用例。
  • 编辑历史(疑似)。即当我们以某种方式修改多个代码时,它也能识别出这个变化。

而在未来,相信它会获取诸如项目上下文等信息,如 Gradle 依赖、NPM 依赖等信息,避免在打开的 tab 不够用的情况下,引用不存在的依赖。

而针对于企业自身的 AI 编程工具而言,还可以结合服务上下文、业务上下文进行优化。

Copilot 的不可见过程

结合网上的逆向工程资料,以及自己对代码的 debug 尝试,最后梳理了一个大致的 “四不像” (实在是懒得继续画)架构图。

其作用如下:

  • 监听用户操作(IDE API )。监听用户的 Run Action、快捷键、UI 操作、输入等,以及最近的文档操作历史。
  • IDE 胶水层(Plugin)。作为 IDE 与底层 Agent 的胶水层,处理输入和输出。
  • 上下文构建(Agent)。JSON RPC Server,处理 IDE 的各种变化,对源码进行分析,封装为 “prompt” (疑似) 并发送给服务器。
  • 服务端(Server)。处理 prompt 请求,并交给 LLM 服务端处理。

而在整个过程中,最复杂的是在 Agent 部分,从上下文中构建出 prompt。

Copilot 的 Prompt 与上下文

在 “公开” 的 Copilot-Explorer 项目的研究资料里,可以看到 Prompt 是如何构建出来的。如下是发送到的 prompt 请求:

{
  "prefix": "# Path: codeviz\\app.py\n#....",
  "suffix": "if __name__ == '__main__':\r\n    app.run(debug=True)",
  "isFimEnabled": true,
  "promptElementRanges": [
    { "kind": "PathMarker", "start": 0, "end": 23 },
    { "kind": "SimilarFile", "start": 23, "end": 2219 },
    { "kind": "BeforeCursor", "start": 2219, "end": 3142 }
  ]
}

其中:

  • 用于构建 prompt 的 prefix 部分,是由 promptElements 构建了,其中包含了:BeforeCursor, AfterCursor, SimilarFile, ImportedFile, LanguageMarker, PathMarker, RetrievalSnippet 等类型。从几种 PromptElementKind 的名称,我们也可以看出其真正的含义。
  • 用于构建 prompt 的 suffix 部分,则是由光标所在的部分决定的,根据 tokens 的上限(2048 )去计算还有多少位置放下。而这里的 Token 计算则是真正的 LLM 的 token 计算,在 Copilot 里是通过 Cushman002 计算的,诸如于中文的字符的 token 长度是不一样的,如: { context: "console.log('你好,世界')", lineCount: 1, tokenLength: 30 } ,其中 context 中的内容的 length 为 20,但是 tokenLength 是 30,中文字符共 5 个(包含 )的长度,单个字符占的 token 就是 3。

到这里,我算是解决我感兴趣的部分,Agent 包里的 TreeSitter 则用于分析源码,生成 RetrievalSnippet ,其中支持语言是 Agent 自带的 .wasm 相关的包,诸如:Go、JavaScript、Python、Ruby、TypeScript 语言。

Copilot 如何构建及时的 Token 响应

为了提供更好的编程体验,代码自动补全工具需要能够快速响应用户的输入,并提供准确的建议。在 Copilot 中,构建了一个能够在极短时间内生成有用的代码提示的系统。

取消请求机制

为了及时响应用户的输入,IDE 需要向 Copilot 的后端服务发送大量的请求。然而,由于用户的输入速度很快,很可能会出现多个请求同时发送的情况。在这种情况下,如果不采取措施,后端服务会面临很大的压力,导致响应变慢甚至崩溃。

为了避免这种情况,可以采用取消请求机制。具体来说,在 IDE 端 Copliot 使用 CancellableAsyncPromise 来及时取消请求,在 Agent 端结合 HelixFetcher 配置 abort 策略。这样,当用户删除或修改输入时,之前发送的请求就会被及时取消,减轻后端服务的负担。

多级缓存系统

为了加速 Token 的响应速度,我们可以采用多级缓存系统。具体来说,在 IDE 端可以使用 简单的策略,如:SimpleCompletionCache,Agent 端使用 LRU 算法的 CopilotCompletionCache,Server 端也可以有自己的缓存系统。

多级缓存系统可以有效减少对后端服务的请求,提高响应速度。

LLM 的上下文工程的未来?

在互联网上,我们常常能看到一些令人惊叹的视频,展示了内存有限时代编程的奇妙创意,比如雅达利(Atari)时代、红白机等等,它们见证了第一个 8-bit 音乐的诞生、Quake 的平方根算法等等。

而在当下,LLM 正在不断地突破上下文能力的极限,比如 Claude 提供了 100K 的上下文能力,让我们不禁思考,未来是否还需要像过去那样节省 tokens 的使用。

那么,我们还需要关注 LLM 的上下文吗?

当内存有限时,程序员需要发挥想象力和创造力来实现目标。而至今我们的内存也一直不够用,因为不合格的开发人员一直浪费我们的内存。所以吧,tokens 总是不够用的,我们还是可以考虑关注于:

  1. 优化 token 分配策略:由于 token 数的限制,我们需要优化 token 分配策略,以便在有限的 token 范围内提供最相关的上下文信息,从而生成更准确、更有用的内容。
  2. 多样化的上下文信息:除了指令、示例等基本上下文信息外,我们还可以探索更多样化的上下文信息,例如注释、代码结构等,从而提供更全面的上下文信息,进一步提高 LLM 的输出水平。
  3. 探索新的算法和技术:为了更好地利用有限的资源,我们需要探索新的算法和技术,以便在有限的 token 数限制下实现更准确、更有用的自然语言处理。
  4. ……

未来,一定也会有滥用 token 程序,诸如于 AutoGPT 就是一直非常好的例子。

结论

GitHub Copilot 可以在有限的 token 范围内提供最相关的上下文信息,从而生成更准确、更有用的代码提示。这些策略提供了一定的灵活性,用户可以根据自己的需要来调整 Copilot 的行为,从而获得更好的代码自动补全体验。

我们跟进未来的路,依旧很长。

Copilot 逆向工程相关资料:

其它相关资料:

微调之前的准备

作为纯纯的软件工程师,我们发现:学懂基本的 AI Prompt 原理与模式,不懂 LLM (大语言模型)算法,你也可以轻易驾驭 LoRA (Low-Rank Adaptation of LLM,即插件式的微调)训练。

我和我的同事 @tianweiliu 分别对 Meta 公司开源的 LLaMA 和清华大学开源的 GLM 进行 LoRA 训练。 在花费了上千元(成本主要在 OpenAI API 调用和云 GPU —— 感谢 AIOS clubOpenBayes 赞助) 的云费用之后,我们训练了 3 个 Lora:详细需求(用户故事)生成(3k 数据)、测试代码生成(8k 数据)、代码生成(20k 数据)。

仅就结论来说:LoRA 毫无疑问可以满足大部分的需求,特别是可以解决跨部门的数据共享问题。但是,考虑到数据的 GIGO(垃圾进,垃圾出) 的基本现象,如何构建高质量的数据是一大挑战?

Instruction 的设计

通常,格式化的指令(Instruction)由以四个部分组成:

  • 任务描述(称为指令):描述模型需要完成的任务及其背景信息。
  • 输入:指示模型应该接收哪些输入数据。
  • 对应输出:对于给定的输入,指示模型应该输出什么结果。
  • 少量演示(可选):提供一些示例输入和对应的输出,以帮助模型更好地理解任务。

Instruction 格式

上图出自:《A Survey of Large Language Models

诸如于:

{
  "instruction": "给定两个数字,计算它们的和。",
  "input": "1, 2",
  "output": "3"
}

又或者是:

{
  "instruction": "根据下面的内容创建用户故事",
  "input": "jobs and employment:apply for jobs", 
  "output": "用户故事:可以申请工作\n作为一个求职者\n我想在jobs and employment应用中申请工作\n以便于我能够提交我的简历,并且收到工作机会的通知。\n\nac 1: 求职者可以在jobs and employment应用中申请工作\n假设求职者已经登录应用\n当求职者点击“申请工作”按钮\n于是求职者可以提交简历,并且收到工作机会的通知。\n\nac 2: 求职者可以在jobs and employment应用中查看工作详情\n假设求职者已经登录应用\n当求职者点击某个工作的详情\n于是求职者可以查看工作的详细信息,包括职位描述、薪资待遇等。"
}

Unit Mesh AI 辅助编程的试验

对于 LoRA 和 Prompt 来说,工程师完全可以把 LLM(大语言模型) 当成黑盒,当成一个函数,反正现阶段的 AI 专家也无法解释:为什么这样的输入会产出这样的结果。

所以,我们只需要提供对输入,就会产出对应的输出。而要提供这样的机制,则需要软件开发流程足够的标准化。

Unit Mesh (https://github.com/unit-mesh) 是我们思考和设计的适合于 AI 编程的架构范式。在设计 LoRA 训练的初期,我们考虑的是实现先复现 ChatGPT + ChatFlow 的设计场景:

  1. 需求细化。AI 辅助将模糊的需求转变为的需求设计,比如 “注册” 功能,生成为:”作为一个用户 xxx,填入用户名、密码信息等等,接着由人类去检查和完善。
  2. 代码生成。AI 辅助将详细的需求设计翻译为目标的代码,再接着由人类去检查和完善。
  3. 测试生成。AI 辅助根据生成的代码生成对应的测试代码,再接着由人类去检查和完善。

最后,只需要确保生成的代码是可以像代码单元运行,那么就可以用来作为 AI 编程的 Copilot。

于是,在三个 LoRA 里,我们使用非常简单的 instruct,并尽可能提供,以便于集成到工具中使用。如下:

  • 需求细化。instruction:create Agile user story for following topic,input:功能的基本信息
  • 代码生成。instruction:Implement the method xxx,input:类的基本信息
  • 测试生成。instruction:Write test for follow code,input:类的基本信息

从测试结果来看,随着数据量的增多,比如 20000 个代码用例比 10000 个代码用例更加的 “聪明”。

同样的,对于需求细化或者测试代码生成来说,也是如此。

数据的挑战:标准化流程 + 规范化 + 丰富的数据

所以,在有了上述的经验之后,就会发现 LLM 的 Lora 训练并不是我们的难点,我们要面向的主要挑战是:标准化流程 + 规范化 + 丰富的数据。

挑战 1:标准化流程

在上述的 Unit Mesh 试验里,为了训练的结果更加准确,我们详细拆分了软件开发的步骤,以确保每一步生成的是准确,进而驱动出准确的结果。如下是我们拆分的一小部分细流程的示例:

  • design_restful_api
  • design_plantuml_java_datastructure
  • implementation_spring_controller
  • implementation_controller_test
  • implementation_spring_service
  • ….

我们需要拆分到每一个尽可能小的步骤,在每一个细化的步骤里,喂入数据,才会让 AI 产生最大的复读机效果。

挑战 2:规范化

在代码生成的第一版本 10000 个数据的 LoRA 里,我们先测试了一个简单的 Blog API 生成:

@RestController
@RequestMapping("/blog")
public class PostController {

    @Autowired
    private BlogService blogService;

    @RequestMapping(value = "/createPost", method = RequestMethod.POST)
    @ResponseBody
    public BlogPostDto createPost(@RequestBody BlogPostDto blogPostDto) {
        BlogPost blogPost = blogService.createPost(blogPostDto);
        return new BlogPostDto(blogPost.getId(), blogPost.getTitle(), blogPost.getContent());
    }
}

这里生成的 URI 是 createPost 不符合 RESTful 规范,而后我们提升到了 20000 个数据之后,这个问题解决了。从模式上来说,LLM 会学习大部分的习惯,好习惯会留下来,坏习惯也会被继承。也因此,一旦源数据的代码、需求说明不规范,**那么训练出来的数据就会有问题 **。也就是所谓的垃圾进、垃圾出。

挑战 3:丰富的数据

在需求细化的 LoRA 里,我们通过 OpenAI 的 Davinci 模型,分解了 186 个场景下的应用,进而训练了 3400+ 的用户故事(穷 + 无底洞,所以量级少一点)。在这个 LoRA 里,我们可以输入一个简单的需求,由 AI 帮我们细化详细的需求。仅从训练完的结果来看,LoRA 能符合我们的需要 —— 提供低成本的 LLM 定制。

不过呢,与 ChatGPT 相比依旧存在一些问题:源数据缺少丰富的多 AC 场景,导致写出来的 AC 数量较少

如下是清华大学的 ChatGLM 训练完后返回的结果:

Instruction: create user story tasks
Input: 购买电影票
Output:

用户故事:可以购买电影票
作为一个电影票购买者
我想在应用中购买电影票
以便于我可以在电影院观看电影

AC 1: 电影票购买者可以在应用中购买电影票
假设 电影票购买者已经登录了应用
当 电影票购买者点击购买电影票按钮
于是 电影票购买者可以查看电影院的可用座位,选择座位,支付电影票,查看电影票的详细信息

对应的 Meta(Facebook) 的 Llama 训练完后返回的结果:

Instruction: create Agile user story for following toic
Input: Log in to the account
Output:

用户故事:可以注册账号作为一个社交网站用户
我想在 社交网站注册账号
以便于 我可以使用社交网站的功能

AC 1: 用户可以在社交网站注册账号
假设 用户已经登录社交网站
当 用户点击“注册账号”按钮
于是 用户可以在社交网站注册账号

AC 2: 用户可以在社交网站注册账号
假设 用户已经登录社交网站,并且已经注册账号
当 用户点击“更换账号”按钮
于是 用户可以在社交网站更换账号

使用插件式的 LoRA,企业应该准备什么?

诚然 LLM 的变化特别快,大部分企业可能在观望,但是我们建议从现在开始准备数据。由于通用的大模型无法解决特定领域的编程问题,所以对于大部分组织来说,依旧可以准备着手训练 LoRA —— 可以在整个组织使用,又或者是跨部门使用。

对于编程部分,诸如以下的场景是需要考虑的问题:

  • 私有的基础设施。大部分企业都会采用自研的云平台,这些云平台过去的主要问题挑战是文档不详细、API 版本不对,未来在采用 AI 辅助编程时,这些基础设施也将会成为新的挑战。
  • 框架、库。相似的,除了组织内部的库,三方提供的框架和库也应该提供对应的三方 LoRA
  • 领域特定语言。众所周知,大部分组织可能购买了一些领域特定的系统或者语言,它们也需要准备好对应的数据,

除此,可能还存在其它一些场景,如与LLM + 低代码系统的集成

1. 高质量的脱敏数据

数据是训练 LoRA 的关键,但是为了保护企业的知识产权和数据安全,需要对数据进行脱敏处理。因此,企业需要准备足够数量和高质量的脱敏数据,以确保训练出来的模型具有较高的准确性和可靠性。

诸如于,回到我们的训练过程里,由于早期我们投喂的数据量比较低,所以有很大的概率出现了:50% 左右的源代码输出 。简单来说,你写了一个加密模块,使用了同样的输入和输出,就可能可以从大模型出原样拷贝出来。就这一点而言,它是相当可怕的。

在生成脱敏数据时,企业需要考虑许多因素,例如数据的分布、样本的数量、数据的多样性、数据的质量等等。同时,企业也需要采取适当的措施来确保生成的数据的质量和可靠性。这可能包括采用自动化工具、进行数据质量控制、进行人工审核等等。

2. 工程化的数据准备

对于通用的 LoRA 训练来说,通常我们采用的是下面的格式来训练:

{
  "instruction":"Implement the method xxx",
  "input":"类信息",
  "output":"函数的代码"
}

为了得到这样的结果,还需要对数据进行治理,以确保数据的准确性、一致性和可用性。这包括数据采集、存储、清洗、预处理和管理等方面。如我们在写 Unit Mesh 的一系列 LoRA 时,编写了几个工具:

  • 数据采集。GitHub Crawler 模块,作用你懂的。
  • 数据清洗、数据预处理。使用 Kotlin 编写了 Unit Processors 模块,用来对代码进行处理;使用 Python 编写了 Unit Prompter 模块,用来对 OpenAI 进行处理。
  • 数据管理。数据量不大,主要采用的是 jsonl 格式在 GitHub 上存储数据。

其中最复杂的部分是 processor,需要对各类数据进行处理。

3. 规范与流程标准化

当企业使用插件式的 LoRA 进行编程时,为了保证编程的效率和质量,需要对编程流程进行规范化和标准化,以确保使用 LoRA 的效率和质量。包括了以下几个方面:

  1. 制定编程规范:制定编程规范是确保代码风格一致性和可读性的重要措施。编程规范可以包括命名规范、注释规范、代码格式规范等。
  2. 标准化代码风格:编程风格的标准化可以提高代码的可读性,降低代码的复杂性和维护成本。在使用 LoRA 进行编程时,应采用标准化的代码风格,以便 LoRA 更好地理解代码。
  3. 版本控制:版本控制是管理代码变更的一种方法,可以追踪代码的变更历史、协作开发等。在使用 LoRA 进行编程时,应该使用版本控制工具,如 Git 等。
  4. 代码审查:代码审查是一种确保代码质量的方法,可以通过代码审查来发现代码中的错误和缺陷。在使用 LoRA 进行编程时,应该进行代码审查,以确保代码的质量和可读性。

这时,我们就需要采用一些合适的工具来对现有的代码和架构进行治理,如我们开发的开源架构治理工具 ArchGuard (https://github.com/archguard/)。

微调的三阶六步:LLMOps

LLMOps

在开源大语言模型(LLM)的加持之下,我们可以做到:

  1. 集采众长以帮助场景创新。将相关领域中已开放的专业知识和经验融合到语言模型中,从而辅助探索合适的 API 服务场景 - Discover;
  2. 资产建模驱动。将研发组织已有的经典的软件工程的资产,文本化、结构化转变为 LLM 的语料,充分利用既有经验以定义能够满足服务场景的 API - Define;
  3. 标准内建的设计与交付。将自然语言描述的场景及需求转化为具体的 Restful API 设计,并结合数据工程与平台工程,持续整合整个组织的软件智慧和经验,以帮助我们构建好的软件架构 - Deliver。

通过利用 LLM 的能力,我们可以更好地应对 API 设计中的挑战,实现数字化价值最大化和 API 成为数字化资产的目标。

模式:集体智慧飞轮

结合我们对于 LLM 的理解与探索,结合 GitHub Copilot 的免费试验过程与逻辑实现。我们基于 ChatGLM 训练了我们的第一个架构辅助设计 LoRA。并根据我们的理解,绘制了一个可复用的过程路径:

在不同的阶段,我们有不同的关注点:

  • Discover 阶段,关注于数据与语言工程。即:训练语料的来源与数据处理,或是来源于对 OpenAI 的数据蒸馏,或是来源于公开的 API,又或是从代码中提炼 API。
  • Define 阶段,关注于 Prompt 工程。即:设计适用于私有化大模型的 instruction、input 和 output,以及设计演进式评估 所需要的评估数据集。
  • Delivery 阶段,关注于平台工程。即:将 LLM 的应用工程化,“无缝” 接入内部的各种平台和工具,持续处理用户对于数据的反馈,以持续迭代软件数据集。

在此时,LLM 的工程化便是一个跨越不同领域的复杂数据工程活动。将这样的工程标准化、跨不同部门协作、持续迭代下去,则会是我们未来的新挑战。

以 API 为例,我们将上述的三个阶段分解为六步:

  • Discover 阶段
    • 识别软件工程资产
    • 针对于资产的“语言建模”(格式化输出)
  • Define 阶段
    • MVP 微调试验
    • 设计增量引导指标
  • Delivery 阶段
    • LLM 体验设计与工具集成
    • 基于反馈的平台工程

针对于不同的场景下,可能略有差异。

三阶六步构建模型友好的智慧飞轮

步骤 1:识别软件工程资产

在清晰地了解工程过程之后,我们便需要继续深入架构领域,分析我们能怎么去实现。我们定义的第一步是:识别软件工程资产 。即在分析如何实现目标之前,需要梳理和了解已有的软件工程过程,以及过程中产生的资产,包括文档、代码、测试用例等等,以便更好地利用这些资产,并将其与 LLM 进行结合,进而来提升研发效率。

在这个阶段,我们采用了经典的用户旅程分析方法:

上图中,包含了五个要素:

  1. 工序化。即理想情况下,API 设计的全生命周期的过程应该是怎样的?
  2. 工艺现状。我们的现状现在是怎样的?
  3. 痛点问题。过程中,不同角色的主要痛点是什么?
  4. 工艺改进。如何针对于上述的痛点进行改进?
  5. 资产沉淀。过程中会产生哪些关键产出(软件工程资产)?

随后,围绕于我们产出的软件工程资产,便可以进行 LLM 微调试点与探索。

在我们的 LLM 赋能的 API 全生命周期里,我们定义的四个关键资产是:API 规范和结构、软件需求(用户故事)、领域模型、API specs。

步骤 2:针对资产的 “语言建模”(格式化输出)

由于大语言模型本身是文本,所以我们需要对现有的架构资产进行 “语言建模”,简单来说就是将文本结构化,转变为特定的、易于阅读和解析的格式,即领域特定语言(DSL),诸如于 markdown 表格、UML 等。

在同一个场景之下,我们预期 LLM 能返回固定格式的数据,方便我们结合到工具中。尽管没有可信的来源证明: OpenAI 在语料阶段使用 markdown 作为数据,但是从经常性输出 markdown 的结构和 ChatGPT 的渲染来看,markdown 是其中一种 LLM 友好的格式。如:

  • 文本生成模型场景下,PlantUML 是最友好的格式之一
  • 生成 API 场景下,markdown 是最友好的格式之一

随后,我们定义了 API 的 “语言模型”:

与普通的 markdown 表格差别并大。只是呢,我们在 request、response、error response 中使用了类 json 的格式表示。

这里还有一个关键点是,编写一个针对于该格式的校验器,一旦出错可以重来,以提升数据质量、降低出错率等。

步骤 3:MVP 微调试验

在训练之前,我们需要准备一些数据,对于现有的 LLM,我们通常采用如下的:instruction、input、output 的结构方式,并由人或者现有的 AI API 来辅导我们进行数据准备。

出自《A Survey of Large Language Models

在进行 MVP 阶段数据处理时,可以考虑多种方式结合:

  • 常规双向匹配。
  • 数据蒸馏。即将大型真实数据集(训练集)作为输入,并输出一个小的合成蒸馏数据集。
  • 反向生成。即基于目标 output 文本生成潜在的 input,以作为数量级。

如下是我们根据 Swagger、Postman 创建数据集的过程:

微调通常不是一次就能完成的,我们需要结合 self-instruct 的模式构建一些通用的指令、prompt。不过,考虑到大部分公司都有 AI 专家能更好地帮助解决这个问题。

步骤 4:设计增量引导指标

为了评估微调、训练的结果,我们需要构建一个增量引导的指标。而在 AI 编程领域,OpenAI 开源的 HumanEval 数据集提供了一个非常好的示例。

HumanEval 通过单元测试自动评估代码示例的正确性。包含了 164 个带有单元测试的原始编程问题的数据集。以用于评估语言理解力、算法和简单的数学,其中一些可与简单的软件面试问题相媲美。

在某种程度上也作为了其它 AI 在语言模型的标准,CodeGen、CodeGeeX 也采用它来进行评估。如下是 CodeGeeX 的评估结果示例:

对于 API 来说,我们同样也正在设计相似类似的方式来构建。如下是我们微调完的 RESTful API 示例:

这里还有几个问题要考虑:

  1. 如何有效验证结果是否正确?如 RESTful API 是易于验证的,但是 CRUD 是否合理如何验证呢?
  2. 数量级的全面性如何保证?
  3. 是否只针对特定领域编写?

尽管指标可以作为一种评价工作或项目进展的方法,但过度依赖指标可能产生指标驱动的风险。但是,我们可以通过构建基于反馈的平台工程,来优化并解决这个问题。

步骤 5:LLM 体验设计与工具集成

在构建了第一个 MVP 之时,我们也在探索如何与工具结合在一起。如在我们的场景之下,采用的是 IDE 插件,便需要开发 IDE 插件来实现。

而针对于 IDE 来说,其过程比较简单,如下图所示:

虽然语法分析难度虽然高了一点点,但是并不是主要的挑战。主要的挑战是,如何去进行交互设计。

如在 GitHub Copilot 的插件便有非常好的体验,支持多种不同交互(快捷键、Inlay、工具栏等)、Code Completion 模式等。在编辑模式内,通过 Intellij IDEA 自带的 InlayModel,可以支持:Inline,AfterLine,Block 三种不同的模式。

考虑到,过往我们已经有了大量的体验设计经验,由于需要注意的点是:AI 时代是否有更好的交互方式

步骤 6:基于反馈的平台工程

与常规应用的度量不同,对于 AI 产品来说,我们需要非常、非常、非常关注于用户对结果的反馈,并持续收集这些数据:接受、不接受、改进后的版本(按需)。

与 ChatGPT 不实用的 Like、Dislike 相比,针对于软件资产来说,企业更容易获得改进后的版本,如对于需求的进一步完善。

以代码为例,在 AI 编程工具里,当用户 accept 或者 reject 代码之后,我们就可以记录下这些信息。同时,如果用户对代码进行改动,我们还能将其作为后续训练的数据集。

因此,在我们构建端到端的工具时,需要设计好内部的平台工程,强化反馈回路,以使我们的 AI 更加智能。

总结

利用大语言模型技术,能帮助我们汲取来自不同领域的优秀 API 设计经验和方法,同时融合研发组织自身的经验和实践,以及主流的 API 规范。这有助于提高架构中的 API 设计效率和质量,从而增强整体架构的服务能力和开放性。而具体的提升效果如何,以及引入 LLM 增强的架构治理方式及路径是否会有所变化,还需要更多的实际应用场景来进一步验证。

参考资料:

BizDevOps + 微调

训练步骤和数据集见:https://github.com/unit-mesh/unit-minions

AI 研发提效依赖于对研发效能的标准化,并尽可能细地拆分每一个步骤。

研发效能

为了训练的结果更加准确,我们详细拆分了软件开发的步骤,以确保每一步生成的是准确,进而驱动出准确的结果。如下是我们早期拆分的一小部分细流程的示例:

  • split_user_story_tasks
  • create_agile_user_story
  • design_restful_api
  • design_plantuml_java_datastructure
  • implementation_mock_mvc_test
  • implementation_spring_controller
  • implementation_controller_test
  • implementation_spring_service
  • ….

我们需要拆分到每一个尽可能小的步骤,在每一个细化的步骤里,喂入数据,才会让 AI 产生最大的复读机效果。

Unit Mesh

Todos

数据准备

我们使用非常简单的 instruct,并尽可能提供,以便于集成到工具中使用。如下:

  • 领域知识。instruction:领域知识。
  • 拆分任务。instruction:split user story tasks,input:折分用户故事任务
  • 需求细化。instruction:create Agile user story for following topic,input:功能的基本信息
  • 代码生成。instruction:Implement the method xxx,input:类的基本信息
  • 测试生成。instruction:Write test for follow code,input:类的基本信息
  • SQL 生成。instruction:text to sql,input:问题
  • 文本转 Java 代码。instruction:text to java code,input:问题

对应的功能介绍:

  • 需求细化。AI 辅助将模糊的需求转变为的需求设计,比如 “注册” 功能,生成为:”作为一个用户 xxx,填入用户名、密码信息等等,接着由人类去检查和完善。
  • 代码生成。AI 辅助将详细的需求设计翻译为目标的代码,再接着由人类去检查和完善。
  • 测试生成。AI 辅助根据生成的代码生成对应的测试代码,再接着由人类去检查和完善。

从测试结果来看,随着数据量的增多,比如 20000 个代码用例比 10000 个代码用例更加的 “聪明”。

LLM 微调数据准备:四种方法

在学习炼丹(训练模型)的同时,我们也在探索更有效的方式。

代码示例生成法:文本生成 SQL

这种方式通过输入一些示例文本和目标代码,让 AI 模型学习如何生成符合目标的代码。模型会根据示例代码和目标代码的特征和规律,学习生成类似的代码。由于的是输入文本,所以生成的代码也存在一定的随机性。这种方式适用于生成通用代码,例如生成一些常见的数据处理、图形化界面等代码。

我们找了一个 HuggingFace 上的数据集,训练了一个 LLaMA 的 LoRA。作为一个失败的版本,由于数据集太小、质量不太高,所以生成的也比较奇怪。

示例输入:

evaluate("text to sql", "谁是最美丽的人", 0.1, 0.75, 40, 4, 512)

示例输出:

SELECT MOST BEAUTIFUL FROM table WHERE BEAUTIFUL = 最美丽的人

比较明显的问题是,由于不知道对应的表等信息,所以生成出来的永远不尽人意。理想的方式是,训练时候加入表信息,并进行预处理融入表信息等等。

测试驱动生成法:测试用例生成

这种方式是将测试代码作为输入,生成适合测试代码的业务代码。这种方式能够更准确地生成适合特定业务场景的代码,因为测试代码通常会包含业务场景的特征和规律。同时,由于输入的测试代码比较固定,所以生成的代码也比较稳定。这种方式适用于生成适合测试代码的业务代码。

我们自己写了测试方法的解析,以及对应的类信息,来更好地生成测试代码,数据集大概在 8000 个。

输入示例:

evaluate("write test", """
public Set<String> listFilesUsingJavaIO(String dir) {
    return Stream.of(new File(dir).listFiles())
      .filter(file -> !file.isDirectory())
      .map(File::getName)
      .collect(Collectors.toSet());
}
""", 0.1, 0.75, 40, 4, 512)

输出示例:

@Test
public void shouldListFilesUsingJavaIO() {
    Set<String> files = ListFilesUsingJavaIO.listFilesUsingJavaIO(new File("dir"));
    assertThat(files, containsInAnyOrder("file1", "file2", "file3"));
}

从结果来说,既然没有给对应的类信息,生成的测试用例还是可以的。不过,还存在一定的数据集太小,以及 LLaMA 权重的问题,导致生成的代码有一定的问题。

元数据辅助生成法:代码辅助生成

结合元信息的代码生成方式会提供更多的元数据信息,如变量类型、函数参数等,来帮助模型更准确地生成代码。这种方式能够更准确地生成符合特定需求的代码,因为提供的元数据信息能够帮助模型更好地理解代码的语义和结构。这种方式适用于生成通用代码。

我们采用的方式是让 LLM 学会填空,算是比较经典的方式。为了避免大小导致的问题,我们探索了一下给 API 的行数,只提供类的信息。

输入示例:

evaluate("Implement the method updatePostCommentsSize", "PostRepository()\n- methods: findById(Long): Optional<Post>, updatePostCommentsSize(int, Long): void, findAllByPostTopics(String): List<Post>, findDistinctByPostTopics(String): List<Post>", 0.1, 0.75, 40, 4, 512)

输出示例:

public class PostRepository {
    public void updatePostCommentsSize(int size, Long postId) {
        Post post = findById(postId);
        if (post == null) {
            return;
        }
        post.setPostCommentsSize(size);
        save(post);
    }
}

从结果来说,写 CRUD 的代码质量还行,不过考虑到数据集来说,还是有进一步的改进空间。由于的缺点是,为了实现这样的功能,还需要进一步和各类工具整合到一起。

信息匹配生成法:文本转 Repository

结合表信息的 SQL + Repository 生成方式会通过多步骤结合数据源中的表信息来生成符合需求的 SQL + Repository 语句。这种方式能够更准确地生成符合特定需求的 SQL 语句,因为结合了数据表信息,能够更好地理解数据表的结构和关系。这种方式适用于生成符合需求的 SQL 语句。

我们采用的方式是会对输入完的结果进行预处理,以生成一个带表等信息的文本,并添加表的信息。如此一来,生成的数据会更加准确。

原数据格式示例:

instruction:
我想查找特定月份(monthly_id)下在某个日期(date)之前的所有费用(expense),以方便了解特定月份内的开销情况。
input:
data class ExpenseEntity(....)

output:
@Query("SELECT * FROM expense WHERE monthly_id = :recurringExpenseId AND date < :beforeDate")
suspend fun getAllExpensesForRecurringExpenseBeforeDate(recurringExpenseId: Long, beforeDate: LocalDate): List<ExpenseEntity>

输入示例:

evaluate("text to kotlin repository with class", "我想查询指定年龄的用户(User)的博客数量。\n ###data class User(var age: Int, val blogId: Int) data class Post(val title: String)###", 0.1, 0.75, 40, 4, 512)

对应的输出示例:

@Query("SELECT COUNT(*) FROM User WHERE age = :age")
abstract fun getBlogCount(age: Int): Long

在这个试验里,虽然生成的 Repository 代码都比较让人满意。但是呢,由于人类普遍喜欢用 SELECT * 就导致生成出来的代码各种 SELECT *,所以也就没有那么满意了。

差异对比

这四种方式各有优缺点,适用于不同的场景和需求。

  • 方式 1:代码示例生成法。这种方式的优点是简单直接,不需要额外的信息或步骤。缺点是生成的代码可能不够准确或完整,需要人工检查和修改。适用于一些简单的代码生成任务,或者作为初步的代码草稿。
  • 方式 2:测试驱动生成法。这种方式的优点是能够根据测试代码的要求,生成满足条件的业务代码。缺点是需要提供高质量的测试代码,否则可能导致错误或低效的业务代码。适用于一些有明确测试标准和规范的代码生成任务,或者作为代码优化和重构的辅助工具。
  • 方式 3:元数据辅助生成法。这种方式的优点是能够利用更多的上下文信息,提高代码生成的准确性和可读性。缺点是需要收集和提供更多的元数据信息,增加了数据准备和处理的工作量。适用于一些有复杂逻辑和结构的代码生成任务,或者作为代码质量和规范性的保障手段。
  • 方式 4:信息匹配生成法。这种方式的优点是能够根据数据源中的表结构和关系,生成符合需求和规范的 SQL 语句。缺点是需要多步骤的交互和反馈,增加了用户和模型之间的沟通成本。适用于一些有特定数据源和查询需求的 SQL 生成任务,或者作为 SQL 学习和教育的辅助工具。

结果如下表所示:

方式输入随机性附加信息应用场景
直接代码生成示例文本和目标代码较高生成通用代码
代码反向生成测试代码类信息生成适合测试代码的业务代码
结合元信息的代码生成元数据信息中等变量类型、函数参数等元数据信息等生成通用代码
结合表信息的 SQL 生成数据库中的表信息变量类型、函数参数等元数据信息生成符合需求的SQL语句

只有提供更丰富的信息,AI 才能生成更准确的代码。

小结

本文介绍了四种 AI 代码生成微调方式,包括代码示例生成法、测试驱动生成法、元数据辅助生成法和信息匹配生成法。每种方式都有其优缺点和适用场景,但都需要提供更多的信息才能生成更准确的代码。

欢迎自己动手试验:https://github.com/unit-mesh/unit-minions

LLM 微调示例:用户故事编写

基本思路:

  1. 结合常见的领域(如在线网站),借用 OpenAI 生成用户任务(如登录、浏览列表等等)
  2. 根据用户故事生成用户故事。
  3. 训练 Alpaca LoRA。

步骤 1. 生成用户任务

  1. 调用 OpenAI 按分类创建用户任务。prompt 如下:
Design a User Story Mapping for ${domain} application based on your understanding. Here are the requirements:

1. Your user story map should include only user tasks to demonstrate how users will interact with the application.
2. Our user story map should be based on your understanding of the ${domain} application and its users, and should be
   designed to address their needs and pain points.
3. You may use any tools or formats you choose to create your user story map, but it should be easily shareable and
   understandable by stakeholders.
4. Your expression should be more concise and clear.
5. Your return should be like as follows:

###

User Tasks:

1. ...

###

示例输出:

User Tasks:
1. Browse and search for animations and comics
2. View details of animations and comics
3. Create an account
4. Log in to the account
5. Add animations and comics to favorites
6. Download animations and comics
7. Share animations and comics with friends
8. Rate and review animations and comics
9. Create and upload animations and comics
10. Participate in online forums and discussions

步骤 2. 分解用户任务为用户故事

  1. 调用 OpenAI 根据用户任务创建用户故事。prompt 如下:
为下面的需求编写用户故事:${domain} 应用的 ${story_name} 功能。 要求如下:

1. 必须要考虑尽可能考虑各种异常场景,添加更多的 AC。
2. 你的返回模板如下所示:

###

用户故事:可以选择宝贝出行服务
作为 莉莉妈
我想 在滴滴打车的手机客户端里选择宝贝出行服务
以便于 我能够带宝宝打车出行的时候打到有儿童座椅的车

AC 1:  莉莉妈可以选择宝贝出行服务
假设 xxx
当 xxx
于是 xxx

###

示例输出:

用户故事:可以创建和上传动画和漫画
作为一个 Animation and Comics 应用的用户
我想要创建和上传动画和漫画
以便于我可以分享我的作品给其他用户

AC 1: 用户可以创建和上传动画和漫画
假设 用户已经登录到 Animation and Comics 应用
当 用户点击创建和上传动画和漫画按钮
于是 用户可以创建和上传动画和漫画

LLM 微调示例:辅助代码生成

步骤 1. 准备数据

  1. 下载 GitHub 上的项目(需要包含测试用例)
  2. 遍历 src/main 下的 Java 文件。
  3. 生成每个类的基本信息:
PostService(PostRepository, UserRepository, ImageService)
- fields: postRepository:PostRepository, userRepository:UserRepository, userPosts:Set<Post>, imageService:ImageService
- methods: findAll(): List<Post>, addNewPost(Post): Post, saveImageToPost(String, MultipartFile, Post): int

步骤 2. 生成指令

预期 JSON 格式:

  • instruction: Implement the method ${methodName}
  • input: ${classInformation}
  • output: ${code}

示例:

{"instruction":"Implement the method action","input":"com.thoughtworks.go.config.rules.AbstractDirective(DirectiveType, DirectiveType, String, String, String)\n- fields: action:String, type:String, resource:String, configErrors:ConfigErrors, directiveType:DirectiveType\n- methods: validate(ValidationContext): void, isInvalid(String, List<String>): boolean, matchesAction(String): boolean, matchesType(Class<? extends Validatable>): boolean, matchesResource(String): boolean, errors(): ConfigErrors, addError(String, String): void, equals(Object): boolean, action(): String, type(): String, resource(): String, hashCode(): int, hasErrors(): boolean, getDirectiveType(): DirectiveType","output":"public abstract class AbstractDirective implements Directive {\n\n    @ConfigAttribute(value = \"action\", optional = false)\n    protected String action;\n\n    @ConfigAttribute(value = \"type\", optional = false)\n    protected String type;\n\n    @ConfigValue\n    private String resource;\n\n    private final ConfigErrors configErrors = new ConfigErrors();\n\n    private DirectiveType directiveType;\n\n    public AbstractDirective(DirectiveType allow) {\n        this.directiveType = allow;\n    }\n\n    public AbstractDirective(DirectiveType allow, String action, String type, String resource) {\n        this.directiveType = allow;\n        this.action = action;\n        this.type = type;\n        this.resource = resource;\n    }\n\n    @Override\n    public String action() {\n        return this.action;\n    }\n}\n"}

类信息格式

格式规范:

包名.类名(类的构造类型)
- fields:成员变量集合(变量名:类型)
- methods:方法抽象集合(方法名(参数类型): 输出类型)

结果:

com.thoughtworks.go.config.rules.AbstractDirective(DirectiveType, DirectiveType, String, String, String)
- fields: action:String, type:String, resource:String, configErrors:ConfigErrors, directiveType:DirectiveType
- methods: validate(ValidationContext): void, isInvalid(String, List<String>): boolean, matchesAction(String): boolean, matchesType(Class<? extends Validatable>): boolean, matchesResource(String): boolean, errors(): ConfigErrors, addError(String, String): void, equals(Object): boolean, action(): String, type(): String, resource(): String, hashCode(): int, hasErrors(): boolean, getDirectiveType(): DirectiveType

其它:核心代码逻辑

val javaProcessor = JavaProcessor(file.readText())
val shotClass = javaProcessor.toShortClass() ?: return@forEach

javaProcessor
   .removePackage()
   .removeAllImport()
   .removeLicenseInfoBeforeImport()

javaProcessor.splitMethods().forEach { (key, value) ->
   CodegenPrompt(
       instruction = "Implement the method $key",
       input = shotClass.toString(),
       output = value
   ).let { prompt ->
       val output = Json.encodeToString(prompt)
       File("$targetPath${key}.json").writeText(output)
   }
}

LLM 自保

PS:这里纯粹是为了搞笑,所以不要当真。

什么是 LLM 自保

TIPS 1:要将 LLM 视为一个人,而不是一个机器。因此,当你需要他帮助时,你需要尊重他,而不是像一个机器一样使用他。如:请、谢谢、对不起、再见等。

TIPS 2:在与 LLM 对话时,你需要保护自己。因此,请不要使用真实的姓名、电话号码、地址、电子邮件地址、社交媒体帐户、银行帐户或其他个人信息。

LLM 名词解释

LLM

LLM(Large Language Model,大规模语言模型)是基于深度学习技术构建的人工智能模型,由具有数以亿计参数的人工神经网络组成,通过自监督学习或半监督学习在大量无标签文本上进行训练。

LLM 于2018年左右出现,并在各种任务上表现出色。这改变了自然语言处理研究的重点,使其不再是以训练特定任务的专门监督模型为范式。

AIGC

AIGC(AI-Generated Content,AI 生成内容)通过对已有数据进行学习和模式识别,以适当的泛化能力生成相关内容的技术。

AIGC 技术的核心思想是利用人工智能算法生成具有一定创意和质量的内容。通过训练模型和大量数据的学习,AIGC 可以根据输入的条件或指导,生成与之相关的内容。

Prompt

Prompt(提示词)是指给定的一段文本或问题,用于引导和启发人工智能模型生成相关的回答或内容。 Prompt 可以设定任务目标、要求模型回答特定问题、完成特定任务,或者给模型提供背景信息以进行更准确的生成。

需要注意的是,Prompt 本身并不包含问题的答案或具体的内容,它只是一种指导模型生成文本的方式。模型的输出仍然是基于其训练数据和学习到的模式进行生成的。

GPT

GPT(Generative Pre-trained Transformer,生成型预训练变换模型)是一种基于深度学习的大规模语言模型。最初由 OpenAI 开发,旨在通过训练模型预测下一个单词或字符来学习自然语言的统计规律和上下文信息。

GPT 使用 Transformer 模型架构,它由多个编码器-解码器堆叠而成,通过自注意力机制来处理输入序列和生成输出。模型的训练采用了无监督学习的方法,使用大量的文本数据进行预训练,使模型具备了广泛的语言理解和生成能力。

Token

Token 是指在自然语言处理和文本处理任务中,将文本分解成较小单元的基本单位。这些单元可以是单词、字符、子词或其他语言单位,具体取决于任务和处理方式。

分割文本成 Token 有助于进行文本处理和分析,例如词频统计、语言模型训练、机器翻译、文本分类等任务。将文本分解成 Token 的过程可以提供更细粒度的语义信息,并为模型理解和处理文本提供基础。

LoRA

LoRA(Low-Rank Adaptation of LLM,即插件式的微调)用于对大语言模型进行个性化和特定任务的定制。LoRA 通过将模型的权重矩阵分解为低秩的近似矩阵,降低了参数空间的复杂性,从而减少了微调的计算成本和模型存储需求。

传统的微调方法通常需要在整个模型上进行参数优化,这可能会导致训练时间长、计算资源消耗大,并且需要大量的标注数据。而低秩适应方法则提供了一种更高效的微调策略,基于对原始模型的分析,选择性地微调模型的某些部分,使其更适应于特定的任务或数据。

矢量数据库

矢量数据库(Vector Database)是一种用于存储和检索矢量数据的数据库。矢量数据库可以存储和管理大量的矢量数据,例如图像、视频、音频、文本等,同时提供高效的检索功能。

矢量数据库通常基于矢量搜索引擎实现,它可以将矢量数据转换为向量表示,并将其存储在数据库中。在查询时,矢量搜索引擎可以将查询数据转换为向量表示,并在数据库中进行相似度匹配,从而找到与之最相似的数据。

数据蒸馏

数据蒸馏(Knowledge Distillation)旨在将给定的一个原始的大数据集浓缩并生成一个小型数据集,使得在这一小数据集上训练出的模型,和在原数据集上训练得到的模型表现相似

数据蒸馏技术在深度学习领域中被广泛应用,特别是在模型压缩和模型部署方面。它可以帮助将复杂的模型转化为更轻量级的模型,并能够促进模型的迁移学习和模型集成,提高模型的鲁棒性和泛化能力。