Search by

laybot / ai-sdk

laybot

Provider-independent AI and speech SDK for PHP/Webman

Package info

github.com/laybot/ai-sdk

pkg:composer/laybot/ai-sdk

Statistics

Installs: 48

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v2.0.0 2026-09-08 14:39 UTC

This package is auto-updated.

Last update: 2026-09-08 14:42:02 UTC


README

灵语智教 · 多模型厂商与实时语音能力聚合 SDK
Ark · DeepSeek · OpenAI-Compatible · Responses · TTS · ASR · Webman Async

Chat · Responses · SSE · Tool Calls · Files · TTS · AUC · SAUC · WebSocket

LayBot 灵语智教 Packagist Apache License 2.0 PHP 8.1+

Powered by LayBot LingTeach AI · Laybot 现代 MPA 工程平台

laybot/ai-sdk 是面向 PHP 8.1+ 的多供应商 AI 与语音协议 SDK。
它在 laybot/request-sdk 之上统一 Chat、Responses、流式文本、Tool Call、Usage、Files、TTS、ASR 和供应商异常。

1. 项目简介

在企业 AI 项目中,接入大模型通常不只是发送一次 HTTP 请求。

实际系统还需要处理:

  • 多供应商模型切换;
  • 同步与异步调用;
  • SSE 增量文本;
  • Reasoning 输出;
  • Tool Call 参数归并;
  • Responses API;
  • 图片、PDF 和 Files API;
  • Token Usage;
  • Request ID 和供应商日志 ID;
  • HTTP Chunked、NDJSON 和二进制协议;
  • 实时 TTS 和 ASR;
  • WebSocket Session 状态机;
  • 连接、请求和流空闲超时;
  • 取消、背压和资源释放;
  • 供应商错误码和重复计费风险;
  • Webman 常驻进程中的租户配置隔离。

laybot/ai-sdk 将这些能力收敛为统一的 PHP API:

业务项目
    ↓
laybot/ai-sdk
    ↓
laybot/request-sdk
    ↓
HTTP / SSE / NDJSON / WebSocket / TLS
    ↓
模型与语音供应商

它解决的不是单纯的“调用一个模型”,而是:

如何在 PHP 企业项目中,以可维护、可观测、可取消、可扩展的方式统一接入模型和实时语音供应商。

2. LayBot 体系

本 SDK 脱胎于:

LayBot 系列组件包括:

项目 职责
laybot/request-sdk HTTP、SSE、NDJSON、WebSocket、TLS、Proxy、Retry、Cancellation
laybot/ai-sdk 模型消息、流式状态、Tool Call、Usage、TTS、ASR、供应商协议
laybot/storage-sdk 文件和对象存储相关能力
业务项目 用户、权限、消息、计费、任务、数据库、OSS 和业务审计

推荐分层:

Webman / Laravel / ThinkPHP / Symfony / CLI
    ↓
业务Service、Worker、Process
    ↓
laybot/ai-sdk
    ↓
laybot/request-sdk
    ↓
供应商API

3. 核心能力

3.1 大模型

  • OpenAI-Compatible Chat Completions
  • 非流式 Chat
  • 同步 SSE Chat
  • Workerman 异步 SSE Chat
  • 正文增量
  • Reasoning 增量
  • 加密 Reasoning 元数据
  • Tool Call 增量与归并
  • Usage 统一映射
  • Finish Reason
  • Request ID
  • Responses API
  • Responses SSE
  • 图片、PDF 和文件输入
  • Files 上传、查询、列表、下载与删除
  • Embeddings、Images、Audio、Batches、Fine-tuning 资源入口
  • 模型能力注册表

3.2 火山语音

  • AUC 文件语音识别
  • AUC Submit / Query
  • SAUC Async 实时识别
  • SAUC Nostream 流式输入识别
  • HTTP SSE TTS
  • HTTP Chunked / JSON Lines TTS
  • 双向 WebSocket TTS
  • 连续文本 TaskRequest
  • PCM、MP3 等音频输出参数
  • 声音复刻音色状态查询
  • Sentence 事件
  • TTS Usage
  • WebSocket 背压、取消和正常关闭

3.3 工程能力

  • PHP 8.1+ 强类型 DTO
  • 同步和异步 API 语义分离
  • 生成式请求默认不透明重试
  • 供应商配置不可变覆盖
  • 多租户 API Key 隔离
  • 连接超时、请求超时和流空闲超时
  • Header 安全合并
  • API Key 日志脱敏
  • 供应商异常统一映射
  • 部分输出与可能计费标记
  • Webman/Workerman 常驻进程适配
  • PHPUnit 和 PHPStan 验证

4. 当前供应商状态

4.1 已通过真实账号验收

供应商 能力 状态
Ark / 豆包方舟 Chat 非流式 已验证
Ark / 豆包方舟 Chat SSE 已验证
Ark / 豆包方舟 Responses 非流式 已验证
Ark / 豆包方舟 Responses SSE 已验证
Ark / 豆包方舟 公网图片 URL 已验证
Ark / 豆包方舟 公网 PDF URL 已验证
Ark / 豆包方舟 Files 上传和 file_id 已验证
DeepSeek Chat 非流式 已验证
DeepSeek Chat SSE 已验证
DeepSeek Reasoner 已验证
火山语音 已有声音复刻音色查询 已验证
火山语音 HTTP SSE TTS 已验证
火山语音 双向 WebSocket TTS 已验证
火山语音 AUC 文件识别 已验证
火山语音 SAUC Async 已验证
火山语音 SAUC Nostream 已验证

4.2 已实现但尚未完成真实账号验收

