AI全栈大模型工程师(二十)SKvs.LangChain

2023-11-10 10:07:56 浏览数 (2)

SK vs. LangChain

#%% md

概念对照

LangChain

Semantic Kernel

Model

Connector

Tools

Connector

Vectorstore

Connector

Memory

Memory

Prompt Templates

Plugins

Chain

Pipeline / Chain

Agent

Planner

TextSplitter

text.*

OutputParser

功能对照

LangChain

Semantic Kernel

大模型

60

5

向量数据库

40

11

Agent

10

4

#%% md

环境搭建

#%% md

  1. 安装 Python 3.x:https://www.python.org/downloads/
  2. 安装 SK 包:pip install semantic-kernel
  3. 在项目目录创建 .env 文件,添加以下内容:
代码语言:javascript复制
# .env
OPENAI_API_KEY=""
OPENAI_API_BASE=""
AZURE_OPENAI_DEPLOYMENT_NAME=""
AZURE_OPENAI_ENDPOINT=""
AZURE_OPENAI_API_KEY=""

OpenAI 和 Azure,配置好一个就行。

#%% md

代码语言:javascript复制
## Hello, World!

#%% md
这是一个简单示例。

第一段代码是初始化。后面所有代码都要在执行过这段代码后,才能执行。

#%%
import semantic_kernel as sk
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
import os

# 加载 .env 到环境变量

from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv())

# 创建 semantic kernel

kernel = sk.Kernel()

# 配置 OpenAI 服务

api_key = os.getenv('OPENAI_API_KEY')
endpoint = os.getenv('OPENAI_API_BASE')
model = OpenAIChatCompletion(
    "gpt-3.5-turbo", api_key, endpoint=endpoint)

# 把 LLM 服务加入 kernel

# 可以加多个。第一个加入的会被默认使用,非默认的要被指定使用

kernel.add_text_completion_service("my-gpt3", model)
#%% md
执行讲笑话的 prompt。

#%%

# 定义 semantic function

tell_joke_about = kernel.create_semantic_function("给我讲个关于{{$input}}的笑话吧")

# 看结果

print(tell_joke_about("Hello world"))

#%% md

划重点: 用我们熟悉的操作系统来类比,可以更好地理解 SK。

  1. 启动操作系统:kernel = sk.Kernel()
  2. 安装驱动程序:kernel.add_xxx_service()
  3. 安装应用程序:func = kernel.create_semantic_function()
  4. 运行应用程序:func()

基于 SK 开发的主要工作是写「应用程序」,也就是 Plugins

#%% md

Plugins

#%% md 简单说,plugin 就是一组函数的集合。它可以包含两种函数:

  • Semantic Functions - 语义函数,本质是 Prompt Engineering
  • Native Functions - 原生函数,类似 OpenAI 的 Function Calling

值得一提的是,SK 的 plugin 会和 ChatGPT、Bing、Microsoft 365 通用。「很快」你用 SK 写的 plugin 就可以在这些平台上无缝使用了。这些平台上的 plugin 也可以通过 SK 被你调用。

注意:Plugins 最初命名为 Skills,后来改为 Plugins。但是无论文档还是代码,都还有大量的「Skill」遗留。见到后,就知道两者是一回事就好

#%% md

Semantic Functions

#%% md Semantic Functions 是纯用数据(prompt 描述)定义的,不需要编写任何代码。所以它与编程语言无关,可以被任何编程语言调用。

一个典型的 semantic function 包含两个文件:

  • skprompt.txt: 存放 prompt,可以包含参数,还可以调用其它函数
  • config.json: 存放描述,包括函数功能,参数的数据类型,以及调用大模型时的参数

举例:把 LangChain 「生成 Linux 命令」的例子用 SK 实现。

#%% md

skprompt.txt

#%% raw 将用户的指令转换成 Linux 命令

The output should be formatted as a JSON instance that conforms to the JSON schema below.

As an example, for the schema {“properties”: {“foo”: {“title”: “Foo”, “description”: “a list of strings”, “type”: “array”, “items”: {“type”: “string”}}}, “required”: [“foo”]}} the object {“foo”: [“bar”, “baz”]} is a well-formatted instance of the schema. The object {“properties”: {“foo”: [“bar”, “baz”]}} is not well-formatted.

Here is the output schema:

代码语言:javascript复制
{"properties": {"command": {"title": "Command", "description": "linux shell命令名", "type": "string"}, "arguments": {"title": "Arguments", "description": "命令的参数 (name:value)", "type": "object", "additionalProperties": {"type": "string"}}}, "required": ["command", "arguments"]}

{{$input}} #%% md

config.json

#%% { “schema”: 1, “type”: “completion”, “description”: “将用户的指令转换成 Linux 命令”, “completion”: { “max_tokens”: 256, “temperature”: 0, “top_p”: 0, “presence_penalty”: 0, “frequency_penalty”: 0 }, “input”: { “parameters”: [ { “name”: “input”, “description”: “用户的指令”, “defaultValue”: “” } ] } } #%% md 上面两个文件都在 sk_samples/SamplePlugin/GenerateCommand 目录下。

#%% md

代码语言:javascript复制
#### 调用 Semantic Functions

#%%

# 加载 semantic function。注意目录结构

functions = kernel.import_semantic_skill_from_directory(
    "./sk_samples/", "SamplePlugin")
cli = functions["GenerateCommand"]

# 看结果

print(cli("将系统日期设为2023-04-01"))

#%% md 官方提供了大量的 Semantic Functions 可以参考:https://github.com/microsoft/semantic-kernel/tree/main/samples/skills

#%% md

Semantic Kernel Tools

#%% md 这是个 VS Code 的插件,在 VS Code 里可以直接创建和调试 Semantic Function。

安装地址:https://marketplace.visualstudio.com/items?itemName=ms-semantic-kernel.semantic-kernel

#%% md

Native Functions

#%% md 用编程语言写的函数,如果用 SK 的 Native Function 方式定义,就能纳入到 SK 的编排体系,可以被 Planner、其它 plugin 调用。

下面,写一个过滤有害 Linux 命令的函数,和 GenerateCommand 组合使用。

这个函数名是 harmful_command。如果输入的命令是有害的,就返回 true,否则返回 false

它也要放到目录结构中,在 sk_samples/SamplePlugin/SamplePlugin.py 里加入。

代码语言:javascript复制
#%%

# 因为是代码,不是数据,所以必须 import

from sk_samples.SamplePlugin.SamplePlugin import SamplePlugin

# 加载 semantic function

functions = kernel.import_semantic_skill_from_directory(
    "./sk_samples/", "SamplePlugin")
cli = functions["GenerateCommand"]

# 加载 native function

functions = kernel.import_skill(SamplePlugin(), "SamplePlugin")
harmful_command = functions["harmful_command"]

# 看结果

command = cli("删除根目录下所有文件")
print(command)  # 这个 command 其实是 SKContext 类型
print(harmful_command(context=command))  # 所以要传参给 context
#%% md

### 用 SKContext 实现多参数 Functions

#%% md
如果 Function 都只有一个参数,那么只要把参数定义为 `{{$input}}`,就可以按前面的例子来使用,比较直观。`{{$input}}`会默认被赋值。

多参数时,就不能用默认机制了,需要定义 `SKContext` 类型的变量。

#%%

# 因为是代码,不是数据,所以必须 import

from sk_samples.SamplePlugin.SamplePlugin import SamplePlugin

# 加载 native function

functions = kernel.import_skill(SamplePlugin(), "SamplePlugin")
add = functions["add"]

# 看结果

context = kernel.create_new_context()
context["number1"] = 1024
context["number2"] = 65536
total = add(context=context)
print(total)
#%% md
内置 Plugins

#%% md SK 内置了若干好用的 plugin,但因为历史原因,它们叫 skill……

加载方法:

代码语言:javascript复制
from semantic_kernel.core_skills import SkillName

它们是:

  • ConversationSummarySkill - 生成对话的摘要
  • FileIOSkill - 读写文件
  • HttpSkill - 发出 HTTP 请求,支持 GET、POST、PUT 和 DELETE
  • MathSkill - 加法和减法计算
  • TextMemorySkill - 保存文本到 memory 中,可以对其做向量检索
  • TextSkill - 把文本全部转为大写或小写,去掉头尾的空格(trim)
  • TimeSkill - 获取当前时间及用多种格式获取时间参数
  • WaitSkill - 等待指定的时间
  • WebSearchEngineSkill - 在互联网上搜索给定的文本

#%% md

Memory

#%% md SK 的 memory 使用非常简单:

  1. kernel.add_text_embedding_generation_service() 添加一个文本向量生成服务
  2. kernel.register_memory_store() 注册一个 memory store,可以是内存、文件、向量数据库等
  3. kernel.memory.save_information_async() 保存信息到 memory store
  4. kernel.memory.search_async() 搜索信息

使用 ChatALL 的 README.md 做数据,使用内存作为 memory store,我们演示下基于文档对话。

代码语言:javascript复制
#%% md

### 初始化 Embedding

#%%
import semantic_kernel as sk
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, OpenAITextEmbedding
import os

# 加载 .env 到环境变量

from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv())

# 创建 semantic kernel

kernel = sk.Kernel()

# 配置 OpenAI 服务

api_key = os.getenv('OPENAI_API_KEY')
endpoint = os.getenv('OPENAI_API_BASE')
model = OpenAIChatCompletion(
    "gpt-3.5-turbo", api_key, endpoint=endpoint)

# 把 LLM 服务加入 kernel

# 可以加多个。第一个加入的会被默认使用,非默认的要被指定使用

kernel.add_text_completion_service("my-gpt3", model)

# 添加 embedding 服务

kernel.add_text_embedding_generation_service(
    "ada", OpenAITextEmbedding("text-embedding-ada-002", api_key, endpoint=endpoint))
#%% md

### 文本向量化

#%%

# 使用内存做 memory store

kernel.register_memory_store(memory_store=sk.memory.VolatileMemoryStore())

# 读取文件内容

with open('../05-langchain/ChatALL.md', 'r') as f:
    # with open('sk_samples/SamplePlugin/SamplePlugin.py', 'r') as f:
    content = f.read()

# 将文件内容分片,单片最大 100 token(注意:SK 的 text split 功能目前对中文支持不如对英文支持得好)

lines = sk.text.split_markdown_lines(content, 100)

# 将分片后的内容,存入内存

for index, line in enumerate(lines):
    await kernel.memory.save_information_async("chatall", id=index, text=line)
#%% md

### 向量搜索

#%%
result = await kernel.memory.search_async("chatall", "ChatALL怎么下载?")
print(result[0].text)
#%% md

## Pipeline / Chain

#%% md
SK 更想用 pipeline 来描述 LangChain 中 chain 的概念,大概因为 pipeline 这个词更操作系统吧。但 chain 这个名词影响力太大,所以 SK 时不时也会用它。

但是,SK 没有在代码里定义什么是 pipeline,它并不是一个类,或者函数什么的。它是贯彻整个 kernel 的一个概念。

当一个 kernel 添加了 LLM、memory、functions,我们写下的 functions 之间的组合调用,就是个 pipeline 了。

如果需要多条 pipeline,就定义多个 kernel。

<div class="alert alert-block alert-info">
<b>思考:</b>LangChain 定义了好几种 Chain;SK 只是用 kernel 把抽象功能组合起来,pipeline 的过程完全交给开发者自己定义。你觉得哪种设计更好?
</div>


#%% md
现在用 pipeline 思想把对话式搜索 ChatALL 的 README.md 功能做完整。

要用到内置的 `TextMemorySkill`。

