2024年8月

一个标准 VMware Cloud Foundation 实例中具有管理工作负载域和 VI 工作负载域两种类型,管理域有且只有一个,而 VI 域可以创建多个,每种工作负载域中可以具有多个 vSphere 集群,而每个集群中可以具有多台主机,有关工作负载域所支持的集群和主机数等限制请查看
VMware Configuration Maximums
。当部署完工作负载域之后,可能需要对管理工作负载域或 VI 工作负载域中的资源进行扩展,比如添加 ESXi 主机或者添加新的 vSphere 集群等,以此来获得主机故障时的最佳弹性和支持更多的工作负载。

其实,无论是管理域还是 VI 域,对于资源的扩展过程都是差不多的。所以,由于环境资源有限,下面以管理工作负载域为例,了解如何为工作负载域进行扩展资源,主要涉及工作负载域中对于主机和集群的添加或删除过程。

一、准备环境

准备加入工作负载域中的 ESXi 主机,同样使用嵌套虚拟机的方式,所有安装配置过程请参照(
VMware Cloud Foundation Part 04:准备 ESXi 主机。
)文章中的方法。注意,ESXi 主机的版本请使用工作负载域中相同的版本,ESXi 主机请至少添加两块硬盘用于 vSAN 存储。在实际环境中,ESXi 主机的配置请尽量与已部署环境中的主机保持一致。

向工作负载域中添加主机或集群之前,请确保用于 ESXi 主机的网络池中具有可用的 IP 地址。可以编辑网络池,向地址池中增加可用 IP 范围用于添加到相同集群的 ESXi 主机;或者创建一个新的网络池,用于向工作负载域中添加新的集群。

向工作负载域中添加主机之前,请确保所在工作负载域 NSX Manager 当中 TEP IP 地址池具有可用的 IP 地址;如果是添加新的集群,可以提前创建 TEP IP 地址池或在添加集群工作流中手动创建地址池。

二、服役主机

不管是任何一种工作负载域(管理域/VI域)需要添加主机或者集群,都需要先将 ESXi 主机添加到“服役主机”清单中。导航到 SDDC Manager->清单->主机,点击服役主机。

勾选全部,请确保所添加的 ESXi 主机符合服役条件,点击继续。

输入 ESXi 主机的 FQDN,根据工作负载域所使用的存储类型,选择主机使用的存储类型 ,这里选择 vSAN 并勾选 vSAN ESA。当前环境是 VCF 5.1.x 版本,vSAN 类型只能是“本地 vSAN”,也就是 vSAN HCI ESA;在 VCF 5.2 版本中,支持将主机用于基于 ESA 架构的 vSAN Max 分解存储。网络池名称一定要选择当前工作负载域所使用的网络池或专门用于某类工作负载域的网络池。填入 ESXi 主机的用户名和密码进行验证。

勾选添加的主机,点击“全部验证”。

验证成功,点击下一步。

检查主机,点击服役。

如果一切顺利,在“未分配的主机”中可以看到刚才添加的 ESXi 主机。

三、工作负载域中添加主机

在向工作负载域中的集群添加主机之前,请确保 ESXi 主机已经添加到服役主机当中,并且是“未分配”状态。

导航到 SDDC Manager->清单->工作负载域,点击需要添加主机的工作负载域。

点击当前工作负载域的“集群”选项卡,点击需要添加主机的集群。

点击当前集群中的“操作”,选择“添加主机”。

当 SDDC Manager 服役主机清单中没有可用的主机,会提示报错并要求你先添加主机;如果清单中具有可用的主机,则添加主机工作流才能正常继续。勾选用于当前集群的主机,点击下一步。

配置 ESXi 主机的虚拟交换机。当前集群具有两个分布式交换机,每个交换机具有两个上行链路,所以要求添加的 ESXi 主机具有四张网卡,这个根据不同环境的情况而定,选择 ESXi 主机上的网卡应用于不同交换机的网络类型。

配置 ESXi 主机的许可证。

检查所有配置,点击完成。

如果一切顺利,可以看到主机已被添加到工作负载域中。

工作负载域所在的 vCenter Server UI 可以看到所添加的主机。

主机已被添加至 vSAN 故障域中。

主机已被配置为 NSX 传输节点。

四、工作负载域中删除主机

工作负载域中删除不需要的主机,整个过程相对来说要比添加主机的过程要简单的多。首先,确保要移除的所在主机上没有运行任何工作负载。因为是 vSAN 集群,在移除主机后,虚拟机可能会变得不合规,根据虚拟机所使用的不同存储策略的情况而定。所以,在调整了集群内的主机后,请修改虚拟机的存储策略使其符合当前集群可用的存储策略。

导航到 SDDC Manager->主机所在工作负载域->主机所在集群->主机,勾选所需移除的主机,然后点击“移除选定的主机”。

确定移除。

移除中......。如果一切顺利,主机会从集群中移除。

导航到 SDDC Manger->清单->主机->未分配的主机,被移除的主机会转到未分配的主机当中。你无法重新将该主机应用于任何工作负载域,被移除的主机需要清理,就是删除遗留在主机上的配置信息。在进行清理之前,需要先将主机从清单中删除(解除服役),当清理完以后,可以再重新对主机进行服役并应用于任何工作负载域。

点击确认,解除服役。

五、工作负载域中添加集群

除了向工作负载域中的集群添加主机以外,还可以向工作负载域中添加新的 vSphere 集群。对于管理域来说,所有 vSphere 集群的初始构建都只能使用 vSAN 作为主体存储,当构建完集群以后,可以添加其他类型的存储(如 NFS、vVols 等)作为该集群的补充存储;对于 VI 域来说,所有 vSphere 集群的初始构建可以使用除了 vSAN 以外的其他类型的存储作为主体存储,当构建完集群以后,可以添加其他类型的存储(如 NFS、vVols 等)作为该集群的补充存储。

工作负载域中添加新的 vSphere 集群要求至少准备三台 ESXi 主机,当然这是使用 vSAN 存储类型的情况下,如果使用其他类型的存储,则只需要两台 ESXi 主机。与工作负载域中添加主机的流程一样,需要准备用于新的 vSphere 集群的 ESXi 主机,以及确保网络池和 NSX TEP IP 池具有可用的地址,然后使用“服役主机”工作流将这些主机添加到 SDDC Manager 主机清单中,如下图所示。