供应商/能力 状态
OpenAI Chat / Responses / Files / Resources 协议已实现,待真实账号验收
Gemini Native API 协议已实现,待真实账号验收
Anthropic / Claude Messages 协议已实现,待真实账号验收
Qwen OpenAI-Compatible 待真实账号验收
xAI OpenAI-Compatible 待真实账号验收
Groq OpenAI-Compatible 待真实账号验收
火山 HTTP Chunked TTS 已实现,待正式账号专项验收
LayBot 平台扩展资源 由实际平台端点决定

“协议已实现”不等同于“已完成目标账号生产验收”。
正式项目应依据自身模型、区域、配额和供应商版本执行集成测试。

5. 框架支持

运行环境 同步 Chat 同步流 异步 HTTP/SSE 实时 TTS/ASR WebSocket
Webman / Workerman 支持 支持 支持 支持
Laravel 支持 支持 需 Workerman 事件循环 需 Workerman 事件循环
ThinkPHP 支持 支持 需 Workerman 事件循环 需 Workerman 事件循环
Symfony 支持 支持 需 Workerman 事件循环 需 Workerman 事件循环
Yii 支持 支持 需 Workerman 事件循环 需 Workerman 事件循环
PHP CLI 支持 支持 需启动 Workerman 需启动 Workerman
PHP-FPM 支持 受网关缓冲和执行时间影响 不建议 不适用
Swoole / Hyperf 同步能力可用 可能阻塞协程 暂无原生适配 暂无原生适配

SDK 不会根据环境自动改变方法语义:

调用同步方法 → 一定同步
调用异步方法 → 一定异步,并要求有效的Workerman事件循环

6. 运行要求

基础要求:

PHP >= 8.1
ext-json
ext-zlib
laybot/request-sdk ^2.0.6
psr/log ^3.0

异步 HTTP、SSE、TTS 和 ASR 需要底层 Workerman 事件循环支持。

推荐生产环境:

PHP 8.4
当前稳定版Webman
Workerman 5.2+
laybot/request-sdk 2.0.6+
Linux

7. 安装

composer require laybot/ai-sdk:^2.0

本地开发:

git clone https://github.com/laybot/ai-sdk.git
cd ai-sdk

composer install

8. 配置

8.1 创建统一 AiClient

<?php

use LayBot\Ai\AiClient;

$ai = new AiClient([
    'default_provider' => 'ark',

    'providers' => [
        'ark' => [
            'api_key' => getenv('ARK_API_KEY'),

            'options' => [
                'log_bodies' => false,
            ],
        ],

        'deepseek' => [
            'api_key' => getenv(
                'DEEPSEEK_API_KEY'
            ),

            'options' => [
                'log_bodies' => false,
            ],
        ],

        'volc-speech' => [
            'api_key' => getenv(
                'VOLC_SPEECH_API_KEY'
            ),

            'options' => [
                'tts_resource_id' => getenv(
                    'VOLC_TTS_RESOURCE_ID'
                ),

                'tts_model' => getenv(
                    'VOLC_TTS_MODEL'
                ),

                'auc_resource_id' => getenv(
                    'VOLC_AUC_RESOURCE_ID'
                ) ?: 'volc.seedasr.auc',

                'sauc_resource_id' => getenv(
                    'VOLC_SAUC_RESOURCE_ID'
                ) ?: 'volc.seedasr.sauc.duration',

                'log_bodies' => false,
            ],
        ],
    ],
]);

8.2 Provider 配置项

[
    'driver' => 'openai',
    'base_uri' => 'https://api.example.com',
    'api_key' => 'server-side-key',

    'headers' => [
        'X-App-Name' => 'xiaoyi-ai',
    ],

    'sensitive_headers' => [
        'X-Private-Token',
    ],

    'endpoints' => [
        'chat' => '/v1/chat/completions',
    ],

    'options' => [
        'require_done_token' => true,
        'log_bodies' => false,
    ],

    'verify_tls' => true,
    'proxy' => null,
]

支持通过别名配置同一协议:

$ai = new AiClient([
    'providers' => [
        'primary-chat' => [
            'driver' => 'openai',
            'base_uri' =>
                'https://internal-gateway.example.com',
            'api_key' => getenv('PRIMARY_CHAT_KEY'),
        ],
    ],
]);

$result = $ai
    ->provider('primary-chat')
    ->chat()
    ->create([...]);

9. Chat 快速开始

9.1 Ark 非流式 Chat

use LayBot\Ai\Vendor\Ark;

$ark = new Ark([
    'api_key' => getenv('ARK_API_KEY'),
]);

$result = $ark->chat()->create([
    'model' => getenv('ARK_CHAT_MODEL'),

    'messages' => [
        [
            'role' => 'system',
            'content' => '你是一名专业的教学助手。',
        ],
        [
            'role' => 'user',
            'content' => '为初二学生讲解浮力定律。',
        ],
    ],

    'stream' => false,
    'max_completion_tokens' => 1024,
]);

echo $result->text;
echo $result->reasoningText;
echo $result->usage->totalTokens;
echo $result->providerRequestId;

9.2 DeepSeek 非流式 Chat

use LayBot\Ai\Vendor\DeepSeek;

$deepSeek = new DeepSeek([
    'api_key' => getenv('DEEPSEEK_API_KEY'),
]);

$result = $deepSeek->chat()->create([
    'model' => getenv('DEEPSEEK_CHAT_MODEL'),

    'messages' => [
        [
            'role' => 'user',
            'content' => '请解释什么是二叉搜索树。',
        ],
    ],

    'stream' => false,
    'max_tokens' => 1024,
]);

echo $result->text;

10. 统一 completions() 入口

completions() 根据请求中的 stream 决定返回类型:

$result = $ai
    ->chat('ark')
    ->completions(
        request: [
            'model' => getenv('ARK_CHAT_MODEL'),
            'messages' => [
                [
                    'role' => 'user',
                    'content' => '你好',
                ],
            ],
            'stream' => true,
        ],
        callbacks: [
            'on_start' => static function (): void {
                echo '[START]';
            },

            'on_text_delta' =>
                static function (string $text): void {
                    echo $text;
                },

            'on_reasoning_delta' =>
                static function (string $text): void {
                    // 按业务要求决定是否展示。
                },

            'on_usage' =>
                static function ($usage): void {
                    echo $usage->totalTokens;
                },

            'on_finish' =>
                static function (?string $reason): void {
                    echo $reason ?? 'unknown';
                },

            'on_complete' =>
                static function ($result): void {
                    // 最终归并结果。
                },

            'on_error' =>
                static function (\Throwable $error): void {
                    report($error);
                },
        ]
    );

返回类型:

stream=false → ChatResult
stream=true  → ChatStreamResult

11. 同步流式 Chat

use LayBot\Ai\DTO\StreamEvent;
use LayBot\Ai\Enum\StreamEventType;

$result = $ai
    ->chat('ark')
    ->stream(
        request: [
            'model' => getenv('ARK_CHAT_MODEL'),

            'messages' => [
                [
                    'role' => 'user',
                    'content' => '请分步骤讲解勾股定理。',
                ],
            ],

            'stream' => true,
            'max_completion_tokens' => 2048,
        ],

        onEvent: static function (
            StreamEvent $event
        ): void {
            match ($event->type) {
                StreamEventType::TEXT_DELTA =>
                    print($event->textDelta),

                StreamEventType::REASONING_DELTA =>
                    null,

                StreamEventType::USAGE =>
                    null,

                StreamEventType::FINISH =>
                    print(PHP_EOL . '[DONE]'),

                default => null,
            };
        },

        options: [
            'connect_timeout' => 10,
            'request_timeout' => 0,
            'idle_timeout' => 120,
        ]
    );

echo $result->text;

流式结果包含:

$result->text;
$result->reasoningText;
$result->encryptedReasoning;
$result->model;
$result->providerRequestId;
$result->finishReason;
$result->usage;
$result->durationMs;
$result->firstTokenDurationMs;
$result->receivedAnyDelta;
$result->completed;
$result->possiblyCharged;
$result->toolCalls;

12. Webman 异步流式 Chat

在 Webman/Workerman 已运行的 Worker 中:

use LayBot\Ai\DTO\ChatStreamResult;
use LayBot\Ai\DTO\StreamEvent;
use LayBot\Ai\Enum\StreamEventType;

$handle = $ai
    ->chat('ark')
    ->streamAsync(
        request: [
            'model' => getenv('ARK_CHAT_MODEL'),

            'messages' => [
                [
                    'role' => 'user',
                    'content' => '请讲解牛顿第二定律。',
                ],
            ],

            'stream' => true,
            'max_completion_tokens' => 2048,
        ],

        onEvent: static function (
            StreamEvent $event
        ): void {
            if (
                $event->type
                === StreamEventType::TEXT_DELTA
            ) {
                // 推送给App或浏览器WebSocket。
                echo $event->textDelta;
            }
        },

        onComplete: static function (
            ChatStreamResult $result
        ): void {
            echo 'Completed: ', $result->text;
        },

        onError: static function (
            \Throwable $error
        ): void {
            echo $error->getMessage();
        },

        options: [
            'connect_timeout' => 10,
            'request_timeout' => 0,
            'idle_timeout' => 120,
        ]
    );

取消:

$handle->cancel('user stopped generation');

在已经运行的 Webman Worker 中不要再次调用:

Workerman\Worker::runAll();

13. ChatRequest DTO

use LayBot\Ai\DTO\ChatRequest;

$request = new ChatRequest(
    model: 'runtime-model',

    messages: [
        [
            'role' => 'user',
            'content' => '你好',
        ],
    ],

    parameters: [
        'temperature' => 0.7,
        'max_completion_tokens' => 1024,
        'tools' => [],
    ]
);

$result = $ai
    ->chat('ark')
    ->create($request);

规则:

model和messages由DTO明确字段决定
其他供应商参数通过parameters自由传入

调用方可以动态传入任何运行时模型和消息,SDK 不会写死模型。

14. Tool Calls

OpenAI-Compatible 请求:

$result = $ai
    ->chat('deepseek')
    ->create([
        'model' => getenv('DEEPSEEK_CHAT_MODEL'),

        'messages' => [
            [
                'role' => 'user',
                'content' => '查询北京天气。',
            ],
        ],

        'tools' => [
            [
                'type' => 'function',
                'function' => [
                    'name' => 'get_weather',
                    'description' => '查询城市天气',
                    'parameters' => [
                        'type' => 'object',
                        'properties' => [
                            'city' => [
                                'type' => 'string',
                            ],
                        ],
                        'required' => ['city'],
                    ],
                ],
            ],
        ],
    ]);

foreach ($result->toolCalls as $toolCall) {
    $name = $toolCall['function']['name'] ?? '';
    $arguments =
        $toolCall['function']['arguments'] ?? '{}';
}

流式 Tool Call 会按 choice_indexindex 归并。

统一结果当前只支持单候选:

OpenAI-Compatible:n=1
Gemini:candidate_count=1

15. Ark Responses API

15.1 文本

use LayBot\Ai\Vendor\Ark;

$ark = new Ark([
    'api_key' => getenv('ARK_API_KEY'),
]);

$result = $ark->responses()->create(
    [
        'model' => getenv('ARK_RESPONSES_MODEL'),

        'input' => [
            [
                'role' => 'user',
                'content' => [
                    [
                        'type' => 'input_text',
                        'text' => '请简要解释光合作用。',
                    ],
                ],
            ],
        ],

        'max_output_tokens' => 2048,
    ],

    [
        'connect_timeout' => 15,
        'request_timeout' => 120,
    ]
);

