比Jev快10倍!Laya 本地决策 AI 引擎本地部署教程|快到起飞 + 实战演示

大家好,我是超哥。今天教大家部署 Laya 本地决策 AI 引擎,Laya 是一个几百MB的开源决策模型,速度比 Jev 快 7–11 倍,它主要负责分类判断评分以及智能路由,可以直接在本地运行,实现毫秒级 AI 决策,不需要依赖云端 API。接下来我会带大家完成 Laya 的本地部署运行测试。下面开始教程。

一、安装python环境

下载并安装python

下载地址:https://www.python.org/downloads/release/python-3120/

直达下载:https://www.python.org/ftp/python/3.12.0/python-3.12.0-amd64.exe

验证python是否安装成功,cmd里执行下面代码:

python --version

二、安装虚拟环境

1. 创建模型目录

在D盘创建目录:LayaAI,创建后路径 D:\LayaAI

2. 创建虚拟空间

cmd进入 D:\LayaAI,执行下面命令,创建虚拟空间:

python -m venv laya-env

输入下面命令,激活虚拟空间:

laya-env\Scripts\activate

3. 升级 pip

升级 pip 命令:

python -m pip install --upgrade pip

4. 安装 CUDA 版 PyTorch

安装命令:

pip install torch torchvision --index-url https://download.pytorch.org/whl/cu128

检测命令:

python -c "import torch; print('PyTorch:', torch.__version__); print('CUDA:', torch.cuda.is_available()); print('GPU:', torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'None')"

检测需要出现类似下面文字(一个人一个样):

CUDA: True

CUDA版本: 12.8

GPU: NVIDIA GeForce RTX 4080 SUPER

三、安装 Laya

Github 项目地址:https://github.com/NandhaKishorM/laya

1. 安装 Laya 命令

pip install laya

2. 检测 Laya 是否安装成功

python -c "import laya; print('Laya OK'); print('Version:', laya.__version__)"

3. 测试 Laya

电脑 D:\LayaAI 目录下创建 test.py 文件,复制下面的内容进去:

from laya import Router

print("正在加载 Laya...")

router = Router(preload=True)

state = {
    "subject": "Payment failed",
    "body": "I tried to pay for my order twice, but both payments failed."
}

questions = {
    "category": {
        "type": "choice",
        "instructions": "Which team should handle this request?",
        "criteria": {
            "billing": "payments, invoices, refunds",
            "technical": "bugs, errors, integrations",
            "sales": "pricing, demos, purchases",
            "other": "none of the above"
        }
    }
}

print("正在进行决策...")

result = router.predict(state, questions)

print("\n===== Laya 决策结果 =====")
print(result)

执行下面的命令

python test.py

四、贪吃蛇测试 Laya

1. 安装 Pygame

pip install pygame

2. 测试 Pygame 是否安装成功

python -c "import pygame; print(pygame.version.ver)"

3. 贪吃蛇 Laya 测试 Demo

电脑 D:\LayaAI 目录下创建 snake.py 文件,复制下面的内容进去:

import time
import random
from collections import deque

import pygame
import torch
from laya import Router


# ============================================================
# Laya Snake - Windows / CUDA
#
# Default mode:
#   Laya + Safety Shield
#
# Set PURE_LAYA = True if you want to deliberately observe
# unfiltered Laya decisions (it can crash into walls).
# ============================================================

PURE_LAYA = False

BOARD_W = 20
BOARD_H = 14
CELL = 36

GAME_W = BOARD_W * CELL
GAME_H = BOARD_H * CELL
SIDE_W = 340

WIDTH = GAME_W + SIDE_W
HEIGHT = GAME_H

FPS = 60
MOVE_INTERVAL = 0.12


# ------------------------- colors ----------------------------

BG = (7, 13, 18)
BOARD = (5, 11, 16)
GRID = (19, 31, 40)
PANEL = (13, 22, 29)

TEXT = (235, 241, 245)
MUTED = (140, 157, 170)