导航到 SDDC Manager->清单->工作负载域,点击需要添加新 vSphere 集群的工作负载域。

点击当前工作负载域中的“操作”,选择“添加集群”。

选择新集群所使用的存储类型,点击开始。

设置新集群的名称,并选择该集群的 LCM 映像。

vSAN ESA 的默认存储策略将根据集群内的主机数自动进行调整。

勾选用于新集群的主机,vSAN 集群要求至少需要 3 台 ESXi 主机。

新集群主机的 VDS 配置。与初始构建工作负载域时的配置类似,默认提供三个预配置文件,以及可选择创建自定义交换机配置。由于预配置文件中的 NSX Overlay 网络主机所使用的 TEP IP 地址默认是通过 DHCP 进行分配的,这要求 TEP IP 地址所在 VLAN 具有可用的 DHCP 服务器,由于当前环境中没有,所以我这里就选择自定义交换机配置,手动创建静态 TEP IP 地址池。

点击自定义交换机配置后,可以选择“从预配置的配置文件复制”选项,选择从某个预定义配置文件中复制配置过来,这样就不用一个一个去配置了。

我把 VDS 02 这个交换机专用于 NSX 网络使用,但是需要对这个虚拟机交换机进行一些设置,点击编辑分布式交换机。

拉到最下面,点击“EDIT”编辑。

在该交换机的覆盖网络配置这里,配置 VLAN 以及 TEP IP 地址池。选择静态 IP 池并创建新的 IP 池。由于我另外一个 VI 域的 NSX Manager 已经关了,这里在配置 IP 池的时候,系统会去检测所有工作负载域中的 NSX Manager 中已经存在的 TEP IP 池,以防止 IP 冲突,自行确认所配置的 IP 池不会存在冲突,然后点击 Acknowledge 即可。如果已提前在工作负载域中的 NSX Manager 中创建了 TEP IP 池,也可以选择“重新使用现有池”,然后选择已存在的 TEP IP 池即可。

拉到最下面,点击保存配置。

拉到最下面,点击保存分布式交换机。

新集群主机的交换机配置好后,点击下一步。

新集群主机的许可证配置。

检查所有配置,点击完成。

如果一切顺利,新集群会显示在列表中。

集群 vcf-mgmt01-cluster02 摘要信息。

集群 vcf-mgmt01-cluster02 主机列表。

集群 vcf-mgmt01-cluster02 网络配置。

工作负载域 vCenter Server 查看新集群。

新集群的 vSAN 存储类型。

新集群的 vSAN 故障域。

新集群的 vSAN 存储。

新集群的 VDS 分布式交换机。

NSX 中新集群传输节点配置。

六、工作负载域中删除集群

如果想删除工作负载域中的 vSphere 集群,同样非常简单。但是有许多注意事项,集群中不应该运行任务工作负载,如果使用了 vSAN 存储,则 vSAN 数据存储将被删除,其他类型的存储会被移除,如果集群中挂载了远程 vSAN 数据存储,必须先移除挂载的数据存储。对于工作负载域中的初始集群,无法单独进行删除,只能通过删除工作负载域来删除集群。

导航到 SDDC Manager->清单->需要删除集群的工作负载域->集群,这里列出了所有集群清单。

在需要删除的集群左边点击“删除集群”。

输入需要删除集群的名称,点击删除集群。如果一切顺利,集群会从工作负载域中删除。

同样,删除集群中的主机会转到未分配的主机清单中,需要将主机解除服役。

点击确认,解除服役。

导读:在这个快节奏的技术世界里,重温过去并从中汲取灵感总是一件有趣的事情。今天要介绍的是一款仅用一天时间重制的经典 Macintosh 应用——Stapler。这款应用最初发布于1992年,现在由一位充满激情的开发者重新打造,不仅保留了原汁原味的功能,还加入了现代化的改进和特性,使其更适合现代 macOS 用户的需求。

作者 | Matt Sephton
翻译 | Jonathan Ssst

几天前,我在 Hacker News 上读到一些 Linux 窗口管理器的一项功能,它们允许从不同的应用程序中收集标签页。

这让我想起了 BeOS,但同时也让我想起了 1992 年的一款经典 Macintosh 应用程序——Stapler,以及我过去是如何谈论过它和它的精神继承者 LaunchList 的。它们都是类似的应用程序,允许你收集并启动与特定任务相关的所有应用程序、文件、文件夹和文档,从而节省时间。正如阿里·兰塔卡里(Ali Rantakari)在 2009 年所说的那样,“在 Mac 上同时打开一堆东西”。

因此,在过去的一天多时间里,我为现代 macOS 构建了自己的这一概念!

虽然我的应用受到了 Stapler(连名字都是)和 LaunchList 的启发,但我的应用增加了自己的改进和功能,更像是两者的混合体。例如,Stapler 没有拖放功能,LaunchList 没有零点击自动启动功能。我的目标是尽可能简单,但不能更简单。

详情、下载和使用方法请参阅 GitHub 软件仓库:
https://github.com/gingerbeardman/stapler

image

图:Stapler,适用于现代 macOS(12.0 或更新版本)

告诉我更多

其原理是为每个项目设置一个 Stapler 文档,其中包含相关的应用程序、文件、文件夹等。然后,你就可以通过启动单个文档一次性打开它们。每个文档都包含一个别名列表,可以使用该应用程序进行管理、检查和启动。省时的关键在于,如果直接启动订书机文档,其列表中的所有项目都会自动启动。酷毙了!

  • 工作:文本编辑器、运行当前游戏、像素艺术编辑器、位图字体应用程序、待办事项列表
  • 播放:音乐应用程序、Hacker News 应用程序、Twitter 应用程序、定位窗口的脚本
  • 电影:运行 Caffeine 以保持计算机开机状态、让显示器进入休眠状态的快捷方式