echo $result->status;
echo $result->text;
echo $result->usage->totalTokens;

completed 只在供应商返回:

status=completed

时为 true

如果供应商返回:

status=incomplete

SDK 不会将其伪装成成功。

15.2 公网图片 URL

$result = $ark->responses()->create([
    'model' => getenv('ARK_RESPONSES_MODEL'),

    'input' => [
        [
            'role' => 'user',
            'content' => [
                [
                    'type' => 'input_image',
                    'image_url' =>
                        'https://example.com/image.png',
                    'detail' => 'high',
                ],
                [
                    'type' => 'input_text',
                    'text' => '请描述图片内容。',
                ],
            ],
        ],
    ],
]);

15.3 公网 PDF URL

$result = $ark->responses()->create(
    [
        'model' => getenv('ARK_RESPONSES_MODEL'),

        'input' => [
            [
                'role' => 'user',
                'content' => [
                    [
                        'type' => 'input_file',
                        'file_url' =>
                            'https://example.com/document.pdf',
                    ],
                    [
                        'type' => 'input_text',
                        'text' => '请概括PDF主题。',
                    ],
                ],
            ],
        ],

        /*
         * 推理模型需要同时为Reasoning和可见正文
         * 预留输出预算。
         */
        'max_output_tokens' => 4096,
    ],

    [
        'connect_timeout' => 15,
        'request_timeout' => 120,
    ]
);

15.4 上传文件并使用 file_id

$uploaded = $ark
    ->files()
    ->upload(
        __DIR__ . '/document.pdf',
        'user_data'
    );

$fileId = $uploaded->id();

if ($fileId === null) {
    throw new RuntimeException(
        'Ark upload response has no file ID'
    );
}

$file = $ark->files()->retrieve($fileId);

$result = $ark->responses()->create(
    [
        'model' => getenv('ARK_RESPONSES_MODEL'),

        'input' => [
            [
                'role' => 'user',
                'content' => [
                    [
                        'type' => 'input_file',
                        'file_id' => $fileId,
                    ],
                    [
                        'type' => 'input_text',
                        'text' => '请概括上传的文件。',
                    ],
                ],
            ],
        ],

        'max_output_tokens' => 4096,
    ],

    [
        'connect_timeout' => 15,
        'request_timeout' => 120,
    ]
);

文件上传后可能暂时处于:

processing
pending
queued

业务项目应轮询文件状态,不能对所有错误盲目重试。

15.5 Responses SSE

$result = $ark->responses()->stream(
    request: [
        'model' => getenv('ARK_RESPONSES_MODEL'),
        'input' => '请介绍LayBot。',
    ],

    onEvent: static function (
        \LayBot\Ai\DTO\StreamEvent $event
    ): void {
        if (
            $event->type
            === \LayBot\Ai\Enum\StreamEventType::TEXT_DELTA
        ) {
            echo $event->textDelta;
        }
    },

    options: [
        'request_timeout' => 0,
        'idle_timeout' => 120,
    ]
);

异步:

$handle = $ark->responses()->streamAsync(
    request: [...],
    onEvent: $onEvent,
    onComplete: $onComplete,
    onError: $onError,
    options: [
        'request_timeout' => 0,
        'idle_timeout' => 120,
    ]
);

16. 火山语音配置

use LayBot\Ai\Vendor\VolcSpeech;

$speech = new VolcSpeech([
    'api_key' => getenv(
        'VOLC_SPEECH_API_KEY'
    ),

    'options' => [
        'tts_resource_id' => getenv(
            'VOLC_TTS_RESOURCE_ID'
        ),

        'tts_model' => getenv(
            'VOLC_TTS_MODEL'
        ),

        'auc_resource_id' => getenv(
            'VOLC_AUC_RESOURCE_ID'
        ) ?: 'volc.seedasr.auc',

        'sauc_resource_id' => getenv(
            'VOLC_SAUC_RESOURCE_ID'
        ) ?: 'volc.seedasr.sauc.duration',
    ],
]);

火山语音 API Key 与 Ark API Key 是不同凭证。

所有凭证都只能保存在服务端,不要放入:

  • 浏览器 JavaScript;
  • uni-app 前端源码;
  • Electron 渲染进程;
  • Git 仓库;
  • App 安装包;
  • 可公开下载的配置文件。

17. HTTP SSE TTS

use LayBot\Ai\DTO\Tts\TtsStreamEvent;
use LayBot\Ai\Enum\TtsStreamEventType;

$audio = '';

$result = $speech
    ->tts()
    ->sse()
    ->streamSse(
        request: [
            'text' => '你好,这是一段语音合成测试。',
            'speaker' => getenv(
                'VOLC_TTS_SPEAKER'
            ),

            'audio' => [
                'format' => 'mp3',
                'sample_rate' => 24000,
                'bit_rate' => 64000,
            ],

            'additions' => [
                'tone_fidelity' => true,
            ],
        ],

        onEvent: static function (
            TtsStreamEvent $event
        ) use (&$audio): void {
            if (
                $event->type
                === TtsStreamEventType::AUDIO_CHUNK
                && $event->audio !== null
            ) {
                $audio .= $event->audio->audio;
            }
        },

        options: [
            'connect_timeout' => 10,
            'request_timeout' => 0,
            'idle_timeout' => 90,
        ]
    );

if (!$result->completed) {
    throw new RuntimeException(
        'TTS did not complete'
    );
}

file_put_contents(
    __DIR__ . '/speech.mp3',
    $audio
);

结果包含:

$result->provider;
$result->status;
$result->clientRequestId;
$result->providerRequestId;
$result->providerLogId;
$result->usage->textWords;
$result->audioBytes;
$result->audioChunks;
$result->durationMs;
$result->completed;
$result->cancelled;
$result->sentences;

