Skip to content

Commit

Permalink
add python
Browse files Browse the repository at this point in the history
  • Loading branch information
liqiankun1111 committed May 25, 2024
1 parent f8c0aaf commit 77a6438
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 13 deletions.
22 changes: 22 additions & 0 deletions _posts/MachineLearning/2024-05-16-langchain_graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ keywords: langchain langgraph lcel
* TOC
{:toc}

## 不再是简单的顺序调用

从顺序式为主的简单架构走向复杂的WorkFlow,推理阶段的RAG Flow分成四种主要的基础模式:顺序、条件、分支与循环。PS: 一个llm 业务有各种基本概念,prompt/llm/memory,整个工作流产出一个流式输出,处理链路上包含多个step,且step有复杂的关系(顺序、条件、分支与循环)。一个llm 业务开发的核心就是个性化各种原子能力 以及组合各种原子能力。

以一个RAG Agent 的工作流程为例
1. 根据问题,路由器决定是从向量存储中检索上下文还是进行网页搜索。
2. 如果路由器决定将问题定向到向量存储以进行检索,则从向量存储中检索匹配的文档;否则,使用 tavily-api 进行网页搜索。
3. 文档评分器然后将文档评分为相关或不相关。
4. 如果检索到的上下文被评为相关,则使用幻觉评分器检查是否存在幻觉。如果评分器决定响应缺乏幻觉,则将响应呈现给用户。
5. 如果上下文被评为不相关,则进行网页搜索以检索内容。
6. 检索后,文档评分器对从网页搜索生成的内容进行评分。如果发现相关,则使用 LLM 进行综合,然后呈现响应。

## LCEL