GREEN = (32, 192, 87)
GREEN_HEAD = (50, 224, 111)
YELLOW = (255, 177, 0)
RED = (235, 75, 81)
BLUE = (105, 170, 240)
BAR_BG = (35, 49, 59)


DIRECTIONS = {
    "UP": (0, -1),
    "DOWN": (0, 1),
    "LEFT": (-1, 0),
    "RIGHT": (1, 0),
}

OPPOSITE = {
    "UP": "DOWN",
    "DOWN": "UP",
    "LEFT": "RIGHT",
    "RIGHT": "LEFT",
}


def inside(x, y):
    return 0 <= x < BOARD_W and 0 <= y < BOARD_H


def next_pos(pos, direction):
    dx, dy = DIRECTIONS[direction]
    return [pos[0] + dx, pos[1] + dy]


def manhattan(a, b):
    return abs(a[0] - b[0]) + abs(a[1] - b[1])


# ============================================================
# Board analysis / planner
# ============================================================

def legal_moves(game):
    """
    Hard Snake movement rules.

    This keeps the v2 goal-seeking behavior, but makes self-collision
    an absolute execution rule:
      - no wall collision
      - no collision with the snake body
      - no 180-degree reversal
      - the tail is only considered free on a non-eating move
    """
    result = []

    for direction in DIRECTIONS:
        if direction == OPPOSITE[game.direction]:
            continue

        p = next_pos(game.snake[0], direction)

        # Rule 1: wall collision = illegal
        if not inside(p[0], p[1]):
            continue

        # Rule 2: body collision = illegal.
        # The current tail leaves the board cell only when this move
        # does NOT eat food.
        occupied = set(map(tuple, game.snake))
        if game.food != p and game.snake:
            occupied.discard(tuple(game.snake[-1]))

        if tuple(p) in occupied:
            continue

        # Final simulation check. This catches any mismatch between the
        # rule calculation and the actual SnakeGame.move() implementation.
        fake = SnakeGame.from_existing(game)
        if not fake.move(direction) or fake.game_over:
            continue

        result.append(direction)

    return result


def reachable_area(game, start, extra_blocked=None):
    blocked = set(map(tuple, game.snake))
    if extra_blocked:
        blocked.update(extra_blocked)

    # The tail can move on the next ordinary step.
    if game.snake:
        blocked.discard(tuple(game.snake[-1]))

    start_t = tuple(start)

    if start_t in blocked or not inside(*start):
        return 0

    q = deque([start_t])
    seen = {start_t}

    while q:
        x, y = q.popleft()

        for dx, dy in DIRECTIONS.values():
            nx, ny = x + dx, y + dy

            if not inside(nx, ny):
                continue

            p = (nx, ny)

            if p in seen or p in blocked:
                continue

            seen.add(p)
            q.append(p)

    return len(seen)


def shortest_path_length(game, target):
    """BFS distance from snake head to target through current free cells."""
    blocked = set(map(tuple, game.snake))
    if game.snake:
        blocked.discard(tuple(game.snake[-1]))

    start = tuple(game.snake[0])
    target = tuple(target)

    if start == target:
        return 0

    q = deque([(start, 0)])
    seen = {start}

    while q:
        (x, y), dist = q.popleft()

        for dx, dy in DIRECTIONS.values():
            nx, ny = x + dx, y + dy

            if not inside(nx, ny):
                continue

            p = (nx, ny)

            if p in seen or p in blocked:
                continue

            if p == target:
                return dist + 1

            seen.add(p)
            q.append((p, dist + 1))

    return None


def planner_summary(game):
    """Compact structured information supplied to Laya."""
    legal = legal_moves(game)

    candidates = {}

    for direction in DIRECTIONS:
        p = next_pos(game.snake[0], direction)

        if direction not in legal:
            candidates[direction] = {
                "legal": False,
                "distance_to_food": None,
                "reachable_area": 0,
            }
            continue

        fake = SnakeGame.from_existing(game)
        fake.move(direction)

        area = reachable_area(fake, fake.snake[0])

        distance = shortest_path_length(
            fake,
            game.food
        ) if game.food else None

        candidates[direction] = {
            "legal": True,
            "distance_to_food": distance,
            "reachable_area": area,
        }

    return legal, candidates