大音频不要长期拼接在内存中,应在 AUDIO_CHUNK 回调中持续写入临时文件。

18. HTTP Chunked TTS

$result = $speech
    ->tts()
    ->chunked()
    ->streamChunked(
        request: [
            'text' => '你好',
            'speaker' => getenv(
                'VOLC_TTS_SPEAKER'
            ),
        ],

        onEvent: static function (
            TtsStreamEvent $event
        ): void {
            if (
                $event->audio !== null
            ) {
                // 消费二进制音频。
            }
        }
    );

HTTP Chunked 使用 JSON Lines 解析。

由于不同火山产品和账号可能返回不同外层结构,正式上线前必须使用目标账号执行协议验收。

19. 查询已有复刻音色

$voice = $speech
    ->tts()
    ->voice()
    ->get(
        speakerId: getenv(
            'VOLC_TTS_SPEAKER'
        )
    );

if (!$voice->availableForSynthesis()) {
    throw new RuntimeException(sprintf(
        'voice status is %d',
        $voice->status
    ));
}

if ($voice->supportsClone20()) {
    echo 'Clone 2.0 is available';
}

当前 SDK 支持:

查询已有音色状态
判断音色是否可合成
判断speaker_status是否包含复刻2.0模型类型

当前未提供完整的:

训练音频上传
创建新音色
重新训练
删除音色

20. 双向 WebSocket TTS

双向 TTS 需要 Workerman 事件循环。

use LayBot\Ai\Contract\BidirectionalTtsConnectionInterface;
use LayBot\Ai\Contract\BidirectionalTtsSessionInterface;
use LayBot\Ai\DTO\Tts\TtsResult;
use LayBot\Ai\DTO\Tts\TtsStreamEvent;
use LayBot\Ai\Enum\TtsStreamEventType;
use LayBot\Ai\Provider\VolcSpeech\Tts\BidirectionalTtsListener;
use LayBot\Request\DTO\WebSocketCloseInfo;

$listener = new class extends BidirectionalTtsListener {
    public function onConnectionReady(
        BidirectionalTtsConnectionInterface $connection
    ): void {
        $connection->startSession([
            'speaker' => getenv(
                'VOLC_TTS_SPEAKER'
            ),

            'audio' => [
                'format' => 'pcm',
                'sample_rate' => 24000,
                'bit_rate' => null,
            ],
        ]);
    }

    public function onSessionStarted(
        BidirectionalTtsConnectionInterface $connection,
        BidirectionalTtsSessionInterface $session
    ): void {
        $session->appendText('你好,');
        $session->appendText('这是双向流式语音。');
        $session->finish();
    }

    public function onEvent(
        BidirectionalTtsConnectionInterface $connection,
        BidirectionalTtsSessionInterface $session,
        TtsStreamEvent $event
    ): void {
        if (
            $event->type
            === TtsStreamEventType::AUDIO_CHUNK
            && $event->audio !== null
        ) {
            $pcm = $event->audio->audio;

            // 立即转发给App或写入播放器。
        }
    }

    public function onSessionCompleted(
        BidirectionalTtsConnectionInterface $connection,
        BidirectionalTtsSessionInterface $session,
        TtsResult $result
    ): void {
        if ($result->completed) {
            $connection->finish();
        } else {
            $connection->cancel(
                'TTS Session did not complete'
            );
        }
    }

    public function onError(
        BidirectionalTtsConnectionInterface $connection,
        \Throwable $error
    ): void {
        report($error);
        $connection->cancel(
            'TTS failed'
        );
    }

    public function onClose(
        BidirectionalTtsConnectionInterface $connection,
        WebSocketCloseInfo $info
    ): void {
        // 清理业务资源。
    }
};

$connection = $speech
    ->tts()
    ->bidirectional()
    ->connectAsync(
        request: [
            'connect_timeout' => 10,
            'idle_timeout' => 180,
            'ping_interval' => 30,
            'close_timeout' => 5,
        ],

        listener: $listener
    );

连接状态:

CONNECTING
    ↓
STARTING
    ↓
READY
    ↓
FINISHING
    ↓
CLOSED

Session 状态:

NEW
    ↓
STARTING
    ↓
STREAMING
    ↓
FINISHING
    ↓
FINISHED

同一连接同时只允许一个活动 Session。

只有收到当前 Session 的终态后,才能创建下一个 Session。

21. Ark 流式文本接入实时 TTS

推荐链路:

Ark文本Delta
    ├── 推送App显示
    └── appendText()发送给双向TTS
                           ↓
                      音频Chunk
                           ↓
                       推送App播放

注意:

  • 只发送最终正文 TEXT_DELTA
  • 不发送 Reasoning;
  • 不重复发送 Delta;
  • 不为每个 Delta 新建 TTS Session;
  • 一条 AI 回复使用一个 TTS Session;
  • AI 失败时取消 TTS Session;
  • AI 完成后调用 TTS Session finish()
  • 等 TTS SessionFinished 后再结束 Connection。

22. AUC 文件语音识别

$submit = $speech
    ->asr()
    ->auc()
    ->submit([
        'audio' => [
            'url' =>
                'https://example.com/audio.mp3',
            'format' => 'mp3',
            'codec' => 'raw',
            'rate' => 24000,
            'bits' => 16,
            'channel' => 1,
        ],

        'request' => [
            'model_name' => 'bigmodel',
            'enable_itn' => true,
            'enable_punc' => true,
            'show_utterances' => true,
        ],
    ]);

$taskId = $submit->taskId;

do {
    usleep(1_500_000);

    $result = $speech
        ->asr()
        ->auc()
        ->query(
            taskId: $taskId,
            resourceId: $submit->resourceId
        );
} while (!$result->state->terminal());