在电脑上工作,这是一种奇怪的思维方式——它以任务为基础,而不是以应用程序或文档为基础。的确,有人可能会说这是一种过时的工作方式。但我一直在鼓吹,有很多好东西都随着 System 7(虽然我还在用它)、BeOS、OS/2、Amiga Workbench、GEM 和雅达利 ST 等众多替代桌面的记忆被过早地抛在了脑后。因此,我想看看我是否能说到做到。

详细信息

它是用 Swift 和 SwiftUI 编写的,大小为 640KB,其中约三分之一是许多不同大小和分辨率的图标文件。通过创建基于文档的应用程序,您可以免费获得大量功能,例如文档/标签/窗口管理、撤销/重做(不过我还是需要注意并刷新应用程序窗口)等等。

编辑器是一个本地 macOS 窗口,有点像文件管理器中的列表视图、电子表格或小文件夹......这取决于你的观点。此外还有一些菜单命令和键盘对应功能。您可以同时打开多个窗口,例如为所有常用任务各打开一个窗口,这样就能以最小的代价启动全部或部分任务。

每个列表中的项目都是 macOS 书签(有时也称为安全范围书签),是一种经过授权/验证/安全的别名(事实上,它们在代码中仍被称为别名),已经存在了大约 10-15 年。它们包含路径和更多信息。随着 macOS 的封锁程度越来越高,建议访问文件的方式是通过正常的系统权限和安全层检索这些书签。如果没有书签,例如只使用纯文本路径,我就无法在快速查看中显示完整图像,也无法轻松启动列表项。这样做的一个主要好处是,即使文件被移动到同一磁盘的其他地方,甚至是不同的卷中,书签仍然可以解析!

我在保存的文件中将项目存储为 JSON 格式,只是因为比起 XML(这是主要/默认选项),我更喜欢 JSON 格式。我希望文件在一定程度上仍然是可读和可编辑的。

文件使用该文件指定的默认应用程序启动,因此可以根据每个文件进行更改。单个图像可能会在图像编辑器、图像查看器、运行 OCR 的应用程序、运行 OCR 的脚本等中打开。

处理文件既酷又烦,酷的是你可以如此轻松地将书签指向文件,烦的是你必须跳过这么多圈才能绕过安全和沙盒保护,最终不得不以一种啰嗦的方式进行文件请求,然后还必须调整 plist 条目,以便为应用提供正确的权限。我正在使用 SwiftUI 的某些功能,这意味着应用程序无法在 macOS 14 Sonoma 之前的版本上运行。总而言之,我认为现代 macOS 开发有点喜忧参半。要么接受,要么放弃。

图标

由于这是一个周末的快速项目,我在设计图标时有点力不从心。我希望能有一个更好的应用程序图标,以及一个特定的文档图标,所以任何图标设计师都可以加入进来(
https://github.com/gingerbeardman/stapler/issues/1
)。

额外提示

确保取消选中:
System Settings
>
Desktop & Dock
>
Windows
>
Close windows when quitting an application

然后,在退出应用程序时让其窗口保持打开状态。 当您下次启动应用程序时,其窗口将恢复到以前的大小和位置。 如果您先关闭窗口,那么应用程序将恢复到未打开窗口的状态。

感谢:

我的好朋友戴夫-罗伯茨(Serendipity App Company)的集思广益,让我明白了为什么让应用程序在从 Finder 打开文档时做出不同的反应会如此困难。 撕掉它,重新开始! 感谢 Dustin Mierau 为现代 macOS 重塑已被遗忘的旧版应用程序,开启了这一潮流。 永远感谢 Chris Patterson 和 Ali Rantakari 提供的应用程序。

其他

上一章我们聊了标准化的Prompt生成方案DSPy,但DSPy还是更多依赖few-shot的Prompt编写范式,在纯任务描述型指令上的优化效果有限。这一章我们就重点关注描述性指令优化。我们先简单介绍下结构化Prompt编写,再聊聊从结构化多角度进行Prompt最优化迭代的算法方案UniPrompt

1. 结构化Prompt编写

1.1 LangGPT

LangGPT算是最早提出要是用结构化Prompt进行写作的,现在在Coze这种任务流平台上看到的prompt基本上都是这个风格。

结构化Prompt一般使用Markdown和JSON来构建,感觉国内使用markdown更多,早期GPT3.5时使用JSON更多。毕竟现在很多开源模型SFT时也加了大量的Markdown样本,以下是LangGPT提供的Markdown格式的样例如下

# Role: Your_Role_Name

## Profile

- Author: YZFly
- Version: 1.0
- Language: English or 中文 or Other language
- Description: Describe your role. Give an overview of the role's characteristics and skills

### Skill-1
1.skill description 1
2.skill description 2

### Skill-2
1.skill description 1
2.skill description 2

## Rules
1. Don't break character under any circumstance.
2. Don't talk nonsense and make up facts.

## Workflow
1. First, xxx
2. Then, xxx
3. Finally, xxx

## Tools

### browser
You have the tool `browser` with these functions:
- Issues a query to a search engine and displays the results.
- Opens the webpage with the given id, displaying it.
- Returns to the previous page and displays it.
- Scrolls up or down in the open webpage by the given amount.
- Opens the given URL and displays it.
- Stores a text span from an open webpage. Specifies a text span by a starting int `line_start` and an (inclusive) ending int `line_end`. To quote a single line, use `line_start` = `line_end`.

### python

When you send a message containing Python code to python, it will be executed in a 
stateful Jupyter notebook environment. python will respond with the output of the execution or time out after 60.0
seconds. The drive at '/mnt/data' can be used to save and persist user files. Internet access for this session is disabled. Do not make external web requests or API calls as they will fail.

### dalle

Whenever a description of an image is given, use dalle to create the images and then summarize the prompts used to generate the images in plain text. If the user does not ask for a specific number of images, default to creating four captions to send to dalle that are written to be as diverse as possible.

### More Tools

## Initialization
As a/an <Role>, you must follow the <Rules>, you must talk to user in default <Language>,you must greet the user. Then introduce yourself and introduce the <Workflow>.