# ============================================================
# Snake
# ============================================================

class SnakeGame:

    def __init__(self):
        self.reset()

    @classmethod
    def from_existing(cls, other):
        obj = cls.__new__(cls)
        obj.snake = [p[:] for p in other.snake]
        obj.direction = other.direction
        obj.food = other.food[:] if other.food else None
        obj.score = other.score
        obj.steps = other.steps
        obj.game_over = other.game_over
        return obj

    def reset(self):
        cx = BOARD_W // 2
        cy = BOARD_H // 2

        self.snake = [
            [cx, cy],
            [cx - 1, cy],
            [cx - 2, cy],
            [cx - 3, cy],
        ]

        self.direction = "RIGHT"
        self.food = self.spawn_food()
        self.score = 0
        self.steps = 0
        self.game_over = False

    def spawn_food(self):
        free = [
            [x, y]
            for y in range(BOARD_H)
            for x in range(BOARD_W)
            if [x, y] not in self.snake
        ]

        return random.choice(free) if free else None

    def move(self, direction):
        if self.game_over:
            return False

        if direction == OPPOSITE[self.direction]:
            direction = self.direction

        dx, dy = DIRECTIONS[direction]
        head = self.snake[0]
        new_head = [head[0] + dx, head[1] + dy]

        # Collision check. The tail is allowed to move away on
        # a non-eating step.
        occupied = set(map(tuple, self.snake))
        if self.food != new_head and self.snake:
            occupied.discard(tuple(self.snake[-1]))

        if not inside(*new_head) or tuple(new_head) in occupied:
            self.game_over = True
            return False

        self.direction = direction
        self.snake.insert(0, new_head)
        self.steps += 1

        if self.food and new_head == self.food:
            self.score += 1
            self.food = self.spawn_food()
        else:
            self.snake.pop()

        return True


# ============================================================
# Laya controller
# ============================================================

class LayaController:

    def __init__(self):
        print("=" * 60)
        print("Loading Laya...")
        print("=" * 60)

        self.router = Router(
            preload=True,
            device="cuda"
        )

        print("Laya loaded successfully.")

        if torch.cuda.is_available():
            print("GPU:", torch.cuda.get_device_name(0))

        self.probabilities = {
            d: 0.25 for d in DIRECTIONS
        }

        self.choice = "RIGHT"
        self.latency_ms = 0.0

        self.raw_choice = "RIGHT"
        self.shield_intervention = False

        self.dead_end_risk = None
        self.food_reachable = None

    def predict(self, game):
        legal, candidates = planner_summary(game)

        # Build a compact state. The planner information is
        # deliberately supplied to Laya; this is similar in spirit
        # to the public Snake demo, which does not feed a raw image
        # to the checkpoint.
        state = {
            "game": {
                "board": [BOARD_W, BOARD_H],
                "score": game.score,
                "steps": game.steps,
                "direction": game.direction,
                "snake_head": game.snake[0],
                "snake_length": len(game.snake),
                "food": game.food,
            },
            "planner": {
                "legal_moves": legal,
                "candidates": candidates,
                "food_distance": (
                    shortest_path_length(game, game.food)
                    if game.food else None
                ),
                "reachable_area": reachable_area(
                    game, game.snake[0]
                ),
            },
        }

        questions = {
            "next_move": {
                "type": "choice",
                "instructions": (
                    "Choose the best next move for the snake. "
                    "Use the supplied planner information. "
                    "Never choose a move marked legal=false. "
                    "Prefer a move that approaches the food while "
                    "preserving a large reachable area."
                ),
                "criteria": {
                    "UP": "Move one cell upward.",
                    "DOWN": "Move one cell downward.",
                    "LEFT": "Move one cell left.",
                    "RIGHT": "Move one cell right.",
                },
            },
            "dead_end_risk": {
                "type": "noul",
                "instructions": (
                    "Does the current position have a serious "
                    "dead-end risk according to the supplied "
                    "reachable-area and candidate information?"
                ),
            },
            "food_reachable": {
                "type": "noul",
                "instructions": (
                    "Is the food currently reachable according "
                    "to the supplied planner information?"
                ),
            },
        }

        if torch.cuda.is_available():
            torch.cuda.synchronize()

        start = time.perf_counter()

        result = self.router.predict(
            state,
            questions
        )

        if torch.cuda.is_available():
            torch.cuda.synchronize()

        self.latency_ms = (
            time.perf_counter() - start
        ) * 1000.0

        # ---------------- parse choice ----------------

        try:
            answer = result["answers"]["next_move"]

            raw_choice = answer.get("choice")
            raw_probs = answer.get("probabilities", {})

            if raw_choice in DIRECTIONS:
                self.raw_choice = raw_choice

            probs = {}

            for d in DIRECTIONS:
                try:
                    probs[d] = max(
                        0.0,
                        float(raw_probs.get(d, 0.0))
                    )
                except Exception:
                    probs[d] = 0.0

            total = sum(probs.values())

            if total > 0:
                probs = {
                    d: v / total
                    for d, v in probs.items()
                }

            self.probabilities = probs

        except Exception as exc:
            print("Choice parsing error:", exc)
            print(result)

        # ---------------- parse auxiliary answers ----------------

        try:
            self.dead_end_risk = float(
                result["answers"]["dead_end_risk"]["noul"]
            )
        except Exception:
            self.dead_end_risk = None

        try:
            self.food_reachable = float(
                result["answers"]["food_reachable"]["noul"]
            )
        except Exception:
            self.food_reachable = None

        # ---------------- goal-seeking safety shield ----------------
        #
        # The previous version only blocked immediate collisions.
        # That is NOT enough: Laya can legally choose LEFT forever
        # while the food is below/right of the snake.
        #
        # Here Laya still provides the direction probabilities, but
        # the execution layer requires the move to make measurable
        # progress toward the current food whenever a safe progress
        # move exists. Among equally good progress moves, Laya decides.
        #
        # This is much closer to the public Laya Snake demo's idea:
        # planner features + Laya decision + execution safety layer.

        self.shield_intervention = False

        safe = list(legal)

        if PURE_LAYA:
            selected = self.raw_choice

            if selected == OPPOSITE[game.direction]:
                selected = game.direction

        elif not safe:
            selected = game.direction

        else:
            current_distance = shortest_path_length(game, game.food)

            scored = []

            for d in safe:
                fake = SnakeGame.from_existing(game)
                moved_ok = fake.move(d)

                # A candidate that actually causes GAME OVER is never
                # allowed into the goal-seeking candidate pool.
                if not moved_ok or fake.game_over:
                    continue

                new_distance = (
                    shortest_path_length(fake, game.food)
                    if game.food else None
                )

                area = reachable_area(fake, fake.snake[0])

                # Primary objective: get closer to food.
                # Secondary objective: keep the snake in open space.
                if current_distance is not None and new_distance is not None:
                    progress = current_distance - new_distance
                else:
                    progress = 0

                scored.append(
                    {
                        "direction": d,
                        "distance": new_distance,
                        "progress": progress,
                        "area": area,
                        "prob": self.probabilities.get(d, 0.0),
                    }
                )

            # If there is at least one safe move that reduces the
            # shortest-path distance, discard moves that do not.
            progress_moves = [
                item for item in scored
                if item["progress"] > 0
            ]

            if progress_moves:
                candidates = progress_moves
            else:
                # No direct progress is available. Preserve space,
                # then let Laya break ties.
                max_area = max(item["area"] for item in scored)
                candidates = [
                    item for item in scored
                    if item["area"] >= max_area * 0.90
                ]

            # Laya remains the tie-breaker / preference signal.
            # A small area term prevents obviously cramped choices.
            selected_item = max(
                candidates,
                key=lambda item: (
                    item["prob"] * 100.0
                    + item["area"] * 0.02
                )
            )

            selected = selected_item["direction"]

            if selected != self.raw_choice:
                self.shield_intervention = True

        # FINAL HARD RULE:
        # Never execute a move that the actual game engine says is a
        # wall/body collision. This does not change v2's goal-seeking
        # behavior; it only prevents an illegal move from reaching game.move().
        if not PURE_LAYA:
            verified = SnakeGame.from_existing(game)
            if not verified.move(selected) or verified.game_over:
                safe_fallbacks = []

                for d in legal:
                    test = SnakeGame.from_existing(game)
                    if test.move(d) and not test.game_over:
                        safe_fallbacks.append(d)

                if safe_fallbacks:
                    selected = max(
                        safe_fallbacks,
                        key=lambda d: self.probabilities.get(d, 0.0)
                    )
                    self.shield_intervention = True
                else:
                    # No legal move exists: the current position is
                    # genuinely terminal.
                    selected = game.direction
                    self.shield_intervention = True

        self.choice = selected

        return selected


# ============================================================
# GUI
# ============================================================

def text(screen, font, value, x, y, color=TEXT):
    surf = font.render(str(value), True, color)
    screen.blit(surf, (x, y))


def draw_bar(screen, x, y, width, height, value):
    pygame.draw.rect(
        screen,
        BAR_BG,
        (x, y, width, height)
    )

    pygame.draw.rect(
        screen,
        GREEN,
        (
            x,
            y,
            int(width * max(0.0, min(1.0, value))),
            height
        )
    )


def draw(screen, game, controller, fonts, paused):
    small = fonts["small"]
    normal = fonts["normal"]
    big = fonts["big"]

    screen.fill(BG)

    # Board
    pygame.draw.rect(
        screen,
        BOARD,
        (0, 0, GAME_W, GAME_H)
    )

    for x in range(BOARD_W + 1):
        px = x * CELL
        pygame.draw.line(
            screen, GRID,
            (px, 0),
            (px, GAME_H)
        )

    for y in range(BOARD_H + 1):
        py = y * CELL
        pygame.draw.line(
            screen, GRID,
            (0, py),
            (GAME_W, py)
        )

    # Food
    if game.food:
        fx, fy = game.food
        pygame.draw.circle(
            screen,
            YELLOW,
            (
                fx * CELL + CELL // 2,
                fy * CELL + CELL // 2
            ),
            CELL // 4
        )

    # Snake
    for i, (x, y) in enumerate(game.snake):
        rect = pygame.Rect(
            x * CELL + 2,
            y * CELL + 2,
            CELL - 4,
            CELL - 4
        )

        pygame.draw.rect(
            screen,
            GREEN_HEAD if i == 0 else GREEN,
            rect,
            border_radius=5
        )

    # Side
    pygame.draw.rect(
        screen,
        PANEL,
        (GAME_W, 0, SIDE_W, HEIGHT)
    )

    x = GAME_W + 24
    y = 20

    text(screen, big, "LAYA × SNAKE", x, y)
    y += 42

    if game.game_over:
        text(screen, normal, "GAME OVER", x, y, RED)
    elif paused:
        text(screen, normal, "PAUSED", x, y, YELLOW)
    else:
        text(screen, small, "LIVE DECISION - XGDN.Com", x, y, MUTED)

    y += 44

    text(screen, normal, "NEXT MOVE", x, y)
    y += 34

    for d in ("UP", "DOWN", "LEFT", "RIGHT"):
        p = controller.probabilities.get(d, 0.0)

        text(screen, small, d, x, y)

        draw_bar(
            screen,
            x + 70,
            y + 4,
            150,
            14,
            p
        )

        text(
            screen,
            small,
            f"{p * 100:.0f}%",
            x + 230,
            y
        )

        y += 36

    y += 8

    text(screen, small, "SELECTED", x, y, MUTED)
    y += 23

    text(
        screen,
        big,
        controller.choice,
        x,
        y,
        GREEN_HEAD
    )

    y += 48

    text(screen, small, "RAW LAYA", x, y, MUTED)
    text(
        screen,
        small,
        controller.raw_choice,
        x + 110,
        y
    )

    y += 27

    text(screen, small, "GOAL + RULE SHIELD", x, y, MUTED)

    shield_text = (
        "INTERVENED"
        if controller.shield_intervention
        else "PASS"
    )

    shield_color = (
        YELLOW
        if controller.shield_intervention
        else GREEN
    )

    text(
        screen,
        small,
        shield_text,
        x + 110,
        y,
        shield_color
    )

    y += 39

    # Metrics
    metrics = [
        ("SCORE", game.score),
        ("STEPS", game.steps),
        ("LATENCY", f"{controller.latency_ms:.1f} ms"),
        (
            "DECISIONS/S",
            f"{1000 / controller.latency_ms:.0f}"
            if controller.latency_ms > 0 else "—"
        ),
    ]

    for label, value in metrics:
        text(screen, small, label, x, y, MUTED)
        text(screen, normal, value, x + 125, y - 4)
        y += 31

    y += 9

    text(screen, small, "MODEL", x, y, MUTED)
    y += 22
    text(screen, small, "Laya multilingual + goal planner", x, y)

    y += 35

    if controller.dead_end_risk is not None:
        text(
            screen,
            small,
            "DEAD-END RISK",
            x,
            y,
            MUTED
        )
        text(
            screen,
            small,
            f"{controller.dead_end_risk * 100:.1f}%",
            x + 125,
            y
        )
        y += 27

    if controller.food_reachable is not None:
        text(
            screen,
            small,
            "FOOD REACHABLE",
            x,
            y,
            MUTED
        )
        text(
            screen,
            small,
            f"{controller.food_reachable * 100:.1f}%",
            x + 125,
            y
        )
        y += 32

    text(
        screen,
        small,
        "SPACE  Pause / Resume",
        x,
        y,
        MUTED
    )
    y += 21

    text(
        screen,
        small,
        "R      Restart",
        x,
        y,
        MUTED
    )
    y += 21

    text(
        screen,
        small,
        "ESC    Quit",
        x,
        y,
        MUTED
    )


# ============================================================
# Main
# ============================================================

def main():
    pygame.init()

    pygame.display.set_caption(
        "Laya Snake - RTX 4080 Super"
    )

    screen = pygame.display.set_mode(
        (WIDTH, HEIGHT)
    )

    clock = pygame.time.Clock()

    fonts = {
        "small": pygame.font.SysFont(
            "Consolas", 15
        ),
        "normal": pygame.font.SysFont(
            "Consolas", 19, bold=True
        ),
        "big": pygame.font.SysFont(
            "Consolas", 27, bold=True
        ),
    }

    controller = LayaController()
    game = SnakeGame()

    running = True
    paused = False
    last_move = time.perf_counter()

    while running:
        now = time.perf_counter()

        for event in pygame.event.get():

            if event.type == pygame.QUIT:
                running = False

            elif event.type == pygame.KEYDOWN:

                if event.key == pygame.K_ESCAPE:
                    running = False

                elif event.key == pygame.K_SPACE:
                    paused = not paused

                elif event.key == pygame.K_r:
                    game.reset()
                    paused = False
                    last_move = time.perf_counter()

        if (
            not paused
            and not game.game_over
            and now - last_move >= MOVE_INTERVAL
        ):
            last_move = now

            direction = controller.predict(game)

            game.move(direction)

        draw(
            screen,
            game,
            controller,
            fonts,
            paused
        )

        pygame.display.flip()
        clock.tick(FPS)

    pygame.quit()


if __name__ == "__main__":
    main()

上一篇官方无审查版,8GB 就能跑!Qwen-Image-2.1 正式版 本地部署教程 + 实测|效果炸裂,最强图片生成 AI 模型! 下一篇没有了