if (!$result->successful()) {
    throw new RuntimeException(
        $result->message
        ?? 'AUC recognition failed'
    );
}

echo $result->text;

foreach ($result->utterances as $utterance) {
    echo $utterance->text;
}

上述轮询示例适用于 CLI 或专用任务 Worker。

在 Webman WebSocket Worker 中,不要使用 usleep() 阻塞事件循环,应使用:

Workerman\Timer

或将 AUC 轮询放入独立任务进程。

23. SAUC 实时 ASR

SAUC 使用双向 WebSocket 上传音频。

use LayBot\Ai\Contract\RealtimeAsrSessionInterface;
use LayBot\Ai\DTO\Asr\RealtimeAsrResult;
use LayBot\Ai\Provider\VolcSpeech\Asr\RealtimeAsrListener;
use LayBot\Request\DTO\WebSocketCloseInfo;

$listener = new class extends RealtimeAsrListener {
    public function onReady(
        RealtimeAsrSessionInterface $session
    ): void {
        /*
         * 初始化确认已经收到。
         * 现在可以开始发送PCM音频。
         */
    }

    public function onResult(
        RealtimeAsrSessionInterface $session,
        RealtimeAsrResult $result
    ): void {
        echo $result->text;
    }

    public function onComplete(
        RealtimeAsrSessionInterface $session,
        RealtimeAsrResult $result
    ): void {
        echo 'Final: ', $result->text;
    }

    public function onBufferFull(
        RealtimeAsrSessionInterface $session
    ): void {
        // 暂停麦克风或上游音频生产。
    }

    public function onBufferDrain(
        RealtimeAsrSessionInterface $session
    ): void {
        // 恢复音频生产。
    }

    public function onError(
        RealtimeAsrSessionInterface $session,
        \Throwable $error
    ): void {
        report($error);
    }

    public function onClose(
        RealtimeAsrSessionInterface $session,
        WebSocketCloseInfo $info
    ): void {
        // 清理录音与业务状态。
    }
};

$session = $speech
    ->asr()
    ->realtime()
    ->connectAsync(
        request: [
            'endpoint' => 'async',

            'audio' => [
                'format' => 'pcm',
                'codec' => 'raw',
                'rate' => 16000,
                'bits' => 16,
                'channel' => 1,
            ],

            'request' => [
                'model_name' => 'bigmodel',
                'enable_itn' => true,
                'enable_punc' => true,
                'show_utterances' => true,
                'enable_nonstream' => false,
            ],
        ],

        listener: $listener,

        options: [
            'connect_timeout' => 10,
            'idle_timeout' => 60,
            'ping_interval' => 20,
            'close_timeout' => 5,
        ]
    );

收到 onReady() 后发送音频:

$session->sendAudio($pcmChunk);

$session->sendAudio(
    $lastPcmChunk,
    last: true
);

或者:

$session->finish($lastPcmChunk);

last=true 时 SDK 会发送火山协议要求的最后一个负序号音频包。

24. AUC 与 SAUC 的选择

场景 推荐
已上传到 OSS 的完整录音 AUC
边录音边显示识别结果 SAUC Async
流式上传、整句返回 SAUC Nostream
业务正式存档识别 AUC
实时输入体验 SAUC

一期业务可以采用:

App录音
    ↓
私有OSS
    ↓
服务端AUC
    ↓
正式识别结果

实时增强:

App采集PCM
    ↓
WebSocket
    ↓
服务端SAUC
    ↓
临时识别结果

如果同时执行 AUC 和 SAUC,应评估双份识别费用。

25. 超时配置

支持:

配置 说明
connect_timeout TCP、代理和 TLS 连接超时
request_timeout 请求总超时
idle_timeout 流连续无数据超时

Chat 流推荐:

[
    'connect_timeout' => 10,
    'request_timeout' => 0,
    'idle_timeout' => 120,
]

Ark PDF/Files 推荐:

[
    'connect_timeout' => 15,
    'request_timeout' => 120,
]

实时语音推荐:

[
    'connect_timeout' => 10,
    'idle_timeout' => 180,
    'ping_interval' => 30,
    'close_timeout' => 5,
]

request_timeout=0 表示不限制流总持续时间,但仍受 idle_timeout 保护。

26. Retry 与重复计费

生成式请求默认:

'retry' => false

这是为了避免:

  • Chat 重复生成;
  • Responses 重复计费;
  • TTS 重复合成和播放;
  • AUC 重复创建任务;
  • 文件重复上传;
  • Tool Call 重复执行。

只有明确幂等的查询才应启用重试:

$result = $speech
    ->asr()
    ->auc()
    ->query(
        taskId: $taskId,
        resourceId: $resourceId,
        options: [
            'idempotent' => true,
            'retry' => [
                'max_attempts' => 3,
            ],
        ]
    );

如果已经收到部分文本或音频,不应自动重新执行完整生成请求。

ProviderException 会提供:

$error->retryable();
$error->partialOutputReceived();
$error->possiblyCharged();

27. 配置覆盖与多租户

27.1 单次网络覆盖

$result = $ai
    ->chat('ark')
    ->create(
        request: [...],

        options: [
            'base_uri' =>
                'https://internal-gateway.example.com',

            'headers' => [
                'X-Tenant-Id' => 'tenant-1001',
            ],

            'connect_timeout' => 5,
            'request_timeout' => 60,
            'proxy' =>
                'http://127.0.0.1:7890',
        ]
    );

27.2 单次 Endpoint 覆盖

Endpoint 应放在调用选项,不是模型请求 Body:

$result = $ai
    ->chat('openai')
    ->create(
        request: [
            'model' => 'model-name',
            'messages' => [...],
        ],

        options: [
            'endpoint' =>
                '/v1/chat/completions',
        ]
    );

27.3 多租户 API Key

$tenantChat = $ai
    ->provider(
        'ark',
        [
            'api_key' => $tenantApiKey,
        ]
    )
    ->chat();

存在实例级覆盖时,AiClient 不会缓存该 ProviderClient,防止租户 Key、Base URI 和 Header 污染其他调用。

28. 常驻进程与并发建议

AiClient 可以在单个 Webman Worker 生命周期内复用:

final class AiService
{
    public function __construct(
        private readonly \LayBot\Ai\AiClient $ai
    ) {
    }
}

以下对象必须按请求或会话创建:

Chat流状态机
Responses流状态机
TTS Session
ASR Session
Listener业务状态
音频临时文件
Cancellation Handle

不要:

  • 在不同用户间共享同一个可变 Listener;
  • 将 Session 保存为全局静态变量;
  • 在多个 Worker 进程间传递连接对象;
  • 在 WebSocket Worker 中执行长时间同步请求;
  • 在回调内执行大量 CPU 密集任务;
  • 忽略 onBufferFull()
  • 将音频 Base64 放入普通 JSON WebSocket。

服务端向 App 转发实时音频时推荐:

控制消息:JSON文本帧
音频Chunk:WebSocket二进制帧

29. 异常体系

基础异常:

AiException
├── ConfigurationException
├── ValidationException
├── ProtocolException
├── ProviderException
└── UnsupportedCapabilityException

示例:

use LayBot\Ai\Exception\ProviderException;
use LayBot\Ai\Exception\ValidationException;

try {
    $result = $ai
        ->chat('ark')
        ->create([...]);
} catch (ValidationException $error) {
    // 请求参数错误。
} catch (ProviderException $error) {
    echo $error->provider();
    echo $error->providerCode();
    echo $error->requestId();
    echo $error->httpStatus();

    if ($error->possiblyCharged()) {
        // 不要直接重复生成。
    }

    if (
        $error->retryable()
        && !$error->partialOutputReceived()
    ) {
        // 由业务幂等策略决定是否重试。
    }
}

30. 日志与敏感信息

SDK 默认注册以下敏感 Header:

Authorization
Proxy-Authorization
X-Api-Key
X-Api-Token
X-Goog-Api-Key
X-Api-App-Key
X-Api-Access-Key

自定义:

[
    'sensitive_headers' => [
        'X-Gateway-Credential',
        'X-Tenant-Secret',
    ],

    'options' => [
        'log_bodies' => false,
    ],
]

生产环境不要记录:

  • 完整 Prompt;
  • 用户心理咨询内容;
  • 音频二进制;
  • Base64 音频;
  • 文件签名 URL;
  • API Key;
  • 原始身份证明;
  • 未脱敏的模型响应。

31. 模型能力注册

未知模型能力默认不会被武断阻断:

$capabilities = $ai
    ->provider('ark')
    ->capabilities('new-model');

项目可以注册能力:

$ai = new \LayBot\Ai\AiClient([
    'capabilities' => [
        'ark' => [
            'custom-model' => [
                'supports_vision' => true,
                'supports_tools' => true,
                'supports_reasoning' => true,
                'max_context_tokens' => 32768,
            ],
        ],
    ],

    'providers' => [
        'ark' => [
            'api_key' => getenv('ARK_API_KEY'),
        ],
    ],
]);

能力状态:

yes
no
unknown

unknown 默认不阻断新模型。

32. LayBot 灵语智教平台能力

通过 LayBot Vendor 可以访问平台能力:

use LayBot\Ai\Vendor\LayBot;

$laybot = new LayBot([
    'api_key' => getenv('LAYBOT_API_KEY'),
]);

Chat:

$result = $laybot->chat()->create([
    'model' => 'LB-Cosmos',

    'messages' => [
        [
            'role' => 'user',
            'content' =>
                '为初二学生讲解浮力定律。',
        ],
    ],

    'edu_features' => [
        'difficulty' => 'middle_school',
        'tiered' => true,
    ],
]);

平台资源入口:

$laybot->files();
$laybot->batches();
$laybot->embeddings();
$laybot->fineTuning();
$laybot->images();
$laybot->audio();
$laybot->portal();
$laybot->api();

教育扩展 API 包括:

文档提取
作文批改
口语评测
听力填空
习题生成
智能组卷
学习报告
路径推荐
知识追踪
数学批改
OCR

具体可用模型、计费、端点和合规能力以:

为准。

33. 旗舰教育模型示例

教育场景 方案 模型示例 代号
口语评测 实时识别与表达分析 灵语·语韵 LB-Phona
能力素养测评 综合分析与成长建议 灵语·慧学 LB-Skillwise
K12 分层教学 梯度内容生成 明心·洞玄 LB-Insight
智能组卷批改 图文试卷处理 灵语·玄穹 LB-Aethel
学业诊断 薄弱点定位 灵语·太初 LB-Primordius
跨学科教学 知识关联推理 灵语·寰宇 LB-Cosmos

模型名称仅作为 LayBot 平台产品示例。
实际开放范围、价格和能力以控制台为准。

34. 与 Laybot MPA 前端集成

本项目是服务端 PHP SDK,不应将供应商 API Key 写入前端组件。

推荐:

<laybot-ai-chat
    endpoint="/api/ai/chat"
    model="LB-Cosmos"
    prompt="用高中难度讲解牛顿第二定律"
></laybot-ai-chat>

正确链路:

Laybot MPA组件
    ↓ 用户登录态和业务Ticket
Webman业务接口
    ↓
laybot/ai-sdk
    ↓
模型供应商

禁止:

<laybot-ai-chat api-key="真实供应商Key">

Laybot MPA/SPA 前端框架相关信息:

35. 安全与合规边界

SDK 提供:

  • 服务端凭证管理入口;
  • Header 脱敏;
  • TLS 验证;
  • 请求大小限制;
  • 异常边界;
  • 请求 ID;
  • 重试安全默认值;
  • 多租户配置隔离。

SDK 本身不自动完成:

  • GDPR 法律合规;
  • K12 内容审核;
  • 用户授权;
  • 数据保留策略;
  • 医疗或心理咨询安全审查;
  • 供应商账单结算;
  • 敏感词业务规则;
  • 内容版权审核。

这些能力应由:

LayBot平台服务
业务项目
部署区域
供应商配置
组织合规制度

共同完成。

36. 当前限制

当前 2.x 的明确边界:

  • 不管理业务数据库;
  • 不管理用户额度;
  • 不负责 OSS;
  • 不保存聊天消息;
  • 不负责浏览器 CORS;
  • 不向 App 签发火山临时 Token;
  • 不提供声音复刻训练完整生命周期;
  • 不自动恢复中断的 WebSocket Session;
  • 不对生成式请求自动重试;
  • 不支持同一统一结果中的多候选;
  • 异步 API 依赖 Workerman 事件循环;
  • 暂无 Swoole、Amp、ReactPHP 原生 Transport;
  • 供应商未公开的私有鉴权协议不会在 SDK 中猜测实现。

37. 测试

37.1 静态检查与单元测试

composer validate --strict

composer dump-autoload \
  --optimize \
  --strict-psr

php tools/lint.php

composer test

composer analyse

当前验收基线:

PHPUnit Unit:63 tests
Assertions:177
PHPStan:153/153

37.2 小毅 AI 集成测试

vendor/bin/phpunit \
  --configuration phpunit.xml.dist \
  --testsuite integration-xiaoyi \
  --colors=always \
  --display-skipped \
  --fail-on-skipped

当前验收基线:

17 tests
93 assertions
0 failures
0 errors
0 skipped

已连续执行三轮通过。

37.3 全部集成测试

composer test:integration

38. 生产发布检查

composer validate --strict
composer dump-autoload --optimize --strict-psr
php tools/lint.php
composer test
composer analyse
composer audit
git diff --check

检查敏感文件:

git ls-files | grep -E \
  '(^|/)(\.env|.*\.pid|.*\.log)$'

检查 Worker 源码目录污染:

find tests/Integration/VolcSpeech/bin \
  -maxdepth 1 \
  -type f \
  \( \
    -name '*.pid' \
    -o -name '*.pid.lock' \
    -o -name '*.status' \
    -o -name '*.log' \
    -o -name '*.stdout.log' \
  \) \
  -print

发布前清理:

rm -rf \
  .phpunit.cache \
  .phpstan.cache \
  tests/Integration/.runtime

真实 API Key 必须保存在本地 .env 或密钥管理系统中,不能进入版本库。

39. Webman 生产建议

普通短请求

可以使用同步 API:

$result = $ai
    ->chat('deepseek')
    ->create([...]);

长时间 Chat、Responses、实时 TTS/ASR

优先使用异步 API:

streamAsync()
responses()->streamAsync()
tts()->bidirectional()->connectAsync()
asr()->realtime()->connectAsync()

进程隔离

推荐:

WebSocket Gateway
AI Chat Worker
TTS Stream Gateway
ASR Stream Gateway
AUC Task Worker

不要让一个 Worker 同时承担:

  • 大文件解析;
  • CPU 密集 Embedding;
  • 大量实时音频连接;
  • 阻塞式模型请求;
  • 用户 WebSocket 心跳。

40. 路线图

  • 更多供应商真实账号矩阵;
  • OpenAI、Gemini、Anthropic 完整集成验收;
  • 火山 HTTP Chunked TTS 专项验收;
  • 声音复刻训练管理;
  • 更多语音格式和字幕 DTO;
  • WebSocket 故障注入测试;
  • 异步并发压力测试;
  • Webman Service Provider;
  • Laravel、ThinkPHP、Symfony 集成示例;
  • 指标和 OpenTelemetry;
  • 更多供应商 Embedding 与 Rerank;
  • PHP 8.1~8.4 CI 矩阵。

41. 贡献指南

git clone https://github.com/laybot/ai-sdk.git
cd ai-sdk

composer install

composer validate --strict
composer test
composer analyse
composer audit

提交代码前必须确保:

Composer Validate通过
严格PSR-4通过
PHP Lint通过
PHPUnit通过
PHPStan通过
没有提交真实API Key
没有提交用户Prompt和音频
没有提交签名URL
没有提交运行日志和PID

欢迎提交 Issue 和 Pull Request。

42. LayBot 系列项目

LayBot 专注于现代 Web 工程、教育智能、知识管理和 AI 基础设施。

如果本项目对你的 PHP AI 工程有帮助,欢迎 Star、反馈和参与建设。

43. License、NOTICE 与署名

本项目采用 Apache License 2.0 开源协议。

Copyright © 2025-2026 Larry / LayBot
LayBot LingTeach AI
https://ai.laybot.cn
https://www.laybot.cn

Apache License 2.0 允许:

  • 商业使用;
  • 修改;
  • 分发;
  • 专利授权范围内使用;
  • 在遵守许可证的情况下闭源集成。

分发本项目或其衍生版本时,应按照 Apache License 2.0 的要求:

  • 保留 LICENSE
  • 保留适用的版权声明;
  • 标明修改内容;
  • 如果发行包包含 NOTICE,保留其中适用的归属说明。

LayBotLaybot灵语智教LayBot LingTeach AI 及相关标识属于其权利人。Apache License 2.0 不授予商标使用权。

技术与产品信息:

LayBot · 灵语智教
稳定连接模型,让 PHP 专注业务。