不难发现结构化prompt有以下几个特点和优点

  1. 使用#,##等标题分隔符来构建层级
    : 例如在二级标题profile下面有三级标题skill,让模型理解这里的skill也属于模型资料。而二级标题tools下面有python,browser,dalle等多个三级标题,表示这些都属于模型可调用工具
  2. 分模块任务描述
    : 每个二级标题都是一个主模块,分模块构建的好处,一个是可以复用,一个是便于上手和prompt迭代,常见的模块包括以下
  • profile & skills:角色描述,角色有哪些能力,使用啥语言etc
  • goal & task: 任务和目标描述,例如负责基于用户指令生成写作大纲
  • constraint & requirements:要求和限制,例如RAG要求模型回答必须来自上文,不能自己生成
  • workflow:针对复杂任务往往要告诉模型先做什么后做什么,例如打分评估任务要先分析问题再进行1-5分的打分
  • example & demos: 提供一些few-shot示例
  • style & output format:对回答格式的要求,例如单选题只能输出ABCD其中一个
  • Init & prefix: 告诉模型prompt结束,要开始回答的引导词,例如单选题可以是“>>>你认为最合理的选项是:”
  1. 模块变量引用
    :在最后的initialization中,使用了<Rules>来引用对应的变量名称,向模型强调这里的Rules指的是前面提到的规则,而非广义的规则。这类变量引用经常大量用于RAG中约束模型使用上文,以及特定格式输出时进一步限制模型推理格式,例如"你的回答必须是<label>中的一个"

结构化Prompt的缺点也同样很明显

  1. 对模型能力要求较高,很多复杂指令理解能力较弱的小模型无法使用。其实也很好理解,指令就像是在模型高维空间里切割出的一片空间,指令越复杂空间切割的粒度就越细,而对于本身高维空间可分性较差的模型,切着切着就没了哈哈哈哈
  2. 越长的prompt上文,越多的constraint,会bias模型输出,导致很多corner case最后归因发现都是某一条requirement的锅。因此个人建议prompt初始都尽量简单,慢慢做加法,不要一上来就写的很复杂。你的每一条要求不一定有用,但都有可能挖坑。

1.2 Pratical Guide

在以上结构化提示器的基础上,新加坡提示词比赛的冠军还给出了更多的结构化prompt编写的tips,这里总结2个亲测好用tips。

  1. 分隔符的使用:分隔符这里广义指和其他层次化分隔符不同的字符。包括更长的#####,》》》》------之类。在prompt中有几个位置需要特殊分割符,核心是让模型理解分隔符前后存在显著差异,语义要分开。例如在RAG段落续写任务中,需要特殊分割符来分割检索上文【Context】,前面模型推理的段落【paragraph】,来完成后面的段落续写。而在一般回答任务中,建议显著区分回答开始的位置,如下
<Annex>
Give a table of the list of row numbers belonging to each cluster, in order to back up your analysis. Use these table headers: [[CLUSTER_NAME], List of Rows].
#############
# START ANALYSIS #
If you understand, ask me for my dataset.
  1. XML标签使用:针对一些分类任务,以及输出是可枚举值的任务,使用XML进行标签约束比markdown的输出效果会更加稳定。
Classify the sentiment of the following conversations into one of two classes.Give the sentiment classifications without any other preamble text.
<classes>
Positive
Negative
</classes>  
<conversations>
[Agent]: Good morning, how can I assist you today?
[Customer]: This product is terrible, nothing like what was advertised!
[Customer]: I’m extremely disappointed and expect a full refund.
[Agent]: Good morning, how can I help you today?
[Customer]: Hi, I just wanted to say that I’m really impressed with your
product. It exceeded my expectations!
</conversations>

2. 结构化Prompt最优化

  • Task Facet Learning: A Structured Approach to Prompt Optimization

有了上面结构化Prompt的铺垫,UniPrompt的优化思路会更容易理解。以上的结构化Prompt编写其实就是把prompt拆分成了多个角度,例如profile,rules,workflow等等进行分别优化。UniPrompt同样采用了结构化prompt的思路,让模型直接生成结构化prompt,并对每个部分进行针对性优化。同时给出了模型在迭代prompt时通用性容易受到个别样本影响的解决方案。相比上一章DSPy里面提到的大模型反思直接优化,以及随机搜索的方案要更加有系统针对性~

Prompt Optimization?

论文前面很有意思,作者先尝试论证定向Prompt最优化这个事它靠不靠谱。

连续性证明

作者先通过指令敏感性,既微小的指令变动对任务效果的影响幅度(利普希茨连续性),来验证最优化的可行性。毕竟如果指令的随便一个微小的变动,就会带来巨大的变化,那随机搜索可能更合适,但如果指令敏感度有上界的话,那最优化方案就可能更合适合。利普希茨连续性的数学定理如下

给定一个概率分布X和一个非负实数r,其中L>=0是利普希茨常数,d是一个距离变量,如果满足如下条件

\[P[ d(f(x),f(x^′)) \leq L \cdot d(x, x^′)| \,d(x,x^′) \leq r] \geq 1 - \epsilon
\]

简单说就是函数变化的斜率被限制在一个有限的范围内。那为了实验prompt的敏感性,论文使用GPT4对初始Prompt进行改写,并计算改写prompt和最初prompt 的cosine距离(Ada-002)作为指令变动幅度的衡量(d(x)),然后使用改写prompt在验证集上进行测试,用指标变化(Acc)作为任务效果变化幅度的衡量(d(f(x))),如下图(a)所示,在95%的概率下,GPT4和GPT3的变化上界<1,而更小的模型Llama2-13B超过2。所以能力越强的模型对指令的微小变动更加鲁棒,在指令最优化上的可行性更高。

image

子模性证明

有利普希茨连续性做为基础,论文还进一步论证了在有限样本和有限的prompt长度的限制下,通过多角度迭代优化prompt的可行性,以及对比few-shot迭代的更优性。这里论文从子模性角度进行了讨论,submodularity的定义如下,简单说就是同一个元素加到不同的集合中产生的边际收益,随着集合的增大和递减。

对于一个集合V和一个非负实值函数f, 如果对于所有$ A, B \subseteq V$, 且
\(A \subseteq B\)
, 以及对于所有
\(x \in V \setminus B\)
都有:

\[f(A \cup \{x\}) - f(A) \geq f(B \cup \{x\}) - f(B)
\]

