动手点关注
干货不迷路
本文旨在让无大模型开发背景的工程师或者技术爱好者无痛理解大语言模型应用开发的理论和主流工具,因此会先从与LLM应用开发相关的基础概念谈起,并不刻意追求极致的严谨和完备,而是从直觉和本质入手,结合笔者调研整理及消化理解,帮助大家能够更容易的理解LLM技术全貌,大家可以基于本文衍生展开,结合自己感兴趣的领域深入研究。若有不准确或者错误的地方也希望大家能够留言指正。
本文体系完整,内容丰富,由于内容比较多,分多次连载 。
第一部分 基础概念
1.机器学习场景类别
2.机器学习类型(LLM相关)
3.深度学习的兴起
4.基础模型
第二部分 应用挑战
1.问题定义与基本思路
2.基本流程与相关技术
1)Tokenization与Embbeding
2)向量数据库
3)finetune(微调)
4)模型部署与推理
5)prompt
6)编排与集成
7)其它(预训练等)
第三部分 场景案例
常用参考
2.基本流程与相关技术
点此查看前面内容:
4)编排与集成
经过前面的介绍,我们对大模型应用开发的通常涉及到的重要构成组件都有了介绍,然而这些零散的组件需要有一根”线“将它们串联起来,最终变成用户可以感受到的LLM应用。这里编排集成服务便起到了这样的“线“的作用,它作为粘合剂和应用骨架,在整个应用构成中起到了提纲挈领的作用。
在本章你将学习到:
1)相关理论和应用开发范式
2)Langchain框架介绍
3)LlamaIndex框架介绍
4)Semantic Kernel框架介绍
5) 技术展望
Semantic Kernel概述
设计理念
作为编排领域的“三国杀”之一的Semantic Kernel,是含着金钥匙出生的贵族,唯一一个巨头公司发布的LLM应用编排框架。2023年3月17日,微软宣布了轻量级SDK Semantic Kernel,其定位编排领域(Prompt Orchestration Engine),在整个微软大模型战略里承担着关键C位。
在微软的Office全系产品Copilot都采用了Semantic Kernel作为底层的实现框架,其作为桥梁连接应用到基座模型。
Semantic Kernel从它的名字可以看出,它强调了语法向语义的转变,它将大型语言模型(LLM)与C#、Python和Java等传统编程语言融合在一起,将自然语言(Prompt)也变成了程序语言的一部分,混合编程,最终完成软件目标。
Semantic Kernel的定位与langchain等集成框架一致,通过编排Pipeline的方式构建AI应用。但Semantic Kernel的设计一开始便面向构建AI Agents展开,旨在通过插件轻松地将现有传统代码添加到AI Agent中。有了插件,就可以通过调用现有的应用程序和服务,让Agent具备与现实世界交互的能力。这样,插件就像AI应用的 "手和臂"。此外,Semantic Kernel的接口允许它灵活地集成任何人工智能服务。这是通过一组连接器来实现的,这些连接器可以轻松添加记忆和人工智能模型。通过这种方式,Semantic Kernel能够为应用添加一个模拟 "大脑",当更新更好的LLM出现时,可以轻松地替换。由于Semantic Kernel利用连接器和插件提供的可扩展性,几乎可以用它来协调现有的任何代码,而不必被锁定在特定的人工智能模型提供商。例如,如果开发者为OpenAI的ChatGPT构建了大量插件,那么通过使用Semantic Kernel一样可以将它们与Azure或Hugging Face等其他提供商的模型进行切换。
微软官方在其发布会上列出他们认为的四大优势:
- 快速集成:SK旨在嵌入任何类型的应用程序中,使您可以轻松测试和运行LLM AI。
- 扩展:借助 SK,您可以连接外部数据源和服务,使其应用程序能够将自然语言处理与实时信息结合使用。
- 更好的提示:SK的模板化提示可让您使用有用的抽象和机制快速设计语义函数,以释放LLM AI的潜力。
- 新奇但熟悉:传统编程语言代码始终可供您作为一流的合作伙伴,帮助您快速完成工程设计,可以两全其美。
在笔者看来,SK最大的亮点是一开始就采用了规划器( Planner )的设 计 ,封装了各种常见的LLM应用模式(也可以定制 ) , 可以要求LLM生成一个能够实现用户独特目标的规划并基于 该计 划执行, 而这样的编排理念 langchain和llamaindex到项目中后期才明确提出,这也体现了微软与其他创业公司的不同,前者是先规划后实施,而后者是先实现再沉淀拔高,本章第一节就引用了微软在编排框架设计上的一些最佳实践原则( S chillace Laws ) 。另一方面,微软在SK生态定位上也突出差异化, 这样的结果也导致了当下 LLM应用编排 开发框架的竞争格局,langchain具备先发优势、吸引了大量的尝鲜 用 户,目标通用框架,平台生态 , llamaindex小而美, 切入 垂直领域,将RAG做深做优 。 而微软
Sem antic Kernel目标传统应用开发者,立足.net生态,辐射java生态,不断补齐自己的能力短板,并和同宗项目Prompt flow,AutoGen一道构建LLM应用开发的生态壁垒。
如图可以直观地看到SK的工作流程,首先用户提出问题,kernel构建运行环境(包括共享的上下文),通过Planner生成任务计划,将知识和工具组织起来形成流水线(Pipeline)流程,再驱动执行从而获得输出结果或者完成任务。
功能与构成
接下来,简单介绍SK的组件,了解每个模块具体承担的作用。
1)内核(Kernel)顾名思义,它是SK的方案生成和运行的载体,注册所有的连接器和插件,并进行必要的配置以运行应用程序。此外,还可以提供必要的日志、可观测性支持,从而方便开发者调试和监控应用的状态和性能。
2)记忆体(Memory)用于存储应用的上下文,比如大模型的对话历史,常见的实现是利用向量存储,但实际上也可以使用简单的KV存储甚至是本地文件。以下是Semantic Kernel支持的存储服务:
3)连接器,顾名思义,它连接各种组件,实现组件之间的信息交换。它在Semantic Kernel中扮演着非常重要的角色,因为它们是连接不同组件的桥梁,可以实现组件之间的信息交换。既可以对接模型也可以对接一些组件如向量数据库等。微软提供了很多开箱即用的连接器,其中不乏很多微软特有的组件,同时也支持开发者自定义连接器。
4)插件(Plugins),也叫技能(Skill),它是一组可以暴露在人工智能应用程序和服务中的功能。从插件的设计上讲,SK采用了OpenAI的插件标准,这意味着您构建的任何插件都可以导出,以便在 ChatGPT、必应和 Microsoft 365 中使用。这样,可以在不重写代码的情况下扩大人工智能功能的覆盖范围。这还意味着,为 ChatGPT、必应和 Microsoft 365 构建的插件可以无缝导入 Semantic Kernel。
插件中的功能可由AI进行协调,以完成用户请求。在Semantic Kernel中,可以通过函数调用或规划器手动或自动调用这些函数。然而,仅仅提供函数还不足以构成一个插件。为了利用规划器实现自动协调,插件还需要提供从语义上描述其行为方式的细节。从函数的输入、输出到副作用,都需要以人工智能能够理解的方式进行描述,否则,规划器将会提供意想不到的结果。例如,在 WriterPlugin 插件中,每个函数都有一个语义描述,说明该函数的作用。然后,规划器可以使用这些描述来选择要调用的最佳函数,以满足用户的要求。
插件的构成角度就是一组函数(function)实现的功能,如图:
在SK中构成的插件函数分为两类:
语 义函数( Semantic Functions ),也叫prompts : 这些功能会聆听用户请求,并使用自然语言 提供响应。 为此,语义函数需要连接器来实现其目的。 定义一个语义函数 需要 两个文件 : 一个用于配置函数,另一个用于定义提示。
例如以下是一个总结插件的函数,包含配置文件和Prompt定义 。 该函数的配置文件,即 JSON 文件,将如下所示:
{
"schema": 1,
"description": "Summarize given text or any text document",
"execution_settings": {
"default": {
"max_tokens": 512,
"temperature": 0.0,
"top_p": 0.0,
"presence_penalty": 0.0,
"frequency_penalty": 0.0
}
},
"input_variables": [
{
"name": "input",
"description": "Text to summarize",
"default": "",
"is_required": true
}
]
}
可以看到,配置中有三个主要部分。第一部分定义函数名称和描述,内核稍后将使用它们来决定何时使用此函数。接下来是输出定义,规定 LLM 的操作,例如最大Token数或温度。最后是输入定义,指定函数运行所需的参数。
现在是定义函数提示的部分,我们在名为 skprompt.txt 的文本文件中进行定义。
[SUMMARIZATION RULES]
DONT WASTE WORDS
USE SHORT, CLEAR, COMPLETE SENTENCES.
DO NOT USE BULLET POINTS OR DASHES.
USE ACTIVE VOICE.
MAXIMIZE DETAIL, MEANING
FOCUS ON THE CONTENT
[BANNED PHRASES]
This article
This document
This page
This material
[END LIST]
Summarize:
Hello how are you?
+++++
Hello
Summarize this
{{$input}}
+++++
其中prompt的模版变量符会用前面的文件进行替换,从而变成完整的prompt被提交给LLM。为防止 Prompt Injection 技术,最好对希望模型工作的内容进行分隔,以避免出现意想不到的结果。
原生函数(Native Functions):这些函数用 C#、Python 和 Java 编写。它们处理LLM不擅长的操作,例如数学操作、调用API等。原生函数是一种执行更基本任务的函数,如本例MathPlugin中的原生函数,包含计算平方根等操作。
//sk_native_function.py
import math
from semantic_kernel.skill_definition import (
sk_function,
sk_function_context_parameter,
)
from semantic_kernel.orchestration.sk_context import SKContext
class MathPlugin:
@sk_function(
description="Takes the square root of a number",
name="Sqrt",
input_description="The value to take the square root of",
)
def square_root(self, number: str) -> str:
return str(math.sqrt(float(number)))
@sk_function(
description="Adds two numbers together",
name="Add",
)
@sk_function_context_parameter(
name="input",
description="The first number to add",
)
@sk_function_context_parameter(
name="number2",
description="The second number to add",
)
def add(self, context: SKContext) -> str:
return str(float(context["input"]) + float(context["number2"]))
@sk_function(
description="Subtract two numbers",
name="Subtract",
)
@sk_function_context_parameter(
name="input",
description="The first number to subtract from",
)
@sk_function_context_parameter(
name="number2",
description="The second number to subtract away",
)
def subtract(self, context: SKContext) -> str:
return str(float(context["input"]) - float(context["number2"]))
@sk_function(
description="Multiply two numbers. When increasing by a percentage, don't forget to add 1 to the percentage.",
name="Multiply",
)
@sk_function_context_parameter(
name="input",
description="The first number to multiply",
)
@sk_function_context_parameter(
name="number2",
description="The second number to multiply",
)
def multiply(self, context: SKContext) -> str:
return str(float(context["input"]) * float(context["number2"]))
@sk_function(
description="Divide two numbers",
name="Divide",
)
@sk_function_context_parameter(
name="input",
description="The first number to divide from",
)
@sk_function_context_parameter(
name="number2",
description="The second number to divide by",
)
def divide(self, context: SKContext) -> str:
return str(float(context["input"]) / float(context["number2"]))
为了向内核暴露这个原生函数,供模型调起,在函数中使用了三个装饰器来描述该函数:
- SKFunction:用于提供功能描述。
- SKParameter:说明函数每个参数的用途(如果参数不止一个)。
- Description:与 SKParameter 类似,但只有一个参数。
有了语义函数和原生函数后,下面是手工调用的例子。
import semantic_kernel as sk
from plugins import MathPlugin
async def main():
# Initialize the kernel
kernel = sk.Kernel()
# Add a text or chat completion service using either:
# kernel.add_text_completion_service()
# kernel.add_chat_service()
# Import the MathPlugin.
math_plugin = kernel.import_skill(MathPlugin(), skill_name="MathPlugin")
# Run the Sqrt function with the context.
result = await kernel.run_async(
math_plugin["Sqrt"],
input_str="12",
)
print(result)
# Run the main function
if __name__ == "__main__":
import asyncio
asyncio.run(main())
SK内置提供了很多常用的插件,包括:
引入Microsoft.SemanticKernel.CoreSkills即可使用:
using Microsoft.SemanticKernel.CoreSkills;
// Instantiate a kernel and configure it
kernel.AddFromType<TimePlugin>();
const string promptTemplate = @"
Today is: {{time.Date}}
Current time is: {{time.Time}}
Answer to the following questions using JSON syntax, including the data used.
Is it morning, afternoon, evening, or night (morning/afternoon/evening/night)?
Is it weekend time (weekend/not weekend)?";
var results = await myKindOfDay.InvokePromptAsync(promptTemplate);
Console.WriteLine(results);
- 顺序计划器(SequentialPlanner):创建一个包含多个函数的计划,这些函数相互连接,每个函数都有自己的输入和输出变量。
- BasicPlanner:SequentialPlanner 的简化版,可将多个函数串联在一起。
- ActionPlanner:使用单个函数创建计划。
- StepwisePlanner:逐步执行每一步,在执行下一步之前观察结果。
以前面例子为例,我们了解了如何手工地调用某一个函数,但不够聪明,而规划器可以理解用户的需求,并在我们的插件中找到必要的函数来执行任务。传递给规划器的是一个Prompt,即用户的需求,规划器负责理解用户的需求,并在我们的插件中找到合适的函数来执行任务。这样的模式体现了Agent的理念,通过一些既定的流程或者LLM自动规划的流程,依靠它完成包含复杂的工作流程的任务,例如,如果您有任务和日历事件插件,计划员就可以将它们组合起来,创建 "提醒我去商店买牛奶 "或 "提醒我明天给妈妈打电话 "这样的工作流,而无需明确为这些场景编写代码。
下面是一个使用规划器的例子:
// Create planner
var planner = new SequentialPlanner(kernel);
// Create a plan for the ask
var ask = "Get statistics for the file located at /Users/adolfo/Documents/Proyectos/Labs/SemanticKernel/Program.cs and convert the code from C# to Swift";
var plan = await planner.CreatePlanAsync(ask);
// Execute the plan
var plannerResult = await kernel.RunAsync(plan);
Console.WriteLine($"Plan results:\n{plannerResult.Result}");
虽然,规划器的功能十分强大,能够自动组合已经定义的函数,但是,官方提示,受限于当前的大模型技术发展情况和特点,在使用之前需要考虑以下因素带来的影响并采取一些措施缓解。
考虑因素 | ||
描述 | ||
改善方法 | ||
性能 | ||
如果在用户提供输入后才依赖规划器,那么在等待计划的过程中,用户界面可能会无意中挂起。 | 在构建用户界面时,必须向用户提供反馈,让他们知道加载体验正在发生变化。您还可以使用 LLM,在计划程序完成计划时为用户生成初始响应,从而拖延时间。最后,可以针对常见场景使用预定义计划,以避免等待新计划。 | |
成本 | ||
提示和生成的计划都会消耗很多token。要生成非常复杂的计划,可能需要消耗模型提供的所有token限额。如果不小心,这可能会导致服务成本高昂,尤其是规划通常需要 GPT 3.5 或 GPT 4 等更高级的模型。 | 函数的原子性越强,需要的token就越多。通过编写高阶函数,可以为规划程序提供更少的函数,使用更少的token。最后,可以针对常见场景使用预定义计划,以避免在新计划上花钱。 | |
正确性 | 规划器可能会生成错误的计划。例如,它可能会错误地传递变量、返回畸形模式或执行不合理的步骤。 | 为使规划器更加稳健,应提供错误处理功能。某些错误,如模式畸形或模式返回不当,可以通过要求计划器 "修复 "计划来恢复。 |
其它特色
当前的很多框架都采用了python作为实现语言,这对于机器学习工程师比较友好,但是受众较窄,对于广大的后端程序员来讲,java,.net才是主体受众,微软Semantic Kernel在这方面关注较多,挖掘了潜在的客户群。并且其在开发效率和质量上也有一定的优势,在过去的文章里也有介绍,如: Langchain危矣?微软Semantic Kernel(SK)来抢人了,剑指大模型应用开发 。
更进一步,广大应用程序员更关注的还是如何在正式环境落地LLM,因此,微软还开发了PromptFlow,用来处理常规的数据任务并将其和大模型连接起来,致力于解决LLM应用从开发到生产的全过程。在笔者过去文章里有对该项目做过介绍: 两个有趣的项目Open Interpreter及Prompt Flow,让你的LLM应用开发如虎添翼 ,更多内容可以参考:https://learn.microsoft.com/en-us/semantic-kernel/agents/planners/evaluate-and-deploy-planners/
总结
虽然,微软相较于其它两个项目处于后发状态,但SK的发展异常迅速,其版本已经迭代到V1.1.0,进入了稳定迭代期,能力也已覆盖了当前AI应用的常见形态。
随着大模型应用开发热潮的持续,作为有广阔应用场景、以及排他的特有开发群体和强大开发团队支持,以SK为代表的微软系工具链将会持续在大模型领域发力,我们拭目以待,期待大模型应用开发生态圈更加繁荣。
参考:https://learn.microsoft.com/en-us/semantic-kernel/
点此查看合集: