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
- 安装 Python 3.x:https://www.python.org/downloads/
- 安装 SK 包:
pip install semantic-kernel
- 在项目目录创建 .env 文件,添加以下内容:
# .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。
- 启动操作系统:
kernel = sk.Kernel()
- 安装驱动程序:
kernel.add_xxx_service()
- 安装应用程序:
func = kernel.create_semantic_function()
- 运行应用程序:
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 和 DELETEMathSkill
- 加法和减法计算TextMemorySkill
- 保存文本到 memory 中,可以对其做向量检索TextSkill
- 把文本全部转为大写或小写,去掉头尾的空格(trim)TimeSkill
- 获取当前时间及用多种格式获取时间参数WaitSkill
- 等待指定的时间WebSearchEngineSkill
- 在互联网上搜索给定的文本
#%% md
Memory
#%% md SK 的 memory 使用非常简单:
- 用
kernel.add_text_embedding_generation_service()
添加一个文本向量生成服务 - 用
kernel.register_memory_store()
注册一个 memory store,可以是内存、文件、向量数据库等 - 用
kernel.memory.save_information_async()
保存信息到 memory store - 用
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)
后记