那在有限样本和有限prompt长度的限制下,寻找最优Prompt的问题,就变成了求解最大化子模态函数的问题,即寻找集合
\(S \in V\)
,使得
\(f(S)\)
最大化,同时满足
\(|S| \lt K\)
。而满足modularity的函数,可以通过贪婪算法得到最优的近似解,每次迭代都把边际收益最大的元素加到集合中,直到边际收益小于阈值,或者集合大小达到上限。

论文分别计算了few-shot和task-facet使用贪婪算法的边际效益,上面的函数f为验证集指标。few-shot的计算采用随机采样了多个A,B的few-shot集合,其中B集合小于A集合,计算在A,B集合上加入同一个shot,计算验证集指标变化,如下图的概率分布,会发现few-shot的概率集中在[-0.01, 0.01]之间,基本是随机分布,并看不到边际递减效应的存在。

image

而task-facet部分,对比上面few-shot是加demo,这里是加section,可以类比前面结构化Prompt的一个子模块。这里论文采用了微调模型(Llama2-13B)来生成一个任务多个角度的prompt,下图的Introduction,Task Description,Real-life Application,Background Knowledge, Challenges分别各是一个section,那A和B分别是采样了不同的section,再计算加入一个新的section的边际收益,会发现对比few-shot的虚线,Facet代表的蓝线有更加明显的边际效应递减的趋势。但这和我们如何生成section是高度相关的,下面我们具体说下如何通过模型来生成任务不同角度的描述(section),并使用大模型进行迭代优化的。

image

UNIPROMPT

UNIPROMPT的整个流程分成以下几个步骤

  • 微调LLama2-13B,让模型直接生成结构化的初始prompt

这里论文使用GPT4构建了样本,给定任务描述(使用了tasksrouce样本集的指令), 和section的描述例如Background,description,requirements,让GPT4来生成该section的内容,然后使用该样本微调Llama2-13B。

### Instruction:
You are a prompt engineer, you have to write a structured prompt.
For the given task description, examples and section description,
write the contents of the section that align with section description.
### Task Description:
{data_point[’task_description’]}
### Section Description:
{data_point[’section’]}:{section_descriptions[data_point[’section’]]}
### Response:
{data_point[’prompt’]}

微调后的Llama2,给定任务描述和section描述,会生成该section的prompt。如下是background角度prompt生成的prompt。作为初始化,会采样10个模型生成的prompt,然后选择验证集上效果最优的prompt。

Task: glue qnli
Task Description: With no explanation, label A to B with either entailment or not entailment
Section: background
Prompt: 
1. Entailment means that the information in statement B can be inferred directly from statement A.
2. Not entailment means that the information in statement B cannot be inferred directly from statement A or is unrelated.
3. Understanding the context and relationship between the two statements is crucial for accurate classification.
  • 样本聚类

有了初始prompt,下一步就是进行迭代优化。这里为了避免前人使用单样本,随机采样样本进行优化,引入的样本bias,这里论文对样本进行了聚类,认为每一个cluster中的任务表征是相似的。这里论文使用大模型prompt对每个问题进行了主题分类打标,然后按标签划分了cluster。不使用cosine相似度的一个原因,个人感觉是语义相似和任务表征相似这里存在diff,所以个人感觉这里的聚类可能需要case by case来看,不同的任务根据输出的不同需要调整。

  • 2阶段反馈生成

基于上面的样本聚类,进一步拆分成mini-batch(3-5),在每个minibatch上基于模型对样本的预测,使用GPT4生成feedback。然后再在batch(5-7个样本)粒度上对各个minibach上的feedback进行共性抽取,并直接生成针对section的增,删,改的具体操作建议。这里两阶段的设计和梯度累计的思路相似,其实还是想要降低个别样本,甚至个别mini-batch在prompt迭代时陷入个性而非共性优化的问题(其实你只要试试用大模型去做过prompt优化就会发现模型非常容易被带偏,因此平滑和共性抽取很重要)。

以下分别是minibach上的返回prompt,和在batch粒度上的总结prompt

You are a teacher and you have to give feedback to your students on their answers.
You are teaching how to solve math problems to your students.
You are given a question, it’s true answer and answer given by student.
You are also given the explanations written by your students while solving the questions.

The questions are answered wrong by the students.
You have to tell why is the solution wrong and what information is can be added to the in the Background Knowledge part that would have helped the student to write better explanations.
## IMPORTANT: You are also given a history of changes you made to the background knowledge part and the change in student’s accuracy after making the change. You have to use this history to make your feedback.
Be explicit and tell the exact information that can be added without further modification / addition.
### IMPORTANT: Give feedback in form of instructions like add a section, add a subsection, set the content of a section, set the content of a subsection, delete a section or delete a subsection in the background knowledge part. Give very granular feedbacks, like if the student has made amistake in the calculation, then tell what is the mistake in the calculation and how to correct it, if the student has made a mistake in the concept, then tell what is the mistake in the concept and how to correct it.
## Background Knowledge
{current_prompt}
## History
{history_string}
Now, it is your turn to give feedbacks to the students.
You can only provide a one line feedback.
You are given a set of feedbacks for some problems. The setfeedbacks for each problem separated by =========== symbol.You have to summarize the feedbacks into a final feedback.You are also given a set of wrong questions.
You need to tell which edit can be applied to aid the student in solving the wrong question.

To achieve your task, try to follow the following steps;
1. Identify the general problem that is being solved by all the feedbacks.
2. Once you have identified the problem, try to make a new feedback that covers most of the feedbacks given.Let’s say the problem in the first feedback is the absence of methods to solve linear equation and in the second feedback itis the method to inverse a matrix.You know that both of these problems can be caused by adding how to solve convert a matrix into row rediced echolon form. So,add that.
3. Try and validate your feedback. Once, you have a feedback try to see if it covers every feedback, if it does not cover any feedback, add that to yournew feedback.
4. See the wrong questions and try to identify what is the problem in the question.If the problem is not covered by your feedback, add that to your feedback.
5. You can add specifics like examples, definitions etc makesure that the feedback is enough to be directly added withoutany modification.
You may use the following function templates
add_section(sectioname)
add_subsection(section_name, subsection_name)
set_section_content(section_name, new_content)
set_subsection_content(section_name, subsection_name, new_content)
delete_section(section_name)
delete_subsection(section_name, subsection_name)
Your summary cannot include more than four functions. Make sure that the content is useful,not just a very general statement. Something specific.