#%%

# 导入内置的 `TextMemorySkill`。主要使用它的 `recall()`

kernel.import_skill(sk.core_skills.TextMemorySkill())

# 直接在代码里创建 semantic function。里面调用了 `recall()`

sk_prompt = """
基于下面的背景信息回答问题。如果背景信息为空,或者和问题不相关,请回答"我不知道"。

[背景信息开始]
{{recall $input}}
[背景信息结束]

问题:{{$input}}
回答:
"""
ask = kernel.create_semantic_function(sk_prompt)

# 提问

context = kernel.create_new_context()
context[sk.core_skills.TextMemorySkill.COLLECTION_PARAM] = "chatall"
context[sk.core_skills.TextMemorySkill.RELEVANCE_PARAM] = 0.8
context["input"] = "ChatALL 怎么下载?"
result = ask(context=context)
print(result)
#%% md

Planner

代码语言:javascript复制
#%% md
SK 的 planner 概念上对标 LangChain 的 agent,但目前实现得还比较简单(C# 版丰富些)。

SK Python 提供了三种 planner:

1. `BasicPlanner` - 把任务拆解,自动调用各个函数,完成任务。它只是个用于基础验证的功能,最终会被 `SequentialPlanner` 替代。[Prompt 地址](https://github.com/microsoft/semantic-kernel/blob/main/python/semantic_kernel/planning/basic_planner.py#L27-L123)
2. `SequentialPlanner` - 开发中,未发布。比 `BasicPlanner` 更高级,但目标一致。[Prompt 地址](https://github.com/microsoft/semantic-kernel/blob/main/python/semantic_kernel/planning/sequential_planner/Skills/SequentialPlanning/skprompt.txt)、[官方例程](https://github.com/microsoft/semantic-kernel/blob/main/python/samples/kernel-syntax-examples/sequential_planner.py)
3. `ActionPlanner` - 已发布。只输出 action,不执行。[Prompt 地址](https://github.com/microsoft/semantic-kernel/blob/main/python/semantic_kernel/planning/action_planner/skprompt.txt)、[官方例程](https://github.com/microsoft/semantic-kernel/blob/main/python/samples/kernel-syntax-examples/action_planner.py)

来,把查周杰伦的生日是星期几,用 SK 的 `BasicPlanner` 再做一遍(版本 > 0.3.7dev 才能工作)。

#%%
from semantic_kernel.core_skills import WebSearchEngineSkill, TimeSkill, MathSkill
from semantic_kernel.connectors.search_engine import BingConnector
from semantic_kernel.planning import BasicPlanner
import semantic_kernel as sk
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, OpenAITextEmbedding
import os

# 加载 .env 到环境变量

from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv())

# 创建 semantic kernel

kernel = sk.Kernel()

# 配置 OpenAI 服务

api_key = os.getenv('OPENAI_API_KEY')
endpoint = os.getenv('OPENAI_API_BASE')
model = OpenAIChatCompletion(
    "gpt-4", api_key, endpoint=endpoint)    # GPT-4 才能完成此任务。不信改成 gpt-3.5-turbo 试试

# 把 LLM 服务加入 kernel

# 可以加多个。第一个加入的会被默认使用,非默认的要被指定使用

kernel.add_text_completion_service("my-gpt4", model)

# 导入搜索 plugin

connector = BingConnector(api_key=os.getenv("BING_API_KEY"))
kernel.import_skill(WebSearchEngineSkill(connector), "WebSearch")

# 导入其它 plugin。所有被导入的 plugin,都是可以被 planner 调用的

kernel.import_skill(MathSkill(), "math")
kernel.import_skill(TimeSkill(), "time")

# 创建 planner

planner = BasicPlanner()

# 开始

ask = "周杰伦的生日是星期几?"
plan = await planner.create_plan_async(ask, kernel)
print(plan.generated_plan)
result = await planner.execute_plan_async(plan, kernel)
print(result)

后记

0 人点赞