流式输出
在 SessionConfig 中指定streaming: true 后,就可以以流式方式接收来自 Copilot 的响应。
无论是 stdio 模式还是 TCP 模式,原本就有类似流式的行为。区别在于会接收 ASSISTANT_MESSAGE_DELTA。
use Revolution\Copilot\Contracts\CopilotSession;
use Revolution\Copilot\Facades\Copilot;
use Revolution\Copilot\Types\SessionEvent;
$config = new SessionConfig(
streaming: true,
);
Copilot::start(function (CopilotSession $session) {
$session->on(function (SessionEvent $event): void {
if ($event->isAssistantMessageDelta()) {
// delta 中消息会分片到达。可用于显示进度等。也可以仅用它就完成整个显示,实现流畅的显示效果。
echo $event->deltaContent();
} elseif ($event->isAssistantMessage()) {
// 分片的 delta 发送结束后,会作为 ASSISTANT_MESSAGE 发出一条包含完整内容的消息。
// 如果还有下一条消息,会再次以 delta 到达。
// 一个 prompt 可能会有多条消息返回,因此在 streaming 模式下会反复出现多个 delta 与 1 次完整内容。
} elseif ($event->isAssistantReasoningDelta()) {
// reasoning delta 也会同样地分片到达。
echo $event->deltaContent();
}
});
$session->sendAndWait(prompt: 'Tell me something about Laravel.');
}, config: $config);
- 为了实现流畅的显示,不要添加换行,直接输出。
- Laravel Prompts 的
spin()会破坏显示效果,因此不要一起使用。
具体的使用模式
Artisan 命令
如上所述,用echo 或 $this->output->write() 直接输出。显示与直接使用 Copilot CLI 时一致,容易理解。使用 Laravel Prompts 的 stream() 还可以以淡入效果显示。
use function Laravel\Prompts\stream;
Artisan::command('copilot:streaming {--resume=}', function () {
$config = new SessionConfig(
streaming: true,
);
Copilot::start(function (CopilotSession $session) {
info('Starting Copilot with streaming: '.$session->id());
$stream = stream();
$session->on(function (SessionEvent $event) use ($stream) {
if ($event->isAssistantMessageDelta()) {
// delta 会分片到达。使用 Laravel Prompts 的 stream() 加淡入效果流畅显示。
$stream->append($event->deltaContent());
} elseif ($event->isIdle()) {
// 到 idle 就关闭。即使在同一会话中多次发送 prompt 也能正常工作,无需再次调用 `$stream = stream()`。
$stream->close();
}
});
while (true) {
$prompt = text(
label: 'Enter your prompt',
placeholder: 'Ask me anything...',
required: true,
hint: 'Ctrl+C to exit',
);
$session->sendAndWait($prompt);
}
}, config: $config, resume: $this->option('resume'));
})->purpose('Copilot streaming');
在 Web 页面上通过 Server-Sent Events (SSE) 投递
使用 response()->eventStream() 的写法
response()->eventStream() 是在 Laravel 12 发布前不久添加的。由于本包仅支持 Laravel 12 及以上,因此可以使用。
Route::get('/copilot/sse', function () {
return response()->eventStream(function () {
yield from Copilot::stream(function (CopilotSession $session) {
foreach ($session->sendAndStream('Tell me something about Laravel.') as $event) {
if ($event->isAssistantMessageDelta()) {
yield $event->deltaContent();
}
}
}, config: new SessionConfig(streaming: true));
});
});
Route::get('/copilot', function () {
return view('copilot');
});
使用 response()->stream() 的写法
Route::get('/copilot/sse', function () {
return response()->stream(function () {
Copilot::start(function (CopilotSession $session) {
$session->on(function (SessionEvent $event) {
if ($event->isAssistantMessageDelta()) {
echo "event: update\n";
echo 'data: '.$event->deltaContent()."\n\n";
ob_flush();
flush();
}
});
$session->sendAndWait('Tell me something about Laravel.');
}, config: new SessionConfig(streaming: true));
echo "event: update\n";
echo "data: </stream>\n\n";
ob_flush();
flush();
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'Connection' => 'keep-alive',
'X-Accel-Buffering' => 'no',
]);
});
Route::get('/copilot', function () {
return view('copilot');
});
前端代码示例
copilot.blade.php 是用于简单验证显示的。生产环境中如果使用 React 或 Vue,推荐使用 Laravel 官方的 npm 包。@laravel/stream-react 或 @laravel/stream-vue。
<html>
<body>
<script>
const source = new EventSource('/copilot/sse');
source.addEventListener('update', (event) => {
if (event.data === '</stream>') {
source.close();
return;
}
console.log(event.data);
document.getElementById("output").innerHTML += event.data;
});
</script>
<h1>Copilot SSE Test</h1>
<div id="output"></div>
</body>
</html>
通过 WebSocket 投递
也可以组合 Laravel 的通知功能、广播功能与 Reverb,通过 WebSocket 投递delta。
Livewire 的 wire:stream
在 Livewire 中,也可以使用 wire:stream 指令进行流式显示。
<?php
use Livewire\Attributes\Layout;
use Livewire\Component;
use Revolution\Copilot\Contracts\CopilotSession;
use Revolution\Copilot\Enums\SessionEventType;
use Revolution\Copilot\Facades\Copilot;
use Revolution\Copilot\Types\SessionEvent;
new class extends Component
{
public string $prompt = 'Tell me about Laravel Livewire.';
public string $question = '';
public string $answer = '';
public ?string $sessionId = null;
function submitPrompt(): void
{
$this->question = $this->prompt;
$this->prompt = '';
$this->answer = '';
$this->js('$wire.copilot()');
}
public function copilot(): void
{
Copilot::start(function (CopilotSession $session) {
$this->sessionId = $session->id();
$session->on(SessionEventType::ASSISTANT_MESSAGE_DELTA, function (SessionEvent $event): void {
$this->stream(
content: $event->deltaContent(),
to: 'answer',
);
$this->answer = $event->deltaContent();
});
$response = $session->sendAndWait(prompt: $this->question);
$this->answer = $response->content();
}, config: ['streaming' => true], resume: $this->sessionId);
}
};
?>
<div>
<div class="border border-gray-200 rounded-lg p-4 mb-6">
@if ($question)
<h3 class="font-extrabold mb-3">{{ $question }}</h3>
<div wire:stream="answer">{{ $answer }}</div>
@endif
</div>
<flux:input.group>
<flux:input wire:model="prompt" placeholder="Ask Copilot"/>
<flux:button wire:click="submitPrompt" icon="paper-airplane" icon:variant="outline"></flux:button>
</flux:input.group>
</div>
最新信息请参考 GitHub 仓库。