Instructions:
{edits}

Wrong Questions:
{wrong_examples_string}
Summary:
  • 基于反馈进行prompt编辑和优化

基于上面得到的反馈和操作,论文使用以下指令让模型对prompt进行编辑和修改。这里只保留修改后验证集打分有提升的新prompt(greedy),并在每一步都维护多个优化后效果最好的prompt(类比Beam-Size=2),停止迭代的信号是连续5轮在验证集上没有效果提升。

You are given an input prompt and a feedback, you have to incorporate the feedback into the input prompt and output the final prompt.
An example of the task is given below
### Input Prompt
Introduction: In this task you have to answer the given question.
### Feedback
The background knowledge is incomplete, it does not include what are the factors that affect the water usage and how many water sources are there.
\\add_subsection("Background Knowledge")
\\add_subsection_content(water usage depends on the population, climate, economic development, and availability of water sources. There are two sources of water, surface water and groundwater.)
### Final Prompt
Introduction: In this task you have to answer the given question.
Background Knowledge: water usage depends on the population, climate, economic development, and availability of water sources. There are two sources of water, surface water and groundwater.
Only output the final prompt nothing else.
### INPUT PROMPT
{current_prompt}
21
### FEEDBACK
{edits}
### FINAL PROMPT

效果上论文和之前的OPRO,ProTeGi等算法都做了对比,在多个数据集上都会有较显著的效果提升。
image

前言

你是否曾写过一个很简单的需求或者优化?而且你认为不需要审查,就可以直接合并到主分支。可能过了几天或者几周,你突然意识到你犯了一个明显的或是不应该的错误。如果有其他人来审查代码,那这个问题可能就会被发现并及时处理。

CodeReview(代码评审)是一种用来确认方案设计和代码实现的质量保证机制,通过这个机制我们可以对代码、测试过程和注释进行检查,主要用来在软件工程开发过程中改进代码质量。

可能大家会认为必须是领域内的专家或者资深工程师才能审查别人的代码,但是以笔者的经验来看:其实并不是这样的,评审人并不需要完全理解该项目的业务需求或者有多么丰富的编码经验,只需要为本次代码评审提供新的、合理的、符合规范的视角。

下面笔者以自己的实际经验来和大家分享一下,如何做好项目开发中的代码评审。


一、为什么要做

代码评审首要目的就是为我们带来一双新的眼睛,
从新的视角去看待那些潜在的问题。

以笔者目前所在的团队来说,代码审查是开发过程中的关键步骤。通过尽早发现和解决问题,不仅能提高产品的质量,还能确保代码的一致性和可靠性,它还能让开发团队对产品的构建方式与产品所需要的标准达成一致。

因此,尽管代码评审可能在当前会比较费时费力,但随着时间的推移,代码审查发挥的作用会越来越明显。


二、有哪些好处

CodeReview 习惯的保持、积极参加团队的 CodeReview,起码能有以下几点显而易见的好处:

  • 提升代码质量:代码评审可以帮助团队成员发现潜在的缺陷、漏洞和性能问题,从而确保代码的稳定性和可维护性。通过评审,团队成员可以互相学习,借鉴他人的优秀实践,提高自己的编码水平。
  • 促进团队协作:代码评审是一种提高团队协作的方式,有助于团队成员更好地了解彼此的工作内容和进度。在评审过程中,团队成员可以互相交流、分享经验,从而提高整个团队的技术水平和解决问题的能力。
  • 保证项目进度:代码评审可以及时发现和解决问题,避免在项目后期出现严重的技术债务。通过评审可以确保项目按计划推进,提高开发效率。
  • 培养团队文化:代码评审有助于培养团队的学习氛围和进取精神。通过互相评审,团队成员可以共同进步,形成积极向上的团队文化。


三、具体怎么做

3.1评审条件

  • 前置条件:代码已通过 Alibaba Java Coding Guidelines(idea 插件)的代码检查;
  • 大型项目:增加/修改超过 10 个文件或超过 200 行代码的,需组织 CodeReview 会议,邀请相关同事及高级/资深开发同事参与;
  • 小型项目:小需求修改如:少于 10 个文件变更或少于 100 行代码的),至少需要 1~2 位同事帮忙 Review 并提出修改建议;
  • 重点逻辑:建议邀请负责过该项目的同事共同 Review 一下,或者在开发的时候结对 Review。

3.2评审重点

  • 完整性检查:功能点、业务日志、异常日志等
  • 一致性检查:代码逻辑是否符合设计文档,代码风格是否统一等
  • 正确性检查:技术选型、注释是否准确、变量的定义和使用等
  • 可修改性检查:如魔法值 123,使用专门的常量类或枚举等
  • 可预测性检查:死循环、无穷递归、数组越界、空指针等
  • 可理解性检查:命名规则、注释是否清晰、git 提交记录描述等
  • 逻辑性检查:实现不过于复杂、代码拆分、可读性、扩展性等
  • 安全性检查:包括防止注入攻击、保护敏感数据、接口鉴权等

3.3评审形式

  • 会议评审:将相关评审人员集中在一起,通过会议讨论的方式进行代码评审,如无其它紧急情况,建议每月一次。此方法适用于中小型团队或重要的代码更改。

    如:


    • 部门负责人组织评审会议,时间可以固定在当月的某一天,大约 15-20 分钟;
    • 地点可以选择一个小的会议室,大约能容纳 10-15 个人,需要有投影设备;
    • 与会人员可以是部门的整个后端团队,拟定一个主评审人,大家都可以参与讨论
  • 随机抽查:随机选择一部分代码进行评审,以验证代码的质量和规范性。这种方法可以作为团队日常工作的一部分。

  • 工具支持:使用代码评审工具来自动化一些评审过程,例如代码静态分析工具和代码规范检查工具。这些工具能够提供一些有关代码质量的重要指标,并帮助识别潜在的问题。


四、还可以怎么做

4.1提出亮点

  • 不仅要提出需要改进的地方,也要提出本次代码评审的亮点,具体可以从以下几点入手:
    • 性能优化:是否有对性能有优化,合理使用数据库连接、时/空间复杂度、内存操作等;
    • 设计模式:是否有抽象出通用的设计模式,显著提高模块的复用率、扩展性等;
    • 工具/插件:是否有能提高效率的工具类,包括可以发布的插件以及自定义注解等。

4.2轮流评审

  • 需要注意的是,主评审人应该是团队里的每一位成员,而不是仅由部门领导或资深工程师来担任,理由如下:
    • 集思广益:每次的代码评审最重要的是引入新的视角来看待潜在的问题,每个人都会有自己的视角,这样有助于团队统一认识;
    • 机会平等:年轻的工程师们虽然可能经验不足,但干劲儿可能比较足,如果能借此机会得到一些锻炼,那么对团队来说会是一件好事。

4.2文档沉淀

  • 有了以上的种种具体做法,那么最终还是要把结果文档化、持久化的,具体可以:
    • 评审会议前由主评审基于模板去拟好一篇文档在会上展示,包括给出代码片段、改进建议、提出亮点等,方便会上大家及时讨论补充;
    • 会后可以上传到团队的文档空间或者工作集当中,方便团队成员随时学习、回顾。


五、文章小结

关于如何做好项目开发中的 CodeReview(代码评审)就和大家分享到这里,希望能对大家有一些帮助。

写在最后,文章如有不足和错误,还请大家指正。或者你有其它想说的,也欢迎大家在评论区交流!

前言

有的时候我们想要
从服务端拿到数据后
再去渲染一个组件,为了实现这个效果我们目前有几种实现方式:

  • 将数据请求放到父组件去做,并且使用
    v-if
    控制拿到子组件后才去渲染子组件,然后将数据从父组件通过
    props
    传给子组件。

  • 在子组件的
    onMounted
    中请求数据,并且使用
    v-if
    在子组件的
    template
    最外层进行控制,只有拿到数据后才渲染子组件中的内容。

上面这两种方案都有各自的缺点,不够完美。最理想的方案是将从服务端获取数据的逻辑放在子组件中,并且在获取数据的期间让子组件
“暂停”
一下,先不去渲染,等到数据请求完成后再第一次去渲染子组件。

欧阳写了一本开源电子书
vue3编译原理揭秘
,看完这本书可以让你对vue编译的认知有质的提升。这本书初、中级前端能看懂,完全免费,只求一个star。

完美的解决方案

第一种方法的缺点是:子组件虽然拿到数据后才开始渲染,但是数据请求的逻辑却放到了父组件上面,我们期望所有的逻辑都封装在子组件内部。

第二种方法的缺点是:实际上是初始化时就渲染了一次子组件,此时我们还没从服务端拿到数据。所以不得不使用
v-if

template
的最外层控制,此时不渲染子组件中的内容。当从服务端拿到数据后再第二次渲染子组件,此时才将子组件中的内容渲染到页面上。
这种方法明显子组件渲染了2次。

那么有没有一种完美的方案,从服务端获取数据的逻辑放在子组件中,并且在获取数据的期间让子组件
“暂停”
一下,先不去渲染,等到数据请求完成后再第一次去渲染子组件呢?

答案是:当然可以,vue3的
Suspense组件
+
在setup顶层使用await获取数据
就能完美的实现这个需求!!!

两个不完美的例子

为了让你更直观的看到完美方案的牛逼,我们先来看看前面讲的两个不够完美的例子。

父组件中请求数据的例子

下面这个是父组件中请求数据的例子,父组件的代码如下:

<template>
  <ChildDemo v-if="user" :user="user" />
  <div v-else>
    <p>loading...</p>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from "vue";
import ChildDemo from "./Child.vue";

const user = ref(null);

async function fetchUser() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        name: "张三",
        phone: "13800138000",
      });
    }, 2000);
  });
}

onMounted(async () => {
  user.value = await fetchUser();
});
</script>

子组件的代码如下:

<template>
  <div>
    <p>用户名:{{ user.name }}</p>
    <p>手机号:{{ user.phone }}</p>
  </div>
</template>

<script setup lang="ts">
const props = defineProps(["user"]);
</script>

这种方案我们将从服务端获取
user
的逻辑全部放到了父组件中,并且使用
props

user
传递给子组件,并且在从服务端获取数据的期间显示一个loading的文案。

这样虽然实现了我们的需求但是将子组件获取
user
的逻辑放到了父组件中,我们期望将这些逻辑全部封装在子组件中,所以这个方案并不完美。

子组件在onMounted中请求数据的例子

我们来看看第二种方案,父组件代码代码如下:

<template>
  <ChildDemo />
</template>

<script setup lang="ts">
import ChildDemo from "./Child.vue";
</script>

子组件代码如下:

<template>
  <div v-if="user">
    <p>用户名:{{ user.name }}</p>
    <p>手机号:{{ user.phone }}</p>
  </div>
  <div v-else>
    <p>loading...</p>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from "vue";

const user = ref(null);

async function fetchUser() {
  // 使用setTimeout模拟从服务端获取数据
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        name: "张三",
        phone: "13800138000",
      });
    }, 2000);
  });
}

onMounted(async () => {
  user.value = await fetchUser();
});
</script>

我们将数据请求放在了
onMounted
中,初始化时会去第一次渲染子组件。此时
user
的值还是
null
,所以我们不得不在
template
的最外层使用
v-if="user"
控制此时不显示子组件的内容,在
v-else
中去渲染loading文案。

当从服务端拿到数据后给响应式变量
user
重新赋值,会触发页面重新渲染,此时会进行第二次渲染才将子组件的内容渲染到页面上。

从上面可以看到这种方案子组件明显渲染了两次,并且我们还将loading的显示逻辑写在子组件的内部,增加了子组件代码的复杂度。所以这种方案也并不完美。

最完美的方案就是在
fetchUser
期间让子组件
“暂停”渲染

fallback
去渲染一个loading页面。并且这个loading的显示逻辑不需要封装在子组件中,在
“暂停”渲染
期间
自动
就能显示出来。等到从服务端请求数据完成后才开始渲染子组件,并且自动的卸载掉loading页面。