[langchain入门3-LCEL核心源码速通](https://juejin.cn/post/7328204968636252198)LCEL实际上是langchain定义的一种DSL,可以方便的将一系列的节点按声明的顺序连接起来,实现固定流程的workflow编排。LCEL语法的核心思想是:一切皆为对象,一切皆为链。这意味着,LCEL语法中的每一个对象都实现了一个统一的接口:Runnable,它定义了一系列的调用方法(invoke, batch, stream, ainvoke, …)。这样,你可以用同样的方式调用不同类型的对象,无论它们是模型、函数、数据、配置、条件、逻辑等等。而且,你可以将多个对象链接起来,形成一个链式结构,这个结构本身也是一个对象,也可以被调用。这样,你可以将复杂的功能分解成简单的组件,然后用LCEL语法将它们组合起来,形成一个完整的应用。
Expand All @@ -36,6 +48,16 @@ chain.stream("dog")

![](/public/upload/machine/langchain_lcel.jpg)


|Component| Input Type| Output Type|
|---|---|---|
|Prompt| Dictionary| PromptValue|
|ChatModel| Single string, list of chat messages or a PromptValue| ChatMessage|
|LLM| Single string, list of chat messages or a PromptValue| String|
|OutputParser| The output of an LLM or ChatModel| Depends on the parser|
|Retriever| Single string| List of Documents|
|Tool| Single string or dictionary, depending on the tool| Depends on the tool|

我们使用的所有LCEL相关的组件都继承自RunnableSerializable,RunnableSequence 顾名思义就按顺序执行的Runnable,分为两部分Runnable和Serializable。其中Serializable是继承自Pydantic的BaseModel。(py+pedantic=Pydantic,是非常流行的参数验证框架)Serializable提供了,将Runnable序列化的能力。而Runnable,则是LCEL组件最重要的一个抽象类,它有几个重要的抽象方法。

```
Expand Down
6 changes: 5 additions & 1 deletion _posts/Technology/Concurrency/2020-06-14-concurrency_cost.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,15 @@ JVM_END
## 协程
协程,其实可以理解为一种特殊的程序调用。特殊的是在执行过程中,在子程序(或者说函数)内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
1. 可中断,这里的中断不是普通的函数调用,而是**类似CPU的中断**,CPU在这里直接释放转到其他程序断点继续执行。
2. 可恢复,等到合适的时候,可以恢复到中断的地方继续执行,什么是合适的时候?
协程一般暗含一个常规操作:比如Go 语言的调度器通过使用与 CPU 数量相等的线程减少线程频繁切换的内存开销。
### 创建协程
为什么协程的开销比线程的开销小?
为什么协程的开销比线程的开销小?协程的切换完全是程序代码控制的,在用户态的切换,就像函数回调的消耗一样,在线程的栈内即可完成。
1. Java Thread 和 kernel Thread 是1:1,Goroutine 是M:N ==> 执行体创建、切换过程不用“陷入”内核态。
2. 只涉及到三个寄存器(PC / SP / DX)的值修改;
Expand Down
49 changes: 46 additions & 3 deletions _posts/Technology/Python/2023-08-19-python_concurrency.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,7 @@ if __name__ == "__main__":

## 协程

异步编程是以进程、线程、协程、函数/方法作为执行任务程序的基本单位,结合回调、事件循环、信号量等机制,以提高程序整体执行效率和并发能力的编程方式。**协程即可以挂起并移交控制权的函数**,协程拥有自己的寄存器上下文和栈(每个线程不再只有一个堆栈)。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来时,恢复先前保存的寄存器上下文和栈。因此它在执行过程中可以中断,转而执行其他的协程,在适当的时候再回来继续执行。

异步编程是以进程、线程、协程、函数/方法作为执行任务程序的基本单位,结合回调、事件循环、信号量等机制,以提高程序整体执行效率和并发能力的编程方式。**协程即可以挂起并移交控制权的函数**,协程拥有自己的寄存器上下文和栈(每个线程不再只有一个堆栈)。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来时,恢复先前保存的寄存器上下文和栈。因此它在执行过程中可以中断,转而执行其他的协程,在适当的时候再回来继续执行。如果熟知了python生成器,还可以将协程理解为生成器+调度策略,生成器中的yield关键字,就可以让生成器函数发生中断,而调度策略,可以驱动着协程的执行和恢复。

```python
# 同步函数,同步函数本身是一个 Callable 对象,调用这个函数的时候,函数体内的代码被执行。
Expand All @@ -158,6 +157,50 @@ print(async_function())

async 修饰词声明异步函数,而调用异步函数,我们便可得到一个协程对象(coroutine object)。

```python
import asyncio
import time

async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)

async def main():
print(f"started at {time.strftime('%X')}")
await say_after(1, 'hello')
await say_after(2, 'world')
print(f"finished at {time.strftime('%X')}")

asyncio.run(main())
#
# started at 14:07:43
# hello
# world
# finished at 14:07:46
```

按道理应该是间隔了2s,可是实际间隔3s。Python中的异步执行模式依赖于Event Loop,在等待的间隙中需要从Event Loop中找其它可以运行的程序,await关键字就是将coroutine转化成一个task加入到Event Loop中去,而执行第一个say_after的时候,第二个say_after并没有加入到Event Loop中去,所以在第一个say_after等待的时候无法去执行第二个say_after,最终导致的结果就是程序运行了3s,并没有达到异步的效果。有两种方式解决这个问题
1. 提前将两个say_after加入到eventloop中
```python
async def main():
task1 = asyncio.create_task(say_after(1, 'hello'))
task2 = asyncio.create_task(say_after(2, 'world'))
print(f"started at {time.strftime('%X')}")
await task1
await task2
print(f"finished at {time.strftime('%X')}")
```
2. 使用gather方法
```python
async def main():
print(f"started at {time.strftime('%X')}")
await asyncio.gather(
say_after(1, 'hello'),
say_after(2, 'world')
)
print(f"finished at {time.strftime('%X')}")
```

### Task

调用异步函数并不能执行函数,**异步函数就不能由我们自己直接执行**(这也是函数与协程的区别),异步代码是以 Task 的形式去运行,被 Event Loop 管理和调度的。`result = async_function()` 协程对象 result 虽然生成了,但是还没有运行,要使代码块实际运行,需要使用 asyncio 提供的其他工具。
Expand Down Expand Up @@ -202,7 +245,7 @@ Python 3.4 加入了asyncio 库,使得Python有了支持异步IO的官方库

多线程还是 Asyncio?如果是 I/O bound,并且 I/O 操作很慢,需要很多任务 / 线程协同实现,那么使用 Asyncio 更合适。如果是 I/O bound,但是 I/O 操作很快,只需要有限数量的任务 / 线程,那么使用多线程就可以了。如果是 CPU bound,则需要使用多进程来提高程序运行效率。I/O 操作 heavy 的场景下,Asyncio 比多线程的运行效率更高。因为 Asyncio 内部任务切换的损耗,远比线程切换的损耗要小;并且 Asyncio 可以开启的任务数量,也比多线程中的线程数量多得多。但需要注意的是,很多情况下,使用 Asyncio 需要特定第三方库的支持。

使用 asyncio 并不是将代码转换成多线程,它不会导致多条Python指令同时执行,也不会以任何方式让你避开所谓的全局解释器锁(Global Interpreter Lock,GIL)。有些应用受 IO 速度的限制,即使 CPU 速度再快,也无法充分发挥 CPU 的性能。这些应用花费大量时间从存储或网络设备读写数据,往往需要等待数据到达后才能进行计算,在等待期间,CPU 什么都做不了。asyncio 的目的就是为了给 CPU 安排更多的工作:当前单线程代码正在等待某个事情发生时,另一段代码可以接管并使用 CPU,以充分利用 CPU 的计算性能。**asyncio 更多是关于更有效地使用单核,而不是如何使用多核**
使用 asyncio 并不是将代码转换成多线程,它不会导致多条Python指令同时执行,也不会以任何方式让你避开所谓的全局解释器锁(Global Interpreter Lock,GIL)。有些应用受 IO 速度的限制,即使 CPU 速度再快,也无法充分发挥 CPU 的性能。这些应用花费大量时间从存储或网络设备读写数据,往往需要等待数据到达后才能进行计算,在等待期间,CPU 什么都做不了。asyncio 的目的就是为了给 CPU 安排更多的工作:当前单线程代码正在等待某个事情发生时,另一段代码可以接管并使用 CPU,以充分利用 CPU 的计算性能。**asyncio 更多是关于更有效地使用单核,而不是如何使用多核**python协程单线程内切换,适用于IO密集型程序中,可以最大化IO多路复用的效果。**协程间完全同步**,不会并行,不需要考虑数据安全。PS:与Go协程不同的地方。

### 事件

Expand Down
40 changes: 40 additions & 0 deletions _posts/Technology/Python/2023-09-05-python_library.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,46 @@ st.text_input('请输入最喜欢的编程语言', key="name")

## pydantic(py+pedantic=Pydantic)

对象的属性,我们都是通过把变量值赋值给对象本身来实现的。直接赋值会存在一个问题,就是无法对属性值进行合法性较验,比如我给 age 赋值的是负数,在业务上这种数据是不合法的。

```
>>> class Student:pass
...
>>>
>>> s = Student()
>>> s.name = "xx"
>>> s.age = 27
```

一个实现了 描述符协议 的类就是一个描述符。描述符协议:在类里实现了 __get__()、__set__()、__delete__() 其中至少一个方法。
1. `__get__`: 用于访问属性。它返回属性的值,若属性不存在、不合法等都可以抛出对应的异常。
2. `__set__`:将在属性分配操作中调用。不会返回任何内容。
3. `__delete__`:控制删除操作。不会返回内容。

```python
class Student:
def __init__(self, name, math, chinese, english):
self.name = name
self.math = math
self.chinese = chinese
self.english = english
class Score:
def __init__(self, default=0):
self._score = default
def __set__(self, instance, value):
if not isinstance(value, int):
raise TypeError('Score must be integer')
if not 0 <= value <= 100:
raise ValueError('Valid value must be in [0, 100]')
self._score = value

def __get__(self, instance, owner):
return self._score
def __delete__(self):
del self._score
```
Student类里的三个属性,math、chinese、english,三个变量的合法性逻辑都是一样的,只要大于0,小于100 就可以。Score 类是一个描述符,当从 Student 的实例访问 math、chinese、english这三个属性的时候,都会经过 Score 类里的三个特殊的方法。这里的 Score 避免了 使用Property 出现大量的校验代码无法复用的尴尬。我们熟悉的@property@classmethod@staticmethod 和 super 等特性的底层实现机制都是基于 描述符协议 的。

pydantic 是一个基于Python类型提示来定义数据验证、序列化和文档(使用JSON模式)的库; 使用Python的类型提示来进行数据校验和settings管理; 可以在代码运行的时候提供类型提示,数据校验失败的时候提供友好的错误提示;
1. 所有基于pydantic的数据类型本质上都是一个BaseModel类
2. pydantic中的一些常用的基本类型 Dict, List, Sequence, Set, Tuple
Expand Down
17 changes: 9 additions & 8 deletions _posts/Technology/Python/2024-03-16-python_practice.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ keywords: Python
* TOC
{:toc}

## yield

return和yield异同
1. 相似的是:yield 和 return 都可以在一个函数里将值返回给调用方;
2. 不同的是:return 后,函数运行就终止了,而 yield 则只是暂停运行。
含有 yield 的函数,不再是普通的函数,直接调用含有 yield 的函数,返回的是一个生成器对象(generator object)。可以使用 for 循环(实际还可以使用 list 或者 next 函数)来遍历该生成器对象,将 yield 的内容一个一个打印出来。

## 闭包

Expand Down Expand Up @@ -61,16 +67,11 @@ hello world

装饰器将额外增加的功能,封装在自己的装饰器函数或类中;如果你想要调用它,只需要在原函数的顶部,加上 @decorator 即可。显然,这样做可以让你的代码得到高度的抽象、分离与简化。

## 内存管理

Python 中一切皆对象。因此,你所看到的一切变量,本质上都是对象的一个指针。那么,怎么知道一个对象,是否永远都不能被调用了呢?引用计数(`sys.getrefcount(a)`,getrefcount 本身也会引入一次计数;在函数调用发生的时候,会产生额外的两次引用,一次来自函数栈,另一个是函数参数。)。

相比 C 语言里,你需要使用 free 去手动释放内存,Python 的垃圾回收在这里可以说是省心省力了。不过,如果我偏偏想手动释放内存,应该怎么做呢?方法同样很简单。你只需要先调用 del a 来删除对象的引用;然后强制调用 gc.collect(),清除没有引用的对象,即可手动启动垃圾回收。

Python 使用标记清除(mark-sweep)算法和分代收集(generational),来启用针对循环引用的自动垃圾回收。先来看标记清除算法。我们先用图论来理解不可达的概念。对于一个有向图,如果从一个节点出发进行遍历,并标记其经过的所有节点;那么,在遍历结束后,所有没有被标记的节点,我们就称之为不可达节点。显而易见,这些节点的存在是没有任何意义的,自然的,我们就需要对它们进行垃圾回收。当然,每次都遍历全图,对于 Python 而言是一种巨大的性能浪费。所以,在 Python 的垃圾回收实现中,mark-sweep 使用双向链表维护了一个数据结构,并且只考虑容器类的对象(只有容器类对象才有可能产生循环引用)。而分代收集算法,则是另一个优化手段。Python 将所有对象分为三代。刚刚创立的对象是第 0 代;经过一次垃圾回收后,依然存在的对象,便会依次从上一代挪到下一代。而每一代启动自动垃圾回收的阈值,则是可以单独指定的。当垃圾回收器中新增对象减去删除对象达到相应的阈值时,就会对这一代对象启动垃圾回收。

## with和上下文管理器

当对象使用 with 声明创建时,上下文管理器允许类做一些设置和清理工作。上下文管理器的行为由下面两个魔法方法所定义: `__enter__()``__exit__()`。PS:魔术方法的支持下的一种语法糖。

with 语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的“清理”操作,释放资源。类似于java的 `try(xx){...}``try...finally...`
```python
with open('path','读写模式‘) as f:
Expand All @@ -83,7 +84,7 @@ do something
f.close()
```
要使用 with 语句,首先要明白上下文管理器 以及 上下文管理协议(Context Management Protocol):
1. 在有两个相关的操作需要在一部分代码块前后分别执行的时候,可以使用 with 语法自动完成。具体的说,包含方法 `__enter__()``__exit__()``__init__()``__enter__()`方法会在with的代码块执行之前执行,`__exit__()`会在代码块执行结束后执行。如果使用了 as 子句,则将 enter() 方法的返回值赋值给 as 子句中的 target。
1. 在有两个相关的操作需要在一部分代码块前后分别执行的时候,可以使用 with 语法自动完成。`__enter__()`方法会在with的代码块执行之前执行,`__exit__()`会在代码块执行结束后执行。如果使用了 as 子句,则将 enter() 方法的返回值赋值给 as 子句中的 target。
2. 使用 with 语法可以在特定的地方分配和释放资源

基于类的上下文管理器
Expand Down
Loading

0 comments on commit 77a6438

Please sign in to comment.