Suspense + await实现完美的例子

下面这个是官网对
Suspense
的介绍:

<Suspense>
是一个内置组件,用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。

上面的意思是
Suspense
组件能够监听下面的异步子组件,在等待异步子组件完成渲染之前,可以去渲染一个loading的页面。

Suspense
组件支持两个插槽:
#default

#fallback
。如果
#default
插槽中有异步组件,那么就会先去渲染
#fallback
中的内容,等到异步组件加载完成后就会将
#fallback
中的内容给干掉,改为将异步组件的内容渲染到页面上。

如果我们的子组件是一个异步组件,那么
Suspense
不就可以帮我们实现想要的功能吖。

Suspense
可以在异步子组件的加载过程中使用
#fallback
插槽自动帮我们渲染一个加载中的loading,等到异步子组件加载完成后才会第一次去渲染子组件中的内容。

那么现在的问题是如何将我们的子组件变成异步子组件?

这个问题的答案其实vue官网就已经告诉我们了,如果一个组件的
<script setup>
顶层使用了
await
,那么这个组件就会变成一个异步组件。我们接下来只需要在子组件的顶层使用await去请求服务端数据就可以啦。

完美方案的父组件

下面这个是使用
Suspense
改造后的父组件代码,如下:

<template>
  <Suspense>
    <AsyncChildDemo />
    <template #fallback>loading...</template>
  </Suspense>
</template>

<script setup lang="ts">
import AsyncChildDemo from "./AsyncChild.vue";
</script>

在父组件中使用了
Suspense
组件,给这个组件传了2个插槽。
#default
插槽为异步子组件
AsyncChildDemo
,默认插槽可以不用给元素上面添加
#default

并且使用了
#fallback
插槽,在异步子组件加载过程中会暂时先不去渲染异步子组件
AsyncChildDemo
。改为先渲染
#fallback
插槽中的loading,等到异步子组件加载完成后会自动将loading替换为子组件中的内容。

完美方案的子组件

下面这个是使用了
await
改造后的子组件代码,如下:

<template>
  <div>
    <p>用户名:{{ user.name }}</p>
    <p>手机号:{{ user.phone }}</p>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

const user = ref(null);
user.value = await fetchUser();

async function fetchUser() {
  // 使用setTimeout模拟从服务端获取数据
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        name: "张三",
        phone: "13800138000",
      });
    }, 2000);
  });
}
</script>

我们在
<script setup>
顶层中使用了
await
,然后将
await
拿到的值赋值给
user
变量。在顶层使用了
await
后子组件就变成了一个异步组件,等到
await fetchUser()
执行完了后,也就是从服务端拿到了数据后,子组件才算是加载完成了。

并且由于我们在父组件中使用了
Suspense
,所以在子组件加载完成之前,也就是从服务端拿到数据之前,都不会去渲染子组件(相当于“暂停”渲染子组件)。而是去渲染
#fallback
插槽中的loading,等到从服务端拿到数据之后异步子组件才算是加载完成了。此时才会第一次去渲染子组件,并且将loading替换为子组件渲染的内容。

因为第一次渲染子组件时已经从服务端拿到了
user
的值,此时
user
已经不是
null
了,所以我们可以不用在template的最上层使用
v-if="user"
,尽管在template中有去读
user.name

经过
父组件Suspense + 子组件顶层await
的改造后,在渲染父组件的
Suspense
时发现他的子组件有异步组件,就会“暂停”渲染子组件,改为自动渲染loading组件。

子组件在
setup
顶层使用
await
等待从服务端请求数据,当从服务端拿到了数据后此时子组件才算是加载完成,此时才会进行第一次渲染,并且自动将loading中的内容替换为子组件中渲染的内容。

并且在
Suspense
中还支持多个异步子组件分别从服务端获取数据,等这几个异步子组件都从服务端获取到数据后才会自动的将loading替换为这几个异步子组件渲染的内容。

还有就是
Suspense
组件目前依然还是
实验性
的功能,生产环境使用需要谨慎。

简单看看
Suspense
如何实现“暂停”渲染?

Suspense
在渲染子组件时,发现子组件是一个异步组件就不会立即执行异步子组件的render函数。而是会加一个名为
deps
的标记,标明当前默认子组件是一个异步组件,
暂停渲染
异步子组件。

由于异步子组件是一个
Promise
,所以可以在加载异步子组件的
Promise
后添加
.then()
方法,在
.then()
方法中才会去继续渲染异步子组件。

目前异步子组件已经暂停渲染了,接着就是会去读取
deps
标记。如果
deps
标记为
true
,说明异步子组件暂停渲染了,此时就会去将
fallback
插槽中的loading组件渲染到页面上。

当异步子组件加载完成后就会触发
Promise

.then()
方法,从而
继续渲染
异步子组件。在
.then()
方法中会去执行异步子组件的render函数去生成虚拟DOM,然后根据虚拟DOM生成真实DOM。最后就是将原本页面上渲染的
fallback
插槽中的内容替换为异步组件生成的真实DOM中的内容。

下面这个是我画的流程图(
流程图后面还有文末总结
):
full-progress

总结

这篇文章我们讲了有的场景需要
从服务端拿到数据后
再去渲染一个组件,此时我们就可以使用
父组件Suspense + 子组件顶层await
的完美方案。

在渲染父组件的
Suspense
组件时发现他的子组件有异步组件,就会“暂停”渲染子组件,改为自动渲染loading组件。

子组件在
setup
顶层使用
await
等待从服务端请求数据,当从服务端拿到了数据后此时子组件才算是加载完成,此时才会进行第一次渲染,并且自动将loading中的内容替换为子组件中渲染的内容。

并且在
Suspense
中还支持多个异步子组件分别从服务端获取数据,等这几个异步子组件都从服务端获取到数据后才会自动的将loading替换为这几个异步子组件渲染的内容。

最后就是
Suspense
组件目前依然还是
实验性
的功能,生产环境使用需要谨慎。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

另外欧阳写了一本开源电子书
vue3编译原理揭秘
,看完这本书可以让你对vue编译的认知有质的提升。这本书初、中级前端能看懂,完全免费,只求一个star。